20191202のiOSに関する記事は19件です。

NSLayoutAnchorでのアニメーション[Swift]

状況

Youtubeチャンネルの素材用にTinderの模擬アプリを作成する際、
NSLayoutAnchorでの制約のActiveの切り替えてアニメーションをやりたかったのですが
なかなかうまくいかなかったのでメモ。
NSLayoutAnchor: コードでAutoLayout設定できる方法の1つ。
StoryboardでいうところのAdd New Constraintsのような感覚で制約を設定できる。

やりたいこと

Tinderの模擬アプリにおいてtouchesEnded(Viewから指を離す)したら
指を離した座標によって

  • Viewを真ん中に戻す
  • ViewをVC外に移動し削除する

どちらかの挙動を選択する

対策

NSLayoutAnchor使わない

やりたいことはViewの移動なので
直接中心座標を指定してアニメーションする

let screenWidth = self.view.frame.width
let screenHeight = self.view.frame.height
        if viewCenter.x <= screenWidth/4 {
            //Viewの中心座標が左右1/4にあれば最前面のViewを消す
            UIView.animate(withDuration: 0.2, animations: {
                draggedView!.center = CGPoint(x: -screenWidth/2, y: (screenHeight+64)/2)
            }) { _ in //completion
                draggedView!.removeFromSuperview()
                self.girlsViewArray.removeLast()
            }

        } else if viewCenter.x >= 3*screenWidth/4 {
            //Viewの中心座標が左右1/4にあれば最前面のViewを消す
            UIView.animate(withDuration: 0.2, animations: {
                draggedView!.center = CGPoint(x: 3*screenWidth/2, y: (screenHeight+64)/2)
            }) { _ in //completion
                draggedView!.removeFromSuperview()
                self.girlsViewArray.removeLast()
            }

        } else {
            //中心座標に戻したいので制約を有効にする //どうやら効いてない
            UIView.animate(withDuration: 0.2) {
                draggedView!.center = CGPoint(x: screenWidth/2, y: (screenHeight+64)/2)
                draggedView!.likeView.alpha = 0
                draggedView!.nopeView.alpha = 0
            }
        }

疑問点

このアニメーションをする前のdraggedViewの初期設定で
こんな感じでNSAutoLayoutでViewの位置をガッチリ固めていたのですが
view.centerview.frameを変更したら
制約を変更(false)しなくてもViewが動いたのが疑問。
frameをハードコードしたら制約は無効になるの??
 わかりません。。

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        Array(0..<girlsViewArray.count).forEach {
            //制約
            girlsViewArray[$0].widthAnchor.constraint(equalTo: self.view.widthAnchor , constant: -2*margin).isActive = true
            girlsViewArray[$0].heightAnchor.constraint(equalTo: self.view.heightAnchor, constant: -(44+20)-2*margin).isActive = true
            //44.0(navHeight) + 20.0(statusBarHeight) + 2*10(margin)
            girlsViewArray[$0].centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
            girlsViewArray[$0].centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: (44+20)/2).isActive = true
            //44.0(navHeight) + 20.0(statusBarHeight)
        }
    }

UIKitのView表示ライフサイクルを理解する - Qiita
->この記事にあるように

  • Viewの読み込み
  • 制約の追加(AutoLayout)
  • 制約を元にViewのframeを計算(レイアウト)
  • frameの位置に描画(レンダリング)

の順番だとすれば、frameをハードコードしたら制約が無効になるのも納得がいくかも。。。

参考

iOS, AutoLayoutで簡単にできるアニメーション - Qiita
UIKitのView表示ライフサイクルを理解する - Qiita

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

新卒エンジニアがSwiftUIで個人アプリをリリースしました📚

みかんが美味しい季節になってきましたね

どうも新卒新米エンジニアの@tsuzuki817です👨‍💻

読書した本の金額を管理できるアプリをSwiftUIを使って作りました📚
BookBank 〜読書銀行〜
image.png

自分は技術書などを主に読むのですが、技術書というものは値段が高いのが常です。
そこで自分が今までにどれくらい本を購入しているのか気になって作ろうと思いました。


作るにあたって調べてみたら、すでにそのようなサービスが図書館などであるようです。
「読書通帳」女の子の通帳には合計50万円
高校生の頃に欲しかった...

お品書き

  • 開発したアプリについて
  • SwiftUIの学習方法
  • SwiftUIで実際にアプリを作ってみて感じたこと

開発したアプリについて

いきなり宣伝!
BookBank 〜読書銀行〜
AVFoundationやActivityViewControllerなどUIKitも使っていますが、ほとんどSwiftUIです。


このアニメーションに出てくるチェックマークはこつこつPathをいじって作りました。
SwiftUIのViewをShareしたりする機能も最近実装して今バージョン2.2.0申請中です!
やり方などは以下の記事にまとめました。
SwiftUIでViewを画像としてUIActivityを利用してSNSに共有する

SwiftUIの学習方法

最初は王道を行く公式チュートリアルを進めました。
そのあとは、海外のサイトを色々と巡りつつ自分で色々作り始めました。
おすすめのサイトは次に続きます!

SwiftUIが学べるおすすめサイト

スクリーンショット 2019-12-02 13.18.35.png
https://fuckingswiftui.com/
なかなか素晴らしいドメイン名のサイト
綺麗にまとまっていて、何かあったらすぐ見にきます。
スクリーンショット 2019-12-02 13.19.48.png
https://swiftui-lab.com/
かなり詳細に開設されているサイトで、がっつりお勉強したい時に読みます。
休日推奨

スクリーンショット 2019-12-02 13.20.28.png
https://liquidcoder.com/
おしゃれなアニメーションとデザインが特徴的なサイトで、見た目も勉強したい時はここ一択!

他にもStackOver flowなどちょくちょく見ていますが、ルーティンとしているのはこの三つです。
なにかおすすめのサイトがあればコメントしていただければ、爆裂的にうれしいです!

SwiftUIで実際にアプリを作ってみて感じたこと

まず一つ言えることは、SwiftUIは楽しくて良いものということです。
見た目を爆速で開発できるので、モックを作る際にも良いと思います。

個人アプリでは、SwiftUIのフレームワークに合わせて作ることで従来の開発より圧倒的に早く開発することができます。
逆に、業務などで決まったデザインや変えることのできない仕様がある場合SwiftUIだけで開発するのは簡単ではなくなります。
どうしてもUIKitが必要になってきてしまうからです。

SwiftUIに合わせて仕様を変えるぐらいの気持ちでやっていくのが良いかもしれません☺️

以上新卒新米へっぽこエンジニアのポエムにお付き合いいただき有り難うございましたmm

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

AppleSearchAdsAPIを使おうとしたら人参で詰んだ件

こんにちは、kumanomiです。

あまりにできなすぎて"私たちはどうして公式ドキュメントが読めないのか?"を読みました。
結果公式ドキュメントで殴られることになりました

ドキュメントを読む

Authenticating with the Search Ads Campaign Management API

Apple Search Ads Campaign Management APIにリクエストを行うには、
Apple Search Ads UIを通じて管理者アカウントから取得したAPI証明書を使用して認証する必要があります。

Generate an API Certificate(証明書の発行編)

SSL認証を確立するには、次の手順を実行します。

1.Click on the upper right-hand carrot and select Settings.
2.Click on API tab, then select Create API Certificate.
3.Select certificate permissions based on roles. For more information, refer to the roleNames field in the UserAcl object.
4.Click Create.
5.To download the API certificate, select Action, then Download.

まず一つ目でつまづきました。そして詰みました。
carrot ってなんですかね・・・?🥕ですか?人参ですか?
Settingsの中にAPIなんて項目がないんです・・・。
スクリーンショット 2019-12-02 19.01.53.png
上のスクショのManage Users辺りにAPIみたいな項目があるらしいのです。ないのです。

できなかったので確認したこと

AppleSearchAdsAPIを使うためにはAdvanceプランに入ることが前提だった
=> Advanceだったけどダメだった。

Chromeブラウザを使っていた
=> safariにしたけどダメだった

ブラウザエラーでてる
=> こればかりはどうしようもない

ブラウザのエラーは下記。getModule of nullってなんだ・・・。割と致命的なのでは・・・?

