便利な解錠方法-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

便利な解錠方法-HomeScreenQuickAction解錠

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

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

3D TouchのHomeScreenQuickActionとは

HomeScreenQuickActionの使い方(解錠方法)

iOS13以降で3D Touch対応端末で、アプリのHomeアイコンを強く押し込んだ時、以下の画面が表示されます。HomeScreenQuickActionに解錠Actionを登録した場合、表示されている鍵/ドア一覧から鍵/ドアをTapすると、アプリが画面が表示され、解錠処理が行われます。

HomeScreenQuickActionの実装方法

HomeScreenQuickActionへのAction登録

静的Actionの登録

アプリのInfo.plistにAction関連ShortcutItemsを追加します。

Key name 説明
UIApplicationShortcutItemType (必須) アクション種別
UIApplicationShortcutItemTitle (必須) Home Screen 上に表示されるタイトル
UIApplicationShortcutItemSubtitle Home Screen 上に表示されるサブタイトル
UIApplicationShortcutItemIconType アイコンの種別(システム指定アイコン)
UIApplicationShortcutItemIconFile カスタムアイコン
UIApplicationShortcutItemUserInfo 起動時に渡されるデータ
UIApplicationShortcutItemIconSymbolName SF Symbol

※IconType/IconFile/IconSymbolNameで画像を指定した場合、以下の順で適用される。

  1. UIApplicationShortcutItemIconSymbolName
  2. UIApplicationShortcutItemIconFile
  3. UIApplicationShortcutItemIconType

Info.plist

<key>UIApplicationShortcutItems</key>
<array>
    <dict>
        <key>UIApplicationShortcutItemType</key>
        <string>SearchAction</string>
        <key>UIApplicationShortcutItemIconType</key>
        <string>UIApplicationShortcutIconTypeSearch</string>
        <key>UIApplicationShortcutItemTitle</key>
        <string>Search</string>
        <key>UIApplicationShortcutItemSubtitle</key>
        <string>Search for an item</string>
    </dict>
    <dict>
        <key>UIApplicationShortcutItemType</key>
        <string>ShareAction</string>
        <key>UIApplicationShortcutItemIconType</key>
        <string>UIApplicationShortcutIconTypeShare</string>
        <key>UIApplicationShortcutItemTitle</key>
        <string>Share</string>
        <key>UIApplicationShortcutItemSubtitle</key>
        <string>Share an item</string>
    </dict>
</array>

動的Actionの登録

let icon = UIApplicationShortcutIcon(templateImageName: "shortcut-unlock")
let item = UIMutableApplicationShortcutItem(
    type: key.id,
    localizedTitle: key.name,
    localizedSubtitle: "sub",
    icon: icon,
    userInfo: nil // 必要な情報があればここに追加([String : NSSecureCoding])
)
// 複数のitemを設定できるが、システムが画面サイズに合わせて最適な数を表示(大体最大4個?)
UIApplication.shared.shortcutItems = [item]

Tapした時の処理

Tapした時のイベント処理はUIKit AppとSwiftUI App、そしてSceneを使うかどうかで処理が異なるので、それぞれの処理について説明します。

UIKit Appの場合

  • Sceneを使わない場合
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
    // typeから鍵/ドアのIDを取得し、画面表示、解錠処理を行う
    actionShortcut(shortcutItem.type)
    completionHandler(true)
}
  • Sceneを使う場合、且つ、アプリが起動されていない場合
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    if let shortcutItem = connectionOptions.shortcutItem {
        handleShortcutItem(shortcutItem)
    }
}

func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) {
    let id = shortcutItem.type

    // DeepLinks(Universal Links or Custom URL)を使って、関連画面に遷移
    if let url = URL(string: "xxx/" + Id) {
        openURL(url)
        // UIApplication.shared.open(url)
    }
}

通常のDebugの場合、既にアプリが起動されているので、初回起動時のDebugは下記の方法で行います。 Schemeを下記通りに設定し、必要なところにBreakPointを入れます。Runを行い、HomeScreenをTapしてアプリを起動すれば、Debugが可能になります。

  • Sceneを使う場合、且つ、アプリが起動されている場合
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
    handleShortcutItem(shortcutItem)
}

SwiftUI Appの場合

デフォルトでAppDelegateとSceneDelegateがないので、カスタムScene Delegateを作成します。 Actionを受信後、openURL()でDeepLinkを送って、対象画面で処理を行います。

@main
struct SmaKeyApp: App {
    @Environment(\.scenePhase) private var scenePhase
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: AppViewModel())
        }
    }
}

class AppDelegate: UIResponder, UIApplicationDelegate {
    // MARK: UISceneSession Lifecycle
    
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Info.plistに"Default Configuration"(名前は任意)を設定し、nameに指定した場合、CustomSceneDelegateは不要かも
        let sceneConfiguration = UISceneConfiguration(name: "Custom Configuration", sessionRole: connectingSceneSession.role)
        sceneConfiguration.delegateClass = CustomSceneDelegate.self
        
        return sceneConfiguration
    }
    
    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }
}

class CustomSceneDelegate: UIResponder, UIWindowSceneDelegate {
    @Environment(\.openURL) var openURL
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let shortcutItem = connectionOptions.shortcutItem {
            handleShortcutItem(shortcutItem)
        }
    }

    func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
        handleShortcutItem(shortcutItem)
        completionHandler(true)
    }
}

extension CustomSceneDelegate {
    // Home Screen Quick Action - Shortcut
    func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) {
        let id = shortcutItem.type
        if let url = URL(string: "xxx/" + id) {
            openURL(url)
        }
    }
}

解錠処理の実装

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

developers.akerun.com

akerun.hateblo.jp

akerun.hateblo.jp

まとめ

実装方法はいくつかありますが、アプリ種類に合わせれば簡単に実装できます。

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


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

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

便利な解錠方法-Spotlight解錠

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

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

Spotlight(検索)とは

Spotlightの使い方

