will and way

ただの自分用メモを人に伝える形式で書くことでわかりやすくまとめてるはずのブログ

アクションシートのクロージャをObservable化して処理を一本化する

アクションシートってよくつかわれるんですかね? 私が携わっているプロジェクトではそこそこ使われています。

f:id:matsuokah:20171012231640g:plain

コレです

アクションシートの基本的な使い方

let actionSheet = UIAlertController(title:"Title", message: "Message", preferredStyle: .actionSheet)

let action1 = UIAlertAction(title: "Action 1", style: .default) {
    (action: UIAlertAction!) in
    print("Selected Action!")
}

// キャンセル
let cancel = UIAlertAction(title: "Cancel", style: .cancel) {
    (action: UIAlertAction!) in
    print("Selected Cancel!")
}

actionSheet.addAction(action1)
actionSheet.addAction(cancel)

present(alert, animated: true, completion: nil)
  1. UIAlertControllerをつくる
  2. アクションを追加する
  3. 表示する

これが基本的な使い方です。

また、UIAlertActionには選択されたときのクロージャが用意されていて、都度、処理を定義する必要があります。

パッとみて感じるのが、繰り返しの記述ということです。 こういうのをループにしたくなるのがプログラマの性ですよね。

ここで私は2つ思い浮かびました

  • UIAlertActionの生成を共通化できないか?
  • 「どれが選択されたか」をストリームとして扱えないか?

ということで、Rxをつかってコレを実現したいと思います。

アクションシートの特徴

簡単に述べると下記の3点

  • アクションシート自体はタイトルとメッセージを持つ
  • アクション毎にタイトルと選択された時の処理を持つ
  • キャンセルは例外

選択をObservable化するにあたっての方針

  • アクションの生成元となる情報の配列を引数に取るメソッドを定義し、UIAlertActionの生成をまとめる。
  • どのアクションが選択されたかをenumで表現する
  • アクションシートの起動はUIViewControllerのextensionにまかせてします

実装

早速実装です。 なお、Githubにもソースを上げてます

ActionSheetAction.swift

まずは、アクションシートの各行を表現する構造体を定義します

// MARK: - Action
internal struct ActionSheetAction<Type: Equatable> {
    internal let title: String
    internal let actionType: Type
    internal let style: UIAlertActionStyle
}

ジェネリクス(Type)を用いることによって外からアクションのタイプ(enum)をインジェクトできるようにします。
この型は最終的にObservableに用います。

UIViewController+RxActionSheet.swift

次に、UIViewControllerにUIAlertControllerを表示するextensionを定義します

import UIKit
import RxSwift

internal extension UIViewController {
    internal func showActionSheet<Type>(title: String?, message: String? = nil, cancelMessage: String = "Cancel", actions: [ActionSheetAction<Type>]) -> Observable<Type> {
        let actionSheet = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)       
        return actionSheet.addAction(actions: actions, cancelMessage: cancelMessage, cancelAction: nil)
            .do(onSubscribed: { [weak self] in
                self?.present(actionSheet, animated: true, completion: nil)
            })
    }
}

サブスクライブされた時にアクションシートを表示しています

UIAlertController+RxActionSheet.swift

最後に、UIAlertControllerにアクション追加と同時にObservable化して返すextensionを実装します

import UIKit
import RxSwift

internal extension UIAlertController {
    internal func addAction<Type>(actions: [ActionSheetAction<Type>], cancelMessage: String, cancelAction: ((UIAlertAction) -> Void)? = nil) -> Observable<Type> {
        return Observable.create { [weak self] observer in
            actions.map { action in
                return UIAlertAction(title: action.title, style: action.style) { _ in
                    observer.onNext(action.actionType)
                    observer.onCompleted()
                }
                }.forEach { action in
                    self?.addAction(action)
            }
            
            self?.addAction(UIAlertAction(title: cancelMessage, style: .cancel) {
                cancelAction?($0)
                observer.onCompleted()
            })
            
            return Disposables.create {
                self?.dismiss(animated: true, completion: nil)
            }
        }
    }
}

こんな感じでObservableを作成し、各アクションのクロージャでactionTypeとともにonNextを発行してあげることで
アクションシートの選択をストリーム化することができました。

使い方

AnimalSelectAction.swift

まずアクションの一覧をenumで定義します

enum AnimalSelectAction: String {
    case dog, cat, rabbit, panda

    static var AnimalSelectActions: [AnimalSelectAction] {
        return [.dog, .cat, .rabbit, .panda]
    }
}

ViewController.swift

import UIKit
import RxSwift

final class ViewController: UIViewController {

    private let disposeBag = DisposeBag()
    
    @IBOutlet weak var showActionSheetButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        showActionSheetButton.rx
            .tap
            .asDriver()
            .drive(onNext: { [weak self] _ in
                self?.showActionSheet()
            }).disposed(by: disposeBag)
    }
}

private extension ViewController {
    func showActionSheet() {
        let actions = AnimalSelectAction.AnimalSelectActions
            .map {
                return ActionSheetAction(title: $0.rawValue, actionType: $0, style: .default)
        }

        // アクションシートを表示し、返ってくるObservableをSubscribeしておく。
        showActionSheet(title: "Which Do you like?", actions: actions)
            .subscribe { (event: Event<AnimalSelectAction>) in
                NSLog(event.debugDescription)
            }.disposed(by: disposeBag)
    }
}

動作

f:id:matsuokah:20171012225321g:plain

まとめ

