Отображение UIActivityViewController в SwitUI

Я хочу, чтобы пользователь мог поделиться местоположением, но я не знаю, как показать UIActivityViewController в Swift UI

16 ответов

Базовая реализация UIActivityViewController в SwiftUI является

import UIKit
import SwiftUI

struct ActivityViewController: UIViewControllerRepresentable {

    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
        return controller
    }

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

}

И вот как им пользоваться.

struct MyView: View {

    @State private var isSharePresented: Bool = false

    var body: some View {
        Button("Share app") {
            self.isSharePresented = true
        }
        .sheet(isPresented: $isSharePresented, onDismiss: {
            print("Dismiss")
        }, content: {
            ActivityViewController(activityItems: [URL(string: "https://www.apple.com")!])
        })
    }
}

В следующем коде, основанном на коде Тихонова, добавлено исправление, чтобы убедиться, что лист активности закрывается должным образом (в противном случае лист не будет представлен).

struct ActivityViewController: UIViewControllerRepresentable {

    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil
    @Environment(\.presentationMode) var presentationMode

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
        controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
            self.presentationMode.wrappedValue.dismiss()
        }
        return controller
    }

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

}

Это одноразовая вещь в настоящее время..sheet отобразит его как лист, но при повторном его вызове из того же представления будут устаревшие данные. Эти последующие показы листа также не будут запускать обработчики завершения. По сути, makeUIViewController вызывается только один раз, и это единственный способ получить данные для совместного использования в UIActivityViewController. updateUIViewController не имеет возможности обновить данные в ваших ActivityItems или сбросить настройки контроллера, потому что они не видны из экземпляра UIActivityViewController.

Обратите внимание, что он также не работает с UIActivityItemSource или UIActivityItemProvider. Использование тех еще хуже. Значение заполнителя не отображается.

Я взломал еще немного и решил, что, возможно, проблема с моим решением была листом, который представлял другой лист, и когда один ушел тогда другой остался.

Этот косвенный способ заставить ViewController делать презентацию, когда он появляется, заставил меня работать.

class UIActivityViewControllerHost: UIViewController {
    var message = ""
    var completionWithItemsHandler: UIActivityViewController.CompletionWithItemsHandler? = nil

    override func viewDidAppear(_ animated: Bool) {
        share()
    }

    func share() {
        // set up activity view controller
        let textToShare = [ message ]
        let activityViewController = UIActivityViewController(activityItems: textToShare, applicationActivities: nil)

        activityViewController.completionWithItemsHandler = completionWithItemsHandler
        activityViewController.popoverPresentationController?.sourceView = self.view // so that iPads won't crash

        // present the view controller
        self.present(activityViewController, animated: true, completion: nil)
    }
}

struct ActivityViewController: UIViewControllerRepresentable {
    @Binding var text: String
    @Binding var showing: Bool

    func makeUIViewController(context: Context) -> UIActivityViewControllerHost {
        // Create the host and setup the conditions for destroying it
        let result = UIActivityViewControllerHost()

        result.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
            // To indicate to the hosting view this should be "dismissed"
            self.showing = false
        }

        return result
    }

    func updateUIViewController(_ uiViewController: UIActivityViewControllerHost, context: Context) {
        // Update the text in the hosting controller
        uiViewController.message = text
    }

}

struct ContentView: View {
    @State private var showSheet = false
    @State private var message = "a message"

    var body: some View {
        VStack {
            TextField("what to share", text: $message)

            Button("Hello World") {
                self.showSheet = true
            }

            if showSheet {
                ActivityViewController(text: $message, showing: $showSheet)
                    .frame(width: 0, height: 0)
            }

            Spacer()
        }
        .padding()
    }
}

Может быть, это не рекомендуется, но это действительно просто и две строки кода для обмена текстом

Button(action: {
     let shareActivity = UIActivityViewController(activityItems: ["Text To Share"], applicationActivities: nil)
      UIApplication.shared.windows.first?.rootViewController?.present(shareActivity, animated: true, completion: nil)
}) {
    Text("Share")
}

Я хочу предложить другую реализацию, которая выглядит более естественной (половина высоты экрана без белого промежутка внизу).

import SwiftUI

struct ActivityView: UIViewControllerRepresentable {
    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil
    @Binding var isPresented: Bool

