SwiftUI Architecture: Observable Objects, the Environment and Combine
Tags: ios, swift, swiftui
Published: 2020-02-11 07:53:03

---
title: SwiftUI Architecture: Observable Objects, the Environment and Combine
published: true
description: 
tags: ios, swift, swiftui
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/exsy3k2s6sika99o2x7l.png
series: CometChat SwiftUI course
canonical_url: https://www.cometchat.com/tutorials/swiftui-architecture-observable-objects-the-environment-and-combine-6-7/
---

By now you already have a good understanding of how SwiftUI views work. You create a state variable and a `body` computed variable that spits out a view based on that state. Whenever the state changes, SwiftUI calls `body` and updates what's on the screen.

This paradigm works incredibly well, but there are some special cases in this paradigm that need extra care. One of those cases is when you want to **share a piece of state between two views**.

There are a couple of ways to share state in SwiftUI:

 1. Pass the current value of the state in the initializer.
 2. Use a binding.
 3. Use observable environment objects.

In this part of the SwiftUI course, you'll take a look at these ways to share data. You'll get an understanding of when and how to share data, as well as the advantages and disadvantages of these three approaches.

> As always, you can find a link to the [finished project code on GitHub](https://github.com/cometchat-pro-tutorials/swift-ui-chat).

## Sharing data with View children and stateless Views

First, let's take a look at the simplest possible way to share data: Passing it in the initializer of a view. You already used the pattern when you created the `AvatarView`:

```swift
let isOnline: Bool

init(url: URL?, isOnline: Bool) {
  ...
  self.isOnline = isOnline
  ...
}

var body: some View {
  ...
  Circle()
    .foregroundColor(isOnline ? .green : .gray)
  ...
}
```

You gave `AvatarView` a property called `isOnline` that you pass in its initializer. You made the online indicator green or gray based on this property.

You later called this initializer from `ContactRow` with a value of whether the user is online or not:

```swift
AvatarView(url: nil, isOnline: item.contact.isOnline)
```

Because SwiftUI is reactive, whenever the contact's status changes to offline, SwiftUI will know to rebuild the avatar view with the new value.

This isn't anything fancy, it's just plain old Swift initializers. That's the biggest advantage of this approach: Simplicity. Here, `AvatarView` has no state, all properties are marked with `let`, which means it's **completely immutable**. Instead of responding to state changes, it will be rebuilt when the state changes. This type of immutable view has different names in different reactive frameworks: functional, stateless... dumb. (Yes, dumb.)

Using stateless views has a lot of advantages. For starters, they're simple. You don't have to track or debug state changes. You can easily preview and test the view without having to modify its state. Stateless views are decoupled from your frameworks and libraries, whether it's networking with Alamofire or a database framework like Core Data. They're easy to reuse and even copy and paste to different parts of the app or entirely different projects.

As you can see, *statelessness is a good thing*. You can't always use this pattern, though. To pass state into the initializer, you need to be the view _creating_ the stateless view. If you think of the view hierarchy as a tree, passing state in the initializer is only possible to the view's _direct children_.

![Passing data through the initializer in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/yfju46a9bz5bhx1g26p7.png)

Think hard about whether a view needs to track its internal state variables or if you can get away with passing the state in the initializer. Try to use stateless views as much as you can.

> I am creating this course in collaboration with CometChat, a modern chat platform to help you add chat to you Swift app. During the next few weeks, we’ll be releasing installments of our free SwiftUI course here on dev.to! In the course, you’ll dive deep into SwiftUI by building a real-world production-scale chat app, learning SwiftUI in a practical way, on a scale larger than a simple example app. Follow me to get notified of future parts of this course! You can also follow [@CometChat](https://twitter.com/cometchat) on Twitter see the course of [CometChat’s blog](https://www.cometchat.com/blog/).

[![](https://thepracticaldev.s3.amazonaws.com/i/avpunu4hqp1ut7enf5tq.png)](https://www.cometchat.com)

## Using `Binding` for two-way data sharing between a view and its children

In families, just like in SwiftUI, parents are usually the ones telling kids the state of the world. Sometimes, however, your dad might not know how to install Viber to call your uncle. In that case, you're the one passing information to your parents.

This happens in SwiftUI too. Passing data in the initializer is great if you want to pass data to the view's children. If you want the reverse, however, you need a **binding**.

Bindings enable two-way communication between your views. They're usually used with **interactive views** like text fields, toggles, checkboxes and similar. They allow two views to share a single piece of state.

For instance, when you created `ErrorTextField`, you used a binding to a `String` instead of a state variable.

```swift
struct ErrorTextField: View {
  ...
  let text: Binding<String>
  ...
}
```

Later, you initialized this view in the login screen by passing it the binding:

```swift
struct LoginView: View {
  
  @State private var email = ""
  
  var body: some View {
    ErrorTextField(
      ...
      text: $email,
      ...)
  }
}
```

In the above code, the `$` operator converted a _state_ to a _binding_. A binding is a kind of pointer to a piece of state. `LoginView` owns the state, but by giving `ErrorTextField` a binding to that state, it lets the text field change the value of the state.

This means that both `LoginView` and `ErrorTextField` can change the value of the email, and both will always update to match the latest value.

![Using a Binding to share data between two SwiftUI views](https://dev-to-uploads.s3.amazonaws.com/i/ogpfoiv8k19rtjn1gzm5.png)

Passing bindings, just like passing the state values directly, is used when you want to share state between a parent view and its direct children. As opposed to passing state values, though, a *binding adds statefulness to the component*, making it more complex to read, write, test and preview. Keep that in mind when using this approach.

## Sharing data between unrelated views

The above two ways of sharing data have all been between two directly linked views. Sometimes, you want to share data between two unrelated screens. For instance, the currently logged in user is shared between the feed, the settings screen and the profile screen. Or, there might be a registration flow with multiple screens that stores the data of each screen at a shared location.

One way to do this is to use bindings or passing the user directly to child views. The disadvantage of that approach is that it's far too cumbersome. You'd have to pass the user to each child separately. For any change that one of the child views make, you'd have to roll back up to the main view to update the user, and then propagate back down the hierarchy so all views update.

Instead, it's much easier to have a single place where the user is stored. Whenever any view changes the user, all other views update instantly. 

You can do this type of **global state sharing** using a combination of Combine and SwiftUI features: **Observable objects and environment objects.**

### Creating Combine's Observable Objects in SwiftUI

Even though this is a SwiftUI course, we'll take a brief detour and talk a little about Combine. Combine is a Swift framework that enables you to track changes to some value over time. Instead of using a plain `String`, Combine gives you a `Publisher` that emits a series of strings over time. If you're familiar with RxSwift or ReactiveCocoa, Combine does the same thing, but it's built into Swift.

SwiftUI uses Combine to track changes to state variables. While you might see the email as a `String`, SwiftUI sees it as a series of different string values over time. Using Combine, it can listen for changes to the value and update the view accordingly.

You can do the same thing SwiftUI does by using Combine's `ObservableObject` to track changes to any value, whether it's in a `View` or an entirely different object. `ObservableObject` object tracks changes to its properties and publishes a Combine event whenever a change occurs.

Let's create a new observable object to track the currently logged in user. Create a new plain Swift file and name it **AppStore.swift**. No, you're not building the iOS _App Store_, instead, you're making a shared storage location for different views in your app.

Add the following class to the file:

```swift
import SwiftUI
import Combine

class AppStore: ObservableObject {
  
  struct AppState {
    var currentUser: Contact?
  }
  
  @Published private(set) var state = AppState(currentUser: nil)
  
  func setCurrentUser(_ user: Contact?) {
    state.currentUser = user
  }
  
}
```

First, you import both Combine and SwiftUI. Then, you declare a class that conforms to the `ObservableObject` protocol. Inside the class, you write a nested struct that will hold the current user and create a property of that type.

The `ObservableObject` protocol has one requirement: `objectWillChange`. This is a Combine `Publisher`, an object that fires a series of events whenever the _observed object_ changes. Thankfully, you don't have to implement this yourself.

By marking `state` with `@Published`, you tell Combine to automatically generate an implementation of `objectWillChange` that tracks changes to `state` and publishes an event whenever you change the state. Thanks, Combine!

You also mark the property with `private(set)`. If you haven't seen this before, this is an access control modifier in Swift that makes the property _private for modification_, but _public for access_. This means that everyone can _read_ `state`, but only `AppStore` can _change_ the state. This is just an insurance policy: When dealing with shared state, it's always a good idea to limit how other objects can change that state.

Finally, you add a method that lets users of this class change the state by setting a new user. Whenever this method is called, `currentUser` is changed, and `AppStore` publishes an event telling everyone that's listening that the state has changed.

For instance, a profile screen could listen to these changes and update the user it's showing when a new one is set. Since you can have multiple listeners, different unrelated views can be updated all at once whenever `AppState` changes.

## Adding Observable Objects to the Environment in SwiftUI

Writing the observable object is only one part of the equation, though. You still need a way to create the object and share it with different views in your app. You'll do this using SwiftUI's **Environment**.

Each view and all of its children exist in an environment. The environment is a shared pool of floating objects and values that the view or any of its children can grab and use at any time.

You'll add an instance of `AppStore` to the environment of your root view. This will enable any view in your app to grab the store and its data.

Remember, the `SceneDelegate` is where you create your root view, so open **SceneDelegate.swift** and add a new property:

```swift
let store = AppStore()
```

This is the store you'll add to the environment. Since you need to add it to your root component, call the `environmentObject` method on the `contentView`:

```swift
let contentView: some View = NavigationView {
  WelcomeView()
}.environmentObject(store)
```

This will add the store to the environment.

Now that the store is in there, it's time to fetch it from different views in your app. Start by opening **LoginView.swift**. Add the following property to the struct:

```swift
@EnvironmentObject private var store: AppStore
```

By declaring the property an `@EnvironmentObject`, you tell SwiftUI to automatically look for an object of that type in the environment. No need to manually instantiate or look for it!

At the bottom of `login`, add a line to save a new user in the store:

```swift
store.setCurrentUser(Contact(name: "Me", avatar: nil, id: "me", isOnline: true))
```

When a user logs in, they will get saved in the app store. This same app store is shared with other views, which will update automatically. One of those is `ContactsView`: Open **ContactsView.swift**.

Instead of hard-coding a user, you'll change the view to grab the user in the app store. First, fetch the store the same way you did for `LoginView`:

```swift
@EnvironmentObject private var store: AppStore
```

Then, inside `body`, replace the `ForEach` block with a `ZStack` of the contact row and a navigation link to the chat screen:

```swift
List {
  ForEach(0..<items.count) { i in
    // New code from here
    ZStack {
      ContactRow(item: self.items[i])
      
      self.store.state.currentUser.map { currentUser in
        NavigationLink(destination: ChatView(
          currentUser: currentUser,
          receiver: self.items[i].contact
        )) {
          EmptyView()
        }
      }
    }
    .background(Color.white)
    .shadow(
       color: i == self.items.count - 1 ? Color.shadow : Color.clear,
       radius: 10, x: 0, y: 2)
    .listRowInsets(EdgeInsets())
    // ... to here
    ...
  }
}.navigationBarTitle("Contacts", displayMode: .inline)
```

You might be confused by the use of `map`. `store.state.currentUser` is optional, so you need to unwrap it before you can use it. However, writing `if let` inside a body will result in a compiler error because Swift will get confused about what kind of view you're trying to make. To get around this, you can use `map` on the optional. It will work the same way as `if let`: The function passed to `map` will only get called if the optional is not `nil`. If the current user is `nil`, the function won't get called, and no navigation link will get created.

Build and run the project now.

![Navigating to a detail View from a SwiftUI List](https://dev-to-uploads.s3.amazonaws.com/i/9jhukzoyhfsiq1p7wa6b.gif)

You now have the app store, an observable object, inside the environment. When you log in, the view will update the store in the environment with a new user. Since the store is an observable object, all other views will get notified of the change. This includes `ContactView`, which will use the currently logged in user to create a chat screen.

Sharing data like this is perfect for **global state** of your app, like the currently logged in user, whether or not the user is logged in, does the user have a premium or regular account and other app-level data.

## When to use which approach

Stateless views, bindings, environment objects... All of these approaches have advantages and disadvantages and should be used at different times. To give you a better understanding of when to use which, I prepared a little cheat sheet. Don't worry, I won't take your exam away for using it!

![SwiftUI Architecture Cheat Sheet: A diagram of different ways to pass data between SwiftUI views](https://dev-to-uploads.s3.amazonaws.com/i/zi6ztzbvoqe1eablfsph.png)

## Using Combine to track the keyboard height

Run your app and navigate to the chat screen. When you start typing your message, you'll see the keyboard appear. Only, it appears _on top_ of the text field, so you can't even see what you're typing! That's no good. Let's dive further into Combine with another use case for observable objects: Observing the keyboard.

You'll fix this issue by creating an observable object that will listen to `NotificationCenter` notifications for changes to the keyboard's height. As the keyboard rises or falls, you'll publish events to everyone that's listening.

You'll listen to this event in the chat screen. When the keyboard pops up, you'll increase the padding of the text field so that it's always above the keyboard.

Create a new plain Swift file and call it **KeyboardObserver.swift**. Add a new `ObservableObject` class to the file:

```swift
import Combine
import SwiftUI

class KeyboardObserver: ObservableObject {
  @Published private(set) var keyboardHeight: CGFloat = 0
}
```

Just like earlier, you use `@Published` to automatically publish events whenever that value changes.

You'll update the height based on two notifications: `keyboardWillShowNotification` and `keyboardWillHideNotification`. Notification Center has Combine extensions that use Publishers instead of manually subscribing to the notification. Add the following property to the class:

```swift
let keyboardWillShow = NotificationCenter.default
  .publisher(for: UIResponder.keyboardWillShowNotification)
  .compactMap { ($0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height }
```

You call `publisher(for:)` on the default Notification Center to get a publisher that emits that notification's events. Remember, Combine lets you deal with streams of values over time. These streams, just like arrays, are _collections_. This means that all of the collection methods you're used to, like `map`, `compactMap` and others, are already there. In the above example, you use `compactMap` to get a value from the `userInfo` of the notification that corresponds to the keyboard's height. This makes `keyboardWillShow` a stream of `CGFloat` values over time.

Next, you'll add another property that tracks when the keyboard hides. Add it below the one you just created:

```swift
let keyboardWillHide = NotificationCenter.default
  .publisher(for: UIResponder.keyboardWillHideNotification)
  .map { _ -> CGFloat in 0 }
```

Whenever you receive a notification that the keyboard has hidden, you'll convert that notification to a `CGFloat` of `0`, since a hidden keyboard has no height. Essentially, `keyboardWillHide` is a stream of zeros.

Now that you are publishing events for those two notifications, it's time to _merge_ them together to update the keyboard height property. Add the following initializer to the class:

```swift
init() {
  Publishers.Merge(keyboardWillShow, keyboardWillHide)
    .subscribe(on: RunLoop.main)
    .assign(to: \.keyboardHeight, on: self)
}
```

`keyboardWillShow` and `keyboardWillHide` are both Publishers. Publishers emit values. To listen and respond to those values, you have to _subscribe_ to the publishers. Subscribing allows you to call a function or closure (using the `sink` method), or update a value whenever a Publisher emits a new event (using `assign(to:on:)`).

In the above case, you first merge the two publishers into one. That way, you'll get the keyboard's height when it raises and a zero when it falls. You'll subscribe to the new publisher on the main thread because it's responsible for updating the UI. By calling `assign`, you tell Combine to set `keyboardHeight` whenever to whichever value the Publisher emits.

At this point, you'll get a warning that the result of `assign` is unused. This is because `assign` returns a **cancellable** that you're not using. A cancellable doesn't do much, it's more of a reference to the subscription. If you used Notification Center, you might remember that you need to eventually remove observers to avoid memory loops. The same is true for Combine: If you don't remove subscriptions from `KeyboardObserver`, it will never get deallocated.

If you store the cancellable in a property of the class, the cancellable will get deallocated when the class does. Upon deallocation, the cancellable automatically destroys the subscription, removing all reference cycles that were created and letting the object die peacefully, as opposed to going on forever as a zombie.

Add a new property to the class:

```swift
private var cancellables: Set<AnyCancellable> = []
```

This is a `Set` which will house all of your cancellables for this class. 

Next, add the following line to the bottom of `init`:

```swift
.store(in: &cancellable)
```

This method will store the cancellable in the set. No more warnings, no more memory leaks!

Now that you have the keyboard observer, it's time to use it **ChatView.swift**. Open the file and add a new property to the struct:

```swift
@ObservedObject private var keyboardObserver = KeyboardObserver()
```

By using `@ObservedObject` you tell SwiftUI to update the UI whenever the object changes. Next, use the observer to grab the keyboard's height in `body`:

```swift
ZStack {
  Color.background.edgesIgnoringSafeArea(.top)
  
  VStack {
    List {
      ...
    }
    
    ChatTextField(sendAction: onSendTapped)
      // These two lines are new:
      .padding(.bottom, keyboardObserver.keyboardHeight)
      .animation(.easeInOut(duration: 0.3))
  }
}.navigationBarTitle(Text(receiver.name), displayMode: .inline)
```

When the keyboard pops up, you'll raise the text field by the height of the keyboard. Since everything is in a `VStack`, the `List` will get shorter automatically. You'll also animate this change by calling `animation` after `padding`. SwiftUI will calculate the to and from values to animate without you having to do anything. Pretty neat, right?

![Handle keyboard rising in SwiftUI using Combine](https://dev-to-uploads.s3.amazonaws.com/i/9tg66bwv03kl8u5mj6e2.gif)

Run the project and navigate to the chat screen. As you start typing, you'll see the keyboard pop up and the text field raises to match it. Now you can _see_ what you're typing.

## Conclusion

You are now almost done with your chat app! In this section of the SwiftUI course, you went one step further from static, hard-coded values. You used initializers, bindings and observable objects to pass data between your views and breathe a bit of life into your app.

You learned that passing data in the initializer is the simplest way to give a bit of state to a direct child of a view. You also learned that you can use bindings for two-way communication between a child and its parent. Using environment objects, you added global state to your app that any view can access. Finally, using Combine you converted a Notification Center notification into an observable object that updates the UI. All in a day's work, right?

Now that you have an understanding of how to deal with data, there's only one final step: Getting the data. The next section of the SwiftUI course deals with connecting everything up to CometChat, a chat SDK that will let you send and receive messages over the Internet. Keep reading to see your app come to life!