UIScrollViewのページングをライフサイクルとして扱えるようにする
もっといいタイトル無いだろうか・・・笑
利用用途
UIViewControllerのviewをページとしてUIScrollViewにマウントしておいて、スワイプで切り替えて使うみたいな想定です。
UIScrollViewでページャーのviewDidAppearみたいなライフサイクルがあれば表示されたViewControllerをリロードしたいときに使えます。
やること
方針
- RxSwiftでUIScrollViewのReactiveを拡張
- プロトコル指向な実装でUIViewControllerにロジックを実装しない
ベタベタに実装すればできるんですが、UIViewControllerから実装を切り分けることで、UIViewControllerをファットにならずに済むわけです
大まかな流れ
- ページが切り替わった
- 表示領域から該当するUIViewControllerの検出
- ライフサイクルメソッドの発行
実装
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周辺はオブジェクト指向な実装になりがちですが
こんな感じでプロトコル指向プログラミングできそうですね!
プロトコル指向プログラミングの紹介はこちら
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) } }
この実装がいけてるかは別としてこんな感じでdisposeBag
をfileprivate
でアクセスすることができます。
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になってしまう
Alamofireでパラメータをenumで扱えるようにする
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.request
のparamters:
なので、ラップする箇所や各所の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に対して特定のキー値があることのほうが多いので問題に当たるケースは少ないかなと楽観してます。
今回載せたコードは下記のリポジトリに載せてます。(テストは動きません無)
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のソースコードは下記のリポジトリにおいてある
プロトコル指向プログラミングとは
- プロトコルに性質を定義し、プロトコルに準拠していくことで処理の共通化をはかっていくアプローチ
- 主にprotocol, struct, enum, extensionで、基本的にはイミュータブルなデータ構造
対比されるもの
オブジェクト指向にとって代わるものとされている。
なぜオブジェクト指向と取って代わるのか
下記に挙げるオブジェクト指向の利点(目的)はSwiftのprotocol
, struct
, extension
で実現し、さらに欠点である複雑性を排除することが出来るから
オブジェクト指向の利点(引用)
Protocol-Oriented Programming in Swift - WWDC 2015 - Videos - Apple Developerのでは下記を上げている
- Encapsulation(カプセル化)
- Access Control(アクセスコントロール)
- Abstraction(抽象化)
- Namespace(名前空間)
- Expressive Syntax表現力のある構文。例えばメソッドチェーン
- 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]()
発生しているワークアラウンド
- 抽象関数を実現するためにスーパークラスで
fatalError
を使っている - 各クラスの実装でランタイムのキャストを行っている
- もし、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
問題点
- CacheableとCarという共通クラスを持つために、CarがCacheableを継承する必要がある
- Carでインスタンスを作ってキャッシュに入れることができてしまう。ラインタイムでエラー
merge
メソッドではfuelCarへのランタイムでのキャストが発生する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
改善されたポイント
merge
では引数の型がコンパイル時に決まるようになった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
改善されたポイント
- キャッシュを
save
するまで、キャッシュをロードした箇所・キャッシュ自体への影響がなくなった - イニシャライズ処理が簡潔になった(複雑な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
改善されたポイント
HasId
とCacheable
を準拠すれば、基本的にkeyの作成実装が不要になったstruct
の本実装と、キャッシュに保存するという戦略を別のブロックで書くことでコードの見通しがよくなった。
また、今回はPlaygroundなので出来ていないが
- FuelCar.swift
- FuelCar+Cacheable.swift
のように実装毎にファイルを分けることが出来るため、FuelCar.swift
ではFuelCarのドメインの処理を実装し、+Cacheable.swift
ではキャッシュの上書き戦略を実装するというパターン化が可能になる
ProtocolとStructのキモ(= POPの旨み)
- Protocolは抽象化・処理(性質)の共通化を記述する。
- Protocolのextensionでデフォルトの共通処理を定義していく
- 持っているプロトコルの組み合わせを条件としてextension共通処理を実装する事ができる
- 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)
問題点
- 変数名の重複が予期せぬところでありうる
- 変数名が重複し、型が同じだった場合にコンパイルできてしまう
- 変数名が重複し、型が違った場合はコンパイルエラーになる
where Self: HasId
,where Self: HasCategoryId
のデフォルト実装をしつつ、両方のプロトコルを持つstruct
を定義すると、両方のプロトコルを定義したextensionも定義しなければならない(どっちの実装を使うかはコンパイラが判断できないため)
したがって、protocolの実装に気をつけなければ、バグを生む可能性やprotocolの定義の仕方によって実装方針が制限される可能性があるということも念頭に置かなくてはならない。
すべてPOPで書くことが出来るのか?
結論から言うと無理。理由はUIKitなどiOSのSDKがオブジェクト指向であるから、
その境界ではその限りではないし、structよりもclassが簡潔に書ける場面もありうる。例えばDI対象のオブジェクトとか、都度インスタンスを作りたくない場合はclassの参照を渡したほうがシンプル。
"実装できるならPOPに寄せる"という温度感で実装するのがちょうどいいように思える。
言い換えれば、「POPで実装できるか?」をOOPで実装する前に1度考えるということ。
まとめ
- Swift書くなら
protocol
,extension
,struct
orenum
でProtocol Oriented Programmingを意識する - 実装するときは1度POPで実装できるか考える
引用
ImeFragmentというライブラリを公開しました!キーボード開発でもFragmentを使う!
この記事はCyberAgent Developers Advent Calendar 2016の20日目の記事です。
19日目はstrskさんでGKEのノードプールを利用したKubernetesのアップグレードでした。 ちなみにstrskさんは元々飲食業界ではたらいていてCSで入社→今はAbemaTVでGKE運用してる方です。スゴイ、、、!
明日は...○○です。
アドベントカレンダーには去年から参加し始めていて、2015年に書いた記事はこちらです。
同期系スマホアプリのリリースサイクル・テストについて - will and way
1年前はAppleのアプリレビューが1週間くらいだったのか。。。2, 3日で返ってくるようになったのは革命的な出来事だったな〜。
さて、本題のImeFragmentに入っていきましょう!
ImeFragmentというライブラリを公開しました
https://github.com/matsuokah/ImeFragmentgithub.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を使ってみた例です。
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つです
- InputMethodServiceはonCreateInputViewの時にViewをさわる状態ができている
- 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だけでもっ!
https://github.com/matsuokah/ImeFragmentgithub.com
まとめ
InputMethodServiceでもFragmentを使えるようにしました。これによってActivityを作る感覚でキーボードを開発できるようになりました!
趣味でキーボード触ってるんですが、アプリとまた違った可能性を感じています!
変換予測とか考えるのタノシイっ(๑•̀ㅂ•́)و✧
また、このライブラリを開発した副産物として、ActivityとFragmentの関係やFragmentのライフサイクルがどのようなコードなのかを知ることが出来ました。
AndroidのBaseやAndroid Support Libraryのリポジトリ、読んでみると面白いですね!
キーボードを掃除した
そういえば、今年HHKBの無印字を買ったんです。今年買ってよかったものの一つです。
そんなHHKBですがホームページに行くと以下のような文章が書いてあります。
アメリカ西部のカウボーイたちは、馬が死ぬと馬はそこに残していくが、どんなに砂漠を歩こうとも、鞍は自分で担いで往く。馬は消耗品であり、鞍は自分の体に馴染んだインタフェースだからだ。 いまやパソコンは消耗品であり、キーボードは大切な、生涯使えるインタフェースであることを忘れてはいけない。 [東京大学 和田英一 名誉教授の談話]
Happy Hacking Keyboard | 和田先生関連ページ | PFUより転載
ということで、オレたちとっての鞍を大切にあつかうべく掃除しました!
キーボードとか携帯とか常に手で触ってるものって汚いって言いますしね!
掃除風景
実は箱に入れて毎日持ち歩いています笑
少し箱の角が擦れてますね。専用のケースが有るらしいですが、この箱で十分です。
キーボードのトップを取るにはKey Pullerを使います。
クリップとかで頑張れば取れますが、傷がつくので気をつけてください...
ちなみに、WindowsPCがメインの頃、FILCOのMajestouchの茶軸を使ってたんですが、Key Pullerがキーボードについてきました。ヤサシイっ!!
こんな感じで抜けます
キートップを横に並べてみました。
叩きやすいように、1行ごとに角度が違っています。
ラップトップのように平面と何が変わらないんだ?と最初思ってたけど、平面のキーボードは手首の移動の距離が多く、不便に慣れてたんだな〜と感じました。(主観)
こんな感じで全部取りました。混ざると面倒なので、行ごとに100円ショップの水切りネットに小分けします。小分けダイジ!!
キートップの洗浄には重曹を使います。
洗面台にお湯を張って、大さじ3杯ほど溶かし、かき回して30分くらい放置します。
つけ置きが完了したら、十分に水で洗いで、外に干します。水切りネットのお陰で干すのも楽! だいたい乾いたら最後にドライヤーで完全に乾かします。これもネットの上からでOK
キートップを洗浄している間に、キーボードのゴミを掃除機で吸い取ります。
取れない細かな汚れは、カメラを清掃する用のブラシ付きのブロアーを使いました。
掃除に夢中になって、最後の方の写真がかなり抜けちゃってますがこんな感じで掃除が終わりました!
キーボードの間から見えるチリがなくなったのでかなり清潔感が出たのでは!?!?!
[asin:B000EXZ0VC:detail] HHKB最高っ!
良いルーターを使うのはもはやライフハックつだ!!! TP-LINK AC3150 レビュー
Amazon Cyber Mondayあざす!!!
買ってしまった!良いルーター!それは TP-LINK Archer 3150!
TP-LINK? 聞いたこと無い?
私自身も日本ではバッファロー、エレコム、NECが有名すぎて、TP-LINKを知らなかったんですがTP-LINKは世界シェア43%の企業。
最速のワイヤレス規格である802.11adのルータを世界初でお披露目するほどです。
ガジェット好きの先輩の家に行ったらTP-LINK Archer C9が置いてあって、繋いで見たところ接続の安定性・速度に感動したのがきっかけ。
そして、現在使っているBuffalo WZR-600DHPも2012年発売の機種なので古くなってきたし、TP-LINKのルータに買い換えよう!と
2週間程しらべたり、チャンスを伺っていました。
"日本で発売される最上位機種"のArcher5400が12月中に発売予定なので、こちらを買うつもりでいたが、
AmazonのCyber MondayでArcher3150がセールになっていて15,800円に値下がりしてたので買ってしまった!!!ヨドバシでは22,000円程なので、ポイントを考えても安かったです。
現在でも20,000円前後なのでAmazonで買うのがおすすめかな?ポイントが付くヨドバシでもいいかも(๑•̀ㅂ•́)و✧
上記で"日本で発売される最上位機種"と書いたのは
日本で公式に発売される予定がたっていないであろうTP-LINK AD7200がAmazonで販売されているから。
AD7200は"世界初の802.11ad対応のルータ"で、Amazonでは最安で4万円程ですが、元値は$350前後のようです。発売されてから1年たつのに割高。 Amazon, Rakuten, 公式サイトをみても品薄な感じです。 公式販売されないということは、技適が通っていないかもしれません。
Archer 3150で満足できない!というかたは素直に12月中発売のArcher 5400を待ちましょう。
話を戻しまして、、、金曜日に注文して、本日土曜日に到着したので早速開封・セットアップしレビューしていきます!
開封の儀・セットアップ
はい、箱ドーーン
でかい。開封の儀を執り行います。
同梱物は本体、アンテナ x 4, 電源コード, 電源アダプタ
ポートが
背面にはWAN x 1, LAN x 4
側面にはUSB2.0 x 1, USB3.0 x 1
Mac Book Air 11inchを横に並べると大きさがより分かる。
あとは接続設定しておしまい
PCでセットアップした場合Wi-Fiに接続しセットアップの手順に従って入力していけば3分くらいで終わりました。
上記のブログではWebのセットアップ手順が載っています。MacBookPro Touchbar 13inchとの大きさの比較も載ってます。考える事同じ笑
そもそも、MacBookAirの11inchと同じくらいの大きさってことになるMacBookProのTouchbarの13inchは相当コンパクトだな・・・!!!
が、今回はせっかくなのでアプリから設定しなおしてみる。
今回はAndroidのアプリです
1. ルーターを選択
2. 設定項目 > インターネット
3. 各欄の入力
これで終わりです。
基本スペック
Archer 3150の製品情報、仕様より抜粋して転載しています。
ワイヤレス規格規格
IEEE 802.11ac/n/a 5GHz IEEE 802.11b/g/n 2.4GHz
セキュリティ
64/128-bit WEP、WPA/WPA2、WPA-PSK/WPA-PSK2 暗号化方式
特徴
1. MU-MIMO
マルチユーザー、マルチインプット、マルチアウトプットの略で
簡単にいうと、複数機種を同時接続してもサクサク!ということです。
しかし、受信側もMU-MIMOに対応していないと有効になりません。
私の普段使いであるスマホのXperiaZ5とNexus9はMIMO対応で、MU-MIMOではないようです。(製品ページのスペックを読んだり、ググったりしたが見つからなかったので。)
私が自宅で使っている2012 midのMac Book Pro Retinaは802.11acにすら対応していない。買い替えどきかな。
2. デュアルコア・コプロセッサx2搭載
ルーターでデュアルコアってきいたことなかったんですが、ハイエンドルーターでは当たり前のようです。
NAS接続したり複数の端末を同時に動画を見てもヌルサクなのはこのおかげかも?
欠点としては若干の発熱があることです。ファンはうるさくないのですが表面がほんのり温かくなります。
ネットワークテストしてみる
5GHzのネットワークに繋いでテストしてみます。
スピードテスト1 単体でテストしてみる
使用アプリ
Speed Test
使用デバイス
Nexus9(MIMO対応)
DL: 67.49Mbps
UL: 93.80Mbps
スピードテスト2 複数台で同時にテストしてみる
使用デバイス
Nexus9(左)の結果
DL: 25.91Mbps
UL: 64.04Mbps
XperiaZ5(MIMO対応)(右)
DL: 24.73Mbps
UL: 32.43Mbps
さすがにWANがボトルネックですね!各値を足すと単体でテストしたときとほぼ同じ数字になります。
スピードテスト3 Youtubeのシークを同時にしてみる
※音が出ます
使用デバイス
Nexus9(左)
XperiaZ5(右)
シークが早い気がする。多分。
RSSI値を調べる
ルーターがを置いているリビングと廊下を隔ててたところにある寝室でテストしてみました。
dB値が大きい(-なので値が低い)方が電波強度は強いです。
MBPのWi-FiをOption + Click
で調べられます。
また、このテストはルーターの置き場所や遮蔽物の有無によって結果が変わって来ることが注意点です。
5GHz
リビング: -39dBm
寝室:-65dBm
2.4GHz
リビング: -40dBm
寝室:-53dBm
ということで、2.4GHzの方がさすがに隔てた部屋でも強いですね!
前のルータだと2.4GHzのみで寝室に移動すると電波強度が80dBm程度でした。(スクショ撮り忘れた)
また主観ではありますが、接続自体も不安定だったので改善されています。寝室では、頻繁にWi-Fiの検索中になっていた。
まとめ
ということでTP-LINKのWi-FiルータArcher 3150を紹介しましたが
快適なWi-Fiはもはやライフハックなのでは?と感じる体験レポートでした。ぜひご検討あれ!
InputMethodService(キーボード)開発の勘所となりそうな項目
この記事はAndroid Advent Calendar 2016 - Qiitaの11日目の記事です。
昨日は@yuyakaidoさんのData Binding Tipsでした。 明日は@rei-mさんのDagger2とMockitoでUIテストはじめる話です。
11日目はAndroid StudioでKotlinのプロジェクトが新規で作られるソースを
そろそろKotlinを選べるになってほしいmatsuokahが担当いたします。
InputMethodServiceに触れていて、キーボードのクセを掴まねば!ということで、勘所となりそうな項目を書いてみます。
InputMethodService(以下、ほぼ同義のIME)とは
いわゆるキーボードの継承元となるServiceです。
Simeji、Google日本語入力、POBox、ATOKが日本では有名ですね。
IMEはググっても、ヒット数が少ないのでマイナーな分野ですね。
IMEアプリの作り方はハンズオン記事を見ればわかります。
サンプルもあります。
ざっくりキーボードの作り方
- InputMethodServiceを継承したクラスを作成
- Android Manifestにサービスの定義を行う
- カスタムしていく
本当にざっくりですがこんな感じです。
ハンズオン記事ではMainActivityに<category android:name="android.intent.category.LAUNCHER"/>
を記載していないですが、
設定アプリとしてActivityを使うはずなので消さなくて良いでしょう。
何のメソッドをオーバーライドすべきか?
を見て、リストしてみました。
Intentionally empty
とコメントが書かれているメソッド
- onBindInput
- onUnbindInput
- onInitializeInterface
- onStartInput・・・初期化
- onStartInputView
- onStartCandidatesView
- onWindowShown
- onWindowHidden
- onDisplayCompletions
- onViewClicked
- onUpdateCursor
- onUpdateCursorAnchorInfo
デフォルトでnullを返却しているメソッド
- onCreateInputView・・・ここでビューを作成する
- onCreateCandidatesView
デフォルトでfalseを返却している
- onTrackballEvent
- onGenericMotionEvent
フルスクリーンモード
- onEvaluateFullscreenMode・・・画面を回転させた時に、フルスクリーンモードにするか否か
ということで、基本的にはここらへんのメソッドをオーバーライドしてIMEをカスタムしていくことになります。
リストにはライフサイクルに関わるメソッドも含まれています。
また、
ライフサイクル
Android Developers - creating-input-methodより転載
というような単純なライフサイクルとなっています。
しかし、思わぬ所でイベントが発火されるケースが幾つかあります。
操作して、ライフサイクルでログを追っていきましょう。
これが最もシンプルなIMEの操作だと思います。実際のログは以下のとおりです
## MyImeへ切り替えを開始 D/ImeService: onCreate D/ImeService: onCreateInputMethodInterface D/ImeService: onCreateInputMethodSessionInterface D/ImeService: onInitializeInterface D/ImeService: onBindInput D/ImeService: onStartInput, restarting : false D/ImeService: onCreateInputView D/ImeService: onCreateCandidatesView D/ImeService: onStartInputView, restarting : false D/ImeService: onWindowShown ## IMEの表示が完了 ## 別のIMEへの切り替えを開始 D/ImeService: onFinishInputView, finishingInput : true D/ImeService: onFinishInput D/ImeService: onStartInput, restarting : false D/ImeService: onStartInputView, restarting : false D/ImeService: onUnbindInput D/ImeService: onFinishInputView, finishingInput : true D/ImeService: onFinishInput D/ImeService: onDestroy
思わぬ所でイベントが発火されるケース
と、述べましたが
onStartInput
, onStartInputView
が何故かonFinishXXX
の後に呼ばれています。
私はStartと書いてあるのでIMEが立ち上がって入力が開始された時だけ発火すると勘違いしていました。
ではなぜ発火されているか。それは、編集していたEditTextのフォーカスが外れた時に、IMEの状態をリフレッシュするためです。
IMEは1つのインスタンスであるのに対し、画面には複数のEditTextが存在することが多いです。
編集するEditTextが切り替わると、すでに入力されているテキストや入力タイプ(Numeric, AlphaNumeric, Passwordなど)が変わります。
したがって、onStartInput
では一時的な入力データなどを破棄し、引数で与えられているEditorInfoオブジェクトの情報から次の入力に備える必要があります。
実はSDKのドキュメント冒頭にあるGenerating Textという項目で述べられています。ドキュメントはしっかり読もう。
そして、ログに出てきてないので気づきにくいのですがonWindowShown
に対し、onWindowHidden
が呼ばれていません。
onWindowHidden
はIMEが破棄されないが非表示になるときかタスクマネージャ起動時に発火されます。
従ってonWindowHidden
でTearDownするのは好ましくない実装だと思います。
onStart系のライフサイクルメソッドでスクラップアンドビルドしていくのが良いかなと個人的に思います。
Fragmentが使えない
私はIMEでもFragmentが使えると思いこんでいました。
ところがFragmentはActivityに依存しているのでServiceで使えないのは当然なわけであります。
iOSのLINEのスタンプのようなUIを実装してみようと思っていたのですが、
Fragmentが使えないのでViewPagerを使うにはAdapterの独自実装が必要となります。
LINEのUIなので、画像が多い前提となるため、Fragmentのライフサイクルの様にページを破棄することも考慮に入れる必要があります。
ということで、ひとまず最低限の実装でFragment・FragmentManager・FragmentPagerAdapterの実装を真似て作ってみました。FragmentのTransactionは実装していません。
ImeFragment.kt
abstract class ImeFragment() { lateinit var service : InputMethodService val context : Context get() = service var tag : String = "" var containerId : Int = 0 var view : View? = null open fun onCreate() {} open fun onCreateView(inflater: LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? { return null } open fun onAttach() {} open fun onDetach() {} open fun onDestroyView() {} open fun onDestroy() {} }
ImeFragmentManager.kt
class ImeFragmentManager(val service : InputMethodService, val rootView : View) { val fragments : HashMap<String, ImeFragment> = HashMap() val attached : HashMap<String, ImeFragment> = HashMap() fun add(containerId : Int, imeFragment : ImeFragment, tag : String) { if (fragments.containsKey(tag)) throw IllegalArgumentException("already container has fragment") imeFragment.containerId = containerId imeFragment.tag = tag fragments[tag] = imeFragment attach(containerId, imeFragment, tag) } fun remove(tag : String) { if (attached.containsKey(tag)) { detach(attached[tag]!!) } val fragment = fragments[tag]!! fragment.onDestroy() fragments.remove(tag) } fun findFragmentByTag(tag : String) : ImeFragment? = fragments[tag] fun attach(containerId : Int, imeFragment : ImeFragment, tag : String) { if (!fragments.containsKey(tag)) throw IllegalArgumentException("fragment is not added") if (attached.containsKey(tag)) throw IllegalArgumentException("already container had attached") attached[tag] = imeFragment imeFragment.service = service val container = rootView.findViewById(containerId) as? ViewGroup imeFragment.view = imeFragment.onCreateView(LayoutInflater.from(container?.context), container, null) container?.addView(imeFragment.view) imeFragment.onAttach() } fun detach(imeFragment : ImeFragment) { if (!attached.containsValue(imeFragment)) throw IllegalArgumentException("fragment not found") val container = rootView.findViewById(imeFragment.containerId) as? ViewGroup container?.removeAllViews() imeFragment.onDestroyView() attached.remove(imeFragment.tag) imeFragment.onDetach() } }
ImeFragmentPagerAdapter.kt
abstract class ImeFragmentPagerAdapter(private val imeFragmentManager : ImeFragmentManager) : PagerAdapter() { abstract fun getItem(position : Int) : ImeFragment override fun instantiateItem(container : ViewGroup?, position : Int) : Any { container ?: throw IllegalArgumentException("") val tag = createTag(container.id, position) var fragment = imeFragmentManager.findFragmentByTag(tag) if (fragment == null) { fragment = getItem(position) imeFragmentManager.add(container.id, fragment, createTag(container.id, position)) } else { imeFragmentManager.attach(container.id, fragment, createTag(container.id, position)) } return fragment } override fun destroyItem(container : ViewGroup?, position : Int, obj : Any?) { val fragment = obj as ImeFragment imeFragmentManager.remove(fragment.tag) } override fun isViewFromObject(view : View?, obj : Any?) : Boolean = (obj as ImeFragment).view == view companion object { fun createTag(viewId : Int, itemId : Int) : String = "android:ime_switcher:$viewId:$itemId" } }
Fragmentっぽい実装を用意することで、Page単位にコントローラを分離できること・ドメインレイヤーとの繋ぎの所にもできるので欠かせないな〜という所。
ココらへん、ライブラリ化して公開します。多分。