天天看點

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

大廠技術  堅持周更  精選好文

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).

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

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

什麼是計算機程式

​​https://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](https://en.wikipedia.org/wiki/Execution_(computing "execute")).

什麼是編譯型語言

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

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

什麼是解析型語言

​​https://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])

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

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

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

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

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

image.png

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

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

​​

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

​​

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

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

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

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 可以參照:https://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。

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

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

​​https://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 的界面設計的體驗

體驗

Single Source of Truth: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. 對應棧
  1. 在滑鼠點選對應的具體代碼的時候,通過 AST 的文本範圍資訊可以反射出來是 AST 的哪個塊,也就找到了對應 UI 的執行個體的值和類型資訊。
  2. 反之也是一樣的,點選視圖元件的時候,可以反射出具體的 AST 塊,通過文本範圍資訊也就找到了對應的代碼。
  3. 修改值的時候,通過 3 的對應邏輯修改 AST 的值,通過 Code Generator 輸入 AST 生成代碼重新整理代碼。
  4. 在進行變更的時候都重新走一次 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 肯定有它的理由(有效市場假說)。
  1. 利于開放生态和與生态結合:
  1. 例如 Copilot 就很硬核。
  2. 例如各種 Dependency Analyzer
  3. 例如 https://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: ​​https://www.raywenderlich.com/5055364-ios-storyboards-getting-started​​

[2]

programming language: ​​https://en.wikipedia.org/wiki/Programming_language​​

[3]

computer: ​​https://en.wikipedia.org/wiki/Computer​​

[4]

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

[5]

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

[6]

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

[7]

代碼: ​​https://zh.wikipedia.org/wiki/%E7%A8%8B%E5%BC%8F%E7%A2%BC​​

[8]

機器代碼: ​​https://zh.wikipedia.org/wiki/%E6%A9%9F%E5%99%A8%E7%A2%BC​​

[9]

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

[10]

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

[11]

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

[12]

機器代碼: ​​https://zh.wikipedia.org/wiki/%E6%A9%9F%E5%99%A8%E7%A2%BC​​

[13]

解釋器: ​​https://zh.wikipedia.org/wiki/%E7%9B%B4%E8%AD%AF%E5%99%A8​​

[14]

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

[15]

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

[16]

Formal Language: ​​https://en.wikipedia.org/wiki/Formal_language​​

[17]

Gottlob Frege: ​​https://en.wikipedia.org/wiki/Gottlob_Frege​​

[18]

Begriffsschrift: ​​https://en.wikipedia.org/wiki/Begriffsschrift​​

繼續閱讀