📋

iOS 15のクリップボード管理 UIPasteboard

2022/01/09に公開

iOS 14 -> iOS 15の変更

iOS 15でUIPasteboardに新機能が追加されていたようなので調べてみました。

ペースト時のアラート

iOS 14から別のアプリからコピーしたテキストをクリップボードから取得するとセキュリティの関係で画面上部にアラートが出てきます。しかしクリップボードのテキストのパターンを知るだけなら、アラートは出てきません。

もしクリップボードのテキストがURLのパターンにマッチしていれば、ユーザーに「クリップボードにURLが含まれているようです。URLを開きますか?」などと気の利いたアクションを安心感をともなって行うことができます。

iOS 14ではそのパターンが3つだけでしたが、iOS 15では11つに増えています。

iOS 14

extension UIPasteboard.DetectionPattern {
    
    /// NSString value, suitable for implementing "Paste and Go"
    @available(iOS 14.0, *)
    public static let probableWebURL: UIPasteboard.DetectionPattern
    
    /// NSString value, suitable for implementing "Paste and Search"
    @available(iOS 14.0, *)
    public static let probableWebSearch: UIPasteboard.DetectionPattern
    
    /// NSNumber value
    @available(iOS 14.0, *)
    public static let number: UIPasteboard.DetectionPattern
}

iOS 15

extension UIPasteboard {

    @available(iOS 15.0, *)
    public struct DetectedValues {
        public var patterns: Set<PartialKeyPath<UIPasteboard.DetectedValues>> { get }
        public var probableWebURL: String { get }
        public var probableWebSearch: String { get }
        public var number: Double? { get }
        public var links: [DDMatchLink] { get }
        public var phoneNumbers: [DDMatchPhoneNumber] { get }
        public var emailAddresses: [DDMatchEmailAddress] { get }
        public var postalAddresses: [DDMatchPostalAddress] { get }
        public var calendarEvents: [DDMatchCalendarEvent] { get }
        public var shipmentTrackingNumbers: [DDMatchShipmentTrackingNumber] { get }
        public var flightNumbers: [DDMatchFlightNumber] { get }
        public var moneyAmounts: [DDMatchMoneyAmount] { get }
    }
}

そしてiOS 14の3パターンは以下のメソッドに依存しており、iOS 15では早くもdeprecatedとなっております😅

extension UIPasteboard {
    @available(iOS, introduced: 14.0, deprecated: 15.0)
    open func detectPatterns(for patterns: Set<UIPasteboard.DetectionPattern>, completionHandler: @escaping (Result<Set<UIPasteboard.DetectionPattern>, Error>) -> ())
    
     @available(iOS, introduced: 14.0, deprecated: 15.0)
    open func detectPatterns(for patterns: Set<UIPasteboard.DetectionPattern>, inItemSet itemSet: IndexSet?, completionHandler: @escaping (Result<[Set<UIPasteboard.DetectionPattern>], Error>) -> ())
    
    @available(iOS, introduced: 14.0, deprecated: 15.0)
    open func detectValues(for patterns: Set<UIPasteboard.DetectionPattern>, completionHandler: @escaping (Result<[UIPasteboard.DetectionPattern : Any], Error>) -> ())
    
    @available(iOS, introduced: 14.0, deprecated: 15.0)
    open func detectValues(for patterns: Set<UIPasteboard.DetectionPattern>, inItemSet itemSet: IndexSet?, completionHandler: @escaping (Result<[[UIPasteboard.DetectionPattern : Any]], Error>) -> ())
}

これらがiOS 15で導入された11パターンを使うメソッド群です。async-await版もありますね。

extension UIPasteboard {

    @available(iOS 15.0, *)
    open func detectPatterns(for keyPaths: Set<PartialKeyPath<UIPasteboard.DetectedValues>>, completionHandler: @escaping (Result<Set<PartialKeyPath<UIPasteboard.DetectedValues>>, Error>) -> ())
    @available(iOS 15.0, *)
    open func detectedPatterns(for keyPaths: Set<PartialKeyPath<UIPasteboard.DetectedValues>>) async throws -> Set<PartialKeyPath<UIPasteboard.DetectedValues>>

