SwiftUI:Better apps. Less code.

blank

SwiftUI:Better apps. Less code.

本文中所有Demo 運行的環境依賴

  • macOS Catalina 10.15 Beta
  • Xcode 11 Beta5

前言

近年來Android 和iOS 開發在兩家上游商業公司的推動下,不斷嘗試著一輪一輪的變革:

  • Google 2011 年發布Dart,2017 年發布Flutter,同年Google I/O 2017 上宣布Android 官方支持Kotlin, Google I/O 2019 上宣布Kotlin-first,同時發布Kotlin Jetpack Compose。
  • Apple 在2014 WWDC 上發布Swift,在2019 WWDC 上發布SwiftUI。

其中很多理念和前端社區的走向也是相同的,比如:

  • 聲明式UI
  • 響應式數據

本文會簡要分享下SwiftUI 的上面兩點理念,以及介紹下Xcode 最新的一些工具設計。

第一印象

如果你還不知道SwiftUI 是什麼,那也沒關係,反正我也不打算從SwiftUI 開始講起...

為了讓廣大前端工程師同學打起興趣,首先我打算先介紹下SwiftWebUI ,它是Github上一個開源的將SwiftUI跑在Web上的實驗性項目。

看看官方的一個簡單計數器:

  • 創建一個空的macos command line 應用
  • 使用Swift Package Manager從github上拉取SwiftUIWeb依賴包

Swift 的包管理是基於源文件+ 配置文件的,也遵循semver 規則。

  • main.swift粘貼如下代碼,最後添加一行啟動服務SwiftWebUI.serve(MainPage())
  • 點擊Xcode build,在瀏覽器打開http://localhost:1337

blank

效果:

blank

從上面這個小例子中就可以看到到SwiftUI 的幾個重要理念了:

  • 聲明式UI (類比React JSX,Flutter)
  • 響應式數據(類比mobx,vue)
  • “CSS In JS”
  • 鍊式調用(官方術語叫ViewModifier,類似react 裡面的HOC 概念)

更複雜的Demo可以體驗: github.com/swiftwebui/A

聲明式UI

Demo

Demo參考: developer.apple.com/tut

SwiftUI 的文檔這次設計了非常精美的tutorials 交互,可以跟著一步一步的操作。

blank

  • 所有視圖組件類型都是View ,它只需要實現一個接口body ,可以類比為React Component的render
  • VStack是垂直佈局的容器, HStack是水平佈局的容器, ZStack是獨立圖層的容器,類似前端zindex
  • 組件可以有屬性(Text的參數),組件也可以應用ViewModifier (鍊式調用)

對SwiftUI視圖語法的深入分析可以閱讀: SwiftUI的一些初步探索(一)

DSL

blank

WWDC-2019 Session 216: SwiftUI Essentials

SwiftUI 和Flutter 的UI 語法都可以看見React JSX 的影子,它們不約而同使用各自的聲明式DSL 來描述UI 。
但是這和前端常用的模板語言(Nunjucks/Vue)還是有一些區別的,它們都是複用語言本身的邏輯控制語句(條件,循環),再加上一種特殊的數據結構(eg: React Virtual DOM)來表達UI。
畢竟對於Google 和Apple 來說,語言、編譯器、開發工具、開發者、平台這整條鏈路都在掌控中,那麼可以做一些集成度更高的設計。

我認為使用類似Nunjucks 此類模版語言的一些缺點:

  • 和業務邏輯的編程語言之間有一層gap,發生關聯的地方會丟失編輯器智能提示,跳轉等上下文訊息(通過開發編輯器插件等也可以實現,但成本較高)
  • 模板內部的屬性綁定,循環,變量插值一些場景會出現字符串形態的邏輯代碼
  • 模板語言仍需要一次提前編譯,比如vue模板的編譯: vuejs-tips.github.io/co

blank

仔細看一下SwiftUI 的一些視圖語法:

