instantiateViewController(identifier:creator:)を試す

使ったことがないAPIはミニマムですぐ書く!

開発環境

> xcodebuild -version 
Xcode 13.3
Build version 13E113

モチベーション

instantiateViewController(identifier:creator:) を使ったことがないので使ってみたい

instantiateViewController(identifier:creator:) とは

Creates the specified view controller from the storyboard and initializes it using your custom initialization code. https://developer.apple.com/documentation/uikit/uistoryboard/3213989-instantiateviewcontroller

ViewControllerの初期化をカスタマイズできる。
どうカスタマイズできるのか。
本題に入る前に上記を使わない初期化方法を書く。

従来のViewControllerの初期化

ViewController -> SecondViewController に値を渡して遷移する場合を例にコードを書く。

ViewController.swift

import UIKit

final class ViewController: UIViewController {
    let number: Int = 1

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let vc = SecondBuilder(number: number).build()
        self.present(vc, animated: true)
    }
}

SecondViewController.swift

import UIKit

final class SecondViewController: UIViewController {
    var number: Int!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        print("number: \(number)")  // "number: Optional(1)"
    }
}

struct SecondBuilder {
    let number: Int
    
    func build() -> SecondViewController {
        let storyboard = UIStoryboard(name: "Second", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "Second") as! SecondViewController
        vc.number = number
        return vc
    }
}

所感

悪くないが、 SecondViewControllernumber を外から注入するため変数にしてしまっている。
変数をやめて定数にしたい。
そんな時に instantiateViewController(identifier:creator:) が有効だぞ!

instantiateViewController(identifier:creator:) を使って書き直す

ViewController.swift

import UIKit

final class ViewController: UIViewController {
    let number: Int = 1

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let storyboard = UIStoryboard(name: "Second", bundle: nil)
        let vc = storyboard.instantiateViewController(identifier: "Second") { coder in
            return SecondViewController(coder: coder, number: 1)
        }
        self.present(vc, animated: true)
    }
}

SecondViewController.swift

import UIKit

final class SecondViewController: UIViewController {
    let number: Int
    
    init?(coder: NSCoder, number: Int) {
        self.number = number
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        print("number: \(number)")  // "number: 1"
    }
}

所感

遷移先のイニシャライザを新規で生やす必要があるが、変数を定数にできるので心理的安全性は高まりそう。

import UIKit

final class SecondViewController: UIViewController {
    let viewModel: ViewModel
    
    struct ViewModel {
        let number: Int
        let hoge: String
        let fuga: String
        let piyo: String
    }
    
    init?(coder: NSCoder, viewModel: ViewModel) {
        self.viewModel = viewModel
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        print("number: \(viewModel.number)")
    }
}

注入したいプロパティが多い時は構造体切るなりするとイニシャライザの引数の個数も抑えられるし、iOS13以降なら使わない手はなさそう。

参考

Info.plistの数値のvalueをインクリメントするワンライナー

ありえんほど疲れた。egrep何も分からん。結局分からんくて使わずに書いた。

モチベーション

Info.plistの数値のvalueをインクリメントするワンライナーを書きたい。

題材として、個人開発しているSwiftUIアプリのInfo.plistを改造して試してみる。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSHealthShareUsageDescription</key>
    <string>Get a bike workout.</string>
    <key>HogeBuildVersion</key>
    <string>156</string>  // インクリメントしたい!
</dict>
</plist>

上記の HogeBuildVersionvalueの156をインクリメントするシェルスクリプトを書く。

コード

ワンライナー

$ plutil -extract HogeBuildVersion xml1 -o - ./Info.plist |  sed -n "s/<string>\(.*\)<\/string>/\1/p" | awk '{print $0+1}' | xargs -I NUM plutil -replace HogeBuildVersion -string NUM ./Info.plist 

見やすく改行したver.

$ plutil -extract HogeBuildVersion xml1 -o - ./Info.plist |
sed -n "s/<string>\(.*\)<\/string>/\1/p" |
awk '{print $0+1}' |
xargs -I NUM plutil -replace HogeBuildVersion -string NUM ./Info.plist

分解して解説

1: plutil -extract HogeBuildVersion xml1 -o - ./Info.plist |
2: sed -n "s/<string>\(.*\)<\/string>/\1/p" |
3: awk '{print $0+1}' |
4: xargs -I NUM plutil -replace HogeBuildVersion -string NUM ./Info.plist

1. plutil

