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を自作していきます。

f:id:churabou:20191029215038p:plain:w300

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

参考

ちゃんとドキュメントがあって助かった。

developer.apple.com

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

ログ設計指針 - Qiita

ログの出力先

  • 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

ReSwift

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

Carbon

Boxパターンとかいうやつ

カレンダーを実装してみた

f:id:churabou:20190930234313p:plain

moment.jsに少々手こずったが1日はかからないくらいで実装できた。 iOSだともう少し時間がかかってしまう。

  • ググった感じ組み方はtableタグにheader2つのやり方と、divで囲んでulで並べる感じのが多かった。
  • table使ったことなかったのと、多分それで組んで困らない程度なので、前者で実装。