Home > Software design >  SwiftUI What exactly happens when use .offset and .position Modifier simultaneously on a View, which
SwiftUI What exactly happens when use .offset and .position Modifier simultaneously on a View, which

Time:05-27

Here I have this question when I try to give a View an initial position, then user can use drag gesture to change the location of the View to anywhere. Although I already solved the issue by only using .position(x:y:) on a View, at the beginning I was thinking using .position(x:y:) to give initial position and .offset(offset:) to make the View move with gesture, simultaneously. Now, I really just want to know in more detail, what exactly happens when I use both of them the same time (the code below), so I can explain what happens in the View below.

What I cannot explain in the View below is that: when I simply drag gesture on the VStack box, it works as expected and the VStack moves with finger gesture, however, once the gesture ends and try to start a new drag gesture on the VStack, the VStack box goes back to the original position suddenly (like jumping to the original position when the code is loaded), then start moving with the gesture. Note that the gesture is moving as regular gesture, but the VStack already jumped to a different position so it starts moving from a different position. And this causes that the finger tip is no long on top of the VStack box, but off for some distance, although the VStack moves with the same trajectory as drag gesture does.

My question is: why the .position(x:y:) modifier seems only take effect at the very beginning of each new drag gesture detected, but during the drag gesture action on it seems .offset(offset:) dominates the main movement and the VStack stops at where it was dragged to. But once new drag gesture is on, the VStack jumps suddenly to the original position. I just could not wrap my head around how this behavior happens through timeline. Can somebody provide some insights?

Note that I already solved the issue to achieve what I need, right now it's just to understand what is exactly going on when .position(x:y:) and .offset(offset:) are used the same time, so please avoid some advice like. not use them simultaneously, thank you. The code bellow suppose to be runnable after copy and paste, if not pardon me for making mistake as I delete few lines to make it cleaner to reproduce the issue.

import SwiftUI

struct ContentView: View {

    var body: some View {

        ButtonsViewOffset()
    }
}

struct ButtonsViewOffset: View {

    let location: CGPoint = CGPoint(x: 50, y: 50)
    @State private var offset = CGSize.zero
    @State private var color = Color.purple

    var dragGesture: some Gesture {
        DragGesture()
            .onChanged{ value in
                self.offset = value.translation
                print("offset onChange: \(offset)")
            }
            .onEnded{ _ in
                if self.color == Color.purple{
                    self.color = Color.blue
                }
                else{
                    self.color = Color.purple
                }

            }
    }
    
    var body: some View {
        VStack {
            Text("Watch 3-1")
            Text("x: \(self.location.x), y: \(self.location.y)")
        }
        .background(Color.gray)
        
        .foregroundColor(self.color)
        .offset(self.offset)
        .position(x: self.location.x, y: self.location.y)
        .gesture(dragGesture)
        
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
        }
            
    }
}

CodePudding user response:

Your issue has nothing to do with the use of position and offset. They actually both work simultaneously. Position sets the absolute position of the view, where as offset moves it relative to the absolute position. Therefore, you will notice that your view starts at position (50, 50) on the screen, and then you can drag it all around. Once you let go, it stops wherever it was. So far, so good. You then want to move it around again, and it pops back to the original position. The reason it does that is the way you set up location as a let constant. It needs to be state.

The problem stems from the fact that you are adding, without realizing it, the values of offset to position. When you finish your drag, offset retains the last values. However, when you start your next drag, those values start at (0,0) again, therefore the offset is reset to (0,0) and the view moves back to the original position. The key is that you need to use just the position or update the the offset in .onEnded. Don't use both. Here you have a set position, and are not saving the offset. How you handle it depends upon the purpose for which you are moving the view.

First, just use .position():

struct OffsetAndPositionView: View {
    
    @State private var position = CGPoint(x: 50, y: 50)
    @State private var color = Color.purple
    
    var dragGesture: some Gesture {
        DragGesture()
            .onChanged{ value in
                position = value.location
                print("position onChange: \(position)")
            }
            .onEnded{ value in
                if color == Color.purple{
                    color = Color.blue
                }
                else{
                    color = Color.purple
                }
            }
    }
    
    var body: some View {
        Rectangle()
            .fill(color)
            .frame(width: 30, height: 30)
            .position(position)
            .gesture(dragGesture)
    }
}

Second, just use .offset():

struct ButtonsViewOffset: View {
    @State private var savedOffset = CGSize.zero
    @State private var dragValue = CGSize.zero
    @State private var color = Color.purple
    
    var offset: CGSize {
        savedOffset   dragValue
    }

    var dragGesture: some Gesture {
        DragGesture()
            .onChanged{ value in
                dragValue = value.translation
                print("dragValue onChange: \(dragValue)")
            }
            .onEnded{ value in
                savedOffset = savedOffset   value.translation
                dragValue = CGSize.zero
                if color == Color.purple{
                    color = Color.blue
                }
                else{
                    color = Color.purple
                }
            }
    }

    var body: some View {
            Rectangle()
                .fill(color)
                .frame(width: 30, height: 30)
                .offset(offset)
                .gesture(dragGesture)
    }
}

// Convenience operator overload
func   (lhs: CGSize, rhs: CGSize) -> CGSize {
    return CGSize(width: lhs.width   rhs.width, height: lhs.height   rhs.height)
}
  • Related