Our story begins with the FAQAccordion
, a seemingly innocent SwiftUI component designed to display frequently asked questions. Everything looked great during development until we noticed that every time we navigated to a new page, our app would slow down, and the memory usage would skyrocket.
import Foundation
import SwiftUI
struct FAQAccordion: View {
@State private var faqs: [FAQ] = loadFAQs()
@State private var expandedID: UUID?
var body: some View {
NavigationView {
List(faqs, id: \.id) { faq in
VStack(alignment: .leading) {
Text(faq.question)
.font(.headline)
.padding()
.onTapGesture {
withAnimation {
self.expandedID = (self.expandedID == faq.id) ? nil : faq.id
}
}
if expandedID == faq.id {
Text(faq.answer)
.padding([.horizontal, .bottom])
.transition(.opacity.combined(with: .slide))
}
}
}
.navigationTitle("FAQs")
}
}
}
func loadFAQs() -> [FAQ] {
guard let url = Bundle.main.url(forResource: "faqs", withExtension: "json"),
let data = try? Data(contentsOf: url) else {
return []
}
do {
let decoder = JSONDecoder()
return try decoder.decode([FAQ].self, from: data)
} catch {
print("Error decoding FAQ data: \(error)")
return []
}
}
The Detective Work: Uncovering the Culprit
It was late one evening, and the app’s sluggishness was gnawing at us like a mosquito that just wouldn’t quit. We needed to find the root cause, and fast. After some sleuthing, we discovered the culprit: loadFAQs(). This function was being called every time the FAQAccordion view was loaded. It was as if every sip of water required a trip to the well.
The Eureka Moment: Xcode Profiler to the Rescue
Our investigation led us to the Xcode Profiler, our trusty magnifying glass for spotting performance issues. By running the app with the Allocations instrument, we saw our memory usage spike every time we navigated to the FAQ section. The profiler revealed that loadFAQs()
was loading the JSON data repeatedly, causing unnecessary memory consumption. It was like finding a hidden leak in a sinking ship—crucial and satisfying.
The Fix: Loading FAQs Asynchronously and Caching
We decided to make loadFAQs asynchronous. This would allow us to load the FAQs once and store them, rather than fetching the JSON file every single time. Then, we integrated this logic into a shared data model. Here’s how we did it:
1. Asynchronous Loading:
func loadFAQs() async -> [FAQ] {
guard let url = Bundle.main.url(forResource: "faqs", withExtension: "json"),
let data = try? Data(contentsOf: url) else {
return []
}
do {
let decoder = JSONDecoder()
return try decoder.decode([FAQ].self, from: data)
} catch {
print("Error decoding FAQ data: \(error)")
return []
}
}
2. Shared Data Model:
import SwiftUI
import Combine
class Store: ObservableObject {
@Published var faqs: [FAQ] = []
init() {
Task {
self.faqs = await loadFAQs()
}
}
}
3. Environment Object:
@main
struct AluminumWeightCalculatorApp: App {
@StateObject private var store = Store()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(store)
}
}
}
4. Optimized FAQAccordion:
struct FAQAccordion: View {
@EnvironmentObject var store: Store
@State private var expandedID: UUID?
var body: some View {
NavigationView {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(store.faqs, id: \.id) { faq in
VStack(alignment: .leading) {
Text(faq.question)
.font(.headline)
.padding()
.onTapGesture {
withAnimation {
self.expandedID = (self.expandedID == faq.id) ? nil : faq.id
}
}
if expandedID == faq.id {
Text(faq.answer)
.padding([.horizontal, .bottom])
.transition(.opacity.combined(with: .slide))
}
}
}
}
.padding()
}
.navigationTitle("FAQs")
}
}
}
The Results: Swift and Efficient
By loading the FAQs once and using the shared store, we saw significant improvements. The FAQAccordion
now loads swiftly, and the memory usage is as efficient as a Hemingway sentence—clear, direct, and free of excess.
Conclusion: From Frustration to Triumph
In the end, our optimization journey taught us a lot about efficient data management in SwiftUI. By using asynchronous loading and environment objects, we not only fixed our immediate issues but also laid a robust foundation for future development. Sometimes, the smallest change in how you load and manage data can lead to the biggest performance gains.