Error, missing Report Suite ID in AppMeasurement initialization
/cm/api/v1/config?pageMode=audit-logs&pageName=orgmanagement:1 Failed to load resource: the server responded with a status of 400 ()
iad-lib-angular-fw-0.0.15-18.js:13294 TypeError: Cannot read property 'getModule' of null
    at new <anonymous> (https://iadcontent.apple.com/cm/modules/search/js/app-search-0.0.8-22.min.js:10:19411)
    at Object.instantiate (https://iadcontent.apple.com/cm/modules/framework/js/iad-lib-angular-fw-0.0.15-18.js:4640:14)
    at $controller (https://iadcontent.apple.com/cm/modules/framework/js/iad-lib-angular-fw-0.0.15-18.js:9912:28)
    at https://iadcontent.apple.com/cm/modules/framework/js/iad-lib-angular-fw-0.0.15-18.js:66851:28
    at invokeLinkFn (https://iadcontent.apple.com/cm/modules/framework/js/iad-lib-angular-fw-0.0.15-18.js:9525:9)
    at nodeLinkFn (https://iadcontent.apple.com/cm/modules/framework/js/iad-lib-angular-fw-0.0.15-18.js:8932:11)
    at compositeLinkFn (https://iadcontent.apple.com/cm/modules/framework/js/iad-lib-angular-fw-0.0.15-18.js:8260:13)
    at publicLinkFn (https://iadcontent.apple.com/cm/modules/framework/js/iad-lib-angular-fw-0.0.15-18.js:8140:30)
    at lazyCompilation (https://iadcontent.apple.com/cm/modules/framework/js/iad-lib-angular-fw-0.0.15-18.js:8478:25)
    at updateView (https://iadcontent.apple.com/cm/modules/framework/js/iad-lib-angular-fw-0.0.15-18.js:66791:23) <div ui-view="" autoscroll="false" class="ng-scope">

結果

これっぽっちもわからない状態です。まさか最初でつまずくとは・・・。
AppleSearchAdsに詳しい方教えていただきたいです :pray:

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

Swiftでアイマスの画像認識やってみる

はじめに

XcodeのCreateMLが簡単に画像認識をアプリケーションを組み込めるようなので試しにやってみました。

https://developer.apple.com/jp/machine-learning/create-ml/
スクリーンショット 2019-12-02 17.06.39.png
上のようにやってくれるらしい。

作成したもの

環境

  • Xcode Version 11.2.1

画像データの準備

何を分類するのか

りんごとかオレンジとかの分類だとなんかつまらないので、今回はアイドルマスターシンデレラガールズ カードギャラリーさんから画像をいただいて、島村卯月渋谷凛本田未央を分類してみようと思います。

学習用のデータとテスト用のデータのフォーマットはこんな感じ。
各ディレクトリの名前がラベルになるのでtrain_datatest_dataの名前を間違えないように気をつけましょう。

train_data
├── mio_honda
├── rin_shibuya
└── uzuki_shimamura

test_data
├── mio_honda
├── rin_shibuya
└── uzuki_shimamura

こんな感じで入ってます。
スクリーンショット 2019-12-02 17.24.46.png

モデルの作成

プロジェクトの作成

Xcode > Open Developer Tool > Create MLから起動
スクリーンショット 2019-12-02 17.08.42.png

プロジェクトのフォルダを決定しテンプレートを選択します。今回は画像認識なのでImage Classifierを選択。
image.png

学習開始

学習用データをTraining Dataに、テスト用データをTesting Dataにデータを追加しましょう。
追加したらTrainボタンを押して完了です。
スクリーンショット 2019-12-02 17.26.04.png

学習が完了したらOutputにある.mlmodelファイルをドラッグして別の場所に保存しておきましょう。スクリーンショット 2019-12-02 17.31.29.png

アプリケーションに組み込む’

XcodeからCreate new Xcode projectSingle View Appでプロジェクトを作成します。私はswiftには疎いのでとりあえず書いたコードを乗っけておきます。
ViewController.swiftとカメラを使用するのでinfo.plistをいじりました。

先ほど作成した.mlmodelをプロジェクトに追加してください。
image.png

info.plist
image.png

ViewController.swift
import UIKit
import AVKit
import CoreML
import Vision

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {

    @IBOutlet weak var cameraDisplay: UIImageView!
    @IBOutlet weak var resultLabel: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpCamere()
    }

    func setUpCamere() {
        guard let device = AVCaptureDevice.default(for: .video) else { return }
        guard let input = try? AVCaptureDeviceInput(device: device) else { return }

        let session = AVCaptureSession()
        session.sessionPreset = .hd4K3840x2160

        let previewLayler = AVCaptureVideoPreviewLayer(session: session)
        previewLayler.frame = view.frame
        cameraDisplay.layer.addSublayer(previewLayler)

        let output = AVCaptureVideoDataOutput()
        output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "CameraOutput"))
        session.addInput(input)
        session.addOutput(output)
        session.startRunning()
    }



    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let sampleBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        scanImage(buffer: sampleBuffer)
    }

    func scanImage(buffer: CVPixelBuffer) {
        // .mlmodelを読み込ませる
        guard let model = try? VNCoreMLModel(for: IdolClassifier_1().model) else { return }
        let request = VNCoreMLRequest(model: model) { request, _ in
            guard let results = request.results as? [VNClassificationObservation] else { return }
            guard let mostConfidentResult = results.first else { return }

            DispatchQueue.main.async {
                if mostConfidentResult.confidence >= 0.9 {
                    let confidenceText = "\n \(Int(mostConfidentResult.confidence * 100))% confidence"
                    switch mostConfidentResult.identifier {
                    case "uzuki_shimamura":
                        self.resultLabel.text = "uzuki_shimamura \(confidenceText)"
                    case "rin_shibuya":
                        self.resultLabel.text = "rin_shibuya\(confidenceText)"
                    case "mio_honda":
                        self.resultLabel.text = "mio_honda\(confidenceText)"

                    default:
                        return
                    }
                } else {
                    self.resultLabel.text = "I don't know"
                }
            }
        }
        let requestHandler = VNImageRequestHandler(cvPixelBuffer: buffer, options: [:])
        do {
            try requestHandler.perform([request])
        } catch {
            print(error)
        }

    }
}

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

Swiftでアイマス の画像認識やってみる

はじめに

XcodeのCreateMLが簡単に画像認識をアプリケーションを組み込めるようなので試しにやってみました。

https://developer.apple.com/jp/machine-learning/create-ml/
スクリーンショット 2019-12-02 17.06.39.png
上のようにやってくれるらしい。

作成したもの

環境

  • Xcode Version 11.2.1

画像データの準備

何を分類するのか

りんごとかオレンジとかの分類だとなんかつまらないので、今回はアイドルマスターシンデレラガールズ カードギャラリーさんから画像をいただいて、島村卯月渋谷凛本田未央を分類してみようと思います。

学習用のデータとテスト用のデータのフォーマットはこんな感じ。
各ディレクトリの名前がラベルになるのでtrain_datatest_dataの名前を間違えないように気をつけましょう。

train_data
├── mio_honda
├── rin_shibuya
└── uzuki_shimamura

test_data
├── mio_honda
├── rin_shibuya
└── uzuki_shimamura

こんな感じで入ってます。
スクリーンショット 2019-12-02 17.24.46.png

モデルの作成

プロジェクトの作成

Xcode > Open Developer Tool > Create MLから起動
スクリーンショット 2019-12-02 17.08.42.png

プロジェクトのフォルダを決定しテンプレートを選択します。今回は画像認識なのでImage Classifierを選択。
image.png

学習開始

学習用データをTraining Dataに、テスト用データをTesting Dataにデータを追加しましょう。
追加したらTrainボタンを押して完了です。
スクリーンショット 2019-12-02 17.26.04.png

学習が完了したらOutputにある.mlmodelファイルをドラッグして別の場所に保存しておきましょう。スクリーンショット 2019-12-02 17.31.29.png

アプリケーションに組み込む’

XcodeからCreate new Xcode projectSingle View Appでプロジェクトを作成します。私はswiftには疎いのでとりあえず書いたコードを乗っけておきます。
ViewController.swiftとカメラを使用するのでinfo.plistをいじりました。

先ほど作成した.mlmodelをプロジェクトに追加してください。
image.png

info.plist
image.png

ViewController.swift
import UIKit
import AVKit
import CoreML
import Vision

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {

    @IBOutlet weak var cameraDisplay: UIImageView!
    @IBOutlet weak var resultLabel: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpCamere()
    }

    func setUpCamere() {
        guard let device = AVCaptureDevice.default(for: .video) else { return }
        guard let input = try? AVCaptureDeviceInput(device: device) else { return }

        let session = AVCaptureSession()
        session.sessionPreset = .hd4K3840x2160

        let previewLayler = AVCaptureVideoPreviewLayer(session: session)
        previewLayler.frame = view.frame
        cameraDisplay.layer.addSublayer(previewLayler)

        let output = AVCaptureVideoDataOutput()
        output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "CameraOutput"))
        session.addInput(input)
        session.addOutput(output)
        session.startRunning()
    }



    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let sampleBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        scanImage(buffer: sampleBuffer)
    }

    func scanImage(buffer: CVPixelBuffer) {
        // .mlmodelを読み込ませる
        guard let model = try? VNCoreMLModel(for: IdolClassifier_1().model) else { return }
        let request = VNCoreMLRequest(model: model) { request, _ in
            guard let results = request.results as? [VNClassificationObservation] else { return }
            guard let mostConfidentResult = results.first else { return }

            DispatchQueue.main.async {
                if mostConfidentResult.confidence >= 0.9 {
                    let confidenceText = "\n \(Int(mostConfidentResult.confidence * 100))% confidence"
                    switch mostConfidentResult.identifier {
                    case "uzuki_shimamura":
                        self.resultLabel.text = "uzuki_shimamura \(confidenceText)"
                    case "rin_shibuya":
                        self.resultLabel.text = "rin_shibuya\(confidenceText)"
                    case "mio_honda":
                        self.resultLabel.text = "mio_honda\(confidenceText)"

                    default:
                        return
                    }
                } else {
                    self.resultLabel.text = "I don't know"
                }
            }
        }
        let requestHandler = VNImageRequestHandler(cvPixelBuffer: buffer, options: [:])
        do {
            try requestHandler.perform([request])
        } catch {
            print(error)
        }

    }
}

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

思いやりのSwift命名規則

はじめに

これは、Swift Advent Calendar 2019 9日目の記事です。

コードレビューの際、何気に指摘が多いのが命名についてです。
命名に関するPRのコメントだけでも、対応すればCIが走るし、開発にかかる総時間は伸びてしまいます。

開発人数が多い場合はもちろん、一人で開発する場合でも、あとから見たときに混乱しない命名にするということは大切です。
しかしながら、命名に関しては頻繁な技術的アップデートが必要だったりするものではないので、一度コツをつかんで適切な命名がシュッとできるようになれば、レビュワーにとってもレビュイーにとっても負担が減ることになります。

今回は、多くのコードレビューを受けてきて、Swiftの命名について気をつけたいと感じたポイントを紹介します。命名規則と題していますが、規則というほどかっちりしたものではないです。

こちらの公式リファレンスも参考にしています。
Swift.org API Design Guidelines

型の名前を変数名に含めない

Swiftは型セーフな言語なので、変数定義の際に必ず何かしらの型に決まり、その変数には明示しなくとも型の情報が持たれることになります。

そのため、たとえローカル変数であっても、型名を変数名として使用するのは避け、何が入っている変数なのかという情報を与えるほうがベターです。

// Bad
let array = ["dog", "cat", "fox"]
// Good
let mammals = ["dog", "cat", "fox"]

振る舞いに合ったメソッド名をつける

たとえば返り値が無いメソッドなのにgetHogeみたいな命名をしてしまうと、これを見た別の開発者は何かしらのオブジェクトが返ってくるgetterの挙動を期待していまうかもしれません。

Swift Standard Libraryでは、何かしらの返り値がある場合は動詞の過去分詞形や名詞形・既存のオブジェクト等に対する操作をする場合には動詞の命令形で命名されていることが多いので、それらも参考に命名することで読む人の期待する振る舞いと実際の振る舞いが一致しやすくなります。

特に先の例のように、get・setといったワードは、安易にメソッド名に含めてしまうと読む人が期待するインターフェイスにならないことがあるので注意が必要です。

公開する情報が最小限になるようにする

アーキテクチャにも依りますが、TableViewのIndexPathの持つ情報や、ViewControllerのviewWillAppearなどのdelegateメソッドをトリガーとしたアクションを他のレイヤーに伝えたいとなったときに、これらの名前を直接公開するような命名をするのは推奨されません。

View以外に公開する命名では、Viewだけが知る情報名は隠蔽し、期待されるアクションに言及するような名前をつけるべきです。

ViewController.swift
import RxSwift
import RxCocoa

class ViewController: UITableViewController {
    // MARK: Internal
    // Bad
    let indexPathTrigger = PublishRelay<IndexPath>()
    let viewWillAppearTrigger = PublishRelay<Void>()
    // Good
    let selectTrigger = PublishRelay<Int>()
    let refreshTrigger = PublishRelay<Void>()

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        refreshTrigger.accept(())
    }
}
Presenter.swift
import RxSwift
import RxCocoa

class Presenter {
    weak var viewController: ViewController
    init(_ viewController: ViewController) {
        self.viewController = viewController
        viewController.refreshTrigger.asSignal()
          .emit(onNext: { _ in
              // ・・・
          })
    }
}

文章として読めることを意識する

例えば、キーワードで記事を検索するためのメソッドに対して以下のような命名をしたとしましょう。
すると、アクションを起こそうとしている対象がずれてしまっているように受け取れます。

// Bad
func search(name: String) {} // nameを探すかのように受け取れる

ここで外部引数に適切な情報を入れ込むことで、上記よりもずっとSwiftらしい命名になります。
呼び出しの部分を見ても、振る舞いがわかりやすくなっていることが分かるかと思います。

// Good
func search(byName name: String) {}
search(byName: "hoge")

ここではこのように「何で探すか」という情報を外部引数に入れていますが、外部引数にどこまで情報を入れるかというところは、実際の引数の型などに依ります。

また、Bool値を返すプロパティは、isEmptyisHiddenなどに習い、アクセスしたときにインスタンスの状態が一文で表されるような命名が良いとされます。

文章として読める命名にすることで、コードリーディングの際に必要な情報が名前に含まれ、余計なコメントを書く必要がなくなります。

まとまりがあるものには同じ命名・Prefix・Suffixを用いる

これはSwiftに限った話ではないかもしれませんが、共通項を持つメソッド・プロパティ達には命名にも共通項をもたせておくことで、特にコメントが書かれていなくてもそのことが他の開発者に伝わりやすくなります。

わかりやすい例として、引数に渡す型が異なるが施すアクションや返る結果が同じという場合には、オーバーロードを活用してメソッド名や引数名は同じものを用いてしまうというのが良いでしょう。
これにより、読む側はその引数の型が何であるかを意識せずに挙動だけを見れるというメリットもあります。

おわりに

この記事では、自分がこの1年命名に関して指摘を頂いてきたポイントをまとめてみました。
つまるところ命名規則というのは、読む人・共同開発者への思いやりだと思います。(主観です)
Swiftコードのレビューをお願いする前に、レビュワーや読み手を思いやり、是非今一度上記ポイントを思い出して確認してもらえると嬉しいです。

最後まで読んでいただきありがとうございました!
もっとこうしたほうが良い!私はこうしてる!等ありましたら是非コメントお願いします。

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

NFC には A, B, F… がある【iOS Core NFC】

Core NFC Advent Calendar 2019、私としては2回目の記事になります。

iOS 13 で大幅に機能が追加され、実用的になった Core NFC ですが、そもそも NFC には大きく3つのタイプがあります。

3つの通信規格

この記事でいう NFC の3つのタイプとは、

  • NFC-A
  • NFC-B
  • NFC-F

のことを指します。また、NFC-A には MIFARE、NFC-F には FeliCa という呼称もあります。

記事タイトルと噛み合いませんが、この記事は Core NFC Advent Calendar に属するので NFC のより詳しい説明はこちらの Qiita 記事「NFC関係用語と解説」が詳しいです。

これら3つのタイプにあうプロトコルが Core NFC では提供されており、読み書きしたいタグ(カード)の規格がどのタイプなのかに合わせてコーディングしていくことになります。

それぞれのカードがどのタイプなのか

では、日本で使われているカードが上に挙げたどのタイプなのか一部列挙します。

  • NFC-A (MIFARE)
    • taspo
    • いわゆる NFC Pay
  • NFC-B
    • 運転免許証
    • パスポート
    • マイナンバーカード
    • 在留カード
    • いわゆる NFC Pay
  • NFC-F (FeliCa)
    • 交通系ICカード(Suica、PASMO、ICOCA、…etc.)
    • 電子マネーカード(SF)(楽天Edy、nanaco、WAON、…etc.)
    • 電子マネー(SF 以外)(QUICPay、iD、…etc.)

Core NFC - Tag Types

