Zhe*_*oni 27 image scrollview pinchzoom swift swiftui
我希望能够在 SwiftUI 中调整大小和移动图像(就像它是地图一样),并通过捏来缩放和拖动它。
使用 UIKit 我将图像嵌入到 a 中UIScrollView并处理它,但我不知道如何在 SwiftUI 中执行此操作。我尝试使用,MagnificationGesture但我无法让它顺利工作。
我已经搜索了一段时间了,有没有人知道是否有更简单的方法?
Jar*_*ren 30
我认为值得一提的极其简单的方法 - 使用 Apple 的PDFKit.
import SwiftUI
import PDFKit
struct PhotoDetailView: UIViewRepresentable {
let image: UIImage
func makeUIView(context: Context) -> PDFView {
let view = PDFView()
view.document = PDFDocument()
guard let page = PDFPage(image: image) else { return view }
view.document?.insert(page, at: 0)
view.autoScales = true
return view
}
func updateUIView(_ uiView: PDFView, context: Context) {
// empty
}
}
Run Code Online (Sandbox Code Playgroud)
优点:
如果您只是展示图像以供查看,那么此方法可能非常适合您。但如果您想添加图像注释等,我会遵循其他答案之一。
根据 maka 的建议进行编辑添加view.autoScales = true。
Jam*_*mes 29
SwiftUI API 在这里非常无用: onChanged 给出了相对于当前缩放手势开始的数字,并且在回调中没有明显的方法来获取初始值。并且有一个 onEnded 回调但很容易错过/忘记。
解决方法,添加:
@State var lastScaleValue: CGFloat = 1.0
Run Code Online (Sandbox Code Playgroud)
然后在回调中:
.gesture(MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
let newScale = self.scale * delta
//... anything else e.g. clamping the newScale
}.onEnded { val in
// without this the next gesture will be broken
self.lastScaleValue = 1.0
}
Run Code Online (Sandbox Code Playgroud)
其中 newScale 是您自己的比例跟踪(可能是状态或绑定)。如果你直接设置你的比例,它会变得一团糟,因为在每个刻度上,数量将相对于之前的数量。
jtb*_*des 28
这里的其他答案对于自定义缩放逻辑过于复杂。如果你想要标准的、久经考验的 UIScrollView 缩放行为,你可以只使用 UIScrollView!
SwiftUI 允许您使用UIViewRepresentable或将任何 UIView 放在 SwiftUI 视图层次结构中UIViewControllerRepresentable。然后要将更多 SwiftUI 内容放入该视图中,您可以使用UIHostingController. 在接口与 UIKit和API 文档中阅读有关 SwiftUI–UIKit 互操作的更多信息。
你可以找到一个更完整的例子,我在一个真实的应用程序中使用它:https : //github.com/jtbandes/SpacePOD/blob/main/SpacePOD/ZoomableScrollView.swift(该例子还包括更多的技巧来使图片。)
var body: some View {
ZoomableScrollView {
Image("Your image here")
}
}
Run Code Online (Sandbox Code Playgroud)
struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// set up the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = 20
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
// create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = self.content
assert(context.coordinator.hostingController.view.superview == uiView)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
init(hostingController: UIHostingController<Content>) {
self.hostingController = hostingController
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
}
}
Run Code Online (Sandbox Code Playgroud)
小智 25
这是我的解决方案,它可以像苹果的照片应用程序一样进行图像缩放。

