This question already has an answer here:
SwiftUI: Error in dragging logic or is this a bug?
(1 answer)
Closed 1 year ago.
I want to drag one view within a ZStack. In this example code the Toolbar has a blue draggable area. It should move Toolbar around the screen, by setting the offset. Instead, it goes into an infinite loop. How can it be fixed to drag correctly?
It prints the following eternally:
changed (36.5, 27.0)
changed (0.0, 0.0)
extension CGPoint {
func toSize() -> CGSize { .init(width:x, height:y) }
}
struct ContentView: View {
#State var offset: CGSize = .zero
var body: some View {
ZStack(alignment: .topLeading) {
Toolbar(dragOffset: $offset)
.offset(offset)
Text(verbatim: "Offset: \(offset)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct Toolbar: View {
#Binding var dragOffset: CGSize
var body: some View {
HStack {
Color.blue.frame(width: 40, height: 40)
.gesture(drag)
Color.green.frame(width: 40, height: 40)
Color.red.frame(width: 40, height: 40)
}
}
var drag: some Gesture {
DragGesture()
.onChanged { value in
print("changed \(value.location)")
dragOffset = value.location.toSize()
}
.onEnded { value in
print("ended")
}
}
}
I needed to specify a CoordinateSpace, because the default is .local. Using .global works. A named space should also work.
It can also be helpful to record the initial offset in a #State variable and then set the new values using that starting value plus the drag's translation.
#State private var dragStartOffset: CGSize? = nil
...
DragGesture(coordinateSpace: .global)
.onChanged { value in
if dragStartOffset == nil {
dragStartOffset = dragOffset
}
dragOffset = dragStartOffset! + value.translation
}
.onEnded { _ in
dragStartOffset = nil
}
You should apply the offset before the gesture.
This ensures you don't create an infinite loop where you drag on the screen, the offset changes, which then changes the position of the view again. This means your gesture offset has now changed, hence creating an infinite loop.
Changes:
struct ContentView: View {
#State var offset: CGSize = .zero
var body: some View {
ZStack(alignment: .topLeading) {
Toolbar(dragOffset: $offset)
// .offset(offset) // <- REMOVE THIS
Text(verbatim: "Offset: \(offset)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct Toolbar: View {
#Binding var dragOffset: CGSize
var body: some View {
HStack {
Color.blue.frame(width: 40, height: 40)
.offset(dragOffset) // <- ADD THIS
.gesture(drag)
Color.green.frame(width: 40, height: 40)
.offset(dragOffset) // <- ADD THIS
Color.red.frame(width: 40, height: 40)
.offset(dragOffset) // <- ADD THIS
}
}
/* ... */
}
Related
I try to make my ScrollView fixed in a specific place, but the scrollTo method will cause the application to crash.
How to make the ScrollView stay in a fixed place?
I want to control the switching of views by MagnificationGesture to switch from one view to another.
But when Scroll() disappdars, the app crashs.
struct ContentView: View {
#State var tabCount:Int = 1
#State var current:CGFloat = 1
#State var final:CGFloat = 1
var body: some View {
let magni = MagnificationGesture()
.onChanged(){ value in
current = value
}
.onEnded { value in
if current > 2 {
self.tabCount += 1
}
final = current + final
current = 0
}
VStack {
VStack {
Button("ChangeView"){
self.tabCount += 1
}
if tabCount%2 == 0 {
Text("some text")
}else {
Scroll(current: $current)
}
}
Spacer()
HStack {
Color.blue
}
.frame(width: 600, height: 100, alignment: .bottomLeading)
}
.frame(width: 600, height: 400)
.gesture(magni)
}
}
This is ScrollView, I want it can appear and disappear. When MagnificationGesture is changing, scrollview can keep somewhere.
struct Scroll:View {
#Binding var current:CGFloat
let intRandom = Int.random(in: 1..<18)
var body: some View {
ScrollViewReader { proxy in
HStack {
Button("Foreword"){
proxy.scrollTo(9, anchor: .center)
}
}
ScrollView(.horizontal) {
HStack {
ForEach(0..<20) { item in
RoundedRectangle(cornerRadius: 25.0)
.frame(width: 100, height: 40)
.overlay(Text("\(item)").foregroundColor(.white))
.id(item)
}
}
.onChange(of: current, perform: { value in
proxy.scrollTo(13, anchor: .center)
})
}
}
}
}
i am trying to add swipe inside list cell , swipe to show more options such as Delete, archive etc .
The swipe is working just fine , but the List ( vertical scroll ) is no longer scrolling up down .
Cell Bite :
import SwiftUI
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
//Swipe to custom options ,by "Jack" this option not yet available in SwiftUI
let drag = DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged {
if (self.offset.width > 0 ){ return }
self.offset.width = $0.translation.width
}.onEnded {
if $0.translation.width < -100 {
self.offset = .init(width: -100, height: 0)
} else {
self.offset = .zero
}
}
ZStack{
Rectangle().foregroundColor(.blue).offset(x: offset.width, y: offset.height)
.gesture(drag)
.animation(.easeIn, value: offset)
Text("test").foregroundColor(.white)
}.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 100,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
struct cl_task_Previews: PreviewProvider {
static var previews: some View {
cl_task().previewLayout(.sizeThatFits)
}
}
List main view :
struct Item {
let uuid = UUID()
let value: String
}
struct w_tasks: View {
#State private var items = [Item]()
var body: some View {
ZStack {
List(self.items, id: \.uuid) {item in
cl_task()
}
.simultaneousGesture(DragGesture().onChanged({ value in
//Scrolled
}))
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
self.items.append(Item(value: "Item"))
}, label: {
Text("+")
.font(.system(size: 50))
.frame(width: 77, height: 70)
.foregroundColor(Color.white)
.padding(.bottom, 7)
})
.background(Color(hex : "#216D94"))
.cornerRadius(38.5)
.padding()
.shadow(color: Color.black.opacity(0.3),
radius: 3,
x: 3,
y: 3)
}
}
}
}
}
struct w_tasks_Previews: PreviewProvider {
static var previews: some View {
w_tasks()
}
}
I've posted my question after spending hours solving this issue as i am new to SwiftUI , any advice how to solve it ?
The solution is to give different distance for swipe, like below
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
// give 25 distance makes vertical scroll enabled !!
let drag = DragGesture(minimumDistance: 25, coordinateSpace: .local)
.onChanged {
Tested with Xcode 12.4 / iOS 14.4
I have the following code for my side menu but I'm not able to set self.offset(x:, y:) as it says that the result is unused? How can I make this work as I want the menu to be draggable to the left.
#Binding var showMenu: Bool
var body: some View {
HStack {
GeometryReader { geometry in
MenuView()
.frame(width: 240, alignment: .leading)
.background(Color.white)
.offset(x: self.showMenu ? 0 : -geometry.size.width)
.animation(.spring())
.transition(.move(edge: .leading))
.zIndex(4)
.gesture(DragGesture()
.onChanged { val in
self.offset(x: val.translation.width)
}
.onEnded { val in
self.offset(x: .zero)
}
)
}
}
}
As code is not testable, so just idea - scratchy. You cannot use self of View struct, change instead some state and use that state value in corresponding modifiers, like below
#Binding var showMenu: Bool
#State private var xOffset: CGFloat = .zero
var body: some View {
HStack {
GeometryReader { geometry in
MenuView()
.frame(width: 240, alignment: .leading)
.background(Color.white)
.offset(x: self.showMenu ? xOffset : -geometry.size.width)
.animation(.spring())
.transition(.move(edge: .leading))
.zIndex(4)
.gesture(DragGesture()
.onChanged { val in
self.xOffset = val.translation.width
}
.onEnded { val in
self.xOffset = .zero
}
)
}
}
}
I would like to be able to save and restore the position of a SwiftUI splitView but I can’t figure out how to do it. I haven’t found any examples and the documentation doesn’t have any info. I have the following:
struct ContentView: View {
var body: some View {
GeometryReader{geometry in
HSplitView(){
Rectangle().foregroundColor(.red).layoutPriority(1)
Rectangle().foregroundColor(.green).frame(minWidth:200, idealWidth: 200, maxWidth: .infinity)
}.frame(width: geometry.size.width, height: geometry.size.height)
}
}
}
Does anyone know how I can get the position of the slider so it can be saved, and also restored on startup?
Thanks!
Here's a workaround I've been using. It uses a plain HStack and a draggable view to recreate what HSplitView does. You can save then save the draggableWidth state however you want.
public struct SlideableDivider: View {
#Binding var dimension: Double
#State private var dimensionStart: Double?
public init(dimension: Binding<Double>) {
self._dimension = dimension
}
public var body: some View {
Rectangle().background(Color.gray).frame(width: 2)
.onHover { inside in
if inside {
NSCursor.resizeLeftRight.push()
} else {
NSCursor.pop()
}
}
.gesture(drag)
}
var drag: some Gesture {
DragGesture(minimumDistance: 10, coordinateSpace: CoordinateSpace.global)
.onChanged { val in
if dimensionStart == nil {
dimensionStart = dimension
}
let delta = val.location.x - val.startLocation.x
dimension = dimensionStart! + Double(delta)
}
.onEnded { val in
dimensionStart = nil
}
}
}
...
struct ContentView: View {
#AppStorage("ContentView.draggableWidth") var draggableWidth: Double = 185.0
var body: some View {
// This will be like an HSplitView
HStack(spacing: 0) {
Text("Panel 1")
.frame(width: CGFloat(draggableWidth))
.frame(maxHeight: .infinity)
.background(Color.blue.opacity(0.5))
SlideableDivider(dimension: $draggableWidth)
Text("Panel 2")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red.opacity(0.5))
}.frame(maxWidth: .infinity)
}
}
I am making a custom Picker in the SegmentedPickerStyle(). I want to have the same behaviour but when I tap on the area between the content and the border of one of the possible selections the onTapGesture does not work. When I add a blue background it does work but with a clear background it doesn't.
Working with blue background
Not working with clear background
Not working code:
import SwiftUI
struct PickerElementView<Content>: View where Content : View {
#Binding var selectedElement: Int
let content: () -> Content
#inlinable init(_ selectedElement: Binding<Int>, #ViewBuilder content: #escaping () -> Content) {
self._selectedElement = selectedElement
self.content = content
}
var body: some View {
GeometryReader { proxy in
self.content()
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
// ##################################################################
// CHANGE COLOR HERE TO BLUE TO MAKE IT WORK
// ##################################################################
.background(Color.clear)
// ##################################################################
.border(Color.yellow, width: 5)
}
}
}
struct PickerView: View {
#Environment (\.colorScheme) var colorScheme: ColorScheme
var elements: [(id: Int, view: AnyView)]
#Binding var selectedElement: Int
#State var internalSelectedElement: Int = 0
private var width: CGFloat = 220
private var height: CGFloat = 100
private var cornerRadius: CGFloat = 20
private var factor: CGFloat = 0.95
private var color = Color(UIColor.systemGray)
private var selectedColor = Color(UIColor.systemGray2)
init(_ selectedElement: Binding<Int>) {
self._selectedElement = selectedElement
self.elements = [
(id: 0, view: AnyView(PickerElementView(selectedElement) {
Text("9").font(.system(.title))
})),
(id: 1, view: AnyView(PickerElementView(selectedElement) {
Text("5").font(.system(.title))
})),
]
self.internalSelectedElement = selectedElement.wrappedValue
}
func calcXPosition() -> CGFloat {
var pos = CGFloat(-self.width * self.factor / 4)
pos += CGFloat(self.internalSelectedElement) * self.width * self.factor / 2
return pos
}
var body: some View {
ZStack {
Rectangle()
.foregroundColor(self.selectedColor)
.cornerRadius(self.cornerRadius * self.factor)
.frame(width: self.width * self.factor / CGFloat(self.elements.count), height: self.height - self.width * (1 - self.factor))
.offset(x: calcXPosition())
.animation(.easeInOut(duration: 0.2))
HStack {
ForEach(self.elements, id: \.id) { item in
item.view
.gesture(TapGesture().onEnded { _ in
print(item.id)
self.selectedElement = item.id
withAnimation {
self.internalSelectedElement = item.id
}
})
}
}
}
.frame(width: self.width, height: self.height)
.background(self.color)
.cornerRadius(self.cornerRadius)
.padding()
}
}
struct PickerView_Previews: PreviewProvider {
static var previews: some View {
PickerView(.constant(1))
}
}
Change the color where I marked it.
Does anyone know why they behave differently and how I can fix this?
The one line answer is instead of setting backgroundColor, please set contentShape for hit testing.
var body: some View {
GeometryReader { proxy in
self.content()
.fixedSize(horizontal: true, vertical: true)
.frame(minWidth: proxy.size.width, minHeight: proxy.size.height)
// ##################################################################
// CHANGE COLOR HERE TO BLUE TO MAKE IT WORK
// ##################################################################
.contentShape(Rectangle())
// ##################################################################
.border(Color.yellow, width: 5)
}
}
Transparent views are not tappable by default in SwiftUI because their content shape is zero.
You can change this behavior by using .contentShape modifier:
Color.clear
.frame(width: 300, height: 300)
.contentShape(Rectangle())
.onTapGesture { print("tapped") }
It appears to be a design decision that any Color with an opacity of 0 is untappable.
Color.clear.onTapGesture { print("tapped") } // will not print
Color.blue.opacity(0).onTapGesture { print("tapped") } // will not print
Color.blue.onTapGesture { print("tapped") } // will print
Color.blue.opacity(0.0001).onTapGesture { print("tapped") } // will print
You can use the 4th option to get around this, as it is visually indistinguishable from the 1st.
I was struggling a similar problem to get the tap on a RoundedRectangle.
My simple solution was to set the opacity to a very low value and it worked
RoundedRectangle(cornerRadius: 12)
.fill(Color.black)
.opacity(0.0001)
.frame(width: 32, height: 32)
.onTapGesture {
...
}