搭配一個可選的已選項目使用 sheet(item:onDismiss:content:),並根據其閉包收到的項目建構工作表。這讓 Mac app 只需一個值,就能同時決定是否顯示工作表,以及工作表顯示的內容。

本教學會實作一項簡單的桌面工作:點選本機範例項目,立即檢視其詳細資訊。也會提供一套診斷流程,協助排查工作表空白開啟、顯示先前的選取項目,或在 AppKit 檢視區中顯示過時文字的情況。

為什麼使用以項目為基礎的工作表?

SwiftUI 提供兩種相關的呈現方法。布林值多載會在其繫結變為 true 時呈現。項目多載接受一個可選的可識別值,並將該值提供給內容閉包。若內容是固定面板,請選擇布林值形式;若面板內容取決於選取項目,請選擇項目形式。請參閱 Apple 關於以布林值為基礎的工作表與以項目為基礎的工作表的文件。

若設計中分別維護選取狀態與呈現狀態,就可能出現「要求呈現,但沒有選取項目」這類組合。將兩者合併為一個可選值,就能從自己的狀態模型中排除此種組合:nil代表沒有要求顯示的項目,有值則代表要檢視的項目。這是根據兩種 API 得出的設計推論,並非宣稱 SwiftUI 存在未公開說明的排程缺陷。

若現有實作先指定選取項目,再切換布林值,光憑這兩個陳述式還無法判定首次呈現失敗的原因。在歸因於時序之前,請先檢查內容的資料路徑。關鍵在於:工作表是否呈現它收到的輸入、讀取另一個屬性,或顯示一份另行儲存的副本。

建立完整的 macOS 範例

在 Xcode 中建立一個使用 SwiftUI 介面的 macOS app 專案。本範例的部署目標設為 macOS 12 或更新版本。以項目為基礎的工作表 API 最早支援於 macOS 10.15,而下方使用的環境關閉動作需要 macOS 12。Apple 的以項目為基礎的工作表與關閉環境值文件說明了這些版本需求。

將產生的 app 宣告與內容檢視區替換成以下完整程式。目標中只保留一個 app 進入點。以下所有標籤與描述都是本機合成範例資料。

import SwiftUI

struct SampleItem: Identifiable {
    let id: String
    let title: String
    let detail: String
}

@main
struct FirstSheetDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ItemListView()
        }
    }
}

struct ItemListView: View {
    private let items = [
        SampleItem(
            id: "tile-a",
            title: "Amber Tile",
            detail: "A square sample with an amber finish."
        ),
        SampleItem(
            id: "tile-b",
            title: "Blue Tile",
            detail: "A rectangular sample with a blue finish."
        )
    ]

    @State private var presentedItem: SampleItem? = nil

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Sample Items")
                .font(.title2)

            ForEach(items) { item in
                Button("Inspect \(item.title)") {
                    presentedItem = item
                }
            }
        }
        .padding(24)
        .frame(minWidth: 360, minHeight: 200)
        .sheet(item: $presentedItem) { item in
            ItemDetailView(item: item)
        }
    }
}

struct ItemDetailView: View {
    let item: SampleItem
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text(item.title)
                .font(.title)

            Text(item.detail)

            Text("Item ID: \(item.id)")
                .font(.caption)

            HStack {
                Spacer()
                Button("Done") {
                    dismiss()
                }
            }
        }
        .padding(24)
        .frame(width: 380, height: 220)
    }
}

按鈕會將點選的值指定給 presentedItem。接著,呈現閉包會把自己的 item 參數傳入 ItemDetailView。請注意,詳細檢視區使用一般輸入屬性 let item,而不是另一個已選項目狀態屬性。Apple 建議維持單一真實資料來源,並透過屬性分享唯讀值;只有子檢視需要寫入權限時,才使用繫結。請參閱 State。

關閉動作應放在 ItemDetailView 內,讓它從工作表的環境讀取。Apple 明確警告,若在呈現檢視區中讀取該動作,卻從工作表內容呼叫,使用的會是呈現檢視區的環境。請像範例一樣,將宣告放在工作表內容旁。請參閱 dismiss。

我已檢查程式中的輸入與參照是否一致;由於此環境沒有 Swift 工具鏈,因此尚未在此編譯或執行。請在 Xcode 中針對目標 macOS 版本執行以下檢查。

透過明確的點選順序驗證首次呈現

每次檢查首次呈現時,都請先重新啟動 app。只在開啟過其他項目後進行測試,可能會掩蓋原本的症狀。以下預期結果根據範例中明確的標籤與資料指定而定,是驗收標準,並非已回報的測試結果。

動作 預期的工作表內容 檢查項目
啟動後,第一個點選 Inspect Blue Tile Blue Tile、其矩形描述,以及 tile-b 首次呈現使用點選的項目
點選 Done,接著檢視 Amber Tile Amber Tile、其正方形描述,以及 tile-a 後續選取項目有傳入內容
關閉後再次檢視 Amber Tile 相同的琥珀色詳細資訊 重新開啟項目時內容一致
重新啟動後,第一個檢視 Amber Tile 立即顯示琥珀色詳細資訊 正確性不取決於先開啟哪個範例

