From e9b94a860bfa97064fa147f3368a541f3c852259 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 6 Nov 2025 04:29:49 +0100 Subject: [PATCH 01/10] Fixed reordering, insertion and removal in ForEach where Element: Identifiable No non-identifiable support in this commit --- Examples/Bundler.toml | 5 + Examples/Package.resolved | 6 +- Examples/Package.swift | 4 + .../Sources/ForEachExample/ForEachApp.swift | 102 +++++++++++++ Package.resolved | 11 +- Package.swift | 5 + Sources/SwiftCrossUI/Views/ForEach.swift | 142 ++++++++++++------ 7 files changed, 222 insertions(+), 53 deletions(-) create mode 100644 Examples/Sources/ForEachExample/ForEachApp.swift diff --git a/Examples/Bundler.toml b/Examples/Bundler.toml index 3b96dc6128..1d9f7d44a5 100644 --- a/Examples/Bundler.toml +++ b/Examples/Bundler.toml @@ -64,3 +64,8 @@ version = '0.1.0' identifier = 'dev.swiftcrossui.HoverExample' product = 'HoverExample' version = '0.1.0' + +[apps.ForEachExample] +identifier = 'dev.swiftcrossui.ForEachExample' +product = 'ForEachExample' +version = '0.1.0' diff --git a/Examples/Package.resolved b/Examples/Package.resolved index 3842945e2a..f7227b2a94 100644 --- a/Examples/Package.resolved +++ b/Examples/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "6977ba851e440a7fbdfc7cb46441e32853dc2ba48ba34fe702e6784699d08682", + "originHash" : "1eacde3553facaf2ac4f087f074b79e01abf68b6b810767996f192601e269d4d", "pins" : [ { "identity" : "aexml", @@ -167,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", - "version" : "1.1.3" + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" } }, { diff --git a/Examples/Package.swift b/Examples/Package.swift index 1735fc7675..b7f7ddd506 100644 --- a/Examples/Package.swift +++ b/Examples/Package.swift @@ -75,6 +75,10 @@ let package = Package( ), .executableTarget( name: "HoverExample", + dependencies: exampleDependencies + ), + .executableTarget( + name: "ForEachExample", dependencies: exampleDependencies ) ] diff --git a/Examples/Sources/ForEachExample/ForEachApp.swift b/Examples/Sources/ForEachExample/ForEachApp.swift new file mode 100644 index 0000000000..4c5e033a93 --- /dev/null +++ b/Examples/Sources/ForEachExample/ForEachApp.swift @@ -0,0 +1,102 @@ +import DefaultBackend +import Foundation +import SwiftCrossUI + +#if canImport(SwiftBundlerRuntime) + import SwiftBundlerRuntime +#endif + +@main +@HotReloadable +struct ForEachApp: App { + @State var items = { + var items = [Item]() + for i in 0..<20 { + items.append(.init("\(i)")) + } + return items + }() + @State var biggestValue = 19 + @State var insertionPosition = 10 + + var body: some Scene { + WindowGroup("ForEach") { + #hotReloadable { + ScrollView { + VStack { + Text("Items") + + Button("Append") { + biggestValue += 1 + items.append(.init("\(biggestValue)")) + } + + Button("Insert in front of current item at position \(insertionPosition)") { + biggestValue += 1 + items.insert(.init("\(biggestValue)"), at: insertionPosition) + } + Slider($insertionPosition, minimum: 0, maximum: items.count - 1) + .onChange(of: items.count) { + guard insertionPosition > items.count - 1 else { + return + } + insertionPosition = max(items.count - 1, 0) + } + + ForEach(items) { item in + ItemRow( + item: item, isFirst: Optional(item.id) == items.first?.id, + isLast: Optional(item.id) == items.last?.id + ) { + items.removeAll(where: { $0.id == item.id }) + } moveUp: { + guard + let ownIndex = items.firstIndex(where: { $0.id == item.id }), + ownIndex != items.startIndex + else { return } + items.swapAt(ownIndex, ownIndex - 1) + } moveDown: { + guard + let ownIndex = items.firstIndex(where: { $0.id == item.id }), + ownIndex != items.endIndex + else { return } + items.swapAt(ownIndex, ownIndex + 1) + } + } + } + .padding(10) + } + } + } + .defaultSize(width: 400, height: 800) + } +} + +struct ItemRow: View { + @State var item: Item + let isFirst: Bool + let isLast: Bool + var remove: () -> Void + var moveUp: () -> Void + var moveDown: () -> Void + + var body: some View { + HStack { + Text(item.value) + Button("Delete") { remove() } + Button("⌃") { moveUp() } + .disabled(isFirst) + Button("⌄") { moveDown() } + .disabled(isLast) + } + } +} + +class Item: Identifiable, SwiftCrossUI.ObservableObject { + let id = UUID() + @SwiftCrossUI.Published var value: String + + init(_ value: String) { + self.value = value + } +} diff --git a/Package.resolved b/Package.resolved index 173c2d3001..de00b7becd 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3b87bbc3d0f0110380f592dc86a1c8c65c20f5a326f484bdbe2f6ef5e357840d", + "originHash" : "61c48c4d9a3bafacef3a6e1e2f03481a75a2966aaa511ff212e5fca86ce667c2", "pins" : [ { "identity" : "jpeg", @@ -28,6 +28,15 @@ "version" : "1.4.1" } }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + }, { "identity" : "swift-cwinrt", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index d854305ca1..2957a41c04 100644 --- a/Package.swift +++ b/Package.swift @@ -110,6 +110,10 @@ let package = Package( url: "https://github.com/stackotter/swift-winui", revision: "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86" ), + .package( + url: "https://github.com/apple/swift-collections.git", + .upToNextMajor(from: "1.3.0") + ), // .package( // url: "https://github.com/stackotter/TermKit", // revision: "163afa64f1257a0c026cc83ed8bc47a5f8fc9704" @@ -129,6 +133,7 @@ let package = Package( dependencies: [ "HotReloadingMacrosPlugin", .product(name: "ImageFormats", package: "swift-image-formats"), + .product(name: "OrderedCollections", package: "swift-collections") ], exclude: [ "Builders/ViewBuilder.swift.gyb", diff --git a/Sources/SwiftCrossUI/Views/ForEach.swift b/Sources/SwiftCrossUI/Views/ForEach.swift index 81ad975c1a..4fa7fabf47 100644 --- a/Sources/SwiftCrossUI/Views/ForEach.swift +++ b/Sources/SwiftCrossUI/Views/ForEach.swift @@ -1,3 +1,5 @@ +import OrderedCollections + /// A view that displays a variable amount of children. public struct ForEach where Items.Index == Int { /// A variable-length collection of elements to display. @@ -40,7 +42,7 @@ extension ForEach where Items == [Int] { } } -extension ForEach: TypeSafeView, View where Child: View { +extension ForEach: TypeSafeView, View where Child: View, Items.Element: Identifiable { typealias Children = ForEachViewChildren public var body: EmptyView { @@ -112,20 +114,69 @@ extension ForEach: TypeSafeView, View where Child: View { children.queuedChanges = [] } - // TODO: The way we're reusing nodes for technically different elements means that if - // Child has state of its own then it could get pretty confused thinking that its state - // changed whereas it was actually just moved to a new slot in the array. Probably not - // a huge issue, but definitely something to keep an eye on. var layoutableChildren: [LayoutSystem.LayoutableChild] = [] - for (i, node) in children.nodes.enumerated() { - guard i < elements.count else { - break + + let oldNodes = children.nodes + let oldMap = children.nodeIdentifierMap + let oldIdentifiers = children.identifiers + let identifiersStart = oldIdentifiers.startIndex + + children.nodes = [] + children.nodeIdentifierMap = [:] + children.identifiers = [] + + // Once this is true, every node that existed in the previous update and + // still exists in the new one is reinserted to ensure that items are + // rendered in the correct order. + var requiresOngoingReinsertion = false + + for (i, element) in elements.enumerated() { + let childContent = child(element) + let node: AnyViewGraphNode + + if let oldNode = oldMap[element.id] { + node = oldNode + + // Checks if there is a preceding item that was not preceding in + // the previous update. If such an item exists, it means that + // the order of the collection has changed or that an item was + // inserted somewhere in the middle, rather than simply appended. + requiresOngoingReinsertion = + requiresOngoingReinsertion + || { + guard + let ownOldIndex = oldIdentifiers.firstIndex(of: element.id) + else { return false } + + let subset = oldIdentifiers[identifiersStart.. 0 { - let startIndex = elements.startIndex.advanced(by: nodeCount) - for i in 0..: ViewGraphNodeChildren where Items.Index == Int { +>: ViewGraphNodeChildren where Items.Index == Int, Items.Element: Identifiable { /// The nodes for all current children of the ``ForEach`` view. var nodes: [AnyViewGraphNode] = [] + + /// The nodes for all current children of the ``ForEach`` view, queriable by their identifier. + var nodeIdentifierMap: [Items.Element.ID: AnyViewGraphNode] + + /// The identifiers of all current children ``ForEach`` view in the order they are displayed. + /// Can be used for checking if an element was moved or an element was inserted in front of it. + var identifiers: OrderedSet + /// Changes queued during `dryRun` updates. var queuedChanges: [Change] = [] @@ -227,10 +259,13 @@ class ForEachViewChildren< snapshots: [ViewGraphSnapshotter.NodeSnapshot]?, environment: EnvironmentValues ) { - nodes = view.elements - .map(view.child) - .enumerated() - .map { (index, child) in + var nodeIdentifierMap = [Items.Element.ID: AnyViewGraphNode]() + var identifiers = OrderedSet() + var viewNodes = [AnyViewGraphNode]() + + for (index, element) in view.elements.enumerated() { + let child = view.child(element) + let viewGraphNode = { let snapshot = index < snapshots?.count ?? 0 ? snapshots?[index] : nil return ViewGraphNode( for: child, @@ -238,7 +273,16 @@ class ForEachViewChildren< snapshot: snapshot, environment: environment ) - } - .map(AnyViewGraphNode.init(_:)) + }() + + let anyViewGraphNode = AnyViewGraphNode(viewGraphNode) + viewNodes.append(anyViewGraphNode) + + identifiers.append(element.id) + nodeIdentifierMap[element.id] = anyViewGraphNode + } + nodes = viewNodes + self.identifiers = identifiers + self.nodeIdentifierMap = nodeIdentifierMap } } From 116b72c0aaecbd9972b149d1a55eab9d783e587d Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 6 Nov 2025 07:21:21 +0100 Subject: [PATCH 02/10] Made it work for non identifiables StressTestExample is broken --- .../Builders/MenuItemsBuilder.swift | 4 +- Sources/SwiftCrossUI/Views/ForEach.swift | 191 ++++++++++++------ 2 files changed, 134 insertions(+), 61 deletions(-) diff --git a/Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift b/Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift index f549a94b74..ef4e786141 100644 --- a/Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift +++ b/Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift @@ -18,7 +18,7 @@ public struct MenuItemsBuilder { } public static func buildPartialBlock( - first: ForEach + first: ForEach ) -> [MenuItem] { first.elements.map(first.child).flatMap { $0 } } @@ -53,7 +53,7 @@ public struct MenuItemsBuilder { public static func buildPartialBlock( accumulated: [MenuItem], - next: ForEach + next: ForEach ) -> [MenuItem] { accumulated + buildPartialBlock(first: next) } diff --git a/Sources/SwiftCrossUI/Views/ForEach.swift b/Sources/SwiftCrossUI/Views/ForEach.swift index 4fa7fabf47..f98e0d9389 100644 --- a/Sources/SwiftCrossUI/Views/ForEach.swift +++ b/Sources/SwiftCrossUI/Views/ForEach.swift @@ -1,78 +1,47 @@ import OrderedCollections /// A view that displays a variable amount of children. -public struct ForEach where Items.Index == Int { +public struct ForEach where Items.Index == Int { /// A variable-length collection of elements to display. var elements: Items /// A method to display the elements as views. var child: (Items.Element) -> Child + /// The path to the property used as Identifier + var idKeyPath: KeyPath } -extension ForEach where Child == [MenuItem] { - /// Creates a view that creates child views on demand based on a collection of data. - @_disfavoredOverload +extension ForEach: TypeSafeView, View where Child: View { + typealias Children = ForEachViewChildren public init( _ elements: Items, - @MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem] + id keyPath: KeyPath, + @ViewBuilder _ child: @escaping (Items.Element) -> Child ) { self.elements = elements self.child = child - } -} - -extension ForEach where Items == [Int] { - /// Creates a view that creates child views on demand based on a given ClosedRange - @_disfavoredOverload - public init( - _ range: ClosedRange, - child: @escaping (Int) -> Child - ) { - self.elements = Array(range) - self.child = child + self.idKeyPath = keyPath } - /// Creates a view that creates child views on demand based on a given Range - @_disfavoredOverload - public init( - _ range: Range, - child: @escaping (Int) -> Child - ) { - self.elements = Array(range) - self.child = child - } -} - -extension ForEach: TypeSafeView, View where Child: View, Items.Element: Identifiable { - typealias Children = ForEachViewChildren - public var body: EmptyView { return EmptyView() } - /// Creates a view that creates child views on demand based on a collection of data. - public init( - _ elements: Items, - @ViewBuilder _ child: @escaping (Items.Element) -> Child - ) { - self.elements = elements - self.child = child - } - func children( backend: Backend, snapshots: [ViewGraphSnapshotter.NodeSnapshot]?, environment: EnvironmentValues - ) -> ForEachViewChildren { - return ForEachViewChildren( + ) -> Children { + return Children( from: self, backend: backend, + idKeyPath: idKeyPath, snapshots: snapshots, environment: environment ) } func asWidget( - _ children: ForEachViewChildren, + _ children: Children, backend: Backend ) -> Backend.Widget { return backend.createContainer() @@ -80,7 +49,7 @@ extension ForEach: TypeSafeView, View where Child: View, Items.Element: Identifi func update( _ widget: Backend.Widget, - children: ForEachViewChildren, + children: Children, proposedSize: SIMD2, environment: EnvironmentValues, backend: Backend, @@ -116,7 +85,6 @@ extension ForEach: TypeSafeView, View where Child: View, Items.Element: Identifi var layoutableChildren: [LayoutSystem.LayoutableChild] = [] - let oldNodes = children.nodes let oldMap = children.nodeIdentifierMap let oldIdentifiers = children.identifiers let identifiersStart = oldIdentifiers.startIndex @@ -130,11 +98,11 @@ extension ForEach: TypeSafeView, View where Child: View, Items.Element: Identifi // rendered in the correct order. var requiresOngoingReinsertion = false - for (i, element) in elements.enumerated() { + for element in elements { let childContent = child(element) let node: AnyViewGraphNode - if let oldNode = oldMap[element.id] { + if let oldNode = oldMap[element[keyPath: idKeyPath]] { node = oldNode // Checks if there is a preceding item that was not preceding in @@ -145,7 +113,8 @@ extension ForEach: TypeSafeView, View where Child: View, Items.Element: Identifi requiresOngoingReinsertion || { guard - let ownOldIndex = oldIdentifiers.firstIndex(of: element.id) + let ownOldIndex = oldIdentifiers.firstIndex( + of: element[keyPath: idKeyPath]) else { return false } let subset = oldIdentifiers[identifiersStart..: ViewGraphNodeChildren where Items.Index == Int, Items.Element: Identifiable { +>: ViewGraphNodeChildren { /// The nodes for all current children of the ``ForEach`` view. var nodes: [AnyViewGraphNode] = [] /// The nodes for all current children of the ``ForEach`` view, queriable by their identifier. - var nodeIdentifierMap: [Items.Element.ID: AnyViewGraphNode] + var nodeIdentifierMap: [ID: AnyViewGraphNode] /// The identifiers of all current children ``ForEach`` view in the order they are displayed. /// Can be used for checking if an element was moved or an element was inserted in front of it. - var identifiers: OrderedSet + var identifiers: OrderedSet /// Changes queued during `dryRun` updates. var queuedChanges: [Change] = [] @@ -254,13 +224,14 @@ class ForEachViewChildren< /// Gets a variable length view's children as view graph node children. init( - from view: ForEach, + from view: ForEach, backend: Backend, + idKeyPath: KeyPath, snapshots: [ViewGraphSnapshotter.NodeSnapshot]?, environment: EnvironmentValues ) { - var nodeIdentifierMap = [Items.Element.ID: AnyViewGraphNode]() - var identifiers = OrderedSet() + var nodeIdentifierMap = [ID: AnyViewGraphNode]() + var identifiers = OrderedSet() var viewNodes = [AnyViewGraphNode]() for (index, element) in view.elements.enumerated() { @@ -278,11 +249,113 @@ class ForEachViewChildren< let anyViewGraphNode = AnyViewGraphNode(viewGraphNode) viewNodes.append(anyViewGraphNode) - identifiers.append(element.id) - nodeIdentifierMap[element.id] = anyViewGraphNode + identifiers.append(element[keyPath: idKeyPath]) + nodeIdentifierMap[element[keyPath: idKeyPath]] = anyViewGraphNode } nodes = viewNodes self.identifiers = identifiers self.nodeIdentifierMap = nodeIdentifierMap } } + +// MARK: - Alternative Initializers +extension ForEach where Items.Element: Hashable, ID == Items.Element { + @_disfavoredOverload + public init( + items elements: Items, + _ child: @escaping (Items.Element) -> Child + ) { + self.elements = elements + self.child = child + self.idKeyPath = \.self + } +} + +extension ForEach where Child == [MenuItem], Items.Element: Hashable, ID == Items.Element { + /// Creates a view that creates child views on demand based on a collection of data. + @_disfavoredOverload + public init( + items elements: Items, + @MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem] + ) { + self.elements = elements + self.child = child + self.idKeyPath = \.self + } +} + +extension ForEach where Child == [MenuItem] { + /// Creates a view that creates child views on demand based on a collection of data. + @_disfavoredOverload + public init( + _ elements: Items, + id keyPath: KeyPath, + @MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem] + ) { + self.elements = elements + self.child = child + self.idKeyPath = keyPath + } +} + +extension ForEach where Items == [Int], ID == Items.Element { + /// Creates a view that creates child views on demand based on a given ClosedRange + @_disfavoredOverload + public init( + _ range: ClosedRange, + child: @escaping (Int) -> Child + ) { + self.elements = Array(range) + self.child = child + self.idKeyPath = \.self + } + + /// Creates a view that creates child views on demand based on a given Range + @_disfavoredOverload + public init( + _ range: Range, + child: @escaping (Int) -> Child + ) { + self.elements = Array(range) + self.child = child + self.idKeyPath = \.self + } +} + +extension ForEach where Items == [Int] { + /// Creates a view that creates child views on demand based on a given ClosedRange + @_disfavoredOverload + public init( + _ range: ClosedRange, + id keyPath: KeyPath, + child: @escaping (Int) -> Child + ) { + self.elements = Array(range) + self.child = child + self.idKeyPath = keyPath + } + + /// Creates a view that creates child views on demand based on a given Range + @_disfavoredOverload + public init( + _ range: Range, + id keyPath: KeyPath, + child: @escaping (Int) -> Child + ) { + self.elements = Array(range) + self.child = child + self.idKeyPath = keyPath + } +} + +extension ForEach where Items.Element: Identifiable, ID == Items.Element.ID { + /// Creates a view that creates child views on demand based on a collection of identifiable data. + public init( + _ elements: Items, + child: @escaping (Items.Element) -> Child + ) { + self.elements = elements + self.child = child + self.idKeyPath = \.id + } +} From 466c259e30b341f4ac5083743d2000e150a3f9a1 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 6 Nov 2025 08:48:27 +0100 Subject: [PATCH 03/10] added fallback to old code where no keypath is specified --- .../StressTestExample/StressTestApp.swift | 3 +- Sources/SwiftCrossUI/Views/ForEach.swift | 150 +++++++++++++++++- 2 files changed, 145 insertions(+), 8 deletions(-) diff --git a/Examples/Sources/StressTestExample/StressTestApp.swift b/Examples/Sources/StressTestExample/StressTestApp.swift index 974c2a5bdd..e76d20e2d4 100644 --- a/Examples/Sources/StressTestExample/StressTestApp.swift +++ b/Examples/Sources/StressTestExample/StressTestApp.swift @@ -43,12 +43,11 @@ struct StressTestApp: App { for _ in 0..<1000 { values.append(Self.options.randomElement()!) } - self.values[tab!] = values } if let values = values[tab!] { ScrollView { - ForEach(values) { value in + ForEach(items: values) { value in Text(value) } }.frame(minWidth: 300) diff --git a/Sources/SwiftCrossUI/Views/ForEach.swift b/Sources/SwiftCrossUI/Views/ForEach.swift index f98e0d9389..f706e24004 100644 --- a/Sources/SwiftCrossUI/Views/ForEach.swift +++ b/Sources/SwiftCrossUI/Views/ForEach.swift @@ -1,13 +1,13 @@ import OrderedCollections /// A view that displays a variable amount of children. -public struct ForEach where Items.Index == Int { +public struct ForEach { /// A variable-length collection of elements to display. var elements: Items /// A method to display the elements as views. var child: (Items.Element) -> Child /// The path to the property used as Identifier - var idKeyPath: KeyPath + var idKeyPath: KeyPath? } extension ForEach: TypeSafeView, View where Child: View { @@ -83,6 +83,17 @@ extension ForEach: TypeSafeView, View where Child: View { children.queuedChanges = [] } + guard let idKeyPath else { + return deprecatedUpdate( + widget, + children: children, + proposedSize: proposedSize, + environment: environment, + backend: backend, + dryRun: dryRun + ) + } + var layoutableChildren: [LayoutSystem.LayoutableChild] = [] let oldMap = children.nodeIdentifierMap @@ -177,6 +188,104 @@ extension ForEach: TypeSafeView, View where Child: View { dryRun: dryRun ) } + + @MainActor + func deprecatedUpdate( + _ widget: Backend.Widget, + children: Children, + proposedSize: SIMD2, + environment: EnvironmentValues, + backend: Backend, + dryRun: Bool + ) -> ViewUpdateResult { + func addChild(_ child: Backend.Widget) { + if dryRun { + children.queuedChanges.append(.addChild(AnyWidget(child))) + } else { + backend.addChild(child, to: widget) + } + } + + func removeChild(_ child: Backend.Widget) { + if dryRun { + children.queuedChanges.append(.removeChild(AnyWidget(child))) + } else { + backend.removeChild(child, from: widget) + } + } + + // TODO: The way we're reusing nodes for technically different elements means that if + // Child has state of its own then it could get pretty confused thinking that its state + // changed whereas it was actually just moved to a new slot in the array. Probably not + // a huge issue, but definitely something to keep an eye on. + var layoutableChildren: [LayoutSystem.LayoutableChild] = [] + for (i, node) in children.nodes.enumerated() { + guard i < elements.count else { + break + } + let index = elements.index(elements.startIndex, offsetBy: i) + let childContent = child(elements[index]) + if children.isFirstUpdate { + addChild(node.widget.into()) + } + layoutableChildren.append( + LayoutSystem.LayoutableChild( + update: { proposedSize, environment, dryRun in + node.update( + with: childContent, + proposedSize: proposedSize, + environment: environment, + dryRun: dryRun + ) + } + ) + ) + } + children.isFirstUpdate = false + + let nodeCount = children.nodes.count + let remainingElementCount = elements.count - nodeCount + if remainingElementCount > 0 { + let startIndex = elements.index(elements.startIndex, offsetBy: nodeCount) + for i in 0..( from view: ForEach, backend: Backend, - idKeyPath: KeyPath, + idKeyPath: KeyPath?, snapshots: [ViewGraphSnapshotter.NodeSnapshot]?, environment: EnvironmentValues ) { + guard let idKeyPath else { + nodes = view.elements + .map(view.child) + .enumerated() + .map { (index, child) in + let snapshot = index < snapshots?.count ?? 0 ? snapshots?[index] : nil + return ViewGraphNode( + for: child, + backend: backend, + snapshot: snapshot, + environment: environment + ) + } + .map(AnyViewGraphNode.init(_:)) + identifiers = [] + nodeIdentifierMap = [:] + return + } var nodeIdentifierMap = [ID: AnyViewGraphNode]() var identifiers = OrderedSet() var viewNodes = [AnyViewGraphNode]() @@ -260,6 +387,12 @@ class ForEachViewChildren< // MARK: - Alternative Initializers extension ForEach where Items.Element: Hashable, ID == Items.Element { + /// Creates a view that creates child views on demand based on a collection of data. + @available( + *, + deprecated, + message: "Use ForEach with id argument on non-Identifiable Elements instead." + ) @_disfavoredOverload public init( items elements: Items, @@ -267,20 +400,25 @@ extension ForEach where Items.Element: Hashable, ID == Items.Element { ) { self.elements = elements self.child = child - self.idKeyPath = \.self + self.idKeyPath = nil } } extension ForEach where Child == [MenuItem], Items.Element: Hashable, ID == Items.Element { /// Creates a view that creates child views on demand based on a collection of data. + @available( + *, + deprecated, + message: "Use ForEach with id argument on non-Identifiable Elements instead." + ) @_disfavoredOverload public init( - items elements: Items, + _ elements: Items, @MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem] ) { self.elements = elements self.child = child - self.idKeyPath = \.self + self.idKeyPath = nil } } From 76968ca581d12a5314ea33142abd196eb55a0a07 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 6 Nov 2025 10:17:45 +0100 Subject: [PATCH 04/10] added node reusing bypass after discovering first duplicate --- .../GreetingGeneratorApp.swift | 2 +- .../StressTestExample/StressTestApp.swift | 2 +- Sources/SwiftCrossUI/Views/ForEach.swift | 89 ++++++++++++------- 3 files changed, 57 insertions(+), 36 deletions(-) diff --git a/Examples/Sources/GreetingGeneratorExample/GreetingGeneratorApp.swift b/Examples/Sources/GreetingGeneratorExample/GreetingGeneratorApp.swift index fa4b984eff..87ce8efc7e 100644 --- a/Examples/Sources/GreetingGeneratorExample/GreetingGeneratorApp.swift +++ b/Examples/Sources/GreetingGeneratorExample/GreetingGeneratorApp.swift @@ -38,7 +38,7 @@ struct GreetingGeneratorApp: App { .padding(.top, 20) ScrollView { - ForEach(greetings.reversed()[1...]) { greeting in + ForEach(items: greetings.reversed()[1...]) { greeting in Text(greeting) } } diff --git a/Examples/Sources/StressTestExample/StressTestApp.swift b/Examples/Sources/StressTestExample/StressTestApp.swift index e76d20e2d4..7eab0de879 100644 --- a/Examples/Sources/StressTestExample/StressTestApp.swift +++ b/Examples/Sources/StressTestExample/StressTestApp.swift @@ -47,7 +47,7 @@ struct StressTestApp: App { } if let values = values[tab!] { ScrollView { - ForEach(items: values) { value in + ForEach(values, id: \.self) { value in Text(value) } }.frame(minWidth: 300) diff --git a/Sources/SwiftCrossUI/Views/ForEach.swift b/Sources/SwiftCrossUI/Views/ForEach.swift index f706e24004..a239841adc 100644 --- a/Sources/SwiftCrossUI/Views/ForEach.swift +++ b/Sources/SwiftCrossUI/Views/ForEach.swift @@ -96,6 +96,7 @@ extension ForEach: TypeSafeView, View where Child: View { var layoutableChildren: [LayoutSystem.LayoutableChild] = [] + let oldNodes = children.nodes let oldMap = children.nodeIdentifierMap let oldIdentifiers = children.identifiers let identifiersStart = oldIdentifiers.startIndex @@ -108,52 +109,63 @@ extension ForEach: TypeSafeView, View where Child: View { // still exists in the new one is reinserted to ensure that items are // rendered in the correct order. var requiresOngoingReinsertion = false + var ongoingNodeReusingDisabled = false + var inserted = false for element in elements { let childContent = child(element) let node: AnyViewGraphNode - if let oldNode = oldMap[element[keyPath: idKeyPath]] { - node = oldNode - - // Checks if there is a preceding item that was not preceding in - // the previous update. If such an item exists, it means that - // the order of the collection has changed or that an item was - // inserted somewhere in the middle, rather than simply appended. - requiresOngoingReinsertion = - requiresOngoingReinsertion - || { - guard - let ownOldIndex = oldIdentifiers.firstIndex( - of: element[keyPath: idKeyPath]) - else { return false } - - let subset = oldIdentifiers[identifiersStart.. Date: Thu, 6 Nov 2025 17:10:33 +0100 Subject: [PATCH 05/10] Fixed ForEach [MenuItem] compatibility, documentation improvement & cleanup sadly an argument name seems to be required on menuitem forEach initializer, the compiler is apparently unable to infer the right child from the context. --- .../Builders/MenuItemsBuilder.swift | 8 ++-- Sources/SwiftCrossUI/Views/Button.swift | 3 +- Sources/SwiftCrossUI/Views/ForEach.swift | 46 ++++++++++++------- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift b/Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift index ef4e786141..349cfc3134 100644 --- a/Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift +++ b/Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift @@ -17,8 +17,8 @@ public struct MenuItemsBuilder { first.items } - public static func buildPartialBlock( - first: ForEach + public static func buildPartialBlock( + first: ForEach ) -> [MenuItem] { first.elements.map(first.child).flatMap { $0 } } @@ -51,9 +51,9 @@ public struct MenuItemsBuilder { accumulated + buildPartialBlock(first: next) } - public static func buildPartialBlock( + public static func buildPartialBlock( accumulated: [MenuItem], - next: ForEach + next: ForEach ) -> [MenuItem] { accumulated + buildPartialBlock(first: next) } diff --git a/Sources/SwiftCrossUI/Views/Button.swift b/Sources/SwiftCrossUI/Views/Button.swift index 739d1ba407..343a23f88b 100644 --- a/Sources/SwiftCrossUI/Views/Button.swift +++ b/Sources/SwiftCrossUI/Views/Button.swift @@ -21,8 +21,7 @@ public struct Button: Sendable { } } -extension Button: View { -} +extension Button: View {} extension Button: ElementaryView { public func asWidget(backend: Backend) -> Backend.Widget { diff --git a/Sources/SwiftCrossUI/Views/ForEach.swift b/Sources/SwiftCrossUI/Views/ForEach.swift index a239841adc..3f2a9264e3 100644 --- a/Sources/SwiftCrossUI/Views/ForEach.swift +++ b/Sources/SwiftCrossUI/Views/ForEach.swift @@ -83,6 +83,8 @@ extension ForEach: TypeSafeView, View where Child: View { children.queuedChanges = [] } + // Use the previous update Method when no keyPath is set on a + // [Hashable] Collection to optionally keep the old behaviour. guard let idKeyPath else { return deprecatedUpdate( widget, @@ -109,13 +111,22 @@ extension ForEach: TypeSafeView, View where Child: View { // still exists in the new one is reinserted to ensure that items are // rendered in the correct order. var requiresOngoingReinsertion = false + + // Forces node recreation when enabled (expensive on large Collections). + // Use only when idKeyPath yields non-unique values. Prefer Identifiable + // or guaranteed unique, constant identifiers for optimal performance. + // Node caching and diffing require unique, stable IDs. var ongoingNodeReusingDisabled = false + + // Avoid reallocation var inserted = false for element in elements { let childContent = child(element) let node: AnyViewGraphNode + // Track duplicates: inserted=false if ID exists. + // Disables node reuse if any duplicate gets found. (inserted, _) = children.identifiers.append(element[keyPath: idKeyPath]) ongoingNodeReusingDisabled = ongoingNodeReusingDisabled || !inserted @@ -123,10 +134,9 @@ extension ForEach: TypeSafeView, View where Child: View { if let oldNode = oldMap[element[keyPath: idKeyPath]] { node = oldNode - // Checks if there is a preceding item that was not preceding in - // the previous update. If such an item exists, it means that - // the order of the collection has changed or that an item was - // inserted somewhere in the middle, rather than simply appended. + // Detects reordering or mid-collection insertion: + // Checks if there is a preceding item that was not + // preceding in the previous update. requiresOngoingReinsertion = requiresOngoingReinsertion || { @@ -139,20 +149,24 @@ extension ForEach: TypeSafeView, View where Child: View { return !children.identifiers.subtracting(subset).isEmpty }() - if requiresOngoingReinsertion { + // Removes node from its previous position and + // re-adds it at the new correct one. + if requiresOngoingReinsertion, !children.isFirstUpdate { removeChild(oldNode.widget.into()) addChild(oldNode.widget.into()) } } else { // New Items need ongoing reinsertion to get - // displayed at the correct locat ion. + // displayed at the correct location. requiresOngoingReinsertion = true node = AnyViewGraphNode( for: childContent, backend: backend, environment: environment ) - addChild(node.widget.into()) + if !children.isFirstUpdate { + addChild(node.widget.into()) + } } children.nodeIdentifierMap[element[keyPath: idKeyPath]] = node } else { @@ -165,10 +179,6 @@ extension ForEach: TypeSafeView, View where Child: View { children.nodes.append(node) - if children.isFirstUpdate, !ongoingNodeReusingDisabled { - addChild(node.widget.into()) - } - layoutableChildren.append( LayoutSystem.LayoutableChild( update: { proposedSize, environment, dryRun in @@ -183,9 +193,11 @@ extension ForEach: TypeSafeView, View where Child: View { ) } - children.isFirstUpdate = false - - if !ongoingNodeReusingDisabled { + if children.isFirstUpdate { + for nodeToAdd in children.nodes { + addChild(nodeToAdd.widget.into()) + } + } else if !ongoingNodeReusingDisabled { for removed in oldMap.filter({ !children.identifiers.contains($0.key) }).values { @@ -200,6 +212,8 @@ extension ForEach: TypeSafeView, View where Child: View { } } + children.isFirstUpdate = false + return LayoutSystem.updateStackLayout( container: widget, children: layoutableChildren, @@ -434,7 +448,7 @@ extension ForEach where Child == [MenuItem], Items.Element: Hashable, ID == Item ) @_disfavoredOverload public init( - _ elements: Items, + menuItems elements: Items, @MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem] ) { self.elements = elements @@ -447,7 +461,7 @@ extension ForEach where Child == [MenuItem] { /// Creates a view that creates child views on demand based on a collection of data. @_disfavoredOverload public init( - _ elements: Items, + menuItems elements: Items, id keyPath: KeyPath, @MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem] ) { From 25c6e66beb5130fedcaa2f59e67ffe346b9fe637 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 6 Nov 2025 17:33:21 +0100 Subject: [PATCH 06/10] added tvOS excluding compiler flag for Slider Component in ForEachExample --- .../Sources/ForEachExample/ForEachApp.swift | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Examples/Sources/ForEachExample/ForEachApp.swift b/Examples/Sources/ForEachExample/ForEachApp.swift index 4c5e033a93..f39c3a0a97 100644 --- a/Examples/Sources/ForEachExample/ForEachApp.swift +++ b/Examples/Sources/ForEachExample/ForEachApp.swift @@ -24,25 +24,28 @@ struct ForEachApp: App { #hotReloadable { ScrollView { VStack { - Text("Items") - Button("Append") { biggestValue += 1 items.append(.init("\(biggestValue)")) } - Button("Insert in front of current item at position \(insertionPosition)") { - biggestValue += 1 - items.insert(.init("\(biggestValue)"), at: insertionPosition) - } - Slider($insertionPosition, minimum: 0, maximum: items.count - 1) - .onChange(of: items.count) { - guard insertionPosition > items.count - 1 else { - return - } - insertionPosition = max(items.count - 1, 0) + #if !os(tvOS) + Button( + "Insert in front of current item at position \(insertionPosition)" + ) { + biggestValue += 1 + items.insert(.init("\(biggestValue)"), at: insertionPosition) } + Slider($insertionPosition, minimum: 0, maximum: items.count - 1) + .onChange(of: items.count) { + guard insertionPosition > items.count - 1 else { + return + } + insertionPosition = max(items.count - 1, 0) + } + #endif + ForEach(items) { item in ItemRow( item: item, isFirst: Optional(item.id) == items.first?.id, @@ -66,6 +69,7 @@ struct ForEachApp: App { } .padding(10) } + .focusable() } } .defaultSize(width: 400, height: 800) From d5825986c39096838a9a76aa48302fffed6009d1 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 6 Nov 2025 04:29:49 +0100 Subject: [PATCH 07/10] Fixed reordering, insertion and removal in ForEach where Element: Identifiable No non-identifiable support in this commit # Conflicts: # Examples/Bundler.toml # Examples/Package.swift # Examples/Sources/ForEachExample/ForEachApp.swift # Sources/SwiftCrossUI/Views/ForEach.swift --- Examples/Package.swift | 2 +- .../Sources/ForEachExample/ForEachApp.swift | 1 - Sources/SwiftCrossUI/Views/ForEach.swift | 56 ++++++++++++++++++- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/Examples/Package.swift b/Examples/Package.swift index b7f7ddd506..7bae86806a 100644 --- a/Examples/Package.swift +++ b/Examples/Package.swift @@ -80,6 +80,6 @@ let package = Package( .executableTarget( name: "ForEachExample", dependencies: exampleDependencies - ) + ) ] ) diff --git a/Examples/Sources/ForEachExample/ForEachApp.swift b/Examples/Sources/ForEachExample/ForEachApp.swift index f39c3a0a97..42e104ee6c 100644 --- a/Examples/Sources/ForEachExample/ForEachApp.swift +++ b/Examples/Sources/ForEachExample/ForEachApp.swift @@ -69,7 +69,6 @@ struct ForEachApp: App { } .padding(10) } - .focusable() } } .defaultSize(width: 400, height: 800) diff --git a/Sources/SwiftCrossUI/Views/ForEach.swift b/Sources/SwiftCrossUI/Views/ForEach.swift index 3f2a9264e3..5bd55b5853 100644 --- a/Sources/SwiftCrossUI/Views/ForEach.swift +++ b/Sources/SwiftCrossUI/Views/ForEach.swift @@ -254,15 +254,64 @@ extension ForEach: TypeSafeView, View where Child: View { // changed whereas it was actually just moved to a new slot in the array. Probably not // a huge issue, but definitely something to keep an eye on. var layoutableChildren: [LayoutSystem.LayoutableChild] = [] - for (i, node) in children.nodes.enumerated() { - guard i < elements.count else { - break + + let oldNodes = children.nodes + let oldMap = children.nodeIdentifierMap + let oldIdentifiers = children.identifiers + let identifiersStart = oldIdentifiers.startIndex + + children.nodes = [] + children.nodeIdentifierMap = [:] + children.identifiers = [] + + // Once this is true, every node that existed in the previous update and + // still exists in the new one is reinserted to ensure that items are + // rendered in the correct order. + var requiresOngoingReinsertion = false + + for (i, element) in elements.enumerated() { + let childContent = child(element) + let node: AnyViewGraphNode + + if let oldNode = oldMap[element.id] { + node = oldNode + + // Checks if there is a preceding item that was not preceding in + // the previous update. If such an item exists, it means that + // the order of the collection has changed or that an item was + // inserted somewhere in the middle, rather than simply appended. + requiresOngoingReinsertion = + requiresOngoingReinsertion + || { + guard + let ownOldIndex = oldIdentifiers.firstIndex(of: element.id) + else { return false } + + let subset = oldIdentifiers[identifiersStart.. Date: Thu, 6 Nov 2025 07:21:21 +0100 Subject: [PATCH 08/10] Made it work for non identifiables StressTestExample is broken --- Sources/SwiftCrossUI/Views/Button.swift | 2 ++ Sources/SwiftCrossUI/Views/ForEach.swift | 10 +++---- Sources/SwiftCrossUI/Views/Menu.swift | 6 ++--- Sources/SwiftCrossUI/Views/MenuItem.swift | 33 ++++++++++++++++++++--- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/Button.swift b/Sources/SwiftCrossUI/Views/Button.swift index 343a23f88b..3631c10164 100644 --- a/Sources/SwiftCrossUI/Views/Button.swift +++ b/Sources/SwiftCrossUI/Views/Button.swift @@ -1,3 +1,5 @@ +import Foundation + /// A control that initiates an action. public struct Button: Sendable { /// The label to show on the button. diff --git a/Sources/SwiftCrossUI/Views/ForEach.swift b/Sources/SwiftCrossUI/Views/ForEach.swift index 5bd55b5853..d05531867b 100644 --- a/Sources/SwiftCrossUI/Views/ForEach.swift +++ b/Sources/SwiftCrossUI/Views/ForEach.swift @@ -255,7 +255,6 @@ extension ForEach: TypeSafeView, View where Child: View { // a huge issue, but definitely something to keep an eye on. var layoutableChildren: [LayoutSystem.LayoutableChild] = [] - let oldNodes = children.nodes let oldMap = children.nodeIdentifierMap let oldIdentifiers = children.identifiers let identifiersStart = oldIdentifiers.startIndex @@ -269,11 +268,11 @@ extension ForEach: TypeSafeView, View where Child: View { // rendered in the correct order. var requiresOngoingReinsertion = false - for (i, element) in elements.enumerated() { + for element in elements { let childContent = child(element) let node: AnyViewGraphNode - if let oldNode = oldMap[element.id] { + if let oldNode = oldMap[element[keyPath: idKeyPath]] { node = oldNode // Checks if there is a preceding item that was not preceding in @@ -284,7 +283,8 @@ extension ForEach: TypeSafeView, View where Child: View { requiresOngoingReinsertion || { guard - let ownOldIndex = oldIdentifiers.firstIndex(of: element.id) + let ownOldIndex = oldIdentifiers.firstIndex( + of: element[keyPath: idKeyPath]) else { return false } let subset = oldIdentifiers[identifiersStart.. Bool { + lhs.hashValue == rhs.hashValue + } + + public func hash(into hasher: inout Hasher) { + hasher.combine( + { + switch self { + case .button(_, let int), .text(_, let int), .submenu(_, let int): + return int ?? 0 + } + }()) + } + + package func addingIDIfNeeded(id: Int) -> Self { + switch self { + case .button(let button, let int): + return .button(button, int ?? id) + case .text(let text, let int): + return .text(text, int ?? id) + case .submenu(let menu, let int): + return .submenu(menu, int ?? id) + } + } } From 30332e62c4896b1a616e7cd88085e873169d0ce3 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 6 Nov 2025 19:04:54 +0100 Subject: [PATCH 09/10] should fix remaining rebase problems --- .../Sources/WebViewExample/WebViewApp.swift | 10 +-- Sources/SwiftCrossUI/Views/Button.swift | 2 - Sources/SwiftCrossUI/Views/ForEach.swift | 62 +++---------------- Sources/SwiftCrossUI/Views/Menu.swift | 6 +- Sources/SwiftCrossUI/Views/MenuItem.swift | 33 +--------- 5 files changed, 19 insertions(+), 94 deletions(-) diff --git a/Examples/Sources/WebViewExample/WebViewApp.swift b/Examples/Sources/WebViewExample/WebViewApp.swift index fb11609c57..8de0c6f77a 100644 --- a/Examples/Sources/WebViewExample/WebViewApp.swift +++ b/Examples/Sources/WebViewExample/WebViewApp.swift @@ -29,10 +29,12 @@ struct WebViewApp: App { } .padding() - WebView($url) - .onChange(of: url) { - urlInput = url.absoluteString - } + #if !os(tvOS) + WebView($url) + .onChange(of: url) { + urlInput = url.absoluteString + } + #endif } } } diff --git a/Sources/SwiftCrossUI/Views/Button.swift b/Sources/SwiftCrossUI/Views/Button.swift index 3631c10164..343a23f88b 100644 --- a/Sources/SwiftCrossUI/Views/Button.swift +++ b/Sources/SwiftCrossUI/Views/Button.swift @@ -1,5 +1,3 @@ -import Foundation - /// A control that initiates an action. public struct Button: Sendable { /// The label to show on the button. diff --git a/Sources/SwiftCrossUI/Views/ForEach.swift b/Sources/SwiftCrossUI/Views/ForEach.swift index d05531867b..2a03041bce 100644 --- a/Sources/SwiftCrossUI/Views/ForEach.swift +++ b/Sources/SwiftCrossUI/Views/ForEach.swift @@ -249,69 +249,22 @@ extension ForEach: TypeSafeView, View where Child: View { } } + let elementsStartIndex = elements.startIndex + // TODO: The way we're reusing nodes for technically different elements means that if // Child has state of its own then it could get pretty confused thinking that its state // changed whereas it was actually just moved to a new slot in the array. Probably not // a huge issue, but definitely something to keep an eye on. var layoutableChildren: [LayoutSystem.LayoutableChild] = [] - - let oldMap = children.nodeIdentifierMap - let oldIdentifiers = children.identifiers - let identifiersStart = oldIdentifiers.startIndex - - children.nodes = [] - children.nodeIdentifierMap = [:] - children.identifiers = [] - - // Once this is true, every node that existed in the previous update and - // still exists in the new one is reinserted to ensure that items are - // rendered in the correct order. - var requiresOngoingReinsertion = false - - for element in elements { - let childContent = child(element) - let node: AnyViewGraphNode - - if let oldNode = oldMap[element[keyPath: idKeyPath]] { - node = oldNode - - // Checks if there is a preceding item that was not preceding in - // the previous update. If such an item exists, it means that - // the order of the collection has changed or that an item was - // inserted somewhere in the middle, rather than simply appended. - requiresOngoingReinsertion = - requiresOngoingReinsertion - || { - guard - let ownOldIndex = oldIdentifiers.firstIndex( - of: element[keyPath: idKeyPath]) - else { return false } - - let subset = oldIdentifiers[identifiersStart.. 0 { - let startIndex = elements.index(elements.startIndex, offsetBy: nodeCount) + let startIndex = elements.index(elementsStartIndex, offsetBy: nodeCount) for i in 0.. Bool { - lhs.hashValue == rhs.hashValue - } - - public func hash(into hasher: inout Hasher) { - hasher.combine( - { - switch self { - case .button(_, let int), .text(_, let int), .submenu(_, let int): - return int ?? 0 - } - }()) - } - - package func addingIDIfNeeded(id: Int) -> Self { - switch self { - case .button(let button, let int): - return .button(button, int ?? id) - case .text(let text, let int): - return .text(text, int ?? id) - case .submenu(let menu, let int): - return .submenu(menu, int ?? id) - } - } + case button(Button) + case text(Text) + case submenu(Menu) } From 1b22d8b0758e7b0a86dc2b27f12220b09a201699 Mon Sep 17 00:00:00 2001 From: Mia Koring Date: Thu, 6 Nov 2025 19:21:10 +0100 Subject: [PATCH 10/10] Changed version of swift-collections to 1.2.1 to match swift 5.10 --- Examples/Package.resolved | 6 +++--- Package.resolved | 6 +++--- Package.swift | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Examples/Package.resolved b/Examples/Package.resolved index f7227b2a94..bc8254d972 100644 --- a/Examples/Package.resolved +++ b/Examples/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "1eacde3553facaf2ac4f087f074b79e01abf68b6b810767996f192601e269d4d", + "originHash" : "f29a33ba90b5b5615d0de581d82e49b8fa747057114f7c3fd44c8916099b361c", "pins" : [ { "identity" : "aexml", @@ -167,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version" : "1.3.0" + "revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341", + "version" : "1.2.1" } }, { diff --git a/Package.resolved b/Package.resolved index de00b7becd..464f684aed 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "61c48c4d9a3bafacef3a6e1e2f03481a75a2966aaa511ff212e5fca86ce667c2", + "originHash" : "812b37087572e0699cecaadb15b7b5b42abb5278782a7809de1c1e72330a6d38", "pins" : [ { "identity" : "jpeg", @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version" : "1.3.0" + "revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341", + "version" : "1.2.1" } }, { diff --git a/Package.swift b/Package.swift index 2957a41c04..8022a25270 100644 --- a/Package.swift +++ b/Package.swift @@ -112,7 +112,7 @@ let package = Package( ), .package( url: "https://github.com/apple/swift-collections.git", - .upToNextMajor(from: "1.3.0") + exact: "1.2.1" ), // .package( // url: "https://github.com/stackotter/TermKit",