Spotlight(検索)について普段から活用している人も多いと思いますが、ロック画面やホーム画面を右スワイプ、あるいは、ホーム画面を下にスワイプした時に出てくる「検索窓」のことです。

  • 右にスワイプ時に表示される検索窓(をTap)

  • 下にスワイプ時に表示される検索窓

Spotlightでの解錠方法

  • Spotlight検索画面でキーワード(アプリで設定された「解錠」文字列、ドア名など)を入力すれば、アプリ経由で解錠できる鍵/ドア一覧が表示される。
    • 鍵/ドア名を「Home」のようは一般的なキーワードにした場合、関係ない検索結果もたくさん表示されてしまうので、アプリ名か独自キーワードで工夫する必要がある。
    • 頻繁に使われている鍵/ドアの場合、自動的に最上位に表示されるので(iOSの独自アルゴリズムなので、表示順はアプリで制御できない)、使えば使えるほど便利になる。

  • 一覧に表示されている鍵/ドアをTapすれば、アプリが表示され、解錠まで1Tapで実現可能。

CoreSpotlightの実装

1Tapで解錠するには、iOSのCoreSpotlightのFrameworkを使って実装する必要があります。

Spotlightへの登録

検索する情報をItemとしてSpotlightのIndexに登録する必要があります。 Item数は数千個までなら最適に機能できるようです。

Core Spotlight works best when you have no more than a few thousand items.

  • CoreSpotlightを使うので、importする
import CoreSpotlight
  • 検索項目ごとにCSSearchableItemAttributeSetのインスタンを生成し、それそれのインスタンスのpropertiesを設定する。例えば、複数の鍵/ドアを持っている場合、それぞれの鍵/ドアのCSSearchableItemAttributeSetのインスタンを生成。
    • iOS13までinit(itemContentType: String)、iOS14からはinit(contentType: UTType)を使ってインスタンスを生成
    • シンプルに検索結果にtitle、画像を表示する場合、.imageのcontentTypeを指定する
    • 基本propertiesとして、title、contentDescription、keywords、thumbnailDataを設定する

  • keywordsには検索しやすい、引っかかりやすいキーワードを複数指定する。例えば、アプリ名、「解錠」などアプリ専用単語、鍵/ドア名など。
let attributeSet = CSSearchableItemAttributeSet(contentType: .image)
attributeSet.title = "解錠 \(key.name)"
attributeSet.contentDescription = "\(key.name)を解錠します"
attributeSet.keywords = ["XXXKey", "解錠", key.name]
attributeSet.thumbnailData = thumbnail
  • 鍵/ドアごとの一意ID(uniqueIdentifier)を指定したCSSearchableItemのインスタンスを生成し、上記のCSSearchableItemAttributeSetのインスタンスを関連付ける。
    • サーバーから取得した鍵/ドアのidが一意になっているので、そのまま指定すればOK
    • uniqueIdentifierに指定されたIDはCSSearchableItemActivityIdentifierをキーとしてNSUserActivityのuserInfo登録されるので、検索結果から鍵/ドアの識別に使われる。
    • groupで複数のItemを管理する場合、domainIdentifierも指定する。com.myCompany.myContentType(domainの逆順)の形式の任意の一意文字列を指定すればOK。
// ble/遠隔を区別する場合、prefixなどをつける
let id = "ble_" + key.id
let domainId = "xxx.yyy.CoreSpotlight.key"
let item = CSSearchableItem(uniqueIdentifier: id, domainIdentifier: domainId, attributeSet: attributeSet)
  • IndexにItemを登録する
    • 複数の鍵/ドアがある場合、配列で一括登録
CSSearchableIndex.default().indexSearchableItems([item]) { (error) in
}

Item登録/削除タイミング

アプリにログインし、サーバーから鍵/ドア一覧情報を取得する度にItemを登録し(2回目以降の場合、既存Itemを一度削除してから再登録)、ログアウトした時全てのItemを削除します。

  • 鍵/ドア情報が変わる可能性があるので、一覧を取得する度に再登録する(一度全て削除してから登録)
    • 削除した場合、今までの評価とランキング情報がなくなるかも?(詳細不明)
    • 更新方法はないようなので、indexSearchableItemsに同じIDを渡して再登録
  • CSSearchableIndexDelegateを実装しても良いかも
  • Batch処理も可能

https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/AppContent.html#//apple_ref/doc/uid/TP40016308-CH7-SW1

Spotlight検索結果からItemをTapした時の解錠処理(UIKit AppとSwiftUI App)

Tapした時のイベント処理はUIKit AppとSwiftUI Appでの処理が異なるので、それぞれの処理について説明します。

UIKit Appの場合

AppDelegateの以下のメソッドでイベントを拾って処理を行います。

  • Itemに指定されたuniqueIdentifierがNSUserActivityのuserInfoのCSSearchableItemActivityIdentifier(定義値:kCSSearchableItemActivityIdentifier文字列)キーの値として渡されるので、そこから鍵/ドアをIDを取得し、解錠処理を行う。
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    guard let userInfo = userActivity.userInfo else {
        return false
    }
    guard userActivity.activityType == CSSearchableItemActionType else {
        return false
    }
    guard let itemId = userInfo[CSSearchableItemActivityIdentifier] as? String else {
        return false
    }

    // itemIdから鍵/ドアのIDを取得し、解錠処理を行う

    return true
}

SwiftUI Appの場合

ViewのonContinueUserActivityメソッドでイベントを拾って処理を行います。

  • 基本的に解錠処理を表示する画面でイベントを拾えばOK。例えばTabViewの場合は下記になる。
struct ContentView: View {
    var body: some View {
        TabView(selection: $selection) {
            ...
        }
        .onContinueUserActivity(CSSearchableItemActionType, perform: handleSpotlight(_:))
    }
    
