SwiftUI - Communicating with the UI

Apple provides three means of communication with different scopes.

@State
@ObservableObject
@EnvironmentObject

The @State and @Environment attributes belong to the SwiftUI framework.

The @ObservableObject attribute is part of the Combine framework, Apple’s native solution to reactive programming. Both frameworks are tightly related.

While the @State attribute is meant to be used on a local scope at your view level (part 2 of this series), the @ObservableObject and @EnvironmentObject can be used through all your views.

class Navigator: ObservableObject {
    enum ModalType {
        case preferences, details
    }
    var presenting: ModalType = .preferences {
        willSet {
            showSheet = true
        }
    }
    @Published var showSheet: Bool = false
}

Our navigator class conforms to the ObservableObject protocol and contains the showSheet property marked with the @Published attribute.

The @Published attribute tells the system the showSheet property must be observed. As a result, whenever the Bool value is toggled, changes will be applied to the views observing the navigator object. (If no property is marked as @Published, nothing will happen since there’s nothing to observe).

Changes within an observable object will automatically be reflected to the views observing that object through the @ObservedObject or the @EnvironmentObject attributes. The environment object must be attached to the hosting controller’s root view in the SceneDelegate class.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    var navigator = Navigator()
    func scene(
        _ scene: UIScene, 
        willConnectTo session: UISceneSession, 
        options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene { 
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(
                rootView: ContentView()
                             .environmentObject(navigator)
            )
            self.window = window
            window.makeKeyAndVisible()
        }
     }
}

The object is initialized only once, there’s no need to pass it around afterwards, it will be accessible by all your views (with same ancestor) using the following declaration.

struct ContentView: View {
    @EnvironmentObject var navigator: Navigator
    var body: some View {
        Text("Communicating with the UI")
    }
}

While the @EnvironmentObject attribute is convenient when all of your views need to access a specific object, @ObservedObject may be a better fit if only few of your views need to observe that specific object. Observed objects are initialized and passed around by reference through your views.

struct ContentView: View {
     @ObservedObject var viewModel = HeadlinesViewModel(
         preferences: UserPreferences()
     )
     var body: some View {
         ContentDetailView(viewModel: viewModel)
     }
}
struct ContentDetailView: View {
    var viewModel: HeadlinesViewModel
    var body: some View {
        Text(viewModel.keyword)
    }
}

@State provides a mean of local communication. @EnvironmentObject and @ObservableObject provides a mean of communication with outer objects.