21

I'm trying to present a UIActivityViewController (share sheet) from a SwiftUI View. I created a view called ShareSheet conformed to UIViewControllerRepresentable to configure the UIActivityViewController, but it's turning out to be not as trivial to actually present this.

struct ShareSheet: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIActivityViewController

    var sharing: [Any]

    func makeUIViewController(context: UIViewControllerRepresentableContext<ShareSheet>) -> UIActivityViewController {
        UIActivityViewController(activityItems: sharing, applicationActivities: nil)
    }

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

    }
}

Doing so naively via .sheet leads to the following.

.sheet(isPresented: $showShareSheet) {
    ShareSheet(sharing: [URL(string: "https://example.com")!])
}

screenshot of failed controller presentation

Is there a way to present this like it's usually presented? As in covering half the screen?

Jonas Deichelmann
  • 3,513
  • 1
  • 30
  • 45
Kilian
  • 2,122
  • 2
  • 24
  • 42

4 Answers4

11

Hope this will help you,

struct ShareSheetView: View {
    var body: some View {
        Button(action: actionSheet) {
            Image(systemName: "square.and.arrow.up")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 36, height: 36)
        }
    }
    
    func actionSheet() {
        guard let data = URL(string: "https://www.apple.com") else { return }
        let av = UIActivityViewController(activityItems: [data], applicationActivities: nil)
        UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
    }
}

enter image description here

Azhagusundaram Tamil
  • 2,053
  • 3
  • 21
  • 36
  • Sorry for the late reply, but thank you for this! – Kilian Jan 11 '21 at 08:47
  • 11
    This is a bad solution and should not be marked as accepted. ` UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)` will fail to get the correct vc in many scenarios. – Kirill Aug 05 '21 at 00:47
  • I saw that issue with trying to use windows first.present. I used a .sheet instead as per OP's version. Something to watch out for is the keyboard not working normally in the share sheet. The responder chain is fragile if your sheet isn't contained as part of the parent view, I moved to use the outermost parent view. Looks like something that has come in with iOS15. – unB Dec 12 '21 at 21:07
3

iOS 15 / Swift 5 / Xcode 13

Extension to get the top presented UIViewController:

import UIKit

extension UIApplication {
    
    // MARK: No shame!
    
    static func TopPresentedViewController() -> UIViewController? {
        
        guard let rootViewController = UIApplication.shared
                .connectedScenes.lazy
                .compactMap({ $0.activationState == .foregroundActive ? ($0 as? UIWindowScene) : nil })
                .first(where: { $0.keyWindow != nil })?
                .keyWindow?
                .rootViewController
        else {
            return nil
        }
        
        var topController = rootViewController
        
        while let presentedViewController = topController.presentedViewController {
            topController = presentedViewController
        }
        
        return topController
        
    }
    
}

Then use it to present your UIActivityViewController:

UIApplication.TopPresentedViewController?.present(activityViewController, animated: true, completion: nil)

Original Answer (deprecated code):

It's not pretty but you can call it directly like this (considering your app has only 1 window):

UIApplication.shared.windows.first?.rootViewController?.present(activityViewController, animated: true, completion: nil)

And if you get some warning blablabla:

Warning: Attempt to present ... which is already presenting ...

you can do something like this to get the top most view controller and call present on it.

JS1010111
  • 341
  • 2
  • 12
3

In iOS 14, Swift 5, Xcode 12.5 at least, I was able to accomplish this fairly easily by simply wrapping the UIActivityViewController in another view controller. It doesn't require inspecting the view hierarchy or using any 3rd party libraries. The only hackish part is asynchronously presenting the view controller, which might not even be necessary. Someone with more SwiftUI experience might be able to offer suggestions for improvement.

import Foundation
import SwiftUI
import UIKit

struct ActivityViewController: UIViewControllerRepresentable {
        
    @Binding var shareURL: URL?
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> some UIViewController {
        let containerViewController = UIViewController()
        
        return containerViewController

    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        guard let shareURL = shareURL, context.coordinator.presented == false else { return }
        
        context.coordinator.presented = true

        let activityViewController = UIActivityViewController(activityItems: [shareURL], applicationActivities: nil)
        activityViewController.completionWithItemsHandler = { activity, completed, returnedItems, activityError in
            self.shareURL = nil
            context.coordinator.presented = false

            if completed {
                // ...
            } else {
                // ...
            }
        }
        
        // Executing this asynchronously might not be necessary but some of my tests
        // failed because the view wasn't yet in the view hierarchy on the first pass of updateUIViewController
        //
        // There might be a better way to test for that condition in the guard statement and execute this
        // synchronously if we can be be sure updateUIViewController is invoked at least once after the view is added
        DispatchQueue.main.asyncAfter(deadline: .now()) {
            uiViewController.present(activityViewController, animated: true)
        }
    }
    
    class Coordinator: NSObject {
        let parent: ActivityViewController
        
        var presented: Bool = false
        
        init(_ parent: ActivityViewController) {
            self.parent = parent
        }
    }
    
}
struct ContentView: View {
    
    @State var shareURL: URL? = nil
    
    var body: some View {
        ZStack {
            Button(action: { shareURL = URL(string: "https://apple.com") }) {
                Text("Share")
                    .foregroundColor(.white)
                    .padding()
            }
            .background(Color.blue)
            if shareURL != nil {
                ActivityViewController(shareURL: $shareURL)
            }
        }
        .frame(width: 375, height: 812)
    }
}
kball
  • 4,923
  • 3
  • 29
  • 31
2

There's a UIModalPresentationStyle which can be used to display certain presentations:

case pageSheet

A presentation style that partially covers the underlying content.

The way you apply the presentation style:

func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
    let v = UIActivityViewController(activityItems: sharing, applicationActivities: nil)
    v.modalPresentationStyle = .pageSheet
    return v
}

A list of the Presentations can be found here:

https://developer.apple.com/documentation/uikit/uimodalpresentationstyle

I haven't yet tested them all myself so I apologise in advance if this didn't end up working like you expected it to.

Alternatively you can have a look at this answer where they mention a third-party library, which will allow you to create a half modal in the way that it's usually presented.

gotnull
  • 26,454
  • 22
  • 137
  • 203
  • Thanks for the hint, but I'm not sure if it's possible to specify a UIModalPresentationStyle with SwiftUI :/ – Kilian Oct 09 '19 at 20:41
  • @KilianKoeltzsch I've updated the answer to include how you apply the `modalPresentationStyle`. In saying that you're probably better off modifying the third party library to suit your needs. You'll have more control as well. – gotnull Oct 10 '19 at 00:08
  • 3
    Ah, interesting, I didn't even think of setting it when creating the view controller. It appears though this doesn't get respected when showing it via `.sheet()`. I'll look into the linked option as well. – Kilian Oct 10 '19 at 05:35
  • 4
    Even if you wrap your SwiftUI view in `UIHostingController` and set `modalPresentationStyle` of the wrapper to `pageSheet`, then present the above using top controller, is still going to be almost full-screen (like on the picture in the question). `pageSheet` is certainly respected though, as setting to something else does change the appearance. Alas, I wasn't able to get sheet half-covering the screen. – NeverwinterMoon Apr 12 '20 at 12:59