さて、Apple Developer Documentation を確認すると、Tag Types の項目は以下のようになっていることがわかります(抜粋)。

以上のプロトコルは先に上げた各 Type に対応します。

  • NFC-A (MIFARE) ⇔ protocol NFCMiFareTag or protocol NFCISO7816Tag
  • NFC-B ⇔ protocol NFCISO7816Tag
  • NFC-F (FeliCa) ⇔ protocol NFCFeliCaTag

Core NFC には protocol NFCISO15693Tag も存在し、RFID にも対応します。

使い方

この Core NFC Advent Calendar 2019 にはこれから実際にいろいろなカードを読み取ってみるというテーマの記事が公開予定なので、詳細はそちらに譲るとして、各 NFC Tag を iPhone が検出したときにどのようにデータが渡ってくるかを示します。

// class に NFCTagReaderSessionDelegate を継承させる

var readerSession: NFCTagReaderSession?

// ...

func scan() {
    self.readerSession = NFCTagReaderSession(pollingOption: [.iso14443, .iso15693, .iso18092], delegate: self, queue: nil)
    self.readerSession?.alertMessage = "Hold your iPhone near an NFC tag."
    self.readerSession?.begin()
}

// ...

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {

    switch tags.first! {
    case let .iso7816(iso7816Tag):
        // iso7816Tag: NFCISO7816Tag
        break
    case let .feliCa(feliCaTag):
        // feliCaTag: NFCFeliCaTag
        break
    case let .iso15693(iso15693Tag):
        // iso15693Tag: NFCISO15693Tag
        break
    case let .miFare(miFareTag):
        // miFareTag: NFCMiFareTag
        break
    @unknown default:
        return
    }

    // ...

}

まずは NFCTagReaderSession を初期化します。上記サンプルでは .iso14443.iso15693.iso18092が指定されています。その後、NFCTagReaderSessionbegin() を呼び出すことで iPhone が NFC の読み取りモードになります。
スクリーンショット 2019-11-29 21.53.12.png
無事に iPhone が NFC Tag を見つけると NFCTagReaderSessionDelegatefunc tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) が呼び出されるので、switch文を使ってどのプロトコルに合う NFCTag が検出されたかを判定します。NFCTag は以下のように定義されています。

@available(iOS 13.0, *)
public enum NFCTag {
    case feliCa(NFCFeliCaTag)
    case iso7816(NFCISO7816Tag)
    case iso15693(NFCISO15693Tag)
    case miFare(NFCMiFareTag)

    public var isAvailable: Bool { get }
}

各カードがどの NFCTag であるかを特定できたら、その後は NFCTagReaderSessionconnect(to:completionHandler:) を使ってタグにアクセスしていきます。

次回以降はいよいよ実際のカードをどのようにして読み取っていくかを紹介します。

参照

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

XLPagerTabStrip でページ切り替えイベントを正しく判定する

iOS で UISegmentedControl ライクでリッチな上タブを実現するライブラリはいくつかありますが、Swift 5 にも対応していて最も有名なもののひとつ、XLPagerTabStrip。

このライブラリを使ってページを切り替えたときのイベントを処理する際に困ったので、備忘録として残しておきます。

前提として、ButtonBarPagerTabStripViewController のサブクラスで実装するとします。

問題: moveToViewController(at:animated:) はスワイプ時に呼ばれない

ページを切り替える操作は、以下の2パターンがあると思います。

1. ページラベルを直接タップする
2. スワイプで隣のページに移動する

上記のうち、2. スワイプで隣のページに移動する とき、moveToViewController(at:animated:) が呼ばれません。

したがって、このメソッドの中でページ切り替え時の処理を書くと、ラベルを直接タップしてページ切り替えをしたときにした期待した動作をしないと思います。

import XLPagerTabStrip

class TabPageViewController : ButtonBarPagerTabStripViewController {

    // スワイプ切替の場合には呼ばれないため、ページ切替のイベントハンドラーとしては使用しないべき
    open override func moveToViewController(at index: Int, animated: Bool = true) {
        super.moveToViewController(at: index, animated: animated)
    }
}

解決策: updateIndicator(for:fromIndex:toIndex:withProgressPercentage:indexWasChanged:) を使う

ただし、少し工夫する必要があります。
そもそもこのメソッドは、ページのインジケータの遷移状況を知らせてくれるものなので、ページの移動が開始されてから完了するまで、数十回にわたって発火します。
以下は渡ってくる引数の説明です。

  • viewContoller: PagerTabStripViewController
    • 親となっている ViewController(このメソッドをオーバーライドしている自分自身)
  • fromIndex: Int
    • 移動元となるページ番号(過渡状態の値が入ってくるので注意!)
  • toIndex: Int
    • 移動先となるページ番号
  • progressPercentage: CGFloat
    • 遷移の進捗率(1.0 = 完了)
  • indexWasChanged
    • 飛び番指定でページ移動を行うときに true が入ってくる

このメソッドの特性を踏まえて、前述の2パターンのページ切り替えのイベントを過不足なく拾い上げるための処理を、以下のように実装しました。

/// ページラベルをタップして切り替える際に飛び番を指定したときに、
/// 過渡状態のイベントをスキップするためのフラグ
///
/// 例)ページ1から4に直接移動するとき、1->2, 2->3, 3->4 が順に呼ばれるが、
///    実質的に処理したいのは 1->4 であるため、途中を省く必要がある
private var skipChangeIndexJumping: Bool = false

/// 上記 skipChangeIndexJumping の説明にあるように、途中のインデックスを省くために
/// 実際の切り替え元のインデックスを憶えておくための一次変数
private var actualFromIndex: Int = 0

open override func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool) {
    super.updateIndicator(for: viewController, fromIndex: fromIndex, toIndex: toIndex, withProgressPercentage: progressPercentage, indexWasChanged: indexWasChanged)

    // 遷移中は省く
    guard progressPercentage == 1.0 else { return }

    // 跳び番を指定すると indexWasChanged == true で入ってくるので、これを飛び番移動が起きた判定とする
    if indexWasChanged { 
        skipChangeIndexJumping = true
    }

    // 飛び番移動したとき、最後に from, to, current が揃うので、そのタイミングを遷移完了とする
    if fromIndex == toIndex, toIndex == currentIndex { 
        skipChangeIndexJumping = false
    }

    guard !skipChangeIndexJumping else { return }

    /*
    ** ここにページ切替後に行いたい処理を書く
    */

    actualFromIndex = fromIndex
}

※ ちなみに、余計なイベントを無視する処理は、RxSwift で sentMessage や methodInvoked を使ってこのメソッドの呼び出しを拾って debounce で間引くという手もあったかもしれませんが、今回はこちらの方法を使いました

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

リリースしようと思ったらValidate Appでエラーが発生してハマった

エラー内容

リリースしようと思いValidate Appを実施した所次の3つのエラーが発生した。

・Unsupported Architectures. The executable for XXX.framework contains unsupported architectures '[x86_64, i386]'.

・Invalid Segment Alignment. The app binary at 'XXX.framework' does not have proper segment alignment. Try rebuilding the app with the latest Xcode version.

・The binary is invalid. The encryption info in the LC_ENCRYPTION_INFO load command is either missing or invalid, or the binary is already encrypted. This binary does not seem to have been built with Apple's linker.

※自分の場合は3つのエラーだったが、調べると状況によってはエラーの数が変わるみたい。

原因

今回あるframeworkを追加したのですが、
そのframeworkにシミュレーター用のアーキテクチャが含まれているのがエラー原因ぽい。

対処方法

1.Build Phasesで左上の+を押下してNew Run Scriptを選択。
2.追加したRun Scriptを展開してShow environment variables in build logにチェック。
  *自分の場合は最初からチェックが入ってました。
3.Run Script内にあるShell /bin/shの下の欄に次の参照先にあるスクリプトを追加してframeworkを対象のframework名に変更。
  リリースビルドからSimulator用のarchitectureを抜く方法(Unity編)
4.再コンパイルなどを行ってからValidate Appを行ってエラーが発生しない事を確認しリリース作業を進める。

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

【Swift】 ログの出力をもう少しいい子にしてみる

現状の問題点

Xcodeを使って開発していると、普通であれば開発中のアプリのログを見たくなることが多いと思います。そんなとき、Swift標準の print() を使ってログを表示しようと、こんな感じのコードを書く人が多いのではないでしょうか。

print("hoge hoge hoge...")

これでも間違っているわけではないですが、この書き方だと実際に表示されるログが、

hoge hoge hoge...

のようになってしまい、正直に言って、とても見やすいとは言えないです。仮にログがこれだけなら問題ないのですが、実際に開発しているとログが1つだけなどという状況になるはずもなく、結果として、

hoge hoge hoge...
hoge hoge hoge...
hoge hoge hoge...
hoge hoge hoge...
hoge hoge hoge...

となり、どのログがどこで吐き出されたものか解らなくなり、ログの場所を見つけるためのログを仕込むという、終わりのないループに陥ってしまうことになりかねません。

というわけで、今回は普段から私が使っているログ出力を軽く紹介していきます。

ちなみに、NSLog()を使えばいいんじゃね、と思ったあなた。あなたはとても正しいですが、この記事は、せっかくSwiftを使ってるんだからNS系の関数はできる限り使いたくないという人向けですので、予めご了承ください。

作ってみた

参考までにNSLog()で出力した場合は、こうなります。

2019-12-31 12:00:00.000000+0900 HogeProject[0000:000000] hoge hoge hoge...

はい、とても見やすいですね。なので、なるべくこの形に近づけるようにログ出力を調整していきます。

ではログにどんな情報を乗せるのかを考えていきたいと思います。
NSLog()を参考にしてみると、

・出力時間
・アプリ名
・メッセージ

といったところでしょうか。
この3つの要素に加えて、

・クラス名
・関数名
・行数

があれば、ログが見やすくなると思うので、今回はそれらを入れていきます。
そして出来上がったのが、こちら

LogUtil.swift
class LogUtil {

    /// 日付の出力フォーマット
    private static let dateFormat = "yyyy-MM-dd HH:mm:ss.SSSZ"

    /// ログ出力
    class func log(_ message: String = "", file: String = #file, function: String = #function, line: Int = #line) {
        let logMessage = createLogMessage(message, file: file, function: function, line: line)
        print(logMessage)
    }

    /// ログ+エラー出力
    class func errorLog(_ message: String = "", error: Error, file: String = #file, function: String = #function, line: Int = #line) {
        let logMessage = createLogMessage(message, error: error, file: file, function: function, line: line)
        print(logMessage)
    }

    /// 現在時刻の取得
    private class func nowDateTime() -> String {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = dateFormat
        return formatter.string(from: Date())
    }

    /// ログに表示する文字列を生成
    private class func createLogMessage(_ message: String, error: Error? = nil, file: String, function: String, line: Int) -> String {
        var logMessage = nowDateTime()

        if let bundleName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String {
            logMessage += " [\(bundleName)]"
        }

        if let swiftFile = file.split(separator: "/").last?.split(separator: ".").first {
            logMessage += " [\(String(swiftFile))]"
        }

        logMessage += " <\(function)>"
        logMessage += " [l: \(line)] "
        logMessage += message

        if let error = error {
            logMessage += "\n\(error)"
        }

        return logMessage
    }
}

これでひとまず、目的のログを出力することができるようになります。

使うときは、ログを出力したい箇所で、

LogUtil.log("hoge hoge hoge...")

と呼び出せば、ログが出力されます。
ログの見え方はこんな感じです。

2019-12-31 12:00:00.000+0900 [HogeProject] [HogeViewController] <hogeFunction()> [l: 14] hoge hoge hoge...

どうでしょうか。NSLog() に近い表現になったと思います。少なくとも、print()で出力するよりはだいぶ解りやすくなったのではないでしょうか。

おわりに

以上が私が開発中に使っているログ出力です。もちろん、人によっては不要な情報も出力されているかもしれませんが、ひとつの例として受け取って貰えればと思います。

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

プロキシアプリCharlesで通信まわりのデバッグが快適になったので布教したい

この記事はチームラボエンジニアリングアドベントカレンダー6日目の記事です。

はじめに

iOSアプリ開発でCharlesを使ってみたところ、通信まわりのデバッグがたいへん快適になりました!
モバイルだけでなく、Web開発でも通信周りのデバッグでCharlesは威力を発揮しそうです。
導入も簡単で、10分あれば設定が完了します。

この記事では、以下についてまとめています。

  • Charlesの導入方法
  • 通信をキャプチャしたい端末側の設定(PC, iOS実機, iOSシミュレータ)
  • Charlesでのデバッグ方法

