Aby utworzyć generyczny komponent SwiftUI, wymagaj jego inicjalizatora w protokole albo wstrzyknij zwracające go domknięcie. W tym samouczku wykorzystano oba podejścia do wyświetlenia tego samego przykładowego elementu w oknie macOS, z wyraźnie określonymi granicami kompilatora i SDK.

Co musi obiecywać generyczny komponent?

Załóżmy, że aplikacja macOS wyświetla niewielką kartę elementu. Chcesz, aby wielokrotnego użytku panel tworzył kartę na podstawie elementu, jednocześnie pozwalając wywołującym wybrać sposób prezentacji. Kluczowe pytanie brzmi: kto odpowiada za utworzenie komponentu — jego zadeklarowany interfejs czy wywołujący, który go dostarcza?

Ograniczenie generyczne udostępnia implementacji możliwości określone przez to ograniczenie. Zapis Component: View określa, że komponent jest widokiem SwiftUI; nie określa, że komponent ma inicjalizator o nazwie init(item:). Rozdział Generics w dokumentacji Swift wyjaśnia ograniczenia protokołów, a dokumentacja Apple dotycząca View opisuje wymagane właściwość widoku body.

W związku z tym poniższy fragment jest celowo niepoprawny:

// Invalid fragment: View does not require init(item:).
@MainActor
func construct<Component: View>(item: Item) -> Component {
    Component(item: item)
}

Dodanie tego inicjalizatora do jednego konkretnego komponentu nie udostępnia go dla każdego parametru generycznego ograniczonego wyłącznie do View. Wielokrotnego użytku API potrzebuje jawnego kontraktu tworzenia. Przed zmianą komponentu skorzystaj z poniższej tabeli decyzyjnej:

Wymaganie dotyczące tworzenia Odpowiedni interfejs Uzasadnienie
Każdy uczestniczący komponent przyjmuje ten sam element Protokół wymagający init(item:) Tworzenie należy do interfejsu rodziny komponentów.
Wywołujący wybierają dodatkową konfigurację lub modyfikatory Domknięcie fabrykujące zwracające generyczny komponent Tworzenie pozostaje po stronie miejsca wywołania.
Wywołujący ma już gotowy komponent Przechowaj wartość generycznego komponentu Komponent został już utworzony.
Prezentacja przełącza się między skończonym zestawem układów Złóż warianty wewnątrz widoku SwiftUI Otaczające API może zachować konkretną strukturę widoku.

Ta tabela jest pomocą w projektowaniu, opartą na udokumentowanych regułach języka. Poniżej przedstawiono razem dwie pierwsze opcje, aby można było bezpośrednio porównać ich działanie.

Zbuduj kompletny przykład dla macOS

Użyj celu aplikacji macOS w Xcode z SwiftUI i minimalną wersją systemu macOS 11 lub nowszą. Przykład napisano dla kompilatora Swift 6 i odpowiadającego mu SDK Apple. Jawne adnotacje @MainActor wymagają Swift 5.5 lub nowszego, dlatego kod nie jest przeznaczony dla starszych kompilatorów. Dokumentacja Apple opisuje WindowGroup jako dostępny od macOS 11 kontener dla hierarchii widoków aplikacji. Zobacz WindowGroup.

Zastąp wygenerowany plik punktu wejścia aplikacji poniższym kompletnym programem. Upewnij się, że cel zawiera tylko jedną deklarację aplikacji @main. Wygenerowany plik ContentView może pozostać nieużywany. Nie są potrzebne żadne pakiety, zewnętrzne zasoby ani pobrane dane przykładowe.

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()
        }
    }
}

Uruchom cel aplikacji macOS. Oczekiwany rezultat to jedno okno zawierające dwie prezentacje w formie kart i prezentację w postaci zwykłego tekstu. Druga karta ma dodatkowe wcięcie z lewej strony, dostarczone przez wywołującego. Są to syntetyczne przykłady działania API, a nie zmierzone wyniki. W środowisku badawczym nie ma kompilatora Swift, więc kod został sprawdzony, ale nie został tu skompilowany ani uruchomiony.

