Development

Intro to SwiftUI State Management for React Developers

With React development experience, learn how to apply patterns and concepts from React to SwiftUI with little mental overhead.

8 min
February 24, 2022
Patrick O'Sullivan
Senior Developer

As you may already know Apple has released their own declarative, composable, state-driven UI library: SwiftUI.

SwiftUI was clearly created as a response to the overwhelming growth in popularity of React (and its mobile variant, React Native), which is the declarative, composable, state-driven UI framework released by Facebook in 2010.

In these libraries the view layer is composed of a tree of independent components that all have either their own state or inherited state from elsewhere in the tree. All UI views are a representation of the app's state and all changes in the UI are a function of changes in the state.

Applying React patterns to SwiftUI

As a React developer you're intimately familiar with managing state in React and the basic "built-in" patterns and structures available, such as the local state management hooks `useState` and `useEffect`, shared or global state with custom hooks, providers, and contexts.

You may also be familiar with the Redux pattern of defining a globally accessible state object and a reducer to update that state with defined actions.

All of these patterns and concepts are easily mapped onto SwiftUI with very little mental overhead.

Let's look at each of the basic React patterns I mentioned one-by-one and talk about how to re-implement them in SwiftUI.


## Local State

With a background in React development, understanding local state in SwiftUI should be very straightforward. In the case of local state, most of the differences are very minor and mostly have to do with the differences between the underlying languages.

#### One major difference

In SwiftUI you do not define an explicit setting function to update a state variable; instead, you mutate that variable directly.

The way SwiftUI handles lifecycle updates and side-effects is also a bit different.



### Declaration

   -- CODE line-numbers language-javascript --

   <!--

       // In React, we call a built-in hook called useState to define a local state variable and a function to set that variable.

       const [isShown, setIsShown] = useState(false)

   -->


#### In SwiftUI


   -- CODE line-numbers language-swift --

   <!--

       // In Swift, we instead use the @State property wrapper to denote that this is a special type of variable.

       // We'll see more property wrappers later.

       @State private var isShown = false

   -->


### Updating

   -- CODE line-numbers language-javascript --

   <!--

       <button onClick={() => setIsShown(!isShown)}> Click! </button>

   -->


#### In SwiftUI


   -- CODE line-numbers language-swift --

   <!--

       Button(action: { isShown.toggle() }) {

         Text("Click!")

       }

   -->


### Props ("Parameters" in SwiftUI) and Binding

Parameters, the SwiftUI term analogous to React's "props", are passed to child components in a way that should be familiar to you.



   -- CODE line-numbers language-swift --

   <!--

       SomeComponent(isShown: isShown)

   -->


One major difference is that in SwiftUI a child can mutate the state variable of a parent via explicit binding (with a `$`):


   -- CODE line-numbers language-swift --

   <!--

       struct SomeParent: View {

         @State var isShown: Bool

         

         var body: some View {

           Group {

             SomeChild(isShown: $isShown)

           }

         }

       }

       

       struct SomeChild: View {

         @Binding var isShown = false

         

         var body: some View {

           Group {

             // Note that this would not work without the explicit binding.

             Button(action: { isShown.toggle() }) {

               Text("Click!")

             }

           }

         }

       }

   -->


In React this would also be made explicit, though it would be through passing the setState function to the child (since in React we never directly change state variables themselves).


   -- CODE line-numbers language-javascript --

   <!--

       const SomeParent = () => (

         <>

           <SomeChild setIsShown={setIsShown} />

         </>

       )

       

       const SomeChild = ({ setIsShown }) => (

         <>

           <button onClick={() => setIsShown(false)} >

         </>

       )

   -->



### Other Props

Other props/parameters work just as you might expect them to.


   -- CODE line-numbers language-javascript --

   <!--

       const CustomButton = ({color, label = "default label", action}) => (

         <>

           <button style={{backgroundColor: color}} onClick={() => action()}> {label} </button>

         </>

       )

   -->


