くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

モバイル開発における同期・非同期処理について

今回はモバイル開発で欠かせない同期・非同期処理の概念についてです

最初の方の概念の説明はAndroidも共通している内容ですが、後半はiOSに特化した内容になります

Androidの同期・非同期処理はこの前、技術書典で出版した本で軽く説明しているので気になる方は読んでみてください!!(今回はAndroidの解説は省きます)

同期・非同期とは?

まずは、同期・非同期処理とはいったい何なのかを説明しようと思います

同期

同期処理のイメージは次のようなものです

同期処理のイメージ

タスクA、タスクBを同期処理するアプリケーションがあったとします。このアプリケーションが、ユーザーAからリクエストを受けた場合を例にします

プログラムに書かれた通りの順番でタスクが処理されるので、タスクBが終わるまでタスクAの処理が中断され、ユーザーからは画面が固まったように見えてしまいます。

非同期

非同期処理とは、あるタスクを実行している最中に、その処理を止めることなく別のタスクを実行できる方式です

処理の流れは次のようなイメージです

非同期処理のイメージ

非同期処理の場合は、実行の順番を待たないため、処理が完了する順序はその都度異なります

非同期処理は、上図のように、ユーザーAのリクエストを処理中にユーザーBからのリクエストがあっても、ユーザーBはユーザーAの処理完了を待たずに、結果を受け取ることができます

非同期処理をうまく活用すると、全体の処理速度を速められるメリットがあります

このとき注意しておきたいのは、非同期処理と並行処理の意味です。並行処理とは文字どおり複数の処理を同時進行で行うことです

一方、非同期処理は処理を止めることなく実行できるというだけです

  • 並行処理:複数の処理を1つの主体が切り替えながらこなすこと
  • 並列処理:複数の処理を複数の主体で同時にこなすこと
  • 同期処理:複数の処理をこなす際、ある処理が別の処理の終了を待つような処理
  • 非同期処理:複数の処理をこなす際、ある処理は別の処理の終了を待たないような処理

iOSで非同期を行うためには?

では、iOSで非同期を行うためにはどうすればいいでしょうか?

iOSにおける並列(非同期)処理の手法には次の手法があります。最近だとasync/awaitが登場して一番使いやすいと思いますがここでは割愛します

  • Thread : スレッドを立ててスレッドの中で処理を行う。スレッドの管理やキューイングなどの管理はアプリケーションが行う
  • GCD(Grand Central Dispatch) : スレッドの管理などをOSレベルで実装したもの。処理をしたいタスクをClosureで渡す。渡されたタスクはQueue(キュー)に挿入されて逐次実行される

この中で、GCDがよく利用されています。Threadだとスレッドの処理などを自前で書かないといけないのでコードが冗長になってしまい大変です

そこで今回は、GCDについて解説を進めて行きます

GCDの仕組みを簡単に説明すると次のような仕組みです

  • DispatchQueueというキューに処理を行いたい内容(タスク)を追加する(GCDではQueueのことをDispatchQueueとも呼びます)
  • キューに追加されたタスクはFIFO(First In First Out)で順番に実行される
  • タスクの実行は別スレッド上で実行される
  • 処理の開始自体はQueueに追加された順に実行されます。

そして次のような特徴があります

  • タスクはClosureとして登録して、キューの管理はアプリケーション独自に作ったもの、アプリケーション起動時にシステム内で自動的に生成されるもの、の二種類がある
  • 適切なスレッドへのタスクの振り分けはOSが行う
  • アプリケーション内部で自動生成されたキューと、そのキューに追加されたタスクを実行するスレッドの管理はOSが自動的に行ってくれる

スレッドの生成や管理はシステムがやってくれるので重要なことは、実行したいタスクを定義して適切なDispatchQueueに追加することです 

次の処理で追加します

関数 補足
func async(group: DispatchGroup? = default, qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default, execute work: @escaping () -> Void) Closureで定義された処理をqueueに追加する。タスクの処理は非同期で実行される
func sync(execute block: () -> Swift.Void) Closureで定義された処理をqueueに追加する。タスクの処理は同期的に実行される

asyncやsyncは次のようにして使います

DispatchQueue.global.sync {
}

DispatchQueue.main.async {
}

DispatchQueueにはキューが次のように存在します。

