Easily updating a large struct?

Problem: We have a struct that echoes what our web-based product will send down as JSON. Unfortunately, it's a large list of separate boolean properties. The struct in Swift is effectively:


struct SomeStruct : Codable {
    var flagOne : Bool
    var flagTwo : Bool
    ...
    var flagN : Bool
}


For testing purposes, I'd like to create a instance, but don't want to expose a massive init or have to have calling code manually assign properties. What I would like to do is to create an enum as follows and then have some simple init() on the struct to basically allow for say "I want an instance with only the following flags set" where "following flags" is a set/array of the enum:


enum Flag : String {
    case flagOne, flagTwo, ... flagN // The rawValue now matches the struct properties
}

init(onlyFlags aFlagsArray: [Flag]) {
    // Not sure what to do here.
}


I haven't found any method such that given a string (e.g. "flagOne"), that I could some how call a "selector" with that name against an instance of the struct. If such a thing is possible, I could create a few variations of init to do stuff like this (after making the enum CaseIteratable):


init(allFlagsEnabled anEnabledFlag: Bool) {
    for theFlag in Flag.allCases {
        // theFlag.rawValue is the name of the property on the struct
        // But how to call the setter of that property?
    }
}


I briefly looked at Mirrors, but that seems to be for read-only access. And, they are effectively value types. So while you can update a value on a mirror, that won't update the original item (struct).


I belive this may be possible if this isn't a struct but an NSObject and then attempt something with selectors. But, I'd rather just manually set up the struct's properties as needed at that point.

Accepted Reply

I think key paths are your best bet here.

struct SomeStruct {
    var flagOne: Bool = false
    var flagTwo: Bool = false
}

extension SomeStruct {
    init(onlyFlags keyPaths: [WritableKeyPath<SomeStruct, Bool>]) {
        var s = SomeStruct()
        for kp in keyPaths {
            s[keyPath: kp] = true
        }
        self = s
    }
}

let s1 = SomeStruct(onlyFlags: [\.flagOne])
print(s1)   // -> SomeStruct(flagOne: true, flagTwo: false)
let s2 = SomeStruct(onlyFlags: [\.flagTwo])
print(s2)   // -> SomeStruct(flagOne: false, flagTwo: true)

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

I think key paths are your best bet here.

struct SomeStruct {
    var flagOne: Bool = false
    var flagTwo: Bool = false
}

extension SomeStruct {
    init(onlyFlags keyPaths: [WritableKeyPath<SomeStruct, Bool>]) {
        var s = SomeStruct()
        for kp in keyPaths {
            s[keyPath: kp] = true
        }
        self = s
    }
}

let s1 = SomeStruct(onlyFlags: [\.flagOne])
print(s1)   // -> SomeStruct(flagOne: true, flagTwo: false)
let s2 = SomeStruct(onlyFlags: [\.flagTwo])
print(s2)   // -> SomeStruct(flagOne: false, flagTwo: true)

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Very cool! Thank you, Quinn.

Whow !


But there is something I do not understand.


Why is extension needed ?


This would not work (at least in simulator), even though I thought it should be equivalent.


Sorry, cannot format code, the editor deletes what is inside brackets !


struct SomeStruct {

var flagOne: Bool = false

var flagTwo: Bool = false


init(onlyFlags keyPaths: [WritableKeyPath<SomeStruct, Bool>]) {

var s = SomeStruct() // Error here

for kp in keyPaths {

s[keyPath: kp] = true

}

self = s

}

}


I get error

Missing argument for parameter 'onlyFlags' in call

Why is extension needed ?

Yeah, that’s quite subtle. If you define a structure without an initialiser, the language synthesises a default one. You can then define another initialiser in an extension and have that use the default initialiser.

In constrast, if you define your initialiser in the structure itself, the language does not synthesise one and thus there’s nothing for your initialiser to call.

Tricky.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Very subtle indeed. Thanks Quinn.