Zadeklaruj treść dostarczaną przez wywołującego jako (Input) -> Content, ograniczając Content do View. Dzięki temu komponent macOS może dostarczyć typowaną wartość, a wywołujący może wybrać sposób jej prezentacji. Kontrakt wymaga przyjęcia tego wejścia; nie może wymagać jego użycia.
W tym samouczku zbudujemy mały podgląd próbki koloru na komputer. Komponent wielokrotnego użytku ma kontrolkę rozmiaru i dostarcza syntetyczną konfigurację do przekazanego przez wywołującego domknięcia renderującego. Wywołujący rysuje próbkę, nie zarządzając wewnętrznym stanem komponentu.
Jaki interfejs wyraża to wymaganie?
Użyj nazwanego parametru generycznego dla zwracanego widoku i konkretnego typu dla wejścia komponentu. W poniższym przykładzie podstawowy podpis to @ViewBuilder content: @escaping (SwatchInput) -> Content w deklaracji z ograniczeniem Content: View.
Wejście i wyjście pełnią odrębne funkcje. SwatchInput określa wartości dostarczane przez komponent. Content reprezentuje konkretny typ widoku tworzony przez domknięcie każdego wywołującego. Nazwane parametry generyczne pozwalają deklaracji wyrażać i ponownie wykorzystywać takie relacje między typami, jak opisano w rozdziale Swift poświęconym typom generycznym.
Apple stosuje ten wzorzec interfejsu w GeometryReader: jego inicjalizator przyjmuje domknięcie typu (GeometryProxy) -> Content. Kontener dostarcza proxy, a wywołujący buduje na jego podstawie treść. To dobry przykład dla własnego komponentu, który dostarcza własną konfigurację. Zobacz dokumentację Apple dotyczącą GeometryReader i init(content:).
Wybierz kontrakt odpowiedni do rzeczywistego zadania:
| Wymaganie | Odpowiedni interfejs |
|---|---|
| Wywołujący dostarcza treść bez wejścia od komponentu | () -> Content |
| Komponent dostarcza określoną konfigurację | (SwatchInput) -> Content |
| Kontener otrzymuje wartość, której typ różni się w zależności od użycia | Nazwany parametr generyczny Input z pasującym domknięciem (Input) -> Content |
| Kompilator musi potwierdzić, że wywołujący używa konfiguracji | Podpis domknięcia nie wyraża tego wymagania |
Te wybory wynikają z typów funkcji w Swift: ich typy parametrów i typ zwracany opisują, co można przekazać i zwrócić. Nie określają, w jaki sposób implementacja musi używać argumentów. Zobacz sekcję Function Type w dokumentacji języka Swift.
Zbuduj kompletny podgląd próbki koloru dla macOS
Użyj celu aplikacji macOS z SwiftUI. Przykład stosuje znaną składnię @ViewBuilder i jest przeznaczony dla Swift 5.7 lub nowszego, z docelową wersją macOS 12 lub nowszą. Nie korzysta z makr podglądu ani nowszych nazw builderów treści. Swift i SDK Apple są niedostępne w środowisku badawczym, dlatego program został sprawdzony, ale nie został tu skompilowany ani uruchomiony.
W projekcie aplikacji SwiftUI w Xcode zastąp wygenerowaną deklarację aplikacji poniższym kompletnym programem. Usuń wygenerowaną deklarację ContentView, jeśli powodowałaby duplikat. Pozostaw dokładnie jeden punkt wejścia aplikacji oznaczony @main. Pomoc Xcode firmy Apple, Xcode help, opisuje rozpoczynanie projektu SwiftUI lub dodawanie pliku SwiftUI do istniejącego projektu.
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()
}
}
}
Oczekiwany początkowy widok zawiera przełącznik Large sample, tytuł Sample A i niebieski zaokrąglony kwadrat o boku długości 64 punktów. Włączenie przełącznika zmienia dostarczany bok na 120 punktów i dodaje napis Large preview. Te rozmiary i etykiety to przykładowe wartości lokalne, a nie wyniki pomiarów.
Komponent zarządza isLarge. Jego obliczane input pakuje bieżący wybór, a content(input) przekazuje ten pakiet do wywołującego. SwiftUI zarządza przechowywaniem stanu i aktualizuje te części hierarchii widoków, które zależą od zmienionej wartości, zgodnie z dokumentacją w sekcji State.
Wywołujący zarządza prezentacją: tytułem, kształtem i opcjonalnym podpisem. Może zastąpić ten układ innym, zachowując to samo typowane wejście. Zarówno panel, jak i jego wywołujący implementują body, aby składać swoje interfejsy, zgodnie z opisanym przez Apple modelem View.
Dlaczego potrzebne są adnotacje buildera i escaping?
@ViewBuilder pozwala wywołującemu zapisać kilka wyrażeń widoków i obsługiwanych gałęzi warunkowych w jednym domknięciu. W tym przykładzie wywołujący dostarcza dwa bezwarunkowe widoki i jeden warunkowy podpis. Builder łączy tę treść w wynik spełniający ograniczenie zwracanego typu komponentu. Apple opisuje wiele widoków podrzędnych i konstrukcję warunkową w sekcji ViewBuilder.
Builder nie dostarcza SwatchInput. Zwykły parametr domknięcia deklaruje wejście, a content(input) dostarcza właściwy argument. Jeśli inicjalizator przyjmowałby () -> Content, dodanie adnotacji buildera nie zmieniłoby go w domknięcie przyjmujące konfigurację.
@escaping jest potrzebne, ponieważ inicjalizator zapisuje domknięcie we właściwości, aby później wywołać je z body. Rozdział Swift poświęcony domknięciom wyjaśnia, że domknięcie ucieka, gdy może zostać wywołane po zakończeniu funkcji, która je otrzymała.
W przypadku komponentu, którego konfiguracja jest już ostateczna w chwili inicjalizacji, można zastosować inne rozwiązanie: natychmiast wywołać nieuciekający builder i zapisać otrzymaną wartość Content. Zmienia to moment zastosowania konfiguracji. Tutaj przechowywanie domknięcia jasno określa zamierzony przepływ: wyprowadź bieżące wejście ze stanu komponentu, a następnie zbuduj treść na jego podstawie.
Zachowaj granicę buildera w Xcode
Aktualna dokumentacja Apple rozróżnia Xcode 26 i wcześniejsze wersje od Xcode 27 i nowszych. Dla starszych narzędzi opisuje ViewBuilder jako builder widoków. W przypadku Xcode 27 i nowszych opisuje konstrukcję niezależną od typu i zaleca alias ContentBuilder, aby wskazać szersze działanie. Zobacz ViewBuilder.
W bazie kodu obsługującej Xcode 27 lub nowszy inicjalizator może zamiast tego używać @ContentBuilder. Apple opisuje go jako alias typu dla ViewBuilder, a zgodność jest wymuszana przez typy treści, które tworzy. Zachowaj w tym komponencie Content: View: jego wynik nadal jest przeznaczony dla hierarchii widoków. Nie zakładaj, że nowsza składnia istnieje w starszych SDK tylko dlatego, że aktualna dokumentacja wymienia starsze docelowe wersje systemu. Zobacz ContentBuilder.
Dlaczego some View nie rozwiązuje wszystkich części tego kontraktu?
some View pasuje do właściwości body w przykładzie. Ukrywa konkretny typ wyniku, zachowując obietnicę, że wynik jest widokiem. Przechowywane domknięcie używa natomiast nazwanego parametru Content, aby komponent mógł odwoływać się do tego samego typu wyniku we właściwości i inicjalizatorze.
Swift 5.7 wprowadził niejawne deklaracje parametrów za pośrednictwem SE-0341. Bezpośredni parametr, taki jak _ value: some View, wprowadza nienazwany parametr generyczny, którego konkretny typ określa wywołujący. Nie oznacza to, że implementacja funkcji może wybrać dowolny widok i przekazać go w miejsce, w którym oczekiwany jest ten nienazwany typ. Zobacz Opaque Parameter Declarations.
W szczególności parametr zwrotny zapisany jako (some View) -> Content nie należy do obsługiwanych pozycji parametrów niejawnych opisanych w SE-0341. Propozycja wyraźnie zabrania typów niejawnych w pozycji wejściowej typu funkcji parametru zwrotnego. Omówienie możliwości wprowadzenia takiej składni znajduje się w przyszłych kierunkach rozwoju, a nie w zaimplementowanych funkcjach. Aktualna dokumentacja Types nadal zawiera to ograniczenie.
W tym zadaniu dla komputera ścieżka jest prosta: typem dostarczanej przez komponent wartości jest konkretny typ konfiguracji. Jeśli inny kontener wielokrotnego użytku musi zachować typ wejścia dostarczonego przez wywołującego, nazwij ten typ parametrem generycznym i konsekwentnie uwzględnij go w przechowywanej wartości oraz domknięciu zwrotnym. Żadne z tych rozwiązań nie wymaga niejawnego wejścia w domknięciu zwrotnym.
Co faktycznie gwarantuje kompilator?
Podpis gwarantuje wywoływalny kształt: jeden argument SwatchInput i wynik spełniający ograniczenie Content: View. Nadaje też parametrowi domknięcia wywnioskowany typ, dzięki czemu wywołujący może uzyskać dostęp do input.title, input.side i input.color bez powtarzania adnotacji typu. Swift opisuje to wnioskowanie na podstawie kontekstu domknięcia w sekcji Closures.
Nie może zagwarantować, że te właściwości wpłyną na wyświetlaną treść. Na przykład poniższe wyrażenie jest poprawnym zamiennikiem wewnątrz ContentView.body; celowo przyjmuje konfigurację i ją ignoruje:
SwatchPanel { _ in
Text("Fixed preview")
}
Ta granica ma znaczenie przy formułowaniu wymagań komponentu. „Treść wywołującego przyjmuje SwatchInput” to kontrakt typów. „Treść wywołującego wyświetla dostarczony rozmiar” to wymaganie behawioralne. Jeśli wyświetlanie tego rozmiaru jest niezbędne dla komponentu, umieść renderowanie wewnątrz komponentu i udostępnij mniejszy obszar personalizacji.
Użyj poniższych kontroli, aby diagnozować problemy integracyjne. To kroki inspekcji wynikające z interfejsu, a nie zgłoszone wyniki testów kompilatora:
| Objaw | Sprawdź |
|---|---|
Brak argumentu przy wywołaniu content |
Przekaż konfigurację: content(input). |
| Diagnostyka dotycząca uciekającego domknięcia w inicjalizatorze | Zachowaj @escaping, gdy zapisujesz callback. |
| Kilka sąsiednich widoków powoduje błąd w treści wywołującego | Zachowaj odpowiednią adnotację buildera na parametrze inicjalizatora. |
| Jawnie typowany callback jest odrzucany | Sprawdź, czy jego wejściem jest SwatchInput, a nie niezwiązany typ, taki jak String. |
| Podgląd ignoruje przełącznik rozmiaru | Sprawdź, czy renderowanie odczytuje input.side. |
| Komponent nadal wyświetla początkową konfigurację | Sprawdź, czy treść została utworzona raz i zapisana zamiast wywołania callbacku z bieżącym wejściem. |
Ogranicz kontrakt do tego, czego wymaga zadanie. Konkretny typ konfiguracji i generyczny typ wyniku widoku wyznaczają jasną granicę tego panelu próbki: komponent dostarcza bieżące wartości, wywołujący otrzymuje je z pełną informacją o typie, a wybraną prezentację można łatwo zastąpić.
Źródła
- 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