Om een generieke SwiftUI-component te construeren, vereis je de initializer ervan in een protocol of injecteer je een closure die de component retourneert. Deze tutorial gebruikt beide benaderingen om hetzelfde synthetische item in een macOS-venster weer te geven, met expliciete grenzen voor compiler en SDK.
Wat moet een generieke component beloven?
Stel dat een macOS-app een kleine itemkaart weergeeft. Je wilt een herbruikbaar paneel dat de kaart op basis van een item construeert, terwijl aanroepers de presentatie kunnen kiezen. De centrale vraag is wie verantwoordelijk is voor de constructie: de gedeclareerde interface van de component of de aanroeper die de component aanlevert.
Een generieke beperking geeft de implementatie toegang tot de mogelijkheden die in die beperking zijn vastgelegd. Met Component: View leg je vast dat de component een SwiftUI-view is; daarmee leg je niet vast dat de component een initializer met de naam init(item:) heeft. Het hoofdstuk Generics van Swift legt protocolbeperkingen uit, terwijl Apples documentatie over View de verplichte eigenschap body van een view beschrijft.
Daarom is dit fragment opzettelijk ongeldig:
// Invalid fragment: View does not require init(item:).
@MainActor
func construct<Component: View>(item: Item) -> Component {
Component(item: item)
}
Als je die initializer aan één concrete component toevoegt, wordt hij daarmee niet beschikbaar via elke generieke parameter die alleen tot View is beperkt. De herbruikbare API heeft een expliciet constructiecontract nodig. Gebruik deze beslismatrix voordat je de component wijzigt:
| Constructievereiste | Geschikte interface | Reden |
|---|---|---|
| Elke deelnemende component accepteert hetzelfde item | Protocol dat init(item:) vereist |
Constructie hoort bij de interface van de componentfamilie. |
| Aanroepers kiezen aanvullende configuratie of modifiers | Factory-closure die de generieke component retourneert | Constructie blijft op de aanroepplek. |
| De aanroeper heeft al een voltooide component | De generieke componentwaarde opslaan | De constructie heeft al plaatsgevonden. |
| De presentatie wisselt tussen een eindig aantal lay-outs | De alternatieven samenstellen in een SwiftUI-view | De omringende API kan een concrete view-structuur behouden. |
Deze tabel is een ontwerphulpmiddel dat is afgeleid van de gedocumenteerde taalregels. Hieronder worden de eerste twee opties samen geïmplementeerd, zodat je hun gedrag rechtstreeks kunt vergelijken.
Een compleet macOS-voorbeeld bouwen
Gebruik een Xcode-appdoel voor macOS met SwiftUI en een minimale macOS-versie van 11 of hoger. Het voorbeeld is geschreven voor een Swift 6-compiler met de bijbehorende Apple SDK. De expliciete annotaties met @MainActor vereisen Swift 5.5 of hoger; de code is dus niet bedoeld voor oudere compilers. Apple documenteert WindowGroup als beschikbaar vanaf macOS 11 en als container voor de view-hiërarchie van een app. Zie WindowGroup.
Vervang het gegenereerde bestand met het app-entrypoint door het volgende volledige programma. Zorg dat het doel slechts één @main-appdeclaratie bevat. Het gegenereerde bestand ContentView kan ongebruikt blijven. Er zijn geen packages, externe assets of gedownloade voorbeeldgegevens nodig.
import SwiftUI
struct Item {
let title: String
let detail: String
}
@MainActor
protocol ItemComponent: View {
init(item: Item)
}
@MainActor
struct ItemCard: ItemComponent {
let item: Item
init(item: Item) {
self.item = item
}
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(item.title)
.font(.headline)
Text(item.detail)
}
.padding()
.background(Color.gray.opacity(0.12))
.cornerRadius(8)
}
}
@MainActor
struct ContractPanel<Component: ItemComponent>: View {
let item: Item
init(item: Item, component: Component.Type) {
self.item = item
}
var body: some View {
Component(item: item)
}
}
@MainActor
struct FactoryPanel<Component: View>: View {
private let makeComponent: @MainActor () -> Component
init(make: @escaping @MainActor () -> Component) {
self.makeComponent = make
}
var body: some View {
makeComponent()
}
}
@MainActor
struct DemoView: View {
private let item = Item(
title: "Sample tile",
detail: "Blue circle on a neutral background"
)
var body: some View {
VStack(alignment: .leading, spacing: 18) {
Text("Initializer contract")
ContractPanel(item: item, component: ItemCard.self)
Text("Injected factory")
FactoryPanel {
ItemCard(item: item)
.padding(.leading, 12)
}
Text("Alternative factory output")
FactoryPanel {
Text(item.title + ": " + item.detail)
}
}
.padding(24)
.frame(width: 420)
}
}
@main
@MainActor
struct ComponentDemoApp: App {
var body: some Scene {
WindowGroup {
DemoView()
}
}
}
Voer het macOS-appdoel uit. Het beoogde resultaat is één venster met twee kaartpresentaties en een presentatie met gewone tekst. De tweede kaart heeft extra opvulling aan de voorzijde die door de aanroeper wordt aangeleverd. Dit zijn synthetische voorbeelden van API-gedrag, geen gemeten resultaten. In de onderzoeksomgeving is geen Swift-compiler geïnstalleerd, dus de code is nagekeken maar hier niet gecompileerd of uitgevoerd.
De annotaties met @MainActor maken de grens voor UI-constructie expliciet. De huidige View-declaratie van Apple is geannoteerd met @MainActor en @preconcurrency; de documentatie legt ook uit hoe isolatie wordt overgenomen wanneer conformance in de oorspronkelijke declaratie van een type staat. Zie View.
Hoe werkt het initializercontract?
ItemComponent verfijnt View en voegt precies één constructievereiste toe: init(item:). Swift ondersteunt initializervereisten rechtstreeks in protocollen. Een conformerend type levert de implementatie; het protocol specificeert de signatuur zonder een initializerbody. Zie Protocollen: initializervereisten.
Dankzij die vereiste is Component(item: item) geldig binnen ContractPanel. Het argument component: Component.Type levert een concreet type voor generieke type-inferentie: door ItemCard.self mee te geven, wordt ItemCard het componenttype van dit paneel. Het paneel hoeft de metatype niet op te slaan, omdat body construeert via de generieke typeparameter.
Dit contract is nuttig wanneer elke component op basis van dezelfde invoer moet kunnen worden geconstrueerd. Een app-eigen familie van compacte en uitgebreide itempresentaties kan bijvoorbeeld dezelfde vereiste delen. Een aanroeper kan dan het geselecteerde componenttype wijzigen zonder de constructie-expressie van het paneel te herschrijven.
Het nadeel blijkt wanneer een implementatie extra configuratie nodig heeft. Door init(item:) te vereisen, moet elke conformerende component dat constructiepad aanbieden, zelfs als de voorkeursinitializer ook een bijschrift- of lay-outinstelling accepteert. Als die extra informatie van de aanroeper moet komen, drukt de factorybenadering hieronder de vereiste meestal directer uit.
Voor implementaties op basis van klassen moet een initializer die aan een protocolvereiste voldoet required zijn, tenzij de klasse final is. Een subklasse die ook een overeenkomende designated initializer van de superklasse overschrijft, heeft mogelijk zowel required als override nodig. Structs zoals ItemCard gebruiken geen required. Deze verschillen worden beschreven in Protocollen.
Wanneer verplaats je de constructie naar een factory?
FactoryPanel accepteert een functie van het type @MainActor () -> Component. De enige belofte is dat een aanroep van de functie het geselecteerde componenttype oplevert. De closure kan een initializer aanroepen, configuratie vastleggen of een view met toegepaste modifiers retourneren.
Het programma laat het praktische verschil zien: de factory kan een ItemCard met opvulling retourneren of Text, zonder dat een van beide resulterende types ItemComponent hoeft te adopteren. Apple legt uit dat view-modifiers hun invoer in een andere view kunnen verpakken; daarom is het hier nuttig om de compiler het resulterende type van de factory te laten afleiden. Zie View.
De initializer markeert de closure met @escaping, omdat het paneel die opslaat om later aan te roepen. Het hoofdstuk Closures van Swift beschrijft escaping closures en hoe ze waarden uit de omringende context kunnen vastleggen. In dit voorbeeld legt de closure het synthetische item vast dat door de omringende view wordt gebruikt.
Kies de invoer van de factory zorgvuldig. Een factory zonder argumenten betekent dat de aanroeper alles vastlegt wat nodig is voor de constructie. Als het paneel zelf een item selecteert, kan een factory met een parameter die verantwoordelijkheid duidelijker uitdrukken. Het volgende is een vervangende componentdefinitie die je samen met de bestaande Item kunt gebruiken; het is geen tweede complete app:
@MainActor
struct ItemFactoryPanel<Component: View>: View {
let item: Item
private let makeComponent: @MainActor (Item) -> Component
init(
item: Item,
make: @escaping @MainActor (Item) -> Component
) {
self.item = item
self.makeComponent = make
}
var body: some View {
makeComponent(item)
}
}
Met die definitie levert het paneel het item en levert de aanroeper de presentatie. Beperk het werk van de factory tot het produceren van de view-waarde: het constructiecontract van het voorbeeld belooft geen eenmalige uitvoering, blijvende opslag of caching.
Hebben factories een result builder nodig?
De factories in het complete programma zijn gewone closures. Elke closure retourneert één expressie, dus een aangepaste builderannotatie is niet nodig. Als er meerdere child-views nodig zijn, volstaat het voor deze API om één VStack te retourneren; de stack bevat de child-views.
Een result builder is nuttig wanneer de factoryparameter zelf een declaratieve reeks expressies of voorwaardelijke inhoud moet accepteren. Swift Evolution vermeldt dat de gestandaardiseerde result-builderfunctie is geïmplementeerd in Swift 5.4. Zie SE-0289: Result builders.
Houd bij het toepassen van builder-syntaxis rekening met de SDK-grens. De huidige documentatie van Apple beschrijft @ViewBuilder voor parameters die views opleveren in Xcode 26 en ouder. Voor Xcode 27 en hoger beschrijft de documentatie type-agnostische constructie en beveelt ze de alias ContentBuilder aan om dat gedrag aan te geven. De gewone closure die hier wordt gebruikt, vereist die nieuwere alias niet. Zie ViewBuilder en ContentBuilder.
Waarom vervangen opaque of existentiële typen het contract niet?
some View verbergt een specifiek onderliggend resultaattype, maar behoudt de identiteit ervan. Dat werkt goed voor de eigenschappen body in dit programma. Het voegt geen init(item:) toe aan de View-interface. Een verpakte waarde die als any View is geschreven, verbergt het onderliggende type op een andere manier en behoudt niet dezelfde statische type-identiteit. Zie Opaque en verpakte protocoltypen.
Maak hiervan geen algemene bewering dat existentiële waarden nooit aan generieke functies kunnen worden doorgegeven. SE-0352 introduceerde impliciet geopende existentials in Swift 5.7, waardoor geschikte aanroepen een generieke parameter kunnen koppelen aan het onderliggende type van een existential. De specificatie beschrijft ook beperkingen, waaronder resultaten die het geopende type in een generieke wrapper opnemen, wanneer het resultaat na type-uitwissing niet kan worden weergegeven. Zie Impliciet geopende existentials.
Voor deze paneeltaak volstaat het om het concrete componenttype via een generieke parameter te behouden. Er is geen reden om opslag met existentials te introduceren alleen zodat aanroepers kunnen kiezen hoe ze een view construeren.
Hoe spoor je een constructiefout op?
Volg de expressie die faalt en controleer welk contract op die plek beschikbaar is:
- Als
Component(item:)faalt, controleer dan of de generieke beperking een protocol bevat dat precies die initializer-signatuur vereist. - Als de compiler
Componentniet kan afleiden, geef dan een concrete metatype op, zoalsItemCard.self, of een factory met een concrete returnexpressie. - Als een component configuratie nodig heeft die specifiek is voor de aanroeper, verplaats de constructie dan naar de factory in plaats van niet-gerelateerde vereisten aan het protocol toe te voegen.
- Als het opslaan van een closure een diagnose over een escaping closure oplevert, controleer dan de annotatie
@escapingvan de initializer. - Als constructie een diagnose over actorisolatie oplevert, controleer dan of zowel de aanroepplek als het functietype van de factory de beoogde grens voor de main actor aangeven.
- Als een existential een fout over het generieke resultaat veroorzaakt, bekijk dan de beperkingen voor resultaattypen in SE-0352 voordat je aanneemt dat een compilerupgrade de beperking opheft.
Vereis bij een gedeelde macOS-componentfamilie de initializer wanneer uniforme constructie deel uitmaakt van de interface. Gebruik een geïnjecteerde factory wanneer de aanroeper de presentatie moet samenstellen en configureren. Beide opties maken de ontbrekende constructiebelofte expliciet en behouden het concrete componenttype dat het paneel nodig heeft.
Bronnen
- Protocollen — The Swift Programming Language, Swift.org.
- Generics — The Swift Programming Language, Swift.org.
- Closures — The Swift Programming Language, Swift.org.
- Opaque en verpakte protocoltypen — The Swift Programming Language, Swift.org.
- View — Apple Developer Documentation.
- WindowGroup — Apple Developer Documentation.
- ViewBuilder — Apple Developer Documentation.
- ContentBuilder — Apple Developer Documentation.
- Result builders — Swift Evolution.
- Impliciet geopende existentials — Swift Evolution.