Understanding Data Management Patterns

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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.

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)
    }
  }
}
@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)
  }
}
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
  }
}
See forum comments
Download course materials from Github
Previous: Introduction: Data Management Patterns Next: Demo