💭

UIViewControllerの単体テストを書くために書いたコード

2023/04/27に公開

雑メモ。

特定のViewを取得する関数

extension UIView {
    func firstMatch<T: UIView>(
        identifier: String,
        file: StaticString = #filePath,
        line: UInt = #line
    ) throws -> T {
        try XCTUnwrap(_firstMatch(identifier: identifier), file: file, line: line)
    }

    private func _firstMatch<T: UIView>(
        identifier: String
    ) -> T? {
        if let matchedView = subviews.lazy
            .compactMap({ $0 as? T })
            .first(where: { $0.accessibilityIdentifier == identifier })
        {
            return matchedView
        }
        for subview in subviews {
            return subview._firstMatch(identifier: identifier)
        }

        return nil
    }
}

リフレクションを使ってAccessibilityIdentifierを設定する関数

private struct LabeledView {
	let view: UIAccessibilityIdentification
	let label: String

	init?(child: Mirror.Child) {
	    if let view = child.value as? UIView,
	       let identifier = child.label?
	       .replacingOccurrences(of: ".storage", with: "")
	       .replacingOccurrences(of: "$__lazy_storage_$_", with: "") {
		self.view = view
		label = identifier
	    } else {
		return nil
	    }
	}
}

func setUpAccessibilityIdentifiers() {
        Mirror(reflecting: base)
            .children
            .compactMap {
                LabeledView(child: $0)
            }
            .forEach {
                $0.view.accessibilityIdentifier = $0.label
            }
}

備考

  • storyboardを使っている場合、loadViewIfNeeded()を呼ぶとIBOutletがloadされる。
  • 安定性が上がるかもと思って、一応UIView.setAnimationsEnabled(false)した
    • 検証はしてない
  • Viewの更新から1msくらい待たないと、Viewの更新が反映されずにテストが実行され失敗することがあった
  • 動的にViewをaddやremoveしているViewはsetUpAccessibilityIdentifiers関数の敵となった
    • 動的にView階層が変わるの怖いから、isHiddenの制御でなるべく実装してほしい。

Discussion