Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

License

NotificationsYou must be signed in to change notification settings

unixzii/swiftui-for-react-devs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 

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 thexxxandsetXXX 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.createElementcalls. 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. Likeifstatement 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

Stars

Watchers

Forks


[8]ページ先頭

©2009-2025 Movatter.jp