SwiftUI 管理多視窗的 WindowGroup & 多視窗的資料存取問題

開發 SwiftUI App 時,我們主要在遵從 App 的型別裡指定 App 的第一個畫面。如以下例子,變數 body 裡 WindowGroup 包含的 ContentView 將是 App 的第一個畫面。

@main
struct DemoApp: App {

var body: some Scene {
WindowGroup {
ContentView()
}
}
}

body 的型別是 some Scene,而 WindowGroup 遵從 Scene,它將負責管理多個視窗。什麼意思呢? 在 iPadOS & macOS,一個 App 可能有多個視窗,比方以下的 iPad 開了兩個 Safari 視窗,我們可以在左邊買 iPhone 12,右邊研究 SwiftUI。(ps: iOS 比較弱一點,只會有一個視窗。)

WindowGroup 怎麼管理多個視窗呢 ? 讓我們瞧瞧它的 init。

public init(@ViewBuilder content: () -> Content)

參數 content 執行後將回傳視窗裡顯示的 view。當 App 啟動生成第一個視窗或是之後新增視窗時,它將呼叫 content,得到視窗要顯示的 view。因為若 App 有三個視窗,以下 WindowGroup 後的 closure 程式將執行三次,產生三個 ContentView。

@main
struct DemoApp: App {

var body: some Scene {
WindowGroup {
ContentView()
}

}
}

了解 WindowGroup 管理多視窗的原理後,接著讓我們認識多視窗的資料存取問題。我們將舉兩個例子,一個是多視窗存取同一份資料,一個是每個視窗有各自獨立的資料。

多視窗存取同一份資料

資料 NumberData 的定義如下。

class NumberData: ObservableObject {
@Published var value = 0
}

在遵從 App 的型別裡宣告儲存資料的 numberData,然後呼叫 environmentObject 讓 ContentView 能存取資料。

import SwiftUI@main
struct DemoApp: App {
@StateObject private var numberData = NumberData()

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(numberData)
}
}
}

ContentView 將顯示 numberData 的數字圖片,點選數字時將換圖。

import SwiftUIstruct ContentView: View {
@EnvironmentObject var numberData: NumberData

var body: some View {
Image(systemName: "\(numberData.value).circle")
.resizable()
.scaledToFit()
.onTapGesture {
numberData.value = Int.random(in: 1...50)
}
}
}

在 iPad 執行 App,App 順利地顯示數字圖片,點選數字後也會隨機更換另一個數字。

接著讓我們新增第二個視窗。從 iPad 底部往上拖曳,開啟 Dock。在 Dock 上按住我們開發的 App icon,然後將它拖離 Dock,移到螢幕的右側。

神奇的事發生了。新增的視窗也是數字 8,不是 NumberData 一開始的初始值 0。當我們點選左側或右側的數字時,兩邊的數字也會同步更新,永遠顯示同一個數字。

為什麼會這樣呢 ? 因為我們在 DemoApp 裡生成 NumberData,它只會生成一次。之後 App 新增視窗產生新的 ContentView 時,存取的將是同一份 NumberData。

@main
struct DemoApp: App {
@StateObject private var numberData = NumberData()

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(numberData)
}
}
}

每個視窗有各自獨立的資料

如果你不想多視窗共享同一份資料,想要每個視窗有各自獨立的資料,則要把生成 NumberData 的程式放到 view 裡,比方我們將它放到 ContentView,因此當 App 新增視窗生成新的 ContentView 時,也會產生新的 NumberData。

struct ContentView: View {
@StateObject private var numberData = NumberData()
var body: some View {
Image(systemName: "\(numberData.value).circle")
.resizable()
.scaledToFit()
.onTapGesture {
numberData.value = Int.random(in: 1...50)
}
}
}

將 DemoApp 裡 numberData 的相關程式移除。

@main
struct DemoApp: App {

var body: some Scene {
WindowGroup {
ContentView()
}
}
}

執行 App,兩個視窗的數字不再同步了。

參考連結

App Structure and Behavior

WindowGroup

App

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com