SwiftUI之从前端视角看SwiftUI语言

一、从 class 迈向 struct,从 class 迈向 function

  • 可以将前端框架归纳为几个要素:
    • 元件化;
    • 响应式机制;
    • 状态管理;
    • 事件监听;
    • 生命周期。
  • 在写 SwiftUI 的时候总是想到 React 的发展史,最初 React 建立元件的方式是透过 JavaScript 的 class 语法,每个 React 的元件都是一个类别。
class MyComponent extends React.Component {
  constructor() {
    this.state = {
      name: 'kalan'
    }
  }
  
  componentDidMount() {
    console.log('component is mounted')
  }
  
  render() {
    return <div>my name is {this.state.name}</div>
  }
}
  • 透过类别定义元件虽为前端元件化带来了很大的影响,但也因为繁琐的方法定义与 this 混淆,在 React16 hooks 出现之后,逐渐提倡使用 function component 与 hooks 的方式来建立元件。
  • 省去了继承与各种 OO 的花式设计模式,建构元件的心智负担变得更小了。从 SwiftUI 当中,也可以看到类似的演进,原本 ViewController 庞大的 class 以及职责,要负责 view 与 model 的互动,掌管生命周期,转为更轻量的 struct,让开发者可以更专注在 UI 互动上,减轻认知负担。

二、元件状态管理

  • React 16 采取了 hooks 来做元件的逻辑复用与状态管理,例如 useState:
const MyComponent = () => {
  const [name, setName] = useState({ name: 'kalan' })
  useEffect(() => { console.log('component is mounted') }, [])
  
  return <div>my name is {name}</div>
}
  • 在 SwiftUI 当中,可以透过修饰符 @State 让 View 也具有类似效果。两者都具备响应式机制,当状态变数发生改变时,React/Vue 会侦测改变并反映到画面当中。虽然不知道 SwiftUI 背后的实作,但背后应该也有类似 diff 机制的东西来达到响应式机制与最小更新的效果。
  • 然而 SwiftUI 的状态管理与 React hooks 仍有差异,在 React 当中,可以将 hook 拆成独立的函数,并且在不同的元件当中使用,例如:
function useToggle(initialValue) {
  const [toggle, set] = useState(initialValue)
  const setToggle = useCallback(() => { set((state) => !state) }, [toggle])
  useEffect(() => { console.log('toggle is set') }, [toggle])
  return [toggle, setToggle]
}
const MyComponent = () => {
  const [toggle, setToggle] = useToggle(false)
  return <button onClick={() => setToggle()}>Click me</button>
}
const MyToggle = () => {
  const [toggle, setToggle] = useToggle(true)
  return <button onClick={() => setToggle()}>Toggle, but fancy one</button>
}
  • 在 React 当中,可以将 toggle 的逻辑拆出,并在不同元件之间使用,由于 useToggle 是一个纯函数,因此内部的状态也不会互相影响。
  • 然而在 SwiftUI 当中 @State 只能作用在 struct 的 private var 当中,不能进一步拆出。如果想要将重复的逻辑抽出,需要另外使用 @Observable 与 @StateObject 这样的修饰符,另外建立一个类别来处理。
class ToggleUtil: ObservableObject {
  @Published var toggle = false
  
  func setToggle() {
    self.toggle = !self.toggle
  }
}
struct ContentView: View {
  @StateObject var toggleUtil = ToggleUtil()
  var body: some View {
    Button("Text") {
      toggleUtil.setToggle()
    }
    if toggleUtil.toggle {
      Text("Show me!")
    }
  }
}
  • 在这个例子当中把 toggle 的逻辑拆成一个 class 似乎有点小题大作了,不过仔细想想像 React 提供的 hook 功能,让轻量的逻辑共用就算单独拆成 hook 也不会觉得过于冗长,若要封装更复杂的逻辑也可以再拆分成更多 hooks,从这点来看 hook 的确是一个相当优秀的机制。后来看到了 SwiftUI-Hooks,不知道实际使用的效果如何。
  • 以 React 来说,在还没有出现 hooks 之前,主要有三个方式来实作逻辑共用:
    • HOC(Higher Order Component):将共同逻辑包装成函数后返回全新的 class,避免直接修改元件内部的实作,例如早期 react-redux 中的 connect;
    • render props:将实际渲染的元件当作属性(props)传入,并提供必要的参数供实作端使用;
    • children function:children 只传入必要的参数,由实作端自行决定要渲染的元件。

