Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e9b94a8
Fixed reordering, insertion and removal in ForEach where Element: Ide…
MiaKoring Nov 6, 2025
116b72c
Made it work for non identifiables
MiaKoring Nov 6, 2025
466c259
added fallback to old code where no keypath is specified
MiaKoring Nov 6, 2025
76968ca
added node reusing bypass after discovering first duplicate
MiaKoring Nov 6, 2025
2dba16b
Fixed ForEach [MenuItem] compatibility, documentation improvement & c…
MiaKoring Nov 6, 2025
25c6e66
added tvOS excluding compiler flag for Slider Component in ForEachExa…
MiaKoring Nov 6, 2025
d582598
Fixed reordering, insertion and removal in ForEach where Element: Ide…
MiaKoring Nov 6, 2025
743a488
Made it work for non identifiables
MiaKoring Nov 6, 2025
30332e6
should fix remaining rebase problems
MiaKoring Nov 6, 2025
1b22d8b
Changed version of swift-collections to 1.2.1 to match swift 5.10
MiaKoring Nov 6, 2025
e49b391
Implemented requested changes, potentially additional ForEach initial…
MiaKoring Dec 15, 2025
ff93fc6
Merge branch 'main' into fix/foreach-removals
MiaKoring Dec 15, 2025
3a965fe
fixed my merge messup
MiaKoring Dec 15, 2025
a2e1080
replaced upToNextMajor with upToNextMinor because major would lead to…
MiaKoring Dec 15, 2025
af14cc8
Merge branch 'main' into fix/foreach-removals
MiaKoring Jan 1, 2026
eb1169f
Merge branch 'main' into fix/foreach-removals
MiaKoring Jan 1, 2026
d6b068f
requested changes: ignored notes.json, directly passed &children.stac…
MiaKoring Jan 3, 2026
325e486
Merge branch 'main' into fix/foreach-removals
MiaKoring Jan 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Examples/Bundler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
6 changes: 3 additions & 3 deletions Examples/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Examples/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ let package = Package(
.executableTarget(
name: "HoverExample",
dependencies: exampleDependencies
)
),
.executableTarget(
name: "ForEachExample",
dependencies: exampleDependencies
)
]
)
105 changes: 105 additions & 0 deletions Examples/Sources/ForEachExample/ForEachApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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 {
Button("Append") {
biggestValue += 1
items.append(.init("\(biggestValue)"))
}

#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,
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
.padding(.top, 20)

ScrollView {
ForEach(greetings.reversed()[1...]) { greeting in
ForEach(items: greetings.reversed()[1...]) { greeting in

Check warning on line 41 in Examples/Sources/GreetingGeneratorExample/GreetingGeneratorApp.swift

View workflow job for this annotation

GitHub Actions / uikit (Vision)

'init(items:_:)' is deprecated: Use ForEach with id argument on non-Identifiable Elements instead.

Check warning on line 41 in Examples/Sources/GreetingGeneratorExample/GreetingGeneratorApp.swift

View workflow job for this annotation

GitHub Actions / uikit-catalyst

'init(items:_:)' is deprecated: Use ForEach with id argument on non-Identifiable Elements instead.

Check warning on line 41 in Examples/Sources/GreetingGeneratorExample/GreetingGeneratorApp.swift

View workflow job for this annotation

GitHub Actions / uikit (TV)

'init(items:_:)' is deprecated: Use ForEach with id argument on non-Identifiable Elements instead.

Check warning on line 41 in Examples/Sources/GreetingGeneratorExample/GreetingGeneratorApp.swift

View workflow job for this annotation

GitHub Actions / uikit (iPhone)

'init(items:_:)' is deprecated: Use ForEach with id argument on non-Identifiable Elements instead.

Check warning on line 41 in Examples/Sources/GreetingGeneratorExample/GreetingGeneratorApp.swift

View workflow job for this annotation

GitHub Actions / uikit (iPad)

'init(items:_:)' is deprecated: Use ForEach with id argument on non-Identifiable Elements instead.
Text(greeting)
}
}
Expand Down
3 changes: 1 addition & 2 deletions Examples/Sources/StressTestExample/StressTestApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(values, id: \.self) { value in
Text(value)
}
}.frame(minWidth: 300)
Expand Down
10 changes: 6 additions & 4 deletions Examples/Sources/WebViewExample/WebViewApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
exact: "1.2.1"
),
// .package(
// url: "https://github.com/stackotter/TermKit",
// revision: "163afa64f1257a0c026cc83ed8bc47a5f8fc9704"
Expand All @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ public struct MenuItemsBuilder {
first.items
}

public static func buildPartialBlock<Items: Collection>(
first: ForEach<Items, [MenuItem]>
public static func buildPartialBlock<Items: Collection, ID: Hashable>(
first: ForEach<Items, ID, [MenuItem]>
) -> [MenuItem] {
first.elements.map(first.child).flatMap { $0 }
}
Expand Down Expand Up @@ -51,9 +51,9 @@ public struct MenuItemsBuilder {
accumulated + buildPartialBlock(first: next)
}

public static func buildPartialBlock<Items: Collection>(
public static func buildPartialBlock<Items: Collection, ID: Hashable>(
accumulated: [MenuItem],
next: ForEach<Items, [MenuItem]>
next: ForEach<Items, ID, [MenuItem]>
) -> [MenuItem] {
accumulated + buildPartialBlock(first: next)
}
Expand Down
3 changes: 1 addition & 2 deletions Sources/SwiftCrossUI/Views/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ public struct Button: Sendable {
}
}

extension Button: View {
}
extension Button: View {}

extension Button: ElementaryView {
public func asWidget<Backend: AppBackend>(backend: Backend) -> Backend.Widget {
Expand Down
Loading
Loading