    func handleSpotlight(_ userActivity: NSUserActivity) {
        guard let userInfo = userActivity.userInfo else {
            return
        }
        guard let itemId = userInfo[CSSearchableItemActivityIdentifier] as? String else {
            return
        }
        // itemIdから鍵/ドアのIDを取得し、解錠処理を行う
    }
}

解錠処理の実装

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

developers.akerun.com

akerun.hateblo.jp

akerun.hateblo.jp

まとめ

実装も簡単な便利な機能なので、積極的にSpotlightを使いましょう。

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


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

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

プロフェッショナルマネジメント

この記事は Akerun Advent Calendar 2020 - Qiita の25日目の記事です。

2020年6月にフォトシンス に入社して、WEBグループのマネージャをしていますNonです。

2000年に大手ベンダーのSEとして社会人をスタートし、今年で21年目となりますが、ベンチャーから大手企業まで、複数の会社を通じて、大小様々なプロジェクトを経験しております。

ここ10数年は、主にエンジニアチームのマネジメントに従事してきて、マネジメントの難しさを地肌で感じると共に、色々な案件を通じてマネジメントスキルを磨くことができました。

今回は、マネジメントに携わる上で、私が重要視していることを少しお話できればと思います。

マネジメントとは?

マネジメントとは?聞かれた場合、皆さんはなんと答えるでしょうか?

アメリカの経営学者P.F.ドラッカーによると下記のように述べています。

  • マネジメント:組織に成果を上げさせるための道具、機能、機関
  • マネージャー:組織の成果に責任を持つ者

【出典】P.F.ドラッカー(1999)「明日を支配するもの 21世紀のマネジメント革命」ダイヤモンド社

組織の成果に注目し、マネージャーは、組織の成果に責任を持たなければいけません。

つまり、メンバーの動機付け、評価、育成などを通じて組織を作っていく役割を求められます。

マネジメントの課題

マネジメントは難しいとよく言われますが、具体的にどういった課題があるのでしょうか?

  • 部下の育成が出来ない、仕事を任せられない
  • 組織目標やビジョンが打ち出せず組織をまとめられない
  • 組織間の調整がうまくできない
  • 上司と部下の間に挟まれる立場になり判断に困る

このようなことを良く耳にします。

一見どれも重要な課題に感じますが、引き起こされる結果に注目する必要があります。

これらが原因となって、組織の目標を達成できないということが最も重要なポイントです。

マネージャーは、組織の成果に責任を持つ者なので、組織の目標を達成すべく、様々な手段を講じる必要があります。

マネジメントに求められるスキル

マネージャーは自分の業務だけではなく、メンバーの業務と組織を管理する能力が問われます。

プレイヤーからマネージャーに昇進すると、今までとは違う壁にぶつかります。

部下を育てられずにいると、いつまで経っても自分自身の業務が減りません。

結果、プレイヤーとしての負荷が高くなってしまい、マネジメントの比率が低くなってしまいます。

この問題を解決するためには、マネジメントスキルを高める必要があります。

では、マネージャーには、どんなスキルが求められるのでしょうか?

私のこれまでの経験から14のスキルに整理してみました。

マネジメントに求められる14のスキル
1 見積管理 要件からタスクを洗い出し、メンバーのスキル状況も考慮して、適切な見積もりができるか。
2 業務管理 「何を(What)」、「いつまでに(When)」、「どうやって(How)」やるかという具体的な指示をメンバー与え、その過程や結果を管理・監督できるか。
3 指導・教育 目標設定を適切に行い、教育プランを考えて部下をゴールまで導いて行けるか。
4 業務改善 日々の業務をこなすだけではなく、課題を解決したり、仕組み化したりして業務効率をあげることができるか。
5 リスク対策 リスクの洗い出しや対策準備、リスク発生後の適切な処置が迅速に行えるか
6 課題の発見と解決 業務における課題や問題点を見つけだし、その解決策を提案できるか。
7 トラブル収束 問題が起きた際にその内容に応じて迅速かつ柔軟に対応できるか。
8 組織ビルディング 組織に必要な役割を明確にし、規模に応じた組織の体制作りができるか。
9 労務管理 残業、休日出勤などの管理を徹底し、健康面を第一に考えた判断ができるか。
10 経営理念の周知・徹底 経営者の目指す方向を正確にメンバーに伝え、同じ方向に導けるか。
11 予算管理 収支・収益を意識し、より少ない投資で最大限の利益をあげられるか。
12 業務成果の適切な評価 メンバーの成果に対して、主観的ではなく客観的に評価ができるか。
13 適材適所への配置 メンバーのスキルや強み・弱みを把握し、育成の観点も含めてリソース配置を行えるか。
14 戦略立案 中長期の戦略や方針を正しく策定できるか。

マネージャになると、大手の企業ではマネジメント研修などもありますが、ベンチャー企業など、教育制度の整ってない企業では、いきなりマネージャーをさせられことも少なくありません。

マネージャーは、プロジェクトの進捗管理をしたり、リソースの管理をしたりすることに、フォーカスされがちですが、もっとたくさんのことを求められます。

組織の成果を出すためには、上記のようなスキルを身につけて、業務を遂行していかなければなりませんので、マネージャーになったら自身の役割と必要なスキルを確認しましょう。

プロジェクトマネジメントで重要なこと

ここまでは、マネジメントという少し広い範囲でのお話をしましたが、もう少しスコープを絞って、プロジェクトマネジメントについても見ていきましょう。

私は、プロジェクトマネジメント目的は、チームパフォーマンスの最大化と考えています。 限られたリソースの中でどれだけパフォーマンスを最大化できるかが、プロマネに求められることだと思います。

プロジェクトマネジメントで重要な三要素
  • タスクを適切に振り分けること
  • トラブルを未然に防ぐこと
  • 仕組み化を徹底し、無駄な時間を減らすこと

簡単にいうとムリ・ムダ・ムラの徹底排除です。

タスクを適切に振り分けること

