t__nabe_log

雑多 作業、学習ログ多め。

Swift実践入門を再読する

iOS開発を初めた当初に一度Swift実践入門を一気に通読してから業務でアプリ開発に入った。それまで半年ほどRailsでWebアプリケーションの開発をしていきなりのモバイルアプリ開発なのでかなり苦労した。それでも少しずつ開発を勧めてやっとこの前ストアにリリースできた。社内で使ってもらう状態にできて新しいデザインと新機能の追加の要望が来たので、宣伝込みの正式なリリースへ向けたアップデートを現在進めている。その過程でSwiftらしいコードを書いて少しでも楽に機能追加やリファクタリングを行いながらSwiftの言語仕様についてきちんと理解したいと考えて以前読んだSwift実践入門を再読することにした。この記事をそのログとして残そうと思う。基本的に自分がうろ覚えになっていた所を残す。

また、記載されているコードはいずれも本とは異なるコードです。 blog.jnito.com を読んで大いに納得したのが理由だが、ブログを書くときにコードも考えているため書籍に紹介されているコードと比べて意味が理解しづらいものも多いかもしれない。playgroundで動作確認済み。

コードの実行環境

Swift4.2

Xcode Version 10.1

制御構文

caseのwhereによる条件指定

caseのwhere。case文、主にenumに使ってwhere句を使った柔軟な分岐をそもそもしなくて済むようにしたくて使っていなかったが、UILabelのtextプロパティに入れる文字列を投稿時間で分けるDate型のextensionを実装する時に使った。

let score = (70, 60)
switch score {
case (let english, let math) where english == 100 && math == 100:
    print("満点")
case (let english, let math) where english >= 70 && math >= 70:
    print("両方70点以上")
case (let english, _) where english >= 70:
    print("英語は70点以上")
default:
    print("それ以外")
}

for case

もっとはやく使えば良かった。where以降の条件に一致する場合、ループ内の処理を実行する。UIStackView内を実データの配列を使ってaddArrangedSubview(_:)していく時に、forやifを使っていたのを思い出してこれを読んだ後書き直した。

let mediaList: [Media] = [
    .book(title: "ドラえもん 本1", author: "藤子・F・不二雄", year: 1997),
    .book(title: "ドラえもん 本2", author: "藤子・F・不二雄", year: 1999),
    .book(title: "ドラえもん 本3", author: "藤子・F・不二雄", year: 1999),
    .movie(title: "ドラえもん 映画1", director: "藤子・F・不二雄", year: 2004),
    .movie(title: "ドラえもん 映画2", director: "藤子・F・不二雄", year: 2007),
    .website(urlString: "https://ja.wikipedia.org/wiki/%E8%97%A4%E5%AD%90%E3%83%BBF%E3%83%BB%E4%B8%8D%E4%BA%8C%E9%9B%84")
]

print("映画版列挙:")
for case let Media.movie(title, _, year) in mediaList {
    print(" - \(title) (\(year))")
}

// 映画版列挙:
//  - ドラえもん 映画1 (2004)
//  - ドラえもん 映画2 (2007)

repeat-while

通常のwhile文だとwhileの条件によっては一度{}内の処理が実行されないかもしれない。条件式の正否を問わず{}内の処理を必ず行う場合にこれを使う。

var a = 10
repeat {
    print("実行されました")
    a += 1
} while a == 10

クロージャ

クロージャの属性

両方使いどころはわかっていたが使い方をなんとなく知っていただけだった。読むまでautoClosure属性は引数をクロージャで包むという処理を暗黙的に行っているのを知らなかったので、 これをつけたら遅延評価される程度の理解だった。実際に遅延評価をのメソッドを実装してからそれを簡単に書けるautoclosure属性を使った実装に変更するとなるほどとなった。

型の構成要素

サブスクリプト