ということで、ViewController側ではアクションシートの選択肢の列挙とそれに対して選択された時の記述をするだけで良くなりました。
UIAlertControllerもほぼ意識せずにできてるのはメリデメあるかもですが記述が簡潔になったかと思います。
extension最高\(^o^)/

サンプルリポジトリ

github.com

DecodableのDecodeを簡潔に書きたい

Swift4でDecodableつかってますか〜?

公式にサポートしてもらえると本当にありがたいですよね。

しかしながら汎用的なパースをしようとすると若干、コードが冗長になります

Encoding and Decoding Custom Types | Apple Developer Documentation

公式ドキュメントによると、カスタムなデコードの戦略を図る場合は下記のようにイニシャライザとデコードを実装する必要があります

extension Coordinate: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        latitude = try values.decode(Double.self, forKey: .latitude)
        longitude = try values.decode(Double.self, forKey: .longitude)
        
        let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
        elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
    }
}

冗長ですね。値がnilや予期しない値だった場合のハンドリングが考慮されていないのでバギーです
これらを簡潔に書く軽いExtensionを作ってみたので、例をもとに説明してみたいと思います。

目指した要件は2点です

  1. パースが失敗したときのデフォルト値を設定する
  2. String以外で表現できるプリミティブな型がStringで渡ってくる場合のフォールバック

1. パースが失敗したときのデフォルト値を設定する

犬が受けたワクチンをリストするAPIのJSONのレスポンスを例にしてみます。

Dog.json
[
  {
    "name": "Spring",
    "vaccinations": [
      {
        "name": "Rabies"
      },
      {
        "name": "Corona"
      }
    ]
  }
]

構造体

Dog.swift
// 予防接種の種類
struct Vaccination: Decodable {
    let name: String
}

struct Dog: Decodable {
    let name: String
    let vaccinations: [Vaccination]
}

例えば、予防接種を受けた数を表現するとき、受けたことがなかったら空の配列を作りたいとします。

しかし、空の配列は返さず、nullもしくはそもそもそのKey&Valueを返さないAPIなどもありえます。

Dog.json
[
  {
    "name": "Spring",
    "vaccinations": null    
  }
]

この場合、デコードエラーになるのでパースエラーのときにはvaccinationsに空配列をセットする実装を追加しましょう

Dog.swift
struct Dog: Decodable {
    let name: String
    let vaccinations: [Vaccination]

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)

        do {
            vaccinations = try values.decode([Vaccination].self, forKey: .vaccinations)
        } catch {
            vaccinations = []
        }
    }

    private enum CodingKeys: String, CodingKey {
        case name
        case vaccinations
    }
}

問題点

  1. do - try - catchが冗長
  2. パラメータが増える度にdo - try - catchを実装する必要がある
  3. decodeの型を明示的に指定する必要がある

ということで

失敗時のデフォルト値を指定しつつ、型推論でよしなに型を指定してくれる実装をしたいと思います。

KeyedDecodingContainerを拡張する

decoder.container(keyedBy: CodingKeys.self)で取得できる型の拡張を書くのが手っ取り早いです

KeyedDecodingContainer+Helper.swift
extension KeyedDecodingContainer {
    func decode<ResultType: Decodable>(forKey key: Key, defaultValue: ResultType? = nil) -> ResultType? {
        do {
            return try decode(ResultType.self, forKey: key)
        } catch let error {
            NSLog(error)
            return defaultValue
        }
    }
}

これで5行が1行になりました

Dog.swift
struct Dog: Decodable {
    let name: String
    let vaccinations: [Vaccination]

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        vaccinations = values.decode(forKey: .vaccinations) ?? []
    }

    private enum CodingKeys: String, CodingKey {
        case name
        case vaccinations
    }
}

2. String以外で表現できるプリミティブな型がStringで渡ってくる場合のフォールバック

次に犬の名前と年をパースする例を上げてみます。

Dog.json
[
  {
    "name": "Spring",
    "age": "10"
  }
]
Dog.swift
struct Dog: Decodable {
    let name: String
    let age: Int

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        age = values.decode(forKey: .age) ?? 0
    }
}

ageがこれでは0になってしまいます。

ここで

  1. StringからIntにパースする
  2. それでも失敗したらデフォルト値にする

という要件をKeyedDecodingContainerのextensionで実装してみます

KeyedDecodingContainer+Helper.swift
extension KeyedDecodingContainer {
    func decodeWithString<ResultType: FallbackableResultType>(forKey key: Key, defaultValue: ResultType? = nil) -> ResultType? {
        do {
            return try decode(ResultType.self, forKey: key)
        } catch let error {
            NSLog(error)
            do {
                let string = try decode(String.self, forKey: key)
                return ResultType.toResultType(string: string, defaultValue: defaultValue)
            } catch let error {
                NSLog(error)
                return defaultValue
            }
        }
    }
}

internal protocol FallbackableType {
    static func toResultType(string: String, defaultValue: Self?) -> Self?
}

typealias FallbackableResultType = FallbackableType & Decodable

extension Int: FallbackableType {
    internal static func toResultType(string: String, defaultValue: Int?) -> Int? {
        return Int(string) ?? defaultValue
    }
}

これで、無事、デコードできるようになりました。

肝としては2点

  1. 最初に各型にデコードできるかトライ
    1. age: 10だった場合でも処理できるのでAPIを修正した場合にバグにならない
  2. FallbackableTypeを用意して、String→各型への変換処理を移譲している
    1. FallbackableTypeに準拠した型に制限していますので、Stringから該当する型に変換する戦略を書く必要があります。

まとめ

