Created
May 15, 2019 09:12
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sorry, but, I don't have any idea. I'll try this later on iOS 13.