サブスクリプトを自分で定義できる。サブスクリプトオーバーロードも普段意識せずに当然のように使っていた。サブスクリプトオーバーロードの例としてArray型が挙げられていて(Intを引数に取ってElement型の要素を返すサブスクリプトとRangeを取ってArraySlice型のスライスを返すサブスクリプト)わかりやすかった。

// サブスクリプトの独自実装サンプル
struct StudentNumber {
    var numbers: [[Int]]
    subscript(classNumber: Int, studentNumber: Int) -> Int {
        get {
            return numbers[classNumber][studentNumber]
        }
        set {
            numbers[classNumber][studentNumber] = newValue
        }
    }
}

let studentNumber = StudentNumber(numbers: [
    [1, 2, 3, 4],
    [1, 2, 3, 4],
    [1, 2, 3, 4]
    ])


let number = studentNumber[1, 2]

extension

いつも使っているので真新しいことはなかったが、使い方や使う時の考え方、注意点などが言語化されているのがこの本の良いところだと思う。曖昧な理解なことに気付かされる。エクステンションでstored propertyを定義することができないがそれを解決するためのワークアラウンドはいくつも出てくるが自分は当面使いたくない。

Stored Properties In Swift Extensions - Marco Santa Dev

Swiftのextensionでstored propertyを追加する?(黒魔術は閉じ込める) • Yuta Tokoro

疑問に思ったことはextensionにstored propertyが定義できない理由。書籍にも書いてなくて自分で調べたが確実にこれだというのものは見つけられなかった。

ドキュメント

他にはextensionで既存の型にinitializerを追加することでアプリケーション固有の情報から既存の型のインスタンスを生成するなどがある。initializerを定義するだけなのでコードは貼らない。実務で開発しているアプリでAPIリクエスト投げた時のエラーメッセージを使う時にUIAlertControllerを使っているが、その時に毎回UIAlertControllerを初期化するのは面倒なのでエラー内容をEnumで受け取るconvenience initializerをextensionで定義すると毎回書かなくて済むの楽だ。

型の種類

classとstatic違いと使い分け

その値がサブクラスで変更される可能性があるかどうかで使い分ければ良さそう。staticがつくとoverride出来ないので。 書籍ではその後例としてコードを記載して終わったが、staticとfinal classはどう違うのか気になってしまった。 staticとclassのみ変更した2つのswiftファイルのSILを観て比較してもアクセスの仕方が違うだけでclass funcもstatic funcと同じ関数が用意されていた。今度はfinal funcとclass funcを比較してみると、SILレベルで同じファイルが生成された。つまりclass funcとstatic funcはどっちを使っても良いのではと思った。

自分はここで調べるのを辞めてしまったが、Qittaの記事でclass funcとfinal class funcを比較している記事がありとても勉強になった。 https://qiita.com/YutoMizutani/items/040d8af5bfc38d9f1fac#_reference-173b518dffeeba29ba18

参照型の比較

==しか使っていなかった。参照元の比較が1回で済む所を、一意なプロパティを定義して値で比較していた。

class HogeClass: Equatable {
    static func == (lhs: HogeClass, rhs: HogeClass) -> Bool {
        return true
    }
    var value: String
    init(value: String) {
        self.value = value
    }
}


let hoge1 = HogeClass(value: "hoge")
let hoge2 = HogeClass(value: "hoge")

// 値の比較
hoge1 == hoge2 // true

// 参照の比較
hoge1 === hoge2 // false

protocol

protocol composition

whereで条件指定する時に&は使っているが関数の引数で2つのprotocolにconformしていることを要求したことがあんまりなかった。大抵の場合2つのprotocolにconformしたprotocolを用意してそれを引数にしていたと思う。

protocol HogeProtocol {
    var hoge: String { get }
}

protocol FugaProtocol {
    var fuga: String { get }
}

struct HogeFuga: HogeProtocol, FugaProtocol {
    var hoge: String
    var fuga: String
}

