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
を表示できる日を心待ちにしております。