ということで、KeyedDecodingContainerを拡張すればデコード処理は簡潔に書ける〜という話でした

汎用的なAPIClientの設計と実装

TL; DR

Swift4でDecodableを使いつつ、
フレキシブルなAPIレスポンスの設計をしていったら結局APIクライアント書いてたという話。

その設計・実装の流れを綴りました。

長くて読みきれないっていう場合はソースコード読んでもらったほうがいいと思います。

github.com

利用ライブラリ

  • Alamofire
  • Result
  • RxSwift

今回は上記を用いてGithubのSearch APIのクライアントを書いてみました

APIClientの抽象化ポイント

まずは抽象化したいポイント、いわゆるジェネリクスで振る舞いの差し替え可能なポイントをさがします。

  1. リクエスト
    1. エンドポイント
    2. パラメータ
  2. レスポンス
    1. パース
    2. エラー

上記の通り、APIクライアントはリクエストに対し対応するレスポンスがあるという点がジェネリクスとの相性がいいです。

リクエストから抽象化していきましょう

リクエスト

protocol Request {
    var endpoint: URL { get }
    var parameters: Alamofire.Parameters { get }
    var responseFormat: ResponseFormat { get }
    var encoding: ParameterEncoding { get }
}

リクエストはこのようにエンドポイントとパラメータを抽象化します。

responseFormatはenumで対象とするフォーマットを列挙しておきます。
今回はレスポンスの型にDecodableを使うので、期待しているレスポンスのフォーマットに応じてデコーダーを差し替える必要があるので、
予めリクエストに仕込むのがいいです。

まずはエンドポイントから

protocol Endpoint {
    var endpoint: URL { get }
    var root: URL { get }
    var path: String { get }
}

URLの構成としてはルートが有り、そちらにパスを付けてURLを完成する形式が汎用的でよいです。

例えば、GithubのSearch APIのレスポンス

// MARK: - GitHubAPIEndpoint
protocol GitHubAPIEndpoint: Endpoint {
    var functionPath: String { get }
}

extension GitHubAPIEndpoint {
    var root: URL {
        return URL(string: URLConstants.GithubAPIURLRoot)!
    }
    
    var endpoint: URL {
        return root.appendingPathComponent([functionPath, path].joined(separator: "/"))
    }
}

enum GithubSearchAPIEndpoint: String, GitHubAPIEndpoint {
    case repositories
    
    var path: String {
        return self.rawValue
    }

    var functionPath: String {
        return "search"
    }
}

GithubのAPIはroot+[機能]+[機能を絞ったパス]という設計なので、functionPathを追加しています。

そこで、機能毎にenumを定義することで[機能を絞ったパス]はenumにまかせています。

パラメータの抽象化とenumで扱えるようにする

protocol ParameterKey: Hashable {
    var key: String { get }
}

protocol AlamofireParameters {
    var parameters: Alamofire.Parameters { get }
}

protocol Parameter {
    associatedtype Key: ParameterKey
    
    var parameter: [Self.Key: Any] { get set }
    mutating func setParameter(_ value: Any, forKey key: Key)
}

extension Parameter {
    mutating func setParameter(_ value: Any, forKey key: Key) {
        parameter[key] = value
    }
    mutating func setParameter<T: RawRepresentable>(_ value: T, forKey key: Key) where T.RawValue == String {
        parameter[key] = value.rawValue
    }
}

extension AlamofireParameters where Self: Parameter {
    var parameters: Alamofire.Parameters {
        return parameter.associate { (key, value) in
            return (key.key, value)
        }
    }
}

検索APIのパラメータは下記の通り

struct SearchAPIParameter: Parameter { 
    var parameter: [SearchAPIParameterKey : Any] = [:]
    
    typealias Key = SearchAPIParameterKey
    
    init() {}
    
    enum SearchAPIParameterKey: String, ParameterKey {
        case q
        case sort
        case order
    
        var key: String {
            return self.rawValue
        }
    }
}

extension SearchAPIParameter:  AlamofireParameters {}
varr parameter = SearchAPIParameter()
parameter.setParameter(q, forKey: .q)

こんな感じで、クエリがenumで指定できるようにします。

これで、パラメータとエンドポイントをprotocol化して汎用的なリクエストを作ることができるようになりました。

クライアント

基本方針はGenericsによって返り値の型を特定し、結果がマップされるようにします。

protocol APIClient {
    var sessionManager: SessionManager { get }
    func get<Response: Decodable>(apiRequest: Request) -> Observable<Response>
}

internal extension APIClient {
    internal func _get<Response>(apiRequest: Request) -> Observable<Response> where Response: Decodable {
        return request(method: .get, apiRequest: apiRequest)
    }
}

private extension APIClient {
    private func request<Response: Decodable>(method: HTTPMethod, apiRequest: Request) -> Observable<Response> {
        return Single.create { observer in
            weak var request = self.sessionManager.request(apiRequest.endpoint, method: method, parameters: apiRequest.parameters, encoding: apiRequest.encoding)
            request?
                .validate()
                .responseData(queue: DispatchQueueManager.shared.queue) {
                    self.handleResponse(response: $0, observer: observer)
            }
            return Disposables.create {
                request?.cancel()
            }
        }.asObservable()
    }

    private func handleResponse<Response: Decodable>(response: DataResponse<Data>, observer: ((SingleEvent<Response>) -> Void)) {
        guard let data = response.data else {
            return observer(.error(APIError()))
        }
        
        let jsonDecoder = JSONDecoder()
        do {
            let parsed = try jsonDecoder.decode(Response.self, from: data)
            observer(.success(parsed))
        } catch let error {
            observer(.error(error))
        }
    }
}

