iosswiftmodel-view-controlleruikit

Delegate returns nill in Swift (Data flow between Model and ViewController)


I'm creating a basic application in swift where I get data from an api and show it. My Model class handles the fetching data part and I use a delegate to communicate data with the ViewController.

However when fetched data is passed into the controller it is nill for some reason. Here are the things I know/have tried.

  1. Extracting the JSON is working. The data is correctly fetched.
  2. I tried using viewWillAppear as well as viewDidLoad
  3. I have set the delegate to self in the View Controller

Abstracted code is below.

//Model Class

protocol DataDetailDelegate {
    
    func datadetailFetched(_ datadetail: DataDetailItem)
}

class Model {
    
    var datadetaildelegate: DataDetailDelegate?
    
    func fetchData(ID: String) {
        
        let url = URL(string: Constants.Data_URL + ID)
        
        guard url != nil else {
            print("Invalid URL!")
            return
        }
        
        let session = URLSession.shared.dataTask(with: url!) { data, response, error in
            
            if error != nil && data == nil {
                print("There was a data retriving error!")
                return
            }
            
            let decoder = JSONDecoder()
            do {
                let response = try decoder.decode(MealDetail.self, from: data!)
                
                if response != nil {
                    
                    DispatchQueue.main.async {
                        self.datadetaildelegate?.datadetailFetched(response)
                    }
                    
                }
                
            } catch  {
                print(error.localizedDescription)
            }
                 
        }   
        session.resume()
        
    }
    
}

//View Controller 
import UIKit

class DetailViewController: UIViewController, DataDetailDelegate {
    
    @IBOutlet weak var instruction: UITextView!
    
    
    var model = Model()
    var datadetail : DataDetailItem?
    

    
        override func viewDidLoad() {
            super.viewDidLoad()
        
           
            model.datadetaildelegate = self   
            model.fetchData(ID: data.id)
     
            
            
            if self.datadetail == nil {
                print("No data detail available")
                return
            }
    
            self.instruction.text = datadetail.instruction
        }
    
    func datadetailFetched(_ datadetail: DataDetailItem) {
        self.datadetail = datadetail
    } 
}





Solution

  • As mentioned in the comments, there are many problems with the above code, but the main one being not recognising the asynchronous nature of the fetchData.

    The below is a rough solution that addresses the critical issues with your code, rather than being best practice and addressing all of them. For example you could also check the contents of error and response codes, and pass them back through the completion handler (maybe as a Result type).

    For starters, streamline your fetchData so that it gets the data and then handles that data via a completion handler in an asynchronous manner. Also, don't call it a Model as it's really not.

    class SessionManager { 
    
      func fetchData(withID id: String, completion: @escaping (data) -> Void) {
        guard let url = URL(string: Constants.Data_URL + ID) else {
           print("Invalid URL!")
           return
        }
    
        let session = URLSession.shared.dataTask(with: url) {  data, response, error in 
    
        guard error == nil, let data = data else {   //could be done far better by checking error and response codes
            print("There was a data retriving error!")
            return
        }
        completion(data)
      }
      session.resume()
    }  
    

    Then adapt your view conroller so that it creates the session manager and fetches the data, but processes it in the completion handler. I've only added the relevant content.

    class DetailViewController: UIViewController, DataDetailDelegate {
       
        lazy var sessionManager = SessionManager()
        var datadetail : DataDetailItem?
    
        override func viewDidLoad() {
           super.viewDidLoad()
           sessionManager.fetchData(withID: data.id){ [weak self] data in
             let decoder = JSONDecoder()
             do {
                let response = try decoder.decode(MealDetail.self, from: data)
                DispatchQueue.main.async {
                  self?.datadetail = response
                  self?.instruction.text = datadetail.instruction
                }
             } catch  {
               print(error.localizedDescription)
             }
          }   
       }
       
       // rest of view controller
     }
    

    That should be enough to get you going. There are many best practice examples/tutorials scattered around that would be worth a look so you can refine things further.

    Note: this has been written in here without a compiler to hand, so there may be some syntax that needs tweaking.