31

The new share sheet on iOS13 shows a preview/thumbnail of the item being shared on its top left corner.

When sharing an UIImage using an UIActivityViewController I would expect a preview/thumbnail of the image being shared to be displayed there (like e.g. when sharing an image attached to the built in Mail app), but instead the share sheet is showing my app's icon.

What code/settings are required to show a thumbnail of the image being exported in the share sheet?

I have set up the UIActivityViewController as follows:

let image = UIImage(named: "test")!
let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil)                          
activityVC.popoverPresentationController?.sourceView = self.view                            
self.present(activityVC, animated: true, completion: nil)
mmklug
  • 2,252
  • 2
  • 16
  • 31
  • 1
    Did you figure anything out here? I'm running into the same problem, and it's very odd: if I use a `URL` of a .`jpg` image in the Documents directory instead of a `UIImage`, it shows information about that image (name and size), but still no thumbnail. However, if I use a `URL` of an image inside the app bundle, it shows a thumbnail of that image! Very strange. – gilby Sep 26 '19 at 16:26
  • Unfortunately not, @gilby – mmklug Sep 28 '19 at 08:15
  • Did anyone manage to do this for the website? I am facing a similar situation where I am trying to display the logo when using native share or ( navigator.share ). There is no clear documentation from where the image is picked up. Anyone with suggestion please post! – Sijan Shrestha Jul 22 '20 at 09:44
  • @SijanShrestha Have you found the solution for this problem? I am trying to get the logo with `navigator.share` as well and I can't seem to get it. – mayk93 Mar 04 '21 at 12:32
  • @mayk93, yes I have the solution. If you want the icon, you should only pass the URL. Do not pass the text. Hope it helps. If you pass the URL, the bots from apple will crawl the site and get the icon. – Sijan Shrestha Mar 05 '21 at 14:46

4 Answers4

41

The simplest code I've implemented to share a UIImage with better user experience:

  1. Import the LinkPresentation framework:
#import <LinkPresentation/LPLinkMetadata.h>  // for Obj-C

import LinkPresentation  // for Swift, below
  1. Present the UIActivityViewController in the UIViewController, with [image, self]:
let image = UIImage(named: "YourImage")!
let share = UIActivityViewController(activityItems: [image, self], applicationActivities: nil)
present(share, animated: true, completion: nil)
  1. Make the UIViewController conform to UIActivityItemSource:
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
    return ""
}

func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
    return nil
}

func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
    let image = UIImage(named: "YourImage")!
    let imageProvider = NSItemProvider(object: image)
    let metadata = LPLinkMetadata()
    metadata.imageProvider = imageProvider
    return metadata
}

Because UIImage has already conformed to NSItemProviderWriting, just serve it for NSItemProvider.

Since it's sharing a UIImage, any URL shouldn't be expected. Otherwise user may get URL sharing, rather than image sharing experience.

To accelerate the share sheet preview, feed LPLinkMetadata object with existing resources. No need to fetch it online again. Check the WWDC19 Tech Talks video What's New in Sharing for more details.

denkeni
  • 969
  • 1
  • 10
  • 22
  • Nice thanks for that answer! One thing I am still missing, can I set the filename via the metadata as well? I use this one to share a pdf file. – laka Mar 09 '20 at 17:07
  • @laka I believe you can do so by properly using `UIActivityViewController` with `URL` – denkeni Mar 10 '20 at 07:38
  • I see a padding around my image. Used every possible size of image and checked. Any idea what could be the problem? – Sayalee Pote Feb 19 '21 at 11:08
21

Update:

As of iOS 13.2.2 the standard way seems to be working as expected (when passing image URL(s) to UIActivityViewController), see @tatsuki.dev 's answer (now set as accepted answer):

On iOS 13.0 that was still not the case:

Original Answer:

I finally was able to figure out a solution to this issue.

To display the preview/thumbnail of the image being shared in the share sheet on iOS 13 it is necessary to adopt the UIActivityItemSource protocol, including its new (iOS13) activityViewControllerLinkMetadata method.