    func makeUIViewController(context: Context) -> ActivityViewWrapper {
        ActivityViewWrapper(activityItems: activityItems, applicationActivities: applicationActivities, isPresented: $isPresented)
    }

    func updateUIViewController(_ uiViewController: ActivityViewWrapper, context: Context) {
        uiViewController.isPresented = $isPresented
        uiViewController.updateState()
    }
}

class ActivityViewWrapper: UIViewController {
    var activityItems: [Any]
    var applicationActivities: [UIActivity]?

    var isPresented: Binding<Bool>

    init(activityItems: [Any], applicationActivities: [UIActivity]? = nil, isPresented: Binding<Bool>) {
        self.activityItems = activityItems
        self.applicationActivities = applicationActivities
        self.isPresented = isPresented
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func didMove(toParent parent: UIViewController?) {
        super.didMove(toParent: parent)
        updateState()
    }

    fileprivate func updateState() {
        guard parent != nil else {return}
        let isActivityPresented = presentedViewController != nil
        if isActivityPresented != isPresented.wrappedValue {
            if !isActivityPresented {
                let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
                controller.completionWithItemsHandler = { (activityType, completed, _, _) in
                    self.isPresented.wrappedValue = false
                }
                present(controller, animated: true, completion: nil)
            }
            else {
                self.presentedViewController?.dismiss(animated: true, completion: nil)
            }
        }
    }
}

struct ActivityViewTest: View {
    @State private var isActivityPresented = false
    var body: some View {
        Button("Preset") {
            self.isActivityPresented = true
        }.background(ActivityView(activityItems: ["Hello, World"], isPresented: $isActivityPresented))
    }
}

struct ActivityView_Previews: PreviewProvider {
    static var previews: some View {
        ActivityViewTest()
    }
}

Если вам нужен более детальный контроль над содержимым, отображаемым на общем листе, вы, вероятно, прекратите реализацию UIActivityItemSource. Я попытался использовать приведенный выше код Майка В., но сначала он не сработал (функции делегата не вызывались). Исправление меняло инициализацию UIActivityController в makeUIViewController следующим образом, теперь проходя [context.coordinator] в качестве activityItems:

      let controller = UIActivityViewController(activityItems: [context.coordinator], applicationActivities: applicationActivities)

Кроме того, я хотел иметь возможность установить значок, заголовок и подзаголовок на общем листе, поэтому я реализовал func activityViewControllerLinkMetadata в Coordinator учебный класс.

Ниже приводится полная расширенная версия ответа Майка В. Обратите внимание, что вам нужно будет добавить import LinkPresentation к коду.

ActivityViewController

      import SwiftUI
import LinkPresentation

struct ActivityViewController: UIViewControllerRepresentable {
    var shareable : ActivityShareable?
    var applicationActivities: [UIActivity]? = nil

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: [context.coordinator], applicationActivities: applicationActivities)
        controller.modalPresentationStyle = .automatic
        return controller
    }

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

    func makeCoordinator() -> ActivityViewController.Coordinator {
        Coordinator(self.shareable)
    }

    class Coordinator : NSObject, UIActivityItemSource {
        private let shareable : ActivityShareable?

        init(_ shareable: ActivityShareable?) {
            self.shareable = shareable
            super.init()
        }

        func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
            guard let share = self.shareable else { return "" }
            return share.getPlaceholderItem()
        }

        func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
            guard let share = self.shareable else { return "" }
            return share.itemForActivityType(activityType: activityType)
        }

        func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
            guard let share = self.shareable else { return "" }
            return share.subjectForActivityType(activityType: activityType)
        }
        
        func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
            guard let share = self.shareable else { return nil }
            
            let metadata = LPLinkMetadata()

            // share sheet preview title
            metadata.title = share.shareSheetTitle()
            // share sheet preview subtitle
            metadata.originalURL = URL(fileURLWithPath: share.shareSheetSubTitle())
            // share sheet preview icon
            if let image = share.shareSheetIcon() {
                let imageProvider = NSItemProvider(object: image)
                metadata.imageProvider = imageProvider
                metadata.iconrovider = imageProvider
            }
            return metadata
        }
    }
}   

