- 投稿日:2020-12-07T23:15:34+09:00
SwiftLintのAnalyzeを使って高度な解析をする方法
はじめに
本記事は Swift/Kotlin愛好会 Advent Calendar 2020 の8日目の記事です。
SwiftLintのAnalyze機能を紹介します。環境
- OS:macOS Big Sur 11.0.1
- Swift:5.3.1
- Xcode:12.2 (12B45b)
- SwiftLint:0.41.0
本記事で説明しないこと
- SwiftLintの概要やセットアップ方法
私が以前書いた記事を参考にしてください。
Swiftの静的解析ツール「SwiftLint」のセットアップ方法 - Qiita- SwiftのASTについて
そもそも私が理解できていないので説明できません。
詳しく学びたい人は以下の動画が参考になると思います。
try! Swift Tokyo 2018 - AST Meta-programming - YouTubeSwiftLintの「Analyze」とは?
かんたんにいうと「ビルドログのASTを使って解析する」機能です。
Analyzeは実験的な機能であり、いつでも変更される可能性があるとのことです。
Analyzeの使い方
Analyzeの使い方を紹介します。
設定ファイルの作成
Analyze用のルールを設定ファイルの
analyzer_rules
に記述します。.swiftlint.ymlanalyzer_rules: #- explicit_self # 関数は `self.` を付けずに呼び出したいため - unused_declaration - unused_import公式ドキュメントで「Analyzer rule: Yes」となっているルールが対象で、私が見た限りでは以下の3つのみでした。
ルール 説明 参考リンク Explicit Self self.
を明示的に書くべきhttps://qiita.com/uhooi/items/7f5d6cf2b240f60ba1ed#explicit-self Unused Declaration 宣言した変数やクラスなどは使われるべき https://qiita.com/uhooi/items/8e9767c2e746f4171ded#unused-declaration Unused Import インポートされたモジュールは使われるべき https://qiita.com/uhooi/items/7f5d6cf2b240f60ba1ed#unused-import 私は同一クラス内の関数を
self.
なしで呼び出したいため、下の2つのみ有効にしています。Analyzeの実行
READMEに記載されている通り
xcodebuild
コマンドでログをファイルに出力し、それを--compiler-log-path
オプションで渡します。コマンドが長くなるので、私は
Makefile
に定義してmake analyze
で実行できるようにしました。
make build-debug
はCIでビルドが通るかの確認にも使っているため、swiftlint analyze
に対して必要最小限にはなっていません。MakefilePRODUCT_NAME := UhooiPicBook # 製品名を適宜変更する PROJECT_NAME := ${PRODUCT_NAME}.xcodeproj SCHEME_NAME := ${PRODUCT_NAME} TEST_SDK := iphonesimulator TEST_CONFIGURATION := Debug TEST_PLATFORM := iOS Simulator TEST_DEVICE ?= iPhone 12 Pro Max TEST_OS ?= 14.2 TEST_DESTINATION := 'platform=${TEST_PLATFORM},name=${TEST_DEVICE},OS=${TEST_OS}' XCODEBUILD_BUILD_LOG_NAME := xcodebuild_build.log .PHONY: analyze analyze: # Analyze with SwiftLint $(MAKE) build-debug mint run swiftlint swiftlint analyze --autocorrect --compiler-log-path ./${XCODEBUILD_BUILD_LOG_NAME} .PHONY: build-debug build-debug: # Xcode build for debug set -o pipefail \ && xcodebuild \ -sdk ${TEST_SDK} \ -configuration ${TEST_CONFIGURATION} \ -project ${PROJECT_NAME} \ -scheme ${SCHEME_NAME} \ -destination ${TEST_DESTINATION} \ build \ | tee ./${XCODEBUILD_BUILD_LOG_NAME} \ | bundle exec xcpretty --color
--autocorrect
オプションを付けると自動で修正されるのでオススメです。
make analyze
を実行します。$ make analyze # ... # `make build-debug` のログは省略 # ... mint run swiftlint swiftlint analyze --autocorrect --compiler-log-path ./xcodebuild_build.log Loading configuration from '.swiftlint.yml' Correcting Swift files at paths Collecting 'Debug.swift' (1/23) Collecting 'ActivityRouter.swift' (2/23) # ... Correcting 'MonsterListInteractor.swift' (15/23) /Users/uhooi/Documents/Repos/GitHub/uhooi/UhooiPicBook/UhooiPicBook/Repository/Spotlight/SpotlightRepository.swift:10:1 Corrected Unused Import Correcting 'MonsterListViewController.swift' (16/23) # ... Correcting 'MonsterDetailViewController.swift' (22/23) Correcting 'SceneDelegate.swift' (23/23) Done correcting 23 files!
.../SpotlightRepository.swift:10:1 Corrected Unused Import
のログからわかる通り、SpotlightRepository.swift
の10行1列目に未使用のモジュールがインポートされているので自動で削除されました。
git diff
で確認します。$ git diff diff --git a/UhooiPicBook/Repository/Spotlight/SpotlightRepository.swift b/UhooiPicBook/Repository/Spotlight/SpotlightRepository.swift index e435a9b..ba45ffb 100644 --- a/UhooiPicBook/Repository/Spotlight/SpotlightRepository.swift +++ b/UhooiPicBook/Repository/Spotlight/SpotlightRepository.swift @@ -7,7 +7,6 @@ import CoreGraphics.CGGeometry import CoreSpotlight -import MobileCoreServices /// @mockable protocol SpotlightRepository: AnyObject { // swiftlint:disable:this file_types_order確かにインポート文が削除されています。
今回は一度実行済みなので1箇所のみ削除されましたが、初回は大量の
import Foundation
とimport UIKit
が削除されて気持ちいいです。おまけ①: 使用しているインポートが削除される場合がある
先ほど削除された
import MobileCoreServices
ですが、実はないとビルドエラーになります。理由としては、iOS 13以前で
kUTTypeData
の呼び出しに使っているためです。SpotlightRepository.swiftprivate func createAttributeSet(title: String, contentDescription: String, thumbnailData: Data?) -> CSSearchableItemAttributeSet { let attributeSet: CSSearchableItemAttributeSet if #available(iOS 14.0, *) { attributeSet = .init(contentType: .data) } else { attributeSet = .init(itemContentType: kUTTypeData as String) // !!!: ここでビルドエラーになる } // ... // 中略 // ... return attributeSet }
xcodebuild
の-destination
オプションでiOS 14.2を指定しているためだと思われます。
仕方ないので手動でインポート文を戻しました。このようなケースはまれですが、自動修正する場合はコミット前にビルドが通るか確認しましょう。
おまけ②: Analyzeの実行タイミング
READMEに記載されている通り、Analyzeには時間がかかります。
そのため、Xcodeのビルドフェイズで毎回実行するのを避けています。現在は手動実行しているのですが、それだと漏れる可能性があるため、いい方法を考え中です。
おわりに
これで未使用のインポート文を大量に削除できます!
以上、 Swift/Kotlin愛好会 Advent Calendar 2020 の8日目の記事でした。
明日はまだ埋まっていません。ぜひ こちら から参加しましょう!
@Sho-heikun さんが参加してくださりました!
ありがとうございます
ウホーイさんを1人にさせるわけにはいかないので明日分入れときました! 1人で1週間埋めたいとかあったらキャンセルしますので教えてください!w
— ShoHeiKun (@ShoHeiKun3) December 8, 2020参考リンク
- 投稿日:2020-12-07T22:21:07+09:00
【備忘録】Swift 実践入門について 第7章
Swift 実践入門のまとめ。
分からない部分の抜粋も記載し、解決できたら随時更新していきます。
なお、ここに記載している以外でも「わけわからん…」となっている部分も多々ありますが、
今は必要ない、と言い聞かせて飛ばしています。第7章
型の構成要素 設計図の共通点。struct / class / enum
理解度:50%くらいか7-3 型を本とすれば、タイトルや筆者名がプロパティ。
本のタイトルを決めることを代入するという風に考えると、変数や定数もプロパティと言える。流れ
型 → インスタンス化 → printなどを実行プロパティにはスタティックプロパティやレイジーストアドプロパティなど、色々なタイプがあるが、ここら辺は本当に分かりません…
7-4 イニシャライザ
7-5 メソッド…型の中にある関数。structの中にあるfunc
7-6 サブスクリプト…コレクション要素へのアクセス方法
7-7 エクステンション…型にプロパティやメソッドなどを追加すること
7-8 型のネスト…型の中に型を入れ込む
はー、厳しい章でした。次の8章はstruct / class / enumの個別の特性。ここも長い…
- 投稿日:2020-12-07T21:42:13+09:00
配置されたTextViewのテキストの量に応じてTableViewのCellの高さを動的に変更させる
初めに
xibで作ったTableViewCellを想定しています
xibでのセルの作り方はこちらを参照ください
https://qiita.com/Secret-Base36/items/2a9a3989479559ba36ba環境
Xcode 12.2
1. TextViewの設定
- Scrolling Enableのチェックを外す
2. TableViewのセルの高さの設定
TableViewのheightForRowAtメソッドで可変に設定
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { //セルの推定最低高さ(不要?) secondTableView.estimatedRowHeight = 50 //セルの高さを可変に設定 return UITableView.automaticDimension }estimatedRowHeight(セルの推定高さ)は記述しなくても結果は変わらなかった
結果
文字量によってセルの高さが変わる様になった!
その他
上の図の様に最小高さの場合に、他のUIが重なるなど影響が出る場合は、制約で調整すると良い
最後に
情報が間違っているなどありましたら、お手数ですがご指摘いただけると幸いです。
- 投稿日:2020-12-07T21:26:15+09:00
条件分岐 ? : の使い方
はじめに
この書き方を見た時に、こんな書き方もあるんや!って思ったが、
この書き方を調べてもわからなかった(?:と調べてもわからなかった)ので、自分なりに一旦まとめていこうと思います。
色々わかり次第、情報を追加していこうと思います。使われ方
条件式 ? 式か値① : 式か値②
説明すると、条件式がtrueなら?の後ろの①、falseなら:の後の②を実行すると言うもの。試してみた
switchを用意してisOnがtrueの時とfalseの時を比較して見た。
let switch1 = UISwitch() let switch2 = UISwitch() switch1.isOn = true let number1 = 1 * (switch1.isOn ? 1 : -1) //1 * 1が実行され、number1 = 1 switch2.isOn = false let number2 = 1 * (switch2.isOn ? 1 : -1) //1 * -1が実行され、number2 = -1このように、trueの時に?の後が実行され、falseの時には:の後の処理が行われる。
- 投稿日:2020-12-07T21:26:11+09:00
クリスマス終了までをカウントダウンしたい
この記事は クソアプリ2 Advent Calendar 2020 の25日目の投稿です。
昨日は@inabajunmrさんの「ここにタイトルを入れます」でした。はじめに
今回初めてQiita アドベントカレンダーに参加しました。
去年や一昨年のアドベントカレンダーの「クソアプリ」の参加者の方の記事を読みましたが、とても楽しそうでした。
僕も今回楽しみたいと思います。今回作ったもの
クリスマス終了までをカウントダウンするアプリです。(スクリーンショットの画像はイメージがしやすいように別の日付を設定しています)
作ろうと思った経緯
このアプリを作ろうと思った経緯は、私が初心者なので勉強した事で「クソアプリ」を作ろうと思いました。
今回ちょうど、タイマーについて学んでいました。
そこで「タイマー機能」と「プログレスバー」を使ったアプリを作ろうと思いました。story boardにLabelなどを配置
タイマーの設定とLabelに時間を表示
let futureDate: Date = { var future = DateComponents( year: 2020, month: 12, day: 25, hour: 0, minute: 0, second: 0 ) return Calendar.current.date(from: future)! }() var countdown: DateComponents { //2つの指定された日付けの差を返す return Calendar.current.dateComponents([.day, .hour, .minute, .second], from: Date(), to: futureDate) } //カウントダウンされている「時間」「分」「秒」が表示される @objc func updateTime() { let countdown = self.countdown let hours = countdown.hour! let minutes = countdown.minute! let seconds = countdown.second! timeLabel.text = String(hours) minutesLabel.text = String(minutes) secondsLabel.text = String(seconds) } func runCountdown() { Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true) }progressBarに値を入れ、バーと%を表示する
runCountdown() let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" let startDate = formatter.date(from: "2020-12-24 00:00:00")! //現時刻と2020年12月25日との時間の差 let duration = formatter.date(from: "2020-12-25 00:00:00")?.timeIntervalSinceNow //絶対値に変換 let durationFabs = fabs(duration!) //少数点以下四捨五入 let durationRound = round(durationFabs) //2020年12月24日から2020年12月25日までのトータル時間 let elapsed = formatter.date(from: "2020-12-25 00:00:00")?.timeIntervalSince(startDate) //パーセンテージを出す let percentage = (durationRound / elapsed!) * 100 let percentageBarProgress = 1 - (durationRound / elapsed!) let percentageInteger = (round(100 * 10) / 10) - (round(percentage * 10) / 10) progressBar.progress = Float(percentageBarProgress) percentLabel.text = String("\(round(percentageInteger * 100) / 100) %") print(durationRound)//今現在と指定した時間との差の秒が表示される print(elapsed!) print(percentageInteger) print(percentageBarProgress)難しかった点
難しかった点は2つありました。
①12月25日(現時点での時間)から12月26日までの差分をどのように出せば良いかわからなかった点。1日のトータル時間を出しました。
let elapsed = formatter.date(from: "2020-12-25 00:00:00")?.timeIntervalSince(startDate)25日0時00分からどれくらい時間が経過したかの値を取得しました。
//現時刻と2020年12月26日との時間の差 let duration = formatter.date(from: "2020-12-26 00:00:00")?.timeIntervalSinceNow //絶対値に変換 let durationFabs = fabs(duration!) //少数点以下四捨五入 let durationRound = round(durationFabs)現在まで経過した時間 / 1日のトータル時間 * 100 でパーセントを出しました。
//パーセンテージを出す let percentage = (durationRound / elapsed!) * 100今日の残りの時間を出す為に1から引きます(パーセントにしていない100をかけていない値を使います)
let percentageBarProgress = 1 - (durationRound / elapsed!)②Labelに残りの%表示で小数点1まで表示する方法がわからなかった点。
小数点以下の表示をする時は、小数点第2位四捨五入は下記のようにする。let percentageInteger = (round(100 * 10) / 10) - (round(percentage * 10) / 10)Labelに小数点第1位まで表示する場合は、10 / 10 ではなく 100 / 100 を使用します。
percentLabel.text = String("\(round(percentageInteger * 100) / 100) %")小数点表示に関しては、手を動かしていたら出来た感じなので、もう少し理解を深めないといけないと感じています。
終わりに
今回初めてアドベントカレンダーに参加をしました。
なかなか、このような機会がないと学びをアウトプットする機会はありません。
このような素敵な発表の場を作っていただき、ありがとうございました。みなさんのような、素敵なクソアプリは作れませんでしたが、とても楽しかったです。
これからもSwiftの勉強に励みたいと思います。
- 投稿日:2020-12-07T21:18:32+09:00
【Swift】オプショナル型の何が嬉しいのか
何が嬉しいのか
nilを参照することができる。
つまり、例えばif
とかで「もしnilならば」という条件を使うことができる。定義してみる
先に正解を言っておくと
var hoge: String? print(hoge) // 出力は nilこう書けば
nil
が出力される。
しかし?
を付けずに書いたりvar hoge: String print(hoge) // エラーこう書いたりしてもエラーになる。
var hoge: String hoge = nil print(hoge) // エラーpaiza.ioとかで気軽に試すことができる。
次回はこのオプショナル型のhoge
を活用する方法を書く。
- 投稿日:2020-12-07T18:10:57+09:00
[Swift]Mintが動かなくて困ったが`~/.git_template/hooks/`のせいでした
概要
Cloning into 'github.com_yonaskolb_xcodegen'... Post Checkout make: *** No rule to make target `project'. Stop. ? Encountered error during "git clone --depth 1 -b 2.18.0 https://github.com/yonaskolb/xcodegen.git github.com_yonaskolb_xcodegen". Use --verbose to see full output ? Couldn't clone https://github.com/yonaskolb/xcodegen.git 2.18.0というログが出てMintのコマンド(bootstrap, install)が失敗して困っていた。
原因
エラーを見ると、「makeコマンド実行してるけどprojectってやつないよ」とのこと。
mintってmakeつかってるんだとか思ってたんですが、
mint内でgit cloneするときに以下のtemplateを参考にしちゃってたので自動でhooksファイルが生成されていた。
~/.git_template/hooks/
hooks ファイルにてmake project
を実行していてエラーになり、途中で処理が止まっていた。解決法
~/.git_template/hooks/
内のファイルを全部消すおまけ
Mint関係なかったので調べるの大変でした
- 投稿日:2020-12-07T17:51:31+09:00
SwiftのKeyPathと戯れる
KeyPath
とは?
KeyPath
とは?Swift4で追加された、プロパティに動的にアクセスするための記法
Use key-path expressions to access properties dynamically.1
Swift3.xまでの
#keyPath
class Hoge: NSObject { // ? @objc var foo: String // ? let bar: String init(foo: String, bar: String) { self.foo = foo self.bar = bar } } let foo = hoge.value(forKeyPath: #keyPath(Hoge.foo)) print(type(of: foo)) // Optional<Any> print(#keyPath(hoge.bar)) // error: argument of '#keyPath' refers to non-'@objc' property 'bar'
Obj-C時代からあるKVOでのtypoを防ぐ程度の役割しかない
NSObject
にしか使えない@objc
宣言されたプロパティのみ- 返却値は
Any!
(型情報が消える?)- 解析が遅い、Darwinでしか使えない2といった問題点も
Swift4からの
KeyPath
- Swiftの強力な型システムに基づいた機構
- structにもenumにも使える
- 型情報が維持される
- Swiftのstdlibに含まれているのでプラットフォームに依存しない
NSObject
も@objc
宣言もいらないclass Hoge { var foo: String let bar: String } let hogeFoo: ReferenceWritableKeyPath<Hoge, String> = \.foo let hogeBar = \Hoge.bar // KeyPath<Hoge, String> let hoge = Hoge(foo: "foo", bar: "bar") print(hoge.foo) // foo hoge[keyPath: hogeFoo] = "hoge-foo" print(hoge.foo) // hoge-foo hoge[keyPath: hogeBar] = "bar" // error: cannot assign through subscript: 'hogeBar' is a read-only key path
structにも使える
struct Fuga { var foo: String let bar: String } let fugaFoo: WritableKeyPath<Fuga, String> = \.foo let fugaBar = \Fuga.bar // KeyPath<Fuga, String> var fuga = Fuga(foo: "foo", bar: "bar") print(fuga.foo) // foo fuga[keyPath: fugaFoo] = "fuga-foo" print(fuga.foo) // fuga-foo fuga[keyPath: fugaBar] = "bar" // error: cannot assign through subscript: 'fugaBar' is a read-only key path
KeyPath
関連クラス_AppendKeyPath └ AnyKeyPath └ PartialKeyPath<Root> └ KeyPath<Root, Value> └ WritableKeyPath<Root, Value> // Value is variable └ ReferenceWritableKeyPath<Root, Value> // Root is class
小ネタ
Tupleも
Root
になれるlet hogeFuga = (hoge, fuga) let hogeKeyPath = \(Hoge, Fuga).0 // no label let fugaKeyPath = \(hoge: Hoge, fuga: Fuga).fuga // with labels hogeFuga[keyPath: hogeKeyPath] // hoge hogeFuga[keyPath: fugaKeyPath] // fuga
プロパティのネストも可能
struct FooBar { var hoge: Hoge var fuga: Fuga } let fooBarHogeFoo = (\FooBar.hoge).appending(path: \Hoge.foo) \FooBar.hoge.foo == fooBarHogeFoo // true print(fooBar.hoge.foo) // hoge-foo fooBar[keyPath: fooBarHogeFoo] = "foo-bar-hoge-foo" print(fooBar.hoge.foo) // foo-bar-hoge-foo fooBar[keyPath: \.hoge.foo] = "foo" print(fooBar.hoge.foo) // foo let fooBarFugaFoo = (\FooBar.fuga).appending(path: \Fuga.foo) fooBar[keyPath: fooBarFugaFoo] = "foo-bar-fuga-foo" // error: cannot assign through subscript: 'fooBar' is a 'let' constant
KeyPath
の実用例
プロパティでソートする3
public extension Sequence { func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] { sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] } } func sorted<T: Comparable>(by keyPath: KeyPath<Element, T?>) -> [Element] { sorted { guard let l = $0[keyPath: keyPath], let r = $1[keyPath: keyPath] else { return false } return l < r } } func min<T: Comparable>(by keyPath: KeyPath<Element, T>) -> Element? { self.min { $0[keyPath: keyPath] < $1[keyPath: keyPath] } } } struct Person { var id: Int var name: String var age: Int } extension Person: CustomStringConvertible { var description: String { name } } let people: [Person] = [ .init(id: 3, name: "Bob", age: 28), .init(id: 1, name: "Emma", age: 40), .init(id: 4, name: "Amelia", age: 18), .init(id: 2, name: "George", age: 22), ] // Before people.sorted(by: { $0.id < $1.id }) // ["Emma", "George", "Bob", "Amelia"] people.sorted(by: { $0.name < $1.name }) // ["Amelia", "Bob", "Emma", "George"] // After people.sorted(by: \.id) // ["Emma", "George", "Bob", "Amelia"] people.sorted(by: \.name) // ["Amelia", "Bob", "Emma", "George"]
同じAnchor同士へのConstraintを貼る
public extension UIView { func equal<Axis, Anchor: NSLayoutAnchor<Axis>>(_ anchor: KeyPath<UIView, Anchor>, to target: UIView) -> NSLayoutConstraint { self[keyPath: anchor].constraint(equalTo: target[keyPath: anchor]) } func equal<Axis, Anchor: NSLayoutAnchor<Axis>>(_ anchor: KeyPath<UIView, Anchor>, to target: UIView, constant: CGFloat) -> NSLayoutConstraint { self[keyPath: anchor].constraint(equalTo: target[keyPath: anchor], constant: constant) } } // Before NSLayoutConstraint.activate([ view.topAnchor.constraint(equalTo: target.topAnchor), view.leadingAnchor.constraint(equalTo: target.leadingAnchor), view.trailingAnchor.constraint(equalTo: target.trailingAnchor), view.bottomAnchor.constraint(equalTo: target.bottomAnchor) ]) // After NSLayoutConstraint.activate([ view.equal(\.topAnchor, to: target), view.equal(\.bottomAnchor, to: target), view.equal(\.leadingAnchor, to: target), view.equal(\.trailingAnchor, to: target) ])
更に関数チックに書いた例
https://www.objc.io/blog/2018/10/30/auto-layout-with-key-paths/
Key Path Expressions as Functions
Key Path Expressions as Functions
- Swift5.2で追加(SE-0249)
KeyPath
表現をクロージャを引数とする関数に渡すことができる
- コレクション操作などが簡潔に書けるように
- Xcode12.xならコード補完が効く
例
struct Department { var name: String var member: [Person] } extension Department { var boss: Person { member.min(by: \.id)! } } let departments: [Department] = [ .init(name: "General Affairs Department", member: [ .init(id: 3, name: "Bob", age: 28), .init(id: 1, name: "Emma", age: 40) ]), .init(name: "Development Department", member: [ .init(id: 4, name: "Amelia", age: 18), .init(id: 2, name: "George", age: 22) ]) ]
Before
departments.flatMap { $0.member } .map { $0.name } // ["Bob", "Emma", "Amelia", "George"] departments.map { $0.boss } .map { $0.name } // ["Emma", "George"] departments.map { $0.boss.name }
After
departments.flatMap(\.member) .map(\.name) // ["Bob", "Emma", "Amelia", "George"] departments.flatMap(\.member.name) // Value of type '[Person]' has no member 'name' departments.map(\.boss) .map(\.name) // ["Emma", "George"] departments.map(\.boss.name) // ["Emma", "George"]
Performance4
extension Person { var canDrink: Bool { age >= 20 } }
注意
変数/定数化したKeyPathを渡すことはできない(2020/12/9現在)
let bossKeyPath = \Department.boss let nameKeyPath = \Person.name let bossNameKeyPath = bossKeyPath.appending(path: nameKeyPath) departments.map(bossKeyPath) // Cannot convert value of type 'KeyPath<Department, Person>' to expected argument type '(Department) throws -> T' .map(nameKeyPath) // Cannot convert value of type 'WritableKeyPath<Person, String>' to expected argument type '(T) throws -> T' departments.map(bossNameKeyPath) // Cannot convert value of type 'KeyPath<Department, String>' to expected argument type '(Department) throws -> T'
最後に
KeyPath
によってSwiftコードがより簡潔に書きやすくなった- RxSwift / Combine のストリーム変換にも使えるのでより宣言的に書ける
- Genericsと併用することで処理の汎用化も大幅に進みそう
- Sample in Xcode Playground
- 投稿日:2020-12-07T17:28:30+09:00
最高の振動をデザインするための下準備〜UnityからCore Haptic FrameWorkを使ってみた〜
はじめに
この記事は「サムザップ #1 Advent Calendar 2020」の12月7日の記事です。
昨日の記事は@KoniroIrisさんの「【Unity】アップデートされたTerrainでトンネルを掘ろう!」でした。今回は、iOS13で新しく追加されたCore Haptic FrameWorkをUnityを使って、iPhoneの振動生成を試行錯誤するツールを作ったので、その紹介をします。
振動作成を試行錯誤ためのツール
振動作成するためのUnityで下記のようなツールを用意しました。
このツールでできること
①強さの調整
②鋭さの調整
③始まりと終わりの強さと鋭さを設定し、徐々に振動を変化させる
④SOSを表現したサンプルの実行
パラメータを調整しながら、自分の表現したい振動を見つけることを目的としています。背景
私はよく通勤時間や隙間時間を使って、よくカジュアルゲームで遊びます。
カジュアルゲームをやっていて良く感じることは、ゲーム中の振動が心地いいということです。
ゲーム内容ではなく、またあの振動に触れたいと思い、ゲームをプレイしてしまうことがしばしばあります。
通勤中など、マナー音をゲームする人が多く、振動は重要なゲーム演出の一つだと言えます。
自分でこの振動をデザインしてみたいなと思い、Core Haptic FrameWorkを使って、上記のようなツールを用意しました。Core Haptic FrameWork
Core Haptic FrameWorkはiOS13で新しく追加されたフレームワークです。
詳しくは公式のドキュメントをご覧ください。
このFrameWorkをUnityで呼び出し、振動生成のツールの実装しています。以前までは下記のようにAppleがあらかじめ用意されたものから好みのものを選択し、振動を表現していました。
以前までの振動の呼び出しextern"C"voidplaySystemSound (int n) { AudioServicesPlaySystemSound(n); }Core Haptic FrameWorkではユーザーが独自に振動を作れるようになりました。
それぞれ強さ、鋭さ、長さ、遅延時間などが設定できます。強さ、鋭さ、遅延時間を指定して、振動を呼び出し(ツールの強さの調整、鋭さの調整に使用)func createHapticEvents() -> [CHHapticEvent] { let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1) let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1) let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 0) return [event] }最初と最後の振動の強さ、鋭さを指定し、どれぐらいずつ振動を変化させるかを指定し、徐々に変化する振動を作ることができます。
fromからtoへと徐々に強さと鋭さを変更していく(ツールの【始まりと終わりの強さと鋭さを設定し、徐々に振動を変化させる】で使用)func createHapticEvents() -> [CHHapticEvent] { var events: [CHHapticEvent] = [] for i in stride(from: 0, to: 1, by: 0.1) { let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(1 - i)) let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(1 - i)) let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: i) events.append(event) } return events }最後により細かく設定する例になります。イベントタイプ、発生タイミングを設定することができます。
eventTypeと振動の継続時間を細かく設定(ツールのSOSサンプルで使用)func createHapticEvents() -> [CHHapticEvent] { let short1 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0) let short2 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.2) let short3 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.4) let long1 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 0.6, duration: 0.5) let long2 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 1.2, duration: 0.5) let long3 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 1.8, duration: 0.5) let short4 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 2.4) let short5 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 2.6) let short6 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 2.8) return [short1, short2, short3, long1, long2, long3, short4, short5, short6] }まとめ
今回はUnityからCore Haptic FrameWorkを呼び出し、振動を試行錯誤するためのツールの紹介しました。
ただツールを使って、振動を調整してきましたが、まだまだ自分の思った通りの振動を表現することはできていません。
デザイナーさんが色を見て、その色をだいたいの構成するRGB値を予想できるように、振動をデザインできるようになれたらと考えています。
明日はアドベントカレンダーの担当は@kurosawa_tomokazuさんです。
- 投稿日:2020-12-07T14:36:05+09:00
条件分岐文について〜switch〜
switch文 とは
switch文とは、パターンを利用して制御式の値に応じて実行文を変える制御構文になります。
switch文の各パターンはcaseキーワードで定義し、
どのパターンともマッチしなかった場合の処理はdafaultキーワードで定義します。switch 制御式 { case パターン1: 制御式がパターン1にマッチした時に実行される箇所 case パターン2: 制御式がパターン2にマッチした時に実行される箇所 default: 制御式がいずれのパターンにもマッチしなかった場合に実行される箇所 }switch文では、制御式がパターンとマッチするか上から順に確認していき、
マッチしたパターンの実行文が読み込まれます。swiftにおけるswitch文は一度実行するとマッチを終了し、
それ以降のパターンは全て無視します。つまり、複数のパターンにマッチする制御式でも、
先頭のパターン以外は意味をなしません。is文やguard文は成立するか否かの2ケースへの分岐でしたが、
switch文はさらに多くの分岐ができます。つまり、switch文は複数のケースを持つ条件分岐に向いています。
また、if文やguard文がBool型の値しか指定できないのに対し、
switch文はどんな型でも指定できます。Int型の定数aの値が、負・0・正の場合のいずれかによって3つに分岐しています。
let a = 10 switch a { case Int.min..<0: print("aは負の値です。") case 1..<Int.max: print("aは正の値です。") default: print("aは0です") } 実行結果 aは正の値です。ケースの網羅性
swiftのswitch文ではコンパイラによってケースの網羅性のチェックが行われ、
網羅されていない場合はコンパイルエラーになります。siwtch文を網羅的にさせるには、
制御式が取り得る全ての値をいずれかのケースにマッチさせる必要があります。
.abc
、.def
、.ghi
の3つのケースを持つSomeEnum型の値を返す制御式を例にします。
(列挙型のenumについては別の記事で詳しく説明します。)enum SomeEnum { case abc case def case ghi } let a = SomeEnum.abc switch a { case .abc: print("abc") case .def: print("def") case .ghi: print("ghi") } 実行結果 abc制御式aはSomeEnum型なので、取り得る値は
.abc
、.def
、.ghi
です。先ほどのswitch文では、いずれの値に対してもケースが記述されているため、
網羅的となっています。先ほどの内容から
case .ghi:
を削除してしまうとコンパイルエラーになります。enum SomeEnum { case abc case def case ghi } let a = SomeEnum.abc switch a { case .abc: print("abc") case .def: print("def") } // .ghiのケースが想定されていないため網羅的ではなくコンパイルエラーswitch文の網羅性を満たすためには、
制御式の型が取り得る全ての値をケースで記述する必要があります。Bool型の場合にはtrueとfalseの両方を記述する必要があります。
let a = true switch a { case true: print("true") case false: print("false") } 実行結果 true制御式が取り得る値がわからない場合や、
ケースを分ける必要がない場合などはdefaultを使用することも可能です。
defaultは網羅性を保証する役割を担っております。先ほどのSomeEnum型を例にすると下記のようになります。
enum SomeEnum { case abc case def case ghi } let a = SomeEnum.ghi switch a { case .abc: print("abc") case .def: print("def") default: print("Default") } 実行結果 Defaultただし、列挙型の制御式に対して
デフォルトケースを用いることは極力避けた方がいいらしいです。
というのもきちんと理由があります。switch文がdefaultケースを持っている場合、
列挙型に新しいケースが追加されたとしても自動的にdefaultケースにマッチします。この場合、網羅性には問題がないためコンパイルエラーにはなりません。
一見良いことのように思えますが、
新たに追加されたケースは至る所でdefaultケースとして扱われます。つまり、意図していない動作になる確率が大幅に上がります。
defaultケースの使用は、楽ですが変更に弱いプログラムを招きます。
defaultケースを使っていない場合は、
変更後は網羅的ではなくなるためコンパイルエラーが発生します。
結果として追加されたケースに対しての実装をどこに追加すべきか分かります。whereキーワード
whereキーワードは、ケースにマッチする条件を追加する機能を持っています。
switch 制御式 { case パターン where 条件式: 制御式のパターンにマッチし、かつ、条件式を満たす時に実行 default: 制御式がいずれのパターンにもマッチしなかった場合に実行 }パターンがマッチしても条件式を満たしていなかった場合は、
そのケースの内容を実行することはできません。let a: Int? = 1 switch a { case .some(let a) where a > 10: print("10よりも大きい値\(a)が存在します。") default: print("値が存在しない、もしくは10以下です。") } 実行結果 値が存在しない、もしくは10以下です。
case .some(let a)
の部分はマッチしています。
ですが、where a > 10
で条件を満たしていないのでdefaultケースが実行されます。
.some(let a)
についてですが、appleから標準で提供されている機能です。
値が入っている時にマッチするケースだと認識しています・・・。一部抜粋 @frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral { /// The absence of a value. /// /// In code, the absence of a value is typically written using the `nil` /// literal rather than the explicit `.none` enumeration case. case none /// The presence of a value, stored as `Wrapped`. case some(Wrapped) /// Creates an instance that stores the given value.break文
break文は、switch文のケースの実行を中断する文になります。
breakキーワードのみか、breakキーワードと後述するラベルの組み合わせで構成されます。break文を使用した時点で処理が中断されるため、
下記の場合は1つ目のprint()関数しか実行されません。let a = 1 switch a { case 1: print("実行:1回目") break print("実行:2回目") default: break } 実行結果 実行:1回目ケース内には少なくとも一つの文が必要なため、
defaultケースを使用しているものの特に処理を行う必要がない場合などに
defaultケース内にbreak文を記述したりする場合もあります。ラベル
ラベルは、break文の制御対象を指定するための仕組みになります。
どの様な時に使うのかというと、
switch文が入れ子(ネスト)構造になっている場合などの、
break文の対象となるswitch文を明示する必要があるケースで使用します。無理やり作ったコードですが・・・。
0~10の間の値だと、1つ目のケースの中に入り、それ以外だと測定不可能と表示される処理です。
また、0~10の間で、奇数の場合は「値は奇数です」と表示され、
値が偶数の場合は「値は偶数です」と表示されます。なお、値が0の場合は「0はダメです」と表示されます。(無理やり)
let a = 0 outer: switch a { case 0...10: let string: String inner: switch a { case 1, 3, 5, 7, 9: string = "奇数" case 2, 4, 6, 8, 10: string = "偶数" default: print("0はダメです。") break outer } print("値は\(string)です。") default: print("10より大きい数字なので測定不能") } 実行結果 0はダメです。今回は定数aに0を代入しています。
outer: switch a
やinner: switch a
は、switch文にラベル名をつけています。
break outer
でどこのswitch文をbreakするかを指定しています。今回は外側のswitch文を抜けるようにしていますが、
break inner
に変えれば内側のswitch文を抜けて
print("値は\(string)です。")
を実行します。ただ
break inner
にしてしまうと、
let string: String
が初期化されておらず、
中に何も値がない状態なのでコンパイルエラーになります。break outerの場合にコンパイルエラーが起きないのは、
外側のswitch文が終了するためprint("値は\(string)です。")
が実行されないからです。fallthrough文
fallthrough文は、switch文のケースの実行を終了し、
次のケースを実行させる制御構文になります。
case 1
内にfallthroughを記述すると、
fallthrough以降の処理は行わず一つしたのケース(case 2)に移行します。let a = 1 switch a { case 1: print("case 1") fallthrough print("case 1-2") case 2: print("case 2") default: print("defaule") } 実行結果 case 1 case 2JavaやC言語などでは、
ケースを実行した後に次のケースに移行するのはデフォルトの挙動になります。しかし、swiftではfallthrough文によって明示しない限り次のケースに移行することはありません。
暗黙的な使用をできる限り排除しようというswiftの思想を見て取れますね!
以上、最後までご覧いただきありがとうございました。
- 投稿日:2020-12-07T14:22:34+09:00
[Swift] Delegateが分からない初心者のための最後の受け皿
今回作るもの
黄色のボタンを押すと画面が遷移され黄色くなる実装をdelegateを使って実装&説明します。
きっかけ
Swiftのdelegateの説明をQiitaでたくさん読みましたがいまいち理解できず、なんとなく実装していました。delegateで詰まることが多かったので、アウトプットの機会にしようと思います。
対象
私のように他の記事を読んでも理解に苦しんだ方々に読んで頂きたいです。
Delegateとは
Delegateは委譲という意味ですが、そのクラスではできないことを他のクラスでやってもらうため委譲が必要になるのです。
詳しくコードと共に見ていきましょう。実際のコード
3ファイル作成しました。
1つめははじめの画面のViewController
2つめは黄色画面遷移先のYellowViewController
3つめはViewControllerのCellのクラス(ButtonTableViewCell)まず1つ目のViewControllerです。
次に黄色画面遷移先のYellowViewControllerです
最後に先ほど作った黄色ボタンのCellのクラスです
ここではcellの中のボタンタップ時の処理を行えます。
「ここに処理を書きたい」の部分にlet yellowViewController = YellowViewController() navigationController?.pushViewController(yellowViewController, animated: true)を入れて黄色画面に飛ばしてやりたいですが、CellではNavigationControllerが使用できないので、他の画面で処理をする必要があるのです。そこでdelegateという考え方が必要になります。
Delegateの使い方
1, protocolを宣言
2, delegateを宣言
3, 発火させたい場所にdelegate設置
4, 受け渡し先のクラスで継承
5, 受け渡し先のViewにdelegateを呼び出す
6, 処理を実装する具体的な実装方法
1, まずprotocolを発火させたい(ここでは@IBActionのあるButtonTableViewCell)クラスで宣言します。protocol [delegate名]: class { func [メソッド名] }2, delegateを発火させたい(ここでは@IBActionのあるButtonTableViewCell)クラスで宣言します
3, 実際に発火させたい箇所(ここでは@IBActionの中)でdelegateを設置
4, ViewControllerに戻って、delegate名を継承する
5, ViewControllerでcellを使える場所で、delegateを呼び出す
6, そのままでは実際にしたい処理が書かれていないので、protocolで宣言したmethodをViewController内で書きますおまけ (元々あるDelegate)
ViewControllerにも元々
buttonTableView.delegate = self buttonTableView.dataSource = selfと書かれていますが、これも同様に
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource,UITableViewDelegate, UITableViewDataSourceを継承しているからです。
その証拠に、
UITableViewDelegateを右クリックして見てみましょう。
protocolが宣言されている訳です。
ViewControllerでデフォルトでtableViewが使えていたのも理由があったのですね。func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { }最後に
最後まで読んでいただきありがとうございます。理解していただけたしょうか?
delegateはswiftを学習し始めで必ずと言っていいほど、詰まる箇所だと思います。
とても複雑に見えていましたが、根本を理解してみると簡単なものでした。
- 投稿日:2020-12-07T11:31:44+09:00
【Swift】CodingKeyを使わず、JSONのsnake_caseをcamelCaseに変換する方法
JSONのデータをSwiftで変換する際に
CodingKey
を使用する方法を学んだのですが、調べているとCodingKey
を使用しない方法もあったのでメモ例として、QiitaのAPIを取得しています。
CodingKey
を使用する場合API用の構造体の中に
enum
を定義する必要があります。struct Qiita: Codable { let title: String let createdAt: String let user: User enum CodingKeys: String, CodingKey { case title = "title" case createdAt = "created_at" case user = "user" } } struct User: Codable { let name: String enum CodingKeys: String, CodingKey { case name = "name" } }・
CodingKey
を使用しない場合構造体内で
enum
を宣言してcamelCaseに変更する代わりに、API取得時に.keyDecodingStrategy
に.convertFromSnakeCase
を代入して変換します。構造体
struct Qiita: Codable { let title: String let createdAt: String let user: User } struct User: Codable { let name: String }
enum
が無くなりスリムになりました。API取得用メソッド
func getQiitaApi() { guard let url = URL(string: "https://qiita.com/api/v2/items?page=1&per_page=5") else { return } var request = URLRequest(url: url) request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: url) { (data, response, error) in if let error = error { print("(1)情報の取得に失敗しました:", error) return } if let data = data { do { let jsonDecoder = JSONDecoder() //snake_caseをcamelCaseにデコードするように指示 jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase let qiita = try jsonDecoder.decode([Qiita].self, from: data) print(qiita) } catch { print("(2)情報の取得に失敗しました:", error) } } } task.resume() } }情報の取得に成功すると、取得したQiitaの記事タイトル、作成日、作成者の名前がログに出力されます。
参考
- 投稿日:2020-12-07T10:38:18+09:00
Swift の乱数生成が遅い?
問題
自作アプリでシミュレーションシステムを開発する時、こんな要件がありました:
カードの特技がある確率で発動することにして、実際に発動するかどうかをシミュレートしてください。だが、シミュレートの回数が合計ほぼ百万回に近い。言い換えると:
ある事象が発生する確率は rate であり、その事象の発生状況を百万回シミュレートしてください。最初は Swift 4.2 から提供された Random API を使いました:
class LSSkill { // 発動確率(0.8 => 80%) let rate: Double // 小数を [0,1) の範囲で生成して、rate より小さい場合、発動したと判定。 func simulatePossibility() -> Bool { Double.random(in: 0..<1) < rate } }理屈はもちろん大丈夫ですが、Instrument で profile したところ、乱数生成が、スコアシミュレーション全体時間のほぼ四分の一を占めました。
シミュレーション自体が 0.3s 未満とはいえ、改善の余地があれば見逃すわけにはいけませんね!!(神経質)
改善策
代わりに見つけたのは
drand48()
という少し Low-Level な関数。drand48 がちょうど [0,1) 範囲の乱数を生成してくれます。func simulatePossibility() -> Bool { drand48() < rate }注意:ここで省きましたが、
drand48()
を使う前に必ずsrand48()
によって一回初期化してください。
drand48
に入れ替わっただけで、時間が先程の方法よりだいぶ縮みました:まともに測ろう
実機ではちゃんと整理できなかったので、テスト環境で
measure
を使って様々な方法の実行速度を比べましょう。測定コードはこのよう、なるべく無関係要素を除きたいので、範囲と乱数 Generator を for…in 循環の外に置きます。
let total = 200_0000 func testDoubleRandom() throws { measure { let range = 0..<1.0 var gene = SystemRandomNumberGenerator() for _ in 0..<total { let _ = Double.random(in: range, using: &gene) } } } // 他の5つを省略測ったのは、[0,1) での小数生成と、[100, 200) での整数と小数の生成。Random API とその以前の方法を比べました:
Random API が範囲・型に関わらず大体同じ速度に保っていて、すべて Low-Level 関数に上回っていますね、やはりちょっと遅い気がします。
なぜ?
Random API が公式が実現してくれたもので、本来から言うと最適化されたはずですが、もともと存在した関数より遅いのはなぜ?
それを究明するには、ソースコードを覗く必要がありました。
Int
の乱数方法は swift/Integers.swift に書いてあります。簡単に説明するため、半開区間だけを切り抜きます:public static func random<T: RandomNumberGenerator>( in range: Range<Self>, using generator: inout T ) -> Self { // ... let delta = Magnitude(truncatingIfNeeded: range.upperBound &- range.lowerBound) // 1 return Self(truncatingIfNeeded: Magnitude(truncatingIfNeeded: range.lowerBound) &+ generator.next(upperBound: delta) // => 次のコードブロック ) // 2 }1: 与えられた範囲の長さを計算。メモリリークにならないためビット演算と
truncatingIfNeeded
による初期化になります。Magnitude は数値の絶対値部分を表す。
2: その長さ delta を上限として乱数 Generator に渡して、[0, delta) の範囲の乱数を生成し、その乱数を最初に与えられた範囲の下限に足して、最終乱数になります。では、その乱数 Generator がどのように乱数を生成したのですか?デフォルトとして
SystemRandomNumberGenerator
が使用されていますが、上限付きのnext()
はプロトコルRandomNumberGenerator
のデフォルト関数として書いています:
(次の2つブロックは swift/Random.swift から)public protocol RandomNumberGenerator { mutating func next() -> UInt64 } extension RandomNumberGenerator { public mutating func next<T: FixedWidthInteger & UnsignedInteger>( upperBound: T ) -> T { _precondition(upperBound != 0, "upperBound cannot be zero.") #if arch(i386) || arch(arm) let tmp = (T.max % upperBound) + 1 let range = tmp == upperBound ? 0 : tmp var random: T = 0 repeat { random = next() // => 次のコードブロック } while random < range return random % upperBound #else // ... #endif } }正直ここの乱数生成のアルゴリズムを理解していませんが、とりあえず
SystemRandomNumberGenerator
のnext()
使ったので、続きます:public struct SystemRandomNumberGenerator: RandomNumberGenerator { public mutating func next() -> UInt64 { var random: UInt64 = 0 swift_stdlib_random(&random, MemoryLayout<UInt64>.size) // => 次のコードブロック return random } }続きは
swift_stdlib_random
。ついに Swift 言語を実現した cpp の領域に踏み入りました。
swift/Random.cpp から#if defined(__APPLE__) // APPLE のプラットフォームでしたら SWIFT_RUNTIME_STDLIB_API void swift_stdlib_random(void *buf, __swift_size_t nbytes) { arc4random_buf(buf, nbytes); } // ...他のプラットフォームでの実現遠回りして、結局 arc4random に辿り着きましたか…ここまでにするともう次に探索する必要がありません。小数の実現 swift/FloatingPointRandom.swift を探ると同じく
SystemRandomNumberGenerator
を使っていることがわかりました。つまり、Random API が
arc4random
を使った上で、他の操作を加えて今のInt.random
関数になりました。生のarc4random
に比べると遅いのは当たり前のことです。しかし、これまでのコードを見た結果、Low-Level 関数にいくつ欠点があります:
- 具体的な型に制限されています。
arc4random
系の関数がUInt32
に、drand48
がDouble
に。実際色んな場面で使うと型変換しなければなりません。- 違う関数の範囲の指定がバラバラで、紛らわしい。例えば
arc4random
が上限しか指定できなくて、[100, 200) 範囲にしたい時、上限を100にして、その後乱数に100を足す必要があります。- Swift が様々なところで実行しています。違うプラットフォームでは違う乱数生成の方法があって、クロスプラットフォーム開発者に対してはかなりの苦痛でしょう。
実際、Random API に関する最初の提案では Proposal Random Unification - Discussion - Swift Forums、同じことが言われたようです。
Swift コミュニティは、これらの欠点を解消するため、型・範囲・プラットフォームに対して汎用化した乱数 API を実装し、統一したことで開発に大きな利便性が持たされました。
まとめ
この記事は Swift の乱数生成の中の仕組みを探索し、ちょっとだけ遅い原因を究明しました。
遅いとは言え、短時間で百万回実行する要件ではなく、一回二回だけでしたら全然気にすることはありません。そんな無視できる程度の性能改善より、コードの統一性と可読性が遥かに重要だと思います。
以上です。なにか間違えたことがあれば気軽にコメントお願いします!
- 投稿日:2020-12-07T05:17:49+09:00
[Swift] SkyWayを使ったWebRTCの映像をFirebase MLに接続して顔認識させる
前書き
とあるハッカソンで使った技術で、実装するにあたり壁にぶつかりました。
おそらく前例がないので、備忘録として残します。技術に関して
1. SkyWayとは
ほとんど画像に記載がありますが、Webでリアルタイムコミュニケーションを実現する標準技術「WebRTC(Web Real Time Communication)」を簡単にアプリに導入できるSDK&APIです。(公式引用)
2. Firebase MLとは
ML Kit は、Google の機械学習の機能を Android アプリや iOS アプリとして提供するモバイル SDK です。(公式引用)
今回はこの部分の中身についての実装は話しません。
あくまでもSkyWayとFirebaseを繋ぐ部分のみになります。
(中身の参考資料「Firebase MLKitを使って動画の顔認識をさせる」)3. 構成図
アプリ間で動画のリアルタイム通信を行いながら、その映像をFirebase MLで判定させるということ行います。
SkyWayが2つ導入されていますが、
- 上部はSkyWayサーバー - 右下アプリ内はSkyWayのSDKになります。
実装
1.前提
Firebase ML
側でデータを分析するためには、動画をCMSampleBufferという型で受け取る必要があります。幸いなことにiOSでは映像から
CMSampleBuffer
型を受け取るために、
「AVCaptureVideoDataOutputSampleBufferDelegate」
というものが用意されています。Example
final class ExampleVideoManager { private var videoOutput: AVCaptureVideoDataOutput! private let queue = DispatchQueue(label: "videoOutput", attributes: .concurrent) init() { self.videoOutput = AVCaptureVideoDataOutput() self.videoOutput.setSampleBufferDelegate(self, queue: queue) // デリゲードのセット } } extension CaptureVideoManager: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // CMSampleBuffer型の値が受け取れる場所。Firebase MLへ接続。 } }2.問題点
通常、カメラや動画を実装するためには
AVKit
をimportとして色々と実装していく必要があります。
(実装説明は割愛するので参考として「Swiftでカメラアプリを作成する」)前提のように自前で
AVCaptureVideoDataOutput
を生成してFirebase ML
接続できれば良いですが、SkyWay
の場合は指定された実装方法で行う必要があるため、わからないようになっています。具体的には、
SKWVideo
という指定されたUI型に、勝手にレンダリングされるようになっていることが問題になります。Example
@IBOutlet weak var streamView: SKWVideo! // レンダリングする画面 func setuo() { /* ~略~ */ let constraints = SKWMediaConstraints() localStream = SKWNavigator.getUserMedia(constraints) localStream?.addVideoRenderer(self.remoteStreamView, track: 0) // レンダリングの指定 /* ~略~ */ }3.解決法
中で隠蔽化されていますが、必ず
AVCaptureVideoDataOutput
は実装されているので、それをSKWVideo
の中から自力で探し当てました。具体的には、レンダリングされる画面のヒエラルキーをちまちまとデバッグして
AVCaptureVideoDataOutput
を持つレイヤー層を探り当てました。探り当てた部分をコードで取り出す場合は、以下のようになります。
// レンダリング画面 @IBOutlet weak var streamView: SKWVideo! // レンダリングしている画面の階層を深掘りし、中から`layer`層を取り出す var videoLayer: AVCaptureVideoPreviewLayer? { return streamView.subviews.first?.layer as? AVCaptureVideoPreviewLayer } // 上記の`layer`層から変換に必要な`AVCaptureVideoDataOutput`を取り出す var output: AVCaptureVideoDataOutput? { return videoLayer?.session?.outputs.first as? AVCaptureVideoDataOutput }上記から
AVCaptureVideoDataOutput
にアクセスできるので、output
にsetSampleBufferDelegate
セットしてあげることで、CMSampleBuffer
型の値を手に入れることができるようになります。これで
Firebase ML
への接続も可能となりました。あとがき
複数のストリームで受け取って加工する、など色々試した結果、この方法が最適なかつ実現可能な方法でした。
少しマニアックなネタだったかもしれませんが、誰かの参考になれば嬉しい所存です。
文献
SkyWay
の実装周りは解説しなかったので、以下を参考にすると良いでしょう。
- SkyWay - アプリやWebサービスに、ビデオ・音声通話をかんたん導入
- iOSのCallKitフレームワークとSkyWay
- SKYWAY SDKを使ったIOS版通話アプリ開発Firebase周りはこちらをご覧ください、
- Firebase MLKitを使って動画の顔認識をさせる
- 投稿日:2020-12-07T05:17:49+09:00
[Swift] SkyWayを使ったWebRTCの映像をFirebase MLに接続する
前書き
とあるハッカソンで使った技術で、実装するにあたり壁にぶつかりました。
おそらく前例がないので、備忘録として残します。技術に関して
1. SkyWayとは
ほとんど画像に記載がありますが、Webでリアルタイムコミュニケーションを実現する標準技術「WebRTC(Web Real Time Communication)」を簡単にアプリに導入できるSDK&APIです。(公式引用)
2. Firebase MLとは
ML Kit は、Google の機械学習の機能を Android アプリや iOS アプリとして提供するモバイル SDK です。(公式引用)
今回は映像部分の顔判定をになっていますが、この部分の中身についての実装は話しません。
あくまでもSkyWayとFirebaseを繋ぐ部分のみになります。
(中身の参考資料「Firebase MLKitを使って動画の顔認識をさせる」)3. 構成図
アプリ間で動画のリアルタイム通信を行いながら、その映像をFirebase MLで顔認識させるということ行います。
SkyWayが2つ導入されていますが、
- 上部はSkyWayサーバー - 右下アプリ内はSkyWayのSDKになります。
実装
1.前提
Firebase ML
側でデータを分析するためには、動画をCMSampleBufferという型で受け取る必要があります。幸いなことにiOSでは映像から
CMSampleBuffer
型を受け取るために、
「AVCaptureVideoDataOutputSampleBufferDelegate」
というものが用意されています。Example
final class ExampleVideoManager { private var videoOutput: AVCaptureVideoDataOutput! private let queue = DispatchQueue(label: "videoOutput", attributes: .concurrent) init() { self.videoOutput = AVCaptureVideoDataOutput() self.videoOutput.setSampleBufferDelegate(self, queue: queue) // デリゲードのセット } } extension CaptureVideoManager: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // CMSampleBuffer型の値が受け取れる場所。Firebase MLへ接続。 } }2.問題点
通常、カメラや動画を実装するためには
AVKit
をimportとして色々と実装していく必要があります。
(実装説明は割愛するので参考として「Swiftでカメラアプリを作成する」)前提のように自前で
AVCaptureVideoDataOutput
を生成してFirebase ML
接続できれば良いですが、SkyWay
の場合は指定された実装方法で行う必要があるため、わからないようになっています。具体的には、
SKWVideo
という指定されたUI型に、勝手にレンダリングされるようになっていることが問題になります。Example
@IBOutlet weak var streamView: SKWVideo! // レンダリングする画面 func setup() { /* ~略~ */ let constraints = SKWMediaConstraints() localStream = SKWNavigator.getUserMedia(constraints) localStream?.addVideoRenderer(self.remoteStreamView, track: 0) // レンダリングの指定 /* ~略~ */ }3.解決法
中で隠蔽化されていますが、必ず
AVCaptureVideoDataOutput
は実装されているので、それをSKWVideo
の中から自力で探し当てました。具体的には、レンダリングされる画面のヒエラルキーをちまちまとデバッグして
AVCaptureVideoDataOutput
を持つレイヤー層を探り当てました。探り当てた部分をコードで取り出す場合は、以下のようになります。
// レンダリング画面 @IBOutlet weak var streamView: SKWVideo! // レンダリングしている画面の階層を深掘りし、中から`layer`層を取り出す var videoLayer: AVCaptureVideoPreviewLayer? { return streamView.subviews.first?.layer as? AVCaptureVideoPreviewLayer } // 上記の`layer`層から変換に必要な`AVCaptureVideoDataOutput`を取り出す var output: AVCaptureVideoDataOutput? { return videoLayer?.session?.outputs.first as? AVCaptureVideoDataOutput }上記から
AVCaptureVideoDataOutput
にアクセスできるので、output
にsetSampleBufferDelegate
セットしてあげることで、CMSampleBuffer
型の値を手に入れることができるようになります。output?.setSampleBufferDelegate(self, queue: /* Queue */)これで
Firebase ML
への接続も可能となりました。あとがき
複数のストリームで受け取って加工する、など色々試した結果、この方法が最適なかつ実現可能な方法でした。
少しマニアックなネタだったかもしれませんが、誰かの参考になれば嬉しい所存です。
文献
SkyWay
の実装周りは解説しなかったので、以下を参考にすると良いでしょう。
- SkyWay - アプリやWebサービスに、ビデオ・音声通話をかんたん導入
- iOSのCallKitフレームワークとSkyWay
- SKYWAY SDKを使ったIOS版通話アプリ開発Firebase周りはこちらをご覧ください
- Firebase MLKitを使って動画の顔認識をさせる
- 投稿日:2020-12-07T04:09:39+09:00
ラズパイとSwiftでSlackに投稿ボタンをつくろう!
はじめに
最近ラズパイをさわり始めました。せっかくなので Swift とラズパイで Slack で今日は休む旨を伝えるボタンを作りたいと思います!
こんな感じです。
必要なもの
今回必要なものは下記です。
- microSD カード
- Raspberry Pi 3 Model
なんとなく3買いましたが4の方がいいと思います(4の場合電源は USB-C です)。- マイクロ USB 電源コード
- 10 K の抵抗×1
- スイッチ×1
- オスーメスジャンバ線×3
- ブレッドボード
具体的なのは下記参照
MacでラズパイのLチカしてみたー必要なものラズパイ設定
OS のインストールや SSH 接続までは下記参照
MacでラズパイのLチカしてみたーSD 準備Swift のインストール
ラズパイに Swift をインストールします!手順は簡単です。
ラズパイに SSH 接続して下記コマンドを実行します。
# Swift-Armのインストール $ curl -s https://packagecloud.io/install/repositories/swift-arm/release/script.deb.sh | sudo bash # Swift のインストール $ sudo apt-get install swift5最初の curl コマンドで OS を調べて適切なものを色々インストールしてくれます。
インストール後に下記コマンドを実行してバージョンが表示されれば成功です。
$ swift --versionSwift version 5.1.5 (swift-5.1.5-RELEASE)
Target: armv6-unknown-linux-gnueabihf参考:Raspberry Pi で Swift を使えるようにする【Swift 5】 - Swift-Arm の入手
ついでに vim もインストールしておきます(ソース書くとき使います)。
$ sudo apt-get install vimSlack 準備
Slack に投稿にはトークンが必要なので準備します。
Slack API にアクセスし App Name と Workspace を入力し Create App をクリックする。
左側メニューの OAuth & Permisions をクリックする。
Scopes -> Bot Token Scopes に chat:wirte を追加する。
OAuth & Permisions ページの上部にある Install App to Workspace をクリックする。
インストール後下記が表示されるのでトークンをコピーする。
Slack の投稿したいチャンネルにアプリを追加する。
配線
配線は下記です(上の図を参考にどうぞ)。
- ブレッドボードの j30 にジャンバ線1のオスを挿す。
- ブレッドボードの j28 にジャンバ線2のオスを挿す。
- ブレッドボードの j23 にジャンバ線3のオスを挿す。
- 抵抗をブレッドボードの i23 と i28 に挿す。
- ボタンをブレッドボードの f30, e30, f28, e28 に挿す。
- ジャンバ線1のメスをラズパイの 3.3V に挿す。
- ジャンバ線2のメスをラズパイの GPIO9 に挿す。
- ジャンバ線3のメスをラズパイの GPIO11 横の GND に挿す。
ラズパイ準備
いよいよ Swift でコードを書いていきます。
- SSH 接続する。
下記コマンドで必要なものを準備する。
// ディレクトリ作成 $ mkdir SlackPost // ディレクトリに移動 $ cd SlackPost // プロジェクト初期化 $ swift package init --type executable // Package.swift編集 $ vim Package.swifti と入力し入力モードにする。
下記を追記する。
// 省略 dependencies: [ .package(url: "https://github.com/uraimo/SwiftyGPIO.git", from: "1.0.0") // ここ追加 // 省略 targets: [ .target(name: "SlackPost", dependencies: ["SwiftyGPIO"]), // ここ追加 // 省略下記コマンドを実行する。
$ vim Sources/SlackPost/main.swifti と入力し入力モードにする。
下記のようにソースを記述する。
import Foundation import FoundationNetworking import SwiftyGPIO extension String { var urlEncoded: String { let charset = CharacterSet.alphanumerics.union(.init(charactersIn: "!$&'()*+,;=/?-._~")) let removed = removingPercentEncoding ?? self return removed.addingPercentEncoding(withAllowedCharacters: charset) ?? removed } } func postMessage(channel: String, text: String) { let token = "調べたトークン" var request = URLRequest(url: URL(string: "https://slack.com/api/chat.postMessage")!) var components = URLComponents(url: request.url!, resolvingAgainstBaseURL: true) components?.percentEncodedQuery = "token=\(token)&channel=\(channel)&text=\(text)".urlEncoded request.url = components?.url let task = URLSession.shared.dataTask(with: request) { (data, response, error) in if let error = error { print("error: \(error.localizedDescription)") return } if let data = data, let textData = String(data: data, encoding: .utf8) { print(textData) } } task.resume() } let gpios = SwiftyGPIO.GPIOs(for: .RaspberryPi3) var gp = gpios[.P9]! gp.direction = .IN var before = 0 while true { let now = gp.value if before == 0 && now == 1 { print("Push!!!") postMessage(channel: "#random", text: "ぽんぽんぺいんで今日休みます:dizzy_face") } Thread.sleep(forTimeInterval: 0.1) before = now }起動
投稿ボタン起動!!
- ラズパイに SSH 接続する。
下記コマンドで起動する。
# ビルド $ swift build # 実行 $ swift runこれでボタンを押せば Slack に投稿できます!(終了する場合は ctrl + C)
ちょっと改良
このままだとボットが休むことを伝えることになります。休む場合は自分で伝えるべきでしょう!
Slack API の OAuth & Permissions ページにアクセスし Scopes の User Token Scopes に chat:write を追加し App の reinstall を行います。
下記のように OAuth Access Token が発行されるのでこのトークンを使うようにします。
起動!!
無事自分の口で休むことを伝えることができるようになりました
おわりに
これで朝起きたらボタン一つで休めるようになりました
私が言いたかったのは Swift でできるのは iOS, Mac アプリを作ることだけじゃない!!!ということです
参考