- 投稿日:2019-10-20T22:48:52+09:00
[iOS] App ID Prefixについて
概要
アプリのApp ID Prefixを変更したい場面が出てきて色々調査したため、その内容を整理しておきます。以下の内容が不明な方向けです。
- App ID PrefixとTeam ID、Bundle IDの関係性
- 新規アプリ作成時にApp ID Prefixが複数存在する場合、どれを選ぶべきか
- App ID Prefixは変更可能か
※本記事の情報は個人で確認したものであり、情報の正確性を保証するものではありません。App IDの仕様についても今後変更される可能性があります。
App IDの構成
App IDはApp ID Prefixと呼ばれる、大文字英数10桁の識別子とBundle ID(Bundle Identifier, App ID Suffix)と呼ばれる識別子で構成されています。
<App ID Prefix>.<Bundle ID>例) ABCDE12345.jp.hogehoge.hogeapp
App IDは開発者を識別するためのものであり、他の開発者とは別のものが割り当てられます。基本的にはTeam IDと呼ばれるデフォルトのApp ID Prefixのみですが、過去にApp ID Prefixを開発者が発行できた経緯があり、開発者によっては複数のApp ID Prefixが存在します。
Bundle IDはアプリをユニークに識別するためのもので、このIDが異なる場合は別アプリとして認識されます。
新規にアプリを作成し、App IDを発行する際には、Team IDを含めたこれらApp ID Prefixから選択することとなります。
App ID PrefixはKeychainでのデータ共有機能に影響する
App ID Prefixを共有するアプリ間ではKeychainを介してデータの共有が可能です。逆を言えば、App ID Prefixを変更すると、もとのKeychainデータにアクセスすることはできなくなりますので注意が必要です。
他にも、App ID Prefixを共有していないアプリ間ではKeychainを介してデータの共有ができないため、新規アプリ作成時には、App ID PrefixをデフォルトのTeam IDに揃えておく、などの考慮が必要です。App ID Prefixは変更できるか
- Bundle IDがワイルドカードを利用している場合、App ID Prefixのみを変更した同じ形式のBundle IDを作成することで、新しいApp ID Prefixのものへ移行可
- Bundle IDがワイルドカードを利用していない場合(explicit)
- アプリのバイナリをAppStoreConnectへアップロードする前であれば、該当のApp IDを削除し、新しいApp ID Prefixを指定し、再度作成することで移行可
- アプリのバイナリをAppStoreConnectへアップロードした後であれば、以下のエラーがでてApp IDが削除できないため、移行不可
There is a problem with the request entity
The App ID '削除しようとしたAppID' appears to be in use by the App Store, so it can not be removed at this time.なお、アプリの配信先の国をすべて削除する、開発者アカウントのAdmin権限でAppStoreConnect上でアプリを削除するなどを行なっても上記のエラーは解消できませんでした。
ただし、Appleに依頼すれば変更できる可能性も
Appleの開発者サポートに確認したところ、デベロッパーアカウントのアカウントホルダーからの依頼があれば、Appleの内部チームがその内容を審査し、受け付けてくれれば変更を実施してくれることもあるようです。(リードタイムは明確になってはおりませんが、少なくとも数日は要するようです)
参考
- 投稿日:2019-10-20T22:33:17+09:00
iosアプリのアプリ内課金テスト(sandbox )で経験したおかしなこと
iOSの課金テスト、特に定期購読サービスのテストは結構不便なところがあります。
その上に、sandboxの課金環境はとても不安定で、テストのときによく躓くことがありました。
何個か書いてみたいと思います。
sandboxユーザーを切り替えることが出来ない
ios13
の問題ですが、iosは2つのアカウントでログインすることが出来ます。普通にApp storeで利用しているアカウントでログインすることが出来ますが、
課金のテスト用で利用出来るsandboxのアカウントでもログインすることが出来ます。
sandboxのアカウントに一度ログインしてしまうと、二度とログアウトすることが出来ないのです・・・。_
これが結構テストのときに不便で、ログアウトする方法は iOS13.1にアップデートするか、端末の設定のところから端末をリセットする事です。
課金すると、端末には課金成功と表示されるが、実際のレスポンスはエラーが返却される・・・
これは結構厄介です。
テスターから、appleの課金は成功したのに、課金に失敗したと表示されますと報告が・・・。
ケーブルをつないでdebugしてみると、レスポンスがエラーが返却されるのに、エラーメッセージは
完了しました
という文言が返却されました。https://forums.developer.apple.com/thread/121096
これは他にも同じ症状の人がいたので、それからは調べることをやめました。
App Storeにアクセスできない・・・
これもどうしようか迷った、結構厄介な問題でした。
ストアからダウンロードしたものは何も問題ないのに、testflightからダウンロードしたものは課金アイテムの情報の取得も、購入も出来ませんでした。
エラーはこんな感じでした。
Error Domain=AMSErrorDomain Code=301 "Invalid Status Code" UserInfo={AMSStatusCode=503, NSLocalizedDescription=Invalid Status Code, NSLocalizedFailureReason=The response has an invalid status code}301だったり、503だったりしたのでサーバーの問題か・・・?
でもappleのsystem statusをみても今は問題ないと表示されている
こちらですが、 DNS設定に 8.8.8.8と8.8.4.4 を追加したら課金周りのテストができるようになりました。
https://stackoverflow.com/questions/58443064/how-to-handle-skproductsrequest-301-invalid-status-code
こちらのstackoverflowに助けられました・・・。
単純に課金ができない
上記の理由でなくても、単純にsandbox環境は課金ができなくなるときがあります。
これは時間がすぎれば症状がなおるので、少し時間を空けてからテストすることにしましょう・・・。
他にもsandboxの問題を経験した方がいらっしゃれば、ぜひコメントお願いします!
- 投稿日:2019-10-20T20:29:48+09:00
iOS13のモーダルはFullscreenもいいけどCurrentContextも検討してみて
iOS13からモーダルを表示するときはセミモーダルがデフォルトの挙動となりました。
完全なフルスクリーンではなく後ろのViewが少し見えるようなデザインで、下にスワイプで元のViewに戻ることができます。これをされると困るデザインがあります。例えばログイン後に表示されるViewはセミモーダルではなくフルスクリーンで表示したいでしょう。
StoryboardからPresentationを
Fullscreen
にすることで次のモーダルを完全なフルスクリーン状態で遷移できます。
Swiftの場合はこうします。
let vc = ViewController() vc.modalPresentationStyle = .fullScreen present(vc, animated: true)
この状態であればスワイプで戻ることもできません。
ここまでであればあまり問題はなさそうに見えます。さらにModalで遷移したいとき
ここからさらにモーダルを表示したいとします。このときはセミモーダルのほうが利便性があがるので普通の遷移にします。
するとどうでしょう。
なんと後ろのViewが下がらないままセミモーダルが出てしまいました。iOS13においてfullscrrenでモーダル表示したものはずっとfullscreenの状態を維持するのでこのような動きになるようです。
でもこれじゃかっこよくない!下がってほしい!というときはCurrent Context
を使ってみます。先ほどのFullscrrenからCurrent Contextに変更します。するとStoryboard上では灰色はフルスクリーンで、その次のモーダルでは灰色Viewが下にずれているのが確認できます。
(よく見ると上のStoryboardでもFullscreen選択時のモーダルの見え方はシミュレートされていましたね)
ではこれで解決!かと思いきや実際実行してみると・・・
なんと今度はステータスバーが全く見えなくなってしまいました。CurrentContext時にStatusBarを見えるようにする
これは推測ですが、CurrentContextによって諸々の状態を参照する先が灰色のViewになっているのだと思われます。灰色Viewの時点ではステータスバーは黒色テキストなので、モーダル遷移後も灰色Viewからステータスバーの情報を取得していると考えると、黒の背景に黒のテキストとなってしまい見えなくなってしまったものと考えられます。
これを回避するにはViewController毎にステータスバーの色は何色にすべきかという情報を設定します。
灰色のViewControllerに
preferredStatusBarStyle
プロパティをオーバーライドします。class ViewController: UIViewController { override var preferredStatusBarStyle: UIStatusBarStyle { if presentedViewController != nil { return .lightContent } else { return super.preferredStatusBarStyle } } }
presentedViewController
は現在のViewController(灰色)からみてさらにモーダル表示しているViewControllerがあればそのViewControllerが、なければnilが返ってきます。
つまりpresentedViewControllerがnilでないなら何かしらモーダルを表示しているということです。
このときはステータスバーを白とし、それ以外はデフォルトの挙動としました。
ここを拡張することでもう少し細かな要望にも応えることができます。例えば次のModalもフルスクリーンだったらデフォルト挙動にするとかですね。青色のViewControllerのpreferredStatusBarStyleは参照されませんでした。これもCurrentContextが影響していると考えられます。
- 投稿日:2019-10-20T20:29:48+09:00
iOS13のモーダルはFullscreenもいいけどCurrentContextも検討したい
iOS13からモーダルを表示するときはセミモーダルがデフォルトの挙動となりました。
完全なフルスクリーンではなく後ろのViewが少し見えるようなデザインで、下にスワイプで前のViewに戻ることができます。これをされると困るデザインがあります。例えばログイン後に表示されるViewはセミモーダルではなくフルスクリーンで表示したいでしょう。
StoryboardからPresentationを
Fullscreen
にすることで次のモーダルを完全なフルスクリーン状態で遷移できます。
Swiftの場合はこうします。
let vc = ViewController() vc.modalPresentationStyle = .fullScreen present(vc, animated: true)
この状態であればスワイプで戻ることもできません。
ここまでであればあまり問題はなさそうに見えます。さらにModalで遷移したいとき
ここからさらにモーダルを表示したいとします。このときはセミモーダルのほうが利便性があがるので普通の遷移にします。
するとどうでしょう。
なんと後ろのViewが下がらないままセミモーダルが出てしまいました。iOS13においてfullscrrenでモーダル表示したものはずっとfullscreenの状態を維持するのでこのような動きになるようです。
でもこれじゃかっこよくない!下がってほしい!というときはCurrent Context
を使ってみます。先ほどのFullscrrenからCurrent Contextに変更します。するとStoryboard上では灰色はフルスクリーンで、その次のモーダルでは灰色Viewが下にずれているのが確認できます。
(よく見ると上のStoryboardでもFullscreen選択時のモーダルの見え方はシミュレートされていましたね)
ではこれで解決!かと思いきや実際実行してみると・・・
なんと今度はステータスバーが全く見えなくなってしまいました。CurrentContext時にStatusBarを見えるようにする
これは推測ですが、CurrentContextによって諸々の状態を参照する先が灰色のViewになっているのだと思われます。灰色Viewの時点ではステータスバーは黒色テキストなので、モーダル遷移後も灰色Viewからステータスバーの情報を取得していると考えると、黒の背景に黒のテキストとなってしまい見えなくなってしまったものと考えられます。
これを回避するにはViewController毎にステータスバーの色は何色にすべきかという情報を設定します。
灰色のViewControllerに
preferredStatusBarStyle
プロパティをオーバーライドします。class ViewController: UIViewController { override var preferredStatusBarStyle: UIStatusBarStyle { if presentedViewController != nil { return .lightContent } else { return super.preferredStatusBarStyle } } }
presentedViewController
は現在のViewController(灰色)からみてさらにモーダル表示しているViewControllerがあればそのViewControllerが、なければnilが返ってきます。
つまりpresentedViewControllerがnilでないなら何かしらモーダルを表示しているということです。
このときはステータスバーを白とし、それ以外はデフォルトの挙動としました。
ここを拡張することでもう少し細かな要望にも応えることができます。例えば次のModalもフルスクリーンだったらデフォルト挙動にするとかですね。青色のViewControllerのpreferredStatusBarStyleは参照されませんでした。これもCurrentContextが影響していると考えられます。
- 投稿日:2019-10-20T19:52:08+09:00
【swifter】Twitter連携アプリを作るためのswifterの主な関数の使い方の記録
Swifterとは
swiftで書かれたTwitter Framework.
ソースコードも公開されているため、使いやすい.
TWitterKitがサポート終了したし、こっち使えばいいんじゃね。ソースコード : github : Swifter
Swifterに用意された主な関数の使い方
連携する
//login処理 // swifter構造体の宣言 let swifter = Swifter(consumerKey: <twitter consumer key>, consumerSecret: <twitter consumer secret>) // のちに使う変数 var twitterTokenKey:String! var twitterTokenSecret:String! swifter.authorize( withCallback: URL(string: "swifter-<twitter consumer key>://")!, presentingFrom: self, success: { accessToken, response in print(response) guard let accessToken = accessToken else { return } twitterTokenKey = accessToken.key twitterTokenSecret = accessToken.secret }, failure: { error in print(error) } )tweetする
返り値はjsonデータ形式
// さっきゲットしたtwitterTokenKeyとtwitterTokenSecretを使ってswifterを宣言し直す swifter = Swifter(consumerKey: <twitter consumer key>, consumerSecret: <twitter consumer secret>, oauthToken: twitterTokenKey, oauthTokenSecret: twitterTokenSecret) swifter.postTweet(status: "Tweetの内容", success: { json in // 成功時の処理 print(json) }, failure: { error in // 失敗時の処理 print(error) })timelineを取得する
返り値はjsonデータ形式
// こっちは、連携中のユーザーのフォローしてるユーザーのツイートを時系列順に。 swifter.getHomeTimeline(count: 50,success: { json in // 成功時の処理 print(json) }, failure: { error in // 失敗時の処理 print(error) }) // こっちは、@~~~で指定したユーザーのツイートを時系列順に取得。 swifter.getTimeline(for: .screenName("<twitterの@~~~のuser名の~~~の部分>"),success: { json in // 成功時の処理 print(json) }, failure: { error in // 失敗時の処理 print(error) })まとめ
他にも、twitter上での操作はコードを通して全部できるようになってるっぽかったから、ソースコード見て必要な機能があったら実装していこう。
- 投稿日:2019-10-20T18:26:57+09:00
iphoneだけでPythonistaでyoutubeから動画をダウンロード出来るようにしよう!
概要
みなさんこんにちは。
Pytohnista3を触り始めてからの僕の念願が一つ達成されました。
ずばりYoutubeの動画をiphoneからダウンロードするです!参考にしたサイト
- https://qiita.com/Gold_Gliders/items/331eac3045c635ce0612
- urlの取得方法などでとても参考になりました。この方はpytubeを使って動画を取得していましたが自分はyoutube_dlを使ってダウンロードしたかったので自分流に改造しました。また、動画のフォルダもスクリプトがあるフォルダ配下に作成したかったので少し手を加えています。
- https://mahimahironron.hateblo.jp/entry/2019/09/29/youtubeの動画をダウンロードしよう!
- youtube_dlでのダウンロード方法を参考にしました。
実際やったこと、ソース
- pythonistaの中にフォルダを作ります。
- 作ったフォルダの中に「DL_videos」というフォルダを作成します。ダウンロードした動画はこのフォルダに保存されます。
- 以下のソースを作成します。ソースは少し多いですが頑張って作ってみてください。
①byte2si.py
byte2si.pyimport math def main(byte): #byteの桁数を取得して、3桁刻みに単位を変えてくだけ if 0 <= byte: dig = int(math.log10(byte)) if 0 <= dig < 3: bytes = byte / pow(10, 0) return bytes, 'B' elif 3 <= dig < 6: bytes = byte / pow(10, 3) return bytes, 'KB' elif 6 <= dig < 9: bytes = byte / pow(10, 6) return bytes, 'MB' elif 9 <= dig < 12: bytes = byte / pow(10, 9) return bytes, 'GB' elif 12 <= dig <= 15: bytes = byte / pow(10, 12) return bytes, 'TB' else: raise ValueError('Not num') def byte2si(byte): num, unit = main(byte) #少数第2位まで丸める renum = round(num, 2) return str(renum) + unit②main.py
main.pyfrom pytube import YouTube #import add_video import my_module import byte2si import url_get import os import glob import time import sys import youtube_dl VIDEO_DIR = os.path.join(os.getcwd(), "DL_videos") OPTS = { "outtmpl": "{VIDEO_DIR}/%(title)s.mp4".format(VIDEO_DIR=VIDEO_DIR), 'ignoreerrors': True } def download(url): """ Download video from YouTube. Parameters ---------- url : str YouTube video URL Returns ---------- info : dict Downloaded video info. """ print("Downloading {url} start..".format(url=url)) with youtube_dl.YoutubeDL(OPTS) as y: info = y.extract_info(url, download=True) print("Downloading {url} finish!".format(url=url)) return info def rename(info): """ Rename downloaded video filename as camelcase. Parameters ---------- info : dict Downloaded video info. """ title = info["title"] pattern = '{VIDEO_DIR}/{title}.mp4'.format(VIDEO_DIR=VIDEO_DIR, title=title) for v in glob.glob(pattern, recursive=True): print("{title}.mp4 found! Renaming start..".format(title=title)) file_path = os.path.join(VIDEO_DIR, v) new_file_path = file_path.replace(' ', '_') os.rename(file_path, new_file_path) print("Renaming finish!".format(title)) def absfpath(path): # ”Download”ディレクトリ下のファイルを取得する flist = glob.glob(path + '/*') if flist: # “Download”ディレクトリ下の1つしかない動画ファイルを取得する fpath = os.path.abspath(flist[0]) # “Download”ディレクトリ下にある動画ファイルの絶対パスを返す return fpath else: raise OSError('The file does not exist.') def main(): #プログレスバー print('Now executing...') # YouTubeアプリからurlを取得 url = url_get.yturl() if not url: raise ('Not YouTube') print(url) info = download(url) rename(info) abspath = absfpath(VIDEO_DIR) if __name__ == '__main__': # 開始時間を取得 start = time.time() main() #終了時間を少数第3位まで取得 finish = round(time.time() - start, 3) #実行所要時間を表示 print('Execute Time:', finish) #終了 print('Done')③url_get.py
url_get.pyimport appex import re def trns(): if not appex.is_running_extension(): raise RuntimeError( 'This script is intended to be run from the sharing extension.') url = appex.get_text() if not url: raise ValueError('No input text found.') elif re.search(r'youtube', url): return url raise ValueError('Not YouTube') def yturl(): geturl = trns() # 取得したurlの余分なテキストを置換し、なくす url = geturl.replace('&feature=share', '') return url if __name__ == '__main__': print(yturl())
- main.pyをSafariやyoutubeの共有→「Run Pythonista Script」の中で実行できるよう登録します。
使い方
- Youtubeを開きます。
- 見たい動画またはプレイリストに行きます。
- 「共有」から「Run Pythonista Script」を選び登録したスクリプトを実行します。
- 動画の場合:動画がダウンロードされDL_Videosフォルダの中に保存されます。
- プレイリストの場合: プレイリスト内全ての動画がダウンロードされDL_Videosフォルダの中に保存されます(プレイリストは公開のものしかダウンロードできないのでご注意!)。
- 投稿日:2019-10-20T17:05:46+09:00
【Swift】TelloとUDP通信②- Telloからメッセージを受信する -
概要
前回、Telloに対しUDP通信でコマンドを送信しましたが、今回はコマンドに対しての応答について説明していきます。
各コマンドに対し、Telloから応答メッセージが送信されるため、端末側で受信処理を行うことでメッセージを取得できます。受信処理
Telloにコマンドを送信した時、Telloから応答メッセージが送信されます。
NWConnection の receive を使用することで、受信処理を行えます。let connection = NWConnection(host: "192.168.10.1", port: 8889, using: .udp) //送信処理 connection.send(content: message, contentContext: .defaultMessage, isComplete: true, completion: .contentProcessed( error in )) //受信処理 connection.receive(minimumIncompleteLength: 0, maximumLength: Int(Int32.max)) { (data: Data?, contentContext: NWConnection.ContentContext?, aBool: Bool, error: NWError?) in //処理内容 }取得したdataは文字に変換することで内容を確認できます。
if let data = data, let message = String(data: data, encoding: .utf8) { //応答メッセージ print("message: \(message)") }応答メッセージ
Tello SDKのコマンドでは、離陸、着陸等の操作に対しての応答と、バッテリー等のステータス情報に対しての応答があります。
操作コマンドは以下になります。応答としてはokかerrorを送信します。
コマンド 説明 command SDKモード開始 takeoff 離陸 land 着地 streamon ビデオストリーム開始 streamoff ビデオストリーム停止 emergency 緊急停止 up x 上にxcm進む
x:20-500down x 下にxcm進む
x:20-500left x 左にxcm進む
x:20-500right x 右にxcm進む
x:20-500forward x 前にxcm進む
x:20-500back x 後ろにxcm進む
x:20-500cw x 時計回りにx度回転
x: 1-3600ccw x 反時計回りにx度回転
x: 1-3600flip x xにひっくり返す
X:l (左) r (右) f (前) b (後ろ)go x y z speed x y zにspeed(cm/s)で進む
x: 20-500 y: 20-500 z: 20-500 speed: 10-100curve x1 y1 z1 x2 y2 z2 speed 現在の座標と2つの与えられた座標で定義された曲線をspeed(cm/s)で飛行
x1, x2: 20-500
y1, y2: 20-500
z1, z2: 20-500
speed: 10-60Telloのステータス情報は以下のコマンドで確認できます。
コマンド 説明 応答 speed? 速度 x:1-100 (cm/s) battery? バッテリー x:0-100 time? 飛行時間 x (s) height? 高さ x:0-3000 (cm) temp? 温度 x:0-90 (℃) attitude? IMU姿勢データ pitch roll yaw baro? 気圧 x (m) acceleration? IMU角加速度データ x y z (0.001g) tof? TOFから距離値 x:30-1000 (cm) wifi? Wi-Fi SNR SNR 動画情報の取得
Telloのカメラからの情報は stremon のコマンドで動画情報を受信できるようになります。
取得する場合、PORT:8889からは取得できないため、IP:0.0.0.0 PORT:11111 のUDPサーバを作り受信させる必要があります。参考
- 投稿日:2019-10-20T16:31:14+09:00
Xcode11で外部ディスプレイに表示できるようにする方法
はじめに
iPhone/iPadとApple Lightning - Digital AVアダプタを用いていることでHDMIモニター(TV)に映像を出力することができます。
通常はミラーリングとなりますが、本方法を用いると液晶(OLED)とHDMIで別の表示することができます。
例えば、iPhoneの液晶(OLED)を操作画面として、HDMIを表示画面にするなどできます。Xcode11で外部ディスプレイに表示する方法を試行錯誤したので投稿します。
まだわかっていないところがあり、コメントいただければと思います。実装
いくつかのサイトを参考に実装しました。
最後に参考サイトのリンクしますので確認してみてください。
また、サンプルプロジェクトをgithubに公開していますので参考にしてください。UISceneを無効化
Xcode11からUIScreenからUISceneに変わっています。しかしながら新しいUISceneで外部ディスプレイ対応する方法がわかりませんでした?
なのでUISceneを無効化します。githubのサンプルプロジェクトのcommitログを参考にしてください。
概要は下記の通りです。
*AppDelegate.swift
のUISceneに関連するメソッドを無効化(削除でも可)
*SceneDelegate.swift
の全てのメソッドを無効化(削除でも可)
*AppDelegate.swift
にvar window: UIWindow?
を追加
*Info.plist
のUISceneに関する記述を削除外部ディスプレイの接続切断通知を受け取れるように設定
接続切断通知を受け取れるようにします。
通知設定/// 外部ディスプレイの接続と切断の通知を受け取れるようにする private func setupNotification() { let center = NotificationCenter.default center.addObserver(self, selector: #selector(externalScreenDidConnect(notification:)), name: UIScreen.didConnectNotification, object: nil) center.addObserver(self, selector: #selector(externalScreenDidDisconnect(notification:)), name: UIScreen.didDisconnectNotification, object: nil) }接続処理を実装
Storyboardで作成した画面にStoryboardIDを設定しておき接続時に画面を生成します。
必要に応じて外部から呼び出せるようにインスタンスを保持しておくと便利だと思います。接続処理/// 外部ディスプレイを接続されたときに処理する /// - Parameter notification:接続されたUIScreenインスタンス @objc func externalScreenDidConnect(notification: NSNotification) { guard let screen = notification.object as? UIScreen else { return } guard externalWindow == nil else { return } guard let viewController = self.storyboard?.instantiateViewController(withIdentifier: "ExternalDisplayViewController") as? ExternalDisplayViewController else { return } externalWindow = UIWindow(frame: screen.bounds) externalWindow?.rootViewController = viewController //iOS13.0からDeprecatedになっているがUISceneを無効化しているのでこちらのプロパティーを設定する externalWindow?.screen = screen externalWindow?.isHidden = false //外部ディスプレイのViewControllerをあとで操作できるようにインスタンスを保持する externalDisplayViewController = viewController }切断処理を実装
外部ディスプレイが切断時の処理も記述します。
切断処理/// 外部ディスプレイを切断されたときに処理する /// - Parameter notification: 切断されたUIScreenインスタンス @objc func externalScreenDidDisconnect(notification: NSNotification) { guard let screen = notification.object as? UIScreen else { return } guard let _externalWindow = externalWindow else { return } //切断通知されたscreenと現在表示中のscreenが同じかチェックする if screen == _externalWindow.screen { _externalWindow.isHidden = true externalWindow = nil //外部ディスプレイのViewControllerを削除する externalDisplayViewController = nil } }最後に
他の記事を参考に作成しましたが、
externalWindow?.screen
するとDeprecatedになっているプロパティで警告されてしまいます。
正しい実装方法を調べてみましたが、わかりませんでした。
コメントをいただければと思います。
(あわせてこの記事とサンプルプロジェクトを変更していきたいと思います。)参考サイト
- 投稿日:2019-10-20T14:25:18+09:00
【Swift5.1】DuctTapeで初期化時の値の代入をもっと簡潔にする
はじめに
Swiftでオブジェクトのインスタンス化をしつつpropertyに値を代入する場合は、以下のような実装になるかと思います。
let label: UILabel = { let label = UILabel() label.numberOfLines = 0 label.textColor = .red label.text = "Hello, World!!" return label }()上記と同じ状態のインスタンスを、メソッドチェーンで実現する方法を紹介しようと思います。
DuctTapeを利用する
DuctTapeというOSSを利用することで、先程の実装を以下のような実装にできます。
let label = UILabel().ductTape .numberOfLines(0) .textColor(.red) .text("Hello, World!!") .build()NSObjectを継承したオブジェクトのインスタンスの
.ductTape
にアクセスすると、Builderが返ってきます。
値を代入したいpropertyがある場合、そのproperty名と同一名のclosureにアクセスできるので、closureの引数として代入したい値を渡します。
一通りの値の設定が終わったら.build()
を呼び出すことで、任意の値が代入された状態のインスタンスが返されます。メソッドにアクセスする場合
インスタンスのメソッドにアクセスしたい場合は、Builderの
.reinforce
を介してインスタンスにアクセス
できるようになるため、渡されたインスタンスからメソッドにアクセスします。let collectionView = UICollectionView().ductTape .backgroundColor(.red) .reinforce { $0.register(UITableViewCell.self, forCellWithReuseIdentifier: "Cell") } .build()NSObjectを継承していないオブジェクトでも利用する
以下のように、Builderのinitializerの引数にインスタンスを渡すことで利用が可能になります。
class Dog { var name: String = "" } let dog = Builder(Dog()) .name("Copernicus") .build()DuctTapeの仕組み
Swift5.1から利用できるKeyPath dynamicMemberLookupを利用して実装してします。
そのためpropertyごとに代入用の処理を実装する必要はなく、subscript<Value>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Value>) -> (Value) -> Builder<Base>
を実装してしまえば、返り値のclosureの引数で受け取った値をKeyPathを介してインスタンスに代入する処理を実現できます。
また、KeyPath dynamicMemberLookupによる実装のため、該当のproperty名が自動補完されます。@dynamicMemberLookup public struct Builder<Base: AnyObject> { private let _build: () -> Base public init(_ build: @escaping () -> Base) { self._build = build } public init(_ base: Base) { self._build = { base } } public subscript<Value>(dynamicMember keyPath: ReferenceWritableKeyPath<Base, Value>) -> (Value) -> Builder<Base> { { [build = _build] value in Builder { let object = build() object[keyPath: keyPath] = value return object } } } public func build() -> Base { _build() } }
- 投稿日:2019-10-20T14:13:43+09:00
Xcode11でiOS13以前の端末で実行できるようにする方法
はじめに
Xcode11はSwiftUIやiPadOS向けのマルチタスク強化などあり、環境変化があると思います。
今回は、アプリの開発要件がiOS12以降などの場合にどうすればいいのかを手順書みたいにまとめます。実際にやってみる
今回は、iOS12.0以降にする前提で進めます。
githubにサンプルプロジェクトをアップしていますのであわせて参考にしてください。
(手順ごとにコミットを分けています。)手順1 iOS Develoyment Targetを変更する
プロジェクト設定のInfoタブより iOS Develoyment Target の項目を12.0にします。
手順2 ビルドエラー箇所を修正する
iOS13以前で非サポートメソッドが全てエラーとなります。
@available(iOS 13.0, *)
をつけてビルドエラーを解決します。
詳しい変更箇所は、githubのcommitログ を参考にしてください。手順3 デバックウィンドに表示されるエラーを解決する
ここまでで一見大丈夫そうなのですが、iOS12上で実行すると画面は真っ暗でなにも表示されません。
デバッグウィンドには [Application] The app delegate must implement the window property if it wants to use a main storyboard file. と表示されています。これは、storyboardファイルがうまく読み込めていないようです。
AppDelegate.swift
にvar window: UIWindow?
を追加することで解決できます。【
var window: UIWindow?
を追加した例】
以上で終わりです。
最後に
他の方も記載されていますが、いまいちよく分からなかったので自分なりにまとめました。
参考にした。記事のリンクを記載させていただきます。参考記事
- 投稿日:2019-10-20T14:13:43+09:00
Xcode11で新規作成したプロジェクトをiOS13以前の端末で実行できるようにする方法
はじめに
Xcode11はSwiftUIやiPadOS向けのマルチタスク強化などあり、環境変化があると思います。
今回は、アプリの開発要件がiOS12以降などの場合にどうすればいいのかを手順書みたいにまとめます。実際にやってみる
今回は、iOS12.0以降にする前提で進めます。
githubにサンプルプロジェクトをアップしていますのであわせて参考にしてください。
(手順ごとにコミットを分けています。)手順1 iOS Develoyment Targetを変更する
プロジェクト設定のInfoタブより iOS Develoyment Target の項目を12.0にします。
手順2 ビルドエラー箇所を修正する
iOS13以前で非サポートメソッドが全てエラーとなります。
@available(iOS 13.0, *)
をつけてビルドエラーを解決します。
詳しい変更箇所は、githubのcommitログ を参考にしてください。手順3 デバックウィンドに表示されるエラーを解決する
ここまでで一見大丈夫そうなのですが、iOS12上で実行すると画面は真っ暗でなにも表示されません。
デバッグウィンドには [Application] The app delegate must implement the window property if it wants to use a main storyboard file. と表示されています。これは、storyboardファイルがうまく読み込めていないようです。
AppDelegate.swift
にvar window: UIWindow?
を追加することで解決できます。【
var window: UIWindow?
を追加した例】
以上で終わりです。
最後に
他の方も記載されていますが、いまいちよく分からなかったので自分なりにまとめました。
参考にした。記事のリンクを記載させていただきます。参考記事
- 投稿日:2019-10-20T09:34:00+09:00
IOS develop!IOS 開発
—VietNamese—
Xin chào mọi người, hiện em/mình đang học về ios developer qua swift, thông qua một số khoá học online.
Udemy: https://www.udemy.com/course/ios11-app-development-bootcamp/learn/lecture/7719428?start=75#overview
Sau đó em/mình sẽ học https://www.raywenderlich.com/
và cuối cùng là khoá của standford online.
em/mình có một số thắc mắc khi học nên nhờ mọi người giúp đỡ.
Hiện tại em đang tự học , sau 1 năm tự học em có thể trở thành senior k? anh chị nào có kiến thức trước xin cho em kinh nghiệm và chia sẽ dùm em con đường và các khoá học tài liệu để e có thể định hình và đi tiếp ạ.
Hiện nay các thị trường ở singapore hay châu âu hay nhật bản việc tuyển dụng ios với senior ra sao ạ? Yêu cầu và lương ra sao ạ? từ một người mới học ios với tự học có thể sau 1 năm sẽ ứng tuyển được k ạ?
Mong có anh chị nào có kinh nghiệm trước đó xin được xin ý kiến ạ.
—English—
Hello everyone, now I am studing about iOS development on swift by online cousre on Udemy:
Link: https://www.udemy.com/course/ios11-app-development-bootcamp/learn/lecture/7719428?start=75#overview
then, I will learn https://www.raywenderlich.com/
after, I learn Stanford online.I have some question, can You help me?
1. Now I am studying swift, after 1 years only I can become senior developer? Can I do it? Can you give me advice to done this plan. Can you give me course, book, or seminar to help me do my plan. Thanks a lot
2. Now, What condition of IOS Developer in Singapore, Europe or Japan. ? after one years only studying, Can I apply?
Can you help me to answer?
---日本語---
みなさん、おはようございます。
現在、私はオンラインコースでIOS開発を学んでいます。
Udemy: https://www.udemy.com/course/ios11-app-development-bootcamp/learn/lecture/7719428?start=75#overview
このコースが終わった後、 https://www.raywenderlich.com/
最後はstandford online.
私は学ぶ中に質問がございます。
1。上のコースを学べば、1年間だけ上級開発ができますか?出来なければ、他のコース、本、セミナーがございましたら、教えてくれませんか?
2。私はシンガポールやヨーロッパや日本にてIOS開発募集について条件はなんでしょうか?1年間だけ勉強すれば、募集しても大丈夫でしょうか?
- 投稿日:2019-10-20T07:01:52+09:00
Azure Face API を利用して爆速で顔認証アプリを作る(パート2)
こんにちは、renです。
今回はAzure Face APIを利用して簡単な顔認証アプリを作っていきます。
こちらの記事はパート2(アプリ編)です
パート1(認証編)はこちら環境
macOS Catalina 10.15
Xcode 11.1
iOS13
Swift 5アプリの流れ
初期表示画面
↓
認証画面へ遷移
↓
カメラ起動
↓
顔検出
↓
画像として取得
↓
データ型に変換し、検出Face - Detect
APIに投げる
↓
レスポンスのfaceId
を 取得し、識別Face - identify
APIに投げる
↓
レスポンスのconfidence
をチェックし、 9割以上で認証を成功とみなす
↓
ホーム画面へ遷移準備
カメラを利用するアプリなので、
Info.plist
のPrivacy - Camera Usage Description
を有効化してください。下記ライブラリを利用するため
Podfile
を編集してください。pod 'Moya/RxSwift' pod 'RxSwift' pod 'SVProgressHUD'
pod install
を実行してください。pod install
APIRequestの実装
初めに
endpoint
やSubscriptionKey
をConst
に定義しておきましょう。public struct Const { static let endpoint = "Your Endpoint" static let subscriptionKey = "Your SubscriptionKey" static let baseURL = "https://\(endpoint)/face/v1.0/" static let personGroupId = "sample_person_group" }APIリクエストでは Moya を利用します。
Moya
を利用するために、まずTarget
を作成します。import Moya enum Target { case detect(imageData: Data) case identify(faceId: String) } extension Target: TargetType { var baseURL: URL { return URL(string: Const.baseURL)! } var path: String { switch self { case .detect: return "detect" case .identify: return "identify" } } var method: Moya.Method { switch self { case .detect: return .post case .identify: return .post } } var sampleData: Data { return Data() } var task: Task { switch self { case .detect(let imageData): return .requestCompositeData(bodyData: imageData, urlParameters: ["recognitionModel" : "recognition_02"]) case .identify(let faceId): let parameters: [String: Any] = [ "personGroupId": Const.personGroupId, "faceIds": [faceId] ] return .requestParameters(parameters: parameters, encoding: JSONEncoding.default) } } var headers: [String: String]? { var header = [ "Ocp-Apim-Subscription-Key" : Const.subscriptionKey ] switch self { case .detect: header["Content-Type"] = "application/octet-stream" return header case .identify: header["Content-Type"] = "application/json" return header } } }次は
APIRequest
クラスを作成します。import RxSwift import Moya import SVProgressHUD enum Result<ResponseModel: Codable, ErrorResponseModel: Codable> { case success(ResponseModel) case invalid(ErrorResponseModel) case failure(Error) } public final class APIRequest { private let provider = MoyaProvider<Target>() func request<ResponseModel: Codable, ErrorResponseModel: Codable> (target: Target, response: ResponseModel.Type, errorResponse: ErrorResponseModel.Type, completion: @escaping ((Result<ResponseModel, ErrorResponseModel>) -> Void )) { SVProgressHUD.setDefaultMaskType(.black) SVProgressHUD.show() provider.request(target) { result in SVProgressHUD.dismiss() switch result { case let .success(response): do { let serializedResponse = try response.filterSuccessfulStatusCodes().map(ResponseModel.self) dump(serializedResponse) completion(.success(serializedResponse)) } catch { guard let errorResponse = self.serializeError(response: response, errorResponse: errorResponse) else { completion(.failure(error)); return } dump(errorResponse) completion(.invalid(errorResponse)) } case let .failure(error): dump(error) completion(.failure(error)) } } } private func serializeError<ErrorResponseModel: Codable> (response: Moya.Response, errorResponse: ErrorResponseModel.Type) -> ErrorResponseModel? { do { let errorResponse = try response.map(ErrorResponseModel.self) dump(errorResponse) return errorResponse } catch { return nil } } }次は、それぞれのAPIのレスポンスモデルを作成します。
struct FaceDetectResponse: Codable { let faceId: String let faceRectangle: FaceRectangle struct FaceRectangle: Codable { let top: Int let left: Int let width: Int let height: Int } } struct FaceIdentifyResponse: Codable { let faceId: String let candidates: [Candidates] struct Candidates: Codable { let personId: String let confidence: Double } } struct ErrorResponse: Codable { public let error: ErrorStatus struct ErrorStatus: Codable { let code: String let message: String } }これにてAPIリクエストの部分は完成です。
Viewの作成
次に
View
を作成します。下の画像を参考に作成してください。真ん中のView(FaceAuthView)のViewControllerを作成します。
import UIKit class FaceAuthViewController: UIViewController { // カメラの映像が映るView @IBOutlet private weak var cameraView: UIView! // cameraViewに覆いかぶさっているView @IBOutlet private weak var overlayView: UIView! // overlayViewの中心の透明なView @IBOutlet private weak var centerView: UIView! // アラートを表示するためのLabel @IBOutlet private weak var alertLabel: UILabel! // 顔検出を行うクラス private var faceDetecer: FaceDetecer? // 顔の周りに表示する枠 private let frameView = UIView() // 顔検出されたときの画像 private var image = UIImage() // APIリクエストを行うクラス private let apiRequest = APIRequest() override func viewDidLoad() { setupFrameView() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) setupFaceDetecer() dispOverlayView() } override func viewWillDisappear(_ animated: Bool) { if let faceDetecer = faceDetecer { faceDetecer.stopRunning() } faceDetecer = nil } private func setupFrameView() { frameView.layer.borderWidth = 3 view.addSubview(frameView) } private func setupFaceDetecer() { faceDetecer = FaceDetecer(view: cameraView, completion: {faceRect, image in self.frameView.frame = faceRect self.image = image self.isInFrame(faceRect: faceRect) }) } private func dispOverlayView() { overlayView.isHidden = false } private func isInFrame(faceRect: CGRect) { let xIsInFrame = centerView.frame.minX < faceRect.minX && faceRect.maxX < centerView.frame.maxX let yIsInFrame = centerView.frame.minY < faceRect.minY && faceRect.maxY < centerView.frame.maxY if xIsInFrame && yIsInFrame { stopRunning() faceDetect(image: image) } } private func faceDetect(image: UIImage) { guard let imageData = image.jpegData(compressionQuality: 1.0) else { return } apiRequest.request(target: .detect(imageData: imageData), response: [FaceDetectResponse].self, errorResponse: ErrorResponse.self) { respose in switch respose { case .success(let faceDetectResponse): guard let faceId = faceDetectResponse.first?.faceId else { self.alertLabel.text = "顔認証に失敗しました" self.startRunning() return } self.faceIdentify(faceId: faceId) case .invalid(let errorResponse): print(errorResponse) self.startRunning() case .failure(let error): print(error) self.startRunning() } } } private func faceIdentify(faceId: String) { apiRequest.request(target: .identify(faceId: faceId), response: [FaceIdentifyResponse].self, errorResponse: ErrorResponse.self) { respose in switch respose { case .success(let faceIdentifyResponse): // 最初の顔で判定 guard let candidate = faceIdentifyResponse.first?.candidates.first?.confidence else { self.alertLabel.text = "顔が登録されていません" self.startRunning() return } let candidateInt = Int(candidate * 100) self.alertLabel.text = "信頼度は \(candidateInt)% です" if candidate > 0.9 { self.login() } else { self.startRunning() } case .invalid(let errorResponse): print(errorResponse) self.startRunning() case .failure(let error): print(error) self.startRunning() } } } private func startRunning() { guard let faceDetecer = faceDetecer else { return } faceDetecer.startRunning() } private func stopRunning() { guard let faceDetecer = faceDetecer else { return } faceDetecer.stopRunning() } private func login() { self.performSegue(withIdentifier: "gotoHome", sender: nil) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } @IBAction private func tappedBackButton(_ sender: UIButton) { dismiss(animated: true, completion: nil) } }顔検出を行うクラス
FaceDetecer
を作成import UIKit import AVFoundation final class FaceDetecer: NSObject { private let captureSession = AVCaptureSession() private var videoDataOutput = AVCaptureVideoDataOutput() private var view: UIView private var completion: (_ rect: CGRect, _ image: UIImage) -> Void required init(view: UIView, completion: @escaping (_ rect: CGRect, _ image: UIImage) -> Void) { self.view = view self.completion = completion super.init() self.initialize() } private func initialize() { addCaptureSessionInput() registerDelegate() setVideoDataOutput() addCaptureSessionOutput() addVideoPreviewLayer() setCameraOrientation() startRunning() } private func addCaptureSessionInput() { do { guard let frontVideoCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else { return } let frontVideoCameraInput = try AVCaptureDeviceInput(device: frontVideoCamera) as AVCaptureDeviceInput captureSession.addInput(frontVideoCameraInput) } catch let error { print(error) } } private func setVideoDataOutput() { videoDataOutput.alwaysDiscardsLateVideoFrames = true guard let pixelFormatTypeKey = kCVPixelBufferPixelFormatTypeKey as AnyHashable as? String else { return } let pixelFormatTypeValue = Int(kCVPixelFormatType_32BGRA) videoDataOutput.videoSettings = [pixelFormatTypeKey : pixelFormatTypeValue] } private func setCameraOrientation() { for connection in videoDataOutput.connections where connection.isVideoOrientationSupported { connection.videoOrientation = .portrait connection.isVideoMirrored = true } } private func registerDelegate() { let queue = DispatchQueue(label: "queue", attributes: .concurrent) videoDataOutput.setSampleBufferDelegate(self, queue: queue) } private func addCaptureSessionOutput() { captureSession.addOutput(videoDataOutput) } private func addVideoPreviewLayer() { let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) videoPreviewLayer.frame = view.bounds videoPreviewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(videoPreviewLayer) } func startRunning() { captureSession.startRunning() } func stopRunning() { captureSession.stopRunning() } private func convertToImage(from sampleBuffer: CMSampleBuffer) -> UIImage? { guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil } CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) let baseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) let width = CVPixelBufferGetWidth(imageBuffer) let height = CVPixelBufferGetHeight(imageBuffer) let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer) let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) guard let imageRef = context?.makeImage() else { return nil } CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) let resultImage = UIImage(cgImage: imageRef) return resultImage } } extension FaceDetecer: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { DispatchQueue.main.sync(execute: { guard let image = convertToImage(from: sampleBuffer), let ciimage = CIImage(image: image) else { return } guard let detector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) else { return } guard let feature = detector.features(in: ciimage).first else { return } sendFaceRect(feature: feature, image: image) }) } private func sendFaceRect(feature: CIFeature, image: UIImage) { var faceRect = feature.bounds let widthPer = view.bounds.width / image.size.width let heightPer = view.bounds.height / image.size.height // 原点を揃える faceRect.origin.y = image.size.height - faceRect.origin.y - faceRect.size.height // 倍率変換 faceRect.origin.x *= widthPer faceRect.origin.y *= heightPer faceRect.size.width *= widthPer faceRect.size.height *= heightPer completion(faceRect, image) } }これで実装は終了です。
AzureFaceAPIは無料プランだと1分間に20回までしかリクエストを送れないのでご注意ください。
あとがき
今回はリアルタイム顔検出とAzureFaceAPIの連携で顔認証アプリを作成しました。
初めてカメラの機能を実装したのですが、思ったよりも簡単にできて驚きました。
これも普段からQiitaを書いてくださる皆様のおかげだと思います。ありがとうございます。サンプルアプリはGitHubに公開しています。
https://github.com/renchild8/FaceAuthSampleこの記事は下記の記事を参考にしています。
[コピペで使える]swift3/swift4/swift5でリアルタイム顔認識をする方法
- 投稿日:2019-10-20T06:57:38+09:00
Azure Face API を利用して爆速で顔認証アプリを作る(パート1)
こんにちは、renです。
今回は
Azure Face API
を利用して簡単な顔認証アプリを作っていきます。こちらの記事はパート1(認証編)です
パート2(アプリ編)はこちら準備
- Azureアカウントの取得
- リソースグループの作成
- リソースの作成
endpoint
,Subscription key
の取得※ 詳しい情報は公式をご参照ください
認証の流れ
検出
Face - Detect
APIに投げる
↓
レスポンスのfaceId
を 取得し、識別Face - Identify
APIに投げる認証の準備
認証を行うためには、認証の対象を作る必要があります。
Face - Identify
ではPersonGroup
(またはLargePersonGroup
)というグループに対して認証を行います。
PersonGroup
とはその名の通りで人のグループです。
まずPersonGroup
を作成し、そこに人の情報PersonGroup Person
を作成します。
その後PersonGroup Person
に対して、顔の情報を追加します。
最後にPersonGroup
をトレーニングすることで認証の対象が作成されます。フロー
PersonGroup - Create
でPersonGroup
を作成
↓
PersonGroup Person - Create
でPersonGroup
にPerson
を作成
↓
PersonGroup Person - Add Face
でPerson
にFace
を追加(20枚くらい)
↓
PersonGroup - Train
でPersonGroup
をトレーニングする認証対象の作成
アプリ側で認証対象を作成するのは面倒なので
cURL
で作成していきます。
${ENDPOINT}
と${SUBSCRIPTION_KEY}
は取得したものに置き換えてください。予め
export
しておくとコピペで動かせます。export ENDPOINT=XXXXX_YOUR_ENDPOINT_XXXXX export SUBSCRIPTION_KEY=XXXXX_YOUR_SUBSCRIPTION_KEY_XXXXX1.PersonGroup - Create で
PersonGroup
を作成
sample_person_group
というPersonGroup
を作成します。Request URL
https://{endpoint}/face/v1.0/persongroups/{personGroupId}
Request parameters
personGroupId
(必須) :PersonGroup
のIDRequest body
name
(必須) :PersonGroup
の表示名
userData
: ユーザー提供のデータ
recognitionModel
: 認識モデル
recognitionModel
について
recognition_01
Face-Detectのデフォルトの認識モデル。
2019年3月より前に作成されたすべてのfaceIdは、この認識モデルに結合されます。
recognition_02
2019年3月にリリースされた認識モデル。
「recognition_01」と比較して全体的な精度が向上しているため、「recognition_02」をお勧めします。今回は
recognition_02
を使用します。サンプル
curl -X PUT "https://${ENDPOINT}/face/v1.0/persongroups/sample_person_group" -H "Ocp-Apim-Subscription-Key: ${SUBSCRIPTION_KEY}" -H "Content-Type: application/json" -d "{\"name\":\"SamplePersonGroup\",\"userData\":\"Sample\",\"recognitionModel\":\"recognition_02\"}"成功時レスポンスなしちゃんと作成できているか確認してみましょう。
PersonGroup - Get を利用するとPersonGroup
の情報を取得できます。curl "https://${ENDPOINT}/face/v1.0/persongroups/sample_person_group?returnRecognitionModel=true" -H "Ocp-Apim-Subscription-Key: ${SUBSCRIPTION_KEY}"{ "recognitionModel": "recognition_02", "personGroupId": "sample_person_group", "name": "SamplePersonGroup", "userData": "Sample" }2.PersonGroup Person - Create で
PersonGroup
にPerson
を作成作成した
sample_person_group
にrenchild
というPerson
を作成します。Request URL
https://{endpoint}/face/v1.0/persongroups/{personGroupId}/persons
Request parameters
personGroupId
(必須) :PersonGroup
のIDRequest body
name
(必須) :Person
の表示名
userData
: ユーザー提供のデータサンプル
curl -X POST "https://${ENDPOINT}/face/v1.0/persongroups/sample_person_group/persons" -H "Ocp-Apim-Subscription-Key: ${SUBSCRIPTION_KEY}" -H "Content-Type: application/json" -d "{\"name\":\"renchild\", \"userData\":\"Sample Person\"}"{ "personId": "53d462fe-c56b-40f2-b435-5003e5eb0297" }ちゃんと作成できているか確認してみましょう。
PersonGroup Person - Get を利用するとPerson
の情報を取得できます。curl "https://${ENDPOINT}/face/v1.0/persongroups/sample_person_group/persons/53d462fe-c56b-40f2-b435-5003e5eb0297" -H "Ocp-Apim-Subscription-Key: ${SUBSCRIPTION_KEY}"{ "personId": "53d462fe-c56b-40f2-b435-5003e5eb0297", "persistedFaceIds": [], "name": "renchild", "userData": "Sample Person" }補足
PersonGroup Person - List を利用すると
PersonGroup
内のPerson
の情報を一覧で取得できます。curl "https://${ENDPOINT}/face/v1.0/persongroups/sample_person_group/persons" -H "Ocp-Apim-Subscription-Key: ${SUBSCRIPTION_KEY}"[ { "personId": "53d462fe-c56b-40f2-b435-5003e5eb0297", "persistedFaceIds": [], "name": "renchild", "userData": "Sample Person" } ]3.PersonGroup Person - Add Face で
Person
にFace
を追加作成した
renchild
にFace
を追加します(20枚くらいあるといいらしい)Request URL
https://{endpoint}/face/v1.0/persongroups/{personGroupId}/persons/{personId}/persistedFaces[?userData][&targetFace][&detectionModel]
Request parameters
personGroupId
(必須) :PersonGroup
のID
personId
(必須) :Person
のID
userData
: ユーザー提供のデータ
targetFace
: 追加するターゲットの顔を指定するための四角形
detectionModel
: 顔検出モデル
detectionModel
について
detection_01
すべての顔検出操作に対する既定の選択です。
小さい顔、横顔、不鮮明な顔には最適化されていません。
検出呼び出しで顔の属性 (頭部姿勢、年齢、感情など) が指定されている場合は、それらを返します。
検出呼び出しで顔のランドマークが指定されている場合は、それらを返します。
detection_02
2019 年 5 月にリリースされ、すべての顔検出操作でオプションとして利用可能です。
小さい顔、横顔、不鮮明な顔での精度が向上しています。
顔の属性を返しません。
顔のランドマークを返しません。今回は
detection_02
を使用します。Request body
画像のURLまたは画像のバイナリデータ
サンプル
curl -X POST "https://${ENDPOINT}/face/v1.0/persongroups/sample_person_group/persons/53d462fe-c56b-40f2-b435-5003e5eb0297/persistedFaces?detectionModel=detection_02" -H "Ocp-Apim-Subscription-Key: ${SUBSCRIPTION_KEY}" -H "Content-Type: application/octet-stream" --data-binary "@image.jpg"4.PersonGroup - Train で
PersonGroup
をトレーニングするトレーニングの前に
PersonGroup Person - List
を利用してPersonGroup
内のPerson
の情報を確認しましょう。curl "https://${ENDPOINT}/face/v1.0/persongroups/sample_person_group/persons" -H "Ocp-Apim-Subscription-Key: ${SUBSCRIPTION_KEY}"[ { "personId": "53d462fe-c56b-40f2-b435-5003e5eb0297", "persistedFaceIds": [ "00f850fd-1f68-46ed-a7cc-5c0db171f4cf", "3f665c92-a141-4544-818c-36a63b39ac5b", "5ce289ec-362e-4be5-8472-18b71674137c", "733ceabc-4ef2-4d10-ba57-cfe2ee8ad7b2", "751850ac-4d54-4011-8f3c-db2c2b48dbb2", ︙ "78cdf5f0-e3ea-4cd8-ba4b-eb156a14faf7", "7f2a834b-5e9a-4238-a099-8e2cc3c1021e", "85d4dc2c-17d0-4f97-9c9d-460b4512dd56", "990778dc-be2a-4a1f-9281-21fcb6aa6b7c", "a511e64b-8f1b-478e-b83a-753e516a93d9", "a735d0e1-26f1-4e50-aafa-9a48a3ff07e0", "b40f1ed1-91ea-4e8d-84f7-f113dccc1bbb" ], "name": "renchild", "userData": "Sample Person" } ]こんな感じで出ればOKです。
PersonGroup
を トレーニングRequest URL
https://{endpoint}/face/v1.0/persongroups/{personGroupId}/train
Request parameters
personGroupId
(必須) :PersonGroup
のIDRequest body
なし
サンプル
curl -X POST "https://${ENDPOINT}/face/v1.0/persongroups/sample_person_group/train" -H "Ocp-Apim-Subscription-Key: ${SUBSCRIPTION_KEY}" -H "Content-Length: 0"成功時レスポンスなしこれにて認証の準備は完了です。
認証
認証を行う際に
faceId
が必要です。
faceId
はFace - Detect
にて生成される顔の特徴の識別子です。
保存されたfaceId
は24時間後に期限切れになり、削除されます。※ 先程
PersonGroup
に登録したpersistedFaceIds
は24時間の制限はなく、永続化されます。Face - Detect で顔を検出する
Request URL
https://{endpoint}/face/v1.0/detect[?returnFaceId][&returnFaceLandmarks][&returnFaceAttributes][&recognitionModel][&returnRecognitionModel][&detectionModel]
Request parameters
returnFaceId
: 検出された顔のfaceId
を返すかどうか。デフォルト値はtrue
returnFaceLandmarks
: 検出された顔の顔のランドマークを返すかどうか。デフォルト値はfalse
returnFaceAttributes
: 年齢、性別、眼鏡、感情 などを分析する属性
recognitionModel
: 認識モデル
returnRecognitionModel
:recognitionModel
を返すかどうか。デフォルト値はfalse
です。
detectionModel
: 顔検出モデル
PersonGroup
の作成時にrecognitionModel
をrecognition_02
に指定しているので、
今回もrecognitionModel
をrecognition_02
に指定します。
recognitionModel
が異なる場合Face - Identify
にて識別を行うことができなくなります。Request body
画像のURLまたは画像のバイナリデータ
サンプル
curl -X POST "https://${ENDPOINT}/face/v1.0/detect?recognitionModel=recognition_02" -H "Ocp-Apim-Subscription-Key: ${SUBSCRIPTION_KEY}" -H "Content-Type: application/octet-stream" --data-binary "@image.jpg" | jq .[ { "faceId": "5e2d9794-3fba-4324-88bd-6ec2944583e0", "faceRectangle": { "top": 679, "left": 667, "width": 969, "height": 969 } } ]Face - Identify で顔を識別する
Face - Detect
で取得したfaceId
で顔の識別を行うRequest URL
https://{endpoint}/face/v1.0/identify
Request body
faceIds
(必須) :Face - Detect
によって作成されたfaceId
の配列
personGroupId
(※必須) :PersonGroup - Create
によって作成されたpersonGroupId
largePersonGroupId
(※必須) :LargePersonGroup - Create
によって作成されたlargePersonGroupId
maxNumOfCandidatesReturned
: 候補者の最大数の範囲。 1〜100の範囲(デフォルトは10)
ConfidenceThreshold
: 0~1 の範囲のカスタマイズされた信頼度しきい値※ パラメーター
personGroupId
とlargePersonGroupId
を同時に指定しないでください。サンプル
curl -X POST "https://${ENDPOINT}/face/v1.0/identify" -H "Ocp-Apim-Subscription-Key: ${SUBSCRIPTION_KEY}" -H "Content-Type: application/json" -d "{\"personGroupId\": \"sample_person_group\", \"faceIds\": [\"5e2d9794-3fba-4324-88bd-6ec2944583e0\"]}"[ { "faceId": "5e2d9794-3fba-4324-88bd-6ec2944583e0", "candidates": [ { "personId": "53d462fe-c56b-40f2-b435-5003e5eb0297", "confidence": 0.93879 } ] } ]レスポンスの
confidence
が信頼度です。
こちらの値を利用して判定を行うことになります。あとがき
準備が若干面倒ですが、数十枚の画像データで結構いい精度で判定してくれるのでみなさんも試してみてください。
次回はアプリとの連携を行います。パート2(アプリ編)はこちら
- 投稿日:2019-10-20T06:54:12+09:00
iOSでリアルタイム顔検出を行う
こんにちはrenです。
今回はリアルタイム顔検出のアプリを作っていきます。
一週間程度でぱぱっと作ったものなので、間違いなどあればご指摘等いただけると嬉しいです。環境
macOS Catalina 10.15
Xcode 11.1
iOS13
Swift 5流れ
「Login」ボタンをタップ
↓
フロントカメラを起動する
↓
出力された映像を切り出し、画像に変換する
↓
画像から顔を検出する
↓
検出された顔の座標にフレームを表示させる準備
今回はカメラを利用するので
Info.plist
にPrivacy - Camera Usage Description
を追加してください実装
// 初期表示View import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBAction func tappedLogin(_ sender: Any) { self.performSegue(withIdentifier: "gotoFaceDetect", sender: nil) } }// 顔検出をするView import UIKit class FaceDetectViewController: UIViewController { @IBOutlet weak var cameraView: UIView! // 顔検出をするためのクラス private var faceDetecter: FaceDetecter? // 検出された顔のフレームを表示するためのView private let frameView = UIView() // 切り出された画像 private var image = UIImage() override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) setup() } override func viewWillDisappear(_ animated: Bool) { if let faceDetecter = faceDetecter { faceDetecter.stopRunning() } faceDetecter = nil } private func setup() { frameView.layer.borderWidth = 3 view.addSubview(frameView) faceDetecter = FaceDetecter(view: cameraView, completion: {faceRect, image in self.frameView.frame = faceRect self.image = image }) } private func stopRunning() { guard let faceDetecter = faceDetecter else { return } faceDetecter.stopRunning() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } @IBAction func tappedBackButton(_ sender: UIButton) { dismiss(animated: true, completion: nil) } }// 顔検出をするクラス import UIKit import AVFoundation final class FaceDetecter: NSObject { private let captureSession = AVCaptureSession() private var videoDataOutput = AVCaptureVideoDataOutput() private var view: UIView private var completion: (_ rect: CGRect, _ image: UIImage) -> Void required init(view: UIView, completion: @escaping (_ rect: CGRect, _ image: UIImage) -> Void) { self.view = view self.completion = completion super.init() self.initialize() } private func initialize() { addCaptureSessionInput() registerDelegate() setVideoDataOutput() addCaptureSessionOutput() addVideoPreviewLayer() setCameraOrientation() startRunning() } private func addCaptureSessionInput() { do { guard let frontVideoCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else { return } let frontVideoCameraInput = try AVCaptureDeviceInput(device: frontVideoCamera) as AVCaptureDeviceInput captureSession.addInput(frontVideoCameraInput) } catch let error { print(error) } } private func setVideoDataOutput() { videoDataOutput.alwaysDiscardsLateVideoFrames = true guard let pixelFormatTypeKey = kCVPixelBufferPixelFormatTypeKey as AnyHashable as? String else { return } let pixelFormatTypeValue = Int(kCVPixelFormatType_32BGRA) videoDataOutput.videoSettings = [pixelFormatTypeKey : pixelFormatTypeValue] } private func setCameraOrientation() { for connection in videoDataOutput.connections where connection.isVideoOrientationSupported { connection.videoOrientation = .portrait connection.isVideoMirrored = true } } private func registerDelegate() { let queue = DispatchQueue(label: "queue", attributes: .concurrent) videoDataOutput.setSampleBufferDelegate(self, queue: queue) } private func addCaptureSessionOutput() { captureSession.addOutput(videoDataOutput) } private func addVideoPreviewLayer() { let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) videoPreviewLayer.frame = view.bounds videoPreviewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(videoPreviewLayer) } func startRunning() { captureSession.startRunning() } func stopRunning() { captureSession.stopRunning() } private func convertToImage(from sampleBuffer: CMSampleBuffer) -> UIImage? { guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil } CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) let baseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) let width = CVPixelBufferGetWidth(imageBuffer) let height = CVPixelBufferGetHeight(imageBuffer) let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer) let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) guard let imageRef = context?.makeImage() else { return nil } CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) let resultImage = UIImage(cgImage: imageRef) return resultImage } } extension FaceDetecter: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { DispatchQueue.main.sync(execute: { guard let image = convertToImage(from: sampleBuffer), let ciimage = CIImage(image: image) else { return } guard let detector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) else { return } guard let feature = detector.features(in: ciimage).first else { return } sendFaceRect(feature: feature, image: image) }) } private func sendFaceRect(feature: CIFeature, image: UIImage) { var faceRect = feature.bounds let widthPer = view.bounds.width / image.size.width let heightPer = view.bounds.height / image.size.height // 原点を揃える faceRect.origin.y = image.size.height - faceRect.origin.y - faceRect.size.height // 倍率変換 faceRect.origin.x *= widthPer faceRect.origin.y *= heightPer faceRect.size.width *= widthPer faceRect.size.height *= heightPer completion(faceRect, image) } }この記事は下記の記事を参考にしています。
[コピペで使える]swift3/swift4/swift5でリアルタイム顔認識をする方法
- 投稿日:2019-10-20T06:29:04+09:00
[iOS]アプリ申請完全自動化
アプリの申請を完全に自動化する
ソースコードをpushしたら、あとは何も考えなくてもストアにリリースされる。
そんな環境を作ってみたのでメモ
まだちゃんと動かなさそう(fastlane_sessionが1ヶ月でexpireする)全体の流れ
↑こんな感じ
Bitriseの部分は別になんでも良いのだけど、無料枠があったのでBitriseにしている。
automate.io は、いわゆるIFTTTのようなサービス。
似たようなサービスでメルカリの採用しているZapierと言うのがあるが、カスタムWebhookがプレミアム専用なのでautomate.ioを使うことにした。
正直Zapierの方がsampleみながらzap作れるしgmailの取得速いし、正規表現ちゃんと使えるのでZapierの方がおすすめです。GithubにpushしてBitriseからappstore connectへアップロードする
ここは簡単。
bitrise.ioにサインアップしたら、チュートリアルの通りに進めばリポジトリとbitriseの連携が完了する。
あとは「Deploy to iTunes Connect - Application Loader」というstepを追加して、適宜情報を書き込めばアップロードされるようになる。
注意点としては、build numberをincrementすることを忘れずに。同じビルド番号のipaはアップロードに失敗します。
build numberの設定は「Set Xcode Project Build Number」というStepを使えば簡単に設定できます。
そして、書いていて思ったけどこの時点でappstore connectに新規のバージョンが無い可能性があるのでここはfastlaneのdeliver使った方がいいですね。AppStore ConnectのCompleted Processを待つ
ipaをアップロードすると、appleは自動的にipaの健康状態をチェックしてくれます。(悪いことしているとこの時点で弾かれる)
ここは大体数分から数時間と幅があるのでひたすら待ちます。
待っていると、AppleからApp Store Connect: Version 1.0 (1) for AppName has completed processing.
というタイトルのメールが届きます。Completed Processingのメールが届いたらbitriseのworkflowを発火させる
automate.ioを使ってgmailを監視して、メールが届いたらbitirseのapiを叩きます。
メールにはbuild numberとversionも記載されているので、これらをパースしてbitriseの環境変数に乗せておきます。automate.ioのbotの中身はこんな感じ。
正規表現機能がガバガバなので、タイトルをスペースで区切って取り出しています。bitriseからサブミットする
automate.ioのwebhookから叩かれたworkflowでfastlaneのdeliverを呼びます。
この辺は特に難しいところはないです。
サブミットするビルドのbundle numberなどは環境変数に入っているのでそれを使います。
fastlaneのascへの接続はFASTLANE_SESSIONを利用していますが、2FA切ったアカウントで認証しても良いと思います。完成
あとはbitriseのtriggerなどを調整しながら、使いやすいようにしていきましょう。
- 投稿日:2019-10-20T06:29:04+09:00
[iOS]アプリ申請完全自動化したい
アプリの申請を完全に自動化する
ソースコードをpushしたら、あとは何も考えなくてもストアにリリースされる。
そんな環境を作ってみたのでメモ
まだちゃんと動かなさそう全体の流れ
↑こんな感じ
Bitriseの部分は別になんでも良いのだけど、無料枠があったのでBitriseにしている。
automate.io は、いわゆるIFTTTのようなサービス。
似たようなサービスでメルカリの採用しているZapierと言うのがあるが、カスタムWebhookがプレミアム専用なのでautomate.ioを使うことにした。
正直Zapierの方がsampleみながらzap作れるしgmailの取得速いし、正規表現ちゃんと使えるのでZapierの方がおすすめです。GithubにpushしてBitriseからappstore connectへアップロードする
ここは簡単。
bitrise.ioにサインアップしたら、チュートリアルの通りに進めばリポジトリとbitriseの連携が完了する。
あとは「Deploy to iTunes Connect - Application Loader」というstepを追加して、適宜情報を書き込めばアップロードされるようになる。
注意点としては、build numberをincrementすることを忘れずに。同じビルド番号のipaはアップロードに失敗します。
build numberの設定は「Set Xcode Project Build Number」というStepを使えば簡単に設定できます。
そして、書いていて思ったけどこの時点でappstore connectに新規のバージョンが無い可能性があるのでここはfastlaneのdeliver使った方がいいですね。AppStore ConnectのCompleted Processを待つ
ipaをアップロードすると、appleは自動的にipaの健康状態をチェックしてくれます。(悪いことしているとこの時点で弾かれる)
ここは大体数分から数時間と幅があるのでひたすら待ちます。
待っていると、AppleからApp Store Connect: Version 1.0 (1) for AppName has completed processing.
というタイトルのメールが届きます。Completed Processingのメールが届いたらbitriseのworkflowを発火させる
automate.ioを使ってgmailを監視して、メールが届いたらbitirseのapiを叩きます。
メールにはbuild numberとversionも記載されているので、これらをパースしてbitriseの環境変数に乗せておきます。automate.ioのbotの中身はこんな感じ。
正規表現機能がガバガバなので、タイトルをスペースで区切って取り出しています。bitriseからサブミットする
automate.ioのwebhookから叩かれたworkflowでfastlaneのdeliverを呼びます。
この辺は特に難しいところはないです。
サブミットするビルドのbundle numberなどは環境変数に入っているのでそれを使います。
fastlaneのascへの接続はFASTLANE_SESSIONを利用していますが、2FA切ったアカウントで認証しても良いと思います。完成
あとはbitriseのtriggerなどを調整しながら、使いやすいようにしていきましょう。