Tim Fuqua
Tim Fuqua

Reputation: 1725

In Swift, how can you call a function immediately after object creation

I have some objects, which are structs, that I initialize from JSON dictionaries ([String : Any]) via an init function provided from an extension on the Decodable protocol (see Init an object conforming to Codable with a dictionary/array).

So basically, I have objects that look like this:

struct ObjectModelA: Codable {
    var stringVal: String
    var intVal: Int
    var boolVal: Bool

    // Coding keys omitted for brevity
}

struct ObjectModelB: Codable {
    var doubleVal: Double
    var arrayOfObjectModelAVal: [ObjectModelA]

    // Coding keys omitted for brevity

    var complicatedComputedVar: String {
        // expensive operations using the values in arrayOfObjectModelAVal
    }
}

ObjectModelB contains an array of ObjectModelA, and it also has a property which I only really want to set if the arrayOfObjectModelAVal changes.

I can use a didSet on arrayOfObjectModelAVal, but that only catches future changes to the arrayOfObjectModelAVal property. The problem is that I'm using a webservice to retrieve JSON data to create an array of ObjectModelB ([[String : Any]]), and I build it like this:

guard let objectDictArray = responseObject as? [[String : Any]] else { return }
let objects = objectDictArray.compactMap({ try? ObjectModelB(any: $0) })

My objects get created inside the compactMap closure, and init doesn't trigger the didSet.

I also can't "override" the init provided by the init from the Decodable protocol (the one in the closure: try? ObjectModelB(any: $0)) because my object is a struct and this isn't inheritance, it's just an initializer provided by a protocol. Otherwise, I'd "override" the init in my object and then just do super.init followed by some sort of mutating function that updates my complicated property and I'd make my complicated property private(set).

The only other option I can think of is creating that mutating function I just mentioned, and calling it in both the didSet when arrayOfObjectModelAVal changes, and then update my object initialization call with something like this:

guard let objectDictArray = responseObject as? [[String : Any]] else { return }
let objects = objectDictArray
    .compactMap({ try? ObjectModelB(any: $0) })
    .forEach({ $0.updateProperties() })

But now updateProperties could be called at any time by anyone (which is bad because it's really taxing), and there's no guarantee that it even gets called when creating the array of objects because the dev could forget to do the forEach part. Hence why I want a way to automatically call the updateProperties function right after object initialization.

Upvotes: 1

Views: 1881

Answers (1)

Tim Fuqua
Tim Fuqua

Reputation: 1725

I figured out a way to accomplish this using a factory method. Like I said in the original question, the initializer I want to use is being provided by a protocol extension on Decodable (see Init an object conforming to Codable with a dictionary/array). I went ahead and added a createFrom static func inside of the Decodable extension like this:

extension Decodable {
    init(any: Any) throws {
        // https://stackoverflow.com/questions/46327302
    }

    static func createFrom(any: Any) throws -> Self {
        return try Self.init(any: any)
    }
}

Now if I define an init on ObjectModelB with the same function signature as the init provided in the Decodable extension, like so:

struct ObjectModelB: Codable {
    var doubleVal: Double {
        didSet {
            computeComplicatedVar()
        }
    }
    var arrayOfObjectModelAVal: [ObjectModelA] {
        didSet {
            computeComplicatedVar()
        }
    }

    // Coding keys omitted for brevity

    private(set) var complicatedVar: String = ""

    mutating private func computeComplicatedVar() {
        // complicated stuff here
    }

    init() {
        doubleVal = 0.0
        arrayOfObjectModelAVal = []
    }

    init(any: Any) throws {
        self.init()
        self = try ObjectModelB.createFrom(any: any)
        computeComplicatedVar()
    }
}

This seems to work. I like it because if I don't add the init that exactly matches the one provided in the Decodable extension, then my object can still use the one provided in the Decodable extension. But if I do provide my own, I just use the createFrom factory method to create an instance of my type using the init from Decodable, and then do whatever else I want after that. This way, I control which objects need special init treatment and which ones don't, but at the point of creating the object, nothing changes. You still use the same init(any:) function.

Upvotes: 0

Related Questions