タスクを漏れなく洗い出し、期日やメンバーの育成などを踏まえて、適切にタスクを振り分けていかなければなりません。経験値の高い人にばかり頼っていては、中長期的に見たときに、チームとしてのパフォーマンスは最大化されません。

トラブルを未然に防ぐこと

トラブルは、どんなにケアしていても起こってしまいますが、トラブルが少なければ少ないほど、計画はスムーズに進みます。ありとあらゆるリスクを想定して、未然に防ぐ努力をしましょう。

仕組み化を徹底し、無駄な時間を減らすこと

同じような作業を、他の人が繰り返していることは、色々な現場でよく見かけます。テンプレート化したり、プログラムで自動化したりすることで、大幅に作業時間を減らすことができます。少しでも、無駄を削減できるように、常に仕組み化できないか考えることが重要です。

また、プロジェクトマネジメントをする上での心構えとして下記の点に注意しています。

  • 自分ができるから、他人もできるはずという考えを捨てること
  • 自分でやったほうが早いと思っても、自身の手を出さないこと
  • チームが持っている力以上のことを求めないこと
  • チームの課題は、チーム全体で取り組むこと

マネージャがやるべきことは、方向性を示すことジャッジメントであり、自分自身で作業をすべきではありません。

一方的に指示を出すのではなく、メンバーの思考停止が起きないように、チームメンバーで決めたことをやらせましょう。

ただし、メンバーの進めようとしてる手段に対して、リスクがある場合には、リスク観点について質問をすることで、リスクの存在と対策をメンバー自身で考えられるようにリードすることも忘れてはいけません。

現在のチーム力を把握し、それをベースにプロジェクトの計画を立て、中長期的にチームの戦力を強化していくことが重要です。

目先の案件に捉われていると、いつまでたっても開発スピードが上がらなかったり、やりたいことができないなど、負のループに陥ってしまいます。

プロジェクトマネジメントにおける仕組み化

プロジェクトマネジメントの手法は色々ありますが、一番重要なのは、タスクの可視化課題の可視化だと思います。

やらなければならないことが全て洗い出せていること、適切な優先順位で作業を行なっていることをマネージャーはチェックしなければなりません。 また、課題を特定しない段階で、手段を考えても課題は解決しませんが、本質的な課題が見つかれば、改善策を考えるのはそんなに難しくありません。

2つの可視化をする上で、私が必ず行なっている仕組みがあります。

デイリーミーティング
  • 昨日やったこと、今日やることをチケット管理ツールをみながらメンバーに共有する
  • 全てのタスクはチケット化する
  • タスクは、担当者、工数、期限を設定する
  • タスクは、1日単位など細かいものに分解する
  • タスクは、完了定義を明確にする
  • 正しい優先順位でタスクに着手しているか確認する
  • 今抱えている課題を共有する
週次KPTミーティング
  • 毎週KPTを実施する
  • プロジェクトメンバーの参加を必須とする
  • 課題(Problem)についてメンバー全員で議論する
  • 本質的な課題にたどり着くまでブレークダウンする
  • Tryはメンバー全員が合意して決定する
  • Tryは1週間でできることを定義する
  • 残った課題は、積み上げないで捨てる

KPTについてわかりやすい記事がありましたので共有します。

この二つのミーティングをやり続けるだけで、プロジェクトは割とうまく回ります。

ただ、意外とこれだけのことをずっと続けるのが難しいのです。

タスクの期限を入れなかったり、完了定義が曖昧だったりというのはよくあります。

期限が不明確でいつまで経っても終わらなかったり、完了の定義が曖昧で、終わったと思っても、実際には終わってなかったりするので、タスクの進捗確認はさらっと終わりがちです。

管理者はタスクが確実に終わるまで、状況を正確に把握し、きっちり管理することが求められます。

KPTでも毎週全員が参加しなかったり、課題をうまくブレークダウンできずに、本質的な課題に辿り着けず、適切な解決手段を打ち出せなかったりということもよくあります。

ファシリテーションをしながら、課題洗い出し方、解決までの思考などの教育も合わせて行っていくのがポイントです。

個の力をチームの力に変えるべく、こういった仕組みを活用しながら、仕組みがうまく回っていることを日々チェックしてマネジメントしてます。

まとめ

マネージャーは、偉い人のポジションという位置付けでは決してありません。

一方で、業務知識に加えて、コミュニケーション能力も求められる難易度の高い仕事ではあります。

性格の合わない人、経歴もバックグラウンドも違う人、カルチャーの違う人など様々な人とお付き合いしていかなければなりません。

マネージャーという役割をしっかり理解し、マネージャに求められるスキルを身につけ、メンバーのパフォーマンスを最大化させるために尽力するのが、プロフェッショナルなマネジメントだと思います。

これからのフォトシンスは、大幅に人を増員していきますが、それに耐えうる組織づくりもしっかりしていきます。 スケールしていく組織の中で、強いチームでの働き方を体感したい方は、是非エントリーしてください。


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

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

目に見えない信号やノイズについて

この記事は Akerun Advent Calendar 2020 - Qiita の24日目の記事です。

初めまして、 hiro_kawa です。2020年3月からPhotosynthの開発部で電気設計を担当しています。今年は久々に一担当としてガッツリ電気設計を行うことが出来て、忙しいながらも楽しい一年だったな~と思います。ホントあっという間でした。

さて、私からは電気設計関連のお話をさせて頂きます。 電気設計で取り扱いが難しく注意が必要なのは、目に見えない信号やノイズです。 電波や磁界、静電気など、種類は様々。静電気の場合はパチンと光って見える事もありますが、その後の電流の流れまでは見えません。 Photosynthに入社してからも、いくつか新しい発見や経験ができたので、その事例をご紹介します。

波形測定の際に

NFCカードリーダーの電源波形をオシロで確認していた時に、大きなリップルノイズが確認されることがありました。ICが正常に動作しない、場合によっては壊れてもおかしくないレベルです。プローブをあたる位置でレベルがかなり変動しており、NFCのアンテナに近い位置だと特に大きくなります。

