0

I'm currently refactoring some old code to store UserDefaults via a @propertyWrapper (using this generic version).

I do store some data for different layers that share some common properties. In order to implement this, I created a protocol (CommonInfo) to which each struct conforms.

protocol CommonInfo {
    var isEnabled: Bool { get set }
    // ...
}



struct PrefsInfo {
    struct Location: CommonInfo, Codable {
        var isEnabled: Bool
    }

    struct Message: CommonInfo, Codable {
        var isEnabled: Bool
        var message: String
    }

    @Storage(key: "LayerLocation", defaultValue: Location(isEnabled: true))
    static var location: Location

    @Storage(key: "LayerMessage", defaultValue: Message(isEnabled: false, message: "Hello There!"))
    static var message: Message
}

This work great, I can read and write to UserPreferences this way :

PrefsInfo.location.isEnabled = false

But in order to make the code a bit more generic with more layer types, I thought I could add an enum:

// Lists each type of layers
enum InfoType: String, Codable {
    case location, message  // ...
}

and something like this to PrefsInfo:

// Helper to quickly access a given struct
static func ofType(_ type: InfoType) -> CommonInfo {
    switch type {
    case .location:
        return location
    case .message:
        return message
    }
}

While this work to get the value, I cannot write to UserDefaults using this :

PrefsInfo.ofType(.location).isEnabled = false

The compiler throwing a Cannot assign to property: function call returns immutable value.

Is there a better way to access the given struct in a generic fashion, via this enum ?

cwizou
  • 108
  • 6
  • You cannot return a `struct`, change it, and expect the original to change. Why don't you create a method `PrefsInfo.setEnabled(type, boolean)`? – Sulthan Dec 31 '19 at 16:16
  • I probably have not correctly explained what I was trying to achieve. I'm wondering if there's a way to use the `enum` to access the struct directly to write to it, as I can do that this way to get the value directly. I could certainly create some setters for each of the properties in question, but I'm wondering if there's a more concise way to do this that would avoid making setters for each property. – cwizou Dec 31 '19 at 16:30
  • 1
    Not in Swift. You cannot get a reference to a struct. – Sulthan Dec 31 '19 at 17:52

2 Answers2

3

To quote another related SO (Cannot assign to property: function call returns immutable value)

This is compiler's way of telling you that the modification of the struct is useless

If you want to have the same object passed to you when you call that function, you should use classes.

structs are value types whereas classes are reference types

This means that if you get a reference to a class (i.e. your ofType function) and you change it, it gets changed on its actual object (your perfInfo location object).

But when you use structs, your ofType function returns a copy of your perfInfo location object, so even if the compiler allows changing of isEnabled, it only gets changed in your copy, not on the main object.

Your fix is simple: just change the structs into classes. That's all.

B.T.
  • 610
  • 6
  • 20
Don
  • 490
  • 3
  • 9
  • This makes perfect sense, as you and Sulthan pointed, that `ofType` returns a copy and is definitely not what I need. I'll go with classes and references, as I don't think that I could achieve something "similar" than what I tried to achieve with `ofType` with any other mechanism with a struct in Swift. Thanks again all! – cwizou Jan 02 '20 at 13:14
2

Continuing Sulthan's comments and Don's answers, you've created a value type that doesn't have value semantics. That indicates an important design mistake.

Consider this:

let l1 = PrefsInfo.location
let l2 = l1

l1.isEnabled = false
l2.isEnabled = true


l1.isEnabled == ????? // What do you expect the answer to be?

If you expect l1 to be independent of l2, then that's a value type, and a struct is the right tool. But if you're expecting shared mutable state, so that changes to one Location are seen by other references to it, then you want classes. But you also need to think a lot harder about your design and make sure it behaves in expected ways when there are multiple saved references. Just switching to classes in your current design looks like it may create surprising kinds of shared state, and the more layers of abstraction you create, the more confusing that shared state can get and the more you need to think very carefully about how you mean it to work.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thanks for your complementary answer. I definitely get that I return a copy this way and that it should not work. I definitely misphrased my question as I was wondering if there was another mechanism I could use to dynamically access a given struct, given an enum. There doesn't seem to be, I don't think that something around keypath are the answer either as they don't seem to work with static structs. – cwizou Jan 02 '20 at 13:28