基本形はこのようになります。

これで各クライアントに APIClientのプロトコルを付加すれば通信できるようになりました

また、戻り値の中身であるResponseにDecodableである制約を設けています。
こうすることによってパースの戦略は各Responseに任せつつ、Responseの型が差し替え可能になるので
拡張に開くことができています。

レスポンスのハンドリング

APIの中身をパースして異常だった場合にそのハンドリングをしたいケースもありえますが
ただ、Response型に欲しい型を指定した場合にそのインターセプトができません、

また、ベースとなるAPIのレスポンスのフォーマットがある場合何度も書くのは面倒です

例えば、下記の場合、Itemsの中身だけがT型として変わる場合です。

{
    total_count: 841,
    incomplete_results: false,
    items: [
        {}
    ]
}

そこで、

  1. Result型でsuccess, failureを透過的に扱う
  2. Result型からsuccessの時はResultからResponseをアンラップする

という要件が出てきます

まずはItemのような差し替え可能な型を内包するProtocolを定義します

protocol BasicAPIResponse: Decodable {
    associatedtype ResponseT: Decodable
}

次に、Githubに特化したベースとなるレスポンスの型を定義します

こんな感じで、T(itemsの型)を差し替え可能なようにしておきます

struct GithubAPIResponseBase<T: Decodable>: BasicAPIResponse {
    typealias ResponseT = T
    typealias ErrorT = APIError
        
    private let total_count: Int
    private let incomplete_results: Bool
    private let items: ResponseT?
}

次に結果をResultで受け取れるようにします
Result型はsuccess, failureの2択で結果を透過的に扱えるようにしているenumのライブラリです

protocol APIResult {
    associatedtype ResponseT: Decodable
    associatedtype ErrorT: Swift.Error
    
    var result: Result<ResponseT, ErrorT> { get }
}

extension GithubAPIResponseBase: APIResult {
    var result: Result<ResponseT, APIError> {
        guard let response = items else { return .failure(APIError()) }
        return .success(response)
    }
}

これで、特別なAPIのパース処理を書きたい時はGithubAPIResponseBaseのイニシャライザを用意すれば良くなりました。
しかしながらResult型はAPIの結果なのでクライアントを呼び出す側では意識したくありません。 そこでClientでResultをアンラップする仕組みを考えます

ここで使えるのがType Erasureです

struct AnyAPIResult<ResponseT: Decodable, ErrorT: Swift.Error>: APIResult {

    let _result: () -> Result<ResponseT, ErrorT>
    init<Base: APIResult> (_ base: Base) where Base.ResponseT == ResponseT, Base.ErrorT == ErrorT {
        _result = { () -> Result<ResponseT, ErrorT> in
            return base.result
        }
    }

    var result: Result<ResponseT, ErrorT> {
        return _result()
    }
}

protocol AnyAPIResultConvartibleType: APIResult {
    var anyAPIResult: AnyAPIResult<Self.ResponseT, Self.ErrorT> { get }
}

extension AnyAPIResultConvartibleType {
    var anyAPIResult: AnyAPIResult<Self.ResponseT, Self.ErrorT> {
        return AnyAPIResult(self)
    }
}

TypeErasureを使ってAnyAPIResultを定義することで、

extension GithubAPIResponseBase: AnyAPIResultConvartibleType {}

GithubAPIResponseBaseをAnyAPIResultConvartibleTypeとして扱うことができるようになりました。
結果的にResult<Response, Error>の型がわかっていなくても、Anyがそこを回避してアンラップ処理を共通化することができるようになりました

extension Observable {
    func unwrapResult<T, E>() -> Observable<T> where Element == AnyAPIResult<T, E> {
        return self
            .asObservable()
            .map { result in
                switch result.result {
                case .success(let t):
                    return t
                case .failure(let e):
                    return Observable.error(e)
                }
            }
    }
}

最終的なGithubAPIClient

struct GithubAPIClient: APIClient {
    var sessionManager: SessionManager
    
    init() {
        sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
    }
}

extension GithubAPIClient {
    func get<Response>(apiRequest: Request) -> Observable<Response> where Response : Decodable {
        typealias BasicResponse = GithubAPIResponseBase<Response>
        return _get(apiRequest: apiRequest)
            .map { (res: BasicResponse) in
                return res.anyAPIResult
            }
            .unwrapResult()
    }
}

ということで、外から欲しい型を注入し、戻り値の型から欲しい型を決めてレスポンスをパースするということが実現できました。

Github Search APIをたたくサービスをつくる

あとは、Itemsの中身の型を定義して

struct GithubRepositoryDTO: Decodable {
    let id: Int    
//以下略
}

Clientを使うサービスを定義し、戻り値の型に戻り値としたい型を指定するだけです。

protocol APIService {
    var client: APIClient { get }
}

protocol SearchAPI: APIService {
    func searchRepository(q: String) -> Observable<[GithubRepositoryDTO]>
}

struct SearchAPIService: SearchAPI {
    
    var client: APIClient {
        return _client
    }
    
    var _client = GithubAPIClient()

    func searchRepository(q: String) -> Observable<[GithubRepositoryDTO]> {
        var parameter = SearchAPIParameter()
        parameter.setParameter(q, forKey: .q)
        return client.get(apiRequest: APIRequest(apiEndpoint: GithubSearchAPIEndpoint.repositories, apiParameters: parameter, encoding: URLEncoding.default))
    }
}

まとめ

ということで、ほぼ、サービスやHTTPクライアントに依存しないインターフェイスができました。

