Deklarera anroparlevererat innehåll som (Input) -> Content, där Content begränsas till View. Då kan en macOS-komponent tillhandahålla ett typat värde medan anroparna väljer hur det presenteras. Kontraktet kräver att värdet tas emot; det kan inte kräva att det används.
Den här handledningen bygger en liten förhandsvisning av färgrutor för datorn. Den återanvändbara komponenten hanterar en storlekskontroll och tillhandahåller en syntetisk konfiguration till anroparens renderingsstängning. Anroparen ritar färgrutan utan att hantera komponentens interna tillstånd.
Vilket gränssnitt uttrycker kravet?
Använd en namngiven generisk parameter för den vy som returneras och en konkret typ för komponentens indata. I exemplet nedan är den centrala signaturen @ViewBuilder content: @escaping (SwatchInput) -> Content, i en deklaration med begränsningen Content: View.
Indata och utdata har separata uppgifter. SwatchInput definierar värdena som komponenten tillhandahåller. Content representerar den konkreta vytypen som varje anropares stängning producerar. Namngivna generiska parametrar låter en deklaration uttrycka och återanvända dessa typrelationer, vilket beskrivs i Swifts kapitel om Generics.
Apple använder det här gränssnittsmönstret i GeometryReader: dess initializer accepterar en stängning av typen (GeometryProxy) -> Content. Containern tillhandahåller proxyn och anroparen bygger innehåll utifrån den. Det är ett användbart föredöme för en anpassad komponent som tillhandahåller sin egen konfiguration. Se Apples dokumentation om GeometryReader och init(content:).
Välj kontrakt utifrån den faktiska uppgiften:
| Krav | Lämpligt gränssnitt |
|---|---|
| Anroparen tillhandahåller innehåll utan indata från komponenten | () -> Content |
| Komponenten tillhandahåller en specifik konfiguration | (SwatchInput) -> Content |
| Containern tar emot ett värde vars typ varierar mellan användningar | En namngiven generisk parameter Input med en matchande stängning av typen (Input) -> Content |
| Kompilatorn måste bevisa att anroparen använder konfigurationen | En signatur för stängningen uttrycker inte det kravet |
Valen följer av Swifts funktionstyper: deras parametertyper och returtyp beskriver vad som kan skickas in och returneras. De beskriver inte hur implementationen måste använda argumenten. Se avsnittet Function Type i Swifts språkreferens.
Bygg en komplett förhandsvisning av färgrutor för macOS
Använd ett macOS-appmål med SwiftUI. Exemplet använder den etablerade stavningen @ViewBuilder och är avsett för Swift 5.7 eller senare, med macOS 12 eller senare som distributionsmål. Det undviker förhandsvisningsmakron och nyare namn för innehållsbyggare. Swift och Apples SDK är inte tillgängliga i forskningsmiljön, så programmet granskades men kompilerades eller kördes inte här.
Ersätt den genererade appdeklarationen med följande kompletta program i ett Xcode-projekt för en SwiftUI-app. Ta bort den genererade deklarationen av ContentView om den skulle duplicera den här. Behåll exakt en appingångspunkt markerad med @main. Apples Xcode-hjälp beskriver hur du börjar med ett SwiftUI-projekt eller lägger till en SwiftUI-fil i ett befintligt projekt.
import SwiftUI
struct SwatchInput {
let title: String
let side: CGFloat
let color: Color
}
@MainActor
struct SwatchPanel<Content: View>: View {
@State private var isLarge = false
private let content: (SwatchInput) -> Content
init(
@ViewBuilder content: @escaping (SwatchInput) -> Content
) {
self.content = content
}
private var input: SwatchInput {
SwatchInput(
title: "Sample A",
side: isLarge ? 120 : 64,
color: .blue
)
}
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Toggle("Large sample", isOn: $isLarge)
content(input)
}
.padding(24)
}
}
@MainActor
struct ContentView: View {
var body: some View {
SwatchPanel { input in
Text(input.title)
.font(.headline)
RoundedRectangle(cornerRadius: 12)
.fill(input.color)
.frame(width: input.side, height: input.side)
if input.side == 120 {
Text("Large preview")
.font(.caption)
}
}
.frame(width: 300, height: 280, alignment: .topLeading)
}
}
@main
@MainActor
struct TypedContentDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Den förväntade första visningen innehåller reglaget Large sample, titeln Sample A och en blå rundad kvadrat med sidlängden 64 punkter. När reglaget aktiveras ändras den tillhandahållna sidlängden till 120 punkter och Large preview visas. Storlekarna och etiketterna är illustrativa lokala värden, inte uppmätta resultat.
Komponenten äger isLarge. Dess beräknade input paketerar det aktuella valet, och content(input) skickar paketet till anroparen. SwiftUI hanterar lagringen av tillstånd och uppdaterar de delar av vyhierarkin som beror på ett ändrat värde, enligt dokumentationen under State.
Anroparen äger presentationen: en titel, en form och en valfri bildtext. Den kan ersätta arrangemanget med ett annat och behålla samma typade indata. Både panelen och dess anropare implementerar body för att sätta samman sina gränssnitt, i enlighet med Apples dokumenterade View-modell.
Varför behövs builder- och escaping-annoteringarna?
@ViewBuilder låter anroparen skriva flera vyuttryck och stödda villkorsgrenar i en och samma stängning. I det här exemplet tillhandahåller anroparen två ovillkorliga vyer och en villkorlig bildtext. Buildern kombinerar innehållet till ett resultat som uppfyller komponentens returtypsbegränsning. Apple dokumenterar flera underordnade vyer och villkorsstyrd konstruktion under ViewBuilder.
Buildern tillhandahåller inte SwatchInput. Den vanliga stängningsparametern deklarerar indatan, och content(input) tillhandahåller det faktiska argumentet. Om initializern accepterade () -> Content skulle en builder-annotering inte göra om den till en stängning som accepterar konfiguration.
@escaping behövs eftersom initializern lagrar stängningen i en egenskap för att anropa den senare från body. Swifts kapitel om Closures förklarar att en stängning lever vidare utanför funktionen när den kan anropas efter att den mottagande funktionen har returnerat.
För en komponent vars konfiguration redan är slutgiltig vid initiering är en annan utformning att anropa en icke-escaping builder direkt och lagra det resulterande Content-värdet. Det ändrar när konfigurationen tillämpas. Här gör lagringen av stängningen det avsedda flödet tydligt: härled aktuell indata från komponentens tillstånd och bygg sedan innehåll med den indatan.
Behåll Xcodes builder-gräns
Apples aktuella dokumentation skiljer mellan Xcode 26 och tidigare versioner samt Xcode 27 och senare. För de äldre verktygen beskriver den ViewBuilder som en builder för vyer. Med Xcode 27 och senare beskriver den typoberoende konstruktion och rekommenderar aliaset ContentBuilder för att ange det bredare beteendet. Se ViewBuilder.
När du underhåller en kodbas för Xcode 27 eller senare kan initializern i stället använda @ContentBuilder. Apple dokumenterar det som ett typsalias för ViewBuilder, där konformitet säkerställs genom de innehållstyper som det producerar. Behåll Content: View i den här komponenten: utdata är fortfarande avsedda för en vyhierarki. Utgå inte från att den nyare stavningen finns i äldre SDK:er bara för att den aktuella dokumentationen anger äldre distributionsmål. Se ContentBuilder.
Varför löser inte some View alla delar av det här kontraktet?
some View passar för exempelns egenskaper body. Det döljer den konkreta resultattypen och bevarar samtidigt löftet att resultatet är en vy. Den lagrade stängningen använder i stället den namngivna parametern Content så att komponenten kan hänvisa till samma resultattyp i sin egenskap och initializer.
Swift 5.7 introducerade opaka parameterdeklarationer genom SE-0341. En direkt parameter som _ value: some View introducerar en namnlös generisk parameter vars konkreta typ bestäms av anroparen. Det betyder inte att funktionsimplementationen kan välja en godtycklig vy och skicka den dit den namnlösa typen förväntas. Se Opaque Parameter Declarations.
I synnerhet ligger en callback-parameter skriven som (some View) -> Content utanför de positioner för opaka parametrar som stöds enligt SE-0341. Förslaget förbjuder uttryckligen opaka typer i indatapositionen i en callbacks funktionstyp. Diskussionen om att eventuellt möjliggöra den syntaxen finns under framtida riktningar, inte som implementerat beteende. Den aktuella referensen för Types behåller den begränsningen.
För den här datoruppgiften är den stödda lösningen enkel: gör värdet som komponenten tillhandahåller till en konkret konfigurationstyp. Om en annan återanvändbar container måste bevara en indatatyp som anroparen tillhandahåller, namnger du typen med en generisk parameter och använder den konsekvent genom det lagrade värdet och callbacken. Ingendera utformningen kräver en opak callback-indata.
Vad garanterar kompilatorn egentligen?
Signaturen garanterar en anropsbar form: ett argument av typen SwatchInput och ett resultat som uppfyller Content: View. Den ger också stängningsparametern dess härledda typ, så att anroparen kan komma åt input.title, input.side och input.color utan att upprepa en typannotering. Swift dokumenterar denna härledning från stängningens kontext i Closures.
Den kan inte garantera att egenskaperna påverkar visningen. Det här är till exempel ett giltigt ersättningsuttryck i ContentView.body; det tar avsiktligt emot konfigurationen men ignorerar den:
SwatchPanel { _ in
Text("Fixed preview")
}
Den gränsen spelar roll när du formulerar komponentens krav. ”Anroparinnehållet accepterar SwatchInput” är ett typkontrakt. ”Anroparinnehållet visar den tillhandahållna storleken” är ett beteendekrav. Om det är avgörande för komponenten att storleken visas, lägger du den renderingen i komponenten och erbjuder ett mindre anpassningsområde.
Använd följande kontroller för att diagnostisera integrationsproblem. De är granskningssteg som härletts från gränssnittet, inte rapporterade resultat från kompilatortester:
| Symptom | Kontroll |
|---|---|
Argument saknas vid anrop av content |
Skicka konfigurationen: content(input). |
| Diagnostik om escaping-stängning i initializern | Behåll @escaping när callbacken lagras. |
| Flera syskonvyer orsakar fel i anroparen | Behåll lämplig builder-annotering på initializerparametern. |
| En explicit typad callback avvisas | Kontrollera att indatan är SwatchInput, inte en orelaterad typ som String. |
| Förhandsvisningen ignorerar storleksreglaget | Kontrollera om renderingen läser input.side. |
| Komponenten fortsätter att visa den ursprungliga konfigurationen | Kontrollera om innehållet konstruerades en gång och lagrades i stället för att callbacken anropades med aktuell indata. |
Håll kontraktet så litet som uppgiften tillåter. En konkret konfiguration och ett generiskt vyresultat ger panelen med färgrutor en tydlig gräns: komponenten tillhandahåller aktuella värden, anroparen tar emot dem med fullständig typinformation och presentationen är enkel att ersätta.
Källor
- Generics — Swift
- Types — Swift
- Closures — Swift
- Opaque Parameter Declarations — Swift Evolution
- GeometryReader — Apple
- init(content:) — Apple
- ViewBuilder — Apple
- ContentBuilder — Apple
- View — Apple
- State — Apple
- Show a preview of your user interface — Apple