Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ShareLink run code before sharesheet is shown

Tags:

swift

swiftui

I have a ShareLink to share an image

ShareLink(item: image) {
    Image(systemName: "square.and.arrow.up")
}

Now before I can share this image, I have to generate it with some function

@State var image: UIImage
var getImage() {
    // some code that updates @State variable
}

My ShareLink itself is in a context menu. My problem is that it is too expensive to generate this image (call the getImage() function) every time the view refreshes or the context menu is opened. Is there any way I can run code if the user taps on this ShareLink, that is then run and the results are then shown in the Share sheet?

Note: I know this is possible using UIKit as fallback to generate the sharesheet, using a function like this:

func actionSheet() {
    guard let urlShare = URL(string: "https://developer.apple.com/xcode/swiftui/") else { return }
    let activityVC = UIActivityViewController(activityItems: [urlShare], applicationActivities: nil)
    UIApplication.shared.windows.first?.rootViewController?.present(activityVC, animated: true, completion: nil)
}

as demonstrated in this article: https://medium.com/swift-productions/sharesheet-uiactivityviewcontroller-swiftui-47abcd69aba6

I am instead wondering if there is a way to do this with the new ios16 ShareLink.

I haven't managed to get it to work with Sharelink yet. Here is the workaround that I am using:

Button {
    shareCollage()
} label: {
    Label("Share Collage", systemImage: "square.and.arrow.up")
}

which calls

func shareCollage() {
    Task {
        if let screenshot = await takeScreenshot() {
            let activityVC = UIActivityViewController(activityItems: [screenshot], applicationActivities: nil)
            
            if UIDevice.current.userInterfaceIdiom == .pad {
                // otherwise iPad crashes
                let thisViewVC = UIHostingController(rootView: self)
                activityVC.popoverPresentationController?.sourceView = thisViewVC.view
            }
            
            UIApplication.shared.connectedScenes.flatMap {($0 as? UIWindowScene)?.windows ?? []}.first {$0.isKeyWindow}?.rootViewController?.present(activityVC, animated: true, completion: nil)
        }
    }
}

Source: https://github.com/charelF/StickerPaste/blob/main/StickerPaste/ContentView.swift

like image 320
charelf Avatar asked Feb 14 '26 02:02

charelf


2 Answers

This worked for me:

let img = ImageModel(getImage: getImage)
ShareLink(item: img, preview: SharePreview(
    "shareImage",
    image: img
)) {
    Label("shareImage", systemImage: "photo")
}

and

struct ImageModel: Transferable {
    let getImage: () async -> UIImage?

    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .jpeg) { item in
            try await { () -> Data in
                if let img = await item.getImage(), let jpeg = img.jpegData(compressionQuality: 0.6) {
                    return jpeg
                } else {
                    throw ExportImageError.noImageFound
                }
            }()
        }
    }
}

Error:

enum ExportImageError: Error {
    case noImageFound
}

The only issue I'm having here is that getImage() is called twice, once for the preview and once for the export itself. You could just not use the preview, like so:

ShareLink(item: ImageModel(getImage: shareImage), preview: SharePreview(
    "shareImage",
    image: Image("photo")
)) {
    Label("shareImage", systemImage: "text.below.photo.fill")
}

or cache the result somehow. Maybe there's also a better way to do this.

like image 109
fer0n Avatar answered Feb 16 '26 20:02

fer0n


Based on the @charelf's solution added to the original question i created a LazyShareLink to hide this complexity. Ideally Apple will introduce something similar in the near future, which can replace this custom implementation fully.

Usage

LazyShareLink("Share") { ... }

Implementation

struct LazyShareLink: View {

    let text: LocalizedStringKey
    let prepareData: () -> [Any]?

    init(_ text: LocalizedStringKey, prepareData: @escaping () -> [Any]?) {
        self.text = text
        self.prepareData = prepareData
    }

    var body: some View {
        Button(text, action: openShare)
    }

    private func openShare() {
        guard let data = prepareData() else {
            return
        }
        let activityVC = UIActivityViewController(activityItems: data, applicationActivities: nil)

        if UIDevice.current.userInterfaceIdiom == .pad {
            // otherwise iPad crashes
            let thisViewVC = UIHostingController(rootView: self)
            activityVC.popoverPresentationController?.sourceView = thisViewVC.view
        }

        UIApplication.shared.connectedScenes
            .flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
            .first { $0.isKeyWindow }?
            .rootViewController?
            .present(activityVC, animated: true, completion: nil)
    }
}
like image 23
Deitsch Avatar answered Feb 16 '26 20:02

Deitsch