20190227のSwiftに関する記事は10件です。

【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同士の場合と同じく、以下のようにsendToKVOAdapterを渡すことで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へ値を反映させる時にアニメーションをしなかったり、UISwitchisOnをコードで直接変更した時にValueHolderへ値が反映されません。それらが必要であれば、さらにコードを追加・変更することになるでしょう。

SwiftChainingUIKitをラップして簡単にするものではないので、このようなUIKitの対応は通常と変わらず考える必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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同士の場合と同じく、以下のようにsendToKVOAdapterを渡すことで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へ値を反映させる時にアニメーションをしなかったり、UISwitchisOnをコードで直接変更した時にValueHolderへ値が反映されません。それらが必要であれば、さらにコードを追加・変更することになるでしょう。

SwiftChainingUIKitをラップして簡単にするものではないので、このようなUIKitの対応は通常と変わらず考える必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モーダル(ポップアップ)からコントローラへの値の渡し方

モーダル(ポップアップのようなもの)から値を受け取れず、この二日間泥沼にはまっていました。
完全に給料泥棒ですwww

何がしたかったかというと具体的には、ユーザがとあるuiSwitchをオンにしたらモーダルが表示されて、そのモーダルが正常に閉じられたらそのまま、異常な閉じ方をされたらuiSwitchオフに戻すといった処理をしたかったわけです。

状況としては
①mainのコントローラでxibからviewを作成(ここにuiSwitchがある)
②ユーザがそのスイッチを押してモーダル(storyboard)を開く
③ユーザがモーダルの閉じるボタンを押す
④異常に閉じられたことをxibに伝えてスイッチをオフにする

やりたかったことはこれだけです。

流れとしては、異常に閉じられたという情報を

モーダルのコントローラ(UIViewControllerを継承)
→mainのコントローラ(UIViewControllerを継承)
→xibのコントローラ(UIViewを継承)

と伝えていこうかなと思ったわけです。

デリゲートを使って試行錯誤してみたものの。。。
xibのコントローラであれって使えるんですかね。
xibの場合、継承してるのがUIViewControllerではなく、UIViewなので無理なのではないかと思いました(真偽は不明)
そういう結論に至ったため、途方に暮れながらmainのコントローラを眺めていたわけですが。。。

「あれ、xibのコントローラがインスタンス化されてる!?」と気づきまして
「なら、モーダルのコントローラからメインのコントローラに情報を送れれば、あとはメインのコントローラ内にあるxibのインスタンスをいじればいけるのでは!」と。

↓こんな感じで

MainController.swift
class MainController: UIViewController {
    // xibのコントローラをインスタンス化
    let xib = Xib()

    // xibに設置されたuiSwitchをオフに
    func setUISwitchOff(){
        xib.uiSwitch.isOn = false
    }
}
Xib.swift
class Xib: UIView {
    // ↓こいつはviewと紐づいてます
    @IBOutlet weak var uiSwitch: UISwitch!
    }
}

というわけで、あとは「モーダルが異常に閉じられた」という情報を受け取って、その時にsetUISwitchOff()を発動してやればオッケーです。

で、その情報を受け取るために使えそうだと目をつけたのは、

presentingViewController

まんま、今見てるビューを司るコントローラだと思います。

使えそうだとは思ったものの、「え、今のpresentingViewControllerってモーダルのコントローラだよな」と、まずはそこから不明でしたww
どちらにせよモーダルのコントローラは要らないので、それを消したらメインのコントローラに移るだろうくらいの予想でやってみました。
何やら強制ダウンキャストが必要だとのことだったので

ModalController.swift
class 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.swift
class 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()
    }
}

なんと、これでいけました。

できてしまえば簡単だな

と言いたいところですが、ふつーにわかりにくいと思いました。

もっと簡単なやり方があったんじゃないかと思います。まる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Xcode】開発途中に手動でディレクトリ構成を変更する

概要

Xcodeでのディレクトリ構成変更方法についてです。初歩的な話ですね。

開発してる途中で「ディレクトリの構成変えたくなったんだけどどうしたらええんや?!」となって調べても今のバージョンで詳しいやり方が載っている記事等がなかったので書いてみました。
僕はちゃんと知らなくて適当にやったらビルドエラーでめちゃくちゃはまってしまったので、同じようにはまった人のためになれば嬉しいです。

自分の調べ方が悪かっただけで、もっとしっかり説明されている記事等があるかもしれないのであれば教えていただけると嬉しいです!