Starting from the code posted in the question, these would be the required steps:

  1. Import the LinkPresentation framework:

    import LinkPresentation
    
  2. create an optional URL property in your UIViewController subclass

    var urlOfImageToShare: URL?
    
  3. Implement the UIActivityItemSource delegate methods as follows:

    extension YourViewController: UIActivityItemSource {
    
        func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
            return UIImage() // an empty UIImage is sufficient to ensure share sheet shows right actions
        }
    
        func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
            return urlOfImageToShare
        }
    
        func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
            let metadata = LPLinkMetadata()
    
            metadata.title = "Description of image to share" // Preview Title
            metadata.originalURL = urlOfImageToShare // determines the Preview Subtitle
            metadata.url = urlOfImageToShare
            metadata.imageProvider = NSItemProvider.init(contentsOf: urlOfImageToShare)
            metadata.iconProvider = NSItemProvider.init(contentsOf: urlOfImageToShare)
    
            return metadata
        }
    }
    
  4. In the part of the code presenting the share sheet, the declaration of activityVC needs to be slightly changed. The activityItems parameter should be [self] instead of [image] as in the code posted in the question above:

    //let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil)   
    let activityVC = UIActivityViewController(activityItems: [self] , applicationActivities: nil)
    

    This is necessary to have the UIActivityItemSource delegate methods declared above being called when presenting the share sheet.

    Also, before presenting activityVC we need to set the value of urlOfImageToShare (which is needed by the UIActivityItemSource delegate methods):

    urlOfImageToShare = yourImageURL // <<< update this to work with your code 
    

The above steps should suffice if your app is not sharing very small or transparent images. The result looks like this:

In my tests while researching about this topic however, I had issues when providing images to metadata.iconProvider which were small (threshold seems to be 40 points) or non-opaque (transparent).

It seems like iOS uses metadata.imageProvider to generate the preview image if metadata.iconProvider delivers an image smaller than 40 points.

Also, on an actual device (iPhone Xs Max running iOS 13.1.2), the image provided by metadata.iconProvider would be displayed in reduced size on the share sheet in case it was not opaque:

On Simulator (iOS 13.0) this was not the case.

To work around these limitations, I followed these additional steps to ensure the preview image is always opaque and at least 40 points in size:

  1. In the implementation of activityViewControllerLinkMetadata above, change the assignment of metadata.iconProvider as follows:

    //metadata.iconProvider = NSItemProvider.init(contentsOf: urlOfImageToShare)
    metadata.iconProvider = NSItemProvider.init(contentsOf: urlInTemporaryDirForSharePreviewImage(urlOfImageToShare))
    

    Method urlInTemporaryDirForSharePreviewImage returns an URL to an opaque and if necessary enlarged copy of the image being shared created in the temporary directory:

    func urlInTemporaryDirForSharePreviewImage(_ url: URL?) -> URL? {
        if let imageURL = url,
           let data = try? Data(contentsOf: imageURL),
           let image = UIImage(data: data) {
    
            let applicationTemporaryDirectoryURL = FileManager.default.temporaryDirectory
            let sharePreviewURL = applicationTemporaryDirectoryURL.appendingPathComponent("sharePreview.png")
    
            let resizedOpaqueImage = image.adjustedForShareSheetPreviewIconProvider()
    
            if let data = resizedOpaqueImage.pngData() {
                do {
                    try data.write(to: sharePreviewURL)
                    return sharePreviewURL
                } catch {
                    print ("Error: \(error.localizedDescription)")
                }
            }
        }
        return nil
    }
    

    The actual generation of the new image is done using the following extension:

    extension UIImage {
        func adjustedForShareSheetPreviewIconProvider() -> UIImage {
            let replaceTransparencyWithColor = UIColor.black // change as required
            let minimumSize: CGFloat = 40.0  // points
    
            let format = UIGraphicsImageRendererFormat.init()
            format.opaque = true
            format.scale = self.scale
    
            let imageWidth = self.size.width
            let imageHeight = self.size.height
            let imageSmallestDimension = max(imageWidth, imageHeight)
            let deviceScale = UIScreen.main.scale
            let resizeFactor = minimumSize * deviceScale  / (imageSmallestDimension * self.scale)
    
            let size = resizeFactor > 1.0
                ? CGSize(width: imageWidth * resizeFactor, height: imageHeight * resizeFactor)
                : self.size
    
            return UIGraphicsImageRenderer(size: size, format: format).image { context in
                let size = context.format.bounds.size
                replaceTransparencyWithColor.setFill()
                context.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))
                self.draw(in: CGRect(origin: .zero, size: size))
            }
        }
    }
    
mmklug
  • 2,252
  • 2
  • 16
  • 31
15

Just pass the image urls to UIActivityViewController not the UIImage objects. For example:

let imageURLs: [URL] = self.prepareImageURLs()
let activityViewController = UIActivityViewController(activityItems: imageURLs, applicationActivities: nil)
self.present(activityViewController, animated: true, completion: nil)

You can see that the image name and the image properties are shown in the top of the UIActivityViewController. Hope it helps!

