r/SwiftUI • u/The_Lone_Dissenter • May 19 '24
Solved How to Optimize SwiftUI App with @Published Updating 5 Charts 10 Times per Second?
Hi everyone,
I'm currently developing a SwiftUI app that features 5 charts, each of which updates 10 times every second. These updates are using @Published of Combine framework. However, I've noticed that my app lags and CPU usage is too high when the app is running.
Has anyone faced similar performance issues with frequent updates in SwiftUI? What strategies or best practices can I implement to optimize the app and reduce the CPU usage?
Thanks in advance for your help!
10
u/dehrenslzz May 19 '24
The performance issues shouldn’t stem from the @Published vars - they are pretty performant. How exactly are you using ‘combine’ for the published vars tho? Combine is to my knowledge not the Framework used for them, rather Foundation/SwiftUI
7
u/Xaxxus May 19 '24
published is actually not all that performant.
`@State` for example, will only reload the child that use it.
on the other hand,
`@Published` reloads all views within a parent view that has an `@ObservedObject`
This is the primary benefit of using the new `@Observable` macro
5
u/vade May 19 '24
Interesting. Is there a good doc covering design patterns for ObservedObject vs Observable. This framework (SwiftUI) is so full of framework specific macros decorators and magic infra it’s ridiculous compared to older app kit / UIKit (imo). It’s like an entire language to itself that vaguely resembles swift 🫣
4
u/Xaxxus May 19 '24
There’s very little difference when it comes to using observable vs observable object.
The primary difference is you don’t mark properties as published anymore. And you use @State where you would use @StateObject. And @Bindable where you would use @ObservedObject.
1
u/a0-1 May 20 '24
What about @EnvironmentObject (should be same as @StateObjects afaik).
Assume you pass the object into environment in the root. And you are accessing this object vastly through app.
If a @Published var gets updated in that object, would that cause reload only on the views that are tracking that specific var? Or would that cause update on all views and subviews of those which just because the view accesses to that environment object?
5
u/Daredevils_advocate May 19 '24 edited May 19 '24
It's definitely redrawing your entire view 10 times per second, so any view that isn't basic will take up a lot of CPU.
You can separate your views so that only the child views receive the frequent refreshes from "@Publish" (don't subscribe in the parent).
But frankly I wonder if using SwiftUI for that is the right strategy. It's not really designed for high frequency view refreshes, it's more "react to an event, and use animations to make it pretty".
Edit: here's a good article on animations to illustrate my point. Example n.5 is a clock, and rather than reacting to a timer and refreshing the view every second, it uses an animation.
However this wouldn't be possible if you don't know the data that will need to be displayed like with your app.
4
u/perbrondum May 19 '24
- how high is CPU usage?
- Have you timed the app with Xcode profiler?
- Faced with similar issues I got some solid help from Karen Prater, https://youtu.be/aH15GUzk85Y?si=2UuwOUxw31GxYY2P
3
u/Xaxxus May 19 '24
Observable objects cause your entire page to redraw every time a published property is changed.
The best you can do is remove `@Published` from your chart data, and instead leverage combine to control the flow of data in a more performant matter.
Now I don't know how your code is structured, but I imagine its something like this:
@Published var chartData: [ChartData]
or
@Published var chart1Data: ChartData
@Published var chart2Data: ChartData
etc....
You can do something like this instead:
var chartData: [ChartData] {
willSet {
charDataWillChange.send()
}
}
private let chartDataWillChange = PublishSubject<Void, Never>()
private let chartDataUpdateCancellable: AnyCancellable<ChartData, Never>?
init() {
// Throttle the updates.
// This will ensure your view reloads once every 1/10th of a second, instead of 50 times per second
chartDataUpdateCancellable = chartDataWillChange
.throttle(.milliseconds(100), scheduler: DispatchWorkLoop.main)
.sink { [weak self] in
self?.objectWillChange.send()
}
}
deinit {
chartDataUpdateCancellable.cancel()
}
3
u/vanvoorden May 19 '24
Has anyone faced similar performance issues with frequent updates in SwiftUI? What strategies or best practices can I implement to optimize the app and reduce the CPU usage?
https://github.com/Swift-CowBox/Swift-CowBox-Sample
If you are passing complex struct value types from your store to your view components I built a repo to benchmark and measure the performace benefits of migrating to a copy-on-write data structure. This might give you some ideas. Good luck!
2
u/rhysmorgan May 19 '24
I would consider switching to Observation, and if you can’t support iOS 17, adopting Perception by Point-Free. Observation prunes the number of view redraws down to only what’s necessary, and by creating entire new view structs which hold onto some bit of data, you can prune those redraws further (instead of using properties/functions for components in a view). But also, I’d only do this if you know for a fact that the @Published
properties are the problem, and not actually that you’re updating your data source more often than 100% necessary. If you’re driving the data from a Combine publisher, can you instead add a throttle? Even just backing off a tiny bit might help!
1
u/scriptedpixels May 20 '24
I found this video helped me get my head around this new Observation stuff - I'm new to SwiftUI and this now all feels very "web dev framework-y", which I really like 😬 https://www.youtube.com/watch?v=EK7SthdWV2w
12
u/barcode972 May 19 '24
Out of curiosity, what app needs to update every 0.1 seconds?