[ACCEPTED]-SwiftUI: Presenting multiple ShareLinks in a menu-menu

Accepted answer
Score: 12

ShareLink inside a menu doesn't currently work. A 11 Menu is technically a new View Controller 10 (UIContextMenuActionsOnlyViewController) presented over the active window. The 9 Share Action Sheet needs a View controller 8 to present from. When a ShareLink inside a Menu is tapped, it 7 dismisses the menu VC, along with the share 6 action sheet. You can verify this by checking 5 the view hierarchy when a Menu is open.

One 4 workaround is to manually create Button/MenuItem/s 3 and show a share action sheet on button 2 tap from the underlying View; which avoids 1 using ShareLink directly.

Workaround:

...
ToolbarItemGroup(placement: SwiftUI.ToolbarItemPlacement.navigationBarTrailing) {
  Menu {
    Button(action: {
      showShareSheet(url: URL("https://www.apple.com")!)
    }) {
      Label("Share1", systemImage: "square.and.arrow.up")
    }
    Button(action: {
      showShareSheet(url: URL(string: "https://www.microsoft.com")!)
    }) {
      Label("Share2", systemImage: "square.and.arrow.up")
    }
  } label: {
    Image(systemName: "square.and.arrow.up")
  }
}
...

// UIActivityViewController can be customised. 
// For examples, see https://www.hackingwithswift.com/articles/118/uiactivityviewcontroller-by-example
func showShareSheet(url: URL) {
  let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil)
  UIApplication.shared.currentUIWindow()?.rootViewController?.present(activityVC, animated: true, completion: nil)
}

// utility extension to easily get the window 
public extension UIApplication {
    func currentUIWindow() -> UIWindow? {
        let connectedScenes = UIApplication.shared.connectedScenes
            .filter { $0.activationState == .foregroundActive }
            .compactMap { $0 as? UIWindowScene }
        
        let window = connectedScenes.first?
            .windows
            .first { $0.isKeyWindow }

        return window
        
    }
}

Score: 2

I've managed to achieve the desired behavior 1 with custom popover bridged from UIKit.

import SwiftUI

struct ShareSheetView: View {
    @State private var isShowingPopover = false
    var body: some View {
        NavigationStack {
            VStack {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
                Text("Hello, world!")
                
                Button("Show Popover") {
                    isShowingPopover = true
                }
                
            }
            .padding()
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button {
                        isShowingPopover.toggle()
                    } label: {
                        Image(systemName: "square.and.arrow.up")
                    }
                    .uiKitPopover(isPresented: $isShowingPopover) {
                        VStack {
                            ShareLink(
                                item: URL(string: "https://www.apple.com")!,
                                preview: SharePreview(
                                    "Test 123",
                                    image: Image(systemName: "plus")
                                )
                            )
                            Divider()
                            ShareLink(
                                item: URL(string: "https://www.microsoft.com")!,
                                preview: SharePreview(
                                    "Tests 321",
                                    image: Image(systemName: "minus")
                                )
                            )
                        }
                        .fixedSize()
                        .padding()
                    }
                }
            }
        }
    }
}

struct ShareSheetView_Previews: PreviewProvider {
    static var previews: some View {
        ShareSheetView()
    }
}

struct PopoverViewModifier<PopoverContent>: ViewModifier where PopoverContent: View {
    @Binding var isPresented: Bool
    let onDismiss: (() -> Void)?
    let content: () -> PopoverContent
    let permittedArrowDirections: UIPopoverArrowDirection = []
    
    func body(content: Content) -> some View {
        content
            .background(
                Popover(
                    isPresented: $isPresented,
                    onDismiss: onDismiss,
                    content: self.content
                )
            )
    }
}

struct Popover<Content: View> : UIViewControllerRepresentable {
    @Binding var isPresented: Bool
    let onDismiss: (() -> Void)?
    @ViewBuilder let content: () -> Content
    let permittedArrowDirections: UIPopoverArrowDirection = []
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self, content: self.content())
    }
    
    func makeUIViewController(context: Context) -> UIViewController {
        return UIViewController()
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        context.coordinator.host.rootView = self.content()
        if self.isPresented, uiViewController.presentedViewController == nil {
            let host = context.coordinator.host
            host.preferredContentSize = host.sizeThatFits(in: CGSize(width: Int.max, height: Int.max))
            host.modalPresentationStyle = UIModalPresentationStyle.popover
            host.popoverPresentationController?.delegate = context.coordinator
            host.popoverPresentationController?.sourceView = uiViewController.view
            host.popoverPresentationController?.sourceRect = uiViewController.view.bounds
            host.popoverPresentationController?.permittedArrowDirections = permittedArrowDirections
            uiViewController.present(host, animated: true, completion: nil)
        }
    }
    
    class Coordinator: NSObject, UIPopoverPresentationControllerDelegate {
        let host: UIHostingController<Content>
        private let parent: Popover
        
        init(parent: Popover, content: Content) {
            self.parent = parent
            self.host = UIHostingController(rootView: content)
        }
        
        func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
            self.parent.isPresented = false
            if let onDismiss = self.parent.onDismiss {
                onDismiss()
            }
        }
        
        func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
            return .none
        }
    }
}

extension View {
    func uiKitPopover<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, content: @escaping () -> Content) -> some View where Content: View {
        self.modifier(PopoverViewModifier(isPresented: isPresented, onDismiss: onDismiss, content: content))
    }
}
 

More Related questions