Data Transformation & Migration

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

Data Transformation and Migration

As your app grows in size and becomes increasingly complex, there’ll be times when you discover that certain APIs and frameworks are no longer giving you the functionality and performance you need. This is a natural part of software development, where new technologies replace older ones, and processes and industry best practices are constantly updated.

Performing Analysis of a Persistence Solution

In this example, imagine you’re tasked with evaluating an app’s storage functionality and deciding whether it should begin to use Swift Data. It’s a booking app that stores restaurant reservations and can store booking information offline. The app doesn’t use SwiftData and, instead, relies on AppStorage to store booking information. Here’s what the code looks like:

struct Booking: Codable, Identifiable {
  let id: UUID
  let restaurantName: String
  let date: Date
  let numberOfPeople: Int
}

struct ContentView: View {
  @AppStorage("bookings") private var bookingsData: Data = Data()
  @State private var bookings: [Booking] = []
  @State private var isShowingAddBooking = false

  var body: some View {
    NavigationView {
      List {
        ForEach(bookings) { booking in
          VStack(alignment: .leading) {
            Text(booking.restaurantName)
              .font(.headline)
            Text("Date: \(formattedDate(booking.date))")
            Text("People: \(booking.numberOfPeople)")
          }
        }
        .onDelete(perform: deleteBooking)
    }
    .navigationTitle("Restaurant Bookings")
    .toolbar {
      Button("Add Booking") {
        isShowingAddBooking = true
      }
    }
    .sheet(isPresented: $isShowingAddBooking) {
      AddBookingView { newBooking in
        addBooking(newBooking: newBooking)
      }
    }
  }
  .onAppear(perform: loadBookings)
}

  private func loadBookings() {
    if let decodedBookings = try? JSONDecoder().decode([Booking].self, from: bookingsData) {
      bookings = decodedBookings
    }
  }

  private func saveBookings() {
    if let encodedBookings = try? JSONEncoder().encode(bookings) {
      bookingsData = encodedBookings
    }
  }

  private func addBooking(newBooking: Booking) {
    bookings.append(newBooking)
    saveBookings()
  }

  private func deleteBooking(at offsets: IndexSet) {
    bookings.remove(atOffsets: offsets)
    saveBookings()
  }

private func formattedDate(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    formatter.timeStyle = .short
    return formatter.string(from: date)
  }
}

struct AddBookingView: View {
  @State var restaurantName: String = ""
  @State var date: Date = Date()
  @State var numberOfPeople: Int = 1
  let addBooking: (Booking) -> Void
  @Environment(\.presentationMode) var presentationMode

  var body: some View {
    NavigationView {
      Form {
        TextField("Restaurant Name", text: $restaurantName)
        DatePicker("Date", selection: $date, displayedComponents: [.date, .hourAndMinute])
        Stepper("Number of People: \(numberOfPeople)", value: $numberOfPeople, in: 1...20)
      }
      .navigationTitle("Add Booking")
      .toolbar {
        Button("Save") {
          let newBooking = Booking(
                              id: UUID(),
                              restaurantName: restaurantName,
                              date: date,
                              numberOfPeople: numberOfPeople)
          addBooking(newBooking)
          presentationMode.wrappedValue.dismiss()
        }
      }
    }
  }
}

Implementing a Data Migration

Now you know that adding Swift Data is a good idea. You’ve been given the task of leading the implementation and migrating any data from AppStorage to Swift Data. Here are some of the tasks you’ll need to do.

// In BookingApp.swift
@main
struct BookingApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
    }.modelContainer(for: Booking.self) // 1
  }
}

// 2
struct OldBooking: Codable, Identifiable {
  let id: UUID
  let restaurantName: String
  let date: Date
  let numberOfPeople: Int
}

// 3
@Model
final class Booking {
  var restaurantName: String
  var date: Date
  var numberOfPeople: Int

  init(restaurantName: String, date: Date, numberOfPeople: Int) {
    self.restaurantName = restaurantName
    self.date = date
    self.numberOfPeople = numberOfPeople
  }
}

struct ContentView: View {
  // 4
  @Environment(\.modelContext) private var modelContext
  @Query private var bookings: [Booking]
  @AppStorage("bookings") private var oldBookingsData: Data = Data()
  // 5
  @AppStorage("hasMigrated") private var hasMigrated = false
  @State private var isShowingAddBooking = false

  var body: some View {
    NavigationView {
      List {
        ForEach(bookings) { booking in
          VStack(alignment: .leading) {
            Text(booking.restaurantName)
              .font(.headline)
            Text("Date: \(formattedDate(booking.date))")
            Text("People: \(booking.numberOfPeople)")
          }
        }
        .onDelete(perform: deleteBookings)
      }
      .navigationTitle("Restaurant Bookings")
      .toolbar {
        Button("Add Booking") {
          isShowingAddBooking = true
        }
      }
      .sheet(isPresented: $isShowingAddBooking) {
        AddBookingView()
      }
    }
    .onAppear {
      // 6
      if !hasMigrated {
        migrateFromAppStorage()
        hasMigrated = true
      }
    }
  }

  // 7
  private func migrateFromAppStorage() {
    guard !oldBookingsData.isEmpty else { return }
      do {
        let oldBookings = try JSONDecoder().decode([OldBooking].self, from: oldBookingsData)
        for oldBooking in oldBookings {
          let newBooking = Booking(
                            restaurantName: oldBooking.restaurantName,
                            date: oldBooking.date,
                            numberOfPeople: oldBooking.numberOfPeople)
          modelContext.insert(newBooking)
        }
        // Clear the old data after successful migration
        oldBookingsData = Data()
      } catch {
        print("Migration failed: \(error)")
      }
  }

  private func deleteBookings(at offsets: IndexSet) {
    for index in offsets {
      modelContext.delete(bookings[index])
    }
  }

  private func formattedDate(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    formatter.timeStyle = .short
    return formatter.string(from: date)
  }
}

struct AddBookingView: View {
  // 8
  @Environment(\.modelContext) private var modelContext
  @Environment(\.dismiss) private var dismiss
  @State var restaurantName: String = ""
  @State var date: Date = Date()
  @State var numberOfPeople: Int = 1

  var body: some View {
    NavigationStack {
      Form {
        TextField("Restaurant Name", text: $restaurantName)
        DatePicker("Date", selection: $date, displayedComponents: [.date, .hourAndMinute])
        Stepper("Number of People: \(numberOfPeople)", value: $numberOfPeople, in: 1...20)
      }
      .navigationTitle("Add Booking")
      .toolbar {
        Button("Save") {
          addBooking()
        }
      }
    }
  }

  private func addBooking() {
    // 9
    let newBooking = Booking(restaurantName: restaurantName, date: date, numberOfPeople: numberOfPeople)
    modelContext.insert(newBooking)
    dismiss()
  }
}
See forum comments
Download course materials from Github
Previous: Introduction: Data Transformation & Migration Next: Demo