Single Source of Truth:XCode + SwiftUI 的介面編輯的設計理念

語言: CN / TW / HK

       

本文為來自飛書 aPaaS Growth 研發團隊成員的文章,已授權 ELab 釋出。

aPaaS Growth 團隊專注在使用者可感知的、巨集觀的 aPaaS 應用的搭建流程,及租戶、應用治理等產品路徑,致力於打造 aPaaS 平臺流暢的 “應用交付” 流程和體驗,完善應用構建相關的生態,加強應用搭建的便捷性和可靠性,提升應用的整體效能,從而助力 aPaaS 的使用者增長,與基礎團隊一起推進 aPaaS 在企業內外部的落地與提效。

背景 1:Define SSOT

Q: What is the meaning of Single Source of Truth (SSOT) in the context of SwiftUI?

A: With SwiftUI, you can either write the code pragmatically or use the design tool to edit the UI, which will also result in the SwiftUI code being modified. Essentially, you only have the source code, there's no separate design file (i.e. nib or Storyboard [1] (2016)), which means that there is no way your UI design and the code handling the UI can ever get out of sync (which was the case previously with nib files or storyboards).

http://stackoverflow.com/questions/58398373/what-is-the-meaning-of-single-source-of-truth-ssot-in-the-context-of-swiftui

背景 2:什麼是程式與語言

什麼是計算機程式

http://en.wikipedia.org/wiki/Computer_program

