Martin Claesson
Martin Claesson

Reputation: 749

Swift: Non destructive JSON Encoder

I store user changeable settings as JSON in the keychain. The app has premium content that is only allowed if the user is subscribed. Depending on the subscription state, the JSONEncoder/Decoder skips some variable and sets it to a default value. The concept is working as expected but there is a situation when the concept isn't optimal.

Say that a user subscribes and changes some of the premium variables. Then the subscription expires and the variables change back to their default values. So far everything's good. But when the user resubscribes and the premium features are enabled again the changes that the user made during the recent subscription is lost. The behavior is expected since I overwrite the JSON data with only the "free" variables. I'm looking for a solution where the "premium" variables are persistent but not accessible (returning default values) for free users. Are there any suggestions for solving this situation?

I have a struct with variables that looks like this:

struct Settings: Codable {
    var someFeature:Bool = false
    var someSetting = 150
    var someURL: URL? = URL(string: "https://www.google.com")
}

When the user changes the settings I save it in the keychain as JSON using JSONEncoder:

func encode(to encoder: Encoder) throws {            
        var container = encoder.container(keyedBy: CodingKeys.self)

        // ALWAYS ALLOWED
        try container.encode(someURL, forKey: .someURL)

        // ONLY ALLOWED FOR PREMIUM USERS
        if settingsManager.shared.isPremium {
            try container.encode(someFeature, forKey: .someFeature)
            try container.encode(someSetting, forKey: .someSetting)
        }

}

The settings are loaded in the app from the keychain using JSONDecoder:

init(from decoder: Decoder) throws {
    
    let container = try decoder.container(keyedBy: CodingKeys.self)
    
    
    
    // ALWAYS ALLOWED
    self.url = try container.decodeIfPresent(URL.self, forKey: .someUrl) ?? URL(string: "https://www.google.com")

    // ONLY ALLOWED FOR PREMIUM USERS
    if settingsManager.shared.isPremium {
        self.someFeature = try container.decodeIfPresent(Bool.self, forKey: .someFeature) ?? false
        self.someSetting = try container.decodeIfPresent(Int.self, forKey: .someSetting) ?? 150
    } else {
        // DEFAULT VALUES FOR FREE USERS
        self.someFeature = false
        self.someSetting = 150
    }

}

Codingkeys:

private enum CodingKeys : String, CodingKey {
        case someFeature, someSetting, someUrl 
}

Upvotes: 0

Views: 45

Answers (1)

loverap007
loverap007

Reputation: 139

I can suggest to use wrapped properties. You can store always only user defined properties, and resolve the actual value when access to properties

struct Settings: Codable {
    private var someFeature:Bool = false
    private var someSetting = 150
    var someURL: URL? = URL(string: "https://www.google.com")

    private enum CodingKeys : String, CodingKey {
        case someFeature, someSetting, someUrl
    }

    var isSomeFeatureEnabled: Bool {
        settingsManager.shared.isPremium ? someFeature : false
    }

    var someSettingValue: Int {
        settingsManager.shared.isPremium ? someSetting : 150
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(someURL, forKey: .someUrl)
        try container.encode(someFeature, forKey: .someFeature)
        try container.encode(someSetting, forKey: .someSetting)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.someURL = try container.decodeIfPresent(URL.self, forKey: .someUrl) ?? URL(string: "https://www.google.com")
        self.someFeature = try container.decodeIfPresent(Bool.self, forKey: .someFeature) ?? false
        self.someSetting = try container.decodeIfPresent(Int.self, forKey: .someSetting) ?? 150
    }
}

So when you will decode this structure, you will get saved values, when you will try override settings, you will save the previous values. But when get properties values via wrapped properties it will be resolved by value defined in settingsManager.shared.isPremium

If you want to update properties in the settings you can define setter and getter:

   var isSomeFeatureEnabled: Bool {
        get {
            settingsManager.shared.isPremium ? someFeature : false
        }
        set {
            if settingsManager.shared.isPremium {
                someFeature = isSomeFeatureEnabled
            }
        }
    }

Upvotes: 2

Related Questions