OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

React NativeアプリでSpotlightを使った検索を実装する

こんにちは、アプリ開発担当のエモトです。みなさま給付金は受給されましたか?僕は、あ、あれ、何かノートパソコンが生えてる。不思議なこともあるもんだな。ちなみ、前回の給付金(リーマンショックを受けて、2009年に施行)では、Realforceを購入して、今それを使って、このブログを書いています。

さて、先日のアップデートで iOS 版オーディオブックアプリは、Spotlight を使ったオーディオブック作品の検索に対応しました。Spotlight とは?アプリではどう使われる?と気になる方は、弊社 note に紹介記事「Spotlightで検索できるようになりました」 が投稿されています。是非ご覧ください。弊ブログでは技術面を少し話したいと思います。

当初、React Native のサードライブラリを利用して、Spotlight の実装をしようを思っていました。しかしながら、我々の想定にあうライブラリが見当たらなかったので、自作することにしました。もちろん、ブログ3回目の登場、Native Module を使います。以前、紹介した Siri Media Intents や Sign in with Apple とは異なり、Spotlight は iOS 8 に登場して機能です。前者と比べると複雑ではないので、比較的簡単に実装できました。

実装

コード全文を紹介すると長くなるので、エッセンシャルを紹介します。Native Modules の基本的な使い方は今回省略しています。

Spotlightの実装

Spotlitght を使用するには、まず最初に、検索対象となる SearchableItem 型の検索データを用意します。React Native での使用を想定しているので、検索データは連想配列(辞書型)で与えられるとしています。

// import Foundation
// import CoreSpotlight
// import Alamofire
// import AlamofireImage

fileprivate class SearchItemKey {
    static let title = "title"
    static let description = "description"
    static let keywords = "keywords"
    static let imageUrl = "imageUrl"
    static let domain = "domain"
    static let id = "id"
}

/// create dictionary to CSSearchableItem
func createSearchableItem(item: [String: Any],
                          completion: @escaping  (CSSearchableItem) -> () ){
  
    var domain = Bundle.main.bundleIdentifier
    if let d = item[SearchItemKey.domain] as? String{
        domain = d
    }
        
    let attr = CSSearchableItemAttributeSet.init(itemContentType: kCIAttributeTypeImage)
    attr.title = item[SearchItemKey.title] as? String
    attr.contentDescription = item[SearchItemKey.description] as? String
    attr.keywords = item[SearchItemKey.keywords] as? [String]
    attr.domainIdentifier = domain
     
    // 画像データがない or 直接画像データがある場合はダウンロード処理は不要です
    // その場合は直接 return item して問題ありません   
    let callCalback = { () in
        let item = CSSearchableItem.init(uniqueIdentifier: item[SearchItemKey.id] as? String,
                                         domainIdentifier: domain,
                                         attributeSet: attr)
        completion(item)
    }
    if let imageUrl = item[SearchItemKey.imageUrl] as? String{
        Alamofire.request(imageUrl, method: .get).responseImage { response in
            if let image = response.result.value{
                attr.thumbnailData = image.pngData()
            }
            callCalback()
        }
    }else{
        callCalback()
    }
}

次に登録を行います。登録は簡単で先ほど生成した CSSearchableItem 配列を登録メソッドに与えます。なお、登録があれば、もちろん削除もあります。興味ある方はアップルの開発ドキュメントをご確認ください。

let searchableItems = [CSSearchableItem]()
let searchableIndex = CSSearchableIndex.default()
searchableIndex.indexSearchableItems(searchableItems) { (error) in
    // 完了やerror処理など
}

iOS と React Native の連携

Spoltlight の準備ができました。次は、その Spoltlight のイベントを React Native に送る方法です。Native Modules の RCTEventEmitter を継承したクラスを用意します。

fileprivate enum SearchOnDeviceListener: String, CaseIterable {
  case onSearchOnDeviceRequest
}

@objc(SearchOnDevice)
class SearchOnDevice: RCTEventEmitter {

    @objc public static let shared = SearchOnDevice()

    override init() {
      super.init()
      for listener in SearchOnDeviceListener.allCases {
        self.addListener(listener.rawValue)
      }
    }
    
    deinit {
      self.removeListeners(Double(SearchOnDeviceListener.allCases.count))
    }
    
    @objc
    override func supportedEvents() -> [String]! {
      return SearchOnDeviceListener.allCases.map {
        $0.rawValue
      }
    }
    