func hogeFugaFunction(x: HogeProtocol & FugaProtocol) -> String {
    return x.hoge + x.fuga
}

let hogefuga: HogeFuga = HogeFuga(hoge: "hoge", fuga: "fuga")
hogeFugaFunction(x: hogefuga)

標準ライブラリのプロトコル

前回読んだ時も思ったが、protocolで1番おもしろいのはここだった。実際に使っている"=="などをprotocolを使ってどのように実装しているか知ることができる。サンプルコードで実際に標準ライブラリのプロトコルにconformした型を定義するので、読んで自分で書いてみながらprotocolの便利さが実感できる。Swift触ったばかりの時はRubyで覚えたOOPしか知らなかったのでprotocolを中心に実装していく利点がいまいちわからなかったのを覚えている。 

ジェネリクス

戻り値からの型推論による特殊化

いつも引数からの型推論による特殊化を行っていたので両方選択肢があった時に戻り値からの型推論による特殊化を採用するイメージがまだ湧かない。property宣言する時に明示的に型を明示したい時とかかなあ。

func getAnyValue<T>(_ any: Any) -> T? {
    return any as? T
}

let a: String? = getAnyValue("abc")
let b: Int? = getAnyValue(2)
let c = getAnyValue("hogehoge") // Generic parameter 'T' could not be inferred

型の設計指針

copy on write

var ids = [1, 2, 3]
var ids2 = ids
ids.append(4)
ids
ids2

idsをids2に代入した時点でコピーが行われているように見えるが、実際はarray1.append(4)の時。違いが生じた時。

その他構造体とクラス、プロトコルと継承の使い分けの基準についての説明等があったが、この点は実務で疑問に思って何度も調べて今の所疑問もないので割愛。

イベント通知

ここで気になったのは細かい所。

#selectorの引数として渡すmethodに@objcをつけないといけない理由

公式のdocumantationに#selectorについての記載がある。

developer.apple.com

SelectorはObjective-CではObjective-Cメソッドの名前を参照する型らしい。Swiftでは、Objective-CのSelectorはSelector strunctで表され、"#selector" 式を使用して構築できる。 "#selector"を使ってメソッドを呼び出す時、Objective-CのMethod dispatchの方式、Messageを使用したdispatchを行うため、@objcが必要になる。数日前にMethod dispatchについて調べたおかげですんなり理解できた。

書籍にもSelector型について説明がある。

セレクタObjective-Cの概念であるため、Selectorgata wo生成するにはメソッドがObjective-Cから参照可能である必要があります。

とのこと

参考

stackoverflow.com

github.com

ja.stackoverflow.com

qiita.com

エラー処理

ここで知りたかったそれぞれのエラー処理のやり方をどのように使い分けるか。そのことが丁寧に説明されている。

do-catch

Result<T, Error>との使い分けの部分勉強になった。Result<T, Error>は型引数Errorと同じ型のエラーしか扱えないのに対し、do-catchではErrorプロトコルに準拠する型であればどのような型でも扱える。それぞれエラーを予測できるResult<T, Error>と複数の種類のエラーを1ヶ所で扱えるdo-catchには明確なメリットとデメリットがあることを理解できた。とはいえ実際のコーディングの時に読んだだけで使えるわけがないのでエラー処理を書くたびにしばらく読み返すことになりそう。

アサーション

一度も使ったことなかった。テストでAssert〜って書くぐらい。

func countHoge(count: Int) -> String {
    var strings: [String] = []
    assert(count >= 5, "引数の数字は5以上に設定してください")
    for _ in 0..<count {
        strings.append("hoge")
    }
    return strings.joined()
}


print(countHoge(count: 5))   // hogehogehogehogehoge
print(countHoge(count: 4))   // Assertion failed: 引数の数字は5以上に設定してください: file ios.playground, line 7

リリース時に無効になるのは良い。たまにprint文が残ったままになっていることがあったので。