キューの種類 説明
serial dispatch queue(直列 ) タスクを同時に一つずつ追加された順に実行する仕組み。タスクは他のキューと独立したスレッド上で動作
concurrent dispatch queue(並列) 複数のタスクを同時に実行する。実行の順番はキューに追加した順番になるが、終了のタイミングの順序は保証されない。同時に実行するタスクの数はシステムの状況に応じて変化する。アプリケーションが所有するglobalQueueとアプリケーション内部で生成するqueueを持つことができる。キューには優先度をつけることができる
main dispatch queue アプリケーションのどこからでも利用することの出来るシリアルキュー(直列)で、アプリケーションのメインスレッド上で実行される。UIの更新などはこのキューを用いて行う必要がある

globalQueueは上記の表にもあるように実行優先度ごとにキューが管理されます

優先度 説明
優先度1:userInteractive ユーザーの入力に対してインタラクティブに実行。即時に実行されなければフリーズしているように見える処理に使う
優先度2:userInitiated ユーザーの入力を受けて実行される処理に使う
優先度3:default 優先度が指定されていない場合に指定される。明示的には指定しない
優先度4:utility プログレスバー付きのダウンロードなどに利用される。視覚的な情報の更新をするが、即時の結果を要求しない処理に使う
優先度5:background バックアップなどの見えないところで使われる。時間がかかっても問題ない処理に使う

例えば次のように使用します

let globalQueque = DispatchQueue.global(qos: .background)
globalQueque.async {
}

最後にThread.isMainThreadで、現在の処理がメインスレッドで実行されているのかを確認することができます。 スレッドのして方法に誤りがあってクラッシュすることもあるので、現在メインスレッドなのかを確認するために使います

そしてここまで説明した概念を元にConcurrencyを勉強しましょう!

参照

qiita.com

dev.classmethod.jp

developer.apple.com

developer.apple.com

developer.apple.com

qiita.com

FragmentManagerについて

今回はFragmentManagerについて調べたことをまとめようと思います

FragmentManagerをどのインタンスから作成するかを意識しないとうまく表示できなかったりと想定外なことが起こるので注意が必要です (今回は指定違いで想定外なことが起こっていました)

FragmentManagerとは?

FragmentManagerは、アプリのFragmentに対する追加、削除、置換、バックスタックへの追加などを実行するためのクラスです

FragmentManagerは生成されたFragmentのインスタンスの状況を管理して、再度呼ばれると復元してくれます

FragmentManagerがTransactionを使うことで、フラグメントに対する追加などのアクションを実行します

Transactionはいくつかの命令をまとめて処理するときに使われるものです。命令のうちどれかに不都合があると、処理自体が停止されます

主に使用するアクションは次のような実装です

// 追加
val testFragment = FirstFragment()
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.fragment_container, testFragment)
fragmentTransaction.commit()

// 置換
val testFragment = FirstFragment()
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragment_container, testFragment)
fragmentTransaction.commit()

// 削除
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.remove(testFragment)
fragmentTransaction.commit()

// バックスタックへの追加
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.addToBackStack("")
fragmentTransaction.commit()

上記の例ではTransactionを定義する際にsupportFragmentManager.beginTransaction()を使いましたが、これはこのまま使用して想定外のことが起きないでしょうか?次の章で説明します!

FragmentManagerの種類(アクセス)

FragmentManagerには、使用するフラグメントの種類に対応したクラスがあります

公式ドキュメントに詳しく説明の記載がありますが、どこからアクセスされるかで使用するFragmentManagerが変わります。次のようにマッピング一覧で表されています

使用するFragmentManagerの一覧

また上記の「使用するFragmentManagerの一覧」図にあるLevelの定義は次のように具体例があります

すべての FragmentActivity とAppCompatActivityなどのサブクラスActivity内でアクセスする場合には、getSupportFragmentManager()メソッドを介して FragmentManager にアクセスします

Fragment内でアクセスする場合には2パターンあります

Fragment内でgetChildFragmentManager()を介して子Fragmentを管理するFragmentManagerへの参照を取得するパターンとそのホストであるFragmentManagerにアクセスする必要がある場合にはgetParentFragmentManager() を使用するパターンです

今回自分はFragment内で子Fragmentを管理するパターンでgetSupportFragmentManager()を使っていて表示がおかしくなっていました(BottomNavigationの1フラグメントの中でViewPagerを追加するという内容です)

一時的には表示がうまくいく可能性はありますが、リロードしたり、タブを切り替えて戻ってきたら表示されなくなるなど想定外のことがあり得ます。 FragmentManagerをどのように取得するかを意識して使うようにしましょう!

参照

developer.android.com

developer.android.com

SwiftのGenericsについて

今回はジェネリクスについてまとめます

ジェネリクス(Generics)は指定したタイプで柔軟に動作する再利用可能な関数や型を指定できる機能です

ジェネリクスを使用するには、関数名のあとに<T>を指定します

