iOS17からTipKitが提供されるようなのでこの手のUIコンポーネントは短命になるかと思いますが、iOS17が広まるまでは今しばらく時間があるかと思うので記載しておきます。
いや、そっちの(Tooltip風)吹き出しじゃないんだけど・・・という方は以下の記事を参照ください。
https://dishware.sakura.ne.jp/swift/archives/603
@available(iOS 15, *)
extension View {
@ViewBuilder
func popover<Content: View>(isPresented: Binding<Bool>,_ arrowDirection: UIPopoverArrowDirection = .any, @ViewBuilder content: @escaping () -> Content) -> some View {
self.background {
PopoverController(isPresented: isPresented, arrowDirection: arrowDirection, content: content())
}
}
}
class CustomHostingView<Content: View>: UIHostingController<Content>{
override func viewDidLoad() {
super.viewDidLoad()
preferredContentSize = view.intrinsicContentSize
}
}
@available(iOS 15, *)
struct PopoverController<Content: View>: UIViewControllerRepresentable {
@Binding var isPresented: Bool
var arrowDirection: UIPopoverArrowDirection = .down
var content: Content
@State var isShown: Bool = false
class Coordinator: NSObject, UIPopoverPresentationControllerDelegate {
var parent: PopoverController
init(parent: PopoverController) {
self.parent = parent
}
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .none
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
self.parent.isPresented = false
}
func presentationController(_ presentationController: UIPresentationController, willPresentWithAdaptiveStyle style: UIModalPresentationStyle, transitionCoordinator: UIViewControllerTransitionCoordinator?) {
DispatchQueue.main.async {
self.parent.isShown = true
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
func makeUIViewController(context: Context) -> some UIViewController {
let viewController = UIViewController()
viewController.view.backgroundColor = .clear
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
if self.isShown {
if !self.isPresented {
uiViewController.dismiss(animated: true) {
self.isShown = false
}
}
} else {
if self.isPresented {
let customView = CustomHostingView(rootView: content)
customView.view.backgroundColor = .systemBackground
customView.modalPresentationStyle = .popover
customView.popoverPresentationController?.permittedArrowDirections = arrowDirection
customView.popoverPresentationController?.delegate = context.coordinator
customView.popoverPresentationController?.sourceView = uiViewController.view
uiViewController.present(customView, animated: true)
}
}
}
}
呼び出し側は以下の通り。
import SwiftUI
struct ContentView: View {
@State var isPresented: Bool = false
var body: some View {
VStack {
Button("Show popover", action: {
self.isPresented.toggle()
}).popover(isPresented: $isPresented, content: {
Text("Hello, World!")
.padding(4)
}).buttonStyle(.borderedProminent)
}
.padding()
}
}
吹き出しを表示したいUIコンポーネントに拡張した .popover を指定して、isPresented を toggle() で変更させることで表示させる利用方法。.popover の中にはあらかじめ表示させておきたい View を指定しておく。