will and way

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

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というキーに対して今はenum値qを用いてますがqueryのような、デスクリプティブな表現もできるようになります。

Pros & Cons

Pros

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

Cons

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

まとめ

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

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

github.com

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

まえがき

6月からAndroidエンジニアからiOSエンジニアになり、Objective-CをSwift化するプロジェクトをやっている。 iOSはiOS5,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などiOSのSDKがオブジェクト指向であるから、
その境界ではその限りではないし、structよりもclassが簡潔に書ける場面もありうる。例えばDI対象のオブジェクトとか、都度インスタンスを作りたくない場合はclassの参照を渡したほうがシンプル。

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

まとめ

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

引用

ImeFragmentというライブラリを公開しました!キーボード開発でもFragmentを使う!

この記事はCyberAgent Developers Advent Calendar 2016の20日目の記事です。

www.adventar.org

19日目はstrskさんでGKEのノードプールを利用したKubernetesのアップグレードでした。 ちなみにstrskさんは元々飲食業界ではたらいていてCSで入社→今はAbemaTVでGKE運用してる方です。スゴイ、、、!

明日は...○○です。

アドベントカレンダーには去年から参加し始めていて、2015年に書いた記事はこちらです。

同期系スマホアプリのリリースサイクル・テストについて - will and way

1年前はAppleのアプリレビューが1週間くらいだったのか。。。2, 3日で返ってくるようになったのは革命的な出来事だったな〜。

さて、本題のImeFragmentに入っていきましょう!

ImeFragmentというライブラリを公開しました

github.com

一言で言うと、InputMethodServiceでもFragmentとほぼ同じように使って実装ができるというライブラリです。
とりあえず作った感じなので、整理はこれからですが。

IME開発のキモとなりそうなポイントをAndroidのAdventCalendar::day11でInputMethodService(キーボード)開発の勘所となりそうな項目という記事に書きました。

その中にServiceではFragmentは使えないという項目がありました。ImeFragmentはそれを解決しライブラリ化したものです。

Fragmentが使えると何が良いのか?

アプリを実装している感覚で部品が開発できる。アプリの実装が使いまわしやすいということです。

アプリ開発の中でFragmentというインターフェースに慣れ親しんでいます。それは、フラグメントはアプリのライフサイクルだったり、ViewPagerのように動的にアタッチ/デタッチがされた場合のハンドリングだったりします。

Fragmentのようなインターフェースを持つクラスがないので、InputMethodServiceの実装がもりもりになってしまいます いわゆる、"マッチョなActivity"のように、"マッチョなInputMethodService"が避けられない状態です。

"マッチョなInputMethodService"を分割していくということは、コントローラとなりうるクラスを作るということになります。また、そのコントローラの要件はInputMethodServiceに応じたライフサイクルを持つことや、アプリ同様にonTrimMemoryのようなアプリのライフサイクルにも対応している必要があります。

結局、Fragmentが欲しいということです。

Fragmentという粒度のクラスができることによって、Fragmentが依存したいクラス(PresenterやUseCaseなど)の単位もアプリと同じように使えますし、使うイメージも湧きやすいです。

Imeに対するImeFragmentのライフサイクルのマッピングについて

InputMethodServiceはActivityよりも幾つかのライフサイクルのステップが少ないことや、アクションバーを持たないなどの違いがありますが、基本となるonCreateからonDestroyまでのライフサイクルは同じようにマッピングすることが出来ました。
なので、実際にはActivityで使う場合と同じように使うことが出来ます。

サンプルの紹介

InputMethodServiceでViewPagerを使ってみた例です。

f:id:matsuokah:20161220002736g:plain

build.gradle
dependencies {
    compile 'jp.matsuokah.imefragment:imefragment:1.0.1'
}

bintray, jcenterにホストしてあります。

SampleImeService.java
public class SampleImeService extends ImeFragmentService {

  @Override public View onCreateInputView() {
    setContentView(R.layout.ime_main);
    adapter = new SampleFragmentPagerAdapter(getImeFragmentManager());

    WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
    DisplayMetrics dm = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(dm);
    int windowHeight = dm.heightPixels;


    View wrapper = findViewById(R.id.ime_wrapper);
    ViewGroup.LayoutParams params = wrapper.getLayoutParams();
    params.height = (windowHeight * INPUT_VIEW_HEIGHT_PERCENTAGE) / 100;
    wrapper.setLayoutParams(params);

    ViewPager pager = (ViewPager) findViewById(R.id.pager);
    pager.setAdapter(adapter);

    return super.onCreateInputView();
  }
}
SamplePageFragment.java
ublic class SamplePageFragment extends ImeFragment {

  private static final String POSITION_KEY = "position_key";

  @Nullable @Override
  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
      @Nullable Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.fragment_ime_page, null);

    Bundle bundle = getArguments();
    int position = bundle.getInt(POSITION_KEY);

    TextView label = (TextView)view.findViewById(R.id.position);
    label.setText(String.valueOf(position));

    return view;
  }

  public static ImeFragment newInstance(int position) {
    ImeFragment fragment = new SamplePageFragment();
    Bundle bundle = new Bundle();
    bundle.putInt(POSITION_KEY, position);
    fragment.setArguments(bundle);
    return fragment;
  }
}

解説

public class SampleImeService extends ImeFragmentService {