そこで、その波形の時間軸を拡大してみると13.56MHzの信号を発見、NFCの周波数です。

f:id:photosynth-inc:20201224100704p:plainf:id:photosynth-inc:20201224100713p:plain
オシロスコープよりキャプチャしたNFC波形

その時は手軽に信号をモニターするためにパッシブプローブを使い、写真のようなリードクリップでGND接続をしていました。

f:id:photosynth-inc:20201224100701j:plain:w300
リードクリップ

実はこのGND線のループが悪さをしており、NFCのループアンテナとリード線とのかぶり加減でノイズレベルが変動している事がわかりました。パッシブプローブなのでインピーダンスが高く、プローブ自体がアンテナとなってNFCの電波をひろってしまっていた、という訳です。

画にしてみるとこんな感じでしょうか。

f:id:photosynth-inc:20201224100708p:plain:w300
プローブが作るループを、アンテナの磁界が通る

プローブ線のループが大きいほど、その中を通る磁束が増えるため、NFCの信号を拾いやすいという原理です。

そこで、ループを小さくするため、プローブ付属のバネ接点にGNDを変更すると、ノイズレベルはぐっと低減して、安定した信号を確認することができました。

f:id:photosynth-inc:20201224100710j:plain:h250f:id:photosynth-inc:20201224100717p:plain:h250
バネクリップと改善後の波形

これなら問題ないレベルです(これでも若干ノイズを拾っていますが)。 更にアクティブプローブにすればより正確な波形が確認できるでしょう。

ちょっとした波形確認の時でも気をつけないといけないな、と思いました。

静電試験の際に

カードリーダーの静電試験を行っていた時のこと。

※静電試験の概要については以下サイトなどご参照下さい。

カードリーダーを試験卓と水平に横置きし静電気を印加すると読み取りが出来なくなる症状が発生しました。

一方で実際に設置される環境通り、縦置きでの静電気印加では問題ありません。 しかも横置きでもカード読み取り部を上にし下側から静電気を印加しても問題ありません。 カード読み取り部を下にして上から電極に静電気を印加した時にのみ問題が発生します。

ここでのポイントは、試験サイトのテーブルには鉄板が敷かれていることです。図にしてみるとこんな感じです。

f:id:photosynth-inc:20201224100658p:plain
静電試験の模式図

NFCのループアンテナを挟むような形で静電気を印加した場合に問題が発生しています。 そこでアンテナが静電気の誘導ノイズをひろっているのではないか?と想定しました。 NFCのデバイスとアンテナとの接続部分を分断し、同条件で症状確認してみると、問題は発生しなくなりました。

原因が特定出来たので、そこを対策しようとなる訳です。

アンテナの性能が良い方が、よりカードを読み取りやすい訳ですが、その分ノイズ影響も受けやすくなります。 アンテナ性能を保ちながら、ノイズ影響は受けにくくする工夫が必要で、そこが設計の面白いところですね。

終わりに

上の2例は電気設計者ならわかりやすい内容だと思います。 他の事例では、動作中の基板の写真を撮ろうとフラッシュをたくと誤動作が発生、ということもありました。この原因はどのように推察されますか? これも調べると興味深いことがいくつかわかったのですが、そのお話はまたの機会に。。。

仕事の上では問題は起きないに越したことはないですが、私なんかは問題が起きると少しワクワクします。 というのも、その問題を解決することで新しい発見や勉強になることが多々ありエンジニアとして成長できるからです。

Photosynthでは、そんな経験が出来ると思いますので、ご興味持たれた方がいらっしゃいましたら、是非、ご連絡下さい。 ひょっとすると来年この記事を見た方と一緒にお仕事しているかも知れませんね。 そんな事になったら嬉しいです。では、皆さま、良いお年を!!


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

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

人に優しい識別子を使おう

この記事は Akerun Advent Calendar 2020 - Qiita の22日目の記事です。

こんにちは。@ps-tsh です。フォトシンスでは API Server などバックエンドシステムの開発を担当しています。先日、設計レビュー中に「識別子として UUID を使うのは妥当か」という話でちょっと盛り上がったので、きょうはそのあたりの話をしたいと思います。

連番IDのデメリット

「データの識別子にはどのような値を使うべきか」というテーマに対し、まずはもっとも一般的な連番IDの話から入りましょう。

たとえば Restful な API であれば、id=123 のユーザがいたとして GET /users/123 のようにアクセスします。実装レベルでは id として RDBMS が生成した連番を使うことが多いので、フレームワークのサポートも手厚く開発するのも楽です。しかし、連番のIDは数値を増減させるだけで他のデータにアクセスすることができるため、いわゆる総当たり攻撃を受けやすいというデメリットがあります。また、商用サービスであれば連番の最大値、すなわちデータ量からおおよその事業規模を推測されてしまうリスクもあるといえるでしょう。

ならばランダムIDだ

そこで、上記の問題を解決するためのアイディアとして「IDをランダムに発番する」という方法が出てきます。ID の本来の役割は識別子(identifier)なので、他と重複しないランダムな文字列を生成して使えばいいんじゃないかと考えるわけです。最近のプログラミング言語であれば、なんらかのランダムな値を生成する方法が提供されていると思います。たとえば Ruby では SecureRandom モジュールのメソッドを使うことで簡単にランダムな文字列を生成することができます。

SecureRandom モジュールの利用例

irb(main):001:0> require 'securerandom'
=> true
irb(main):002:0> SecureRandom.uuid # ランダムなUUIDを生成する
=> "5308b22c-2692-4b99-a404-316eaaf3088a"
irb(main):003:0> SecureRandom.base64 # ランダムなBase64文字列を生成する
=> "e/5gLBPoaxntIDeRwZlWPA=="
irb(main):004:0> SecureRandom.random_number(100000000) # 最大100000000のランダムな数値を生成する
=> 35831984

