Post

Replies

Boosts

Views

Activity

SwiftData: How can I tell if a migration went through successfully?
I created 2 different schemas, and made a small change to one of them. I added a property to the model called "version". To see if the migration went through, I setup the migration plan to set version to "1.1.0" in willMigrate. In the didMigrate, I looped through the new version of Tags to check if version was set, and if not, set it. I did this incase the willMigrate didn't do what it was supposed to. The app built and ran successfully, but version was not set in the Tag I created in the app. Here's the migration: enum MigrationPlanV2: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { [DataSchemaV1.self, DataSchemaV2.self] } static let stage1 = MigrationStage.custom( fromVersion: DataSchemaV1.self, toVersion: DataSchemaV2.self, willMigrate: { context in let oldTags = try? context.fetch(FetchDescriptor<DataSchemaV1.Tag>()) for old in oldTags ?? [] { let new = Tag(name: old.name, version: "Version 1.1.0") context.delete(old) context.insert(new) } try? context.save() }, didMigrate: { context in let newTags = try? context.fetch(FetchDescriptor<DataSchemaV2.Tag>()) for tag in newTags ?? []{ if tag.version == nil { tag.version = "1.1.0" } } } ) static var stages: [MigrationStage] { [stage1] } } Here's the model container: var sharedModelContainer: ModelContainer = { let schema = Schema(versionedSchema: DataSchemaV2.self) let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) do { return try ModelContainer( for: schema, migrationPlan: MigrationPlanV2.self, configurations: [modelConfiguration]) } catch { fatalError("Could not create ModelContainer: \(error)") } }() I ran a similar test prior to this, and got the same result. It's like the code in my willMigrate isn't running. I also had print statements in there that I never saw printed to the console. I tried to check the CloudKit console for any information, but I'm having issues with that as well (separate post). Anyways, how can I confirm that my migration was successful here?
0
0
70
14h
SwiftData: Migrate from un-versioned to versioned schema
I've realized that I need to use migration plans, but those required versioned schemas. I think I've updated mine, but I wanted to confirm if this was the proper procedure. To start, none of my models were versioned. I've since wrapped them in a VersionedSchema like this: enum TagV1: VersionedSchema { static var versionIdentifier: Schema.Version = .init(1, 0, 0) static var models: [any PersistentModel.Type] { [Tag.self] } @Model final class Tag { var id = UUID() var name: String = "" // Relationships var transactions: [Transaction]? = nil init(name: String) { self.name = name } } } I also created a type alias to point to this. typealias Tag = TagV1.Tag This is what my container looks like in my app file. var sharedModelContainer: ModelContainer = { let schema = Schema([ Tag.self ]) let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) do { return try ModelContainer(for: schema, configurations: [modelConfiguration]) } catch { fatalError("Could not create ModelContainer: \(error)") } }() The application builds and run successfully. Does this mean that my models are successfully versioned now? I'm trying to avoid an error I came across in earlier testing. That occurred because none of my models were versioned and I tried to setup a migration plan Cannot use staged migration with an unknown coordinator model version.
0
0
73
1d
SwiftData: Default value for added property
Let's say I have a model like this: @Model final class DataModel { var firstProperty: String = "" } Later on I create a new property as such: @Model final class DataModel { enum DataEnum { case dataCase } var firstProperty: String = "" var secondProperty: DataEnum? = .dataCase } My expectation is for the data that is already stored, the secondProperty would be added with a default value of .dataCase. However, it's being set to nil instead. I could have sworn it would set to the default value given to it. Has that changed, or has it always been this way? Does this require a migration plan?
0
0
91
2d
SwiftData: Failed to decode a composite attribute
I changed an enum value from this: enum Kind: String, Codable, CaseIterable { case credit } to this: enum Kind: String, Codable, CaseIterable { case credit = "Credit" } And now it fails to load the data. This is inside of a SwiftData model. I get why the error is occurring, but is there a way to resolve this issue without having to revert back or delete the data? Error: dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "Cannot initialize Kind from invalid String value credit", underlyingError: nil))
3
0
189
1w
MacOS SwiftUI: Back button in NavigationSplitView detail view
I have a Split View with the sidebar, content, and detail. Everything is working, but when I select on a NavigationLink in my detail view, the back button is seen next to the title above the content view. I want that back button to be displayed in the top bar on the left side of the detail view. I was expecting it to do this automatically, but I must be missing something. This is where I want it to appear. This is where it appears. I made a simplified version of my code, because it would be too much to post but this has the same behavior. struct TestView: View { enum SidebarSelections { case cycles } @State private var sidebarSelection: SidebarSelections = .cycles var body: some View { NavigationSplitView(sidebar: { List(selection: $sidebarSelection, content: { Label("Cycles", systemImage: "calendar") .tag(SidebarSelections.cycles) }) }, content: { switch sidebarSelection { case .cycles: NavigationStack { List { // Displayed in Content NavigationLink("Cycle link", destination: { // Displayed in the Detail NavigationStack { List { NavigationLink("Detail Link", destination: { Text("More details") }) } } }) } } } }, detail: { ContentUnavailableView("Nothing to see here", systemImage: "cloud") }) } } Is what I want to do possible? Here is a Stack Overflow post that had it working at one point.
2
0
173
2w
MacOS SwiftUI: Is it impossible to clear a Table selection?
I figured it would be as simple as changing the selection variable back to nil, but that seemingly has no effect. The Table row stays selected, and if I press on it again, it won't trigger my .navigationDestination(). How can I resolve this? I found another post asking the same thing, with no resolution. The only way I can clear it is by clicking with my mouse somewhere else. @State private var selectedID: UUID? = nil Table(tableData, selection: $selectedID, sortOrder: $sortOrder)... .navigationDestination(item: $selectedID, destination: { id in if let cycle = cycles.first(where: {$0.id == id}) { CycleDetailView(cycle: cycle) .onDisappear(perform: { selectedID = nil }) } })
2
0
139
2w
SwiftUI Charts not working in Xcode 16 Beta 2
Is there a way to workaround this issue? Can I revert back to Beta 1? Failed to build module 'Charts'; this SDK is not supported by the compiler (the SDK is built with 'Apple Swift version 6.0 effective-5.10 (swiftlang-6.0.0.7.41 clang-1600.0.24.1)', while this compiler is 'Apple Swift version 6.0 effective-5.10 (swiftlang-6.0.0.9.11 clang-1600.0.26.2)'). Please select a toolchain which matches the SDK.
2
1
459
Sep ’24
How to close the keyboard using SwiftUI
I've been having trouble with finding a good way to allow the user to close the keyboard. Naturally, I'd like to use the keyboard toolbar but it's so inconsistent, it's impossible. Sometimes it shows up, other times it doesn't. At the moment, it's not appearing at all no matter where I put it. On the NavigationStack, on the List inside of it, on the TextField. It just doesn't appear. I added a TapGesture to my view to set my FocusState that I'm using for the fields back to nil, however, this stops the Picker I have from working. I tried using SimultaneousGesture with TapGesture, and that didn't work. For example: .simultaneousGesture( TapGesture() .onEnded() { if field != nil { field = nil } } ) With the current setup, each TextField switches to the next TextField in the onSubmit. The last one doesn't, which will close the keyboard, but I want to give people the option to close the keyboard before that. Does anyone have a good way to close the keyboard?
0
0
191
Sep ’24
Question about saving Data with SwiftData and CloudKit
My app is using SwiftData with CloudKit integration. Everything at the moment is working fine. I have a struct that saves Data as an optional. This is Data related to an Image. It saves and loads as expected. When I disconnect my phone from wifi or my phone network, the image still loads. I'm assuming that means the Data is being stored locally on the phone as well. Is there a way to display what's stored locally to the user inside the application? Edit: I've realized that CloudKit is saying the data is too large, but the images are still being saved. Does that mean they're only locally being saved?
1
0
276
Aug ’24
SwiftData: Unknown Relationship Type - nil
I'm getting an error: Unknown Relationship Type - nil when using SwiftData and CloudKit. I've searched for this on Google, but seems like one has experienced it before. This is on iOS 18. I have a Transaction model: @Model final class Transaction { var timestamp: Date = Date() var note: String = "" var cost: Cost? = nil var receipt: Receipt? = nil //Relationships var merchant: Merchant? = nil var category: TransactionCategory? = nil var tags: [Tag]? = nil init(timestamp: Date) { self.timestamp = timestamp } } This has a relationship with TransactionCategory. @Model final class TransactionCategory { var id = UUID() var name: String = "" //Relationship var transactions: [Transaction]? = [] init() { } init(name: String) { self.name = name } } I've tried using @Relationship here in TransactionCategory, but it didn't make a difference. There is Picker that allows you to select a Category. In this case I created a random one, which was inserted into the context and saved. This is successful by the way. When you press a done button, a Transaction is created and the Category modified like this: newTransaction.category = category. It fails as this point with that error. I also tried to create the Transaction, insert into the context, and then update the Category but it then failed at the context insertion, prior to updating the Category. As you can see, I have another model called Merchant. When you press the done button, if you've typed in a Merchant name, it will create it, insert it into the context and then update the transaction as such: newTransaction.merchant = getMerchant() private func getMerchant() -> Merchant? { //Create merchant if applicable if merchant.name.isEmpty == false { if let first = merchants.first(where: {$0.name == merchant.name.trim()}) { // Set to one that already exist return first } else { // Insert into context and insert into transction context.insert(merchant) return merchant } } return nil } This code works fine and Merchant has the same relationship with Transaction as Category does. Does anyone have any idea what could be causing this problem?
1
0
340
Jul ’24
SwiftUI and MapKit: Map camera position not updating when refreshing location
Map(initialPosition: .camera(mapCamera)) { Marker("Here", coordinate: location) } .frame(height: 300) .clipShape(RoundedRectangle(cornerSize: CGSize(width: 10, height: 10))) .onMapCameraChange(frequency: .continuous) { cameraContext in locationManager.location = cameraContext.camera.centerCoordinate } .onReceive(locationManager.$location, perform: { location in if let location { mapCamera.centerCoordinate = location } }) class LocationDataManager: NSObject, CLLocationManagerDelegate, ObservableObject { enum LoadingState { case loading case notLoading case finished } static let shared = LocationDataManager() private let locationManager = CLLocationManager() @Published var location: CLLocationCoordinate2D? = nil @Published var loading: LoadingState = .notLoading override init() { super.init() locationManager.delegate = self } func resetLocation() { loading = .notLoading location = nil } func getLocation() { locationManager.requestLocation() loading = .loading } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { location = locations.first?.coordinate if location != nil { loading = .finished } } func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) { print("Failed to retrieve location: \(error.localizedDescription)") loading = .notLoading } } So the when the LocationButton is selected, the location is found and the marker is set correctly. You can also move the camera around to adjust the marker position, which works correctly. However, if you press the LocationButton again, it updates the marker position but it won't move the MapCamera to the new location. I can see the marker move. mapCamera.centerCoordinate = location should be doing it, but it's not. Anyone know how to fix this?
1
0
718
May ’24
SwiftUI: Keyboard toolbar item not working in iOS 17.4
It seems like this may have been an issue for a while based on what I've seen, but I have added a toolbar item to a textfield keyboard and it doesn't show. The only way I can get it to show is by opening the keyboard, typing something, closing the keyboard, and then reopening it. Anyone have a workaround for this? It's like Apple purposely wants to make it difficult to close the keyboard. TextField("Something here...", text: $text, axis: .vertical) .multilineTextAlignment(.leading) .toolbar { ToolbarItemGroup(placement: .keyboard, content: { Button("Close") { } }) }
0
4
555
May ’24
EditMode while using deleteDisabled works in one view but not the other
Here is the view in which it works struct MileageHistoryView: View { let vehicle: Vehicle init(for vehicle: Vehicle) { self.vehicle = vehicle } @Environment(\.modelContext) private var context @Environment(\.editMode) private var editMode var sorted: [Mileage] { guard let history = vehicle.mileageHistory else { return [] } return history.sorted(by: { $0.timestamp > $1.timestamp }) } var body: some View { List { ForEach(sorted) { mileage in MileageListItem(mileage, editing: Binding(get: {editMode?.wrappedValue.isEditing ?? false}, set: {_ in })) } .onDelete(perform: deleteMileage) .deleteDisabled(editMode?.wrappedValue.isEditing ?? false ? false : true) } .id(editMode?.wrappedValue.isEditing) .navigationTitle("Mileage History") .scrollContentBackground(.hidden) .toolbar { ToolbarItem(placement: .topBarTrailing, content: { EditButton() }) } } } Here is the other view where it doesn't work. In this view, it seems like when the EditButton is pressed, no change is happening with the editMode so deleteDisabled() is always set to true. struct VehiclesView: View { @Environment(\.modelContext) private var context @Environment(\.editMode) private var editMode // Local @Query private var vehicles: [Vehicle] @State private var addVehicle = false @AppStorage("vehicle-edit-alert") private var showEditAlert = true @State private var editAlert = false @State private var editShown = false var body: some View { NavigationStack { List { ForEach(vehicles) { vehicle in NavigationLink(destination: VehicleView(vehicle), label: { VehicleListItem(vehicle) }) } .onDelete(perform: deleteVehicle) .deleteDisabled(self.editMode?.wrappedValue.isEditing ?? false ? false : true) } .id(self.editMode?.wrappedValue.isEditing) .scrollContentBackground(.hidden) .navigationTitle("Vehicles") .toolbar { ToolbarItem(placement: .topBarLeading, content: { if showEditAlert && !editShown { Button("Edit") { editAlert = true } } else { EditButton() } }) ToolbarItem(placement: .topBarTrailing, content: { Button(action: { addVehicle.toggle() }, label: { Image(systemName: "plus") }) .accessibilityHint("Opens the view to add a Vehicle") }) } .fullScreenCover(isPresented: $addVehicle, content: { VehicleEditor() }) } .scrollIndicators(.hidden) } } When EditButton() is used in the second view the list item is grayed out, but the buttons to delete aren't there. Does anybody know why this is happening?
0
0
301
Apr ’24
What's the correct way to delete a SwiftData model that is in a many to many relationship?
The deletion is working, but it does not refresh the view. This is similar to a question I asked previously but I started a new test project to try and work this out. @Model class Transaction { var timestamp: Date var note: String @Relationship(deleteRule: .cascade) var items: [Item]? init(timestamp: Date, note: String, items: [Item]? = nil) { self.timestamp = timestamp self.note = note self.items = items } func getModifierCount() -> Int { guard let items = items else { return 0 } return items.reduce(0, {result, item in result + (item.modifiers?.count ?? 0) }) } } @Model class Item { var timestamp: Date var note: String @Relationship(deleteRule: .nullify) var transaction: Transaction? @Relationship(deleteRule: .noAction) var modifiers: [Modifier]? init(timestamp: Date, note: String, transaction: Transaction? = nil, modifiers: [Modifier]? = nil) { self.timestamp = timestamp self.note = note self.transaction = transaction self.modifiers = modifiers } } @Model class Modifier { var timestamp: Date var value: Double @Relationship(deleteRule: .nullify) var items: [Item]? init(timestamp: Date, value: Double, items: [Item]? = nil) { self.timestamp = timestamp self.value = value self.items = items } } struct ContentView: View { @Environment(\.modelContext) private var context @Query private var items: [Item] @Query private var transactions: [Transaction] @Query private var modifiers: [Modifier] @State private var addItem = false @State private var addTransaction = false var body: some View { NavigationStack { List { Section(content: { ForEach(items) { item in LabeledText(label: item.timestamp.formatAsString(), value: .int(item.modifiers?.count ?? -1)) } .onDelete(perform: { indexSet in withAnimation { for index in indexSet { context.delete(items[index]) } } }) }, header: { LabeledView(label: "Items", view: { Button("", systemImage: "plus", action: {}) }) }) Section(content: { ForEach(modifiers) { modifier in LabeledText(label: modifier.timestamp.formatAsString(), value: .currency(modifier.value)) } .onDelete(perform: { indexSet in indexSet.forEach { index in context.delete(modifiers[index]) } }) }, header: { LabeledView(label: "Modifiers", view: { Button("", systemImage: "plus", action: {}) }) }) Section(content: { ForEach(transactions) { transaction in LabeledText(label: transaction.note, value: .int(transaction.getModifierCount())) } .onDelete(perform: { indexSet in withAnimation { for index in indexSet { context.delete(transactions[index]) } } }) }, header: { LabeledView(label: "Transactions", view: { Button("", systemImage: "plus", action: {addTransaction.toggle()}) }) }) } .navigationTitle("Testing") .sheet(isPresented: $addTransaction, content: { TransactionEditor() }) } } } } Here's the scenario. Create a transaction with 1 item. That item will contain 1 modifier. ContentView will display Items, Modifiers, and Transactions. For Item, it will display the date and how many modifiers it has. Modifier will display the date and its value. Transactions will display a date and how many modifiers are contained inside of its items. When I delete a modifier, in this case the only one that exist, I should see the count update to 0 for both the Item and the Transaction. This is not happening unless I close the application and reopen it. If I do that, it's updated to 0. I tried to add an ID variable to the view and change it to force a refresh, but it's not updating. This issue also seems to be only with this many to many relationship. Previously, I only had the Transaction and Item models. Deleting an Item would correctly update Transaction, but that was a one to many relationship. I would like for Modifier to have a many to many relationship with Items, so they can be reused. Why is deleting a modifier not updating the items correctly? Why is this not refreshing the view? How can I resolve this issue?
2
0
528
Apr ’24