Skip to content

Commit 7f7b18d

Browse files
authored
Merge pull request #80 from cashapp/entin/scale-tiles
Respect original safe area insets when tiling and stitching snapshot
2 parents 9edbbf2 + 77bbef6 commit 7f7b18d

File tree

6 files changed

+126
-40
lines changed

6 files changed

+126
-40
lines changed

Example/Podfile.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,4 @@ SPEC CHECKSUMS:
4545

4646
PODFILE CHECKSUM: 9d965121b60425a32e1b4c884c4c6b7aff62fa52
4747

48-
COCOAPODS: 1.9.1
48+
COCOAPODS: 1.11.0.rc.1

Example/SnapshotTests/AccessibilityPropertiesTests.swift

+73-32
Original file line numberDiff line numberDiff line change
@@ -85,22 +85,10 @@ final class AccessibilitySnapshotTests: SnapshotTestCase {
8585

8686
// This test is currently disabled due to a bug in iOSSnapshotTestCase. See cashapp/AccessibilitySnapshot#75.
8787
func testLargeViewThatRequiresTiling() throws {
88-
let view = UIView(frame: CGRect(x: 0, y: 0, width: 3000, height: 3000))
89-
90-
let gradientLayer = CAGradientLayer()
91-
gradientLayer.colors = [UIColor.blue.cgColor, UIColor.white.cgColor]
92-
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
93-
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
94-
view.layer.addSublayer(gradientLayer)
95-
gradientLayer.frame = view.bounds
96-
97-
let label = UILabel()
98-
label.text = "Hello world"
99-
label.textColor = .red
100-
view.addSubview(label)
101-
102-
label.sizeToFit()
103-
label.center = view.point(at: .center)
88+
let view = GradientBackgroundView(
89+
frame: CGRect(x: 0, y: 0, width: 3000, height: 3000),
90+
showSafeAreaInsets: true
91+
)
10492

10593
usingDrawViewHierarchyInRect {
10694
SnapshotVerifyAccessibility(view, useMonochromeSnapshot: false)
@@ -158,24 +146,13 @@ final class AccessibilitySnapshotTests: SnapshotTestCase {
158146

159147
// This test is currently disabled due to a bug in iOSSnapshotTestCase. See cashapp/AccessibilitySnapshot#75.
160148
func testLargeViewInViewControllerThatRequiresTiling() {
161-
let view = UIView(frame: CGRect(x: 0, y: 0, width: 3000, height: 3000))
162-
163-
let gradientLayer = CAGradientLayer()
164-
gradientLayer.colors = [UIColor.blue.cgColor, UIColor.white.cgColor]
165-
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
166-
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
167-
view.layer.addSublayer(gradientLayer)
168-
gradientLayer.frame = view.bounds
169-
170-
let label = UILabel()
171-
label.text = "Hello world"
172-
label.textColor = .red
173-
view.addSubview(label)
174-
175-
label.sizeToFit()
176-
label.center = view.point(at: .center)
149+
let view = GradientBackgroundView(
150+
frame: CGRect(x: 0, y: 0, width: 3000, height: 3000),
151+
showSafeAreaInsets: true
152+
)
177153

178154
let viewController = UIViewController()
155+
viewController.additionalSafeAreaInsets = .init(top: 600, left: 1000, bottom: 300, right: 100)
179156
viewController.view = view
180157

181158
let parent = UIViewController()
@@ -196,4 +173,68 @@ final class AccessibilitySnapshotTests: SnapshotTestCase {
196173
usesDrawViewHierarchyInRect = oldValue
197174
}
198175

176+
// MARK: - Private Types
177+
178+
private final class GradientBackgroundView: UIView {
179+
180+
// MARK: - Life Cycle
181+
182+
init(frame: CGRect, showSafeAreaInsets: Bool) {
183+
super.init(frame: frame)
184+
185+
gradientLayer.colors = [UIColor.blue.cgColor, UIColor.white.cgColor]
186+
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
187+
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
188+
layer.addSublayer(gradientLayer)
189+
190+
label.text = "Hello world"
191+
label.textColor = .red
192+
label.backgroundColor = .black
193+
addSubview(label)
194+
195+
safeAreaView.layer.borderColor = UIColor.red.cgColor
196+
safeAreaView.layer.borderWidth = 1
197+
safeAreaView.isHidden = !showSafeAreaInsets
198+
addSubview(safeAreaView)
199+
200+
layoutMargins = .init(top: 8, left: 8, bottom: 8, right: 8)
201+
insetsLayoutMarginsFromSafeArea = true
202+
203+
layoutMarginsView.layer.borderColor = UIColor.green.cgColor
204+
layoutMarginsView.layer.borderWidth = 0.5
205+
layoutMarginsView.isHidden = !showSafeAreaInsets
206+
addSubview(layoutMarginsView)
207+
}
208+
209+
@available(*, unavailable)
210+
required init?(coder: NSCoder) {
211+
fatalError("init(coder:) has not been implemented")
212+
}
213+
214+
// MARK: - Private Properties
215+
216+
private let gradientLayer: CAGradientLayer = .init()
217+
218+
private let label: UILabel = .init()
219+
220+
private let safeAreaView: UIView = .init()
221+
222+
private let layoutMarginsView: UIView = .init()
223+
224+
// MARK: - UIView
225+
226+
override func layoutSubviews() {
227+
gradientLayer.frame = bounds
228+
229+
let insetBounds = bounds.inset(by: safeAreaInsets)
230+
231+
label.sizeToFit()
232+
label.center = CGPoint(x: insetBounds.midX, y: insetBounds.midY)
233+
234+
safeAreaView.frame = insetBounds
235+
layoutMarginsView.frame = bounds.inset(by: layoutMargins)
236+
}
237+
238+
}
239+
199240
}

Sources/AccessibilitySnapshot/Core/Swift/Classes/AccessibilitySnapshotView.swift

+44-5
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ public final class AccessibilitySnapshotView: UIView {
129129
let viewController = containedView.next as? UIViewController
130130
let originalParent = viewController?.parent
131131
let originalSuperviewAndIndex = containedView.superviewWithSubviewIndex()
132-
viewController?.removeFromParent()
133132

133+
viewController?.removeFromParent()
134134
addSubview(containedView)
135135

136136
defer {
@@ -156,6 +156,10 @@ public final class AccessibilitySnapshotView: UIView {
156156
)
157157
snapshotView.bounds.size = containedView.bounds.size
158158

159+
// Complete the layout pass after the view is restored to this container, in case it was modified during the
160+
// rendering process (i.e. when the rendering is tiled and stitched).
161+
containedView.layoutIfNeeded()
162+
159163
let parser = AccessibilityHierarchyParser()
160164
let markers = parser.parseAccessibilityElements(in: containedView)
161165

@@ -832,13 +836,44 @@ private extension UIView {
832836
return
833837
}
834838

839+
let originalSafeArea = bounds.inset(by: safeAreaInsets)
840+
835841
let originalSuperview = superview
842+
let originalOrigin = frame.origin
843+
let originalAutoresizingMask = autoresizingMask
836844
defer {
837845
originalSuperview?.addSubview(self)
846+
frame.origin = originalOrigin
847+
autoresizingMask = originalAutoresizingMask
838848
}
839849

840-
let frameView = UIView()
841-
frameView.addSubview(self)
850+
let frameView = UIView(frame: frame)
851+
originalSuperview?.addSubview(frameView)
852+
defer {
853+
frameView.removeFromSuperview()
854+
}
855+
856+
autoresizingMask = []
857+
frame.origin = .zero
858+
859+
let containerViewController = UIViewController()
860+
let containerView = containerViewController.view!
861+
containerView.frame = frame
862+
containerView.autoresizingMask = []
863+
containerView.addSubview(self)
864+
frameView.addSubview(containerView)
865+
866+
// Run the run loop for one cycle so that the safe area changes caused by restructuring the view hierarhcy are
867+
// propogated. Then calculate the required additional safe area insets to create the equivalent original safe
868+
// area. This new change will be propogated automatically when we draw the hierarchy for the first time.
869+
RunLoop.current.run(until: Date())
870+
let currentSafeArea = containerView.convert(bounds.inset(by: safeAreaInsets), from: self)
871+
containerViewController.additionalSafeAreaInsets = UIEdgeInsets(
872+
top: originalSafeArea.minY - currentSafeArea.minY,
873+
left: originalSafeArea.minX - currentSafeArea.minX,
874+
bottom: currentSafeArea.maxY - originalSafeArea.maxY,
875+
right: currentSafeArea.maxX - originalSafeArea.maxX
876+
)
842877

843878
let bounds = self.bounds
844879
var tileRect: CGRect = .zero
@@ -849,9 +884,13 @@ private extension UIView {
849884

850885
while tileRect.minX < bounds.maxX {
851886
tileRect.size.width = min(tileRect.minX + UIView.tileSideLength, bounds.maxX) - tileRect.minX
852-
853887
frameView.frame.size = tileRect.size
854-
frame.origin = CGPoint(x: -tileRect.minX, y: -tileRect.minY)
888+
889+
// Move the origin of the `frameView` and `containerView` such that the frame is over the right area of
890+
// the snapshotted view, but the snapshotted view stays fixed relative to the `frameView`'s superview
891+
// (so the view's position on screen doesn't change).
892+
frameView.frame.origin = CGPoint(x: tileRect.minX, y: tileRect.minY)
893+
containerView.frame.origin = CGPoint(x: -tileRect.minX, y: -tileRect.minY)
855894

856895
UIGraphicsImageRenderer(bounds: frameView.bounds)
857896
.image { _ in

Sources/AccessibilitySnapshot/iOSSnapshotTestCase/FBSnapshotTestCase+Accessibility.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ extension FBSnapshotTestCase {
2525
/// When `recordMode` is true, records a snapshot of the view. When `recordMode` is false, performs a comparison
2626
/// with the existing snapshot.
2727
///
28+
/// - Note: This method will modify the view hierarchy in order to snapshot the view. It will attempt to restore the
29+
/// hierarchy to its original state as much as possible, but is not guaranteed to be without side effects (for
30+
/// example if something observes changes in the view hierarchy).
31+
///
2832
/// - parameter view: The view that will be snapshotted.
2933
/// - parameter identifier: An optional identifier included in the snapshot name, for use when there are multiple
3034
/// snapshot tests in a given test method. Defaults to no identifier.
@@ -34,8 +38,10 @@ extension FBSnapshotTestCase {
3438
/// - parameter useMonochromeSnapshot: Whether or not the snapshot of the `view` should be monochrome. Using a
3539
/// monochrome snapshot makes it more clear where the highlighted elements are, but may make it difficult to
3640
/// read certain views. Defaults to `true`.
37-
/// - parameter suffixes: NSOrderedSet object containing strings that are appended to the reference images directory.
38-
/// Defaults to `FBSnapshotTestCaseDefaultSuffixes()`.
41+
/// - parameter markerColors: An array of colors to use for the highlighted regions. These colors will be used in
42+
/// order, repeating through the array as necessary.
43+
/// - parameter suffixes: NSOrderedSet object containing strings that are appended to the reference images
44+
/// directory. Defaults to `FBSnapshotTestCaseDefaultSuffixes()`.
3945
/// - parameter file: The file in which the test result should be attributed.
4046
/// - parameter line: The line in which the test result should be attributed.
4147
public func SnapshotVerifyAccessibility(

0 commit comments

Comments
 (0)