W przypadku wielokrotnego użytku panelu SwiftUI w aplikacji na Maca umieść główny builder zawartości jako pierwszy, nadaj pozostałym builderom etykiety i użyj ograniczonych inicjalizatorów dla pominiętych obszarów. Dzięki temu wywołania są czytelne, a nieobecne nagłówki i stopki mają jawne typy.

W tym samouczku utworzymy mały komponent z obszarami zawartości, nagłówka i stopki. Celem jest obsługa czterech przydatnych wywołań — samej zawartości, zawartości z dowolnym z elementów dodatkowych oraz zawartości z obydwoma — bez polegania na konkurujących domyślnych domknięciach. Wszystkie wyświetlane wartości to syntetyczny tekst lokalny.

Co sprawia, że inicjalizator z wieloma builderami jest jednoznaczny?

Składnia Swifta z wieloma końcowymi domknięciami pomija etykietę argumentu pierwszego końcowego domknięcia i wymaga etykiet dla kolejnych. Dlatego komponent powinien jasno wskazywać przeznaczenie pierwszego buildera swoją nazwą, a późniejszym builderom nadawać różne etykiety. Składnię tę wprowadzono w Swift 5.3. SE-0279: Multiple Trailing Closures

W przypadku tego panelu pełny inicjalizator to init(content:header:footer:). W miejscu wywołania pierwszy blok przekazuje zawartość, a header: i footer: identyfikują pozostałe bloki. Kolejność deklaracji określa kolejność argumentów, natomiast body komponentu określa kolejność wizualną. Zawartość może więc być pierwszym argumentem, mimo że nagłówek wyświetla się nad nią.

Obecny interfejs Apple Section stosuje ten układ z zawartością na pierwszym miejscu. Jego pełny inicjalizator przyjmuje trzy domknięcia builderów, a osobne inicjalizatory obsługują kombinacje z pominiętymi obszarami. To przydatny wzorzec projektowania interfejsu; przedstawiony niżej własny panel ma odrębny układ i zachowanie. Apple: Section, Apple: init(content:header:footer:)

Atrybut buildera i etykieta parametru rozwiązują różne problemy. Atrybut result buildera przekształca treść przekazanego domknięcia, a etykiety argumentów określają, jak wywołujący identyfikują parametry. Dodanie @ViewBuilder nie przywraca pierwszej etykiety, gdy składnia końcowego domknięcia ją pomija. SE-0289: Result builders

To rozróżnienie prowadzi do praktycznej zasady projektowania: unikaj sąsiadujących inicjalizatorów, których jedyne przydatne rozróżnienie znika, gdy pominięta zostaje etykieta pierwszego końcowego domknięcia. Na przykład dwa poza tym równoważne inicjalizatory o nazwach init(content:header:) i init(body:header:) udostępniałyby ten sam pierwszy nienazwany blok, po którym następowałoby header:. To konsekwencja reguł dotyczących końcowych domknięć, a nie powód, by dodawać więcej adnotacji builderów.

Które granice kompilatora i SwiftUI mają znaczenie?

Weź pod uwagę trzy kwestie: kompilator, wybrany tryb języka Swift i SDK SwiftUI. Wiele końcowych domknięć pojawiło się w Swift 5.3, a formalną funkcję result builderów zaimplementowano w Swift 5.4. Te kamienie milowe opisują obsługę języka; nie określają dostępności każdego inicjalizatora frameworka. SE-0279, SE-0289

Obecna dokumentacja Apple wyznacza też granicę zależną od toolchaina: Xcode 26 i wcześniejsze wersje korzystają z dotychczasowego sposobu budowania widoków, a Xcode 27 i nowsze tworzą na podstawie tych builderów zawartość niezależną od typu. W przypadku nowszego zachowania Apple zaleca programistom zapis @ContentBuilder. ContentBuilder jest aliasem typu dla ViewBuilder; ograniczenia otaczającego API nadal określają, jaka zawartość jest dopuszczalna. Apple: ViewBuilder, Apple: ContentBuilder

Poniższy kompletny przykład używa zapisu @ViewBuilder, który obsługuje bazę Xcode 26, i jest przeznaczony dla macOS 11 lub nowszego. Przyjmując udokumentowany w Xcode 27 styl interfejsu, zastąp adnotacje parametrów inicjalizatora przez @ContentBuilder. Zachowaj trzy ograniczenia generyczne View: ten komponent umieszcza wszystkie trzy obszary w układzie widoku.

Nie używaj w kontrakcie komponentu nazw wygenerowanych typów krotek buildera. Apple dokumentuje niezgodności źródłowe związane z jawnymi ograniczeniami TupleView przy przejściu na nową implementację buildera. Generyczne typy obszarów pozwalają temu panelowi uniknąć tej zależności. Apple: TN3211, Resolving SwiftUI source incompatibilities for State and ContentBuilder

Zbuduj panel z czterema jawnymi wariantami inicjalizatora

