SwiftUI + TCA (Onevcat) 上集

更新於 發佈於 閱讀時間約 38 分鐘
New Zealand — Deer Park Heights Queenstown

New Zealand — Deer Park Heights Queenstown

這篇是閱讀了喵神的文章 TCA - SwiftUI 的救星?,這一系列喵神分成了四篇來講解,每篇都有許多重點,而且也是從易到難,很好閱讀,以下是個人的學習筆記,內容幾乎原文搬過來的,但也有是學習過程中額外補充的,因為有語法的更新。 TCA 的核心思想還是大同小異,所以建議可以先閱讀原文,假如遇到語法不支援之類的問題,在參考這邊。

因為篇幅的關係,分成上下兩集,下集在此

以下程式碼語法已更新至 TCA v1.20.2

附上完整 demo,使用 Xcode 16.4

第一章

前言

從2019年發表 SwiftUI 至今,也要6年的歷史了,Apple 發表至今沒有給他特定的架構,例如以前的 UIKit 有 MVC 架構。

官方很多教學,有資料傳遞,也有狀態管理,但似乎也不夠指導我們寫出穩定的 app。

在 SwiftUI 中做到了 single source of truth: 所有的 View 都是由狀態變化出來的,但是也有一些問題,例如:

  • 在使用時數據管理是個人覺得學習門檻比較高的地方,因為有各種狀態的屬性包裝器(Property Wrapper)要先搞懂,像是 @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject 之類的,假如沒有先搞清楚他們的特性,很多時候是不知道如何正確使用。
  • 很多修改狀態的程式碼室內檻在 View.body 內,或者只能在 body 中和其他 View 的程式碼混雜再一起。而且同一個狀態可能會被多個不相關的 View 直接修改(例如: @Binding),這些修改難以追蹤定位,在 app 很複雜的情況下簡直是噩夢。
  • 測試困難,這點有一點反直覺,因為 SwiftUI 框架的 View 是由狀態決定的,所以理論上我們只需要測試狀態(也就是 Model 層),這應該是很容易的事,但是照著 Apple 官方教學來看,app 中會有許多 private 狀態,這些難以 Mock,而且就算可以,如何測試對這些狀態修改也是問題。

簡單地克服方法當然也有,把各種狀態的屬性包裝器完全搞懂,盡可能減少共享可變狀態來避免被意外修改,或者按照 Apple 的推薦準備一組 preview 的數據,然後打開 View 文件去挨個檢查 preview 的結果,但是還是有些自動化的工具可以協助

但結論就是我們需要一種架構,讓使用 SwiftUI 更容易輕鬆。

從 Elm 啟發


Elm 架構 ( The Elm Architecture, TEA)

raw-image
  1. 在 View 上做某個操作(例如點擊某個按鈕),這時會以消息的方式傳送。在 Elm 中的某種機制將會捕獲到這消息。
  2. 偵測到新消息來時,它會和當前 Model 一起作為輸入傳遞給 update 函數。這個函數通常是開發者需要花費時間最多的部分,控制 app 狀態的變化。是 Elm 架構的核心,需要根據輸入的消息和狀態,演算出新的 Model 。
  3. 這個新的 Model 將替換原有的 Model ,並準備在下一個 msg 到來時,再次重複上面的過程,去捕獲新的狀態。
  4. Elm 執行時負責在得到新 Model 後呼叫 View 函數,渲染出結果。用戶可以透過他再次發送新的消息,重複上面的循環。

目前對 TEA 有基本的了解,我們回頭看一下 SwiftUI 中的實現,就像步驟4一樣:當 @State 或 @ObservedObject 的 @Published 發生變化時,SwiftUI 會自動呼叫 View.body 為我們渲染新畫面。

簡單範例


一個很簡單的範例,功能就是有一個數字,可以分別用加和減按鈕控制,使用 SwiftUI + TCA 做法如下:

// logic
@Reducer
struct Counter {

@Dependency(\.counterEnvironment) var environment

@ObservableState
struct State: Equatable {
var count: Int = 0
}

enum Action {
case increment
case decrement
}

struct Environment {}

var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .decrement:
state.count -= 1
case .increment:
state.count += 1
}
return .none
}
}
}