同時顯示標題與 ID,比只檢查標題更有用。若實際介面允許顯示名稱重複,開發期間可建立兩個標題相同、ID 與描述不同的合成項目。這能揭露是否意外透過顯示文字來選取項目。

從按鈕到最終檢視區診斷過時內容

請依序進行以下檢查。每一步都能縮小不一致發生的位置,讓你修正負責的層,而不必加入無關的呈現權宜作法。

  1. **檢查動作的輸入。**在點選按鈕的動作中設定中斷點,並檢視 item.id。確認動作收到的是預期的記錄。
  2. **檢查呈現輸入。**檢視工作表閉包中的 item 參數。請將該參數傳入詳細檢視區,不要再次查詢另一個選取狀態。
  3. **檢查最終顯示的屬性。**確認標題與描述讀取的是詳細檢視區的輸入。留意是否有快取屬性或區域草稿保留了較早的值。
  4. **檢查任何 AppKit 橋接。**如果一般 SwiftUI 文字正確,但包裝的文字仍過時,請檢查 representable 的更新方法。

維持穩定的識別資訊

以項目為基礎的 API 要求遵循 Identifiable。Swift 將此協定定義為提供穩定識別資訊,其持續時間與範圍則由遵循該協定的型別及其使用者決定。範例使用儲存的字串 ID 區分兩筆記錄。請參閱 Identifiable。

對於實際清單,請選擇一個能在介面支援的更新期間持續識別同一筆記錄的識別碼。避免根據可能變更或多個項目共用的標題產生識別碼。也要避免使用計算式的 id,因為它每次讀取時都會建立新的 UUID,無法提供穩定的識別資訊。

將顯示輸入與可編輯草稿分開

檢視器可以直接顯示 item.title。編輯器可能會刻意建立本機草稿,但草稿會引入另一個值,而你必須定義其生命週期。請決定何時建立草稿、後續模型變更是否應取代草稿,以及使用者完成或取消時要如何處理。

Apple 說明,SwiftUI 會依檢視區的識別資訊保留狀態儲存空間,並在該識別資訊生命週期開始時初始化。重新建立檢視區值本身不會產生全新狀態。因此,從輸入初始化本機狀態,並非讓該狀態持續與後續輸入同步的一般方法。請參閱 Demystify SwiftUI。

對於這項唯讀工作,請保留一般輸入屬性。若要擴充為編輯功能,請明確設計草稿與提交流程,或將繫結連到預期的模型值。避免只為了更新文字而變更檢視區識別資訊:變更識別資訊會取代相關狀態,可能丟棄尚未完成的草稿。這是根據 Apple 文件所述的狀態生命週期模型得出的結果。

工作表包含 AppKit 文字檢視區時,會有哪些不同?

包含 NSTextView 的工作表仍需要相同的已選項目資料路徑,也需要正確實作橋接。Apple 的 NSViewRepresentable 協定將初始建立與後續設定分別交由 makeNSView(context:) 和 updateNSView(_:context:) 負責。若包裝器只在建立期間指定文字,就沒有任何實作可套用後續的文字輸入。請參閱 NSViewRepresentable。

對於唯讀包裝器,請使用提供的文字初始化原生檢視區,並讓其更新方法在有需要時,使顯示文字與目前輸入一致。Apple 說明,當影響對應 AppKit 檢視區的內容發生變更時,SwiftUI 會呼叫更新方法。請參閱 updateNSView(_:context:)。

可編輯的包裝器需要額外的策略:不要在每次更新時都盲目取代文字,以免覆寫進行中的編輯。請使用 coordinator,將原生委派或 target-action 的變更回傳給 SwiftUI,如 Apple 在 representable 文件中所述。讓 SwiftUI 控制所表示檢視區的框架與邊界;Apple 警告不要直接設定這些版面配置屬性。

TextKit 負責文字儲存、版面配置與繪製。Apple 說明了 NSTextView 提供的新式 NSTextLayoutManager 路徑與舊式 NSLayoutManager 路徑。這些功能不會提供工作表的已選記錄。請先確認橋接收到正確的項目文字,再調查文字版面配置;導入其他 TextKit API 前,也請另行檢查其可用性。請參閱 TextKit。

選擇符合工作需求的呈現模型

若內容固定,且唯一會變動的輸入是顯示狀態,請使用布林值多載。本檢視器工作適合使用項目多載。Apple 說明,變更提供的項目可能會關閉並取代現有工作表,因此不要將項目替換視為在同一次呈現期間可不中斷編輯的保證。請參閱 sheet(item:onDismiss:content:)。

若要確保首次呈現正確,請讓已選記錄易於追蹤:按鈕動作中有一次指定、工作表閉包中有一個項目參數,詳細檢視區中有一個輸入。從全新啟動狀態驗證兩個範例項目。若結果仍不一致,診斷流程便能找出不一致是出在選取、保留的狀態,還是原生檢視區更新路徑。

來源