便利な解錠方法-NSUserActivity-SiriShortcuts解錠

WebエンジニアのBunです。主にiOSアプリの開発を担当しています。

日々いろんな解錠方法について考えています。特にiOSアプリで使えるいろんな便利な機能を使った解錠方法とその実装手順をまとめます。 今回はNSUserActivityを利用したSiri Shortcutsでの解錠について説明します。

NSUserActivityとは

抽象的な言い方だと、NSUserActivityは「ある時点でのアプリの状態」を表現します。

ユーザーがアプリを使って、コンテンツの表示、ドキュメントの編集、Webページの表示、ビデオの視聴などを行った時の状態(関連情報)をNSUserActivityのオブジェクトとして保存すれば、再度アプリが起動された時、上記の状態から復元することができます。

一例として、以下が可能です。詳細について、今後順次紹介します。

  • Shortcutsアプリに登録したShortcutのActionを実行した時、指定画面に遷移させたり、機能を実現可能
  • 音声でアプリを起動し、指定画面に遷移させたり、機能を実現可能
  • Hand-off/Hand-freeでMacで途中までの作業をそのままシームレスでiPhoneで作業再開可能
  • 端末内での検索可能
  • Webと連携すれば、Global検索も可能
  • iOS15からのQuickNoteでも使われている(まだ使ったことない)

Siri Shortcutsの使い方(解錠方法)

  • Siri Shortcutsに登録すれば、使用頻度が上がる時、Siriからの提案にShortcut(解錠)が表示され、1Tapで解錠可能

※Siriからの提案:Home画面を下にSwipe、ロック画面で左にSwipeした時表示される検索窓(Spotlight)の一番上位に表示される

  • Siriを呼び出して、音声から解錠可能。登録済みの音声フレーズ「あけるん」を言えば、アプリが起動され、指定画面に遷移し、対象ドアを解錠できる。

※端末によるかも知れないが、 Home画面(生体認証後のLock画面)でサイドボタンを2回ClickでSiriを呼び出せる。もちろん「Hey Siri」でもOK

  • オートメーションを作れば、「xxx」の時「解錠」することも可能。最後に説明する。

Siri Shortcutsの実装方法

Siri Shortcutsの登録

Siri Shortcutsの登録方法には、NSUserActivityを利用した方法とIntentsを利用する方法がありますが、今回は前者について説明します。Intentsについてはまた別の機会でご紹介します。

NSUserActivityTypesを定義

Info.plistのNSUserActivityTypesにNSUserActivity作成に必要なIDを定義します。 一意であれば何でも良いですが、アプリのbundleId+機能ごとの文字列で良いと思います。

<key>NSUserActivityTypes</key>
<array>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER).unlock-door</string>
</array>

アクションを行うNSUserActivityオブジェクトを生成

  • オブジェクト生成
class SKUserActivity {
    enum ActivityType: String {
        case doorUnlock = "unlock-door"
        case keyShare = "share-key"
        
        var id: String {
            // TODO: add team id (info.plist)
            return (Bundle.main.bundleIdentifier ?? "") + "." + self.rawValue
        }
    }
    class func create(_ type: ActivityType, _ title: String, _ phrase: String? = nil) -> NSUserActivity {
        // Info.plistに登録したID
        let userActivity = NSUserActivity(activityType: type.id)
        userActivity.persistentIdentifier = type.id
        userActivity.title = title
        // 音声フレーズ(コマンド)。Siri Shortcutsに登録時変更可能
        userActivity.suggestedInvocationPhrase = phrase
        // Siriからの提案を可能にする
        userActivity.isEligibleForPrediction = true
        // 検索可能にする
        userActivity.isEligibleForSearch = true
        return userActivity
    }
}
  • Spotlight検索結果に表示する場合
