0

My SwiftUI app has a class which parses JSON Data. As you can see from the comments near the print, I do not understand while the line print("return: " + self.productName) print the variable still empty (basically the initialized one). Why is the variable updated inside the if block, but not outside?

Any idea? Thanks.


class ProductDecoer: ObservableObject {

  @Published var productName: String = ""

  func checkBarCode(barcode: String) -> String {

    let api = <url>
    let barcode = barcode
    let format = ".json"
    let url = URL(string: api + barcode + format)

    URLSession.shared.dataTask(with: url!) { [self] data, response, error in
      if let data = data {
        if let jsonString = String(data: data, encoding: .utf8) {
          productName = self.getProductInfo(json: jsonString)
          print("checkBarCode: " + self.productName) // --> print correctly
        }
      }
    }.resume()

    self.productName = productName
    print("return: " + self.productName) // --> is empty
    return self.productName
  }

  func getProductInfo(json: String) -> String {
    let jsonData = json.data(using: .utf8)!
    let productInfo: ProductInfo = try! JSONDecoder().decode(ProductInfo.self, from: jsonData)
    self.productName = productInfo.product_name
    print("AAA: " + self.productName)
    print("Prodotto: " + productInfo.product_name)

    productName = productInfo.product_name
    print("CC: " + productName)

    return productName
  }

  struct ProductInfo: Codable {

    var code: String
    var product_name: String

    enum CodingKeys: String, CodingKey {
      case code
      case product

      enum ProductCodingKeys: String, CodingKey {
        case product_name
      }
    }

    init(from decoder: Decoder) throws {
      let rootContainer = try decoder.container(keyedBy: CodingKeys.self)
      let productContainer = try rootContainer.nestedContainer(keyedBy: CodingKeys.ProductCodingKeys.self, forKey: .product)

      code = try rootContainer.decode(String.self, forKey: .code)
      product_name = try productContainer.decode(String.self, forKey: .product_name)

      print("BB: " + product_name)
    }

    func encode(to encoder: Encoder) throws {
      var rootContainer = encoder.container(keyedBy: CodingKeys.self)
      var productContainer = rootContainer.nestedContainer(keyedBy: CodingKeys.ProductCodingKeys.self, forKey: .product)

      try rootContainer.encode(code, forKey: .code)
      try productContainer.encode(product_name, forKey: .product_name)
    }
  }

}
egeeke
  • 753
  • 1
  • 6
  • 18
MarcoGT
  • 53
  • 6

3 Answers3

1

URLSession.shared.dataTask is an asynchronous function. That means that it executes independently and finishes an an indeterminate time in the future.

What you provide to it inside the { } that follows it is called a completion handler or callback function -- this is the code that dataTask will call after its execution finishes, so anything that depends on the returned value needs to happen inside that closure.

Unfortunately, that means you won't be able to return String directly from your checkBarCode function. But, you can create a callback function for it as well:

func checkBarCode(barcode: String, completion: @escaping (String) -> Void) {
        
        let api = "URL"
        let barcode = barcode
        let format = ".json"
        let url = URL(string: api + barcode + format)
        
        URLSession.shared.dataTask(with: url!) { [self] data, response, error in
            if let data = data {
                if let jsonString = String(data: data, encoding: .utf8) {
                    self.productName = self.getProductInfo(json: jsonString)
                    print("checkBarCode: " + self.productName)
                    completion(self.productName)
                }
            }
        }.resume()
    }

Which can be called like this:

checkBarCode(barcode: "") { productName in 
  //do something that depends on the value
}
jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • Thanks a lot; I am calling this function from another class, that means I have to do something like this: var productDecoder = ProductDecoder() productDecoder.checkBarCode("xyz") { productName in } ? – MarcoGT Apr 15 '21 at 16:06
  • Looks like you have an extra `productDecoder` in there... But, with an asynchronous function, you usually want to hold a reference to the object doing the calling. So, store a `ProductionDecoder()` instance somewhere and then call `productDecoder.checkBarCode...` on it. – jnpdx Apr 15 '21 at 16:08
1

Your URLSession.shared.dataTask(with: url!) ... is an asynchronous task. This means that the closure which you pass in [self] data, response, error will be called after the URL request has been made. The print statement below will therefore be run before the URL request has been completed which is why it is empty.

The print inside the closure has the correct value because it has been fetched by that time.

George
  • 25,988
  • 10
  • 79
  • 133
1

Since your ProductDecoer class conforms to ObservableObject and the property productName is published using @Publisher you don't need to return anything from the function checkBarCodeso change the signature of the function to

func checkBarCode(barcode: String) { ... }

and remove everything after .resume() in it.

Then in your SwiftUI view you create a property for the class and use the @ObservedObject property

@ObservedObject var decoder = ProductDecoer()

Below is a complete but simple example of a view that will request the product name for the bar code given in a text field when a button is pressed. Once the information has been downloaded the published property productName will be set and the view will display it.

struct ContentView: View {
    @ObservedObject var decoder = ProductDecoer()
    @State private var barCode: String = ""
    var body: some View {
        VStack(alignment: .leading) {
            TextField("Bar code", text: $barCode)
            Text(decoder.productName)

            Button {
                decoder.checkBarCode(barcode: barCode)
            }
            label: {
                Text("Load")
            }
        }
        .padding()
    }
}  
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52