三、Redux 与 TCA

  • 受到 Redux 的影响,在 Swift 当中也有部分开发者使用了采用了类似手法,甚至也有相对应的实作 ReSwift 的说明文。从说明文可以看到主要原因,传统的 ViewController 职责暧昧,容易变得肥大导致难以维护,透过 Reducer、Action、Store 订阅来确保单向资料流,所有的操作都是向 store dispatch 一个 action,而资料的改动(mutation)则在 reducer 处理。
  • 而最近的趋势似乎从 Redux 演变成了 TCA(The Composable Architecture),跟 Redux 的中心思想类似,更容易与 SwiftUI 整合,比较不一样的地方在于以往涉及 side effect 的操作在 Redux 当中会统一由 middleware 处理,而在 TCA 的架构中 reducer 可以回传一个 Effect,代表接收 action 时所要执行的 IO 操作或是 API 呼叫。
  • 既然采用了类似 redux 的手法,不知道 SwiftUI 是否会遇到与前端开发类似的问题,例如 immutability 确保更新可以被感知;透过优化 subscribe 机制确保 store 更新时只有对应的元件会更新;reducer 与 action 带来的 boilerplate 问题。
  • 虽然 Redux 在前端仍然具有一定地位,也仍然有许多公司正在导入,然而在前端也越来越多弃用 Redux 的声音,主要因为 redux 对 pure function 的追求以及 reducer、action 的重复性极高,在应用没有到一定复杂程度之前很难看出带来的好处,甚至连 Redux 作者本人也开始弃坑 redux 了 4。与此同时,react-redux 仍然有在持续更新,也推出了 redux-toolkit 来试图解决导入 redux 时常见的问题。
  • 取而代之的是更加轻量的状态管理机制,在前端也衍生出了几个流派:

四、全域状态管理

  • 在全域状态管理上,SwiftUI 也有内建机制叫做 @EnvrionmentObject,其运作机制很像 React 的 context,让元件可以跨阶层存取变数,当 context 改变时也会更新元件:
class User: ObservableObject {
  @Published var name = "kalan"
  @Published var age = 20
}
struct UserInfo: View {
  @EnvironmentObject var user: User
  var body: some View {
    Text(user.name)
    Text(String(user.age))
  }
}
struct ContentView: View {
 var body: some View {
  UserInfo()
  }  
}
ContentView().envrionmentObject(User())
  • 从上面这个范例可以发现,不需要另外传入 user 给 UserInfo,透过 @EnvrionmentObject 可以拿到当前的 context。转换成 React 的话会像这样:
const userContext = createContext({})
const UserInfo = () => {
  const { name, age } = useContext(userContext)
  return <>
    <p>{name}</p>
    <p>{age}</p>
  </>
}
const App = () => {
  <userContext.Provider value={{ user: 'kalan', age: 20 }}>
    <UserInfo />
  </userContext.Provider>
}
  • React 的 context 可让元件跨阶层存取变数,当 context 改变时也会更新元件。虽然有效避免了 prop drilling 的问题,然而 context 的存在会让测试比较麻烦一些,因为使用 context 时代表了某种程度的耦合。

五、响应机制

  • 在 React 当中,状态或是 props 有变动时都会触发元件更新,透过框架实作的 diff 机制比较后反映到画面上。在 SwfitUI 中也可以看到类似的机制:
struct MyView: View {
  var name: String
  @State private var isHidden = false
  