條件:直接使用條件控制語句

blank

循環:直接使用循環控制語句

blank

List:

blank

如果是動態List 的場景下,SwiftUI 還會要求開發者提供Identifiable:類似react 的key 或者antd Table.props.rowKey, 其作用是讓框架可以精確定位和更新List 中的某一行元素

使用子元素來表達結構:

blank

Form:

blank

此外按從SwiftUI談聲明式UI與類型系統這篇文章的分析,因為SwiftUI強類型的,那麼一個組件可能的幾種視圖結構是可以被靜態分析出來的,那麼在運行時甚至可以基於靜態分析的類型訊息來對視圖的更新算法做優化,可以達到比React Vitual DOM 更好更新性能。

ViewModifier

官方的文檔這麼介紹ViewModifier :A modifier that you apply to a view or another view modifier, producing a different version of the original value.

blank

它的用法有點類似React 的HOC(High Order Component),附加在某個視圖上改變它原本的結構或樣式。
除了內置的modifier,SwiftUI 也允許開發者定制自己的modifier 的,比如下面是一個自定義例子:

blank

而且modifier 是應用於整顆子樹的,例如下面這兩種寫法都可以實現禁用整個表單的效果:

blank

blank

Stack & 佈局

Stack 前文已經介紹過,是內置的一些佈局容器

  • HStack:水平排布它的子元素的一個容器視圖。
  • VStack:垂直排布它的子元素的一個容器視圖。
  • ZStack:層疊排布它的子元素的一個容器視圖。
  • Spacer:一個彈性的空白間距元素,會充滿容器元素的主軸方向
  • Divider:一個虛擬的分界元素完整列表可以參考: View Layout and Presentation

blank

blank

雖然沒有真實開發過ios 應用,但是從App Store 裡面的App 設計風格來看,iOS 由於屏幕尺寸小,單頁內部的元素層級不多,佈局也不會太複雜,並且時常搭配NavigationView,TabView 這種導航類的組件繼續對視圖做細粒度切割。
而且Apple 對設計規範的掌控力非常強,因此SwiftUI 的佈局體系相對比Web 要簡單很多,有點類似簡化版的Flex。

系列文章深度解讀|SwiftUI背後那些事兒這篇文章對SwifUI佈局算法有一部分解釋

跨平台?

內置組件是有跨平台的不同實現的,有點類似當初React Native 的做法:

blank

blank

blank

但是Apple官方是說為了用戶體驗考慮,不推薦為Mac,iPhone,Apple Watch, Apple TV這些不同尺寸的設備採用一套界面設計,因此他們不推崇Write once, run anywhere ,而是宣稱Learn once, apply anywhere

blank

blank

響應式數據

響應式數據在前端領域已經有非常多的實現了,這裡隻大致介紹一下:

What?

假設抽象的認為組件就是消費一些數據去生成特定區塊視圖的邏輯封裝體,那麼Component都是滿足State => View這麼一個協議的封裝。
而響應式數據的最重要效果就是當依賴的數據(State)發生變化的時候,組件視圖(View)會自動更新。

blank

Why?

那麼為什麼要採用這種開發模式呢?
下面這張圖來自WWDC 2019 - Session 204:Introducing SwiftUI: Building Your First App ,詮釋得非常好。

blank

blank

GUI 程序通常使用場景是處理一個長時間段的用戶交互過程,這其中涉及到許多用戶輸入訊息,視圖臨時狀態的存儲。此外整個頁面樹上可能有數以百計的元素之間要互相通信和聯動。如果按照傳統的事件驅動的編程模型,依靠程序員的大腦去掌控所有數據到視圖,數據到數據之間的衍生依賴關係,腦力負擔是非常大的,一旦這個複雜度超越了人腦的算力,就會導致關聯關係不正確,也就是Bug 的產生。