#### In SwiftUI


   -- CODE line-numbers language-swift --

   <!--

       struct CustomButton: View {

         var color: UIColor // passing a parmeter of a specified type with a default.

         var label: String = "default label" //passing with a default.

         var action: () -> Void // specifying that a closure is expected.

         

         var body: some View {

           Button(action: $action) {

             Text(label)

           }

           .backgroundColor(color)

         }

       }

   -->



### Effects and Lifecycle (where it gets weird)

The patterns themselves are very different, but the overall idea of handling the life cycle of a component is similar:


   -- CODE line-numbers language-javascript --

   <!--

       // In React, we use a built-in hook called "useEffect" to react to changes in state and trigger effects as necessary.

       

       const FancyButton = () => {

         const [clicked, setClicked] = useState(false)

         

         

         useEffect(() => {

         // Do something when the component mounts.

         }, [])

       

       

         useEffect(() => {

         // Do something when clicked changes.

         }, [clicked])

       

         return <button onClick={() => setClicked(!clicked)}>some button</button>

       }

   -->


#### In SwiftUI


   -- CODE line-numbers language-swift --

   <!--

       struct FancyButton: View {

         let color: UIColor = .blue

         

         var body: some View {

           VStack {

             Button()

             .onAppear {

               // Do somethign when the component mounts

             }

           }

           .onChange(of: color) {

             // Do something when color changes.

           }

           HStack {

           Text("Hi")

             .task(id: color) {

               // run some async code when color changes.

             }

           }

           .task {

             // Run some async code on appear

           }

         }

         

       }

   

   -->



## Global/Shared State (Combine)

These first few examples were very straightforward. Things change a bit as we move into global and shared state. In 2019 Apple debuted Combine. Combine is a reactive programming framework for representing changes in values (asynchronously) in your app.

### Combine consists of two core object abstractions:

#### 1. Publishers

These classes make data available to other classes.

#### 2. Subscribers

These make requests to and watch publisher classes for updates to data.


You'll find this paradigm very similar to React's custom hooks and contexts patterns.


### Custom Hooks, Context (Observable objects in SwiftUI)

SwiftUI's equivalent to a "custom hook" is something called an Observable Object. It's best used for staring state in particular branches of a component tree.


   -- CODE line-numbers language-javascript --

   <!--

       const defaultState = {

         ready: false,

         color: ""

       }

       

       const SharedStateContext = createContext(defaultState)

       

       const SharedStateProvider = ({children}) => {

         const [ready, setReady] = useState(false)

         const [color, setColor] = useState("blue")

         

         const { Provider } = SharedStateContext()

         

         return (

           <Provider

             value={{

               ready,

               setReady,

               color,

               setColor

             }}

           >

             {children}

           </Provider>

         )

       }

   

       const useSharedState = () => useContext(SharedStateContext)

       

       const someParentComponent = () => (

         <>

           <SettingsProvider>

             <SomeComponent/>

             <SomeOtherComponent/>

           </SettingsProvider>

         </>

       )

       

       const SomeComponent = () => {

         const {ready, setReady} = useSharedState()

         

         useEffect(() => {

           if(!ready) {

             setReady(true)

           }

         }, [ready])

         

         return (

           <>

             "Hi"

           </>

         )

         

       }

   

   -->


#### In SwiftUI


   -- CODE line-numbers language-swift --

   <!--

       class SomeViewModel: ObservableObject {

         @Published var ready = false

       

       func makeReady() {

         self.ready = true

       }

       

       func makeNotReady() {

         self.ready = false

       }

       }

       

       class SomeView: View {

       @StateObject var someVM = SomeViewModel()

       

       var body: some View {

         HStack {

         Text("Hi")

         }

           .onAppear {

             if someVM.ready == false {

               someVM.makeReady()

             }

           }

         }

       }

   

   -->



#### Types of Observable Objects:

`@ObservedObjects` were the first type of observable object available in SwiftUI. These objects are recreated (and their values reset) each time the view that instantiates them is remounted.


Swift's `@EnvironmentObject` property wrapper works similarly to `@ObservedObject`, but makes data available to all views within your application and the values are persistent across all component mounts/remounts.