Adnotacje aktora głównego jawnie określają granicę tworzenia interfejsu. Bieżąca deklaracja View w dokumentacji Apple ma adnotacje @MainActor i @preconcurrency; dokumentacja wyjaśnia też, jak dziedziczona jest izolacja, gdy zgodność zostaje zadeklarowana w oryginalnej deklaracji typu. Zobacz View.

Jak działa kontrakt inicjalizatora?

ItemComponent rozszerza View i dodaje dokładnie jedno wymaganie dotyczące tworzenia: init(item:). Swift bezpośrednio obsługuje wymagania inicjalizatorów w protokołach. Typ zgodny z protokołem dostarcza implementację; protokół określa sygnaturę bez treści inicjalizatora. Zobacz Protocols: Initializer Requirements.

To wymaganie sprawia, że zapis Component(item: item) jest poprawny wewnątrz ContractPanel. Argument component: Component.Type dostarcza konkretny typ na potrzeby wnioskowania generycznego: przekazanie ItemCard.self określa, że typem komponentu tego panelu jest ItemCard. Panel nie musi przechowywać metatypu, ponieważ jego body tworzy instancję za pomocą parametru generycznego typu.

Ten kontrakt przydaje się, gdy każdy komponent powinien dać się utworzyć z tych samych danych wejściowych. Na przykład należąca do aplikacji rodzina kompaktowych i rozszerzonych prezentacji elementów może współdzielić to wymaganie. Wywołujący może wtedy zmienić wybrany typ komponentu bez przepisywania wyrażenia tworzącego panel.

Kompromis staje się widoczny, gdy implementacja wymaga dodatkowej konfiguracji. Wymaganie init(item:) oznacza, że każdy zgodny komponent musi udostępniać tę ścieżkę tworzenia, nawet jeśli preferowany inicjalizator przyjmuje również podpis lub ustawienie układu. Jeśli te dodatkowe dane należą do wywołującego, podejście z fabryką opisane poniżej zwykle wyraża to wymaganie bezpośredniej.

W implementacjach opartych na klasach inicjalizator spełniający wymaganie protokołu musi być oznaczony jako required, chyba że klasa jest final. Podklasa, która również nadpisuje pasujący inicjalizator wyznaczony nadklasy, może wymagać zarówno required, jak i override. Struktury takie jak ItemCard nie używają required. Te rozróżnienia opisano w Protocols.

Kiedy przenieść tworzenie do fabryki?

FactoryPanel przyjmuje funkcję typu @MainActor () -> Component. Jej obietnica jest prosta: wywołanie funkcji tworzy wybrany typ komponentu. Domknięcie może wywołać inicjalizator, przechwycić konfigurację albo zwrócić widok z zastosowanymi modyfikatorami.

Program pokazuje praktyczną różnicę: fabryka może zwrócić ItemCard z wcięciem albo Text, nie wymagając, aby którykolwiek z tych typów przyjmował protokół ItemComponent. Apple wyjaśnia, że modyfikatory widoków mogą opakowywać dane wejściowe w inny widok, dlatego przydatne jest tutaj pozwolenie kompilatorowi na wywnioskowanie typu zwracanego przez fabrykę. Zobacz View.

Inicjalizator oznacza domknięcie jako @escaping, ponieważ panel przechowuje je do późniejszego wywołania. Rozdział Closures dokumentacji Swift opisuje domknięcia uciekające i ich zdolność do przechwytywania otaczających wartości. W tym przykładzie domknięcie przechwytuje syntetyczny element używany przez otaczający widok.

Dobieraj argumenty fabryki świadomie. Fabryka bez argumentów oznacza, że wywołujący przechwytuje wszystko, co jest potrzebne do utworzenia komponentu. Jeśli to panel wybiera element, fabryka z parametrem może wyraźniej określić ten zakres odpowiedzialności. Poniższa definicja zastępuje definicję komponentu i powinna być używana razem z istniejącym typem Item; nie jest to druga kompletna aplikacja:

@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)
    }
}

Przy tej definicji panel dostarcza element, a wywołujący — sposób prezentacji. Ogranicz pracę fabryki do utworzenia wartości widoku: kontrakt tworzenia w przykładzie nie gwarantuje jednokrotnego wykonania, trwałego przechowywania ani buforowania.

Czy fabryki potrzebują result buildera?