let attributeSet = CSSearchableItemAttributeSet(contentType: .image)
// 実際userActivity.titleが表示され、このtitleは表示されない
attributeSet.title = "Test"
attributeSet.contentDescription = "Unlock"
userActivity.contentAttributeSet = attributeSet

アクションで開く画面のUIViewControllerにオブジェクトを設定

NSUserActivityのオブジェクトを画面(UIViewController)に紐づけることで、Shortcutsアプリに表示され、登録可能になります。(一度アプリを起動し、該当画面に遷移する必要があるようです)

override func viewDidLoad() {
    super.viewDidLoad()
    self.userActivity = self.siriUserActivity
}

Siri Shortcutsボタンを追加

ユーザーが簡単にSiri Shortcutsを登録できるように、画面に専用ボタンを配置することも可能です。

UIKitの場合、上記のUIViewControllerにINUIAddVoiceShortcutButtonを配置すれば良いですが、SwiftUIの場合、INUIAddVoiceShortcutButtonそのまま使えないので、UIViewControllerRepresentableを使って、UIKit->SwiftUIパーツに変換する必要があります。

  • Siriボタンのパーツを作成
import Foundation
import SwiftUI
import Intents

struct SiriButton: UIViewControllerRepresentable {
    private let userActivity: NSUserActivity
    
    init(userActivity: NSUserActivity) {
        self.userActivity = userActivity
    }
    
    func makeUIViewController(context: Context) -> some UIViewController {
        return SiriShortcutViewController(userActivity: self.userActivity)
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        
    }
}
  • SiriShortcutViewControllerを作成し、INUIAddVoiceShortcutButtonを配置する
import Foundation
import SwiftUI
import IntentsUI

class SiriShortcutViewController: UIViewController {
    private let siriUserActivity: NSUserActivity
    
    init(userActivity: NSUserActivity) {
        self.siriUserActivity = userActivity
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.userActivity = self.siriUserActivity
        // これはなくても良い?(詳細不明)
        self.userActivity?.becomeCurrent()
        
        let siriButton = INUIAddVoiceShortcutButton(style: .blackOutline)
        siriButton.shortcut = INShortcut(userActivity: self.siriUserActivity)
        siriButton.delegate = self
        siriButton.translatesAutoresizingMaskIntoConstraints = false
        
        self.view.addSubview(siriButton)
        self.view.centerXAnchor.constraint(equalTo: siriButton.centerXAnchor).isActive = true
        self.view.centerYAnchor.constraint(equalTo: siriButton.centerYAnchor).isActive = true
    }
}
  • SwiftUIの画面にSiriボタンを配置
struct TestView: View {
    var body: some View {
        VStack() {
            SiriButton(userActivity: SKUserActivity.create(.doorUnlock, "unlock", "あけるん"))
        }
    }
}

SiriボタンでSiri Shortcutsに追加する時の処理

Siri Shortcutsに新規追加、編集、削除時の画面表示するための最小限の実装になります。

extension SiriShortcutViewController: INUIAddVoiceShortcutButtonDelegate {
    func present(_ addVoiceShortcutViewController: INUIAddVoiceShortcutViewController, for addVoiceShortcutButton: INUIAddVoiceShortcutButton) {
        addVoiceShortcutViewController.delegate = self
//        addVoiceShortcutViewController.modalPresentationStyle = .formSheet
        // Shortcut画面を閉じる時の動作検知
        addVoiceShortcutViewController.presentationController?.delegate = self
        
        self.present(addVoiceShortcutViewController, animated: true, completion: nil)
    }
    
    func present(_ editVoiceShortcutViewController: INUIEditVoiceShortcutViewController, for addVoiceShortcutButton: INUIAddVoiceShortcutButton) {
        editVoiceShortcutViewController.delegate = self
        // Shortcut画面を閉じる時の動作検知
        editVoiceShortcutViewController.presentationController?.delegate = self
        
        self.present(editVoiceShortcutViewController, animated: true, completion: nil)
    }
}

