Skip to content

0.3.24

TL;DR. Stability is improved, code was refactored to support new changes.

Description of the bug:

The bug was in an app that doesn't initialize accessibility hierarchy using private API. The workaround for that app was to initialize accessibility hierarchy,
but when I read a comment in that app, about why initialization of accessibility via private API was disabled, I decided to get rid of using accessibility and recreate hierarchy from scratch.

Notes about why:
- Accessibility hierarchy is unstable and can sometimes fails to be initialized (via private API), sometimes it initializes by itself without us using private API.
- Accessibility hierarchy has problems with compatibility, it may be different for different iOS versions
- Accessibility hiearachy often doesn't represent visual state of the GUI, lacks elements, or provides combined element instead of separate elements
- Etc, etc

Attemptes solution:

I changed implementations of `ViewHierarchyElement` to use `KeyboardPrivateApi` instead of accessibility, but realized that it won't work and the whole thing should be rewritten.

`OverridingChildrenTestabilityElementViewHierarchyElement`, `KeyboardLayoutViewHierarchyElement`,  `KeyboardKeyViewHierarchyElement` were removed.

Why it didn't work:

The reason is that getting view hierarchy is a separate mechanism (`ViewHierarchyProviderImpl`), the other one is visibility checking (`CheckVisibilityIpcMethodHandler`),
which goes to `AccessibilityUniqueObjectMap`, gets instance of another type (`TestabilityElement`) and doesn't see same fields as in those custom `ViewHierarchyElement` implementations.
That was a mistake. Now only `TestabilityElementViewHierarchyElement` is used, so visibility check is in sync with `ViewHierarchyProviderImpl`.
In practice there was a bug, `CheckVisibilityIpcMethodHandler` called `NonViewVisibilityCkeckerImpl` and it couldn't get the frame of the element (`frameRelativeToScreen`).

Final solution:

Instead, the old mechanism to create accessibility hierarchy was used - `TestabilityElement`.

I added categories for `UIKeyboardLayout` and `UIKBTree` that replace default behavior: `UIKeyboardLayout` returns children of type `UIKBTree`, `UIKBTree` returns proper frames and custom values (and other things).

This required to implement getter of custom values in Objective-C to override `customValues` (without messing with the state, for example, previously custom values couldn't be computed, only stored in associated value of NSObject as a state).

Notes about why it was needed and implementation details:
- The whole thing with `TestabilityElement` being an Objective-C protocol was initially created,
  because it allows us to easily override methods of NSObject (and especially), while reusing the inheritance hierarchy of Objective-C (we can write a custom method of getting test of UILabel, for example,
  in a separate file, in few lines of code, and it will work fast).
- The extensions could be written in Swift, but there were problems, I described them in headers for those Obj-C categories.

New private API was exposed, Swift wrappers were added. The new hierarchy now looks the same as before (that was based on accessibility initialized using private API)

Major changes you should notice:
- `TestabilityElement` now has `mb_testability_getSerializedCustomValues`, this means that it can be overriden in subclasses of `NSObject` via a category (or an extension in Swift)

Minor changes:
- `mb_cast` was added, it is a throwing version of `as?` cast, it throws error with default text if `as?`-cast fails.
- Added another example in `Disassembling.md`

Refactorings:
- Code of creating `TestablilityElement` was moved to `NonTestabilityElementFallbackTestabilityElement`, because we have OOP, let's use it
Small optimizations:
- `static let` was replaced with `static var` in `DefaultTestabilityElementValues` and dummy version of `TestabilityCustomValues`, because the creation of a small object or value is faster than synchronization inside `static let`

Changes in tests:
- Modern iOS versions (15 and 16) are now tested WITHOUT initializing accessibility hierarchy via private API
- Tests on iOS 14 remain using it, because it's still a feature, it is used, and it should be tested
Assets 2