• Stars
    star
    131
  • Rank 275,867 (Top 6 %)
  • Language
  • License
    Creative Commons ...
  • Created over 4 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A cheat sheet that helps React developers to quickly start with SwiftUI.

SwiftUI for React Developers

This is a cheat sheet that helps you React developers to quickly start with SwiftUI.

Note

I assume that you are familiar with React Hooks. For the transformation from Class Components to Hooks, I highly recommend you to visit Thinking in React Hooks, which is a great visualized explanation.

Basics

Building the Contents

One of the core parts of these declarative UI frameworks is its DSL syntax, both of them do provide the special inline syntax for building the content. For React, that calls JSX and need to be transpiled by Babel (with plugins) or tsc. For SwiftUI, it's a built-in syntax in Swift 5.1 called Function Builders.

In React:

const Hello = () => {
  return (
    <div>
      <p>Hello</p>
      <p>React is awesome!</p>
    </div>
  );
};

In SwiftUI:

struct Hello: View {
    var body: some View {
        VStack {
            Text("Hello")
            Text("SwiftUI is awesome!")
        }
    }
}

As you can see, Swift's syntax feels more natural and JSX seems to be more exotic. Actually, Web developers should be more familiar with JSX, after all, it's just like HTML.

Props

Most of components render different contents depend on what input is given to it. That is what props comes to play.

In React:

const Hello = ({name}) => {
  return <p>Hello, {name}!</p>;
};

In SwiftUI:

struct Hello: View {
    let name: String

    var body: some View {
        Text("Hello, \(name)!")
    }
}

Almost the same in semantic!

Conditional & List

Structure of the contents can be dynamic, the most common patterns are conditional and list.

In React:

const UserList = ({ users }) => {
  if (!users.length) {
    return <p>No users</p>;
  }

  return (
    <ul>
      {users.map(e => (
        <li key={e.id}>{e.username}</li>
      ))}
    </ul>
  );
}

In SwiftUI:

struct UserList: View {
    let users: [User]

    var body: some View {
        Group {
            if users.isEmpty {
                Text("No users")
            } else {
                VStack {
                    ForEach(users, id: \.id) {
                        Text("\($0.username)")
                    }
                }
            }
        }
    }
}

SwiftUI has built-in ForEach element, you don't need to manually map the data array to views, so you can have a much neater code.

Events Handling

In React:

const Hello = () => {
  const clickHandler = useCallback(e => {
    console.log('Yay, the button is clicked!');
  }, []);
  return <button onClick={clickHandler}>Click Me</button>;
};

In SwiftUI:

struct Hello: View {
    var body: some View {
        Button("Click Me") {
            print("Yay, the button is clicked!")
        }
    }
}