1: plutil -extract HogeBuildVersion xml1 -o - ./Info.plist -n

plutil は .plist ファイルをいい感じに処理できるコマンド。
key HogeBuildVersionvalueをstdoutする。plistタグより上の内容もstdoutされてしまうので2以降でトリミングしていく。

$ plutil -extract HogeBuildVersion xml1 -o - ./Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<string>156</string>
</plist>

2. sed

2: sed -n "s/<string>\(.*\)<\/string>/\1/p"

ここに一番時間割いた。sedが一番理解しやすかった気がする。
-n で引っかかった行のみ出力。

s/BEFORE/AFTER/p としたときに、BEFORE内で \(\)で囲んだものを AFTER 内で \1 として利用できる。あとはスラッシュのエスケープシーケンスで見づらくなっているだけ。 末尾の p-n と併用することで結果を出力するために記述。

3. awk

3: awk '{print $0+1}'

パイプで受け取った値をインクリメント。

4. xargs + plutil

4: xargs -I NUM plutil -replace HogeBuildVersion -string NUM ./Info.plist

xargs -I {VAR} でパイプで受け取った値を変数らしきものに入れることができる。後方で使用可能。(処理的にはreplaceしているっぽい?)

plutil で指定したkeyのvalueを更新。

参考

Combineのdebounceを試す

開発環境

> xcodebuild -version
Xcode 13.3
Build version 13E113

モチベーション

debounceを使ったことがないので使ってみたい

debounce

Publishes elements only after a specified time interval elapses between events.
https://developer.apple.com/documentation/combine/publisher/debounce(for:scheduler:options:)

イベント間に指定したインターバルが経過した後にのみ要素をpublishする。

とりあえず公式ドキュメントのコードを動かしてみる。

公式ドキュメントのコード

let bounces:[(Int,TimeInterval)] = [
    (0, 0),
    (1, 0.25),  // 0.25s interval since last index
    (2, 1),     // 0.75s interval since last index
    (3, 1.25),  // 0.25s interval since last index
    (4, 1.5),   // 0.25s interval since last index
    (5, 2)      // 0.5s interval since last index
]

let subject = PassthroughSubject<Int, Never>()
cancellable = subject
    .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
    .sink { index in
        print ("Received index \(index)")
    }

for bounce in bounces {
    DispatchQueue.main.asyncAfter(deadline: .now() + bounce.1) {
        subject.send(bounce.0)
    }
}

// Prints:
//  Received index 1
//  Received index 4
//  Received index 5

コメントを読んで手元で順を追っていったら分かった。 以下の観点が大事っぽい。

  • イベント間に指定したインターバルが経過した後にのみ要素をpublishする
  • sendした時間をs, インターバルをtとすると、[s, s+t) の間に別の値がsendされなければpublishされる
    • [s, s+t] ではない

上記を踏まえて処理を見ていくと、以下のようになる。

  • Time 0: index 0がSend
    • Subscriber側で0.5秒のdebounceが設定されているため、0.5秒までに何もSendされなければ.sinkに到達する
  • Time 0.25: index 1がSend
    • 0.5秒になるまでにSendされたため、index 0は破棄されて、index 1は0.75秒までに何もSendされなければ.sinkに到達する
  • Time: 0.75
    • index 1が.sinkに到達する
  • Time: 1: index 2がSend
    • 同様に1.5(= 1.0 + 0.5)秒までに何もSendされなければ.sinkに到達する
  • Time: 1.25: index 3がSend
    • 1.5秒になるまでにSendされたため、index 2は破棄されて、index 3は1.75(1.25+0.5)秒までに何もSendされなければ.sinkに到達する
  • Time: 1.5: index 4がSend
    • 1.75秒になるまでにSendされたため、index 3は破棄されて、index 4は2.0(1.5+0.5)秒までに何もSendされなければ.sinkに到達する
  • Time 2.0: index 4が.sinkに到達し、index 5がSend
  • Time 2.5: index 5が.sinkに到達する

使い時

  • CoreLocationでGPSデータを取得する時
    • 徒歩などの、ある程度速度が知れていてGPSデータの取得を抑制できるシチュエーション

無理やり一つ出してみたが、大量のイベントをアプリ側で処理する事例が他に思いつかなかった。というのもサーバー側で抑制しないと通信量えらいことになりそう。アプリ内で大量の処理を抑制したい時に使う?例が思いついたら別記事を書く。

参考