I think the issue the author had with SwiftUI is not understanding the reactivity system, or frequently choosing an expedient option that caused over-rendering - unnecessary re-renders in response to state changes that aren't required for the UX to work correctly from the user's perspective. Even if your UI's actual rendering work takes 1ms to paint, you want to avoid re-calculating busy work, allocation, etc, which can drive poor experience.
With a framework like React that provides very few primitives, it's quite clear when the framework will re-render. With a complex framework like SwiftUI, there are many primitives and different ways to represent state, subscribe to updates, etc, and the framework is quite under-documented. After spending 15 hours with SwiftUI, I still don't have a clear understanding of how the system detects a subscription, or what the re-render semantics are.
React I would describe this way:
1. Every re-render starts when you setState to a value that does not (!==) equal the current value in a parent component, or a reducer stores a new value internally.
2. Child components of that component will re-render except in two cases:
2a. The child component uses React.memo, and the props passed by the parent are shallow-equal to the previous props
2b. The child component JSX nodes are strict-equal to those returned by the previous render of the parent component, eg a subtree in the parent wrapped in useMemo
I can't come up with a similar definition for SwiftUI after 15 hours building an app in it. I need to consider @Binding, @State, ObservableObject, @ObservedObject, projected values. When is the system creating fine-grained subscriptions automatically? When is the system subscribing to an entire object (deeply?). Hard to find a cogent explanation of all the options.
Because of my different understanding of these frameworks, I personally write React apps that perform better than most people expect web apps to perform, and SwiftUI apps that perform much worse than people expect Apple native apps to perform.
Basically, unless an identity is explicitly specified using `.id()`, it uses the static types of your views to determine their identity. Naturally, this has a huge effect on what is re-drawn, but also animations.
This is also why AnyView can lead to worse performance - it erases the static type information.
[1]: https://developer.apple.com/videos/play/wwdc2021/10022/
With TCA you can side-step these issues to an extent, in that you can scope the store down to what the view needs. Then, the view body only re-renders when the data you scoped down to changes.
I'm frustrated also because there's often times when we want to pass state from a parent view through an intermediate view to a grandchild view. How can we avoid excessive re-rendering in the intermediate view? Often I feel like I'm able to scope down subscriptions, but not eliminate them entirely, again probably because I don't understand the system well enough / the system documentation is lacking.
If I understood your comment correctly, in SwiftUI developer should learn a lot of stuff to make re-rendering upon state change efficient?
CoreGraphics uses CPU rasterization of primitives and that's the problem on modern displays.
Retina grade displays have ~9 times more pixels on surface than old 96 ppi monitors. So you need 10 times more performant CPUs for rendering - CPU rasterization is O(Npix) complex - as better (resolution) monitor you have as slow your UI.
On the other side games use GPUs (Metal, OpenGL) to render primitives. In that case CPUs just generate lists of primitive definitions in vector forms (polygons, etc.). Rasterization of those primitives is done in parallel by crowd of GPU processors. Therefore, games UI is not tied with monitor resolution directly - same [display] lists are generated by CPU for any monitor resolution. In that respect GPU accelerated UI rendering is O(1) complex.
https://www.hackingwithswift.com/books/ios-swiftui/enabling-...
You can also embed a UIViewRepresentable to "manually" embed a metal kit view inside a SwiftUI hierarchy. Basically use SwiftUI to manage most of it, but use metal to handle a bottleneck somewhere. Here is an example:
- swiftui for macos apps is slower (smaller teams at apple to optimize it, all focus is on ios)
- with appkit you'd use reusable views, and swiftui is having hard time with them
- with reusable views you model data on view presentation, whereas with swiftui you often need to generate view data ahead of time, for all views (so 400 views can take some time just for data formatting)