<T>は型引数(型パラメータ)と呼ばれるもので、この関数ではTという型を使用することを意味しています

<T>を指定すると、関数が呼び出されたときにSwiftのシステムで自動で適切な型に置き換えて処理を実行してくれます

ジェネリクスの型は<T>でなくても構いせんが、慣例として<T>が使われています

ジェネリクスは関数やクラスで使用することができます

Generics関数

func isEqualCheck<T: Equatable>(x: T, y: T) -> Bool {
      return x == y
}

上記ではxとyが等しいかをチェックしています。 EquatableをつけているのはTがEquatableプロトコルに準拠している型というのを表しています。 StringやFloat、BoolなどはEquatableプロトコルに準拠しているので使用できます。 逆に型によっては使用できない可能性がある場合(今回の場合に比較や+-などのオペレータの場合)にはTがどのプロトコルに準拠しているかを明示する必要があります

Genericsクラス

ジェネリクスクラスは次のようにして使います

class GenericsClass<T> {
    
    var item: T
    
    init(item: T) {
        self.item = item
    }
    
}
 
let generics1 = GenericsClass(item: 100)
 
let generics2 = GenericsClass(item: "TEST")

指定方法はGenerics関数と同じで型引数(<T>)を指定するだけです

Genericsプロトコル

Swiftではプロトコルにもジェネリクスを使用することができます

例えば、次のように使います

protocol TestProtocol {
    associatedtype T
    var item:T { get }
    func testValue(item:T)
}

// 1つ目の継承クラス
class Test1:TestProtocol {
    var item:Int = 100
    
    func testvalue(item: Int) {
        self.item += 100
    }
}

// 2つ目の継承クラス
class Test2:TestProtocol {
    var item:String = "100"
    
    func fruitvalue(item: String) {
        self.item = item + "円です"
    }
}

プロトコルジェネリクスを使用するためには、定義内でassociatedtype Tのように任意の型を指定する必要があります

型制約

Generics関数の説明で、TがEquatableプロトコルに準拠している型というのを表しました。 このように準拠するプロトコルスーパークラスなどの型引数に制限を追加することがあります。 これを型制約といいます

型制約を追加することでジェネリクス関数やジェネリクス型を細かくコントロールすることができます

今回はwhereを使った制限を紹介します!

func sort<T: Collection>(numbers: T) -> [T.Iterator.Element] where T.Iterator.Element: Comparable {
      return numbers.sorted()
}

上記の例の場合には型引数TはCollectionプロトコルに準拠している上にwhere T.Iterator.Element: Comparableで定義されているようにComparableプロトコルに準拠している T.Iterator.Elementに制限されます

ちなみに、Elementは複数要素のそれぞれの型を表しています

このように細かい制限を加えることによって想定外の型が入る余地を省いています。ジェネリクスはどんな方も入る可能性があるため注意が必要です

参照

docs.swift.org

TableViewCellの再利用について

あるときこんなレビュー指摘がありました。

「CellのViewをhiddenにしているけど、ずっとhiddenになったままでいいの?再利用されるときに想定外の場合になる可能性があるよ」

最初、??と思ったのですが、そのときにTableViewCellは再利用されていることを知りました ※結構前の話です

今回はそんなセルの再利用についてまとめてみようと思います。

Cellの再利用

Cellを使う場面で真っ先に思いつくのはTableViewだと思います

TableViewのセルが大量にある場合にはViewの描画数が非常に多くなり、特にスクロール時には、パフォーマンスの低下が懸念されます

そのため、Cellの再利用で毎回新しくViewを作るのではなく以前に生成したcellを利用することで、メモリ割り当てを最小限にします

ただし、Cellの見た目を変更した際にリサイクル前のCellに対する変更が残ってしまうという問題がしばしば起こります。 例えばカスタムセルクラスの中で、データの中に画像が存在しない場合は、Cellに設置したimageViewを非表示にする処理を書いたとします。 そして、imageViewが非表示となったCellを再利用する時、画像の有無にかかわらずimageViewは非表示のままで表示されるので、注意が必要になります

Cell再利用の流れ

Cellを再利用するために、まず事前にregister(_:forCellReuseIdentifier:)を呼んで、ViewをCellのテンプレートとして登録します。 テンプレートはユニークなreuseIdentifierによって管理されます

次に、Cellの生成(参照)時、つまりfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellメソッドが呼ばれます。 このメソッドは、再利用可能なCellがあればそれを、なければ新しく作成したCellを返します。再利用可能なCellは、reuseIdentifierに紐づけられたreuse queueに格納されています