    @available(iOS 15.0, *)
    open func detectPatterns(for keyPaths: Set<PartialKeyPath<UIPasteboard.DetectedValues>>, inItemSet itemSet: IndexSet?, completionHandler: @escaping (Result<[Set<PartialKeyPath<UIPasteboard.DetectedValues>>], Error>) -> ())
    @available(iOS 15.0, *)
    open func detectedPatterns(for keyPaths: Set<PartialKeyPath<UIPasteboard.DetectedValues>>, inItemSet itemSet: IndexSet?) async throws -> [Set<PartialKeyPath<UIPasteboard.DetectedValues>>]

    @available(iOS 15.0, *)
    open func detectValues(for keyPaths: Set<PartialKeyPath<UIPasteboard.DetectedValues>>, completionHandler: @escaping (Result<UIPasteboard.DetectedValues, Error>) -> ())
    @available(iOS 15.0, *)
    open func detectedValues(for keyPaths: Set<PartialKeyPath<UIPasteboard.DetectedValues>>) async throws -> UIPasteboard.DetectedValues

    @available(iOS 15.0, *)
    open func detectValues(for keyPaths: Set<PartialKeyPath<UIPasteboard.DetectedValues>>, inItemSet itemSet: IndexSet?, completionHandler: @escaping (Result<[UIPasteboard.DetectedValues], Error>) -> ())
    @available(iOS 15.0, *)
    open func detectedValues(for keyPaths: Set<PartialKeyPath<UIPasteboard.DetectedValues>>, inItemSet itemSet: IndexSet?) async throws -> [UIPasteboard.DetectedValues]
}

実装例

他にサンプルがなかったので試行錯誤しました。間違いなどありましたらコメントをください。

テキストを取得するため、アラートが出るケース

    func detectPasteboardValues() {
        guard UIPasteboard.general.hasStrings else {
            return
        }
        let somePatterns: Set<PartialKeyPath<UIPasteboard.DetectedValues>> = [
             \.probableWebURL,
             \.probableWebSearch,
             \.number,
             \.links,
             \.phoneNumbers,
             \.emailAddresses
        ]
        UIPasteboard.general.detectValues(for: somePatterns) { result in
            switch result {
            case .success(let detectedValues):
                let detectedPatterns = detectedValues[keyPath: \.patterns]
                for pattern in detectedPatterns {
                    print("\(detectedValues[keyPath: pattern])")
                }
            case .failure: 
	        break
            }
        }
    }

テキストが"test@example.comだ!"の場合は次の2つにマッチします。
\.emailAddresses: "test@example.com" (メールは配列でDDMatchEmailAddressとして格納)
\.probableWebSearch: "test@example.comだ!"

パターン検出のみ必要でアラートは出ないケース

    func detectPasteboardPattern() {
        guard UIPasteboard.general.hasStrings else {
            return
        }
        let allPatterns: Set<PartialKeyPath<UIPasteboard.DetectedValues>> = [
             \.probableWebURL,
             \.probableWebSearch,
             \.number,
             \.links,
             \.phoneNumbers,
             \.emailAddresses,
             \.postalAddresses,
             \.calendarEvents,
             \.shipmentTrackingNumbers,
             \.flightNumbers,
             \.moneyAmounts
        ]
        UIPasteboard.general.detectPatterns(for: allPatterns) { result in
            switch result {
            case .success(let detectedPatterns):
                if detectedPatterns.contains(\.probableWebURL) {
                    // URLを含んだテキスト
                } else if detectedPatterns.contains(\.probableWebSearch) {
                    // probableWebSearchはほとんどのテキストにマッチ
		    // するため、他に優先するものがあれば最後に置くといい
                } else {
                    // その他、空文字など
                }
            case .failure:
                break
            }
        }
    }

async-await版を使ったケース

    func detectPasteboardPatternsAndValues() {
        guard UIPasteboard.general.hasStrings else {
            return
        }
        let somePatterns: Set<PartialKeyPath<UIPasteboard.DetectedValues>> = [
             \.probableWebURL,
             \.probableWebSearch,
             \.number,
             \.links,
             \.phoneNumbers,
             \.emailAddresses
        ]
        Task {
            do {
                let detectedPatterns = try await UIPasteboard.general.detectedPatterns(for: somePatterns)
                let detectedValues = try await UIPasteboard.general.detectedValues(for: detectedPatterns)
                for pattern in detectedPatterns {
                    print("\(detectedValues[keyPath: pattern])")
                }
            } catch {
                print(error)
            }
        }
    }

Discussion

ログインするとコメントできます