APIClientの実装をAlamofireから別のHTTPクライアントにしたり、レスポンスをデコードする戦略を書き換えたりしても他の層には基本的に影響がない実装ができました。

ここまで、自分で読み返してもうまく説明できてる気がしないのでソースコードを見ることをおすすめします(笑)

github.com

Swift4のDictionaryのアプデの目玉が公式ブログ?に掲載されてたので読んでみた

原文はこちら

Swift4ではDictionaryがより便利に使えるようになっているということで サンプルを見ていく

Grouping Values By a Key

Swift3系では

グループ機能がなく、かなりの手順を要していた

// Swift <= 3.1
var grouped: [GroceryItem.Department: [GroceryItem]] = [:]
for item in groceries {
    if grouped[item.department] != nil {
        grouped[item.department]!.append(item)
    } else {
        grouped[item.department] = [item]
    }
}
  1. まずグループの配列があるかチェック
  2. あれば配列についか
  3. なければ配列を作ってgroupedに登録

これがSwift4ではDictionaryのイニシャライザに用意され、1行で書くことができるようになりました。

// Swift >= 4.0
let groceriesByDepartment = Dictionary.init(grouping: groceries, by: { item in item.department })

このまま紹介しても味気ないので僕は下記のように使ってみました

extension Sequence {
    func group<Key>(by predicate: (Element) throws -> Key) rethrows -> [Key: [Element]] {
        return try Dictionary(grouping: self, by: predicate)
    }
}

let groceriesByDepartment = groceries.group(by: { item in item.department })
// [seafood: [{…}, {…}], bakery: [{…}, {…}], produce: [{…}, {…}, {…}]]

grouping対象はSequenceに準拠している必要があるということで、もはやSequeceに用意しちゃえばいいじゃん!と。

Transforming a Dictionary’s Values

Swift3まではmapすると、(key, value)形式で走査してましたがmapValueを使えば、valueだけを走査することができるようになりました。

なお、transformの対象はValueのみ。

let nameByDepartment = groceriesByDepartment.mapValues { items in
    items.map { item in
        item.name
    }
}

// [seafood: ["Salmon", "Shrimp"], bakery: ["Croissants", "Bread"], produce: ["Apples", "Bananas", "Grapes"]]

Uniquing Keys With

let pairs = [("dog", "🐕"), ("cat", "🐱"), ("dog", "🐶"), ("bunny", "🐰")]
let petmoji = Dictionary(pairs,
                         uniquingKeysWith: { (old, new) in new })
// petmoji["cat"] == "🐱"
// petmoji["dog"] == "🐶"

SequenceのElementの型がCompound Typeなのでこちらはextensionかけない…

Using Default Values

for item in [🍌, 🍌, 🍞] {
    cart[item, default: 0] += 1
}

下記でも解説されてますね

Swift 4の魅力の一面を3行で表す - Qiita

Merging Two Dictionaries Into One

var cart = [🍌: 1, 🍇: 2]
let otherCart = [🍌: 2, 🍇: 3]
cart.merge(otherCart, uniquingKeysWith: +)

この実装見ていて初めて気づいたんですが、closureとしてオペレータ渡せるんですね。
知らなかったので今後の実装に使えそう。

func +(left: T, right: T) -> T { }

冷静に考えると、+ operatorはinfixな関数なので、なるほどSwiftよくできてるな〜と感心した案件でした。

RxCocoaのUITableViewのbind(to: )にRegistrableを使って処理の簡略化

前提

qiita.com

Registrable型に則ればあとは型推論によるextensionの実装で済ませようというアプローチです

キャストが失敗したら?だったり、各型のIdentifierを取得する手間をextensionに閉じ込めることができるので、
シーケンスに集中することができるためコードの見通しが良くなります。

RxCocoaのBindToにこの仕組を使う

RxCocoaではbind(to)でシーケンスなElementをUITableViewのデータソースとして扱い、それを表示する拡張があります。

下記がその実装になります。

items<S: Sequence, Cell: UITableViewCell, O : ObservableType> (cellIdentifier: String, cellType: Cell.Type = Cell.self)

    public func items<S: Sequence, Cell: UITableViewCell, O : ObservableType>
        (cellIdentifier: String, cellType: Cell.Type = Cell.self)
        -> (_ source: O)
        -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void)
        -> Disposable
        where O.E == S {
        return { source in
            return { configureCell in
                let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper<S> { (tv, i, item) in
                    let indexPath = IndexPath(item: i, section: 0)
                    let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! Cell
                    configureCell(i, item, cell)
                    return cell
                }
                return self.items(dataSource: dataSource)(source)
            }
        }
    }

しかしこのままではRegistrableにしているのにIdentifierをまた書かなければなりません。そこで、Registrableの仕組みをextensionに内包してしまおうという発送です。

デフォルトの実装に対してRegistrableを使ったインターフェイスを定義して、処理を軽くラップすれば実現することができます。

実際の変更点としては2点です

  1. cellTypeのみを引数にとるようにジェネリクスの型を調整
  2. 内部でcellIdentifierにはRegistrable.reuseIdentifireを使ってもとの実装に処理を委譲

UITableView+Rx.swift

extension Reactive where Base: UITableView {
    public func items<S: Sequence, Cell: UITableViewCell & NibRegistrable, O : ObservableType>
        (cellType: Cell.Type = Cell.self)
        -> (_ source: O)
        -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void)
        -> Disposable
        where O.E == S {
            return self.items(cellIdentifier: cellType.reuseIdentifier, cellType: cellType.self)
    }
}