tableviewは裏側でreuse queueという、データの箱のようなものを持っており、reuseIdentifierごとにreuse queueが存在します。 画面外に出たCellは、自身のidentifierに紐づいたreuse queueに追加されます。 そして、同じidentifierのCellが表示されようとする時、queueから取り出されます。 次の図のようにスクロールしたら次のcellがqueueから取り出されるイメージです

Cell再利用で想定外の表示にならないようにするために

ここからはCell再利用で想定外の表示にならないためにやるべきデータ初期化のタイミングについて2つ紹介します。2つともやることは同じです

まずはfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellで行うパターンです

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cellidendifier",  for: indexPath)

      // 画像を初期化
      cell.imageView.image = nil
      cell.imageView.image = UIImage(named: "test")

      return cell
}

imageViewのimageを初期化した上で画像を設定しています。この方法だとfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellに多く初期化する処理を書く可能性があり、微妙なところです

もう1つはカスタムセルのprepareForReuse()で初期化を行うです

prepareForReuse()は再利用可能なセルを準備するときに呼ばれます。このタイミングで初期化を行います

class CustomTableViewCell: UITableViewCell {
    @IBOutlet private weak var label: UILabel!

    // セルのリサイクル対策での初期化処理を行う
    override func prepareForReuse() {
        super.prepareForReuse()
    }

    func configure(text: String) {
        label.text = text
    }
}

上記のようにするとconfigureメソッドやオートレイアウトを使う場合にはawakeFromNib()で初期化を行う必要がないです

参照

shiba1014.medium.com

developer.apple.com

Rx(Swift)とは?

今回はリアクティブプログラミングについてまとめようと思います

RxSwiftをベースに説明しますが、概念の部分はAndroidや他のプログラミング言語でリアクティブプログラミングを行う場合でも共通すると思います

Rxとは?

Rx(Reactive X)とは、「オブザーバパターン」「イテレータパターン」「関数型プログラミング」の概念を実装している機能です

Rxを導入するメリットは、「値の変化を検知できる」「非同期の処理を簡潔に書ける」です

値の変化というのは変数の変化やUIの変化も含まれます(ボタンのタッチなども含める)

Observerパターン

Rx全般において、処理の流れの基本的な設計として用いられるものの1つに、Observerパターンと呼ばれるデザインパターンがあります

Observerパターンとは、あるオブジェクトで発生したイベントを別のオブジェクト(オブザーバー)へ通知する処理で用いられるデザインパターンの1つです

Rx関連でのイベントとは、画面上で発生するタップイベントの他に、変数の値が変わった・APIから値が取得できたなどプログラム内におけるオブジェクトの状態の変化全てを指します

RxSwiftでは、イベントストリームの中で流れて来るデータの部分をデータストリームと言われています。Observerパターンでデータストリームを扱うクラスには、次の2つのクラスがあります

  • Observable:監視可能なものを表すクラス
  • Observer:Observableからデータストリームを受け取るクラス

Observable監視可能なものを表すクラスで通知を行うことができます。一方、Observerは監視を行う側です。

Observableを「出版」、Observerを「購読」と呼ぶ場合もあります。それは次のような由来があります

出版社が事業を始め、定期的な刊行物を発行します。購読者は、出版社に購読の申し込みをすると定期的に刊行物が届けられます。 購読を解除すると、解除日以降は刊行物は届けられません。 出版者が新聞や雑誌などの定期刊行物を発行し、それを誰でも好きなタイミングで購読したり辞めたりできることと同じように考えるものです。

用語説明

Observable

Observerパターンで説明したObserverはイベントを流せるクラスで、Observableはイベントを受け取れるクラスです

RxSwiftでのイベントは次の3種類があります

イベントの種別 発生するタイミング 渡される値
next(Element) イベントが発生した際に何度でも イベント派生元の値(データストリーム)
error エラーが発生した際に一度だけ エラー情報
completed 処理が完了した際に一度だけ なし

Observableはイベントを受け取れるのですが、イベントを受け取る際にはsubscribe(_:)メソッドを利用します。次のソースコードはRxSwiftの一例です

let disposable = textField.rx.text.asObservable()
.subscribe(
  onNext: { value in
    // 通常イベント発生時の処理
  },
  onError: { error in
    // エラー発生時の処理
  },
  onCompleted: {
    // 完了時の処理
  }
)

textField.rx.text.asObservable()が Observableです。Observableが値が更新されたというイベントを受け取って、Observerはイベントに対して登録された処理を行います(今回だとonNext: { value in }など)

先程のソースコードtextField.rx.text.asObservable().subscribe()まではObservableで、onNext・onError・onCompletedの処理はObserverです(Observerはイベントを流せます)

