20210425のiOSに関する記事は10件です。

【iOS】App Store申請の方法

背景 最近初めてのAppStoreへ申請までたどり着き、リリースしたので、手順をまとめてみました。 申請手順 ⓵ 本番用の証明書の発行 ・Xcodeで [Preference] → [Account] で 開発用アカウントでログイン。 ・該当するチームを選択して、[manage certificates]をクリック。 左下の「+」から「Apple Distribution」を選択して証明書を作成。 ・Developer Center にログインして、証明書をダウンロードする。 ⓶プロビジョニングファイルの作成 ・Developer Centerにログイン(⓵と同じアカウント) ・[Profile] → [App Store]を選択して、[Continue] ・該当するApple IDを選択して、[Continue]] ・⓵で作成した、証明書を選択して[Continue] ・[Profile]で作成したプロビジョニングファイルを[Download]しておく。 ⓷Xcodeで作成したプロビジョニングファイルを使用するように設定する ・[Project] → [Targets] → [Build Setting] → [Signing] → [Code Signing Identity]で「IOS Distribution」を選択 ・[Project] → [Targets] → [General] → [Singing (Debug)] の[Provisioning file]から[Download Profile]を選択。⓶で作成したプロビジョニングファイルを選択する。 ⓸アーカイブする ・Xcodeのメニューバーから、[Product] → [Archive] ・恐らく「codesignは、キーチェーンに含まれるキー"xxxx"へアクセスしようとしています。許可するには、キーチェーン"ログイン"へのパスワードを入力してください」というダイアログが出るのでパスワードを入力して、[常に許可]をクリックする。 ・Organizer画面が表示される。 ⓹リリース用のビルドが申請上問題がないかをチェック ・Organizer画面にて[Validate App]をクリック ・App Store Connect distribution options画面で、各ボタンのラジオボタンを選択して、[Next]をクリック。 ・Select certificate and iOS App Store profiles画面にて、「Distribution certificate」で⓵で作成した証明書を選択する。 ・「Select Profile」で⓶で作成したプロビジョニングファイルを選択する。 ・ipaファイルが作成されるので、[Validate]をクリックすると検証が始まる。 ・OrganizerのTOP画面にてアプリの「status」が「Validated」になればOK ⓺アプリをAppStoreにアップロード ・[Distribute App]をクリック。 ・Select a metod of distributionにて、「App Store Conect」を選択して、[Next]をクリック ・Select a distinationにて、「Upload」を選択して[Next]をクリック ・App Store Connect distribution optionsにて、各項目を選択して[Next]をクリック ・Select certificate and iOS App Store profiles画面にて、「Distribution certificate」で⓵で作成した証明書を選択する。 ・Select certificate and iOS App Store profiles画面にて、「Select Profile」で⓶で作成したプロビジョニングファイルを選択する。 ・ipaファイルが作成されるので、[Upload]をクリック ⑦AppStore Connectで必要な情報を入力する。 ・⓺が完了すると「App Store Connect: Version 1.0.6 (1) for アプリ名 has completed processing.」のようなメールが送信される。 ・メールが来たら「App Store Connect」を開いて、iOS Appの隣の[ + ]ボタンをクリックして、今回のバージョン番号を入力する。 ・以下各項目に必要な情報を記載して[提出]をクリック。(※今回は審査後に手動でリリースを選択しました。) ○アプリ名 ○サブタイトル ○カテゴリ ○アプリの概要文 ○プロモーション用テキスト ○アプリの価格 ○アプリのレーティング ○検索のためのキーワード ○サポートURL ○プライバシーポリシーURL ○Appのプライバシー ○App Store用のアイコン ○スクリーンショット ○連絡用のメールアドレス、電話番号 ○アップデート内容を説明する文 ○輸出コンプライアンスの設定 ⑧審査完了後リリース ・審査完了後に「now "Pending Developer Release"」がタイトルに入っているメールが送信される。(※1日で審査は完了しました。) ・上記メールが送信されたら、AppStoreConnectにログインして、リリースをクリック。 ・暫くしたらAppStoreに反映されます。 まとめ 今回は初めて、iOSアプリの開発〜リリースまでを経験しました。 大変でしたが、色々と勉強になったので良しとします!初めてリリースする方等の助けになればと思います! 参考: ○輸出コンプライアスについて https://qiita.com/osamu1203/items/35df25d93e9d12b11222 ○AppStore申請の一連の流れ https://zenn.dev/moutend/articles/feebf0120dce6e6426fa https://qiita.com/koji4104/items/5f4d717cba6e5223b552
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ローカルのUSDZファイルをSCNNodeとして読み込む

1回sceneにしてrootNodeを取得する。 let localURL = Bundle.main.url(forResource: "object", withExtension: "usdz", subdirectory: "art.scnassets")! let scene = try! SCNScene(url: localURL, options: [.checkConsistency: true]) let objectNode = scene.rootNode ? フリーランスエンジニアです。 お仕事のご相談こちらまで rockyshikoku@gmail.com Core MLを使ったアプリを作っています。 機械学習関連の情報を発信しています。 Twitter Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iPhone]アクティブなアプリから別アプリを起動する[Flutter]

概要 アクティブなアプリ(画面上に表示している)から別アプリを起動する方法をメモしています。 実装 呼び出される側(別アプリ) info.plistに以下の内容を追加する。 ・URL types > URL identifier > 「設定したい文字列」(下記の例だとjp.sample.test.app) ・URL types > URL Schemes > 「設定したい文字列」(sample-app) 上記の方法は「Custom URL Scheme」と呼ばれています。iOSには他に「Universal Links」というhttpから始まるリンクを設定することもできます。 呼び出し側(アクティブ) 呼び出す側のアプリは以下の様に「launch」メソッドに先ほどinfo.plistに設定した文字列を渡す事で呼び出せます。 この「launch」はpubspec.yamlに「url_launcher: ^6.0.3」(バージョンは任意)を設定し、flutter pub getにより事前にプラグインをDLしとかないといけません。 import 'package:url_launcher/url_launcher.dart'; _lancherCameraApp() { return launch("sample-app://jp.sample.test.app"); }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ios概論1: View