こうすることで、tableViewの処理が4行(bind() {}で書いているところ)で収まります。

サンプル(ViewController.swift#L39-L42)

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    let searchModel = GithubSearchModel()
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(SearchResultCell.self)        
        bind()
        searchModel.search(q: "Rx")
    }
}

private extension ViewController {
    func bind() {
        searchModel.searchResult.asObservable()
            .bind(to: tableView.rx.items(cellType: SearchResultCell.self)) { (row, element, cell) in
                cell.setData(data: element)
            }.disposed(by: disposeBag)
    }
}

上記の前提としては

  1. RegistrableでUITableViewCellの登録処理や取得処理をラップ
  2. Modelには検索結果をVariable<ResultData>として公開しておき、ViewController側わModelをSubscribeしておく

ただし、このbind(to:)は複数のcellのタイプを使えないのでご注意を。。。

objc_getAssociatedObject で必ずnil返ってくる件(解決済み)

blog.matsuokah.jp

こちらのエントリーでExtensionPropertyをExtensionとしてつけるだけでassociated objectにアクセスしやすくするというエントリーを書きました

しかしながら、この記事にはバグがありました。 それは getProperty が必ずnilになってしまい、デフォルト値が必ず返ってきていました。

原因

該当するコード

public extension ExtensionProperty {
    public func getProperty<K: ExtensionPropertyKey, V>(key: K, initialValue: V) -> V {
        var keyString = key.keyString

        if let variable = objc_getAssociatedObject(self, &keyString) as? V {
            return variable
        }

        setProperty(key: key, newValue: initialValue)
        return initialValue
    }

    public func setProperty<K: ExtensionPropertyKey, V>(key: K, newValue: V) {
        objc_setAssociatedObject(self, key.keyString, newValue, .OBJC_ASSOCIATION_RETAIN)
    }
}

var keyString = key.keyString

Stringをコピーしていたため、インスタンスのメモリの番地が違ってしまっていた。結果的にキーが違うのでnilが返ってくるという理屈でした。

対応

本当はExtensionPropertyKeyのkeyStringの参照を渡すことができれば容易なのですが、 inoutな引数にletな値を渡すことはできません。
そこで、UnsafeRawPointerを取得して渡すことで解決しました。これならenumをキーとして使いたいという当初の目的を達成しています。

internal extension ExtensionPropertyKey {
    var keyPointer: UnsafeRawPointer {
        return unsafeBitCast(self.keyString, to: UnsafeRawPointer.self)
    }
}


// MARK: - ExtensionProperty
public extension ExtensionProperty {
    public func getProperty<K: ExtensionPropertyKey, V>(key: K, defaultValue: V) -> V {
        if let variable = objc_getAssociatedObject(self, key.keyPointer) as? V {
            return variable
        }
        
        setProperty(key: key, newValue: defaultValue)
        return defaultValue
    }
    
    public func setProperty<K: ExtensionPropertyKey, V>(key: K, newValue: V, policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN) {
        objc_setAssociatedObject(self, key.keyPointer, newValue, policy)
    }
}

いや〜。ナマのポインタは最強ですね

UserDefaultsをSwiftのEnumで扱えるように拡張する

UserDefaultsって便利ですよね。

基本的にはユーザーのアプリ内の設定値保存に使うことを主としていますが、
キルされても保持したいけど、アンインストール→インストールでは消されてもいい。DBを作るまでもないといった
値の軽いキャッシュとして利用したりもできます。

UserDefaultsのキーはString...

Stringって安易に使うとバギーですよね。

どこかでKeyを集約書けばよくね?って思いますがそれってつまり列挙なんです。 ということで、enumでラップして使ってみました。

アプローチ

  1. キーのprotocol UserDefaultKeyを定義
  2. UserDefaultKeyを引数にとって各々の型の値を返すextensionを定義
  3. UserDefaultKeyを使ったUserDefaultのデータストアの実装

1. キーのprotocol UserDefaultKeyを定義

public protocol UserDefaultKey: RawRepresentable, CustomStringConvertible { }

public extension UserDefaultKey where RawValue == String {
    public var description: String {
        return self.rawValue
    }
}

Enumって実はよしなにコンパイラが作ってくれるEquatableRawRepresentableに準拠しているstructのstaticな変数の列挙のようなものです。
そこでEnumを包含する方の定義としてRawRepresentableを定義しておきます。

今回はCustomStringConvertible.descriptionをキーとして使う戦略です。

なので、RawValueがStringのときはdescriptionの値としてself.rawValueを返せばいいことになります

したがって下記のようにenumを作るだけで、キーとして扱えるということです

enum DefaultKeys: String, UserDefaultKey {
    case autoReloadEnabled
}

2. UserDefaultKeyを引数にとって各々の型の値を返すextensionを定義

UserDefaultKeyを受け取れるインターフェースとして定義しています。
これで透過的にUserDefaultKeyをキーとして扱えるようになりました

// MARK: getter
public extension UserDefaults {
    public func object<Key: UserDefaultKey>(forKey key: Key) -> Any? {
        return object(forKey: key.description)
    }
    public func url<Key: UserDefaultKey>(forKey key: Key) -> URL? {
        return url(forKey: key.description)
    }
    
    public func array<Key: UserDefaultKey>(forKey key: Key) -> [Any]? {
        return array(forKey: key.description)
    }
    
    public func dictionary<Key: UserDefaultKey>(forKey key: Key) -> [String: Any]? {
        return dictionary(forKey: key.description)
    }
    