連番のかわりにランダムな値を使用することで、連番IDで課題となっていた総当たり攻撃やデータ量の推測は難しくなります。

ランダムIDのデメリット

連番のIDをランダム値に置き換えることで、エンジニアリング視点では問題が解決されたように見えます。では、ランダムなIDはあらゆる点でパーフェクトな解決策なのでしょうか。もちろんそんなことはなく、ランダムなIDにもデメリットがあります。

デメリット1: 長い

ランダムなIDは登録時の衝突を回避する必要があるので、連番の数値と比べると長い文字列になってしまいます。コードで処理する分には変わりませんが、人間が目視して入力する必要がある環境では、長いID文字列はどうしても使い勝手が悪くなってしまいます。たとえばUUIDを目視一回で覚えて、ミスせずにすべて入力するのはなかなか難しいですよね。

ランダムなIDの例: UUID

irb(main):009:0> SecureRandom.uuid
=> "033f9297-4393-463d-b63d-d6e3b4b05339"

また、長いランダム値はAPI設計にも影響をおよぼします。APIエンドポイントにパラメータとしてIDを含める場合、数値型のidであれば

GET /organizations/123/members/456?option=789

のようになりますが、これがUUIDだと

GET /organizations/648df1f4-03a7-450a-9d0a-352e742a5d57/members/83f50878-3726-4cb2-ab25-6db4f994ccb1?option=fd1b42bb-226c-4242-a667-13b13523147d

のようにとても長い文字列になってしまいます。頻繁に起こるケースではありませんが、パスパラメータやクエリパラメータに長大なIDが多数並ぶとブラウザやWebサーバが許容するURLの上限を超えてしまうこともあるため、IDはできるだけ短くしておいたほうが良いといえるでしょう。

この他、ストレージ効率が悪いとかインデックスサイズが増えて検索パフォーマンスが落ちるといった話もありますが、これらはあくまで副次的な話で、計算機リソースが安い現在においては最初に考えることではないと思います。

デメリット2: 紛らわしい文字が入る

「ランダムIDの文字列長が長くなってしまうなら短くすればいいじゃないか」とうことで、今度は使用する文字を増やして桁数を減らすことを考えてみましょう。たとえば Base64で使用する範囲の文字(0-9A-Za-z, +, /)を用いたランダムIDを生成してみます。

ランダムなIDの例: Base64

irb(main):008:0> SecureRandom.base64(16)
=> "ClnkCQEMel0XeDLYjgI0bg=="

同じ16byteのデータですが、Base64で表現することで文字列としてはUUIDと比べて(36→24と)短くなりました。しかし、文字の種類が増えたことで今度はIDに紛らわしい文字が含まれてしまいます。これも人間が手入力する上で困難なポイントになりえます。紛らわしい文字はITリテラシーの高くない人ほど間違えやすく、そうでなくてもフォントによっては非常に見分けがつきにくいため、クーポンコードや問い合わせ番号のようなものに採用すべきではありません。以下に、数字と紛らわしい英字の例を挙げます。

数字 紛らわしい英字の例
1 I, i, l
2 Z, z
5 S, s
6 b
9 q, g
8 B
0 O, o

また、英字には大文字・小文字で同じ形のものが多いため、文字種を増やしたいからといって混在させることも避けた方がよいでしょう。

デメリット3: 並べ替えられない

これは必ずしもランダムIDのデメリットとして挙げるべきものではないのですが、連番IDに期待されている性質として「並べ替えができる」というものがあります。たとえば「注文履歴を注文番号で並べ替えたら時系列順に並んで欲しい」といった要件がある場合、ランダムIDを採用してしまうと目的を果たすことができなくなります。もちろんこのケースは「注文日時で並べ替える」という方法で解決すべきですが、「IDに大小関係(並べ替え可能性)が期待されていることは多い」と心得ておいて損はないと思います。

人にやさしいIDをつくろう

ここまで、ランダム値ベースのIDについてデメリットをいくつか挙げてみました。ここからは、ランダム値の性質を保持しつつ、かつ人にもやさしいIDを設計するためのポイントについて紹介したいと思います。

ポイント1: 名前空間のサイズを考える

最初に考えるべきことは名前空間のサイズ想定です。IDを短くするためには必要以上に広い名前空間を使わないことがポイントになります。本記事の前半ではランダムIDの例としてUUIDを紹介しましたが、IDとして本当にUUIDが必要なケースははたして多いのでしょうか。UUIDの名前空間は128bitもあります。対象データの件数が数十万〜数千万程度であれば、32bit(正の整数で約21億)もあれば十分に収まります。多少上振れしても64bitあれば十分であることがほとんどです。

ポイント2: 表記上の桁数を減らす

UUID→Base64のところでも少し触れましたが、ID文字列の桁数を減らすためには、使用する文字数を増やす方法が効果的です。以下に示すように、同じ数値を表す場合でも2進数より8進数、10進数、16進数の方が桁が短くなりますよね。数字と英字の組み合わせであれば36進数まで表現できます。Rubyの場合、Numeric#to_s(n)を使えば36進数までの数を文字列に変換してくれます。

同じ数値(12345678)を2, 8, 10, 16, 36進数で出力したもの

irb(main):010:0> 12345678.to_s(2)
=> "101111000110000101001110"
irb(main):011:0> 12345678.to_s(8)
=> "57060516"
irb(main):012:0> 12345678.to_s(10)
=> "12345678"
irb(main):013:0> 12345678.to_s(16)
=> "bc614e"
irb(main):014:0> 12345678.to_s(36)
=> "7clzi"

ポイント3: 文字を増やした上で紛らわしい文字は除外する

上記で紹介した36進数を使うと、紛らわしい文字が含まれてしまいますよね。そこで、以下のように紛らわしい文字を除外したエンコーダを作ってn進数を出力することを考えます。

