20191202のSwiftに関する記事は23件です。

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で続きを読む

xcframeworkを作成する(第2回)

GMOアドマーケティングのT.Oです。

前回に引き続き、Swiftでxcframeworkを作成、利用する手順についてまとめます。
今回は手順3.からになります。

●xcframeworkの作成、利用手順
1. プロジェクト作成
2. xcframework生成のためのビルド設定
3. Objective-C対応
4. テスト用アプリ作成

3.Objective-C対応

Swiftで定義したメソッドやクラスをObjective-Cからも利用可能にするには@objcまたは@objcMembersアトリビュートを設定します。
@objcは特定のメソッドのみ利用する場合に使用し、@objcMembersはクラス全体のメソッドを利用する場合に使用します。
ここでは前回作成したクラスのメソッドに@objcアトリビュートを設定することにします。

Ore.swift
import UIKit

public class Ore: NSObject {
    @objc public func oreMethod(){
        print("Hello Ore XCFramework");
    }
}

Objective-Cで参照可能な定義は、xcframework以下の<プロジェクト名>-Swift.hに記述されています。
前回ビルドしたxcframeworkでは@objcアトリビュート設定前は以下のような記述内容となっています。

OreXCFramework-Swift.h(@objc設定前)
(略)
SWIFT_CLASS("_TtC14OreXCFramework3Ore")
@interface Ore : NSObject
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
(略)

@objcアトリビュート設定後に、xcframeworkを再度ビルドします。
すると以下のように出力内容が変更されてoreMethod()が参照可能になっています。

OreXCFramework-Swift.h(@objc設定後)
(略)
SWIFT_CLASS("_TtC14OreXCFramework3Ore")
@interface Ore : NSObject
- (void)oreMethod;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
(略)

4.テスト用アプリ作成

xcframeworkの機能をテストするためのアプリを作成します。今回はxcframeworkと同じプロジェクトにSwift版、Objective-C版のテストアプリをそれぞれ追加することにします。

4.1.Swift版テスト用アプリ作成

xcframeworkのプロジェクトでメニューから「File」→「New」→「Target」を選択します。テンプレートの選択画面で「Single View App」を選択します。
xcframework2_p1.png

オプション選択画面に遷移します。Languageには「Swift」を選択し、Product Nameにテストアプリ名を指定して「Finish」を選択します。
ここではテストアプリ名を「OreSwiftTestApp」としました。
xcframework2_p2.png

テストアプリにフレームワークを利用するための設定を行います。
「TARGETS」でSwift版テストアプリの「OreSwiftTestApp」を選択し、「General」タブを選びます。「Frameworks, Libraries, and Embedded Content」にビルドしたxcframeworkをドラッグ&ドロップします。
xcframework2_p4.png

テストアプリのViewControllerにフレームワークを利用するためのコードを記述します。

ViewController.swift
import UIKit
import OreXCFramework

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        Ore().oreMethod()
    }

}

アプリをビルドし動作を確認します。

今回のxcframeworkはログ出力を行いますので、ログ出力を確認するためにXcodeの表示設定を変更します。画面上部のツールバーからデバッグエリアを表示する「Hide or show the Debug Area」ボタンを選択します。次に画面下部の「Show the Console」ボタンを選択します。
xcframework2_p5.png

画面の実行設定がテストアプリ(OreSwiftTestApp)になっていることを確認して、実行ボタンを選択します。xcframeworkの組み込みに問題がなければ、以下のようにコンソールに文字(”Hello Ore XCFramework”)が表示されているはずです。
xcframework2_p6.png

4.2.Objective-C版テスト用アプリ作成

xcframeworkのプロジェクトでメニューから「File」→「New」→「Target」を選択します。テンプレートの選択画面で「Single View App」を選択します。

オプション選択画面に遷移しますので、Languageには「Objective-C」を選択し、Product Nameにテストアプリ名(OreObjectiveCTestApp)を指定して「Finish」を選択します。

テストアプリにフレームワークを利用するための設定を行います。
「TARGETS」でObjective-C版テストアプリの「OreObjectiveCTestApp」を選択し、「General」タブを選びます。「Frameworks, Libraries, and Embedded Content」にビルドしたxcframeworkをドラッグ&ドロップします。

テストアプリのViewControllerにフレームワークを利用するためのコードを記述します。
xcframeworkのヘッダファイルは、"(プロジェクト名)/(プロジェクト名)-Swift.h"で参照可能です。

ViewController.m
#import "ViewController.h"
#import "OreXCFramework/OreXCFramework-Swift.h"

@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id oreObj;
    oreObj = [[Ore alloc] init];
    [oreObj oreMethod];
}
@end

アプリをビルドし動作を確認します。xcframeworkの組み込みに問題がなければ、Swift版と同様にコンソールに文字(”Hello Ore XCFramework”)が表示されているはずです。

「xcfameworkを作成する」をご紹介しました。いかがだったでしょうか?

明日は、「新オフィスの渋谷フクラスでエンジニアイベント「GMO Developers Night」を開催します!」です。
引き続き、GMOアドマーケティング Advent Calendar 2019 をお楽しみください!

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

ViewControllerのテンプレとファイル生成時にAlso Create Storyboardを追加してみた

この記事はフラー Advent Calendar 2019の 2 日目の記事です。

弊社のiOSチームでは現在、 ViewControllerとStoryboardを1:1にする形で開発しています。

その中でいちいち、ViewControllerにテンプレコードを入れて、ViewControllerとStoryboardを作ってという、作業に嫌気がさしたので、ViewControllerを作成したら一連の作業が終わっているテンプレートを作りたいと思います。

1.ViewControllerにお約束コードを入れる

お約束コードというほどではないんですが、普通に作るとこうですね

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }
*以下略*
}

まずコメント消すの面倒だなと。
あと社内でXcode11からの変更を鑑みてMARK:をきちんと入れようという話になったので、それもテンプレに含めたいなと。
スクリーンショット 2019-12-02 17.14.42.png
意: // MARK:を入れるとエディタで区切り線を入れられるよ
https://developer.apple.com/documentation/xcode_release_notes/xcode_11_release_notes

まあここは他でもまとめられているのでさっくりと行きます。
以下の階層にXcodeのテンプレート群があります。

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File\ Templates