とにかく、みんなCharles使おうぜ!という布教記事です:innocent:

Charlesについて

公式サイト
https://www.charlesproxy.com/

モバイル開発・Web開発かかわらず、こんなことでお困りではないでしょうか?

  • サーバがメンテナンス中で通信まわりの実装の確認ができない
  • 開発用のモックサーバを書き換えに行くのが面倒
  • 正しい順番で複数のAPIを呼べているか不安…

プロキシアプリCharlesを使えば、自分のPC上で通信をキャプチャ&編集できるので、これらの悩みが一気に解決します!

有料アプリ(50US$)ですが、30日間は無料で利用できます。
ただし5分に1回ぐらい購入してねアラートが出る、かつ30分でアプリが強制終了します…
イラっとしたら購入しましょう :moneybag:

Charlesでできること

  • 通信のモニタリングがリアルタイムできる!(ブレイクポイントも置ける)
  • 通信のリクエスト・レスポンスを自由に書き換えられる!
  • 低速の通信状況をシミュレータできる!
  • リバースプロキシを試せる!

…などなど通信まわりのハンドリングが自由自在にできます。
激アツです:fire:

Charlesの導入方法

バージョン: 4.5.4 (2019/11/28時点での最新版)

1. Charlesのダウンロード

2. プロキシの設定

  • プロキシの設定を行う
    スクリーンショット 2019-11-26 14.32.14.png

  • 画像のように設定されていればOK
    スクリーンショット 2019-11-26 14.31.59.png

3. 証明書の設定

  • 証明書の設定を行う
    スクリーンショット 2019-11-26 14.33.11.png

  • キーチェーンアクセスが表示されるので、Charles Proxy CAを探す
    スクリーンショット 2019-11-26 14.30.41.png

  • 「常に信頼」に変更する
    スクリーンショット 2019-11-26 14.31.01.png

4. SSLプロキシの設定

  • SSLプロキシの設定を行う

    スクリーンショット 2019-11-26 14.34.24.png

  • 「Enable SSL Proxying」にチェックを入れ、「Add」をクリック
    スクリーンショット 2019-11-26 14.34.42.png

  • Hostにワイルドカード「*」を設定
    スクリーンショット 2019-11-26 14.35.25.png

これで下準備は完了です!簡単!

Charlesで通信をキャプチャしたい端末側の設定

1. ローカルPC

  • 「Proxy」 → 「macOS Proxy」を選択
    スクリーンショット 2019-12-01 23.09.30.png

  • PCの通信が表示されればOK
    スクリーンショット_2019-12-01_23_18_21.png

2. iOSの実機

※ 画像のOSのバージョンは12.3.1ですが、13.2.2も同様に設定できました。

プロキシの設定

実機の通信をキャプチャーするためには、Charlesを起動させているPCと同じIPアドレスで接続する必要があります。

  • [Charles側]以下を選択して、表示されたローカルIPアドレスをメモしておく

    スクリーンショット 2019-11-26 14.38.13.png

  • [実機側]Charlesを起動させているPCと同じwifiに接続して、接続先の詳細から「プロキシを構成」を選択
    スクリーンショット 2019-11-26 14.38.13.png

  • 「手動」を選択して先ほどメモしたIPアドレスを入力
    IMG_0016.png

証明書の設定

  • [実機側]PC側でCharlesを起動した状態で、Safariから http://www.charlesproxy.com/getssl を開き、「許可」を選択
    IMG_0018.PNG

  • 「設定」を開いて、インストールを完了させる
    IMG_0020.PNG
    IMG_0020.PNG

  • [Charles側]設定が完了すると、一度だけ以下のアラートが表示されるので「Allow」を選択
    スクリーンショット 2019-11-26 14.41.22.png

  • Charlesにシミュレータの通信が表示されていればOK

3.シミュレータ

※ 画像のシミュレータのXcodeバージョンは11.1です。

  • [Charles側]「Help」→「Install Charles Root Certificate in iOS Simulators」を選択して証明書をインストールする
    スクリーンショット 2019-12-02 0.52.46.png

  • [シミュレータ側]設定から「about」を選択
    スクリーンショット 2019-12-02 0.52.46.png

  • 「Certificate Trust Settings」を選択
    スクリーンショット 2019-12-02 0.52.46.png

  • Charlesの証明書をONに変更する
    スクリーンショット 2019-12-02 0.52.46.png

  • Charlesにシミュレータの通信が表示されていればOK

Charlesでのデバッグ方法

Charlesには色々なデバッグ機能がありますが、ここでは私が最もよく使っている2つのデバッグ機能をご紹介します。

1. 通信のレコーディング

  • 「Proxy」 → 「Start Recording」 を選択  
    スクリーンショット 2019-12-02 1.40.26.png

  • もしくは画面上部の記録スタートボタンを押下する スクリーンショット 2019-12-02 1.40.47.png

  • 通信が一覧で表示されるので、詳細を確認したいURLをクリック
    スクリーンショット_2019-12-01_23_18_21.png

2. 通信のレスポンス書き換え

  • ブレイクポイントを設定する 「Proxy」→ 「Brakepoint Settings」

    スクリーンショット 2019-11-26 15.05.35.png

  • 「Add」→ブレイクポイントを貼りたいURL(= 書き換えたいURL)を設定する
    スクリーンショット 2019-11-26 15.06.07.png

  • ブレイクポイントが追加される
    スクリーンショット_2019-11-26_15_05_56.png

  • ブレイクポイントを貼ったURLとの通信が走ると、通信をキャプチャするので「Edit Response」タブで通信のレスポンスを書き換え
    スクリーンショット_2019-11-26_15_05_10.png

  • 編集が終わったら画面下部の「Excute」をクリックすると、編集後の通信が走る

まとめ

プロキシアプリCharlesの導入とデバッグ方法について、簡単にご紹介しました。

公式サイトでは以下のようにCharlesが紹介されています。

Charlesは、デバッグを迅速で信頼性が高く、高度なものにします。時間とフラストレーションを節約できます!

実際にこのアプリを使ってみて、デバッグの時間とフラストレーションを節約できてるなーと実感しています:innocent:
Charlesを使って快適なデバッグライフを送りましょう!

参考文献

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

iOS リアルタイム入力でハッシュタグ形式に文字装飾するTIPS

本稿の目的

#から始まるハッシュタグの部分の色が変わり、タップするとハッシュタグの内容に応じたフィードの検索などを行う機能は最近ではあたりまえのUXになっています。

image.png

iOSでの文字装飾はNSAttributedStringで行うことが一般的ですが、文字装飾した部分をクリックできるようにしたり、TwitterやFacebookのようにリアルタイムで入力した内容に応じて文字装飾を行うためにはどのように実装するべきかを説明します。

Androidについても記述しています。よろしければどうぞ。

ラベルにハッシュタグの形式に相当する部分を文字装飾する&クリックできるようにする

ハッシュタグの形式に相当する部分を正規表現をして抽出する

文字装飾するにはUITextView.attributedTextに任意のattributeを設定したNSAttributedStringの実装を設定します。

今回では、ハッシュタグの形式に相当する部分を文字装飾するためにNSMutableAttributedStringを使用し、任意の文字列、もしくはすでにUITextViewに設定されている文字列からハッシュタグの形式に相当する部分を正規表現をして抽出し、文字装飾を行います。

extension UITextView {
    /// 「#」から始まるハッシュタグに文字装飾を設定する
    public func decorateHashTag() {
        do {
            // フォントの大きさなどを引き継ぐため、`NSMutableAttributedString init(attributedString:)`を使用する
            let attrString = NSMutableAttributedString(attributedString: self.attributedText)

            attrString.beginEditing()
            defer {
                attrString.endEditing()
            }
            // Emojiはサロゲートペアを含む
            // このため、Emojiを含んだ正規表現でのNSRangeのlengthは[String.utf16.count]を使用する
            let range = NSRange(location: 0, length: self.text.utf16.count)
            let regix = try NSRegularExpression(pattern: "(?:^|\\s)(#([^\\s]+))[^\\s]?", options: .anchorsMatchLines)
            let matcher = regix.matches(in: self.text, options: .withTransparentBounds, range: range)

            let results = matcher.compactMap { (tagRange: $0.range(at: 1), contentRange: $0.range(at: 2)) }
            for result in results {
                let attributes: [NSAttributedString.Key: Any] =
                    [.foregroundColor: UIColor.blue,
                     .underlineStyle: NSUnderlineStyle.single.rawValue]
                attrString.addAttributes(attributes, range: result.tagRange)
            }
            attrString.endEditing()
            self.attributedText = attrString
        } catch {
            debugPrint("convert hash tag failed.:=\(error)")
        }
    }
}

上記例ではUITextViewのextensionとしてハッシュタグの形式に相当する部分を文字装飾するメソッドdecorateHashTagを追加しています。
decorateHashTagでは、「空白、または行頭で#からはじまる1文字以上の空白までの最短一致の文字列」をハッシュタグとして検出、下線と文字色を青に装飾しています。

