muapps

iOSアプリ開発で得られた知見をメモ代わりに投稿します。

SwiftLint導入

前提

環境:
M1 Mac
Xcode13.2.1
SwiftLint0.46.3

インストール

SwiftLintのreadme
https://zgithub.com/realm/SwiftLint

まずMintでインストールしようとしたが以下のエラーが出た。
warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint

上記はBuild Phasesに記載したコードのelseに入った結果の警告文。
解決には以下が何かしら参考になるかもしれない。

今回はSwiftLintを試すことが目的なのでHomebrewでのインストールに変更した。

運用方針

SwiftLintは初めからデフォルトルールが有効になっている。

  • disabled_rules:
    デフォルトで有効になっているルールの中で無効にするものを指定する
  • opt_in_rules:
    デフォルトで有効になっていないルールで有効にしたいものを指定する

まずデフォルトで実行して必要ないものを無効にする、次にopt_inを眺めて有効にしたいものを見つけるとかがいいのかなと思った。

関連リンク

Clousure - クロージャ

公式ドキュメントを読んでクロージャの整理。

docs.swift.org

クロージャはコード内で受け渡しできる自己完結型のブロック。
クロージャは定数および変数への参照をそれらが定義されているコンテキストからキャプチャして保存できる。

グローバル関数とネストされた関数はクロージャの特殊なケース。 クロージャの形式は以下の3つ。

クロージャ

外からクロージャを渡す場合

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

クロージャ式の構文の一般的な形式

{ (parameters) -> return type in
    statements
}

一般的なクロージャ式の構文そのままで書くと以下のように書ける。

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

この時Swiftは引数の型と戻り値の型を推測できるので以下のように書ける。

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

Swiftはインラインクロージャに短縮引数名を自動的に提供する。 inキーワードも省略できる。

reversedNames = names.sorted(by: { $0 > $1 } )

関数の末尾のクロージャ(トレーリングクロージャ)

関数の最後の引数としてクロージャ式を関数に渡す必要があるとき末尾のクロージャとして記述できる。末尾のクロージャ構文を使用する場合引数ラベルは記述しない。

sortedメソッドを末尾のクロージャを使って書くと

reversedNames = names.sorted() { $0 > $1 }

クロージャ式が関数またはメソッドの唯一の引数で末尾のクロージャとして書く場合関数またはメソッドの名前の後に括弧のペアは記述しなくていい。

reversedNames = names.sorted { $0 > $1 }

末尾のクロージャは、クロージャが十分に長く、1行にインラインで書き込むことができない場合に最も役に立つ。

関数が複数のクロージャを取る場合

関数を呼び出すとき最初の末尾のクロージャの引数ラベルを省略し、残りの末尾のクロージャにラベルを付ける。

定義

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

呼び出し

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

値をキャプチャする

クロージャは自身が定義されている周囲のコンテキストから定数と変数をキャプチャできる。定数と変数を定義した元のスコープが存在しなくなった場合でも、ボディからこれらの定数と変数の値を参照および変更できる。

ネストされた関数の場合

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

incrementer()はrunningTotalとamountを周囲からキャプチャする。

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

参照によるキャプチャはrunningTotalとamountがmakeIncrementerの呼び出しが終了した時消えないこととrunningTotalが次にincrementerが呼ばれたときに使用可能なことを保証する。

次のincrementByTenはincrementer関数を参照する。incrementer関数は呼び出される度にrunningTotalに10を足す。

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

2つ目のincrementerを作ると先述のものとは別の新しいrunningTotalへの参照を持つ。

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

クロージャエスケープする

関数の引数として渡されたクロージャが関数が返された後に呼ばれるときクロージャが関数をエスケープすると言う。エスケーピングクロージャがselfをキャプチャするときは明示的にselfを記載するかキャプチャリストに含める必要がある。selfを明示的に記載することで意図を表現でき参照サイクルがないことを確認するように促される。

// エスケーピングクロージャ
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

// 通常のクロージャ
func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 } // xを明示的にキャプチャする
        someFunctionWithNonescapingClosure { x = 200 } // xを暗黙的にキャプチャする
    }
}
キャプチャリストにselfを含める場合の書き方
// someFunctionWithEscapingClosure { [self] in x = 100 }

※オプショナル型はデフォルトで@escapingになる Optional Non-Escaping Closures – Ole Begemann

構造体・列挙体の場合

selfが構造体または列挙体のインスタンスである時はselfを常に暗黙的に参照できる。ただしこの時エスケーピングクロージャはselfへのmutableな参照をキャプチャできない。構造体と列挙体は共有された可変性を許可していない。

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

上記でsomeFunctionWithEscapingClosureはmutationgメソッドの中にありselfはmutable なのでエラーになる。

オートクロージャ