そこのSourceフォルダ下にCocoa Touch Class.xctemplateがあります。
元ファイルをいじるのは後が怖いので、今回はコピーしてEdited Cocoa Touch Class.xctemplateを編集します。
スクリーンショット 2019-12-02 17.34.02.png

Edited Cocoa Touch Class.xctemplate内のUIViewControllerSwiftフォルダ下にある___FILEBASENAME___.swiftを編集します。
今回はコメントを削除して、MARKを1つ追加しました。

//___FILEHEADER___

import UIKit

class ___FILEBASENAMEASIDENTIFIER___: ___VARIABLE_cocoaTouchSubclass___ {

    // MARK: - View Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

ちなみにテンプレートフォルダ内では編集できないので、適宜デスクトップなどに移して編集する必要があります

これでXcodeでファイルを追加するときに、編集したテンプレファイルを使用できます。
スクリーンショット 2019-12-02 17.43.17.png

2. ViewControllerとStoryboardを同時生成する

まず、Edited Cocoa Touch Class.xctemplate内のUIViewControllerSwiftフォルダをコピーして、UIViewControllerStoryBoardSwiftを作ります。

次に以下の階層にある___FILEBASENAME___.storyboardUIViewControllerStoryBoardSwiftに入れます。

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/User Interface/Storyboard.xctemplate

こんな感じになってれば、ViewControllerを作るときに、同名のStoryboardが生成されます。
スクリーンショット 2019-12-02 18.15.44.png

ただ、このままだとStoryboardが必要ない場合も自動で生成されてしまうため、XIBファイルと同じようにチェックボックス式にします。
再びEdited Cocoa Touch Class.xctemplateに戻り、TemplateInfo.plistを開きます。

スクリーンショット 2019-12-02 18.20.51.png

青枠で囲われている部分がAlso Create XIB fileの設定なので、コピーして、適宜書き換えます。
スクリーンショット 2019-12-02 18.22.32.png

これでファイル生成時にAlso Create Storyboard fileを選択できるようになりました。
スクリーンショット 2019-12-02 18.23.22.png

チェックボックスのデフォルトはTemplateInfo.plistからお好みで。
ちなみに、 どっちが編集したテンプレか一目でみたい場合は、
スクリーンショット 2019-12-02 18.40.27.png

Edited Cocoa Touch Class.xctemplate内のTemplateIcon.pngを差し替えれば、
スクリーンショット 2019-12-02 18.52.05.png
こんな感じで見分けられます。

終わりに

本当はStoryboardにView追加して、Classを紐付けて、Is Initial View Controllerにチェックつけるところまでやりたかったけど、sceneのIDを一意に振り分ける方法が見つからず断念しました。

本記事でテンプレート化できたのは半分までと言ったところなので、どうにかいい方法を見つけて次回の記事としたいと思います。

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

Swift マテリアルデザイン TextField ライブラリ不使用

マテリアルデザインをライブラリ不使用で利用できるものがどこにも記載がなかったのでまとめました。
MaterialDesignのライブラリを利用すると、既存のアプリの改修の際、継承関係が難しくなると思います。
理由は@IBOutlet@IBActionの2つをを接続して、@IBActionでマテリアルデザインの動きの部分を担当させるからです。
なんとか、デフォルトの機能で実装する必要があり、これを紹介したいと思います。
なお、この説明は、初学者にわかりやすく説明したため、説明がくどいと感じるかもしれません。あらかじめご了承ください。

まず完成形の確認から

入力すると、そのプレースホルダが上部に移動して、枠線の色が変わり、
入力状態で有ることがわかりやすくなります。

portfolio.gif

まずxibファイルを作成します。

コントローラーは今回作成した下記を指定します。
iPhone8など、カメラのために画面上部が、曲面になっていない長方形の機種を選択します。
そしてサイズをここでは320*56pxに指定しています。
高さの56pxは、マテリアルデザインの基準です。
FilesOwnerに今回作成するクラスを指定します。

スクリーンショット 2019-12-03 15.24.49.png

その後,StoryBoardと接続できるようにします。

import UIKit

class MaterialTextField: UIView {

    //これを最下部のViewに接続(すべてが乗っているView)
    @IBOutlet var contentView: UIView!

    //viewを配置する、これが外枠線となる
    @IBOutlet var borderView: UIView!

    //通常のテキストフィールド、イベントをgetするために配置する
    @IBOutlet var textField: UITextField!

    //プレースホルダのラベル
    @IBOutlet var placeholderLabel: UILabel!

    //プレースホルダの位置を変更するために接続、この値を変更すると位置が変更される
    @IBOutlet var placeholderLabelTopLayout: NSLayoutConstraint!



    //xibファイルを読み込むときには必ず必要
    override class func awakeFromNib() {
        super.awakeFromNib()
    }

    //メモリとのやり取り等
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        initSubViews()
       }

    //ここのframeにオブジェクトが配置されていく。
    override init(frame: CGRect) {
        super.init(frame: frame)
         //今回の設定を呼び出す
        initSubViews()
    }

    //今回の設定
    private func initSubViews(){
        Bundle.main.loadNibNamed("MaterialTextField", owner: self, options: nil)
        contentView.frame = bounds
        contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        addSubview(contentView)
        textField.delegate = self
        addBorderTextField()
    }

    func addBorderTextField(){

        //viewのlayerは外枠線のプロパティを持っている
        borderView.layer.borderColor = UIColor.gray.cgColor
        borderView.layer.borderWidth = 1
        borderView.layer.cornerRadius = 3

        //labelのテキスト等の設定
        placeholderLabel.textColor = UIColor.gray
        placeholderLabel.backgroundColor = .white

    }

xibファイルのオブジェクトを接続していきます。

xibファイルの構造はこんな感じ
順番は

1. viewを配置する (外枠の線を描画するため)
2. その中にtextFieldを配置する。(textFieldのイベントを取得するため)
3.更にプレースホルダ用の文字のlabelを配置する(この文字が移動する)

スクリーンショット 2019-12-02 10.09.23.png

ポイントが、labelの上部との距離
ここを@IBoutletで接続して,コードの方で値を変化させます。
ちょっとイメージがわかないといけないので、動画を添付しますね。
この動画では、@IBoutletは先に、コードの方に記載してあるので、xib画面にて接続できます。

portfolio.gif

Material Designの動きの部分

