För att få en radbrytande NSTextView att passa i en SwiftUI-ram representerar du en NSScrollView, godtar ändliga föreslagna dimensioner via sizeThatFits och låter textdokumentet växa vertikalt inuti den. Konfigurera textcontainern så att den följer redigerarens bredd, så radbryts långa rader.
Den här handledningen bygger en vanlig textredigerare för din egen Mac-app med ett begränsat synligt område. När du lägger till text ska den rullas vertikalt utan att SwiftUI-layouten blir större. Det fullständiga exemplet riktar sig till macOS 13 eller senare, där Apple tillhandahåller callbacken för representerbar storleksanpassning. Apples dokumentation om sizeThatFits
Varför kan en NSTextView verka ignorera en SwiftUI-ram?
Tre storlekar måste samordnas. SwiftUI storleksanpassar den representerade AppKit-vyn. En NSScrollView tillhandahåller det synliga området. Dess dokumentvy innehåller texten, medan en NSTextContainer bestämmer området som är tillgängligt för textlayouten. Apple beskriver dokumentvyn som vyn som rullvyn rullar inuti sin innehållsvy. Apples dokumentation om documentView
En SwiftUI-.frame(width:height:) skapar en omslutande layout-ram. Den konfigurerar inte textcontainerns radbrytning separat. Apple förklarar också att ett underordnat objekts gränser kan sträcka sig utanför en omslutande ram. Därför är en kantlinje användbar vid felsökning: den kan ha de begärda måtten även när det representerade innehållet beter sig annorlunda. Apples dokumentation om ramar
Den centrala begränsningen gäller ägarskap. Apple säger uttryckligen att SwiftUI styr den representerade AppKit-vyns frame och bounds; om du ändrar dessa egenskaper själv hamnar du i konflikt med SwiftUI. Returnera i stället din önskade storlek via callbacken för representerbar storleksanpassning. I den här implementationen är den representerade vyn rullvyn, medan AppKit hanterar dess interna textdokument. Apples dokumentation om NSViewRepresentable
Välj beteende innan du ändrar layoutflaggor:
| Önskat beteende | Policy för yttre storlek | Policy för textdokumentet |
|---|---|---|
| Panel med fast storlek för redigeraren | Godta angiven bredd och höjd | Radbryt vågrätt; väx vertikalt och rulla |
| Redigerare som fyller en begränsad panel | Godta panelens ändliga föreslagna dimensioner | Radbryt vågrätt; väx vertikalt och rulla |
| Redigerare som växer för att visa all text | Beräkna höjden utifrån den föreslagna bredden | Samordna innehållsmätning och layoutuppdateringar |
De två första raderna är uppgiften som täcks av det körbara exemplet. Den tredje kräver en separat policy för innehållshöjd; att godta den föreslagna höjden implementerar inte den policyn.
Bygg en begränsad redigerare med radbrytning
Skapa en macOS SwiftUI-app i Xcode och ange macOS 13 eller senare som distributionsmål. Lägg till följande fil som FramedTextEditor.swift. Det andra kodblocket innehåller appens startpunkt och exempelinnehåll; tillsammans utgör de två filerna det fullständiga exemplet.
Konfigurationen följer Apples dokumenterade upplägg för lodrät rullning: textvyn växer vertikalt, dess bredd följer supervyn och textcontainern följer textvyns bredd. Apples monteringsguide är arkiverad, så exemplet använder aktuella Swift-API:er och väljer uttryckligen TextKit 2 via NSTextView(usingTextLayoutManager: true). Apple beskriver det valet av initierare i sin presentation om TextKit. Apples guide till rullande textvyer, Nyheter i TextKit och textvyer
import AppKit
import SwiftUI
@MainActor
struct FramedTextEditor: NSViewRepresentable {
@Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator($text)
}
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSScrollView()
scrollView.borderType = .noBorder
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
scrollView.autohidesScrollers = true
let textView = NSTextView(usingTextLayoutManager: true)
// This is the internal document, not the represented root view.
textView.setFrameSize(scrollView.contentSize)
textView.minSize = .zero
textView.maxSize = NSSize(
width: CGFloat.greatestFiniteMagnitude,
height: CGFloat.greatestFiniteMagnitude
)
textView.isVerticallyResizable = true
textView.isHorizontallyResizable = false
textView.autoresizingMask = [.width]
textView.isRichText = false
textView.importsGraphics = false
textView.font = .systemFont(ofSize: 14)
textView.textContainerInset = NSSize(width: 8, height: 8)
guard let container = textView.textContainer else {
preconditionFailure("Expected a text container")
}
container.size = NSSize(
width: max(1, scrollView.contentSize.width),
height: CGFloat.greatestFiniteMagnitude
)
container.widthTracksTextView = true
container.heightTracksTextView = false
textView.string = text
textView.delegate = context.coordinator
scrollView.documentView = textView
return scrollView
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
context.coordinator.text = $text
guard let textView = nsView.documentView as? NSTextView else {
preconditionFailure("Expected an NSTextView document")
}
if textView.string != text {
textView.string = text
}
}
func sizeThatFits(
_ proposal: ProposedViewSize,
nsView: NSScrollView,
context: Context
) -> CGSize? {
let ideal = CGSize(width: 320, height: 180)
let proposed = proposal.replacingUnspecifiedDimensions(by: ideal)
return CGSize(
width: proposed.width.isFinite
? max(0, proposed.width) : ideal.width,
height: proposed.height.isFinite
? max(0, proposed.height) : ideal.height
)
}
static func dismantleNSView(
_ nsView: NSScrollView,
coordinator: Coordinator
) {
(nsView.documentView as? NSTextView)?.delegate = nil
}
@MainActor
final class Coordinator: NSObject, NSTextViewDelegate {
var text: Binding<String>
init(_ text: Binding<String>) {
self.text = text
super.init()
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
if text.wrappedValue != textView.string {
text.wrappedValue = textView.string
}
}
}
}
Ersätt den genererade appens startpunkt och innehållsvy med den här andra filen. Ha bara en @main-deklaration i målet. Alla etiketter och all text är syntetiska, och exemplet fungerar helt och hållet i sitt eget appfönster.
import SwiftUI
@main
@MainActor
struct FrameSizingDemoApp: App {
var body: some Scene {
WindowGroup {
EditorDemo()
}
}
}
@MainActor
struct EditorDemo: View {
@State private var compact = false
@State private var text = Array(
repeating: "Sample note: arrange the blue tiles beside the green tiles.",
count: 16
).joined(separator: "\n\n")
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Toggle("Compact editor", isOn: $compact)
FramedTextEditor(text: $text)
.frame(width: compact ? 280 : 420, height: 180)
.border(Color.secondary)
Text("Character count: \(text.count)")
.font(.caption)
}
.padding(20)
.frame(minWidth: 460, minHeight: 280, alignment: .topLeading)
}
}
Filerna har granskats men har inte kompilerats eller körts här: den tillgängliga miljön är Linux och saknar både Swift-körbar fil och macOS-ramverk. Bygg och kontrollera appen i Xcode med verifieringsstegen nedan.
Hur fungerar inställningarna för storlek och radbrytning?
Godta ändliga förslag utan att mäta dokumentet
För den här redigeraren blir ändlig föreslagen bredd och höjd de önskade måtten för visningsområdet. Reservvärdet 320 × 180 är ett illustrativt designval för ospecificerade eller icke-ändliga dimensioner. Det är inte en uppmätt textstorlek. Noll förblir noll, så en avsökning av minsta storlek inte blir en godtyckligt positiv minimistorlek.
SwiftUI kan efterfråga olika storlekar under en och samma layoutpassering. De vanliga förslagen omfattar frågor om minsta storlek, största storlek och idealstorlek. Det här exemplet tillhandahåller avsiktligt ett ändligt reservvärde för ett obegränsat förslag; det är avsett för en begränsad redigerarpanel och inte som en generell storlekspolicy för alla behållare. Om du returnerar nil väljer du i stället SwiftUIs standardalgoritm för storleksanpassning. Apples dokumentation om ProposedViewSize, Apples callback för representerbar storleksanpassning
Callbacken ändrar inte rullvyns ram, ersätter inte text och skriver inte till SwiftUI-tillstånd. Dess uppgift är att besvara storleksfrågan. Därför avgör textlängden inte den yttre redigerarens höjd i den här implementationen.
Låt bredden följa visningsområdet och höjden följa innehållet
De två breddinställningarna fyller olika funktioner. Den interna textvyns autoresizingmask .width gör att dess bredd anpassas när supervyn ändras. Textcontainerns widthTracksTextView = true gör att dess layoutbredd anpassas när textvyn ändras. Båda sambanden spelar roll när redigeraren blir smalare. Apples dokumentation om autoresizingMask, Apples dokumentation om widthTracksTextView
Vertikalt tillåter isVerticallyResizable = true att dokumentvyn ändrar höjd så att den passar texten. Den stora höjden på textcontainern ger utrymme för den vertikala layouten, medan heightTracksTextView = false undviker att knyta layoutgränsen till varje storleksändring av dokumentvyn. Rullvyn förblir det begränsade synliga området. Apples dokumentation om vertikal storleksändring, Apples dokumentation om heightTracksTextView
Räkna med lite utrymme mellan kantlinjen och texten. Exemplet lägger uttryckligen till en textcontainerinmatning på åtta punkter. Texten kan också ha ytterligare indrag från containerns utfyllnad för radfragment, så varken visningsområdets bredd eller textvyns bredd behöver motsvara den exakta bredd som tecknen upptar. Apples dokumentation om textContainerInset
Håll textuppdateringar åtskilda från storleksförhandling
Koordinatorn vidarebefordrar ändringar under redigering via textDidChange, och bindningen kopplar dem till den överordnade vyns tillstånd. updateNSView hanterar ändringar som flödar tillbaka från SwiftUI. Likhetskontrollen gör att redigerarens sträng inte ersätts när samma värde kommer tillbaka efter en vanlig redigering. Apples dokumentation om delegaten för textändringar, Apples dokumentation om Binding
En inkommande sträng som skiljer sig från den befintliga behandlas som en fullständig ersättning. Om appen även ersätter text programmässigt under redigering ska du bestämma hur markering, ångrahistorik och pågående textinmatning ska bevaras innan du bygger vidare på exemplet. Demonstrationen ändrar bara strängen genom redigeraren.
Exemplet markerar sina UI-typer och sin koordinator med @MainActor. Swifts guide om samtidighet beskriver isolering på huvudaktören som mekanismen som skyddar UI-tillstånd och kräver att annoterad kod körs på den aktören. The Swift Programming Language: Concurrency
Verifiera resultatet och hitta kvarvarande layoutproblem
Kör de här kontrollerna i exempelappen. De skiljer mellan storleken på visningsområdet, radbrytning, rullning och tillståndssynkronisering, i stället för att behandla varje symptom som ett fel i SwiftUI-ramen.
- Titta på redigerarens kantlinje. Det begärda ursprungliga redigerarområdet är
420 × 180punkter. - Aktivera
Compact editor. Bredden ska bli280punkter medan höjden förblir180punkter. Meningarna ska radbrytas till fler rader. - Rulla igenom de medföljande styckena. Dokumentet ska förflyttas innanför samma kantlinje.
- Skriv in fler rader. Teckenantalet ska ändras utan att redigerarens begärda höjd ökar.
- Ta bort texten. Visningsområdet ska behålla sina begärda dimensioner även om dokumentet innehåller lite text.
| Symptom | Kontrollera först |
|---|---|
| Kantlinjen har rätt storlek, men långa rader radbryts inte | Kontrollera både dokumentets autoresizingmask för bredd och widthTracksTextView |
| När text läggs till blir den omgivande layouten större | Kontrollera att den representerbara vyn returnerar rullvyn och inte returnerar innehållshöjden från sizeThatFits |
| Det går inte att nå senare rader | Kontrollera lodrät rullning, vertikal storleksändring av dokumentet och textcontainerns höjdgräns |
| Redigeraren förblir smal i en annan föräldralayout | Granska bredden som föräldern föreslår; callbacken godtar ändliga förslag i stället för att hitta på extra utrymme |
| Textens position ändras vid orelaterade tillståndsuppdateringar | Kontrollera om textView.string tilldelas ovillkorligt |
De här kontrollerna är en felsökningssekvens som bygger på sambanden mellan storlekarna ovan, inte rapporterade testresultat från körning.
Vad ändras för innehållsstyrd höjd eller äldre macOS-versioner?
En redigerare som expanderar för att visa varje rad har en annan uppgift. Den behöver en beräkning av innehållshöjden vid den föreslagna bredden och en policy för eventuell maxhöjd. Att bara aktivera vertikal storleksändring av det interna dokumentet förmedlar inte en ny önskad höjd till SwiftUI. På samma sätt kan det inte beräkna en textstyrd höjd att godta den föreslagna höjden, vilket det här exemplet gör.
Bevara den valda textmotorn när du lägger till mätning. Apple förklarar att åtkomst till en NSTextView-instanss layoutManager kan aktivera kompatibilitetsläge för TextKit 1. Den dokumenterade motsvarigheten i TextKit 2 för textcontainerns användningsgränser är NSTextLayoutManager.usageBoundsForTextContainer. Den här begränsade redigeraren behöver ingetdera mät-API:t. Apples vägledning för migrering av TextKit
Versionsgränsen spelar också roll. NSViewRepresentable är tillgängligt från macOS 10.15, men callbacken sizeThatFits kräver macOS 13. För tidigare distributionsmål behåller du upplägget med rullande dokument och använder den äldre representerbarhetens standardbeteende för storleksanpassning, tillsammans med AppKits storleksprioriteringar eller en behållare som är utformad för det målet. Räkna inte med att callbacken styr layouten där; verifiera den implementationen separat. Apples dokumentation om representerbarhet, Apples tillgänglighetskrav för callbacken för storleksanpassning
För en begränsad Mac-redigerare bör ansvarsfördelningen vara tydlig: SwiftUI tilldelar den synliga rektangeln, rullvyn innehåller det växande dokumentet och textcontainern följer den tillgängliga bredden. Det ger dig ett konkret sätt att felsöka ramens storlek utan att göra textmätning till en del av en redigerare med fast höjd.
Källor
- NSViewRepresentable — Apple.
- sizeThatFits(_:nsView:context:) — Apple.
- ProposedViewSize — Apple.
- frame(width:height:alignment:) — Apple.
- documentView — Apple.
- Putting an NSTextView Object in an NSScrollView — Apple Documentation Archive.
- What’s new in TextKit and text views — Apple.
- autoresizingMask — Apple.
- widthTracksTextView — Apple.
- heightTracksTextView — Apple.
- isVerticallyResizable — Apple.
- textContainerInset — Apple.
- textDidChange(_:) — Apple.
- Binding — Apple.
- Concurrency — Swift.org.