extension DependencyValues {
var counterEnvironment: Counter.Environment {
get { self[Counter.Environment.self] }
set { self[Counter.Environment.self] = newValue }
}
}

// view
struct CounterView: View {

@Bindable var store: StoreOf<Counter>

var body: some View {
HStack {
Button("-") { store.send(.decrement) }
Text("\(store.count)")
Button("+") { store.send(.increment) }
}
}
}

#Preview {
CounterView(
store: Store(initialState: Counter.State()) {
Counter()
}
)
}

我們先看最核心的 Reducer:

  1. 發送訊息,而非直接改變狀態: 任何操作,都向 store 發送 Action ,而 Action 適合用 enum 定義。
  2. 只在 Reducer 中改變狀態: Reducer 是邏輯核心,也是 TCA 中最靈活的地方,所以狀態的改變,應在 Reducer 中完成,他的初始化是長這樣:
public func reduce(
into state: inout Body.State, action: Body.Action
) -> Effect<Body.Action>

inout 的 State 讓我們可以「原地」對 state 進行變更,而不需要明確地返回它。這個函數的回傳值是一個 Effect,它代表不應該在 Reducer 中的副作用,例如 API 請求,取得當前時間等。

但記得在非同步的事件,例如 API 請求,我們通常會寫 .run { send in ... } ,不要在這裡面直接改變狀態,應該要由一個 Action 來改變,所以通常 call API 後的結果處理會有兩個 Action ( Success 和 Failure)。

  1. 更新狀態並觸發渲染: 在 Reducer 閉包中改變狀態是合法的,新的狀態將被 TCA 用來觸發 view 的渲染,保存下來等待下一次 Action 到來。

Reducer 的核心是純函式特性

  • Action 一般來說就像 User 的某個操作,例如點擊按鈕
  • Environment 提供依賴解決了 reducer 輸入階段的副作用(比如 reducer 需要獲取某個 Date 等),這很關鍵,Dependency injection (依賴注入)都從這來做。
  • Effect 解決的則是 reducer 輸出階段的副作用,如果在 Reducer 接收到某個行為之後,需要做出非狀態變化的反應,比如發送一個網路請求、向硬碟寫一些數據,或是監聽某個通知等,都需要透過 Effect 進行。
  • Effect 定義了需要在純函數外執行的程式碼,以及處理結果的方式:一般來說這個執行過程會是一個耗時的行為,行為的結果通過 Action 的方式在未來某個時間再次觸發 reducer 並更新最終狀態。
  • TCA 在運行 reducer 的程式碼,並獲取到返回的 Effect 後,負責執行它所定義的程式碼,然後按照需要發送新的 Action 。

Debug & Test


在 TCA 中有非常方便的 debug() 方法,打印出接收到 Action 以及其中 State 的變化

 var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
...
}
}
._printChanges()
}
raw-image


而且他只有在 #if DEBUG 的編譯條件下打印

import ComposableArchitecture
import Foundation
import Testing

@MainActor
struct CounterTests {

@Test
func increment() async throws {
let store = TestStore(initialState: Counter.State()) {
Counter()
}
await store.send(.increment) {
$0.count = 1
}
}
}

假設出錯的話,故意寫一個錯誤的測試

    @Test
func increment() async throws {
await store.send(.increment) {
$0.count = 2
}
}
raw-image

也可以很好 debug,一目瞭然

另外官方建議測試的 store 都是各自獨立,不要寫成共享的 gloabl 變數

@MainActor
struct FeatureTests {
// 👎 Don't do this:
- let store = TestStore(initialState: Feature.State()) {
- Feature()
- }

@Test
func basics() async {
// 👍 Do this:
+ let store = TestStore(initialState: Feature.State()) {
+ Feature()
+ }
// ...
}
}

原因:

  • 這樣可以確保每個測試都有乾淨的初始狀態,避免受其他測試影響。
  • 你可以根據不同的測試需求,精確地設定 initialState 和 dependencies
  • 假如真的需要用 global 變數共享,記得每個測試使用完要 await store.finish()

Store 和 ViewStore


切分 Store 避免不必要的 view 更新, Store 是狀態持有者,同時也負責在運行的時候連結 State 和 Action 。Single source of truth 是狀態驅動 UI 的最基本原則之一,由於這個要求,我們希望持有狀態的角色只有一個。因此很常見的選擇是,一整個 app 只有一個 Store 。UI 對這個 Store 進行觀察。UI 對這 Store 進行觀察 (比如透過將它設置為 @ObservedObject),攫取它们所需要的狀態,並對狀態的變化做出響應。

