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

// 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

// 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

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

In SwiftUI

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.

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 `$`):

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).

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.

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

  <>

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

  </>

)

In SwiftUI

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:

// 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

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.

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

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:

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

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 with 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.