From a Frontend Perspective: SwiftUI

Written byKalanKalan
💡

If you have any questions or feedback, pleasefill out this form

This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.

Introduction

I have limited experience with iOS development, mobile development, and SwiftUI development, so please feel free to point out any misunderstandings.

From a UI perspective, front-end and mobile development encounter similar issues. Although the languages or development techniques may differ, we all strive to create a user-friendly interface. Given this, we often face similar challenges, such as component-based development, state management, data flow, and managing side effects (like API or IO). For me, this is a great area for mutual learning.

From past experiences, it can be observed that libraries like ReSwift (which embodies the core concepts of Redux) have borrowed elements from the ever-evolving practices of front-end development. It’s clear that both worlds share common challenges.

Although discussing this now feels a bit belated, I still want to share my reflections after getting started with SwiftUI.

Similarities Between SwiftUI and React

We can categorize front-end frameworks into several key elements:

  • Componentization
  • Reactive mechanisms
  • State management
  • Event listening
  • Lifecycle

In the following sections, we will focus on these themes for discussion. To avoid ambiguity, I will primarily use React as an example, but the principles should apply equally to other front-end frameworks.

Moving from Class to Struct; from Class to Function

Writing in SwiftUI often reminds me of the history of React's development. Initially, React established components using JavaScript's class syntax, where each React component was a class.

class MyComponent extends React.Component {
  constructor() {
    this.state = {
      name: 'kalan'
    }
  }
  
  componentDidMount() {
    console.log('component is mounted')
  }
  
  render() {
    return <div>my name is {this.state.name}</div>
  }
}

While defining components through classes greatly impacted front-end componentization, the complex method definitions and confusion around this led to a gradual shift towards using functional components and hooks after React 16.

This shift reduced the cognitive load of constructing components by eliminating inheritance and various OO design patterns. Constructing components became less mentally taxing. We can see a similar evolution in SwiftUI, where the traditionally large ViewController classes—responsible for interactions between views and models and managing lifecycles—have transitioned to more lightweight structs, allowing developers to focus more on UI interactions and reducing cognitive burden.

Component State Management

React 16 introduced hooks for logic reuse and state management, such as useState.

const MyComponent = () => {
  const [name, setName] = useState({ name: 'kalan' })
  useEffect(() => { console.log('component is mounted') }, [])
  
  return <div>my name is {name}</div>
}

In SwiftUI, we can achieve a similar effect using the @State property wrapper, allowing the View to have reactive capabilities. Both frameworks incorporate reactive mechanisms, where changes in state variables are detected and reflected in the UI. Although I'm not sure about the underlying implementation of SwiftUI, it likely employs a similar diffing mechanism to achieve reactivity and minimal updates.

However, state management in SwiftUI differs from React hooks. In React, we can break hooks into independent functions and use them across different components, for example:

function useToggle(initialValue) {
  const [toggle, set] = useState(initialValue)
  const setToggle = useCallback(() => { set((state) => !state) }, [toggle])
  useEffect(() => { console.log('toggle is set') }, [toggle])
  return [toggle, setToggle]
}

const MyComponent = () => {
  const [toggle, setToggle] = useToggle(false)
  return <button onClick={() => setToggle()}>Click me</button>
}

const MyToggle = () => {
  const [toggle, setToggle] = useToggle(true)
  return <button onClick={() => setToggle()}>Toggle, but fancy one</button>
}

In React, we can extract the toggle logic and use it across different components. Since useToggle is a pure function, the internal state does not interfere with one another.

In contrast, in SwiftUI, @State can only be applied to private variables within a struct and cannot be further extracted. If we want to abstract repeated logic, we need to use modifiers like @Observable and @StateObject, which requires creating a separate class to handle it.

class ToggleUtil: ObservableObject {
  @Published var toggle = false
  
  func setToggle() {
    self.toggle.toggle()
  }
}

struct ContentView: View {
  @StateObject var toggleUtil = ToggleUtil()
  var body: some View {
    Button("Text") {
      toggleUtil.setToggle()
    }

    if toggleUtil.toggle {
      Text("Show me!")
    }
  }
}