なお、Twitterなどでは#以降は記号や数字を許容していませんが、上記の例では空白以外のすべてを対象としています。
Twitterのように記号と数字を除き、「ひらがな、カナ、英字、漢字」のみを対象とする例だと正規表現は(?:^|\\s)(#([ぁ-んァ-ンーa-zA-Z一-龠\\-\\r]+))[^\\s]?とおきかえてください。

大事な注意点について

フォントの大きさなどを引き継ぐため、NSMutableAttributedString init(attributedString:)を使用する

NSMutableAttributedString init(string:)を使用して文字装飾の処理を開始すると、View側で設定していたフォントの大きさの設定が失われます。
NSMutableAttributedString init(attributedString:)で設定するか、改めて文字の大きさ設定を処理内で定義してください。

NSRangeのlengthはString.utf16.countを使用する

😡👨‍👩‍👧‍👧らのEmojiはサロゲートペアで表現されており、それぞれ、UTF-16で表現した場合は次の通りです。

let emoji = "😡"
NSLog("%@ count:=%d, utf16_count:=%d", emoji, emoji.count, emoji.utf16.count)

😡 count:=1, utf16_count:=2

let emoji = "👨‍👩‍👧‍👧"
NSLog("%@ count:=%d, utf16_count:=%d", emoji, emoji.count, emoji.utf16.count)

👨‍👩‍👧‍👧 count:=1, utf16_count:=11

このとき、NSRangeはString.UTF16Viewcountから長さを作るようにしないと、適切な範囲で正規表現マッチしてくれません。
こちらは 絵文字を支える技術の紹介 の記事を大変参考にさせていただきました。

(?:^|\\s)(#([ぁ-んァ-ンーa-zA-Z一-龠\\-\\r]+))[^\\s]?では世界を制せない

「記号と数字はハッシュタグの対象にしたくない」ということで、(?:^|\\s)(#([ぁ-んァ-ンーa-zA-Z一-龠\\-\\r]+))[^\\s]?の正規表現への置き換え例をあげましたが、この正規表現だと、ギリシャ文字(λ)などはハッシュタグとして認識されなくなります。これでは世界を制せませんね。

extension UITextView {
    /// 「#」から始まるハッシュタグに文字装飾を設定する
    public func decorateHashTag() {
        do {
            let attrString = NSMutableAttributedString(attributedString: self.attributedText)

            attrString.beginEditing()
            defer {
                attrString.endEditing()
            }
            // Emojiはサロゲートペアを含む
            // このため、Emojiを含んだ正規表現でのNSRangeのlengthは[String.utf16.count]を使用する
            let range = NSRange(location: 0, length: self.text.utf16.count)
            let regix = try NSRegularExpression(pattern: "(?:^|\\s)(#([^\\s]+))[^\\s]?", options: .anchorsMatchLines)
            let matcher = regix.matches(in: self.text, options: .withTransparentBounds, range: range)

            let results = matcher.compactMap { (tagRange: $0.range(at: 1), contentRange: $0.range(at: 2)) }
            let nsString = NSString(string: self.text)
            for result in results {
                let content = nsString.substring(with: result.contentRange)
                if !content.isOnlySupportedHashTag {
                    // Emojiを含む場合は対象外とする
                    continue
                }

                let attributes: [NSAttributedString.Key: Any] =
                    [.foregroundColor: UIColor.blue,
                     .underlineStyle: NSUnderlineStyle.single.rawValue]
                attrString.addAttributes(attributes, range: result.tagRange)
            }
            attrString.endEditing()
            self.attributedText = attrString
        } catch {
            debugPrint("convert hash tag failed.:=\(error)")
        }
    }
}

ただ、😡👨‍👩‍👧‍👧といった文字はハッシュタグにしたくないよ、という場合は、前述のコード例でサロゲートペアは除外する処理を追加してください。
!content.isOnlySupportedHashTagでは次のような拡張関数を定義することでチェックしています。Swiftでの絵文字のチェックは非常に大変なのでもし考慮不足があったらすみません。

private extension String {
    /// ハッシュタグサポート文字のみかチェックする
    /// 別途Stringの拡張で定義されている [String.containsEmoji]は一部のサロゲートペアなどに対応しておらず、
    /// 本処理ではハッシュタグとしてサポートしている文字のみが含まれているかをチェックするprivate拡張
    /// - returns: true...ハッシュタグサポート文字のみ、false...ハッシュタグでサポートしていない文字が含まれている
    var isOnlySupportedHashTag: Bool {
        return !self.contains { $0.isSingleEmoji || $0.isContainsOtherSymbol }
    }
}
private extension Character {
    /// OtherSymbolが含まれるかチェックする
    /// なお、OtherSymbolとは算術記号、通貨記号、または修飾子記号以外の記号を示す
    /// - returns: true...otherSymbolが含まれる、false...otherSymbolが含まれない
    var isContainsOtherSymbol: Bool {
        return self.unicodeScalars.count > 1
            && self.unicodeScalars.contains { $0.properties.generalCategory == Unicode.GeneralCategory.otherSymbol }
    }

    /// 1️⃣などの単体UnicodeであるEmoji是非
    /// - returns: true...Emoji、false...Emojiではない
    var isSingleEmoji: Bool {
        return self.unicodeScalars.count == 1
            && self.unicodeScalars.first?.properties.isEmojiPresentation ?? false
    }
}
いろいろな文字のUTF-16の単位

ハッシュタグ形式に文字装飾することと若干内容が外れてすみませんが、String.countString.utf16.countの比較で検査する際の参考資料として、各種文字がUnicode上でどのようにカウントされるかを示した図が以下になります。

説明 String.count String.utf16.count
絵文字 😎 1 2
囲い文字(1) 1 1
囲い文字(2) 1 1
囲い文字(3) 1 1
英字 Z 1 1
数字 1 1 1
漢字 1 1
ハングル文字 1 1
西方ギリシア文字 Δ 1 1
コプト文字 1 1
ラテン文字 è 1 1
キリル文字 Ж 1 1
グルジア文字 1 1
アルメニア文字 ա 1 1

このとき、上述に記載の表の通り、UTF-16単位数と比較するだけだと「囲い文字」などはハッシュタグとして認識されるようになります。
また、上述の表には入力の関係上記載がありませんが、フェニキア文字などは合成文字として取り扱われます。

ハッシュタグの形式に相当する部分をクリックできるようにする

前述の正規表現で抽出した範囲に対してNSAttributedString.Keylinkを設定し、UITextViewDelegate textView(_:shouldInteractWith:in:interaction:)でインタラプトしてください。

var attributes: [NSAttributedString.Key: Any] =
     [.foregroundColor: UIColor.blue,
      .underlineStyle: NSUnderlineStyle.single.rawValue]
// 自身のDelegateがHashTagInteractableのprotocolを実装しているかチェックする
if let interactable: HashTagInteractable = self.delegate as? HashTagInteractable {
   attributes[.link] = interactable.createHashTagURL(hashTag: content)
}
attrString.addAttributes(attributes, range: result.tagRange)

上記コードで初出したHashTagInteractableは次のようなprotocolです。

protocol HashTagInteractable: class {
    var paramKey: String { get }
    func getHashTag(interactURL: URL) -> String?
    func createHashTagURL(hashTag: String) -> URL
}
extension HashTagInteractable {

    var paramKey: String {
        return "hash_tag"
    }

    func getHashTag(interactURL: URL) -> String? {
        if let urlComponents = URLComponents(url: interactURL, resolvingAgainstBaseURL: true),
            let queryItems = urlComponents.queryItems {
            return queryItems.first(where: { queryItem -> Bool in queryItem.name == self.paramKey })?.value
        }
        return nil
    }
    func createHashTagURL(hashTag: String) -> URL {
        var urlComponents = URLComponents(string: "app://hashtag")!
        urlComponents.queryItems = [URLQueryItem(name: self.paramKey, value: hashTag)]
        return urlComponents.url!
    }
}

UITextViewDelegateを適合させた任意のクラスに対して、上記のHashTagInteractableのprotocolを適合させることで処理のインタラプトできるようになります。

extension ViewController: UITextViewDelegate, HashTagInteractable {
    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        // deleageに処理を委譲し、URLによる起動は行わない
        if let hashTag = getHashTag(interactURL: URL) {
            delegate?.sectionController(self, didTappedHashTag: hashTag)
            return false
        }
        return false
    }

}

UITextViewで入力中にハッシュタグ形式に変換する

UITextViewDelegate textViewDidChange:(UITextView *)textViewで入力の監視を行い、前述のUITextViewのextensionとして追加したメソッドdecorateHashTagを呼び出します。

ただし、このときに次のような点に注意して実装する必要があります。

  1. 文字列編集中の場合、とくに日本語変換中などはこの処理を行わないように制御する
  2. 前回編集時の文字装飾設定を除去する

これらの注意点を実装したUITextViewのextensionは次の通りです。

extension UITextView {
    /// 「#」から始まるハッシュタグに文字装飾を設定する
    public func decorateHashTag() {
        if self.markedTextRange != nil {
            // 日本語入力中などは適用しないように制御
            return
        }

        // 自身のDelegateがHashTagInteractableのprotocolを実装しているかチェックする
        let hashTagInteractable = self.delegate as? HashTagInteractable

        do {
            let attrString = NSMutableAttributedString(attributedString: self.attributedText)

            attrString.beginEditing()
            defer {
                attrString.endEditing()
            }

            // Emojiはサロゲートペアを含む
            // このため、Emojiを含んだ正規表現でのNSRangeの長さは[String.utf16.count]を使用する
            let range = NSRange(location: 0, length: self.text.utf16.count)
            let regix = try NSRegularExpression(pattern: "(?:^|\\s)(#([^\\s]+))[^\\s]?", options: .anchorsMatchLines)
            let matcher = regix.matches(in: self.text, options: .withTransparentBounds, range: range)

            // 前回設定していたハッシュタグ用の文字装飾を除去する
            attrString.removeAttribute(.foregroundColor, range: range)
            attrString.removeAttribute(.underlineStyle, range: range)

            let results = matcher.compactMap { (tagRange: $0.range(at: 1), contentRange: $0.range(at: 2)) }
            let nsString = NSString(string: self.text)
            for result in results {
                let content = nsString.substring(with: result.contentRange)
                if !content.isOnlySupportedHashTag {
                    // Emojiを含む場合は対象外とする
                    continue
                }

                var attributes: [NSAttributedString.Key: Any] =
                    [.foregroundColor: UIColor.blue,
                     .underlineStyle: NSUnderlineStyle.single.rawValue]
                if let hashTagInteractable: HashTagInteractable = hashTagInteractable {
                    // リンク化する場合は UITextViewDelegate textView(_:shouldInteractWith:in:interaction:)による起動を行う
                    attributes[.link] = hashTagInteractable.createHashTagURL(hashTag: content)
                }
                attrString.addAttributes(attributes, range: result.tagRange)
            }
            attrString.endEditing()
            self.attributedText = attrString
        } catch {
            debugPrint("convert hash tag failed.:=\(error)")
        }
    }
}

文字列編集中の場合、とくに日本語変換中などはこの処理を行わないように制御する

前述の例では、次のように編集中かどうかをmarkedTextRangeというメソッドで検査しています。

if self.markedTextRange != nil {
    // 日本語入力中などは適用しないように制御
    return
}

UITextViewDelegate textViewDidChange:(UITextView *)textViewで毎回このメソッドを呼び出している場合、で日本語入力で変換中であるかを検査せずに文字装飾を行うと、変換前で文字入力が確定してしまいます。
たとえば、ローマ字入力などで「か」という文字を入力しようとする場合、kaのキーを入力する必要がありますが、kの段階で入力が確定してしまい、「kあ」と入力のまま変換ができなくなります。
これを防止するために現在のUITextViewmarkedTextRangeを検査し、入力中かどうかを判定する必要があります。

前回編集時の文字装飾設定を除去する

UITextViewDelegate textViewDidChange:(UITextView *)textViewで毎回このメソッドを呼び出しており、NSMutableAttributedString init(attributedString:)を使用して文字装飾を開始している場合は、前回のハッシュタグ形式の文字装飾が引き継がれるため、内容の編集時に想定しない挙動を示します。
このため、事前に文字列範囲でハッシュタグ形式の文字装飾を除去してください。

// 前回設定していたハッシュタグ用の文字装飾を除去する
attrString.removeAttribute(.foregroundColor, range: range)
attrString.removeAttribute(.underlineStyle, range: range)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Combine で RxSwift の Single を置きかえる

この記事は iOS Advent Calendar 2019 の 2日目の記事です。

iOS13から、非同期処理を便利に扱うことができる、 Combine というフレームワークが使えるようになりました。

iOSアプリ開発でよく使われている RxSwift とよく似ているため、今までは RxSwift を使っていたけど、新規開発では Combine を使ってみようかな、と思っている方もいらっしゃるのではないかと思います。

個人的に RxSwift でよく使うのが Single で、 (本当はこれだけならもっと軽量なライブラリでも実現できるのですが) API通信の部分などによく使っています。

この記事では、この Single を使った実装を Combine で実現しようと思ったときに、意外と調べたりハマったりして時間を使ってしまったため、基本的な部分についてまとめてみました。

Single -> Future

Single に対応する Publisher (RxSwift の Observable に対応するものだが、 Observable は class なのに対して Publisher は protocol) は Future になります。

final class Future<Output, Failure> where Failure : Error

Single と同様、 1発だけ値を発行します。また、 Errorを指定する必要があります (Error は Swift言語にある Error 型です)。

RxSwiftの場合:

APIRequester
     .send(number: 1) // Single<String>
     .subscribe { event in
         switch event {
             case .success(let value):
                 print(value)
             case .error(let error):
                 print("error:\(error.localizedDescription)")
         }
     }

Combine の場合:

APIRequester
    .send(number: 1) // Future<String, Error>
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("finished")
        case .failure(let error):
            print("error:\(error.localizedDescription)")
        }
    }) { value in
        print(value)
}

Single.create {} -> Future {}

無名のObservable Future のインスタンスを作って返す関数を定義すれば良いです。

RxSwiftの場合:

import UIKit
import RxSwift

class APIRequester {
    static func send(number: Int) -> Single<String> {
        return Single.create { single in
            single(.success("\(number)"))
            return Disposables.create()
        }
    }
}

class ViewController: UIViewController {

    var disposeBag: DisposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        APIRequester
            .send(number: 1)
            .subscribe { event in
                switch event {
                    case .success(let value):
                        print(value)
                    case .error(let error):
                        print("error:\(error.localizedDescription)")
                }
            }.disposed(by: disposeBag)
    }
}

出力:

1

Combineの場合:

import UIKit
import Combine

class APIRequester {
    static func send(number: Int) -> Future<String, Error> {
        return Future<String, Error> { promise in
            promise(.success("\(number)")) // dummy
        }
    }
}

class ViewController: UIViewController {

    var cancellables: [AnyCancellable] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        APIRequester
            .send(number: 1)
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("finished")
                case .failure(let error):
                    print("error:\(error.localizedDescription)")
                }
            }) { value in
                print(value)
        }.store(in: &cancellables)
    }
}

出力:

1
finished

Single.zip -> Publishers.Zip

複数のFutureを実行して、すべてを待ち合わせて何らかの処理がしたい場合は、 Publishers.Zip (先頭が大文字)を使えば良いです。

RxSwiftの場合:

import UIKit
import RxSwift

class APIRequester {
    static func send(number: Int) -> Single<String> {
        return Single.create { single in
            single(.success("\(number)"))
            return Disposables.create()
        }
    }
}

class ViewController: UIViewController {

    var disposeBag: DisposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        Single.zip(
            APIRequester.send(number: 1),
            APIRequester.send(number: 2)
        ).subscribe { event in
            switch event {
                case .success(let value):
                    print(value)
                case .error(let error):
                    print("error:\(error.localizedDescription)")
            }
        }.disposed(by: disposeBag)

    }
}

出力:

("1", "2")

Combineの場合:

import UIKit
import Combine

class APIRequester {
    static func send(number: Int) -> Future<String, Error> {
        return Future<String, Error> { promise in
            promise(.success("\(number)")) // dummy
        }
    }
}

class ViewController: UIViewController {