はじめに 都度更新します。 間違っていたらご指摘していただけると嬉しいです。 文字ばっかりで退屈になってしまったらすみません。 構成は以下になります。 1.View 2.Draw 3.Animation 4.Touch Viewの根幹となる描画機能を3章まで説明し、最後にタッチ機能を説明します。 1.View 全体観 UIViewとはなんでしょうか? ドキュメントを見てみると、たくさんのクラスやプロトコルから構成されているのがわかります。 これらの機能を取りまとめて管理するクラスがUIViewなのですが、 このうち根幹的な機能を担っているのは2番目のCALayerDelegateです。 つまり描画機能です。 画面に表示されるというのがViewとしての根幹機能であることは共感していただけるかと思います。 UIViewのほとんどメンバ変数メンバ関数が、どこに何を描画するのかに関するものでもあります。 この章に限らず、この記事にわたって描画周りの仕組みを主に説明致します。 描画 画面の表示は全て描画によるものです。 ざっくり説明すると、UIViewは描画キャンバスを1枚持っていて、frameやbackgroundcolorなどのパラメータをもとにキャンバスを描画し、画面に表示させます。 UITextViewのtextやUIImageViewのimageなども描画パラメータの一種と考えることができるかと思います。 また、このキャンバスは実はCALayerなのですが、後でまた説明します。 親viewから順番に上書き描画されていきます。subviewsは若い順です。 考えれば当たり前ですが、描画位置はbound(自分座標系)ではなくframe(親座標系)を参照して描画しています。 draw()をoverrideすれば、追加でキャンバスに描きこめます。 layout 1.Viewで主に説明したいのはこの章です。 少し長くなりますがご容赦ください。 基本 方法は3種類あります。 1.frame直指定 2.AutoResizing 3.AutoLayout 結論から言うと、3.AutoLayoutを普段使いにした方がいいでしょう。 理由と合わせて各layoutの説明します。 1.frame直指定 frameの値(+回転があるなら.transform)を参考にViewの描画位置が決められるのですが、frame(x/y/w/h)を直に設定します。 2と3は制約によってframeの値を指定する方法と言えます。端末サイズや親viewのframeサイズの変化にも動的に対応できるので便利です。 2.AutoResizing 親viewに対する辺の制約と自身のサイズ(w/h)を設定できます。 子view同士の制約はできないのでちょっと不便です。 AutoLayoutがなかった時代は主流だったようです。 3.AutoLayout AutoResizingでは設定できなかった種類の制約も付けれるようになった制約です。 AutoResizingを補完するため(?)っぽい感じで後から生まれた制約です。 参考書ではAutoLayout推奨でした。 例えば、leadingAnchorなど設定するやつですね。 注意すべき点は、frameやAutoResizing、とAutoLayoutを併用すると、 frameやAutoResizingの設定が内部で自動的にAutoLayoutに変換されることです。 結果、生のAutoLayoutとコンフリクトを起こす可能性があります。し、設定が実現すればいいやと言うスタンスで変換されるので、どんなAutoLayoutが出来上がってくるか予測がつきません。 この自動変換を止めるには、 a.swift translatesAutoresizingMaskIntoConstraints = false に設定します。(よく見かけますよね。) ただ、この設定でlayoutを併用したところで、frameは制約によって書き変わりますし、AutoResizingでできることはAutoLayoutでもできるので、併用よりはAutoLayout一本の方が更なる混乱を避けられるように個人的には思います。 これが先程の理由です。 wrap_contentは実現できる androidには、wrap_contentやmatch_parentといった便利な制約ショートカットが用意されていますが、iosにはないです。 でも、ショートカットが用意されてないだけで、iosでもほぼ任意の制約を作ることが可能です。 制約にはpriorityというパラメータがあります。 制約がバッティングした時、priorityが高い方が優先されます。 例えばwrap_contentで考えると、 親Viewのサイズを規定する制約のpriorityが低かったり、そもそも制約がなかったりする状態で、 子viewの制約が親viewに紐づいていると、子viewの制約が優先されて親viewが縮んだりします。 4辺全てにこれを適用するとwrap_contentが実現可能です。 intrinsic content size 特定のviewクラスはintrinsicContentSize(本質的なサイズ)というメンバ変数を持っています。 例えば、UITextView文字列の大きさ、UIImageViewのimageの大きさなどがその本質的なサイズにあたります。 実は、デフォルトではこのサイズでviewが表示されるように制約がついています。 デフォルトのpriorityが低いのですが、以下のメソッドで値の確認と上書きが可能です。 contentHuggingPriority(for:) contentCompressionResistancePriority(for:) setContentCompressionResistancePriority(_:for:) setContentHuggingPriority(for:) その他 ・xibfileからでもcodeとほぼ同等のlayoutが実装可能です。 (唯一UILayoutGuideだけxibからは使用不可だそうです。これは制約用viewのようなオブジェクトで、描画を行いません。スペーサーとして無色のviewを入れるしかないときに代用すると処理量を減らせます。xibfileでは無色のviewを使うしかなさそうです。) ・xibfileからでも端末サイズごとに制約や色、文字列などのパラメータを設定可能です。 2.Draw ※準備中 登場人物 context layer 描画方法 手順 いくつかの手法 描画しない方法 clear/clip/mask 3.Animation ※準備中 5.Touch この章では、UIViewの機能の中で、描画の次にたくさん触れるであろうタッチ機能を説明します。 タップ情報取得までの流れ ざっくりいうと、以下の流れでタップ情報がViewまで届きます。 デバイスがタップ情報を検知 ↓ applicationに渡される ↓ 該当のViewに渡される 以下もう少し詳細を説明します。 UITouchとUIEvent UITouch 指一つあたりに一つインスタンスが生成されます。 その指が離れるまで同じインスタンスが保持され、離れるとデリートされます。 タップに関する情報を持っています。(タップ座標、開始時のview、今タップ中のviewなど) UIEvent 全てのUITouchインスタンスを保持します。 UITouchの数が0->1になった時にインスタンスが生成され、1->0になった時にインスタンスがデリートします。 UITouchの状態 UITouchは、状態を格納するphaseパラメータを持ちます。 主に以下の値を取ります。 .began: タッチ開始 .moved: タッチが移動 .stationary: タッチがその場にとどまる。(変化なし) .ended: タッチ終了 .canceled: タッチ中断 UIViewがタップ情報を受け取る UIEventを受け取って処理するためのUIResponderというクラスが存在します。 最初の写真からもわかりますが、UIViewはUIResponderを継承しています。 どれか一つでもUITouchインスタンスのphaseの値が変化すると、UIEventインスタンスが該当のview(phaseが変化したUITouch座標のview)に送られます。 UIResponderは、UITouchのphaseに対応した検知メソッドを持ちます。変化したphaseに対応する検知メソッドが一つだけ呼ばれます。 touchesBegan(Set<UITouch>, with: UIEvent?) touchesMoved(Set<UITouch>, with: UIEvent?) touchesEnded(Set<UITouch>, with: UIEvent?) touchesCancelled(Set<UITouch>, with: UIEvent?) タップ検知 方法1:生のタップ情報を使う(UIResponder) UIResponderの上記検知メソッドを使用します。 自由度が高く、複雑なタップ検知を設定可能ですが、その反面簡単なタップ検知も設定が難しくなりがちです。 可能なら、UIGestureRecognizerのテンプレートを使った方が楽で良いかと思います。 3タップ後にドラッグして再度タップ、などの特殊なタップ検知などで必要になります。 以前簡単な記事を書きました。 方法2:UIGestureRecognizerを使う。 実は、viewが受け取ったタップ情報は、viewのもつUIGestureRecognizerインスタンスにも送られます。 ViewはgestureRecognizersパラメータにインスタンスを格納しています。 UIGestureRecognizerはUIResponderではないのですが、UIResponderの検知メソッドとほぼ同じメソッドを持ち、UIRecognizerと同様の働きをします。(どうしてこの二つを別々のクラスにしたのか不思議ですが、、Androidも同様な仕組みが存在した気がします。) https://developer.apple.com/documentation/uikit/uigesturerecognizer テンプレート UIGestureRecognizerは豊富なテンプレートを持っています。 ワンタップ、ドラッグ、ピンチ、回転、ダブルタップ、、、 などなど典型的なタップ検知は大概用意してくれています。大概この範囲で済みそうですね。 こちらも以前簡単な記事を書きました。 参考文献 0.ほぼこれです。 1. 2. 3.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSの共有機能で自作アプリを起動したいときはShare ExtensionではなくSiriKitショートカットを使うべきだった話