 func addBorderTextField(){

        //viewのlayerは外枠線のプロパティを持っている
        borderView.layer.borderColor = UIColor.gray.cgColor
        borderView.layer.borderWidth = 1
        borderView.layer.cornerRadius = 3

        //labelのテキスト等の設定
        placeholderLabel.textColor = UIColor.gray
        placeholderLabel.backgroundColor = .white

    }



    func movePlaceholderToTop(){
        placeholderLabel.isHidden = false
        //クロージャー内でselfを使うときに循環参照が起きる可能性があるので[weak self]を利用
        UIView.animate(withDuration: 0.3, animations: {[unowned self] in
            //constraintの値を0へ
            self.placeholderLabelTopLayout.constant = 0.0
            //即座にレイアウトをupdateするメソッド
            self.contentView.layoutIfNeeded()

            }){ [unowned self] (completed) in
                self.borderView.layer.borderColor = UIColor.systemIndigo.cgColor
                self.placeholderLabel.textColor = UIColor.systemIndigo
        }
    }

    func movePlaceholderToCenter(){
        placeholderLabel.isHidden = textField.text != ""
        UIView.animate(withDuration: 0.3, animations: {[unowned self] in
            self.placeholderLabelTopLayout.constant = 20.0
            self.contentView.layoutIfNeeded()
            }){ [unowned self] (completed) in
                self.borderView.layer.borderColor = UIColor.gray.cgColor
                self.placeholderLabel.textColor = UIColor.gray
        }
    }


TextFieldのイベント処理の実装

個人的な好みでクラスは分けています。読みやすいので

extension MaterialTextField: UITextFieldDelegate {

//TextFieldの編集が始まったときに呼ばれるメソッド 
    func textFieldDidBeginEditing(_ textField: UITextField) {

//編集が始まると、labelを上に動かす
        movePlaceholderToTop()
    }

//TextFieldの編集が終わったときに呼ばれるメソッド     
    func textFieldDidEndEditing(_ textField: UITextField) {

//編集が終わると元に戻す
        movePlaceholderToCenter()
    }


//TextFieldでリターンキーがクリックされたときに呼ばれるメソッド 
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    } 
}


StoryBoardにて実装

ポイントは

1- viewを配置する(TextFieldではない)
2- クラスを指定しておく(MaterialTextField)
3- 接続時にクラスを指定。(IBOutlet)

スクリーンショット 2019-12-02 15.59.36.png

コードはこんなイメージ

import UIKit

//UITextFieldDelegateを継承しておく
class ViewController: UIViewController , UITextFieldDelegate{

//上記で接続したview クラスの指定は MaterialTextField
    @IBOutlet weak var materialTextField: MaterialTextField!

    override func viewDidLoad() {
        super.viewDidLoad()

//初期値の文字を設定
        materialTextField.placeholderLabel.text = "Address"

    }
}

間違っている点や、修正点がありましたら、教えていただけますか。
わかりにくい点もありましたら、修正依頼お願いします。

  • このエントリーをはてなブックマークに追加
  • 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の関数

趣味でIOSアプリ開発をかじっていた自分が、改めてSwiftを勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。

参考文献

この記事は以下の書籍の情報を参考にして執筆しました。

関数の概要

func 関数名(仮引数: 型) -> 型{
文…
}
関数内の処理で呼び出し元から値を使わない場合、「仮引数: 型」は記述しない。
関数から戻り値を返さない場合、「 -> 型」は記述しない。

returnの省略

関数本体がreturn1行だけの時はreturnを省略することができる。

func add(a: Int, b: Int) -> Int{
  return a+b
}
print(add(a:3, b:5))        //8
func add(a: Int, b: Int) -> Int{
  a+b
}
print(add(a:3, b:5))        //8

引数ラベルの指定

func 関数名(引数ラベル名 仮引数: 型)で引数ラベルを指定することができる。

func area(height h: Int, weight w: Int) -> Int{
  h * w
}
print(area(height: 3, weight: 5))    //15

引数ラベルの省略

引数ラベルの代わりに「 _ 」を使用することで引数ラベルを省略できる。

func add(_ a: Int, _ b: Int) -> Int{
  a+b
}
print(add(3, 5))    //8

inout引数

SwiftにはC言語のようなポインタ変数がない。代わりにinout引数を使うことで、呼び出し側の変数を変更できる。

func swap(_ a: inout Int, _ b: inout Int) {
  let t = a; a = b; b = t
}

inout引数を使用して関数を呼び出す際は「 & 」をつけて呼び出しを行う。

var x = 10
var y = 3
print(swap(&x, &y))
print("x = \(x) , y = \(y)")        //x = 3 , y = 10

引数ラベルを使ったオーバーロードの定義

引数ラベルで異なる引数を定義することで同名で別の関数を定義することができる。

func swap(_ a: inout Int, _ b: inout Int) {
  let t = a; a = b; b = t
}
func swap(little a: inout Int, great b: inout Int) {
  if b > a{
    let t = a; a = b; b = t
  }
}
var x = 10
var y = 3
print(swap(&x, &y))
print("x = \(x) , y = \(y)")
print(swap(little: &x, great: &y))      //x = 3 , y = 10
print("x = \(x) , y = \(y)")        //x = 10 , y = 3

関数の引数に規定値を設定する

仮引数に規定値を指定することができる。規定値を指定する場合、呼び出し時に省略することが可能。

func setSeparator(color: String = "Black", width: CGFloat){
    //線を追加する処理
}
//色はデフォルト値でいい
setSeparator(width: 200)
//色は赤がいい
setSeparator(color:"red", width: 200)

返り値を使わない場合を許す指定

次のようにBool型を返す関数があったとしても通常返り値を使いたくないのであれば関数定義の直前に@discardableResultをつけることでコンパイルできるようになり、帰り値を使わなくてもエラーにならない。

@discardableResult func hello(name: String) -> Bool{
    var b: Bool = false
    if name == ""{
      print("Hello NANASHIsan")
      b = false
    } else {
      print("Hello\(name)san")
      b = true

    }
    return b
  }
