iosswiftnetwork-programmingurlsessionios-lifecycle

do network call from current ViewController or parent ViewController?


I'm writing an app that contains network call in every other screen. The result of calls would be the dataSource for a specific screen.

The question is, should I do network call in the parent viewController and inject the data before pushing current viewController or push currentViewController and do network call on viewDidLoad()/viewWillAppear()?

Both the methods makes sense to me.


Solution

  • Where you make the request to network should actually make no difference. You are requesting some data which you will have to wait for and present it. Your question is where should you wait for the data to be received.

    As @Scriptable already mentioned you can do either of the two. And which to use depends on what kind of user experience you wish to have. This varies from situation to situation but in general when we create a resource we usually wait for it on current screen and when we are reading resources we wait for it on the next screen:

    1. For instance if you are creating a new user (sign up) after you will enter a new username and password an indicator will appear and once the request is complete you will either navigate to next screen "enter your personal data" or you will receive a message like "User already exists".
    2. When you then for instance press "My friends" you will be navigated to the list first where you will see activity indicator. Then the list appears or usually some screen like "We could not load your data, try again."

    There are still other things to consider because for the 2nd situation you can add more features like data caching. A lot of messaging applications will for instance have your chats saved locally and once you press on some chat thread you will be navigated directly to seeing whatever is cached and you may see after a bit new messages are loaded and shown.

    So using all of this if we get back to where you should "call" the request it seem you best do it before you show the new controller or at the same time. At the same time I mean call it the load on previous view controller but load the new view controller before you receive the new data.

    How to do this best is having a data model. Consider something like this:

    class UsersModel {
        private(set) var users: [User]?
    }
    

    For users all we need is a list of them so all I did was wrapped an array. So in your case we should have an option to load these users:

    extension UsersModel {
        func fetchUsers() {
            User.fetchAll { users, error in
                self.users = users
                self.error = error // A new property needed
            }
        }
    }
    

    Now a method is added that loads users and assigns them to internal property. And this is enough for what we need in the first view controller:

    func goToUsers() {
        let controller = UserListViewController()
        let model = UserModel()
        controller.model = model
        model.fetchUsers()
        navigationController.push(controller...
    }
    

    Now at this point all we need is to establish the communication inside the second view controller. Obviously we need to refresh on viewDidLoad or even on view will appear. But we would also want some delegate (or other type of connections) so our view controller is notified of changes made:

    func viewDidLoad() {
        super.viewDidLoad()
    
        self.refreshList()
        self.model.delegate = self
    }
    

    And in refresh we should now have all the data needed:

    func refreshList() {
        guard let model = model else {
            // TODO: no model? This looks like a developer bug
            return      
        }
    
        if let users = model.users {
            self.users = users
            tableView?.reloadData()
            if users.count.isEmpty {
                 if let error = model.error {
                     // TODO: show error screen
                 } else {
                     // TODO: show no data screen
                 }
            }
        } else {
            // TODO: show loading indicator screen
        }
    
    }
    

    Now all that needs to be done here is complete the model with delegate:

    extension UsersModel {
        func fetchUsers() {
            User.fetchAll { users, error in
                self.users = users
                self.error = error // A new property needed
                self.delegate?.usersModel(self, didUpdateUsers: self.users)
            }
        }
    }
    

    And the view controller simply implements:

    func usersModel(_ sender: UserModel, didUpdateUsers users: [User]?) {
        refreshList()
    }
    

    Now I hope you can imagine the beauty of such a system that your model could for instance first asynchronously load users from some local cache or database and call the delegate and then call the request to server and call the delegate again while your view controller would show appropriate data for any situation.