やりたいこと:他のアプリから自分のアプリを起動したい iOSで自作のアプリに対して、他のアプリから文字列や画像などを受け取りつつ自作アプリを起動したいと思うことがあると思います。例えばSafariでWebサイトを見ている時に文字列や画像を選択して「共有」ボタンを押すと以下のような画面(共有シート)が立ち上がります。 ここで「メッセージ」や「Twitter」のアイコンを押して友達に送ったりすることがよくあると思います。 「メッセージ」や「Twitter」ではなく、自分のアプリが立ち上げられたら良いのに、と思ってこの仕組みを調べてみました。この仕組みはShare ExtensionというApp Extensionの一種です。確かに他のアプリから「共有」ボタン経由で自作のアプリの処理を行うことができるのですが、実はここで立ち上げられるのはあくまでもExtensionの画面になります。「メッセージ」にしても「Twitter」にしても共有ボタンから立ち上がるのは投稿するだけの専用画面で、「メッセージ」や「Twitter」のアプリ本体が起動するわけではありません。 なので、画面1枚を新たに作ってそれで済むような起動の仕方であればShare Extensionを使えば良いですが、自作アプリ本体を起動しようと思ったらこれではうまくいきません。 ここで今回やりたいことを整理しておきます。 他のアプリの共有シートから自分のアプリを起動したい それも共有シート用の簡易画面ではなくアプリ本体を起動したい その際に、他のアプリ側からテキストや画像を受け取りたい 要は他のアプリで表示されているテキストや画像を自分のアプリに取り込んで処理したいということです。他のアプリというのは例えばSafari、Twitterなどを想定しています。 試行錯誤 Share Extensionから何とかして自分のアプリを起動する 次のアイディアとして、Share Extensionからユニバーサルリンクや(既に非推奨になっていますが)カスタムURLスキームを使って自分のアプリを起動すれば良いのではないかということを思いつきます。 ところが、URLを開くための関数であるopen(_:completionHandler:)の説明を見ると、残念なことにTodayとiMessageのExtensionからしかopen(_:completionHandler:)を使用することができません。 ネット上を検索すると、それでも何とかして起動するためのハックが見つかりますが、裏技っぽいやり方でいつAppleに止められてもおかしくないような方法に見えます(試してみたところうまく動かなかったので、既にダメなのかもしれません) Action Extensionを使う 先ほどの画像でアプリアイコンが並んでいるところがShare Extensionの部分ですが、その下の「リーディングリストに追加」とかの部分はAction Extensionという仕組みで拡張可能です。 ただ、これも結局はShare Extensionと同じ制約があって、自分のアプリ本体を起動することはできません。 解決策:ショートカットを使う ショートカットとは ショートカットというのはiOS標準(おそらくiOS13から?)でインストールされているアプリです。 自分でアクションを組み合わせて、ショートカットとして登録しておき、ワンタッチで呼び出すことができます。 4枚スクリーンショットを貼りましたが、ショートカット作成の+ボタンを押してから、App Storeアプリが提供している機能をショートカット内の1アクションとして選択するフローを表しています。2枚目でApp以外にスクリプティングというのもあるように、アプリの機能だけでなく条件分岐やループなども作ることができます。 このように、ショートカットというのは各アプリが提供している機能をスクリプト的に組み合わせて簡単に実行する仕組みです。 さらに、ショートカットの以下の設定画面を見るとわかるように、各アプリの共有シートに作成したショートカットを登録することができるのです。さらに、共有シートが共有対象としているコンテンツのタイプ(文字列とか画像とか)も選ぶことができます。 今回、文字列や画像を受け取りたいので共有シートタイプとして「テキスト」や「イメージ」を選ぶと実現できます。 自作アプリからショートカットに機能を提供する方法 では、どうやったら先ほどのApp Storeのように、アプリの機能を提供できるのでしょうか? その仕組みがSiriKit ショートカットです。(どう検索したら良いかがわからなかったので、これにたどり着くまで結構苦労しました) ここまでわかれば、あとは公式のリファレンスなり、実際に作った人のQiitaの記事なりを読んで実装すれば簡単、といけば良いのですが、意外とここからも大変でした。 SiriKitはその名前からわかる通り、基本的にはSiriを使った音声での呼びかけに対してアプリで応答するためのSDKですが、その枠組みでショートカット向けに機能提供することも可能になっています。世間の情報は割と音声寄りのものが多かったので、ショートカットを作るのに必要な部分が何なのか、今回自分がやりたいことを実現するためにどの範囲までやれば良いのかがなかなか読み取るのが難しい状況でした。そこで、今回やった内容をメモとして記録に残しておくことにしました。 具体的な作り方 SiriKitの公式ドキュメントが参考になるはずなのですが、今回使い方が若干特殊なのか、実際にやった内容は以下の記事の部分のみです。 SiriショートカットとショートカットAppによるユーザー操作の追加 application(_:continue:restorationHandler:) Siriで音声応答するわけでもなく、共有シート用の別画面を用意するわけでもなく、直接本体アプリで文字列やファイルを受け取るので、Intents App Extensionもそれに関わるハンドル等の操作も不要でしたし、SiriKitの使用許可とかエンタイトルメントも不要でした。後で記述するように、ショートカットのドネートとかも行わず、ドメインとかボキャブラリとかも一切無視で大丈夫でした。 Intent definitionを作成する まずはSiriショートカットとショートカットAppによるユーザー操作の追加に記載されているIntent Definition Fileを作成します。今回はテキストをアプリに取り込んで起動する機能と、画像をアプリに取り込んで起動する機能の2つを作りたかったので、Intentを2つ作成しました。パラメータはそれぞれString型、File型の1つずつを定義しました。重要なポイントとして、以下の設定を行いました。 ショートカットに関係の深そうな以下の設定を行いました Custom intentセクションのIntent is user-configurable in the Shortcuts ap and Add to Siriをチェック ParametersセクションのUser can edit value in Shortcuts, widgets, and Add to Siriをチェック Shortcuts appのInput parameter, Key parameterは同じ値を設定しました(Intent1つにつき1つずつしかパラメータがなかったので) Siriからの操作を行う想定はないので、Siriにしか関係なさそうな設定は外しました Custom intentセクションのIntent is eligible for Siri Suggestionsのチェックを外す ParametersセクションのSiri can ask for value when runのチェックを外す また、結果を受け取る想定はなかったので、Responseはデフォルトのままとしました。 さて、公式ドキュメントではこの後ショートカットをドネートする手順が書かれていますが、これは何でしょうか?正確なところはよくわかりませんが、現時点の筆者の理解の仕方としては先ほど作ったIntentがオブジェクト指向でいうところのクラスのようなもので、ドネートはインスタンスを作成してOSに登録するような行為のようです。 ただ、今回の場合は文字列型とファイル型の1個ずつあれば良いだけで、それはIntent作成時に既にできているようだったので、ドネートの記事に記載されていることは特に行いませんでした。自分のアプリのこの画面に対してショートカットを作ったり削除したりしたいとか、動的に登録・削除をしたい場合はドネートという操作を行う必要があるようです。今回は固定で2個だったのでドネートは行いません。(Intent定義時点で暗黙に1個ドネートされているとかいう解釈なのかもしれませんが) 受け取った後の処理を記述する Intent Definition Fileはビルドすると内部的にクラスファイルができるようで、定義したIntentのクラスが使えるようになります。これを利用して、application(_:continue:restorationHandler:)の説明を見ながらアプリがテキストや画像を受け取ってからの処理を記述しました。 func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { if #available(iOS 13.0, *) { if userActivity.activityType == String(describing: OpenBoardImageIntent.self) { // 画像を取得 guard let intent = userActivity.interaction?.intent as? OpenBoardImageIntent else { return false } guard let data = intent.image?.data, let img = UIImage(data: data) else { return false } // 変数imgにUIImageが入ったので以下必要な処理を行う // : // : return true } } return false } アプリはSceneDelegateではなく、ApplicationDelegateを使用していたので上記関数内に実装しました。上記コードでOpenBoardImageIntentというのが定義したIntentの名前で、imageというのがFileパラメータになっています。 [application(_:continue:restorationHandler:)]にはユニバーサルリンク等からも呼び出されるので、まずuserActivityTypeで所定のIntentによる呼び出しかどうかを判断し、該当する場合はパラメータを取り出して以下必要な処理を行なっています。 上記はパラメータが画像だったのでちょっと複雑ですが、テキストの場合はそのままString型で取り出して処理できます。 ショートカットアプリでスクリプトを作成する アプリ側の対応が終わったので、ショートカットアプリでスクリプトを作成します。 他のアプリで画像を選択した状態で共有シートを開いたら、作成したショートカットが表示されて、それを選ぶと自分のアプリに画像が飛ぶ、というイメージです。 ショートカットの詳細定義 共有シートに表示をオンにして共有シートに表示されるようにします。今回、画像を受け取るためのショートカットについては共有シートタイプをイメージ+URLにしました。URLも入れたのは、ブラウザで画像自体がリンクになっていてその先にオリジナルの画像があるようなケースではURLを入れておかないと取れなかったからです。 スクリプトの作成 基本的には受け取った画像をアプリに渡すだけなのですが、画像が渡ってこなかったときのフォールバックとして条件分岐をしています。イメージの個数(項目数)を数えて0より大きいかどうかを判断している部分がそれですが、1個以上渡ってきた場合は先ほど作成したIntentを呼んでアプリに渡すようにしています。 渡ってこなかった場合は写真アプリ内の写真を選択させて、それをアプリに渡すようにしました。こうしておくことでショートカットアプリ内で直接実行することもできます。 作ったショートカットを配布する ショートカットアプリ内にはギャラリーというものがあって、既製のショートカットが配布されているようなので、AppStoreみたいに作ったショートカットを登録する仕組みがあるのかと思いましたが、どうもそれはないようでした。 ショートカットを他人に配布するためには、ショートカット自体を共有することで、セットアップするためのURLを生成することができます。 受け取る側の注意事項として、設定アプリで「信頼されていないショートカットを許可」というちょっとセキュリティ的に不安になる設定をしないとURLをタップしてもダウンロードできません。 さらに、この設定をオンにするためには、一度でもショートカットを使ったことがないといけないので、何かショートカットを実行してもらう必要があります。 ハマったこと Intent Definition Fileでは、Storyboard等と同様に、stringsファイルによって他言語へのローカライズができるのですが、Intentを2つ作成したときに一部の文言の翻訳が行われなくなるという現象が発生しました。 これはその部分だけを切り出したアプリでも再現したので再現性はあると思うのですが、何故かしばらく(数日間)放置しておいたら直るという謎の直り方をしました。放置していた間は本当に何もしていないので、何故直ったのかは不明です。もし同じ状況に陥った場合は放置することで直るかもしれません(?) 最後に 最初の方にも書いた通り、やりたかったことは共有シートから自分のアプリを呼び出したいというだけなのですが、なかなか良い方法が見つからず、今回のショートカットの方法にしても一般的な使い方ではないのか情報が見つからずに苦労しました。折角なので、できたやり方をアウトプットしておきます。 ただ、もしかしたらもっと良いやり方があるのかもしれないので、ご存知の方がいらっしゃいましたら教えていただけると助かります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

床や壁を利用して、ARをより現実っぽく【平面検出】

ARPlaneDetection【平面検出】 平面検出で床や壁を利用して、オブジェクトを配置できます。 1、平面検出設定でARセッションを開始 ARSCNViewから平面のトラッキング結果を受け取るために、ARSCNViewDelegateを継承します。 class ViewController: UIViewController, ARSCNViewDelegate { override func viewDidLoad() { super.viewDidLoad() sceneView.delegate = self } } Plane Detection【平面検出設定】のオプションをつけてARWorldTrackingConfiguration【世界検出構成】でARKitのセッションを開始します。 ARWorldTrackingConfiguration.PlaneDetection.horizontalを設定すると地面や床やテーブル面など水平面を検出します。 .verticalを設定すると、壁などの垂直面を検出します。 .horizontalと.verticalを同時に検出することもできます。 override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) let configuration = ARWorldTrackingConfiguration() configuration.planeDetection = [.horizontal, .vertical] sceneView.session.run(configuration) } 2、検出結果を受け取る アンカーを検出したら情報を得る ARSCNViewのデリゲートメソッドで平面検出結果を取得します。 // 新しいアンカーがみつかったときに呼ばれるデリゲートメソッド func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor else { return } print(planeAnchor) } 床を検出してみた結果 平面アンカーのid、向き(horizontal)、平面アンカーの中心、範囲 (1.4m,1.1m)を取得しました。 ARPlaneAnchor: 0x1165ab6c0 identifier="C843F11D-80C0-41DB-B7F5-492BA12B52FD" transform= alignment=horizontal center=(-0.011180 0.000000 -0.055902) extent=(1.498166 0.000000 1.185117) わかりやすいようにPlane geometryを持ったSCNNodeを平面に自動追加された空のSCNNodeにaddChildしてみます。 var visualizedPlaneNode = SCNNode() // 平面視覚化用のノードをつくる func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor else { return } print(planeAnchor) let planeGeometry = SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z)) // アンカーと同じ大きさのPlaneGeometryをつくる visualizedPlaneNode.geometry = planeGeometry // ノードのgeometryにPlaneを設定 planeGeometry.firstMaterial?.diffuse.contents = UIColor.blue.withAlphaComponent(0.7) // 青色にしてみる visualizedPlaneNode.simdPosition = planeAnchor.center // アンカーの中心に配置 visualizedPlaneNode.eulerAngles.x = -.pi / 2 // SCNPlaneはデフォルトでは垂直なため、回転させます。 node.addChildNode(visualizedPlaneNode) // 平面に自動追加された空ノードに入れる } 平面情報をアップデートしていく 別のレンダラーメソッドで平面情報をアップデートしていきます。 func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor, // 更新された平面アンカーを取得 let visualizedPlaneNode = node.childNodes.first, // 自動追加された空のノードの子ノードを確認 let planeGeometry = visualizedPlaneNode.geometry as? SCNPlane else { return } // 子ノードのplanegeometryを確認 planeGeometry.width = CGFloat(planeAnchor.extent.x) // 幅を更新された平面アンカーの幅に planeGeometry.height = CGFloat(planeAnchor.extent.z) // 高さを更新された平面アンカーの高さに visualizedPlaneNode.simdPosition = planeAnchor.center // 中心を更新された平面アンカーの中心に } ちなみに、組み込まれている検出された平面アンカーそのものの正方形ではないgeometryの形を取ることもできます。 func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor, let meshGeometry = ARSCNPlaneGeometry(device: sceneView.device!) else { fatalError("Can't create plane geometry") } meshGeometry.update(from: planeAnchor.geometry) meshNode = SCNNode(geometry: meshGeometry) meshNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red.withAlphaComponent(0.7) node.addChildNode(meshNode) } func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor if let meshGeometry = meshNode.geometry as? ARSCNPlaneGeometry { meshGeometry.update(from: planeAnchor.geometry) } } 3、検出した平面の利用 壁紙を貼ってみる planeGeometry.firstMaterial?.diffuse.contents = UIImage(named:"flowerWallPaper") オブジェクトをおいてみる 画面をタップした先に平面geometry(ここではARPlaneGeometryをとってます)があれば、オブジェクトのy座標を平面ノードに合わせて、x,z軸を平面とタップの交差点に合わせます。 @objc func sceneViewTapped(recognizer:UITapGestureRecognizer) { let location = recognizer.location(in: sceneView) let hitResults = sceneView.hitTest(location, options: [:]) if !hitResults.isEmpty { guard let planeNode = hitResults.first?.node else {return} let interiorHeight = interior.boundingBox.max.y-interior.boundingBox.min.y // オブジェクトノードの高さを計算 let tappedCoodinates = hitResults.first?.localCoordinates // タップと平面ノードの交差点 interior.position = SCNVector3(tappedCoodinates!.x, interiorHeight/2, tappedCoodinates!.z) // オブジェクトの原点はオブジェクトの中心にしたので、高さの半分だけ上に置く planeNode.addChildNode(interior) } 壁でオブジェクトを遮蔽する .verticalの子ノードを手前にレンダリングされるようにすることで、後ろのノードが壁に隠れている表現ができます。 if planeAnchor.alignment == .vertical { planeNode.renderingOrder = -1 planeNode.geometry!.firstMaterial!.colorBufferWriteMask = [] } ? フリーランスエンジニアです。 お仕事のご相談こちらまで rockyshikoku@gmail.com Core MLを使ったアプリを作っています。 機械学習関連の情報を発信しています。 Twitter Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【平面検出】で、ARをより現実っぽく

