20190915のSwiftに関する記事は10件です。

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でやり過ごさずに済み、後に負債を残さない助けに成れれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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リクエストの一連の流れ.swift
app.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を使用して書くともう少しスマートに書けるので別記事で紹介したいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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リクエストの一連の流れ.swift
app.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を使用して書くともう少しスマートに書けるので別記事で紹介したいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

位置情報を含んだ画像をMapに埋め込むまでにやったことや参考になったサイト

位置情報がある画像をMapに埋め込むまでの経緯などを備忘録的に掲載していきます。
Mapの表示やピンの表示はこちらです。
【SWIFT】MapViewを利用した現在位置情報取得

参考サイト
【Swift】UIImagePickerControllerでカメラロールから写真を取得

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.swift
extension 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.swift
class ThumbnailCell: UICollectionViewCell {
    // サムネを表示するImageView
    @IBOutlet weak var thumbnailImageView: UIImageView!
    // Cellで入れる画像のPHImageRequestID
    public var requestId: PHImageRequestID?

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }
}

まとめ

これらのコードをはgitに置いてあります。
よろしければ参考にしてください。

iOSたのちい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.framework
output.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

以上で、無事に実機での動作確認を行えるようになりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.framework
output.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

以上で、無事に実機での動作確認を行えるようになりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今回、アプリ内で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と指定します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】UIRefreshControl

UIRefreshControlとは


UIRefresh Controlとは以上の動画のようなテーブルビューを引っ張って、Viewを更新するためのUIコンポーネントと実装するためのクラスです。

以下が公式ドキュメントになっています。
https://developer.apple.com/documentation/uikit/uirefreshcontrol

iOS9以前の実装方法

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()を呼びます
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.swift

ReduxとRealmとRxDataSourcesの部分

Entities: Realmのモデルたち
RxDataSourcesのモデルとしても使いたいのでIdentifiableTypeを継承しています。

DiaryItem.swift
import 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.swift
import 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.swift
import 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.swift
import 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.swift
import 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.swift
import 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.swift
import 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.swift
import 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.swift
import 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に載せているので参考にしたい方は参考にしてみてください。

https://github.com/azuma317/ReduxDiary

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む