UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Core Data - Changing data in detail

Forums > SwiftUI

What I'm looking for is a sort of best practice at this point. My template is a Core Data with a bunch of Entries (Main (formerly Master) with a list), and then click on one to edit the details. What's the best way to send the "entry" to the detail view, allow editing of the attributes of the "entity", then save when finished.

What I'm currently doing is passing the "entry" to the detail

@ObservedObject var entry: Entry

Then to change an attribute called active or name, I just use

Toggle(isOn: self.$entry.active) {
                                Text("Active")
                            }

TextField("Enter your name", text: $entry.name)

I use save on .onDisappear()

The other "philosphy" on doing this would be to create @State variables for every item in the CoreData (@State name: String, @State active: Bool), and using .onAppear set them from the CoreData being passed in, and .onDisappear set the CoreData back from the @State variables and save.

So is there any drawback to using $entry. for updating these items directly?

2      

hi,

i think either of these approaches works, but the current project i'm doing (more as a learning experience, but also i've found it useful) uses the second structure, because of one particular reason. not only can i edit an existing object, but i use the same view to create a new object (in CoreData). the view functions as both an "addNew" and "editExisting" view -- which it does depends on how it's called (something non-nil or with nil).

that causes a problem in setting up @ObservedObject var entry: Entry -- i can't use it without an object to observe. so i offload all data to @State properties at the start in .onAppear(), with defaults set up for a new item, and then put those all back together on a "save," creating a new object on the way out if necessary.

feel free to take a look at the Add/Modify Views in my ShoppingList project.

hope that helps,

DMG

  • added as an afterthought: if you edit a real live object with the first method, it's a little more difficult to do a "Cancel" or "Reset" operation when editing.

2      

so i offload all data to @State properties at the start in .onAppear(), with defaults set up for a new item

You should be able to set up your @State properties in the initializer?

@ObservedObject var entry: Entry

@State private var someString: String
@State private var someInt: Int

init(entry: Entry) {
    self.entry = entry
    self._someString = State<String>(initialValue: entry.someOtherString)
    self._someInt = State<Int>(initialValue: entry.someOtherInt)
}

2      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

hi,