In this example, separating the toggle logic into a class might seem a bit excessive. However, considering the functionality provided by React hooks, the ability to share lightweight logic without feeling cumbersome is quite appealing. More complex logic can be further broken down into additional hooks, highlighting the excellence of hooks as a mechanism. I wonder if SwiftUI has a similar mechanism. 1

In React, before hooks were introduced, there were primarily three methods for implementing logic reuse:

  • HOC (Higher Order Component) 2: Wrap shared logic in a function that returns a new class, avoiding direct modifications to the internal implementation of components. For instance, the early react-redux connect.
  • Render props 3: Pass the actual rendered component as a prop, providing necessary parameters for implementation.
  • Children function: Only pass the necessary parameters to children, allowing the implementation to determine the components to render.

While hooks elegantly address the problem of logic reuse, I believe the evolution of the above development techniques is also worth considering.

Redux and TCA

Influenced by Redux, some developers in Swift have adopted similar techniques, and there’s even a corresponding implementation described in ReSwift. The documentation reveals the primary reasons. The traditional ViewController has ambiguous responsibilities, leading to bloat and maintenance difficulties. Through Reducer, Action, and Store subscriptions, a unidirectional data flow is ensured; all operations dispatch an action to the store, while data mutations are handled in the reducer.

Recently, the trend seems to have evolved from Redux to TCA (The Composable Architecture), which shares a similar philosophy with Redux but integrates more seamlessly with SwiftUI. A notable difference is that in TCA, the reducer can return an Effect for handling side effects, representing IO operations or API calls to be executed when receiving actions.

Given that similar Redux techniques have been adopted, I wonder if SwiftUI will encounter challenges akin to those faced in front-end development, such as ensuring immutability for observable updates, optimizing subscribe mechanisms so that only the relevant components update when the store changes, and dealing with boilerplate issues arising from reducers and actions.

While Redux still holds a significant position in the front-end landscape and many companies are adopting it, there is an increasing sentiment against Redux, primarily due to its strict adherence to pure functions and the high redundancy of reducers and actions. Before applications reach a certain level of complexity, it is hard to see the benefits it brings. Even the creator of Redux has begun to move away from it. 4 Meanwhile, react-redux continues to receive updates, and the redux-toolkit aims to address common issues encountered when adopting Redux.

In its place, more lightweight state management mechanisms have emerged in the front-end, resulting in several new trends:

Global State Management

In terms of global state management, SwiftUI also includes a built-in mechanism called @EnvironmentObject, which operates similarly to React's context, allowing components to access variables across hierarchy levels. When the context changes, the components also update.

class User: ObservableObject {
  @Published var name = "kalan"
  @Published var age = 20
}

struct UserInfo: View {
  @EnvironmentObject var user: User
  var body: some View {
    Text(user.name)
    Text(String(user.age))
  }
}

struct ContentView: View {
  var body: some View {
    UserInfo()
  }  
}

ContentView().environmentObject(User())

From the example above, we can see that we don't need to pass user to UserInfo separately; we can access the current context through @EnvironmentObject. In React, this would look something like:

const userContext = createContext({})

const UserInfo = () => {
  const { name, age } = useContext(userContext)
  return <>
    <p>{name}</p>
    <p>{age}</p>
  </>
}

const App = () => {
  return (
    <userContext.Provider value={{ user: 'kalan', age: 20 }}>
      <UserInfo />
    </userContext.Provider>
  );
}

React's context allows components to access variables across hierarchy levels, and updates occur when the context changes. While this effectively avoids the problem of prop drilling, the existence of context can complicate testing, as it implies a certain level of coupling.

Reactive Mechanism

In React, changes to state or props trigger component updates, which are reflected in the UI through the framework's diffing mechanism. In SwiftUI, we can observe a similar mechanism:

struct MyView: View {
  var name: String
  @State private var isHidden = false
  
