LoginSignup
57

More than 3 years have passed since last update.

【Swift, iOS】iOS13の新機能 LinkPresentation.frameworkの使い方を調べる

Last updated at Posted at 2020-03-21

iOS13でLinkPresentationという新機能が追加されました。
これによってURLリンク先の情報を
より表現豊かに表示することができるようになるようです。

今回はその新機能について
AppleのWWDC2019の動画と検証結果などから見ていきたいと思います。

Embedding and Sharing Visually Rich Links
https://developer.apple.com/videos/play/wwdc2019/262


あまり情報がなく
検証した結果から記載した部分もありますので
間違っている部分やもっと良い方法ご存知の方いらっしゃいましたらぜひ教えてください🙇🏻‍♂️

※ 
下記の記事にも記載があるように
UITableViewの中で利用すると色々問題があるようなので現在検証中です🙇🏻‍♂️
https://www.raywenderlich.com/7565482-visually-rich-links-tutorial-for-ios-image-thumbnails

LinkPresentation.frameworkとは?

iOS13で新しく追加された
リンクのプレビューを画像や埋め込み動画、音楽再生と合わせて
リッチに一貫した方法で表示できるようにした
フレームワークです。

iOS10とmacOS Sierraから
Appleのメッセージアプリなどでは先行して
このフレームワークに含まれている機能を利用していたようです。

スクリーンショット 2020-03-21 8.50.12.png

主なクラス

非常にシンプルで主に登場するクラスは3です。

  • LPMetadataProvider
  • LPLinkMetadata
  • LPLinkView

LPMetadataProvider

URLのメタ情報を取得します。
https://developer.apple.com/documentation/linkpresentation/lpmetadataprovider

※ メタ情報とは?

HTMLタグに含まれるタイトルやアイコン、画像、動画などの情報を読み取ります。

特に
OpenGraphというプロトコルを使用した
<meta og:XXX>の情報を優先して読み取ります。

例えば下記のようなものです。


<html prefix="og: http://ogp.me/ns#">
<head>
<title>The Rock (1996)</title>
<meta property="og:title" content="The Rock" />
<meta property="og:type" content="video.movie" />
<meta property="og:url" content="http://www.imdb.com/title/tt0117500/" />
<meta property="og:image" content="http://ia.media-imdb.com/images/rock.jpg" />
...
</head>
...
</html>

詳細は下記をご参照ください。
OpenGraph
https://ogp.me/

LPLinkMetadata

URLのメタ情報を保持するクラスです。
https://developer.apple.com/documentation/linkpresentation/lplinkmetadata

LPLinkView

URLのメタ情報をリッチに表示するUIViewのサブクラスです。
https://developer.apple.com/documentation/linkpresentation/lplinkview

使い方

すごいシンプルです。

  1. LPMetadataProviderstartFetchingMetadataでURLのリンク先からLPLinkMetadataを取得する
  2. LPLinkMetadataLPLinkViewに設定する

SwiftUIでの実装

UIViewRepresentableに適合したクラスの生成

LPLinkViewに対応する
UIViewRepresentableに適合したクラスを生成します。


import SwiftUI
import LinkPresentation

struct LinkPresentationView: UIViewRepresentable {
    typealias UIViewType = LPLinkView

    func makeUIView(context: UIViewRepresentableContext<LinkPresentationView>) -> UIViewType {
    }

    func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<LinkPresentationView>) {
    }
}

メタ情報の取得

LPMetadataProviderからLPLinkMetadataを取得します。
取得するURLが必要なので初期化時に引数で受け取るように変数を宣言します。


struct LinkPresentationView: UIViewRepresentable {
    let url: URL

    private func fetchMetadata(for url: URL, completion: @escaping (Result<LPLinkMetadata, Error>) -> Void) {
        let provider = LPMetadataProvider()

        provider.startFetchingMetadata(for: url) { metadata, error in
            if let error = error {
                completion(.failure(error))
            } else if let metadata = metadata {
                completion(.success(metadata))
            } else {
                completion(.failure(LPError(.unknown)))
            }
        }
    }
}