ARPlaneDetection【平面検出】 平面検出で床や壁を利用して、オブジェクトを配置できます。 1、平面検出設定でARセッションを開始 ARSCNViewから平面のトラッキング結果を受け取るために、ARSCNViewDelegateを継承します。 class ViewController: UIViewController, ARSCNViewDelegate { override func viewDidLoad() { super.viewDidLoad() sceneView.delegate = self } } Plane Detection【平面検出設定】のオプションをつけてARWorldTrackingConfiguration【世界検出構成】でARKitのセッションを開始します。 ARWorldTrackingConfiguration.PlaneDetection.horizontalを設定すると地面や床やテーブル面など水平面を検出します。 .verticalを設定すると、壁などの垂直面を検出します。 .horizontalと.verticalを同時に検出することもできます。 override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) let configuration = ARWorldTrackingConfiguration() configuration.planeDetection = [.horizontal, .vertical] sceneView.session.run(configuration) } 2、検出結果を受け取る アンカーを検出したら情報を得る ARSCNViewのデリゲートメソッドで平面検出結果を取得します。 // 新しいアンカーがみつかったときに呼ばれるデリゲートメソッド func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor else { return } print(planeAnchor) } 床を検出してみた結果 平面アンカーのid、向き(horizontal)、平面アンカーの中心、範囲 (1.4m,1.1m)を取得しました。 ARPlaneAnchor: 0x1165ab6c0 identifier="C843F11D-80C0-41DB-B7F5-492BA12B52FD" transform= alignment=horizontal center=(-0.011180 0.000000 -0.055902) extent=(1.498166 0.000000 1.185117) わかりやすいようにPlane geometryを持ったSCNNodeを平面に自動追加された空のSCNNodeにaddChildしてみます。 var visualizedPlaneNode = SCNNode() // 平面視覚化用のノードをつくる func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor else { return } print(planeAnchor) let planeGeometry = SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z)) // アンカーと同じ大きさのPlaneGeometryをつくる visualizedPlaneNode.geometry = planeGeometry // ノードのgeometryにPlaneを設定 planeGeometry.firstMaterial?.diffuse.contents = UIColor.blue.withAlphaComponent(0.7) // 青色にしてみる visualizedPlaneNode.simdPosition = planeAnchor.center // アンカーの中心に配置 visualizedPlaneNode.eulerAngles.x = -.pi / 2 // SCNPlaneはデフォルトでは垂直なため、回転させます。 node.addChildNode(visualizedPlaneNode) // 平面に自動追加された空ノードに入れる } 平面情報をアップデートしていく 別のレンダラーメソッドで平面情報をアップデートしていきます。 func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor, // 更新された平面アンカーを取得 let visualizedPlaneNode = node.childNodes.first, // 自動追加された空のノードの子ノードを確認 let planeGeometry = visualizedPlaneNode.geometry as? SCNPlane else { return } // 子ノードのplanegeometryを確認 planeGeometry.width = CGFloat(planeAnchor.extent.x) // 幅を更新された平面アンカーの幅に planeGeometry.height = CGFloat(planeAnchor.extent.z) // 高さを更新された平面アンカーの高さに visualizedPlaneNode.simdPosition = planeAnchor.center // 中心を更新された平面アンカーの中心に } ちなみに、組み込まれている検出された平面アンカーそのものの正方形ではないgeometryの形を取ることもできます。 func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor, let meshGeometry = ARSCNPlaneGeometry(device: sceneView.device!) else { fatalError("Can't create plane geometry") } meshGeometry.update(from: planeAnchor.geometry) meshNode = SCNNode(geometry: meshGeometry) meshNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red.withAlphaComponent(0.7) node.addChildNode(meshNode) } func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { guard let planeAnchor = anchor as? ARPlaneAnchor if let meshGeometry = meshNode.geometry as? ARSCNPlaneGeometry { meshGeometry.update(from: planeAnchor.geometry) } } 3、検出した平面の利用 壁紙を貼ってみる planeGeometry.firstMaterial?.diffuse.contents = UIImage(named:"flowerWallPaper") オブジェクトをおいてみる 画面をタップした先に平面geometry(ここではARPlaneGeometryをとってます)があれば、オブジェクトのy座標を平面ノードに合わせて、x,z軸を平面とタップの交差点に合わせます。 @objc func sceneViewTapped(recognizer:UITapGestureRecognizer) { let location = recognizer.location(in: sceneView) let hitResults = sceneView.hitTest(location, options: [:]) if !hitResults.isEmpty { guard let planeNode = hitResults.first?.node else {return} let interiorHeight = interior.boundingBox.max.y-interior.boundingBox.min.y // オブジェクトノードの高さを計算 let tappedCoodinates = hitResults.first?.localCoordinates // タップと平面ノードの交差点 interior.position = SCNVector3(tappedCoodinates!.x, interiorHeight/2, tappedCoodinates!.z) // オブジェクトの原点はオブジェクトの中心にしたので、高さの半分だけ上に置く planeNode.addChildNode(interior) } 壁でオブジェクトを遮蔽する .verticalの子ノードを手前にレンダリングされるようにすることで、後ろのノードが壁に隠れている表現ができます。 if planeAnchor.alignment == .vertical { planeNode.renderingOrder = -1 planeNode.geometry!.firstMaterial!.colorBufferWriteMask = [] } ? フリーランスエンジニアです。 お仕事のご相談こちらまで rockyshikoku@gmail.com Core MLを使ったアプリを作っています。 機械学習関連の情報を発信しています。 Twitter Medium
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Xcode] Find Call Hierarchyで古いコードがヒットしてしまう事象の回避策(共有&情報募集)

