Reputation: 3169
I've been becoming more familiar with the "copy on write" behavior of Swift structs. I think it's a really nice way to get around having to manage references for structs, but it's a bit cumbersome when dealing with deeply nested structures.
If you want to update a deeply nested value, you need a direct path to that value so you can modify it on a single line:
myStruct.nestedArray[index].nestedValue = 1
The compiler will copy myStruct.nestedArray[index]
and set nestedValue
to 1
on that new value. It will then copy myStruct.nestedArray
and set the new value at index
. It will then copy myStruct
and replace the previous value with a new one that has all of the above changes.
This works just fine and it's pretty cool that you can do this with a single line of code without having to worry about anything that was referencing myStruct
and its children before. However, if there is more complicated logic involved in resolving the path to the value, the logic becomes much more verbose:
struct MyStruct {
var nestedEnum: MyEnum
}
enum MyEnum {
case one([NestedStruct])
case two([NestedStruct])
}
struct NestedStruct {
var id: Int
var nestedValue: Int
}
var myStruct = MyStruct(nestedEnum: .one([NestedStruct(id: 0, nestedValue: 0)]))
if case .one(var nestedArray) = myStruct.nestedEnum {
if let index = nestedArray.firstIndex(where: { $0.id == 0 }) {
nestedArray[index].nestedValue = 1
myStruct.nestedEnum = .one(nestedArray)
}
}
Ideally you'd be able to do something like this:
if case .one(var nestedArray) = myStruct.nestedEnum {
if var nestedStruct = nestedArray.first(where: { $0.id == 0 }) {
nestedStruct.nestedValue = 1
}
}
But as soon as nestedStruct.nestedValue
is set, the new value of nestedStruct
is swallowed.
What would be nice is if Swift had a way to use inout
semantics outside of functions, so I could take a "reference" to nestedArray
and then nestedStruct
within it and set the inner nestedValue
, causing the copy to propagate back up to myStruct
the same way as it would if I'd been able to do it in one line.
Does anyone have any nice ways to deal with deeply nested structs that might be able to help me out here? Or am I just going to have to put up with the pattern from my second example above?
Upvotes: 5
Views: 1047
Reputation: 3169
The solution I ended up arriving at was pretty SwiftUI specific, but it may be adaptable to other frameworks.
Basically, instead of having a single top-level method responsible for deeply updating the struct, I arranged my SwiftUI hierarchy to mirror the structure of my struct, and passed Binding
s down that just manage one node of the hierarchy.
For example, given my struct defined above:
struct MyStruct {
var nestedEnum: MyEnum
}
enum MyEnum {
case one([NestedStruct])
case two([NestedStruct])
}
struct NestedStruct {
var id: Int
var nestedValue: Int
}
I could do this:
struct MyStructView: View {
@Binding var myStruct: MyStruct
var body: some View {
switch myStruct.nestedEnum {
case .one: OneView(array: oneBinding)
case .two: TwoView(array: twoBinding)
}
}
var oneBinding: Binding<[NestedStruct]> {
.init(
get: {
if case .one(array) = myStruct.nestedEnum {
return array
}
fatalError()
},
set: { myStruct.nestedEnum = .one($0) }
)
}
var twoBinding: Binding<[NestedStruct]> { /* basically the same */ }
}
struct OneView: View {
@Binding var array: [NestedStruct]
var body: some View {
ForEach(0..<array.count, id: \.self) {
NestedStructView(nestedStruct: getBinding($0))
}
}
func getBinding(_ index: Int) -> Binding<NestedStruct> {
.init(get: { array[index] }, set: { array[index] = $0 })
}
}
struct NestedStructView: View {
@Binding var nestedStruct: NestedStruct
var body: some View {
NumericInput(title: "ID: \(nestedStruct.id)", value: valueBinding)
}
var valueBinding: Binding<Int> {
.init(get: { nestedStruct.value }, set: { nestedStruct.value = $0 })
}
}
The only annoying bit is that it can be a bit verbose to construct a Binding
manually. I wish SwiftUI had some syntax for getting nested Binding
s from a Binding
containing an array or struct.
Upvotes: 1