  var body: some View {
    Toggle(isOn: $isHidden) {
      Text("Hidden")
    }

    Text("Hello world")
    
    if !isHidden {
      Text("Show me \(name)")
    }
  }
}

A typical SwiftUI component is a struct, which defines the UI through the body variable. Similar to React, they are both abstract descriptions of the UI, where minimal differences are calculated based on data structure comparisons before updating the screen.

I’m curious to learn how SwiftUI calculates diffs behind the scenes; I hope to see articles on that in the future.

The @State property wrapper defines the internal state of the component, updating and reflecting changes in the UI when the state changes.

In SwiftUI, properties (like name in MyView) can be passed from outside, much like props in React.

// Using MyView in another View
struct ContentView: View {
  var body: some View {
    MyView(name: "kalan")
  }
}

If we were to rewrite this component in React, it would look like this:

const MyView = ({ name }) => {
  const [isHidden, setIsHidden] = useState(false)
  return (
    <div>
      <button onClick={() => setIsHidden(state => !state)}>hidden</button>
      <p>Hello world</p>
      {isHidden ? null : `show me ${name}`}
    </div>
  );
}

When writing SwiftUI, you'll notice that it's quite different from traditional UIKit or UIController development.

Lists

Both SwiftUI and React can render lists, and the syntax is quite similar. In SwiftUI, you can write it like this:

struct TextListView: View {
  var body: some View {
    List {
      ForEach([
        "iPhone",
        "Android",
        "Mac"
      ], id: \.self) { value in
        Text(value)
      }
    }
  }
}

In React, this would roughly translate to:

const TextList = () => {
  const list = ['iPhone', 'Android', 'Mac']
  
  return list.map(item => <p key={item}>{item}</p>)
}

To ensure performance and reduce unnecessary comparisons when rendering lists, React requires developers to provide a key, while SwiftUI has a similar mechanism that requires developers to implement the Identifiable protocol or explicitly pass in an id.

Binding

In addition to binding variables to the UI, we can also bind interactions to variables. For example, in SwiftUI, we can write:

struct MyInput: View {
  @State private var text = ""
  var body: some View {
    TextField("Please type something", text: $text)
  }
}

In this example, even without listening for input events, using $text directly modifies the text variable. When using @State, a property wrapper is added, automatically introducing a prefix $, which is of type Binding.

React does not have a two-way binding mechanism, requiring explicit listening to input events to ensure a unidirectional data flow. However, frameworks like Vue and Svelte do provide two-way binding, saving developers from manually listening for events.

The Emergence of Combine

Although I'm not yet very familiar with Combine, from the official documentation and videos, it seems akin to a Swift specialization of RxJS, offering APIs and operators that significantly simplify complex data flows. This reminds me of my earlier days studying various operations with RxJS and redux-observable, which brings back fond memories.

Fundamental Differences

Having discussed the similarities, it's important to note that web and mobile development still differ significantly. One of the most prominent distinctions for me is static compilation versus dynamic execution. Dynamic execution can be considered one of the greatest features of web development.

As long as there’s a browser, JavaScript, HTML, and CSS can run successfully on any device without needing to pre-download content that ranges from 1 MB to several hundred MB. Web pages can dynamically execute scripts and load content based on the pages being viewed.

Since pre-compilation isn't necessary, anyone can see the web content and execute scripts. Additionally, HTML's streaming capability allows for content to be rendered while still being loaded. A remarkable aspect is that the web is decentralized; as long as there's a server, IP address, and domain, anyone can access the website's content. In contrast, apps must pass review processes before being published.

However, the ecosystems and development methodologies of the two are quite different. It’s still advisable to learn from each other's developments; even if you're not directly involved, viewing things from different perspectives often reveals new insights and can enhance your technical acuity.

Footnotes

  1. Later, I came across SwiftUI-Hooks and am curious about how effective it is in practice.

  2. https://zh-hant.reactjs.org/docs/higher-order-components.html

  3. https://zh-hant.reactjs.org/docs/render-props.html

  4. https://youtu.be/XEt09iK8IXs

If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨

Buy me a coffee