💭
UIViewControllerの単体テストを書くために書いたコード
雑メモ。
特定の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