- 投稿日:2019-02-27T23:00:00+09:00
【SwiftChaining】UIとバインディングする
SwiftChainingの解説記事その2です。
前回の記事はこちら -> Swiftでデータバインディングするライブラリを作ったiOSアプリでデータバインディングをするからには、UIと値を同期できないと始まりません。
今回は例として、
UISwitch
とバインディングする例を紹介します。UISwitchとバインディングする
UISwitch
のスイッチのON・OFFの状態であるisOn
と、前回の記事で紹介したValueHolder
の値をバインディングしてみます。コードは以下の通りです。import UIKit import Chaining class ExampleViewController: UIViewController { @IBOutlet weak var mySwitch: UISwitch! lazy var isOnAdapter = { KVOAdapter(self.mySwitch, keyPath: \UISwitch.isOn )}() lazy var valueChangedAdapter = { UIControlAdapter(self.mySwitch, events: .valueChanged) }() let isOnHolder = ValueHolder(true) var pool = ObserverPool() override func viewDidLoad() { super.viewDidLoad() self.pool += self.isOnHolder.chain().sendTo(self.isOnAdapter).sync() self.pool += self.valueChangedAdapter.chain().map { $0.isOn }.sendTo(self.isOnHolder).end() } }解説
KVOに対応したプロパティを
SwiftChaining
で扱えるようにするためのクラスがKVOAdapter
です。KVOAdapter(self.mySwitch, keyPath: \UISwitch.isOn )対象となるオブジェクトとプロパティの
keyPath
を渡して生成し、値を監視したり更新したりできます。
ValueHolder
同士の場合と同じく、以下のようにsendTo
にKVOAdapter
を渡すことでValueHolder
からの値を受け取ることができます。self.isOnHolder.chain().sendTo(self.isOnAdapter).sync()逆に
KVOAdapter
からisOn
の変更を受け取るのには、以下のように逆方向に繋げれば良いのですが、今回の場合、残念ながらうまくいきません。// うまくいかない self.isOnAdapter.chain().sendTo(self.isOnHolder).sync()
UISwitch
をタップしてON・OFFの状態を変更したときはKVOでは通知されないので、UIControl
のイベントを監視するようにします。
UIControl
のイベントを監視するクラスとしてUIControlAdapter
を用意しています。UIControlAdapter(self.mySwitch, events: .valueChanged)
events
で監視したいUIControl
のイベントを指定します。
UIControlAdapter
からイベントを監視してValueHolder
に値を反映させるには以下のように書きます。self.valueChangedAdapter.chain().map { $0.isOn }.sendTo(self.isOnHolder).end()
UIControlAdapter
から送られる値はUIControl
(ここではUISwitch
)なので途中でmap
関数を使ってisOn
の値に変換してValueHolder
が受け取れるようにしています。このように、それぞれ変更があった時に双方向に値を送り合うことで、値を同期することができるという感じになります
SwiftChaining
では値の送信が循環しても延々とループにならないように内部でロックしているので、それこそchain
したオブジェクトをsendTo
にそのまま渡すようなことをしてもハングアップしたりはしません。ちなみに、今回のコードで出てきたクラスに
ObserverPool
というものがあります。var pool = ObserverPool() ... self.pool += self.isOnHolder.chain().sendTo(self.isOnAdapter).sync()複数のObserverを保持しておかないといけない時に別々に管理するのは面倒なので、Observerをまとめて保持しておけるものとして用意しています。
ObserverPool
を破棄したりinvalidate()
を呼んだら、保持しているObserverがまとめて無効になります。Observerの追加を関数でやると扱いにくそうだったので、
+=
で追加できるようにしています。補足
なお今回紹介したコードだと、
ValueHolder
からUISwitch
へ値を反映させる時にアニメーションをしなかったり、UISwitch
のisOn
をコードで直接変更した時にValueHolder
へ値が反映されません。それらが必要であれば、さらにコードを追加・変更することになるでしょう。
SwiftChaining
はUIKit
をラップして簡単にするものではないので、このようなUIKit
の対応は通常と変わらず考える必要があります。
- 投稿日:2019-02-27T23:00:00+09:00
【SwiftChaining】 UIとバインディングする
SwiftChainingの解説記事その2です。
前回の記事はこちら -> Swiftでデータバインディングするライブラリを作ったiOSアプリでデータバインディングをするからには、UIと値を同期できないと始まりません。
今回は例として、
UISwitch
とバインディングする例を紹介します。UISwitchとバインディングする
UISwitch
のスイッチのON・OFFの状態であるisOn
と、前回の記事で紹介したValueHolder
の値をバインディングしてみます。コードは以下の通りです。import UIKit import Chaining class ExampleViewController: UIViewController { @IBOutlet weak var mySwitch: UISwitch! lazy var isOnAdapter = { KVOAdapter(self.mySwitch, keyPath: \UISwitch.isOn )}() lazy var valueChangedAdapter = { UIControlAdapter(self.mySwitch, events: .valueChanged) }() let isOnHolder = ValueHolder(true) var pool = ObserverPool() override func viewDidLoad() { super.viewDidLoad() self.pool += self.isOnHolder.chain().sendTo(self.isOnAdapter).sync() self.pool += self.valueChangedAdapter.chain().map { $0.isOn }.sendTo(self.isOnHolder).end() } }解説
KVOに対応したプロパティを
SwiftChaining
で扱えるようにするためのクラスがKVOAdapter
です。KVOAdapter(self.mySwitch, keyPath: \UISwitch.isOn )対象となるオブジェクトとプロパティの
keyPath
を渡して生成し、値を監視したり更新したりできます。
ValueHolder
同士の場合と同じく、以下のようにsendTo
にKVOAdapter
を渡すことでValueHolder
からの値を受け取ることができます。self.isOnHolder.chain().sendTo(self.isOnAdapter).sync()逆に
KVOAdapter
からisOn
の変更を受け取るのには、以下のように逆方向に繋げれば良いのですが、今回の場合、残念ながらうまくいきません。// うまくいかない self.isOnAdapter.chain().sendTo(self.isOnHolder).sync()
UISwitch
をタップしてON・OFFの状態を変更したときはKVOでは通知されないので、UIControl
のイベントを監視するようにします。
UIControl
のイベントを監視するクラスとしてUIControlAdapter
を用意しています。UIControlAdapter(self.mySwitch, events: .valueChanged)
events
で監視したいUIControl
のイベントを指定します。
UIControlAdapter
からイベントを監視してValueHolder
に値を反映させるには以下のように書きます。self.valueChangedAdapter.chain().map { $0.isOn }.sendTo(self.isOnHolder).end()
UIControlAdapter
から送られる値はUIControl
(ここではUISwitch
)なので途中でmap
関数を使ってisOn
の値に変換してValueHolder
が受け取れるようにしています。このように、それぞれ変更があった時に双方向に値を送り合うことで、値を同期することができるという感じになります
SwiftChaining
では値の送信が循環しても延々とループにならないように内部でロックしているので、それこそchain
したオブジェクトをsendTo
にそのまま渡すようなことをしてもハングアップしたりはしません。ちなみに、今回のコードで出てきたクラスに
ObserverPool
というものがあります。var pool = ObserverPool() ... self.pool += self.isOnHolder.chain().sendTo(self.isOnAdapter).sync()複数のObserverを保持しておかないといけない時に別々に管理するのは面倒なので、Observerをまとめて保持しておけるものとして用意しています。
ObserverPool
を破棄したりinvalidate()
を呼んだら、保持しているObserverがまとめて無効になります。Observerの追加を関数でやると扱いにくそうだったので、
+=
で追加できるようにしています。補足
なお今回紹介したコードだと、
ValueHolder
からUISwitch
へ値を反映させる時にアニメーションをしなかったり、UISwitch
のisOn
をコードで直接変更した時にValueHolder
へ値が反映されません。それらが必要であれば、さらにコードを追加・変更することになるでしょう。
SwiftChaining
はUIKit
をラップして簡単にするものではないので、このようなUIKit
の対応は通常と変わらず考える必要があります。
- 投稿日:2019-02-27T19:58:13+09:00
モーダル(ポップアップ)からコントローラへの値の渡し方
モーダル(ポップアップのようなもの)から値を受け取れず、この二日間泥沼にはまっていました。
完全に給料泥棒ですwww何がしたかったかというと具体的には、ユーザがとあるuiSwitchをオンにしたらモーダルが表示されて、そのモーダルが正常に閉じられたらそのまま、異常な閉じ方をされたらuiSwitchオフに戻すといった処理をしたかったわけです。
状況としては
①mainのコントローラでxibからviewを作成(ここにuiSwitchがある)
②ユーザがそのスイッチを押してモーダル(storyboard)を開く
③ユーザがモーダルの閉じるボタンを押す
④異常に閉じられたことをxibに伝えてスイッチをオフにするやりたかったことはこれだけです。
流れとしては、異常に閉じられたという情報を
モーダルのコントローラ(UIViewControllerを継承)
→mainのコントローラ(UIViewControllerを継承)
→xibのコントローラ(UIViewを継承)と伝えていこうかなと思ったわけです。
デリゲートを使って試行錯誤してみたものの。。。
xibのコントローラであれって使えるんですかね。
xibの場合、継承してるのがUIViewControllerではなく、UIViewなので無理なのではないかと思いました(真偽は不明)
そういう結論に至ったため、途方に暮れながらmainのコントローラを眺めていたわけですが。。。「あれ、xibのコントローラがインスタンス化されてる!?」と気づきまして
「なら、モーダルのコントローラからメインのコントローラに情報を送れれば、あとはメインのコントローラ内にあるxibのインスタンスをいじればいけるのでは!」と。↓こんな感じで
MainController.swiftclass MainController: UIViewController { // xibのコントローラをインスタンス化 let xib = Xib() // xibに設置されたuiSwitchをオフに func setUISwitchOff(){ xib.uiSwitch.isOn = false } }Xib.swiftclass Xib: UIView { // ↓こいつはviewと紐づいてます @IBOutlet weak var uiSwitch: UISwitch! } }というわけで、あとは「モーダルが異常に閉じられた」という情報を受け取って、その時にsetUISwitchOff()を発動してやればオッケーです。
で、その情報を受け取るために使えそうだと目をつけたのは、
presentingViewController
まんま、今見てるビューを司るコントローラだと思います。
使えそうだとは思ったものの、「え、今のpresentingViewControllerってモーダルのコントローラだよな」と、まずはそこから不明でしたww
どちらにせよモーダルのコントローラは要らないので、それを消したらメインのコントローラに移るだろうくらいの予想でやってみました。
何やら強制ダウンキャストが必要だとのことだったのでModalController.swiftclass ModalController: UIViewController{ @IBAction func closeBtnAction(_ sender: UIButton) { // 閉じるボタンを押されたらモーダルを消します self.dismiss(animated: false, completion: nil) // メインのコントローラに強制ダウンキャスト let originVC = self.presentingViewController as! MainController // setUISwitch()を発動してXibのスイッチをオフに!! originVC.setUISwitchOff() } }決まった!!!!
…当然そう甘くはなくて
「navigationControllerをMainControllerにキャストできません」
的なエラーが出ました。あ、はい
どうしよ
「あ、こいつnavigationControllerなのか!!なんか聞いたことあるぞ!!」
調べました。
そしたらどうやら、navigationControllerの一番上のコントローラを取って来なけりゃいけないらしくModalController.swiftclass ModalController: UIViewController{ @IBAction func closeBtnAction(_ sender: UIButton) { // 閉じるボタンを押されたらモーダルを消します self.dismiss(animated: false, completion: nil) // とりあえずキャスト let naviVC = self.presentingViewController as! UINavigationController // 一番上のコントローラを取得し、そいつはMainControllerだからキャスト!! let originVC = naviVC.topViewController as! MainController originVC.setStandByOff() } }なんと、これでいけました。
できてしまえば簡単だな
と言いたいところですが、ふつーにわかりにくいと思いました。
もっと簡単なやり方があったんじゃないかと思います。まる
- 投稿日:2019-02-27T19:18:36+09:00
【Xcode】開発途中に手動でディレクトリ構成を変更する
概要
Xcodeでのディレクトリ構成変更方法についてです。初歩的な話ですね。
開発してる途中で「ディレクトリの構成変えたくなったんだけどどうしたらええんや?!」となって調べても今のバージョンで詳しいやり方が載っている記事等がなかったので書いてみました。
僕はちゃんと知らなくて適当にやったらビルドエラーでめちゃくちゃはまってしまったので、同じようにはまった人のためになれば嬉しいです。自分の調べ方が悪かっただけで、もっとしっかり説明されている記事等があるかもしれないのであれば教えていただけると嬉しいです!
環境
【Xcode】 Version 10.1
正しいやり方
この状態からディレクトリの構成を変更していきます!
今回はMVCモデルに沿ってディレクトリ構成を変更してみます。
まず、Finderにて新規フォルダを作成します。
作成するのはModel
,View
,Controller
の3つのフォルダです。
作成したらそれぞれのフォルダにファイルを振り分けていきます。
先ほどまでなんともなかったファイルたちが赤くなっていますね。
赤い状態は「ファイルの参照先にファイルがなくなってるよ〜」と知らせてくれているものです。一つファイルを選択してinspectorのFullPathをみてみるとファイルのパスが指定されています。がしかし、そのパスにファイルが存在しないので怒られるんですね。
ということでこの赤くなっているファイルを消しても参照が消えるだけなので削除しちゃいます。
最後に先ほど作成したフォルダたちをXcode上に追加していきます。
FinderからDrag&Dropで追加すると以下のような表示が出ます。
大事なのがここで
Create Groups
を選択することです。
この状態でFinishを押すと黄色のフォルダがXcode上に現れます。
他のフォルダも同様に入れていき、ビルドができるかどうかの確認が完了すれば…
ディレクトリ構成の変更完了です!
誤ったやり方
僕が何も知らずにやった方法です。
Finder場でフォルダ作成してファイルを分けるところまでは同じ。
この後
Create Group
ではなくCreate folder References
を選択すると以下の感じになります。
一見大丈夫そうですが、ビルドして実行するとエラーが生じるようになります。
あくまでも参照はファイルに対して行うものであって、フォルダに対して行うことはできないのだと思います。(見当違いなこと言っていたらごめんなさい)
最後に
他にこういうやり方あるよ!とかこんなやり方しなくてもこうやりゃいいじゃろが等あれば教えていただけると幸せになれます!
以上です。
- 投稿日:2019-02-27T18:25:32+09:00
Twilio Voice Swift Quickstart for iOS(日本語訳)
Twilio Voice Swift Quickstart for iOS
iOSでVoiceを始めよう:
- Quickstart - クイックスタートアプリを実行する
- Access Tokens - アクセストークンを使用
- Managing Audio Interruptions - 音声の中断の管理
- Managing Push Credentials - プッシュ認証情報の管理
- More Documentation - Voice iOS SDKに関連するその他のドキュメント
- Issues and Support - ファイリングの問題と一般的なサポート
Quickstart
クイックスタートアプリケーションを使い始めるには、次の手順に従います。
手順1〜6により、アプリケーションは電話をかけることができます。
残りの手順7〜10では、アプリケーションはAppleのVoIPサービスを使用してプッシュ通知の形式で着信通話を受信できるようになります。
- TwilioVoice frameworkをインストールする
- Voice API keyを作成する
- アプリで使用するアクセストークンを生成するようにサーバーを構成する
- TwiML applicationを作成する
- アプリケーションサーバーを設定する
- アプリを実行する
- VoIP Service Certificateを作成する
- VoIP Service Certificateを使ってPush Credentialを作成する
- XcodeのプロジェクトにVoIPpush通知の設定をする
- 着信を受ける
- clientからclientに電話をかける
- clientからPSTNに電話をかける
1. TwilioVoice frameworkをインストールする
Carthage
Cartfileに次の行を追加します。
github "twilio/twilio-voice-ios"
carthage bootstrap
を実行します (SDKをアップデートしている場合はcarthage update
を実行します)対象のapplication targetの
Build Phases
タブにて,+
アイコンをクリックして、New Run Script Phase
を選びます。 Run Scriptにshell(ex:/bin/sh
)を指定し , 以下の内容のscriptを追加します:/usr/local/bin/carthage copy-frameworks“Input Files”の下に、使用したいframeworkのパスを追加します:
$(SRCROOT)/Carthage/Build/iOS/TwilioVoice.frameworkCocoapods
quickstartのフォルダ内で
pod install
を実行すると、Cocoapodsがworkspaceを作成します。
また、必ず Cocoapods v1.0 以降を使用してください。
Cocoapodsのインストールが完了し,SwiftVoiceQuickstart.xcworkspace
を開くとSwiftを用いた基本的なquickstartプロジェクトとCallKit quickstartプロジェクトが見つかります。Note: TwilioVoice の最新版を取得するには、
pod repo update master
を実行し CocoaPods Master Spec Repo から最新版を取得する必要があるかもしれません。2. Voice API keyを作成する
Voice API Keys ページに移動して新しいAPI keyを作成してください:
作成した
API_KEY
とAPI_KEY_SECRET
をメモをとってください。 次のステップで必要となります。3. アプリで使用するアクセストークンを生成するようにサーバーを構成する
サーバー用のスタータープロジェクトの1つをダウンロードします。
- voice-quickstart-server-java
- voice-quickstart-server-node
- voice-quickstart-server-php
- voice-quickstart-server-python
サーバーのREADMEに記載されている手順に従って、アプリケーションサーバーをローカルで起動し、パブリックインターネット経由でアクセスできるようにします。consoleから取得できる Twilio Account SID と先ほどメモをとった
API_KEY
とAPI_SECRET
を置き換えてください。ACCOUNT_SID = 'AC***' API_KEY = 'SK***' API_KEY_SECRET = '***'4. TwiML applicationを作成する
次にTwiMLアプリケーションを作成する必要があります。TwiML applicationは TwiML呼び出し先に設定されているパブリックURLを参照します。
iOSアプリがTwilioを呼び出すと、TwilioはこのURLにWebhookを用いてリクエストを送信し、アプリケーションサーバーはTwiMLを生成してResponseを返し、返ってきたResponseに沿ってTwilioは応答します。
TwiMLアプリケーションを作成するには TwiML app pageにアクセスしてください。
新しいTwiML applicationを作成し, アプリケーションサーバーの/makeCall
エンドポイントをVoice Request URLに設定します。(あなたのアプリケーションサーバーがPHPで書かれている場合、最後に.php
拡張子が必要です)。ご覧の通りRequest URLにはngrokのパブリックアドレスを使用しています。
TwiML Applicationの設定を保存し, TwiML Application SID(AP
から始まる長い識別子)を取得します。5. アプリケーションサーバーを設定する
残りの
APP_SID
設定情報をサーバーのコードに入れましょう。ACCOUNT_SID = 'AC***' API_KEY = 'SK***' API_KEY_SECRET = '***' APP_SID = 'AP***'それが終わったら、サーバーを再起動して新しい設定情報を使用します。今度はテストする時が来ました。
ブラウザを開き、アプリケーションサーバーのAccess Tokenを取得するためのURLをご覧ください。
Access Token endpoint:https://{YOUR_SERVER_URL}/accessToken
(あなたのアプリケーションサーバーが、PHPで書かれている場合は、最後に.php拡張子をつける必要があります)
すべてが正しく設定されていれば、長い文字列と数字が表示されるはずです。これがTwilioアクセストークンです。iOSアプリはTwilioに接続するためにこのようなトークンを使用します。6. アプリを実行する
それでは
SwiftVoiceQuickstart.xcworkspace
に戻りましょう。baseURLString
をngrokのパブリックURLで更新します。import UIKit import AVFoundation import PushKit import TwilioVoice let baseURLString = "https://3b57e324.ngrok.io" let accessTokenEndpoint = "/accessToken" let identity = "alice" let twimlParamTo = "to" class ViewController: UIViewController, PKPushRegistryDelegate, TVONotificationDelegate, TVOCallDelegate, AVAudioPlayerDelegate, UITextFieldDelegate {アプリをビルドして実行します
テキストフィールドを空のままにして、callボタンを押して通話を開始します。お祝いメッセージが聞こえます。別のclientまたは番号への発信は、手順11および12で説明されています。切断するには、"Hang Up"をタップします。
7. VoIP Service Certificateを作成する
Programmable Voice SDKはAppleのVoIPサービスを使用して、着信呼び出しを受信したことをアプリケーションに通知します。ユーザーに着信を受けさせる場合は、アプリケーションでVoIPサービスを有効にしてVoIP Services Certificateを生成する必要があります。
Apple Developer portalへアクセスして、以下を行う必要があります:
- 証明書を作成できるようにApple Developer Programの会員になる。
- 対象のApp IDで“Push Notifications”サービスが有効になっていることを確認してください。
- App IDに対応するプロビジョニングプロファイルを作成します。
- Certificates - > Productionに移動し、右上の
+
をクリックして新しい証明書を追加して、このアプリのApple VoIP Services Certificateを作成します。8. VoIP Service Certificateを使ってPush Credentialを作成する
Keychain Accessを使用してVoIP Service Certificateを生成したら、それをTwilioにアップロードして、Twilioがあなたに代わってアプリにプッシュ通知を送信できるようにする必要があります。
VoIPサービス証明書をKeychain Accessから.p12ファイルとしてエクスポートしてから、opensslコマンドを使用して.p12ファイルから証明書と秘密鍵を抽出します。
.p12が書き出しのオプションではない場合は、Keychain Accessの検索バーにvoip
を入力して、証明書を書き出すときに必ず両方の項目を選択してください。$> openssl pkcs12 -in PATH_TO_YOUR_P12 -nocerts -out key.pem $> openssl rsa -in key.pem -out key.pem $> openssl pkcs12 -in PATH_TO_YOUR_P12 -clcerts -nokeys -out cert.pemPush Credentials pageに移動して新しいPush Credentialを作成します。証明書と秘密鍵を貼り付けます。plaintextとしてキーを貼り付ける必要があります:
cert.pem
の-----BEGIN CERTIFICATE-----
から-----END CERTIFICATE-----
まで貼り付ける必要があります。key.pem
の-----BEGIN RSA PRIVATE KEY-----
から-----END RSA PRIVATE KEY-----
まで貼り付ける必要があります。「Sandbox」オプションを必ずチェックしてください。これは重要。生成したVoIP Service Certificateは、本番環境でもAppleのsandbox infrastructureでも使用できます。このチェックボックスをオンにすると、開発プロビジョニングプロファイルに適したApplesandbox infrastructureにプッシュが送信されます。
アプリのストア送信準備が整ったら、「APS Environment:production」でplistを更新し、同じVoIP Certificateを使用して「Sandbox」オプションをチェックせずに別のPush Credentialを作成します。
それでは、サーバーのコードに戻り、Push Credential SIDを更新しましょう。Push Credential SIDがアクセストークンに埋め込まれます。
PUSH_CREDENTIAL_SID = 'CR***'9. Xcodeプロジェクト設定にプッシュ通知の設定をする
プロジェクトの Capabilities タブで,“Push Notifications”を有効化します。
Xcode 8以前では、バックグラウンドモードで “Voice over IP” と “Audio, AirPlay Picture in Picture” の両方の機能を有効にします。Xcode 9以降では、“Audio, AirPlay and Picture in Picture”機能が有効になっていることと、"audio"と"voip"を含む"UIBackgroundModes"がアプリのplistにあることを確認してください。
<key>UIBackgroundModes</key> <array> <string>audio</string> <string>voip</string> </array>10. 電話を受ける
これで着信を受ける準備が整いました。アプリをRebuildして、アプリケーションサーバーのエンドポイント /placeCall にアクセスします。
https://{YOUR_SERVER_URL}/placeCall
(あなたのアプリケーションサーバーがPHPで書かれている場合、最後に.php
拡張子が必要です)
これによりTwilio REST APIリクエストがトリガーされ、モバイルアプリへのインバウンドコールが行われます。アプリが通話を受け付けると、お祝いのメッセージが聞こえます。11. client から client に発信する
clientからclientへの発信を行うには、2つのデバイスでアプリケーションを実行する必要があります。 追加のデバイスでアプリケーションを実行するには、新しいデバイスを登録するときに必ずアクセストークンに別のIDを使用してください。例えば,
identity
をbob
に変更してアプリを実行します。let accessTokenEndpoint = "/accessToken" let identity = "bob" let twimlParamTo = "to"テキストフィールドを使用して通話受信者のIDを指定してから、“Call”ボタンをタップして発信します。
TwilioVoice.call()
メソッドで使用されるTwiMLパラメーターはサーバーで使用されている名前と一致する必要があります。12. client から PSTN に発信する
検証済電話番号を一つ用意します。検証済電話番号はTwilioから外線発信できるあなたの電話番号が使えます。この番号はTwilioに移植されていないので、この電話番号のためにTwilioに支払う必要はありません。
clientから発信するためには, まずhttps://jp.twilio.com/console/phone-numbers/verified から検証済電話番号を取得します。 サーバーコードの
CALLER_NUMBER
を検証済電話番号に置き換えます。サーバーを再起動させて、新しい設定値を使用します。TwiML applicationのVoice Request URLはパブリックなアプリケーションサーバーのエンドポイント/makeCall
を指定する必要があります。Access Tokens
access tokenはサーバーコンポーネントのjwtにより作成されます。
作成されたaccess tokenにはProgrammable Voiceのgrant
、あなたが指定したidentity
、有効期限を設定するtime-to-live
が含まれます。time-to-live
の初期値は1時間でTwilio helper ライブラリを使用して最大24時間まで設定することができます。用途
iOS SDKでは、access tokenは次の目的で使用されます:
TwilioVoice.call(...)
経由で外線発信するTwilioVoice.registerWithAccessToken(...)
とTwilioVoice.unregisterWithAccessToken(...)
経由で着信時のVoIP Push Notificationsの登録、解除。登録されると着信時にTVOCallInvite
経由で承認するか拒否するか選ぶことができます。着信を受けるときはaccess tokenは必要ありません。TVOCallInvite
は内部的にaccess tokenを保持しており、それは私たちのインフラストラクチャにアクセスすることができます。有効期限切れ
前述のように、アクセストークンは最終的に期限切れになります。アクセストークンが期限切れになった場合、私たちのインフラストラクチャは登録時に
TVOCallDelegate
を介してTVOErrorAccessTokenExpired
/20104
エラーまたはcompletionエラーを返します。アクセストークンの有効期限を適切に管理するテクニックは数多くあります:
- 発信呼び出しを行う前に、必ずアクセストークンサーバーから新しいアクセストークンを取得してください。
TVOErrorAccessTokenExpired
/20104
エラーが発生するまでアクセストークンを保持し、エラーが発生した後に新しいトークンを取得してください。- アクセストークンを要求されたときのタイムスタンプと一緒に保存しておくと、サーバーで使用されている「有効期限」に基づいて、トークンがすでに期限切れになっているかどうかを事前に確認できます。
- 発信呼び出しに関連付けられた
UIApplication
またはUIViewController
が作成されるたびにアクセストークンをフェッチします。音声割り込みの管理
iOSのバージョンが異なると AVAudioSession 割り込みの処理方法が若干異なります。このセクションでは、Programmable Voice iOS SDKが音声の中断を管理し、中断が終了した後に通話の音声を再開する方法について説明します。 iOSでは、中断が終了したことを示すために必要な通知が提供されないため、SDKが通話音声を自動的に再開できない場合があります。
Programmable Voice iOS SDKがオーディオの中断をどのように処理するか
- SDKは自身を
TwilioVoice.initialize()
の中でAVAudioSessionInterruptionNotification
のオブザーバーとして登録します。- 通知が発生し、中断の種類が
AVAudioSessionInterruptionTypeBegan
の場合、SDKは自動的にオーディオデバイスを無効にし、アクティブな通話を保留にします。- SDKが
AVAudioSessionInterruptionTypeEnded
で通知を受け取ると、SDKはオーディオデバイスを再度有効にしてアクティブな通話のオーディオを再開します。- iOS 8と9では、
AVAudioSessionInterruptionTypeEnded
による割り込み通知が常に発生するわけではないため、SDKは通話音声を自動的に再開できません。これは既知の問題であり、別の方法はUIApplicationDidBecomeActiveNotification
を使用して中断後にアプリが再びアクティブになったときに音声を再開することです。異なるiOSバージョンでの通知
以下は、音声の中断を引き起こし、Voice SDKの通話中に再開するためのさまざまな手順で受信したシステム通知の一覧です。 (アプリがアクティブなVoice SDK通話中であると仮定します)
シナリオ 中断通知の開始 中断終了の通知 通話の再開? Note PSTN割り込み A.
PSTN割り込み
PSTN着信通話を受け入れる
電話相手が電話を切るiOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11B.
PSTN割り込み
PSTN着信通話を受け入れる
自分が電話を切るiOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11C.
PSTN割り込み
PSTNを拒否iOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11D.
PSTN割り込み
PSTNを無視iOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11E.
PSTN割り込み
自分が応答する前に相手が電話を切るiOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11そのほかのAudio Interruption
(例えばYouTubeアプリ)F.
YouTubeアプリに切り替えてビデオを再生する
ビデオを停止
Voiceアプリに戻るiOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11中断終了通知はiOS 9では発生しません。
iOS 10/11でVoiceアプリに切り替えた後、数秒まで割り込み終了通知は発生しません。AVAudioSessionInterruptionOptionShouldResume
フラグはfalse
です。G.
YouTubeアプリに切り替えてビデオを再生する
ビデオを停止せずにVoiceアプリに戻るiOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11中断終了通知はiOS 9では発生しません。
iOS 10/11でVoiceアプリに切り替えた後、数秒まで割り込み終了通知は発生しません。AVAudioSessionInterruptionOptionShouldResume
フラグはfalse
です。H.
YouTubeアプリに切り替えてビデオを再生する
ホームボタンを2回押してYouTubeアプリを終了する
Voiceアプリに戻るiOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11iOS 9
iOS 10
iOS 11Voiceアプリがアクティブ状態に戻るまで、中断終了通知は発生しません。 AVAudioSessionInterruptionOptionShouldResume
フラグはfalse
です。CallKit
iOS 10以降では、CallKit(統合されている場合)は、一連のデリゲートメソッドを提供することで中断を処理し、アプリケーションが適切なオーディオデバイスの処理と状態の遷移で応答できるようにします。
Notifications & 割り込み中のコールバック
CallKitフレームワークへの呼び出しを報告するときに
CXCallUpdate
オブジェクトのsupportsHolding
フラグを有効にすることで、別のPSTNまたはCallKit対応の呼び出しがあるときに"Hold&Accept" オプションが表示されます。 “Hold&Accept” オプションを押すと、一連のシナリオとコールバックが起こります:
provider:performSetHeldCallAction:
delegateメソッドはCXSetHeldCallAction.isOnHold = YES
で呼び出されます。ここで音声通話を保留にして、操作を実行します。AVAudioSessionInterruptionNotification
はAVAudioSessionの中断が始まったことを示すために起動されます。- CallKitはあなたのアプリのAVAudioSessionを無効にし、
provider:didDeactivateAudioSession:
コールバックを起動します。あなたはTwilioVoice.audioEnabled = NO
を呼び出してSDKオーディオデバイスを無効にする必要があります。- 割り込んだ電話が終了した時、
AVAudioSessionInterruptionNotification
を通知します。システムがそれを通知した時、provider:performSetHeldCallAction:
メソッドを再度呼ぶことで、あなたは中断された通話を再開することができます。Note 割り込みコールがリモートパーティによって切断された場合、このコールバックは実行されません。- AVAudioSessionが再度有効化されます。あなたは
provider:didActivateAudioSession:
メソッド内にて、TwilioVoice.audioEnabled = YES
を行い、再度SDKのaudio deviceを有効化する必要があります。
シナリオ 中断後に音声を再開しますか? Note A.
保留 & 承認
自分が電話を切るiOS 10
iOS 11B.
保留 & 承認
相手が電話を切るiOS 10
iOS 11割り込みが終了しても provider:performSetHeldCallAction:
は呼ばれません。C.
保留 & 承認
システムUIで音声通話に戻るiOS 10
iOS 11D.
拒否するiOS 10
iOS 11割り込まれた通話に応答しないため、通話の中断は発生しません。 E.
無視するiOS 10
iOS 11割り込まれた通話に応答しないため、通話の中断は発生しません。 ケース2の場合、CallKitは
provider:performSetHeldCallAction:
メソッドを呼び出して通話音声を自動的に再開しませんが、システムUIは音声通話がまだ保留中であることを示します。あなたは"Hold"ボタンを使って呼び出しを再開することができます、あるいはCXSetHeldCallAction
を使ってプログラムで保留状態を解除することができます。アプリはまた、ユーザーの混乱を避けるために通話の「保留」状態を示すためにUI状態を更新する責任があります。// Resume call audio programmatically after interruption CXSetHeldCallAction *setHeldCallAction = [[CXSetHeldCallAction alloc] initWithCallUUID:self.call.uuid onHold:holdSwitch.on]; CXTransaction *transaction = [[CXTransaction alloc] initWithAction:setHeldCallAction]; [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) { if (error) { NSLog(@"Failed to submit set-call-held transaction request"); } else { NSLog(@"Set-call-held transaction successfully done"); } }];Push認証情報の管理
push認証情報はプッシュ通知チャネルのレコードです。iOSの場合、このpush認証情報はAPNS VoIPのプッシュ通知チャネルレコードです。push認証情報はMobile Push Credentialsで管理されます。
iOS SDKの
TwilioVoice.registerWithAccessToken:deviceToken:completion
を介して登録が実行されるたびに、JWTベースのアクセストークンよりidentity
とPush Credential SID
が提供されます。電話をかけるたびにidentity
、Push Credential SID
とdevice token
をユニークなアドレスとしてAPNS VoIP push通知を送信します。TwilioVoice.unregisterWithAccessToken:deviceToken:completion
を使うと、そのidentity
に対する関連付けが削除されます。Push Credentialの更新
Appleから提供された認証情報を変更または更新する必要がある場合は、consoleでPush Credentialを選択し、次に示すPush Credentialページのテキストボックスに新しい
certificate
とprivate key
を追加します。Push Credentialの削除
作成したアプリケーションが使用されていない場合を除き、プッシュ資格情報を削除することはお勧めしません。
APNS VoIP証明書がもうすぐ期限切れになるか期限切れになった場合は、Push Credentialを削除しないでください。 代わりに、「Push Credentialの更新」セクションに従ってPush Credentialを更新する必要があります。
Push Credentialが削除されるとこのPush Credentialで行われた関連登録はすべて削除されます。将来的にこの削除されたPush CredentialのPush Credential SIDを使用して登録された
identity
を参照しようとすると失敗するでしょう。確実にPush Credentialを削除したい場合は、選択したPush Credentialページで
Delete this Credential
をクリックしてください。Push Credentialを削除した後、新しいアクセストークンを生成するときには必ずPush Credential SIDを削除または置き換えてください。
More Documentation
あなたは以下のgetting startedとAppleの最新ドキュメントより、たくさんのドキュメントを見つけることができます。
Twilio Helper Libraries
TwiMLとProgrammable Voice Calls APIの使用方法の詳細については、TwiMLクイックスタートをご覧ください。:
- TwiML Quickstart for Python
- TwiML Quickstart for Ruby
- TwiML Quickstart for PHP
- TwiML Quickstart for Java
- TwiML Quickstart for C#
Issues and Support
あなたが見つけた問題をGithubで提出してください: Voice Swift Quickstart.
問題を報告する際にPersonally Identifiable Information(PII)または機密アカウント情報(APIキー、資格情報など)を共有していないことを確認してください。Voice SDKに関する一般的なお問い合わせはfile a support ticketをご覧ください。
License
MIT
- 投稿日:2019-02-27T16:05:05+09:00
NavigationControllerの画面遷移で
概要
ナビゲーションコントローラーでの画面遷移アニメーションを変更する。
実装環境
Xcode 10.1
Swift 4.2.1コード
先ずは通常の画面遷移
AppDelegate.swiftimport UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var navigationController: UINavigationController? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let viewController: ViewController = ViewController() //set viewController as rootViewController of navi navigationController = UINavigationController(rootViewController: viewController) //hide navigation bar navigationController?.setNavigationBarHidden(true, animated: false) //set window self.window = UIWindow(frame: UIScreen.main.bounds) self.window!.backgroundColor = .white //set navi as rootViewController self.window!.rootViewController = navigationController self.window!.makeKeyAndVisible() return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } }ViewController.swiftimport UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() createImage() createButton() } func createImage() { let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)) imageView.image = UIImage(named: "image1") imageView.contentMode = .scaleAspectFill self.view.addSubview(imageView) } func createButton(){ let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50)) button.center = self.view.center button.setTitle("next", for: .normal) button.backgroundColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1.0) button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside) self.view.addSubview(button) } @objc func onButtonTapped(){ /* if you want to use default transitions, please uncomment out below. */ //let transition = CATransition() //transition.duration = 0.5 //transition.type = CATransitionType.fade //following types are used with subtype //transition.type = CATransitionType.moveIn //transition.type = CATransitionType.push //transition.type = CATransitionType.reveal //example of subtype //transition.subtype = CATransitionSubtype.fromBottom //navigationController?.view.layer.add(transition, forKey: nil) let view = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)) view.alpha = 0 view.backgroundColor = .white self.view.addSubview(view) UIWindow.animate(withDuration: 1.0, animations: { view.alpha = 1.0 }) { (Bool) in DispatchQueue.main.asyncAfter(deadline: .now()+0.2, execute: { //Attention "animated" is "false" self.navigationController?.pushViewController(nextViewController(), animated: false) }) DispatchQueue.main.asyncAfter(deadline: .now()+1.0, execute: { view.removeFromSuperview() }) } } }nextViewController.swiftimport UIKit class nextViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() createImage() createButton() } func createImage() { let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)) imageView.image = UIImage(named: "image2") imageView.contentMode = .scaleAspectFill self.view.addSubview(imageView) } func createButton(){ let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50)) button.center = self.view.center button.setTitle("back", for: .normal) button.backgroundColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1.0) button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside) self.view.addSubview(button) } @objc func onButtonTapped(){ let view = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)) view.alpha = 0 view.backgroundColor = .white self.view.addSubview(view) UIWindow.animate(withDuration: 1.0, animations: { view.alpha = 1.0 }) { (Bool) in DispatchQueue.main.asyncAfter(deadline: .now()+0.2, execute: { //Attention not pushViewController //popToViewController is back ViewController method self.navigationController?.popToViewController(self.navigationController!.viewControllers[0], animated: false) }) DispatchQueue.main.asyncAfter(deadline: .now()+1.0, execute: { view.removeFromSuperview() }) } } }結果
解説
今回の実装ではstoryboardを使わなかったためstoryboardのファイルは削除している。
それなのでInfo.plistからstoryboard file base nameを削除しAppDelegateでwindowを設定した。遷移アニメーションの変更部分
navigationControllerのanimatedをfalseにすることで横にスライドするアニメーションを行わないようにする。
self.navigationController?.pushViewController(nextViewController(), animated: false)その他のやり方
コメントアウトしているところを参考にしてもらえれば分かるようにnavigationControllerのtypeを変更することで遷移方法を変更できる。
segueのcross dissolveのようなanimationであればtypeをfadeにすることで簡単に実装できるだろう。
参考
CATransition
How do I cross dissolve when pushing views on a UINavigationController in iOS 7?
How do I create a new Swift project without using Storyboards?
- 投稿日:2019-02-27T12:27:22+09:00
Swift エラー集
'lazy' cannot be used on a letグーグル翻訳
'lazy'はletでは使えません
let
をvar
に変更
- 投稿日:2019-02-27T10:01:19+09:00
Swiftで現在位置の取得と地図にピンを立てる
概要
Swiftで現在位置の取得と地図にピンを立てる方法です。
1. Info.pistに位置情報へのアクセス許可を追加(NSLocationWhenInUseUsageDescription)
2. MKMapViewを全面に貼りstartUpdatingLocation()で位置情報の取得を開始
3. CLGeocoderで住所から緯度経度を取得してaddAnnotation()でピンを立てる他の記事も探しましたが、すぐに使えるコードがなかったのでシェアしておきます。
環境
Swift 4
Xcode 10.1結果
コード
import UIKit import MapKit import CoreLocation struct Annotation { let address: String let title: String? let subtitle: String? } class MapViewController: UIViewController { @IBOutlet weak var mapView: MKMapView! let locationManager = CLLocationManager() override func viewDidLoad() { super.viewDidLoad() configureSubviews() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) startUpdatingLocation() let annotation = Annotation( address: "神奈川県横浜市青葉区青葉台1-7-3", title: "青葉台駅です", subtitle: "田園都市線はいつも激混み") add(with: annotation) } private func add(with annotation: Annotation) { CLGeocoder().geocodeAddressString(annotation.address) { [weak self] (placeMarks, error) in guard let placeMark = placeMarks?.first, let latitude = placeMark.location?.coordinate.latitude, let longitude = placeMark.location?.coordinate.longitude else { return } let point = MKPointAnnotation() point.coordinate = CLLocationCoordinate2DMake(latitude, longitude) point.title = annotation.title point.subtitle = annotation.subtitle self?.mapView.addAnnotation(point) } } private func startUpdatingLocation() { switch CLLocationManager.authorizationStatus() { case .notDetermined: locationManager.requestWhenInUseAuthorization() default: break } locationManager.startUpdatingLocation() } private func configureSubviews() { locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.distanceFilter = 100 locationManager.startUpdatingLocation() mapView.delegate = self mapView.mapType = .standard mapView.userTrackingMode = .follow mapView.userTrackingMode = .followWithHeading } } // MARK: - MKMapViewDelegate extension MapViewController: MKMapViewDelegate { } // MARK: - CLLocationManagerDelegate extension MapViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { print("位置情報の取得に成功しました") } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { let alert = UIAlertController(title: nil, message: "位置情報の取得に失敗しました", preferredStyle: .alert) alert.addAction(UIAlertAction.init(title: "OK", style: .default, handler: { (_) in self.dismiss(animated: true, completion: nil) })) present(alert, animated: true, completion: nil) } }
- 投稿日:2019-02-27T05:50:49+09:00
Swift:CoreML×CreateMLでスタンドアローンな機械学習分類器を実装する
はじめに
CoreMLに加えてCreateMLが登場したことで,ようやく機械学習モデルの生成から推定まで行う処理をSwiftで実装可能となりました.しかし,CreateMLで機械学習モデルを生成する系の記事のほとんどは
Playground
を使う方法のみでコードだけで,機械学習モデルを生成する方法が載っていませんでした.そこで今回は,データセットの収集,機械学習モデルの生成,分類(推定)までをスタンドアローンで行えるサンプルプログラムを実装したので,そのソースを掻い摘みながら実装方法解説したいと思います.サンプルプロジェクト(Github公開)
macOS向けのアプリケーションとして実装しています.
(おそらく同様の方法でiOSでも動作するものが実装できると思います.)
今回は簡単のためSandBoxをオフにしています.Githubのリンク:https://github.com/Kyome22/ClassificationSample
5×5のマス目の中に描いた○と△を分類します.
Youtubeリンク:フルバージョンのデモ動画1.データセットの用意
CSVファイルを生成してアプリケーションからアクセス可能な場所に保存します.どんなセンサを使ってどんな風にデータを集めてもOKですが,以下のフォーマットに従う必要があります.
- CSVファイルの一行目にはヘッダ(各カラムのタイトル)が必要
- 一つのサンプルデータは一つの目的変数と一つ以上の説明変数で構成されるサンプルが用いるCSVファイルはこんな感じです.
sampling.csvobject,X0Y0,X1Y0,X2Y0,X3Y0,X4Y0,X0Y1,X1Y1,X2Y1,X3Y1,X4Y1,X0Y2,X1Y2,X2Y2,X3Y2,X4Y2,X0Y3,X1Y3,X2Y3,X3Y3,X4Y3,X0Y4,X1Y4,X2Y4,X3Y4,X4Y4 circle,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0,1.0,1.0 circle,1.0,1.0,1.0,1.0,0.0,1.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0,1.0,1.0 circle,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0,1.0,1.0 circle,0.0,1.0,1.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,1.0,0.0 circle,0.0,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,1.0,0.0,1.0,1.0,1.0,1.0 triangle,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,1.0,1.0,1.0,1.0,0.0,1.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0 triangle,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,1.0,0.0,1.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,1.0,1.0,0.0,0.0 triangle,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,1.0,0.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0 triangle,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.0,1.0,1.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0 triangle,1.0,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,1.0,0.0,1.0,0.0,1.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0objectが目的変数で,残りが説明変数ですね.
例えば書類フォルダにCSVファイルを保存する場合は以下のようにします.
func saveCSV(fileName: String, text: String) { let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let url = dir.appendingPathComponent(fileName) if FileManager.default.fileExists(atPath: url.path) { //追記する場合 if let handle = try? FileHandle(forWritingTo: url) { handle.seekToEndOfFile() handle.write(text.data(using: String.Encoding.utf8)!) } } else { //新規作成の場合 let header: String = "object,X0Y0,X1Y0,X2Y0,...,X4Y4\n" //ヘッダ行 try? (header + text).write(to: url, atomically: true, encoding: String.Encoding.utf8) } } // csvDataは目的変数と説明変数のカンマ区切り文字列(行末改行) saveCSV(fileName: "sampling.csv", text: csvData)2.機械学習モデルのファイルの生成
生成したCSVファイルを読み込んで機械学習モデルを生成します.最終的に
.mlmodel
ファイルをアプリケーションからアクセス可能な場所に吐き出せればOKです.
先ほどと同様書類フォルダに機械学習モデルファイルを保存する場合を示します.func saveModel(fileName: String, _ classifier: MLClassifier, _ metaData: MLModelMetadata) { let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let url = dir.appendingPathComponent(fileName) try? classifier.write(to: url, metadata: metaData) } func learn() { guard let url = FileIO.getCSV(fileName: "sampling.csv") else { return } //targetColumnには目的変数のタイトルを指定します. (このためにCSVにヘッダが必要.) if let dataTable = try? MLDataTable(contentsOf: url), let classifier = try? MLClassifier(trainingData: dataTable, targetColumn: "object") { // 機械学習モデルファイルのメタデータ設定 let metaData = MLModelMetadata(author: "Author Name", // 開発者の名前とか shortDescription: "Object Distinction", // モデルの短い説明 version: "1.0") // モデルのバージョン saveModel(fileName: "object.mlmodel", classifier, metaData) } } learn()3.機械学習モデルのインスタンスの生成
ここが一番鬼門です!
Playground
の手を借りずに機械学習モデルのインスタンスを生成するには一工夫必要です.
- まず,2の方法で機械学習モデルのファイルを一旦生成してしまいます.
- 生成した
.mlmodel
ファイルをXcodeのプロジェクトに追加します.- その状態でどこでもいいので,
.mlmodel
ファイルの拡張子を除いた部分をソースに書き込みます.- 入力した文字列をコマンドキーを押しながらクリックして
Jump to Definition
で飛びます.
5. 機械学習モデル用の自動生成クラスファイルが表示されるので,メニューバーのFile
->Export
を押してプロジェクトにファイルを追加します.(ファイル名はいい感じにつけてください)
6. このソースファイルの〇〇
,〇〇Input
,〇〇Output
となっているところをいい感じにつけたファイル名と同じ感じで書き換えてください.(詳しくはGitHubのサンプルコードを参照.)サンプルコードではObjectModel
,ObjectInput
,ObjectOutput
としました.
7. また,init(contentsOf url: URL) throws
以外のinit
は不要なので削除してください.
8. ここまでできたら,Xcodeのプロジェクトに追加した.mlmodel
ファイルをプロジェクトから除いてください(Move to trash).
9. あとは以下の例のようにすれば機械学習モデルのインスタンスを生成できます.func getModel(fileName: String) -> ObjectModel? { let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let url = dir.appendingPathComponent(fileName) if let compiledUrl = try? MLModel.compileModel(at: url), let model = try? ObjectModel(contentsOf: compiledUrl) { return model } return nil } let model: ObjectModel? = getModel(fileName: "object.mlmodel")ちなみに,なぜ自動生成のクラスファイルをそのまま使わないのかというと,複数のユーザにシステムを対応させる場合CSVファイルや
.mlmodel
ファイルが複数になるため,一意のクラスからインスタンスを生成することができなくなるからです.(.mlmodel
ファイルの数だけその名を冠したクラスファイルが自動生成されるので...)4.分類
prediction
メソッドを使って推定結果を取得します.引数には説明変数をCSVの順番通りに全部渡します.~~Probability
プロパティから確信度を取得できます.// dataは説明変数が入った配列 func predict(_ data: [Double]) { if model == nil { return } if let output: ObjectOutput = try? model!.prediction(X0Y0: data[0], X1Y0: data[1], X2Y0: data[2], X3Y0: data[3], X4Y0: data[4], X0Y1: data[5], X1Y1: data[6], X2Y1: data[7], X3Y1: data[8], X4Y1: data[9], X0Y2: data[10], X1Y2: data[11], X2Y2: data[12], X3Y2: data[13], X4Y2: data[14], X0Y3: data[15], X1Y3: data[16], X2Y3: data[17], X3Y3: data[18], X4Y3: data[19], X0Y4: data[20], X1Y4: data[21], X2Y4: data[22], X3Y4: data[23], X4Y4: data[24]) { let probability = round(1000 * output.objectProbability[output.object]!) / 10.0 Swift.print("\(output.object): \(probability)%") } } }終わりに
少し工夫は必要ですがCreateMLのおかげで機械学習を導入したプログラムをSwiftだけで実装可能になりました.今回はアプリ開発者向けというよりかは研究開発者向けとして記事を書いたのですが,これで「センシング→分類」系の実装に悩んでいる人を少しでも救えたら嬉しいですね.
蛇足
今回サンプルコードとして○と△を分類するというのを適当に作ってみたのですが,それぞれ5サンプルずつ収集して学習かけるだけでもかなりの正答率を叩き出してびっくりしました.
- 投稿日:2019-02-27T01:46:16+09:00
本日のエラー
String interpolation produces a debug description for an optional value; did you mean to make this explicit?
Google翻訳
「文字列補間は、オプションの値に対するデバッグ記述を生成します。これを明確にするつもりでしたか?」原因
Optional型の文字列をunwrapせず直接出力しようとした。
解決方法
インターンで扱ったコードを簡略化したものです。
var text:String? = nil text = "Internship" print("\(text ?? "text is nil")")こんな感じでやったと思う。
あと使わなかったけどこれ。
let text: String? = nil text = "Internship" if let text = text { print("\(text)") } else { print("nil") }強制アンラップは基本使わないと以前教わりました。
エラーもメモだけじゃなくてQiitaにもあげていこうと思います!
以上です!3月から就活の合間合間で100DaysOfCodeやってみようかなと思います!