Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/unixzii/swiftui-for-react-devs

A cheat sheet that helps React developers to quickly start with SwiftUI.
https://github.com/unixzii/swiftui-for-react-devs

cheatsheet ios javascript react swift swiftui swiftui-learning tutorial

Last synced: about 2 months ago
JSON representation

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

Awesome Lists containing this project

README

        

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

.. contents:: :local:

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:

.. code-block:: javascript

const Hello = () => {
return (


Hello


React is awesome!



);
};

In SwiftUI:

.. code-block:: swift

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:

.. code-block:: javascript

const Hello = ({name}) => {
return

Hello, {name}!

;
};

In SwiftUI:

.. code-block:: swift

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:

.. code-block:: javascript

const UserList = ({ users }) => {
if (!users.length) {
return

No users

;
}

return (

    {users.map(e => (
  • {e.username}

  • ))}

);
}

In SwiftUI:

.. code-block:: swift

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:

.. code-block:: javascript

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

In SwiftUI:

.. code-block:: swift

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:

.. code-block:: javascript

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

return (


{counter}


Increase

);
};

In SwiftUI:

.. code-block:: swift

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

.. code-block:: javascript

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

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

return

{greeting}, {name}!

;
};

In SwiftUI:

.. code-block:: swift

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:

.. code-block:: javascript

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

return

Hello

;
};

In SwiftUI:

.. code-block:: swift

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:

.. code-block:: javascript

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

return

Hello

;
};

In SwiftUI:

.. code-block:: swift

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:

.. code-block:: swift

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:

.. code-block:: javascript

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

return

;
};

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:

.. code-block:: swift

struct MapView: UIViewRepresentable {
let mapType: MKMapType
let ref: RefBox

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:

.. code-block:: swift

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

@StateObject
var mapViewRef = RefBox()

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:

.. code-block:: javascript

const UserContext = createContext({});

const UserInfo = () => {
const { username, logout } = useContext(UserContext);
if (!username) {
return

Welcome, please login.

;
}
return (


Hello, {username}.
Logout


);
}

const Panel = () => {
return (





);
}

const App = () => {
const [username, setUsername] = useState('cyan');
const logout = useCallback(() => {
setUsername(null);
}, [setUsername]);
return (




);
}

Even if the ```` 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:

.. code-block:: swift

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:

.. code-block:: javascript

const UserInfo = ({ users }) => {
if (!users.length) {
return

No users

;
}

return (

Great!


We have {users.length} users!



);
}

And this is the output from Babel with ``react`` preset:

.. code-block:: javascript

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:

.. code-block:: swift

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:

.. code-block:: swift

struct UserInfo: View {
let users: [User]

var body: some View {
let v: _ConditionalContent>>
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:

.. code-block:: swift

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
}

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.

.. References:

.. _`Thinking in React Hooks`: https://wattenberger.com/blog/react-hooks
.. _`[SE-0258] Property Wrappers`: https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md
.. _`Function builders (draft proposal)`: https://github.com/apple/swift-evolution/blob/9992cf3c11c2d5e0ea20bee98657d93902d5b174/proposals/XXXX-function-builders.md