I am implementing a copy-on-write type and using it in SwiftUI. I was hoping to have a single instance so that expensive copy is minimized. In the below example, the expensive copy and deinit occur every time a tap occurs. Is this a limitation of SwiftUI or is there an error or better way to implement my COW type? Thank you for any insights or hints you might have!
import SwiftUI
struct CowType {
init() {
self.detail = Detail(value: 0)
}
private var detail: Detail
var value: Int { detail.value }
mutating func setValue(_ value: Int) {
if isKnownUniquelyReferenced(&detail) {
detail.value = value
} else {
print("performing expensive copy")
detail = Detail(value: value)
}
}
private final class Detail {
init(value: Int) {
self.value = value
}
deinit {
print("calling deinit")
}
var value: Int
}
}
struct ContentView: View {
@State var state = CowType()
var body: some View {
Text("Hello, world! \(state.value)")
.padding()
.onTapGesture {
state.setValue(Int.random(in: 1...100))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I want a type that has value semantics but dynamic internal storage. Much like a Swift standard container type. Conceptually, the UI has only one of these objects and the copy operation is expensive so I want to avoid the copy as much as possible. In a unit test if I put the mutation in a loop, no problem, one copy is made and it is mutated in place.
In SwiftUI, it happens for every single drag event. Internally, SwiftUI must be doing something like this:
state.insert(...)
// becomes
var secretCopy = state
secretCopy.insert(....)
My solution seems to work although it requires an extra level of wrapping. (I don't want my CowType to know anything about SwiftUI.)
It is an interesting thought, thank you. If I make CowType a class, SwiftUI doesn't get message saying that the state has changed and doesn't re-render the body.
That is not how CoW is meant to work. The original example is the right way to implement it.
I only quickly scanned the thread and the issue is that State is probably capturing a reference to the cached storage which essentially makes the private reference to Detail no longer unique, which then would cause a copy on every tap.
I'm also afraid State won't help you here. To avoid the expensive copy you'd need either a reference type using StateObject/ObservedObject or β for value semantics β a persistent data structure with inexpensive copies.
The behaviour you're observing has to do with State<CowType> storing a copy of the last rendered CowType value in its value type layout, as you can tell by adding dump(_state) in your code:
.onTapGesture {
let value = Int.random(in: 1...100)
print("changing value from \(state.value) to \(value)")
state.setValue(value)
dump(_state)
}
Output:
changing value from 95 to 39
performing expensive copy
βΏ SwiftUI.State<Example.CowType>
βΏ _value: Example.CowType
βΏ detail: Example.CowType.(unknown context at $101fee6e4).Detail #0
- value: 95
βΏ _location: Optional(SwiftUI.StoredLocation<Example.CowType>)
[skipping 181 lines of _location details]
calling deinit
I believe this is necessary for the way SwiftUI's handling of state and view rendering works (although hard to tell without seeing source code). The _value is probably updated only just before the view's body is evaluated by SwiftUI, as is explained in the documentation for State.update().
If there are parts of your type that are expensive to copy, but those parts aren't the parts that you need to be modifying frequently, consider using a hybrid approach where those parts are stored inline and only the expensive parts are out-of-line.