Skip to content

Conversation

@MiaKoring
Copy link
Contributor

@MiaKoring MiaKoring commented Nov 6, 2025

This pr fixes the issue of only the last items getting removed from the rendered content, even if they werent the ones changing and item insertion/reordering correctness like mentioned in #243

I added apple/swift-collections as a dependency, I’m using OrderedSet in the changes.

If a duplicate identifier is detected every node gets replaced with a new one, diffing is not possible.

For unique identifiers:

  • If it was included in the previous update, the node gets reused.
    It checks if there is an identifier it doesn’t know from the previous update anywhere before it, if there is, ongoing every existing node gets removed from the view graph and reinserted at its new place.

  • if it wasn’t included, a new node gets created and ongoing every existing node gets readded.

  • if its the first update every node gets added to the view graph

  • if there was no duplicate the nodes included in the previous update not included in the current one get removed from the view graph

  • if there were duplicate(s) every node gets replaced

Also I added some new initializers:
for Identifiable, setting the identifier key path to .id if not specified otherwise
for existing identifier a new one allowing customization of the id KeyPath

Sadly this PR needs to be a breaking change. I was forced to add labels to the elements property on some existing initializers. Otherwise the Swift Compiler wouldn’t select the right initializer (or even select one). disfavouredOverload didn’t help. While updating you can decide between adding the label (if needed) or a keyPath. Choosing the label option uses the same update method as before. With adding a keyPath for the Identifier you choose the new, improved update method. ForEach with Range or [Identifiable] automatically recieve the new method.

I added a new Example App, ForEachExample, showcasing deletion, insertion, appending and reordering.

I tested iOS, macOS, macCatalyst, GtkBackend on Linux and WinUIBackend on Windows successfully.

While it works with non-unique Identifiers I strongly recommend using unique and constant Identifiers. It should be considerably more performant due to it making as few as possible operations on the view graph.

In my tests it still was consistently about 11% faster than the previous implementation.

As soon as the reason for a view update is available ForEach’s update method should be optimized to do only whats necessary. For Example the whole diffing, reordering,… is obsolete when only the size changed. For now at least the correctness got fixed and performance at least slightly improved.

@MiaKoring
Copy link
Contributor Author

MiaKoring commented Nov 6, 2025

Edit: Fixed

oh, apparently I mixed some of my branches… there are changes that not belong to this pr… I’m going to try and remove them...

…ntifiable

No non-identifiable support in this commit
StressTestExample is broken
…leanup

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.
…ntifiable

No non-identifiable support in this commit

# Conflicts:
#	Examples/Bundler.toml
#	Examples/Package.swift
#	Examples/Sources/ForEachExample/ForEachApp.swift
#	Sources/SwiftCrossUI/Views/ForEach.swift
StressTestExample is broken
@MiaKoring MiaKoring force-pushed the fix/foreach-removals branch from 996b537 to 743a488 Compare November 6, 2025 17:50
Copy link
Owner

@stackotter stackotter left a comment

Choose a reason for hiding this comment

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

I'll review ForEach once I get the time. For now I've just reviewed all of the other files changed by the PR. I unfortunately ran out of time

Copy link
Owner

@stackotter stackotter left a comment

Choose a reason for hiding this comment

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

Just requested a few small changes this time. I'll also just have a quick look over all of the changes again because it's been a while since I last looked at this PR (and for this review I just looked at the 'changes since last viewed')

Copy link
Owner

@stackotter stackotter left a comment

Choose a reason for hiding this comment

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

I've gone through the code again, and I'm happy to merge once remaining comments have been resolved and once I've tested locally.

However, there some are some things that I think can be improved about the algorithm, which of course could be done in this PR, but could also be done by yourself or someone else in a future PR given that this PR does already fix the correctness issues that you set out to fix.

My main issue is that we're removing+adding a lot of nodes, which will impact certain backends (such as GtkBackend) quite heavily. In GtkBackend's case that is slow because it'll remove all signal handlers and re-add them again. We should probably be surgically moving things by index instead (if all backends are able to support that).

If we use moves/swaps instead of removing+adding, we may be able to update the algorithm to first produce a series of swaps, then a series of insertions and removals (strictly for new or removed elements).

But again, I'm happy for that to be left as a future direction in order to get your existing ForEach improvements merged.

@MiaKoring
Copy link
Contributor Author

But again, I'm happy for that to be left as a future direction in order to get your existing ForEach improvements merged.

Good point, I was not aware of this being that bad on Gtk. I’m going to create an issue for the required update and fix it in a future PR.

…kLayoutCache, moved inserted inside the for loop
# Conflicts:
#	Package.resolved
#	Package.swift
Copy link
Owner

@stackotter stackotter left a comment

Choose a reason for hiding this comment

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

Looks good now, thanks!

@stackotter
Copy link
Owner

Actually, I just realised that we should have the original init with some fallback implementation and a deprecation warning. I'll do that now.

@stackotter stackotter changed the base branch from main to stackotter/add_foreach_deprecated_init January 5, 2026 05:30
@stackotter stackotter merged commit 706957e into stackotter:stackotter/add_foreach_deprecated_init Jan 5, 2026
11 checks passed
stackotter pushed a commit that referenced this pull request Jan 5, 2026
…ess (#245)

* Fixed reordering, insertion and removal in ForEach where Element: Identifiable

No non-identifiable support in this commit

* Made it work for non identifiables

StressTestExample is broken

* added fallback to old code where no keypath is specified

* added node reusing bypass after discovering first duplicate

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

* added tvOS excluding compiler flag for Slider Component in ForEachExample

* 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

* Made it work for non identifiables

StressTestExample is broken

* should fix remaining rebase problems

* Changed version of swift-collections to 1.2.1 to match swift 5.10

* Implemented requested changes, potentially additional ForEach initializer pending

* fixed my merge messup

* replaced upToNextMajor with upToNextMinor because major would lead to pulling packages unsupported on swift 5.10 tooling

* requested changes: ignored notes.json, directly passed &children.stackLayoutCache, moved inserted inside the for loop
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants