अपने SwiftUI App में साझा observable state रखें, फिर उसी instance को दोनों scenes में पास करें। हर window का draft उसके root view में रखें। इससे menu bar का control हर window को अपडेट कर सकता है, जबकि हर window अपना अधूरा टेक्स्ट बनाए रखती है।
इस ट्यूटोरियल में एक छोटे Mac लेबल एडिटर के लिए यह व्यवस्था बनाई गई है: कई windows एक साझा लेबल दिखाती हैं, हर window का अपना स्वतंत्र draft होता है, और menu bar panel साझा लेबल को संपादित कर सकता है या दूसरी window खोल सकता है। सभी उदाहरण टेक्स्ट ऐप का हिस्सा हैं और उनमें कृत्रिम लेबल इस्तेमाल किए गए हैं।
तय करें कि कौन-से बदलाव हर window तक पहुँचने चाहिए
हर मान के अपेक्षित व्यवहार से शुरुआत करें। संपादित किए जाने पर साझा लेबल हर जगह बदलना चाहिए। अधूरा draft केवल उस window में बदलना चाहिए जिसमें कोई टाइप कर रहा है। ये आवश्यकताएँ तय करती हैं कि मान कहाँ रखे जाएँ।
Apple दस्तावेज़ करता है कि WindowGroup से बनाई गई हर window में उसके view hierarchy के अंदर instantiated state के लिए स्वतंत्र storage होता है। इसके विपरीत, Apple का model-data मार्गदर्शन top-level App में observable reference रखने और उसे view hierarchy में पास करने का उदाहरण देता है। इन पैटर्न को मिलाने से यह ownership योजना बनती है। WindowGroup, Managing model data in your app।
| मान | इस उदाहरण में owner | अपेक्षित व्यवहार |
|---|---|---|
| साझा लेबल | @Observable model के साथ App का @State |
हर window और menu bar panel एक ही मान संपादित करते हैं |
| अधूरा draft | हर window के root view का @State |
टाइपिंग उसी window तक सीमित रहती है |
| Menu bar controls | App के स्वामित्व वाला model प्राप्त करते हैं | बदलाव उस instance के सभी उपभोक्ताओं तक पहुँचते हैं |
| Window खोलने की कार्रवाई | Menu bar view के environment से पढ़ी जाती है | पहचाने गए group से window खुलती है |
फर्क instance के owner का है। दो अलग-अलग बनाए गए objects को एक ही class name देने से उनकी सामग्री साझा नहीं हो जाती। इसी तरह, कई windows में एक app-owned object का reference रखने से उस object की स्वतंत्र प्रतियाँ नहीं बनतीं। इसलिए उदाहरण साझा model को app स्तर पर केवल एक बार बनाता है।
Scenes जोड़ने से पहले macOS का आधार तय करें
macOS SwiftUI app project इस्तेमाल करें, जिसका deployment target macOS 14 या बाद का हो और Xcode toolchain Observation को समर्थन देता हो, जैसे Xcode 15 या बाद का। SwiftUI का Observation integration macOS 14 से शुरू होता है। MenuBarExtra और उसका window style macOS 13 से उपलब्ध हैं, इसलिए इस implementation के लिए Observation ऊँचा न्यूनतम संस्करण तय करता है। Managing model data in your app, MenuBarExtra, window।
Menu bar panel .menuBarExtraStyle(.window) का इस्तेमाल करता है, क्योंकि इसमें text fields और custom layout हैं। Apple इस style को popover-जैसी window बताता है, जिसके controls सामान्य window के controls की तरह styled होते हैं। यह editor के WindowGroup से अलग scene है। window।
उदाहरण अपने model और UI types को स्पष्ट रूप से @MainActor तक सीमित भी रखता है। Apple main actor को ऐसा actor बताता है जिसका executor main dispatch queue के बराबर है। यहाँ सभी बदलाव UI controls से आते हैं; background work की ज़रूरत नहीं है। MainActor।
साझा model और स्वतंत्र window drafts बनाएँ
बनाए गए app entry point को नीचे दिए गए संपूर्ण single-file उदाहरण से बदलें। Target में केवल एक @main declaration रखें। नमूने में imports, model, views और शुरुआती टेक्स्ट शामिल हैं। इसे documented APIs के आधार पर जाँचा गया, लेकिन इस environment में compile नहीं किया जा सका क्योंकि Swift toolchain उपलब्ध नहीं था।
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)
}
}
Environment में दो स्पष्ट injections हैं: एक window content पर और दूसरा menu bar content पर। दोनों को store मिलता है। यह न मानें कि window के content पर लगाया गया environment modifier किसी sibling scene को dependency देगा। Apple view hierarchy के ज़रिए environment sharing का दस्तावेज़ देता है; ये दोनों injections हर hierarchy की dependency को स्पष्ट बनाते हैं। Managing model data in your app।
जहाँ control को binding चाहिए, वहाँ @Bindable इस्तेमाल करें
SharedLabelControls, @Environment(LabelStore.self) के ज़रिए model पढ़ता है। इसके body के अंदर local @Bindable variable, TextField के लिए ज़रूरी projected binding उपलब्ध कराता है। यह मौजूदा observable object का संदर्भ देता है और कोई दूसरा store नहीं बनाता। Apple environment से binding तक के इसी पैटर्न का दस्तावेज़ देता है। Bindable।
इसके बजाय draft field $draft का इस्तेमाल करता है, क्योंकि उसका मान पहले से window view के @State के स्वामित्व में है। Button मौजूदा draft को साझा लेबल में कॉपी करता है। यह एक स्पष्ट कार्रवाई है: साझा लेबल में बाद के बदलाव किसी भी window के draft को दोबारा नहीं लिखते। ये नमूने के चुने हुए editing semantics हैं।
Window group identifier का हूबहू मिलान करें
Menu bar button अपने environment से openWindow प्राप्त करता है और उसे \"label-editor\" के साथ call करता है, जो group declaration से मेल खाता है। Apple group से window बनाने के लिए इस identifier-आधारित तरीके का दस्तावेज़ देता है। Button का उद्देश्य एक और editor खोलना है, इसलिए यह उदाहरण किसी खास मौजूदा window को खोजकर फिर से इस्तेमाल करने का वादा नहीं करता। WindowGroup।
दो-window क्रम से दायरा जाँचें
नीचे दिया क्रम नमूने के अपेक्षित व्यवहार की जाँच है। अतिरिक्त instrumentation के बिना यह shared model identity और स्वतंत्र draft storage के बीच फर्क दिखाता है।
- App लॉन्च करें। पहला editor साझा लेबल
Sample Alphaऔर draftSample Draftदिखाता है। - Menu bar panel खोलें और
Open Another Editorचुनें। - पहली window का draft बदलकर
Draft Oneकरें। दूसरी window का draft अपरिवर्तित रहने दें। - Menu bar panel में साझा लेबल बदलकर
Sample Betaकरें। दोनों editor windows में वही साझा लेबल दिखना चाहिए, जबकि उनके अलग-अलग draft मान बने रहने चाहिए। - पहली window में
Use Draft as Shared Labelचुनें। दोनों windows और menu bar panel में साझा लेबलDraft Oneहो जाता है। दूसरी window का draftSample Draftही रहता है।
यह क्रम तीन अलग रास्तों की जाँच करता है: स्थानीय टाइपिंग, menu bar से बदलाव और स्थानीय मान को स्पष्ट रूप से साझा मान में कॉपी करना। अगर स्थानीय टाइपिंग से दूसरी window का draft बदलता है, तो जाँचें कि कहीं draft को app-owned store में तो नहीं रखा गया। अगर menu bar के बदलाव windows तक नहीं पहुँचते, तो जाँचें कि menu bar hierarchy में कोई दूसरा LabelStore() तो नहीं बनाया गया।
| लक्षण | सबसे पहले क्या जाँचें |
|---|---|
| Window और menu bar के लेबल अलग हो जाते हैं | एक साझा instance के बजाय अलग-अलग model initialization |
| सभी drafts एक साथ बदलते हैं | Draft storage को shared model में रखा गया है |
| Model पढ़ते समय view विफल हो जाता है | उस hierarchy में .environment(store) मौजूद नहीं है |
| Editor अचानक reset हो जाता है | Owning view की identity में बदलाव या उसका model बदलना |
| दूसरी editor window नहीं खुलती | घोषित और अनुरोधित scene identifiers में अंतर |
Missing-environment मामले का परिणाम दस्तावेज़ों में बताया गया है: type के आधार पर nonoptional observable object पढ़ने का अर्थ है कि किसी ancestor ने उसे दिया है; Apple कहता है कि object न मिलने पर exception होती है। Previews और वैकल्पिक entry points में भी dependency दें। Managing model data in your app।
नमूना सब कुछ memory में रखता है और इसमें save या restore implementation नहीं है। इसलिए window बंद करने पर उसका अधूरा draft मिट जाता है और app बंद करने पर साझा लेबल मिट जाता है। अगर drafts को इन कार्रवाइयों के बाद भी बनाए रखना है, तो इस उदाहरण को बढ़ाने से पहले persistence की अलग आवश्यकता तय करें।
Focused commands को साझा menu bar controls से अलग रखें
साझा लेबल के लिए active-window selection की ज़रूरत नहीं होती: हर उपभोक्ता को वही model मिलता है। वर्तमान में focused window के draft को संपादित करने वाली कार्रवाई की आवश्यकता अलग है, क्योंकि उसे किसी विशेष window की पहचान करनी होती है।
मानक application menu commands के लिए Apple, window-owned model को focusedSceneValue के साथ publish करने और Commands implementation में @FocusedValue के ज़रिए इस्तेमाल करने का दस्तावेज़ देता है। उसके उदाहरण में focused model मौजूद न होने पर command को disable किया जाता है। Active scene के साथ उसका target बदलने वाली कार्रवाइयों के लिए यह एक समर्थित तरीका है। Building and customizing the menu bar with SwiftUI।
उस command उदाहरण से यह न मानें कि MenuBarExtra खोलने पर उसे पहले active रहे editor का भरोसेमंद reference मिल जाता है। इस लेख का panel जानबूझकर app-व्यापी state पर काम करता है। अगर panel को किसी एक editor को target करना है, तो किस window को कार्रवाई मिलनी चाहिए यह मान लेने के बजाय app-owned editor identifiers की सूची जैसी स्पष्ट target-selection व्यवस्था जोड़ें।
AppKit या TextKit तभी जोड़ें जब editor को उनकी ज़रूरत हो
लेबल editor को केवल SwiftUI controls चाहिए। अगर उसकी आवश्यकताओं में custom AppKit text view शामिल हो, तो SwiftUI hierarchy में NSView बनाने, अपडेट करने और हटाने के लिए NSViewRepresentable Apple का समर्थित bridge है। Apple यह भी बताता है कि SwiftUI में delegate और target-action messages भेजने के लिए coordinator का इस्तेमाल कैसे करें। NSViewRepresentable।
यह bridge जोड़ते समय ownership योजना बनाए रखें: हर editor का view instance और editing state उसकी window से जुड़ा रहे, और साझा content को app model के ज़रिए तभी पास करें जब साझा करना अपेक्षित हो। Coordinator को edits किसी binding या model action के ज़रिए आगे भेजने चाहिए। Implementation में सावधानी के तौर पर, text view की सामग्री बदलने से पहले incoming content की तुलना करें, ताकि असंबंधित shared updates चल रहे edit को बार-बार reset न करें।
TextKit 2 implementation के लिए Apple textLayoutManager को modern layout-engine access point बताता है। उसके TextKit session में बताया गया है कि TextKit 2 text view पर legacy layoutManager तक पहुँचने से compatibility mode चालू हो सकता है, जिससे वह view TextKit 1 पर चला जाता है और अपने आप वापस नहीं बदलता। केवल text view की जाँच करने के लिए उस legacy property का इस्तेमाल न करें। TextKit, What’s new in TextKit and text views।
अगर macOS 13 का समर्थन ज़रूरी है, तो ownership की यही व्यवस्था रखें, लेकिन पुराना observation तरीका इस्तेमाल करें: ObservableObject के साथ @Published, स्वामित्व के लिए @StateObject, और .environmentObject के साथ प्राप्त करने वाली @EnvironmentObject properties। Apple की migration guide बताती है कि ये तरीके नए Observation approach के अनुरूप कैसे हैं। Observation systems बदलते समय sharing की इच्छित सीमाएँ बनाए रखें। Migrating from the Observable Object protocol to the Observable macro।
Ownership तय करते समय interface से अपेक्षित व्यवहार अपनाएँ: साझा लेबल के लिए app के स्वामित्व वाला एक instance, हर अधूरे draft के लिए window के स्वामित्व वाला एक मान, और इन दोनों को जोड़ने वाली स्पष्ट कार्रवाई। जब भी menu bar panel या editor में विस्तार करें, दो-window क्रम को regression check के रूप में रखें।
स्रोत
- Managing model data in your app — Apple
- WindowGroup — Apple
- MenuBarExtra — Apple
- window — Apple
- MainActor — Apple
- Bindable — Apple
- Building and customizing the menu bar with SwiftUI — Apple
- NSViewRepresentable — Apple
- TextKit — Apple
- What’s new in TextKit and text views — Apple
- Migrating from the Observable Object protocol to the Observable macro — Apple