# 所定の文字セットを使って数値をn進数表記に変換する
def encode(n)
  # 例として数字の0, 1と英大文字のI, O を除外したものを使う。英小文字は使わない
  # 並べ替え可能性を保つため、文字の順番はASCIIコード順を保っておく
  chars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'.split('')
  result = []
  while n > 0 do
    div, mod = n.divmod(chars.length)
    result.unshift(chars[mod])
    n = div
  end
  # 連結して出力
  result.join
end

出力例

irb(main):067:0> encode(12345678)
=> "DSSCG"

ポイント4: 並べ替え可能にする

前述の通りこれはIDとしての必須要件ではありませんが、ランダムIDを並べ替え可能にするためには、先頭に並べ替え可能なパートを付与する必要があります(厳密にはこの時点でもはやランダムではないのですが、そこは置いておきましょう)。以下はタイムスタンプを含めたランダムIDの例です。

# 前半にタイムスタンプのパートが含まれているため並べ替え可能になっている。
# 実際はID文字列を連結する場合「各パートを固定長にする」「タイムスタンプの精度が秒単位でよいか確認する」など
# もうひと手間かける必要がある
irb(main):097:0> encode(Time.now.to_i) + encode(SecureRandom.random_number(100000000))
=> "3HY23UC34C4EF"
irb(main):098:0> encode(Time.now.to_i) + encode(SecureRandom.random_number(100000000))
=> "3HY23UE3AQUGQ"
irb(main):099:0> encode(Time.now.to_i) + encode(SecureRandom.random_number(100000000))
=> "3HY23UF3QBUMH"

ポイント5: 桁区切りを入れる

最後になりますが、人間からみた可読性を上げるテクニックとして、適切な文字数で桁区切りを入れる方法も有効です。たとえば3HY23UF3QBUMH であれば 3HY2-3UF3Q-BUMH のように分割することで入力値の確認がしやすくなると思います。

おわりに

今回は「人に優しい識別子を使おう」ということで、識別子としてランダムIDを採用する場合に考えておきたいことをいくつか紹介しました。id設計においてちょっとでも役立ったと思っていただけたら嬉しいです。


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

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

専用アプリはもう要らない?LINE BotからAkerunを操作する(その2)

この記事は Akerun Advent Calendar 2020 - Qiita の13日目の記事です。

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

ちょうど1年前、「LINE BotからAkerunを操作する(その1)」を書かせていただきました。 LINE Botのグループ機能を使ったBotLINE BOT AWARDS グループトーク部門賞)を作成した経験もあるので、今回はその続きの内容(LINEグループでAkerunを操作する)になります。

akerun.hateblo.jp

17日目の記事(LINE Botから施錠解錠する)も書いたので、こちらも読んでみてください。

akerun.hateblo.jp

事前準備

LINE Botの設定

その1を参考にLINE Botを作成し、Botと友達になります。

Akerun合鍵発行

対象となるAkerunの合鍵を発行します。弊社のAkerun Developersをご参照ください。

developers.akerun.com

LINE Botのリッチメニューからインタラクティブ形式で複数の組織、Akerun、合鍵の設定も可能ですが、長くなってしまうので、別の機会で書きたいと思います。

データ保存用DB

グループ、ユーザー情報を保存する必要があるので、Bot側でデータベースを使う必要があります。Firebase、AWSなど各自の環境に合わせてDBを構築すれば良いと思います。 今回は、軽量ドキュメントデータベースのTinyDBを使いますが、DB周りの詳細処理は割愛します。

LINE Loginチャンネル作成

LINE Developersにログインし(アカウント、Providerが無い場合作成する)、LINE Loginチャンネルを作成します。

  • チャンネル作成

チャンネル作成

  • Channel IDとChannel secret

環境変数に設定する必要があるので、Channel IDとChannel secretをメモします。

Channel ID

Channel secret

  • Callback URLを設定

Botに合わせてドメインとPathを設定します。

Callback URL

LINEのグループ(或いはルーム)を作成

LINEで任意のグループを作成し、Akerunを使うユーザーを招待します。Botと連携してから招待しても問題ありません。

Botをグループに招待する

通常ユーザーの招待と同じ方法で既に友達になったBotをグループに招待します。

  • JoinEvent

Botをグループに追加した場合、JoinEventが発生します。eventからグループID(或いはルームID)を取得し、DBにグループを追加します。

  • LINE Login用Link作成

グループ内のユーザーがBotと友達になるようにLINE LoginのLinkを作成し、グループにメッセージを通知します。 LinkにLINE LoginチャンネルのChannel IDとCallback URLを指定します。 LINE Loginボタン(画像デザイン)について、以下をご参照ください。

developers.line.biz

# BotをGourpに追加した時、/callbackが呼ばれて、その後JointEventが呼ばれる
# 同じグループに複数Bot追加できない。2個目からは招待待ちになる。
@handler.add(JoinEvent)
def handle_join_message(event):
  print(event)
  if event.source.type == 'group':
    gid = event.source.group_id
  elif event.source.type == 'room':
    gid = event.source.room_id
  
  # 既に存在する場合追加されない
  db.add_group(gid, event.source.type)

  msgs=[]

  text = u'下のログインボタンで鍵を申請してください。'
  msgs.append(TextSendMessage(text = text))
  
  link_uri = 'https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id={}&redirect_uri={}&bot_prompt=normal&scope=profile&state={}'.format(line_login_channel_id, urllib.parse.quote(auth_url), gid)

  print(link_uri)
  msgs.append(ImagemapSendMessage(
    base_url=base_url + '/images/LINELogin',
    alt_text='this is an imagemap',
    base_size=BaseSize(height=302, width=1040),
    actions=[
      URIImagemapAction(
        link_uri=link_uri,
        area=ImagemapArea(x=0, y=0, width=1040, height=302)
      ),
    ]
  ))

  line_bot_api.reply_message(event.reply_token, msgs)

LINE Loginでログインし、Botと友達になる

