Połącz DragGesture, RotateGesture i MagnifyGesture za pomocą simultaneously(with:), a następnie renderuj ich wartości w jednym potoku transformacji. W obszarze roboczym Maca pozostaw powierzchnię wejściową nieruchomą, a tymczasową zmianę każdego gestu przechowuj osobno od zatwierdzonego wyniku.
W tym samouczku zbudujesz w swojej aplikacji macOS prosty podgląd kształtu: przeciągnij, aby przesunąć, uszczypnij, aby powiększyć, i użyj gestu obrotu na gładziku, aby obrócić kształt. Wykorzystuje on wyłącznie prostokąt i okrąg przechowywane w pamięci. Kompozycja umożliwia nakładanie się rozpoznawanych gestów; to, które wejścia faktycznie się nałożą, zależy od zdarzeń dostarczanych przez urządzenie.
Co oznacza jednoczesność na Macu?
Apple’owskie simultaneously(with:) łączy dwa gesty w jeden, więc wynik można połączyć z trzecim. Zapewnia to kompozycję potrzebną do przesuwania, obracania i powiększania bez konieczności długiego naciśnięcia ani innego działania wstępnego. Dokumentacja Apple dotycząca gestów jednoczesnych opisuje tę kombinację.
Zanim napiszesz kod, zdecyduj, co oznacza przesuwanie. W tym przykładzie jest to przeciąganie z wciśniętym głównym przyciskiem myszy, w tym kliknięcie i przeciągnięcie na gładziku. Przesunięcie dwoma palcami po gładziku zwykle generuje zdarzenia przewijania. Zarchiwizowany przewodnik Apple dotyczący zdarzeń wyraźnie odróżnia ten strumień przewijania od gestów powiększania i obrotu; aktualna dokumentacja AppKit nadal udostępnia przewijanie za pomocą scrollWheel(with:). Dodanie gestu przeciągania nie zamienia przewijania w przeciąganie. Obsługa zdarzeń gładzika, scrollWheel(with:)
Skorzystaj z tej tabeli decyzyjnej, aby wybrać warstwę wejściową dla obszaru roboczego:
| Oczekiwana interakcja | Punkt wyjścia | Ograniczenie, o którym należy pamiętać |
|---|---|---|
| Przesuwanie przez kliknięcie i przeciągnięcie, powiększanie przez uszczypnięcie, obrót na gładziku | Poniższy przykład w SwiftUI | Kompozycja umożliwia nakładanie się rozpoznawanych strumieni gestów. |
| Przesuwanie przez przewijanie dwoma palcami | Obszar roboczy AppKit osadzony w SwiftUI | Przewijanie pozostaje strumieniem zdarzeń przewijania. |
| Gwarantowana transformacja z trzech składowych wywołana jednym układem palców | Osobno zdefiniowana niestandardowa interakcja | Sama kompozycja jednoczesna nie gwarantuje, że sprzęt lub macOS wygeneruje każdą składową. |
Przykład wymaga systemu macOS 14 lub nowszego, w którym dostępne są zarówno MagnifyGesture, jak i RotateGesture. Używa ich podstawowych inicjalizatorów, a nie nowszych interfejsów API konfigurujących rodzaj wejścia. MagnifyGesture, RotateGesture
Jak zorganizować stan transformacji?
Nadaj każdej składowej wartość zatwierdzoną i wartość gestu na żywo. Przesunięcie i obrót łączą się przez dodawanie; skala — przez mnożenie. Poniższe równania określają zachowanie przykładu, a nie wymagania narzucone przez SwiftUI:
wyświetlane przesunięcie = zatwierdzone przesunięcie + przesunięcie gestu na żywo
wyświetlany obrót = zatwierdzony obrót + obrót na żywo
wyświetlana skala = ogranicz(zatwierdzona skala × powiększenie na żywo)
Użyj @GestureState dla wartości na żywo. Apple dokumentuje, że stan ten resetuje się, gdy gest przestaje być aktywny. Zatwierdzaj każdą składową w osobnym wywołaniu zwrotnym onEnded, które uruchamia się tylko po pomyślnym zakończeniu gestu. Dzięki temu anulowanie ma jasne znaczenie: odrzuć niedokończony podgląd tej składowej i zachowaj jej poprzednią zatwierdzoną wartość. GestureState, onEnded(_:)
Umieść te wywołania zwrotne w poszczególnych gestach przed ich połączeniem. Każde otrzyma wtedy własną wartość o określonym typie i zmieni tylko przypisaną mu właściwość. Nie ma wspólnej migawki początkowej transformacji, którą mógłby nadpisać gest rozpoczęty później.
Wartość przesunięcia gestu przeciągania wymaga szczególnej uwagi: określa całkowity ruch od początku gestu. Dodawanie tej sumy do zatwierdzonego przesunięcia przy każdej aktualizacji wielokrotnie uwzględniałoby wcześniejszy ruch. Przypisuj sumę do podglądu na żywo, a po zakończeniu dodaj ją tylko raz. DragGesture.Value.translation
Zbuduj kompletny przykład w SwiftUI
Utwórz aplikację macOS w SwiftUI z minimalną wersją wdrożeniową macOS 14 lub nowszą. Zastąp wygenerowany punkt wejścia aplikacji i widok zawartości poniższym pojedynczym plikiem; w celu docelowym pozostaw dokładnie jedną deklarację @main. Nie są potrzebne zasoby, pakiety ani dane zewnętrzne.
Gestem zarządza szare płótno. Jego widok podrzędny kształtu otrzymuje transformacje wizualne, więc przesuwanie lub obracanie kształtu nie przekształca powierzchni wejściowej gestu. Przykład celowo powiększa i obraca kształt wokół jego środka.
import SwiftUI
@main
struct TransformCanvasApp: App {
var body: some Scene {
WindowGroup {
TransformCanvas()
.frame(minWidth: 400, minHeight: 300)
}
.defaultSize(width: 720, height: 500)
}
}
struct TransformCanvas: View {
@State private var offset = CGSize.zero
@State private var angle = Angle.zero
@State private var scale: CGFloat = 1
@GestureState private var liveOffset = CGSize.zero
@GestureState private var liveAngle = Angle.zero
@GestureState private var liveScale: CGFloat = 1
private func boundedScale(_ value: CGFloat) -> CGFloat {
min(max(value, 0.25), 4)
}
private var pan: some Gesture {
DragGesture(minimumDistance: 3, coordinateSpace: .local)
.updating($liveOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
offset = CGSize(
width: offset.width + value.translation.width,
height: offset.height + value.translation.height
)
}
}
private var rotation: some Gesture {
RotateGesture()
.updating($liveAngle) { value, state, _ in
state = value.rotation
}
.onEnded { value in
angle = angle + value.rotation
}
}
private var zoom: some Gesture {
MagnifyGesture()
.updating($liveScale) { value, state, _ in
state = value.magnification
}
.onEnded { value in
scale = boundedScale(scale * value.magnification)
}
}
private var manipulation: some Gesture {
pan.simultaneously(with: rotation)
.simultaneously(with: zoom)
}
var body: some View {
ZStack {
Color.gray.opacity(0.12)
RoundedRectangle(cornerRadius: 18)
.fill(Color.blue)
.overlay(alignment: .topLeading) {
Circle()
.fill(Color.white)
.frame(width: 24, height: 24)
.padding(16)
}
.frame(width: 180, height: 120)
.scaleEffect(
boundedScale(scale * liveScale),
anchor: .center
)
.rotationEffect(angle + liveAngle, anchor: .center)
.offset(CGSize(
width: offset.width + liveOffset.width,
height: offset.height + liveOffset.height
))
.allowsHitTesting(false)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.clipped()
.gesture(manipulation)
}
}
Apple opisuje podstawowe typy gestów przeciągania, powiększania i obrotu oraz dołączanie gestów do widoków. Przykład łączy te interfejsy API z jawną polityką stanu i układu współrzędnych. SwiftUI nie było dostępne w środowisku badawczym, dlatego program sprawdzono na podstawie dokumentacji, ale nie skompilowano ani nie uruchomiono. DragGesture, MagnifyGesture, RotateGesture
Dlaczego najpierw skala, potem obrót, a na końcu przesunięcie?
Zamierzona transformacja skaluje kształt wokół jego środka, obraca go wokół tego samego środka, a na końcu przesuwa we współrzędnych płótna. Zastosowanie przesunięcia na końcu sprawia, że przesuwanie nie podlega skali ani obrotowi kształtu. Poziomy ruch wskaźnika powinien przesuwać kształt poziomo nawet po jego obróceniu.
Ta kolejność jest decyzją projektową wynikającą z potoku transformacji przykładu. Apple dokumentuje, że scaleEffect skaluje renderowany wynik wokół punktu zakotwiczenia, rotationEffect obraca renderowany wynik, a offset przesuwa wyświetlaną zawartość. Te efekty wizualne nie zmieniają nieruchomego płótna w przekształcany kontener układu. scaleEffect(_:anchor:), rotationEffect(_:anchor:), offset(_:)
Granice 0,25 i 4 to przykładowe ustawienia aplikacji. To samo ograniczenie jest stosowane podczas podglądu i zatwierdzania, więc po zwolnieniu gestu wyświetlana skala pozostaje bez zmian. Swiftowe funkcje min i max wybierają mniejszą i większą wartość potrzebną do wyrażenia tych granic. min(::), max(::)
Sprawdź interakcję, korzystając z konkretnych oczekiwań
Najpierw wypróbuj każdą składową osobno, a potem spróbuj nałożyć gesty, korzystając z dostępnych urządzeń wejściowych. Białe koło sprawia, że obrót jest widoczny nawet wtedy, gdy prostokąt jest mały.
- Kliknij i przeciągnij na płótnie, zwolnij przycisk, a następnie przeciągnij ponownie. Drugie przeciągnięcie powinno rozpocząć się od pozycji zatwierdzonej po pierwszym.
- Uszczypnij, aby powiększyć kształt, zwolnij i uszczypnij ponownie. Każdy gest powinien pomnożyć zachowaną skalę.
- Obróć kształt, zwolnij i obróć ponownie. Kąt powinien się kumulować.
- Po powiększeniu i obróceniu przeciągnij poziomo. Kształt powinien nadal przesuwać się wzdłuż poziomej osi płótna.
- Jeśli urządzenia dostarczają nakładające się wejścia, zakończ jedną składową, kontynuując drugą. Pozostała składowa powinna nadal aktualizować własną właściwość.
- Przerwij aktywny gest, na przykład przełączając aplikacje. Każdy gest, który staje się nieaktywny bez pomyślnego zakończenia, powinien utracić podgląd na żywo, zamiast pozostawiać zablokowany stan tymczasowy.
Dla przykładu: jeśli zachowana skala wynosi 1,5, a gest uszczypnięcia zwraca wartość 1,2, wyświetlany wynik to 1,8. Jeśli zachowany kąt wynosi 30 stopni, a obrót na żywo to 15 stopni, wyświetlany kąt wynosi 45 stopni. Przesunięcie o 40 punktów pozostaje przesunięciem płótna o 40 punktów, niezależnie od skali i kąta.
W tym przykładzie składowe są zatwierdzane niezależnie. Jeśli obrót zakończy się pomyślnie, a aktywne przeciąganie zostanie później anulowane, zakończony obrót zostanie zachowany, a niedokończone przeciąganie wycofane. Edytor wymagający jednej atomowej operacji obejmującej wszystkie składowe potrzebuje dodatkowego modelu sesji interakcji z jawną polityką zatwierdzania i wycofywania.
Kształt można też przeciągnąć poza widoczne płótno, ponieważ przesunięcie celowo nie ma ograniczeń. Płótno pozostaje dostępne do przeciągania, więc można przesunąć kształt z powrotem w przeciwnym kierunku. Gotowy podgląd może udostępniać osobne polecenie resetowania albo ograniczać przesunięcie zgodnie z granicami własnej zawartości.
Kiedy zamiast tego użyć obszaru roboczego AppKit?
Wybierz warstwę wejściową AppKit, gdy zadanie wymaga przesuwania przez przewijanie dwoma palcami albo bezpośredniego sterowania bezwładnością przewijania. Osadź własny widok AppKit za pomocą NSViewRepresentable, który Apple udostępnia do tworzenia, aktualizowania i niszczenia widoków AppKit w SwiftUI. NSViewRepresentable
Przypisz każdemu strumieniowi wejściowemu jednego właściciela. W obszarze roboczym AppKit obsługuj przewijanie przez scrollWheel(with:), a do powiększania i obrotu używaj rozpoznawaczy gestów. Przekazuj wynikające z nich zmiany do tego samego modelu przesunięcia, kąta i skali. Unikaj dołączania dodatkowej obsługi SwiftUI, która stosuje te same wejścia do tego modelu.
Aby rozpoznawacze gestów AppKit mogły działać jednocześnie, zaimplementuj gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) i zwracaj true tylko dla zamierzonych par rozpoznawaczy należących do obszaru roboczego. Apple dokumentuje, że metoda delegata dopuszcza jednoczesne rozpoznawanie; nie służy ona do tworzenia brakującego strumienia wejściowego. Metoda delegata dopuszczająca jednoczesne rozpoznawanie
Zachowaj konwencje wartości podczas przekazywania danych między interfejsami. Rozpoznawacz gestu powiększania AppKit zwraca wartość, którą trzeba dodać do 1, aby uzyskać współczynnik skali, a rozpoznawacz gestu obrotu udostępnia radiany. Nie przenoś bez zmian przypisań wartości z wersji SwiftUI. NSMagnificationGestureRecognizer.magnification, NSRotationGestureRecognizer.rotation
W przypadku przewijania Apple wskazuje scrollingDeltaX jako preferowaną właściwość dla poziomego przesunięcia i zaznacza, że wartości niedokładne mogą wymagać korekty. Dokumentacja bezwładności przewijania opisuje zdarzenia przewijania, które są wysyłane dalej po zakończeniu fizycznego gestu. Zdecyduj, czy płótno ma przyjmować te dalsze zdarzenia, i przekazuj każde zaakceptowane zdarzenie tylko raz. scrollingDeltaX, momentumPhase
Warstwa pośrednicząca powinna zmieniać transformację zawartości płótna, natomiast SwiftUI zachowuje kontrolę nad układem reprezentowanego widoku. Apple wyraźnie ostrzega przed bezpośrednim ustawianiem właściwości frame lub bounds reprezentowanego widoku. Użyj koordynatora widoku reprezentującego, aby przekazywać zmiany delegata i akcji celu z powrotem do SwiftUI. NSViewRepresentable
W przypadku podglądu obsługiwanego gestem kliknięcia i przeciągnięcia implementacja w SwiftUI stanowi kompletny punkt wyjścia: jedna nieruchoma powierzchnia wejściowa, trzy niezależnie zarządzane składowe gestów i przemyślana kolejność transformacji. Zachowaj te granice podczas rozbudowy i wybierz rozwiązanie AppKit, gdy interakcja przeznaczona dla użytkownika wymaga przesuwania przez przewijanie.
Źródła
- simultaneously(with:) — Apple
- Obsługa zdarzeń gładzika — Apple, archiwum dokumentacji
- scrollWheel(with:) — Apple
- MagnifyGesture — Apple
- RotateGesture — Apple
- GestureState — Apple
- onEnded(_:) — Apple
- translation — Apple
- DragGesture — Apple
- scaleEffect(_:anchor:) — Apple
- rotationEffect(_:anchor:) — Apple
- offset(_:) — Apple
- min(::) — Apple
- max(::) — Apple
- NSViewRepresentable — Apple
- gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) — Apple
- magnification — Apple
- rotation — Apple
- scrollingDeltaX — Apple
- momentumPhase — Apple