前提環境 Xcode 12.1 前置き Xcode上で、関数やprotocolの呼び出し元を調べたい場合、 カーソルを合わせて右クリックし、 コンテキストメニューから Find Call Hierarchy を選択すると、 左ペインの Find navigator に、呼び出し元のツリーが表示されます。 問題事象 コードを改修しながら Find Call Hierarchy を実行すると、改修前のコードがヒットしてしまい、もう呼び出していないはずなのに、呼び出し元のツリーに表示されてしまう場合があります。 地味に煩わしいです… Xcodeメニューの Product > Clean Build Folder で直る場合があるような気もするし、 Gitのブランチを切り替えると直る場合があるような気もするし、 Xcode再起動で直る場合があるような気もするし、 どれをやっても一向に解消しない場合もあります。 Xcode上部のステータス領域に"Indexing..."が表示されているタイミングで再構築しているのかな?と思いきや、その後でもやはり解消しない場合があるような… 都度あれこれ試すのもイライラするので、確実に回避したい!と考えた次第です。 私なりにたどり着いた回避方法 私は今のところ以下の方法で回避しています。 まず、Macアプリ DevCleaner for Xcode を入れます。 詳細はこちらの記事が素晴らしいのでご紹介しておきます。 Xcodeの面倒なキャッシュ削除をGUIで行えるMacアプリ『DevCleaner for Xcode』 - DevelopersIO 以下の操作を行います。 DevCleanerにて、"Derived Data"から該当アプリのデータを削除する。 Xcodeプロジェクトを開き直す。 「もっと簡単・確実に回避できるよ!」という情報をお持ちの方がいらっしゃいましたら、ぜひ情報提供をお願いいたします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Core MLの仕組みを利用した、.mlmodelの配信・暗号化の現状

