Mobile App Developer

Using Environment Object for Reactive Views

Traditionally, writing networking code for iOS involved creating one or more Networking Classses. So, after you are done with creating your model objects, you might create a networking class which could look something like this:

class NetworkManager {
    static let shared = NetworkManager()
    
     func searchRepo(withName name: String, userName: String, completion: @escaping ([Repo]?)->(Void)) {
        ...
        let task = URLSession(configuration: URLSessionConfiguration.default).dataTask(with: requiredURL) {
            (data, response, error) in
            guard let httpResponse = response as? HTTPURLResponse,
                  (200...299).contains(httpResponse.statusCode), let data = data else {
                completion(nil)
                return
            }
            if let decodedResponse = try? JSONDecoder().decode([Repo].self, from: data) {
                completion(decodedResponse)
            }
        }
        task.resume()
    }
}

Or you might use a networking library like Alamofire for this purpose. Either way, the structure remains pretty much the same, a closure which provides you with the result of your call. But while working with SwiftUI, you might find this networking closure to be a bit clunky:

struct YourView: View {
    
    var viewModel: ViewModel
    @State var list: [Repo] = []
    
    var body: some View {
      List(list) { repo in
        //Your row
      }
    }.onAppear {
            viewModel.fetchRepos { (repoList) in
                self.list = repoList
            }
    }
}

You rely on settig a @State property to refresh your view after the closure returns. Also, if you have multiple views which utilize the same datasource, you might end up either making the same API call mutliple times, or building a complex mesh of initialzer where you pass the required object from one view to another.

However, SwiftUI has tools which make it easier for you to share data across views, and make reactive bindings around them to ensure your views update when the data changes. Which brings us to one such tool, the EnvironmentObject

EnvironmentObject and NetworkStore

First, lets build a NetworkStore

import Combine

class NetworkStore: ObservableObject {
    @Published private(set) var repos: [Repo] = []
    @Published private(set) var isLoadingRepos: Bool = false

    func fetch() {
        self.isLoadingRepos = true
        NetworkManager.shared.getRepoList { (repos) -> (Void) in
            if let repos = repos {
                DispatchQueue.main.async {
                    self.repos = repos
                    self.isLoadingRepos = false
                }
            }
        }
    }
}

Calling fetch gets the list of GitHub repos, and assigns it to the repos property. A boolean property isLoadingRepos keeps track of the status of the network call.

NetworkStore is an ObservableObject allowing it to be passed along as an EnvironmentObject. But what exactly is that? From the docs:

An environment object invalidates the current view whenever the observable object changes.

Perfect for our usecase! An environment object can be injected into a view via its parent, or even be made available app-wide.

  • Injecting into a view:

    YourView().environmentObject(NetworkStore())
    
  • Injecting app-wide:

    // Note: The App Struct is a replacement for the AppDelegate, available from iOS 14+. 
    // You can also use the App/Scene Delegate for injecting an environment object.
      
    @main
    struct YourAppName: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .environmentObject(NetworkStore())
            }
        }
    }
    

You can then use it any number of your views:

struct MainView: View {
    @EnvironmentObject var netStore: NetworkStore
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack{
                if netStore.isLoadingRepos {
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle())
                }
                ForEach(netStore.repos) { repo in
                    // add a view
                }
            }
        }.onAppear {
          netStore.fetch()
        }
    }
}

This frees you from making networking call in a specific view. You can call netStore.fetch() from any view, and every view which requires the repository list automatically gets updated, as long as it uses the NetworkStore environment object.


SwiftUI and Combine complement each other in many ways, and can be mixed and matched to build complex, reactive views with reduced efforts. You can check out my GitBrowser app, a simple Github client which displays your followers and public repositories, built on pure SwiftUI (Requires Xcode 12 beta to run).