hello("hoge")

キーワード付きのタプル

タプル : 複数のデータを組みにしてまとめたもの。
要素を取り出す時は番号で位置を指名することができる。
タプルは同じ型を持つ変数に代入できる。

let book = ("hoge hoge book", 276)
print("\(book.0)のページ数\(book.1)")
let (title, page) = book
print("\(title)のページ数\(page)")

一部変数だけ取得することも可能

let book = ("hoge hoge book", 276)
let (title, _) = book
print("\(title)読んだ")

要素を取り出す時にキーワード指定で取り出すことができる。
キーワードのついたタプルは同じキーワードがついたタプルかキーワード指定のないタプル間でしか代入できない。
ただしキャストするとキーワード情報がリセットされるので代入できるようになる。

let book = (title: "hoge hoge book", page: 276)
print("\(book.title)のページ数\(book.page)")
let v1: (String, Int) = book
let v2: (title: String, page: Int) = book
let v3: (bookName: String, count: Int) = book   //エラー
let v4: (bookName: String, count: Int) = book as(String, Int)

演算子の定義

概要
・二項演算子
infix operator 演算子 : 優先度グループ名(省略可能)
・前置演算子
prefix operator 演算子
・後置演算子
postfix operator 演算子

Swiftでは自分で演算子を定義することができる。ただ、どのような演算子を定義するかによって定義方法が異なる。
(1)全く新しい演算子の定義。
(2)既存の演算子に別の使い方を定義。
(a)これまで単項演算子として使用されていたものに、二項演算氏の役割を定義。またはその逆。
(b)これまでとは違うデータ型(の組み合わせ)に対して適応可能にする。

(1),(2)-(a)の場合、演算子として使う記号と使い方(前置演算子/後置演算子/二項演算子)を定義する必要がある。
この宣言はどの関数の定義にも属さずトップレベルで行う必要がある。
(2)-(b)の場合、改めて宣言する必要がない。

二項演算子の定義

infix operator  : RangeFormationPrecedence

func (a: Int,b: Int) -> String {
  String(a) + String(b)
}
print(2019)    //2019

単項演算子の定義(前置演算子、後置演算子)

postfix operator %    //後置演算子を定義

postfix func % (n: Double) -> Double {
  n * 0.01
}
print(10250%)    //102.5

演算子として独自定義できない文字列

= , -> , . , // , /* , */ , ? ,
前置演算子として定義できない。
& , >
後置演算子として定義できない。
< , !

演算子として使える文字列

下記のASCEII文字列の記号
/ , - , + , ! , * , % , , | , ^ , ~
Unicode文字列の記号
→ , Δ , ★ , (etc…)
組み合わせとして使用できる記号
= , ? , ! , > , < , &
これらを1つまたは複数組み合わせて定義する。
また「.」から始まる場合のみ、「.」を使用可能。
優先度グループの定義

優先度グループの定義

優先度 : 1つの式に異なる演算子が複数あった場合に、どちらが先に解釈されるかを示します。
(記号例は一部抜粋。他にもあるので注意。)

グループ名 記号例
BitwiseShiftPrecedence << , &>>
MultiplicationPrecedence * , /
AdditionPrecedence + , - , ^
RangeFormationPrecedence ..<
CastingPrecedence is , as
NilCoalescingPrecedence ??
ComparisonPrecedence < , >=
LogicalConjunctionPrecedence &&
LogicalDisjunctionPrecedence ||
TernaryPrecedence con , ?
AssignmentPrecedence = , += , %=

演算子の優先度を独自に定義することもできる。

precedencegroup HogeGroup {
    associativity: none    //結合規則
    higherThan: LogicalDisjunctionPrecedence    //優先度
    lowerThan: LogicalConjunctionPrecedence    //優先度
    assignment: false    //代入演算子
}
infix operator .%% : HogeGroup
func .%% (a: Double , b: Double) -> String {
    String((a+b)*0.1)
}
print(3000 .%% 4400)
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

忘備録-Swiftでのプログラミング

趣味でIOSアプリ開発をかじっていた自分が、改めてSwiftを勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。

参考文献

この記事は以下の書籍の情報を参考にして執筆しました。

Swiftでのデータ型

・型の実体全てインスタンスと総称する。
・データの本体とそれを示すポインタという概念はない。
・データの持ち方には「値型」と「参照型」がある。
値型 : 数値データは変数に代入したり関数に渡した後、値の変更や演算をすときにコピーが作成されるため、元の値データに影響を及ぼさない。
参照型 : 代入に対してコピーを作らず、データ自体に対する参照を渡す。(例)クラスのインスタンス

識別子

Swiftでは識別子に多くのUnicode文字が利用できる。
日本語や絵文字なども使用できる。

let ? = "ごめんなさい"
let 寝坊時間 = 1
let 待ち合わせ = 10
print("\(?)\(寝坊時間 + 待ち合わせ)時につきます?")     //ごめんなさい11時につきます?

SwiftのCase文の特徴

・分岐先の文の実行が終了しても、breakで抜ける必要がない。(何も処理をしない場合はbreak必須です。)
・分岐に用いる型に、文字列や構造を持つ型も使える
・必要に応じて、caseに条件を記述できる。

let n = Int.random(in: 1...6)

switch n {
case 1:
  print("最小")
case 2,3:
  print("小さい")
case 4,5:
  print("大きい")
case 6:
  print("最大")
default:    //上記のどのcaseにも該当しない場合、ここの処理が実行される。
  break
}
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

【Swift】細かいけど忘れたくないものメモ

初めに

教えて貰った細かいことや当たり前に覚えたいことをメモしていきます。

コード

1.変更しないクラスや変数にprivateをつける

表題の通り、変更する予定がないものや変更さたくない変数やクラスにprivateをつけること

2.キーボードを閉じる時はresignFirstResponderを使用

↓を画面遷移する時やキーボードのenterを押す時などに実行する

textField.resignFirstResponder()

Storyboard

1.Storyboard上からローカライズファイルを作成できる

↓のlocalizationのEnglishタブをチェック
image.png

その他

1.

終わりに

随時追加予定です。

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

脱RxSwift初心者のためのtips