はじめに ※この記事の結論は2021年4月時点での調査結果に基づいています。 AppleはWWDC 20で、Core MLモデルの「配信」と「暗号化」の仕組みをAppleとして提供すること発表しました。 それぞれのコンセプト・仕様の説明は上記動画を見ていただくとして(日本語字幕もあります)、この記事では 実際に調査・検証してわかった使用する際の問題点 その問題点を解消するためにとり得る選択肢 を書きます。 結論を先にまとめると、残念ながら「配信」の仕組みは2021年4月時点で実用可能な状態ではありませんでした。 「暗号化」の方は実用可能でしたが、「配信」の問題に引きずられる形で使用に制限があります。 モデルの配信 配信手順 利用手順や諸々のスクショが書いてある公式ドキュメントは以下です。 Creating and Deploying a Model Collection Xcode 12上で.mlmodelを選択すると、Utilitiesのタブに「Create Model Archive」のボタンが存在しているのがわかると思います。それをタップすると.mlarchiveというファイルが生成されます。このファイルをCore ML Model Deploymentというダッシュボードにアップロードすると、モデルの配信は完了です。 モデルはアップロードしたダッシュボードと同一team idのアプリのみでDL可能です。また、配信する地域やデバイスなどある程度セグメントを分けてモデルを配信することもできます。 利用する場合は、アプリ内でMLModelCollectionのstatic methodを一つ叩くだけで自動DLされます。基本的にはいつどのようにDLされているのかはブラックボックスで、iOSが適切なタイミングを判断してくれるようです。 beginAccessing(identifier:completionHandler:) // identifierにはダッシュボードのModel Collectionの名前を入れる。 // ダッシュボード上で配信のマークがついているバージョンが自動でDLされる。 let progress = MLModelCollection.beginAccessing(identifier: "HogeMLModel") { result in switch result { case .success(modelCollection): let fileURL = modelCollection.entries["HogeModel"] // ★ アップロードしたときのモデルの名前を指定するとDL先のファイルパスが取得できる case .failure(error): break } } また、一度アクセスすれば次からはディスクに保存したmlmodelが利用されるようになるため通信は発生しません。 現状の課題 とても便利な仕組みなのですが、実際に使ってみるとうまくDLが走らない不可解な挙動が多くありました。例えば、ありえるシチュエーションとして次の3つのパターンを考えます。 アプリをDL直後の起動で初めて.mlmodelをDLする .mlmodelを一度DLした後に、ダッシュボードで新しい.mlmodelへ差し替える .mlmodelを一度DLした後に、アプリを一度削除してさらに再度アプリのDLを行って起動 1のケースでは正常にモデルのDLは成功で返ってきて、★のコードが実行されたときにファイルパスが取得できました。 しかし、2と3のケースではモデルのDLは成功で返ってくるもののファイルパスが取得できず、モデル自体の利用ができなくなりました。 この問題に関しては、Developer Forum上で同様の問題についていくつも指摘がされています。ただしApple側からの反応はなく、問題が解消したという開発者の報告もありません。 WWDCでの話を聞く限り、この機能は「基本的には予めアプリの中にあるモデルを使いつつ、場合によって補助的にアップデートできる仕組み」というコンセプトのようにも読み取れます。そこまで正確な制御を期待できない仕様なのかもしれません。 DLの仕組みはブラックボックスのため調査も難しく、現状では配信を前提とした設計では利用可能な状態になさそうです。 モデルの暗号化 利用手順 公式ドキュメントは以下です。 Generating a Model Encryption Key 「配信」の時と流れは同様です。先程のUtilitiesのタブに「Create Encryption Key」のボタンが存在しています。これをクリックすると.mlmodelkeyというファイルが生成されます。この鍵を使ってモデルを暗号化します。 鍵を生成する際にteam idを選択するステップがあり、このとき選択したteam idのアプリでのみモデルの復号化ができます。復号化に使われる鍵は鍵生成時に自動的にAppleのサーバーにアップロードされるようです。 暗号化のステップを一旦飛ばして、復号化するときは特に実装時にあまり意識することはありません。一点だけ注意が必要なのはiOS14以降で使えるloadメソッドを経由させないといけない点です。 HogeHogeModel.load(contentOf: fileUrl) { result in switch result { case .success(let model): print(model) case .failure(let error): break } } load(contentsOf:configuration:completionHandler:) loadメソッドを呼んだときに、まだ鍵が端末に存在していなければ鍵のDLが走ります。そのため初回の利用時にはユーザーは必ず通信できる環境である必要があります。それ以降は端末に保存された鍵が利用されて、モデルの利用タイミングで都度復号化されます。 鍵の生成・復号化については以上になります。 もう一つモデルの暗号化が必要になるのですが、ここに関して現状では課題があります。 暗号化の課題 暗号化のステップには現状3つの選択肢があります。 アプリと一緒にコンパイルする際に暗号化して、アプリへモデルを組み込む .mlarchive生成時に暗号化して、Core ML Model Deploymentで配信する コマンドラインでモデルをコンパイルするときに暗号化して、独自の配信システムで配信する 1. アプリと一緒にコンパイルする際に暗号化して、アプリへモデルを組み込む この方法が一番考えることが少なく簡単です。 Xcode内の.mlmodelを入れると、「Build Phases」の「Compile Sources」にmlmodelも追加されます。モデルもコンパイルされた上で利用されるためここに表示されるのですが、Compiler Flagとして鍵を指定可能です。次のオプションを追加します。 --encrypt $SRCROOT/HogeHogeModel.mlmodelkey こうすると、暗号化済み.mlmodelc(コンパイルされた.mlmodel)が生成されて、アプリ内に組み込まれます。 あとは復号化の説明に記載したとおりloadでモデルを読み込むと自動的に復号化されます。 2. .mlarchive生成時に暗号化して、Core ML Model Deploymentで配信する この方法は「配信」と「暗号化」を組み合わせて使いたいケースを想定しています。やり方は簡単で、「配信」の時に説明した「Create Model Archive」のボタンをクリックすると鍵を指定するチェックボックスがあるためこれにチェックをつけるだけです。これで暗号化した状態で.mlarchiveが生成されます。 ただし、この方法は使えません。 「配信」のところで説明したとおり、Core ML Model Deploymentは21/4時点で実用的な状態ではないためめです。暗号化したモデルをアプリへ組み込まずオンデマンドで取得・復号化するためには他の方法で暗号化する必要があります。 3. コマンドラインでモデルをコンパイルするときに暗号化して、独自の配信システムで配信する 残念ながら、1と2の選択肢以外の方法はドキュメントには記載がありません。 しかしちゃんとしたドキュメントはどこにもないのですが、mlmodelには実はコマンドでコンパイルする方法があります。 xcrun coremlcompiler compile HogeHogeModel.mlmodel . Appleがこのコマンドの使い方を公式に言及しているのはDeveloper Forumになります。 このコマンドはオプションも受け取ることができます。これに関してはどこにも記載がないのですが、先程のCompiler Flagも実は受け取ることができます。 したがって、コマンドと先程のCompiler Flagを組み合わせて、 xcrun coremlcompiler compile HogeHogeModel.mlmodel . --encrypt $SRCROOT/HogeHogeModel.mlmodelkey と書くと、暗号化済みの.mlmodelcが生成されます。 実際にそのコンパイル済みモデルを独自の配信サーバーに置いてURLSessionでDLし、loadメソッドで読み込んだところ、無事復号化されモデルを使うことができました。 また、鍵と異なるteam idでビルドしたアプリでも試したところ復号化に失敗していました。 モデルはちゃんと特定のteam idのアプリでのみ利用可能になっているようです。 自前の暗号化ツールは利用可能か? そもそも.mlmodelkeyを利用せず、自分で鍵を用意してそれでモデルを暗号化するのはどうでしょうか。例えばAppleが提供するフレームワーク、CryptoKitでは「AES-GCM」方式の暗号化をサポートしています。 これで手元で共通鍵を生成してモデルを暗号化し、同じ鍵をアプリのバイナリへハードコードしてCryptoKitで復号化することが可能です。 暗号化をSwiftで行う場合はこのようになります。 import CryptoKit let destinationURL = // 暗号化したデータの保存先 let rawData = // .mlmodelをzip化したものなど暗号化したいデータ let symmetricKey = SymmetricKey(size: .bits256) // 鍵の生成 let sealedBox = try AES.GCM.seal(data, using: symmetricKey) // 鍵をBase64文字列にする let keyString = symmetricKey.withUnsafeBytes { pointer in Data(Array(pointer)).base64EncodedString() } let encryptoData = sealedBox.combined! encryptoData.write(to: destinationURL) // モデルを暗号化してファイルに保存 print(keyString) // 鍵をBase64形式で保存する アプリ内での復号化のコードはこのようになります。 import CryptoKit let keyString = // 保存した文字列をソルトを使うなど何らかのルールを組み合わせてハードコードする let encryptoData = // DLしてきた暗号化されたモデルデータ let keyData = Data(base64Encoded: keyString)! let symmetricKey = SymmetricKey(data: keyData) let sealedBox = try AES.GCM.SealedBox(combined: encryptoData) let decryptoData = try AES.CGM.open(sealedBox, using: symmetricKey) // 共通鍵を使って復号化の実行 上記コードで通信経路上のデータそのものと保存したファイルに関しては暗号化された状態を作ることができます。 しかしこの方法には問題があります。MLModelのロードには.mlmodel(もしくは.mlmodelc)のファイルパスを指定する方法しかなく、メモリ上のデータをロードする方法がありません。そのため、復号化した素のモデルを一旦ファイルに書き込まなくてなりません。 MLModelのAPIリファレンスを見る限りそれを回避する方法は今の所ありませんでした。 そのためこの方法は避けたほうが良さそうです。 その他暗号化を使わないで似たような効果を得る方法 機械学習モデルの一部のレイヤーをアプリ内の実装に移すことで、.mlmodelを不完全版にしてしまうという方法も考えられます。 その場合は.mlmodel単体では機能が利用できないという状態にできます。 まとめ 以上をまとめると、現時点では.mlmodelの「配信」「暗号化」を実現するためには次のやり方が最も適切です(2021年4月)。 Core ML Model Deploymentは使わず、自前の配信サーバーを用意する xcrunで.mlmodelのコンパイルと暗号化を予めやっておく 配信サーバー経由で暗号化済み.mlmodelcを配信し、URLSessionによるDLを自分で実装する 生成した鍵と同じteam idでアプリをビルドする この方法で最新の.mlmodelをDLして、自分のアプリでのみ復号化できます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Core MLの仕組みを利用した.mlmodelの配信・暗号化の実情