Активность протокола

      protocol ActivityShareable {
    func getPlaceholderItem() -> Any
    func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any?
    func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String
    func shareSheetTitle() -> String
    func shareSheetSubTitle() -> String
    func shareSheetIcon() -> UIImage?
}

В моем случае я использую общий лист для экспорта текста, поэтому я создал структуру с именем ActivityShareableText, которая соответствует ActivityShareable:

      struct ActivityShareableText: ActivityShareable {
    let text: String
    let title: String
    let subTitle: String
    let icon: UIImage?
    
    func getPlaceholderItem() -> Any {
        return text
    }
    
    func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any? {
        return text
    }
    
    func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String {
        return "\(title): \(subTitle)"
    }
    
    func shareSheetTitle() -> String {
        return title
    }
    
    func shareSheetSubTitle() -> String {
        return subTitle
    }
    
    func shareSheetIcon() -> UIImage? {
        return icon
    }
}

В моем коде я называю общий лист следующим образом:

      ActivityViewController(shareable: ActivityShareableText(
    text: myShareText(),
    title: myShareTitle(),
    subTitle: myShareSubTitle(),
    icon: UIImage(named: "myAppLogo")
))

Я получил его на работу сейчас, используя

.sheet(isPresented: $isSheet, content: { ActivityViewController() }

.presentation устарела

Он занимает весь экран в стиле iOS 13.

FWIW - Обеспечивает небольшое улучшение ответов, включая реализацию для UIActivityItemSource. Код упрощен для краткости, особенно в отношении возврата по умолчаниюitemForActivityType а также activityViewControllerPlaceholderItem, они всегда должны возвращать один и тот же тип.

ActivityViewController

struct ActivityViewController: UIViewControllerRepresentable {

    var activityItems: [Any]
    var shareable : ActivityShareable?
    var applicationActivities: [UIActivity]? = nil

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
        controller.modalPresentationStyle = .automatic
        return controller
    }

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

    func makeCoordinator() -> ActivityViewController.Coordinator {
        Coordinator(self.shareable)
    }

    class Coordinator : NSObject, UIActivityItemSource {

        private let shareable : ActivityShareable?

        init(_ shareable: ActivityShareable?) {
            self.shareable = shareable
            super.init()
        }

        func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
            guard let share = self.shareable else { return "" }
            return share.getPlaceholderItem()
        }

        func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
            guard let share = self.shareable else { return "" }
            return share.itemForActivityType(activityType: activityType)
        }

        func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
            guard let share = self.shareable else { return "" }
            return share.subjectForActivityType(activityType: activityType)
        }
    }
}

ActivityShareable

protocol ActivityShareable {

    func getPlaceholderItem() -> Any
    func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any?

    /// Optional
    func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String
}

extension ActivityShareable {

    func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String {
        return ""
    }
}

Вы можете передать ссылку на ActivityViewController или лежащий в основе UIActivityViewController но это кажется ненужным.

Расширение решения @Shimanski Artem. Я думаю, мы можем написать этот код более кратким. Так что я в основном вставляю ActivityViewController в пустом UIViewControllerи представить его оттуда. Таким образом, мы не получаем полный лист «оверлей», а вы получаете естественное поведение. Как и @Shimanski Artem.

      struct UIKitActivityView: UIViewControllerRepresentable {
    @Binding var isPresented: Bool

    let data: [Any]

    func makeUIViewController(context: Context) -> UIViewController {
        UIViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        let activityViewController = UIActivityViewController(
            activityItems: data,
            applicationActivities: nil
        )

        if isPresented && uiViewController.presentedViewController == nil {
            uiViewController.present(activityViewController, animated: true)
        }

        activityViewController.completionWithItemsHandler = { (_, _, _, _) in
            isPresented = false
        }
    }
}

использование

      struct ActivityViewTest: View {
    @State private var isActivityPresented = false

    var body: some View {
        Button("Preset") {
           self.isActivityPresented = true
        }
        .background(
            UIKitActivityView(
                isPresented: $viewModel.showShareSheet,
                data: ["String"]
            )
        )
    }
}

Вы можете попробовать портировать UIActivityViewController в SwiftUI следующее:

struct ActivityView: UIViewControllerRepresentable {

