Reputation: 1725
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
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