Duck
Duck

Reputation: 35993

How do I create this effect using SwiftUI

See this gif

enter image description here

What is happening on this gif is: the finger touches the white area on any point on the right part and drags to the left. As the finger drags, these 3 buttons zoom in and appear.

Assuming the buttons zoom from scale = 0 to scale = 1, Does not matter if I release the finger when the scale is at any value bigger than 0. The buttons will zoom to scale 1 automatically.

NOTE: The animation is slow because I am sliding the finger slowly, but the animation follows the finger drag. If I drag left, buttons zoom in, if I drag right, buttons zoom out.

How do I do that with SwiftUI.

I have this code so far for the whole thing.

struct FileManagerPanelListItem: View {
  var body: some View {
    
    ZStack{
      
      VStack {
        
        ZStack {
          Image("image")
            
            .resizable()
            .frame(width: 190, height: 190, alignment: .center)

          
          HStack(alignment:.center){
            FileManagerPanelButton("share", Color.white, Color.gray, {})
            FileManagerPanelButton("duplicate", Color.white, Color.gray, {})
            FileManagerPanelButton("delete", Color.white, Color.red, {})
          }
          .frame(maxWidth:.infinity)
        }
        

        
        Text("name")
          .frame(width:170)
          .background(Color.yellow)
          .lineLimit(2)
          .fixedSize(horizontal: false, vertical: true)

        Text("notes")
          .frame(width:170)
          .background(Color.blue)
          .lineLimit(2)
          .fixedSize(horizontal: false, vertical: true)
        
      }
      


    }
    .frame(maxWidth:240, maxHeight: 240)

    
  }
}

struct FileManagerPanelListItem_Previews: PreviewProvider {
    static var previews: some View {
        FileManagerPanelListItem()
          .previewDevice(PreviewDevice(rawValue: "iPhone 12 Pro Mini"))
    }
}

struct FileManagerPanelButton: View {
  
  typealias runOnSelectHandler = ()->Void
  private var runOnSelect:runOnSelectHandler?
  private var label:String
  private var fgColor:Color
  private var bgColor:Color

  
  init(_ label: String,
       _ fgColor:Color,
       _ bgColor:Color,
       _ runOnSelect: runOnSelectHandler? ) {
    self.label = label
    self.fgColor = fgColor
    self.bgColor = bgColor
    self.runOnSelect = runOnSelect
  }
  
  var body: some View {
  
    Button(action: {
      runOnSelect?()
    }, label: {
      Text(label)
        .avenir(.ROMAN, size: 16)
        .foregroundColor(fgColor)
        .frame(height:60)
        .frame(maxWidth:.infinity)
    })
    .frame(maxWidth:.infinity)
    .background(bgColor)
    .cornerRadius(10)
    
  }
}

any ideas?

Upvotes: 1

Views: 116

Answers (1)

Phil Dukhov
Phil Dukhov

Reputation: 88024

Something like this should work:

// view cannot be scaled to zero, so we take some small value near
private let minScale: CGFloat = 0.001

// relative distance from right side of view to open and from left side to close
private let effectiveDragSidePart: CGFloat = 0.1

struct FileManagerPanelListItem: View {
    @State
    var scale: CGFloat = minScale
    @State
    var opened = false
    
    var body: some View {
        ZStack{
            VStack {
                ZStack {
                    Image("profile")
                        .resizable()
                        .frame(width: viewWidth, height: 190, alignment: .center)
                        .gesture(
                            DragGesture()
                                .onChanged { value in
                                    guard shouldProcessStartLocation(value.startLocation) else { return }
                                    scale = translationToScale(value.translation)
                                }
                                .onEnded { value in
                                    guard shouldProcessStartLocation(value.startLocation) else { return }
                                    withAnimation(.spring()) {
                                        if shouldRestore(value.predictedEndTranslation) {
                                            scale = opened ? 1 : minScale
                                        } else {
                                            scale = opened ? minScale : 1
                                            opened.toggle()
                                        }
                                    }
                                }
                        )
                    
                    HStack(alignment:.center){
                        FileManagerPanelButton("share", Color.white, Color.gray, {})
                            .scaleEffect(scale, anchor: .center)
                        FileManagerPanelButton("duplicate", Color.white, Color.gray, {})
                            .scaleEffect(scale, anchor: .center)
                        FileManagerPanelButton("delete", Color.white, Color.red, {})
                            .scaleEffect(scale, anchor: .center)
                    }
                    .frame(maxWidth:.infinity)
                }
            }
        }
        .frame(maxWidth:240, maxHeight: 240)
    }
    
    private let viewWidth: CGFloat = 190
    
    private func shouldRestore(_ predictedEndTranslation: CGSize) -> Bool {
        if opened {
            return translationToScale(predictedEndTranslation) == 1
        } else {
            return translationToScale(predictedEndTranslation) == minScale
        }
    }
    
    private func shouldProcessStartLocation(_ startLocation: CGPoint) -> Bool {
        if opened {
            return startLocation.x < viewWidth * effectiveDragSidePart
        } else {
            return startLocation.x > viewWidth * (1 - effectiveDragSidePart)
        }
    }
    
    private func translationToScale(_ translation: CGSize) -> CGFloat {
        if opened {
            return max(0.001, min(1, 1 - translation.width / viewWidth))
        } else {
            return max(0.001, -translation.width / viewWidth)
        }
    }
}

You can setup .spring() if the default one is not springy enough

Upvotes: 1

Related Questions