- Notifications
You must be signed in to change notification settings - Fork15
A cheat sheet that helps React developers to quickly start with SwiftUI.
License
unixzii/swiftui-for-react-devs
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
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 fromClass Components toHooks, I highly recommend you to visitThinking in React Hooks, which is a great visualized explanation.
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 byBabel (with plugins)ortsc. For SwiftUI, it's a built-in syntax in Swift 5.1 calledFunction Builders.
In React:
constHello=()=>{return(<div><p>Hello</p><p>React is awesome!</p></div>);};
In SwiftUI:
structHello:View{varbody:someView{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 justlike HTML.
Most of components render different contents depend on what input is given to it.That is what props comes to play.
In React:
constHello=({name})=>{return<p>Hello,{name}!</p>;};
In SwiftUI:
structHello:View{letname:Stringvarbody:someView{Text("Hello,\(name)!")}}
Almost the same in semantic!
Structure of the contents can be dynamic, the most common patterns are conditionaland list.
In React:
constUserList=({ users})=>{if(!users.length){return<p>No users</p>;}return(<ul>{users.map(e=>(<likey={e.id}>{e.username}</li>))}</ul>);}
In SwiftUI:
structUserList:View{letusers:[User]varbody:someView{Group{if users.isEmpty{Text("No users")}else{VStack{ForEach(users, id: \.id){Text("\($0.username)")}}}}}}
SwiftUI has built-inForEach
element, you don't need to manually map the dataarray to views, so you can have a much neater code.
In React:
constHello=()=>{constclickHandler=useCallback(e=>{console.log('Yay, the button is clicked!');},[]);return<buttononClick={clickHandler}>Click Me</button>;};
In SwiftUI:
structHello:View{varbody:someView{Button("Click Me"){print("Yay, the button is clicked!")}}}
SwiftUI looks cleaner because there is nouseCallback
meme. In JavaScript, ifyou create a function inside another function (let's sayfoo
), the formeralways has a different reference every timefoo
is called. That means, thecomponent receives the function as aprop will be rerendered every time.
In consideration of performance, React provideduseCallback
. It takes a valueasdependency, and will return the same reference if the dependency is notchanged.
In SwiftUI, Apple have not provided such mechanism, and developers can just takeno account of that.
Sometimes, a component may retain some internal state even it's get updated by newprops. Or it need to update itself without the props changed. State was born forthis mission.
The example combines all the things we've talked above. Let's create a simplecounter.
In React:
constCounter=({ initialValue})=>{const[counter,setCounter]=useState(initialValue);constincreaseCounter=useCallback(()=>{setCounter(counter+1);},[counter]);return(<div><p>{counter}</p><buttononClick={increaseCounter}>Increase</button></div>);};
In SwiftUI:
structCounter:View{letinitialValue:Int@Statevarcounter:Intinit(initialValue:Int){self.initialValue= initialValue _counter=State(initialValue: initialValue)}varbody:someView{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 theinput 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 realcounter
value is wrapped in the_counter
member variable (whichhas type ofState<Int>
), and we can use the input propinitialValue
toinitialize it.
We trigger an update by directly setting thecounter
value. This is not justan assignment, instead, this will cause some logic insideState
to take effectand notify the SwiftUI framework to update our view. SwiftUI packed thexxx
andsetXXX
functions into this little syntactic sugar to simplify our code.
How can we perform some side-effects when the component is updated? In React, wehaveuseEffect
:
constHello=({ greeting, name})=>{useEffect(()=>{console.log(`Hey,${name}!`);},[name]);useEffect(()=>{console.log('Something changed!');});return<p>{greeting},{name}!</p>;};
In SwiftUI:
func uniqueId()->someEquatable{returnUUID().uuidString // Maybe not so unique?}structHello:View{letgreeting:Stringletname:Stringvarbody:someView{Text("\(greeting),\(name)!").onChange(of: name){ nameinprint("Hey,\(name)!")}.onChange(of:uniqueId()){ _inprint("Something changed!")}}}
In SwiftUI, we have neither hook functions nor lifecycle functions, but we havemodifiers! Every view type has a lot of modifier functions attached to it.
onChange
behaves just likeuseEffect
, theaction
closure is calledevery time thevalue
changes and the first time the receiver view renders.But we must pass a value, if you need perform something whenever somethingchanged, you can use a trick:
Create a function that returns an unique object every time it gets called. You canuseUUID, global incrementing integer and even timestamps!
In React:
constHello=()=>{useEffect(()=>{console.log('I\'m just mounted!');return()=>{console.log('I\'m just unmounted!');};},[]);return<p>Hello</p>;};
In SwiftUI:
structHello:View{varbody:someView{Text("Hello").onAppear{print("I'm just mounted!")}.onDisappear{print("I'm just unmounted!")}}}
It's that easy.
Components can have some internal state that will not trigger view update when itis changed. In React, we haveref:
In React:
constHello=()=>{consttimerId=useRef(-1);useEffect(()=>{timerId.current=setInterval(()=>{console.log('Tick!');},1000);return()=>{clearInterval(timerId.current);};});return<p>Hello</p>;};
In SwiftUI:
structHello:View{privateclassRefs:ObservableObject{vartimer:Timer?}@StateObjectprivatevarrefs=Refs()varbody:someView{Text("Hello").onAppear{ refs.timer=Timer.scheduledTimer(withTimeInterval:1, repeats:true){ _inprint("Tick!")}}.onDisappear{ refs.timer?.invalidate()}}}
And we've got two approaches:
structHello:View{@Stateprivatevartimer:Timer?=nilvarbody:someView{Text("Hello").onAppear{self.timer=Timer.scheduledTimer(withTimeInterval:1, repeats:true){ _inprint("Tick!")}}.onDisappear{self.timer?.invalidate()}}}
You may wonder why setting the state will not lead to view updates. SwiftUI ispretty clever to handle the state, it uses a technique calledDependency Tracking. If you are familiar withVue.js orMobX, you mayunderstand it immediately. That's say, if we neveraccess the state's value inthe view's building process (which not includesonAppear
calls), that statewill be unbound and can be updated freely without causing view updates.
Accessing the native DOM object is an advanced but essential feature for Webfrontend development.
In React:
constHello=()=>{constpEl=useRef();useEffect(()=>{pEl.current.innerHTML='<b>Hello</b>, world!';},[]);return<pref={pEl}></p>;};
In SwiftUI, we apparently don't have DOM, but for native applications,View isa common concept. We can bridge native views to SwiftUI and gain control of them bythe way.
First, let's bridge an existedUIView
to SwiftUI:
structMapView:UIViewRepresentable{letmapType:MKMapTypeletref:RefBox<MKMapView>typealiasUIViewType=MKMapViewfunc makeUIView(context:Context)->MKMapView{returnMKMapView(frame:.zero)}func updateUIView(_ uiView:MKMapView, context:Context){ uiView.mapType= mapType ref.current= uiView}}
Every time we modified the input props, theupdateUIView
gets called, we canupdate ourUIView
there. To export theUIView
instance to the outer, wedeclare a ref prop, and set it'scurrent
property to the view instancewhenever theupdateUIView
gets called.
Now we can manipulate the native view in our SwiftUI views:
structHello:View{@StatevarmapType=MKMapType.standard@StateObjectvarmapViewRef=RefBox<MKMapView>()varbody:someView{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{iflet 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 adedicated SwiftUI view. It's not a good practice to manipulate native objectseverywhere, as well as in React.
Passing data between the components can be hard, especially when you travelthrough the hierachy. AndContext to the rescue!
Let's look at an example in React:
constUserContext=createContext({});constUserInfo=()=>{const{ username, logout}=useContext(UserContext);if(!username){return<p>Welcome, please login.</p>;}return(<p> Hello,{username}.<buttononClick={logout}>Logout</button></p>);}constPanel=()=>{return(<div><UserInfo/><UserInfo/></div>);}constApp=()=>{const[username,setUsername]=useState('cyan');constlogout=useCallback(()=>{setUsername(null);},[setUsername]);return(<UserContext.Providervalue={{ username, logout}}><Panel/><Panel/></UserContext.Provider>);}
Even if the<UserInfo>
is at a very deep position, we can use context to grabthe data we need through the tree. And also, contexts are often used by componentsto communicate with each other.
In SwiftUI:
classUserContext:ObservableObject{@Publishedvarusername:String?init(username:String?){self.username= username}func logout(){self.username=nil}}structUserInfo:View{@EnvironmentObjectvaruserContext:UserContextvarbody:someView{Group{if userContext.username==nil{Text("Welcome, please login.")}else{HStack{Text("Hello,\(userContext.username!).")Button("Logout"){self.userContext.logout()}}}}}}structPanel:View{varbody:someView{VStack{UserInfo()UserInfo()}}}structApp:View{@StateObjectvaruserContext=UserContext(username:"cyan")varbody:someView{VStack{Panel()Panel()}.environmentObject(userContext)}}
Contexts are provided byenvironmentObject
modifier and can be retrieved via@EnvironmentObject
property wrapper. And in SwiftUI, context objects can useto update views. We don't need to wrap some functions that modifies the providerinto the context objects. Context objects areObservableObject
, so they cannotify all the consumers automatically when they are changed.
Another interesting fact is that the contexts are identified by the type ofcontext objects, thus we don't need to maintain the context objects globally.
In SwiftUI, theView
objects are different from theReact.Component
objects.Actually, there is noReact.Component
equivalent in SwiftUI.View
objectsare stateless themselves, they are just likeWidget
objects in Flutter, whichare 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 buildingprocess, but meanwhile views may keep stable.
To explain this question, you should know what isproperty wrapper
before.This proposal describe that in detail:[SE-0258] Property Wrappers.
Before theView
is mounted, SwiftUI will use type metadata to find out all theState
fields (backends of the properties marked with@State
), and add themto aDynamicPropertyBuffer
sequentially, we call this process as "registration".
The buffer is aware of the view's lifecycle. When a newView
object is created,SwiftUI enumerates theState
fields, and get its corresponding previous valuefrom the buffer. These fields are identified by their storage index in containerstruct, pretty like howHook works in React.
In this way, even though theView
objects are recreated frequently, as long asthe view is not unmounted, the state will be kept.
As we mention earlier, SwiftUI useFunction Builders as DSL to let us buildcontents. 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:
constUserInfo=({ 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 withreact
preset:
constUserInfo=({ 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 toReact.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:
structUserInfo:View{letusers:[User]varbody:someView{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:
structUserInfo:View{letusers:[User]varbody:someView{letv:_ConditionalContent<Text,VStack<TupleView<(Text,Text)>>>if users.isEmpty{ v=ViewBuilder.buildEither(first:Text("No users"))}else{ v=ViewBuilder.buildEither(second:VStack{returnViewBuilder.buildBlock(Text("Great!"),Text("We have\(users.count) users!"))})}return v}}
Voila! All the dynamic structures are replaced byViewBuilder
method calls. Inthis way, we can use a complex type to represent the structure. Likeif
statement will be transformed toViewBuilder.buildEither
call, and its returnvalue contains the information of bothif
block andelse
block.
ViewBuilder.buildBlock
is used to represent a child element that containsmultiple views.
With function builders, you can even create your own DSLs. And this year in WWDC20,Apple released more features based on function builders, likeWidgetKit andSwiftUIApp Structure.
All views in SwiftUI are likePureComponent in React by default. That means,all the member variables (props) will be used to evaluate the equality, of courseit's shallow comparison.
What if you want to customize the update strategy? If you take a look at thedeclaration ofView
protocol, you will notice this subtle thing:
extensionViewwhere Self:Equatable{ /// Prevents the view from updating its child view when its new value is the /// same as its old value.@inlinablepublicfunc equatable()->EquatableView<Self>}
SwiftUI provides anEquatableView
to let you achieve that. All you need to dois make your view type conformEquatable
and implement the==
function.Then wrap it intoEquatableView
at the call-site.
About
A cheat sheet that helps React developers to quickly start with SwiftUI.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.