`@StateObject` is nearly identitical to `@ObservedObject`, except that while an `@ObservedObject` is recreated each time a subscribing view is mounted, a `@StateObject` will persist values across mounts. For this reason, Apple recommends using `@StateObject`s.


#### Redux pattern

We can use SwiftUI's @EnvironmentObjects to create a global state object whose values are only mutable via actions dispatched to a reducer, similar to the Redux pattern you may be familiar with in React. This method requires some boilerplate upfront, but allows for fairly ergonomic state interaction throughout your app later on.


#### The boilerplate:


   -- CODE line-numbers language-swift --

   <!--

       stuct YourAppState {

         var loggedIn: Bool = false

         var user: User?

         var errorMessage: String = ""

       }

       

       struct YourAppActions {

         case login

         case logout

         case setUser(UUID)

         case setErrorMessage(String)

       }

       

   

       typealias YourAppStore = Store<YourAppState, YourAppActions>

       

       class Store<State, Action>: ObservableObject {

       

       @Published private(set) var state: State

         private let reducer: Reducer<State, Action>

         private let queue = DispatchQueue(

         label: "com.your.app",

         qos: .userInitiated)

       

         init(

           initial: State,

           reducer: @escaping Reducer<State, Action>

         ) {

           self.state = initial

           self.reducer = reducer

         }

       

       func dispatch(_ action: Action) {

         queue.sync {

           self.dispatch(**self**.state, action)

         }

       }

       

       private func dispatch(_ currentState: State, _ action: Action) {

         let newState = reducer(currentState, action)

         state = newState

         }

       }

   

     

   

       extension Store {

       func binding<Value>(

         for keyPath: KeyPath<State, Value>,

         transform: @escaping (Value) -> Action

         ) -> Binding<Value> {

         Binding<Value>(

         get: { self.state[keyPath: keyPath] },

         set: { self.dispatch(transform($0)) }

         )

       }

       }

       

       typealias Reducer<State, Action> = (State, Action) -> State

   

   

       let YourAppReducer: Reducer<YourAppState, YourAppActions> = { state, action in

         var mutatingState = state

                                     

         switch action {

           case .login:

             mutatingState.loggedIn = true

           case .logOut:

             mutatingState.loggedIn = false

           case .setUser(let newUserId):

             mutatingState.user = newUserId

           case .setErrorMessage(let errorMessage):

             mutatingState.errorMessage = errorMessage

           }

                                     

         return mutatingState

       }

   

   -->



### Using the Redux pattern within your app


   -- CODE line-numbers language-swift --

   <!--  

       struct SomeView: View {

         @EnvironmentObject private var store: YourAppStore

         

         var body: some View {

           VStack {

             if store.state.loggedIn {

               Text("Hi, logged in user")

             } else {

               Text("Hi, logged out user")

               Button(action: { store.dispatch.login() }) {

                 Text("Log in")

               }

             }

           }

         }

         

       }

   -->



Conclusion

Hopefully this post has adequately demonstrated that, despite the changes in syntax and boilerplate code, the actual use of state in SwiftUI is not all the different from React. This post is only intended to cover basic state concepts and does not cover app or project structure generally.

If you're interested in diving deeper into mobile app architecture you should learn more about MVC (Model, View, Controller), MVVM (Model, View, ViewModel), VIPER (View, Interactor, Presenter, Routing, Entity).

At the very least, any React developer should be able to pick up and use these concepts immediately in a SwiftUI application regardless of the app architecture chosen for the project.

Actionable UX audit kit

  • Guide with Checklist
  • UX Audit Template for Figma
  • UX Audit Report Template for Figma
  • Walkthrough Video
By filling out this form you agree to receive our super helpful design newsletter and announcements from the Headway design crew.

Create better products in just 10 minutes per week

Learn how to launch and grow products less chaos.

See what our crew shares inside our private slack channels to stay on top of industry trends.

By filling out this form you agree to receive a super helpful weekly newsletter and announcements from the Headway crew.