- 投稿日:2019-09-15T19:34:26+09:00
protocol内のオプショナルなメソッドについて
はじめに
話的には前回の続きですが、この記事だけでも理解はできるように書いています。Swiftのプロトコルについて、もう少し掘り下げて書いていきます。
Swiftのプロトコル
以下に、プロトコルの実装を書きます。
protocol Animal { func cry() }これをクラスに準拠させることで、Animalプロトコルを持つクラスはcry()メソッドが実装されていることが約束されます。
class Dog: Animal { func cry() { print("wan!") } }オプショナルなメソッド
ここで、ある人がこう考えたとします。
「Animalプロトコルの中に、walk()メソッドも実装したいな。
あ、でも魚とかは歩かないか。なら、実装を必須にはしない形で宣言できないかな。。」Objective-Cのプロトコルではできる
プロトコルの概念はSwiftの前身であるObjective-Cの頃からありました。そしてObjective-Cのプロトコルでは実装が必須ではないメソッドも簡単に宣言することができます。以下に、Objective-Cのプロトコルの実装を書きます。
@protocol Animal @required // 実装必須なメソッド (@requiredは省略可能) - (void) cry; @optional // 実装が必須ではない、実装しなくてもいいメソッド - (void) walk; @endこのように、@optionalをつけるだけで宣言ができました。
Swiftのプロトコルではできない
しかし、同様の考え方でswiftでも実装しようとするとうまくいきません。
protocol Animal { func cry() optional func walk() } //コンパイルエラー上のコードの場合はコンパイルするとエラーになります。それをコンパイラの言うように訂正していくと、下のコードのようになるかと思います。
@objc protocol Animal { func cry() @objc optional func walk() }なんだかよくわからない、@objcというものが出てきました。これはプロトコルをObjective-Cで宣言されたものとして扱ってください、とコンパイラに伝えるために使われます。つまり、Swiftのみのコードでオプショナルなメソッドは存在せず、宣言することができません。
@objcをつけることの弊害
@objcをつけたとしてもオプショナルなメソッドを実装できたのであればいい、と考える人もいますが、極力避けたほうがいいと私は考えます。@objcをつけることで、swiftのプロトコルでできていた以下のようなことができなくなります。
- プロトコルを構造体や列挙型に準拠させられなくなる。
- プロトコル内でジェネリクスが使えなくなる。
これらの弊害は、Swiftの思想に反するもので後々に大きな弊害となる可能性があります。SwiftUIでは構造体で様々なViewを宣言するので、今からこういう使い方はしないように習慣づけておいて方がよいでしょう。
オプショナルなメソッドを、Delegateで利用
@objcをつければオプショナルなメソッドを一応定義することができることがわかりました。では、このAnimalプロトコルを準拠したクラスをdelegateで利用するとどうなるのでしょうか。以下に、コードで示します。
@objc protocol Animal { @objc optional func cry() -> String } class Dog: Animal { func cry() -> String { return "wan!" } } let delegate: Animal = Dog() print(delegate.cry?()) // 出力結果:Optional("wan!")このようにメソッドの返り値はString型でオプショナルではありませんが、メソッド自体がオプショナルであるため返り値もオプショナルになります。クラス内のメソッドだけを見ているとオプショナルということがわかりませんし、オプショナルを外す処理が後に必要になりますし、あまりきれいな書き方ではありませんね。このことからもなるべくであれば使いたくない書き方であることがわかるかと思います。
Swiftの思想
なぜこのような仕様になっているかと言うと、Swiftの言語思想的にプロトコルで実装が任意のメソッドは存在しないと考えられているからです。上記の例ではwalkを実装する時点で、本当にAnimalプロトコルという名前で正しかったのでしょうか。そしてこのプロトコルはFishクラスにも準拠させる必要があったのでしょうか。というような点を考える必要があったと思います。(Fishクラスには専用のFishプロトコルを用意する、FishプロトコルとAnimalプロトコルの内容がかぶるところが多いようであれば、その部分を抜き出して親プロトコルを作る、など)
まとめ
Objective-CからiOS開発をしている人からすると、一度はプロトコル内にオプショナルなメソッドを宣言しようとして詰まった部分だと思います。しかし、Xcodeが修正方法を提案してくれるので詳しくは調べずに@objcをつけてやり過ごした人もいるのではないでしょうか。
かくいう私もその一人です。既存のプロトコルを準拠させたいけど新しくメソッドを実装するのはめんどくさい、という理由でメソッドをオプショナルにしそうになったこともあります。そういう人たちが、安易に@objcでやり過ごさずに済み、後に負債を残さない助けに成れれば幸いです。
- 投稿日:2019-09-15T18:27:19+09:00
KituraでRESTful APIの作りかた (GET POSTのみ)
はじめに
この記事ではIBM製のSwiftでサーバーサイドを実装することのできるフレームワークのKituraを使用してRESTful APIを作成するときの大まかなポイントをまとめていきます。ハンズオンではないです。
今回は、PostgreSQLのORMを併用してデータを保存と取得ができるAPIを作る想定で進めていきます。
今回使用するライブラリのうち、自分で追加する必要のあるライブラリ
ライブラリ名 バージョン SwiftKueryORM 0.6.1 SwiftKueryPostgreSQL 2.1.1 Model
まずは、永続化するモデルの定義から。
SwiftKueryORMには
Model
というプロトコルがあり、それに準拠してモデルを作成します。ModelはCodableに準拠しています。また、
var id: Int?
と記述しておくと、idがnilの場合に自動でidを振り与えてくれるらしいです。注意としては、手動でもidを設定することができるのでそのidがもう既に存在していないかは保存する前に確認した方が良いようです。// id以外のプロパティは適当です。 struct Diary: Model, Equatable { var id: Int? var title: String? var content: String? var posterName: String? var likedCount: Int? var createdAt: Date? var updatedAt: Date? }このあと、PostgreSQLをインストールしたり、Terminal上で
createdb データベース名などを叩いて、初期の設定しなければならないのですが今回は省略します。
それでは、本題に入ります!
GET
Kituraは
Router
というクラスを使用してルーティングを設定します。App
クラスからプロパティとして取得できるのでそれを使用します。app.router.get("/api/v1/diary/read") { (request, response, next) in }getメソッドの第一引数はルーティングを指定します。この場合、ローカル環境なら
http://localhost:8080/api/v1/diary/read
にアクセスすればコールバックが呼ばれます。もしクエリパラメータ
?id=1234
みたいなものを取得したい場合は、request.queryParameters
で取得できます。let id = request.queryParameters["id"]READ (PostgreSQL)
GETリクエストを受け取ったら全件取得をします。
ORMを使用して非常に簡潔にかけます。
取得したいエンティティクラスから.findAll
メソッドを呼ぶだけです。例えば、上のDiaryモデルとして保存したデータを取得したい場合はDiary.findAll { (diaries: [Diary]?, error: RequestError?) in }このような感じで取得することができます。
まとめると、
app.router.get("/api/v1/diary/read") { (request, response, next) in Diary.findAll { (diaries: [Diary]?, error: RequestError?) in guard let diaries = diaries else { return next() } response.send(diaries) next() } }これで、データを取得してレスポンスで返せると思います。
POST
postは初見でやったときは結構はまりました。。。まとめにも記載しますが、Codable Routingを使用しないで
request.body?.asRaw
でData型を取得するにはミドルウェアを指定する必要があるようです。app.router.post("/api/v1/diary/create", middleware: BodyParser())このBodyParser()によって、送られてきたBody部分を適切なプロパティに流してくれるのだと僕は考えています。
Note: If you have not declared a Content-Type header, ParsedBody will be nil.
引用: https://ibm-swift.github.io/Kitura/Classes/BodyParser.htmlただ、リクエスト時に
Content-Type
を指定しないとBody部分はnilになってしまうので注意です。また、今回はCodableなので、
asRaw
プロパティにアクセスします。app.router.post("/api/v1/diary/create") { (request, response, next) in guard let data = request.body?.asRaw else { return next() } guard let diary = try? JSONDecoder().decode(Diary.self, from: data) else { return next() } }上のような流れで、送られてきたBody部分をDiaryモデルにデコードしています。
CREATE (PostgreSQL)
KueryORMが
save
メソッドを用意してくれているのでそれを使いましょう。// 本当は、保存する前に、idがユニークなものかを確認した方が良いです。。。 diary.save() { (diary: Diary?, error: RequestError?) in if let error = error { print(error) return next() } guard let diary = diary else { return next() } response.send(diary) }POSTリクエストの一連の流れ.swiftapp.router.post("/api/v1/diary/create", middleware: BodyParser()) app.router.post("/api/v1/diary/create") { (request, response, next) in guard let data = request.body?.asRaw else { return next() } guard let diary = try? JSONDecoder().decode(Diary.self, from: data) else { return next() } diary.save() { (diary: Diary?, error: RequestError?) in if let error = error { print(error) return next() } guard let diary = diary else { return next() } response.send(diary) } }まとめ
今回は、
- KituraでGETリクエスト・POSTリクエストをどのように行うのか。
- KituraでORMを使用したときにどのような流れでデータの取得・保存を行うのか。
がなんとなくでも掴めていただけたらめちゃくちゃ嬉しいです。
自分もまだ分かっていない部分が多いので、もっとKituraに強くなりたいと思います。ちなみに、Codable Routingを使用して書くともう少しスマートに書けるので別記事で紹介したいです。
- 投稿日:2019-09-15T18:27:19+09:00
ORMを使用してAPIを作る (Kitura/ PostgreSQL)
はじめに
この記事ではIBM製のSwiftでサーバーサイドを実装することのできるフレームワークのKituraを使用してRESTful APIを作成するときの大まかなポイントをまとめていきます。ハンズオンではないです。
今回は、PostgreSQLのORMを併用してデータを保存と取得ができるAPIを作る想定で進めていきます。
今回使用するライブラリのうち、自分で追加する必要のあるライブラリ
ライブラリ名 バージョン SwiftKueryORM 0.6.1 SwiftKueryPostgreSQL 2.1.1 Model
まずは、永続化するモデルの定義から。
SwiftKueryORMには
Model
というプロトコルがあり、それに準拠してモデルを作成します。ModelはCodableに準拠しています。また、
var id: Int?
と記述しておくと、idがnilの場合に自動でidを振り与えてくれるらしいです。注意としては、手動でもidを設定することができるのでそのidがもう既に存在していないかは保存する前に確認した方が良いようです。// id以外のプロパティは適当です。 struct Diary: Model, Equatable { var id: Int? var title: String? var content: String? var posterName: String? var likedCount: Int? var createdAt: Date? var updatedAt: Date? }このあと、PostgreSQLをインストールしたり、Terminal上で
createdb データベース名などを叩いて、初期の設定しなければならないのですが今回は省略します。
それでは、本題に入ります!
GET
Kituraは
Router
というクラスを使用してルーティングを設定します。App
クラスからプロパティとして取得できるのでそれを使用します。app.router.get("/api/v1/diary/read") { (request, response, next) in }getメソッドの第一引数はルーティングを指定します。この場合、ローカル環境なら
http://localhost:8080/api/v1/diary/read
にアクセスすればコールバックが呼ばれます。もしクエリパラメータ
?id=1234
みたいなものを取得したい場合は、request.queryParameters
で取得できます。let id = request.queryParameters["id"]READ (PostgreSQL)
GETリクエストを受け取ったら全件取得をします。
ORMを使用して非常に簡潔にかけます。
取得したいエンティティクラスから.findAll
メソッドを呼ぶだけです。例えば、上のDiaryモデルとして保存したデータを取得したい場合はDiary.findAll { (diaries: [Diary]?, error: RequestError?) in }このような感じで取得することができます。
まとめると、
app.router.get("/api/v1/diary/read") { (request, response, next) in Diary.findAll { (diaries: [Diary]?, error: RequestError?) in guard let diaries = diaries else { return next() } response.send(diaries) next() } }これで、データを取得してレスポンスで返せると思います。
POST
postは初見でやったときは結構はまりました。。。まとめにも記載しますが、Codable Routingを使用しないで
request.body?.asRaw
でData型を取得するにはミドルウェアを指定する必要があるようです。app.router.post("/api/v1/diary/create", middleware: BodyParser())このBodyParser()によって、送られてきたBody部分を適切なプロパティに流してくれるのだと僕は考えています。
Note: If you have not declared a Content-Type header, ParsedBody will be nil.
引用: https://ibm-swift.github.io/Kitura/Classes/BodyParser.htmlただ、リクエスト時に
Content-Type
を指定しないとBody部分はnilになってしまうので注意です。また、今回はCodableなので、
asRaw
プロパティにアクセスします。app.router.post("/api/v1/diary/create") { (request, response, next) in guard let data = request.body?.asRaw else { return next() } guard let diary = try? JSONDecoder().decode(Diary.self, from: data) else { return next() } }上のような流れで、送られてきたBody部分をDiaryモデルにデコードしています。
CREATE (PostgreSQL)
KueryORMが
save
メソッドを用意してくれているのでそれを使いましょう。// 本当は、保存する前に、idがユニークなものかを確認した方が良いです。。。 diary.save() { (diary: Diary?, error: RequestError?) in if let error = error { print(error) return next() } guard let diary = diary else { return next() } response.send(diary) }POSTリクエストの一連の流れ.swiftapp.router.post("/api/v1/diary/create", middleware: BodyParser()) app.router.post("/api/v1/diary/create") { (request, response, next) in guard let data = request.body?.asRaw else { return next() } guard let diary = try? JSONDecoder().decode(Diary.self, from: data) else { return next() } diary.save() { (diary: Diary?, error: RequestError?) in if let error = error { print(error) return next() } guard let diary = diary else { return next() } response.send(diary) } }まとめ
今回は、
- KituraでGETリクエスト・POSTリクエストをどのように行うのか。
- KituraでORMを使用したときにどのような流れでデータの取得・保存を行うのか。
がなんとなくでも掴めていただけたらめちゃくちゃ嬉しいです。
自分もまだ分かっていない部分が多いので、もっとKituraに強くなりたいと思います。ちなみに、Codable Routingを使用して書くともう少しスマートに書けるので別記事で紹介したいです。
- 投稿日:2019-09-15T15:49:11+09:00
位置情報を含んだ画像をMapに埋め込むまでにやったことや参考になったサイト
位置情報がある画像をMapに埋め込むまでの経緯などを備忘録的に掲載していきます。
Mapの表示やピンの表示はこちらです。
【SWIFT】MapViewを利用した現在位置情報取得
- 投稿日:2019-09-15T14:23:02+09:00
UICollectionViewで画像一覧を出した時のメモ
まえおき
UICollectionViewとPhotosフレームワークを初めて使ったので、つまづきそうだなーと思ったところのメモです。
写真appから画像一覧を取得してサムネイルを並べることを目的としています。Xcode 11.0 GM
画像を取得した時のつまづき
Photos
フレームワークで取得します。
PHAsset.fetchAssets
でフェッチした結果のPHFetchResult
から写真のPHAsset
を取得します。この時アクセス権の確認アラートが出現しますが、OKを押しても写真は読み込まれませんでした。どうやらアラートはアセットのフェッチによるものではなく、アセットへのアクセス行為(PHAssetの取得部分)によるもののようです。
フェッチでもアクセス権は必要ですからフェッチは失敗しています。取得には失敗していますから、ここで許可が出ようとも画像は表示できません。よって
PHPhotoLibrary.requestAuthorization
を使用することで、アクセス権が取得できた、もしくは既にアクセス権があるときだけフェッチを開始するようにします。// 取得したPHAssetを格納する配列 var photoAssets: [PHAsset] = [] PHPhotoLibrary.requestAuthorization { (status) in switch status { case .authorized: // 許可された場合のみ読み込み開始 loadPhotos() case .denied: // 拒否されている場合アラートを出すなり設定appへ誘導するなり default: // このほかに restricted と determined が存在する } } func loadPhotos() { // 取得するものをimageに指定 // 取得したい順番などあれば options に指定する let assets: PHFetchResult = PHAsset.fetchAssets(with: .image, options: nil) // PHAssetを一つ一つ格納 assets.enumerateObjects { [weak self] (asset, index, stop) in self?.photoAssets.append(assets[index]) } }これでアクセス権を考慮して一覧を取得できました。
あとはPHAsset
からPHImageManager
を使ってUIImage
として取り出すだけです。UICollectionViewでサムネ表示した時のつまづき
Cellのサイズが思った通りにならない
サムネが4列になるようにCellのサイズを指定しました。
func setup() { // Cellのレイアウト let layout = UICollectionViewFlowLayout() // 列の分割数 let columnsCount = 4 // CollectionViewの枠とCell達の間の隙間 let sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) // Cellの左右のスペースの最小値 let minimumInteritemSpacing: CGFloat = 5.0 // 行の上下のスペースの最小値 let minimumLineSpacing: CGFloat = 5.0 let viewWidth = thumbnailCollectionView.frame.size.width // Cellのサイズ // sectionInsetとminimumInteritemSpacingで決めたスペースからcolumnsCountになるよう調整 let cellWidth = floor((viewWidth - sectionInset.left * 2 - minimumInteritemSpacing * CGFloat(columnsCount - 1)) / CGFloat(columnsCount)) layout.itemSize = CGSize(width: cellWidth, height: cellWidth) layout.minimumInteritemSpacing = minimumInteritemSpacing layout.minimumLineSpacing = minimumLineSpacing layout.sectionInset = sectionInset thumbnailCollectionView.collectionViewLayout = layout }これを
viewDidLoad()
で呼んでしまっていたのが原因でした。thumbnailCollectionViewのサイズが決まっていないためです。
viewDidLayoutSubviews()
で呼ぶようにしたところうまくいきました。素早くスクロールした時にCellの挙動がおかしい
iOS13でスクロールバーを摘んで高速スクロール?が可能になりました。
普通にスクロールする分には問題ないのですが…高速でスクロールした後、なにやらCellの中身が目まぐるしく変化しています。紙芝居みたい。Cellは使い回しますから、初期化がができていないのかな?と思い次の画像を読む前に前画像を
nil
にする処理を入れてみましたが…変わらず原因は高速スクロールによって、
PHImageManager
による画像生成が追いていなかったことでした。
前の画像の生成が終わる前に次の画像が…ということを繰り返し、そのCellに対する生成タスクが溜まっていき、スクロールが止まった時にタスクが順次処理されて順に画像が表示された、ということでした。
PHImageManager
には画像生成をキャンセルするcancelImageRequest
メソッドがあるので、次の画像を生成する前にキャンセルすることで解決しました。ThumbnailListViewController.swiftextension ThumbnailListViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return photoAssets.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = thumbnailCollectionView.dequeueReusableCell(withReuseIdentifier: "ThumbnailCell", for: indexPath) as? ThumbnailCell else { return UICollectionViewCell() } // cellを使い回す前に前のrequestをキャンセルして画像を削除 if let requestId = cell.requestId { imageManager.cancelImageRequest(requestId) } cell.thumbnailImageView.image = nil let item = photoAssets[indexPath.row] cell.requestId = imageManager.requestImage(for: item, targetSize: CGSize(width: 480, height: 480), contentMode: .aspectFit, options: nil) { (image, info) in cell.thumbnailImageView.image = image } return cell } }ThumbnailCell.swiftclass ThumbnailCell: UICollectionViewCell { // サムネを表示するImageView @IBOutlet weak var thumbnailImageView: UIImageView! // Cellで入れる画像のPHImageRequestID public var requestId: PHImageRequestID? override func awakeFromNib() { super.awakeFromNib() // Initialization code } }まとめ
これらのコードをはgitに置いてあります。
よろしければ参考にしてください。iOSたのちい
- 投稿日:2019-09-15T13:29:35+09:00
RxSwiftをCarthageで入れて起動時に`Library not loaded`エラーが出たらRxRelayも追加して解決した
RxSwiftとRxCocoaを追加後、ビルドはサクセスするのですが、実機で動作確認ししようとすると以下のエラーで落ちてしまいました。
dyld: Library not loaded: @rpath/RxRelay.framework/RxRelay Referenced from: /var/containers/Bundle/Application/xxxxxxx-xxxx-xxxx-xxxx-477EBE5CC202/xxx.app/xxx Reason: image not found今回の解決方法は、RxRelayも追加することでした。
RxSwiftライブラリをビルドすると、以下の5つのフレームワークが生成されます。
- RxSwift
- RxCocoa
- RxRelay
- RxBlocking
- RxTest
RxSwift, RxCocoaのみXcodeで追加すれば良いと思っていたのですが、どうやらRxRelayも必要みたいですね?
TARGETS -> General -> LinkedFrameworks and Libraries
にRxRelayも追加し、
Build Phases の Input Files Lists, Output Files Lists にも追記しました。input.xcfilelist$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework $(SRCROOT)/Carthage/Build/iOS/RxCocoa.framework $(SRCROOT)/Carthage/Build/iOS/RxRelay.frameworkoutput.xcfilelist$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/RxSwift.framework $(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/RxCocoa.framework $(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/RxRelay.framework以上で、無事に実機での動作確認を行えるようになりました。
- 投稿日:2019-09-15T13:29:35+09:00
RxSwiftをCarthageで入れたら実機で起動できなかったので、RxRelayも追加して解決した
RxSwiftとRxCocoaを追加後、ビルドはサクセスするのですが、実機で動作確認ししようとすると以下のエラーで落ちてしまいました。
dyld: Library not loaded: @rpath/RxRelay.framework/RxRelay Referenced from: /var/containers/Bundle/Application/xxxxxxx-xxxx-xxxx-xxxx-477EBE5CC202/xxx.app/xxx Reason: image not found今回の解決方法は、RxRelayも追加することでした。
RxSwiftライブラリをビルドすると、以下の5つのフレームワークが生成されます。
- RxSwift
- RxCocoa
- RxRelay
- RxBlocking
- RxTest
RxSwift, RxCocoaのみXcodeで追加すれば良いと思っていたのですが、どうやらRxRelayも必要みたいですね?
TARGETS -> General -> LinkedFrameworks and Libraries
にRxRelayも追加し、
Build Phases の Input Files Lists, Output Files Lists にも追記しました。input.xcfilelist$(SRCROOT)/Carthage/Build/iOS/RxSwift.framework $(SRCROOT)/Carthage/Build/iOS/RxCocoa.framework $(SRCROOT)/Carthage/Build/iOS/RxRelay.frameworkoutput.xcfilelist$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/RxSwift.framework $(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/RxCocoa.framework $(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/RxRelay.framework以上で、無事に実機での動作確認を行えるようになりました。
- 投稿日:2019-09-15T13:20:10+09:00
今回、アプリ内でQRコードを生成して表示する必要があったのでRSBarcodesを使いました
バーコード生成用クラスを作成
import AVFoundation import UIKit import RSBarcodes /// バーコードを生成する final class BarcodeGenerator { /// 生成予定のコードを検証する func validateCode(_ code: String, codeObjectType: AVMetadataObject.ObjectType) -> Bool { return RSUnifiedCodeValidator.shared.isValid( code, machineReadableCodeObjectType: codeObjectType.rawValue ) } /// コードを生成する func generateCodeImage(_ code: String, codeObjectType: AVMetadataObject.ObjectType) -> UIImage? { return RSUnifiedCodeGenerator.shared.generateCode( code, machineReadableCodeObjectType: codeObjectType.rawValue ) } /// スケールを使ってリサイズする func resizeCodeImage(_ image: UIImage, scale: CGFloat) -> UIImage? { return RSAbstractCodeGenerator.resizeImage(image, scale: scale) } /// サイズを指定してリサイズする func resizeCodeImage(_ image: UIImage, size: CGSize, contentMode: UIView.ContentMode) -> UIImage? { return RSAbstractCodeGenerator.resizeImage(image, targetSize: size, contentMode: contentMode) } }専用のクラスを用意して、ライブラリが持つ機能をメソッド化しました。
import AVFoundation
は必須です。QRコードを生成する場合は、
codeObjectType: AVMetadataObject.ObjectType
を.qr
と指定します。
- 投稿日:2019-09-15T12:46:31+09:00
【Swift】UIRefreshControl
UIRefreshControlとは
UIRefresh Controlとは以上の動画のようなテーブルビューを引っ張って、Viewを更新するためのUIコンポーネントと実装するためのクラスです。以下が公式ドキュメントになっています。
https://developer.apple.com/documentation/uikit/uirefreshcontroliOS9以前の実装方法
class View Controller: UITableViewController { override func viewDidLoad() { super.viewDidLoad() refreshControl = UIRefreshControl() refreshControl?.addTarget(self, action: #selector(ViewController.refresh(_:)), forControlEvent: .ValueChanged) } func refresh(sender: UIRefreshControl) { //ここに通信処理などデータフェッチの処理を書く //データフェッチが終わったらUIRefreshControl.endRefreshing()を呼ぶ必要がある。 } }iOS9以前はレフレッシュを使用するためにUIRefreshControlを生成し、それをUITableViewControllerのrefreshControlプロパティにセットする形で使用することで実装していました。
しかしこの時点でUIRefreshControlはUITableViewControllerを使わずにUIViewControllerで管理することが難しく動作が保証されていませんでした。
それについての公式リファレンスに記させれいるので抜粋文を下記に記載しておきます。「Because the refresh control is specifically designed for use in a table view that's managed by a table view controller, using it in a different context can result in undefined behavior.」
iOS 10以降の実装方法
iOS10ではiOS9までの方法も使用することが可能です。
しかしUIScrollViewのプロパティにrefreshControlが追加された事により、UITableViewControllerに依存する事なく使用する事が可能になったのでそれを紹介していきます。@available(iOS 2.0, *) open class UIScrollView : UIView, NSCoding { //省略 @available(iOS 10.0, *) open var refreshControl: UIRefreshControl? }以上のコードのようにUIScrollViewのプロパティに追加されています。
UIViewController管理下のUITableViewに対しての実装
ストーリボードでの表示
ストーリボードの表示を見るとViewController配下にTableViewが配置されています。ここではこのような状況の時の実装方法になります。class ViewController: UIViewController { @IBOutlet weak var tableView: UITavleView! private let refreshControl = UIRefreshControl() override func viewDidLoad() { super.viewDidLoad() tableView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(ViewController.refresh(sender:)), for: .valueChanged) } func refresh(sender: UIRefreshControl) { //ここに通信処理などのデータフェッチの処理を記述する //最後には必ずUIRefreshControl.endRefreshing()で終了する }以上のコードが実装コードになります。ViewControllerクラスの下でUIRefreshControlをインスタンス化、そしてそれをtabelViewに対して使用します。最後にrefresh関数を定義し、その内部でデータ処理とrefreshの終了コードを書けばOKです。
UICollectionViewでの使用
こちららも前回と同じViewController配下での実装になります。
処理内容に前回と大差ないので、コードのみ記しておきます。class ViewController: UIViewController { @IBOutlet weak var collectionView: UICollectionView! private let refreshControl = UIRefreshControl() override func viewDidLoad() { super.viewDidLoad() collectionView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(ViewController.refresh(sender:)), for: .valueChanged) } func refresh(sender: UIRefreshControl) { // ここに通信処理などでのデータフェッチの処理を書きます // データフェッチが終わったらUIRefreshControl.endRefreshing()を呼びます } }UIScrollViewでの実装
以下も処理内容は同じです
class ViewController: UIViewController { @IBOutlet weak var scrollView: UIScrollView! private let refreshControl = UIRefreshControl() override func viewDidLoad() { super.viewDidLoad() scrollView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(ViewController.refresh(sender:)), for: .valueChanged) } func refresh(sender: UIRefreshControl) { // ここに通信処理などデータフェッチの処理を書きます // データフェッチが終わったらUIRefreshControl.endRefreshing()を呼びます } }
- 投稿日:2019-09-15T00:28:04+09:00
Redux+RxSwift+Realm+RxDataSourcesで日記アプリ
Reduxを使ったアプリ開発をしてみたかったのですが、いまいちReduxについてわかってないことが多かったので実際にアプリを作ってみました。
今回、簡単なアプリなのでRxDataSourcesを使う必要はなかったのですが、実際にReduxとRxDataSourcesを使った場合、どのように書くのかを知りたかったので使っています。
コードについては、SwiftやRx、Realmなどすべて独学でやっているので至らない点もあると思います。。。完成形
日記一覧 日記詳細(編集) ファイル構造
Entities: Realmのモデル
Model: RxDataSourcesのSectionModel
Store: ReduxのStore
Actions: ReduxのAction
States: ReduxのState
Reducers: ReduxのReducer以下のような構成で作りました
. ├── Actions │ └── Action.swift ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Entities │ └── DiaryItem.swift ├── Info.plist ├── Model │ └── DiarySectionModel.swift ├── Reducers │ └── Reducer.swift ├── States │ └── State.swift ├── Store │ └── RxStore.swift ├── View │ └── DiaryCell.swift └── ViewControllers ├── DiaryDetailViewController.swift └── DiaryListViewController.swiftReduxとRealmとRxDataSourcesの部分
Entities: Realmのモデルたち
RxDataSourcesのモデルとしても使いたいのでIdentifiableTypeを継承しています。DiaryItem.swiftimport Foundation import RealmSwift import RxDataSources class DiaryItem: Object { @objc dynamic var id = UUID().uuidString @objc dynamic var date = Date() @objc dynamic var text = "" @objc dynamic var createdAt = Date() @objc dynamic var updatedAt = Date() override static func primaryKey() -> String? { return "id" } } extension DiaryItem: IdentifiableType { typealias Identity = String var identity: String { return id } }Model: RxDataSourcesのSectionModel
ItemTypeがDiaryItem(Realmのモデル)を想定しています。DiarySectionModel.swiftimport Foundation import RxDataSources struct DiarySectionModel<ItemType: IdentifiableType> { var header: String var items: [ItemType] init(header: String, items: [Item]) { self.header = header self.items = items } } extension DiarySectionModel: SectionModelType { typealias Item = ItemType init(original: DiarySectionModel<ItemType>, items: [ItemType]) { self.header = original.header self.items = items } }Store: ReduxのStore
今回は、StateをObservableな型として扱いたかったのでRxStoreというクラスを作成しました。
画面ごとにStateを管理しようと思ったのでAnyStateTypeには画面ごとのStateの型が入る想定です。RxStore.swiftimport Foundation import ReSwift import RxSwift import RxCocoa class RxStore<AnyStateType>: StoreSubscriber where AnyStateType: StateType { private let store: Store<AnyStateType> private let stateRelay: BehaviorRelay<AnyStateType> var state: AnyStateType { return stateRelay.value } lazy var stateObservable: Observable<AnyStateType> = { return self.stateRelay.asObservable() }() init(store: Store<AnyStateType>) { self.store = store self.stateRelay = BehaviorRelay(value: store.state) self.store.subscribe(self) } deinit { self.store.unsubscribe(self) } func newState(state: AnyStateType) { stateRelay.accept(state) } func dispatch(_ action: Action) { store.dispatch(action) } }Actions: ReduxのActionたち
DiaryListViewController内のTableViewを更新するためにreloadをおいています。
ActionCreatorやMiddlewareでRealm内のオブジェクトを更新するような設計であればreloadが必要なくなると思います。(この構成にしたあとに気づきました。。。)Action.swiftimport ReSwift // DiaryListViewControllerで使うAction enum DiaryAction: Action { case reload case delete(diary: DiaryItem) } // DiaryDetailViewControllerで使うAction enum DiaryDetailAction: Action { case create case set(diary: DiaryItem) case update(text: String) }States: ReduxのStateたち
State.swiftimport RealmSwift import ReSwift import RxDataSources // DiaryListViewControllerのState struct DiaryState: StateType { var sectionModel: [DiarySectionModel<DiaryItem>] = [] var realm = try! Realm() } // DiaryDetailViewControllerのState struct DiaryDetailState: StateType { var diary = DiaryItem() var realm = try! Realm() }Reducers: ReduxのReducerたち
今回はアプリ全体のStateを統括するReducerは作りませんでした。Reducer.swiftimport Foundation import ReSwift import RealmSwift // DiaryListViewControllerのReducer struct DiaryReducer { static func reduce(_ action: Action, state: DiaryState?) -> DiaryState { var state = state ?? DiaryState() guard let action = action as? DiaryAction else { return state } state.realm.beginWrite() switch action { case .reload: break case .delete(let diary): state.realm.delete(diary) } try! state.realm.commitWrite() let diaries = state.realm.objects(DiaryItem.self).sorted(byKeyPath: "updatedAt", ascending: false) state.sectionModel = [DiarySectionModel(header: "header", items: diaries.map({ $0 }))] return state } } // DiaryDetailViewControllerのReducer struct DiaryDetailReducer { static func reduce(_ action: Action, state: DiaryDetailState?) -> DiaryDetailState { var state = state ?? DiaryDetailState() guard let action = action as? DiaryDetailAction else { return state } state.realm.beginWrite() switch action { case .create: state.realm.add(state.diary) case .set(let diary): state.diary = diary case .update(let text): state.diary.text = text state.diary.updatedAt = Date() } try! state.realm.commitWrite() return state } }View
DiaryCell.swiftimport UIKit class DiaryCell: UITableViewCell { @IBOutlet private weak var diaryDateLabel: UILabel! @IBOutlet private weak var updatedLabel: UILabel! func configure(_ diary: DiaryItem) { diaryDateLabel.text = diary.text let formatter = DateFormatter() formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "ydMMM", options: 0, locale: Locale.current) updatedLabel.text = formatter.string(from: diary.updatedAt) } }ViewController
まず、日記の一覧を表示する画面のViewControllerです。
こちら、プログラム内に説明を記述しました。DiaryListViewController.swiftimport UIKit import RxSwift import RxCocoa import ReSwift import RxDataSources import RealmSwift class DiaryListViewController: UIViewController { @IBOutlet private weak var diaryListView: UITableView! @IBOutlet private weak var addDiaryButton: UIBarButtonItem! // Store private let store = RxStore(store: Store<DiaryState>(reducer: DiaryReducer.reduce, state: nil)) private let disposeBag = DisposeBag() // diaryListViewのdataSource private let dataSource = RxTableViewSectionedReloadDataSource<DiarySectionModel<DiaryItem>>(configureCell: { (_, tableView, indexPath, result) -> UITableViewCell in let cell = tableView.dequeueReusableCell(withIdentifier: "DiaryCell", for: indexPath) as! DiaryCell cell.configure(result) return cell }, canEditRowAtIndexPath: { _, _ in return true }) override func viewDidLoad() { super.viewDidLoad() bind() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // realmの更新を確認 store.dispatch(DiaryAction.reload) } private func bind() { // diaryListViewにdataSourceを適用 store.sectionModel .bind(to: diaryListView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) // Cellがスワイプで削除されたときの処理 diaryListView.rx.itemDeleted .subscribe({ [unowned self] event in guard let indexPath = event.element else { return } self.store.dispatch(DiaryAction.delete(diary: self.dataSource.sectionModels[indexPath.section].items[indexPath.item])) }) .disposed(by: disposeBag) // Cell押下時の画面遷移の処理 diaryListView.rx.itemSelected .subscribe({ [unowned self] event in guard let indexPath = event.element else { return } self.performSegue(withIdentifier: "UpdateDiarySegue", sender: self.dataSource.sectionModels[indexPath.section].items[indexPath.item]) }) .disposed(by: disposeBag) diaryListView.rx.setDelegate(self).disposed(by: disposeBag) // 右上の追加ボタン押下時の処理 addDiaryButton.rx.tap .subscribe({ [unowned self] _ in self.performSegue(withIdentifier: "CreateDiarySegue", sender: nil) }) .disposed(by: disposeBag) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard let vc = segue.destination as? DiaryDetailViewController else { return } if segue.identifier == "CreateDiarySegue" { vc.diary = nil } else if segue.identifier == "UpdateDiarySegue" { guard let diary = sender as? DiaryItem else { return } vc.diary = diary } } } extension DiaryListViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 64.0 } } extension RxStore where AnyStateType == DiaryState { // stateのsectionModelをObservableに変更 var sectionModel: Observable<[DiarySectionModel<DiaryItem>]> { return stateObservable.map({ $0.sectionModel }) } }次に日記を記述する画面のViewControllerです。
DiaryDetailViewController.swiftimport UIKit import RxSwift import ReSwift class DiaryDetailViewController: UIViewController { @IBOutlet private weak var diaryTextView: UITextView! // DiaryListViewControllerから渡されるパラメータ var diary: DiaryItem? // Store private let store = RxStore(store: Store<DiaryDetailState>(reducer: DiaryDetailReducer.reduce, state: nil)) private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() setup() diaryTextView.text = store.state.diary.text bind() } private func setup() { if let diary = diary { // diaryがある場合はdiaryをDiaryDetailStateにセット store.dispatch(DiaryDetailAction.set(diary: diary)) } else { // diaryがない場合はDiaryを作成 store.dispatch(DiaryDetailAction.create) } } private func bind() { // TextViewを記述するたびにRealmに保存 diaryTextView.rx.text .subscribe({ [unowned self] event in self.store.dispatch(DiaryDetailAction.update(text: event.element! ?? "")) }) .disposed(by: disposeBag) } }終わりに
Realmへの保存の処理については、ActionCreatoreやMiddlewareに書いたほうが良いみたいですが最初わからないまま書き始めたのでこのような形になりました。。。
全体のコードはGithubに載せているので参考にしたい方は参考にしてみてください。