Home > other >  How to build a Swift object that can control the mutability of its stored properties
How to build a Swift object that can control the mutability of its stored properties

Time:02-16

I would like to create a set of objects that exhibit the following behavior:

  1. Each has a BOOL property -- call it dataLocked -- that is initially false.
  2. Each has a set of stored properties whose values may be set, but not read, whenever dataLocked == false.
  3. Those same stored properties may be read, but not set, whenever dataLocked == true
  4. dataLocked can be set only once.

Below is a sample implementation. Is there any Swifty way to achieve this without having to reproduce all those get and set conditions for every property of every object?

The neatest solution I believe would be to create a Property Wrapper, but I haven't found any way to make the wrapper change its behaviors based on the value of the `locked` property in the enclosing object.

class ImmutableObjectBase {
    var dataLocked: Bool = false {
        didSet { dataLocked = true }
    }
    private var _someIntValue: Int = 42
    var someIntValue: Int {
        get {
            precondition(dataLocked, "Cannot access object properties until object is locked")
            return _someIntValue
        }
        set {
            precondition(!dataLocked, "Cannot modify object properties after object is locked")
            _someIntValue = newValue
        }
    }
}

let i = ImmutableObjectBase()
i.someIntValue = 100
i.dataLocked = true     // or false, it doesn't matter!
print (i.someIntValue)  // 100
print (i.dataLocked)    // true
i.someIntValue = 200    // aborts

CodePudding user response:

I found a solution for your problem, but it is probably not the cleanest way. You might find a better way if you check this or this.

I managed to lock and unlock the properties by using this tutorial:

import Foundation
import Combine

public class Locker: ObservableObject {
    @Published public var isLocked = false
    public static let shared = Locker()
    private init() {}
}

@propertyWrapper
struct Lockable<Value> {
    private var _wrappedValue: Value
    
    init(wrappedValue: Value) {
        self._wrappedValue = wrappedValue
    }
    
    var wrappedValue: Value {
        get {
            precondition(Locker.shared.isLocked, "Cannot access object properties until object is locked")
            return _wrappedValue
        }
        set {
            precondition(!Locker.shared.isLocked, "Cannot modify object properties after object is locked")
            _wrappedValue = newValue
        }
    }
}

class ImmutableObjectBase {
    var isLocked: Bool = false {
        didSet {
            Locker.shared.isLocked = self.isLocked
        }
    }
    @Lockable var someString: String = "initial"
    @Lockable var someInt: Int = 1
}


var i = ImmutableObjectBase()
i.isLocked = true
print(i.someInt, i.someString) // 1, initial
i.isLocked = false
i.someInt = 2
i.someString = "new value"
i.isLocked = true
print(i.someInt, i.someString) // 2, new value

EDIT:

I just found a similar question with an accepted answer. Check here

CodePudding user response:

import Foundation

protocol PropertyWrapperWithLockableObject {
    var enclosingObject: LockableObjectBase! {get set}
}

@propertyWrapper
class Lockable<Value>: PropertyWrapperWithLockableObject {
    private var _wrappedValue: Value
    var enclosingObject: LockableObjectBase!
    
    init (wrappedValue: Value) { self._wrappedValue = wrappedValue }
    
    var wrappedValue: Value {
        get {
            precondition(enclosingObject.isLocked, "Cannot access object properties until object is locked")
            return _wrappedValue
        }
        set {
            precondition(!enclosingObject.isLocked, "Cannot modify object properties after object is locked")
            _wrappedValue = newValue
        }
    }
}

class LockableObjectBase {
    internal var isLocked: Bool = false {
        didSet { isLocked = true }
    }
    
    init () {
        let mirror = Mirror(reflecting: self)
        for child in mirror.children {
            if var child = child.value as? PropertyWrapperWithLockableObject {
                child.enclosingObject = self
            }
        }
    }
}

Usage:

class DataObject: LockableObjectBase {
    @Lockable var someString: String = "Zork"
    @Lockable var someInt: Int
    
    override init() {
        someInt = 42
        // super.init() // Not needed in this particular example.
    }
}

var testObject = DataObject()
testObject.isLocked = true
print(testObject.someInt, testObject.someString) // 42, Zork
testObject.isLocked = false             // Has no effect: Object remained locked
print (testObject.isLocked)             // true
testObject.someInt = 2                  // Aborts the program

arsenius's answer here provided the vital reflection clue!

  • Related