I have a expensive GIF animated view that I use as a activity indicator and it needs to be used in a lot of places in the UI, wherever the user encounters a API call.
In the UIKit
world, I would warm up a UIView
with the animated gif, and create a new UIWindow
, keep a reference to it and add it as a root view and make the window visible and hidden when required. Easy.
However I have a hard time figuring out how I could achieve the same in SwiftUI. Or maybe I’m going about this all wrong. Help appreciated!
Not that it’d help, but what I had in mind for the loading indicator, which I would call via .modifier(LoadingIndicator(...))
wherever required.
struct LoadingIndicator<ProgressViewType: View>: ViewModifier {
var isShowing: Bool
@ViewBuilder
var progressView: () -> ProgressViewType
func body(content: Content) -> some View {
ZStack {
content
if isShowing {
loadingView
}
}
}
private var loadingView: some View {
GeometryReader { proxyReader in
ZStack {
Color.white.opacity(0.1)
.frame(maxWidth: .infinity, maxHeight: .infinity)
progressView()
}
}
.ignoresSafeArea()
//.transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2)))
}
}
Edit:
Ok, this is what I have so far. To clarify the expensive bit, the loading indicator that is being used is a GIF. I ran and profiled my app a couple of times and the memory usage shoots up.
Every time I profiled when the loading indicator is displayed, the CPU usage reads 20% for a moment, while hovering around 2% otherwise. I’m no a expert, but doesn’t frequently allocating and deallocating drain battery life?
If this is a non-issue, I am happy to leave it as is, since this works as expected.
I have trimmed down and added a reproducible example:
This requires the SDWebImageSwiftUI package. And any 10MB GIF.
import Foundation
import SwiftUI
import SDWebImageSwiftUI
extension View {
@ViewBuilder
func `if`(_ condition: Bool, transform: (Self) -> some View) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
struct LoadingIndicator<ProgressViewType: View>: ViewModifier {
var isShowing: Bool
@ViewBuilder
var progressView: () -> ProgressViewType
func body(content: Content) -> some View {
ZStack {
content
if isShowing {
loadingView
}
}
}
private var loadingView: some View {
GeometryReader { proxyReader in
ZStack {
Color.white.opacity(0.1)
.frame(maxWidth: .infinity, maxHeight: .infinity)
progressView()
}
}
.ignoresSafeArea()
.transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2)))
}
fileprivate static var defaultProgressView: some View {
ProgressView()
.progressViewStyle(
CircularProgressViewStyle(tint: .white)
)
.scaleEffect(x: 2, y: 2, anchor: .center)
.background(
RoundedRectangle(cornerRadius: 16)
.foregroundColor(Color.black.opacity(0.7))
.frame(width: 80, height: 80)
)
}
}
extension View {
func loadingIndicator<ProgressView: View>(_ isShowing: Bool, @ViewBuilder progressView: @escaping () -> ProgressView) -> some View {
self.if(isShowing) { $0.blur(radius: 10) }
.modifier(LoadingIndicator(isShowing: isShowing, progressView: progressView))
}
func tpActivityIndicator(_ isShowing: Bool) -> some View {
loadingIndicator(isShowing) {
VStack {
AnimatedImage(name: "loading_indicator.gif")
.resizable()
.scaledToFit()
.frame(width: 130, height: 150)
Text("Loading...")
.font(.body)
.foregroundStyle(Color.white)
}
}
}
}
#Preview {
VStack {
Text("Lorem Ipsum")
}
.padding()
.background { Color.yellow }
.clipShape(RoundedRectangle(cornerRadius: 5))
.tpActivityIndicator(true)
}
7
Coming from UIKit with reference-based views, the value-based system of SwiftUI can be a little unfamiliar at first.
The most important thing to understand compared to UIKit is that in SwiftUI views should be as lightweight as possible and they are (as mentioned) value types. This also indirectly answers your question about how to make views “reusable” in SwiftUI. The way you use the term, the answer would be “not at all”, because value types are copied and not referenced for reuse.
SwiftUI works in such a way that views are constantly recreated and only their state is transferred to a new instance if required.
I.e. the creation of a SwiftUI view should never be sth. expensive. In principle, the same applies to the layout process of SwiftUI views.
Your actual problem is that you want to optimize the use of an expensive resource that could affect the performance of your app or device.
In your case, according to your description, this is a very large local image (10MB GIF) that needs to be loaded into memory, decompressed and rendered.
As is often the case with this type of problem, you have to choose between different strategies. You can try to load these resources into memory in advance (maybe even in a decompressed state), but this will increase the memory footprint of your app.
Or you can load the resource only when needed, in which case the short-term CPU load increases which may affect your UI.
You have to decide for yourself which strategy is best for your app, as this depends heavily on your specific app and use case.
The tools Instruments provides help you to identify bottlenecks. There are many WWDC videos and blog posts on the web regarding this topic.
For images, you could typically load their data into memory in advance if the image needs to be constantly loaded and displayed in a lot of areas in the app. But then you should always make sure that bitmap-based images do not have to be transformed for display.
You should also ask yourself whether it absolutely has to be a 10MB GIF or whether you can use another more suitable method for the animation display.
For example, by using something like Lottie or another vector-based approach.
If you absolutely want to rely on SDWebImage, you need to take a look at what is offered in the framework in terms of pre-loading or options for retrieving images from a cache.
I.e. SDImageCache
could be what you’re looking for.