当前位置: 首页 > 科技观察

从前端角度看SwiftUI

时间:2023-03-13 13:07:33 科技观察

前言本人iOS开发、手机开发、SwiftUI开发经验有限。如果我理解有误,请纠正我。从UI的角度来说,前端和手机开发遇到的问题是相似的。尽管使用的语言或开发方式不同,但我们都需要创建一个易于使用的用户界面。既然如此,彼此就会遇到类似的问题。组件开发、状态管理、数据流、管理副作用(API或IO)等等,是一个非常适合我相互学习的领域。从以往的经验中可以发现,ReSwift[1](Redux的中心思想)等库或多或少借鉴了前端不断演进的开发方式。不难看出,双方会遇到的问题其实都是相似的地方。虽然说这个时间点有点晚,但是还是想写下我上手SwiftUI后的感想。SwiftUI和React的相同点我们可以将前端框架概括为几个要素:组件响应机制状态管理事件监控生命周期在后面的段落中,我们也将以这些话题为核心进行讨论。为了不模糊重点,我会尽量只以React为例,但应用于其他前端框架时,原理应该是一样的。从类到结构;从类到函数,总让我想起写SwiftUI时React的发展历程。React最初创建组件的方式是通过JavaScript的类语法,每个React组件都是一个类别。classMyComponentextendsReact.Component{constructor(){this.state={name:'kalan'}}componentDidMount(){console.log('componentismounted')}render(){return

mynameis{this.state.name}
}}虽然通过类别定义组件对前端组件化影响很大,但也因为繁琐的方法定义与this混淆,在React16hooks出现后,使用功能组件和钩子来创建组件。消除了继承和各种OO花哨的设计模式,构建组件的精神负担变小了。从SwiftUI中,我们也可以看到类似的演变。本来ViewController庞大的类和职责就是负责view和model的交互,负责生命周期。它被转化为更轻量级的结构体,以便开发人员可以更专注于UI交互。,以减少认知负荷。组件状态管理React16采用hooks对组件进行逻辑复用和状态管理,例如useState。constMyComponent=()=>{const[name,setName]=useState({name:'kalan'})useEffect(()=>{console.log('组件已挂载')},[])return
mynameis{name}
}在SwiftUI中,我们可以使用修饰符@State来让View有类似的效果。两者都有响应机制。当状态变量发生变化时,React/Vue会检测到变化并将其反映在屏幕上。虽然不知道SwiftUI背后的实现,但应该是背后有类似diff机制的东西,达到响应式机制和最小更新的效果。但是,SwiftUI的状态管理与Reacthooks还是有区别的。在React中,我们可以将hook拆分成独立的函数,在不同的组件中使用,例如:(state)=>!state)},[toggle])useEffect(()=>{console.log('toggleisset')},[toggle])return[toggle,setToggle]}constMyComponent=()=>{const[toggle,setToggle]=useToggle(false)returnsetToggle()}>点我}constMyToggle=()=>{const[toggle,setToggle]=useToggle(true)returnsetToggle()}>Toggle,butfancyone}在React中,我们可以分离toggle的逻辑并将其放在不同的元素之间使用。由于useToggle是一个纯函数,内部状态不会相互影响。但是在SwiftUI中,@State只能用在struct的privatevar中,不能再去掉。如果要抽取重复逻辑,需要使用@Observable、@StateObject等修饰符,另外创建一个类来处理。类ToggleUtil:ObservableObject{@Publishedvartoggle=falsefuncsetToggle(){self.toggle=!self.toggle}}structContentView:View{@StateObjectvartoggleUtil=ToggleUtil()varbody:someView{Button("Text"){toggleUtil.setToggle()}iftoggleUtil.toggle{Text("Showme!")}}}在这个例子中,将toggle的逻辑拆分到一个类中似乎有点矫枉过正,但仔细想想React是什么提供了hook功能,使得轻量级的逻辑共享,即使单独拆分成hooks,也不会觉得太繁琐。如果要封装更复杂的逻辑,也可以拆分成更多的hooks。从这一点来看,hook确实是一个非常优秀的。机制。后来看到了SwiftUI-Hooks[2],但不知道实际效果如何。以React为例,在hooks出现之前,实现逻辑共享的方式主要有以下三种:HOC(HigherOrderComponent)[3]:将公共逻辑包装成一个函数并返回一个新的类,避免直接修改内部组件的做法.例如在早期的react-redux中连接。renderprops[4]:将实际渲染的组件作为props传入,提供必要的参数供实现端使用。children函数:children只传入必要的参数,渲染哪些组件由实现者决定。虽然hooks以更优雅的方式解决了逻辑共享的问题,但我认为上述开发方式的演进也值得参考。Redux和TCA都受到了Redux的影响。在Swift中,一些开发者也采用了类似的做法,甚至有相应的ReSwift实现。从描述中可以看出主要原因。传统的ViewController职责不明,容易肥大难维护。通过Reducer、Action和Store订阅确保单向数据流。所有操作都作为一个动作分派到商店,数据更改(突变)在减速器中。处理。最近的趋势似乎是从Redux进化到了TCA(TheComposableArchitecture),它与Redux的中心思想相似,更容易与SwiftUI集成。不同的是,以往涉及副作用的操作在Redux中会统一由中间件处理,而在TCA架构中,reducer可以返回一个Effect,代表接收到action时要执行的IO操作或API调用。现在采用类似redux的方式,不知道SwiftUI会不会遇到前端开发类似的问题,比如不可变性保证更新可以被感知;通过优化订阅机制,保证store更新时只更新对应的组件;reducers和actions带来了样板文件问题。虽然Redux在前端还是有一定地位的,很多公司还在导入,但是前端放弃Redux的声音越来越多,主要是因为redux追求纯函数和reducer的高重复性和行动。在应用达到一定的复杂度之前很难看到好处,甚至连Redux作者自己也开始放弃redux4。同时,react-redux还在不断更新,redux-toolkit也已经已启动以尝试解决导入redux时的常见问题。取而代之的是更轻量级的状态管理机制,前端衍生出几个流派:GraphQL→使用apollo[5]或relay[6]react-query[7]react-swr[8]recoil[9]jotai[10]全局状态管理在全局状态管理方面,SwiftUI也内置了一个机制,叫做@EnvrionmentObject。它的运行机制与React的context非常相似,允许组件跨层访问变量,当context发生变化时更新组件。类用户:ObservableObject{@Publishedvarname="kalan"@Publishedvarage=20}structUserInfo:View{@EnvironmentObjectvaruser:Uservarbody:someView{Text(user.name)Text(String(user.age))}}structContentView:View{varbody:someView{UserInfo()}}ContentView().envrionmentObject(User())从上面的例子我们可以发现我们不需要传入额外的userUserInfo,通过@EnvrionmentObject可以得到当前上下文。转换为React,它看起来像这样:constuserContext=createContext({})constUserInfo=()=>{const{name,age}=useContext(userContext)return<>

{name}

{age}

}constApp=()=>{}React的context允许元素跨层访问变量,并在context变化时更新元素。虽然有效避免了propdrilling的问题,但是context的存在会让测试更加麻烦,因为context的使用代表了一定程度的耦合。响应机制在React中,当state或props发生变化时,会触发组件更新,通过框架实现的diff机制进行比较后反映在屏幕上。在SwfitUI中也可以看到类似的机制:Helloworld")if!isHidden{Text("Showme\(name)")}}}一个典型的SwiftUI组件是一个结构体,它通过定义body变量来确定UI。和React一样,它们只是对UI的抽象描述。在比较数据结构计算出最小差异后,将它们更新到屏幕上。我真的很想知道SwiftUI背后的差异是如何计算的。希望以后有类似的文章。@State修饰符可用于定义组件的内部状态。当状态改变时,它会更新并反映在屏幕上。在SwiftUI中,可以从外部传入属性(MyView中的name),类似于React中的属性(props)。//在其他View中使用MyViewstructContentView:View{varbody:someView{MyView(name:"kalan")}}如果你用React重写这个元素,它将看起来像这样:constMyView=({name})=>{const[isHidden,setIsHidden]=useState(false)return
setIsHidden(state=>!state)}>隐藏

