SwiftUIでUIActivityViewControllerを表示する
SwiftUIでUIActivityViewController
を表示する方法を調べたので記事にしておきます。
環境
- Xcode 14.1
- iOS 16.1
UIActivityViewController
アプリで情報をシェアさせたい時などに表示するViewControllerです。
UIActivityViewController
はUIKitのコンポーネントでSwiftUIで表示する為には一工夫が必要です。
UIViewControllerRepresentableを使用する
iOS 16以上の場合
iOS 16からは、presentationDetents(_:)
モディファイアが使用できる為、比較的簡単に実装出来ます。
ActivityView
UIViewControllerRepresentable
に準拠させて作成したUIActivityViewController
をラップしたViewになります。
import SwiftUI import UIKit struct ActivityView: UIViewControllerRepresentable { let activityItems: [Any] let applicationActivities: [UIActivity]? func makeUIViewController(context: Context) -> UIActivityViewController { let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) return controller } func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { } }
使用方法
.sheet
で呼び出しするActivityView
に.presentationDetents([.medium])
を付けるとハーフモーダルにすることが出来ます。今回は、.medium
と.large
で設定しておきました。
struct ContentView: View { @State private var isPresentActivityController = false var body: some View { Button("ActivityController表示") { isPresentActivityController = true } .sheet(isPresented: $isPresentActivityController) { ActivityView(activityItems: ["シェア"], applicationActivities: nil) .presentationDetents([.medium, .large]) } } }
無事にUIActivityViewController
を表現出来ました。
iOS 15
presentationDetents(_:)
はiOS 15では使用できないですが、UIKitのUISheetPresentationController.Detent
はiOS 15から使用できる為、ハーフモーダル対応Viewを作成してそれをSwiftUI側で表示する方法です。
ハーフモーダル表示できるSheet
struct Sheet<Content>: UIViewRepresentable where Content: View { @Binding var isPresented: Bool let detents: [UISheetPresentationController.Detent] @ViewBuilder let content: Content func makeUIView(context: Context) -> UIView { .init() } func updateUIView(_ uiView: UIView, context: Context) { let hostingController = UIHostingController(rootView: content) if let sheetController = hostingController.sheetPresentationController { sheetController.detents = detents sheetController.prefersGrabberVisible = true sheetController.prefersScrollingExpandsWhenScrolledToEdge = false sheetController.largestUndimmedDetentIdentifier = .medium } if isPresented { uiView.window?.rootViewController?.present(hostingController, animated: true) } else { uiView.window?.rootViewController?.dismiss(animated: true) } } }
Modifier
import SwiftUI struct SheetPresentationModifier<SheetContent: View>: ViewModifier { @Binding var isPresented: Bool let onDismiss: (() -> Void)? let detents: [UISheetPresentationController.Detent] @ViewBuilder let sheetContent: () -> SheetContent func body(content: Content) -> some View { ZStack { Sheet(isPresented: $isPresented, detents: detents, content: { sheetContent() .onDisappear { isPresented = false onDismiss?() } }) content } } }
View+Extension
extension View { func sheet(isPresented: Binding, onDismiss: (() -> Void)? = nil, detents: [UISheetPresentationController.Detent], content: @escaping () -> SheetContent) -> some View { modifier(SheetPresentationModifier(isPresented: isPresented, onDismiss: onDismiss, detents: detents, sheetContent: content)) } }
使用方法
作成したハーフモーダル表示できるSheetのコンテンツにActivityView
を設定して表示します。
struct ContentView: View { @State private var isPresentActivityController = false var body: some View { Button("ActivityController表示") { isPresentActivityController = true } .sheet(isPresented: $isPresentActivityController, detents: [.medium(), .large()]) { ActivityView(activityItems: ["シェア"], applicationActivities: nil) } } }
最前面のViewControllerを取得してUIActivityViewControllerを表示する
最後は、アプリのKeyWindowのrootViewController
を取得し、そのrootViewController
の最前面のUIViewController
を取得して、そのUIViewController
にUIActivityViewController
を表示する方法です。
import UIKit extension UIApplication { private var KeyWindowRootViewController: UIViewController? { return UIApplication.shared.connectedScenes .map { $0 as? UIWindowScene } .compactMap { $0 } .first? .windows .filter { $0.isKeyWindow } .first? .rootViewController } // 最前面のViewControllerを取得 var topViewController: UIViewController? { guard let rootViewController = UIApplication.shared.KeyWindowRootViewController else { return nil } var presentedViewController = rootViewController.presentedViewController if presentedViewController == nil { return rootViewController } while presentedViewController?.presentedViewController != nil { presentedViewController = presentedViewController?.presentedViewController } return presentedViewController } }
取得した最前面のUIViewController
にUIActivityViewController
を表示するメソッドを作成しました。
extension UIActivityViewController { static func show(activityItems: [Any], applicationActivities: [UIActivity]?) { let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) guard let topViewController = UIApplication.shared.topViewController else { return } // iPadのクラッシュ対策 activityViewController.popoverPresentationController?.sourceView = topViewController.view activityViewController.popoverPresentationController?.sourceRect = CGRect(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height / 4, width: 0, height: 0) topViewController.present(activityViewController, animated: true) } }
使用方法
struct ContentView: View { var body: some View { Button("ActivityController表示") { UIActivityViewController.show(activityItems: ["シェア"], applicationActivities: nil) } } }
こちらの方法でも無事にUIActivityViewController
を表示させることが出来ました。
おわりに
iOS 15でハーフモーダルを実現する方法は、こちらのGitHubリポジトリのコードを参考にさせていただきました。
最前面のUIViewController
を取得する方法はこちらを参考にさせていただきました。
SwiftUIでUIKitの力を借りずにUIActivityViewController
を表示できる日を心待ちにしております。