Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
)
]
)
92 changes: 92 additions & 0 deletions Examples/Sources/ForEachExample/ForEachApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import DefaultBackend
import Foundation
import SwiftCrossUI

#if canImport(SwiftBundlerRuntime)
import SwiftBundlerRuntime
#endif

@main
@HotReloadable
struct ForEachApp: App {
@State var items = (0..<20).map { Item("\($0)") }
@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) {
let upperLimit = max(items.count - 1, 0)
insertionPosition = min(insertionPosition, upperLimit)
}
#endif

ForEach(Array(items.enumerated()), id: \.element.id) { (index, item) in
ItemRow(
item: item,
isFirst: index == 0,
isLast: index == items.count - 1
) {
items.remove(at: index)
} moveUp: {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be best to have the ForEach iterate over items.enumerated so that each ItemRow can know its own index and avoid this linear operation in what could be a constant-time moveUp implementation (same for moveDown). I'm pointing this out cause I can see ForEachExample being used to stress test SwiftCrossUI's performance, in which case these move functions should be implemented with best practice.

Copy link
Contributor Author

@MiaKoring MiaKoring Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ForEach would look like this:

ForEach(Array(items.enumerated()), id: \.element.id) { (index, item) in

EnumeratedSequence.Element is not identifiable, so I have to specify the identifier path

Also EnumeratedSequence is only conform to Collection on macOS 26+, so I had to wrapp it in an array.

If this is fine with you we can leave it like that. Otherwise additional initializers could do it for you and I could remove the necessity to specify a path on enumerated sequences where the element is identifiable.

Just let me know ^^

guard index != items.startIndex else { return }
items.swapAt(index, index - 1)
} moveDown: {
guard index != items.endIndex else { return }
items.swapAt(index, index + 1)
}
}
}
.padding(10)
}
}
}
.defaultSize(width: 400, height: 800)
}
}

struct ItemRow: View {
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)
}
}
}

struct Item: Identifiable {
let id = UUID()
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 @@ struct GreetingGeneratorApp: App {
.padding(.top, 20)

ScrollView {
ForEach(greetings.reversed()[1...]) { greeting in
ForEach(greetings.reversed()[1...], id: \.self) { greeting in
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
12 changes: 8 additions & 4 deletions Examples/Sources/WebViewExample/WebViewApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@ struct WebViewApp: App {
}
.padding()

WebView($url)
.onChange(of: url) {
urlInput = url.absoluteString
}
#if !os(tvOS)
WebView($url)
.onChange(of: url) {
urlInput = url.absoluteString
}
#else
Text("WebView isn't supported on tvOS")
#endif
}
}
}
Expand Down
13 changes: 11 additions & 2 deletions 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 @@ -122,6 +122,10 @@ let package = Package(
url: "https://github.com/stackotter/swift-winui",
revision: "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86"
),
.package(
url: "https://github.com/apple/swift-collections.git",
.upToNextMinor(from: "1.2.1")
),
.package(
url: "https://github.com/stackotter/swift-benchmark",
.upToNextMinor(from: "0.2.0")
Expand All @@ -145,6 +149,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
Loading
Loading