  var body: some View {
    Toggle(isOn: $isHidden) {
      Text("Hidden")
    }
    Text("Hello world")
    
    if !isHidden {
      Text("Show me \(name)")
    }
  }
}
  • 一个典型的 SwiftUI 元件是一个 struct,透过定义 body 变数来决定 UI。跟 React 相同,它们都只是对 UI 的抽象描述,透过比对资料结构计算最小差异后,再更新到画面上。
  • @State 修饰符可用来定义元件内部状态,当状态改变时会更新并反映到画面中。在 SwiftUI 当中,属性(MyView 当中的 name)可以由外部传入,跟 React 当中的属性(props)类似。
// 在其他 View 当中使用 MyView
struct ContentView: View {
  var body: some View {
    MyView(name: "kalan")
  }
}
  • 用 React 改写这个元件的话会像这样:
const MyView = ({ name }) => {
  const [isHidden, setIsHidden] = useState(false)
  return <div>
    <button onClick={() => setIsHidden(state => !state)}>hidden</button>
    <p>Hello world</p>
    {isHidden ? null : `show me ${name}`}
  </div>
}
  • 在撰写 SwiftUI 时会发现这跟以往用 UIKit、UIController 的开发方式不太一样。

六、列表

  • SwiftUI 与 React 当中都可以渲染列表,而撰写的方式也有雷同之处。在 SwiftUI 当中可以这样写:
struct TextListView: View {
  var body: some View {
    List {
      ForEach([
        "iPhone",
        "Android",
        "Mac"
      ], id: \.self) { value in
        Text(value)
      }
    }
  }
}
  • 转成 React 大概会像这样子:
const TextList = () => {
  const list = ['iPhone', 'Android', 'Mac']
  
  return list.map(item => <p key={item}>{item}</p>)
}
  • 在渲染列表时为了确保效能,减少不必要的比对,React 会要求开发者提供 key,而在 SwiftUI 当中也有类似的机制,开发者必须使用叫做 Identifiable[11] 的 protocol,或是显式地传入 id。

七、Binding

  • 除了将变数绑定到画面之外,也可以将互动绑定到变数之中。例如在 SwiftUI 当中我们可以这样写:
struct MyInput: View {
  @State private var text = ""
  var body: some View {
    TextField("Please type something", text: $text)
  }
}
  • 在这个范例当中,就算不监听输入事件,使用 $text 也可以直接改变 text 变数,当使用 @State 时会加入 property wrapper,会自动加入一个前缀 $,型别为 Binding
  • React 并没有双向绑定机制,必须要显式监听输入事件确保单向资料流。不过像 Vue、Svelte 都有双向绑定机制,节省开发者手动监听事件的成本。

八、Combine 的出现

  • 虽然我对 Combine 还不够熟悉,但从官方文件与影片看起来,很像RxJS 的 Swift 特化版,提供的 API 与操作符大幅度地简化了复杂资料流。这让我想起了以前研究 RxJS 与 redux-observable 各种花式操作的时光,真令人怀念。

九、总结

  • 前文提到那么多,然而网页与手机开发仍然有相当大的差异,其中对我来说最显著的一点是静态编译与动态执行。动态执行可以说是网页最大的特色之一。
  • 只要有浏览器,JavaScript、HTML、CSS,不管在任何装置上都可以成功执行,网页不需要事先下载 1xMB ~ 几百 MB 的内容,可以动态执行脚本,根据浏览的页面动态载入内容。
  • 由于不需要事先编译,任何人都可以看到网页的内容与执行脚本,加上 HTML 可以 streaming 的特性,可以一边渲染一边读取内容。难能可贵的一点是,网页是去中心化的,只要有伺服器、ip 位址与网域,任何人都可以存取网站内容;而 App 如果要上架必须事先通过审查。
  • 不过两者的生态圈与开发手法有很大的不同,仍然建议参考一下彼此的发展,就算平时不会碰也没关系,从不同的角度看往往可以发现不同的事情,也可以培养对技术的敏锐度。
  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

╰つ栺尖篴夢ゞ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值