SwiftUI で ファイルを外部にShareするには

目次

以下のような感じで SwiftUI で アプリケーション内部のファイルを外部に Share する方法です。 UIActivityViewController を使うんだろうな、というのは少しググるとわかるのですが SwiftUI 初心者には具体的にどう書いたら良いかわからなかったので書き残しておきます。

ファイル共有機能の呼び出しイメージ

環境

  • MacOS: 12.1
  • Xcode: Version 13.2.1 (13C100)
  • Swift: 5.5.2

Case1: SwiftUI で UIActivityViewController を呼び出す方法(シンプル版)

SwiftUI から UIActivityViewController を直接は扱えないので、 UIViewControllerRepresentable というのを経由して呼び出すと良いようです。

Step1: UIActivityViewController を扱う ActivityViewController を作る

// ActivityViewController.swift
import SwiftUI

struct ActivityViewController: UIViewControllerRepresentable {
    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil
    var completionHandler: ((Bool) -> Void)? = nil

    func makeUIViewController(context: UIViewControllerRepresentableContext<Self>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
        if self.completionHandler != nil {
            controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
                self.completionHandler?(completed)
            }
        }
        return controller
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<Self>) {}
}

completionHandler は、共有が終わったら呼び出されます(今回は使わなかったですが…)。

Step2: .sheet を使って、ボタンを押したら出現するようにする

例えば、こんな感じになります。

struct SoundDetailView: View {
    @State var showingSheet: Bool = false
    let fileUrl: URL

    var body: some View {
        Button {
            showingSheet = true
        } label: {
            Text("共有する!")
        }
        .sheet(isPresented: $showingSheet) {
            createFileShareView(fileUrl)
        }
    }
}

func createFileShareView(_ url: URL) -> some View {
    return ActivityViewController(activityItems: [url])
}

Case2: SwiftUI で UIActivityViewController を呼び出す方法(ファイル名を指定したい場合)

共有されるときのファイル名を指定したい場合があります。が、API で指定することはできないようです。 代わりに、ファイル名がデフォルトの共有ファイル名になるので、事前にコピーして、終わったら削除すれば一応実現できます。 ちなみに、Symlink ではダメでした。

先程の createFileShareView を以下のようにするとできるようになります。


...
        .sheet(isPresented: $showingSheet) {
            createFileShareView(fileUrl, newName: "awesome_file")
        }
...

func createFileShareView(_ origUrl: URL, newName: String) -> some View {
    let ext = origUrl.pathExtension
    let newUrl = origUrl.deletingLastPathComponent().appendingPathComponent(newName, isDirectory: false).appendingPathExtension(ext)

    if FileManager.default.fileExists(atPath: newUrl.path) {
        try! FileManager.default.removeItem(at: newUrl)
    }
    try! FileManager.default.copyItem(at: origUrl, to: newUrl)

    return ActivityViewController(activityItems: [newUrl])
        .onDisappear {
            if FileManager.default.fileExists(atPath: newUrl.path) {
                try! FileManager.default.removeItem(at: newUrl)
            }
        }
}

.onDisappear() でコピーしたファイルを消すのが一つポイントです。 ActivityViewControllercompletionHandler で消してしまうと、1 回の Sheet 表示中にユーザーが何度も共有しようとした場合に、 1 回目の終了後にファイルが消えてしまうので 2 回目以降が失敗してしまいます。

参考