will and way

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

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

SwiftでExtensionのプロパティの黒魔術感をなくす(追記アリ)

SwiftでExtensionに追加するプロパティの黒魔術感が異常。

クラス全体には関係ないけど、特定のextension内に閉じ込めたいpropertyが欲しくなることがあると思います。

しかし、Swiftではextensionローカルなプロパティを持とうとするとobjc_getAssociatedObjectを使うため、
急に黒魔術感が異常なほどに感じられるソースが出来上がります。

associatedObjectは設計で回避できますが、より簡潔にextensionにpropertyにアクセスしたいという場合に有効です。

Associated Objectをつかう

// Model.swift
protocol Model {
    func load(id: String)
}

// Dog.swift
class Dog {
    var name: String
}

// Dog+Model.swift
fileprivate struct AssociatedKeys {
    static var disposeBagKey = "disposeBagKey"
}

extension Dog: Model {
    fileprivate(set) var disposeBag: DisposeBag {
        get {
            if let disposeBag = objc_getAssociatedObject(self, &AssociatedKeys.disposeBagKey) as? DisposeBag {
                return disposeBag
            }
            
            let initialValue = DisposeBag()
            objc_setAssociatedObject(self, &AssociatedKeys.disposeBagKey, initialValue, .OBJC_ASSOCIATION_RETAIN)
            return initialValue
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.disposeBagKey, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }

    func load(id: String) -> Dog {
        api.load(id: id)
            .subscribe(onNext: { [weak self] model in
                self?.name = model.name
            }).disposed(by: disposeBag)
    }
}

この実装がいけてるかは別としてこんな感じでdisposeBagfileprivateでアクセスすることができます。
disposeBagをDogに持たなくて済むので近いところにデータを置くことができてハッピーです。

黒魔術にフタをする

ExtensionPropertyというものを用意します。またキーはenumで定義出来るといいので、RawRepresentable かつ、RawValue == String でkeyStringでアクセスすればいい感じにキーを取得できるようにしています。

// MARK: - ExtensionPropertyKey
/// Extension内にセットするデータのキー
public protocol ExtensionPropertyKey: RawRepresentable {
    var keyString: String { get }
}

// MARK: - ExtensionPropertyKey
/// A default implementation for enum which is extend String
public extension ExtensionPropertyKey where Self.RawValue == String {
    var keyString: String {
        return self.rawValue
    }
}

// MARK: - ExtensionProperty

/// A strategy for manage extension local property.
public protocol ExtensionProperty: class {
    func getProperty<K: ExtensionPropertyKey, V>(key: K, defaultValue: V) -> V
    func setProperty<K: ExtensionPropertyKey, V>(key: K, newValue: V)
}

// MARK: - ExtensionProperty
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)
    }
}

実際につかってみる

// Dog+Model.swift
fileprivate enum ExtensionPropertyKey: String, ExtensionPropertyKey {
    case disposeBag
}

extension Dog: Model {
    fileprivate(set) var disposeBag: DisposeBag {
        get {
            return getProperty(key: ExtensionPropertyKey.disposeBag, initialValue: DisposeBag())
        }
        set {
            setProperty(key: ExtensionPropertyKey.disposeBag, newValue: newValue)
        }
    }
}

こんな感じで、若干黒魔術感にフタをすることができました。

キーかぶりの可能性を排除する

現状ですと、disposeBagが他の拡張で上書きされる可能性があります。また、型が違うキャストに失敗するのでバグを埋め込むことになります。そこで#file, #lineの出番です

