How can create a share sheet/view with SwiftUI for iOS 13 ?

How can create a new "Share View" ( now called "Share sheet" ? ) for iOS 13 ?

In the new features of the iOS 13, we saw how the look (and possibly the way) with which we can share with other applications has changed.


As I read in this article : iOS 13: The Ars Technica review

I want to be able to display this menu with all the share options available in my SwiftUI app:



So, even easier, how can I simply share a link - a text - to another App with Swift 5 using SwiftUI?

What is the new way proposed ? What is the code that I have to write in my View to display this menu of options?


Thank you.

Accepted Reply

It works in much the same way as in UIKit, but you'll have to use

.sheet(isPresented:content:)
to make the view appear.


What you need is the

UIActivityViewController
, which means you'll have to create a SwiftUI
UIViewControllerRepresentable
view for that. In this case it's fairly straightforward, since you're not modifying the content or maintaining any state, you're effectively wrapping the initializer. This version works for me:


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
    }
}


In your other view, you want this to appear modally, so you use the

.sheet()
method to do so, along with an
isPresented
state variable, like so:


struct ContentView: View {
    @State private var showShareSheet = false
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Hello World")
            Button(action: {
                self.showShareSheet = true
            }) {
                Text("Share Me").bold()
            }
        }
        .sheet(isPresented: $showShareSheet) {
            ShareSheet(activityItems: ["Hello World"])
        }
    }
}


One inconvenience is that it always appears full-size for me, and I'm not sure if there's something I'm supposed to do to get the half-covered appearance that occurs in most apps.


Hope this helps. I've posted a sample project on Github here: https://github.com/AlanQuatermain/SwiftUIShareSheetDemo

  • Thank you Jim. I hope share my file to Wechat(a sns app in China), but when I select this app, it's not work because "The extension point's NSExtensionContext subclass must implement +_allowedItemPayloadClasses. This must return the set of allowed NSExtensionItem" Do you know any information about how to implement the _allowedItemPayloadClasses. Thank you very much.

Add a Comment

Replies

It works in much the same way as in UIKit, but you'll have to use

.sheet(isPresented:content:)
to make the view appear.


What you need is the

UIActivityViewController
, which means you'll have to create a SwiftUI
UIViewControllerRepresentable
view for that. In this case it's fairly straightforward, since you're not modifying the content or maintaining any state, you're effectively wrapping the initializer. This version works for me:


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
    }
}


In your other view, you want this to appear modally, so you use the

.sheet()
method to do so, along with an
isPresented
state variable, like so:


struct ContentView: View {
    @State private var showShareSheet = false
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Hello World")
            Button(action: {
                self.showShareSheet = true
            }) {
                Text("Share Me").bold()
            }
        }
        .sheet(isPresented: $showShareSheet) {
            ShareSheet(activityItems: ["Hello World"])
        }
    }
}


One inconvenience is that it always appears full-size for me, and I'm not sure if there's something I'm supposed to do to get the half-covered appearance that occurs in most apps.


Hope this helps. I've posted a sample project on Github here: https://github.com/AlanQuatermain/SwiftUIShareSheetDemo

  • Thank you Jim. I hope share my file to Wechat(a sns app in China), but when I select this app, it's not work because "The extension point's NSExtensionContext subclass must implement +_allowedItemPayloadClasses. This must return the set of allowed NSExtensionItem" Do you know any information about how to implement the _allowedItemPayloadClasses. Thank you very much.

Add a Comment

@Jim Dovey Thank you very much for your detailed, complete, understandable and complete answer!

As I now see your code above, implementation in SwiftUI is similar, but as a new user, - unfortunately - I had not fully understood it and had trouble with it as I had not found it anywhere in any documentation or third party example.

I didn't know how to create (override) a SwiftUI UIViewControllerRepresentable view for that.


Personally I don't mind that the (share) sheet view opens across the screen, because now I think that's the way it is in iOS 13.

I am wrong ? In the past, it was different, but I think it now opens up to full screen.

After all, "share view" is not a built-in function common to all apps and ready to use it?

I suppose it should be a standard code supported by Apple and apply to all apps. Because this is how I imagine it should be.


But thanks to you, my question has been completely resolved and I believe you will help many more people!

Really your code works perfectly for me just like I expected, I think, and I should!


Your help is incredible! Your help is incredible! Even more you upload a sample xcode project to github! I have no words!

Thanks so much for your help and detailed information!

In Photos it's full screen, because Photos includes a lot of content (specifically the option to swipe through a list of your images to select them). Inside other apps without the preview section (I tested in Reeder 3, I think Maps—a couple, anyway), it appears half-height by default, and an upward swipe will bring it to fill the screen. The choice seems to be based on the presence of the custom preview/selection section, but a quick glance at the headers and the only open-source app I remembered that used it (NetNewsWire 5) showed nothing special being done. I tried altering the presentation mode, but that seemed to be ignored: it was always 'pageForm' when I checked after presentation, and changing it in updateViewController() didn't seem to make a difference.


It's on my list of bug reports to file now. That list's getting quite long, but happily it seems that ScrollView no longer disables multi-line Text() in its contents, so that's my chief concern out of the way 😉

Actually, thinking about it, maybe the half-screen effect is enabled purely for apps not linked against iOS 13. I know that UIKit does a bunch of these sorts of checks, to keep older behavior for apps built against older SDKs.

@Jim Dovey Thank you :-) - saved my day
@JimCovey - this is great.

Code seems to work great on beta 5, any iPhone device but seems to crash on iPadOS. Wondering if you/anyone else is seeing the same issue?

Is there a way to set the subject line when using the email app?

I followed your sample code but there's one catch. My button (that triggers the share sheet) is on a sheet as well. Not sure if that's causing the below issue:

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.
2022-07-08 15:19:09.297048+0100 Cluedo Notes[68004:12824046] [LayoutConstraints] Unable to simultaneously satisfy constraints.
	Probably at least one of the constraints in the following list is one you don't want. 
	Try this: 
		(1) look at each constraint and try to figure out which you don't expect; 
		(2) find the code that added the unwanted constraint or constraints and fix it. 
(
  "<NSLayoutConstraint:0x28029f200 _UIActivityActionCellTitleLabel:0x1293c41c0.height >= 22  (active)>",
  "<NSLayoutConstraint:0x28029ecb0 UIView:0x1293c32e0.height >= _UIActivityActionCellTitleLabel:0x1293c41c0.height + 30  (active)>",
  "<NSLayoutConstraint:0x2802881e0 'UIView-Encapsulated-Layout-Height' UIView:0x1293c32e0.height == 30  (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x28029f200 _UIActivityActionCellTitleLabel:0x1293c41c0.height >= 22  (active)>

Any ideas on how to resolve this? This isn't causing any trouble to my app but I do not like to see messages on my console. Last but not least, thanks a mil for your sample code.

Using Swift 5 and iOS 15

  • My Button is also an a sheet already. I'm looking for a solution.

Add a Comment