Skip to content

Latest commit

 

History

History

NorthwindSwiftUI

Lighter Example: Northwind SwiftUI App

A SwiftUI example that accesses the Northwind database. It displays the products and allows simple editing operations.

IMPORTANT: This is NOT a demo on how to structure a SwiftUI application. It doesn't make use of proper concepts like ViewControllers, but keeps everything in local states.

Note: The example requires Swift 5.7 / Xcode 14b for proper plugin support.

Northwind API: Documentation

Overview

The example in a nutshell. The source just demonstrates the concepts (e.g. lacks error handlers), check the sources for the real implementation.

The example has to implementations: The main one is for iOS 16 and macOS Ventura and uses the new NavigationSplitView. A subfolder contains a NavigationView variant that works well on macOS 12.

In the SwiftUI application struct the Northwind database is bootstrapped by copying the database file embedded in the Northwind package. The database struct is then passed down to the views in the SwiftUI environment.

@main
struct NorthwindApp: App {

  let database = try Northwind.bootstrap(
    copying: Northwind.module.connectionHandler.url
  )
  var body: some Scene {
    WindowGroup {
      ContentView()
        .environment(\.database, database)
    }
  }
}

The ContentView is just a three-pane NavigationSplitView:

struct ContentView: View {

  @State private var section           : Section?    = .products
  @State private var products          : [ Product ] = []
  @State private var selectedProductID : Product.ID?
  
  var body: some View {
    NavigationSplitView(
      sidebar: { Sidebar(section: $section) },
      content: { 
        ProductsList(products: $products,
                     selectedProduct: $selectedProductID)
      },
      detail: {
        ProductPage(snapshot: product, onSave: updateSavedProduct)
      }
    )
  }
}

The database is eventually accessed in the ProductList, a SwiftUI View using a List to display the fetched products. The products are fetched using the .task modifier:

@available(iOS 16.0, macOS 13, *)
struct ProductsList: View {
  @Environment(\.database) private var database
  @Binding var products        : [ Product ]
  @Binding var selectedProduct : Product.ID?

  @State private var searchString = ""
    
  var body: some View {
    List(products, selection: $selectedProduct) {
      ProductCell(product: $0)
    }
    .searchable(text: $searchString)
    .task(id: searchString) {
      products = try await database.products.fetch(orderBy: \.productName) {
        $0.productName.contains(
          searchString.trimmingCharacters(in: .whitespacesAndNewlines),
          caseInsensitive: true
        )
      }
    }
  }
}

An example for record updates can be found in the ProductPage View. Database records are always Equatable and Hashable (because they are just values in a SQLite database), so it is easy to check whether there are changes to be saved.

struct ProductPage: View {
  @Environment(\.database) private var database

                 let snapshot : Product
  @State private var product  : Product = Product(id: 0, productName: "")

  private var hasChanges : Bool {
    snapshot != product
  }

  var body: some View {
    Form {
      ProductForm(product: $product)
    }
    .onChange(of: snapshot) { product = $0 }
    .onAppear               { product = $0 }
    .toolbar {
      ToolbarItem(placement: .destructiveAction) {
        Button("Revert") { product = snapshot }
          .disabled(!hasChanges)
      }
      ToolbarItem(placement: .confirmationAction) {
        Button("Save") {
          try database.update(product)
        }
        .disabled(!hasChanges)
      }
    }
  }
}

Screenshot

Screenshot 2022-08-16 at 16 41 31

Who

Lighter is brought to you by Helge Heß / ZeeZide. We like feedback, GitHub stars, cool contract work, presumably any form of praise you can think of.