    public func string<Key: UserDefaultKey>(forKey key: Key) -> String? {
        return string(forKey: key.description)
    }
    
    public func stringArray<Key: UserDefaultKey>(forKey key: Key) -> [String]? {
        return stringArray(forKey: key.description)
    }
    
    public func data<Key: UserDefaultKey>(forKey key: Key) -> Data? {
        return data(forKey: key.description)
    }
    
    public func bool<Key: UserDefaultKey>(forKey key: Key) -> Bool? {
        return bool(forKey: key.description)
    }
    
    public func integer<Key: UserDefaultKey>(forKey key: Key) -> Int? {
        return integer(forKey: key.description)
    }
    
    public func float<Key: UserDefaultKey>(forKey key: Key) -> Float? {
        return float(forKey: key.description)
    }
    
    public func double<Key: UserDefaultKey>(forKey key: Key) -> Double? {
        return double(forKey: key.description)
    }
}

// MARK: - setter
public extension UserDefaults {
    public func set<Key: UserDefaultKey>(_ value: Any?, forKey key: Key) {
        set(value, forKey: key.description)
    }
    
    public func set<Key: UserDefaultKey>(_ value: URL?, forKey key: Key) {
        set(value, forKey: key.description)
    }
    
    public func set<Key: UserDefaultKey>(_ value: Bool, forKey key: Key) {
        set(value, forKey: key.description)
    }
    
    public func set<Key: UserDefaultKey>(_ value: Int, forKey key: Key) {
        set(value, forKey: key.description)
    }
    
    public func set<Key: UserDefaultKey>(_ value: Float, forKey key: Key) {
        set(value, forKey: key.description)
    }
    
    public func set<Key: UserDefaultKey>(_ value: Double, forKey key: Key) {
        set(value, forKey: key.description)
    }
}

3. UserDefaultKeyを使ったUserDefaultのデータストアの実装

protocol UserDefaultsDataStore {
    var autoReloadEnabled: String? { get set }
}

fileprivate enum UserDefaultsDataStoreKeys: String, UserDefaultKey {
    case autoReloadEnabled
}

struct UserDefaultsDataStoreImpl: UserDefaultsDataStore {
    var autoReloadEnabled: Bool {
        get {
            return bool(forKey: .autoReloadEnabled) ?? false
        }
        set {
            set(value: newValue, forKey: .authToken)
        }
    }

    private var defaults: UserDefaults {
        return UserDefaults.standard
    }
}

private extension UserDefaultsDataStoreImpl {
    func object(forKey key: UserDefaultsDataStoreKeys) -> Any? {
        return defaults.object(forKey: key)
    }
    
    func url(forKey key: UserDefaultsDataStoreKeys) ->URL? {
        return defaults.url(forKey: key)
    }
    
    func array(forKey key: UserDefaultsDataStoreKeys) ->[Any]? {
        return defaults.array(forKey: key)
    }
    
    func dictionary(forKey key: UserDefaultsDataStoreKeys) ->[String: Any]? {
        return defaults.dictionary(forKey: key)
    }
    
    func string(forKey key: UserDefaultsDataStoreKeys) ->String? {
        return defaults.string(forKey: key)
    }
    
    func stringArray(forKey key: UserDefaultsDataStoreKeys) ->[String]? {
        return defaults.stringArray(forKey: key)
    }
    
    func data(forKey key: UserDefaultsDataStoreKeys) ->Data? {
        return defaults.data(forKey: key)
    }
    
    func bool(forKey key: UserDefaultsDataStoreKeys) ->Bool? {
        return defaults.bool(forKey: key)
    }
    
    func integer(forKey key: UserDefaultsDataStoreKeys) ->Int? {
        return defaults.integer(forKey: key)
    }
    
    func float(forKey key: UserDefaultsDataStoreKeys) ->Float? {
        return defaults.float(forKey: key)
    }
    
    func double(forKey key: UserDefaultsDataStoreKeys) ->Double? {
        return defaults.double(forKey: key)
    }

    func set<V>(value: V?, forKey key: UserDefaultsDataStoreKeys) {
        if let v = value {
            defaults.set(v, forKey: key)    
            defaults.synchronize()
        }
    }
}

DataStore側でもgetterを定義していて若干冗長ですが、.autoReloadEnabledのようにEnumを省略形で書くためそうしています。

今回はUserDefaultKeyというprotocolを用意しましたが
もし、enumは1個しか定義しないと仮定するならば
具象enumを引数に取るUserDefaultsのextensionを書けばいいのでもっと簡潔にはできると思います。

UIScrollViewのページングをライフサイクルとして扱えるようにする

もっといいタイトル無いだろうか・・・笑

利用用途

UIViewControllerのviewをページとしてUIScrollViewにマウントしておいて、スワイプで切り替えて使うみたいな想定です。

UIScrollViewでページャーのviewDidAppearみたいなライフサイクルがあれば表示されたViewControllerをリロードしたいときに使えます。

やること

https://github.com/matsuokah/PagerLifecycle/blob/master/Images/behavior.gif?raw=true

方針

  1. RxSwiftでUIScrollViewのReactiveを拡張
  2. プロトコル指向な実装でUIViewControllerにロジックを実装しない

ベタベタに実装すればできるんですが、UIViewControllerから実装を切り分けることで、UIViewControllerをファットにならずに済むわけです

大まかな流れ

  1. ページが切り替わった
  2. 表示領域から該当するUIViewControllerの検出
  3. ライフサイクルメソッドの発行

実装

1. RxでUIScrollViewのページング完了を検出する

