SwiftUI exporting or sharing files
We can call the UIActivityViewController directly from the View (SwiftUI) without using UIViewControllerRepresentable
.
import SwiftUI
enum Coordinator {
static func topViewController(_ viewController: UIViewController? = nil) -> UIViewController? {
let vc = viewController ?? UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController
if let navigationController = vc as? UINavigationController {
return topViewController(navigationController.topViewController)
} else if let tabBarController = vc as? UITabBarController {
return tabBarController.presentedViewController != nil ? topViewController(tabBarController.presentedViewController) : topViewController(tabBarController.selectedViewController)
} else if let presentedViewController = vc?.presentedViewController {
return topViewController(presentedViewController)
}
return vc
}
}
struct ActivityView: View {
var body: some View {
Button(action: {
self.shareApp()
}) {
Text("Share")
}
}
}
extension ActivityView {
func shareApp() {
let textToShare = "something..."
let activityViewController = UIActivityViewController(activityItems: [textToShare], applicationActivities: nil)
let viewController = Coordinator.topViewController()
activityViewController.popoverPresentationController?.sourceView = viewController?.view
viewController?.present(activityViewController, animated: true, completion: nil)
}
}
struct ActivityView_Previews: PreviewProvider {
static var previews: some View {
ActivityView()
}
}
And this is a preview:
Hoping to help someone!
EDIT: Removed all code and references to UIButton
.
Thanks to @Matteo_Pacini for his answer to this question for showing us this technique. As with his answer (and comment), (1) this is rough around the edges and (2) I'm not sure this is how Apple wants us to use UIViewControllerRepresentable
and I really hope they provide a better SwiftUI
("SwiftierUI"?) replacement in a future beta.
I put in a lot of work in UIKit
because I want this to look good on an iPad, where a sourceView
is needed for the popover. The real trick is to display a (SwiftUI) View
that gets the UIActivityViewController
in the view hierarchy and trigger present
from UIKit
.
My needs were to present a single image to share, so things are targeted in that direction. Let's say you have an image, stored as a @State
variable - in my example the image is called vermont.jpg and yes, things are hard-coded for that.
First, create a UIKit
class of type `UIViewController to present the share popover:
class ActivityViewController : UIViewController {
var uiImage:UIImage!
@objc func shareImage() {
let vc = UIActivityViewController(activityItems: [uiImage!], applicationActivities: [])
vc.excludedActivityTypes = [
UIActivity.ActivityType.postToWeibo,
UIActivity.ActivityType.assignToContact,
UIActivity.ActivityType.addToReadingList,
UIActivity.ActivityType.postToVimeo,
UIActivity.ActivityType.postToTencentWeibo
]
present(vc,
animated: true,
completion: nil)
vc.popoverPresentationController?.sourceView = self.view
}
}
The main things are;
- You need a "wrapper"
UIViewController
to be able topresent
things. - You need
var uiImage:UIImage!
to set theactivityItems
.
Next up, wrap this into a UIViewControllerRepresentable
:
struct SwiftUIActivityViewController : UIViewControllerRepresentable {
let activityViewController = ActivityViewController()
func makeUIViewController(context: Context) -> ActivityViewController {
activityViewController
}
func updateUIViewController(_ uiViewController: ActivityViewController, context: Context) {
//
}
func shareImage(uiImage: UIImage) {
activityViewController.uiImage = uiImage
activityViewController.shareImage()
}
}
The only two things of note are:
- Instantiating
ActivityViewController
to return it up toContentView
- Creating
shareImage(uiImage:UIImage
) to call it.
Finally, you have ContentView
:
struct ContentView : View {
let activityViewController = SwiftUIActivityViewController()
@State var uiImage = UIImage(named: "vermont.jpg")
var body: some View {
VStack {
Button(action: {
self.activityViewController.shareImage(uiImage: self.uiImage!)
}) {
ZStack {
Image(systemName:"square.and.arrow.up").renderingMode(.original).font(Font.title.weight(.regular))
activityViewController
}
}.frame(width: 60, height: 60).border(Color.black, width: 2, cornerRadius: 2)
Divider()
Image(uiImage: uiImage!)
}
}
}
Note that there's some hard-coding and (ugh) force-unwrapping of uiImage
, along with an unnecessary use of @State
. These are there because I plan to use `UIImagePickerController next to tie this all together.
The things of note here:
- Instantiating
SwiftUIActivityViewController
, and usingshareImage
as the Button action. - Using it to also be button display. Don't forget, even a
UIViewControllerRepresentable
is really just considered a SwiftUIView
!
Change the name of the image to one you have in your project, and this should work. You'll get a centered 60x60 button with the image below it.
You can define this function anywhere (preferably in the global scope):
@discardableResult
func share(
items: [Any],
excludedActivityTypes: [UIActivity.ActivityType]? = nil
) -> Bool {
guard let source = UIApplication.shared.windows.last?.rootViewController else {
return false
}
let vc = UIActivityViewController(
activityItems: items,
applicationActivities: nil
)
vc.excludedActivityTypes = excludedActivityTypes
vc.popoverPresentationController?.sourceView = source.view
source.present(vc, animated: true)
return true
}
You can use this function in a button action, or anywhere else needed:
Button(action: {
share(items: ["This is some text"])
}) {
Text("Share")
}