SwiftUI looks cleaner because there is no useCallback meme. In JavaScript, if you create a function inside another function (let's say foo), the former always has a different reference every time foo is called. That means, the component receives the function as a prop will be rerendered every time.

In consideration of performance, React provided useCallback. It takes a value as dependency, and will return the same reference if the dependency is not changed.

In SwiftUI, Apple have not provided such mechanism, and developers can just take no account of that.

State

Sometimes, a component may retain some internal state even it's get updated by new props. Or it need to update itself without the props changed. State was born for this mission.

The example combines all the things we've talked above. Let's create a simple counter.

In React:

const Counter = ({ initialValue }) => {
  const [counter, setCounter] = useState(initialValue);
  const increaseCounter = useCallback(() => {
    setCounter(counter + 1);
  }, [counter]);

  return (
    <div>
      <p>{counter}</p>
      <button onClick={increaseCounter}>Increase</button>
    </div>
  );
};

In SwiftUI:

struct Counter: View {
    let initialValue: Int

    @State
    var counter: Int

    init(initialValue: Int) {
        self.initialValue = initialValue
        _counter = State(initialValue: initialValue)
    }

    var body: some View {
        VStack {
            Text("\(counter)")
            Button("Increase") {
                self.counter += 1
            }
        }
    }
}

It seems to be a little complicated, let's decompose them into pieces.

The counter has a internal state: counter, and it's initial value is from the input props. In SwiftUI, a state is declared with @State property wrapper. I'll explain that later but now, you could just consider it as a special mark.

The real counter value is wrapped in the _counter member variable (which has type of State<Int>), and we can use the input prop initialValue to initialize it.

We trigger an update by directly setting the counter value. This is not just an assignment, instead, this will cause some logic inside State to take effect and notify the SwiftUI framework to update our view. SwiftUI packed the xxx and setXXX functions into this little syntactic sugar to simplify our code.

Effects

How can we perform some side-effects when the component is updated? In React, we have useEffect:

const Hello = ({ greeting, name }) => {
  useEffect(() => {
    console.log(`Hey, ${name}!`);
  }, [name]);

  useEffect(() => {
    console.log('Something changed!');
  });

  return <p>{greeting}, {name}!</p>;
};

In SwiftUI:

func uniqueId() -> some Equatable {
    return UUID().uuidString  // Maybe not so unique?
}

struct Hello: View {
    let greeting: String
    let name: String

    var body: some View {
        Text("\(greeting), \(name)!")
            .onChange(of: name) { name in
                print("Hey, \(name)!")
            }
            .onChange(of: uniqueId()) { _ in
                print("Something changed!")
            }
    }
}

In SwiftUI, we have neither hook functions nor lifecycle functions, but we have modifiers! Every view type has a lot of modifier functions attached to it.

onChange behaves just like useEffect, the action closure is called every time the value changes and the first time the receiver view renders. But we must pass a value, if you need perform something whenever something changed, you can use a trick:

Create a function that returns an unique object every time it gets called. You can use UUID, global incrementing integer and even timestamps!

Lifecycle Callbacks

In React:

const Hello = () => {
  useEffect(() => {
    console.log('I\'m just mounted!');
    return () => {
      console.log('I\'m just unmounted!');
    };
  }, []);

  return <p>Hello</p>;
};

In SwiftUI:

struct Hello: View {
    var body: some View {
        Text("Hello")
            .onAppear {
                print("I'm just mounted!")
            }
            .onDisappear {
                print("I'm just unmounted!")
            }
    }
}

It's that easy.

Refs

Components can have some internal state that will not trigger view update when it is changed. In React, we have ref:

In React:

const Hello = () => {
  const timerId = useRef(-1);
  useEffect(() => {
    timerId.current = setInterval(() => {
      console.log('Tick!');
    }, 1000);
    return () => {
      clearInterval(timerId.current);
    };
  });

  return <p>Hello</p>;
};

In SwiftUI:

struct Hello: View {
    private class Refs: ObservableObject {
        var timer: Timer?
    }

    @StateObject
    private var refs = Refs()

    var body: some View {
        Text("Hello")
            .onAppear {
                refs.timer =
                    Timer.scheduledTimer(withTimeInterval: 1,
                                        repeats: true) { _ in
                        print("Tick!")
                    }
            }
            .onDisappear {
                refs.timer?.invalidate()
            }
    }
}

And we've got two approaches:

struct Hello: View {
    @State
    private var timer: Timer? = nil

    var body: some View {
        Text("Hello")
            .onAppear {
                self.timer =
                    Timer.scheduledTimer(withTimeInterval: 1,
                                        repeats: true) { _ in
                        print("Tick!")
                    }
            }
            .onDisappear {
                self.timer?.invalidate()
            }
    }
}

You may wonder why setting the state will not lead to view updates. SwiftUI is pretty clever to handle the state, it uses a technique called Dependency Tracking. If you are familiar with Vue.js or MobX, you may understand it immediately. That's say, if we never access the state's value in the view's building process (which not includes onAppear calls), that state will be unbound and can be updated freely without causing view updates.

DOM Refs

Accessing the native DOM object is an advanced but essential feature for Web frontend development.

In React:

const Hello = () => {
  const pEl = useRef();
  useEffect(() => {
    pEl.current.innerHTML = '<b>Hello</b>, world!';
  }, []);

  return <p ref={pEl}></p>;
};

In SwiftUI, we apparently don't have DOM, but for native applications, View is a common concept. We can bridge native views to SwiftUI and gain control of them by the way.

First, let's bridge an existed UIView to SwiftUI:

struct MapView: UIViewRepresentable {
    let mapType: MKMapType
    let ref: RefBox<MKMapView>

    typealias UIViewType = MKMapView

    func makeUIView(context: Context) -> MKMapView {
        return MKMapView(frame: .zero)
    }

    func updateUIView(_ uiView: MKMapView, context: Context) {
        uiView.mapType = mapType
        ref.current = uiView
    }
}

Every time we modified the input props, the updateUIView gets called, we can update our UIView there. To export the UIView instance to the outer, we declare a ref prop, and set it's current property to the view instance whenever the updateUIView gets called.

Now we can manipulate the native view in our SwiftUI views:

struct Hello: View {
    @State
    var mapType = MKMapType.standard

    @StateObject
    var mapViewRef = RefBox<MKMapView>()

    var body: some View {
        VStack {
            MapView(mapType: mapType, ref: mapViewRef)
            Picker("Map Type", selection: $mapType) {
                Text("Standard").tag(MKMapType.standard)
                Text("Satellite").tag(MKMapType.satellite)
                Text("Hybrid").tag(MKMapType.hybrid)
            }
            .pickerStyle(SegmentedPickerStyle())
        }
        .onAppear {
            if let mapView = self.mapViewRef.current {
                mapView.setRegion(.init(center: .init(latitude: 34, longitude: 108),
                                        span: MKCoordinateSpan(latitudeDelta: 50,
                                                               longitudeDelta: 60)),
                                  animated: true)
            }
        }
    }
}

Note that, we'd better encapsulate all the manipulations of native views to a dedicated SwiftUI view. It's not a good practice to manipulate native objects everywhere, as well as in React.

Context

Passing data between the components can be hard, especially when you travel through the hierachy. And Context to the rescue!

Let's look at an example in React:

const UserContext = createContext({});

const UserInfo = () => {
  const { username, logout } = useContext(UserContext);
  if (!username) {
    return <p>Welcome, please login.</p>;
  }
  return (
    <p>
      Hello, {username}.
      <button onClick={logout}>Logout</button>
    </p>
  );
}

const Panel = () => {
  return (
    <div>
      <UserInfo />
      <UserInfo />
    </div>
  );
}

const App = () => {
  const [username, setUsername] = useState('cyan');
  const logout = useCallback(() => {
    setUsername(null);
  }, [setUsername]);
  return (
    <UserContext.Provider value={{ username, logout }}>
      <Panel />
      <Panel />
    </UserContext.Provider>
  );
}

Even if the <UserInfo> is at a very deep position, we can use context to grab the data we need through the tree. And also, contexts are often used by components to communicate with each other.

In SwiftUI:

class UserContext: ObservableObject {
    @Published
    var username: String?

    init(username: String?) {
        self.username = username
    }

    func logout() {
        self.username = nil
    }
}

struct UserInfo: View {
    @EnvironmentObject
    var userContext: UserContext

    var body: some View {
        Group {
            if userContext.username == nil {
                Text("Welcome, please login.")
            } else {
                HStack {
                    Text("Hello, \(userContext.username!).")
                    Button("Logout") {
                        self.userContext.logout()
                    }
                }
            }
        }
    }
}

struct Panel: View {
    var body: some View {
        VStack {
            UserInfo()
            UserInfo()
        }
    }
}

struct App: View {
    @StateObject
    var userContext = UserContext(username: "cyan")

    var body: some View {
        VStack {
            Panel()
            Panel()
        }
        .environmentObject(userContext)
    }
}

Contexts are provided by environmentObject modifier and can be retrieved via @EnvironmentObject property wrapper. And in SwiftUI, context objects can use to update views. We don't need to wrap some functions that modifies the provider into the context objects. Context objects are ObservableObject, so they can notify all the consumers automatically when they are changed.

Another interesting fact is that the contexts are identified by the type of context objects, thus we don't need to maintain the context objects globally.

Implementations Behind the Scene

What are View objects?

In SwiftUI, the View objects are different from the React.Component objects. Actually, there is no React.Component equivalent in SwiftUI. View objects are stateless themselves, they are just like Widget objects in Flutter, which are used to describe the configuration of views.

That means, if you want attach some state to the view, you must mark it using @State. Any other member variables are transient and live shorter than the view. After all, View objects are created and destroyed frequently during the building process, but meanwhile views may keep stable.

How @State works?

To explain this question, you should know what is property wrapper before. This proposal describe that in detail: [SE-0258] Property Wrappers.

Before the View is mounted, SwiftUI will use type metadata to find out all the State fields (backends of the properties marked with @State), and add them to a DynamicPropertyBuffer sequentially, we call this process as "registration".

The buffer is aware of the view's lifecycle. When a new View object is created, SwiftUI enumerates the State fields, and get its corresponding previous value from the buffer. These fields are identified by their storage index in container struct, pretty like how Hook works in React.

In this way, even though the View objects are recreated frequently, as long as the view is not unmounted, the state will be kept.

How function builders works?

As we mention earlier, SwiftUI use Function Builders as DSL to let us build contents. There is also a draft proposal about it: Function builders (draft proposal).

Let's first take a look at how JSX is transpiled to JavaScript. We have this:

const UserInfo = ({ users }) => {
  if (!users.length) {
    return <p>No users</p>;
  }

  return (
    <div>
      <p>Great!</p>
      <p>We have {users.length} users!</p>
    </div>
  );
}

And this is the output from Babel with react preset:

const UserInfo = ({
  users
}) => {
  if (!users.length) {
    return /*#__PURE__*/React.createElement("p", null, "No users");
  }

  return /*#__PURE__*/React.createElement("div", null,
    /*#__PURE__*/React.createElement("p", null, "Great!"),
    /*#__PURE__*/React.createElement("p", null, "We have ", users.length, " users!")
  );
};

Most of the structure is identical, and the HTML tags are transformed to React.createElement calls. That makes sense, the function doesn't produce component instances, instead, it produces elements. Elements describe how to configure components or DOM elements.

Now, let's back to SwiftUI. There is the same example:

struct UserInfo: View {
    let users: [User]

    var body: some View {
        Group {
            if users.isEmpty {
                Text("No users")
            } else {
                VStack {
                    Text("Great!")
                    Text("We have \(users.count) users!")
                }
            }
        }
    }
}

And this is the actual code represented by it:

struct UserInfo: View {
    let users: [User]

    var body: some View {
        let v: _ConditionalContent<Text, VStack<TupleView<(Text, Text)>>>
        if users.isEmpty {
            v = ViewBuilder.buildEither(first: Text("No users"))
        } else {
            v = ViewBuilder.buildEither(second: VStack {
                return ViewBuilder.buildBlock(
                    Text("Great!"),
                    Text("We have \(users.count) users!")
                )
            })
        }
        return v
    }
}

Voila! All the dynamic structures are replaced by ViewBuilder method calls. In this way, we can use a complex type to represent the structure. Like if statement will be transformed to ViewBuilder.buildEither call, and its return value contains the information of both if block and else block.

ViewBuilder.buildBlock is used to represent a child element that contains multiple views.

With function builders, you can even create your own DSLs. And this year in WWDC20, Apple released more features based on function builders, like WidgetKit and SwiftUI App Structure.

How SwiftUI determine when to update a view?

All views in SwiftUI are like PureComponent in React by default. That means, all the member variables (props) will be used to evaluate the equality, of course it's shallow comparison.

What if you want to customize the update strategy? If you take a look at the declaration of View protocol, you will notice this subtle thing:

extension View where Self : Equatable {

    /// Prevents the view from updating its child view when its new value is the
    /// same as its old value.
    @inlinable public func equatable() -> EquatableView<Self>
}

SwiftUI provides an EquatableView to let you achieve that. All you need to do is make your view type conform Equatable and implement the == function. Then wrap it into EquatableView at the call-site.

More Repositories

1

Daysquare

๐Ÿ“† An elegant calendar control for iOS.
Objective-C
702
star
2

SwiftUI-2048

A 2048 game writing with SwiftUI.
Swift
658
star
3

android-FancyBehaviorDemo

A simple demo of customized CoordinatorLayout behavior.
Java
184
star
4

XcodePatch

A patch collection to save your Xcode.
Objective-C
99
star
5

CVPasscodeController

๐Ÿ”’ A native looks passcode input interface.
Swift
98
star
6

SwiftUI-Hooks

A PoC for implementing hooks in SwiftUI
Swift
80
star
7

MetalPlayer

A video player using Metal.
Swift
69
star
8

CyandevToys

๐ŸŽ‰ Sort of fusion code.
JavaScript
65
star
9

android-source-codes

โš™๏ธ Code analysis of common Android projects and components.
Java
59
star
10

ZhihuDaily-iOS

A minimal Zhihu daily app for iOS without any 3rd-party frameworks.
Objective-C
48
star
11

android-SpringAnimator

A framer.js DHO and RK4 spring animation port for Android.
Java
39
star
12

CVTouchHints

Add touch hints to your screencasts in a super easy way.
Objective-C
37
star
13

objc-encodingparser

A cross-platform library to parse Objective-C type encoding.
C
36
star
14

the-silence-campaign

They don't let us talk, they just want to silence us.
JavaScript
34
star
15

SAOLinkStart

A Metal application that mimics SAO "Link Start" scene.
Swift
29
star
16

Snapin

A simple tool to pin screenshots on your screen.
Objective-C
29
star
17

type.js

A tool for creating typewriter simulations.
JavaScript
28
star
18

Waver.js

A Siri like waver library.
JavaScript
23
star
19

StringExplorer

A handy tool to explore various string encoding.
Swift
21
star
20

MacReachability

Using Baidu to determine your network condition without really open it.
Swift
16
star
21

revue

A library for bridging React components to Vue.
TypeScript
13
star
22

Coverflow

A coverflow implementation of Qt Quick
Makefile
12
star
23

Framed

Embed your screenshots into device frames.
Objective-C
11
star
24

MachOInsight

A powerful cross-platform toolkit for inspecting Mach-O files.
C++
11
star
25

XituGold-iOS

a simple gold.xitu.io client for iOS
Swift
11
star
26

CVStackViewSeparator

A drop-in library for adding separators in UIStackView.
Objective-C
11
star
27

ctf-2024

TypeScript
11
star
28

DyldExtractor

A command-line tool to extract dylib files from the dyld shared cache file.
C++
10
star
29

SliderTuner

An easy tool to adjust slider precisely for macOS.
Objective-C
9
star
30

LogDigger

The missing log viewer.
Objective-C
9
star
31

FuckBaiduVIP

A music downloader for downloading those need VIP membership.
Objective-C
7
star
32

QtMaterialDesignComponents

A Qt Quick port of some components using Google Material Design.
QML
6
star
33

libdscoro

๐Ÿš„ The dead-simple stackful coroutine library.
Objective-C
6
star
34

UZDragSortController

Add drag sort behavior to your collection view in a super easy way.
Swift
5
star
35

node-applescript

Run AppleScript through Node.js
Objective-C++
4
star
36

clover-b360m-9700kf-rx590

ๅˆ†ไบซๆˆ‘็š„ Clover ้…็ฝฎ๏ผŒ็กฌไปถๅ…ณ้”ฎ่ฏ๏ผšๆŠ€ๅ˜‰ใ€b360mใ€9700ใ€rx590
4
star
37

fiberize

A library for creating cooperative programs
JavaScript
4
star
38

cyandev.io

My personal website.
TypeScript
4
star
39

Scroll.js

An extremely small but awesome javascript library for web developer.
JavaScript
4
star
40

NavigationController

An iOS-like NavigationController for Qt Quick
4
star
41

Mixture

A declarative, run-time dependency injection framework for Swift.
Swift
4
star
42

opencore-b360m-9700kf-rx590

My Hackintosh configuration with OpenCore and macOS Monterey (GIGABYTE, B360M, i7 9700KF, RX590)
3
star
43

SpringBoard

Launcher page for my web browser
JavaScript
2
star
44

unixzii.github.io

Personal blog
HTML
2
star
45

Gank.io-iOS

A simple Gank.io client for iOS
Swift
2
star
46

game-of-life

A Conway's Game of Life implementation with Rust and WebAssembly.
Rust
2
star
47

vscode-spotlight

A spotlight that help you focus on some lines
TypeScript
2
star
48

Coinstatus

A simple & elegant cryptocurrency status app for iOS / watchOS
Objective-C
2
star
49

my-nvim-config

Lua
1
star
50

Whisper

A message encryption app with ease and fun.
Swift
1
star
51

WinDelete

A simple Win32 program to force deleting files.
C++
1
star
52

iServe

A minimal static files server with GUI manager.
JavaScript
1
star
53

FunctionInterpreter

A simple functional-expression interpreter
C#
1
star
54

Octocat

A lightweight GitHub client for iOS.
Swift
1
star
55

Android-DevStack

Tricks & tips in Android developing
1
star
56

AndroidPanelSlider

A simple library to make panel collapsable.
Java
1
star
57

CardMaker

C++
1
star
58

algo.rs

LeetCode solutions in Rust!
Rust
1
star