0

I'd like to have the name field of a core data object be trimmed before it's saved to the context. My idea was to use the validation function for this. The following implementation works:

@objc
public func validateName(_ value: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
    guard let trimmedValue = (value.pointee as? String)?.trimmingCharacters(in: .whitespaces) else {
        return
    }

    if name != trimmedValue {
        name = trimmedValue
    }
}

Without the if statement which sets the name field only in case of a change, core data throws an exception because of a cycle.

Now I have to questions:

  1. Is the validation function the best way of doing this?
  2. The above code is quite verbose if you have multiple objects with fields that have to be trimmed. I've tried to create a function trimField with an inout variable value and moved the code for trimming, checking for a change and setting the inout variable. But when I call it from the validation function, an exception is thrown because of a cycle. So it seems that a function with inout parameter does set the field value to "dirty" even if it's not set explicitly. Are there any other options?
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
G. Marc
  • 4,987
  • 4
  • 32
  • 49
  • See https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/ObjectValidation.html#//apple_ref/doc/uid/TP40001075-CH20-SW4 It might be better to trim in `willSave`. – Larme Jan 10 '22 at 10:46

1 Answers1

1

Validation does this because, as you've found, any time you set a value, the validation function gets called. To change a value while validating, you need to bypass validation using setPrimitiveValue(_:forKey:) instead of setting name as usual. So, setPrimitiveValue(trimmed, forKey: "name") or similar.

Validation is probably not the best time to do this. You'll need to refresh any views displaying the object data, for example. There are some other options.

One is via didChangeValue, something like this:

public override func didChangeValue(forKey key: String) {
    if key == "name", let name = self.name {
        let trimmedValue = nametrimmingCharacters(in: .whitespaces)
        setPrimitiveValue(trimmedValue, forKey: "name")
    }
    super.willChangeValue(forKey: key)
}

You could also use willSave, and use the object's changedValues() to see if the name changed.

The right way to do it-- as far as Core Data is concerned, anyway-- is to override the setter for name. Unfortunately you need to turn off automatic code generation for Core Data to do that. I described the process in this answer: Using property observers on NSManaged vars

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • Thanks for this, it works as expected. There's just one little mistake in the example: please change super.willChangeValue to super.didChangeValue. – G. Marc Jan 11 '22 at 06:21
  • Suggestion: Using `#keypath(Entity.name)` instead of hard coding `"name"` string as the variable name? – Larme Jan 11 '22 at 08:17