オートクロージャとは関数の引数として渡される式をラップするために自動的に作成されるクロージャのこと。引数をとらず、呼び出されるとその中にラップされている式の値を返す。

オートクロージャを使用すると引数として関数を取る時に{}を使ってクロージャとして書かずに通常の引数のように書ける。

@autoclosureを使わずに書く場合

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

@autoclosureを使って書く場合

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

オートクロージャを使用すると、クロージャを呼び出すまで内部のコードが実行されないため評価を遅らせることができる。評価の遅延はコードがいつ評価されるかを制御できるため、副作用があるコードや計算コストが高いコードに役立つ。

エスケープを許可するオートクロージャが必要な場合は@autoclosure@escaping属性の両方を使用する。

Dispatchqueueの挙動整理

以下の記事を参考にDispatchqueueの種類ごとにPlaygroundで実行してみた

dev.classmethod.jp

実行環境:
Xcode13.0
Swift5.2

メインキュー

sync(同期)

// メインキューは直列(前のタスクが完了次第、次のタスクが実行される)
let mainQueue = DispatchQueue.main
// syncを使うと呼び出し元がブロックされる
print("start")
for i in 0...5 {
    mainQueue.sync() {
        print(i)
    }
}
print("end")

実行結果

error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation.

async(非同期)

// 非同期なので呼び出し元がブロックされずstart、endが最初に出力される
// 直列なので番号は順番に出力される
print("start")
for i in 0...5 {
    mainQueue.async() {
        sleep(1)
        print(i)
    }
}
print("end")

実行結果

start
end
0
1
2
3
4
5

グローバルキュー

sync(同期)

// グローバルキューは並列(前のタスクの処理状況に関わらず、次のタスクが実行される)
let grobalQueue = DispatchQueue.global(qos: .default)

// 同期なので呼び出し元がブロックされてendが最後に出力される
// 並列だが同期なので番号は順番に出力される
print("start")
for i in 0...5 {
    grobalQueue.sync() {
        sleep(1)
        print(i)
    }
}
print("end")

実行結果

start
0
1
2
3
4
5
end

async(非同期)

// 非同期なので呼び出し元がブロックされずstart、endが最初に出力される
// 並列なので番号が出力される順番はバラバラ
print("start")
for i in 0...5 {
    grobalQueue.async() {
        sleep(1)
        print(i)
    }
}
print("end")

実行結果

start
end
4
2
3
0
1
5

自分でDispatchQueueを用意する場合

// 直列
let serialQueue = DispatchQueue(label: "serialQueue")
// 並列
let parallelQueue = DispatchQueue(label: "parallelQueue", attributes: .concurrent)

同期および非同期での実行結果はメインキュー、グローバルキューの結果から推察できる通り。
メインキューではエラーになったが直列で同期の場合は以下のようになる。

print("start")
for i in 0...5 {
    serialQueue.sync() {
        sleep(1)
        print(i)
    }
}
print("end")

実行結果

start
0
1
2
3
4
5
end

リーダブルコードを読んでほしい

PRレビューの度に一々この本に書いてあることを指摘するのはとてもつらい。本の内容を要約してそれに沿って書いてもらうようにお願いするという手もあるが、相手が納得してくれなかった場合など結局本の内容から補足する形になりそう。なのでやはり原著を読んでもらうのが一番確実。

読んでもらえるかは相手次第だが。。

自分だけが読みやすいコードを書いているという状況はとてもつらい。他の人がコードを書くたびに読みづらいコードが入ってくる。掃除しているそばからゴミを捨てられているような感覚だ。
- リーダブルコード(日本語版) 解説より

www.oreilly.co.jp

Androidアプリ開発時に便利なツール

前提

端末: Mac

ツール

Android File Transfer

Mac-Android間のファイル転送。AndroidのUSB接続が「充電」になっている場合「ファイル転送」をONにする必要あり

www.android.com

Android Tool

Androidのスクショや画面収録

github.com

Derived Data削除を試そう

直近で以下のようなことがあった。

  • よく分からないエラーがあって結局Derived Data削除したら直った。。

  • 当時原因分からなかったけどあとから考えればDerived Dataが原因だったかも。。

特に問題起きることなかったからClean Build Folderくらいしかしなくなってたけど、特に原因わかりづらいエラー出たらちゃんとDerived Data削除も試そうと思った(自戒

(削除手順は以下参照)

[Xcode][小ネタ] DerivedDataの削除についての備忘録 | DevelopersIO

レビュアー指名されたPRを確認する

複数アプリを担当したり開発メンバーが増えてくるとレビューしなければいけないPRも増えレビュー状況把握がコストになってきたので簡単にチェックできる方法を調べた。

qiita.com

上記記事にGitHubの検索修飾子が書いてあるのでそれを使用すればいいが、毎回検索するのは手間なので検索後のページをブックマークして管理することにした。

GitHubの検索についてはこちら。

docs.github.com