Wijs een NSTextFieldDelegate toe, implementeer controlTextDidEndEditing(_:), en kopieer de stringValue van het veld naar een SwiftUI-binding. Een coordinator koppelt die callback aan de weergavestatus, zodat een label kan worden bijgewerkt zodra het bewerken is voltooid.
Deze tutorial bouwt een kleine Mac-app met twee bewerkbare labels en een samenvatting die hun vastgelegde waarden toont. De taak is om de samenvatting ongewijzigd te laten terwijl iemand typt, en deze vervolgens bij te werken wanneer AppKit de bewerksessie beëindigt. Apple documenteert dat de callback voor het beëindigen van het bewerken plaatsvindt nadat het besturingselement de wijzigingen heeft vastgelegd. Apple-documentatie voor controlTextDidEndEditing(_:)
Welke gebeurtenis moet de weergave bijwerken?
Gebruik bij een bestaande NSTextField de delegatecallback voor het bewerken van het besturingselement. De delegate ontvangt een melding waarvan object het besturingselement is dat de melding heeft verzonden. Zo kan de handler het veld identificeren en de waarde ervan uitlezen. Apple-documentatie voor textDidEndEditingNotification
Kies de gebeurtenis op basis van het gewenste gedrag van de interface:
| Gewenst gedrag | Geschikte aanpak |
|---|---|
| Een vastgelegde waarde bijwerken wanneer een AppKit-tekstveld klaar is met bewerken | Implementeer controlTextDidEndEditing(_:) in de delegate. |
| Een bestaande AppKit-target-action-handler aanroepen wanneer het bewerken is voltooid | Configureer de eigenschap sendsActionOnEndEditing van de cel. |
Een native SwiftUI-TextField-invoer afhandelen |
Koppel .onSubmit voor het gedocumenteerde invoergedrag. |
Deze aanpakken hebben verschillende contracten. Apple documenteert dat het inschakelen van sendsActionOnEndEditing de actie verstuurt na Return, nadat je met Tab het veld verlaat of nadat je in een ander tekstveld klikt; als je dit uitschakelt, wordt de actie alleen bij Return verstuurd. SwiftUI documenteert dat .onSubmit voor een TextField reageert op Return. Het biedt dus geen algemene callback voor focusverlies bij een ingesloten AppKit-veld. Apple-documentatie voor sendsActionOnEndEditing, Apple-documentatie voor onSubmit(of:_:)
In deze app is de delegate de enige plek waar de tekst van een veld wordt vastgelegd. Als je later ook een target-action-handler toevoegt, voer dan niet in beide handlers hetzelfde neveneffect voor het vastleggen uit.
Bouw een volledig voorbeeld met SwiftUI en AppKit
Maak in Xcode een macOS-appproject met Swift en de SwiftUI-interface. Gebruik voor dit voorbeeld een recente Swift 6-compatibele toolchain en stel het deploymentdoel in op macOS 13 of later. Vervang het gegenereerde app-entrybestand door het volledige programma hieronder en verwijder de gegenereerde ContentView.swift als daarin nog een ContentView wordt gedeclareerd. Behoud precies één @main-entrypoint. Apple beschrijft deze entrypointstructuur in de documentatie voor App. Apple-documentatie voor App
Het macOS 13-doel van dit voorbeeld is een projectkeuze en niet de introductieversie van de bridge: Apple vermeldt dat NSViewRepresentable beschikbaar is vanaf macOS 10.15. Door het gebruik van AppKit is deze implementatie specifiek voor een macOS-doel. Apple-documentatie voor NSViewRepresentable
import SwiftUI
import AppKit
@main
@MainActor
struct EditingDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@MainActor
struct ContentView: View {
@State private var cardTitle = "Blue Card"
@State private var shelfLabel = "Shelf A"
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Card title")
CommitTextField(value: $cardTitle, label: "Card title")
.frame(height: 24)
Text("Shelf label")
CommitTextField(value: $shelfLabel, label: "Shelf label")
.frame(height: 24)
Divider()
Text("Committed values")
.font(.headline)
Text("Card: \(cardTitle)")
Text("Shelf: \(shelfLabel)")
}
.padding(20)
.frame(minWidth: 420)
}
}
@MainActor
struct CommitTextField: NSViewRepresentable {
@Binding var value: String
let label: String
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
func makeNSView(context: Context) -> NSTextField {
let field = NSTextField(string: value)
field.placeholderString = label
field.delegate = context.coordinator
return field
}
func updateNSView(_ field: NSTextField, context: Context) {
context.coordinator.parent = self
field.placeholderString = label
field.isEnabled = context.environment.isEnabled
// Leave the active editing buffer alone.
if field.currentEditor() == nil, field.stringValue != value {
field.stringValue = value
}
}
static func dismantleNSView(
_ field: NSTextField,
coordinator: Coordinator
) {
field.delegate = nil
}
@MainActor
final class Coordinator: NSObject, NSTextFieldDelegate {
var parent: CommitTextField
init(parent: CommitTextField) {
self.parent = parent
super.init()
}
func controlTextDidEndEditing(_ notification: Notification) {
guard let field = notification.object as? NSTextField else {
return
}
let committedValue = field.stringValue
if parent.value != committedValue {
parent.value = committedValue
}
}
}
}
Dit is een volledig appvoorbeeld met synthetische lokale waarden. Het is gecontroleerd aan de hand van de opgehaalde API-documentatie, maar hier niet gecompileerd of uitgevoerd: de beschikbare uitvoeringsomgeving is Linux en heeft geen Swift-toolchain of macOS SDK. Bouw en test het in Xcode op een Mac.
Hoe de waarde de samenvatting bereikt
Elke @State-waarde van de bovenliggende weergave levert een binding aan de wrapper van het veld. De coordinator schrijft via die binding wanneer het bewerken eindigt. SwiftUI werkt vervolgens de samenvatting bij omdat die de gewijzigde status gebruikt. Apple documenteert zowel het bijwerken van de weergave op basis van de status als het gebruik van bindings waarmee onderliggende weergaven de status van de bovenliggende weergave kunnen wijzigen. Apple-documentatie voor State
makeNSView maakt het bewerkbare AppKit-besturingselement aan en wijst de coordinator toe als delegate. updateNSView verwerkt wijzigingen die vanuit SwiftUI naar dat besturingselement stromen. De delegatecallback verwerkt de omgekeerde richting. In de integratierichtlijnen van Apple wordt dezelfde combinatie van coordinator en binding gebruikt om AppKit-weergaven in te sluiten. Apples sessie ‘Use SwiftUI with AppKit’
De toewijzing context.coordinator.parent = self houdt de opgeslagen representable van de coordinator actueel. Een coordinator blijft bestaan gedurende de levensduur van de ingesloten weergave, terwijl de waarden van de representable kunnen veranderen. Apple laat expliciet zien hoe je die opgeslagen waarde tijdens updateNSView ververst. Uitleg van Apple over de levenscyclus van coordinators
Behoud het concept tijdens SwiftUI-updates
De essentiële bewaking is field.currentEditor() == nil. Dit voorbeeld houdt niet-afgemaakte tekst bewust binnen AppKit, terwijl SwiftUI de laatst vastgelegde waarde bewaart. Zonder een bewerkingscontrole kan het kopiëren van de vastgelegde waarde naar het veld tijdens een SwiftUI-update het onafgemaakte concept vervangen.
Apple documenteert dat currentEditor() de field editor van het besturingselement retourneert, of nil als er geen is. De field editor is een NSTextView die door besturingselementen in het venster wordt gedeeld. Daarom is alleen controleren of firstResponder van het venster gelijk is aan de NSTextField een slechte vervanging voor het besturingselement vragen naar zijn editor. Apple-documentatie voor currentEditor()
Het resulterende gedrag is een expliciet ontwerpbeleid:
| Situatie | Gedrag in het voorbeeld |
|---|---|
| Een veld is inactief en de binding verandert | Kopieer de binding naar het veld bij de volgende representable-update. |
| Een veld heeft een actieve editor | Behoud de bewerkingsbuffer. |
| Het bewerken eindigt met andere tekst | Schrijf de voltooide tekst naar de binding. |
| Het bewerken eindigt met ongewijzigde tekst | Sla de overbodige statustoewijzing over. |
Dit beleid geeft het lokale concept voorrang tijdens een bewerksessie. Als andere appcode dezelfde binding wijzigt terwijl iemand aan het bewerken is, kan een latere vastlegging die wijziging vervangen. De demonstratie heeft geen concurrerende schrijver. Bepaal in een grotere editor hoe gelijktijdige wijzigingen moeten worden afgehandeld, bijvoorbeeld door de huidige sessie te beëindigen voordat het geselecteerde item wordt gewijzigd, of door aparte concept- en vastgelegde waarden te bewaren en een expliciete keuze aan te bieden. Verwijder onafgemaakte tekst niet alleen om een hertekening te synchroniseren.
Voor deze taak zijn geen aanpassingen aan de TextKit-indeling nodig. De relevante grens is de bewerksessie van het besturingselement en zijn field editor. Apples handleiding over de tekstarchitectuur legt de relatie uit tussen NSTextField, de delegate en de gedeelde NSTextView-field editor; die handleiding is gearchiveerd, terwijl de actuele referentie voor currentEditor() het model met de gedeelde editor bevestigt. Apples handleiding ‘Text Editing’, Apples actuele referentie voor field editors
Controleer in de app of het bewerken wordt voltooid
Gebruik na het bouwen de volgende stappen om het voorbeeld te controleren. Dit zijn verwachte controles op basis van de implementatie en het gedocumenteerde bewerkingsgedrag, geen gerapporteerde uitvoeringsresultaten.
- Vervang
Blue CarddoorGreen Card. Tijdens het typen moet de samenvatting nog steedsCard: Blue Cardtonen. - Druk op Return. De samenvatting moet veranderen in
Card: Green Card. - Bewerk de kaarttitel opnieuw en klik vervolgens op het veld
Shelf label. De voltooide kaarttitel moet in de samenvatting verschijnen. - Bewerk de plankwaarde en druk vervolgens op Tab. Controleer of de plankwaarde in de samenvatting wordt bijgewerkt en controleer de toetsenbordnavigatie.
- Leg een lege waarde vast. De bijbehorende samenvatting moet het voorvoegsel zonder waarde tonen; het voorbeeld accepteert lege tekenreeksen.
- Beëindig een bewerking zonder de tekst te veranderen. De gelijkheidscontrole moet een nieuwe statustoewijzing voorkomen.
Return kan het bewerken voltooien zonder de focus te verplaatsen zoals je verwacht. Apple documenteert dat NSTextField na de melding dat het bewerken is beëindigd de actie en andere verwerking van Return probeert uit te voeren en mogelijk de tekst selecteert. Tab en Shift-Tab proberen naar de volgende of vorige key view te navigeren. Beschouw voltooiing en de bestemming van de focus als afzonderlijke gedragingen bij het controleren van de interface. Apple-documentatie voor textDidEndEditing(_:)
Een callback die niet binnenkomt opsporen
Controleer eerst of field.delegate nog naar de coordinator verwijst. De eigenschap is zwak, dus een tijdelijk delegate-object zonder aparte eigenaar blijft niet beschikbaar. De coordinator van de representable biedt hier de vereiste levensduur. Apple-documentatie voor delegate, Uitleg van Apple over de levenscyclus van coordinators
Controleer vervolgens of het bewerken daadwerkelijk is beëindigd. Een klik op een willekeurige achtergrond is hiervoor geen betrouwbare test. Gebruik in deze demonstratie Return of een ander bewerkbaar veld en controleer de specifieke responderovergang als het resultaat afwijkt.
Als je app validatie toevoegt, controleer dan control(_:textShouldEndEditing:). De gedocumenteerde retourwaarde bepaalt of het bewerken mag eindigen. Een afgewezen poging om het veld te verlaten, mag niet worden behandeld als een geslaagde vastlegging. Apple-documentatie voor control(_:textShouldEndEditing:)
Controleer ook annuleringspaden. Apple stelt expliciet dat het aanroepen van abortEditing() om wijzigingen in behandeling te verwerpen niet leidt tot een aanroep van controlTextDidEndEditing(_:). Een annuleringsbewerking heeft daarom een eigen, bewust gekozen statusgedrag nodig. Apple-documentatie voor de callback bij beëindiging van het bewerken
Vraag voltooiing aan via een expliciet appcommando
Voor een toekomstige Done-opdracht die een verwijzing naar dit veld heeft, is de ondersteunde responderroute om het eigen venster te vragen makeFirstResponder(nil) aan te roepen. Controleer de geretourneerde Boolean voordat je verdergaat: Apple documenteert dat de huidige responder kan weigeren de status als eerste responder op te geven, waardoor de methode false retourneert. Gebruik het daadwerkelijke venster van het veld en ga er niet van uit dat een toepassingsbreed key window bij deze editor hoort. Apple-documentatie voor makeFirstResponder(_:)
Voer de AppKit-toegang en UI-callbacks van deze bridge uit op de main actor. De actuele delegate-declaratie is geïsoleerd op de main actor, en Swift documenteert dat MainActor een executor gebruikt die gelijkwaardig is aan de main dispatch-queue. Het voorbeeld geeft die isolatie rechtstreeks aan met @MainActor; er wordt geen uitgestelde taak toegevoegd tussen het voltooien en het schrijven naar de binding. Apples callbackdeclaratie, Swift-documentatie voor MainActor
De implementatie heeft één duidelijke vastleggingsgrens: AppKit beëindigt het bewerken, de coordinator leest het veld uit en de binding werkt de afhankelijke SwiftUI-tekst bij. Door het concept tijdens representable-updates te beschermen, is deze grens praktisch voor alledaags bewerken van labels zonder aangepaste tekstindelingsmechanismen.
Bronnen
- controlTextDidEndEditing(_:) — Apple Developer-documentatie
- textDidEndEditingNotification — Apple Developer-documentatie
- sendsActionOnEndEditing — Apple Developer-documentatie
- onSubmit(of:_:) — Apple Developer-documentatie
- App — Apple Developer-documentatie
- NSViewRepresentable — Apple Developer-documentatie
- State — Apple Developer-documentatie
- Use SwiftUI with AppKit — Apple Developer
- currentEditor() — Apple Developer
- Text Editing — Apple Documentatiearchief
- textDidEndEditing(_:) — Apple Developer-documentatie
- delegate — Apple Developer-documentatie
- control(_:textShouldEndEditing:) — Apple Developer-documentatie
- makeFirstResponder(_:) — Apple Developer-documentatie
- MainActor — Apple Developer-documentatie