Counter with useReducer+Flux 的な何か
後で振り返るように。
action&state
interface Action<ActionType> { type: ActionType; payload?: any; } enum CounterActionType { Increase, Decrease } interface CounterState { count: number; } type CounterAction = Action<CounterActionType>;
function reducer(state: CounterState, action: CounterAction): CounterState { switch (action.type) { case CounterActionType.Increase: return { count: state.count + 1 }; case CounterActionType.Decrease: return { count: state.count - 1 }; } } class ActionCreater { private readonly dispach: React.Dispatch<CounterAction>; constructor(dispach: React.Dispatch<CounterAction>) { this.dispach = dispach; } increase() { const action: CounterAction = { type: CounterActionType.Increase }; this.dispach(action); } decrease() { const action: CounterAction = { type: CounterActionType.Decrease }; this.dispach(action); } }
View
interface CounterViewProps { count: number; increase: () => void; decrease: () => void; } const CounterView: React.FC<CounterViewProps> = props => ( <div> <p>Count: {props.count} </p> <button onClick={() => props.decrease()}>-</button> <button onClick={() => props.increase()}>+</button> </div> );
import React, { useReducer } from 'react'; function CounterPage(): any { const initialState: CounterState = { count: 0 }; const [state, dispach] = useReducer(reducer, initialState); const actionCreater = new ActionCreater(dispach); return ( <CounterView count={state.count} increase={() => actionCreater.increase()} decrease={() => actionCreater.decrease()} /> ); } export default CounterPage;
Swift URLからパラメーターを取り出す
ディープリンク対応する際にこちらのリポジトリを参考に実装しました。
urlのパラメーター(具体的にはuser?id=0
user/:id
的なやつ)はpathParameterとqueryParameterと呼ばれているらしいです。
使用例
URLの文字列をpattern文字列を指定してパースします。 パース結果のオブジェクトはqueryParamsとpathParamsプロパティーを持ちます。 これらはSwift4.2で追加されたDynamicMemberLookUpを使ったPrameterオブジェクトとして定義しています。
例えば通常のパラメーターの場合はqueryParamsで取得できます
let pattern: "myapp://user" let actual: "myapp://user?id=123" ParsedObject(url: actual, pattern: pattern).queryParams.id // "123"
パターン文字列:id
のように指定すると
pathParamsの値にマップされてpathParams.id
として取得できます。
let pattern: "myapp://user/:id" let actual: "myapp://user/123" ParsedObject(url: actual, pattern: pattern).pathParams.id // "123"
先述した通り内部でDynamicMemberLookUpを利用することで、 パターンの文字列がそのまま変数名になります。
let pattern: "myapp://user/:userId" let actual: "myapp://user/123" ParsedObject(url: actual, pattern: pattern).pathParams.userId // "123"
どちらもある場合はこんな感じです。
let pattern: "myapp://user/:id/" let actual: "myapp://user/xxx?id=yyy" ParsedObject(url: actual, pattern: pattern).queryParams.id // "yyyy" ParsedObject(url: actual, pattern: pattern).pathParams.id // "xxxx"
ソース
Ap
struct ParsedURL { let openUrlOption: OpenURLOption let pathParams: Parameter let queryParams: Parameter init?(_ patternUrl: URL, url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) { guard url.scheme == patternUrl.scheme, url.host == patternUrl.host , patternUrl.pathComponents.count == url.pathComponents.count else { return nil } let keywordPrefix = ":" // init pathParams do { var params: [String: String] = [:] // compare host if patternHost.hasPrefix(keywordPrefix) { let key = String(patternHost[keywordPrefix.endIndex...]) params[key] = url.host } // compare path components for (component, pathCompoent) in zip(patternUrl.pathComponents, url.pathComponents) { if component.hasPrefix(keywordPrefix) { let key = String(component[keywordPrefix.endIndex...]) params[key] = pathCompoent } else if component != pathCompoent { return nil } } self.pathParams = Parameter(values: params) } // init queryParams do { if let components = URLComponents(url: url, resolvingAgainstBaseURL: true) { var params: [String: String] = [:] components.queryItems?.forEach { params[$0.name] = $0.value } self.queryParams = Parameter(values: params) } else { self.queryParams = Parameter(values: [:]) } } self.openUrlOption = OpenURLOption(options: options) } } extension Scheme { @dynamicMemberLookup struct Parameter { let values: [String: String] subscript(dynamicMember name: String) -> String? { return values[name] } } struct OpenURLOption { let sourceApplication: String? let annotation: UIDocumentInteractionController? let openInPlace: Bool init(options: [UIApplication.OpenURLOptionsKey: Any]) { self.sourceApplication = options[.sourceApplication] as? String self.annotation = options[.annotation] as? UIDocumentInteractionController self.openInPlace = options[.openInPlace] as? Bool ?? false } } }
ContainerViewController / Custom TabBarControllerを作る
TabBarをカスタマイズしたい
標準のUITabBarControllerだと、痒いところをカスタマイズするのが大変です。 TabBarをUIViewのサブクラスとして自作することで、自由にUIやアニメーションの実装ができるようになると思います。 UITabBarController - UIKit | Apple Developer Documentation
TabViewControllerの挙動
iOS5からContainerViewControllerを作成するAPIが公開されたようで、それを使ってTabViewControllerを自作していきます。
TabViewControllerの挙動を整理しておくと、以下のようにタブ切り替えのUIを実現しつつ適切なタイミングでライフサイクルメソッドが呼ばれることになります。
- タブをタップすると画面が切り替わる
切り替わる際は以下の順でライフサイクルイベントが発火する
- 遷移先ViewControllerのViewDidDisappear
- 遷移元ViewControllerのviewWillDisappear
- 遷移元ViewControllerのviewDidDisappear
- 遷移先ViewControllerのDidAppear
PushやModalで遷移すると、TabViewControllerと選択されているViewControllerのViewDidAppearが呼ばれる
- 同一タブを選択しても何も起こらない。
必要なメソッド
以下の3種類のUIViewControllerのメソッドを使うことで実現することができます。
- willMove / didMove
- addChild / removeFromParent
- begeinAppearanceTransition / endAppearenceTransition
実装例
ViewControllerにTabViewとContentViewを配置します。
final class ContainerTapViewController: UIViewController { private let contentView = UIView() private let tabView = TabView() private let controllers: [UIViewController] = [] override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white view.addSubview(contentView) view.addSubview(tabView) } }
最初に選択したいViewControllerを追加します。
let controller = controllers[0] controller.willMove(toParent: self) controller.view.frame = view.bounds addChild(controller) contentView.addSubview(controller.view)
タグが切り替わった際に以下のようにメソッドを呼びます
func didSelectTag(_ selectedIndex: Int) { let fromController = selectedViewController let toController = controllers[selectedIndex] selectedViewController = toController self.remove(controller: fromController) self.add(controller: toController) }
add, removeメソッドの実装はこのようにします。
func add(controller: UIViewController) { addChild(controller) controller.view.frame = view.bounds contentView.addSubview(controller.view) controller.didMove(toParent: self) } func remove(controller: UIViewController) { controller.willMove(toParent: nil) controller.removeFromParent() controller.view.removeFromSuperview() }
またUITabBarViewConntrollerとライフサイクルイベントの発火タイミングを揃えるとこんな感じになります。 toViewController.begin, fromViewController.begin, fromViewController.end, toViewController.end とfromではさんであげる必要があります。
func didSelectTab(_ selectedIndex: Int) { guard let fromViewController = selectedViewController, viewControllers.count > selectedIndex else { return } let toViewController = viewControllers[selectedIndex] if fromViewController !== toViewController { toViewController.beginAppearanceTransition(true, animated: true) fromViewController.beginAppearanceTransition(false, animated: true) add(controller: toViewController) remove(controller: fromViewController) fromViewController.endAppearanceTransition() toViewController.endAppearanceTransition() selectedViewController = toViewController } }
上記のようにお作法に従えばUITabViewControllerとほぼ同じなContainerViewControllerを作成することができます 実際に手を動かさないと理解をできないのとメソッドを呼ばなくても動いたりするのでちょっと勉強しにくい コピペで動くものを一応貼っておきます。
Custom TabBarViewController · GitHub
参考
ちゃんとドキュメントがあって助かった。
https://cocoacasts.com/managing-view-controllers-with-container-view-controllers
https://github.com/mixi-inc/iOSTraining/wiki/2.3-Custom-Container-View-Controller
Swift Logger
Literal Expressionを使って情報を付け足し出力する。
struct Logger { static func debug(_ item: Any, file: String = #file, line: Int = #line, function: String = #function) { #if DEBUG let components = [ "💚Debug💚 \(Date())", "file: \(file)", "line: \(line)", "function: \(function)", "\(item)" ] print(components.joined(separator: "\n")) #endif } }
Logger.debug("debug string")
💚Debug💚 2019-10-24 11:22:52 +0000 file: /ViewController.swift line: 7 function: viewDidLoad() debug string
Consoleに色をつけて出力する
xcode8からcolorをサポートされなくなったとなっている。 絵文字💚で色の代用はできるが、本当にできないのだろうか。
Logクラスの設計
メソッド | 優先度 |
---|---|
trace | 低 |
debug | |
info | |
warn | |
error | 高 |
ログの出力先
- console
- file
- remote
DI Containerをサクッと実装してみる
AngularJSのInjectorのソースを読んでから一回DIライブラリを作って見たくなったので、何も考えずSwiftで実装してみる この方法が良い悪いの話は置いておいて、Angularのようなインターフェースを目指す。
class ApiClient {} class QiitaRepository { let client: ApiClient init(client: ApiClient) { self.client = client } }
let container = Container() .register(ApiClient.self) { ApiClient() } .register(QiitaRepository.self) { (client: ApiClient) in QiitaRepository(client: client) } container.resolve(QiitaRepository.self)
登録する値を一意に定めるためのKeyを作成する。 生成する(クラス|構造体)、それに必要なDependency、パラーメーターに応じて一意に定めればいい。
struct ServiceKey { let serviceType: Any.Type init(serviceType: Any.Type) { self.serviceType = serviceType print(serviceType) print(ObjectIdentifier(serviceType)) } } extension ServiceKey: Equatable { static func == (lhs: ServiceKey, rhs: ServiceKey) -> Bool { return lhs.serviceType == rhs.serviceType } } extension ServiceKey: Hashable { public func hash(into hasher: inout Hasher) { ObjectIdentifier(serviceType).hash(into: &hasher) } }
typealias Factory = () -> Any? final class Container { private var services: [ServiceKey: Factory] = [:] private func getService<Service>(_ type: Service.Type) -> Service? { let key = ServiceKey(serviceType: type) let service = services[key]?() return service as? Service } func register<Service>(_ type: Service.Type, value: @escaping () -> Service) { let key = ServiceKey(serviceType: type) services[key] = { value() } } func register<Service, Argument>(_ type: Service.Type, factory: @escaping (Argument) -> Service) { let key = ServiceKey(serviceType: type) services[key] = { guard let arg1 = self.getService(Argument.self) else { // argunemt1 is not registerd return nil } return factory(arg1) } } func resolve<Service>() -> Service { return getService(Service.self)! } }
DI
静的DI/動的DI
- container
- resolver
- injector
provider
inject
- register
- invoke
Object Pool
Containerにこのserviceをこのargumentとこのdepencdencyでお願いしますと登録する。 ContainerはAnyで型消去されたProviderを持つ resolveするタイミングで型情報を教えてProviderを取り出す。
Swift Type Eraser is 何?
ググると出てくるやつ
多分 try! Swift 2016で発表があってそれのまとめ記事がよく出てくる。
protocol Pokemon { associatedtype Element var element: Element { get } }
associatedtype を持つProtocolで変数を宣言するとエラーになる。
let pokemon: Pokemon // Protocol 'Pokemon' can only be used as a generic constraint because it has Self or associated type requirements
struct Thunder {} struct Fire {} class Pikachu: Pokemon { var element: Thunder { Thunder() } } class Raichu: Pokemon { var element: Thunder { Thunder() } } class Charmander { var element: Fire { Fire() } }
具体型なら定義できる
let pikachu: Pikachu let raichu: Raichu
でんきタイプのポケモンの配列を定義するにはどうしたらいいか?
class AnyPokemon<PokemonType>: Pokemon { var element: PokemonType init<P: Pokemon>(_ pokemon: P) where P.Element == PokemonType { self.element = pokemon.element } }
let thumderPokemon1: AnyPokemon<Thunder> = AnyPokemon(Pikachu()) let thumderPokemon2: AnyPokemon<Thunder> = AnyPokemon(Raichu())
当たり前という気がするのだが。。
Type Eraserの実装パターン
Closure APIKitのsendメソッドのクロージャーとかも、これに似てるかもしれない。
Box
ライブラリでよく使ってるので参考に
RealmSwift
ReSwfit
protocol AnyPokemon { var _element: Any { get } } protocol Pokemon: AnyPokemon { associatedtype Element var element: Element { get } } extension Pokemon { var _element: Any { element } }
PokemonをAnyPokemonに準拠するという方法
let pokemons: [AnyPokemon] = [Pikachu(), Raichu()]
Carbon
Boxパターンとかいうやつ
カレンダーを実装してみた
moment.jsに少々手こずったが1日はかからないくらいで実装できた。 iOSだともう少し時間がかかってしまう。
- ググった感じ組み方はtableタグにheader2つのやり方と、divで囲んでulで並べる感じのが多かった。
- table使ったことなかったのと、多分それで組んで困らない程度なので、前者で実装。