Użyj niezależnego typu generycznego dla każdego obszaru. Ponowne użycie tego samego parametru generycznego wymagałoby, aby obszary miały ten sam konkretny typ — to niepotrzebne ograniczenie, gdy jeden zawiera pojedynczy widok tekstu, a inny kilka widoków.

W przypadku nieobecnego obszaru wyspecjalizuj jego parametr generyczny jako EmptyView. Apple opisuje dokładnie taką rolę EmptyView: reprezentowanie nieobecnego elementu potomnego w generycznym typie widoku. Poniższe ograniczone inicjalizatory jawnie dokonują tego wyboru, zamiast wymagać od wywołującego wywnioskowania nieokreślonego typu elementu dodatkowego. Apple: EmptyView

Dwa bloki kodu Swift w tej sekcji składają się na jeden kompletny plik aplikacji. W docelowym projekcie aplikacji SwiftUI na macOS zastąp wygenerowane deklaracje aplikacji i widoku zawartości tymi blokami, zachowując dokładnie jedną deklarację @main. Pierwszy blok definiuje komponent i wszystkie cztery warianty inicjalizatora.

import SwiftUI

struct BuilderPanel<Content: View, Header: View, Footer: View>: View {
    private let content: Content
    private let header: Header
    private let footer: Footer

    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder header: () -> Header,
        @ViewBuilder footer: () -> Footer
    ) {
        self.content = content()
        self.header = header()
        self.footer = footer()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            header
            content
            footer
        }
        .padding()
    }
}

extension BuilderPanel where Footer == EmptyView {
    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder header: () -> Header
    ) {
        self.init(
            content: content,
            header: header,
            footer: { EmptyView() }
        )
    }
}

extension BuilderPanel where Header == EmptyView {
    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder footer: () -> Footer
    ) {
        self.init(
            content: content,
            header: { EmptyView() },
            footer: footer
        )
    }
}

extension BuilderPanel where Header == EmptyView, Footer == EmptyView {
    init(@ViewBuilder content: () -> Content) {
        self.init(
            content: content,
            header: { EmptyView() },
            footer: { EmptyView() }
        )
    }
}

Pełny inicjalizator wywołuje każdy builder raz podczas inicjalizacji wartości danego panelu i zapisuje zwrócone wartości. Inicjalizatory pomocnicze delegują do niego. To zachowanie czasowe wynika z tej implementacji: nie oznacza, że builder zostanie uruchomiony tylko raz w całym cyklu życia interfejsu, ponieważ utworzenie kolejnej wartości panelu ponownie wywołuje jego inicjalizator.

Dołącz drugi blok do tego samego pliku. Sprawdza on wszystkie obsługiwane warianty, w tym wiele instrukcji wewnątrz jednego buildera.

struct PanelDemo: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                BuilderPanel {
                    Text("Canvas A")
                }

                BuilderPanel {
                    Text("Canvas B")
                    Text("Grid: 12 columns")
                } header: {
                    Text("Layout")
                }

                BuilderPanel {
                    Text("Canvas C")
                } footer: {
                    Text("Units: points")
                }

                BuilderPanel {
                    Text("Canvas D")
                } header: {
                    Text("Preview")
                } footer: {
                    Text("Scale: 100 percent")
                }
            }
            .padding()
        }
        .frame(minWidth: 360, minHeight: 440)
    }
}

@main
struct BuilderPanelDemoApp: App {
    var body: some Scene {
        WindowGroup {
            PanelDemo()
        }
    }
}

Zbuduj i uruchom aplikację w Xcode, aby sprawdzić kontrakt z wybranym SDK. Listing został przejrzany, ale nie został tutaj skompilowany ani uruchomiony; środowisko badawcze nie miało ani kompilatora Swift, ani SDK SwiftUI dla macOS.

Dopasuj każde wywołanie do konkretnego kontraktu typów

Przydatnym narzędziem jest tabela wariantów wywołań. Określa, który inicjalizator powinien zostać wybrany i jaki konkretny typ przypada nieobecnym obszarom. To oczekiwania wynikające z deklaracji w przykładzie, gotowe do sprawdzenia w projekcie.

Przekazane obszary Etykiety inicjalizatora Ograniczenia nieobecnych obszarów
Tylko zawartość content: Header == EmptyView, Footer == EmptyView
Zawartość i nagłówek content:header: Footer == EmptyView
Zawartość i stopka content:footer: Header == EmptyView
Wszystkie trzy content:header:footer: Brak

Każdy krótszy inicjalizator wymaga wszystkich zadeklarowanych w nim domknięć. Żaden argument domyślny nie sprawia, że trzybuilderowy inicjalizator konkuruje przy wywołaniu z samą zawartością. Warianty z samym nagłówkiem i samą stopką rozróżniają drugie etykiety, nawet jeśli oba domknięcia zwracają Text.

