40. Объединяем Magnification и Drag жесты
Oleg991В SwiftUI есть разные жесты, объединив которые можно получить классный результат. В этой статье покажу как можно объединить MagnificationGesture и DragGesture для управления вьюшкой.
Сделаем такой экран:

Чтобы было интереснее, используем такие вводные:
- Минимальный зум: 1
- Максимальный зум: 3
Код выглядит так:
import SwiftUI
struct ZoomableViewExample: View {
private let minZoom: CGFloat = 1.0
private let maxZoom: CGFloat = 3.0
@State private var scale: CGFloat = 1.0
@State private var lastScale: CGFloat = 1.0
// Изменяем в `onChanged` у жеста, но сбрасываем в `onEnded`
@State private var dragOffset = CGSize.zero
// Изменяем в `onEnded` у жеста
@State private var position = CGSize.zero
private var xOffset: CGFloat {
dragOffset.width + position.width
}
private var yOffset: CGFloat {
dragOffset.height + position.height
}
var body: some View {
NavigationView { // Для навбара и кнопки "Сброс"
// Подопытная вьюшка
exampleView
// Применяем зум и положение
.scaleEffect(scale)
.offset(x: xOffset, y: yOffset)
// Анимируем смену состояний
.animation(.easeInOut(duration: 0.2), value: scale)
.animation(.linear(duration: 0.2), value: dragOffset)
.animation(.linear(duration: 0.2), value: position)
// Добавляем жесты
.gesture(
magnificationGesture
.simultaneously(with: dragGesture)
)
.toolbar {
ToolbarItem {
// Кнопка для сброса зума и положения
resetButton
}
}
}
}
Наша вьюшка, над которой ставим эксперименты:
var exampleView: some View {
Rectangle()
.frame(width: 250, height: 250)
.overlay {
VStack {
Text("scale: \(scale, specifier: "%.2f")")
Text("x: \(xOffset, specifier: "%.2f")")
Text("y: \(yOffset, specifier: "%.2f")")
}
.foregroundStyle(.white)
.transaction { transaction in
// Выключаем анимации внутри `VStack`, чтобы текст не размазывался
transaction.animation = nil
}
}
}
MagnificationGesture
var magnificationGesture: some Gesture {
MagnificationGesture(minimumScaleDelta: 0.0)
.onChanged { gesture in
process(new: gesture.magnitude, updating: &scale)
}
.onEnded { gesture in
process(new: gesture.magnitude, updating: &lastScale)
scale = lastScale
}
}
func process(new magnitude: CGFloat, updating scale: inout CGFloat) {
let magnification = lastScale + magnitude - 1.0
if magnification >= minZoom && magnification <= maxZoom {
// Не вышли за лимиты, задаем новый `scale`
scale = magnification
} else if magnification < minZoom {
// Вышли за нижнюю границу, задаем минимальный `scale`
scale = minZoom
} else if magnification > maxZoom {
// Вышли за верхнюю границу, задаем максимальный `scale`
scale = maxZoom
}
}
DragGesture
var dragGesture: some Gesture {
DragGesture()
.onChanged { gesture in
dragOffset = gesture.translation
}
.onEnded { gesture in
dragOffset = .zero
position.width += gesture.translation.width
position.height += gesture.translation.height
}
}
Бонус
Сделаем похожий функционал с использованием UIKit и PDFView:

Код выглядит так:
import PDFKit
import SwiftUI
struct PDFViewRepresentable: UIViewRepresentable {
let image: UIImage
func makeUIView(context _: Context) -> PDFView {
let view = PDFView()
guard let page = PDFPage(image: image) else { return view }
view.document = PDFDocument()
view.document?.insert(page, at: 0)
view.autoScales = true
view.minScaleFactor = 0.5 // минимальный зум
view.maxScaleFactor = 3 // максимальный зум
return view
}
func updateUIView(_ : PDFView, context _: Context) {}
}
Код для этой статьи можно посмотреть тут, а другие статьи - тут.