Open7

FlutterエンジニアのSwift勉強1 ~Swift6編~

heyhey1028heyhey1028

アプローチ

  1. 言語仕様を知る
  2. swiftでのアプリ開発の全体像を知る
  3. UI構築を知る
  4. ライブラリを知る

バージョン

現在、最新版のSwiftは5系

Swiftのバージョン Xcodeのバージョン リリース日
5.10 Xcode 15.3 2024年3月5日
5.9 Xcode 15 2023年9月18日
5.8.1 Xcode 14.3.1 2023年6月1日
5.8 Xcode 14.3 2023年3月30日
5.7 Xcode 14 2022年9月12日
5.6 Xcode 13.3 2022年3月14日
5.5 Xcode 13.0 2021年9月20日
5.4 Xcode 12.5 2021年4月26日
5.3 Xcode 12.0 2020年9月16日
5.2 Xcode 11.4 2020年3月24日
5.1 Xcode 11.2 2019年9月20日
5.0 Xcode 10.2 2019年3月25日
4.2 Xcode 10.0 2018年9月17日
4.1 Xcode 9.3 2018年3月29日
4.0 Xcode 9.0 2017年9月19日
3.1 Xcode 8.3 2017年3月27日
3.0 Xcode 8.0 2016年9月13日
2.2 Xcode 7.3 2016年3月21日
2.1 Xcode 7.1 2015年10月20日
2.0 Xcode 7.0 2015年9月21日
1.2 Xcode 6.3 2015年4月8日
1.1 Xcode 6.1 2014年10月22日
1.0 Xcode 6.0.1 2014年9月9日
heyhey1028heyhey1028

言語仕様を知る

https://qiita.com/cabbage/items/6993b1a9280a5ab854d8

  • 変数定義
  • 関数の定義
  • オプショナル型
  • クラスの定義
  • 制御構文と例外処理
  • 配列操作と辞書操作

変数定義

Swiftの変数定義は以下のルールに則ります。
修飾子 変数名:データ型 = 代入する値
また変数はvar, 定数はletの修飾子を付けます。
末尾の;(セミコロン)などは不要です。

var hoge: Int = 3
let fuga: String = "This is fuga"

また以下のように型推論を利用することも出来ます。

var hoge = 3

データ型

type content example
String 文字列 var hoge: String = "this is hoge"
Int 整数値 var fuga: Int = 12
Double 実数 let mumu: Double = 12.0
Bool 真偽値 let moge: Bool = false
Array 配列 var fumu: [Int] = [0, 1, 5, 19]
Dictionary 辞書 var fuwa:[String:Int] = ["apple": 1, "orange": 5]
nil 何もない値 nil

空の配列、空の辞書

初期値を持たない空の配列と辞書は以下のように定義します

// 型を明示して空の配列を定義
var anotherEmptyArray: [Int] = []

// 型を明示して空の辞書を定義
var anotherEmptyDictionary: [String: String] = [:]

[データ型]()[データ型:データ型]()で型推論を用いた空配列、空辞書の定義も可能です

// 型推論を使って空の配列を定義
var emptyArray = [String]()

// 型推論を使って空の辞書を定義
var emptyDictionary = [String: Int]()

タプル(tuple)

reference

https://qiita.com/funacchi/items/329143aa3983a5f56ec4

heyhey1028heyhey1028

配列操作と辞書操作

配列では配列名[n]で格納した値にアクセスする事が出来ます。

// 参照
print(integerArray[0]) // 0
print(stringArray[2]) // バナナ

// 更新
stringArray[0] = "パイナップル"
print(stringArray[0]) // パイナップル

ただし定数で宣言された配列は中身を更新する事はできません。

var hogeArray: [Int] = [1,3,5]
print(hogeArray[2]) // 5
hogeArray[2] = 8 // 更新OK
print(hogeArray[2]) // 8

let fugaArray: [String] = ["バナナ","りんご","ぶどう"]
fugaArray[1] = "もも" // 更新NG
heyhey1028heyhey1028

オプショナル型

オプショナル型はその変数に値が存在するかもしれないし、存在しないかもしれない変数を表すのに用いられるデータ型の表現方法です。

定義方法はデータ型の後ろに?を付けるだけです。値が存在しない場合はnilが格納されています。

var optionalString: String? = "Hello, World"
var optionalInt: Int? = nil

オプショナル型の変数はそのままだとオプショナル型である為、値が格納されていてもそのまま使うことが出来ません。例えば以下の様にInt型が格納されているオプショナル型の変数をInt型として直接加工する事が出来ません。

var a: Int  = 10 // 非オプショナル型
var b: Int? = 10 // オプショナル型
a + b
// => エラーが起きる

その為、オプショナル型の変数から中の値を取り出す処理が必要になります。この処理を「アンラップ」と呼びます。

