Codementor Events

Fixing Developer Inconveniences — iOS Automation

Published Sep 29, 2019Last updated Mar 26, 2020

While automating iOS apps with Appium, QA team tends to use xPath to find UI elements. But, xPath is very slow and making Automation world hell to work with.

So, we came up with an idea to use AccessibilityIdentifier provided by UIKit framework and it worked great.

But, now the problem is in developer’s lounge.

Providing AccessibilityIdentifier for each UI element is not an easy task. Also, to ensure all future UI elements to include that variable is prone to error.

  • Pull Requests will be bloated with messages to add AccessibilityIdentifier whenever a developer misses doing that .
  • The feedback loop between different teams for missing/bad AccessibilityIdentifier is high.

We looked for better ways and came up with a crazy idea to generate AccessibilityIdentifier for each variable automatically.

We looked into Swift’s Mirror API which internally uses reflection and found a way to mirror an instance in runtime. Mirror API greatly helped in iterating through all the variables inside an instance.

But, there is one caveat. Mirror API provides read-only variables. So, we had to use UnsafePointer to modify the instance directly on its memory level.

Time to see some code.

protocol AccessbilityIdentifierInjector { 
  func injectAccessibilityIdentifiers()
}

AccessibilityIdentifierInjector protocol provides a function with a default implementation to auto-generate accessibility identifiers.

extension AccessibilityIdentifierInjector { 
  func injectAccessibilityIdentifiers() { 
    var mirror: Mirror? = Mirror(reflecting: self) 
    repeat { 
      if let mirror = mirror { 
        injectOn(mirror: mirror) 
      } 
      mirror = mirror?.superclassMirror 
    } while (mirror != nil) 
  } 
  
  private func injectOn(mirror: Mirror) { 
    for (name, value) in mirror.children { 
      if var value = value as? UIView { 
        UnsafeMutablePointer(&value).pointee.accessibilityIdentifier = name 
      } 
    }
  }
}

injectAccesibilityIdentifiers() does two things.

  • Iterates starting from current instance to its superclass till there is no superclass.
  • Filters only UIView types and inject AccessibilityIdentifier using UnsafePointer object of that variable.

Then, we need to make UIView & UIViewController conform to this protocol.

extension UIViewController: AccessibilityIdentifierInjector {}  
extension UIView: AccessibilityIdentifierInjector {}

and call injectAccessibilityIdentifiers() function once it is initialized.

class BaseView: UIView { 
  override init(frame: CGRect) { 
    super.init(frame: frame) 
    injectAccessibilityIdentifiers() 
  }
}

class BaseViewController: UIViewController { 
  override func viewDidLoad() { 
    super.viewDidLoad() 
    injectAccessibilityIdentifiers() 
  }
}

That’s it. We are done.

Caveat: We won’t be able to set accessibilityIdentifier for lazy variables unless it is initialized when you call injectAccessibilityIdentifiers() function.

Note:

  • Our app is closed for a specific audience. We don’t have an actual requirement to set accessibilityIdentifiers. Not recommended, if your app has customized namings for accessibilityIdentifiers.
  • Using Mirror API’s can be slower sometimes. If accessibilityIdentifier is not needed in release builds, we can inject it only for debug/staging builds.

Thank you.

Discover and read more posts from Dinesh Raja
get started