Operator

RxにはObservableで作ったストリームを操作する機能が提供されています

ストリーム(Observable)を変換・合成・生成などの操作をオペレーターと言います

オペレーターにはmapfilterなど多くのものがあります

いくつかみていこうと思います。まずはmapです。mapは流れてきた値を変換するために使います。 下記例では入力された Int 型の値を 10倍にしています

_ = Observable.of(1,2,3)
      .map { $0 * 10 }
      .subscribe(onNext: {
          print("onNext: ", $0)
      })

// 出力結果
onNext:  10
onNext:  20
onNext:  30

次はfilterです。filterは流れてきた値で条件に合うものだけを残すために使います。 下記例では4で割り切れる数字のみを残しています

 = Observable.of(1, 2, 3, 4, 5)
 .filter { $0 % 4 == 0 }
 .subscribe(onNext: {
  print($0) // => 4
 })

そして、flatMapについてです。flatMapは今まで触れたmapやfilterとは違いがあります。それは、新たなObservableを発行するかどうかです

flatMapは元のObservableのイベント毎に、Observableに変換して、そのObservableが発行するイベントを1つに集約します

_ = Observable.of(1,2,3)
                .flatMap { number in
                    // mapとは違い新たにObservableを作成する
                    Observable.of("\(number)" + "a", "\(number)" + "b", "\(number)" + "c")
                }
                .subscribe(onNext: {
                    print("onNext: ", $0)
                })


// 出力結果(非同期なので毎回この順番という保証はありません)
onNext:  1a
onNext:  1b
onNext:  2a <- 非同期に処理が行われる
onNext:  1c
onNext:  2b
onNext:  3a
onNext:  2c
onNext:  3b
onNext:  3c

上記の例をみるとわかるようにflatMapでは新たにObservableを作成しています。マーブルダイアグラムでは次のようになります

flatMapは非同期で受け取る値の順番は毎回変わる可能性があります。APIの結果を受け取る場合にはflatMapLatestという常に一番新しいObservableを実行するオペレーターを使うようにしましょう

Subject

RxSwiftでのSubject は、ObservableとObserver両方の機能を持っています(イベントを流すこともでき、イベントを受け取ることもできる)

Observableでもsubscribeすると、onNext・onError・onCompletedの処理が行われますが、流れてくる値を受け取るための手段でObservable自らイベントを発せさせるわけではありません

Subjectは自身でイベントを流すことができます。次の例をみてください

let subject = PublishSubject<String>()
let _ = subject.subscribe(onNext: {
    print("onNext:", $0)
}, onCompleted: {
})

// イベントを流す
subject.onNext("A")
subject.onNext("B")
subject.onNext("C")
subject.onNext("D")
subject.onCompleted()

Observableクラスではsubject.onNext("A")などイベントを流すことはできませんが、Subjectだと可能です

Subjectでよく使うのは次の種類があります

種類 動作
PublishSubject キャッシュせず、来たイベントをそのまま通知する
ReplaySubject 指定した値だけキャッシュを持ち、subscribe時に直近のキャッシュしたものを通知する
BehaviorSubject 初期値を持つことができ、1つだけキャッシュを持ち、subscribe時に直近のキャッシュしたものを通知する(初期値をsubscribeで受け取れる)

HotとCold

ObservableはHotColdの2種類の性質があります

ColdなObservableはsubscribeされるまで動作せず、複数subscribeした場合にはそれぞれに対してObservableが複製されてしまいます。 なので、同じ値が複数回流れることになります。ほとんどのOpertorがColdなObservableです

Observable.of("event")
  .map { text -> String in
      print(text)
      return text
  }

// subscribeしていないので、何も出力されない

HotなObservableはsubscribeされなくても動作し、複数subscribeしているのにも関わらず、1つのObservableで同じ値がそれぞれに流れます

SubjectはHotで、mapなどのオペレーターは内部でObservableを返しますがColdなObservableを返すので、ColdなObservableです

Rxのストリームは基本的にSubscribeされた瞬間に各オペレータの動作が始まるようになっています。 ですがHot Observableをストリームの途中に挟むことで、Subscribeを実行するより前にストリームを稼働させることができます。

また、Hot Observableはストリームを分岐することができます。そして、分岐したストリームそれぞれに同一の値を渡します

最後に

大まかな説明になってしまいましたが、概念を理解することでCombineなどに活かせると思います。 また、iOS以外でのプログラミングでもリアクティブプログラミングという考え方は使うので知っておくとで他の場面で活用できる可能性もあるのでぜひ学習してみてください!

参照

github.com

open8tech.hatenablog.com

qiita.com

github.com

