20201207のSwiftに関する記事は16件です。

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の「Analyze」とは?

かんたんにいうと「ビルドログのASTを使って解析する」機能です。

Analyzeは実験的な機能であり、いつでも変更される可能性があるとのことです。

Analyzeの使い方

Analyzeの使い方を紹介します。

設定ファイルの作成

Analyze用のルールを設定ファイルの analyzer_rules に記述します。

.swiftlint.yml
analyzer_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 に対して必要最小限にはなっていません。

Makefile
PRODUCT_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 Foundationimport UIKit が削除されて気持ちいいです。

おまけ①: 使用しているインポートが削除される場合がある

先ほど削除された import MobileCoreServices ですが、実はないとビルドエラーになります。

理由としては、iOS 13以前で kUTTypeData の呼び出しに使っているためです。

SpotlightRepository.swift
    private 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 さんが参加してくださりました!
ありがとうございます :relaxed:

参考リンク

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

【備忘録】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の個別の特性。ここも長い…

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

配置されたTextViewのテキストの量に応じてTableViewのCellの高さを動的に変更させる

初めに

xibで作ったTableViewCellを想定しています

xibでのセルの作り方はこちらを参照ください
https://qiita.com/Secret-Base36/items/2a9a3989479559ba36ba

環境

Xcode 12.2

1. TextViewの設定

  • Scrolling Enableのチェックを外す

スクリーンショット 2020-12-07 20.35.06.png

2. TableViewのセルの高さの設定

TableViewのheightForRowAtメソッドで可変に設定

  func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    //セルの推定最低高さ(不要?)
    secondTableView.estimatedRowHeight = 50
    //セルの高さを可変に設定
    return UITableView.automaticDimension
  }

estimatedRowHeight(セルの推定高さ)は記述しなくても結果は変わらなかった

結果

スクリーンショット 2020-12-07 21.28.56.png

文字量によってセルの高さが変わる様になった!

その他

上の図の様に最小高さの場合に、他のUIが重なるなど影響が出る場合は、制約で調整すると良い

スクリーンショット 2020-12-07 21.31.44.png

スクリーンショット 2020-12-07 21.39.35.png

最後に

情報が間違っているなどありましたら、お手数ですがご指摘いただけると幸いです。

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

条件分岐 ? :  の使い方

はじめに

この書き方を見た時に、こんな書き方もあるんや!って思ったが、
この書き方を調べてもわからなかった(?:と調べてもわからなかった)ので、自分なりに一旦まとめていこうと思います。
色々わかり次第、情報を追加していこうと思います。

使われ方

条件式 ? 式か値① : 式か値②
説明すると、条件式が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の時には:の後の処理が行われる。

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

クリスマス終了までをカウントダウンしたい

この記事は クソアプリ2 Advent Calendar 2020 の25日目の投稿です。
昨日は@inabajunmrさんの「ここにタイトルを入れます」でした。

はじめに

今回初めてQiita アドベントカレンダーに参加しました。
去年や一昨年のアドベントカレンダーの「クソアプリ」の参加者の方の記事を読みましたが、とても楽しそうでした。
僕も今回楽しみたいと思います。

今回作ったもの

クリスマス終了までをカウントダウンするアプリです。(スクリーンショットの画像はイメージがしやすいように別の日付を設定しています)
スクリーンショット 2020-12-06 11.22.02.png

作ろうと思った経緯

このアプリを作ろうと思った経緯は、私が初心者なので勉強した事で「クソアプリ」を作ろうと思いました。
今回ちょうど、タイマーについて学んでいました。
そこで「タイマー機能」と「プログレスバー」を使ったアプリを作ろうと思いました。

story boardにLabelなどを配置

スクリーンショット 2020-12-06 14.16.02.png

タイマーの設定と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の勉強に励みたいと思います。

twitter

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

【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 を活用する方法を書く。

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