環境

【Xcode】 Version 10.1

正しいやり方

まず以下のファイルたちがプロジェクト内にある状態とします。
スクリーンショット 2019-02-27 18.39.11.png

Finderで見るとこんな感じ
スクリーンショット 2019-02-27 18.38.01.png

この状態からディレクトリの構成を変更していきます!
今回はMVCモデルに沿ってディレクトリ構成を変更してみます。
まず、Finderにて新規フォルダを作成します。
作成するのはModel , View , Controller の3つのフォルダです。
スクリーンショット 2019-02-27 18.41.12.png

作成したらそれぞれのフォルダにファイルを振り分けていきます。
スクリーンショット 2019-02-27 18.42.23.png

振り分け終わったら、Xcodeを見てみましょう。
スクリーンショット 2019-02-27 18.43.41.png

先ほどまでなんともなかったファイルたちが赤くなっていますね。
赤い状態は「ファイルの参照先にファイルがなくなってるよ〜」と知らせてくれているものです。

一つファイルを選択してinspectorのFullPathをみてみるとファイルのパスが指定されています。がしかし、そのパスにファイルが存在しないので怒られるんですね。
スクリーンショット 2019-02-27 18.45.16.png

ということでこの赤くなっているファイルを消しても参照が消えるだけなので削除しちゃいます。
スクリーンショット 2019-02-27 18.48.04.png

最後に先ほど作成したフォルダたちをXcode上に追加していきます。
FinderからDrag&Dropで追加すると以下のような表示が出ます。
スクリーンショット 2019-02-27 18.49.44.png

大事なのがここでCreate Groupsを選択することです。
この状態でFinishを押すと黄色のフォルダがXcode上に現れます。
スクリーンショット 2019-02-27 18.51.59.png

他のフォルダも同様に入れていき、ビルドができるかどうかの確認が完了すれば…
スクリーンショット 2019-02-27 18.58.42.png

ディレクトリ構成の変更完了です!

誤ったやり方

僕が何も知らずにやった方法です。
Finder場でフォルダ作成してファイルを分けるところまでは同じ。
スクリーンショット 2019-02-27 19.00.56.png

この後Create GroupではなくCreate folder Referencesを選択すると以下の感じになります。
スクリーンショット 2019-02-27 19.03.34.png

一見大丈夫そうですが、ビルドして実行するとエラーが生じるようになります。
スクリーンショット 2019-02-27 19.16.31.png

あくまでも参照はファイルに対して行うものであって、フォルダに対して行うことはできないのだと思います。(見当違いなこと言っていたらごめんなさい)

最後に

他にこういうやり方あるよ!とかこんなやり方しなくてもこうやりゃいいじゃろが等あれば教えていただけると幸せになれます!

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Twilio Voice Swift Quickstart for iOS(日本語訳)

原本

Twilio Voice Swift Quickstart for iOS

iOSでVoiceを始めよう:

Quickstart

クイックスタートアプリケーションを使い始めるには、次の手順に従います。
手順1〜6により、アプリケーションは電話をかけることができます。
残りの手順7〜10では、アプリケーションはAppleのVoIPサービスを使用してプッシュ通知の形式で着信通話を受信できるようになります。

  1. TwilioVoice frameworkをインストールする
  2. Voice API keyを作成する
  3. アプリで使用するアクセストークンを生成するようにサーバーを構成する
  4. TwiML applicationを作成する
  5. アプリケーションサーバーを設定する
  6. アプリを実行する
  7. VoIP Service Certificateを作成する
  8. VoIP Service Certificateを使ってPush Credentialを作成する
  9. XcodeのプロジェクトにVoIPpush通知の設定をする
  10. 着信を受ける
  11. clientからclientに電話をかける
  12. 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.framework

Cocoapods

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_KEYAPI_KEY_SECRET をメモをとってください。 次のステップで必要となります。

3. アプリで使用するアクセストークンを生成するようにサーバーを構成する

サーバー用のスタータープロジェクトの1つをダウンロードします。

サーバーのREADMEに記載されている手順に従って、アプリケーションサーバーをローカルで起動し、パブリックインターネット経由でアクセスできるようにします。consoleから取得できる Twilio Account SID と先ほどメモをとった API_KEYAPI_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.pem

Push 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を使用してください。例えば, identitybob に変更してアプリを実行します。

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は次の目的で使用されます:

  1. TwilioVoice.call(...)経由で外線発信する
  2. 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着信通話を受け入れる
