skip to Main Content

I am trying to load custom data type from my API and display it on my landing page of an iOS app.

As i understand, I should call something like:

override func viewDidLoad() {
  performSelector(inBackground: #selector(fetchJSON), with: nil)
  let myCustomDataType = //how so I get this back
  tableView.reloadData()
 //...
}

Obviously the declaration of fetchJSON should be in another file so I don’t fill my Controller up with things it doesn’t need to do. However I need this function to return a list of [MyCustomDataType] and display those on my landing page.

func fetchJSON() -> [MyCustomDataType] {
  //get api, fetch data, put it in my custom data type list
  return myCustomDataType
}

Do I use closures? Create a global [MyCystomDataType]. Or how do I achieve this?
Be mindful of the task being async, since I display table cells kinda like facebook, or instagram news feed page.

3

Answers


  1. performSelector(inBackground:) is not GCD. It’s a pre-GCD method from OS X 10.5 that you should almost never use. (I don’t think I’ve had a single reason to use it since 10.6 introduced GCD.)

    As a rule, you don’t need to use GCD directly at all for network requests. URLSessionTask is already asynchronous. See Fetching Website data into memory for more. As shown there, you often will need to use GCD (DispatchQueue.main.async) to return data to the UI, but you don’t need it to launch the request.

    But to your underlying question, the answer is that by the time viewDidLoad completes, you won’t have the data yet. You need to be able to deal with not having the data, and draw your UI appropriately. When the data shows up, then you can update your UI.

    Login or Signup to reply.
  2. If fetchJSON loads the data asynchronously you have to add a completion handler

    func fetchJSON(completion: @escaping ([MyCustomDataType]) -> Void){
        API.fetch { fetchedData in
            completion(fetchedData)
        }
    }
    

    Then call fetchJSON on a background thread and reload the table view on the main thread

    override func viewDidLoad() {
       DispatchQueue.global().async {
          self.fetchJSON { data in 
              DispatchQueue.main.async {
                  self.myCustomDataType = data
                  self.tableView.reloadData()
              }
          }
       }
    }
    

    Popular asynchronous APIs like URLSession and Alamofire perform their network requests on a background thread implicitly. If you are going to use one of them you can omit the first async block.

    performSelector is an outdated objective-c-ish API. Don’t use it in Swift

    Login or Signup to reply.
  3. As Vadian said (+1), you want to use asynchronous pattern, and using closures is a good pattern. But the question is not just how to hand back data, but in the case of an error, how to report the Error, too. We often use Result<Success, Failure> pattern to accomplish this. E.g. rather than a [MyCustomDataType] parameter, it might have a Result<[MyCustomDataType], Error> type.

    E.g., let’s imagine that you are performing your request via URLSession. Then you might have a routine like:

    enum ApiError: Error {
        case networkError(Data?, URLResponse?)
    }
    
    func fetchJSON<T: Decodable>(_ request: URLRequest, queue: DispatchQueue = .main, completion: @escaping (Result<T, Error>) -> Void) {
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            // detect and report error
            guard
                error == nil,
                let responseData = data,
                let httpResponse = response as? HTTPURLResponse,
                200 ..< 300 ~= httpResponse.statusCode
            else {
                queue.async {
                    completion(.failure(error ?? ApiError.networkError(data, response)))
                }
                return
            }
    
            // no network error, so let's parse the response
            do {
                let responseObject = try JSONDecoder().decode(T.self, from: responseData)
                queue.async {
                    completion(.success(responseObject))
                }
            } catch let parseError {
                queue.async {
                    completion(.failure(parseError))
                }
            }
        }
        task.resume()
    }
    

    And to perform a particular request:

    func fetchMyCustomDataTypes(completion: @escaping (Result<[MyCustomDataType], Error>) -> Void) {
        let request: URLRequest = ... // build your request here
        fetchJSON(request) { result in
            completion(result)
        }
    }
    

    Now, do not get too lost in the details, above, as your implementation might vary (e.g. you could just as easily use Alamofire or whatever). The key point is that we have a completion handler closure that includes a Swift.Result parameter by which data or error information will be supplied to the caller.

    And now, the caller can act accordingly based upon whether the result was a success or a failure:

    override func viewDidLoad() {
        super.viewDidLoad()       // make sure to call `super`
    
        fetchMyCustomDataTypes { result in
            switch result {
            case .failure(let error):
                // handle error here, e.g., report the error in the UI; or at the minimum, during development, just print the error
                print(error)
        
            case .success(let objects):
                // use the `[MyCustomDataType]` array, `objects` here, e.g.
                self.objects = objects
                self.tableView.reloadData()
            }
        }
    }
    

    But the caller doesn’t use performSelector. Nor does it need to dispatch this network request to a background queue, because network requests are already inherently asynchronous. Just call fetchJSON and specify what UI update must happen in the completion handler.

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