Desired Outcome
- Initialize MyClass with fixedValues as the dictionaryRepresentation argument.
- Set fixed values before executing the init line
Problem
- fixedValues isn’t being set before the
self.init(
… line is ran.
Attempted Solutions
- Returns instead of completions (posted state)
- Completions, DispatchGroups, Notify
- Semaphore wait for set
class WaitForValue {
private var semaphore = DispatchSemaphore(value: 0)
var valueToWaitFor: String? {
didSet {
if valueToWaitFor != nil {
semaphore.signal()
}
}
}
func waitForValue() {
_ = semaphore.wait(timeout: .distantFuture)
}
}
let waitForValueInstance = WaitForValue()
// This will block until valueToWaitFor is set to a non-nil value
DispatchQueue.global().async {
waitForValueInstance.waitForValue()
print("Value received: (waitForValueInstance.valueToWaitFor ?? "nil")")
}
// Simulate setting the value after some delay
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
waitForValueInstance.valueToWaitFor = "Hello, World!"
}
My Code:
class MyClass {
convenience init?(_ object: Object) {
guard let values = object.getValues(forKeys: MyAttributes.allAttributes) else { return nil }
var fixedValues = values
MyClass.correctedValues(object: object, values: values) { correctedValues in
fixedValues = correctedValues
}
self.init(dictionary: fixedValues, type: object.type)
}
private static func correctedValues(object: Object, values: [String: Any], completion: @escaping([String: Any]) -> Void) {
var valuesDictionary = values
let fixes = objectFixes(object: object)
let valueObjects = values["children"] as? [Object] ?? []
var wrappedObjects = [WrappedObject]()
valueObjects.forEach {
let wrapped = WrappedObject($0)
wrappedObjects.append(wrapped)
}
fixedChildrenValues(originalValues: wrappedObjects, fixes: fixes) { correctedChildren in
valuesDictionary["children"] = correctedChildren
completion(valuesDictionary)
}
}
private static func fixedChildrenValues(originalValues: [WrappedObject], fixes: [String: [Attribute: String]], completion: @escaping ([[String: Any]]) -> Void) {
var fixedChildrenArray = [[String: Any]]()
for childValue in originalValues {
var childAttributes = childValue.getValues(forKeys: [
"children",
"identifier",
"props",
"name"
]) ?? [:]
// Apply fixes
if let matchingIdentifier = childAttributes["identifier"] as? String,
let fix = fixes[matchingIdentifier] {
fix.forEach { (key, value) in
childAttributes[key] = value
}
}
// Attempt to extract child elements and continue traversal
if let childrenObjects = childAttributes["children"] as? [Object] {
var wrappedObjects = [WrappedObject]()
for childObject in childrenObjects {
wrappedObjects.append(WrappedObject(childObject))
}
if !wrappedObjects.isEmpty {
fixedChildrenValues(originalValues: wrappedObjects, fixes: fixes) { correctedChildren in
childAttributes["children"] = correctedChildren
fixedChildrenArray.append(childAttributes)
}
} else {
fixedChildrenArray.append(childAttributes)
}
} else {
fixedChildrenArray.append(childAttributes)
}
}
completion(fixedChildrenArray)
}
private static func objectFixes(object: Object) -> [String: [Attribute: String]] {
var objectFixes = [String: [Attribute: String]]()
let objects = object.objects
for i in 0..<objects.count {
let obj = objects[i]
if !obj.isEnabled {
objectFixes[obj.identifier] = ["props": "false"]
}
}
return objectFixes
}
}
2
Answers
I’m really not sure what you are trying to do here.
I tried you code in a playground (I had to guess a lot of code tho) but it’s working for me.
here is the code I have :
And here are the prints I get :
Which indicate that the fixedValue is set before self.init is called.
Also I’m not quite confident in the design pattern here.
It seems to me that you should use a factory pattern instead of the convenience init.
But maybe I miss something.
Finally I don’t see any async code (except in the WaitForValue class which doesn’t seems to be used anywhere. Therefore I wouldn’t use completion handler here, because it makes your code complicated to read, when it’s simply linear.
You say:
If that is indeed the case, then it suggests that
correctedValues
is running asynchronously, as the use of the completion handler pattern would suggest that was your intent.I will set aside, for a second, the fact that you are not actually doing anything asynchronous in the rendition of
correctedValues
that you have shared with us (nor infixedChildrenValues
); simply using completion handlers does not make a method asynchronous, but would generally entail dispatching code to some background queue. (Needless to say, if it was synchronous, as your code snippet actually is, then your problem obviously rests elsewhere.)But, if
correctedValues
really was asynchronous, that would mean that you are attempting to implement an “asynchronous initializer”. But in the world of completion handlers, we do not really have any such concept.There are a few alternative solutions:
You could make this synchronous. You are not currently doing anything asynchronous in what you have shared with us, anyway. This is the simplest approach. And if you’re concerned about the performance here (as we are with any computationally intensive process), the caller could put its instantiation of this tree-object on some background queue, and be done with it.
You could pull the asynchronous code out of
init
and put it in some asynchronous instance method. That would decouple the asynchrony with the initialization.You could implement an “asynchronous initializer” in Swift concurrency. Admittedly, in the world of completion handlers, we do not really have any such concept, but
async
–await
does.Now, this having been said, I freely acknowledge that converting a large GCD project to Swift concurrency may be a large ask. But it is the more modern concurrency pattern, and addresses many problems that plague completion handler code bases. (See SE0296 – Completion handlers are suboptimal.)
So, let’s go back to your existing initializer, so we can see what the issue is:
Because
correctValues
calls its completion handler asynchronous, you are proceeding toself.init
beforefixedValues
has been set.But, Swift concurrency does have a concept of an asynchronous initializers. So
init
can beasync
andawait
the results ofcorrectedValues
. E.g., you might have:Or, if you want to give the app developer a clue as to why it failed, rather than just returning
nil
on failure, it might not benil
-able and insteadthrow
a meaningful error:Now that obviously assumes that
correctValues
has been refactored to adopt Swift concurrency. Or you can just write a wrapper implementation:Now, I appreciate that blithely proposing “use Swift concurrency” is a non-trivial exercise if you have never used it before (and especially if you are dealing with a large codebase). But if you are really trying to manage complex dependencies of asynchronous tasks, Swift concurrency handles this far more gracefully than GCD.
See WWDC 2021 video Meet async/await in Swift and Swift concurrency: Update a sample app.