電話相手が電話を切る
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
B.
PSTN割り込み
PSTN着信通話を受け入れる
自分が電話を切る
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
C.
PSTN割り込み
PSTNを拒否
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
D.
PSTN割り込み
PSTNを無視
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
E.
PSTN割り込み
自分が応答する前に相手が電話を切る
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
そのほかのAudio Interruption
(例えばYouTubeアプリ)
F.
YouTubeアプリに切り替えてビデオを再生する
ビデオを停止
Voiceアプリに戻る
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:x: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:x: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
中断終了通知はiOS 9では発生しません。
iOS 10/11でVoiceアプリに切り替えた後、数秒まで割り込み終了通知は発生しません。
AVAudioSessionInterruptionOptionShouldResumeフラグはfalseです。
G.
YouTubeアプリに切り替えてビデオを再生する
ビデオを停止せずにVoiceアプリに戻る
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:x: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:x: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
中断終了通知はiOS 9では発生しません。
iOS 10/11でVoiceアプリに切り替えた後、数秒まで割り込み終了通知は発生しません。
AVAudioSessionInterruptionOptionShouldResumeフラグはfalseです。
H.
YouTubeアプリに切り替えてビデオを再生する
ホームボタンを2回押してYouTubeアプリを終了する
Voiceアプリに戻る
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11
Voiceアプリがアクティブ状態に戻るまで、中断終了通知は発生しません。
AVAudioSessionInterruptionOptionShouldResumeフラグはfalseです。

CallKit

iOS 10以降では、CallKit(統合されている場合)は、一連のデリゲートメソッドを提供することで中断を処理し、アプリケーションが適切なオーディオデバイスの処理と状態の遷移で応答できるようにします。

Notifications & 割り込み中のコールバック

CallKitフレームワークへの呼び出しを報告するときに CXCallUpdateオブジェクトのsupportsHoldingフラグを有効にすることで、別のPSTNまたはCallKit対応の呼び出しがあるときに"Hold&Accept" オプションが表示されます。 “Hold&Accept” オプションを押すと、一連のシナリオとコールバックが起こります:

  1. provider:performSetHeldCallAction:delegateメソッドは CXSetHeldCallAction.isOnHold = YESで呼び出されます。ここで音声通話を保留にして、操作を実行します。
  2. AVAudioSessionInterruptionNotificationはAVAudioSessionの中断が始まったことを示すために起動されます。
  3. CallKitはあなたのアプリのAVAudioSessionを無効にし、 provider:didDeactivateAudioSession:コールバックを起動します。あなたは TwilioVoice.audioEnabled = NOを呼び出してSDKオーディオデバイスを無効にする必要があります。
  4. 割り込んだ電話が終了した時、AVAudioSessionInterruptionNotificationを通知します。システムがそれを通知した時、provider:performSetHeldCallAction:メソッドを再度呼ぶことで、あなたは中断された通話を再開することができます。Note 割り込みコールがリモートパーティによって切断された場合、このコールバックは実行されません。
  5. AVAudioSessionが再度有効化されます。あなたはprovider:didActivateAudioSession:メソッド内にて、TwilioVoice.audioEnabled = YESを行い、再度SDKのaudio deviceを有効化する必要があります。
シナリオ 中断後に音声を再開しますか? Note
A.
保留 & 承認
自分が電話を切る
:white_check_mark: iOS 10
:white_check_mark: iOS 11
B.
保留 & 承認
相手が電話を切る
:x: iOS 10
:x: iOS 11
割り込みが終了してもprovider:performSetHeldCallAction:は呼ばれません。
C.
保留 & 承認
システムUIで音声通話に戻る
:white_check_mark: iOS 10
:white_check_mark: iOS 11
D.
拒否する
:white_check_mark: iOS 10
:white_check_mark: iOS 11
割り込まれた通話に応答しないため、通話の中断は発生しません。
E.
無視する
:white_check_mark: iOS 10
:white_check_mark: 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ベースのアクセストークンよりidentityPush Credential SIDが提供されます。電話をかけるたびにidentityPush Credential SIDdevice tokenをユニークなアドレスとしてAPNS VoIP push通知を送信します。TwilioVoice.unregisterWithAccessToken:deviceToken:completionを使うと、そのidentityに対する関連付けが削除されます。

Push Credentialの更新