extension SiriShortcutViewController: INUIAddVoiceShortcutViewControllerDelegate {
    func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) {
        controller.dismiss(animated: true, completion: nil)
    }
}

extension SiriShortcutViewController: INUIEditVoiceShortcutViewControllerDelegate {
    func editVoiceShortcutViewController(_ controller: INUIEditVoiceShortcutViewController, didUpdate voiceShortcut: INVoiceShortcut?, error: Error?) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func editVoiceShortcutViewController(_ controller: INUIEditVoiceShortcutViewController, didDeleteVoiceShortcutWithIdentifier deletedVoiceShortcutIdentifier: UUID) {
        controller.dismiss(animated: true, completion: nil)
    }
    
    func editVoiceShortcutViewControllerDidCancel(_ controller: INUIEditVoiceShortcutViewController) {
        controller.dismiss(animated: true, completion: nil)
    }
}

extension SiriShortcutViewController: UIAdaptivePresentationControllerDelegate {
    func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
        // Shortcut will be closed
    }
    
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        // Shortcut was closed
    }
}

Siriに追加したら、Shortcutsアプリにも表示されます。

直接Shortcutsアプリから登録する場合、ある程度アプリを使わないと表示されないようです。

下記のAppをTapし、表示されるアプリ一覧には表示されません。

Siri Shortcutsから解錠

Shortcut、Siri音声コマンド、Spotlight検索窓のSiriからの提案に表示されているShortcutなどから1Tapでアプリを起動し、解錠することが可能です。 下記でアクションをハンドリングします。

  • UIKitアプリの場合、以下のDelegate関数を実装
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard let userInfo = userActivity.userInfo else {
        return false
    }
    guard userActivity.activityType == SKUserActivity.ActivityType.doorUnlock.id else {
        return false
    }
    // userInfoから鍵/ドアのIDを取得し、関連画面に遷移し、解錠処理を行う
    return true
}
  • SwiftUIアプリの場合、表示する画面で以下を実装
struct ContentView: View {
    var body: some View {
        NavigationView {
            ...
        }
        .onContinueUserActivity(SKUserActivity.ActivityType.doorUnlock.id, perform: handleShortcut(_:))
    }
}

extension ContentView {
    func handleShortcut(_ userActivity: NSUserActivity) {
        if let userInfo = userActivity.userInfo {
            // userInfoから鍵/ドアのIDを取得し、解錠処理を行う
            print("userInfo: \(userInfo)")
        }
    }

解錠処理の実装

Akerun公開APIで合鍵発行、解錠が可能です。 具体的な実装は割愛します。弊社のAkerun Developersと下記の関連記事をご参照ください。

developers.akerun.com

akerun.hateblo.jp

akerun.hateblo.jp

Siri Shortcutsを使った便利機能(拡張版解錠方法)

Shortcutsアプリからオートメーションを作成すれば、色んな方法で解錠することができます。

iPhone背面タップして解錠

iOS設定->アクセシビリティ->タッチ->背面タップからアプリのShortcutを登録します。

f:id:photosynth-inc:20210906214945g:plain

QRコードで解錠

ShortcutsアプリからQRスキャンしてアプリを起動するShortcutを作成し、Home画面に追加します。

  • ギャラリーからマイショートカットにQRコードスキャンを追加

  • 既存アクションを削除し、自作アプリを登録

f:id:photosynth-inc:20210906215027g:plain

NFCタグで解錠

新規オートメーションを作成し、NFCスキャンをトリガーに、アプリをアクションに登録します。

f:id:photosynth-inc:20210906215326g:plain

まとめ

NSUserActivityではいろんなことができます。Webと連携することも可能です(別の機会で書く予定)。

その一つのSiri Shortcutsを実装すれば、iOS純正アプリShortcutsを使って無限な便利な機能を提供できそうです。

次回は、Widget、AppClipについて書こうと思います。


株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co

Akerun Proの購入はこちらから akerun.com