エラーが発生した場合はLPErrorが返ってきます。
https://developer.apple.com/documentation/linkpresentation/lperror

ネットワークに繋がっていなかったり
接続が遅すぎてタイムアウトになったり
リクエストがキャンセルされた場合に生じます。

また
LPMetadataProviderには
タイムアウトの時間を設定することもできます。
デフォルトは30秒です。


provider.timeout = 5

LPLinkViewの生成

次に
makeUIViewの中でLPLinkViewを生成します。


struct LinkPresentationView: UIViewRepresentable {
    var url: URL

    func makeUIView(context: UIViewRepresentableContext<LinkPresentationView>) -> UIViewType {
        let view = LPLinkView(url: url) // ※
        self.fetchMetadata(for: url) { result in
            switch result {
            case .success(let metadata):
                self.update(view: view, with: metadata)
            case .failure:
                let metadata = LPLinkMetadata()
                metadata.title = "Error"
                let url = URL(fileURLWithPath: Bundle.main.path(forResource: "error", ofType: "png")!)
                metadata.iconProvider = NSItemProvider(contentsOf: url)
                self.update(view: view, with: metadata)
            }
        }
        return view
    }

    private func update(view: UIViewType, with metadata: LPLinkMetadata) {
        DispatchQueue.main.async {
            view.metadata = metadata
            view.sizeToFit()
        }
    }
}


ここは疑問が残っているのですが
ここでurlを引数にLPLinkViewを初期化しないと
画面に何も表示されませんでした。

おそらく内部で取得したLPLinkMetadataのURLと
LPLinkViewのURLを比較しているんじゃないかと思っているのですが
もし何かご存知の方いらっしゃいましたら
教えていただけると嬉しいです🙇🏻‍♂️

updateメソッドの中で


private func update(view: UIViewType, with metadata: LPLinkMetadata) {
      ....
      view.sizeToFit()
}

としていますが
これは
LPLinkView自体もintrinsic sizeを持っているものの
sizeToFitを使用することで
現在のレイアウトに最適なサイズで表示されるようにするためです。

エラーが起きた時は
その場でLPLinkMetadataを生成して表示することもできます。


case .failure:
    let metadata = LPLinkMetadata()
    metadata.title = "Error"
    let url = URL(fileURLWithPath: Bundle.main.path(forResource: "error", ofType: "png")!)
    metadata.iconProvider = NSItemProvider(contentsOf: url)
    self.update(view: view, with: metadata)

実際に表示してみます。


struct ContentView: View {
    var urls: [URL] = [
        URL(string: "https://www.apple.com/mac")!,
        URL(string: "https://www.apple.com/ipad")!,
        URL(string: "https://youtu.be/V85CQzsyvj4")!,
        URL(string: "https://twitter.com/yuukikikuchi/status/1240946299467259905")!,
    ]
    var body: some View {
        List(self.urls, id: \.self) { url in
            LinkPresentationView(url: url)
        }.onAppear {
            UITableView.appearance().separatorStyle = .none
        }
    }
}

下記の様に表示されます。

Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-21 at 11.22.26.png

適切なサイズで表示がされていません。

これはLPLinkViewの初期化時のサイズのままになっており
取得した画像のサイズなどが反映されていないためです。

そこでLPLinkMetadataの取得が完了した時点で
サイズの再計算を行うように親のViewに伝達するようにします。


struct LinkPresentationView: UIViewRepresentable {
    ...

    @Binding var redraw: Bool

    private func update(view: UIViewType, with metadata: LPLinkMetadata) {
        ...

        redraw.toggle()
    }
}

struct ContentView: View {
    ....

    @State var redraw = false

