iOS13分享sheet:分享UIImage时如何设置预览缩略图

iOS13 share sheet: how to set preview thumbnail when sharing UIImage

iOS13 上的新分享 sheet 在其左上角显示 preview/thumbnail 正在分享的项目。

当使用 UIActivityViewController 共享 UIImage 时,我希望共享的图像 preview/thumbnail 显示在那里(例如,当共享附加到内置邮件应用程序的图像时),而不是共享sheet 正在显示我的应用程序图标。

要在共享 sheet 中显示正在导出的图像的缩略图,需要什么 code/settings?

我已经设置了 UIActivityViewController 如下:

let image = UIImage(named: "test")!
let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil)                          
activityVC.popoverPresentationController?.sourceView = self.view                            
self.present(activityVC, animated: true, completion: nil)

更新:

从 iOS 13.2.2 开始,标准方式似乎按预期工作(将图像 URL(s) 传递给 UIActivityViewController 时),请参阅@tatsuki.dev 的回答(现在设置为已接受的答案):

在 iOS 13.0 上仍然不是这样:

原答案:

我终于找到了解决这个问题的方法。

要在 iOS 13 上的共享 sheet 中显示共享图像的 preview/thumbnail,必须采用 UIActivityItemSource 协议,包括其新的 (iOS13) activityViewControllerLinkMetadata 方法。

从问题中发布的代码开始,这些将是必需的步骤:

  1. 导入 LinkPresentation 框架:

    import LinkPresentation
    
  2. 在您的 UIViewController 子类中创建一个可选的 URL 属性

    var urlOfImageToShare: URL?
    
  3. 按如下方式实现 UIActivityItemSource 委托方法:

    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. 在呈现分享的代码部分sheet,activityVC的声明需要稍作改动。 activityItems 参数应该是 [self] 而不是 [image] ,如上面问题中发布的代码:

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

    在呈现共享时调用上面声明的 UIActivityItemSource 委托方法是必需的 sheet。

    此外,在呈现 activityVC 之前,我们需要设置 urlOfImageToShare 的值(UIActivityItemSource 委托方法需要):

    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. 在上面activityViewControllerLinkMetadata的实现中,将metadata.iconProvider的赋值改成如下:

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

    方法 urlInTemporaryDirForSharePreviewImage returns 一个 URL 一个不透明的,如果需要的话,在临时目录中创建共享图像的放大副本:

    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
    }
    

    新图像的实际生成是使用以下扩展完成的:

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

只需将图像 URL 传递给 UIActivityViewController 而不是 UIImage 对象。 例如:

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

您可以看到图像名称和图像属性显示在 UIActivityViewController 的顶部。希望对您有所帮助!

我实现的最简单的代码分享了一个 UIImage 更好的用户体验:

  1. 导入 LinkPresentation 框架:
#import <LinkPresentation/LPLinkMetadata.h>  // for Obj-C

import LinkPresentation  // for Swift, below
  1. UIViewController 中显示 UIActivityViewController,[image, self]:
let image = UIImage(named: "YourImage")!
let share = UIActivityViewController(activityItems: [image, self], applicationActivities: nil)
present(share, animated: true, completion: nil)
  1. 使UIViewController符合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
}

因为UIImage已经符合了NSItemProviderWriting,就给NSItemProvider服务吧。

由于它共享一个 UIImage,因此不应期望任何 URL。否则用户可能会获得 URL 分享,而不是图片分享体验。

要加速共享 sheet 预览,请使用现有资源提供 LPLinkMetadata 对象。无需再次在线获取它。查看 WWDC19 技术讲座视频 What's New in Sharing 了解更多详情。

此代码仅适用于 iOS 13 作为最低目标。我添加了一个代码示例以在 SwiftUI 视图中使用共享按钮,以防其他人需要它。此代码也适用于 iPad.

您可以使用此 class LinkMetadataManager 并添加您选择的图像。非常重要的部分是,您 必须将图像放在项目目录 中,而不是 Assets.xcassets 文件夹中。不然不行。

设置好所有内容后,您将在 SwiftUI 视图中以这种方式使用按钮。

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

这是将与 Apple Store link 共享您的应用程序的 class。你可以从中分享你想要的任何东西。您可以使用 LPLinkMetadata 查看图像是如何添加的,因为它是您感兴趣的部分。

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

使用 View 的扩展在 SwiftUI 视图上触发共享 sheet。

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

然后,创建一个 ShareButton 作为组件,以便在您的任何 SwiftUI 视图中使用它。这是在 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()
    }
  }
}