[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関係なかったので調べるの大変でした

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

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

image.png

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

最高の振動をデザインするための下準備〜UnityからCore Haptic FrameWorkを使ってみた〜

はじめに

この記事は「サムザップ #1 Advent Calendar 2020」の12月7日の記事です。
昨日の記事は@KoniroIrisさんの「【Unity】アップデートされたTerrainでトンネルを掘ろう!」でした。

今回は、iOS13で新しく追加されたCore Haptic FrameWorkをUnityを使って、iPhoneの振動生成を試行錯誤するツールを作ったので、その紹介をします。

振動作成を試行錯誤ためのツール

振動作成するためのUnityで下記のようなツールを用意しました。
Screen Shot 2020-12-07 at 16.23.11.png
このツールでできること
①強さの調整
②鋭さの調整
③始まりと終わりの強さと鋭さを設定し、徐々に振動を変化させる
④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さんです。

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

条件分岐文について〜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 ainner: 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 2

JavaやC言語などでは、
ケースを実行した後に次のケースに移行するのはデフォルトの挙動になります。

しかし、swiftではfallthrough文によって明示しない限り次のケースに移行することはありません。

暗黙的な使用をできる限り排除しようというswiftの思想を見て取れますね!

以上、最後までご覧いただきありがとうございました。

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

[Swift] Delegateが分からない初心者のための最後の受け皿

今回作るもの

黄色のボタンを押すと画面が遷移され黄色くなる実装をdelegateを使って実装&説明します。

image.png image.png

きっかけ

Swiftのdelegateの説明をQiitaでたくさん読みましたがいまいち理解できず、なんとなく実装していました。delegateで詰まることが多かったので、アウトプットの機会にしようと思います。

対象

私のように他の記事を読んでも理解に苦しんだ方々に読んで頂きたいです。

Delegateとは

Delegateは委譲という意味ですが、そのクラスではできないことを他のクラスでやってもらうため委譲が必要になるのです。
詳しくコードと共に見ていきましょう。

実際のコード

3ファイル作成しました。
1つめははじめの画面のViewController
2つめは黄色画面遷移先のYellowViewController
3つめはViewControllerのCellのクラス(ButtonTableViewCell)

まず1つ目のViewControllerです。

スクリーンショット 0002-12-07 12.38.21.png
ここでは一枚目の黄色ボタンのViewの部分を作っています

次に黄色画面遷移先のYellowViewControllerです

スクリーンショット 0002-12-07 13.06.19.png
黄色の画面にするViewの部分を作っています

最後に先ほど作った黄色ボタンのCellのクラスです

スクリーンショット 0002-12-07 13.04.26.png
ここではcellの中のボタンタップ時の処理を行えます。
「ここに処理を書きたい」の部分に

let yellowViewController = YellowViewController()
navigationController?.pushViewController(yellowViewController, animated: true)

を入れて黄色画面に飛ばしてやりたいですが、CellではNavigationControllerが使用できないので、他の画面で処理をする必要があるのです。そこでdelegateという考え方が必要になります。

Delegateの使い方

1, protocolを宣言
2, delegateを宣言
3, 発火させたい場所にdelegate設置
4, 受け渡し先のクラスで継承
5, 受け渡し先のViewにdelegateを呼び出す
6, 処理を実装する

具体的な実装方法

スクリーンショット 0002-12-07 13.44.04.png
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を継承しているからです。
その証拠に、
スクリーンショット 0002-12-07 13.57.30.png
UITableViewDelegateを右クリックして見てみましょう。
スクリーンショット 0002-12-07 13.59.19.png
protocolが宣言されている訳です。
ViewControllerでデフォルトでtableViewが使えていたのも理由があったのですね。

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    }

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    }

最後に

最後まで読んでいただきありがとうございます。理解していただけたしょうか?
delegateはswiftを学習し始めで必ずと言っていいほど、詰まる箇所だと思います。
とても複雑に見えていましたが、根本を理解してみると簡単なものでした。

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

【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の記事タイトル、作成日、作成者の名前がログに出力されます。

参考

Qiita API v2 ドキュメント 投稿
Jsonのsnake caseを直すのにCodingKeyは必要ない

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

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 したところ、乱数生成が、スコアシミュレーション全体時間のほぼ四分の一を占めました。

ins.jpg

シミュレーション自体が 0.3s 未満とはいえ、改善の余地があれば見逃すわけにはいけませんね!!(神経質)

改善策

代わりに見つけたのはdrand48()という少し Low-Level な関数。drand48 がちょうど [0,1) 範囲の乱数を生成してくれます。

func simulatePossibility() -> Bool {
    drand48() < rate
}

注意:ここで省きましたが、drand48()を使う前に必ず srand48()によって一回初期化してください。

drand48に入れ替わっただけで、時間が先程の方法よりだいぶ縮みました:

ins2.jpg

まともに測ろう

実機ではちゃんと整理できなかったので、テスト環境で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 とその以前の方法を比べました:

ran.jpg

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
  }
}