    var body: some View {
        List(self.urls, id: \.self) { url in
            LinkPresentationView(url: url, redraw: self.$redraw)
        }.onAppear {
            UITableView.appearance().separatorStyle = .none
        }
    }
}

こうするとこんな形で表示されます。
またYoutubeの動画はクリックすると再生することができたり
Twitterの表示も自動で設定してくれます。

(表示時のアニメーションをどうにかしたいですが。。。)

メタ情報をキャッシュする

URLから毎回メタ情報を取得してくるのは
ユーザにとっては通信料がかかってしまいますし
アプリとして同じリンク先から毎回同じ情報を取得するためには
パフォーマンスコストがかかるため
キャッシュをするべきだと
Appleの動画でも言っていました。

そこでローカルにキャッシュするためのクラスを用意します。

LPLinkMetadataはNSSecureCodingに適合しており
信頼性の高い形でシリアライズ可能になっています。


今回は実装を簡単にするために
シングルトンのUserDefaults.standardやsharedを使用しています。


final class MetaCache {
    static let shared = MetaCache()
    private init(){}

    private let storage = UserDefaults.standard

    private let key = "Metadata"

    func store(_ metadata: LPLinkMetadata) {
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: metadata, requiringSecureCoding: true)
            var metadatas: [String: Data] = storage.dictionary(forKey: key) as? [String: Data] ?? [:]
            metadatas[metadata.originalURL!.absoluteString] = data
            storage.set(metadatas, forKey: key)
        }
        catch {
            print("Failed storing metadata with error \(error as NSError)")
        }
    }

    func metadata(for url: URL) -> LPLinkMetadata? {
        guard let metadatas = storage.dictionary(forKey: key) as? [String: Data] else {
            return nil
        }

        guard let data = metadatas[url.absoluteString] else {
            return nil
        }

        do {
            return try NSKeyedUnarchiver.unarchivedObject(ofClass: LPLinkMetadata.self, from: data)
        } catch {
            print("Failed to unarchive metadata with error \(error)")
            return nil
        }
    }
}

最後にこれを使用します。


    func makeUIView(context: UIViewRepresentableContext<LinkPresentationView>) -> UIViewType {
        let view = LPLinkView(url: url)
        if let cachedData = MetaCache.shared.metadata(for: url) {
            update(view: view, with: cachedData)
        } else {
            self.fetchMetadata(for: url) { result in
                switch result {
                case .success(let metadata):
                    MetaCache.shared.store(metadata)
                    ...
                case .failure:
                    ...
                }
            }
        }
        return view
    }

注意点

ドキュメントにも記載がありますが
startFetchingMetadata(for:completionHandler:)のcompletionHandlerは
バックグラウンドで実行されるため
UI関連の処理を行う場合はメインキューで実行するようにします。

The completion handler executes on a background queue. 
Dispatch any necessary UI updates back to the main queue. 

また、LPMetadataProviderは一回のリクエストにしか使用できないため
例えば


struct LinkPresentationView: UIViewRepresentable {
    let provider = LPMetadataProvider()

    func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<LinkPresentationView>) {
        provider.startFetchingMetadata(for: url) { metadata, error in
        }
    }
}

などと実装してみると
ListやForEachを使用した時に

'Trying to start fetching on an LPMetadataProvider that has already started. 
LPMetadataProvider is a one-shot object.'

といったエラーでクラッシュします。

最終的なコード

import SwiftUI
import LinkPresentation

struct LinkPresentationView: UIViewRepresentable {
    typealias UIViewType = LPLinkView

    let url: URL
    @Binding var redraw: Bool