import SwiftUI
public struct SwiftUIImageViewer: View {
let image: Image
@State private var scale: CGFloat = 1
@State private var lastScale: CGFloat = 1
@State private var offset: CGPoint = .zero
@State private var lastTranslation: CGSize = .zero
public init(image: Image) {
self.image = image
}
public var body: some View {
GeometryReader { proxy in
ZStack {
image
.resizable()
.aspectRatio(contentMode: .fit)
.scaleEffect(scale)
.offset(x: offset.x, y: offset.y)
.gesture(makeDragGesture(size: proxy.size))
.gesture(makeMagnificationGesture(size: proxy.size))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
}
}
private func makeMagnificationGesture(size: CGSize) -> some Gesture {
MagnificationGesture()
.onChanged { value in
let delta = value / lastScale
lastScale = value
// To minimize jittering
if abs(1 - delta) > 0.01 {
scale *= delta
}
}
.onEnded { _ in
lastScale = 1
if scale < 1 {
withAnimation {
scale = 1
}
}
adjustMaxOffset(size: size)
}
}
private func makeDragGesture(size: CGSize) -> some Gesture {
DragGesture()
.onChanged { value in
let diff = CGPoint(
x: value.translation.width - lastTranslation.width,
y: value.translation.height - lastTranslation.height
)
offset = .init(x: offset.x + diff.x, y: offset.y + diff.y)
lastTranslation = value.translation
}
.onEnded { _ in
adjustMaxOffset(size: size)
}
}
private func adjustMaxOffset(size: CGSize) {
let maxOffsetX = (size.width * (scale - 1)) / 2
let maxOffsetY = (size.height * (scale - 1)) / 2
var newOffsetX = offset.x
var newOffsetY = offset.y
if abs(newOffsetX) > maxOffsetX {
newOffsetX = maxOffsetX * (abs(newOffsetX) / newOffsetX)
}
if abs(newOffsetY) > maxOffsetY {
newOffsetY = maxOffsetY * (abs(newOffsetY) / newOffsetY)
}
let newOffset = CGPoint(x: newOffsetX, y: newOffsetY)
if newOffset != offset {
withAnimation {
offset = newOffset
}
}
self.lastTranslation = .zero
}
}
Run Code Online (Sandbox Code Playgroud)
另外,我在 GitHub 中将此解决方案作为 Swift Package提供。
Ava*_*rio 22
这是向 SwiftUI 视图添加捏合缩放的一种方法。它覆盖一个UIView带UIPinchGestureRecognizer一个UIViewRepresentable,并转发相关的值回SwiftUI绑定的。
您可以添加这样的行为:
Image("Zoom")
.pinchToZoom()
Run Code Online (Sandbox Code Playgroud)
这增加了类似于在 Instagram 提要中缩放照片的行为。这是完整的代码:
import UIKit
import SwiftUI
class PinchZoomView: UIView {
weak var delegate: PinchZoomViewDelgate?
private(set) var scale: CGFloat = 0 {
didSet {
delegate?.pinchZoomView(self, didChangeScale: scale)
}
}
private(set) var anchor: UnitPoint = .center {
didSet {
delegate?.pinchZoomView(self, didChangeAnchor: anchor)
}
}
private(set) var offset: CGSize = .zero {
didSet {
delegate?.pinchZoomView(self, didChangeOffset: offset)
}
}
private(set) var isPinching: Bool = false {
didSet {
delegate?.pinchZoomView(self, didChangePinching: isPinching)
}
}
private var startLocation: CGPoint = .zero
private var location: CGPoint = .zero
private var numberOfTouches: Int = 0
init() {
super.init(frame: .zero)
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch(gesture:)))
pinchGesture.cancelsTouchesInView = false
addGestureRecognizer(pinchGesture)
}
required init?(coder: NSCoder) {
fatalError()
}
@objc private func pinch(gesture: UIPinchGestureRecognizer) {
switch gesture.state {
case .began:
isPinching = true
startLocation = gesture.location(in: self)
anchor = UnitPoint(x: startLocation.x / bounds.width, y: startLocation.y / bounds.height)
numberOfTouches = gesture.numberOfTouches
case .changed:
if gesture.numberOfTouches != numberOfTouches {
// If the number of fingers being used changes, the start location needs to be adjusted to avoid jumping.
let newLocation = gesture.location(in: self)
let jumpDifference = CGSize(width: newLocation.x - location.x, height: newLocation.y - location.y)
startLocation = CGPoint(x: startLocation.x + jumpDifference.width, y: startLocation.y + jumpDifference.height)
numberOfTouches = gesture.numberOfTouches
}
scale = gesture.scale
location = gesture.location(in: self)
offset = CGSize(width: location.x - startLocation.x, height: location.y - startLocation.y)
case .ended, .cancelled, .failed:
isPinching = false
scale = 1.0
anchor = .center
offset = .zero
default:
break
}
}
}
protocol PinchZoomViewDelgate: AnyObject {
func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool)
func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat)
func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint)
func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize)
}
struct PinchZoom: UIViewRepresentable {
@Binding var scale: CGFloat
@Binding var anchor: UnitPoint
@Binding var offset: CGSize
@Binding var isPinching: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> PinchZoomView {
let pinchZoomView = PinchZoomView()
pinchZoomView.delegate = context.coordinator
return pinchZoomView
}
func updateUIView(_ pageControl: PinchZoomView, context: Context) { }
class Coordinator: NSObject, PinchZoomViewDelgate {
var pinchZoom: PinchZoom
init(_ pinchZoom: PinchZoom) {
self.pinchZoom = pinchZoom
}
func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) {
pinchZoom.isPinching = isPinching
}
func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) {
pinchZoom.scale = scale
}
func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint) {
pinchZoom.anchor = anchor
}
func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) {
pinchZoom.offset = offset
}
}
}
struct PinchToZoom: ViewModifier {
@State var scale: CGFloat = 1.0
@State var anchor: UnitPoint = .center
@State var offset: CGSize = .zero
@State var isPinching: Bool = false
func body(content: Content) -> some View {
content
.scaleEffect(scale, anchor: anchor)
.offset(offset)
.animation(isPinching ? .none : .spring())
.overlay(PinchZoom(scale: $scale, anchor: $anchor, offset: $offset, isPinching: $isPinching))
}
}
extension View {
func pinchToZoom() -> some View {
self.modifier(PinchToZoom())
}
}
Run Code Online (Sandbox Code Playgroud)
Lou*_*Lac 14
其他答案都可以,这里有一个额外的提示:如果您使用 SwiftUI 手势,您可以使用 a@GestureState而不是 a@State来存储手势状态。手势结束后,它会自动将状态重置为初始值,因此您可以简化此类代码:
@State private var scale: CGFloat = 1.0
.gesture(MagnificationGesture().onChanged { value in
// Anything with value
scale = value
}.onEnded { value in
scale = 1.0
})
Run Code Online (Sandbox Code Playgroud)
和:
@GestureState private var scale: CGFloat = 1.0
.gesture(MagnificationGesture().updating($scale) { (newValue, scale, _) in
// Anything with value
scale = newValue
})
Run Code Online (Sandbox Code Playgroud)
我的两分钱。我确实搜索并找到了解决方案:iOSCretor repo(https://github.com/ioscreator/ioscreator,感谢 Arthur Knopper!)
我在这里稍微修改了一下,复制过来,为了方便,添加了reset方法。
从技术上讲我们:
添加具有比例和状态的图像。
添加 2 个同时工作的手势
还可以通过双击添加“重置”
import SwiftUI
struct ContentView: View {
@GestureState private var scaleState: CGFloat = 1
@GestureState private var offsetState = CGSize.zero
@State private var offset = CGSize.zero
@State private var scale: CGFloat = 1
func resetStatus(){
self.offset = CGSize.zero
self.scale = 1
}
init(){
resetStatus()
}
var zoomGesture: some Gesture {
MagnificationGesture()
.updating($scaleState) { currentState, gestureState, _ in
gestureState = currentState
}
.onEnded { value in
scale *= value
}
}
var dragGesture: some Gesture {
DragGesture()
.updating($offsetState) { currentState, gestureState, _ in
gestureState = currentState.translation
}.onEnded { value in
offset.height += value.translation.height
offset.width += value.translation.width
}
}
var doubleTapGesture : some Gesture {
TapGesture(count: 2).onEnded { value in
resetStatus()
}
}
var body: some View {
Image(systemName: "paperplane")
.renderingMode(.template)
.resizable()
.foregroundColor(.red)
.scaledToFit()
.scaleEffect(self.scale * scaleState)
.offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
.gesture(SimultaneousGesture(zoomGesture, dragGesture))
.gesture(doubleTapGesture)
}
}
Run Code Online (Sandbox Code Playgroud)
为了您的方便,这里有一个 GIST: https: //gist.github.com/ingconti/124d549e2671fd91d86144bc222d171a
SwiftUI 的 ScrollView 似乎没有本机支持,但是,仍然有一种非常简单的方法可以做到。
创建一个MagnificationGesture你想要的样子,但一定要把你当前的比例乘以你在手势的.onChanged闭包中得到的值。这个闭包让你改变缩放而不是当前的比例值。
当您缩小并开始放大时,它不会从当前比例增加(任意示例为 0.5 到 0.6),它会从 1 增加到 1.1。这就是为什么你看到奇怪的行为。
如果 位于MagnificationGesture具有.scaleEffect. 否则,詹姆斯的回答会更好。
struct ContentView: View {
@State var scale: CGFloat
var body: some View {
let gesture = MagnificationGesture(minimumScaleDelta: 0.1)
.onChanged { scaleDelta in
self.scale *= scaleDelta
}
return ScrollView {
// Your ScrollView content here :)
}
.gesture(gesture)
.scaleEffect(scale)
}
}
Run Code Online (Sandbox Code Playgroud)
PS 您可能会发现ScrollView为此目的使用 a很笨拙,并且您无法同时拖动和缩放。如果是这种情况并且您对此不满意,我会考虑添加多个手势并手动调整内容的偏移量,而不是使用ScrollView.
我的体验MagnificationGesture非常糟糕,它非常滞后并且消耗了大量的 CPU 和 RAM(就像许多其他解决方案一样)。最好的解决方案是使用基本的UIScrollView.
基于另一个解决方案,我实现了更动态的结构,允许您:
View随心所欲地使用它最重要的是- 它已经过测试,可以保证它不会耗尽您的 CPU 和 RAM!
// ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
ZoomableContainer{
// Put here any `View` you'd like (e.g. `Image`, `Text`)
}
}
}
Run Code Online (Sandbox Code Playgroud)
// ZoomableContainer.swift
import SwiftUI
fileprivate let maxAllowedScale = 4.0
struct ZoomableContainer<Content: View>: View {
let content: Content
@State private var currentScale: CGFloat = 1.0
@State private var tapLocation: CGPoint = .zero
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func doubleTapAction(location: CGPoint) {
tapLocation = location
currentScale = currentScale == 1.0 ? maxAllowedScale : 1.0
}
var body: some View {
ZoomableScrollView(scale: $currentScale, tapLocation: $tapLocation) {
content
}
.onTapGesture(count: 2, perform: doubleTapAction)
}
fileprivate struct ZoomableScrollView<Content: View>: UIViewRepresentable {
private var content: Content
@Binding private var currentScale: CGFloat
@Binding private var tapLocation: CGPoint
init(scale: Binding<CGFloat>, tapLocation: Binding<CGPoint>, @ViewBuilder content: () -> Content) {
_currentScale = scale
_tapLocation = tapLocation
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
// Setup the UIScrollView
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator // for viewForZooming(in:)
scrollView.maximumZoomScale = maxAllowedScale
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
scrollView.clipsToBounds = false
// Create a UIHostingController to hold our SwiftUI content
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: content), scale: $currentScale)
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// Update the hosting controller's SwiftUI content
context.coordinator.hostingController.rootView = content
if uiView.zoomScale > uiView.minimumZoomScale { // Scale out
uiView.setZoomScale(currentScale, animated: true)
} else if tapLocation != .zero { // Scale in to a specific point
uiView.zoom(to: zoomRect(for: uiView, scale: uiView.maximumZoomScale, center: tapLocation), animated: true)
// Reset the location to prevent scaling to it in case of a negative scale (manual pinch)
// Use the main thread to prevent unexpected behavior
DispatchQueue.main.async { tapLocation = .zero }
}
assert(context.coordinator.hostingController.view.superview == uiView)
}
// MARK: - Utils
func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect {
let scrollViewSize = scrollView.bounds.size
let width = scrollViewSize.width / scale
let height = scrollViewSize.height / scale
let x = center.x - (width / 2.0)
let y = center.y - (height / 2.0)
return CGRect(x: x, y: y, width: width, height: height)
}
// MARK: - Coordinator
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>
@Binding var currentScale: CGFloat
init(hostingController: UIHostingController<Content>, scale: Binding<CGFloat>) {
self.hostingController = hostingController
_currentScale = scale
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
currentScale = scale
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
我也在为这个问题而挣扎。但是使用此视频制作了一些工作示例-(https://www.youtube.com/watch?v=p0SwXJYJp2U)
这还没有完成。很难用锚点进行缩放。希望这是对其他人的暗示。
struct ContentView: View {
let maxScale: CGFloat = 3.0
let minScale: CGFloat = 1.0
@State var lastValue: CGFloat = 1.0
@State var scale: CGFloat = 1.0
@State var draged: CGSize = .zero
@State var prevDraged: CGSize = .zero
@State var tapPoint: CGPoint = .zero
@State var isTapped: Bool = false
var body: some View {
let magnify = MagnificationGesture(minimumScaleDelta: 0.2)
.onChanged { value in
let resolvedDelta = value / self.lastValue
self.lastValue = value
let newScale = self.scale * resolvedDelta
self.scale = min(self.maxScale, max(self.minScale, newScale))
print("delta=\(value) resolvedDelta=\(resolvedDelta) newScale=\(newScale)")
}
let gestureDrag = DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { (value) in
self.tapPoint = value.startLocation
self.draged = CGSize(width: value.translation.width + self.prevDraged.width,
height: value.translation.height + self.prevDraged.height)
}
return GeometryReader { geo in
Image("dooli")
.resizable().scaledToFit().animation(.default)
.offset(self.draged)
.scaleEffect(self.scale)
// .scaleEffect(self.isTapped ? 2 : 1,
// anchor: UnitPoint(x: self.tapPoint.x / geo.frame(in: .local).maxX,
// y: self.tapPoint.y / geo.frame(in: .local).maxY))
.gesture(
TapGesture(count: 2).onEnded({
self.isTapped.toggle()
if self.scale > 1 {
self.scale = 1
} else {
self.scale = 2
}
let parent = geo.frame(in: .local)
self.postArranging(translation: CGSize.zero, in: parent)
})
.simultaneously(with: gestureDrag.onEnded({ (value) in
let parent = geo.frame(in: .local)
self.postArranging(translation: value.translation, in: parent)
})
))
.gesture(magnify.onEnded { value in
// without this the next gesture will be broken
self.lastValue = 1.0
let parent = geo.frame(in: .local)
self.postArranging(translation: CGSize.zero, in: parent)
})
}
.frame(height: 300)
.clipped()
.background(Color.gray)
}
private func postArranging(translation: CGSize, in parent: CGRect) {
let scaled = self.scale
let parentWidth = parent.maxX
let parentHeight = parent.maxY
let offset = CGSize(width: (parentWidth * scaled - parentWidth) / 2,
height: (parentHeight * scaled - parentHeight) / 2)
print(offset)
var resolved = CGSize()
let newDraged = CGSize(width: self.draged.width * scaled,
height: self.draged.height * scaled)
if newDraged.width > offset.width {
resolved.width = offset.width / scaled
} else if newDraged.width < -offset.width {
resolved.width = -offset.width / scaled
} else {
resolved.width = translation.width + self.prevDraged.width
}
if newDraged.height > offset.height {
resolved.height = offset.height / scaled
} else if newDraged.height < -offset.height {
resolved.height = -offset.height / scaled
} else {
resolved.height = translation.height + self.prevDraged.height
}
self.draged = resolved
self.prevDraged = resolved
}
}
Run Code Online (Sandbox Code Playgroud)
这是 @James 接受的响应的完整示例,它还具有通过调整隐藏矩形来滚动新缩放图像的基本支持,该隐藏矩形根据图像比例调整滚动视图内容的大小:
import SwiftUI
struct EnlargedImage: View {
var image = UIImage(named: "YourImageName")
@State var scale: CGFloat = 1.0
@State var lastScaleValue: CGFloat = 1.0
var body: some View {
ScrollView([.vertical, .horizontal], showsIndicators: false){
ZStack{
Rectangle().foregroundColor(.clear).frame(width: image!.size.width * scale, height: image!.size.height * scale, alignment: .center)
Image(uiImage: image!).scaleEffect(scale)
.gesture(MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
var newScale = self.scale * delta
if newScale < 1.0
{
newScale = 1.0
}
scale = newScale
}.onEnded{val in
lastScaleValue = 1
})
}
}.background(Color(.systemBackground).edgesIgnoringSafeArea(.all))
}
}
Run Code Online (Sandbox Code Playgroud)
我的 GitHub中有一个更好的版本。
这是另一个解决方案,基于 jtbandes 的答案。它仍然将 a 包裹UIScrollView在 a 中UIViewRepresentable,但有一些更改:
UIImageSwiftUI 内容,而不是通用的 SwiftUI 内容:它适用于这种情况,并且不需要包装底层UIImage到 SwiftUI 中Image使用:
struct EncompassingView: View {
let uiImage: UIImage
var body: some View {
GeometryReader { geometry in
ZoomableView(uiImage: uiImage, viewSize: geometry.size)
}
}
}
Run Code Online (Sandbox Code Playgroud)
定义:
struct ZoomableView: UIViewRepresentable {
let uiImage: UIImage
let viewSize: CGSize
private enum Constraint: String {
case top
case leading
}
private var minimumZoomScale: CGFloat {
let widthScale = viewSize.width / uiImage.size.width
let heightScale = viewSize.height / uiImage.size.height
return min(widthScale, heightScale)
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = minimumZoomScale * 50
scrollView.minimumZoomScale = minimumZoomScale
scrollView.bouncesZoom = true
let imageView = UIImageView(image: uiImage)
scrollView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
let topConstraint = imageView.topAnchor.constraint(equalTo: scrollView.topAnchor)
topConstraint.identifier = Constraint.top.rawValue
topConstraint.isActive = true
let leadingConstraint = imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
leadingConstraint.identifier = Constraint.leading.rawValue
leadingConstraint.isActive = true
imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func updateUIView(_ scrollView: UIScrollView, context: Context) {
guard let imageView = scrollView.subviews.first as? UIImageView else {
return
}
// Inject dependencies into coordinator
context.coordinator.zoomableView = imageView
context.coordinator.imageSize = uiImage.size
context.coordinator.viewSize = viewSize
let topConstraint = scrollView.constraints.first { $0.identifier == Constraint.top.rawValue }
let leadingConstraint = scrollView.constraints.first { $0.identifier == Constraint.leading.rawValue }
context.coordinator.topConstraint = topConstraint
context.coordinator.leadingConstraint = leadingConstraint
// Set initial zoom scale
scrollView.zoomScale = minimumZoomScale
}
}
// MARK: - Coordinator
extension ZoomableView {
class Coordinator: NSObject, UIScrollViewDelegate {
var zoomableView: UIView?
var imageSize: CGSize?
var viewSize: CGSize?
var topConstraint: NSLayoutConstraint?
var leadingConstraint: NSLayoutConstraint?
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
zoomableView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
let zoomScale = scrollView.zoomScale
print("zoomScale = \(zoomScale)")
guard
let topConstraint = topConstraint,
let leadingConstraint = leadingConstraint,
let imageSize = imageSize,
let viewSize = viewSize
else {
return
}
topConstraint.constant = max((viewSize.height - (imageSize.height * zoomScale)) / 2.0, 0.0)
leadingConstraint.constant = max((viewSize.width - (imageSize.width * zoomScale)) / 2.0, 0.0)
}
}
}
Run Code Online (Sandbox Code Playgroud)
SwiftUI 中图像缩放和拖动的实现
struct PhotoViewer: View {
@State private var uiimage = UIImage(named: "leaf.png")
@GestureState private var scaleState: CGFloat = 1
@GestureState private var offsetState = CGSize.zero
@State private var offset = CGSize.zero
@State private var scale: CGFloat = 1
var magnification: some Gesture {
MagnificationGesture()
.updating($scaleState) { currentState, gestureState, _ in
gestureState = currentState
}
.onEnded { value in
scale *= value
}
}
var dragGesture: some Gesture {
DragGesture()
.updating($offsetState) { currentState, gestureState, _ in
gestureState = currentState.translation
}.onEnded { value in
offset.height += value.translation.height
offset.width += value.translation.width
}
}
var body: some View {
Image(uiImage: uiimage!)
.resizable()
.scaledToFit()
.scaleEffect(self.scale * scaleState)
.offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
.gesture(SimultaneousGesture(magnification, dragGesture))
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
15461 次 |
| 最近记录: |