问题
I want to let the user to be able to share a Location but I don't know how to show UIActivityViewController in Swift UI
回答1:
The basic implementation of UIActivityViewController
in SwiftUI
is
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>) {}
}
And here is how to use it.
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")!])
})
}
}
回答2:
It's a one time thing currently. .sheet will show it as a sheet, but bringing it up again from the same view will have stale data. Those subsequent shows of the sheet will also not trigger any completion handlers. Basically, makeUIViewController is called only once which is the only way to get the data to share into the UIActivityViewController. updateUIViewController has no way to update the data in your activityItems or reset the controller because those are not visible from an instance of UIActivityViewController.
Note that it doesn't work with UIActivityItemSource or UIActivityItemProvider either. Using those is even worse. The placeholder value doesn't show.
I hacked around some more and decided that maybe the problem with my solution was a sheet that was presenting another sheet, and when one went away then the other stayed.
This indirect way of having a ViewController do the presentation when it appears made it work for me.
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()
}
}
回答3:
You could try porting UIActivityViewController
to SwiftUI
as follows:
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>) {
}
}
but the app will crash when you try to display it.
I tried: Modal
, Popover
and NavigationButton
.
To test it:
struct ContentView: View {
var body: some Body {
EmptyView
.presentation(Modal(ActivityView()))
}
}
It doesn't seem to be usable from SwiftUI
.
回答4:
I got it to work now using
.sheet(isPresented: $isSheet, content: { ActivityViewController() }
.presentation is deprecated
It takes up the whole screen iOS 13 style.
回答5:
Based on Tikhonov's, the following code added a fix to make sure the activity sheet is dismissed properly (if not subsequently the sheet will not be presented).
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>) {}
}
回答6:
FWIW - Providing a slight improvement to answers that includes an implementation for UIActivityItemSource
. Code simplified for brevity, specifically around the default return on itemForActivityType
and activityViewControllerPlaceholderItem
, they must always return the same type.
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 ""
}
}
You could pass in the reference for ActivityViewController
or the underlying UIActivityViewController
but that feels unnecessary.
回答7:
Thanks for the helpful answers in this thread.
I tried to solve the stale data
problem. The issue from not not implementing updateUIViewController
in UIViewControllerRepresentable
. SwiftUI calls makeUIViewController
only once to create the view controller. The method updateUIViewController
is responsible to make changes to view controller based on changes of the SwiftUI view.
As UIActivityViewController
does not allow to change activityItems
and applicationActivities
, I used a wrapper view controller. UIViewControllerRepresentable
will update the wrapper and the wrapper will create a new UIActivityViewController
as needed to perform the update.
Below my code to implement a "share" button in my application. The code is tested on iOS 13.4 beta, which has fixed several SwiftUI bugs - not sure if it works on earlier releases.
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
}
}
回答8:
I want to suggest another implementation that looks more native (half screen height without white gap bottom).
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()
}
}
来源:https://stackoverflow.com/questions/56533564/showing-uiactivityviewcontroller-in-swiftui