你好世界

{是隐藏的?null:`showme${name}`}
}在写SwiftUI的时候,你会发现这和之前用UIKit和UIController的开发方式不一样。SwiftUI和React都可以渲染列表,写法也差不多。在SwiftUI中,可以这样写:structTextListView:View{varbody:someView{List{ForEach(["iPhone","Android","Mac"],id:\.self){valueinText(value)}}}}转换为React可能看起来像这样:constTextList=()=>{constlist=['iPhone','Android','Mac']returnlist.map(item=>{item}

)}为了保证性能,减少渲染列表时不必要的比较,React会要求开发者提供key,SwiftUI中也有类似的机制,开发者必须使用Identifiable[11]协议,或者显式传入id。绑定除了将变量绑定到屏幕,我们还可以将交互绑定到变量。例如,在SwiftUI中我们可以这样写:structMyInput:View{@Stateprivatevartext=""varbody:someView{TextField("Pleasetypesomething",text:$text)}}不监听输入事件,使用$text直接改变text变量。使用@State时,会添加一个属性包装器,并自动添加一个前缀$,类型为Binding[12]。React没有双向绑定机制,必须显式监听输入事件以确保单向数据流。但是,像Vue和Svelte一样,有双向绑定机制,可以节省开发者手动监听事件的成本。Combine的出现虽然我对Combine还不够熟悉,但是从官方文档和视频来看,它看起来像是RxJS的Swift特化,提供的API和运算符大大简化了复杂的数据流。这让我想起以前研究RxJS和redux-observable的各种花哨操作的时候,真是怀念啊。上面说了这么多本质上的区别,但是网页开发和手机开发还是有相当大的区别的,对我来说最显着的就是静态编译和动态执行。动态执行可以说是网页最大的特点之一。只要有浏览器,JavaScript、HTML、CSS都可以在任何设备上顺利执行。网页不需要预先下载1xMB到数百MB的内容,可以动态执行脚本,根据浏览的页面动态加载内容。由于不需要提前编译,任何人都可以看到网页内容并执行脚本,再加上HTML的流式特性,可以边渲染边读取内容。难能可贵的是网页是去中心化的,任何人只要有服务器、ip地址和域名就可以访问网站内容;APP上架必须提前审核。但是,两者的生态圈和发展方式却大相径庭。还是建议参考一下对方的开发。就算平时不碰也没关系。经常可以从不同的角度发现不同的东西,也可以培养对技术的敏感度。花费。参考资料[1]ReSwift:https://github.com/ReSwift/ReSwift[2]SwiftUI-Hooks:https://github.com/ra1028/SwiftUI-Hooks[3]HOC(HigherOrderComponent):https://zh-hant.reactjs.org/docs/higher-order-components.html[4]渲染道具:https://zh-hant.reactjs.org/docs/render-props.html[5]阿波罗:https://github.com/apollographql/apollo-client[6]中继:https://relay.dev/[7]反应查询:https://react-query.tanstack.com/[8]react-swr:https://swr.vercel.app/zh-CN[9]后坐力:https://recoiljs.org/[10]jotai:https://github.com/pmndrs/jotai[11]可识别:https://developer.apple.com/documentation/swift/identifiable[12]绑定:https://developer.apple.com/documentation/swiftui/binding