  @Override public View onCreateInputView() {
    setContentView(R.layout.ime_main);
    return super.onCreateInputView();
  }
}

onCreateはこれはお作法になります。setContentでrootとなるViewをImeFragmentServiceでinflateしています。

理由は、以下の2つです

  1. InputMethodServiceはonCreateInputViewの時にViewをさわる状態ができている
  2. Fragmentを管理する機構の中で親のビューを必要とするため

また、ImeFragmentServiceにはfindViewByIdを実装したので、ActivityのようにViewを取得できるようになっています。

Fragmentに関してはほぼ、解説する必要はないですね。元のFragmentと全く同じです。

実装内容について

本家のサポートライブラリの実装を参考にし、必要なメソッドを再実装していった形になります。Fragmentの管理に使われているクラスにはActivity/Serviceに依存しない実装のものがいくつかあったのですがパッケージプライベートな処理に手を入れる必要があり、コピーしてパッケージにいれています。サポートライブラリのクラスをそのまま使っていたりもします。インターフェースとか。

変わっている点は以下のとおりです。

  • Activityにあって、Serviceにない機能の削除
    • Picture in PictureやMultiWindowMode、OptionsMenuはImeでは不要。
  • InputMethodServiceに合わせて、fragmentのライフサイクルのマッピングを調整
    • onCreate ⇔ onCreate
    • onCreateInputView ⇔ onCreateView
    • onStartInput ⇔ onStart
    • onWindowShown ⇔ onResume
    • onFinishInput ⇔ onPause
    • onWindowHidden ⇔ onStop
    • onDestory ⇔ onDestroy

バグや機能追加のPRまってます!

Androidの開発を始めてから2ヶ月の人間が作ったライブラリなので、保証はできません!リファクタもまだまだ。。。 ということで、コントリビューションをお待ちしております!issueだけでもっ!

github.com

まとめ

InputMethodServiceでもFragmentを使えるようにしました。これによってActivityを作る感覚でキーボードを開発できるようになりました!
趣味でキーボード触ってるんですが、アプリとまた違った可能性を感じています!
変換予測とか考えるのタノシイっ(๑•̀ㅂ•́)و✧

また、このライブラリを開発した副産物として、ActivityとFragmentの関係やFragmentのライフサイクルがどのようなコードなのかを知ることが出来ました。
AndroidのBaseやAndroid Support Libraryのリポジトリ、読んでみると面白いですね!

キーボードを掃除した

そういえば、今年HHKBの無印字を買ったんです。今年買ってよかったものの一つです。

そんなHHKBですがホームページに行くと以下のような文章が書いてあります。

アメリカ西部のカウボーイたちは、馬が死ぬと馬はそこに残していくが、どんなに砂漠を歩こうとも、鞍は自分で担いで往く。馬は消耗品であり、鞍は自分の体に馴染んだインタフェースだからだ。 いまやパソコンは消耗品であり、キーボードは大切な、生涯使えるインタフェースであることを忘れてはいけない。 [東京大学 和田英一 名誉教授の談話]

Happy Hacking Keyboard | 和田先生関連ページ | PFUより転載

ということで、オレたちとっての鞍を大切にあつかうべく掃除しました!
キーボードとか携帯とか常に手で触ってるものって汚いって言いますしね!

掃除風景

f:id:matsuokah:20161217171139j:plain

実は箱に入れて毎日持ち歩いています笑
少し箱の角が擦れてますね。専用のケースが有るらしいですが、この箱で十分です。

f:id:matsuokah:20161217171210j:plain キーボードのトップを取るにはKey Pullerを使います。
クリップとかで頑張れば取れますが、傷がつくので気をつけてください...

ちなみに、WindowsPCがメインの頃、FILCOのMajestouchの茶軸を使ってたんですが、Key Pullerがキーボードについてきました。ヤサシイっ!!

f:id:matsuokah:20161217171211j:plain こんな感じで抜けます

f:id:matsuokah:20161217171213j:plain キートップを横に並べてみました。
叩きやすいように、1行ごとに角度が違っています。
ラップトップのように平面と何が変わらないんだ?と最初思ってたけど、平面のキーボードは手首の移動の距離が多く、不便に慣れてたんだな〜と感じました。(主観)

f:id:matsuokah:20161217171214j:plain こんな感じで全部取りました。混ざると面倒なので、行ごとに100円ショップの水切りネットに小分けします。小分けダイジ!!

f:id:matsuokah:20161217171217j:plain キートップの洗浄には重曹を使います。
洗面台にお湯を張って、大さじ3杯ほど溶かし、かき回して30分くらい放置します。

f:id:matsuokah:20161217171219j:plain つけ置きが完了したら、十分に水で洗いで、外に干します。水切りネットのお陰で干すのも楽! だいたい乾いたら最後にドライヤーで完全に乾かします。これもネットの上からでOK

f:id:matsuokah:20161217171218j:plain キートップを洗浄している間に、キーボードのゴミを掃除機で吸い取ります。
取れない細かな汚れは、カメラを清掃する用のブラシ付きのブロアーを使いました。

f:id:matsuokah:20161217171220j:plain 掃除に夢中になって、最後の方の写真がかなり抜けちゃってますがこんな感じで掃除が終わりました!
キーボードの間から見えるチリがなくなったのでかなり清潔感が出たのでは!?!?!

HHKB最高っ!