jchitel
jchitel

Reputation: 3169

Simple way to modify deeply nested struct

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

Answers (1)

jchitel
jchitel

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 Bindings 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 Bindings from a Binding containing an array or struct.

Upvotes: 1

Related Questions