値のアンラップ

アンラップにはいくつかの方法があります。

強制アンラップ
変数の後に!を用いる事で強制的にアンラップする事ができます。この際、値がnilの場合はランタイムエラーとなります。

print(optionString!)

オプショナルバインディング
オプショナル型の中身を確認し、その値に応じた処理を定義する事ができる変数代入です。

if let unwrappedString = optionalString{
    print(unwrappedString)
} else {
    print("optonalString is nil")
}

上記ではoptionalStringに値が存在する場合、unwrappedStringに値を代入し、その上で代入された値をプリントアウトします。optionalStringnilの場合はunwrappedStringに値は代入されず、else内の処理が実行されるのみとなります。

オプショナルチェイニング
オプショナル型が持つプロパティやメソッドに安全にアクセスする為の仕組みです。クラスのフィールドがオプショナルの場合に特に有効です。

下記のようにプロパティとメソッドを持つクラスを定義したとする。

class Residence {
    var numberOfRooms: Int
    init(numberOfRooms: Int) {
        self.numberOfRooms = numberOfRooms
    }
}

class Person {
    var residence: Residence?
}

それらのクラスを代入した変数が持つプロパティやメソッドがオプショナルの場合、末尾に?を付け、オプショナルチェイニングでアンラップします。

let john = Person()

// john.residence が nil の場合、この式全体が nil になります
if let roomCount = john.residence?.numberOfRooms {
    print("Johnの家には\(roomCount)部屋があります")
} else {
    print("Johnは住んでいる場所がありません")
}

// john.residence に Residence オブジェクトを設定
john.residence = Residence(numberOfRooms: 3)

// john.residence が nil でないため、numberOfRooms にアクセスでき、値がアンラップされます
if let roomCount = john.residence?.numberOfRooms {
    print("Johnの家には\(roomCount)部屋があります")
} else {
    print("Johnは住んでいる場所がありません")
}

オプショナルバインディングやオプショナルチェイニングでは値がnilだった場合、そのブロックがスキップされるだけで後続の処理が実行されます。対してそのアンラップ処理を含む関数やメソッド全体を早期に中断させるアンラップ方法が次に紹介するguardです。

Guard
上述の通り、値がnilの場合にそのアンラップ処理が含まれる関数やメソッドを早期リターンしてくれるのがguardです。以下ではoptionalStringnilの場合、else内の処理が実行され、process関数が中断されます。

func process(optionalString: String?){
    guard let unwrappedString = optionalString else{
        print("optionalStringはnilです")
        return
    }

 print("アンラップされた値: \(unwrappedString)")
    // ここにアンラップされた値を使用した処理を記述
    print("このメッセージはアンラップされた値が存在する場合のみ表示されます")
}

guardのelse内では必ずスコープを抜ける為の制御文(return, break, continue, throwのいずれか)が必要となります。それらの制御文がない場合、guardはコンパイルエラーとなります。

暗黙的アンラップ型(Implicitly Unwrapped Optional)

オプショナル型に対し、アクセスする際に強制的にアンラップされた値を返すようにする事が可能です。この型を「暗黙的アンラップ型」と呼びます。

記述方法は以下のようにデータ型の後ろに!を記述するだけです。

var foo: String! = "Hello"
print(foo.lowercaseString) // hello

要はオプショナル型にアクセスする際に!を付けて強制アンラップしアクセスするのと等価です。その為、オプショナルの強制アンラップと同じく、値がnilの場合、クラッシュします。

var foo: String? = "Hello"
print(foo!.lowercaseString) // hello

Reference

https://qiita.com/maiki055/items/b24378a3707bd35a31a8
https://qiita.com/m_kt/items/e30f901098b8f3bd0c46#guard-letを用いたアンラップ

heyhey1028heyhey1028

関数定義

以下のフォーマットで定義可能

 func 関数名(引数名: 型) -> 戻り値の型{
     実行する処理
 }

以下の例ではInt型の引数を受け取る関数を定義しています。

func squareNum(int: Int) -> Int{
    return int*int
}
squareNum(2) // 4

引数

外部引数名

また関数外部に対して引数名を定義したい場合は以下の通り、引数名の前に外部引数名を記述。複数同じ型の引数がある場合などに有効です。

func squareNum(input int:Int){
    ...
}
squareNum(input: 10) // 100

デフォルト引数

引数を渡さなかった場合に使用されるデフォルト値を設定する事も可能です。

func squareNum(int: Int = 8){
    ...
}
squareNum(2) // 4
squareNum() // 64

変数への代入

Swiftで関数はDart同様にファーストクラスのオブジェクトの為、変数に代入したり、引数として渡したり、戻り値として受け取る事が可能です。