而“響應式”就是將這個依賴關係的維護交由框架來處理,開發者只需要“聲明”出依賴關係,而且這個“聲明”是指用對業務邏輯的描述語言來順帶地描述出這種依賴關係,比如:

  • State => View狀態到視圖的過程中,生產視圖的過程中消費(getter)了哪些數據,就可以大體認為是該視圖對該數據建立了依賴

blank

  • State => Drived State衍生狀態,比如計算屬性的定義過程中,該屬性的終值依賴了哪些中間數據,就可以認為是該屬性對其他屬性建立了依賴

在這個理論體系下,State始終是single source of truth,視圖只是數據的一個衍生值(Derived Value)

blank

SwiftUI 用響應式狀態管理和數據綁定避免了大量的命令式控制邏輯,從而使應用開發者用“數據驅動” 或“模型驅動” 的替代傳統的事件驅動,開發者的腦海中始終在思考此刻控制我的視圖的數據狀態是如何,而背後衍生的視圖如何更新,則不用關注。

當然這種模式也有一些弊端,參閱: vue.js會是那顆銀彈嗎?

How?

以前端領域的Observable實現庫@nx-js/observer-util的Demo為例子:

blank

可以看出這里分幾部分:

  • 定義響應式數據:想方設法利用語言特性實現對對象的讀寫(getter,setter) 做攔截
    • vue@2是使用JS defineProperty特性
    • observer-util和Vue@3都是利用JS Proxy特性
    • SwiftUI是利用Swift語言的Property Wrapper特性,@state @Binding在swift語言標準裡都屬於Property Wrapper的實現

blank

  • 建立依賴關係:observer-util 是通過在function 執行的前後打上標誌位,記錄下整個方法執行過程中的依賴關係key-value map,其key 是發生getter 的JS 數據對象,value 是function 本身

完成上面兩步之後,一旦框架監聽到某個對象的setter,就從依賴的key-value Map 裡面找到當前對象的依賴函數列表,一一執行。

更詳細的解讀可觀看:WWDC 2019 - Session 226: Data Flow Through SwiftUI

SwiftUI 的狀態管理API:

  • @State:定義一個響應式狀態,它的變化會導致依賴它的視圖自動更新(單向)
  • @Binding:視圖和數據的雙向綁定
  • @ObjectBinding:作用等價於@Binding,但是支持使用一個外部對象
  • @BindableObject、Combine:Apple官方新發布的combine ,說是用來處理外部事件或服務端推送等場景,其實筆者也不懂這是個啥,但只要說它是RxJS的Swift版本實現,大家應該就會心一笑吧
  • @EnviromemntObject: 沿著View 樹的層級一直向下共享的數據,實現類似Scoped Data 的效果
  • @Enviroment:可以全局共享的數據,同時它的變化會導致UI 自動刷新,類似React 這邊的Provider

Xcode 工具設計

首先Xcode 一直是一個非常強的IDE,包括項目管理,依賴管理,版本管理,開發測試構建等等項目研發需要的功能它都支持。

本文主要介紹新紅框圈出來與SwiftUI 有關的一些新功能。

blank

Live Preview

對於UI開發來說,能實時預覽而不用泡杯咖啡等待構建真是太愉悅了,Flutter也是將Hot Reload當作一個很重要的賣點來打,而SwiftUI更是將這個體驗做到了極致。

上圖右下角紅框的圖標,點擊即是在Live Preview 和傳統的Build Simulator Preview 之間切換,Apple 把這兩種預覽都做到了一個位置,just click to switch!

  • Live Preview:基於AST 分析,無構建流程,實時刷新,無交互(不能點擊和跳轉)
  • Simulator Preview:傳統構建流程,真實應用運行(和傳統預覽一樣,但不用切換屏幕到模擬器了)

為什麼Live Preview 能做到極其快?

  • 對當前文件進行AST分析,找到所有遵守PreviewProvider協議的類型進行預覽渲染,只展示靜態的視圖結構
  • 監聽代碼或畫布的變化,只差量更新最細粒度變化的元素部分