概要 ※この記事の結論は2021年4月時点での調査結果に基づいています。 AppleはWWDC 2020で、Core MLモデルの「配信」「暗号化」の仕組みを提供すると発表しました。 それぞれのコンセプト・仕様は上記動画を見ていただくとして(日本語字幕もあります)、この記事では 実際に調査・検証してわかった利用する際の問題点 その問題点を解消するためにとり得る選択肢 を書きます。 結論を先にまとめると、残念ながら「配信」の仕組みは2021年4月時点で実用可能な状態ではありませんでした。 「暗号化」の方は実用可能でしたが、「配信」の問題に引きずられる形で使用に制限があります。 モデルの配信 配信するまでの簡単な流れ 利用手順や諸々のスクショなど記載されている公式ドキュメントは以下です。 Creating and Deploying a Model Collection Xcode 12上で.mlmodelを選択すると、Utilitiesのタブに「Create Model Archive」のボタンが存在しているのがわかると思います。それをタップすると.mlarchiveというファイルが生成されます。このファイルをCore ML Model Deploymentというダッシュボードにアップロードすると、モデルの配信は完了です。 モデルはアップロードしたダッシュボードと同一team idのアプリのみでDL可能です。また、配信する地域やデバイスなどある程度セグメントを分けてモデルを配信することもできます。 利用する場合は、アプリ内でMLModelCollectionのstatic methodを一つ叩くだけで自動DLされます。基本的にはいつどのようにDLされているのかはブラックボックスで、iOSが適切なタイミングを判断してくれるようです。 beginAccessing(identifier:completionHandler:) // identifierにはダッシュボードのModel Collectionの名前を入れる。 // ダッシュボード上で配信のマークがついているバージョンが自動でDLされる。 let progress = MLModelCollection.beginAccessing(identifier: "HogeMLModel") { result in switch result { case .success(modelCollection): let fileURL = modelCollection.entries["HogeModel"] // ★ アップロードしたときのモデルの名前を指定するとDL先のファイルパスが取得できる case .failure(error): break } } また、一度アクセスすれば次からはディスクに保存したmlmodelが利用されるようになるため通信は発生しません。 まだ仕様通りに動作していないという課題 とても便利な仕組みなのですが、実際に使ってみるとうまくDLが走らない不可解な挙動が多くありました。例えば、ありえるシチュエーションとして次の3つのパターンを考えます。 アプリをDL直後の起動で初めて.mlmodelをDLする .mlmodelを一度DLした後に、ダッシュボードで新しい.mlmodelへ差し替える .mlmodelを一度DLした後に、アプリを一度削除してさらに再度アプリのDLを行って起動 1のケースでは正常にモデルのDLは成功で返ってきて、★のコードが実行されたときにファイルパスが取得できました。 しかし、2と3のケースではモデルのDLは成功で返ってくるもののファイルパスが取得できず、モデル自体の利用ができなくなりました。 この問題に関しては、Developer Forum上で同様の問題についていくつも指摘がされています。ただしApple側からの反応はなく、問題が解消したという開発者の報告もありません。 WWDCでの話を聞く限り、この機能は「基本的には予めアプリの中にあるモデルを使いつつ、場合によって補助的にアップデートできる仕組み」というコンセプトのようにも読み取れます。そこまで正確な制御を期待できない仕様なのかもしれません。 DLの仕組みはブラックボックスのため調査も難しく、現状では配信を前提とした設計では利用可能な状態になさそうです。 モデルの暗号化 暗号化できた前提での鍵生成と復号化の簡単な流れ 公式ドキュメントは以下です。 Generating a Model Encryption Key 「配信」の時と流れは同様です。先程のUtilitiesのタブに「Create Encryption Key」のボタンが存在しています。これをクリックすると.mlmodelkeyというファイルが生成されます。この鍵を使ってモデルを暗号化します。 鍵を生成する際にteam idを選択するステップがあり、このとき選択したteam idのアプリでのみモデルの復号化ができます。復号化に使われる鍵は鍵生成時に自動的にAppleのサーバーにアップロードされるようです。 暗号化のステップを一旦飛ばします。その次の復号化では実装時に意識しなければならないことは殆どありません。一点だけ注意が必要なのはiOS14以降で使えるloadメソッドを経由させないといけない点です。 HogeHogeModel.load(contentOf: fileUrl) { result in switch result { case .success(let model): print(model) case .failure(let error): break } } load(contentsOf:configuration:completionHandler:) loadメソッドを呼んだときに、まだ鍵が端末に存在していなければ鍵のDLが走ります。そのため初回の利用時にはユーザーは必ず通信できる環境である必要があります。それ以降は端末に保存された鍵が利用されて、モデルの利用タイミングで都度復号化されます。 鍵の生成・復号化については以上になります。 もう一つモデルの暗号化が必要になるのですが、ここに関して現状では課題があります。 暗号化の選択可能な方法 暗号化のステップには現状3つの選択肢があります。 アプリと一緒にコンパイルする際に暗号化して、アプリへモデルを組み込む .mlarchive生成時に暗号化して、Core ML Model Deploymentで配信する コマンドラインでモデルをコンパイルするときに暗号化して、独自の配信システムで配信する 1. アプリと一緒にコンパイルする際に暗号化して、アプリへモデルを組み込む この方法が一番考えることが少なく簡単です。 Xcode内の.mlmodelを入れると、「Build Phases」の「Compile Sources」にmlmodelも追加されます。モデルもコンパイルされた上で利用されるためここに表示されるのですが、Compiler Flagとして鍵を指定可能です。次のオプションを追加します。 --encrypt $SRCROOT/HogeHogeModel.mlmodelkey こうすると、暗号化済み.mlmodelc(コンパイルされた.mlmodel)が生成されて、アプリ内に組み込まれます。 あとは復号化の説明に記載したとおりloadでモデルを読み込むと自動的に復号化されます。 2. .mlarchive生成時に暗号化して、Core ML Model Deploymentで配信する この方法は「配信」と「暗号化」を組み合わせて使いたいケースを想定しています。やり方は簡単で、「配信」の時に説明した「Create Model Archive」のボタンをクリックすると鍵を指定するチェックボックスがあるためこれにチェックをつけるだけです。これで暗号化した状態で.mlarchiveが生成されます。 ただし、この方法は使えません。 「配信」のところで説明したとおり、Core ML Model Deploymentは2021/04時点では実用的な状態でないためです。暗号化したモデルをアプリへ組み込まずオンデマンドで取得・復号化するためには他の方法で暗号化する必要があります。 3. コマンドラインでモデルをコンパイルするときに暗号化して、独自の配信システムで配信する 残念ながら、1と2の選択肢以外の方法は正式なドキュメントとしての記載がありません。 しかし、mlmodelには実はコマンドでコンパイルする方法があります。 xcrun coremlcompiler compile HogeHogeModel.mlmodel . // 対象.mlmodelとコンパイルしたファイルの保存先を指定する Appleがこのコマンドの使い方を公式に言及しているのはDeveloper Forumになります。 このコマンドはオプションも受け取ることができます。どこにも記載がないものの、先程のCompiler Flagも受け取ることができます。 したがって、コマンドとCompiler Flagを組み合わせて、 xcrun coremlcompiler compile HogeHogeModel.mlmodel . --encrypt $SRCROOT/HogeHogeModel.mlmodelkey と書くと、暗号化済みの.mlmodelcが生成されます。 実際にそのコンパイル済みモデルを独自の配信サーバーに置いてURLSessionでDLし、loadメソッドで読み込んだところ、無事復号化されモデルを使うことができました。 また、鍵と異なるteam idでビルドしたアプリでも試したところ復号化に失敗していました。 モデルはちゃんと特定のteam idのアプリでのみ利用可能になっているようです。 自前の暗号化ツールは利用可能か? そもそも.mlmodelkeyを利用せず、自分で鍵を用意してそれでモデルを暗号化するのはどうでしょうか。例えばAppleが提供するフレームワーク、CryptoKitでは「AES-GCM」方式の暗号化をサポートしています。 手元で共通鍵を生成してモデルを暗号化し、同じ鍵をアプリのバイナリへハードコードした上でCryptoKitで復号化することが可能です。 暗号化をSwiftで行う場合はこのようになります。 import CryptoKit let destinationURL = // 暗号化したデータの保存先 let rawData = // .mlmodelをzip化したものなど暗号化したいデータ let symmetricKey = SymmetricKey(size: .bits256) // 鍵の生成 let sealedBox = try AES.GCM.seal(data, using: symmetricKey) // 鍵をBase64文字列にする let keyString = symmetricKey.withUnsafeBytes { pointer in Data(Array(pointer)).base64EncodedString() } let encryptoData = sealedBox.combined! encryptoData.write(to: destinationURL) // モデルを暗号化してファイルに保存 print(keyString) // 鍵をBase64形式で保存する アプリ内での復号化のコードはこのようになります。 import CryptoKit let keyString = // 保存した文字列をソルトを使うなど何らかのルールを組み合わせてハードコードする let encryptoData = // DLしてきた暗号化されたモデルデータ let keyData = Data(base64Encoded: keyString)! let symmetricKey = SymmetricKey(data: keyData) let sealedBox = try AES.GCM.SealedBox(combined: encryptoData) let decryptoData = try AES.CGM.open(sealedBox, using: symmetricKey) // 共通鍵を使って復号化の実行 上記コードで通信経路上のデータそのものと保存したファイルに関しては暗号化された状態を作ることができます。 しかしこの方法には問題があります。MLModelのロードには.mlmodel(もしくは.mlmodelc)のファイルパスを指定する方法しかなく、メモリ上のデータをロードする方法がありません。そのため、復号化した素のモデルを一旦ファイルに書き込まなくてなりません。 MLModelのAPIリファレンスを見る限りそれを回避する方法は今の所ありませんでした。 そのためこの方法は避けたほうが良さそうです。 その他暗号化を使わないで似たような効果を得る方法 機械学習モデルの一部のレイヤーをアプリ内の実装に移すことで、.mlmodelを不完全版にしてしまうという方法も考えられます。 その場合は.mlmodel単体では機能が利用できないという状態にできます。 結論と現状のベストプラクティス 以上をまとめると、現時点(2021年4月)では.mlmodelの「配信」「暗号化」を実現するためには次のやり方が最も適切です。 Core ML Model Deploymentは使わず、自前の配信サーバーを用意する xcrunで.mlmodelのコンパイルと暗号化を予めやっておく 配信サーバー経由で暗号化済み.mlmodelcを配信し、URLSessionによるDLを自分で実装する 生成した鍵と同じteam idでアプリをビルドする この方法で最新の.mlmodelをDLして、自分のアプリでのみ復号化できます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む