    var cancellables: [AnyCancellable] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        Publishers.Zip(
            APIRequester.send(number: 1),
            APIRequester.send(number: 2)
        )
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("finished")
                case .failure(let error):
                    print("error:\(error.localizedDescription)")
                }
            }) { value in
                print(value)
        }.store(in: &cancellables)
    }
}

出力:

("1", "2")
finished

RxSwift to Apple’s Combine Cheat Sheet

GitHubに、RxSwiftとCombineのオペレータの対応表をまとめてくれている人がいます。

https://github.com/CombineCommunity/rxswift-to-combine-cheatsheet

これってCombineだとどうやるんだろう?と思ったときに大変便利です。

image.png

image.png

ただこの表の通りに機械的に置き換えられない部分はあり、たとえば今回ご紹介した Futureのイニシャライザ や、 Publishers.Zip など基本的な部分に関しても若干ひねりを加える必要がありました。

image.png

また、個人的に intervalオペレータを使いたいときがあり、 Timer.publish で代用できないかと試行錯誤してみたのですが、良い案が思いつきませんでした。もしアイデアのある方いらっしゃいましたらコメントくださると幸いです。

おわりに

この記事ではよく使われる RxSwift の Single と、Combine による非同期処理の実装例を紹介しました。

Combine は外部ライブラリの導入なしに書き始めることができる素晴らしい公式ライブラリですが、RxSwift とは細かいところが色々違うので慣れるのに時間がかかったり、Combine で提供されていないオペレータはどう実現するか悩む部分もありました。

もし導入や置き換えを迷ってらっしゃる方は、RxSwiftで実装している部分が、どう置き換えることができそうか、アタリをつけてから導入しはめるのがおすすめかもしれません。

以上 iOS Advent Calendar 2019 の 2日目の記事でした。 明日は @fromkk さんの記事です。

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

iOSのUIを構築する仕組みと学ぶステップを考える

過去を振り返って
iOSを学びはじめて一番最初に戸惑ったことは

どうやってUIを作成するのか?

ということでした。

最初Xcodeでプロジェクトを作成すると
Main.storyboardがあり
Storyboardを使ってUIを作成していくものだと思いましたが

色々なサイトで情報を調べてみると

  • コードレイアウト
  • Xib(Nib)
  • AutoLayout
  • autoResizingMask

など色々なUIの構築方法が出てきて
結局何が良いのかがわからなくなりました。

今回は
これからiOSを学ぶ人向けへの
UIの構築方法のまとめ記事があったので
それを参考にして

どのようなUIを構成する方法があるのか?
どういう時にどの方法が選ばれているのか?
何を学び、どう学ぶのか?

について見ていきたいと思います。

Xcodeの始まり

AppleのSDKは
1997年のスティーブジョブズの2番目のスタートアップの「NeXTSTEP」に由来し
そこから約30年もの間継続的に改良を続けてきました。
こちらの動画
Interface Builder(以降IB)のデモ動画でXcodeのルーツとなるものです。

Xcodeはその後改良が積み重ねられていますが
根本的な要素は同じです。

2種類のUIの構築方法

いくつかUIを作成する方法はありますが
大きく分けて2つのパターンに分かれます。
- グラフィカルなデザインツールを使用する。XcodeのIBを使って紙にステッカーを貼るようにUIの構成要素を貼っていきます。
- コードを中心にコードで個々の要素のインスタンスを生成し正しい位置に配置されるようにレイアウトします。

分類をグラフにすると下記のようになります。

UIHistory.png

ここからは個々について見ていきます。

XIB

macOSのアプリを作成するための一番最初のGUIのツールです。
XIBは「XML Interface Builder」の略で
かつてのNIB(NeXT Interface Builder)を継承しています。

スクリーンショット 2019-11-30 7.57.31.png

XIBはアプリがビルドされる際にNIBへと変換されます。
NIBファイルはアプリのバンドルの中に配置され
実行時にUIKitによって読み込まれ
様々なUIのオブジェクトのインスタンスを生成と設定を行います。

現在ではほとんどの役割をStoryboardに取って代わられていますが
TableViewやCollectionViewのCellや他の似た様なUIの要素を構築するのには有用です。

特にTableViewやCollectionViewはNIBに対しての特別なメソッドも存在します。
https://developer.apple.com/documentation/uikit/uitableview/1614937-register

XIBファイルは多くの場合にファイルの~Owner~としてトップレベルのオブジェクトを有しています。
多くの場合
これはUIViewControllerのサブクラスであったり
UITableViewCellやUICollectionViewCellであったりしますが
特に特別な制約はなく
Objective-Cに互換性のあるどんなオブジェクトも~Owner~として使用できます。

より具体的には
NSCodingプロトコルに適合しているクラスでしたら可能なため
特にXIBファイル用の特別なクラスを用意する必要はありません。

https://developer.apple.com/documentation/foundation/nscoding

Storyboard

エンジニアの中には

XIB + Flow = Storyboard

と考える人もいるように
Storyboardは
XIBで構成する複数のスクリーンと
それらの画面遷移をSegue
で構成します。

スクリーンショット 2019-11-30 7.57.14.png

アプリの流れが把握しやすくなった

例えばボタンをタップした時に他の画面へ遷移する場合は
下記のようにStoryboard上でそれを構成することができます。

スクリーンショット 2019-11-30 8.00.49.png

Storyboardの登場によって
画面もグラフィカルに見ることができるに加え
アプリの全体的な動き(Flow)もひと目で把握できるようになりました。

LaunchScreen

プロジェクトを作成した際にLaunchScreen.storyboardというファイルがあります。
これはアプリが完全に起動する前にユーザーに表示されるデフォルトのイメージを設定できます。

スクリーンショット 2019-11-30 8.07.22.png

これはinfo.plistで他のStoryboardに変更が可能です。

スクリーンショット 2019-11-30 8.08.00.png

このLaunchScreenでは
アプリのデフォルトの状態を表す
「土台」的な画面がよく使用されています。
(アプリのロゴを真ん中に表示するなど)

元々は
デバイスのサイズごとに複数のPNG画像を
用意していましたが
iOSやiPadOSで様々な解像度をサポートするようになったことから
storyboardを使って自動で調整されるようにすることが
推奨されています。

一方で
このLaunchStoryboardには多くの制限があります。

例えば
システムで用意されているクラスを使用しなければならず
カスタムクラスを設定することができません。(UIViewControllerのサブクラスなど)
これはアプリが完全に起動する前に表示される画面のため
アプリ内で定義されているクラスの準備がまだできていないからです。

また
ローカライズが必要な要素など
起動時に動的に決定されるようなものも指定できません。

Always create a final version of the interface you want. 
Never include temporary content or content that requires localization. 

https://developer.apple.com/documentation/uikit/app_and_environment/responding_to_the_launch_of_your_app

Spring and Strut

これはiOSのViewをレイアウトするための独自の方法です。

スクリーンショット 2019-11-30 8.30.41.png

SpringまたはStrutという
親のViewの変化に応じてどのようにViewを拡大縮小されるかを
決める2つの仕組みがあります。

Spring

親Viewがリサイズされるのに比例してViewをリサイズします。

Strut

親Viewとの距離を保つようにします。

拡大縮小できるのは下記の6つになります。

  • Subviewの高さ(Spring)
  • Subviewの幅(Spring)
  • SubViewの上下左右4つのmargin(Strut)

下記のようにIB上でどのように動くのかを見ることができます。

画面収録-2019-11-30-8.37.24.gif

この場合は
Springは高さ、幅両方に設定されているため親Viewの拡大縮小に合わせて変化します。
Strutは上と左に設定されているため上と左の距離は保っています。

また
autoresizingMaskという
プロパティからコードで設定することも可能です。

https://developer.apple.com/documentation/uikit/uiview/1622559-autoresizingmask

このSpringとStrutはかなり歴史の古いもので
新しいプロジェクトはほとんどAutoLayoutに置き換えられています。
実際UIKitはautoresizingMaskをAutoLayoutの制約に内部で変換しています。


コードレイアウトを行う際など
この変換が不要な場合は
translatesAutoresizingMaskIntoConstraints
をfalseにします。

https://developer.apple.com/documentation/uikit/uiview/1622572-translatesautoresizingmaskintoco

Auto Layout

iOS6で登場しUIKitの要素を配置する最も好ましい仕組みとされています。
https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/index.html

AutoLayoutの基本設計は1998年の論文を元にされています。
https://constraints.cs.washington.edu/cassowary/

AutoLayoutは
あるSubviewの属性(attribute)が
他のSubviewや親Viewの属性と
どのように関連しているのかを表す制約(Constraint)を
宣言的に作成します。

これらの制約は線形方程式でこの方程式が解かれるタイミングで
Subviewの位置と大きさが決まります。

つまり
この線形方程式はUIWindowをルートにした
それぞれのViewの階層において解決される必要があります。

逆に言うと
この方程式が解決できない場合にレイアウトの設定に失敗します。
Consoleにエラーのログが出てくるのはこれが原因です。

Viewのレイアウトを決定する方法は主に2つあります。

NSLayoutConstraint

一つの制約を表現するオブジェクトで
これを生成して親Viewに追加していきます。

Visual Format Language

StringベースのDSLで一つの宣言で複数の制約を設定できます。

https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/VisualFormatLanguage.html

AutoLayoutは複雑になってしまう場合がありますが
iOS9で登場したUIStackViewを利用することで
横並びまたは縦並びのSubview同士のレイアウトは
自動で設定してくれるなど
自分で制約を特定する必要も減ってきています。

UIKit Dynamics

これまでに紹介してきたものは全て「静的な」レイアウトの設定をするものでしたが
UIKit Dynamicsは名前の通り「動的な」レイアウトを設定します。

https://developer.apple.com/documentation/uikit/animation_and_haptics/uikit_dynamics

これは2次元の物理演算エンジンで
スクリーン上であたかも本当の物質に触れているような動きを
シミュレーションします。

UIKit DynamicsはAutoLayoutに類似した制約を作成しますが
位置やサイズを固定する代わりに対象のものを動かすために設定されます。
そのためUIに一定レベルの動きを生み出します。

例えば下記のような動きをUIKitが自動で引き起こしてくれます。

画面収録-2019-11-30-9.34.23_480.gif

コード例
import UIKit

class ViewController: UIViewController {

    @IBOutlet private weak var snapView: UIView!
    private var animator: UIDynamicAnimator!
    private var snapBehavior: UISnapBehavior!

    override func viewDidLoad() {
        super.viewDidLoad()

        animator = UIDynamicAnimator(referenceView: view)
        snapBehavior = UISnapBehavior(item: snapView, snapTo: view.center)
        animator.addBehavior(snapBehavior)

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(pannedView))
        snapView.addGestureRecognizer(panGesture)
        snapView.isUserInteractionEnabled = true
    }

    @objc
    private func pannedView(recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            animator.removeBehavior(snapBehavior)
        case .changed:
            let translation = recognizer.translation(in: view)
            snapView.center = CGPoint(x: snapView.center.x + translation.x,
                                      y: snapView.center.y + translation.y)
            recognizer.setTranslation(.zero, in: view)

        case .ended, .cancelled, .failed:
            animator.addBehavior(snapBehavior)
        case .possible:
            break
        @unknown default:
            break
        }
    }
}

注意する点としては
UIKit DynamicsとAuto Layoutの制約は
お互いに干渉しません

一般的なAuto Layoutは全てのSubviewの大きさを取り扱うべきで
UIKit DynamicsはそのSubview達が親Viewの中で
どのように配置されるのかを取り扱います。

※ UIKit DynamicsとAuto Layoutの両方が適用された場合です。

Manual Layout

これまではUIKitにサイズや位置を決めてもらえるように
設定をしてきましたが
手動で計算をすることもできます。

一番シンプルな方法は
layoutSubViews()をoverrideする中で
全てのSubviewsのframeに値を設定することでできます。
https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews

intrinsicContentSizesystemLayoutSizeFitting(_:)
UIKitが自動計算した結果ですが
このような値も参照することで手動で計算する際に活用できます。

https://developer.apple.com/documentation/uikit/uiview/1622600-intrinsiccontentsize
https://developer.apple.com/documentation/uikit/uiview/1622624-systemlayoutsizefitting

Manual Layoutはおそらく最も強力なレイアウト方法ではありますが
最も難しい方法でもあります。

しかし
カスタムUIControlを作成する時など
Manual Layoutが必要な場合もしばしば見られます。

https://developer.apple.com/documentation/uikit/uicontrol

SwiftUI

iOS13で登場したUIを構築する新しいフレームワークです。
SwiftUIには下記のような特徴があります。

Declarative(宣言的)

「宣言的」というのは各要素がどういったものかを「宣言」するだけで
どうやってそれらを組み合わせていくのかはフレームワークに任せるような方法です。