RxSwift使いこなせてますか?:smiley_cat:
僕もまだ十分とは言えないですが、半年以上はプロダクトに使用してきたので、レビューで指摘されたことを中心に細々としたtipsをまとめてみようと思います:bulb:
全体的な勉強方針は前回の記事にまとめたので、こちらも良ければ見てください:eyes:
RxとMVVMの勉強ガイド(自分がやってきたこと)

クロージャの使用を避ける

subscribe(onNext:)などはクロージャに任意のコードが書けるので非常に便利なのですが、やりすぎると手続き型的なコードになりがちだったり、selfを適切に弱参照でキャプチャしないといけなかったりするので、他に代替手段がない場合のみ使用するのが良いと思います。
以下に自分が指摘されたことのある代替手段を示します。

オプショナルをアンラップしたい

元のコード
someObservable // Int?が流れてくるとする
    .subscribe(onNext: { [weak self] optionalInt in
        guard let someInt = optionalInt else {
            return
        }
        self?.someFunc(someInt) // Intを1つ引数に取る関数とする
    })
    .disposed(by: disposeBag)

こちらは、flatMap()を使って書き換えることができます。

代替コード(オプショナルのアンラップ)
someObservable // Int?が流れてくるとする
    .flatMap { $0.flatMap(Observable.just) ?? Observable.empty() } // ここでアンラップ
    .subscribe(onNext: { [weak self] someInt in // Intが流れてきた!
        self?.someFunc(someInt)
    })
    .disposed(by: disposeBag)

参考:
Observableをnilでフィルタしてアンラップする

関数を呼び出したい

上のコードの続きで、subscribe(onNext:)の中身はsomeFunc()実行するだけなので、クロージャではなく関数のシグネチャを渡すだけに書き換えられます。

代替コード(関数呼出)
someObservable // Int?が流れてくるとする
    .flatMap { $0.flatMap(Observable.just) ?? Observable.empty() } // ここでアンラップ
    .subscribe(onNext: someFunc) // someFuncを呼び出す、流れてきたIntが引数として渡される
    .disposed(by: disposeBag)

:warning::warning::warning:
コメントにて指摘をいただきました。
上の形だと、selfを強参照で持ってしまうようなので、循環参照を避けるためにはキャプチャを明示的に書いてあげないといけないようです!
よって、代替コード(オプショナルのアンラップ)の形で書くのが良いかと思います!


エラーが流れてこないのであればbind(onNext:)を使うこともできます。万が一エラーを流してしまうとクラッシュするので気をつけてください!