speakerdeck.com

github.com

qiita.com

CompositionalLayoutsを使ってみる

今回はCompositionalLayoutsについてです

CompositionalLayoutsはiOS13から登場したCollectionViewのレイアウト構築方法です

これまではUICollectionViewFlowLayoutクラスを使用していましたが、次のような課題がありました

  • Boilerplate code: ボイラープレートコード(定型的なコードで冗長になりがち)
  • Performance considerations: パフォーマンスへの配慮
  • Supplementary and decoration view challenges: 補足・装飾表示の課題
  • Self-sizing challenges: セルフサイジングの課題

その解決策として登場したのが、CompositionalLayoutsです

CompositionalLayoutsは次のように説明されています

  • Composing small layout groups together:小さなレイアウトグループをまとめて合成
  • Layout groups are line-based:レイアウトグループはラインベース
  • Composition instead of subclassing:サブクラス化ではなくコンポジション

CompositionalLayoutsの構造

CompositionalLayoutsは次のように4つの構成があります

Item -> Group -> Section -> Layoutという階層構造になっています。まずは上記図のような構成を再現するようにします

NSCollectionLayoutItem

Compositional Layoutsの階層構造の最小部品のitemは生成時にNSCollectionLayoutSizeを設定する必要があります。 NSCollectionLayoutSizeというのは大まかにいうとセルのサイズです

NSCollectionLayoutSizeでサイズを指定するには次の設定値があります

  • fractionalWidth、fractionalHeight: 画面サイズに対しての比率の割合を指定
  • absolute: 指定した値にサイズを固定
  • estimated: 値の指定はするものの優先される制約がある場合は値は変化する

次のようにサイズをして、itemは生成します

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                              heightDimension: .fractionalHeight(2/3))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

上記の場合、横幅は画面一杯、縦幅は画面に対して2/3のサイズを指定しています。 absoluteやestimatedは.absolute(44)などのように指定します

NSCollectionLayoutGroup

Groupはレイアウトの基本単位を構成します

以下3つの形で定義することが可能です

  • horizontal(縦方向に並べる)
  • vertical(横方向に並べる)
  • custom(並べる方式をカスタマイズ)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                               heightDimension: .fractionalHeight(2/3))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)

上記の場合にはデータを1つずつ縦方向に並べるという実装になります

NSCollectionLayoutSection

NSCollectionLayoutSectionはCollectionViewにおけるSectionと同じ要素です。 Groupを持ち、CompositionalLayoutsにおけるレイアウトの単位になります

NSCollectionLayoutSectionでは、次のようにGroupをSectionに追加します

let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging

また、section.orthogonalScrollingBehaviorでスクロールする形式を設定することができます。 .pagingで横スクロールを設定します

Layouts

Layoutsはここまでで作成したセクション単位のレイアウトを並べます

UIcollectionViewのcollectionViewLayoutに作成するレイアウトを設定します

enum SectionType: CaseIterable {
    case main
    case sub
 }

collectionViewLayout = UICollectionViewCompositionalLayout { sectionIndex,_ -> NSCollectionLayoutSection? in
        let sectionLayoutKind = SectionType.allCases[sectionIndex]
        // sectionによって作成するセクションを変更する
        switch sectionLayoutKind {
        case .main:
            return self.generateApplianceHorizontalLayout()
        case .sub:
            return self.generateArticleLayout()
        }
    }

ここまで説明したものをまとめたソースコードは以下になります

import UIKit

class CompositionalLayoutsCollectionView: UICollectionView {
    enum SectionType: CaseIterable {
        case main
        case sub
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        registerNib()
        setupLayouts()
    }
    
 // セルを登録する処理を行う
    private func registerNib() {
    }
    
    private func setupLayouts() {
        collectionViewLayout = UICollectionViewCompositionalLayout { sectionIndex,_ -> NSCollectionLayoutSection? in
            let sectionLayoutKind = SectionType.allCases[sectionIndex]
            switch sectionLayoutKind {
            case .main:
                return self.generateApplianceHorizontalLayout()
            case .sub:
                return self.generateArticleLayout()
            }
        }
    }
}