    @objc(handle:)
    static public func handle(_ userActivity: NSUserActivity) -> Bool {
        var identifier: String? = nil
        if userActivity.activityType == CSSearchableItemActionType{
            identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String
        }
        var query: String? = nil
        if userActivity.activityType == CSQueryContinuationActionType {
            // iOS 10以上かつ、plistで設定が必要
            query = userActivity.userInfo?[CSSearchQueryString] as? String
        }
        let obj = SearchOnDevice.shared
        obj.sendEvent(withName: SearchOnDeviceListener.onSearchOnDeviceRequest.rawValue,
                           body: ["id": identifier, "query": query])
    }
}

上記を AppDelegate.m にて Spotlight が実行されると呼ばれるメソッド内で設定します。これにより、実行のたびに、React Native へ検索データが送られます。そのデータを使って、詳細画面や検索画面を開けば OK です。

// AppDelegate.m
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
  return [SearchOnDevice handle:userActivity];
}

まとめ

開発当初、適切なライブラリが見つからないとアクシデントがありましたが、Spotlight 自体の実装コストが低いのもあり、比較的に容易に実装できました(これまでの Siri Media Intents や Sign in with Apple の Native Module 実装の経験が役立ちました)。Spotlight は低い開発コストのうえ、iPhone の検索からユーザーが Safari に行くことなく自アプリへ導入できるという非常に魅力的な機能なので、おすすめです。

最後に、オトバンクではエンジニアを募集中です。日頃は Swift や Kotlin を書かれてる方でも、オーディオブックや、React Native 開発(ネイティブコードも書いてます)にご興味があれば、是非どうぞ。

お待ちしております。

自宅の狭い作業環境を拡張してみた

おひさしぶりです。サーバエンジニアのyukimuraです!

全国で緊急事態宣言が解除されましたね。 まだまだ油断せずに節度ある生活を心がけていこうと思います。

さて、今回の自粛期間中フルリモートとなり会社からリモートワーク一時金をいただきましたので、自宅の作業環境を拡張してみました。
https://note.com/otobank/n/n40323daa3c93

タイトルの通り私の作業スペースは横幅約70cmと結構狭いです。 しかし、狭くても快適な環境を作りたい!ということで拡張の様子を晒していきます👐

拡張前

f:id:yukimura0301:20200526172141j:plain

横幅70cmくらいのPCデスクを置いて作業しています。 本当はもっと横長の机を置いてディスプレイを横に並べたいのですが、 デスク横に扉があるのでこれ以上横長の机は置けないんですよね。 う〜ん狭い。

横に並べられないなら縦に並べれば良いんじゃない?ということで、 今回はモニターアームを買って縦にディスプレイを並べてみることにしました。

拡張後

モニターとモニターアームを購入しました!

縦にモニターを2枚配置しています。(上部のプリンタ台は撤去。プリンタは足下の台に移動) f:id:yukimura0301:20200529125342j:plain

後ろ。モニタの角度を変えられて良い感じです。 f:id:yukimura0301:20200529125412j:plain

うちは扉があるため出来ませんが、ネジを緩めて高さ調整すれば横に2枚配置する事も可能です。
なお、このモニターアーム(FLEXIMOUNTS社製M6SD)を組み立てる際にはモニタを手で支えながらアームのねじ止めをする必要があるため、1人で組み立てを行うのはかなり厳しいです。
(説明書にも安全のため大人2名で組み立てるよう、注意書きがあります。)
モニターアームの購入を考えている方がいたら、誰かに組み立てを手伝ってもらってくださいね。

構成

  • PC: MacBook Pro 13-inch 2019(Apple)
  • PCデスク: HLN-60BKN(サンワサプライ)
  • 既存モニタ: RDT234WLM(MITSUBISHI)
  • 新モニタ: GW2780(BENQ)
  • USBTypeCハブ × 1 : A8335(Anker)
    ※ ハブは無くても良いですが、USB2.0のデバイスを接続するためにあると便利。この機種の場合はmacの電源ケーブルを接続して充電もできます。
  • HDMIケーブル(USBTypeC to HDMI) × 1
  • HDMIケーブル × 1
  • モニタアーム: M6SD(FLEXIMOUNTS)

接続は以下構成です。

  • macbookのTypeCポート --- USBTypeCハブ --- HDMIケーブル --- 新モニタ

  • macbookのTypeCポート --- HDMIケーブル(USBTypeC to HDMI) --- 既存モニタ

