Personalizando gestos no SwiftUI
Ao contrário de muitos controles internos, o SwiftUI não envolve UIGestureRecognizer (ou NSGestureRecognizer), mas reestrutura seu próprio sistema de gestos. Os gestos do SwiftUI diminuem a barreira de entrada até certo ponto, mas devido à falta de APIs que fornecem dados subjacentes, os desenvolvedores são severamente limitados em sua capacidade de personalização. No SwiftUI, não podemos criar um novo UIGestureRecongnizer. Os chamados gestos personalizados são, na verdade, apenas uma refatoração dos gestos predefinidos do sistema. Este artigo demonstrará como personalizar o gesto desejado usando ferramentas nativas do SwiftUI por meio de vários exemplos.
Fundamentos
Gestos predefinidos
Atualmente, o SwiftUI fornece 5 gestos predefinidos, que são toque, toque longo, arrastar, ampliação e rotação. O método de chamada, como onTapGesture, é na verdade uma extensão de exibição criada para conveniência.
- ToqueGesto
- Gesto de Pressão Longa
- ArrastarGesto
- AmpliaçãoGesto
- RotationGesture
Clicar, pressionar e arrastar suporta apenas um dedo. SwiftUI não fornece uma função para definir o número de dedos.
Além dos gestos fornecidos aos desenvolvedores, o SwiftUI na verdade possui um grande número de gestos internos (não publicados) para controles do sistema, como ScrollGesture, _ButtonGesture, etc.
A implementação do gesto embutido de Button é mais complexa do que TapGesture. Além de oferecer mais oportunidades de invocação, também oferece suporte ao processamento inteligente do tamanho da área de impressão (para melhorar a taxa de sucesso do toque do dedo).
Valor
SwiftUI fornece diferentes conteúdos de dados de acordo com o tipo de gesto.
- Clique: o tipo de dados é Void (no SwiftUI 4.0, o tipo de dados é CGPoint, indicando a posição do clique em um espaço de coordenadas específico).
- Toque longo: O tipo de dados é Bool, fornecendo true após o início do toque
- Arrastar: fornece as informações de dados mais abrangentes, incluindo posição atual, deslocamento, hora do evento, ponto final previsto, deslocamento previsto, etc.
- Zoom: O tipo de dados é CGFloat, indicando a quantidade de zoom
- Girar: O tipo de dados é Ângulo, indicando o ângulo de rotação
Tempo
Não há conceito de estado dentro dos gestos do SwiftUI. Ao configurar fechamentos correspondentes a tempos específicos, os gestos serão chamados automaticamente no momento apropriado.
- onEnded
- onChanged
- atualizando
Gestos diferentes têm focos diferentes no tempo. Clicar geralmente se concentra apenas em onEnded; onChanged (ou atualização) é mais importante para arrastar, dimensionar e girar. O pressionamento longo é chamado apenas quando a duração definida é atendida e, em seguida, onEnded é chamado.
GestureState
Um tipo de wrapper de propriedade projetado especificamente para gestos do SwiftUI, que pode gerar atualizações de exibição como uma dependência. Difere do Estado nos seguintes aspectos:
- Ele só pode ser modificado no método de atualização do gesto e é somente leitura em qualquer outro lugar da exibição.
- Quando o gesto associado a ele (usando atualização) termina, ele restaura automaticamente seu conteúdo ao seu valor inicial.
- O estado da animação ao restaurar os dados iniciais pode ser definido por meio de resetTransaction.
O SwiftUI fornece vários métodos para combinar gestos, permitindo que vários gestos sejam conectados e reestruturados em outros tipos de gestos.
- simultaneamente
- sequenciado
- exclusivamente
Depois de combinar gestos, o tipo de valor também mudará. Você ainda pode usá- maplo para convertê-lo em um tipo de dados mais utilizável.
Definição do Formato do Gesto
Os desenvolvedores geralmente criam gestos personalizados dentro da exibição, que tem menos código e é fácil de combinar com outros dados na exibição. Por exemplo, o código a seguir cria um gesto compatível com escala e rotação na exibição:
struct GestureDemo: View {
@GestureState(resetTransaction: .init(animation: .easeInOut)) var gestureValue = RotateAndMagnify()
var body: some View {
let rotateAndMagnifyGesture = MagnificationGesture()
.simultaneously(with: RotationGesture())
.updating($gestureValue) { value, state, _ in
state.angle = value.second ?? .zero
state.scale = value.first ?? 0
}
return Rectangle()
.fill(LinearGradient(colors: [.blue, .green, .pink], startPoint: .top, endPoint: .bottom))
.frame(width: 100, height: 100)
.shadow(radius: 8)
.rotationEffect(gestureValue.angle)
.scaleEffect(gestureValue.scale)
.gesture(rotateAndMagnifyGesture)
}
struct RotateAndMagnify {
var scale: CGFloat = 1.0
var angle: Angle = .zero
}
}
Encapsular gestos ou lógica de manipulação de gestos em extensões de exibição pode simplificar ainda mais seu uso.
Para destacar certos aspectos da funcionalidade, o código de demonstração fornecido nas seções a seguir pode parecer detalhado. Na prática, pode ser simplificado conforme necessário.
Exemplo 1: Deslize
1.1 Objetivo
Crie um gesto de deslizar e demonstre como criar uma estrutura em conformidade com o protocolo Gesture e converter dados de gesto.
1.2 Ideia
Nos gestos predefinidos do SwiftUI, apenas DragGesture fornece dados que podem ser usados para determinar a direção do movimento. A direção do deslizamento é determinada com base no deslocamento e o mapa é usado para converter dados complicados em dados direcionais simples.
1.3 Implementação
public struct SwipeGesture: Gesture {
public enum Direction: String {
case left, right, up, down
}
public typealias Value = Direction
private let minimumDistance: CGFloat
private let coordinateSpace: CoordinateSpace
public init(minimumDistance: CGFloat = 10, coordinateSpace: CoordinateSpace = .local) {
self.minimumDistance = minimumDistance
self.coordinateSpace = coordinateSpace
}
public var body: AnyGesture<Value> {
AnyGesture(
DragGesture(minimumDistance: minimumDistance, coordinateSpace: coordinateSpace)
.map { value in
let horizontalAmount = value.translation.width
let verticalAmount = value.translation.height
if abs(horizontalAmount) > abs(verticalAmount) {
if horizontalAmount < 0 { return .left } else { return .right }
} else {
if verticalAmount < 0 { return .up } else { return .down }
}
}
)
}
}
public extension View {
func onSwipe(minimumDistance: CGFloat = 10,
coordinateSpace: CoordinateSpace = .local,
perform: @escaping (SwipeGesture.Direction) -> Void) -> some View {
gesture(
SwipeGesture(minimumDistance: minimumDistance, coordinateSpace: coordinateSpace)
.onEnded(perform)
)
}
}
struct SwipeTestView: View {
@State var direction = ""
var body: some View {
Rectangle()
.fill(.blue)
.frame(width: 200, height: 200)
.overlay(Text(direction))
.onSwipe { direction in
self.direction = direction.rawValue
}
}
}
- Por que usar AnyGesture
extension Gesture where Self.Value == Self.Body.Value {
public static func _makeGesture(gesture: SwiftUI._GraphValue<Self>, inputs: SwiftUI._GestureInputs) -> SwiftUI._GestureOutputs<Self.Body.Value>
}
Neste exemplo, fatores como a duração e a velocidade do movimento do gesto não foram totalmente considerados. A implementação atual não pode ser estritamente considerada um furto verdadeiro. Para obter um furto verdadeiro, o seguinte método de implementação pode ser usado:
- Modifique a implementação para a do Exemplo 2, usando um ViewModifier para agrupar o DragGesture
- Use o estado para registrar a duração do furto
- Em onEnded, apenas chama o fechamento do usuário e passa a direção se a velocidade, distância, desvio e outros requisitos forem atendidos.
2.1 Objetivo
Para implementar um gesto de imprensa que pode gravar a duração. Durante a impressão, um retorno de chamada semelhante a onChangedpode ser chamado com base em um intervalo de tempo especificado. Este exemplo demonstra como agrupar gestos com modificadores de exibição e como usar GestureState.
2.2 Ideia
Use um cronômetro para passar a duração atual da pressão para um fechamento após um intervalo de tempo especificado. Use GestureStatepara salvar a hora de início da impressora. Após o término do pressionamento, o horário de início do último pressionamento será apagado automaticamente pelo gesto.
2.3 Implementação
public struct PressGestureViewModifier: ViewModifier {
@GestureState private var startTimestamp: Date?
@State private var timePublisher: Publishers.Autoconnect<Timer.TimerPublisher>
private var onPressing: (TimeInterval) -> Void
private var onEnded: () -> Void
public init(interval: TimeInterval = 0.016, onPressing: @escaping (TimeInterval) -> Void, onEnded: @escaping () -> Void) {
_timePublisher = State(wrappedValue: Timer.publish(every: interval, tolerance: nil, on: .current, in: .common).autoconnect())
self.onPressing = onPressing
self.onEnded = onEnded
}
public func body(content: Content) -> some View {
content
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.updating($startTimestamp, body: { _, current, _ in
if current == nil {
current = Date()
}
})
.onEnded { _ in
onEnded()
}
)
.onReceive(timePublisher, perform: { timer in
if let startTimestamp = startTimestamp {
let duration = timer.timeIntervalSince(startTimestamp)
onPressing(duration)
}
})
}
}
public extension View {
func onPress(interval: TimeInterval = 0.016, onPressing: @escaping (TimeInterval) -> Void, onEnded: @escaping () -> Void) -> some View {
modifier(PressGestureViewModifier(interval: interval, onPressing: onPressing, onEnded: onEnded))
}
}
struct PressGestureView: View {
@State var scale: CGFloat = 1
@State var duration: TimeInterval = 0
var body: some View {
VStack {
Circle()
.fill(scale == 1 ? .blue : .orange)
.frame(width: 50, height: 50)
.scaleEffect(scale)
.overlay(Text(duration, format: .number.precision(.fractionLength(1))))
.onPress { duration in
self.duration = duration
scale = 1 + duration * 2
} onEnded: {
if duration > 1 {
withAnimation(.easeInOut(duration: 2)) {
scale = 1
}
} else {
withAnimation(.easeInOut) {
scale = 1
}
}
duration = 0
}
}
}
}
- O tempo de restauração dos dados GestureState é antes de onEnded e, em onEnded, startTimestamp já foi restaurado para nil
- DragGesture ainda é a melhor operadora de implementação. TapGesture e LongPressGesture encerrarão automaticamente o gesto após atender às condições de acionamento e não podem suportar durações arbitrárias
A solução atual não fornece uma configuração para limitação de deslocamento da posição pressionada semelhante a LongPressGesture. Além disso, a duração total desse pressionamento não foi fornecida em onEnded.
- Na atualização, julgue o valor do deslocamento. Se o deslocamento do ponto de toque exceder o intervalo especificado, o tempo será interrompido. Chame o encerramento onEnded fornecido pelo usuário na atualização e marque-o.
- No onEnded do gesto, se o encerramento onEnded fornecido pelo usuário tiver sido chamado, ele não será chamado novamente.
- Substitua GestureState por State, para que a duração total possa ser fornecida no onEnded do gesto. Você mesmo precisa escrever o código de recuperação de dados do estado.
- Como State é usado em vez de GestureState, o julgamento lógico pode ser movido de update para onChanged.
SwiftUI 4.0 apresenta um novo gesto — SpatialTapGesture, que pode obter diretamente a posição do clique. onTapGesture também foi aprimorado, e o valor em onChange e onEnd representará a posição do clique em um espaço de coordenadas específico (CGPoint).
3.1 Objetivo
Para implementar um gesto de clique que fornece informações de localização de toque (suporta a configuração do número de cliques). Este exemplo demonstra principalmente o uso de simultâneo e como escolher o tempo de retorno de chamada apropriado (onEnded).
3.2 Abordagem
A resposta do gesto deve ser exatamente igual ao TapGesture. Use simultaneamente para combinar os dois gestos, obter dados de posição do DragGesture e sair do TapGesture.
3.3 Implementação
public struct TapWithLocation: ViewModifier {
@State private var locations: CGPoint?
private let count: Int
private let coordinateSpace: CoordinateSpace
private var perform: (CGPoint) -> Void
init(count: Int = 1, coordinateSpace: CoordinateSpace = .local, perform: @escaping (CGPoint) -> Void) {
self.count = count
self.coordinateSpace = coordinateSpace
self.perform = perform
}
public func body(content: Content) -> some View {
content
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: coordinateSpace)
.onChanged { value in
locations = value.location
}
.simultaneously(with:
TapGesture(count: count)
.onEnded {
perform(locations ?? .zero)
locations = nil
}
)
)
}
}
public extension View {
func onTapGesture(count: Int = 1, coordinateSpace: CoordinateSpace = .local, perform: @escaping (CGPoint) -> Void) -> some View {
modifier(TapWithLocation(count: count, coordinateSpace: coordinateSpace, perform: perform))
}
}
struct TapWithLocationView: View {
@State var unitPoint: UnitPoint = .center
var body: some View {
Rectangle()
.fill(RadialGradient(colors: [.yellow, .orange, .red, .pink], center: unitPoint, startRadius: 10, endRadius: 170))
.frame(width: 300, height: 300)
.onTapGesture(count:2) { point in
withAnimation(.easeInOut) {
unitPoint = UnitPoint(x: point.x / 300, y: point.y / 300)
}
}
}
}
- Quando o MinimumDistance de DragGesture é definido como 0, os primeiros dados produzidos por ele são garantidos como anteriores ao tempo de ativação de TapGesture (contagem: 1)
- No
simultaneously, há trêsonEndedocasiões no total. OonEndeddo gesto 1, oonEndeddo gesto 2 e oonEndeddo gesto mesclado. Neste exemplo, optamos por chamar o fechamento do usuário noonEndedTapGesture.
Atualmente, os gestos do SwiftUI estão em um limite baixo de uso, mas seu limite de capacidade é insuficiente. Usar apenas os meios nativos do SwiftUI não pode alcançar uma lógica de gesto muito complexa. No futuro, estudaremos questões relacionadas à prioridade entre gestos, a invalidação seletiva usando GestureMask e como trabalhar com UIGestureRecognizer para criar gestos complexos através de outros artigos.
Espero que este artigo possa ser útil para você. Você também pode se comunicar comigo através do Twitter , do canal Discord ou do quadro de mensagens do meu blog .





































![O que é uma lista vinculada, afinal? [Parte 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)