raw-image

通常一個 Store 會存在非常多的狀態,但是具體的 View 一般只需要其中一個很小的子集。比如上圖中 View1 只需要 Sate1 不需要知道 State2

如果讓 View 直接觀察整個 Store ,某個狀態發生變化時,SwiftUI 將會要求所有對 Store 進行觀察的 UI 更新,這樣就造成所有的 view 都對 body 進行重新渲染,是非常消耗資源的。 (這讓我聯想到 iOS 17 後 Apple 推出 Observation ,也是一樣的道理,原本使用 ObservableObject 的 Model,假如有更新時,會更新的 UI,效能較差,但新的 Observation 不會)

例如下圖的 State 2 發生了變化,但是不依賴 State 2 的 View 1 和 View 1-1 只是因為觀察了 Store,也會由於 @ObservedObject 的特性,重新對 body 進行求值:

raw-image

而 TCA 為了避免這問題,把傳統意義的 Store 進行拆分,發明了 ViewStore 的概念:

Store 依然是狀態的實際管理者和持有者,它代表了 app 狀態的 純數據層 的表示。在 TCA 的使用者來看 Store 最重要的功能,是對狀態進行切分

程式碼

// state:
struct State1 {
struct State1_1 {
var foo: Int
}

var childState: State1_1
var bar: Int
}

struct State2 {
var baz: Int
}

struct AppState {
var state1: State1
var state2: State2
}

let store = Store(
initialState: AppState( /* */ ),
reducer: appReducer,
environment: ()
)

在將 Store 傳遞給不同頁面使用時,可以用 .scope 將其「切分」,這在後面章節會再詳細說明

let store: Store<AppState, AppAction>

var body: some View {
TabView {
View1(
store: store.scope(
state: \.state1, action: AppAction.action1
)
)
View2(
store: store.scope(
state: \.state2, action: AppAction.action2
)
)
}
}

這樣就可以限制每個頁面能夠訪問到的狀態,抱持清晰。

raw-image
struct CounterView: View {
let store: Store<Counter, CounterAction>
var body: some View {
WithViewStore(store) { viewStore in
HStack {
Button("-") { viewStore.send(.decrement) }
Text("\(viewStore.count)")
Button("+") { viewStore.send(.increment) }
}
}
}
}

TCA 透過 WithViewStore 來把一個代表純資料的 Store 轉換成 SwiftUI 可觀測的資料。不出意外,當 WithViewStore 接受的閉包滿足 View 協議時,它本身也將滿足 View,這也是為什麼我們能在 CounterView 的 body 直接用它來構建一個 View 的原因。 WithViewStore 這個 view,在內部持有一個 ViewStore 類型,它進一步保持了對於 store 的引用。作為 View,它透過 @ObservedObject 對這個 ViewStore 進行觀察,並回應它的變更。因此,如果我們的 View 持有的只是切分後的 Store,那麼原始 Store 其他部分的變更,就不會影響到目前這個 Store 的切片,從而保證那些和當前 UI 不相關的狀態改變,不會導致當前 UI 的刷新。

注意! WithViewStore 的用法已棄用,可參考 Migrated to 1.7 的文件

iOS 17 以下,用 WithPerceptionTracking ,且 store property 要這樣宣告 @Perception.Bindable var store: StoreOf<Feature> 。

但假設是 iOS 17 以上,WithViewStore 和 WithPerceptionTracking 都不用了

而且現在會使用 @ObservableState 在 State 上,而 view 上的 store 加上 @Bindable 。

@Reducer
struct Feature {
@ObservableState
struct State {
// ...
}
}

struct View: View {
@Bindable var store: StoreOf<Feature>
// ...
}

第二章

綁定和普通狀態的區別


在上篇的功能裡,基本上狀態驅動 UI 的流程是:點擊按鈕 → 發送 Action → 更新 State → 觸發 UI 更新

除了單純透過狀態來更新 UI,在 SwiftUI 同時也支持反向使用 @Binding 的方式把某個 State 綁定給 UI 元件,這些元件不用經由我們的程式碼就能改變某個狀態,例如 TextFieldToogle 等。