Appleから提供された認証情報を変更または更新する必要がある場合は、consoleでPush Credentialを選択し、次に示すPush Credentialページのテキストボックスに新しいcertificateprivate 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クイックスタートをご覧ください。:

Issues and Support

あなたが見つけた問題をGithubで提出してください: Voice Swift Quickstart.
問題を報告する際にPersonally Identifiable Information(PII)または機密アカウント情報(APIキー、資格情報など)を共有していないことを確認してください。

Voice SDKに関する一般的なお問い合わせはfile a support ticketをご覧ください。

License

MIT

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NavigationControllerの画面遷移で

概要

ナビゲーションコントローラーでの画面遷移アニメーションを変更する。

実装環境

Xcode 10.1
Swift 4.2.1

コード

先ずは通常の画面遷移

AppDelegate.swift
import 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.swift
import 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.swift
import 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()
            })
        }
    }
}

結果

ezgif.com-video-to-gif-10.gif

解説

今回の実装では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?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift エラー集

'lazy' cannot be used on a let

グーグル翻訳
'lazy'はletでは使えません
letvarに変更

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftで現在位置の取得と地図にピンを立てる

概要

Swiftで現在位置の取得と地図にピンを立てる方法です。

 1. Info.pistに位置情報へのアクセス許可を追加(NSLocationWhenInUseUsageDescription)
 2. MKMapViewを全面に貼りstartUpdatingLocation()で位置情報の取得を開始
 3. CLGeocoderで住所から緯度経度を取得してaddAnnotation()でピンを立てる

他の記事も探しましたが、すぐに使えるコードがなかったのでシェアしておきます。

環境

Swift 4
Xcode 10.1

結果

IMG_2432.png
IMG_2433.png

コード

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)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift:CoreML×CreateMLでスタンドアローンな機械学習分類器を実装する

はじめに

CoreMLに加えてCreateMLが登場したことで,ようやく機械学習モデルの生成から推定まで行う処理をSwiftで実装可能となりました.しかし,CreateMLで機械学習モデルを生成する系の記事のほとんどはPlaygroundを使う方法のみでコードだけで,機械学習モデルを生成する方法が載っていませんでした.そこで今回は,データセットの収集,機械学習モデルの生成,分類(推定)までをスタンドアローンで行えるサンプルプログラムを実装したので,そのソースを掻い摘みながら実装方法解説したいと思います.

サンプルプロジェクト(Github公開)

macOS向けのアプリケーションとして実装しています.
(おそらく同様の方法でiOSでも動作するものが実装できると思います.)
今回は簡単のためSandBoxをオフにしています.

Githubのリンク:https://github.com/Kyome22/ClassificationSample
5×5のマス目の中に描いた○と△を分類します.
classification_sample.gif
Youtubeリンク:フルバージョンのデモ動画

1.データセットの用意

CSVファイルを生成してアプリケーションからアクセス可能な場所に保存します.どんなセンサを使ってどんな風にデータを集めてもOKですが,以下のフォーマットに従う必要があります.
- CSVファイルの一行目にはヘッダ(各カラムのタイトル)が必要
- 一つのサンプルデータは一つの目的変数と一つ以上の説明変数で構成される

サンプルが用いるCSVファイルはこんな感じです.

sampling.csv
object,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.0

objectが目的変数で,残りが説明変数ですね.

例えば書類フォルダに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の手を借りずに機械学習モデルのインスタンスを生成するには一工夫必要です.

  1. まず,2の方法で機械学習モデルのファイルを一旦生成してしまいます.
  2. 生成した.mlmodelファイルをXcodeのプロジェクトに追加します.
  3. その状態でどこでもいいので,.mlmodelファイルの拡張子を除いた部分をソースに書き込みます.
  4. 入力した文字列をコマンドキーを押しながらクリックしてJump to Definitionで飛びます.

jump.png
5. 機械学習モデル用の自動生成クラスファイルが表示されるので,メニューバーのFile->Exportを押してプロジェクトにファイルを追加します.(ファイル名はいい感じにつけてください)

model.png
6. このソースファイルの〇〇〇〇Input〇〇Outputとなっているところをいい感じにつけたファイル名と同じ感じで書き換えてください.(詳しくはGitHubのサンプルコードを参照.)サンプルコードではObjectModelObjectInputObjectOutputとしました.
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サンプルずつ収集して学習かけるだけでもかなりの正答率を叩き出してびっくりしました.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

本日のエラー

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やってみようかなと思います!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む