Korzystaj z tej tabeli przy zmianach komponentu: jeśli nowy inicjalizator akceptuje istniejący wariant wywołania, sprawdź, czy rozróżnienie pozostaje widoczne po pominięciu pierwszej etykiety końcowego domknięcia. Dzięki temu wykryjesz nakładanie się interfejsów, zanim doprowadzi do trudnego do zdiagnozowania błędu w większym widoku.

Dlaczego unikać kilku domyślnych domknięć builderów?

Argumenty domyślne są przydatne, ale dopasowywanie wielu parametrów domyślnych typu closure zależy od trybu języka Swift. SE-0286 opisuje dopasowywanie od przodu, mechanizm zgodności pozwalający pomijać parametry domyślne, oraz stosowane w starszych trybach zastępcze dopasowywanie od tyłu. Opisuje też flagę nadchodzącej funkcji ForwardTrailingClosures, zaimplementowaną w Swift 5.8. SE-0286: Forward-scan matching for trailing closures

Praktyczne zalecenie jest tu celowo węższe niż te reguły: niewielki panel wielokrotnego użytku może uniknąć polegania na nich. Główny builder powinien być wymagany, a obsługę pominięć należy wyrazić przez ograniczone inicjalizatory. Dzięki temu obsługiwane kombinacje są widoczne w deklaracjach i nie trzeba uzależniać zamierzonego znaczenia komponentu od zachowania dopasowywania zależnego od wersji kompilatora.

Pominięty element dodatkowy i puste domknięcie elementu dodatkowego to też różne wybory interfejsu. Inicjalizator przyjmujący samą zawartość jawnie wybiera EmptyView dla obu elementów dodatkowych. Przekazanie pustego buildera pozostawia określenie wyniku domknięcia implementacji buildera SDK. W przypadku tego komponentu pomijaj nieużywane obszary i pozwól, aby inicjalizator pomocniczy zapewnił udokumentowany typ nieobecności.

To podejście ma swoje ograniczenia skali. Trzy obszary dają cztery przydatne kombinacje, gdy zawartość jest zawsze wymagana. Komponent z wieloma niezależnie opcjonalnymi obszarami wymagałby znacznie większego zestawu przeciążeń. Zanim je dodasz, ustal, których kombinacji czytelnicy rzeczywiście potrzebują; mniejszy zestaw sensownych wariantów komponentu może być łatwiejszy w użyciu i utrzymaniu.

Jak diagnozować niejednoznaczne wywołanie?

Zacznij od najprostszego wywołania panelu, które odtwarza problem. Następnie wykonaj poniższe kroki; pozwalają oddzielić dopasowywanie argumentów od błędów wewnątrz buildera.

  1. Umieść wszystkie argumenty domknięć w nawiasach. Zapisz jawnie content:, header: i footer: w kolejności deklaracji. Jeśli to rozwiąże problem, sprawdź, które rozróżnienie usunęła składnia końcowych domknięć.
  2. Tymczasowo zastąp każdy obszar jednym wyrażeniem Text. Jeśli wywołanie wtedy działa, przywracaj oryginalne wyrażenia po jednym, aby znaleźć problematyczne ciało buildera.
  3. Porównaj warianty inicjalizatorów. Poszukaj konkurujących argumentów domyślnych, odwróconej kolejności domknięć oraz przeciążeń rozróżnianych wyłącznie etykietą pierwszego buildera.
  4. Sprawdź ograniczenia nieobecnych obszarów. Każdy pominięty obszar generyczny powinien otrzymać zamierzony konkretny typ w wybranym inicjalizatorze.
  5. Sprawdź granicę toolchaina. Po przejściu na Xcode 27 sprawdź jawne ograniczenia wyniku buildera oraz kolidujące nazwy typów, zanim przeprojektujesz zestaw inicjalizatorów.

Zapisanie etykiet w nawiasach zachowuje nazwy argumentów pomijane przez pierwsze końcowe domknięcie. Swift obsługuje obie formy, więc wariant z pełnymi etykietami jest przydatny przy diagnozowaniu i może być odpowiednim zapisem końcowym, jeśli dzięki niemu konkretne wywołanie staje się czytelniejsze. The Swift Programming Language: Closures

W przypadku diagnostyki nowszych SDK Apple wyraźnie zaleca kwalifikowanie typów kolidujących między modułami oraz aktualizację ograniczeń zależnych od starszych typów wynikowych krotek. Takie problemy mogą występować wewnątrz poprawnie zaprojektowanego wywołania inicjalizatora; dodanie kolejnych przeciążeń nie rozwiązałoby ich źródła. Apple: TN3211

Przejrzysty kontrakt inicjalizatora zaczyna się od wywołań, które komponent musi obsługiwać. Umieść podstawową zawartość jako pierwszą, nadaj builderom elementów dodatkowych znaczące etykiety i przypisz jawne typy nieobecnym obszarom. Następnie zbuduj każdy obsługiwany wariant wywołania przy użyciu wybranego kompilatora, trybu języka i SDK, zanim rozszerzysz interfejs.

Źródła