Mutating Struct property with asynchronous function

Viewed 347

I have the following Struct that I want to initialize, and then use its method query() to mutate its result property.

Query() sends and fetches JSON data, then decodes it to a String. When I declare query() as a mutating function, I receive the error "Escaping closure captures mutating 'self' parameter" in my URLSession.

What do I need to change?

The call:

var translation = Translate(string: "hello", base: "en", target: "de", result: "")
translation.query()
let translated = translation.result

The struct:

struct Translate {
    
    let string: String, base: String, target: String
    var result: String
    
    mutating func query() {
        
        let body: [String: String] = ["q": self.string, "source": self.base, "target": self.target]
        let bodyData = try? JSONSerialization.data(withJSONObject: body)

        guard let url = URL(string: "https://libretranslate.com/translate") else { return }
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        
        request.httpBody = bodyData
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        
        URLSession.shared.dataTask(with: request) { data, response, error in
            
            guard let data = data, error == nil else {
                print(error?.localizedDescription ?? "No data")
                return
            }
            
            DispatchQueue.main.async {
                let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
                if let responseJSON = responseJSON as? [String: Any] {
                    if responseJSON["translatedText"] != nil {
                        self.result = responseJSON["translatedText"] as! String
                    }
                }
            }
            
            return
            
        }
        .resume()
        
    }
    
}

Xcode error: enter image description here

1 Answers

There are many issues in the code.

The most significant issue is that the URLRequest is asynchronous. Even if no error occurred result will be always empty.

You have to add a completion handler – it fixes the errors you got by the way – and it's highly recommended to handle all errors.

Instead of JSONSerialization the code uses JSONDe/Encoder

struct Translation : Decodable { let translatedText : String }

struct Translate {
    
    let string: String, base: String, target: String
    
    func query(completion: @escaping (Result<String,Error>) -> Void) {
        
        let body: [String: String] = ["q": self.string, "source": self.base, "target": self.target]
        do {
            let bodyData = try JSONEncoder().encode(body)
            
            let url = URL(string: "https://libretranslate.com/translate")!
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            
            request.httpBody = bodyData
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            
            URLSession.shared.dataTask(with: request) { data, response, error in
                if let error = error { completion(.failure(error)); return }                                       
                completion( Result{ try JSONDecoder().decode(Translation.self, from: data!).translatedText} )
            }                
            .resume()
        } catch {
            completion(.failure(error))
        }
    }
}

let translation = Translate(string: "hello", base: "en", target: "de")
translation.query() { result in
    DispatchQueue.main.async {
        switch result {
            case .success(let translated): print(translated)
            case .failure(let error): print(error)
        }
    }
}

Both exclamation marks (!) are safe.

Related