A computer program is a sequence or set of instructions in a programming language [2] for a computer [3] to [execute](http://en.wikipedia.org/wiki/Execution_(computing "execute")).

什麼是編譯型語言

http://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E8%AA%9E%E8%A8%80

編譯語言(英語:Compiled language)是一種 程式語言 [4] 型別,通過 編譯器 [5] 來實現。它不像 解釋型語言 [6] 一樣,由直譯器將程式碼一句一句執行,而是以編譯器,先將 程式碼 [7] 編譯為 機器程式碼 [8] ,再加以執行。理論上,任何程式語言都可以是編譯式,或直譯式的。它們之間的區別,僅與程式的應用有關。

什麼是解析型語言

http://zh.wikipedia.org/wiki/%E7%9B%B4%E8%AD%AF%E8%AA%9E%E8%A8%80

解釋型語言(英語:Interpreted language)是一種 程式語言 [9] 型別。這種型別的程式語言,會將程式碼一句一句直接執行,不需要像 編譯語言 [10] (Compiled language)一樣,經過 編譯器 [11] 先行編譯為 機器程式碼 [12] ,之後再執行。這種程式語言需要利用 直譯器 [13] ,在執行期,動態將程式碼逐句解釋(interpret)為機器程式碼,或是已經預先編譯為機器程式碼的 子程式 [14] ,之後再執行。

程式語言應該符合的數學模型和程式語言的發展歷程

首先每門語言都應該至少具備圖靈完備性,至於什麼是圖靈完備可以參照我之前的一篇分享: 證明 JS 和 TS 型別程式設計是圖靈完備的 [15] ,簡而言之一門語言只要能實現三個基本函式和三個基本組合,那麼它就是圖靈完備的,一定能表達其它圖靈完備的語言所能表達的邏輯,注意這裡所說的是純邏輯,從數學的角度來看就是實現一個函式,輸入和輸出都屬於同一個域,有新的域必須在上下文增加輸入,例如裝置/檔案 IO 需要有機器指令支援。

程式語言都是形式語言( Formal Language [16] ),關於形式語言的研究早在計算機出現之前就由語言學家提出。

The first use of formal language is thought to be Gottlob Frege [17] 's 1879 Begriffsschrift [18] , meaning "concept writing", which described a "formal language, modeled upon that of arithmetic, for pure thought."([2])

要注意,圖靈是否完備跟語法是否簡單沒有直接關係,一門語法非常簡單但是圖靈完備的語言可以參照:http://esolangs.org/wiki/SNUSP ,一些我們常用的,我們看上去覺得表達力還不錯的語言它其實不是圖靈完備的(例如微信的 WXML,也例如 UIDL),簡單的原因是它們實現不了偏遞迴函式所必須具備的「最小化」操作,也就是它們實現不了 while 迴圈。

我們縱觀計算機語言的發展歷程,會發現計算機語言的語法是從簡單到複雜的一個過程,最開始我們用打孔卡來表示程式(http://en.wikipedia.org/wiki/Punched_card),這本質上就是一串數字,到後來發明了機器碼,基本上就是一些將數字和一些助記單詞一一對應,然後指令後面接記憶體地址這樣子來表達程式。再後來彙編添加了「過程」的概念,再後來我們有 Lisp 這樣的語言,語言裡添加了「表示式」的概念,後面 Lisp 的方言越來越多,添加了「函式」、「語句」的概念。再後來我們有較為正式的現代語言 C,裡面添加了塊、控制流這樣的概念。再後來,我們有 Java 這樣的面嚮物件語言,裡面添加了例如面向物件設計相關的概念(類、介面、封裝、多型、繼承),到現在我們有 ES 2022、Swift、Golang、Rust 多種多樣的語言,它們面向不同的領域有不同的語法和特性。

回顧整個過程,時候從簡單到複雜的過程,是從易學到難學的過程,是從表達力弱到表達力強的過程,是從不實用到實用的過程,是從易於解析到難以解析的過程。

以下純粹個人觀點:一門語言很難面面俱到,每門語言通常有它的適用場景,往往面臨類似 三元悖論 [19] 的場景(當然可以引入更多維度形成四五六七八九元悖論 ...),例如:

image.png

例如 Javascript 等解析型語言語法相對簡單,表達力又相對強的語言(支援面向物件程式設計,async await 語法,function as first citizen, ...),、但是它效能會相對弱,因為你不能控制鎖,無鎖就需要引入 eventloop,造成效能消耗,沒有記憶體控制就需要引入 GC,造成效能消耗。

又例如 Rust 引入了記憶體的 Ownership,語言的學習複雜度一下子上來了,但是效能上和在效能場景的表達力上也能做得更好。

背景 3:一個簡單的解析型語言實現

話說回來,再正式介紹 XCode + SwiftUI 之前,我需要介紹一門語言是如何解析的,我們用最簡單的語言之一的 Lisp 為例,用最簡單的自迴圈解析器來解釋。

image.png

假設我們有一個一段簡單的 Lisp 程式:

// 語義為:(60 * 9 / 5) + 32
(+ (* (/ 9 5) 60) 32)

那麼第一步解析器程式會執行 tokenize 程式,結果為:

[
"(",
"+",
"(",
"*",
"(",
"/",
"9",
"5",
")",
"60",
")",
"32",
")"
]

第二步執行 parse,將 token list 轉化為 AST:

[
"+",
[
"*",
[
"/",
9,
5
],
60
],
32
]

第三步就是解析 AST,解析的過程本質上是一個深度遞迴從底向上求值的過程,例如上述 AST 的求值過程是這樣子的:

// 輸入
[
"+", // <- 第一層指標,發現子屬性是陣列,先求陣列的值
[
"*", // <- 第二層指標,發現子屬性是陣列,先求陣列的值
[
"/", // <- 第二層指標,沒有子屬性是陣列了,不需要遞迴了,在這裡求第一次值
9,
5
],
60
],
32
]

// 第一遍求值
[
"+",
[
"*",
1.8,
60
],
32
]

// 第二遍求值
[
"+",
108,
32
]

// 第三遍求值
140

一個完整的 Demo 可以參照:http://gist.github.com/Enichan/4a9fa87aef6405e13e1c072baa117beb

function interp(x, env) {
env = env || g;
if (typeof x === "string") { // symbol
return env.find(x)[x];
}
else if (!Array.isArray(x)) { // constant literal
return x;
}
else if (x[0] === "quote") { // (quote exp)
let exp = x[1];
return exp;
}
else if (x[0] === "if") { // (if test conseq alt)
let test = x[1], conseq = x[2], alt = x[3];
let exp = interp(test, env) ? conseq : alt;
return interp(exp, env);
}
else if (x[0] === "define") { // (define symbol exp)
let symbol = x[1], exp = x[2];
env[symbol] = interp(exp, env);
}
else if (x[0] === "set!") { // (set! symbol exp)
let symbol = x[1], exp = x[2];
return (env.find(symbol)[symbol] = interp(exp, env));
}
else if (x[0] === "eval") { // custom shenanigans
let exp = interp(x[1], env);
return interp(exp, env);
}
else if (x[0] === "lambda") { // (lambda (symbol...) body)
let parms = x[1], body = x[2];
return makeProc(parms, body, env);
}
else {
let proc = interp(x[0], env);
let args = x.slice(1).map(exp => interp(exp, env));
if (typeof proc !== "function") {
throw new Error("Expected function, got " + (proc || "").toString());
}
return proc.apply(proc, args);
}
}

同時,如果我們稍微加個 wrapper,就可以得到這個程式的呼叫棧了。

let stack = [];
function wrap(func) {
return function(...args) {
stack.push(args[0]);
let resp = func(...args);
stack.pop();
return resp;
};
}
interp = wrap(interp);

看來這裡,你知道 stackoverflow 異常是什麼東西了吧?本質意義上就是我們程式對 AST 做遞迴,遞迴的過程中不斷入棧,如果程式寫得不好,就會使得棧空間溢位。

怎麼樣,這種程式碼的感覺是不是有種似曾相識的感覺?如果我們開啟 kunlun-fe 的 parseComponentMeta.ts 這個檔案,你會發現它也是個自迴圈解析器:

export function parseComponentMeta(
meta: ComponentMeta,
components: Components = {},
...
): JSX.Element {
const { name, type, children, events, selectors: selectorMeta } = meta;
...
const { props = {} } = meta;
const { key: propKey } = props;
if (propKey === undefined) {
// props = { ...props, key: name };
props.key = name;
}

props.__component_name__ = name;

let normalizedChildren = null;
if (typeof children === 'string') {
normalizedChildren = children;
} else if (Array.isArray(children) && children.length > 0) {
normalizedChildren = children. map ( ( childMeta ) =>
parseComponentMeta (
childMeta,
components,
connect,
stateKey,
payload,
componentCache,
selectors,
decorator,
),
);
}

if (isHostComponent(type)) {
return createElement(type, props, normalizedChildren);
}

let ComponentType = deepGet(components, type) as React.ComponentType;
if (ComponentType === undefined) {
window.console.error(type, ' is not found in components:', components);
ComponentType = NotFound;
}
if (events !== undefined && connect === undefined) {
throw new DangerousCustomErrorWithoutSensitiveMessage({
label: 'page-meta-engine',
message: '"connect" is required when "events" passed.',
});
}
if (selectorMeta !== undefined && connect === undefined) {
throw new DangerousCustomErrorWithoutSensitiveMessage({
label: 'page-meta-engine',
message: '"connect" is required when "selectors" passed.',
});
}
if (isInBlacklistOfConnect(type) || connect === undefined) {
return createElement(ComponentType, props, normalizedChildren);
}

// 做了一些 redux wrapping 相關的東西
return ...
}

換個角度來思考,kunlun 的 UI Meta 或者後續 UIDL 都是直接定義了一套 AST,然後實現了一個自迴圈解析器。

引申思考:

  1. 如果我現在需要用 UI Meta 或者 UIDL 來實現一個 infinite loading 的 list 元件,能實現嗎?如果你來擴充套件 UI Meta,不實現自定義 React Component 的話,你會引入什麼樣的 Meta 屬性,這些屬性的原子操作是怎麼樣的,你怎麼解析它?

  2. 如果我要為 UI Meta 新增條件渲染的功能,應該如何實現呢?

如果我們實現一個 JS 的解析器的話會不會很難呢?其實也不是很難,如果不考慮效率的話,實現 ES5 的語義我們只需要 1000 來行程式碼就可以實現對 ES5 的 AST 的 eval。

http://github.com/axetroy/vm.js/blob/master/src/standard/es5.ts

我們 ES 的 AST 要比 Lisp 的細節豐富得多,且更易於理解,例如同一個表示式,ES 的表示式是這樣的:

http://astexplorer.net/#/gist/c40c85b756de9a4e10fb5bfe668fa000/ff2ab3d89b593035af82b97cddf72a68910dcab9

{
"type": "File",
},
"errors": [],
"program": {
"type": "Program",
},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "ExpressionStatement",
},
"expression": {
"type": "BinaryExpression",
},
"left": {
"type": "BinaryExpression",
},
"left": {
"type": "BinaryExpression",
},
"left": {
"type": "NumericLiteral",
},
"extra": {
"rawValue": 60,
"raw": "60"
},
"value": 60
},
"operator": "*",
"right": {
"type": "NumericLiteral",
},
"extra": {
"rawValue": 9,
"raw": "9"
},
"value": 9
}
},
"operator": "/",
"right": {
"type": "NumericLiteral",
},
"extra": {
"rawValue": 5,
"raw": "5"
},
"value": 5
},
"extra": {
"parenthesized": true,
"parenStart": 0
}
},
"operator": "+",
"right": {
"type": "NumericLiteral",
},
"extra": {
"rawValue": 32,
"raw": "32"
},
"value": 32
}
}
}
],
"directives": []
},
"comments": []
}