func greet(name: String)-> String{
    return "Hi, \(name)!"
}

// Stringの引数を取り、Stringの戻り値を持つ関数型である変数に代入
let greeter: (String)->String = greet

let message = greet("James")
print(message) // Hi, James!

一度しか定義しない場合、関数として定義するのは冗長です。そういった場合は無名関数である「クロージャ」を使いましょう。

クロージャ

「{}」で囲まれた何らかの処理を持つブロックのことを指す。特徴としては定数や変数に代入することが可能な無名関数です。inの後に実行する処理を記述します。

let greet = { (name: String) -> String in
    return "Hello、\(name)!"
}

greet("John") // output: Hello, John!

また様々な省略記法が存在します。

1. returnの省略

2. 戻り値の型の省略

3. 引数の()と型の省略

4. 引数自体の省略

Reference

https://swift.codelly.dev/guide/関数/#関数の書き方
https://blog.code-candy.com/swift_closure_basic/

heyhey1028heyhey1028

非同期処理

DartではFuture関数を用いる事で比較的簡単に非同期処理を定義する事が出来ますが、Swiftでは様々な非同期処理の実現方法があります。代表的な実現方法として以下のような方法があります。

  • GCD(Grand Central Dispatch): 低レベルで非常に効率的、単純な非同期タスクに最適。
  • OperationQueue: 高レベルで複雑な依存関係やタスク管理が必要な場合に最適。
  • Thread: 低レベルで直接スレッドを制御したい場合に適しているが、一般的には推奨されない。
  • Swift Concurrency (async/await): 高レベルで可読性が高く、非同期処理の標準的な方法として推奨される。
  • Combine: リアクティブプログラミングに最適、データのストリーム処理に強力。

ここではこの中でも特に代表的なGDC、OperationQueue、Swift Concurrency、Combineを解説します。詳しくは参考にした記事が非常に分かりやすく且つ網羅的なのでそちらをお読みください。

GCD (Grand Central Dispatch)

DispatchQueueクラスを使用して、非同期タスクをキューに追加し、バックグラウンドで実行します。スレッド管理を全く意識せずに非同期処理を実現できることが特徴です。

タスクをキューに追加しておくと、スレッドプールという仕組みによって処理が随時実行されます。開発者はスレッドプール自体は全く操作する事なく、管理はシステムが自動で行い、CPUのコア数や負荷の状況を考慮して自動で最適化を行なってくれます。

◆DispatchQueueクラス

上記でキューとして紹介されていたのがDispatchQueueです。開発者はこのクラスに対してタスクを追加していきます。

⚫︎どのDispatchQueueを使うか決める

DispatchQueueにはMain, Global, Privateの3分類があり、どのようにタスクを実行して欲しいかに応じて選択します。

分類 個数 生成方法 タスク引渡し 概要
Main 1つ OSがデフォルトで用意 直列式 ・DispatchQueue.mainで呼び出す
・UIの更新はこのキューで行われなければならない
Global 5つ OSがデフォルトで用意 並列式 ・DispatchQueue.global(qos:)で呼び出す
・QoSの数だけ存在し、QoSで優先順位を定義できる
Private いくらでも ユーザーが生成 ユーザーが指定 ・カスタマイズしたキューを定義し、インスタンス化する
・QoSやタスク引渡し方式などをカスタマイズ可能

⚫︎優先度を決める:QoS (Quality as Service)

上記で出てきたGlobalのDispatchQueueQoSという優先度を定義する仕組みを備えています。QoSには以下の種類があり、この数だけGlobalのキューが存在し、開発者はタスクの性質に応じて、QoSを選択し、優先度を定義します。

分類 概要 優先順位
userInteractive ユーザーからの入力に対して即座に反映されるべき処理 アニメーションの実行など 1
userInitiate ユーザーの入力を受けて実行される一般的な処理 ボタンタップ時の内部処理など 2
default userInitiateとutilityの間の優先度 (デフォルト値の為、ユーザーによって明示的に指定されるべきではない) 3
utility 視覚的な情報更新を伴いながらも、即時の結果を要求しない処理 プログレスバー付きのダウンロードなど 4
background バックグラウンドで行われ、数分もしくは数時間かかても支障のない処理 データバックアップなど 5

⚫︎実用例

main.async
Mainキューに非同期処理を追加する場合は以下の通り

DispatchQueue.main.async {
    // メインスレッドで実行するタスク
    // 例: UIの更新
    self.label.text = "Hello, World!"
}

global(qos: xxx).async
Main同様、asyncで非同期処理をキューに追加します。以下では.userInteractiveのキューにタスクを追加しています。

// グローバルキューでのタスク
DispatchQueue.global(qos: .userInteractive).async {
    print("Global queue task")
}