Manual LayoutよりもAuto Layoutの制約の設定に近い形で
コードを記載していきます。

Binding

これはデータとそれに応じて変化するUIが双方向に繋がっていて
どちらかが変化するとそれと同期してもう一方も変更されることで
不整合な状態を防ぐことができます。

フレームワークがこれを内部で実行するため
コードで記載する必要もありません。

元々macOS用のIBでは可能でしたが
SwiftUIの登場で他のプラットフォームでも可能になりました。
https://developer.apple.com/documentation/appkit/nsobjectcontroller

まだまだこれから進化していく段階

SwiftUIにはUIKitの機能を完全にカバーし切れておらず
UIKitと並列で使用する機会多いように見られます。

しかし
SwiftUIの開発が進み
将来的にはUIKitの全ての機能を網羅した際には
UIKitに取って代わるかもしれません。

いつ何を使うのか?

これまで見てきた仕組みにはそれぞれ長所と短所があります。
それに応じて使い所や使い方も変わってきます。

UIの構築

StoryboardとXIBで構築する

Storyboardは一連のUIとFlowを一緒に構築できます。
これによって他の開発者もアプリ流れや構成を理解しやすくなります。

XIBはほとんどStoryboardに取って代わられましたが
TableViewやCollectionViewのCellの作成や
複数のStoryboardで繰り返し使用するようなカスタムViewを生成にも適しています。

Cellが一種類の場合はStoryboard上で直接レイアウトをする方が簡単ですが
複数種類のCellを表示する必要がある場合などは
XIBで個々のCellを作成した方が管理がしやすくなります。

コードで構築する

大規模なiOSアプリの開発ではコードでレイアウトを書くケースも多くあります。
例えば複数人で同じ画面の機能を実装する可能性があります。

この場合に
同じ箇所に変更を加えると
GitのようなSource Controlシステムで
コンフリクトが発生します。

コードの場合はある程度自動でコンフリクトを解消してくれますが
StoryboardやXIBはXMLベースのファイルで
システムがどの解決方法が正しいのかを判断できず
コンフリクトを解決するのは非常に困難です。

結果として手動で解決しなければならなくなりますが
ファイル自体が人が理解できるような形で記載されていないため
解読に時間も労力もかかってしまいます。

レイアウト

Auto Layoutをデフォルトで使用するべきです。
SpringとStrutは
iOS5以前をサポートしている場合など特別な理由がない限り
使用する理由は見つかりません。

一方でAuto Layoutだけでは表現しきれない時は
Manual Layoutは現実的な選択肢になります。

例えば
Auto Layoutの制約が有効な範囲は
親Viewとその直接のSubview(子View)との間だけで
SubviewのSubview(孫View)には効きません。
こういう場合にManual Layoutが必要になります。

アニメーション

ユーザの動きに合わせたアニメーションが必要な際は
UIKit Dynamicsを使用すると
Appleの提供するアプリのような動きを実現することができます。

例えばApple Storeのアプリで
アプリを選択したときのズームしてバウンドする動作は
Core Animationで手動で実装しなくても
UIKit Dynamicsを使って実現できます。

SwiftUI

もしiOS13以降をターゲットをしているならば
入力フォームなどはSwiftUIのBindingがとても役に立ちます。

一方で
まだまだ不足している機能や不具合と思われる動きもあり
全てをSwiftUIにするのは難しい段階であります。

rootViewControllerやトップレベルのViewControllerには
UIKitを使用しその先の部分的な機能にSwiftUIを活用していくのが
良いのかと思われます。

何から学び、どう学ぶ?

最後にこれらの仕組みの中で
何を学びまたどうやって学ぶのかについて考えたいと思います。

もしiOSエンジニアとして働きたいと思うならば
上記で紹介したもの全てに関して学ぶ必要があります。

参加した開発プロジェクトで
SpringとStrutを使用している可能性もありますし
手動で位置やサイズを計算しているかもしれません。

学習ステップ

SwiftUIに関しては少し控えておいて良いでしょう。
多くのアプリでは
2世代前のOSバージョンまでサポートしていることが多く
SwiftUIを本格的に使用するのは
2年以上先になる場合が多いのではないかと考えられます。

StoryboardでAuto Layoutを利用する

学習を開始してそれ用のアプリを作ろうとした時は
まずStoryboard上でAuto Layoutを使ってUIを構築してみましょう。
その際にUIStackViewも効果的に使えるように学んでいきましょう。

コードでAuto Layoutを利用する

次にVisual Format Languageや
NSConstraintでのAuto Layoutの設定方法についても学びましょう。

Storyboardに加えてコードで設定する方法を学ぶことで
Auto Layoutがどのように働くのかが把握できるようになります。

コードでAuto Layoutを動的に変更する

次にアプリの実行中に動的に制約を変更する方法を学びます。
これはNSConstraintで個々の制約を作成し
constantプロパティを変更することで実現できます。

https://developer.apple.com/documentation/uikit/nslayoutconstraint/1526928-constant

制約を追加したり削除することで
レイアウトがどのように変化するかも学ぶことができます。

animationブロックで囲むことでレイアウトの変化を
視覚的に見ることもできるようになります。

UIKit Dynamicsを利用する

コードでのAuto Layoutの設定に慣れてきたら
UIKit Dynamicsについて学んでみましょう。
制約をどのように設定することで
上手くアニメーションするのかを学びましょう。

Apple Storeアプリの動きなどは
良いお手本になります。

デバイスのスクリーンの録画などもできますので
録画して比較してみるのも良いかもしれません。
https://developer.apple.com/documentation/uikit/nslayoutconstraint/1526928-constant

SwiftUIを学び始める

これらの学習の上にSwiftUIを触ってみましょう。
全ての要素が含まれているUIViewControllerを実装しているのに近い感覚で
使ってみましょう。
使う際はより小さく明確な機能を扱うコンポーネントとして使用しましょう。
ユーザがデータを入力するようなFormなどのUIで力を発揮します。

UIHostingControllerを利用することで
UIViewControllerの階層に追加することもできます。
https://developer.apple.com/documentation/swiftui/uihostingcontroller

UIViewControllerRepresentable
UIViewRepresentableに適合することで
UIViewとして利用することも可能になります。
https://developer.apple.com/documentation/swiftui/uiviewcontrollerrepresentable
https://developer.apple.com/documentation/swiftui/uiviewrepresentable

まとめ

iOSでUIを構築する仕組みについて見ていきました。

歴史を重ねるに連れて
使われている仕組みは進歩していますが
どんなものにでも使える方法というものは確立されておらず
それぞれの長所や短所を考慮して
色々な仕組みは利用されているプロジェクトに出会うこともあると思います。

そこで
全体的な知識や経験を持っていることで
開発をスムーズに進めることができるのではなるでしょう。

大事なのは
学習して使ってみて感覚として知ることですので
少しづつでも触ってみるのがよいのかなと思います😃

もし何か間違いなどございましたらご指摘頂けましたら幸いです🙇🏻‍♂️

参考記事

https://cutecoder.org/programming/newbie-learn-ios-user-interface-programming/
https://medium.com/@raulriera/uikit-dynamics-in-the-real-world-ef0dfd924260

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

PowerBIのスマホ用Vizについて考えてみた

はじめに

 2019年9月に、KibunlogというiOSのアプリで入力した気分を自分なりの観点で集計しよう、ということで、気分入力後、Twitterにハッシュタグ付きで投稿された気分を、Power Automateで拾い、OD4B上のExcelに記録、Power BIでパソコンでもスマホでも見えるようにしました。

IMG_0869.jpeg

 その時、自分が作ったスマホ用Vizがイマイチだなぁ、、と感じ、その後もこれ以外にスマホで見えるものを作っているのですが、やっぱりレイアウトや使う側の視点からみて等イマイチ...。ということが続きましたので、サンプルアプリ等をあらためて眺め、モバイルで少しでもクールにPower BIで見るには...と見てみた、やってみたことについて書きたいと思います。

モバイルのシンプルでカッコいいサンプルを見ると...

 まずは、サンプルの説明ページなどを見て調べてみました。

すると...
いくつかのサンプルは、「カード」が複数枚並べられていて、典型的な棒グラフが並んでいる... だけだけど、シンプルでクールでかっこいい(私が前に作ったものはイマイチ)

Power BIサービスで見てみると、ダッシュボードでPhoneビュー幅一杯につくられている。
それを並べているだけといえば、だけ!か。

IMG_0869.jpeg

この画面はダッシュボードで一番上のカードを広めに縦に引っ張ってみたのですが、リアルタイムにスマホでも反映が見られるんですね。面白い〜。

これまで作ったものは、やけに幅を縮小させたり、縦に長くするのをけちったりしていたので、スマホで見たらかっこ悪いし、操作性が悪かったのでした。

モバイルアプリの入り口はダッシュボードで

 ダッシュボードの他に、レポートも見られるのですが、スマホに適した入り口はやっぱりダッシュボードですね。スワイプの操作性てにぴったり。レポートですといきなり横表示になったりして、なんだ?と思っていました。

やってみよう! Formsアンケートの可視化

 
 では、気がついたことを教訓に、本日から開始した先週土曜日に行ったISACA調査研究委員会の合同研究会のアンケートをFormsで作成、それをPower AutomateでOD4B上のExcelに流し、それをPower BIサービスで読み込んで...と。

 「Formsのアンケートを可視化してスマホで見る必要ありますか?」という声が聞こえてきそうですが、標準のアンケート集計画面

  • 表示形式が決まっている
  • テキストの項目は直近が少ししか読めない
  • スマホでは特に詳細が見られない
  • 自分のみたい集計結果になっていない

といった感じなので、自分の好きな回答結果はPower BIで作るべし、というセルフBIの価値はあると思っています。

FormsからPower BIのデータソースとなる部分は、Power Automateで適当に...

Phoneビューで幅一杯に調整して...

それで... できました~

コメントもちゃんと見られました~(横にすれば大きくてたくさん読める :-) )

IMG_0419.png

運営企画サイドとしては、モチベーションあがる~

Power Automateをスマホアプリでみたらこんな感じ。あらためてですが、スマホで見られる、なんて、すごい...

実行履歴もちゃんと詳細に確認できます。

Siriからも呼べるレコメンドもあったりww

便利だな~

まとめ

 今回は、Power BI Desktop Proを使わずに、Power BIサービスのみで作りました。Yugoさんに、OD4B上のExcelで5分程度で、Vizが更新される、というのを教えてもらった方法でやりました。ブラウザのみでちゃっちゃと作って、スマホでも見られるって素敵ですな~

 朝仕事前にアンケートを作り、夜仕事毎に独自のVizでスマホで回答集を見られる、なんて素敵な時代なんでしょう!

 また一般論でも、(私のような)初級者にとって、サンプルのVizやアプリってとても大事だなぁと思っています。そのサンプルを(まだまだ浅いですがww)深堀りして見てみることって大事だよね、と考え書きました~。今後も、Microsoft Teamsがどんどん使いやすくなっていく中、スマホでPower BIで気軽に見ることが、増えていくんじゃないですかね。

 お読みいただきありがとうございます。

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

Apple Human Interface Guidelinesを読んでみた① 〜設計原則について〜

はじめに

iOS開発を行なっているうちにUI(UX)の重要性を感じ始めた。またiOSにおいては「なんとなくカッコイイから使ってみよう」と好き勝手にデザインして行けばいいものでは無く、Apple様がある程度ガイドラインを示して下さってるため、それに最低限従わねばならないというのにもうすうす感じ始めた。
察し始めた以上は勉強せねば、ということで改めてAppleが推奨するUIデザインを勉強しようと思い学習記録を付けていくことにした。

基本的にはタイトルにもある通り、Apple Human Interface Guidelinesを読みながらAppleが推奨するUIデザインを学んでいく。

Human_Interface_Guidelines.png

iOSが他のプラットフォームと違う点

Appleによると3つの点で他のプラットフォームと違うようだ。
その3つとは「Clarity(明快さ)」、「Deference(敬意)」, 「Depth(深さ)」である。

Clarity(明快さ)

デザインの分かりやすさである。

Deference(敬意)

ユーザーがコンテンツと対話(?)することができる。具体的には、ベゼル、グラデーション、ドロップシャドウの使用を最小限に抑えて、インターフェースを分かりやすい状態にすること。

Depth(深さ)

階層構造が目に見えて分かりやすく、現実的な動きを実現されていることで、ユーザーが階層を深く潜って行った際もコンテキスト(文脈)を理解できるようにする。

このあたりは、頭の片隅に置いておいて、あとあと具体的事例を見て行く際に思い出してあげるとしよう。