    func makeUIView(context: UIViewRepresentableContext<LinkPresentationView>) -> UIViewType {
        let view = LPLinkView(url: url)

        // 画像を取得するまでの表示されないように設定しています
        view.isHidden = true
        if let cachedData = MetaCache.shared.metadata(for: url) {
            update(view: view, with: cachedData)
        } else {
            self.fetchMetadata(for: url) { result in
                switch result {
                case .success(let metadata):
                    MetaCache.shared.store(metadata)
                    self.update(view: view, with: metadata)
                case .failure:
                    let metadata = LPLinkMetadata()
                    metadata.title = "Error"
                    let url = URL(fileURLWithPath: Bundle.main.path(forResource: "error", ofType: "png")!)
                    metadata.iconProvider = NSItemProvider(contentsOf: url)
                    self.update(view: view, with: metadata)
                }
            }
        }
        return view
    }

    func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<LinkPresentationView>) {
    }

    private func fetchMetadata(for url: URL, completion: @escaping (Result<LPLinkMetadata, Error>) -> Void) {
        let provider = LPMetadataProvider()

        provider.startFetchingMetadata(for: url) { metadata, error in
            if let error = error {
                completion(.failure(error))
            } else if let metadata = metadata {
                completion(.success(metadata))
            } else {
                completion(.failure(LPError(.unknown)))
            }
        }
    }

    private func update(view: UIViewType, with metadata: LPLinkMetadata) {
        DispatchQueue.main.async {
            view.metadata = metadata
            view.sizeToFit()
            self.redraw.toggle()
            view.isHidden = false
        }
    }
}

struct ContentView: View {
    var urls: [URL] = [
        URL(string: "https://www.apple.com/mac")!,
        URL(string: "https://www.apple.com/ipad")!,
        URL(string: "https://youtu.be/V85CQzsyvj4")!,
        URL(string: "https://twitter.com/yuukikikuchi/status/1240946299467259905")!,
    ]

    @State var redraw = false

    var body: some View {
        List(self.urls, id: \.self) { url in
            LinkPresentationView(url: url, redraw: self.$redraw)
        }.onAppear {
            UITableView.appearance().separatorStyle = .none
        }
    }
}

struct LinkPresentationView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

UIViewでの実装(検証中)

UITableViewでも実装をしてみました。

LinkPresentationが
セルの再利用に対応していないというような記載は
いくつかのサイトで見ていました。

実際に色々とやってみて
何とか表示できることはできますが
色々とおかしな部分があるので検証中です。

セルの実装部分を記載します。

final class Cell: UITableViewCell {
    static let identifier = "Cell"

    private let activityIndicator = UIActivityIndicatorView()
    private var linkView = LPLinkView()

    // 再利用の際に繰り返しmetaDataを取得しないように設定
    private var isFetching = false

    // ViewControllerにセルサイズを再計算をさせるためにLPLinkMetadata取得したことを伝える
    var onUpdate: (() -> Void)?

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setup()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setup() {
        contentView.addSubview(activityIndicator)
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            activityIndicator.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            activityIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
        ])
    }
    // 毎回subViewをクリアする必要がある
    override func prepareForReuse() {
        super.prepareForReuse()
        linkView.removeFromSuperview()
    }

    func configure(with url: URL) {
        activityIndicator.startAnimating()
        setLPLinkView(for: url)
    }

    private func setLPLinkView(for url: URL) {
        if let cachedData = MetaCache.shared.metadata(for: url) {
            linkView = LPLinkView(metadata: cachedData)

            addSubView(linkView: linkView)
            linkView.sizeToFit()

            onUpdate?()

            activityIndicator.stopAnimating()
            return
        }

        fetchMetadata(for: url) { [weak self] result in
            guard let self = self else {
                return
            }
            self.linkView = LPLinkView(url: url)
            switch result {
            case .success(let metadata):
                MetaCache.shared.store(metadata)
                self.update(with: metadata)
            case .failure(let error):
                let errorMessage = error is LPError ? (error as! LPError).errorMessage : "error"
                let metadata = LPLinkMetadata()
                metadata.title = errorMessage
                let url = URL(fileURLWithPath: Bundle.main.path(forResource: "error", ofType: "png")!)
                metadata.iconProvider = NSItemProvider(contentsOf: url)
                self.update(with: metadata)
            }
        }
    }

    private func fetchMetadata(for url: URL, completion: @escaping (Result<LPLinkMetadata, Error>) -> Void) {
        if isFetching {
            return
        }
        isFetching = true

        let provider = LPMetadataProvider()
        provider.startFetchingMetadata(for: url) { [weak self] metadata, error in
            guard let self = self else {
                return
            }

            if let error = error {
                completion(.failure(error))
            } else if let metadata = metadata {
                completion(.success(metadata))
            } else {
                completion(.failure(LPError(.unknown)))
            }
            self.isFetching = false
        }
    }

    private func update(with metadata: LPLinkMetadata) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else {
                return
            }

            self.linkView.metadata = metadata
            self.addSubView(linkView: self.linkView)
            self.linkView.sizeToFit()

            self.onUpdate?()

            self.activityIndicator.stopAnimating()
        }
    }

    private func addSubView(linkView: LPLinkView) {
        contentView.addSubview(linkView)
        linkView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            linkView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
            linkView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
            linkView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
            linkView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12),
        ])
    }
}

