SwiftUI exporting or sharing files
Asked Answered
D

6

18

I'm wondering if there is a good way export or share a file through SwiftUI. There doesn't seem to be a way to wrap a UIActivityViewController and present it directly. I've used the UIViewControllerRepresentable to wrap a UIActivityViewController, and it crashes if I, say, present it in a SwiftUI Modal.

I was able to create a generic UIViewController and then from there call a method that presents the UIActivityViewController, but that's a lot of wrapping.

And if we want to share from the Mac using SwiftUI, is there a way to wrap NSSharingServicePicker?

Anyway, if anyone has an example of how they're doing this, it would be much appreciated.

Decastyle answered 29/6, 2019 at 17:7 Comment(2)
I will be trying to do this same thing this weekend. There's a question about using UIImagePickerController #56516371 that I have working and want to integrate into my app first. Take a look at that answer - particularly ContentView - and see f that can work for you. My concern is with getting an iPad "popover" instead of a full screen modal. I'll post my code if I get things working in the next day or two.Karriekarry
I have something working as a UIViewControllerRepresentable. My only issue i that it requires a View - I'm getting console warnings about presenting a view not in the view hierarchy. I think I should clear that up by using a Coordinator. I'll post some code later today.Karriekarry
A
6

Since iOS 16 you can use ShareLink:

ShareLink(item: "http://www.myappurl.com") {
    Label("Share app", systemImage: "square.and.arrow.up")
}

or just:

ShareLink(item: "http://www.myappurl.com")
Anzus answered 7/10, 2023 at 17:0 Comment(0)
C
21

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")
}
Costin answered 28/12, 2019 at 22:2 Comment(6)
while this is the way I want ti to work; it does not work for me. It just freezes the UI.Episcopalian
for image i am using share(items: [UIImage(named:"image")])Transitive
Did nothing for me.Fidelia
For me, this works with Xcode 12.4 and iOS 14.4. Just make sure to call the function from the main thread.Functionary
It takes a long time, then it shows the share sheet indeed, however, I get a lot of warnings on the console indicating that this will cause a failure (crash) in a future version.Drud
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene insteadHeraclea
K
8

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:

enter image description here

Hoping to help someone!

Keyes answered 27/7, 2020 at 2:24 Comment(1)
not working iPadOdetteodeum
T
8

Most of the solutions here forget to populate the share sheet on the iPad.

So, if you intend to have an application not crashing on this device, you can use this method where popoverController is used and add your desired activityItems as a parameter.

import SwiftUI

/// Share button to populate on any SwiftUI view.
///
struct ShareButton: View {

  /// Your items you want to share to the world.
  ///
  let itemsToShare = ["https://itunes.apple.com/app/id1234"]

  var body: some View {
    Button(action: { showShareSheet(with: itemsToShare) }) {
      Image(systemName: "square.and.arrow.up")
        .font(.title2)
        .foregroundColor(.blue)
    }
  }
}

extension View {
  /// Show the classic Apple share sheet on iPhone and iPad.
  ///
  func showShareSheet(with activityItems: [Any]) {
    guard let source = UIApplication.shared.windows.last?.rootViewController else {
      return
    }

    let activityVC = UIActivityViewController(
      activityItems: activityItems,
      applicationActivities: nil)

    if let popoverController = activityVC.popoverPresentationController {
      popoverController.sourceView = source.view
      popoverController.sourceRect = CGRect(x: source.view.bounds.midX,
                                            y: source.view.bounds.midY,
                                            width: .zero, height: .zero)
      popoverController.permittedArrowDirections = []
    }
    source.present(activityVC, animated: true)
  }
}
Theurich answered 20/12, 2020 at 13:42 Comment(0)
A
6

Since iOS 16 you can use ShareLink:

ShareLink(item: "http://www.myappurl.com") {
    Label("Share app", systemImage: "square.and.arrow.up")
}

or just:

ShareLink(item: "http://www.myappurl.com")
Anzus answered 7/10, 2023 at 17:0 Comment(0)
K
5

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 to present things.
  • You need var uiImage:UIImage! to set the activityItems.

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 to ContentView
  • 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 using shareImage as the Button action.
  • Using it to also be button display. Don't forget, even a UIViewControllerRepresentable is really just considered a SwiftUI View!

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.

Karriekarry answered 30/6, 2019 at 21:10 Comment(3)
Thanks for this @dfd . I ended up doing something similar since I posted the question. I especially like using the activityViewController as the label for the button so that the iPad popover works well.Decastyle
Glad I could help @MScottWaller. Questions about sourceView? Back in the day (iOS 9 & UIKit) I remember some "quirks" on setting it and even today my apps use the root VC view as it. My example, while working, actually covers most of the button. Is this something you know how to fix? Is it "intended behavior"? Or it it related to SwiftUI and/or iOS 13 (which really changed the appearance of it)?Karriekarry
That I cannot say. I haven't implemented the iPad popover as of yes, but I can let you know if I find anything.Decastyle
T
4

Take a look at AlanQuatermain -s SwiftUIShareSheetDemo

In a nutshell it looks like this:

@State private var showShareSheet = false
@State public var sharedItems : [Any] = []

Button(action: {
    self.sharedItems = [UIImage(systemName: "house")!]
    self.showShareSheet = true
}) {
    Text("Share")
}.sheet(isPresented: $showShareSheet) {
    ShareSheet(activityItems: self.sharedItems)
}
struct ShareSheet: UIViewControllerRepresentable {
    typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void

    let activityItems: [Any]
    let applicationActivities: [UIActivity]? = nil
    let excludedActivityTypes: [UIActivity.ActivityType]? = nil
    let callback: Callback? = nil

    func makeUIViewController(context: Context) -> UIActivityViewController {
        let controller = UIActivityViewController(
            activityItems: activityItems,
            applicationActivities: applicationActivities)
        controller.excludedActivityTypes = excludedActivityTypes
        controller.completionWithItemsHandler = callback
        return controller
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
        // nothing to do here
    }
}
Testudinal answered 3/2, 2020 at 10:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.