Fabryki w kompletnym programie są zwykłymi domknięciami. Każde zwraca jedno wyrażenie, więc nie jest potrzebna własna adnotacja buildera. Jeśli wymaganych jest kilka widoków podrzędnych, wystarczy zwrócić pojedynczy VStack; stos zawiera widoki podrzędne.

Result builder jest przydatny, gdy sam parametr fabryki ma przyjmować deklaratywną sekwencję wyrażeń lub zawartość warunkową. Zapis Swift Evolution wskazuje, że ustandaryzowana funkcja result builderów została wdrożona w Swift 5.4. Zobacz SE-0289: Result builders.

Przy korzystaniu ze składni buildera uwzględnij granicę SDK. Bieżąca dokumentacja Apple opisuje @ViewBuilder dla parametrów tworzących widoki w Xcode 26 i starszych wersjach. Dla Xcode 27 i nowszych opisuje konstrukcję niezależną od typu i zaleca alias ContentBuilder, aby wskazać takie działanie. Zobacz ViewBuilder i ContentBuilder. Zwykłe domknięcie użyte w tym przykładzie nie wymaga nowszego aliasu.

Dlaczego typy niejawne ani egzystencjalne nie zastępują kontraktu?

some View ukrywa konkretny bazowy typ wyniku, zachowując jego tożsamość. Dobrze sprawdza się we właściwościach body w tym programie. Nie dodaje jednak init(item:) do interfejsu View. Wartość opakowana jako any View ukrywa typ bazowy w inny sposób i nie zachowuje tej samej statycznej tożsamości typu. Zobacz Opaque and Boxed Protocol Types.

Nie wyciągaj z tego ogólnego wniosku, że wartości egzystencjalnych nigdy nie można przekazywać do funkcji generycznych. SE-0352 wprowadziło niejawnie otwierane typy egzystencjalne w Swift 5.7, dzięki czemu kwalifikujące się wywołania mogą powiązać parametr generyczny z bazowym typem egzystencjalnym. Dokument opisuje też ograniczenia, w tym wyniki osadzające otwarty typ w generycznym wrapperze, gdy wyniku nie da się przedstawić po wymazaniu typu. Zobacz Implicitly Opened Existentials.

W przypadku tego panelu wystarczy zachować konkretny typ komponentu za pomocą parametru generycznego. Nie ma potrzeby wprowadzać przechowywania egzystencjalnego tylko po to, by pozwolić wywołującym wybrać sposób tworzenia widoku.

Jak zdiagnozować błąd tworzenia?

Prześledź wyrażenie, które powoduje błąd, i sprawdź kontrakt dostępny w tym miejscu:

  1. Jeśli zapis Component(item:) powoduje błąd, sprawdź, czy ograniczenie generyczne obejmuje protokół wymagający dokładnie takiej sygnatury inicjalizatora.
  2. Jeśli kompilator nie może wywnioskować Component, podaj konkretny metatyp, taki jak ItemCard.self, albo fabrykę z konkretnym wyrażeniem zwracanym.
  3. Jeśli komponent wymaga konfiguracji zależnej od wywołującego, przenieś tworzenie do fabryki zamiast dodawać do protokołu niezwiązane z nim wymagania.
  4. Jeśli przechowywanie domknięcia powoduje diagnostykę dotyczącą uciekającego domknięcia, sprawdź adnotację @escaping inicjalizatora.
  5. Jeśli tworzenie powoduje diagnostykę izolacji aktora, sprawdź, czy zarówno miejsce wywołania, jak i typ funkcji fabryki określają zamierzoną granicę aktora głównego.
  6. Jeśli typ egzystencjalny powoduje błąd dotyczący wyniku generycznego, przed założeniem, że aktualizacja kompilatora usunie ograniczenie, sprawdź ograniczenia typów wynikowych w SE-0352.

Wspólna rodzina komponentów macOS powinna wymagać inicjalizatora, jeśli jednolity sposób tworzenia stanowi część interfejsu. Użyj wstrzykniętej fabryki, gdy to wywołujący powinien złożyć i skonfigurować prezentację. Obie ścieżki jawnie określają brakującą obietnicę dotyczącą tworzenia, zachowując konkretny typ komponentu wymagany przez panel.

Źródła