下記のように動きます。

データが少ないとうまく行っているように見えますが
データ量が多いと

最終的なコード

import UIKit
import LinkPresentation

class ViewController: UIViewController {
    private var urls: [URL] = [
        URL(string: "https://www.apple.com/mac")!,
        URL(string: "https://www.apple.com/ipad")!,
        URL(string: "https://youtu.be/V85CQzsyvj4")!,
        URL(string: "https://twitter.com/yuukikikuchi/status/1240946299467259905")!,
    ]

    lazy var tableView: UITableView = UITableView()

    private var loadedIndexPaths: Set<IndexPath> = []

    override func viewDidLoad() {
        super.viewDidLoad()
        configureTableView()
    }

    private func configureTableView() {
        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])

        tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier)
        tableView.dataSource = self
        tableView.separatorStyle = .none
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return urls.count
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let url = urls[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier, for: indexPath) as! Cell
        cell.configure(with: url)

        cell.onUpdate = { [weak self] in
            guard let self = self else {
                return
            }

            // LPLinkMetadataが取得されたらreloadする
            if !self.loadedIndexPaths.contains(indexPath) {
                self.loadedIndexPaths.insert(indexPath)
                self.tableView.reloadRows(at: [indexPath], with: .none)
            }
        }
        return cell
    }
}

final class Cell: UITableViewCell {
    static let identifier = "Cell"

    private let activityIndicator = UIActivityIndicatorView()
    private var linkView = LPLinkView()

    private var isFetching = false

    var onUpdate: (() -> Void)?

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setup()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setup() {
        contentView.addSubview(activityIndicator)
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            activityIndicator.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            activityIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
        ])
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        linkView.removeFromSuperview()
    }

    func configure(with url: URL) {
        activityIndicator.startAnimating()
        setLPLinkView(for: url)
    }

    private func setLPLinkView(for url: URL) {
        if let cachedData = MetaCache.shared.metadata(for: url) {
            linkView = LPLinkView(metadata: cachedData)

            addSubView(linkView: linkView)
            linkView.sizeToFit()

            onUpdate?()

            activityIndicator.stopAnimating()
            return
        }

        fetchMetadata(for: url) { [weak self] result in
            guard let self = self else {
                return
            }
            self.linkView = LPLinkView(url: url)
            switch result {
            case .success(let metadata):
                MetaCache.shared.store(metadata)
                self.update(with: metadata)
            case .failure(let error):
                let errorMessage = error is LPError ? (error as! LPError).errorMessage : "error"
                let metadata = LPLinkMetadata()
                metadata.title = errorMessage
                let url = URL(fileURLWithPath: Bundle.main.path(forResource: "error", ofType: "png")!)
                metadata.iconProvider = NSItemProvider(contentsOf: url)
                self.update(with: metadata)
            }
        }
    }

    private func fetchMetadata(for url: URL, completion: @escaping (Result<LPLinkMetadata, Error>) -> Void) {
        if isFetching {
            return
        }
        isFetching = true

        let provider = LPMetadataProvider()
        provider.startFetchingMetadata(for: url) { [weak self] metadata, error in
            guard let self = self else {
                return
            }

            if let error = error {
                completion(.failure(error))
            } else if let metadata = metadata {
                completion(.success(metadata))
            } else {
                completion(.failure(LPError(.unknown)))
            }
            self.isFetching = false
        }
    }

    private func update(with metadata: LPLinkMetadata) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else {
                return
            }

            self.linkView.metadata = metadata
            self.addSubView(linkView: self.linkView)
            self.linkView.sizeToFit()

            self.onUpdate?()

            self.activityIndicator.stopAnimating()
        }
    }

    private func addSubView(linkView: LPLinkView) {
        contentView.addSubview(linkView)
        linkView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            linkView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
            linkView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
            linkView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12),
            linkView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12),
        ])
    }
}

