skip to Main Content

Desired Outcome

  1. Initialize MyClass with fixedValues as the dictionaryRepresentation argument.
  2. Set fixed values before executing the init line

Problem

  • fixedValues isn’t being set before the self.init(… line is ran.

Attempted Solutions

  1. Returns instead of completions (posted state)
  2. Completions, DispatchGroups, Notify
  3. 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


  1. 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 :

    class MyClass {
        
        init(dictionary: [String: Any], type: String) {
            print("main init")
        }
        
        convenience init?(_ object: Object) {
            
            guard let values = object.getValues(forKeys: MyAttributes.allAttributes) else { return nil }
            
            var fixedValues = values
            print(fixedValues)
            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
        }
    }
    
    typealias Attribute = String
    //enum Attribute:String, Hashable {
    //    case props
    //}
    
    class Object {
        let type: String = ""
        let objects:[SomeObject] = []
        func getValues(forKeys:[String]) -> [String: Any]? {
            return ["a": 1, "b": 2]
        }
    }
    
    struct SomeObject {
        var isEnabled: Bool
        var identifier: Attribute
    }
    
    class WrappedObject {
        func getValues(forKeys:[String]) -> [String : Any]?{
            return [:]
        }
        
        init(_ obj: Object){
            
        }
    }
    
    struct MyAttributes{
        static var allAttributes: [String] = []
    }
    
    MyClass(Object())
    

    And here are the prints I get :

    ["a": 1, "b": 2]
    main init
    

    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.

    Login or Signup to reply.
  2. You say:

    fixedValues isn’t being set before the self.init(…) line

    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 in fixedChildrenValues); 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:

    1. 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.

    2. 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.

    3. 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 asyncawait 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:

      convenience init?(_ object: Object) {    
          guard let values = object.getValues(forKeys: MyAttributes.allAttributes) else {
              return nil
          }
      
          var fixedValues = values
      
          MyClass.correctedValues(…) { correctedValues in
              fixedValues = correctedValues                     // pretend this is actually updated asynchronously, i.e., later
          }
      
          self.init(dictionary: fixedValues, type: object.type) // by the time you get here, `fixedValues` has not yet been set
      }
      

      Because correctValues calls its completion handler asynchronous, you are proceeding to self.init before fixedValues has been set.

      But, Swift concurrency does have a concept of an asynchronous initializers. So init can be async and await the results of correctedValues. E.g., you might have:

      convenience init?(_ object: Object) async {
          guard let values = object.getValues(forKeys: MyAttributes.allAttributes) else {
              return nil
          }
      
          let fixedValues = await MyClass.correctedValues(object: object, values: values)
      
          self.init(dictionary: fixedValues, type: object.type)
      }
      

      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 be nil-able and instead throw a meaningful error:

      convenience init(_ object: Object) async throws {
          guard let values = object.getValues(forKeys: MyAttributes.allAttributes) else {
              throw MyClassError.noAttributes
          }
      
          let fixedValues = await MyClass.correctedValues(object: object, values: values)
      
          self.init(dictionary: fixedValues, type: object.type)
      }
      

      Now that obviously assumes that correctValues has been refactored to adopt Swift concurrency. Or you can just write a wrapper implementation:

      private static func correctedValues(object: Object, values: [String: Any]) async -> [String: Any] {
          await withCheckedContinuation { continuation in
              correctedValues(object: object, values: values) { result in
                  continuation.resume(returning: result)
              }
          }    
      }
      

      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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search