参考:
rxswift bind(onNext: VS subscribe(onNext:

複数のストリームを一つにまとめる

merge()を使用することで複数のストリームを一つにまとめることができます。
結果が同じになる複数の操作をまとめて書くことができます。
検索履歴の操作を例にコードを示します。操作には追加、全削除、削除の3つがあり、全て操作後の検索履歴の配列を返します。その配列をDBなどのデータストアに保存したあと、画面を更新します。

元のコード
input.didTapSearch // didTapSearch: PublishRelay<String>
    .map { history.add(word: $0) } // func add(word: String) -> [String]
    .do(onNext: dataStoreManager.saveHistory) // func saveHistory(list: [String])
    .bind(to: output.history) // history: PublishRelay<[String]>
    .disposed(by: disposeBag)

input.didTapAllDelete // didTapAllDelete: PublishRelay<Void>
    .map { history.clear() } // func clear() -> [String]
    .do(onNext: dataStoreManager.saveHistory) // func saveHistory(list: [String])
    .bind(to: output.history) // history: PublishRelay<[String]>
    .disposed(by: disposeBag)

input.didTapDelete // didTapDelete: PublishRelay<String>
    .map { history.delete(word: $0) } // func delete(word: String) -> [String]
    .do(onNext: dataStoreManager.saveHistory) // func saveHistory(list: [String])
    .bind(to: output.history) // history: PublishRelay<[String]>
    .disposed(by: disposeBag)

重複しているコードが多いのでまとめられそうな感じがします。
merge()を使うと以下のようになります。

代替コード(merge使用)
Observable.merge(
    input.didTapSearch.map { history.add(word: $0) }, // Observable<[String]>
    input.didTapAllDelete.map { history.clear() }, // Observable<[String]>
    input.didTapDelete.map { history.delete(word: $0) } // Observable<String>
)
    .do(onNext: dataStoreManager.saveHistory)
    .bind(to: output.history)
    .disposed(by: disposeBag)

merge()でまとめる場合、当然ながら型は一致している必要があります。
型が同じでも、違う概念のものはまとめないほうが良いです。その場合は後続の処理が一致してないのでまとめられないと思いますが。
例のコードではすべて検索履歴に対する処理だったのでうまくまとめることができました。

カスタムビューにrxを生やす

ReactiveのExtensionを実装すると、標準ビューのように.rxを生やすことができます。
Rxに関連あるプロパティであることを明示できるので、ControlEventBinderで使うと良いと思います。

元のコード
class SomeCustomView: UIView {

    let someStatus: BehaviorRelay<Bool>
    let someAction: PublishRelay<Void>
...
}

let customView = CustomView()
customView.someStatus.subscribe(onNext: {...
customView.someStatus.accept(...)
customView.someAction.subscribe(onNext: {...

クラスのプロパティを公開することでも、外から状態やイベントを監視することもできますが、ReactiveのExtensionを使ったほうがわかりやすくなると思います。Binderの場合はイベントが流れてきた処理も一緒に書けるのでオススメです。

代替コード(ReactiveのExtension使用)
extension Reactive where Base: CustomView {

    var someStatus: Binder<Bool> {
        return Binder(base) { view, value in
            // 状態が更新されたときの処理
        }
    }

    var someAction: ControlEvent<Void> {
        return .init(events: /* 外に伝えたいイベント */)
        // UIButton.rx.tapなどもともとControlEventのものはそのまま返せばOK
        // return base.someButton.rx.tap 例えばこんな感じ
    }
}

let customView = CustomView()
someBool.bind(to: customView.rx.someStatus)...
customView.rx.someAction.subscribe(onNext: {...

Rxを使っている感が高まりました!

フラグによる状態管理を無くす

特定の状態になるまではイベントを無視したい、あるいは画面が表示されたときに1度だけ処理したい、などという要求は度々発生します。
そんなときにはskip()take()というオペレータを使用することができます。

最初のn回のイベントを無視する

skip()を使うことで、引数で与えた回数分だけイベントを無視することができます。
個人的にはBehaviourRelayと組み合わせて使うことが多いです。
APIの結果をBehaviourRelayに入れたいが、BehaviourRelayには初期値が必要なのと、subscribe()したときにその時保持している値が流れてしまうので、初期値を無視するために使用します。

元のコード
var isLoaded: Bool = false // APIが完了したらtrueになる
let apiResult = BehaviorRelay<[String]>(value: []) // APIの結果が入る、初期値はいらない
apiResult
    .subscribe(onNext: { result in
        if isLoaded { // APIが完了している場合のみ処理したい
            // 何かしらの処理
        }
    })
    .disposed(by: disposeBag)

skip()を使うことでフラグを消すことができます。

代替コード(skip使用)
let apiResult = BehaviorRelay<[String]>(value: []) // APIの結果が入る、初期値はいらない
apiResult
    .skip(1) // 最初の1回(初期値)は無視する
    .subscribe(onNext: { result in
        // 何かしらの処理
    })
    .disposed(by: disposeBag)

最初のn回以降イベントを無視する

take()を使うことで、引数で与えた回数分以降のイベントを無視することができます。
個人的には、画面が表示されたときに1回だけ処理を行いたい(けどAutoLayoutの関係でviewDidLoad()には書けない)時に使用することが多いです。

元のコード
var isInitial: Bool = true // 1回だけ実行したい処理を終えたらfalseになる
let viewDidAppear = PublishRelay<Void>() // viewDidAppearのタイミングで発火されるイベント
viewDidAppear
    .subscribe(onNext: { result in
        if isInitial { // 最初の1回だけ
            // 何かしらの処理
            isInitial = false // フラグ下ろす
        }
    })
    .disposed(by: disposeBag)

take()を使ってフラグを消します。

代替コード(skip使用)
let viewDidAppear = PublishRelay<Void>() // viewDidAppearのタイミングで発火されるイベント
viewDidAppear
    .take(1) // 最初の1回以降は無視
    .subscribe(onNext: { result in
        // 何かしらの処理
    })
    .disposed(by: disposeBag)

エラーが出たときは型を注意深く確認する

最後は開発中の話ですが、Rx関連でコンパイラが出すエラーは当てにならないことが多いです。
なにかエラーが出たときは、ストリームを流れるイベントの型を注意深く追ってみましょう。

おわりに

自分の経験を元に注意すべき点やこうすればもっと良くなるという点をまとめてみました:pencil:
まだまだ自分も勉強中なので、もっと気をつける点などあると思います:warning:
是非そういったところや経験談などをコメントいただけると嬉しいです!:bow:

  • このエントリーをはてなブックマークに追加
  • 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] 最新のSpeech Recognitionについて

はじめに

iOS10から利用できるようになった音声認識API: Speech FrameworkについてのWWDC2019でアップデートがあったので最新事情をサンプルとともにお届けします。

APIに関する導入の詳細はiOSのSpeechフレームワークで音声認識 - 対応言語は58種類! - Qiitaをご覧ください。

WWDC2019のセッション内容

WWDC2019のセッションAdvances in Speech Recognition - WWDC 2019 - Videos - Apple Developerの内容を見ていきます。

サマリーとしては、

  1. macOSでのサポート
  2. デバイス上で動作可能に
  3. API強化のおかげで、豊富な音声分析ができるように

です。

macOSでのサポート

このサポートは、MacのAppKitアプリとiPadアプリの両方で利用できます。
iOSと同様に、50以上の言語がサポートされており、マイクにアクセスして音声を録音するには、ユーザーの承認が必要です。
また、ユーザーはSiriを有効にする必要があります。

デバイス上で動作可能に

表題の通りローカル環境のみで動作するようになりました。
ネットワーク通信の必要がなくなり、データがAppleに送信されないため、よりプライバシーにも配慮してアプリ開発をすることができます。
しかし、トレードオフとして

  • 精度の差異
    • 継続的な学習を行なっているためサーバを通した方が優れている。
  • リアルタイム性
    • ローカル実行すると遅延が少ない
  • 制限
    • ネットワーク通信ではリクエスト数と音声の長さを制限
    • ローカルでは無制限
  • 10の言語のみ対応
    • English, Spanish, Italian, Brazilian Portuguese, Russian, Turkish, Chinese

など、それぞれで問題点もあり、導入には要検討、と言ったところでしょうか。
特に、日本語対応していないのはかなり辛いですね。 :cry:

実装としては、SFSpeechRecognizerのインスタンスのsupportsOnDeviceRecognition(iOS13+)でローカルに対応しているかを確認して、SFSpeechAudioBufferRecognitionRequestなどのリクエストに生えたrequiresOnDeviceRecognitionをonにするだけです。

if speechRecognizer.supportsOnDeviceRecognition {
    recognitionRequest.requiresOnDeviceRecognition = true
} else {
    // do something    
}

Advances in Speech Recognition - WWDC 2019 - Videos - Apple Developerの冒頭で手元で検証してみたところ下記のようになっており、トランスクリプトと比べてみました。再現環境にもよるかもしれませんが、どちらにしても正確さに欠けるかなと言う印象です。
サーバ通信の方は、最初のSEを無理やり言語化しようとしてしまったみたいですし。。

トランスクリプト
Hi. I'm Neha Agrawal, and I'm a software engineer working on speech recognition. In 2016, we introduced the Speech Recognition framework for developers to solve their speech recognition needs. For anyone who is new to this framework, I highly recommend watching this Speech Recognition API session by my colleague Henry Mason.

ローカル サーバ通信
IMG_4885.PNG IMG_4886.PNG

豊富な音声分析について

Speech RecognitionがiOS10で提供が始まってからは、

  • 書き起こし(Transcription)
  • 代替解釈(Alternative interpretations)
  • 信頼度(Confidence levels)
  • タイミング情報(Timing information)

が結果として帰ってきていました。

iOS13からは新たに以下の結果も返してくれるようになりました。

  • 速度(Speaking rate): 人が1分あたりの単語で話す速さ
  • 平均休止時間(Average pause duration): 単語間の平均休止時間
  • 音声解析機能(Voice Analytics features): より高度で専門的な解析
    • Jitter: 音声のピッチの変化
    • Shimmer: 音声の振幅の変化
    • Pitch: トーンの高低
    • Voicing: スピーチの中で発生した領域(?)

下記のように、recognitionTaskメソッドの結果からアクセスすることができます。

if let result = result {

    let formattedString = result.bestTranscription.formattedString
    let speakingRate = result.bestTranscription.speakingRate
    let averagePauseDuration = result.bestTranscription.averagePauseDuration

    for segment in result.bestTranscription.segments {
        let jitter = segment.voiceAnalytics?.jitter.acousticFeatureValuePerFrame
        let shimmer = segment.voiceAnalytics?.shimmer.acousticFeatureValuePerFrame
        let pitch = segment.voiceAnalytics?.pitch.acousticFeatureValuePerFrame
        let voicing = segment.voiceAnalytics?.voicing.acousticFeatureValuePerFrame
    }
}

おわり

以上、Advances in Speech Recognition - WWDC 2019 - Videos - Apple Developerの内容に沿ってお届けしました。

分析のJitterやShimmerといった専門領域に踏み込めなかったので、より詳細な解説はどなたかにお譲りしたいと思います。分かりやすい記事が読みたい...!
また、Voicingが何をあらわすかもまだよくわかっていないです。。

サンプルコードはgithub.com/mtfum/SpeechSamplerに置いておきますのでご覧くださいませ。

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

参考

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

SwiftのStringの == 演算子とUnicodeの関係

SwiftのStringはEquatableプロトコルに準拠しています。したがって、以下のような比較ができます。

let string1 = "abc"
let string2 = "bcb"

string1 == string2 // => false

こうみると単純ですが、他の言語と比べて変わった点があります。
例としてRubyと比べてみましょう。

Swift
let pokemon1 = "Poke\u{0301}mon" // Pokémon
let pokemon2 = "Pok\u{00e9}mon" // Pokémon

pokemon1 == pokemon2 // true
Ruby
pokemon1 = "Poke\u{0301}mon" # Pokémon
pokemon2 = "Pok\u{00e9}mon" # Pokémon

pokemon1 == pokemon2 # false

SwiftではtrueなのにRubyではfalseになっています。
この記事ではなぜこのような違いが生まれるのか、詳細に解説します。

その前に é について少し解説

let pokemon1 = "Poke\u{0301}mon"
let pokemon2 = "Pok\u{00e9}mon"

の違いは という文字の表しかたです。
pokemon1 ではアルファベットのe と アキュート・アクセント´を合成して という一つの文字を作っています。0301´ の符号位置です。
pokemon2 ではそれ単体で é を表す文字で é を表しています。00e9はその文字の符号位置です。

Unicodeの等価とSwiftの等価

先ほどのSwiftのコードでは == で比較した結果がtrueになっていました。
つまり、Swiftの考え方では "Poke\u{0301}mon""Pok\u{00e9}mon" は等価だということですね。

結論から言うと、SwiftのStringの等価性の評価はUnicodeの正準等価(英語だとCannonical Equivalent)という定義に従っています1。Unicodeでは「等価と見なせるもの」を正準等価と呼びます。 2
実は、pokemon1pokemon2 のそれぞれの文字は正準等価です。だからこそ == で比較するとtrueになるわけです。

Unicodeの正準等価

SwiftのStringの == は正準等価か否かを判断していることは分かりました。それでは正準等価の定義やその性質について確認していきましょう。そうすることで == が何をやっているか深く理解できるはずです。

同じ文字でも複数の表現方法がある

まず前提として、Unicodeには同じ文字でも複数の表現方法があります。以下に例を示します。

  • Ç = C + ◌̧
    • 「Ç」単体は、「C」と「 ¸ 」を合成したものと等しい
  • é = e + ◌́
    • 「é」単体は、「か」と「 ´ 」を合成したものと等しい
  • = か + ◌゙
    • 「が」単体は、「か」と「 ゛」を合成したものと等しい

上記以外にも組み合わせは多数あります。

同じ文字でも複数の表現方法があるのは歴史的経緯や他の文字コードとの互換性のためですが、ソフトウェアを使っているユーザーにとっては同じものとして表示したいです。たとえば、 を含む文字列に対して か + ◌゙ で文字列を検索してもヒットして欲しいですよね。
この「同じもの」(つまり等価)を規格としてきちんと定義したものが正準等価です。

正準等価の定義

二つの文字の並びを最後まで 正準分解(Canonical Decomposition) した結果が等しければそれらは互いに正準等価です。 3

正準分解とは正規化のひとつで以下のようなアルゴリズムです(公式な定義はドキュメントのD68を参照してください )。

  • データベースに従ってそれぞれの文字を分解する。

  • 分解した文字がさらに分解可能なら再起的に分解する。

  • Canonical Ordering Algorithmにしたがって順番を揃える。(アルゴリズムについてはドキュメントの 3.11を参照してください)

この定義をもとに例を見てみます。

例1

(3071) と は + ◌゚ (306F + 309A)は正準等価か

データベースによると「ぱ」は(306F + 309A)に分解可能です 4
◌゜ もこれ以上分解できず、順番も変わらないので分解は以上です。
二つの分解結果が一致するので、(3071) と は + ◌゚ (306F + 309A)と正準等価です。

例2

(1F89) と Α + ◌ͺ + ◌̔ (0391 + 0345 + 0314)は正準等価か

データベースによるとは以下のように分解できます。

1F86 -> 1F09 + 0345
1F09は、さらに0391 + 0314に分解することができます。したがって、

1F89 -> 0391 + 0314 + 0345 に分解されます。
また、0391 + 0345 + 0314 は Canonical Ordering Algorithmによって順番が変わり、0391 + 0314 + 0345 になります。

これ以上はどちらも分解できず、順番も変わりません。その結果、両者は一致するので正準等価です。

Untitled.png
分かりづらそうなので表にしました。二つの表の一番下の行が一致することに着目してください。

注記: ᾉはギリシャ語の文字らしいです。

Swiftで正準分解してみる

NSStringのdecomposedStringWithCanonicalMappingというメソッドを使います。

// NSStringのメソッドなのでFoundationのインポートが必要です。
import Foundation

let greekLetter = "ᾉ"

let letters = greekLetter.decomposedStringWithCanonicalMapping.unicodeScalars.map { scalar in
    String(format: "%04x", scalar.value)
}
print(letters) // ["0391", "0314", "0345"]

Swiftで他の言語風の比較をする

最初の例で示したRubyのやっているような比較(つまり符号単位での比較)をしたければ以下のようにします。

let pokemon1 = "Poke\u{0301}mon" // Pokémon
let pokemon2 = "Pok\u{00e9}mon" // Pokémon

pokemon1.utf8.elementsEqual(pokemon2.utf8) // false

pokemon1.utf16.elementsEqual(pokemon2.utf16) でも良いですが、Swift 5.0以降はStringの内部表現はUTF-8になっているので.utf8で比較する方がパフォーマンスが良いです。


凡例

この記事のサンプルコードは以下の環境で試しました。

  • macOS Catalina 10.15.1
  • Swift 5.1.2
  • Ruby 2.6.5

参考

この記事を書くにあたり以下の資料を参考にしました。

SwiftのStringのcountについて

記事を読んでいただきありがとうございます!
iOSDC Japan 2019で「SwiftのStringの文字の数え方を完全理解する」というタイトルで30分トークしました。
この記事に大いに関連する内容になっているので、もしタイトルに興味をひかれたらそちらもチェックしてください。

この記事では文字とは抽象文字(Abstract Character)という意味で、iOSDCでのトークでは拡張書記素クラスタ(Extended Grapheme Cluster)という意味で使っています。
(Unicodeの専門知識レベルの話で、Swiftのコードを書くのには気にはならないはずです。)

最後に

間違いがないように気をつけて調べて書きましたが、もしかしたら間違いがあるかもしれません。もしあればご指摘よろしくお願いいたします :bow:


  1. https://developer.apple.com/documentation/swift/string#overview に Comparing strings for equality using the equal-to operator (==) or a relational operator (like < or >=) is always performed using Unicode canonical representation とあるとおり、ドキュメントに明記されています。 

  2. 似た定義に互換等価(Compatibility Equivalent)というものがあるのですが、SwiftのStringにはあまり関わりはありません。互換等価は正準等価より広い概念で、たとえばメートルは互換等価ですが、正準等価ではありません。 

  3. 英語による定義 "Two character sequences are said to be canonical equivalents if their full canonical decompositions are identical." を筆者が訳しました。full はどうやって訳すか迷ったのですが「最後まで」としました。「再起的に」と訳しても良かったかもしれません。 

  4. データベースを「3071;」で検索してヒットする行の6列目を見てください。 

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

SwiftのOptionalのmapメソッドについて(Unwrapとして使うmapメソッド)

Optional型のArrayのmapメソッドを使う際、mapの扱いを見落として使ってしまいがちだったので、戒めのためにメモ

@koherさんの記事「ArrayとOptionalのmapは同じです」で

Optional や Array を値を入れる箱だと考えると、 map は箱から値を取り出さずに操作するためのメソッド/関数です

という概念で理解できました。

配列の中身を一つづつ、処理しようと、以下のように書くと

let numbers: [Int]?
numbers = [1, 2, 3, 4]
numbers.map {
    print($0)
}

結果は
print($0) // [1, 2, 3, 4]
になります。

これは
Optional型に、mapメソッドを通した場合
Unwrapの処理が行われ、Optional型 → 非Optional型になるからです。

期待する動作(配列の中身を一つづつ処理する)をしたい場合は、以下です

let texts: [String]?
numbers.map {
    print($0) // [1, 2, 3, 4]
    let update_numbers = $0.map {
        $0 + 2
    }
    print(update_numbers) // [3, 4, 5, 6]
}

これは、

if let numbers = numbers {
    print(numbers) // [1, 2, 3, 4]
    let update_numbers = numbers.map {
        $0 + 2
    }
    print(update_numbers) // [3, 4, 5, 6]
}

と同じ動作をします

違いは
前者のmapでのUnwrapは、箱から値を取り出さずに操作
後者のif letでのUnwrapは、一旦、変数に代入して、if文の中で操作
という違いで、特に大きな違いはないので、好みでいいのかと思いますが
自分は前者の方がスッキリして、見やすいかなと思います。
ただ、後者は何をやってるのかは、パッと見で分かりやすいので
レビュー者との共通認識で、どちらかを使うか決めた方が良さそうです。

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

個人的によく使うUIViewExtension

他の方が取り上げていたら:bow_tone1:

四隅のアンカー作成

extension UIView {
    func addEdgeConstraint() {
        guard let superview = self.superview else {
            return
        }
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            self.topAnchor.constraint(equalTo: superview.topAnchor),
            self.leadingAnchor.constraint(equalTo: superview.leadingAnchor),
            self.trailingAnchor.constraint(equalTo: superview.trailingAnchor),
            self.bottomAnchor.constraint(equalTo: superview.bottomAnchor),
        ])
    }

    func addEdgeSafeAreaConstraint() {
        guard let superview = self.superview else {
            return
        }
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            self.topAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.topAnchor),
            self.leadingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.leadingAnchor),
            self.trailingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.trailingAnchor),
            self.bottomAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.bottomAnchor),
        ])
    }
}

使い方

class ViewController: UIViewController {
    let imageView = UIImageView()
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(imageView)
        imageView.addEdgeSafeAreaConstraint()
    }
}

インスタンス生成時のプロパティ編集

protocol Layout: UIView {}
extension UILabel: Layout {}
extension UICollectionView: Layout {}
extension UIButton: Layout {}
extension UIImageView: Layout {}
extension Layout {
    func layout(_ layout: (Self) -> Void) -> Self {
        layout(self)
        return self
    }
}

使い方

class ViewController: UIViewController {
     let label: UILabel = UILabel()
                            .layout {
                               $0.textColor = .white
                            }
}

もっと上手に書ける方法があったら教えていただけると非常に嬉しいです

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