2020/3/29 更新

アニメーションですが
UITableViewの更新方法を下記に変更したら少し改善?しました


self.tableView.reloadRows(at: [indexPath], with: .none)



self.tableView.beginUpdates()
self.tableView.endUpdates()

ShareSheetにLPLinkMetadataを利用する

LPLinkMetadataはShareSheetでも
UIActivityItemSource
activityViewControllerLinkMetadata(_:)から
利用することができ
プレビュー情報をリンク先から取得して表示できるようになりました。

UIActivityItemSourceに適合したクラスを作成

ShareSheetに表示するアイテムを表すクラスを定義します


import UIKit
import LinkPresentation

final class ShareActivityItemSource: NSObject, UIActivityItemSource {
    private let linkMetadata: LPLinkMetadata

    init(_ url: URL) {
        linkMetadata = LPLinkMetadata()
        super.init()
        setPlaceholder(for: url)

        if let cachedData = MetaCache.shared.metadata(for: url) {
            setMetadata(cachedData)
            return
        }

        let metadataProvider = LPMetadataProvider()
        metadataProvider.startFetchingMetadata(for: url) { [weak self] metadata, error in
            guard let self = self, let metadata = metadata else {
                return
            }
            self.setMetadata(metadata)
        }
    }

    private func setPlaceholder(for url: URL) {
        linkMetadata.title = "loading..."
        linkMetadata.originalURL = url
        let loadingImageURL = URL(fileURLWithPath: Bundle.main.path(forResource: "loading", ofType: "png")!)
        linkMetadata.iconProvider = NSItemProvider(contentsOf: loadingImageURL)
    }

    private func setMetadata(_ metadata: LPLinkMetadata) {
        linkMetadata.title = metadata.title
        linkMetadata.url = metadata.url
        linkMetadata.originalURL = metadata.originalURL
        linkMetadata.iconProvider = metadata.iconProvider
        linkMetadata.imageProvider = metadata.imageProvider
    }

    func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
        linkMetadata
    }

    func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
        linkMetadata as Any
    }

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

ここではLPLinkMetadataに事前のデータを設定しておき
データが取得できたらメタ情報を入れ替えるようにします。

SwiftUIでの実装方法

UIActivityViewControllerの機能を有するViewを用意します。


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

ナビゲーションバーのボタンを押すと
ShareSheetを表示するようにします。


struct ContentView: View {
    var urls: [URL] = [
        URL(string: "https://www.apple.com/mac")!,
        URL(string: "https://www.apple.com/ipad")!,
        URL(string: "https://youtu.be/V85CQzsyvj4")!,
        URL(string: "https://twitter.com/yuukikikuchi/status/1240946299467259905")!,
    ]

    @State var redraw = false
    @State var showShareSheet = false