XCode + SwiftUI 的介面設計的體驗

Screen Recording 2022-07-07 at 01.01.20.2022-09-01 21_30_32.gif

只要大家看完了背景一二三之後,理解它的原理起來應該還是相對簡單的:

  1. XCode 在編輯實現 protocal View 的結構體的時候,會在編輯的同時把程式碼走一遍編譯流程然後丟到虛擬機器執行,同時執行實現 protocal PreviewProvider 的結構體,在執行的過程中可以知道:

    1. AST

    2. 對應棧

  2. 在滑鼠點選對應的具體程式碼的時候,通過 AST 的文字範圍資訊可以反射出來是 AST 的哪個塊,也就找到了對應 UI 的例項的值和型別資訊。

  3. 反之也是一樣的,點選檢視元件的時候,可以反射出具體的 AST 塊,通過文字範圍資訊也就找到了對應的程式碼。

  4. 修改值的時候,通過 3 的對應邏輯修改 AST 的值,通過 Code Generator 輸入 AST 生成程式碼重新整理程式碼。

  5. 在進行變更的時候都重新走一次 hot compile 和執行,實現預覽,這個速度比想象中的會快。

  1. Code Out:輸出的是程式碼,能夠走編譯,不要看小基於 LLVM 架構的語言(Swift、Golang、C++,Rust),效能會比解析好的多得多。

  2. Swift as SSOT:

    1. 永遠可以手寫程式碼,視覺化程式設計的語義基於必然是永遠是現代化程式語言的子集,在遇到複雜場景永遠可以用手寫程式碼作為 fallback。

    2. 一門語言描述所有的東西,包括介面,在假設上述 a 會發生的場景下,使用者不需要學習兩套語法兩套設計模式,NoCode 和 ProCode 可以隨時切換,XCode 8 的 Storyboard(2016) 就是一個基於 XML 語法的的 NoCode 方案,但是今天 XCode 主推 SwiftUI 肯定有它的理由(有效市場假說)。

  3. 利於開放生態和與生態結合:

    1. 例如 Copilot 就很硬核。

    2. 例如各種 Dependency Analyzer

    3. 例如 http://marketplace.visualstudio.com/items?itemName=thankcreate.power-fsm-viewer

    4. ...

    5. 任何人可以基於你的語法開發包,你也可以引這些包,實現了 protocal View 都可以想用同樣的 UI Inspector/Editor,其實在遊戲行業的遊戲引擎這種操作是司空見慣的,可以參照 Unity/UE 5/Cocos 這些成熟的,支援外掛和有良好反射機制的遊戲引擎。

    6. 任何人可以基於開源的 Lang Server 做進一步的擴充套件,新增更多的視覺化編輯模式,例如狀態機的編輯模式、例如工作流的編輯模式,最終都是生成程式碼,也同樣可以 Code Out 和 Compile。

    7. ...