extension CompositionalLayoutsCollectionView {
    private func generateApplianceHorizontalLayout() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                              heightDimension: .fractionalHeight(2/3))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                               heightDimension: .fractionalHeight(2/3))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
        
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .paging
        
        return section
    }
    
    private func generateArticleLayout() -> NSCollectionLayoutSection {
        let fullArticleItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                         heightDimension: .fractionalHeight(1/4))
        let fullArticleItem = NSCollectionLayoutItem(layoutSize: fullArticleItemSize)
        
        let pairArticleItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/2),
                                                         heightDimension: .fractionalHeight(1))
        let pairArticleItem = NSCollectionLayoutItem(layoutSize: pairArticleItemSize)
        let pairGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                   heightDimension: .fractionalHeight(1/4))
        let pairGroup = NSCollectionLayoutGroup.horizontal(layoutSize: pairGroupSize, subitem: pairArticleItem, count: 2)
        
        let nestedGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                     heightDimension: .fractionalHeight(1))
        let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: nestedGroupSize, subitems: [pairGroup, fullArticleItem])
        
        let section = NSCollectionLayoutSection(group: nestedGroup)
        
        return section
    }
}

実際にビルドしてみると次のような画面になります。最初に表示していた作成イメージと構成が一致しているのがわかると思います

実装後の表示画面

アイテム間のスペース

アイテム間のスペースを開けたい場合があると思います。そんなときに設定するプロパティを少し紹介します

interGroupSpacing

interGroupSpacingはSectionに設定するプロパティで、セクション内でのグループ間のスペースを設定します。 上下左右一律に設定した値のスペースができます

let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 10

interItemSpacing

interItemSpacingはGroupに設定するプロパティで、グループ内でのアイテム間のスペースを設定します。 上下左右一律に設定した値のスペースができます

let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
group.interItemSpacing = .fixed(10.0)

contentInsets

contentInsetsはGroupやSectionなど限定なしに設定できるプロパティで、スペースを設定します。 上下左右に設定した値のスペースができます

let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 10, trailing: 10)

最後に

ここまで説明しましたが、まだ完全に説明できているわけではありません。 気がついたら更新しようと思いますが、公式ドキュメントやWWWDCの動画を見たりして補いながら学習できるので、試してください!!

参照

developer.apple.com

muchan611.hatenablog.com

future-architect.github.io

blog.shota-ito.com

qiita.com

iOSの証明書について

今回はiOSで使用する証明書についてです

アプリをビルドしたり、リリースしたりする際に証明書関連の対応手順は理解していても、それぞれどんなことをしているのかがあまり理解できていなかったので、調べてまとめます

まずは全体像を下記に載せます

ビルドやアーカイブを行うために必要な手順を図にしました。それぞれの手順をもう少し詳しく説明します

証明(署名)とは?

ビルドやアーカイブを行うために必要な手順を説明する前に、そもそも証明とは一体なんでしょうか?

証明(署名)は、iOSが信頼できるアプリしかインストールしないようにするための仕組みです

信頼できるアプリとは開発者を特定できるというのと開発者が署名してからアプリが改変されていないという2つの条件を満たすものを指しています

信頼できないアプリは適当な人が作った適当なアプリや、ちゃんとした開発者が作り、その後別の誰かによって改変されたアプリをインストールすることが可能ということになります

これを防ぐため、iOSでは署名されたアプリしか実行できないようになっており、開発者はアプリに署名をする必要になります

証明(署名)の大まかな流れ

アプリの開発者は、公開鍵/秘密鍵のペアを用意します

公開鍵はデジタル証明書の形でインストール先のiOSに渡され、秘密鍵は鍵ペアを生成したマシンに保持され、署名に使われます

アプリをアーカイブする際、あらかじめ決められた手順でアプリからハッシュ値を計算し、これを秘密鍵で暗号化してアプリに同梱します。このプロセスを署名と呼びます

アプリをインストールしたiOSは、同じ手順でアプリからハッシュ値を計算します

一方で、アーカイブ時に計算されて暗号化されたハッシュ値を証明書に含まれている開発者の公開鍵で復号する。

これら2つの方法で得られたハッシュ値が一致することを確認します

ここで、まず暗号化されたハッシュ値が正しく復号できたことで、開発者の特定ができます

ハッシュ値が公開鍵で復号できるということはそれを暗号化したのは開発者の秘密鍵であり、秘密鍵を持っているのは開発者本人しかいないからです

次に、2つのハッシュ値が一致したということから、開発者が署名した後アプリが改変されていないことの確認ができます

もし第三者がアプリの内容をいじっていたらiOSで計算されるハッシュ値が変わってしまい一致しません

ハッシュ値の方も一緒に変えられた場合には、第三者は開発者の秘密鍵を持っておらずハッシュ値の暗号化ができないため、改変したハッシュ値を差し替えることができないです

適当な秘密鍵で暗号化することは可能ですが、そうすると開発者の公開鍵では復号できず、そこでiOSでの検証に引っかかります

CSRファイル作成

まずは図の①のCSR(証明書署名要求)についてです

