Skip to content

Commit b69e7da

Browse files
caldaiago849
authored andcommitted
Fix issue where Repeater would be ignored if not at top level (airbnb#2221)
1 parent 50cd306 commit b69e7da

15 files changed

+110
-32
lines changed

Sources/Private/CoreAnimation/Layers/ShapeLayer.swift

Lines changed: 98 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,8 @@ final class ShapeLayer: BaseCompositionLayer {
3636
private let shapeLayer: ShapeLayerModel
3737

3838
private func setUpGroups(context: LayerContext) throws {
39-
// If the layer has a `Repeater`, the `Group`s are duplicated and offset
40-
// based on the copy count of the repeater.
41-
if let repeater = shapeLayer.items.first(where: { $0 is Repeater }) as? Repeater {
42-
try setUpRepeater(repeater, context: context)
43-
} else {
44-
let shapeItems = shapeLayer.items.map { ShapeItemLayer.Item(item: $0, groupPath: []) }
45-
try setupGroups(from: shapeItems, parentGroup: nil, parentGroupPath: [], context: context)
46-
}
47-
}
48-
49-
private func setUpRepeater(_ repeater: Repeater, context: LayerContext) throws {
50-
let items = shapeLayer.items.filter { !($0 is Repeater) }
51-
let copyCount = Int(try repeater.copies.exactlyOneKeyframe(context: context, description: "repeater copies").value)
52-
53-
for index in 0..<copyCount {
54-
let shapeItems = items.map { ShapeItemLayer.Item(item: $0, groupPath: []) }
55-
for groupLayer in try makeGroupLayers(from: shapeItems, parentGroup: nil, parentGroupPath: [], context: context) {
56-
let repeatedLayer = RepeaterLayer(repeater: repeater, childLayer: groupLayer, index: index)
57-
addSublayer(repeatedLayer)
58-
}
59-
}
39+
let shapeItems = shapeLayer.items.map { ShapeItemLayer.Item(item: $0, groupPath: []) }
40+
try setupGroups(from: shapeItems, parentGroup: nil, parentGroupPath: [], context: context)
6041
}
6142

6243
}
@@ -181,6 +162,9 @@ final class GroupLayer: BaseAnimationLayer {
181162
}
182163

183164
extension CALayer {
165+
166+
// MARK: Fileprivate
167+
184168
/// Sets up `GroupLayer`s for each `Group` in the given list of `ShapeItem`s
185169
/// - Each `Group` item becomes its own `GroupLayer` sublayer.
186170
/// - Other `ShapeItem` are applied to all sublayers
@@ -191,21 +175,79 @@ extension CALayer {
191175
context: LayerContext)
192176
throws
193177
{
194-
let groupLayers = try makeGroupLayers(
195-
from: items,
196-
parentGroup: parentGroup,
197-
parentGroupPath: parentGroupPath,
198-
context: context)
199-
200-
for groupLayer in groupLayers {
201-
addSublayer(groupLayer)
178+
// If the layer has any `Repeater`s, set up each repeater
179+
// and then handle any remaining groups like normal.
180+
if items.contains(where: { $0.item is Repeater }) {
181+
let repeaterGroupings = items.split(whereSeparator: { $0.item is Repeater })
182+
183+
// Iterate through the groupings backwards to preserve the expected rendering order
184+
for repeaterGrouping in repeaterGroupings.reversed() {
185+
// Each repeater applies to the previous items in the list
186+
if let repeater = repeaterGrouping.trailingSeparator?.item as? Repeater {
187+
try setUpRepeater(
188+
repeater,
189+
items: repeaterGrouping.grouping,
190+
parentGroup: parentGroup,
191+
parentGroupPath: parentGroupPath,
192+
context: context)
193+
}
194+
195+
// Any remaining items after the last repeater are handled like normal
196+
else {
197+
try setupGroups(
198+
from: repeaterGrouping.grouping,
199+
parentGroup: parentGroup,
200+
parentGroupPath: parentGroupPath,
201+
context: context)
202+
}
203+
}
204+
}
205+
206+
else {
207+
let groupLayers = try makeGroupLayers(
208+
from: items,
209+
parentGroup: parentGroup,
210+
parentGroupPath: parentGroupPath,
211+
context: context)
212+
213+
for groupLayer in groupLayers {
214+
addSublayer(groupLayer)
215+
}
216+
}
217+
}
218+
219+
// MARK: Private
220+
221+
/// Sets up this layer using the given `Repeater`
222+
private func setUpRepeater(
223+
_ repeater: Repeater,
224+
items allItems: [ShapeItemLayer.Item],
225+
parentGroup: Group?,
226+
parentGroupPath: [String],
227+
context: LayerContext)
228+
throws
229+
{
230+
let items = allItems.filter { !($0.item is Repeater) }
231+
let copyCount = Int(try repeater.copies.exactlyOneKeyframe(context: context, description: "repeater copies").value)
232+
233+
for index in 0..<copyCount {
234+
let groupLayers = try makeGroupLayers(
235+
from: items,
236+
parentGroup: parentGroup,
237+
parentGroupPath: parentGroupPath,
238+
context: context)
239+
240+
for groupLayer in groupLayers {
241+
let repeatedLayer = RepeaterLayer(repeater: repeater, childLayer: groupLayer, index: index)
242+
addSublayer(repeatedLayer)
243+
}
202244
}
203245
}
204246

205247
/// Creates a `GroupLayer` for each `Group` in the given list of `ShapeItem`s
206248
/// - Each `Group` item becomes its own `GroupLayer` sublayer.
207249
/// - Other `ShapeItem` are applied to all sublayers
208-
fileprivate func makeGroupLayers(
250+
private func makeGroupLayers(
209251
from items: [ShapeItemLayer.Item],
210252
parentGroup: Group?,
211253
parentGroupPath: [String],
@@ -346,6 +388,32 @@ extension Collection {
346388

347389
return (trueElements, falseElements)
348390
}
391+
392+
/// Splits this collection into an array of grouping separated by the given separator.
393+
/// For example, `[A, B, C]` split by `B` returns an array with two elements:
394+
/// 1. `(grouping: [A], trailingSeparator: B)`
395+
/// 2. `(grouping: [C], trailingSeparator: nil)`
396+
func split(whereSeparator separatorPredicate: (Element) -> Bool)
397+
-> [(grouping: [Element], trailingSeparator: Element?)]
398+
{
399+
guard !isEmpty else { return [] }
400+
401+
var groupings: [(grouping: [Element], trailingSeparator: Element?)] = []
402+
403+
for element in self {
404+
if groupings.isEmpty || groupings.last?.trailingSeparator != nil {
405+
groupings.append((grouping: [], trailingSeparator: nil))
406+
}
407+
408+
if separatorPredicate(element) {
409+
groupings[groupings.indices.last!].trailingSeparator = element
410+
} else {
411+
groupings[groupings.indices.last!].grouping.append(element)
412+
}
413+
}
414+
415+
return groupings
416+
}
349417
}
350418

351419
// MARK: - ShapeRenderGroup

Tests/Samples/Issues/issue_2220.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

Tests/SnapshotConfiguration.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,20 @@ extension SnapshotConfiguration {
5353
"Nonanimating/FirstText": .precision(0.99),
5454
"Nonanimating/verifyLineHeight": .precision(0.99),
5555
"Nonanimating/blend_mode_test": .precision(0.99),
56+
"Nonanimating/base64Test": .precision(0.9),
5657
"Issues/issue_2066": .precision(0.9),
5758
"LottieFiles/dog_car_ride": .precision(0.95),
5859
"Issues/issue_1800": .precision(0.95),
5960
"Issues/issue_1882": .precision(0.95),
6061
"Issues/issue_1717": .precision(0.95),
62+
"Issues/issue_1887": .precision(0.95),
63+
"Issues/issue_1683": .precision(0.93),
64+
"Issues/pr_1763": .precision(0.95),
6165
"Issues/pr_1964": .precision(0.95),
66+
"Issues/pr_1930_rx": .precision(0.93),
67+
"Issues/pr_1930_ry": .precision(0.93),
68+
"Issues/pr_1930_all_axis": .precision(0.93),
69+
"Issues/issue_1169_four_shadows": .precision(0.93),
6270
"DotLottie/animation_external_image": .precision(0.95),
6371
"DotLottie/animation_inline_image": .precision(0.95),
6472
"LottieFiles/gradient_shapes": .precision(0.95),
@@ -95,7 +103,7 @@ extension SnapshotConfiguration {
95103

96104
"Issues/issue_1664": .customValueProviders([
97105
AnimationKeypath(keypath: "**.base_color.**.Color"): ColorValueProvider(.black),
98-
]),
106+
]).precision(0.95),
99107

100108
"Issues/issue_1854": .customValueProviders([
101109
AnimationKeypath(keypath: "**.Colors"): GradientValueProvider(

Tests/SnapshotTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ class SnapshotTests: XCTestCase {
143143
matching: animationView,
144144
as: .imageOfPresentationLayer(
145145
precision: SnapshotConfiguration.forSample(named: sampleAnimationName).precision,
146-
perceptualPrecision: 0.98),
146+
perceptualPrecision: 0.97),
147147
named: "\(sampleAnimationName) (\(Int(percent * 100))%)",
148148
testName: testName)
149149
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Supports Core Animation engine
59.9 KB
Loading
59.9 KB
Loading
78.1 KB
Loading
77.5 KB
Loading
62 KB
Loading

0 commit comments

Comments
 (0)