Search code examples
macosswiftuigesturemagnification

SwiftUI: magnification gesture that changes value between min and max


I want to make the same thing that is possible to do with a slider:

@State itemSize: CGFloat = 55.60

....

Text("ItemSize: \(itemSize)")
Slider(value: $itemSize, in: 45...120)

but with magnification gesture.

I have created modifier for this:

import SwiftUI

@available(OSX 11.0, *)
public extension View {
    func magnificationZoomer(value: Binding<CGFloat>,min: CGFloat, max: CGFloat) -> some View
    {
         self.modifier( MagnificationZoomerMod(minZoom: min, maxZoom: max, zoom: value) )
    }
}

@available(OSX 11.0, *)
public struct MagnificationZoomerMod: ViewModifier {
    let minZoom: CGFloat
    let maxZoom: CGFloat
    @Binding var zoom: CGFloat
    
    public func body (content: Content) -> some View
    {
        content
            .gesture(
                MagnificationGesture()
                    .onChanged() { val in
                        let magnification = (val - 1) * 2.2
                        
                        print("magnification = \(magnification)")
                        
                        zoom = max(min(zoom + magnification, maxZoom), minZoom)
                    }
            )
    }
}

sample of usage:

@State itemSize: CGFloat = 55.60

var body: some View {
    HStack {
       VStack {
           Text("ItemSize: \(itemSize)")
           Slider(value: $itemSize, in: 45...120)
       }
    }
    .frame(width: 500, height: 500)
    .magnificationZoomer(value: $itemSize, min: 45, max: 120)
}

But I have a few problems with this code:

  1. It slide value by a strange way -- sometimes with correct speed, it does not change it
  2. after some time of usage it stop to work at all

What did I do wrong?


Solution

  • It works almost like a magic, you can change the size/scale of Circle via Slider or your finger on MagnificationGesture, both working together and sinked together.


    enter image description here


    enter image description here


    import SwiftUI
    
    struct ContentView: View {
        
        let minZoom: CGFloat = 0.5
        let maxZoom: CGFloat = 1.5
        
        @State private var scale: CGFloat = 1.0
        @State private var lastScale: CGFloat = 1.0
        @State private var magnitudeIsActive: Bool = Bool()
        
        var body: some View {
            
            ZStack {
                
                Circle()
                    .fill(Color.red)
                    .scaleEffect(scale)
                    .gesture(magnificationGesture.simultaneously(with: dragGesture)) // <<: Here: adding unneeded dragGesture! on macOS! no need on iOS!
                
                VStack {
                    
                    Spacer()
                    
                    Text("scale: " + scale.rounded)
                    
                    HStack {
                        
                        Button("min") { scale = minZoom; lastScale = scale }
                        
                        Slider(value: Binding.init(get: { () -> CGFloat in return scale },
                                                   set: { (newValue) in if !magnitudeIsActive { scale = newValue; lastScale = newValue } }), in: minZoom...maxZoom)
    
                        Button("max") { scale = maxZoom; lastScale = scale }
                    }
                    
                }
                
            }
            .padding()
            .compositingGroup()
            .shadow(radius: 10.0)
            .animation(Animation.easeInOut(duration: 0.2), value: scale)
    
            
        }
    
        var magnificationGesture: some Gesture {
            
            MagnificationGesture(minimumScaleDelta: 0.0)
                .onChanged { value in
                    
                    if !magnitudeIsActive { magnitudeIsActive = true }
                    
                    let magnification = (lastScale + value.magnitude - 1.0)
                    
                    if (magnification >= minZoom && magnification <= maxZoom) {
                        
                        scale = magnification
                        
                    }
                    else if (magnification < minZoom) {
                        
                        scale = minZoom
                    }
                    else if (magnification > maxZoom) {
                        
                        scale = maxZoom
                    }
                    
                }
                .onEnded { value in
                    
                    let magnification = (lastScale + value.magnitude - 1.0)
                    
                    if (magnification >= minZoom && magnification <= maxZoom) {
                        
                        lastScale = magnification
                        
                    }
                    else if (magnification < minZoom) {
                        
                        lastScale = minZoom
                    }
                    else if (magnification > maxZoom) {
                        
                        lastScale = maxZoom
                    }
                    
                    scale = lastScale
                    magnitudeIsActive = false
                    
                }
            
        }
        
        
        var dragGesture: some Gesture { DragGesture(minimumDistance: 0.0) } // <<: Here: this Extra un-needed gesture would keep magnificationGesture alive! And Stop it to get killed in macOS! We do not need this line of code in iOS!
        
    }
    

    extension CGFloat { var rounded: String { get { return String(Double(self*100.0).rounded()/100.0) } } }