感想

今のところ上モニタにブラウザやコンソール、下モニタにエディタ、Macbook本体画面にslackやその他ツールを表示・・という形で使っておりなかなか快適です。
アプリを開いたり非表示にしたりという動作が減って作業が効率的になっている気がしています。

しかし予想していたことではありますが、椅子を高くしないと上の画面を見上げると首が痛くなりますね・・。 椅子を高くした状態で使っていると、手元のmacbookを見ると背中が丸まってしまい 気付くと姿勢が悪くなっているということがあるため良い対処法がないか検討中です😓

おわりに

まだまだ改善点があるため、今後も理想的な作業環境を模索していきたいと思います。 ヘッドセットを買おうと思っているためおすすめがある方は教えていただけると嬉しいです!!

それでは、他のエンジニアの作業環境も見たいな〜と前振りして終わります🙏

React Nativeアプリで Sign in with Apple を実装する

こんにちは、アプリ開発担当のエモトです。待望の映画 SHIROBAKO が地方映画館でも上映される矢先、昨今の事情で休館になり、悲しみに暮れております。しかしながら、首都圏中心だったIT技術系の勉強会がリモート開催されるようになったりと、新しい世界を楽しもうと思った田舎民です。

さて、先日のアップデートでオーディオブックアプリは iOS / Android ともに Sign in with Apple に対応しました。今回は、その話を少ししたいと思います。Webでの話は こちら をご覧ください。

Sign in with Apple

Sign in with Apple は、アップル社が Apple ID を使った認証システムを提供します。ユーザーはパスワード不要で Face ID / Touch ID でアプリに登録やログインできる、サービス提供側は承認済みの Apple ID なのでメール確認不要など、様々な利点のもとで利用することができます。

React Nativeアプリで実装する

弊社サービスは自社の会員登録に加えて、Facebookでのログインをサポートしているので、Sign in with Apple は開発ガイドラインからも対応必須でした。アプリは React Native でクロスプラットフォーム開発を行っていますが、この実装は iOS / Android それぞれ別々に実装をしました。

iOS

こちらのライブラリ invertase/react-native-apple-authentication を使用しました。iOS の SDK をラッパーしているので、一連の認証フェーズは問題なく実装できました。手前味噌ですが、このライブラリのネイティブ部分のコードに不足があったので、私の方でPRを出して、コードを修正しました。

検証中に気づいた点として、シミュレーターでは Sign in with Apple はサポートされてないようなので、実際の認証を確認する場合は実機でデバッグするとよいです。また、当初は iPod touch で実機検証していたのですが、検証のたびに Apple ID のパスワードを入れるのが億劫でして、検証機を Touch ID がある iPhone 8 に入れ替えました。開発時は、利点でもある Touch ID や Face ID がある端末を利用するのがおすすめです。

Android

Android で Sign in with Apple を用いるのは個人的にもレアケースだと思います。弊サービスは iOS / Android / Web と複数のプラットフォームで提供しており、スマホは Android だけどタブレットは iPad などのケース(その反対も)も考えられるので、Android でも実装しました。

アップルが Android 用のネイティブ開発の SDK を用意していないのもあり、開発時点で React Native で利用できるライブラリは見つけることはできませんでした。そこで、直接認証URLにアクセスして、認証を行いました。

こちらのサイト Incorporating Sign in with Apple into Other Platforms / Send the Required Query Parameters を参考にして、https://appleid.apple.com/auth/authorize の認証URLを生成しました。redirect_uri に指定したURLに認証結果が戻ってくるので、それからユーザー情報を取得して、アプリを実装しました。

当たり前ですが、SDK で組んだ iOS に比べて、Android の方が手間がかかりました。

まとめ

実装自体は簡単にできると思います。しかしながら、名前が取得できるタイミングが最初の認証時のみなど、Sign in with Apple の仕様面を自社サービスに組み込むところに苦労しました。通常な使い方では問題ないと思いますが、開発時や、ユーザーがいったんサービスを退会してから再登録する場合など、いろいろ考えて作りました。

Sign in with Apple の既存アプリの対応締め切りが6月末と迫ってきています。まだ、React Native アプリで実装がまだの方はがんばっていきましょう。

最後に、オトバンクではエンジニアを募集中です。オーディオブックや、React Native開発に少しでも興味があれば、是非どうぞ。

お待ちしております。