但假如 view 有能力直接改變狀態,其實就違反了 TCA 中關於只能在 reducer 中更改狀態的規定。對於這種情況,TCA 中為 View Store 增加了將狀態轉換爲一種「特殊綁定關係」的方法。

實作


enum Action {
case increment
case decrement
+ case setCount(String)
case reset
}

var body: some ReducerOf<Self> {
Reduce { state, action in
// ...
+ case .setCount(let text):
+ if let value = Int(text) {
+ state.count = value
+ }
+ return .none
// ...
}

再把 body 中原來的 Text 替換 TextField

var body: some View {
// ...
- Text("\(viewStore.count)")
+ TextField(
+ String(viewStore.count),
+ text: store.binding(
+ get: { String($0.count) },
+ send: { CounterAction.setCount($0) }
+ )
+ )
+ .frame(width: 40)
+ .multilineTextAlignment(.center)
.foregroundColor(colorOfCount(viewStore.count))
}

viewStore.binding 方法接受 get 和 send 兩個參數,它們都是和目前 View Store 及綁定 view 類型相關的泛型函數。在特化 (將泛型在這個上下文中轉換為具體類型) 後

  • get: (Counter) -> String 負責為物件 View (這裡的 TextField ) 提供資料。
  • send: (String) -> CounterAction 負責將 View 新發送的值轉換為 View Store 可以理解的 action,並發送它來觸發 counterReducer

在 counterReducer 接到 binding 給予的 setCount 事件後,我們就回到使用 reducer 進行狀態更新,並驅動 UI 的標準 TCA 循環中了。

但在我的 demo code ,是使用 swiftUI native 的 Binding,在 binding 的時候,當 textField 輸入文字時就是發一個 action setCont(String) ,待會在 View 的部分會看到。

再來我們可以簡化一下程式碼,到 Counter 裡新增:

extension Counter.State {
var countString: String {
get { String(count) }
set { count = Int(newValue) ?? count }
}

var countFloat: Float {
get { Float(count) }
set { count = Int(newValue) }
}
}

Reduce 的部分,就可以變成這樣

 case .setCount(let text):
state.countString = text

然後 View 的部分,TextField 改成這樣

TextField(
String(store.count),
text: Binding(
get: { store.countString },
set: { store.send(.setCount($0)) }
)
)

多個綁定值

如果在同一個 Feature 中,有多個綁定值的話,使用例子中這樣的方式,每次都會需要新增一個 action,然後再 binding 中 send 它。這是千篇一律的模板程式碼,TCA 中設計了 BindingStateBindableAction, 以及 BindingReducer 來解決,讓多個綁定的寫法簡單一點,具體來說,分成三步:

  1. 為 State 中的需要和 UI 綁定的變數新增 @BindableState 。
  2. 將 Action 聲明為 BindableAction ,然後新增一個「特殊」的 case binding(BindingAction<Feature>) 。
  3. 在 Reducer 中處理這個 .binding ,並新增 .binding() 調用。

用程式碼來說就是:

// 1
struct MyState: Equatable {
+ @BindableState var foo: Bool = false
+ @BindableState var bar: String = ""
}

// 2
- enum MyAction {
+ enum MyAction: BindableAction {
+ case binding(BindingAction<MyState>)
}

// 3
let myReducer = //...
// ...
+ case .binding:
+ return .none
}
+ .binding()

這樣一番操作後,可以在 View 裡用類似標準 SwiftUI 的做法,使用 $ 取 projected value 來進行 Binding 了:

struct MyView: View {
let store: Store<MyState, MyAction>
var body: some View {
WithViewStore(store) { viewStore in
+ Toggle("Toggle!", isOn: viewStore.binding(\.$foo))
+ TextField("Text Field!", text: viewStore.binding(\.$bar))
}
}
}

這樣一來,即使有多個 binding 值,都可以只用一個 .binding action 處理。這段程式碼能運作是因為 BindableAction 要求一個簽名為 BindingAction<State> -> Self 且名為 binding 的函式:

public protocol BindableAction {
static func binding(_ action: BindingAction<State>) -> Self
}

利用了 enum case 作為函式使用的 Swift 新特性,程式碼變得很優雅。

詳細可以參考官方文件(寫法有稍微不一樣)

Environment

現在可以輸入數字了,我們來做個猜數字的小遊戲,玩法就是從 -100 到 100 之間,隨機一個數字,我們要猜這數字,數字太大太小都要回報給 User。

所以我們先加一個 var secret = Int.random(in: -100...100) 至 State 裡,由他來產出一個隨機數。

    @ObservableState
struct State: Equatable {
var count: Int = 0
let secret = Int.random(in: -100...100)
}

再來檢查 count 和 secret 的關係,返回答案

extension Counter {
enum CheckResult {
case lower, equal, higher
}

var checkResult: CheckResult {
if count < secret { return .lower }
if count > secret { return .higher }
return .equal
}
}

再來就可以修改 View

struct CounterView: View {
@Bindable var store: StoreOf<Counter>
var body: some View {
VStack {
+ checkLabel(with: store.checkResult)
HStack {
Button("-") { store.send(.decrement) }
// ...
}

func checkLabel(with checkResult: Counter.CheckResult) -> some View {
switch checkResult {
case .lower:
return Label("Lower", systemImage: "lessthan.circle")
.foregroundColor(.red)
case .higher:
return Label("Higher", systemImage: "greaterthan.circle")
.foregroundColor(.red)
case .equal:
return Label("Correct", systemImage: "checkmark.circle")
.foregroundColor(.green)
}
}
}

最後我們就有

raw-image

外部依賴

當我們答對後,Reset 按鈕只能清除,不能重新一局,我們讓遊戲好玩一點,變成下一局

所以我們調整

enum Action {
// ...
- case reset
+ case playNext
}

struct CounterView: View {
// ...
var body: some View {
// ...
- Button("Reset") { store.send(.reset) }
+ Button("Next") { store.send(.playNext) }
}
}

在邏輯的部分

@ObservableState
struct State: Equatable {
var count: Int = 0
- let secret = Int.random(in: -100 ... 100)
+ var secret = Int.random(in: -100 ... 100)
}

var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
- case .reset:
+ case .playNext:
state.count = 0
+ state.secret = Int.random(in: -100 ... 100)
return .none
// ...

現在來跑一下測試,結果沒意外是失敗的

func testReset() throws {
- store.send(.reset) { state in
+ store.send(.playNext) { state in
state.count = 0
}
}

這是因為 .playNext 現在不僅重設 count,也會隨機產生新的 secret。而 TestStore 會把 send 閉包結束時的 state 和真正的由 reducer 操作的 state 進行比較並斷言:前者沒有設定合適的 secret,導致它們並不相等,所以測試失敗了。

我們需要一種穩定的方式,來確保測試成功。

使用環境值解決依賴

在 TCA 中,為了保證可測試性,reducer 必須是純函數,也就是說相同輸入 (state, action 和 environment) 的組合,必須能給出相同的輸入(在這輸出是 state 和 effect,後面的章節在接觸 effect 角色)

其中使用了 Int.random 所以無法保證每次的結果,TCA 中的 Environment 就是要處理這種情況,對應外部依賴的情況。

首先程式碼是

struct Environment {
+ var generateRandom: (ClosedRange<Int>) -> Int
}

再來原本 CounterEnvironment() 加上 generateRandom 的設定。

另一種更常見和簡潔的做法,是為 Counter.Environment 定義一組環境,然後把他們傳到相對應的地方:

struct Environment {
var generateRandom: (ClosedRange<Int>) -> Int

+ static let liveValue = Self(
+ generateRandom: Int.random
+ )
+ static let testValue = Self(
// ...
}

CounterView(
store: Store(initialState: Counter.State()) {
Counter()
} withDependencies: {
$0.counterEnvironment = .testValue
}
)

現在,在 reducer 中,就可以使用注入的環境值來達到和原來一樣的結果了

let counterReducer = // ... {
- state, action, _ in
+ state, action, environment in
// ...
case .playNext:
state.count = 0
- state.secret = Int.random(in: -100 ... 100)
+ state.secret = environment.generateRandom(-100 ... 100)
return .none
// ...
}

在 test target 中,就可以創建一個 .test 環境

extension Counter.Environment {
static let test = CounterEnvironment(generateRandom: { _ in 5 })
}

現在,在生成 TestStore 的時候,使用 .test ,然後在斷言時生成合適的 Counter 作為新的 state,測試就能順利通過了

store = TestStore(initialState: Counter.State()) {
Counter()
} withDependencies: {
$0.counterEnvironment = .init(
generateRandom: { _ in 10 },
uuid: { .dummy }
)
}

@Test
func playNext() async throws {
await store.send(.playNext) {
$0.count = 0
$0.secret = 5
}
}

其他常見的依賴

除了像是 random 系列以外,凡事隨著調用環境變化(包括 ID、時間、地點、各種外部狀態等等)而打破 reducer 純函數特性的外部依賴,都應該被納入 Environment 的範疇。

有些可以同步完成,像是 Int.random ,但有些則需要一定的時間才得到結果,比如獲取位置和發送網路請求。對於後者,可以轉換為 Effect 。

接下來我可以參考下集

留言
avatar-img
留言分享你的想法!
avatar-img
CHENGYANG的沙龍
0會員
12內容數
CHENGYANG的沙龍的其他內容
2025/05/17
What is Observation? Observation 是 Swift 中用於追蹤屬性變化的新功能,透過 macro 對它們進行轉換。在 SwiftUI 裡針對特別的 property 發生變化時,才重新計算 View 的 body。
Thumbnail
2025/05/17
What is Observation? Observation 是 Swift 中用於追蹤屬性變化的新功能,透過 macro 對它們進行轉換。在 SwiftUI 裡針對特別的 property 發生變化時,才重新計算 View 的 body。
Thumbnail
2025/01/09
觀看 WWDC23 Write Swift macros 筆記 Overview Apple 一開始提出一個範例,把計算的過程顯示成字串,用一個 tuple 組合起來,但這種方式很明顯,有個大缺點,就是容易有人為疏失,導致錯誤發生,還無法用 compiler 檢查字串是否相等於左邊的算數。
Thumbnail
2025/01/09
觀看 WWDC23 Write Swift macros 筆記 Overview Apple 一開始提出一個範例,把計算的過程顯示成字串,用一個 tuple 組合起來,但這種方式很明顯,有個大缺點,就是容易有人為疏失,導致錯誤發生,還無法用 compiler 檢查字串是否相等於左邊的算數。
Thumbnail
2024/12/08
前言 常在寫 leet code 的朋友應該都知道,除了解出題目很重要,還有時間複雜度以及空間複雜度的問題,若能更理解時間複雜度,然後優化解法,就可以讓寫程式的基本底子更上一層樓,以下簡單介紹常見的時間複雜度以及演算法。 除了時間複雜度,也會順便簡單說明相關東西,如下: 相關演算法
Thumbnail
2024/12/08
前言 常在寫 leet code 的朋友應該都知道,除了解出題目很重要,還有時間複雜度以及空間複雜度的問題,若能更理解時間複雜度,然後優化解法,就可以讓寫程式的基本底子更上一層樓,以下簡單介紹常見的時間複雜度以及演算法。 除了時間複雜度,也會順便簡單說明相關東西,如下: 相關演算法
Thumbnail
看更多
你可能也想看
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
全球科技產業的焦點,AKA 全村的希望 NVIDIA,於五月底正式發布了他們在今年 2025 第一季的財報 (輝達內部財務年度為 2026 Q1,實際日曆期間為今年二到四月),交出了打敗了市場預期的成績單。然而,在銷售持續高速成長的同時,川普政府加大對於中國的晶片管制......
Thumbnail
全球科技產業的焦點,AKA 全村的希望 NVIDIA,於五月底正式發布了他們在今年 2025 第一季的財報 (輝達內部財務年度為 2026 Q1,實際日曆期間為今年二到四月),交出了打敗了市場預期的成績單。然而,在銷售持續高速成長的同時,川普政府加大對於中國的晶片管制......
Thumbnail
先前提到 Quasar 的 Dialog Plugin 很好用,再讓我補充一個用法。
Thumbnail
先前提到 Quasar 的 Dialog Plugin 很好用,再讓我補充一個用法。
Thumbnail
本文介紹瞭如何在SwiftUI中調整元件的對齊方式,包括置中、向左/向右/向上/向下對齊的方法。透過調整HStack、VStack以及frame的maxWidth、maxHeight和alignment屬性,可以達到想要的對齊效果。
Thumbnail
本文介紹瞭如何在SwiftUI中調整元件的對齊方式,包括置中、向左/向右/向上/向下對齊的方法。透過調整HStack、VStack以及frame的maxWidth、maxHeight和alignment屬性,可以達到想要的對齊效果。
Thumbnail
本文檔介紹了在Swift中使用套件的詳細方法,包括如何引用第三方套件和自定義模組,如何創建自定義套件,以及一些常見的Swift套件。這些套件可以幫助開發者快速添加功能到項目中,提高開發效率和程式碼品質。
Thumbnail
本文檔介紹了在Swift中使用套件的詳細方法,包括如何引用第三方套件和自定義模組,如何創建自定義套件,以及一些常見的Swift套件。這些套件可以幫助開發者快速添加功能到項目中,提高開發效率和程式碼品質。
Thumbnail
此章節旨在解釋Swift語言中函數的基本結構和操作方式,包括函數的聲明、呼叫、參數和返回值。閱讀這個章節可以幫助你理解並掌握如何在Swift編程中有效地使用和管理函數。
Thumbnail
此章節旨在解釋Swift語言中函數的基本結構和操作方式,包括函數的聲明、呼叫、參數和返回值。閱讀這個章節可以幫助你理解並掌握如何在Swift編程中有效地使用和管理函數。
Thumbnail
本章節介紹了如何建立並設置Swift項目以及如何選擇和設置Swift代碼編輯器。這包括在Xcode和命令行中建立Swift項目,選擇Xcode、Visual Studio Code或AppCode作為編輯器,以及如何使用SPM安裝插件。
Thumbnail
本章節介紹了如何建立並設置Swift項目以及如何選擇和設置Swift代碼編輯器。這包括在Xcode和命令行中建立Swift項目,選擇Xcode、Visual Studio Code或AppCode作為編輯器,以及如何使用SPM安裝插件。
Thumbnail
本章節旨在為讀者提供Swift程式語言的基礎知識,包括其基本語法、註解方法和變數使用方式,並通過具體的程式碼示例來說明這些概念。這將幫助讀者理解Swift的基本結構,並學會如何在Swift中定義變數並使用註解。
Thumbnail
本章節旨在為讀者提供Swift程式語言的基礎知識,包括其基本語法、註解方法和變數使用方式,並通過具體的程式碼示例來說明這些概念。這將幫助讀者理解Swift的基本結構,並學會如何在Swift中定義變數並使用註解。
Thumbnail
這份文件的目的是介紹Swift語言,包括它的特性、應用範疇,以及誰在使用它。它也提供了一些學習Swift的資源和工具,以及一些常見的Swift庫和框架。
Thumbnail
這份文件的目的是介紹Swift語言,包括它的特性、應用範疇,以及誰在使用它。它也提供了一些學習Swift的資源和工具,以及一些常見的Swift庫和框架。
Thumbnail
Part.1 搞定基本的 UI 開始開發 iOS App。 首先準備一台 Mac,然後安裝 Xcode,新增專案,系統即刻生成基本的專案結構。coding 的起點在檔案 ContentView.swift: import SwiftUI struct ContentView: View {  
Thumbnail
Part.1 搞定基本的 UI 開始開發 iOS App。 首先準備一台 Mac,然後安裝 Xcode,新增專案,系統即刻生成基本的專案結構。coding 的起點在檔案 ContentView.swift: import SwiftUI struct ContentView: View {  
Thumbnail
這是為了搭建自己想要的工作流而開始的研究工作。
Thumbnail
這是為了搭建自己想要的工作流而開始的研究工作。
Thumbnail
ComfyUI教學第一階段之[全面安裝指南],帶你一步一步從頭做起,它確實沒有那麼簡單,卻也沒有那麼困難。本篇介紹從安裝前準備、安裝步驟,到添加擴充功能。帶你開啟AI算圖的深度旅程。配有影片。
Thumbnail
ComfyUI教學第一階段之[全面安裝指南],帶你一步一步從頭做起,它確實沒有那麼簡單,卻也沒有那麼困難。本篇介紹從安裝前準備、安裝步驟,到添加擴充功能。帶你開啟AI算圖的深度旅程。配有影片。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News