    let activityItems: [Any]
    let applicationActivities: [UIActivity]?

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
        return UIActivityViewController(activityItems: activityItems,
                                        applicationActivities: applicationActivities)
    }

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

    }
}

но приложение вылетает при попытке отобразить его.

Я пытался: Modal, Popover а также NavigationButton,

Чтобы проверить это:

struct ContentView: View {
    var body: some Body {
        EmptyView
        .presentation(Modal(ActivityView()))
    }
}

Кажется, он не может быть использован из SwiftUI,

Пример использования SwiftUIX

Существует библиотека SwiftUIX, в которой уже есть оболочка для UIActivityViewController. Посмотрите краткую схему того, как представить его через.sheet() который следует разместить где-нибудь в var body: some View {}.

import SwiftUIX

/// ...

@State private var showSocialsInviteShareSheet: Bool = false

// ...

.sheet(isPresented: $showSocialsInviteShareSheet, onDismiss: {
    print("Dismiss")
}, content: {
    AppActivityView(activityItems: [URL(string: "https://www.apple.com")!])
})

Предложите другой способ решения 🤔

Вы можете создать пустой контроллер представления для представления листа

      struct ShareSheet: UIViewControllerRepresentable {
  // To setup the share sheet
  struct Config {
    let activityItems: [Any]
    var applicationActivities: [UIActivity]?
    var excludedActivityTypes: [UIActivity.ActivityType]?
  }

  // Result object
  struct Result {
    let error: Error?
    let activityType: UIActivity.ActivityType?
    let completed: Bool
    let returnedItems: [Any]?
  }

  @Binding var isPresented: Bool

  private var handler: ((Result) -> Void)?
  private let shareSheet: UIActivityViewController

  init(
    isPresented: Binding<Bool>,
    config: Config,
    onEnd: ((Result) -> Void)? = nil
  ) {
    self._isPresented = isPresented
    shareSheet = UIActivityViewController(
      activityItems: config.activityItems,
      applicationActivities: config.applicationActivities
    )
    shareSheet.excludedActivityTypes = config.excludedActivityTypes
    shareSheet.completionWithItemsHandler = { activityType, completed, returnedItems, error in
      onEnd?(
        .init(
          error: error,
          activityType: activityType,
          completed: completed,
          returnedItems: returnedItems
        )
      )
      // Set isPresented to false after complete
      isPresented.wrappedValue = false
    }
  }

  func makeUIViewController(context: Context) -> UIViewController {
    UIViewController()
  }

  func updateUIViewController(
    _ uiViewController: UIViewController,
    context: Context
  ) {
    if isPresented, shareSheet.view.window == nil {
      uiViewController.present(shareSheet, animated: true, completion: nil)
    } else if !isPresented, shareSheet.view.window != nil {
      shareSheet.dismiss(animated: true)
    }
  }
}

Вы также можете создать оператор в расширении представления

      extension View {
  func shareSheet(
    isPresented: Binding<Bool>,
    config: ShareSheet.Config,
    onEnd: ((ShareSheet.Result) -> Void)? = nil
  ) -> some View {
    self.background(
      ShareSheet(isPresented: isPresented, config: config, onEnd: onEnd)
    )
  }
}

Просто используйте самоанализ. Тогда вы можете легко закодировать что-то вроде этого:

      YourView().introspectViewController { controller in
    guard let items = viewModel.inviteLinkParams, viewModel.isSharePresented else { return }
    let activity = UIActivityViewController(activityItems: items, applicationActivities: nil)
    controller.present(activity, animated: true, completion: nil)
}

Спасибо за полезные ответы в этой теме.

Я пытался решить stale dataпроблема. Проблема из-за невыполненияupdateUIViewController в UIViewControllerRepresentable. SwiftUI звонкиmakeUIViewControllerтолько один раз, чтобы создать контроллер представления. МетодupdateUIViewController отвечает за внесение изменений в контроллер представления на основе изменений представления SwiftUI.

В качестве UIActivityViewController не позволяет изменить activityItems а также applicationActivities, Я использовал контроллер представления оболочки. UIViewControllerRepresentable обновит оболочку, а оболочка создаст новый UIActivityViewController по мере необходимости для выполнения обновления.

Ниже мой код для реализации кнопки "поделиться" в моем приложении. Код протестирован на бета-версии iOS 13.4, в которой исправлено несколько ошибок SwiftUI - не уверен, работает ли он в более ранних выпусках.