/// Page
struct UIScrollViewPage {
    let vertical: Int
    let horizontal: Int
}

extension UIScrollViewPage: Equatable {
    public static func ==(lhs: UIScrollViewPage, rhs: UIScrollViewPage) -> Bool {
        return lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal
    }
}

/// Pageの切り替わりの検出
extension Reactive where Base: UIScrollView {
    /// ページが切り替わった場合に選択されたページを流すイベント発行します
    /// 慣性スクロールなしのスクロール停止 or 慣性スクロールが停止した場合にPageが選択されたと検出しています
    var: Observable<UIScrollViewPage> {
        return Observable.merge(didEndDragging.asObservable().filter { return !$0 },
                                didEndDecelerating.asObservable().map { _ in return true })
            .map { _ in
                // 1以下(0)で割るとページが異常値になることがありうるのでフレームサイズが1以下のときは強制的に0ページ扱いにする
                let verticalPage = Int(self.base.frame.height < 1 ? 0 : self.base.contentOffset.y / self.base.frame.height)
                let horizontalPage = Int(self.base.frame.width < 1 ? 0 : (self.base.contentOffset.x / self.base.frame.width))
                return UIScrollViewPage(vertical: verticalPage, horizontal: horizontalPage)
        }
    }
}
extension UIScrollViewPage: Equatable {

Equatableに準拠している理由はdidSelectedPageのイベントをdistinctUntilChangeしたいため。

2. バインディングするViewControllerのProtocolを定義する

protocol PagerScrollViewControllerProtocol: class {
    func setupPagerScrollView()
    var pager: UIScrollView { get }
    var pagerDelegateViews: [PageDelegate] { get }
    var disposeBag: DisposeBag { get }
}

3. ページャーがあるUIViewControllerでPagerScrollViewControllerProtocolに準拠

class ViewController: UIViewController {
    var disposeBag = DisposeBag()
    @IBOutlet weak var scrollView: UIScrollView!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupPagerScrollView()
    }
}

extension ViewController: PagerScrollViewControllerProtocol {
    var pager: UIScrollView {
        return scrollView
    }
    
    var pagerDelegateViews: [PageDelegate] {
        return childViewControllers.filter { $0 is PageDelegate }.map { $0 as! PageDelegate }
    }
}

pagerのライフサイクルを伝えるViewの選択戦略はここで差し替えればOK
IBOutletで登録していたり、

var pagerDelegateViews: [PageDelegate] {
    return childViewControllers.filter { $0 is PageDelegate }.map { $0 as! PageDelegate }
}

4. ページャーのライフサイクルを受け取るコンテンツのプロトコルを定義

protocol UIPageViewProtocol: class {
    var targetPageView: UIView { get }
}

protocol PageState: class {
    var isVisiblePage: Bool { get set }
}

protocol PageDelegate: PageState, UIPageViewProtocol {
    func viewDidAppear()
    func viewDidDisappear()
}

targetPageViewはUIScrollViewの表示区域内にマウントされているかを検出する対象のViewです

ライフサイクルを受け取る側はPageDelegateの拡張準拠実装すればOK

final class PageViewController: UIViewController {
    var isVisiblePage = false
}

extension PageViewController: PageDelegate {
    var targetPageView: UIView { 
            return self.view
        }

    func viewDidAppear() {}
    func viewDidDisappear() {}
}

5. PagerScrollViewControllerProtocolのデフォルト拡張でページング検知を実装

extension PagerScrollViewControllerProtocol where Self: UIViewController {
    /// ページ選択を検出するためのセットアップを行います
    /// ターゲットとなるScrollViewのイベントのバインディング
    /// 選択ステータスの初期化
    func setupPagerScrollView() {
        pager.rx.didSelectedPage
            .distinctUntilChanged()
            .subscribe(onNext: { [weak self] page in
                guard let `self` = self else { return }
                self.applyAppearPage()
            }).disposed(by: disposeBag)
        
        // initialise for state
        applyAppearPage()
    }
    
    private func applyAppearPage() {
        let offset = pager.contentOffset
        let pagerSize = pager.frame.size
        let visibleArea = CGRect(x: offset.x, y: offset.y, width: pagerSize.width, height: pagerSize.height)
        
        pagerDelegateViews
            // 表示View, 非表示Viewの振り分け
            .reduce(into: Dictionary<Bool, Array<PageDelegate>>()) { base, content in
                let isVisisble = visibleArea.intersects(content.targetPageView.frame)
                base[isVisisble, default: []].append(content)
            }
            .forEach { (isVisisble, views) in
                if isVisisble {
                    // 表示されていなかったViewがページ切り替えによって表示になった
                    views
                        .filter { page in
                            return !page.isVisiblePage
                        }
                        .forEach { page in
                            page.viewDidAppear()
                            page.isVisiblePage = true
                    }
                } else {
                    // 表示されていたViewがページ切り替えによって非表示になった
                    views.filter { page in
                        return page.isVisiblePage
                        }
                        .forEach { page in
                            page.viewDidDisappear()
                            page.isVisiblePage = false
                    }
                }
        }
    }
}

まとめ

ということで、Rx拡張, Protocolのデフォルト拡張, 各Protocolに必要なプロパティを持たせるという実装に分けることで
各々がプロトコルのプロパティの部分だけ拡張実装すればOKという状態ができました。

UIKitがオブジェクト指向なので、UIKit周辺はオブジェクト指向な実装になりがちですが
こんな感じでプロトコル指向プログラミングできそうですね!

プロトコル指向プログラミングの紹介はこちら

blog.matsuokah.jp