    var body: some View {
        NavigationView {
            List(self.urls, id: \.self) { url in
                LinkPresentationView(url: url, redraw: self.$redraw)
            }.onAppear {
                UITableView.appearance().separatorStyle = .none
            }
        }.navigationBarItems(trailing:
            Button(action: {
                self.showShareSheet = true
            }) {
                Text("Share").bold()
            }
        ).sheet(isPresented: $showShareSheet) {
            ShareSheet(activityItems: [ShareActivityItemSource(self.urls[Int.random(in: 0..<4)])])
        }
    }
}

ShareSheetactivityItems
ShareActivityItemSourceを渡します。

すると下記のような動きをします。

1回目のShareSheetの表示では
事前に準備した情報がまず表示され
取得後に入れ替わっています。

2回目はメタ情報をすでに取得しているので
最初からメタ情報が表示されています。

UIKitでの実装方法

同じ様にナビゲーションバーのボタンを押すと
ShareSheetが表示されるようにします。


class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        ...
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Share", style: .plain, target: self, action: #selector(shareTapped))
    }

    @objc func shareTapped() {
        showShareSheet(url: urls[Int.random(in: 0..<4)])
    }
}

extension ViewController {
    func showShareSheet(url: URL) {
        let item = ShareActivityItemSource(url)
        let activity = UIActivityViewController(activityItems: [item], applicationActivities: nil)
        present(activity, animated: true)
    }
}

下記のように動きます。

メタ情報のベストプラクティス

以上のように使い方を見てきましたが
Appleがメタ情報にどういう情報を載せるべきかを
紹介している動画があるので
次に見ていきます。

メタ情報にはあらゆる情報を含めることができますが
その中でもAppleの公式動画の中でベストプラクティスを紹介しています。

タイトルについてのベストプラクティス

  • タイトルからリンク先の内容がわかるようにする
  • <head><title>からタイトルを読み取ることもできるがサイト名がURLのドメインなどと重複して表示されるのを避けるために<meta og:title="">を設定する
  • JavaScriptは動かないので動的なタグの生成をしないようにする

アイコンについてのベストプラクティス

<link rel="icon">の情報を読み取るが
<meta og:image="">を指定するとアイコンが表示されなくなるので
アイコンを表示したい場合は<meta og:image="">を指定しないようにする

画像についてのベストプラクティス

  • 興味を引かせるような特定のページの内容を表す画像のみog:imageを設定する
  • 画像が取得できなかった場合の対処としてアイコンを設定する
  • テキストは含めない方が良い(全サイズのデバイスで表示する際にサイズなどがスケールしない)

動画についてのベストプラクティス

  • アイコン、画像、動画合わせて10MBまでなのでサイズに気を付ける
  • 自動再生をするためには直接参照したビデオファイルを使用する(不可能な場合、Youtubeの埋め込み動画のURLを指定すればユーザがタップして再生ができる。Youtube以外のサービスでは不可能)
  • HTMLやプラグインが必要な埋め込み動画のサポートはしていない

まとめ

LinkPresentation.frameworkについて見てみました。

使い方は非常にシンプルで便利ですが
表示時のアニメーションがいまいちであったり
まだ使い方が完全に把握できていないため
今後も試してみて理解を深めていく必要があります。

ここに記載したことはあくまで検証結果に基づいていますので
もし間違いなどございましたらぜひご指摘ください🙇🏻‍♂️

参照先

https://developer.apple.com/videos/play/wwdc2019/262/
https://developer.apple.com/videos/play/tech-talks/205
https://medium.com/better-programming/ios-13-rich-link-previews-with-swiftui-e61668fa2c69
https://augmentedcode.io/2019/09/15/loading-url-previews-using-linkpresentation-framework-in-swift/
https://nshipster.com/ios-13/
https://www.swiftjectivec.com/linkpresentation-introduction/
https://qiita.com/ezura/items/6036c6e100599b601482
https://forums.developer.apple.com/thread/123951
https://github.com/SDWebImage/SDWebImageLinkPlugin

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57