The prerequisite to my question is the following: I have an @NSManaged
object and I wish to observe and manipulate it. However, I do not want to receive all updates. To illustrate why – suppose it has properties selectedIndex
and currentArray
. Those are @NSManaged
and they are all I need to store and recreate currentItem
.
There are two ways I could do this. The wrong one would be to use currentArray[currentIndex]
in my views. Wrong because should the update get triggered when I set index and before I change array – I can easily get out of bounds exception. Same if I change array first.
The second approach would be to restrict direct access to these properties from other classes and instead provide an interface to interact with it which would also take care of the internal logic. For example goToNextItem()
, changeArrayTo()
, etc.
And along this route the easiest approach would be to mark selectedIndex
and currentArray
as private. And, one would assume, this would also prevent other views from receiving updates when they change.
The problem – it doesn't.
Here is a super simple example:
final class DataModel:ObservableObject {
@Published private var internalCounter = 0
var externalCounter = 0
func incInternalCount() {
externalCounter += 1
internalCounter += 1
}
}
struct Playground: View {
@ObservedObject var dataModel = DataModel()
var body: some View {
Text("Refresh count \(dataModel.externalCounter)")
Button {
dataModel.incInternalCount()
} label: {
Text("Increment")
}
}
}
And this is what I get:
As you can see, even though I am not able to access internalCounter
from my Playground
AND that changes to externalCounter
are not published – I still receive updates whenever internalCounter
changes.
This doesn't necessarily create an issue for me – as long as my publicly facing interface has a solid logic, nothing will crash. However, I kind of don't like this idea of having body
recalculations in response to properties changes, when I explicitly made those properties unavailable for access :)
Hence my question – is there a way to make @NSManaged
property not @Published
? :)
UPDATE
I see that my simplified example is causing some confusion, so let me outline a slightly more elaborate one:
final class DataModel:ObservableObject {
@Published private var itemIndex:Int = 0
@Published private var items:[String] = ["hello", "world"]
@Published var currentItem:String
var operationsCount = 0
init() {
currentItem = ""
currentItem = items[itemIndex]
}
func goToNextItem() {
operationsCount += 1
itemIndex += 1
currentItem = items[itemIndex] //Comment this to see problem
}
func goToPreviousItem() {
operationsCount += 1
itemIndex -= 1
currentItem = items[itemIndex] //AND this
}
func nextItemAvailable() -> Bool {
return itemIndex < items.count - 1
}
func previousItemAvailable() -> Bool {
return itemIndex > 0
}
}
struct Playground: View {
@ObservedObject var dataModel = DataModel()
var body: some View {
Text("Item: \(dataModel.currentItem) \(dataModel.operationsCount)")
HStack {
Button {
dataModel.goToPreviousItem()
} label: {
Text("Previous")
}
.disabled(!dataModel.previousItemAvailable())
Button {
dataModel.goToNextItem()
} label: {
Text("Next")
}
.disabled(!dataModel.nextItemAvailable())
}
}
}
items
and itemIndex
is what I need to persist my data. However to display it, I need currentItem
and next/previous functionality.
If you comment out the lines indicated - updates will happen. And although they are not a problem given that nothing will crash – would be nice to avoid them for the benefits of performance :)
The data model object should only store the array of data, not any info about selection, that should be stored in the
View
struct hierarchy using@State
and then@Binding
for write access, make funcs for logic, computed properties to transform data. If you think about it, what if you wanted to allow selection of the same data in 2 screens? That wouldn't be possible if the selection is within the data model.That's why the first step in designing a
View
is ask yourself what data does it need to do its job? In your case it doesn't need the whole model object, only write access to the property so it should be:If you want a side effect when setting the counter, in a model struct you can use
didSet
or in an ObservableObject configure a Combine pipeline in init, starting with$counter
and ending with.assign
. Usually objects are only used when you need reference type semantics, ie async (Not async/await though, because we have.task
's internal convenience object for that).