Magmay-ari ng shared observable state sa SwiftUI App mo, pagkatapos ay ipasa ang iisang instance sa dalawang scene. Panatilihin ang draft ng bawat window sa loob ng root view nito. Dahil dito, maa-update ng menu bar control ang bawat window habang napapanatili ng bawat window ang sarili nitong hindi pa tapos na teksto.
Binubuo ng tutorial na ito ang ganitong ayos para sa isang maliit na Mac label editor: iisang shared label ang ipinapakita sa maraming window, may hiwalay na draft ang bawat window, at kayang baguhin ng menu bar panel ang shared label o magbukas ng isa pang window. Ang lahat ng halimbawang teksto ay para sa app at gumagamit ng mga sintetikong label.
Magpasya kung aling mga pagbabago ang dapat makita sa lahat ng window
Magsimula sa inaasahang gawi ng bawat value. Dapat magbago sa lahat ng lugar ang shared label kapag inedit ito. Dapat magbago lang sa window kung saan nagta-type ang isang tao ang hindi pa tapos na draft. Tinutukoy ng mga kinakailangang ito kung saan dapat ilagay ang mga value.
Idinodokumento ng Apple na may hiwalay na storage ang bawat window na ginagawa ng WindowGroup para sa state na ini-instantiate sa loob ng view hierarchy nito. Sa kabilang banda, ipinapakita ng gabay ng Apple sa model data kung paano magmay-ari ng observable reference sa pinakamataas na antas ng App at ipasa ito sa isang view hierarchy. Pinagsasama ng mga pattern na iyon ang sumusunod na plano ng pagmamay-ari. WindowGroup, Pamamahala ng model data sa iyong app.
| Value | May-ari sa halimbawang ito | Nilalayong gawi |
|---|---|---|
| Shared label | App, gamit ang @State kasama ng @Observable model |
Ini-edit ng bawat window at ng menu bar panel ang iisang value |
| Hindi pa tapos na draft | Root view ng bawat window, gamit ang @State |
Nananatili sa window na iyon ang pagta-type |
| Mga control ng menu bar | Tumatanggap ng model na pag-aari ng app | Umaabot ang mga pagbabago sa lahat ng consumer ng instance na iyon |
| Aksyon para magbukas ng window | Binabasa mula sa environment ng menu bar view | Nagbubukas ng window mula sa tinukoy na grupo |
Ang pagkakaiba ay kung sino ang may-ari ng instance. Hindi nagiging shared ang laman ng dalawang magkahiwalay na ginawang object dahil lang pareho ang pangalan ng kanilang class. Gayundin, hindi gumagawa ng magkakahiwalay na kopya ng object ang paglalagay ng reference sa iisang object na pag-aari ng app sa ilang window. Kaya isang beses lang ginagawa ng halimbawang ito ang shared model sa antas ng app.
Itakda ang macOS baseline bago idagdag ang mga scene
Gumamit ng macOS SwiftUI app project na may deployment target na macOS 14 o mas bago at Xcode toolchain na sumusuporta sa Observation, gaya ng Xcode 15 o mas bago. Nagsisimula sa macOS 14 ang integrasyon ng SwiftUI sa Observation. Available ang MenuBarExtra at ang window style nito mula sa macOS 13, kaya Observation ang nagtatakda ng mas mataas na baseline para sa implementasyong ito. Pamamahala ng model data sa iyong app, MenuBarExtra, window.
Gumagamit ang menu bar panel ng .menuBarExtraStyle(.window) dahil naglalaman ito ng mga text field at custom na layout. Inilalarawan ng Apple ang style na ito bilang isang popover-like na window na may mga control na kahawig ng mga nasa karaniwang window. Hiwalay itong scene sa WindowGroup ng editor. window.
Malinaw ding nililimitahan ng halimbawa ang model at UI type nito sa @MainActor. Tinutukoy ng Apple ang main actor bilang may executor na katumbas ng main dispatch queue. Dito, nagmumula sa mga UI control ang lahat ng pagbabago; hindi kailangan ng anumang background work. MainActor.
Buuin ang shared model at magkakahiwalay na draft ng mga window
Palitan ang ginawang app entry point ng sumusunod na kumpletong halimbawa sa iisang file. Mag-iwan lang ng isang deklarasyong @main sa target. Kasama sa sample ang mga import, model, view, at panimulang teksto nito. Sinuri ito batay sa mga dokumentadong API, pero hindi ito nakompile sa environment na ito dahil walang Swift toolchain na magagamit.
import SwiftUI
import Observation
@MainActor
@Observable
final class LabelStore {
var sharedLabel = "Sample Alpha"
}
@main
@MainActor
struct LabelScopeApp: App {
@State private var store = LabelStore()
var body: some Scene {
WindowGroup("Label Editor", id: "label-editor") {
EditorWindow()
.environment(store)
}
MenuBarExtra("Label Controls", systemImage: "square.grid.2x2") {
LabelMenuPanel()
.environment(store)
}
.menuBarExtraStyle(.window)
}
}
@MainActor
struct EditorWindow: View {
@Environment(LabelStore.self) private var store
@State private var draft = "Sample Draft"
var body: some View {
VStack(alignment: .leading, spacing: 16) {
SharedLabelControls()
Divider()
TextField("This window's draft", text: $draft)
.textFieldStyle(.roundedBorder)
Button("Use Draft as Shared Label") {
store.sharedLabel = draft
}
Text("Publishing the draft keeps its local text intact.")
.font(.caption)
}
.padding(20)
.frame(minWidth: 420, minHeight: 220)
}
}
@MainActor
struct SharedLabelControls: View {
@Environment(LabelStore.self) private var store
var body: some View {
@Bindable var editableStore = store
VStack(alignment: .leading, spacing: 8) {
Text("Shared label: \(store.sharedLabel)")
TextField("Shared label", text: $editableStore.sharedLabel)
.textFieldStyle(.roundedBorder)
}
}
}
@MainActor
struct LabelMenuPanel: View {
@Environment(\.openWindow) private var openWindow
var body: some View {
VStack(alignment: .leading, spacing: 16) {
SharedLabelControls()
Button("Open Another Editor") {
openWindow(id: "label-editor")
}
}
.padding(16)
.frame(width: 320)
}
}
May dalawang tahasang environment injection: isa sa content ng window at isa sa content ng menu bar. Pareho nilang tinatanggap ang store. Huwag asahang magbibigay ng dependency sa kapatid na scene ang environment modifier na inilakip sa content ng isang window. Idinodokumento ng Apple ang pagbabahagi ng environment sa loob ng view hierarchy; malinaw na ipinapakita ng dalawang injection kung anong dependency ang kailangan ng bawat hierarchy. Pamamahala ng model data sa iyong app.
Gamitin ang @Bindable kapag kailangan ng control ng binding
Binabasa ng SharedLabelControls ang model sa pamamagitan ng @Environment(LabelStore.self). Sa loob ng body nito, nagbibigay ang lokal na @Bindable variable ng projected binding na kailangan ng TextField. Tinutukoy nito ang umiiral nang observable object at hindi ito gumagawa ng panibagong store. Idinodokumento ng Apple ang eksaktong pattern na ito mula environment hanggang binding. Bindable.
Samantala, ginagamit ng draft field ang $draft dahil pag-aari na ng @State ng view ng window ang value nito. Kinokopya ng button ang kasalukuyang draft papunta sa shared label. Tahasang aksyon ito: hindi muling isusulat ng mga susunod na pagbabago sa shared label ang draft ng alinmang window. Ito ang piniling paraan ng pag-edit sa sample.
Itugma nang eksakto ang identifier ng window group
Kinukuha ng menu bar button ang openWindow mula sa sarili nitong environment at tinatawag ito gamit ang \"label-editor\", na katugma ng deklarasyon ng grupo. Idinodokumento ng Apple ang paraang ito na gumagamit ng identifier para gumawa ng window mula sa isang grupo. Magbukas ng isa pang editor ang layunin ng button, kaya hindi ipinapangako ng halimbawang ito na hahanapin at gagamiting muli nito ang isang partikular na umiiral nang window. WindowGroup.
Suriin ang saklaw gamit ang sunod-sunod na hakbang sa dalawang window
Inaasahang pagsusuri ng gawi ng sample ang sumusunod na pagkakasunod-sunod. Ipinapakita nito ang pagkakaiba ng iisang shared model identity at magkakahiwalay na draft storage nang hindi nangangailangan ng dagdag na instrumentation.
- Ilunsad ang app. Ipinapakita ng unang editor ang shared label na
Sample Alphaat ang draft naSample Draft. - Buksan ang menu bar panel at piliin ang
Open Another Editor. - Palitan ang draft ng unang window ng
Draft One. Huwag baguhin ang draft ng ikalawang window. - Sa menu bar panel, palitan ang shared label ng
Sample Beta. Dapat ipakita ng dalawang editor window ang shared label na iyon habang pinananatili ang magkaibang draft value nila. - Sa unang window, piliin ang
Use Draft as Shared Label. MagigingDraft Oneang shared label sa dalawang window at sa menu bar panel. MananatilingSample Draftang draft ng ikalawang window.
Sinusuri ng pagkakasunod-sunod na ito ang tatlong magkakaibang daloy: lokal na pagta-type, pag-edit sa menu bar, at tahasang pagkopya mula lokal papuntang shared. Kung binabago ng lokal na pagta-type ang draft ng kabilang window, tingnan kung nailipat ang draft sa store na pag-aari ng app. Kung hindi umaabot sa mga window ang mga pagbabago sa menu bar, tingnan kung may isa pang LabelStore() na ginawa sa hierarchy ng menu bar.
| Sintomas | Unang dapat suriin |
|---|---|
| Hindi nagtutugma ang mga label ng window at menu bar | Magkakahiwalay na pag-initialize ng model sa halip na iisang shared instance |
| Sabay-sabay na nagbabago ang lahat ng draft | Nailagay ang draft storage sa shared model |
| Nagkakaroon ng error ang view kapag binabasa ang model nito | Walang .environment(store) sa hierarchy na iyon |
| Hindi inaasahang nagre-reset ang editor | Pagbabago sa identity ng may-ari nitong view o pagpapalit sa model nito |
| Hindi nagbubukas ng isa pang editor | Hindi magkatugma ang idineklarang scene identifier at ang hinihinging identifier |
May dokumentadong resulta ang kawalan ng environment: kapag nagbasa ang code ng nonoptional na observable object batay sa type nito, ipinapalagay nitong may ancestor na nagbigay nito; ayon sa Apple, magdudulot ng exception kapag wala ang object. Ibigay rin ang dependency sa mga preview at alternatibong entry point. Pamamahala ng model data sa iyong app.
Nasa memory lang ang lahat ng data sa sample at wala itong implementasyon para sa pag-save o pagpapanumbalik. Kaya dapat ituring ang pagsasara ng window bilang pagtatapon sa hindi pa tapos na draft nito, at ang pag-quit bilang pagtatapon sa shared label. Kung kailangang mapanatili ang mga draft sa kabila ng mga aksiyong iyon, magtakda muna ng hiwalay na requirement sa persistence bago palawakin ang halimbawang ito.
Panatilihing hiwalay ang mga command para sa aktibong window sa mga shared menu bar control
Hindi kailangan ng pagpili sa aktibong window para sa shared label: iisang model ang natatanggap ng lahat ng consumer. Iba ang kailangan ng aksyong nag-e-edit sa draft ng kasalukuyang nakapokus na window dahil kailangang tukuyin nito ang isang partikular na window.
Para sa karaniwang mga command sa menu ng app, idinodokumento ng Apple ang paglalathala ng model na pag-aari ng window gamit ang focusedSceneValue at paggamit dito sa pamamagitan ng @FocusedValue sa isang implementasyong Commands. Hindi pinapagana ng halimbawa nito ang command kapag walang nakapokus na model. Suportadong paraan ito para sa mga aksyong sumusunod sa aktibong scene bilang target. Pagbuo at pag-customize ng menu bar gamit ang SwiftUI.
Huwag ipagpalagay mula sa halimbawang command na iyon na magkakaroon ang MenuBarExtra ng maaasahang reference sa dating aktibong editor. Sadyang app-wide state ang ginagamit ng panel sa artikulong ito. Kung kailangang mag-target ang panel ng isang partikular na editor, magdagdag ng tahasang disenyo sa pagpili ng target, gaya ng listahan ng mga identifier ng editor na pag-aari ng app, sa halip na ipagpalagay kung aling window ang dapat tumanggap ng aksyon.
Idagdag lang ang AppKit o TextKit kung kailangan ng editor ang mga ito
SwiftUI controls lang ang kailangan ng label editor. Kung lumawak ang mga requirement nito at mangailangan ng custom na AppKit text view, suportadong tulay ng Apple ang NSViewRepresentable para gumawa, mag-update, at mag-alis ng NSView sa isang SwiftUI hierarchy. Inilalarawan din ng Apple ang paggamit ng coordinator para ipasa sa SwiftUI ang mga mensahe mula sa delegate at target-action. NSViewRepresentable.
Panatilihin ang plano sa pagmamay-ari kapag idinagdag ang tulay na iyon: panatilihing nakaugnay sa window nito ang instance ng view at editing state ng bawat editor, at ipasa lang sa app model ang shared content kapag nilalayong ibahagi ito. Dapat ipasa ng coordinator ang mga pag-edit sa pamamagitan ng binding o aksyon ng model. Bilang pag-iingat sa implementasyon, ihambing ang dumarating na content bago palitan ang nilalaman ng text view para hindi paulit-ulit na ma-reset ng mga walang-kaugnayang shared update ang kasalukuyang pag-edit.
Para sa implementasyon gamit ang TextKit 2, tinutukoy ng Apple ang textLayoutManager bilang modernong puntong daanan sa layout engine. Ipinapaliwanag sa TextKit session nito na maaaring magpasimula ng compatibility mode ang pag-access sa lumang layoutManager sa isang TextKit 2 text view. Lilipat ang view na iyon sa TextKit 1 at hindi ito awtomatikong babalik. Iwasang gamitin ang lumang property na iyon para lang siyasatin ang isang text view. TextKit, Mga bagong feature sa TextKit at text view.
Kung kailangan ang suporta para sa macOS 13, panatilihin ang parehong ayos ng pagmamay-ari pero gamitin ang mas lumang paraan ng observation: ObservableObject na may @Published, isang may-ari na @StateObject, at .environmentObject kasama ng mga tumatanggap na property na @EnvironmentObject. Ipinapaliwanag ng gabay sa migration ng Apple kung paano tumutugma ang mga mekanismong iyon sa mas bagong paraan ng Observation. Dapat mapanatili ng pagpapalit ng sistema ng observation ang nilalayong mga hangganan ng pagbabahagi. Paglipat mula sa Observable Object protocol papunta sa Observable macro.
Piliin ang pagmamay-ari batay sa inaasahan ng isang tao sa interface: isang instance na pag-aari ng app para sa shared label, isang value na pag-aari ng bawat window para sa bawat hindi pa tapos na draft, at tahasang aksyong nag-uugnay sa mga ito. Gamitin ang pagkakasunod-sunod sa dalawang window bilang regression check sa tuwing lalawak ang menu bar panel o editor.
Mga Sanggunian
- Pamamahala ng model data sa iyong app — Apple
- WindowGroup — Apple
- MenuBarExtra — Apple
- window — Apple
- MainActor — Apple
- Bindable — Apple
- Pagbuo at pag-customize ng menu bar gamit ang SwiftUI — Apple
- NSViewRepresentable — Apple
- TextKit — Apple
- Mga bagong feature sa TextKit at text view — Apple
- Paglipat mula sa Observable Object protocol papunta sa Observable macro — Apple