正直ここの乱数生成のアルゴリズムを理解していませんが、とりあえずSystemRandomNumberGeneratornext()使ったので、続きます:

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に、drand48Doubleに。実際色んな場面で使うと型変換しなければなりません。
  • 違う関数の範囲の指定がバラバラで、紛らわしい。例えばarc4randomが上限しか指定できなくて、[100, 200) 範囲にしたい時、上限を100にして、その後乱数に100を足す必要があります。
  • Swift が様々なところで実行しています。違うプラットフォームでは違う乱数生成の方法があって、クロスプラットフォーム開発者に対してはかなりの苦痛でしょう。

実際、Random API に関する最初の提案では Proposal Random Unification - Discussion - Swift Forums、同じことが言われたようです。

Swift コミュニティは、これらの欠点を解消するため、型・範囲・プラットフォームに対して汎用化した乱数 API を実装し、統一したことで開発に大きな利便性が持たされました。

まとめ

この記事は Swift の乱数生成の中の仕組みを探索し、ちょっとだけ遅い原因を究明しました。

遅いとは言え、短時間で百万回実行する要件ではなく、一回二回だけでしたら全然気にすることはありません。そんな無視できる程度の性能改善より、コードの統一性と可読性が遥かに重要だと思います。

以上です。なにか間違えたことがあれば気軽にコメントお願いします!

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

[Swift] SkyWayを使ったWebRTCの映像をFirebase MLに接続して顔認識させる

前書き

とあるハッカソンで使った技術で、実装するにあたり壁にぶつかりました。
おそらく前例がないので、備忘録として残します。

技術に関して

1. SkyWayとは

スクリーンショット 2020-12-07 3.46.42.png