global().async(flags: .barrier)
通常Globalキューは並列処理を行いますが、asyncメソッドに対して.barrierオプションを付ける事で例外的に直列処理を行うことも可能です。

DispatchQueue.global(qos: .userInitiate).async(flags: .barrier){
    // some tasks
}

この他にもGlobalキューには.asyncAfter().suspend().resume()など様々なメソッドが用意されています。

◆DispatchGroupクラス

DispatchGroupクラスを用いて、複数の非同期タスクをグループ化して、すべてのタスクが完了したことを検出することも可能です。

使い方としては、

  • DispatchGroupをインスタンス化
  • .enter()でグループにタスクを追加
  • .leave()でグループからタスクを削除
  • .notify()でDispatchGroupの全ての処理の完了をトリガーにして処理を実行
let dispatchGroup = DispatchGroup()

dispatchGroup.enter()
DispatchQueue.global.async{
    performTaskA()
    dispatchGroup.leave()
}

dispatchGroup.enter()
DispatchQueue.global.async{
    performTaskB()
    dispatchGroup.leave()
}

dispatchGroup.notify(queue: .main){
    print("All tasks are done")
    updateUI()
}

Operation, OperationQueue

内部ではGCDを用いつつ、より複雑なユースケースに対応する為の扱いやすいインターフェースを提供しているのがOperation, OperationQueueです。タスクをOperationクラスのサブクラスとして定義し、OperationQueueに追加していくことで非同期処理を実現します。

◆Operationを用いたタスク定義

前述の通り、タスクはOperationクラスのサブクラスとして定義します。

class MultiplyOperation: Operation {
    let initialValue: Int

    init(value: Int){
        self.initialValue = value
    }

    override func main(){
        print(initialValue)
    }
}

Operationクラスを継承する際にオーバーライド必須はmainメソッドのみです。mainメソッドはタスクがキューに追加された際に呼び出される処理です。

その他mainメソッド以外にも数多くのクラスメソッド、プロパティが存在し、オーバーライドすることが可能です。以下に代表的なクラスメソッド、プロパティを記述します。

  1. startメソッド
    • タスク開始時に実行する処理
    • 内部的にはstartメソッドがmainメソッドを呼び出す為、mainメソッド実行前に実行したい処理を定義する
  2. cancelメソッド
    • タスクがキャンセルされた際に実行される処理
  3. isReadyプロパティ
  4. isExecutingプロパティ
  5. isFinishedプロパティ
  6. completionBlockプロパティ
  7. addDependencyメソッド
  8. dependenciesプロパティ

◆OperationQueueへのタスク追加

GCDに対してこちらの手法が持つ優位性は「タスク間の依存関係を簡単に記述できる事」と「キューに追加したタスクを簡単にキャンセルできる事」などにあります。

Swift Concurrency

Combine

DartのStreamに相当。

Reference

https://k0mach1.hatenablog.com/entry/2023/12/11/180153#Swift-Concurrencyを用いた非同期処理
https://qiita.com/shtnkgm/items/d9b78365a12b08d5bde1

heyhey1028heyhey1028

クラスと構造体

Dartのクラスに相当するものとして、swiftではクラス(class)構造体(struct)という2つのデータ型が用意されています。これらの違いには値型参照型という異なるデータ型である事が挙げられます。

値型、参照型

値型と参照型の大きな違いはメモリ領域の確保方法にあります。直接値を持つ型を値型と呼び、値ではなく値への参照を持つ型を参照型と呼びます。クラスと構造体では、クラスが参照型構造体が値型のデータとなっています。

この違いが最も顕著に出るのが、インスタンスを変数に代入した際です。

値型では変数に代入した際、そのインスタンスのコピーが生成され、変数に代入されます。その時点で元のインスタンスとは切り離されるため、変数に格納したインスタンスを更新しても、元のインスタンスには影響を与えません。

struct Person {
  var name: String
  var age: Int
}

var person1 = Person(name: "Kazuki", age: 36)
var person2 = person1

person2.age = 50

// 元のperson1はperson2の影響を受けない
print(person1.age)  // 36
print(person2.age) // 50

一方、参照型では変数に代入した際、そのインスタンスへの参照が変数に代入されます。変数が格納しているのはあくまでも参照のため、変数に対して更新をかけると元のインスタンスにも影響を与えます。

class Person {
  var name: String
  var age: Int

  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }
}

let person1 = Person(name: "John", age: 25)
let person2 = person1

person2.age = 30

// 値を共有しているため、影響を受ける
print(person1.age) // 30
print(person2.age) // 30

クラスと構造体の違い

データ型が異なることでクラスと構造体では以下のような違いがあります。

||クラス|構造体|

reference

https://swift.codelly.dev/guide/構造体とクラス/値型と参照型.html#値型と参照型