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
)
]
)
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
}()
Comment on lines +12 to +18
Copy link
Owner

Choose a reason for hiding this comment

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

Replace with

@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) {
guard insertionPosition > items.count - 1 else {
return
}
insertionPosition = max(items.count - 1, 0)
Comment on lines +42 to +45
Copy link
Owner

Choose a reason for hiding this comment

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

It feels odd having a guard to protect the 'bad' path. I think it should be

let upperLimit = max(items.count - 1, 0)
insertionPosition = min(insertionPosition, upperLimit)

If you want you can add an if statement to only set the insertionPosition if it has changed, but I don't believe that that will be necessary for performance at all once we do dependency analysis stuff.

}
#endif

ForEach(items) { item in
ItemRow(
item: item, isFirst: Optional(item.id) == items.first?.id,
Copy link
Owner

Choose a reason for hiding this comment

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

Move isFirst onto a new line

isLast: Optional(item.id) == items.last?.id
) {
items.removeAll(where: { $0.id == item.id })
Copy link
Owner

Choose a reason for hiding this comment

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

The comment I left on moveUp applies here too. We can make this a little more efficient by directly removing at an index (although the gain won't be as big as for moveUp and moveDown.

} 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.

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
Copy link
Owner

Choose a reason for hiding this comment

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

Item shouldn't be State here cause its true source of truth is from the external array. Can we make the Item class into a struct instead of an observable object?

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.
Copy link
Owner

Choose a reason for hiding this comment

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

Fix this up to use the new non-deprecated syntax.

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)
Copy link
Owner

Choose a reason for hiding this comment

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

Add an #else with Text("WebView isn't supported on tvOS").

.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"
Copy link
Owner

Choose a reason for hiding this comment

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

Is there a reason that this has to be exact and not from? We should avoid exact dependencies because then it's harder to e.g. use swift-collections and swift-cross-ui in the same project.

),
// .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