How to deal with concurrency during class init

I have the following class. It is intended to cache a parsed DOM after fetching from a URL:

class DOMWindow {
    var cachedDOM: Elements
    var url: URL
    
    init(_ url: String)throws {
        self.url = URL(string: url)!
        self.cachedDOM = try fetchDOM()
    }
    
    func fetchDOM()throws -> Elements {
        var html: String
        
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            html = String(data: data!, encoding: .utf8)!
        }
        
        task.resume()
        
        return try DOM.parse(html)
    }
}

I am getting the errors that Variable 'html' used before initialization and Variable 'html' captured by closure before being initialized

I assume this is a detected race condition that the closure is run async and so the DOM.parse(html) will always be reached before the closure is executed. So how best should I handle this in Swift?

I think you need to add a completion handler to your fetchDOM method:

func fetchDOM(completion: @escaping (Result<Elements, Error>) -> Void) {
  let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let error = error {
      completion(.failure(.networkError(error)))
      return
    }
    if let data = data, let html = String(data: data, encoding: .utf8) {
      do {
        let parsedHTML = try DOM.parse(html)
        completion(.success(parsedHTML))
      } catch {
        completion(.failure(.parsingFailure(error)))
      }
      return
    }
    completion(.failure(.genericFailure))
  }.resume()
}

and give cachedDOM a default value or make it optional.

Though, you might also need something to let clients know if the DOM has been loaded or not. You could model it as an enum:

enum DOMState {
  case notLoaded
  case loading
  case loaded(Elements)
  case failedToLoad
}

and then have var cachedDOM: DOMState = .notLoaded as default. When you call fetchDOM, set it to .loading and then set to .loaded or .failedToLoad depending on the result.

There's various other ways to do it though. You can allow clients to call fetchDOM manually instead of calling it by default in init for example.

1 Like

I'm new to completion handlers. How would this function get called? Or is there somewhere I can read more about them?

fetchDOM { result in
  switch result {
    case let .success(elements):
      // do something with elements
    case let .failure(error):
     // do something with the error 
  }
}

You can read more about closures here: Closures — The Swift Programming Language (Swift 5.7). If you want to read more about completion handlers specifically, there are some resources available on the internet, such as this blog post.

1 Like