Jake A.
  • 540
  • 5
  • 12
  • Thanks for the update, tatsuki.dev. As mentioned in @gilby 's comment above, this was not working properly under iOS 13.0 and seems to have been fixed since then. I have set yours to be the accepted answer. – mmklug Nov 24 '19 at 12:35
  • 1
    how do you get the url from UIimage? – Nic Wanavit Apr 20 '20 at 06:06
  • 1
    @NicWanavit As far as I understand, you can't. UIImage represents the image data itself and doesn't have its location. Either when you have the image saved in the device or when you load it from the web it comes with a uri. If you want to use the images in the assets catalog, please refer to https://forums.developer.apple.com/thread/93123 – Jake A. Apr 21 '20 at 05:58
  • Hey @tatsuki.dev, is prepareImageURLs() a method that would return the URL for the image in an array? How exactly does that work? I kept trying to use Bundle.main.url(forResource:, withExtension) to get the path of the image I'm sharing, but it's always nil. – CristianMoisei Oct 11 '20 at 22:20
3

This code is only available for iOS 13 as a minimum target. I added a code example to use a share button in a SwiftUI view in case other people need it. This code also work for iPad.

You can use this class LinkMetadataManager and add the image of your choice. The very important part, is that you must have your image in your project directory, not in a Assets.xcassets folder. Otherwise, it won't work.

When everything will be setup, you will use the button this way in your SwiftUI view.

struct ContentView: View {
    
  var body: some View {
    VStack {
      ShareButton()
    }
  }
}

This is the class that will be sharing your application with the Apple Store link. You can share whatever you want from that. You can see how the image is added using LPLinkMetadata as it is the part that interests you.

import LinkPresentation

//  MARK: LinkMetadataManager
/// Transform url to metadata to populate to user.
final class LinkMetadataManager: NSObject, UIActivityItemSource {

  var linkMetadata: LPLinkMetadata

  let appTitle = "Your application name"
  let appleStoreProductURL = "https://apps.apple.com/us/app/num8r/id1497392799"  // The url of your app in Apple Store
  let iconImage = "appIcon"  // The name of the image file in your directory
  let png = "png"  // The extension of the image

  init(linkMetadata: LPLinkMetadata = LPLinkMetadata()) {
    self.linkMetadata = linkMetadata
  }
}

// MARK: - Setup
extension LinkMetadataManager {
  /// Creating metadata to population in the share sheet.
  func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {

    guard let url = URL(string: appleStoreProductUR) else { return linkMetadata }

    linkMetadata.originalURL = url
    linkMetadata.url = linkMetadata.originalURL
    linkMetadata.title = appTitle
    linkMetadata.iconProvider = NSItemProvider(
      contentsOf: Bundle.main.url(forResource: iconImage, withExtension: png))

    return linkMetadata
  }

  /// Showing empty string returns a share sheet with the minimum requirement.
  func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
    return String()
  }

  /// Sharing url of the application.
  func activityViewController(_ activityViewController: UIActivityViewController,
                              itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
    return linkMetadata.url
  }
}

Use this extension of View to trigger the share sheet on a SwiftUI view.

import SwiftUI

//  MARK: View+ShareSheet
extension View {

  /// Populate Apple share sheet to enable user to share Apple Store link.
  func showAppShareSheet() {
    guard let source = UIApplication.shared.windows.first?.rootViewController else {
      return
    }

    let activityItemMetadata = LinkMetadataManager()

    let activityVC = UIActivityViewController(
      activityItems: [activityItemMetadata],
      applicationActivities: nil)

    if let popoverController = activityVC.popoverPresentationController {
      popoverController.sourceView = source.view
      popoverController.permittedArrowDirections = []
      popoverController.sourceRect = CGRect(
        x: source.view.bounds.midX,
        y: source.view.bounds.midY,
        width: .zero, 
        height: .zero)
    }
    source.present(activityVC, animated: true)
  }
}

Then, create a ShareButton as a component to use it in any of your SwiftUI view. This is what is used in the ContentView.

import SwiftUI

//  MARK: ShareButton
/// Share button to send app store link using the Apple 
/// classic share screen for iPhone and iPad.
struct ShareButton: View {

  @Environment(\.horizontalSizeClass) private var horizontalSizeClass

  var body: some View {
    ZStack {
      Button(action: { showAppShareSheet() }) {
        Image(systemName: "square.and.arrow.up")
          .font(horizontalSizeClass == .compact ? .title2 : .title)
          .foregroundColor(.accentColor)
      }
      .padding()
    }
  }
}
Roland Lariotte
  • 2,606
  • 1
  • 16
  • 40