fileprivate enum ExtensionPropertyKey: String, ExtensionPropertyKey {
    case disposeBagKey

        var keyString: String {
        return [#file.description, String(#line), self.rawValue].joined(separator: "::")
    }
}

こうすることで、ExtensionPropertyKeyが定義されているファイルのパスとソースの行番号が入るので絶対にかぶることはありません。

黒魔術感が復活した気がしますがこんな感じでextensionにpropertyを閉じ込められたので見通しも良くなりそうです。

2017/10/04 追記 getPropertyが必ずnilになってしまう

blog.matsuokah.jp

Alamofireでパラメータをenumで扱えるようにする

github.com

Alamofireといえば、言わずと知れたSwift界のHTTPクライアント。名前の由来はテキサスの花の名前らしいすね。
今回はAlamofireのリクエストパラメータをenumで扱うという話。

大前提。Stringは脆い!

Stringはすべての表現を兼ね備える万能な型です。
色は"#FFFFFF"という値で扱うこともできるし、URLも"http://blog.matsuokah.jp/"と扱うことができます。

故に、APIレスポンスの型にStringが採用される変数も多いですよね。
(数値とBool以外Stringで受け取ってクライアントでパースするのが一般的なような)

しかしながらStringを型として用いる場合、その変数には「特定の値が入っている」ことを前提として扱わなければならないので、コードの堅牢性を下げうる型とも言えます。

let color: String = "http://blog.matsuokah.jp/"と入力できてしまうが故にエラーはランタイムで発生することでしょう。コンパイラ言語のありがたみもありません。

今回はこれをAlamofireのパラメータに当てはめて考え、キー値をenumで扱えるようにしました

Alamofireのパラメータは [String: Any]という形式

AlamofireではAlamofire.Parametersというエイリアス[String:Any]に参照しており、
Dictionaryをセットするだけでリクエストに応じて柔軟にパラメータを設定してくれる形式になっています。

dictionaryを代入するポイントはAlamofire.requestparamters:なので、ラップする箇所や各所のAPIサービス周辺でパラメータを用意する形式がシンプルなやり方可と思います。

APIClientにAPIのエンドポイント、パラメータ、返却する方をジェネリクスで決めてDataStoreやModelからコールするイメージです

struct SearchAPIService {        
    let apiClient: APIClient

    // APIをコールするServiceのメソッドの一例
    func searchUser(userName: String) -> Observable<SearchResult> {
        var paramter =  [String: Any]()        
        parameter["user_name"] = userName
        return apiClient.get(endpoint: endpoint, parameter: parameter)
    }
}

Parameterをenum化する

import Foundation

import Alamofire

/// Dictionaryから新たなDictionaryを作る
// MARK: - Dictionary
extension Dictionary {
    // MARK: Public Methods        
    public func associate<NKey, NValue>(transformer: @escaping ((Key, Value) -> (NKey, NValue))) -> Dictionary<NKey, NValue> {
        var dic: [NKey: NValue] = [:]
        forEach {
            let pair = transformer($0.key, $0.value)
            dic[pair.0] = pair.1
        }
        return dic
    }
}

// MARK: - ParameterKey
public protocol ParameterKey: Hashable {
    var key: String { get }
}

// MARK: - RequestParameter
public protocol RequestParameter {
    associatedtype T : ParameterKey
    
    // MARK: Internal Properties
    var parameters: Alamofire.Parameters { get }
    
    // MARK: Private Properties
    var parameter: [Self.T: Any] { get set }
    
    // MARK: Internal Methods
    mutating func setParameter(_ key: Self.T, _ value: Any)
}

// MARK: - APIParameter
public extension RequestParameter {
    public var parameters: Alamofire.Parameters {
        if parameter.isEmpty {
            return [:]
        }
        
        return parameter.associate { (param, any) in
            return (param.key, any)
        }
    }

    public mutating func setParameter(_ key: Self.T, _ value: Any) {
        parameter[key] = value
    }
}
// MARK: - SearchParameterKeys
public enum SearchParameterKeys: ParameterKey {
    case userName
    
    // MARK: Internal Properties
    public var key: String {
        switch self {
        case .userName:
            return "user_name"
        }
    }
}

// MARK: - SearchParameter
public struct SearchParameter: RequestParameter {
    public typealias T = SearchParameterKeys
    public var parameter: [SearchParameterKeys : Any] = [:]
}
struct SearchAPIService {        
    let apiClient: APIClient

    // APIをコールするServiceのメソッドの一例
    func searchUser(userName: String) -> Observable<SearchResult> {
        var paramter =  SearchParamter()        
        parameter.set(.userName, userName)
        return apiClient.get(endpoint: endpoint, parameter: parameter.parameters)
    }
}

こうすることで、SearchAPIに対して設定できるパラメータはSearchParameterのみ。SearchParameterでセットできるキー値はSearchParameterKeysのみとなりました。
また、qというキーに対して今はenumqを用いてますがqueryのような、デスクリプティブな表現もできるようになります。

Pros & Cons

Pros

  • キーに設定できるパラメータが絞られるようになった
  • APIServiceのインタフェースにParameterを用いることができるようになったので、引数が少なく、拡張性をもたせることができる
  • キーenumはキー文字列へのaliasなのでより、説明的な表現ができる

Cons

  • キー値の共通がこのままだとできないので、API毎に似たような定義が増える

まとめ

Consに関してはプロトコルで共通部分を抜き出すなどすればなんとかできそうだと思っています。
また、主観ですがAPIに対して特定のキー値があることのほうが多いので問題に当たるケースは少ないかなと楽観してます。

今回載せたコードは下記のリポジトリに載せてます。(テストは動きません無)

https://github.com/matsuokah/Alamofire-Parametergithub.com

Swiftをせっかく使うならProtocol Oriented Programmingしたい

まえがき

6月からAndroidエンジニアからiOSエンジニアになり、Objective-CをSwift化するプロジェクトをやっている。 iOSiOS5,6時代に開発した経験はあるがSwiftは0からということで、最近色々記事を読んでいた。Swiftいいですね。僕は好きです。

その中でWWDCのセッションである「Protocol-Oriented Programming in Swift - WWDC 2015 - Videos - Apple Developer」に出会い、
オブジェクト指向な実装をしてしまっていたところを軌道修正中であります。

この記事はオブジェクト指向のアプローチからプロトコル指向のアプローチまで段階を踏んで実装することで、オブジェクト指向との違いやプロトコル指向の理解を深めようというモチベーションで書いた。

また、Playgroundのソースコードは下記のリポジトリにおいてある

github.com

プロトコル指向プログラミングとは

  • プロトコル性質を定義し、プロトコルに準拠していくことで処理の共通化をはかっていくアプローチ
  • 主にprotocol, struct, enum, extensionで、基本的にはイミュータブルなデータ構造

対比されるもの

オブジェクト指向にとって代わるものとされている。

なぜオブジェクト指向と取って代わるのか

下記に挙げるオブジェクト指向の利点(目的)はSwiftのprotocol, struct , extensionで実現し、さらに欠点である複雑性を排除することが出来るから

オブジェクト指向の利点(引用)

Protocol-Oriented Programming in Swift - WWDC 2015 - Videos - Apple Developerのでは下記を上げている

  1. Encapsulation(カプセル化)
  2. Access Control(アクセスコントロール)
  3. Abstraction(抽象化)
  4. Namespace(名前空間)
  5. Expressive Syntax表現力のある構文。例えばメソッドチェーン
  6. Extensibility(拡張性)

これらは型の特徴であり、オブジェクト指向ではclassによって上記を実現している。

また、classでは継承を用いることで親クラスのメソッドを共有したり、オーバーライドによって振る舞いを変えるということ実現している。

しかし、これらの特徴はstructとenumで実現することが可能

クラスの問題点

暗黙的オブジェクトの共有

classは参照であるため、プロパティの中身が書き換わると参照しているすべての箇所に影響が及ぶ。即ち、その変更を考慮した実装による複雑性が生まれているということ。

継承関係

Swiftを含め、多くの言語ではスーパークラスを1つしか持てないため、親を慎重に選ぶという作業が発生している。また、継承した後はそのクラスの継承先にも影響が及ぶので後から継承元を変えるという作業が非常に困難になる。

型関係の消失

オブジェクト指向をSwiftで実現しようとすると、ワークアラウンドが必要になる

/// データクラスをキャッシュするクラスをCacheとし、更新のためにmergePropertyというメソッドを用意した
class Cache {

    func key() -> String {
        fatalError("Please override this function.")
    }
  
    func mergeProperty(other: Cache) {
        fatalError("Please override this function.")
    }
}

class FuelCar: Cache {
    var fuel: Int = 0
    var id: String 
    init(id: String) { self.id = id }
  
    override func key() -> String {
        return String(describing: FuelCar.self) + self.id
    }

    override func mergeProperty(other: Cache) {
        guard let car = other as? FuelCar { return }
      fuel = car.fuel
    }
}

var memoryCache = [String:Cache]()
発生しているワークアラウンド
  1. 抽象関数を実現するためにスーパークラスfatalErrorを使っている
  2. 各クラスの実装でランタイムのキャストを行っている
  3. もし、FuelCar, BatteryCarで共通処理を実装するCarというスーパークラスを定義したくなったら、CacheFuelCarなどとデータクラスを分けるような実装が必要になる

簡単なキャッシュをオブジェクト指向からプロトコル指向にリファクタしてみる

classを使ってオブジェクト指向な実装

typealias CacheKey = String

class Cacheable {
    func key() -> CacheKey {
        fatalError("Please override this function")
    }
    
    func merge(other: Cacheable) {
        fatalError("Please override this function")
    }
}

class CacheStore<CacheableValue: Cacheable> {
    var cache = [CacheKey:CacheableValue]()
    
    func save(value: CacheableValue) {
        if let exist = cache[value.key()] {
            exist.merge(other: value)
            cache[value.key()] = exist
            return
        }
        
        cache[value.key()] = value
    }
    
    func load(cacheable: CacheableValue) -> CacheableValue? {
        return cache[cacheable.key()]
    }
}

class FuelCar: Car {
    var fuelGallon: Int
    init(id: String, fuelGallon: Int = 0) {
        self.fuelGallon = fuelGallon
        super.init(id: id)
    }
    
    override func key() -> CacheKey {
        return id
    }
    
    override func merge(other: Cacheable) {
        guard let fuelCar = other as? FuelCar else {
            return
        }

        self.fuelGallon = fuelCar.fuelGallon
    }
}

var fuelCarCache = CacheStore<FuelCar>()
var car1 = FuelCar(id: "car1", fuelGallon: 0)
fuelCarCache.save(value: car1)

print(cacheable: car1, store: fuelCarCache)
// print: 0

car1.fuelGallon = 10

print(cacheable: car1, store: fuelCarCache)
// print: 10

fuelCarCache.save(value: car1)

print(cacheable: car1, store: fuelCarCache)
// print: 10
問題点
  1. CacheableとCarという共通クラスを持つために、CarがCacheableを継承する必要がある
  2. Carでインスタンスを作ってキャッシュに入れることができてしまう。ラインタイムでエラー
  3. mergeメソッドではfuelCarへのランタイムでのキャストが発生する
  4. car1.fuelGallon = 10を記述した時点で、キャッシュを参照している部分全てに影響が出ている

protocolを使う

//: Playground - noun: a place where people can play

import Foundation

protocol HasId {
    var id: String { get }
}

protocol Mergeable {
    func merge(other: Self) -> Self
}

typealias CacheKey = String

protocol KeyCreator {
    func key() -> CacheKey
}

protocol Cacheable: KeyCreator, Mergeable { }

class CacheStore<CacheableValue: Cacheable> {
    var cache = [CacheKey:CacheableValue]()
    
    func save(value: CacheableValue) {
        if let exist = cache[value.key()] {
            cache[value.key()] = exist.merge(other: value)
            return
        }
        
        cache[value.key()] = value
    }
    
    func load(keyCreator: KeyCreator) -> CacheableValue? {
        return cache[keyCreator.key()]
    }
}

class Car: HasId {
    var id: String
    init (id: String) {
        self.id = id
    }
}

class FuelCar: Car, Cacheable {
    var fuelGallon: Int
    init(id: String, fuelGallon: Int = 0) {
        self.fuelGallon = fuelGallon
        super.init(id: id)
    }

    func key() -> CacheKey {
        return id
    }

    func merge(other: FuelCar) -> Self {
        if self.id == other.id {
            self.fuelGallon = other.fuelGallon
        }
        return self
    }
}

func print<Key: KeyCreator>(key: Key,store: CacheStore<FuelCar>) {
    print("fuelGallon: \(store.load(keyCreator: key)!.fuelGallon)")
}

var fuelCarCache = CacheStore<FuelCar>()
var car1 = FuelCar(id: "car1", fuelGallon: 0)
fuelCarCache.save(value: car1)

print(key: car1, store: fuelCarCache)
// print: 0

car1.fuelGallon = 10

print(key: car1, store: fuelCarCache)
// print: 10

fuelCarCache.save(value: car1)

print(key: car1, store: fuelCarCache)
// print: 10
改善されたポイント
  1. mergeでは引数の型がコンパイル時に決まるようになった
  2. CarクラスをCacheできないようになった。

structを使う

//: Playground - noun: a place where people can play

import Foundation

protocol HasId {
    var id: String { get }
}

protocol Mergeable {
    func merge(other: Self) -> Self
}

typealias CacheKey = String

protocol KeyCreator {
    func key() -> CacheKey
}

protocol Cacheable: KeyCreator, Mergeable { }

struct CacheStore<CacheableValue: Cacheable> {
    var cache = [CacheKey:CacheableValue]()
    
    mutating func save(value: CacheableValue) {
        if let exist = cache[value.key()] {
            cache[value.key()] = exist.merge(other: value)
            return
        }
        
        cache[value.key()] = value
    }
    
    func load(keyCreator: KeyCreator) -> CacheableValue? {
        return cache[keyCreator.key()]
    }
}

protocol Car: HasId { }

struct FuelCar: Car, Cacheable {
    var id: String
    var fuelGallon: Int
    
    func key() -> CacheKey {
        return id
    }
    
    func merge(other: FuelCar) -> FuelCar {
        return FuelCar(id: self.id, fuelGallon: other.fuelGallon)
    }
}

func print<Key: KeyCreator>(key: Key,store: CacheStore<FuelCar>) {
    print("fuelGallon: \(store.load(keyCreator: key)!.fuelGallon)")
}

var fuelCarCache = CacheStore<FuelCar>()
var car1 = FuelCar(id: "car1", fuelGallon: 0)
fuelCarCache.save(value: car1)

print(key: car1, store: fuelCarCache)
// print: 0

car1.fuelGallon = 10

print(key: car1, store: fuelCarCache)
// print: 0

fuelCarCache.save(value: car1)

print(key: car1, store: fuelCarCache)
// print: 10
改善されたポイント
  1. キャッシュをsaveするまで、キャッシュをロードした箇所・キャッシュ自体への影響がなくなった
  2. イニシャライズ処理が簡潔になった(複雑なstructの場合この限りではない)

extensionをつかう

//: Playground - noun: a place where people can play

import Foundation

protocol HasId {
    var id: String { get }
}

protocol Mergeable {
    func merge(other: Self) -> Self
}

typealias CacheKey = String

protocol KeyCreator {
    func key() -> CacheKey
}

protocol Cacheable : KeyCreator, Mergeable {}

extension Cacheable where Self: HasId {
    func key() -> CacheKey {
        return id
    }
}

struct CacheStore<CacheableValue: Cacheable> {
    var cache = [CacheKey:CacheableValue]()
    
    mutating func save(value: CacheableValue) {
        if let exist = cache[value.key()] {
            cache[value.key()] = exist.merge(other: value)
            return
        }
        
        cache[value.key()] = value
    }
    
    func load(keyCreator: KeyCreator) -> CacheableValue? {
        return cache[keyCreator.key()]
    }
}

protocol Car: HasId { }

struct FuelCar: Car {
    var id: String
    var fuelGallon: Int
}

extension FuelCar: Cacheable {
    func merge(other: FuelCar) -> FuelCar {
        return FuelCar(id: self.id, fuelGallon: other.fuelGallon)
    }
}

func print<Key: KeyCreator>(key: Key,store: CacheStore<FuelCar>) {
    print("fuelGallon: \(store.load(keyCreator: key)!.fuelGallon)")
}

var fuelCarCache = CacheStore<FuelCar>()
var car1 = FuelCar(id: "car1", fuelGallon: 0)
fuelCarCache.save(value: car1)

print(key: car1, store: fuelCarCache)
// print: 0

car1.fuelGallon = 10

print(key: car1, store: fuelCarCache)
// print: 0

fuelCarCache.save(value: car1)
car1 = fuelCarCache.load(keyCreator: car1)!

print(key: car1, store: fuelCarCache)
// print: 10
改善されたポイント
  1. HasIdCacheableを準拠すれば、基本的にkeyの作成実装が不要になった
  2. structの本実装と、キャッシュに保存するという戦略を別のブロックで書くことでコードの見通しがよくなった。

また、今回はPlaygroundなので出来ていないが

  • FuelCar.swift
  • FuelCar+Cacheable.swift

のように実装毎にファイルを分けることが出来るため、FuelCar.swiftではFuelCarのドメインの処理を実装し、+Cacheable.swiftではキャッシュの上書き戦略を実装するというパターン化が可能になる

ProtocolとStructのキモ(= POPの旨み)

  1. Protocolは抽象化・処理(性質)の共通化を記述する。
  2. Protocolのextensionでデフォルトの共通処理を定義していく
  3. 持っているプロトコルの組み合わせを条件としてextension共通処理を実装する事ができる
  4. Structに複数のProtocol(性質)を持つことで使えるメソッドが増えていく

Protocolとextensionの欠点

複数のプロトコル継承とextensionの実装

//: [Previous](@previous)

import Foundation

protocol HasId {
    var id: String { get }
}
protocol HasCategoryId {
    var id: String { get }
}

struct Book: HasId, HasCategoryId, CustomDebugStringConvertible {
    var id: String
}

let book = Book(id: "isbn-9784798142494")

if let hasId = book as? HasId {
    print("id: \(hasId.id)")
}

if let categoryId = book as? HasCategoryId {
    print("id: \(categoryId.id)")
}

extension CustomDebugStringConvertible where Self: HasId {
    var debugDescription: String {
        return "hasId: \(id)"
    }
}

extension CustomDebugStringConvertible where Self: HasCategoryId {
    var debugDescription: String {
        return "hasCategiryId: \(id)"
    }
}

// If you make comment below, you can get any compile errors.
extension CustomDebugStringConvertible where Self: HasId & HasCategoryId {
    var debugDescription: String {
        return "hasId: \(id), hasCategoryId: \(id)"
    }
}

debugPrint(book)
問題点
  1. 変数名の重複が予期せぬところでありうる
  2. 変数名が重複し、型が同じだった場合にコンパイルできてしまう
  3. 変数名が重複し、型が違った場合はコンパイルエラーになる
  4. where Self: HasId, where Self: HasCategoryIdのデフォルト実装をしつつ、両方のプロトコルを持つstructを定義すると、両方のプロトコルを定義したextensionも定義しなければならない(どっちの実装を使うかはコンパイラが判断できないため)

したがって、protocolの実装に気をつけなければ、バグを生む可能性やprotocolの定義の仕方によって実装方針が制限される可能性があるということも念頭に置かなくてはならない。

すべてPOPで書くことが出来るのか?

結論から言うと無理。理由はUIKitなどiOSSDKオブジェクト指向であるから、
その境界ではその限りではないし、structよりもclassが簡潔に書ける場面もありうる。例えばDI対象のオブジェクトとか、都度インスタンスを作りたくない場合はclassの参照を渡したほうがシンプル。

"実装できるならPOPに寄せる"という温度感で実装するのがちょうどいいように思える。
言い換えれば、「POPで実装できるか?」をOOPで実装する前に1度考えるということ。

まとめ

  1. Swift書くならprotocol, extension, struct or enumでProtocol Oriented Programmingを意識する
  2. 実装するときは1度POPで実装できるか考える

引用