- 投稿日:2019-12-02T23:26:53+09:00
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.center
やview.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
- 投稿日:2019-12-02T20:56:30+09:00
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.swiftimport 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」を選択します。
オプション選択画面に遷移します。Languageには「Swift」を選択し、Product Nameにテストアプリ名を指定して「Finish」を選択します。
ここではテストアプリ名を「OreSwiftTestApp」としました。
テストアプリにフレームワークを利用するための設定を行います。
「TARGETS」でSwift版テストアプリの「OreSwiftTestApp」を選択し、「General」タブを選びます。「Frameworks, Libraries, and Embedded Content」にビルドしたxcframeworkをドラッグ&ドロップします。
テストアプリのViewControllerにフレームワークを利用するためのコードを記述します。
ViewController.swiftimport UIKit import OreXCFramework class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() Ore().oreMethod() } }アプリをビルドし動作を確認します。
今回のxcframeworkはログ出力を行いますので、ログ出力を確認するためにXcodeの表示設定を変更します。画面上部のツールバーからデバッグエリアを表示する「Hide or show the Debug Area」ボタンを選択します。次に画面下部の「Show the Console」ボタンを選択します。
画面の実行設定がテストアプリ(OreSwiftTestApp)になっていることを確認して、実行ボタンを選択します。xcframeworkの組み込みに問題がなければ、以下のようにコンソールに文字(”Hello Ore XCFramework”)が表示されているはずです。
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 をお楽しみください!
- 投稿日:2019-12-02T20:09:59+09:00
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:
をきちんと入れようという話になったので、それもテンプレに含めたいなと。
意:// 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
を編集します。
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でファイルを追加するときに、編集したテンプレファイルを使用できます。
2. ViewControllerとStoryboardを同時生成する
まず、
Edited Cocoa Touch Class.xctemplate
内のUIViewControllerSwift
フォルダをコピーして、UIViewControllerStoryBoardSwift
を作ります。次に以下の階層にある
___FILEBASENAME___.storyboard
をUIViewControllerStoryBoardSwift
に入れます。/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/User Interface/Storyboard.xctemplateこんな感じになってれば、ViewControllerを作るときに、同名のStoryboardが生成されます。
ただ、このままだとStoryboardが必要ない場合も自動で生成されてしまうため、XIBファイルと同じようにチェックボックス式にします。
再びEdited Cocoa Touch Class.xctemplate
に戻り、TemplateInfo.plist
を開きます。青枠で囲われている部分が
Also Create XIB file
の設定なので、コピーして、適宜書き換えます。
これでファイル生成時に
Also Create Storyboard file
を選択できるようになりました。
チェックボックスのデフォルトは
TemplateInfo.plist
からお好みで。
ちなみに、 どっちが編集したテンプレか一目でみたい場合は、
Edited Cocoa Touch Class.xctemplate
内のTemplateIcon.png
を差し替えれば、
こんな感じで見分けられます。終わりに
本当はStoryboardにView追加して、Classを紐付けて、
Is Initial View Controller
にチェックつけるところまでやりたかったけど、sceneのIDを一意に振り分ける方法が見つからず断念しました。本記事でテンプレート化できたのは半分までと言ったところなので、どうにかいい方法を見つけて次回の記事としたいと思います。
- 投稿日:2019-12-02T18:33:00+09:00
Swift マテリアルデザイン TextField ライブラリ不使用
マテリアルデザインをライブラリ不使用で利用できるものがどこにも記載がなかったのでまとめました。
MaterialDesignのライブラリを利用すると、既存のアプリの改修の際、継承関係が難しくなると思います。
理由は@IBOutletと@IBActionの2つをを接続して、@IBActionでマテリアルデザインの動きの部分を担当させるからです。
なんとか、デフォルトの機能で実装する必要があり、これを紹介したいと思います。
なお、この説明は、初学者にわかりやすく説明したため、説明がくどいと感じるかもしれません。あらかじめご了承ください。まず完成形の確認から
入力すると、そのプレースホルダが上部に移動して、枠線の色が変わり、
入力状態で有ることがわかりやすくなります。まずxibファイルを作成します。
コントローラーは今回作成した下記を指定します。
iPhone8など、カメラのために画面上部が、曲面になっていない長方形の機種を選択します。
そしてサイズをここでは320*56pxに指定しています。
高さの56pxは、マテリアルデザインの基準です。
FilesOwnerに今回作成するクラスを指定します。その後,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を配置する(この文字が移動する)
ポイントが、labelの上部との距離
ここを@IBoutletで接続して,コードの方で値を変化させます。
ちょっとイメージがわかないといけないので、動画を添付しますね。
この動画では、@IBoutletは先に、コードの方に記載してあるので、xib画面にて接続できます。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)
コードはこんなイメージ
import UIKit //UITextFieldDelegateを継承しておく class ViewController: UIViewController , UITextFieldDelegate{ //上記で接続したview クラスの指定は MaterialTextField @IBOutlet weak var materialTextField: MaterialTextField! override func viewDidLoad() { super.viewDidLoad() //初期値の文字を設定 materialTextField.placeholderLabel.text = "Address" } }間違っている点や、修正点がありましたら、教えていただけますか。
わかりにくい点もありましたら、修正依頼お願いします。
- 投稿日:2019-12-02T17:58:36+09:00
Swiftでアイマスの画像認識やってみる
はじめに
Xcodeの
CreateML
が簡単に画像認識をアプリケーションを組み込めるようなので試しにやってみました。https://developer.apple.com/jp/machine-learning/create-ml/
上のようにやってくれるらしい。作成したもの
作った pic.twitter.com/FuWqPuZsdH
— 清水 幸佑 (@thimi0412) December 2, 2019環境
- Xcode Version 11.2.1
画像データの準備
何を分類するのか
りんごとかオレンジとかの分類だとなんかつまらないので、今回はアイドルマスターシンデレラガールズ カードギャラリーさんから画像をいただいて、
島村卯月
、渋谷凛
、本田未央
を分類してみようと思います。学習用のデータとテスト用のデータのフォーマットはこんな感じ。
各ディレクトリの名前がラベルになるのでtrain_data
とtest_data
の名前を間違えないように気をつけましょう。train_data ├── mio_honda ├── rin_shibuya └── uzuki_shimamura test_data ├── mio_honda ├── rin_shibuya └── uzuki_shimamuraモデルの作成
プロジェクトの作成
Xcode > Open Developer Tool > Create ML
から起動
プロジェクトのフォルダを決定しテンプレートを選択します。今回は画像認識なので
Image Classifier
を選択。
学習開始
学習用データを
Training Data
に、テスト用データをTesting Data
にデータを追加しましょう。
追加したらTrain
ボタンを押して完了です。
学習が完了したら
Output
にある.mlmodel
ファイルをドラッグして別の場所に保存しておきましょう。アプリケーションに組み込む’
Xcodeから
Create new Xcode project
でSingle View App
でプロジェクトを作成します。私はswiftには疎いのでとりあえず書いたコードを乗っけておきます。
ViewController.swift
とカメラを使用するのでinfo.plist
をいじりました。先ほど作成した
.mlmodel
をプロジェクトに追加してください。
ViewController.swiftimport 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) } } }
- 投稿日:2019-12-02T17:58:36+09:00
Swiftでアイマス の画像認識やってみる
はじめに
Xcodeの
CreateML
が簡単に画像認識をアプリケーションを組み込めるようなので試しにやってみました。https://developer.apple.com/jp/machine-learning/create-ml/
上のようにやってくれるらしい。作成したもの
作った pic.twitter.com/FuWqPuZsdH
— 清水 幸佑 (@thimi0412) December 2, 2019環境
- Xcode Version 11.2.1
画像データの準備
何を分類するのか
りんごとかオレンジとかの分類だとなんかつまらないので、今回はアイドルマスターシンデレラガールズ カードギャラリーさんから画像をいただいて、
島村卯月
、渋谷凛
、本田未央
を分類してみようと思います。学習用のデータとテスト用のデータのフォーマットはこんな感じ。
各ディレクトリの名前がラベルになるのでtrain_data
とtest_data
の名前を間違えないように気をつけましょう。train_data ├── mio_honda ├── rin_shibuya └── uzuki_shimamura test_data ├── mio_honda ├── rin_shibuya └── uzuki_shimamuraモデルの作成
プロジェクトの作成
Xcode > Open Developer Tool > Create ML
から起動
プロジェクトのフォルダを決定しテンプレートを選択します。今回は画像認識なので
Image Classifier
を選択。
学習開始
学習用データを
Training Data
に、テスト用データをTesting Data
にデータを追加しましょう。
追加したらTrain
ボタンを押して完了です。
学習が完了したら
Output
にある.mlmodel
ファイルをドラッグして別の場所に保存しておきましょう。アプリケーションに組み込む’
Xcodeから
Create new Xcode project
でSingle View App
でプロジェクトを作成します。私はswiftには疎いのでとりあえず書いたコードを乗っけておきます。
ViewController.swift
とカメラを使用するのでinfo.plist
をいじりました。先ほど作成した
.mlmodel
をプロジェクトに追加してください。
ViewController.swiftimport 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) } } }
- 投稿日:2019-12-02T17:47:53+09:00
忘備録-Swiftの関数
趣味でIOSアプリ開発をかじっていた自分が、改めてSwiftを勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。
参考文献
この記事は以下の書籍の情報を参考にして執筆しました。
関数の概要
func 関数名(仮引数: 型) -> 型{
文…
}
関数内の処理で呼び出し元から値を使わない場合、「仮引数: 型」は記述しない。
関数から戻り値を返さない場合、「 -> 型」は記述しない。returnの省略
関数本体がreturn1行だけの時はreturnを省略することができる。
func add(a: Int, b: Int) -> Int{ return a+b } print(add(a:3, b:5)) //8func 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)) //8inout引数
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(20☆19) //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)
- 投稿日:2019-12-02T17:24:44+09:00
思いやりの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.swiftimport 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.swiftimport 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値を返すプロパティは、
isEmpty
、isHidden
などに習い、アクセスしたときにインスタンスの状態が一文で表されるような命名が良いとされます。文章として読める命名にすることで、コードリーディングの際に必要な情報が名前に含まれ、余計なコメントを書く必要がなくなります。
まとまりがあるものには同じ命名・Prefix・Suffixを用いる
これはSwiftに限った話ではないかもしれませんが、共通項を持つメソッド・プロパティ達には命名にも共通項をもたせておくことで、特にコメントが書かれていなくてもそのことが他の開発者に伝わりやすくなります。
わかりやすい例として、引数に渡す型が異なるが施すアクションや返る結果が同じという場合には、オーバーロードを活用してメソッド名や引数名は同じものを用いてしまうというのが良いでしょう。
これにより、読む側はその引数の型が何であるかを意識せずに挙動だけを見れるというメリットもあります。おわりに
この記事では、自分がこの1年命名に関して指摘を頂いてきたポイントをまとめてみました。
つまるところ命名規則というのは、読む人・共同開発者への思いやりだと思います。(主観です)
Swiftコードのレビューをお願いする前に、レビュワーや読み手を思いやり、是非今一度上記ポイントを思い出して確認してもらえると嬉しいです。最後まで読んでいただきありがとうございました!
もっとこうしたほうが良い!私はこうしてる!等ありましたら是非コメントお願いします。
- 投稿日:2019-12-02T16:56:44+09:00
忘備録-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 }
- 投稿日:2019-12-02T16:18:21+09:00
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 で間引くという手もあったかもしれませんが、今回はこちらの方法を使いました
- 投稿日:2019-12-02T16:03:25+09:00
【Swift】細かいけど忘れたくないものメモ
- 投稿日:2019-12-02T13:19:57+09:00
【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.swiftclass 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()
で出力するよりはだいぶ解りやすくなったのではないでしょうか。おわりに
以上が私が開発中に使っているログ出力です。もちろん、人によっては不要な情報も出力されているかもしれませんが、ひとつの例として受け取って貰えればと思います。
- 投稿日:2019-12-02T12:42:54+09:00
脱RxSwift初心者のためのtips
RxSwift使いこなせてますか?
僕もまだ十分とは言えないですが、半年以上はプロダクトに使用してきたので、レビューで指摘されたことを中心に細々としたtipsをまとめてみようと思います
全体的な勉強方針は前回の記事にまとめたので、こちらも良ければ見てください
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)
コメントにて指摘をいただきました。
上の形だと、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に関連あるプロパティであることを明示できるので、ControlEvent
やBinder
で使うと良いと思います。元のコード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関連でコンパイラが出すエラーは当てにならないことが多いです。
なにかエラーが出たときは、ストリームを流れるイベントの型を注意深く追ってみましょう。おわりに
自分の経験を元に注意すべき点やこうすればもっと良くなるという点をまとめてみました
まだまだ自分も勉強中なので、もっと気をつける点などあると思います
是非そういったところや経験談などをコメントいただけると嬉しいです!
- 投稿日:2019-12-02T09:39:25+09:00
iOS リアルタイム入力でハッシュタグ形式に文字装飾するTIPS
本稿の目的
#
から始まるハッシュタグの部分の色が変わり、タップするとハッシュタグの内容に応じたフィードの検索などを行う機能は最近ではあたりまえのUXになっています。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.UTF16View
のcount
から長さを作るようにしないと、適切な範囲で正規表現マッチしてくれません。
こちらは 絵文字を支える技術の紹介 の記事を大変参考にさせていただきました。
(?:^|\\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.count
とString.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.Key
のlink
を設定し、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
を呼び出します。ただし、このときに次のような点に注意して実装する必要があります。
- 文字列編集中の場合、とくに日本語変換中などはこの処理を行わないように制御する
- 前回編集時の文字装飾設定を除去する
これらの注意点を実装した
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
で毎回このメソッドを呼び出している場合、で日本語入力で変換中であるかを検査せずに文字装飾を行うと、変換前で文字入力が確定してしまいます。
たとえば、ローマ字入力などで「か」という文字を入力しようとする場合、k
、a
のキーを入力する必要がありますが、k
の段階で入力が確定してしまい、「kあ」と入力のまま変換ができなくなります。
これを防止するために現在のUITextView
のmarkedTextRangeを検査し、入力中かどうかを判定する必要があります。前回編集時の文字装飾設定を除去する
UITextViewDelegate textViewDidChange:(UITextView *)textView
で毎回このメソッドを呼び出しており、NSMutableAttributedString init(attributedString:)
を使用して文字装飾を開始している場合は、前回のハッシュタグ形式の文字装飾が引き継がれるため、内容の編集時に想定しない挙動を示します。
このため、事前に文字列範囲でハッシュタグ形式の文字装飾を除去してください。// 前回設定していたハッシュタグ用の文字装飾を除去する attrString.removeAttribute(.foregroundColor, range: range) attrString.removeAttribute(.underlineStyle, range: range)
- 投稿日:2019-12-02T08:04:34+09:00
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 : ErrorSingle と同様、 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) } }出力:
1Combineの場合:
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 finishedSingle.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") finishedRxSwift to Apple’s Combine Cheat Sheet
GitHubに、RxSwiftとCombineのオペレータの対応表をまとめてくれている人がいます。
https://github.com/CombineCommunity/rxswift-to-combine-cheatsheet
これってCombineだとどうやるんだろう?と思ったときに大変便利です。
ただこの表の通りに機械的に置き換えられない部分はあり、たとえば今回ご紹介した Futureのイニシャライザ や、
Publishers.Zip
など基本的な部分に関しても若干ひねりを加える必要がありました。また、個人的に intervalオペレータを使いたいときがあり、 Timer.publish で代用できないかと試行錯誤してみたのですが、良い案が思いつきませんでした。もしアイデアのある方いらっしゃいましたらコメントくださると幸いです。
おわりに
この記事ではよく使われる RxSwift の Single と、Combine による非同期処理の実装例を紹介しました。
Combine は外部ライブラリの導入なしに書き始めることができる素晴らしい公式ライブラリですが、RxSwift とは細かいところが色々違うので慣れるのに時間がかかったり、Combine で提供されていないオペレータはどう実現するか悩む部分もありました。
もし導入や置き換えを迷ってらっしゃる方は、RxSwiftで実装している部分が、どう置き換えることができそうか、アタリをつけてから導入しはめるのがおすすめかもしれません。
以上 iOS Advent Calendar 2019 の 2日目の記事でした。 明日は @fromkk さんの記事です。
- 投稿日:2019-12-02T07:11:40+09:00
[iOS] 最新のSpeech Recognitionについて
はじめに
iOS10から利用できるようになった音声認識API: Speech FrameworkについてのWWDC2019でアップデートがあったので最新事情をサンプルとともにお届けします。
APIに関する導入の詳細はiOSのSpeechフレームワークで音声認識 - 対応言語は58種類! - Qiitaをご覧ください。
WWDC2019のセッション内容
WWDC2019のセッションAdvances in Speech Recognition - WWDC 2019 - Videos - Apple Developerの内容を見ていきます。
サマリーとしては、
- macOSでのサポート
- デバイス上で動作可能に
- API強化のおかげで、豊富な音声分析ができるように
です。
macOSでのサポート
このサポートは、MacのAppKitアプリとiPadアプリの両方で利用できます。
iOSと同様に、50以上の言語がサポートされており、マイクにアクセスして音声を録音するには、ユーザーの承認が必要です。
また、ユーザーはSiriを有効にする必要があります。デバイス上で動作可能に
表題の通りローカル環境のみで動作するようになりました。
ネットワーク通信の必要がなくなり、データがAppleに送信されないため、よりプライバシーにも配慮してアプリ開発をすることができます。
しかし、トレードオフとして
- 精度の差異
- 継続的な学習を行なっているためサーバを通した方が優れている。
- リアルタイム性
- ローカル実行すると遅延が少ない
- 制限
- ネットワーク通信ではリクエスト数と音声の長さを制限
- ローカルでは無制限
- 10の言語のみ対応
- English, Spanish, Italian, Brazilian Portuguese, Russian, Turkish, Chinese
など、それぞれで問題点もあり、導入には要検討、と言ったところでしょうか。
特に、日本語対応していないのはかなり辛いですね。実装としては、
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.
ローカル サーバ通信 豊富な音声分析について
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に置いておきますのでご覧くださいませ。
お読みいただきありがとうございました。
参考
- 投稿日:2019-12-02T05:12:02+09:00
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の構成要素を貼っていきます。
- コードを中心にコードで個々の要素のインスタンスを生成し正しい位置に配置されるようにレイアウトします。分類をグラフにすると下記のようになります。
ここからは個々について見ていきます。
XIB
macOSのアプリを作成するための一番最初のGUIのツールです。
XIBは「XML Interface Builder」の略で
かつてのNIB(NeXT Interface Builder)を継承しています。XIBはアプリがビルドされる際にNIBへと変換されます。
NIBファイルはアプリのバンドルの中に配置され
実行時にUIKitによって読み込まれ
様々なUIのオブジェクトのインスタンスを生成と設定を行います。現在ではほとんどの役割をStoryboardに取って代わられていますが
TableViewやCollectionViewのCellや他の似た様なUIの要素を構築するのには有用です。特にTableViewやCollectionViewはNIBに対しての特別なメソッドも存在します。
https://developer.apple.com/documentation/uikit/uitableview/1614937-registerXIBファイルは多くの場合にファイルの~Owner~としてトップレベルのオブジェクトを有しています。
多くの場合
これはUIViewControllerのサブクラスであったり
UITableViewCellやUICollectionViewCellであったりしますが
特に特別な制約はなく
Objective-Cに互換性のあるどんなオブジェクトも~Owner~として使用できます。より具体的には
NSCoding
プロトコルに適合しているクラスでしたら可能なため
特にXIBファイル用の特別なクラスを用意する必要はありません。https://developer.apple.com/documentation/foundation/nscoding
Storyboard
エンジニアの中には
XIB + Flow = Storyboard
と考える人もいるように
Storyboardは
XIBで構成する複数のスクリーンと
それらの画面遷移をSegue
で構成します。アプリの流れが把握しやすくなった
例えばボタンをタップした時に他の画面へ遷移する場合は
下記のようにStoryboard上でそれを構成することができます。Storyboardの登場によって
画面もグラフィカルに見ることができるに加え
アプリの全体的な動き(Flow)もひと目で把握できるようになりました。LaunchScreen
プロジェクトを作成した際にLaunchScreen.storyboardというファイルがあります。
これはアプリが完全に起動する前にユーザーに表示されるデフォルトのイメージを設定できます。これはinfo.plistで他のStoryboardに変更が可能です。
この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.Spring and Strut
これはiOSのViewをレイアウトするための独自の方法です。
SpringまたはStrutという
親のViewの変化に応じてどのようにViewを拡大縮小されるかを
決める2つの仕組みがあります。Spring
親Viewがリサイズされるのに比例してViewをリサイズします。
Strut
親Viewとの距離を保つようにします。
拡大縮小できるのは下記の6つになります。
- Subviewの高さ(Spring)
- Subviewの幅(Spring)
- SubViewの上下左右4つのmargin(Strut)
下記のようにIB上でどのように動くのかを見ることができます。
この場合は
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.htmlAutoLayoutの基本設計は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で一つの宣言で複数の制約を設定できます。
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が自動で引き起こしてくれます。
コード例
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
intrinsicContentSize
やsystemLayoutSizeFitting(_:)
は
UIKitが自動計算した結果ですが
このような値も参照することで手動で計算する際に活用できます。https://developer.apple.com/documentation/uikit/uiview/1622600-intrinsiccontentsize
https://developer.apple.com/documentation/uikit/uiview/1622624-systemlayoutsizefittingManual 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-constantSwiftUIを学び始める
これらの学習の上に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
- 投稿日:2019-12-02T05:11:40+09:00
SwiftのStringの == 演算子とUnicodeの関係
SwiftのStringはEquatableプロトコルに準拠しています。したがって、以下のような比較ができます。
let string1 = "abc" let string2 = "bcb" string1 == string2 // => falseこうみると単純ですが、他の言語と比べて変わった点があります。
例としてRubyと比べてみましょう。Swiftlet pokemon1 = "Poke\u{0301}mon" // Pokémon let pokemon2 = "Pok\u{00e9}mon" // Pokémon pokemon1 == pokemon2 // trueRubypokemon1 = "Poke\u{0301}mon" # Pokémon pokemon2 = "Pok\u{00e9}mon" # Pokémon pokemon1 == pokemon2 # falseSwiftでは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
実は、pokemon1
とpokemon2
のそれぞれの文字は正準等価です。だからこそ==
で比較すると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 になります。これ以上はどちらも分解できず、順番も変わりません。その結果、両者は一致するので正準等価です。
分かりづらそうなので表にしました。二つの表の一番下の行が一致することに着目してください。注記: ᾉはギリシャ語の文字らしいです。
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
参考
この記事を書くにあたり以下の資料を参考にしました。
- String Reference
- Stringのリファレンスです。
- Advanced Swift
- Swiftの文法や言語仕様についての非常に優れた技術書です。おすすめです。
- UNICODE NORMALIZATION FORMS
- 正準等価についての公式の解説です。
- Unicode Glossary
- 用語集です。
- Unicode Terminology English - Japanese
- 用語の日本語対訳集です。
SwiftのStringのcountについて
記事を読んでいただきありがとうございます!
iOSDC Japan 2019で「SwiftのStringの文字の数え方を完全理解する」というタイトルで30分トークしました。
この記事に大いに関連する内容になっているので、もしタイトルに興味をひかれたらそちらもチェックしてください。この記事では文字とは抽象文字(Abstract Character)という意味で、iOSDCでのトークでは拡張書記素クラスタ(Extended Grapheme Cluster)という意味で使っています。
(Unicodeの専門知識レベルの話で、Swiftのコードを書くのには気にはならないはずです。)最後に
間違いがないように気をつけて調べて書きましたが、もしかしたら間違いがあるかもしれません。もしあればご指摘よろしくお願いいたします
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 とあるとおり、ドキュメントに明記されています。 ↩
似た定義に互換等価(Compatibility Equivalent)というものがあるのですが、SwiftのStringにはあまり関わりはありません。互換等価は正準等価より広い概念で、たとえば
㍍
とメートル
は互換等価ですが、正準等価ではありません。 ↩英語による定義 "Two character sequences are said to be canonical equivalents if their full canonical decompositions are identical." を筆者が訳しました。full はどうやって訳すか迷ったのですが「最後まで」としました。「再起的に」と訳しても良かったかもしれません。 ↩
データベースを「3071;」で検索してヒットする行の6列目を見てください。 ↩
- 投稿日:2019-12-02T01:55:04+09:00
アイコン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
- 投稿日:2019-12-02T01:23:23+09:00
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文の中で操作
という違いで、特に大きな違いはないので、好みでいいのかと思いますが
自分は前者の方がスッキリして、見やすいかなと思います。
ただ、後者は何をやってるのかは、パッと見で分かりやすいので
レビュー者との共通認識で、どちらかを使うか決めた方が良さそうです。
- 投稿日:2019-12-02T01:01:30+09:00
iOS13のUISearchBarにつまずいた
はじめに
今まで
UISearchBar
のテキストフィールドをカスタマイズしたい場合は下記のようにアクセスする方法が主流でした。let textField = searchBar.value(forKey: "_searchField") as? UITextFieldしかし、iOS13 では実行すると下記のようなエラーとなりアプリがクラッシュします
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
対応
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
でアクセスできるようになりましたその他UISearchBarの新機能
iOS13 からは下記のクラスが追加されており色々新機能があるようです。
UISearchTextFieldDelegate というプロトコルも追加されているみたいです。
下記の記事に詳しく書いてありました。
UISearchToken
がおもしろそうさいごに
UISearchBar
はvalue(forKey: "_searchField")
という非合法のやり方でテキストフィールドにアクセスする Developer があまりにも多いので公式で API を用意してくれたんですかね?This is an application bug
というくらいですからお前らもう公式で用意したからそういうことやめろよ?って感じがします
余談ですが
UIAlertController
も下記のように色々カスタマイズできます。
UIAlertAction
のsetValue(_:forKey:)
で下記のようにキーを指定すると色々できるようです。(参考)
- image
- imageTintColor
- titleTextColor
他にも色々いじれそう...(参考)
こんな感じ
UIAlertController
のカスタマイズもみんなが使いまくれば公式 API が公開されるかも?参考
- 投稿日:2019-12-02T00:56:44+09:00
個人的によく使うUIViewExtension
他の方が取り上げていたら
四隅のアンカー作成
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 } }もっと上手に書ける方法があったら教えていただけると非常に嬉しいです
- 投稿日:2019-12-02T00:41:54+09:00
アマゾネス、MacOSアプリを作ってVTuber(偽)デビュー
Advent Calendar 2019/12/02
アマゾネスでございます。
本日は、ワイがVTuberデビューした話、いや、そのMacOSアプリを作った話をします。はじまり
弊社では一週間の動向を社内ニュース(動画)として全社員に共有しています。
そして、最近私が実装した機能がリリースされたので、出演依頼がきてしまった...
依頼が来ること自体は、非常な光栄なことですし、一社員として協力したと思ました。が、ワイ(実体)が動画で記録されるなんてマジ無理
ということで、イラストでの出演交渉を行い、承諾してもらいました。
ちなみに、ここの段階では動かないイラストでの出演予定でした。あの日の白ワイン
出演前の週末、私はランチにカキフライを食べていました。
「白ワインあいそうやな...飲んじゃおっかしら(゚∀゚)」
白ワイン、マジで美味しい。牡蠣にあう。
一杯のつもりが、が、ががが....絶好調になった私は思いました。
「ワイのイラストでVTuber的なことできるんじゃねぇか...?」どういう脈絡...だよ、と思うかもしれませんが、
酔っ払いが考えることは大体意味ありませんのでそういうことです。私はまず、イラストをVTuber化できる無料ソフトを探しました。
が、本気度高いものばかりで、5秒でかける私のイラストに適した超軽量ソフトがない更に絶好調になった私は思いました。
「つくれるやろ」はい、そしてソフトを作ることにしました。
どんなものをつくったか
こちらをご覧ください
酔っ払った勢いでAppleの顔認識&音声認識でワイ動かしてしまったw pic.twitter.com/pwmANqZN5D
— アマゾネスいけばた (@k191k) November 10, 2019カメラやマイクを使うための準備の処理や、画像切り替えの処理については特に変わったことはしていません。
なので、今回は目と口を動している判定処理のところだけご紹介したいとおもいます。しゃべるワイ
まず、しゃべっているのを表現したかったので、口を動かすことにしました。
画像は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)という判定となるようになりました。また、判定をかける回数を制限しています。
制限しないと、口をパクパクしているアニメ(口を開けた画像と閉じた画像を交互に表示)が超高速すぎて口がなくなる、また、画像の更新が多すぎてフリーズするためですまばたきするワイ
しゃべるのが割とあっさりできたので、Macのカメラで私を撮り、私のまばたきにあわせて、イラストの目もパチパチさせてみることに。
目は Core Imageフレームワークに含まれる、CIDetector を使用し、目を閉じているかどうかを判定して実現しました。判定しているコードはこんな感じ
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を都度渡して目を開けたり閉じたりする処理にせず、一度の実行でまばたきを一回するようにしています。また、しゃべるのと同様に、判定をかける回数を制限しています。
しゃべる判定より判定回数が多いのは、瞬きは発声の様に連続的な事象ではなく、一瞬で完了してしまうので、判定する回数自体は増やしています。まとめ
普段はiOSのエンジニアをしている私が、今回MacOSのアプリを作ってみました。
やってみる前は、超難しそうというイメージでしたが、全然そんなことありませんでした。
多少なりと違うところはありますが、調べれはすぐ分かりましたので、今後他のツールも作ってみようかと思います。それから、顔認識ってすっっっごくハードル高く感じていましたが、Appleが提供しているフレームワークを使用すれば私でも作りたいものを作ることができました。
普段Swiftを書いていなくても、Macユーザーだったら気軽に触って遊んでみてほしいなと思いました以上、アマゾネスからのAdvent Calendar Blogでした
追伸 : 白ワインは1人で一本飲みました