グループ内のユーザーはLINE Loginボタンを押してBotと友達になります。 Botと友達になった場合、LINE Loginチャンネルで設定されたCallback URLにリダイレクトされます。ここでログインユーザー情報を取得し、DB上のグループにユーザーを追加します。LINEグループにもメッセージを通知します。

# グループに入るユーザーにログイン用ボタンLink(clientID/redirectURL)を用意し、ログインしてもらう
# ユーザーがログインボタンを押して、Botと友たちになる時、/authが呼ばれる
# ログイン成功後、/authにcodeが発行される(10min)ので、アクセストークンを取得する
@app.route("/auth", methods=['GET'])
def auth_callback():  
  print(request)
  code = request.args.get('code')
  gid = request.args.get('state')

  # 認証エラー
  if(code is None):
    #print 'Auth error: '
    error = request.args.get('error')
    errorState = request.args.get('state')
    errorMessage = request.args.get('error_description')
    print(error)
    print(errorState)
    print(errorMessage)
    return 'Auth Error'

  # token取得
  token = line_login_get_access_token(code)
  profile = line_login_get_user_profiles(token)
  uid = profile.user_id
  name = profile.display_name

  db.add_user(uid, name)

  msgs = []
  msgs.append(TextSendMessage(text = u'{}さんがグループに入りました'.format(name)))

  line_bot_api.push_message(gid, msgs)

  # ログイン成功時の画面を用意
  return render_template("index.html", title="Akerun Login", message=u"ログイン成功", friend_url=line_friend_url, qr_url=line_qr_url)

Token取得処理は下記通りです。LINE LoginチャンネルのChannel IDとChannel Secretが必要です。

def line_login_get_access_token(code):
  headers = {'Content-Type': 'application/x-www-form-urlencoded'}
  payload = {
    'grant_type': 'authorization_code',
    'client_id': line_login_channel_id,
    'client_secret': line_login_channel_secret,
    'code': code,
    'redirect_uri': auth_url
    }

  obj_request = Request(
          "POST",
          'https://api.line.me/oauth2/v2.1/token',
          headers = headers,
          data = payload
  )

  obj_session = Session()
  obj_prepped = obj_session.prepare_request(obj_request)
  obj_response = obj_session.send(obj_prepped,
                  verify=True,
                  timeout=60
                  )
  print('status_code:' + str(obj_response.status_code))
  print('obj_response.text:' + obj_response.text)
  response_dict = json.loads(obj_response.text)
  print(response_dict)

  #{
  #  "access_token": "xxxx",
  #  "expires_in": 2592000,
  #  "id_token": "xxx",
  #  "refresh_token": "xxx",
  #  "scope": "profile",
  #  "token_type": "Bearer"
  #}

  return response_dict['access_token']

以下でログインユーザーの情報を取得できます。

def line_login_get_user_profiles(token):
  headers = {'Authorization': 'Bearer ' + token}

  obj_request = Request(
          "GET",
          'https://api.line.me/v2/profile',
          headers = headers,
  )
  obj_session = Session()
  obj_prepped = obj_session.prepare_request(obj_request)
  obj_response = obj_session.send(obj_prepped,
                  verify=True,
                  timeout=60
                  )
  print('status_code:' + str(obj_response.status_code))
  # {
  # 'userId': 'xxxx', 
  # 'displayName': 'xxxx', 
  # 'pictureUrl': 'https://profile.line-scdn.net/xxxx'
  # }
  print('obj_response.text:' + obj_response.text)
  response_dict = json.loads(obj_response.text)
  print(response_dict)

  return response_dict

LINEグループでも誰がBotと友達になったか分かります。

ログイン

施錠解錠する

Botと友達になったユーザーは事前に設定されたAkerunに対して施錠解錠操作ができます。DBに施錠解錠時のログを追加すれば、履歴を管理することも可能です。

lock unlock メニュー

施錠解錠処理についてはその1もご参照ください。

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
  reply_msgs = []
  push_msgs = []
  uid = get_id(event.source)

  #profile.display_name #-> 表示名
  #profile.user_id #-> ユーザーID
  #profile.image_url #-> 画像のURL
  #profile.status_message #-> ステータスメッセージ
  profile = line_bot_api.get_profile(event.source.user_id)
  name = profile.display_name
  if event.message.type == 'text':
    if event.message.text[0] == cmd_prefix:
      cmd = event.message.text
      if cmd == cmd_prefix + 'lock':
        job_id = akerun_bot_lock('akerun api token', '組織ID', 'AkerunID')
        reply_msgs.append(TextSendMessage(text=u'施錠'))
        push_msgs.append(TextSendMessage(text = u'{}さんが解錠しました'.format(name)))
      elif cmd == cmd_prefix + 'unlock':
        job_id = akerun_bot_unlock('akerun api token', '組織ID', 'AkerunID')
        reply_msgs.append(TextSendMessage(text=u'解錠'))
        push_msgs.append(TextSendMessage(text = u'{}さんが施錠しました'.format(name)))

  # todo:pooling -> push
  if len(reply_msgs) > 0:
    line_bot_api.reply_message(
      event.reply_token,
      reply_msgs)
  if len(push_msgs) > 0:
    line_bot_api.push_message(test_group_id, push_msgs)

施錠解錠時、LINEグループにもメッセージを通知できるので、誰が操作したが分かります。

施錠解錠

まとめ

  • LINEグループ機能を活用すれば、簡単に複数のユーザーに合鍵を共有し、管理できるので、楽ですね。
  • 追加対応すれば、複数のAkerun、合鍵の設定と管理も全てLINE上で完結できるので、更に便利になります。
  • その1で既にAkerunを施錠解錠するBotを作成したので、グループでの施錠解錠は割と簡単にできました。ただ、DB周りもしっかり作る場合、そこそこ手間がかかりそうです。
  • LINE Bot、グループの制限かもしれませんが、ユーザーは同じBotが所属している複数のグループを同時に使うことができないので、複数のBotを作るなど工夫する必要があります。

そのうちLIFFも試しみたいと思います。


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

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