struct Share: View {
    var document: ReaderDocument   // UIDocument subclass
    @State var showShareSheet = false

    var body: some View {
        Button(action: {
            self.document.save(to: self.document.fileURL, for: .forOverwriting) { success in
                self.showShareSheet = true
            }
        }) {
            Image(systemName: "square.and.arrow.up")
        }.popover(isPresented: $showShareSheet) {
            ActivityViewController(activityItems: [ self.document.text, self.document.fileURL,
                                                    UIPrintInfo.printInfo(), self.printFormatter ])
              .frame(minWidth: 320, minHeight: 500)  // necessary for iPad
        }
    }

    var printFormatter: UIPrintFormatter {
        let fontNum = Preferences.shared.readerFontSize.value
        let fontSize = ReaderController.readerFontSizes[fontNum < ReaderController.readerFontSizes.count ? fontNum : 1]
        let printFormatter = UISimpleTextPrintFormatter(text: self.document.text)
        printFormatter.startPage = 0
        printFormatter.perPageContentInsets = UIEdgeInsets(top: 72, left: 72, bottom: 72, right: 72)
        return printFormatter
    }
}

struct ActivityViewController: UIViewControllerRepresentable {

    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil
    @Environment(\.presentationMode) var presentationMode

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>)
        -> WrappedViewController<UIActivityViewController> {
        let controller = WrappedViewController(wrappedController: activityController)
        return controller
    }

    func updateUIViewController(_ uiViewController: WrappedViewController<UIActivityViewController>,
                                context: UIViewControllerRepresentableContext<ActivityViewController>) {
        uiViewController.wrappedController = activityController
    }

    private var activityController: UIActivityViewController {
        let avc = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
        avc.completionWithItemsHandler = { (_, _, _, _) in
            self.presentationMode.wrappedValue.dismiss()
        }
        return avc
    }
}

class WrappedViewController<Controller: UIViewController>: UIViewController {
    var wrappedController: Controller {
        didSet {
            if (wrappedController != oldValue) {
                oldValue.removeFromParent()
                oldValue.view.removeFromSuperview()
                addChild(wrappedController)
                view.addSubview(wrappedController.view)
                wrappedController.view.frame = view.bounds
            }
        }
    }

    init(wrappedController: Controller) {
        self.wrappedController = wrappedController
        super.init(nibName: nil, bundle: nil)
        addChild(wrappedController)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func loadView() {
        super.loadView()
        view.addSubview(wrappedController.view)
        wrappedController.view.frame = view.bounds
    }
}

Swift 5 / собственный интерфейс SwiftUI

Просто, с обратным вызовом завершения и собственным SwiftUI @Binding

      import SwiftUI

struct ShareSheet: UIViewControllerRepresentable {
    typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
    
    @Binding var isPresented: Bool
    @Binding var activityItem: String
    
    let applicationActivities: [UIActivity]? = nil
    let excludedActivityTypes: [UIActivity.ActivityType]? = nil
    let callback: Callback?
    
    func makeUIViewController(context: Context) -> UIActivityViewController {
        
        let controller = UIActivityViewController(
            activityItems: [activityItem],
            applicationActivities: applicationActivities)
        
        controller.excludedActivityTypes = excludedActivityTypes
        controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
            callback?(activityType, completed, returnedItems, error)
            isPresented = false
        }
        
        return controller
    }
    
    func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
        
    }
}

пример использования:

      ShareSheet(isPresented: $showShareSheet, activityItem: $sharingUrl, callback: { activityType, completed, returnedItems, error in
    
    print("ShareSheet dismissed: \(activityType) \(completed) \(returnedItems) \(error)")
                    
})

Для ios 13 используйте пользовательский вид в качестве индикатора загрузки, для ios 14 используйте ProgressView()

      struct LoadingIndicator: View {
    @State private var animating = false
    var body: some View {
        Image(systemName: "rays")
            .rotationEffect(animating ? Angle.degrees(360) : .zero)
            .animation(Animation
                        .linear(duration: 2)
                        .repeatForever(autoreverses: false)
            )
            .onAppear { self.animating = true }
    }
}
Другие вопросы по тегам