SwiftUIでUIActivityViewControllerを表示する

2022.12.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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を表現出来ました。

swiftui-activity-view-controller-1

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を取得して、そのUIViewControllerUIActivityViewControllerを表示する方法です。

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
    }
}

取得した最前面のUIViewControllerUIActivityViewControllerを表示するメソッドを作成しました。

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を表示させることが出来ました。

swiftui-activity-view-controller-2

おわりに

iOS 15でハーフモーダルを実現する方法は、こちらのGitHubリポジトリのコードを参考にさせていただきました。

最前面のUIViewControllerを取得する方法はこちらを参考にさせていただきました。

SwiftUIでUIKitの力を借りずにUIActivityViewControllerを表示できる日を心待ちにしております。

参考