ほとんど画像に記載がありますが、Webでリアルタイムコミュニケーションを実現する標準技術「WebRTC(Web Real Time Communication)」を簡単にアプリに導入できるSDK&APIです。(公式引用

2. Firebase MLとは

maxresdefault.jpg

ML Kit は、Google の機械学習の機能を Android アプリや iOS アプリとして提供するモバイル SDK です。(公式引用)

今回はこの部分の中身についての実装は話しません。
あくまでもSkyWayとFirebaseを繋ぐ部分のみになります。
(中身の参考資料「Firebase MLKitを使って動画の顔認識をさせる」)

3. 構成図

アプリ間で動画のリアルタイム通信を行いながら、その映像をFirebase MLで判定させるということ行います。

スクリーンショット 2020-12-07 3.44.07.png

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を持つレイヤー層を探り当てました。

イメージ図
0v2Bh.png

探り当てた部分をコードで取り出す場合は、以下のようになります。

// レンダリング画面
@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にアクセスできるので、outputsetSampleBufferDelegateセットしてあげることで、CMSampleBuffer型の値を手に入れることができるようになります。

これでFirebase MLへの接続も可能となりました。

あとがき

複数のストリームで受け取って加工する、など色々試した結果、この方法が最適なかつ実現可能な方法でした。

少しマニアックなネタだったかもしれませんが、誰かの参考になれば嬉しい所存です。

文献

SkyWayの実装周りは解説しなかったので、以下を参考にすると良いでしょう。
- SkyWay - アプリやWebサービスに、ビデオ・音声通話をかんたん導入
- iOSのCallKitフレームワークとSkyWay
- SKYWAY SDKを使ったIOS版通話アプリ開発

Firebase周りはこちらをご覧ください、
- Firebase MLKitを使って動画の顔認識をさせる

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

[Swift] SkyWayを使ったWebRTCの映像をFirebase MLに接続する

前書き

とあるハッカソンで使った技術で、実装するにあたり壁にぶつかりました。
おそらく前例がないので、備忘録として残します。

技術に関して

1. SkyWayとは

スクリーンショット 2020-12-07 3.46.42.png

ほとんど画像に記載がありますが、Webでリアルタイムコミュニケーションを実現する標準技術「WebRTC(Web Real Time Communication)」を簡単にアプリに導入できるSDK&APIです。(公式引用

2. Firebase MLとは

maxresdefault.jpg

ML Kit は、Google の機械学習の機能を Android アプリや iOS アプリとして提供するモバイル SDK です。(公式引用)

今回は映像部分の顔判定をになっていますが、この部分の中身についての実装は話しません。
あくまでもSkyWayとFirebaseを繋ぐ部分のみになります。
(中身の参考資料「Firebase MLKitを使って動画の顔認識をさせる」)

3. 構成図

アプリ間で動画のリアルタイム通信を行いながら、その映像をFirebase MLで顔認識させるということ行います。

スクリーンショット 2020-12-07 3.44.07.png

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を持つレイヤー層を探り当てました。

イメージ図
0v2Bh.png

探り当てた部分をコードで取り出す場合は、以下のようになります。

// レンダリング画面
@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にアクセスできるので、outputsetSampleBufferDelegateセットしてあげることで、CMSampleBuffer型の値を手に入れることができるようになります。

output?.setSampleBufferDelegate(self, queue: /* Queue */)

これでFirebase MLへの接続も可能となりました。

あとがき

複数のストリームで受け取って加工する、など色々試した結果、この方法が最適なかつ実現可能な方法でした。

少しマニアックなネタだったかもしれませんが、誰かの参考になれば嬉しい所存です。

文献

SkyWayの実装周りは解説しなかったので、以下を参考にすると良いでしょう。
- SkyWay - アプリやWebサービスに、ビデオ・音声通話をかんたん導入
- iOSのCallKitフレームワークとSkyWay
- SKYWAY SDKを使ったIOS版通話アプリ開発

Firebase周りはこちらをご覧ください
- Firebase MLKitを使って動画の顔認識をさせる

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

ラズパイとSwiftでSlackに投稿ボタンをつくろう!

Raspbian-10 Swift-5.1.5 SwiftyGPIO-1.3.1

はじめに

最近ラズパイをさわり始めました。せっかくなので Swift とラズパイで Slack で今日は休む旨を伝えるボタンを作りたいと思います!

こんな感じです。

bot_user

必要なもの

今回必要なものは下記です。

  • 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 --version

Swift 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 vim

Slack 準備

Slack に投稿にはトークンが必要なので準備します。

  1. Slack API にアクセスし App Name と Workspace を入力し Create App をクリックする。

    create_app

  2. 左側メニューの OAuth & Permisions をクリックする。

  3. Scopes -> Bot Token Scopes に chat:wirte を追加する。

  4. OAuth & Permisions ページの上部にある Install App to Workspace をクリックする。

  5. インストール後下記が表示されるのでトークンをコピーする。

    token

  6. Slack の投稿したいチャンネルにアプリを追加する。

    add_app

配線

GPIO

公式ドキュメントから引用

配線は下記です(上の図を参考にどうぞ)。

  1. ブレッドボードの j30 にジャンバ線1のオスを挿す。
  2. ブレッドボードの j28 にジャンバ線2のオスを挿す。
  3. ブレッドボードの j23 にジャンバ線3のオスを挿す。
  4. 抵抗をブレッドボードの i23 と i28 に挿す。
  5. ボタンをブレッドボードの f30, e30, f28, e28 に挿す。
  6. ジャンバ線1のメスをラズパイの 3.3V に挿す。
  7. ジャンバ線2のメスをラズパイの GPIO9 に挿す。
  8. ジャンバ線3のメスをラズパイの GPIO11 横の GND に挿す。

ラズパイ準備

いよいよ Swift でコードを書いていきます。

  1. SSH 接続する。
  2. 下記コマンドで必要なものを準備する。

    // ディレクトリ作成
    $ mkdir SlackPost
    
    // ディレクトリに移動
    $ cd SlackPost
    
    // プロジェクト初期化
    $ swift package init --type executable
    
    // Package.swift編集
    $ vim Package.swift
    
  3. i と入力し入力モードにする。

  4. 下記を追記する。

    // 省略
    dependencies: [
        .package(url: "https://github.com/uraimo/SwiftyGPIO.git", from: "1.0.0") // ここ追加
    // 省略
    targets: [
        .target(name: "SlackPost", dependencies: ["SwiftyGPIO"]), // ここ追加
    // 省略
    
  5. 下記コマンドを実行する。

    $ vim Sources/SlackPost/main.swift
    
  6. i と入力し入力モードにする。

  7. 下記のようにソースを記述する。

    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
    }
    

起動

投稿ボタン起動!!

  1. ラズパイに SSH 接続する。
  2. 下記コマンドで起動する。

    # ビルド
    $ swift build
    
    # 実行
    $ swift run
    

これでボタンを押せば Slack に投稿できます!(終了する場合は ctrl + C)

bot

ちょっと改良

このままだとボットが休むことを伝えることになります。休む場合は自分で伝えるべきでしょう!

Slack API の OAuth & Permissions ページにアクセスし Scopes の User Token Scopes に chat:write を追加し App の reinstall を行います。

scope

下記のように OAuth Access Token が発行されるのでこのトークンを使うようにします。

token

起動!!

bot_user

無事自分の口で休むことを伝えることができるようになりました:clap:

おわりに

これで朝起きたらボタン一つで休めるようになりました:relaxed:

私が言いたかったのは Swift でできるのは iOS, Mac アプリを作ることだけじゃない!!!ということです:sunglasses:

参考

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