- 投稿日:2021-02-13T22:46:37+09:00
SectionModelType.ItemにEnumを用いたUITableView+RxDataSourcesの実装サンプル
環境
- Xcode 12.4
- Swift 5.3.2
- RxSwift 6.0.0
- RxCocoa 6.0.0
- RxDataSources 5.0.0
前置き
RxSwift
とRxCocoa
に触れたことがあり、
RxDataSources
をこれから利用するかもしれない方に向けて書いています。自分にとってRxSwift/RxCocoa同様、学習コストが高く苦労した覚えがあったので、
サンプルが一つでも多くあるといいなと思い、今回記事にしました。RxSwiftとRxCocoaの記述についてはあまり触れないのでご了承ください
実装内容
SectionModelType
のtypealias Item
にはEnum(Associated value)を利用しています。
表示するデータはViewModelから適当に流すようにしています。
リロードする度に新しいセクション情報を構成して表示する仕様になっています。
iPhone8(iOS14.4) 実装サンプルコード
ViewController.swiftclass ViewController: UIViewController { private let viewModel = ViewModel() private let tableView = UITableView() private let refreshControl = UIRefreshControl() private let refreshRelay = PublishRelay<Void>() private let disposeBag = DisposeBag() // ジェネリッククラスのRxTableViewSectionedReloadDataSourceの型パラメータには // SectionModelTypeを適用したクラスを指定する private let dataSource = RxTableViewSectionedReloadDataSource<SectionModel>( // tableView(_:cellForRowAt:)で行っている処理 configureCell: { _, tableView, indexPath, item in switch item { case .stringSection(let text): guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") else { fatalError() } cell.textLabel?.text = text.rawValue return cell case .intSection(let number): guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") else { fatalError() } cell.textLabel?.text = "\(number.rawValue)" return cell } }, // tableView(_:titleForHeaderInSection:)で行っている処理 titleForHeaderInSection: { dataSource, section in dataSource.sectionModels[section].title } ) override func loadView() { super.loadView() view.backgroundColor = .systemBackground refreshControl.addTarget(self, action: #selector(reloadTableView), for: .valueChanged) tableView.refreshControl = refreshControl tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.tableFooterView = UIView() tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), ]) } override func viewDidLoad() { super.viewDidLoad() let viewDidLoadRelay = PublishSubject<Void>() viewModel .observeSectionInfo(viewDidLoad: viewDidLoadRelay, refresh: refreshRelay) .drive(tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) viewDidLoadRelay.onNext(()) } @objc private func reloadTableView() { refreshRelay.onNext(()) refreshControl.endRefreshing() } }ViewModel.swift// セクション情報 enum SectionItem { case stringSection(StringSectionRowItem) // ① case intSection(IntSectionRowItem) // ② } // ①の要素(row) enum StringSectionRowItem: String { case first case second case third } // ②の要素(row) enum IntSectionRowItem: Int { case first = 1 case second = 2 case third = 3 } // SectionModelTypeを適用した構造体を定義する。 // sectionのtitle用のプロパティとinitを追加しています。 struct SectionModel: SectionModelType { typealias Item = SectionItem var items: [SectionItem] // 追加したプロパティ var title: String init(original: SectionModel, items: [Item]) { self = original self.items = items } // 追加したinitializer init(title: String, items: [Item]) { self.title = title self.items = items } } // MARK: - ViewModel struct ViewModel { func observeSectionInfo(viewDidLoad: Observable<Void>, refresh: Observable<Void>) -> Driver<[SectionModel]> { Observable .merge(viewDidLoad, refresh) .map { // section, rowの並びをシャッフルして返す [ SectionModel(title: "String Section", items: [.stringSection(.first), .stringSection(.second), .stringSection(.third)].shuffled()), SectionModel(title: "Int Section", items: [.intSection(.first), .intSection(.second), .intSection(.third)].shuffled()) ] .shuffled() } .asDriver(onErrorRecover: { _ in fatalError() }) } }終わりに
慣れるまで時間はかかりましたが、DelegateとDataSourceのメソッドをRxでまとめられるので便利だなと思っています(小並感)
section, rowがそこまで複雑ではない場合は利用しなくても良さそうです。
- 投稿日:2021-02-13T22:46:37+09:00
Enumを用いたUITableView+RxDataSourcesの実装サンプル
環境
- Xcode 12.4
- Swift 5.3.2
- RxSwift 6.0.0
- RxCocoa 6.0.0
- RxDataSources 5.0.0
前置き
RxSwift
とRxCocoa
に触れたことがあり、
RxDataSources
をこれから利用するかもしれない方に向けて書いています。自分にとってRxSwift/RxCocoa同様、学習コストが高く苦労した覚えがあったので、
サンプルが一つでも多くあるといいなと思い、今回記事にしました。RxSwiftとRxCocoaの記述についてはあまり触れないのでご了承ください
実装内容
SectionModelType
のtypealias Item
にはEnum(Associated value)を利用しています。
表示するデータはViewModelから適当に流すようにしています。
リロードする度に新しいセクション情報を構成して表示する仕様になっています。
iPhone8(iOS14.4) 実装サンプルコード
ViewController.swiftclass ViewController: UIViewController { private let viewModel = ViewModel() private let tableView = UITableView() private let refreshControl = UIRefreshControl() private let refreshRelay = PublishRelay<Void>() private let disposeBag = DisposeBag() // ジェネリッククラスのRxTableViewSectionedReloadDataSourceの型パラメータには // SectionModelTypeを適用したクラスを指定する private let dataSource = RxTableViewSectionedReloadDataSource<SectionModel>( // tableView(_:cellForRowAt:)で行っている処理 configureCell: { _, tableView, indexPath, item in switch item { case .stringSection(let text): guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") else { fatalError() } cell.textLabel?.text = text.rawValue return cell case .intSection(let number): guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell") else { fatalError() } cell.textLabel?.text = "\(number.rawValue)" return cell } }, // tableView(_:titleForHeaderInSection:)で行っている処理 titleForHeaderInSection: { dataSource, section in dataSource.sectionModels[section].title } ) override func loadView() { super.loadView() view.backgroundColor = .systemBackground refreshControl.addTarget(self, action: #selector(reloadTableView), for: .valueChanged) tableView.refreshControl = refreshControl tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.tableFooterView = UIView() tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), ]) } override func viewDidLoad() { super.viewDidLoad() let viewDidLoadRelay = PublishSubject<Void>() viewModel .observeSectionInfo(viewDidLoad: viewDidLoadRelay, refresh: refreshRelay) .drive(tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) viewDidLoadRelay.onNext(()) } @objc private func reloadTableView() { refreshRelay.onNext(()) refreshControl.endRefreshing() } }ViewModel.swift// セクション情報 enum SectionItem { case stringSection(StringSectionRowItem) // ① case intSection(IntSectionRowItem) // ② } // ①の要素(row) enum StringSectionRowItem: String { case first case second case third } // ②の要素(row) enum IntSectionRowItem: Int { case first = 1 case second = 2 case third = 3 } // SectionModelTypeを適用した構造体を定義する。 // sectionのtitle用のプロパティとinitを追加しています。 struct SectionModel: SectionModelType { typealias Item = SectionItem var items: [SectionItem] // 追加したプロパティ var title: String init(original: SectionModel, items: [Item]) { self = original self.items = items } // 追加したinitializer init(title: String, items: [Item]) { self.title = title self.items = items } } // MARK: - ViewModel struct ViewModel { func observeSectionInfo(viewDidLoad: Observable<Void>, refresh: Observable<Void>) -> Driver<[SectionModel]> { Observable .merge(viewDidLoad, refresh) .map { // section, rowの並びをシャッフルして返す [ SectionModel(title: "String Section", items: [.stringSection(.first), .stringSection(.second), .stringSection(.third)].shuffled()), SectionModel(title: "Int Section", items: [.intSection(.first), .intSection(.second), .intSection(.third)].shuffled()) ] .shuffled() } .asDriver(onErrorRecover: { _ in fatalError() }) } }終わりに
慣れるまで時間はかかりましたが、DelegateとDataSourceのメソッドをRxでまとめられるので便利だなと思っています(小並感)
section, rowがそこまで複雑ではない場合は利用しなくても良さそうです。
- 投稿日:2021-02-13T16:58:21+09:00
Swiftのオプショナル型について
オプショナル型とは
オプショナル型とはデータ型の一つで、変数の宣言時に使用します。
特徴としては変数にnil
の代入を許可することで、逆に非オプショナル型はnil
の代入を許可しないことになります。
nil
とはデータが無い(変数が空の状態)を表します。iOSでは
nil
に対して操作するとアプリケーションが落ちてしまうことがあり、そのような問題を解決するためにswift
ではnil
を基本的には許容しないとのことです。しかし、オプショナル型を使うことでnil
を扱うことができるようになります。オプショナル型の使い方
// オプショナル型 var age: Int? var name: String! // 非オプショナル型 var age: Int var name: Stringオプショナル型は最後に
?
か!
をつけます。非オプショナル型は最後に何もつけません。
また、一般的にオプショナル型 ( Optional Value ) というと?
、!
は暗黙的アンラップ型 ( Implicitly Unwrapped Optional )と呼ばれるオプショナル型になります。非オプショナル型の変数は普通に扱う変数で、
nil
を扱うことのできるオプショナル型の変数は通常とは扱い方が少し異なります。オプショナル型の変数を通常の変数と同様に扱おうとするとおもわぬエラーが起きるため、オプショナル型の値を通常の値に変換するアンラップという方法が必要になります。暗黙的アンラップ型
一般的なオプショナル型と違い、使用するとき必ず強制的にアンラップされます。利点として、初期値はないが後から必ず値を入れる保証があるものに使うと効果を発揮します。
なぜアンラップが必要なのか?
まずは、オプショナル型と非オプショナル型の出力結果の違いを見ていきます。
// オプショナル型 var name: String? name = "Taro" print(name) // 出力結果 Optional("Taro") // 非オプショナル型 var name: String name = "Taro" print(name) // 出力結果 Taro上記の通り、オプショナル型は Optional("Taro") 、非オプショナル型は Taro と出力されていることから大きな違いがあるとわかります。
また、「Optional(値)」と「値」は異なるもので、同様には扱うことができません。
var a: Int? = 10 // オプショナル型 var b: Int = 10 // 非オプショナル型 a + b /* -> a + b はエラーとなる Value of optional type 'Int?' must be unwrapped to a value of type 'Int' オプションタイプ「Int?」の値 タイプ「Int」の値にアンラップする必要があります。 */結果はエラーとなり実行することができません。
なぜなら、変数a
は Optional(10) 、変数b
は 10 という値であるため、Optional(10) + 10
は計算することができないということになります。
ここで、オプショナル型の値を通常の値 (Optiona(10)
を10
) に変換するためにアンラップという処理が必要ということになります。アンラップとは
Optional
は値を包み込むラップのようなイメージで、オプショナル型は値をOptional
というラップで包み込んでいます。そうすると、たとえ中身がない (nil
) 状態でも包み紙だけは存在するため、扱うことができます。
しかし、この便利な包み紙があるために値はラッピングされ、直接扱うことができません。扱うためにはラップを取り除き、中身の値を取り出さなくてはいけません。この包み紙を取り除き、値を取り出すことをアンラップと言います。アンラップの種類
1. 強制的アンラップ
そのままの意味になりますが、
オプショナル型を強制的にアンラップする方法
です。オプショナル型の変数の中にどんな値が入っていても関係なく、その値を取り出します。var name: String? name = "Taro" print(name!) // 出力結果 Taroオプショナル型の変数に対して
!
をつけ強制的アンラップをすると、出力がTaro
となりました。
簡単に扱える強制的アンラップですが、アンラップする対象のオプショナル型の変数の中身がnil
だった場合、エラーとなりアプリケーションが落ちてしまうという欠点があります。なので、強制的アンラップをする場合は、必ずラッピングの中身が存在する(nil
ではない)ことが保証されていなければいけません。2. オプショナルバインディング
強制的アンラップはオプショナル型の変数を中身の値に関係なくアンラップしました。そのため変数に
nil
が入ってしまうとアプリケーションが落ちてしまうという問題があり、それを解決するアンラップの方法がオプショナルバインディングです。
オプショナルバインディングの特徴は条件式を用いることで、アンラップする対象のオプショナル型の変数がnil
かどうかを判定するためのものです。
if let
nil
だった場合に行う処理が異なる時に使用します。nil
の場合はAの処理、nil
ではない場合はBの処理をするケースでよく使います。let name: String? = "Taro" if let unwrappedName = name { //「name」が「nil」ではない場合の処理 print(unwrappedName) } else { //「name」が「nil」である場合の処理 print("名前がありません") } // -> 出力結果 Taro
guard let
- これ以上処理を進めたくない場合に使用します。
nil
が入っていたらエラーとして扱うケースだった場合などによく使います。let name: String? = "Taro" guard let unwrappedName = name else { return } //「name」が「nil」ではない場合の処理を進める print(unwrappedName) // -> 出力結果 Taro3. オプショナルチェイニング
オプショナルチェイニングもオプショナル型の変数の中身が
nil
のときに安全にプログラムを実行するための機能です。
オプショナルチェイニングの使い方はオプショナル型の変数のあとに?
をつけます。オプショナル型の変数?.メソッド() オプショナル型の変数?.プロパティ
変数!
で強制的にアンラップする場合と、変数?
でオプショナルチェイニングを使用する場合で比較してみます。
変数!
で強制的にアンラップする場合var str1: String? = "HELLO" let str2 = str1!.lowercased() print(str2) // 出力結果 hello結果として
hello
が表示されます。ただし、変数str1
がnil
の場合にはエラーとなり、プログラムが落ちます。
変数?
で強制的にアンラップする場合// 変数str1が「nil」でない場合 var str1: String? = "HELLO" let str2 = str1?.lowercased() print(str2) // 出力結果 Optional("hello") // 変数str1が「nil」である場合 var str1: String? = nil let str2 = str1?.lowercased() print(str2) // 出力結果 nilこの場合、
変数str2
の値はオプショナル型になり、出力結果はOptional("hello")
と表示されます。また、変数str1
がnil
の場合にもエラーとならず、結果はnil
となります。オプショナルチェイニングの結果はオプショナル型となるため、オプショナルバインディングと組み合わせて使用されます。オプショナルバインディングと組み合わせた使用
var str1: String? = "HELLO" if let str2 = str1?.lowercased() { print(str2) } else { print("str1はnilです!") } // -> 出力結果 hello
??演算子
でnil
であった場合のデフォルト値を設定するオプショナル型の変数 ?? デフォルト値
??演算子
は、左辺のオプショナル型の変数がnil
でない場合にはその値を戻し、nil
の場合には右辺のデフォルト値を戻します。var name: String? let defaultName = "Taro" let yourName = name ?? defaultName print(yourName) // 出力結果 Taroまとめ
オプショナル型についてまとめてみました。私の意見にはなりますが、強制的アンラップ
!
はアプリのクラッシュの要因の一つになると思いましたので、基本的には条件分岐が可能なif let
を用いてアンラップし、エラー処理の場合は以降の処理が実行されないguard let
を用いてアンラップする考え方で行こうと思いました。参考
- 投稿日:2021-02-13T08:23:50+09:00
配列の基礎を総復習!
今回は、swiftにおける基礎中の基礎、「配列」の復習をしたいと思います!
この記事を読むと、以下のことがざっとおさらいできます。
- 配列とは、宣言方法
- 空の配列、配列の初期化
- 配列から値を取り出す
- 配列の追加と削除
それではいきましょう!
配列とは、宣言方法
配列とは、複数の値を入れられる箱のようなもの。
これを作ることによって、様々なデータを自由に格納したり取り出したりできます。宣言方法は3通りあります。
let strings = ["aa", "bb", "cc"] let strings:[String] = ["aa", "bb", "cc"] let strings:Array<String> = ["aa", "bb", "cc"]もちろんvarでもOKです。
空の配列、配列の初期化
空の配列は、値をなし[]にすればオッケー
また、isEmptyプロパティを使えば空かどうかを判別します。var intArray = [1,4,6] intArray = [] if intArray.isEmpty { print("空です") } //空です配列から値を取り出す
インデックス番号を指定することによって取り出せます。
さらに、スライスすることもできます。var intArray = [1,4,6] let int = intArray.1 print(int) //4配列の追加と削除
配列に追加する場合はappend
挿入する場合はinsert
削除する場合はremoveでできます。var colors = ["yellow", "green"] colors.append("red") print(colors) colors.removeFirst //最初を削除できます print(colors) colors.insert("blue", at:1) print(colors) // "yellow", "green" // "yellow", "green", "red" // "green", "red" // "green", "blue","red"以上です。
他にもソートとかマップとかあるんですけど、やる気が高いときにまた改めて投稿します。笑
- 投稿日:2021-02-13T02:06:39+09:00
iOSにおけるマルチスレッドを実装して確認してみる
iOSにおけるマルチスレッドの概念について、
理解を深めるために実際に動きで確認できるよう実装してみました。マルチスレッドについて
- マルチスレッドとは並行処理のことである
- スレッドには、メインスレッドとバックグラウンドスレッドの二種類がある
- メインスレッドはUI更新時に、バックグラウンドスレッドは通信などUI以外の処理に使用される
iOSでは、主にGCD(Grand Central Dispatch)を用いて、タスクをクロージャで渡す。
なぜマルチスレッドが必要か?
- 通信処理時には待ち時間が発生する
- この処理をメインスレッドで行うと、画面描画やユーザーからのアクションにも待ちが生じる
- iPhone自体の処理能力は非常に高いので、CPUを有効活用する必要がある
GCD を用いた並行処理
- メインスレッド
DispatchQueue.main.async { // UI更新処理 }
- バックグランド
DispatchQueue.global.async { // バックグランド処理 }以下で、Web API のリクエストを行い、結果をUIに表示させるだけの簡単な実装を行います。
viewdidloadで呼ばれた"ロード中"の表示は、メインスレッドで呼ばれます。
その時、非同期のバックグラウンドスレッドでは通信が行われています。
そして通信が終了したところで、レスポンス結果を受け取ります。それをUIに反映させるのはメインスレッドで行うため、DispatchQueue.main.asyncで記述します。
- 使用API - Discogs API
ドキュメントの中に、
To browse different pages, or change the number of items per page (up to 100), use the page and per_page query string parameters:
(別のページを参照したり、1ページあたりのアイテム数を変更したりするには(最大100個まで)、pageとper_pageクエリ文字列パラメータを使用します。)とあるので、例として以下の様にURLを組み立てます。
let page = "2" let per_page = "5" let urlString = "https://api.discogs.com/artists/1/releases?page=\(page)&per_page=\(per_page)"これを使って、ViewControllerだけでマルチスレッドの処理を確認します。
class ViewController: UIViewController { var label: UITextView! override func viewDidLoad() { super.viewDidLoad() label = UITextView() label.text = "ロード中" label.frame = CGRect(x: 10, y: 30, width: self.view.frame.width - 20, height: 300) self.view.addSubview(label) let page = "2" let per_page = "5" let urlString = "https://api.discogs.com/artists/1/releases?page=\(page)&per_page=\(per_page)" let url = URL(string: urlString)! var request = URLRequest(url: url) request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } do { let object = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] DispatchQueue.main.async { // UI更新の処理 self.label.text = object?.description self.label.sizeToFit() } } catch let e { print(e) } } task.resume() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } }繰り返しになりますが。上記のHTTP通信処理はバックグラウンドスレッドで行われています。
一方で、UI更新の DispatchQueue.main.async の処理は、メインスレッドで行われます。"ロード中"が表示され、その後通信結果が表示されることが確認できました。
参考文献
- 投稿日:2021-02-13T02:06:39+09:00
iOSにおけるマルチスレッドを実装で確認してみる
iOSにおけるマルチスレッドの概念について、
理解を深めるために実際に動きで確認できるよう実装してみました。マルチスレッドについて
- マルチスレッドとは並行処理のことである
- スレッドには、メインスレッドとバックグラウンドスレッドの二種類がある
- メインスレッドはUI更新時に、バックグラウンドスレッドは通信などUI以外の処理に使用される
iOSでは、主にGCD(Grand Central Dispatch)を用いて、タスクをクロージャで渡す。
なぜマルチスレッドが必要か?
- 通信処理時には待ち時間が発生する
- この処理をメインスレッドで行うと、画面描画やユーザーからのアクションにも待ちが生じる
- iPhone自体の処理能力は非常に高いので、CPUを有効活用する必要がある
GCD を用いた並行処理
- メインスレッド
DispatchQueue.main.async { // UI更新処理 }
- バックグランド
DispatchQueue.global.async { // バックグランド処理 }以下で、Web API のリクエストを行い、結果をUIに表示させるだけの簡単な実装を行います。
通信中にメインスレッドでUIを更新し、"ロード中"と表示させ、
通信が終了したところで、レスポンス結果を反映させることで、
マルチスレッドの動きを目視確認してみます。
- 使用API - Discogs API
ドキュメントの中に、
To browse different pages, or change the number of items per page (up to 100), use the page and per_page query string parameters:
(別のページを参照したり、1ページあたりのアイテム数を変更したりするには(最大100個まで)、pageとper_pageクエリ文字列パラメータを使用します。)とあるので、例として以下の様にURLを組み立てます。
let page = "2" let per_page = "5" let urlString = "https://api.discogs.com/artists/1/releases?page=\(page)&per_page=\(per_page)"これを使って、ViewControllerだけでマルチスレッドの処理を確認します。
class ViewController: UIViewController { var label: UITextView! override func viewDidLoad() { super.viewDidLoad() label = UITextView() label.text = "ロード中" label.frame = CGRect(x: 10, y: 30, width: self.view.frame.width - 20, height: 300) self.view.addSubview(label) let page = "2" let per_page = "5" let urlString = "https://api.discogs.com/artists/1/releases?page=\(page)&per_page=\(per_page)" let url = URL(string: urlString)! var request = URLRequest(url: url) request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } do { let object = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] DispatchQueue.main.async { // UI更新の処理 self.label.text = object?.description self.label.sizeToFit() } } catch let e { print(e) } } task.resume() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } }上記のHTTP通信処理はバックグラウンドスレッドで行われています。
一方で、UI更新の DispatchQueue.main.async の処理は、メインスレッドで行われます。"ロード中"が表示され、その後通信結果が表示されることが確認できました。
参考文献