CSRは証明書署名リクエストです。公開鍵に開発者情報などを付加したもので、これををAppleに渡すことで、Appleから証明書をもらうことができます

ローカルのキーチェーンで作成し、証明書を作成するのに使います

前の項に書いたように、コード署名というのは大雑把に言うとこのとき生成された秘密鍵で開発者がアプリに署名をし、それをインストール先のiOSが公開鍵で検証するというものなので、ここで生成される鍵ペアは非常に重要な役割を持ちます。

作成時に、秘密鍵と公開鍵も同時に作られます

作成方法はキーチェーンアクセスから「証明書アシスタント」>「認証局に証明書を要求」を選択します。すると、鍵ペア情報が表示されるので、鍵のサイズは2048ビット、アルゴリズムRSAを選択して作成します

作成したファイルはApp Developer Programで登録(アップロード)します

.cerファイル作成

ここでは図の②、③について説明します

まずは、作成したCSRファイルをApp Developer Programで登録します

登録する際には開発用か本番用かを選択したり、プッシュ通知を導入するかなどの選択を行います

ここではユーザーの公開鍵含まれていて、認証局秘密鍵で暗号化します

設定が完了してCSRファイルをアップロードしたら、拡張子が.cerのファイルをダウンロードできると思います

一般的に.cerファイルと呼ばれているこのファイルは開発者個人を特定するための証明書、またはAd Hoc用やリリース用の証明書です

.cerファイルをダウンロードして取り込む(ファイルをダブルクリック)ことで、ローカルマシン内のキーチェーンアクセス上で証明書と秘密鍵が紐付きます(登録されます)

.cerファイルを取り込んだけれども、キーチェーンアクセス上で証明書しか表示されないという場合、CSRファイルを作ったローカルマシンと.cerファイルを取り込んだマシンが一致してない可能性があります

p12ファイル

例えば、PCが壊れたので別のPCを使うとなった場合に、CSRファイル作成からやり直さなければいけないでしょうか?

やり直さなくていいように.p12ファイルを取得するという方法があります

.p12ファイルとは証明書の他に秘密鍵を含めたファイルです。ビルドを行うPCを変更する場合にも.cerファイルを取り込むのと同じようにできます

キーチェーンアクセス上で取り込んだ証明書を右クリックで「○○○○○を書き出す」を選択します

そして、フォーマットで.p12を指定して保存ボタンを押すと.p12ファイルが保存されます

別のPCに変更する場合には.p12ファイルをダブルクリックで取り込めば証明書を取り込んだことになります

Provision Profile登録

次にProvision Profileを取り込む必要があります

Provision ProfileとはアプリのIDや証明書、開発用のデバイス情報を内包したファイルです

これにより、開発用にビルドしたアプリが指定した端末以外で立ち上げられないようになります

また、これに保存されている証明書と、前々節で説明したキーチェーンに登録した証明書が一致した時のみビルドが許される仕組みとなっています

Provision Profileを取り込むためにはApp Developer Programで設定が必要になります

App Developer Programでの追加設定

Provision Profileを作成するためにAppIDの登録と実行デバイス情報の登録を行います

まずはAppIDの登録ですが、アプリのBundleIDを登録します

配布アプリが端末内で動作する条件の中にアプリのBundleIDが、AppIDと一致するというものがあります

よって、開発環境や本番環境など複数の環境のProvision Profileを作成する場合には複数のBundleIDが必要になります(Provision Profileは環境ごと必要)

そして、使用する端末のUDIDを登録する必要があります。登録した端末のみでビルドできるように制限するためです

App Developer ProgramでAppIDや使用する端末情報を登録するとProvision Profileがダウンロードできると思います

ダウンロードしたProvision Profileをダブルクリックして取り込んだら、Xcodeの対象のプロジェクトでBundle IDやSigning(署名)の設定を行うとビルドやアーカイブができるようになるはずです

参照

mmorley.hatenablog.com

blue-red.ddo.jp

scrapbox.io

技術書典13にサークル参加します

告知になります

明日9/10(土)からの技術書典13にサークル参加します!!(公式サイトはこちら)

今回はオンライン・オフライン両方で実施されます

今回オンラインのみで本を出します!(紙の本用の表紙と印刷が間に合わなさそうだったので...)

8月にブログを更新できていなかったのは準備と執筆を行っていたためです

どんな本を出すのかというと、Androidの基礎的な部分を解説した本です。初めてAndroidに触れる方やまだ経験が浅い方でも読んでもらえるような本になっています

気になった方は手にとっていただければ幸いです

また、読んでいただいた方は感想をお持ちしております

↓↓ココにあります↓↓

techbookfest.org