Skip to content

Instantly share code, notes, and snippets.

@ngtk
Created May 15, 2019 09:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ngtk/7d4a92ad506df7f9a36f75f6c63ec835 to your computer and use it in GitHub Desktop.
Save ngtk/7d4a92ad506df7f9a36f75f6c63ec835 to your computer and use it in GitHub Desktop.
`Accessible` provides the way to find the element by a consistent name in UI tests.
import UIKit
/**
`Accessible` provides the way to find the element by a consistent name
in UI tests.
`generateAccessibilityIdentifiers()` creates the identifier that forms
the combination of the class name and the property name that is styled
like "ClassName.propertyName" and sets it to `accessibilityIdentifier`
to each property that is UIView.
e.g.
- `WelcomeViewController.view`
- `WelcomeViewController.buttonStackView`
You can conform `Accessible` to your custom `UIViewController` or `UIView`
and call `generateAccessibilityIdentifiers()` after all view properties
are initalized like in the last line of `viewDidLoad()` or an initalizer.
**/
public protocol Accessible {
func generateAccessibilityIdentifiers()
}
public extension Accessible where Self: NSObject {
func generateAccessibilityIdentifiers() {
#if DEBUG
let mirror = Mirror(reflecting: self)
generateAccessibilityIdentifiers(mirror)
#endif
}
private func generateAccessibilityIdentifiers(_ mirror: Mirror) {
// If it has no children, we treat the object as objc implementation.
if mirror.children.isEmpty {
generateAccessibilityIdentifiersForObjc(mirror)
} else {
for child in mirror.children {
generateAccessibilityIdentifier(for: child)
}
}
if let superclassMirror = mirror.superclassMirror {
generateAccessibilityIdentifiers(superclassMirror)
}
}
private func generateAccessibilityIdentifier(for child: Mirror.Child) {
guard let view = child.value as? UIView else { return }
guard let label = child.label else { return }
let property = label.replacingOccurrences(of: ".storage", with: "") // lazy vars appends ".storage".
view.accessibilityIdentifier = "\(type(of: self)).\(property)"
}
private func generateAccessibilityIdentifiersForObjc(_ mirror: Mirror) {
var outCount: UInt32 = 0
guard let properties = class_copyPropertyList(mirror.subjectType as? AnyClass, &outCount) else { return }
// swiftlint:disable:next identifier_name
for i: UInt32 in 0 ..< outCount {
let property = properties[Int(i)]
let ignoreProperties = [
"window", // Weak referenced as the property of a view controller.
"viewIfLoaded", // `view` has been overridden by this property.
]
if
let name = property.name(),
!ignoreProperties.contains(name),
property.type() is UIView.Type,
let view = value(forKey: name) as? UIView {
view.accessibilityIdentifier = "\(type(of: self)).\(name)"
}
}
}
}
private extension objc_property_t {
func name() -> String? {
return String(cString: property_getName(self), encoding: .utf8)
}
func type() -> AnyClass? {
guard let attributes = String(cString: property_getAttributes(self)!, encoding: .utf8)
else { return nil }
guard let regexp = try? NSRegularExpression(pattern: "T@\"(.*)\"", options: []) else { return nil }
guard let result = regexp.firstMatch(
in: attributes,
options: [],
range: NSRange(location: 0, length: attributes.count)
) else { return nil }
let range = result.range(at: 1)
let className = attributes[range]
return NSClassFromString(className)
}
}
@a25max
Copy link

a25max commented Jul 9, 2019

L95はNSRangeの型なので
Range型にConvertしないと
attributes[range] でコンパイルエラーになります
なので以下のように変更したら動きました。

let range = result.range(at: 1)
if let range = Range(nsRange, in: attributes) {
    attributes[range]
}

@ngtk
Copy link
Author

ngtk commented Jul 9, 2019

環境差異ぽいですが、より新しくはそうですね。コメントありがとうございます!😊

@kamvoick
Copy link

kamvoick commented Sep 24, 2019

Hello, on iOS13 this protocol throws exception
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Project.ViewController 0x7ff2cf714870> valueForUndefinedKey:]: this class is not key value coding-compliant for the key tabBarObservedScrollView.' particularly in line 72. It's strange bc i have only one VC which is not a tabBarVC, it's an empty project and still throws that error. Do you know what might causing this problem, or how to fix it? Btw. thanks for sharing code

EDIT: quick fix between line 72 and 71 name != "tabBarObservedScrollView" and it's working. I think this might be a bug bc tabBarObservedScrollView applies to tvOS 13+ so why it's there idk

@ngtk
Copy link
Author

ngtk commented Sep 24, 2019

Sorry, but, I don't have any idea. I'll try this later on iOS 13.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment