TLDR: My question is at the bottom of all of this code. But I wanted people to be able to understand my question fully, and recreate the project easily if they want to.
Lets say I want create an app that allows a user to be able to create Bicycle
objects from different components (Frame
, Wheels
, Tires
), so I create a data model like this...
import Foundation
import SwiftData
@Model
class Bicycle {
var name: String = ""
var frame: Frame?
var wheels: Wheels?
var tires: Tires?
init(frame: Frame? = nil, wheels: Wheels? = nil, tires: Tires? = nil) {
self.frame = frame
self.wheels = wheels
self.tires = tires
}
}
@Model
class Frame {
var make: String
var model: String
var bicycles: [Bicycle] = []
init(make: String, model: String) {
self.make = make
self.model = model
}
}
@Model
class Wheels {
var make: String
var diameter: Double
var bicycles: [Bicycle] = []
init(make: String, diameter: Double) {
self.make = make
self.diameter = diameter
}
}
@Model
class Tires {
var make: String
var diameter: Double
var bicycles: [Bicycle] = []
init(make: String, diameter: Double) {
self.make = make
self.diameter = diameter
}
}
Now, I want the user to see a list of all of the Bicycle that they have created, and be able to push a button to add a new Bicycle, which will take them to another view where they can select the components for their new Bicycle. So, we start with this view...
ContentView.swift
import SwiftData
import SwiftUI
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@Environment(\.dismiss) var dismiss
@Query(sort:[SortDescriptor(\Bicycle.name)]) var bicycles: [Bicycle]
@State private var addingBicycle = false
var body: some View {
NavigationStack() {
List {
ForEach(bicycles) { bicycle in
Text(bicycle.name)
}
.onDelete(perform: deleteBicycles)
}
.navigationTitle("Bicycles")
.navigationDestination(isPresented: $addingBicycle) {
AddBicycleView()
}
.toolbar {
Button("Add Bicycle", systemImage: "plus", action: addBicycle)
}
}
}
func addBicycle() {
addingBicycle.toggle()
}
func deleteBicycles(_ indexSet: IndexSet) {
for index in indexSet {
let deletableBicycle = bicycles[index]
modelContext.delete(deletableBicycle)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And when the add button is tapped, we are taken to this view. So far so good.
import SwiftData
import SwiftUI
struct AddBicycleView: View {
@Environment(\.modelContext) var modelContext
@Environment(\.dismiss) var dismiss
//Queries to get a list of each type of component, to be used in the Pickers
@Query(sort: [SortDescriptor(\Frame.make, comparator: .localizedStandard), SortDescriptor(\Frame.model, comparator: .localizedStandard)]) var frames: [Frame]
@Query(sort: [SortDescriptor(\Wheels.make, comparator: .localizedStandard), SortDescriptor(\Wheels.diameter)]) var wheels: [Wheels]
@Query(sort: [SortDescriptor(\Tires.make, comparator: .localizedStandard), SortDescriptor(\Tires.diameter)]) var tires: [Tires]
//Variables to store the values entered by the user
@State private var enteredName = ""
@State private var selectedFrame: Frame?
@State private var selectedWheels: Wheels?
@State private var selectedTires: Tires?
var body: some View {
Form {
TextField("Enter a name", text: $enteredName)
Picker("Frame", selection: $selectedFrame) {
ForEach(frames) { frame in
Text("\(frame.make) - \(frame.model)")
}
}
Picker("Wheels", selection: $selectedWheels) {
ForEach(wheels) { wheel in
Text("\(wheel.make) - \(wheel.diameter)")
}
}
Picker("Tires", selection: $selectedTires) {
ForEach(tires) { tire in
Text("\(tire.make) - \(tire.diameter)")
}
}
}
.navigationTitle("Add Bicycle")
.toolbar {
Button("Add") {
let newBicycle = Bicycle(name: enteredName, frame: selectedFrame, wheels: selectedWheels, tires: selectedTires)
modelContext.insert(newBicycle)
dismiss()
}
}
}
}
//You can ignore this part if you want to, but this will make preview show some example data.
#Preview {
do {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Bicycle.self, configurations: config)
let exampleFrame = Frame(make: "WorstBrand", model: "A")
let exampleWheels = Wheels(make: "WorstBrand", diameter: 10.0)
let exampleTires = Tires(make: "WorstBrand", diameter: 20.0)
container.mainContext.insert(exampleFrame)
container.mainContext.insert(exampleWheels)
container.mainContext.insert(exampleTires)
return AddBicycleView()
.modelContainer(container)
} catch {
fatalError("Failed to create preview model container.")
}
}
Don't forget to add the modelContainer
to your WindowGroup
if you are recreating this project yourself...
struct BicycleBuilderApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Bicycle.self)
}
}
But here's where the twist comes. What if I want the user to be able to select any of the parts that already exist in my database from the pickers, but also have an additional option "Add New" available in each of the Pickers. When the user selects the "Add New" option, I would want a sheet to be presented, allowing the user to fill in info for, and add a new Frame, Wheels, or Tires object, and have that show up as their selection from the picker. Is that possible?
I have tried this...
Picker("Frame", selection: $selectedFrame) {
ForEach(frames) { frame in
Text("\(frame.make) - \(frame.model)")
}
Text("Add New")
.tag(nil as Frame?)
}
But I get a bunch of warnings in the console so I don't know if this is a valid solution that I should use.
"Picker: the selection "nil" is invalid and does not have an associated tag, this will give undefined results."