如何處理Mock 數據,預覽態與真實運行態的差異?
Preview is Code.
下圖左下角的代碼可以看出#if DEBUG #endif說明是編輯時代碼,每一個SwiftUI視圖文件都可以在這個編輯時代碼區域內實現一個PreviewProvider方法,在這個內部,開發者可以灌入mock數據渲染開發好的視圖組件,甚至可以準備多套mock 數據或者環境參數,同時在右邊展示出視圖的多種狀態。

blank

添加資產

點擊編輯器右上角,會彈出浮層,因為浮層不佔用編輯器控件,所以這裡甚至可以容納組件文檔:

blank

這個浮層的訊息架構還有二級分類,第一個Tab是View (組件資產),其它幾個Tab分別是:
Modifiers:

blank

Snippets:

blank

Media:

blank

Color:

blank

同時在資產面板這邊可以看出Apple 的一些設計細節:
資產面板可以從List 模式切換到Grid 模式:

blank

資產面板可以折疊文檔區域:

blank

Inspect(屬性面板)

blank

在畫布或代碼視圖中選中某一個元素, command + click即可呼出action面板,第一位action就是Inspect,先介紹下Inspect面板的內容:

基礎屬性:

blank

容器屬性:

blank

佈局padding:

blank

寬高:

blank

除此之外Action 列表裡面還有一些提效的操作(實質是對視圖代碼的智能轉換),比如對

  • 視圖層級結構的快速改變:在元素外面快速套上一些常見的佈局容器
  • Repeat迭代:將單個元素轉換成for循環包裹
  • Conditional 條件:將單個元素轉換成if else 包裹
  • Optional 可選:將單個元素轉換成if 包裹

代碼和視圖雙向同步

SwiftUI 實現了一個閉環,實際體驗還是不錯的。

  • 選中代碼可定位視圖,選中視圖也可以定位代碼
  • 選中視圖點擊可喚出屬性面板,修改屬性可自動回寫到代碼
  • 資產面板拖入可以拖入到代碼,也可以拖入到視圖

Code Lint & AutoFix

代碼編寫過程中的實時錯誤提示與AutoFix

blank

Extract SubView

  • Action 面板裡有一個一鍵將某一塊視圖提取成子組件的操作
  • 提取完成之後,如果該子組件內部使用到了外部變量,Lint 系統會檢測到並報錯

結語

因為筆者也在做Web 端中後台Low Code 可視化建站的產品,雖然SwiftUI 是Pro Code 開發模式的工具,但是正如SwiftUI 的宣傳語鼓吹的一樣:

  • "The shortest path to building great apps on every device" ,
  • "Better apps. Less code."

大家努力的方向都是一致的,如何用更少更簡潔的代碼,更直觀更高效的工具來提速研發流程,真正解放生產力。
同時SwiftUI 的方向我認為是正確的,它結合了幾乎當前整個領域所有最優秀的理念和設計:

  • 強類型的語言讓代碼維護性更高
  • 聲明式UI 讓視圖代碼更簡潔
  • 響應式狀態管理讓開發者心智更簡單
  • 可視化和代碼的兩種編輯模式雙向轉換,讓新手用戶可以通過可視化編輯器邊開發邊觀察生成的代碼學習,熟練用戶可以完全代碼編輯,使用畫布進行實時的輔助預覽
  • 從設計之初就保證對應用代碼的理解力,那麼類似一鍵提取自定義組件,一鍵轉換成repeat,list,condition 這些智能操作會越來越多

朝著正確的方向,輔以Apple 強大的工具整合和體驗打磨能力,加上它完整的鏈路掌控,全平台覆蓋的設計規範,只要持續打磨,SwiftUI 應該會在iOS 開發領域得到普及。

相關資料

What do you think?

Written by marketer

blank

雲鳳蝶中台研發提效實踐

blank

第5 屆FEDAY 來啦! 9月21日,成都見!