Understanding Data Management Patterns
Looking at Data Management Patterns
Apps that require a lot of data can find themselves overwhelmed by the volume they need to deal with. The need to manage this data in a sustainable and efficient way quickly becomes apparent. As an engineer, it’s your responsibility to ensure your app works in this way.
One technique you can rely on is ensuring your app has a Data Strategy in place so that when it comes to dealing with data in a particular situation, there are established patterns in place to deal with it.
What does this look like in practice? Take a look at an example.
Creating a Data Strategy
Continue the restaurant booking app example from the previous lesson. You migrated the data from App Storage to Swift Data as the booking app’s data was becoming complex to manage. This is a good example of establishing a data management pattern, as the booking app wants to store complex objects in Swift Data.
As you know from lesson 1, Swift Data is a good choice for storing complex information offline. You can make this part of your data strategy. That way, when an engineer wants to store complex information for offline use in the app, there’s already an established pattern for doing this.
Relying on established patterns is a good way to ensure data is managed in standardized ways in your app. It reduces the time needed to handle new information and the risk of things going wrong. You can also share this knowledge with other engineers, speeding up their development times. It’s a force multiplier!
Over time, you’ll establish a few different data management patterns in your app. Your app will be able to handle data from different sources and store it in different technologies. By leveraging the benefits of each technology, your Data Strategy will become established.
Take a look at the booking app code, which has gone through the process of establishing data management patterns as part of its data strategy.
A Data Strategy In Practice
First, let’s look at the ContentView:
struct ContentView: View {
// 1
@Query private var bookings: [Booking]
// 2
@State private var viewModel: RestaurantListViewModel
@State private var isShowingAddBooking = false
init(modelContext: ModelContext) {
let viewModel = RestaurantListViewModel(modelContext: modelContext)
_viewModel = State(initialValue: viewModel)
}
var body: some View {
NavigationStack {
List {
Section(header: Text("Your Bookings")) {
ForEach(bookings) { booking in
BookingRow(booking: booking)
}
.onDelete(perform: deleteBooking)
}
Section(header: Text("Restaurants")) {
// 3
ForEach(viewModel.restaurants, id: \.name) { restaurant in
RestaurantRow(restaurant: restaurant)
}
}
}
.navigationTitle("Restaurant App")
.toolbar {
Button("Add Booking") {
isShowingAddBooking = true
}
}
.sheet(isPresented: $isShowingAddBooking) {
AddBookingView()
}
.onAppear {
Task {
// 4
await viewModel.fetchRestaurants()
}
}
}
}
private func deleteBooking(at offsets: IndexSet) {
for index in offsets {
let booking = bookings[index]
// 5
viewModel.deleteBooking(booking: booking)
}
}
}
Here are the important parts of this file relating to data management:
- The
ContentViewis receiving a list of bookings from Swift Data using the@Queryproperty wrapper. - The
ContentViewhas aRestaurantListViewModel, responsible for retrieving restaurant information and writing information to Swift Data. - The ViewModel is used to populate the list with restaurants.
- When the View first appears, it calls
viewModel.fetchRestaurants(). This retrieves restaurants from an external source. - If a booking is deleted,
viewModel.deleteBooking()is called, which will remove the booking from Swift Data.
In this View, you can quickly tell the two sources of information are Swift Data and the RestaurantListViewModel. This gives you an indication of how data management is handled in the app. Swift Data is used to query information for SwiftUI, and ViewModels are used to handle the interactions between SwiftUI and lower-level operations like writing to Swift Data or working with other objects to make network requests.
Take a look at RestaurantListViewModel:
@Observable
class RestaurantListViewModel {
// 1
var restaurants: [Restaurant] = []
// 2
private let networkManager = NetworkManager.shared
private var modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
loadRestaurants()
}
// 3
private func loadRestaurants() {
let descriptor = FetchDescriptor<Restaurant>(sortBy: [SortDescriptor(\.name)])
do {
restaurants = try modelContext.fetch(descriptor)
} catch {
print("Error loading restaurants: \(error)")
}
}
func fetchRestaurants() async {
do {
// 4
let fetchedRestaurants = try await networkManager.fetchRestaurants()
// Update existing restaurants or add new ones
for fetchedRestaurant in fetchedRestaurants {
if let existingRestaurant = restaurants.first(where: { $0.name == fetchedRestaurant.name }) {
existingRestaurant.cuisine = fetchedRestaurant.cuisine
} else {
let newRestaurant = Restaurant(name: fetchedRestaurant.name, cuisine: fetchedRestaurant.cuisine)
modelContext.insert(newRestaurant)
restaurants.append(newRestaurant)
}
}
// Remove restaurants that no longer exist
let fetchedNames = Set(fetchedRestaurants.map { $0.name })
restaurants.forEach { restaurant in
if !fetchedNames.contains(restaurant.name) {
modelContext.delete(restaurant)
}
}
restaurants = restaurants.filter { fetchedNames.contains($0.name) }
try modelContext.save()
} catch {
print("Error fetching restaurants: \(error)")
}
}
// 5
func deleteBooking(booking: Booking) {
modelContext.delete(booking)
}
}
Here are the important parts relating to data management in RestaurantListViewModel:
- A property called
restaurantsis exposed for other objects to observe. This stores the current list of restaurants the app has received, whether from Swift Data or the network. - A
NetworkManagerproperty is created here. Its responsibility is to retrieve data from the network to keep the restaurant information that the app knows updated. - A function called
loadRestaurantsis defined. This function is called when the ViewModel is first created. It queries Swift Data usingmodelContextto search for restaurants and provide data immediately. - In the function
fetchRestaurants, the NetworkManager fetches new restaurant information asynchronously and updates Swift Data and therestaurantsproperty with the information. Any old information is removed. - Finally, a function called
deleteBookingis defined. Other classes use this to let the ViewModel know a booking has been removed by passing the booking into themodelContext.
The ViewModel here is interesting because it receives information from different data sources, Swift Data and the network. It also exposes information because it’s observable; external objects can observe the restaurant property and receive updates.
You can see the data management patterns here are as follows:
- To receive information from the network, you need to use
NetworkManager. - To store information locally, you need to use
Swift Data. - To receive information from the ViewModel, use the
restaurantsproperty to receive updates.
If you were to create another ViewModel, You could use these patterns as a base, speeding up your development!
Finally, look at the NetworkManager to understand what’s happening:
class NetworkManager {
static let shared = NetworkManager()
private init() {}
// 1
let cacheKey = "restaurants" as NSString
private let cache = NSCache<NSString, NSArray>()
func fetchRestaurants() async throws -> [Restaurant] {
// 2
if let cachedRestaurants = cache.object(forKey: cacheKey) as? [Restaurant] {
return cachedRestaurants
}
// Simulated network request
try await Task.sleep(for: .seconds(1))
let restaurants = [
Restaurant(name: "Pasta Palace", cuisine: "Italian"),
Restaurant(name: "Sushi Sensation", cuisine: "Japanese"),
Restaurant(name: "Burger Bonanza", cuisine: "American")
]
// 3
cache.setObject(restaurants as NSArray, forKey: cacheKey)
return restaurants
}
}
Here are the important parts related to data management:
- An
NSacheand aNSStringfor the cache key are created, these are used to store values from thefetchRestaurantsfunction. - Inside
fetchRestaurants, the cache is checked to see if any values are stored under the cache key. If there are, then the values are returned, and the network request doesn’t occur. - If no values are in the cache, then the network request happens, and the objects are placed inside the cache for use in the next request. The response is returned outside the function. In this case, the network request is simulated for the lesson.
The NetworkManager here shows how to retrieve information from the network or the cache. If you wanted to add another function to make another request, you could use the existing data management patterns in the class to build another network request and choose to store any previous requests in NSCache.
Putting all the data management patterns together, your data strategy for the app looks like this:
- To query data from Swift Data in SwiftUI views, use Swift Data.
- To query data from ViewModels in SwiftUI views, use exposed properties and observe changes.
- To receive information from the network or cache, use
NetworkManager. - To store information locally inside a ViewModel, you need to use a
modelContextprovided by Swift Data.
This isn’t an exhaustive list, as it doesn’t talk about App Storage. For this app however, it provides reasonable guidelines for developers to follow if they need to work on a new feature or fix a bug.
Now that you know how to use data management patterns and create a data strategy, you can apply these techniques to TheMet App.