參考資料

[1]

Storyboard: http://www.raywenderlich.com/5055364-ios-storyboards-getting-started

[2]

programming language: http://en.wikipedia.org/wiki/Programming_language

[3]

computer: http://en.wikipedia.org/wiki/Computer

[4]

程式語言: http://zh.wikipedia.org/wiki/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80

[5]

編譯器: http://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E5%99%A8

[6]

解釋型語言: http://zh.wikipedia.org/wiki/%E7%9B%B4%E8%AD%AF%E8%AA%9E%E8%A8%80

[7]

程式碼: http://zh.wikipedia.org/wiki/%E7%A8%8B%E5%BC%8F%E7%A2%BC

[8]

機器程式碼: http://zh.wikipedia.org/wiki/%E6%A9%9F%E5%99%A8%E7%A2%BC

[9]

程式語言: http://zh.wikipedia.org/wiki/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80

[10]

編譯語言: http://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E8%AA%9E%E8%A8%80

[11]

編譯器: http://zh.wikipedia.org/wiki/%E7%B7%A8%E8%AD%AF%E5%99%A8

[12]

機器程式碼: http://zh.wikipedia.org/wiki/%E6%A9%9F%E5%99%A8%E7%A2%BC

[13]

直譯器: http://zh.wikipedia.org/wiki/%E7%9B%B4%E8%AD%AF%E5%99%A8

[14]

子程式: http://zh.wikipedia.org/wiki/%E5%AD%90%E7%A8%8B%E5%BC%8F

[15]

證明 JS 和 TS 型別程式設計是圖靈完備的: http://bytedance.feishu.cn/docs/doccnRnXc5HMxIPzUKq91mbfRph

[16]

Formal Language: http://en.wikipedia.org/wiki/Formal_language

[17]

Gottlob Frege: http://en.wikipedia.org/wiki/Gottlob_Frege

[18]

Begriffsschrift: http://en.wikipedia.org/wiki/Begriffsschrift

[19]

三元悖論: http://zh.wikipedia.org/wiki/%E4%B8%89%E5%85%83%E6%82%96%E8%AE%BA

- END -

:heart: 謝謝支援

以上便是本次分享的全部內容,希望對你有所幫助^_^

喜歡的話別忘了 分享、點贊、收藏 三連哦~。

歡迎關注公眾號 ELab團隊 收貨大廠一手好文章~

aPaaS Growth 團隊專注在使用者可感知的、巨集觀的 aPaaS 應用的搭建流程,及租戶、應用治理等產品路徑,致力於打造 aPaaS 平臺流暢的 “應用交付” 流程和體驗,完善應用構建相關的生態,加強應用搭建的便捷性和可靠性,提升應用的整體效能,從而助力 aPaaS 的使用者增長,與基礎團隊一起推進 aPaaS 在企業內外部的落地與提效。