i forget how long ago i wrote my response (my screen shows only Aug '20 as the date of the my post), and i have changed a lot of code since then.

short answer: yes.

i have used both of these approaches

  • supply an init() as you do, pass the object, and then pull out all the data into separate variables (in which case there is no need to mark your entry as @ObservedObject, because the individual @State variables handle the view updating function for you). i just don't like the underscore syntax to do all of this (!)

  • offload the values of the Core Data object to a custom struct either in a custom init() as you do or in .onAppear() -- you can avoid the underscore syntax in .onAppear() -- and use that struct as the single @State variable for the View. this usually means an initializer for the struct, perhaps something like this:

struct EditablePlayerData {
    var firstName = ""
    var lastName = ""
    var emailAddress = ""
  // other offloads as needed

  init(from player: PlayerCD) { // a Player object from Core Data
    firstName = player.firstName
    lastName = player.lastName
    emailAddress = player.emailAddress
    // 
    }
}

and then a complementary extension on the Core Data class like this so that you can reload the struct's values to the object before dismissing the View:

extension PlayerCD {
  func updateValues(from editablePlayerData: EditablePlayerData) {
    firstName = editablePlayerData.firstName
    lastName = editablePlayerData.lastName
    emailAddress = editablePlayerData.emailAddress
    // other data
  }
}

of course, all of this is not much more than a syntactic mechanism to keep the View code focused on the view, and not on the mechanics of moving data from here to there and then from there to here. it also puts the off-load and re-load code all together in one place.

you can find some relevant code for this in the Shopping List project i mentioned in the earlier message.

hope that helps,

DMG

3      

I have this exact issue - I am trying to use the same view to create new items or edit existing ones in a cloudkit coredata project.

Up until now I have state variables for all the details of a new item and then these are pulled together and validated before initialising a new struct and coredata record when the user clicks add.

I decided it would be good to let them edit the item afterwards and have tried both approaches here - unpacking the properties of the optional item passed into the view in init or in onappear. In both situations I dont get any visible results.

my state variables are uninitialised like:

    @State private var name: String

    @State private var dueType: DueType
    @State private var repetitions: Int

and the init looks like:

 init(editMode: Bool = false, task: Task? = nil) {
        self.editMode = editMode
            // load from task
        print("loading from edit mode task : \(task?.name ?? "no task")")

        self.task = task
        self._name = State<String>(initialValue: task?.name ?? "")
        self._dueType = State<DueType>(initialValue: task?.dueType ?? .after)

I previously had an if statement in the init to give default values if not editmode but that made it crash as it claimed not all variables were initialised when they were, just in two different logic paths. Any idea why the edit values are not showing up?

2      

hi,

i have some curiosity in looking at this problem, since i use Core Data and dual-purpose add/modify views in some apps (it's been awhile since my last comment!), but i think more code is required.

for example, what happened to repetitions? is it also initialized by, say, the following?

_repetitions = State<Int>(initialValue: task?.repetitions ?? 0)

failing to do that would account for the "not all variables were initialized" message.

i'd also like to see how the View is instantiated in code ... in a NavigationLink, or using a .sheet?

and when you say

I dont get any visible results

not sure exctly where you don't see them, but it could because of something else not shown above. are the edited values saved in Core Data? do they appear correctly on the next run of the app?

... looking forward to perhaps helping with this,

DMG

2      

Hi DMG,

Thanks so much for helping out.

Sorry I have been a bit light on the info. I was posting those more as an example of the approach. Indeed repetitions is a initialised exactly like that, together with about another 10 variables following the same pattern. Originally the all had defaults in the property declaration but now I moved them to init after checking if the optional task is present.

This view is a .sheet popover called from two different places - either the main list view with an add button or from within the list cell view when an edit button is pressed.

When initialising with a task in edit mode I dont see the task details only the defaults.

Here is where the plot thickens - to troubleshoot I added

print("loading from edit mode task : \(task?.name ?? "no task")")

and it actually prints

loading from edit mode task : At Task loading from edit mode task : no task loading from edit mode task : no task

And because my main view has a timer every second forcing the list view to update that seems to also force this edit screen to refresh every second as seen when my debug window repeats those three lines every second forever. So thats why I say no 'visible' results - I suspect maybe it is changing an instance of the view and just not showing it to the user maybe. I have no idea why its doing the 3 initialisations each second.

My attempt at editting is actually a slight cheat - I thought I could delete the old task and add a new one with all the same values except its unique identifier. So the logic to update coredate (and cloudkit) would be exactly the same as a normal add task, except if editmode is enabled it deletes the old task afterwards.

An alternative I thought about was changing the whole add task model to bind directly to a task object and when a user clicks 'new task' creating a default one that they edit and deleting it if its not completed.

However in the meantime I looked at your repo and thought I might try having an intermediary viewmodel struct that is initialised either as a default values task or to match the task being editted. It seems a bit repetitious to have to do that and not use the existing class from coredata though.

2      


     @Environment(\.managedObjectContext) private var viewContext
    @Environment(\.presentationMode) var presentationMode

    var editMode: Bool
    var task: Task? = nil

    @State private var name: String

    @State private var dueType: DueType
    @State private var repetitions: Int
    @State private var dueDate: Date

    @State private var dueTimePart: TimePart
    @State private var dueTimePartAmount: Int

    @State private var repeats: Bool
    @State private var repeatsForever: Bool
    @State private var repetitionStatus: RepetitionStatus
    @State private var dueEvery: TimePart
    @State private var dueEveryAmount: Int

    @State private var customCompletionDate = false
    @State private var completetionDate: Date

    @State private var showingError = false
    @State private var error: TaskError? = nil
    @State private var startingFrom = Date()

    init(editMode: Bool = false, task: Task? = nil) {
        self.editMode = editMode
            // load from task
        print("loading from edit mode task : \(task?.name ?? "no task")")

        self.task = task
        self._name = State<String>(initialValue: task?.name ?? "")
        self._dueType = State<DueType>(initialValue: task?.dueType ?? .after)

        self._repetitions = State<Int>(initialValue: Int(task?.repetitions ?? 0))
        self._dueDate = State<Date>(initialValue: task?.due ?? Date())
        var repeats = false
        var repeatsForever = false
        if let task = task {
            if task.repetitionStatus != .none {
                repeats = true
            }
            if task.repetitionStatus == .forever {
                repeatsForever = true
            }
        }
        self._repeats = State<Bool>(initialValue: repeats)
        self._repeatsForever = State<Bool>(initialValue: repeatsForever)
        self._repetitionStatus = State<RepetitionStatus>(initialValue: task?.repetitionStatus ?? .none)
        self._dueEvery = State<TimePart>(initialValue: task?.dueEvery ?? .day)
        self._dueEveryAmount = State<Int>(initialValue: Int(task?.dueEveryAmount ?? 1))
        self._completetionDate = State<Date>(wrappedValue: task?.due ?? Date())

        self._dueTimePart = State<TimePart>(initialValue: .day)
        self._dueTimePartAmount = State<Int>(initialValue: 1)

    }

This is the full init. The properties that still have defaults are not stored in the data object for task. Its handy to have everything split as state because the form adjusts based on these options. Tbh its not absolutely critical that I have an edit mode so I dont really want to redesign the entire bindings for it. I also considered a seperate edit view to new view but that seems a bit silly.

2      

Some updates: I changed var task: Task? = nil to be var task: Task? as its initialised in init. No difference. I also printed the value of name after initialisation in onappear and it does say its been updated to the task value ('At Task') so its just not showing it for some reason. Also even splitting out the edit and new into 2 views doesnt fix it - I just cant set the values of @State variables from my task. At least not so its visible.

And there is an annoying obstable to binding directly to the task class - it forced me to have optional values as when you swipe to delete it crashes if not (some stupid thing where the detail view still wants to show the object after it no longer exists) so it wont let you bind to an optional. The only other approach I can think of is dumping all the properties into an intermediary observedobject class and then adding it. Surely there must be a simpler way?!

edit: possibly this sheds some light on it? https://forums.swift.org/t/assignment-to-state-var-in-init-doesnt-do-anything-but-the-compiler-gened-one-works/35235

2      

hi Oli,

i was hoping you would say more about the call site that brings up the sheet you have, but i can guess. i think that's where the problem lies, since the values are not being transmitted properly to your Add/Modify view.

here's what i imagine you're trying to do with your list view:

struct MyListView: View {

// variables to open and transfer a value to the sheet
@State private var selectedItem? = nil
@State private var showAddModifySheet = false

// something that defines the array of items you want to show in the list
// it's probably a @FetchRequest

var body: some View {
  List {
    ForEach(items) { item in
      ItemRowView(item: item)
        .onTapGesture {
          selectedItem = item
          showAddModifySheet = true
        }
      }
    }
  }
  .sheet(isPresented: $showAddModifySheet) {
    AddOrModifyItemView(item: selectedItem))
  }
}

and then, maybe using a NavigationBarItem or some other Button, you have a way to trigger adding a new item:

Button(action: { 
  selectedItem = nil
  showAddModifySheet = true 
}) {
  Text("Add New Item")
}

i am reasonably certain this worked fine in XCode 11.7, but this started to fail (somewhere) in XCode 12 releases. there seem to be issues with XCode 12.2 and/or iOS 14.2 that interfere with properly transferring @State variables to a sheet. i have seen several posts about this such as this one two days ago on the Apple Developers Forum.

i have found a work-around, which i think is a little awkward and one that you're hinting at: don't pass the item directly, but instead pass all its editable data, so to speak, in a single struct that is marked as @Binding in your sheet view. so instead of setting selectedItem above to an item or to nil, use an @State private var selectedItemData = EditableItemData(), where EditableItemData is a struct that encapsulates all the variables you are trying to unload in your sheet view into @State variables, and trigger the sheet with either of these, depending on whether it comes from the Button or from the list:

selectedItemData = EditableItemData(item: item) // convenience init from an item's values
showAddModifySheet = true

or

selectedItemData = EditableItemData(item: nil) // convenience init which gives default values
showAddModifySheet = true

it helps if the EditableItemData struct includes an id of the Item (nil if adding a new Item), so when the sheet "saves" all the data, you know whether you need to create a new Core Data item, or go find the item on which to apply the edits.

again, this may not be the best solution, but your current code is pulling out all the values from a task at some point; it becomes a question of whether to want the sheet view to do all that work after it comes up or have the calling view do the work before the sheet comes up.

(disclaimer: my use of "before and after" in that last phrase is not the best: the sheet is instantiated when the calling view appears; changing the @State variable in the calling view will update the values in the sheet view exactly because of the use of @Binding. that instantiation sequence is, in part, why you see so many messages that you probably don't think should be there.)

wow, that was a mouthful ... sorry for the length and, of course, hope that helps,

DMG

2      

Hi again!

Thanks so much for your help. I took your advice, it didnt work and I tried every single thing I could think of including not using sheets at all. And this didnt work either but did show me the problem.

I was doing something really stupid by accident and passing in the state to show the new task view to the list table cell view so that whenever I tried to edit it also called new task and then called edit on all the other tasks in the list... DOH! So it was doing what I wanted but I could only see the top most sheet. Hence the bizarre printing behaviour in my debugging where it claimed it had loaded the edit task but it wasnt visible.

I think this probably was the main problem and I could have avoided most of the steps on the way. However I am happy to have a taskViewData class or struct (I tried both on the way) to tidy up the view so I think Il stick with your way.

I really appreciate the attention you paid to my problem, its very rare someone will make the effort to help someone else as much as that just to be helpful!

2      

@bcyng  

I spent ages trying to get the right pattern for this. Have settled on creating a temp context (which has the summary view context as its parent context) for any edits in the summary view. then opening the object to be edited in the temp context which is then passed to the detail view to be edited.

the detail view then edits the core data managed object directly and saves the temp context which pushes it to the summary views context and the view picks up the change and updates the display. the summary view can then save the parent managed object context when it itself is saved or straight away on disappear.

Any new objects are created in the summary view in the temp context and then passed to the detail view to edit.

cancelling out of the detail view leaves the changes in the temp context and therefore are not persisted. Core data cleans up the unused temp context automatically when it feels like it.

this keeps the hierarchy nice and clean and the detail views reuseable without having to write a whole bunch of @State variables. Also allows several levels of detail views without any issue. The @State variables get tedious to maintain after a while.

something like this for new items:

func showNewTenantView() -> some View {

        //populate a temp managed object context to manage persisting or cancelling of the new object
        let tempManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        tempManagedObjectContext.parent = managedObjectContext
        let newTenant = Tenant(context: tempManagedObjectContext)
        let editingLease = tempManagedObjectContext.object(with: parentObject.objectID) as! ImportsTenants
        editingLease.addToTenants(newTenant)

        return NavigationView  {
            EditTenantView(tenant: newTenant)
                .environment(\.managedObjectContext, tempManagedObjectContext)
                .environment(\.editMode, Binding(.constant(EditMode.active)))
                .navigationTitle("Add Tenant")
        }
    }

for editing items

 func editableRow(tenant: Tenant) -> some View {

        //populate a temp managed object context to manage persisting or cancelling of the edit of object
        let tempManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        tempManagedObjectContext.parent = managedObjectContext
        let editingTenant = tempManagedObjectContext.object(with: tenant.objectID) as! Tenant

        return HStack {
            NavigationLink(
                destination: EditTenantView(tenant: editingTenant)
                    .environment(\.managedObjectContext, tempManagedObjectContext)
                    .navigationBarTitle("\(editingTenant.firstName ?? "") \(editingTenant.lastName ?? "")")
                    .environment(\.editMode, Binding(.constant(EditMode.active))),
                tag: editingTenant.id,
                selection: self.$tenantSelection)
            {
                TenantRow(tenant: tenant)
            }
            .onDisappear {
                self.tenantSelection = nil
                //can save here if you want or using a save button in the summary view
            }

        }
        .onTapGestureForced(perform: { self.tenantSelection = tenant.id })
    }

4      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.