設計原則

デザインをの効果を最大限発揮するために、留意しておくべき基本原則である。

具体的には
「Aesthetic Integrity(美的完全性)」、「Consistency(一貫性)」、「Direct Manipulation(直感的な操作)」、「Feedback(フィードバック)」、「Metaphors(比喩)」、「User Control(ユーザーの制御)」
の6つがある。それぞれの詳細を見て行く。

Aesthetic Integrity(美的完全性)

美的整合性はアプリの外観と動作がその機能とどの程度うまく統合されているかを表します。

要するに「ユーザーの意図しない変な動きはしない」ということである。突然変な動作をされるとユーザーはアプリケーションに集中できなくなってしまう。

Consistency(一貫性)

一貫性のあるアプリは、システムが提供するインターフェイス要素、よく知られているアイコン、標準のテキストスタイル、統一された用語を使用することにより、使い慣れた標準とパラダイムを実装します。

基本的にapple公式のデザインを使えば、アプリ間での一貫性が担保されるため、どのユーザーでも使いやすい実装になりますよということ。

Direct Manipulation(直感的な操作)

画面上のコンテンツを直接操作することで、人々を引き付け、理解を促進します。

Feedback(フィードバック)

ユーザーの行なった操作に対して、適切なフィードバックを返すことでユーザーに対してアクションの結果を明確に示す。
アプリ一覧画面でアプリを長押しした際に発動する微振動も立派なフィードバックで、実装以降知らず知らずのうちにユーザーはそのリアクションに頼っている。

Metaphors(比喩)

アプリの仮想オブジェクトとアクションが身近な体験のメタファーである場合、(中略)、人々はより素早く学習します。

本を読むことができるアプリでは、「現実世界でページをめくるようにスワイプして次のページへ行く」などユーザーが理解しやすいような実装が望ましい。

User Control(ユーザーの制御)

iOSでは、アプリではなく人が制御されます。アプリは一連のアクションを提案したり、危険な結果について警告したりできますが、通常はアプリが意思決定を引き継ぐのは間違いです。最高のアプリは、ユーザーを有効にすることと、望ましくない結果を回避することの間の正しいバランスを見つけます。

終わりに

初回ということもあり、かなり抽象度の高い内容が多かった。しかし、これらの設計原則は会社でいう基本理念と同じくらい重要なものであるに違いないため、これから個別具体的な事例を学ぶ上で常に頭に入れておきたい。

参考

Apple Human Interface Guidelines

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

アイコンWebフォントをiOSで使う

FontAwesome 4.0
http://fortawesome.github.io/Font-Awesome/icons/
FontAwesome 3.2
http://fortawesome.github.io/Font-Awesome/3.2.1/icons/

FontAwesomeKitを使ってiOS 7用のアイコンを動的生成する
http://qiita.com/shobyshoby/items/8f04e97146794057a69b

よく使うアイコン画像を「Font-Awesome」を使って生成する方法
http://qiita.com/EntreGulss/items/fdf0c04a0bc2b926d35c
あと、BButtonってやつでも、FontAwesomが使えます
BButton
https://github.com/mattlawer/BButton
こういうのは アイコンフォントと言って、いくつか種類があって、
FontAwesomeKit の中で、FontAwesome 以外にも、
Foundation Icon とかいろんなアイコンフォント使えます。

画像アイコンはもう古い!CSSでスタイル自由自在のアイコンWebフォント
http://w3q.jp/t/2396

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

iOS13のUISearchBarにつまずいた

はじめに

今まで UISearchBar のテキストフィールドをカスタマイズしたい場合は下記のようにアクセスする方法が主流でした。

let textField = searchBar.value(forKey: "_searchField") as? UITextField

しかし、iOS13 では実行すると下記のようなエラーとなりアプリがクラッシュします:scream:

Terminating app due to uncaught exception 'NSGenericException', reason: 'Access to UISearchBar's _searchField ivar is prohibited. This is an application bug'

This is an application bug

:scream::scream::scream:

対応

iOS13 からは searchTextField というプロパティが追加されたようです。

こちらは UISearchTextField というクラスのようですが UITextField を継承しているので今まで通りテキストフィールドとして利用できます。

iOS13 以前では今まで通り value(forKey: "_searchField") でアクセスしないといけないので下記のような extension を作ると使いやすいと思います。

extension UISearchBar {
    var textField: UITextField {
        if #available(iOS 13.0, *) {
            return searchTextField
        } else {
            return value(forKey: "_searchField") as! UITextField
        }
    }
}

これでどの画面でも searchBar.textField でアクセスできるようになりました:tada::tada::tada:

その他UISearchBarの新機能

iOS13 からは下記のクラスが追加されており色々新機能があるようです。

UISearchTextFieldDelegate というプロトコルも追加されているみたいです。

下記の記事に詳しく書いてありました。

UISearchToken がおもしろそう

さいごに

UISearchBarvalue(forKey: "_searchField") という非合法のやり方でテキストフィールドにアクセスする Developer があまりにも多いので公式で API を用意してくれたんですかね?

This is an application bug

というくらいですからお前らもう公式で用意したからそういうことやめろよ?って感じがします:sweat_smile:

余談ですが UIAlertController も下記のように色々カスタマイズできます。

UIAlertAction の setValue(_:forKey:) で下記のようにキーを指定すると色々できるようです。(参考)

  • image
  • imageTintColor
  • titleTextColor

他にも色々いじれそう...(参考)

こんな感じ

alert

UIAlertController のカスタマイズもみんなが使いまくれば公式 API が公開されるかも?:hatching_chick:

参考

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

アマゾネス、MacOSアプリを作ってVTuber(偽)デビュー

:snowman::christmas_tree:Advent Calendar 2019/12/02:christmas_tree::snowman:

アマゾネスでございます。
本日は、ワイがVTuberデビューした話、いや、そのMacOSアプリを作った話をします。

はじまり :cinema:

弊社では一週間の動向を社内ニュース(動画)として全社員に共有しています。
そして、最近私が実装した機能がリリースされたので、出演依頼がきてしまった...
依頼が来ること自体は、非常な光栄なことですし、一社員として協力したと思ました。が、

ワイ(実体)が動画で記録されるなんてマジ無理 :innocent:

ということで、イラストでの出演交渉を行い、承諾してもらいました。
ちなみに、ここの段階では動かないイラストでの出演予定でした。

あの日の白ワイン :champagne:

出演前の週末、私はランチにカキフライを食べていました。
「白ワインあいそうやな...飲んじゃおっかしら(゚∀゚)」
白ワイン、マジで美味しい。牡蠣にあう。
一杯のつもりが、が、ががが....

絶好調になった私は思いました。
「ワイのイラストでVTuber的なことできるんじゃねぇか...?」

どういう脈絡...だよ、と思うかもしれませんが、
酔っ払いが考えることは大体意味ありませんのでそういうことです。

私はまず、イラストをVTuber化できる無料ソフトを探しました。
が、本気度高いものばかりで、5秒でかける私のイラストに適した超軽量ソフトがない:cry:

更に絶好調になった私は思いました。
「つくれるやろ」

はい、そしてソフトを作ることにしました。

どんなものをつくったか :construction_worker_tone2:

こちらをご覧ください

カメラやマイクを使うための準備の処理や、画像切り替えの処理については特に変わったことはしていません。
なので、今回は目と口を動している判定処理のところだけご紹介したいとおもいます。

しゃべるワイ :lips:

まず、しゃべっているのを表現したかったので、口を動かすことにしました。

ベースとなるイラストはこちら。
スクリーンショット 2019-12-01 22.06.38.png
用意したパーツはこの二枚。
スクリーンショット 2019-12-01 22.06.01.png

画像はTayasui Sketchesというアプリを使ってiPadで書きました。

口を動かすのはVisionフレームワークを使って試す?と考えたりしたんですが、口を動かすイメージがサウスパークの感じ、且つ、簡単に実装したかったので、Macのマイクからの音声入力を利用し、私がしゃべったら口がパクパクするようにしました。

判定しているコードはこんな感じ

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection){
        if output is AVCaptureAudioDataOutput {
            caputredAudioCount += 1
            let audioChannels = connection.audioChannels
            audioChannels.forEach {
                audioOutput(averagePowerLevel: $0.averagePowerLevel)
            }
        }
    }

    private func audioOutput(averagePowerLevel: Float){
        // 判定回数の制限
        guard caputredAudioCount % 10 == 0 else { return }

        // 音声があるかどうかの判定
        findVoice(-20 < averagePowerLevel)
    }

findVoice()は、声があった場合はTrue, ない場合Falseを渡して実行するようにしています。実行される処理は、Trueの時は口を開けた画像と閉じた画像を交互に表示、Falseの時は口を閉じた画像だけを表示する、となっています。

averagePowerLevelには、マイクの最大入力を 0dB として、入力レベルの値が入ってくるみたいです。
割と静かな環境だと、大体 -30 前後ぐらいの値が入ってきて、マイクに向かって声を発すると -7 〜 -3 ぐらいの値が入ってきました。
色々試した結果、-20 ぐらいを閾値とすると、小さな環境音には反応せず、楽にしゃべっても、しゃべっている(True)という判定となるようになりました。

また、判定をかける回数を制限しています。
制限しないと、口をパクパクしているアニメ(口を開けた画像と閉じた画像を交互に表示)が超高速すぎて口がなくなる、また、画像の更新が多すぎてフリーズするためです:angel_tone2:

まばたきするワイ :eye:

しゃべるのが割とあっさりできたので、Macのカメラで私を撮り、私のまばたきにあわせて、イラストの目もパチパチさせてみることに。
目は Core Imageフレームワークに含まれる、CIDetector を使用し、目を閉じているかどうかを判定して実現しました。

用意したパーツはこの二枚。
スクリーンショット 2019-12-01 22.06.27.png

判定しているコードはこんな感じ

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection){
        if output is AVCaptureVideoDataOutput {
            caputredVideoCount += 1
            guard caputredVideoCount % 5 == 0 else { return }
            videoOutput(buffer: sampleBuffer)
        }
    }

    private func videoOutput(buffer: CMSampleBuffer){
        // 判定回数の制限
        guard caputredVideoCount % 5 == 0 else { return }

        // 認識に使用するCIImageを作る
        let pixelBuffer:CVImageBuffer = CMSampleBufferGetImageBuffer(buffer)!
        let ciImage = CIImage(cvPixelBuffer: pixelBuffer)

        // 目の状態を認識するための準備
        let detector : CIDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options:[CIDetectorAccuracy: CIDetectorAccuracyLow] )!
        let options = [CIDetectorEyeBlink : true]

        // 目が閉じているかを判定
        let features = detector.features(in: ciImage, options: options)
        for feature in features as! [CIFaceFeature] {
            if feature.leftEyeClosed { eyeBlink() }
        }
    }

CIDetector を使うと、キャプチャされた画像の中に特徴、例えば顔やバーコードがあるかを認識することができます。そして、オプションに指定されたキーを渡すことで、詳細な特徴の情報が得られるようになります。

今回は目が閉じているかどうか知るためCIDetectorEyeBlinkを使用しました。結果では、右目、左目両方の情報が別々に取得できます。人間は片目づつまばたきすることはあまりないので、今回は左目が閉じているかの情報だけで判定しています。

左目を閉じたときにeyeBlink()を実行するようにしています。eyeBlink()は、目が閉じている画像を表示し、0.5秒後に目を開けている画像に戻すという処理を実行します。
まばたきは口のように頻繁に画像を変更する必要がない(パクパクさせなくていい) ので、Boolを都度渡して目を開けたり閉じたりする処理にせず、一度の実行でまばたきを一回するようにしています。

また、しゃべるのと同様に、判定をかける回数を制限しています。
しゃべる判定より判定回数が多いのは、瞬きは発声の様に連続的な事象ではなく、一瞬で完了してしまうので、判定する回数自体は増やしています。

まとめ :v:

普段はiOSのエンジニアをしている私が、今回MacOSのアプリを作ってみました。
やってみる前は、超難しそうというイメージでしたが、全然そんなことありませんでした。
多少なりと違うところはありますが、調べれはすぐ分かりましたので、今後他のツールも作ってみようかと思います。

それから、顔認識ってすっっっごくハードル高く感じていましたが、Appleが提供しているフレームワークを使用すれば私でも作りたいものを作ることができました。
普段Swiftを書いていなくても、Macユーザーだったら気軽に触って遊んでみてほしいなと思いました:relaxed:

以上、アマゾネスからのAdvent Calendar Blogでした:santa_tone2:
Image from iOS.jpg

追伸 : 白ワインは1人で一本飲みました:wink:

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