DragGesture: Get rid of temporary state (SwiftUI) - ios

While the following code works (shows and hides a sidebar through a drag gesture), I would like to get rid of tempOffsetX if possible. Since I cannot temporarily save offsetX to a variable when the drag gesture begins, I need to save it to this additional temp state when the drag gesture ended, so it will be available for the next drag gesture. Is there a more elegant way to achieve this without tempOffsetX?
struct ContentView: View {
#State var offsetX: CGFloat = 0
#State var tempOffsetX: CGFloat = 0
var body: some View {
ZStack {
sidebar
.offset(x: offsetX)
.gesture(
DragGesture()
.onChanged { gesture in
offsetX = min(gesture.translation.width + tempOffsetX, 0)
}
.onEnded { _ in
withAnimation(.easeOut(duration: 0.15)) {
if offsetX < -150 {
offsetX = -300
}
else {
offsetX = 0
}
}
tempOffsetX = offsetX
}
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
var sidebar: some View {
Text("Sidebar")
.frame(width: 300)
.frame(maxHeight: .infinity)
.background(Color.gray.opacity(0.4))
}
}

Related

Swift UI - UltraThinMaterial Glitch on Moving Views

When I move a view with ultra thin material background, it turns black. Is it a bug or am I doing something wrong?
Is there a workaround to achieve this view on a moving view?
I noticed only happens when there is angular motion. If I delete rotation effect the problem goes away.
Testable code:
struct Test: View {
#State var offset: CGFloat = 0
#GestureState var isDragging: Bool = false
var body: some View {
GeometryReader { reader in
ZStack {
Image(systemName: "circle.fill")
.font(.largeTitle)
.frame(width: 300, height: 300)
.background(.red)
.overlay(alignment: .bottom) {
Rectangle()
.frame(height: 75)
.background(.ultraThinMaterial)
}
.clipShape(
RoundedRectangle(cornerRadius: 15, style: .continuous)
)
.compositingGroup()
.offset(x: offset)
.rotationEffect(.degrees(getRotation(angle: 8)))
.compositingGroup()
.gesture(
DragGesture()
.updating($isDragging) { _, state, _ in
state = true
}
.onChanged { value in
let translation = value.translation.width
offset = (isDragging ? translation : .zero)
}
.onEnded { value in
let width = getRect().width
let translation = value.translation.width
let checkingStatus = translation > 0 ? translation : -translation
withAnimation {
if checkingStatus > (width / 2) {
offset = (translation > 0 ? width : -width) * 2
} else {
offset = 0
}
}
}
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func getRotation(angle: Double) -> Double {
let rotation = (offset / getRect().width) * angle
return rotation
}
}
Code is not testable so hard to say definitely, but
try to move all that image with modifiers into separated standalone view
try composite it
.clipShape(
RoundedRectangle(cornerRadius: 15, style: .continuous)
)
.compositingGroup() // << here !!
.offset(y: -topOffset)
For me, just adding .compositingGroup() modifier changed nothing, I still had a glitch
In my case, I also use blur, shadows and other modifiers as well as rotationEffect. Sure, the glitch is connected with rotation
I managed to solve the problem via changing the order or view modifiers. Make sure that rotationEffect(_:axes:) is used before others

How to create dragable view from top in SwiftUI?

I am trying to create dragable view from top which is kind of overlay .. I am attaching something same but having bottom sheet but i am looking for Top sheet. When overlay come background should be blur.
I am not sure about how to implement hence dont have code..I tried parent child for List in SwiftUI but that is having diff. Behaviour.
Any example or suggestion is appropriated.
something like this?
struct SheetView: View {
let title: String
var body: some View {
NavigationView {
List {
ForEach(0..<30) { item in
Text("Item \(item)")
}
}
.listStyle(.plain)
.navigationTitle(title)
}
}
}
struct ContentView: View {
#State private var currentDrag: CGFloat = 0
#State private var stateDrag: CGFloat = -300
let minDrag: CGFloat = -UIScreen.main.bounds.height + 120
var body: some View {
ZStack {
VStack(spacing: 0) {
Spacer(minLength: 36)
SheetView(title: "Bottom Sheet")
.background(.primary)
.opacity(stateDrag + currentDrag > minDrag ? 0.5 : 1)
.blur(radius: stateDrag + currentDrag > minDrag ? 5 : 0)
}
VStack(spacing: 0) {
SheetView(title: "Top Sheet")
.background(.background)
.offset(x: 0, y: stateDrag + currentDrag)
Image(systemName: "line.3.horizontal").foregroundColor(.secondary)
.padding()
.frame(maxWidth: .infinity)
.background(.background)
.contentShape(Rectangle())
.offset(x: 0, y: stateDrag + currentDrag)
.gesture(DragGesture()
.onChanged { value in
withAnimation {
currentDrag = value.translation.height
}
}
.onEnded { value in
stateDrag += value.translation.height
stateDrag = max(stateDrag, minDrag)
currentDrag = .zero
})
}
}
}
}

SwiftUI - Custom Snap Scroll With Offset

I want to create a custom scroll that snaps views to middle
But I can't figure out how to set the offset correctly.
This is the sample of the scroll:
struct contentView: View {
#State var startOffset: CGFloat = 0
#State var translationWidth: CGFloat = 0
#State var offset: CGFloat = 0
#State var index: Int = 0
var body: some View {
GeometryReader { geo in
HStack {
ForEach(0..<5, id: \.self) { num in
Button(action: {
// some action
}) {
Circle()
}
.frame(width: geo.size.width)
.offset(x: -self.offset*CGFloat(self.index) + self.translationWidth)
}
}.frame(width: geo.size.width, height: 200)
.offset(x: self.startOffset )
.highPriorityGesture (
DragGesture(minimumDistance: 0.1, coordinateSpace: .global)
.onChanged({ (value) in
self.translationWidth = value.translation.width
})
.onEnded({ (value) in
if self.translationWidth > 40 {
self.index -= 1
} else if self.translationWidth < -40 {
self.index += 1
}
self.translationWidth = 0
})
)
.onAppear {
withAnimation(.none) {
let itemsWidth: CGFloat = CGFloat(geo.size.width*5) - (geo.size.width)
self.offset = geo.size.width
self.startOffset = itemsWidth/2
}
}
}
}
}
It works but it is slightly off the middle, it doesn't scroll exactly in the middle and I don't understand why.
The problem occurs with the onAppear closure:
.onAppear {
withAnimation(.none) {
let itemsWidth: CGFloat = CGFloat(geo.size.width*5) - (geo.size.width)
self.offset = geo.size.width
self.startOffset = itemsWidth/2
}
}
I might be missing some small pixels in my calculations.
UPDATE
So Apparently the HStack has a default value for spacing between views.
so to fix it you should remove it like so:
HStack(spacing: 0) {
....
}
So after a bit of experimenting, I realized the HStack is using a default spacing which is not equal to zero.
It means that in my calculations I didn't include the spacing between items
so to fix this all you need to add is:
HStack(spacing: 0) {
...
}

SwiftUI Can't PanGesture an Image

I have not been able to find an equivalent to the pan gesture in SwiftUI. I do see
and use magnify, tap, drag and rotate - but I do not see any built in pan. In the
following code snippet I add an image and allow the user to zoom - but I want the
user to also move the zoomed image to focus on the area of interest. Dragging, of
course does not do the job - it just moves the frame.
I tried layering a frame on top and moving the bottom image but could not make that
work either.
struct ContentView: View {
#State var scale: CGFloat = 1.0
#State var isScaled: Bool = false
#State private var dragOffset = CGSize.zero
var body: some View {
GeometryReader { geo in
VStack {
ZStack{
RoundedRectangle(cornerRadius: 40)
.foregroundColor(Color.white)
.frame(width: geo.size.width - 45, height: geo.size.width - 45)
.shadow(radius: 10)
Image("HuckALaHuckMedium")
.resizable()
.scaleEffect(self.scale)
.frame(width: geo.size.width - 60, height: geo.size.width - 60)
.cornerRadius(40)
.aspectRatio(contentMode: .fill)
.shadow(radius: 10, x: 20, y: 20)
//need pan not drag
.gesture(
DragGesture()
.onChanged { self.dragOffset = $0.translation }
.onEnded { _ in self.dragOffset = .zero }
)
//this works but you can't "zoom twice"
.gesture(MagnificationGesture()
.onChanged { value in
self.scale = self.isScaled ? 1.0 : value.magnitude
}
.onEnded({ value in
//self.scale = 1.0
self.isScaled.toggle()
})
)
.animation(.easeInOut)
.offset(self.dragOffset)
}//zstack
Spacer()
}
}
}
}
An original image example:
And that image after zoom - but with drag not pan:
Any guidance would be appreciated. Xcode 11.3 (11C29)
To make it simpler and more readable, I created an extension/modifier for that
struct DraggableView: ViewModifier {
#State var offset = CGPoint(x: 0, y: 0)
func body(content: Content) -> some View {
content
.gesture(DragGesture(minimumDistance: 0)
.onChanged { value in
self.offset.x += value.location.x - value.startLocation.x
self.offset.y += value.location.y - value.startLocation.y
})
.offset(x: offset.x, y: offset.y)
}
}
extension View {
func draggable() -> some View {
return modifier(DraggableView())
}
}
Now all you have to do is call the modifier:
Image(systemName: "plus")
.draggable()
For others. This is really simple. Ensure that the magnification is accomplished before the drag is allowed - it will work like a pan. First define the drag gesture:
let dragGesture = DragGesture()
.onChanged { (value) in
self.translation = value.translation
}
.onEnded { (value) in
self.viewState.width += value.translation.width
self.viewState.height += value.translation.height
self.translation = .zero
}
Then create an #State value that you toggle to true after the magnification gesture has been activated. When you attach the gesture to the image, do so conditionally:
.gesture(self.canBeDragged ? dragGesture : nil)
I'm a little late, but for anyone looking, please try this as it worked for me. What you want to do is change the offset of the image right after you zoom out in your MagnificationGesture.onChanged() lifecycle.
var magnification: some Gesture {
MagnificationGesture()
.updating($magnifyBy) {
.....
}
.onChanged() { _ in
//Zoom Out value of magnifyBy < 1.0
if self.magnifyBy <= 0.6{
//Change offset of image after zoom out to center of screen
self.currentPosition = CGSize(width: 1.0, height: 1.0)
}
self.newPosition = self.currentPosition
}
.onEnded{
.....
} }
var body: some View{
Image()
.offset(x: self.currentPosition.width, y: self.currentPosition.height)}
Anyone questions please let me know

SwiftUI: top-anchoring resizable views in a scroll view

How do I anchor resizing views in a scrollview to the top-leading point so that when they grow, they grow downwards?
Context: I'm trying to make a scrollviews of views that I can resize in height by dragging a handle in the bottom of it up and down. My problem is that when it resizes, it resizes equally much up as down. I want the top to stay put and only adjust how far down it goes.
I don't think the problem lies with the scrollview, as the behaviour is the same if I replace it with a VStack. In the context of the scrollview, though, it resizing upwards makes the user not able to scroll up far enough to see the top of the view.
Full sample code follows under the screenshots. The issue is on both iPad and iPhone simulator
In the following screenshots, the scrollview is scrolled to the top in both. The first screenshot shows the start-state before resizing the topmost item
The second screenshot shows the state after resizing the topmost item - the topmost item now goes outside the list so we cannot see scroll up to see the top
Here follows the full code, runnable with Xcode 11.0, to show the issue
struct ScaleItem: View {
static let defaultHeight: CGFloat = 240.0
#State var heightDiff: CGFloat = 0.0
#State var currentHeight: CGFloat = ScaleItem.defaultHeight
var resizingButton: some View {
VStack {
VStack {
Spacer(minLength: 15)
HStack {
Spacer()
Image(systemName: "arrow.up.and.down.square")
.background(Color.white)
Spacer()
}
}
Spacer()
.frame(height: 11)
}
.background(Color.clear)
}
var body: some View {
ZStack {
VStack {
Spacer()
HStack {
Spacer()
Text("Sample")
Spacer()
}
Spacer()
}
.background(Color.red)
.overlay(
RoundedRectangle(cornerRadius: 5.0)
.strokeBorder(Color.black, lineWidth: 1.0)
.shadow(radius: 3.0)
)
.padding()
.frame(
minHeight: self.currentHeight + heightDiff,
idealHeight: self.currentHeight + heightDiff,
maxHeight: self.currentHeight + heightDiff,
alignment: .top
)
resizingButton
.gesture(
DragGesture()
.onChanged({ gesture in
print("Changed")
let location = gesture.location
let startLocation = gesture.startLocation
let deltaY = location.y - startLocation.y
self.heightDiff = deltaY
print(deltaY)
})
.onEnded { gesture in
print("Ended")
let location = gesture.location
let startLocation = gesture.startLocation
let deltaY = location.y - startLocation.y
self.currentHeight = max(ScaleItem.defaultHeight, self.currentHeight + deltaY)
self.heightDiff = 0
print(deltaY)
print(String(describing: gesture))
})
}
}
}
struct ScaleDemoView: View {
var body: some View {
ScrollView {
ForEach(0..<3) { _ in
ScaleItem()
}
}
}
}
One way to resolve this is to redraw the ScrollView to its original position during the dragging. Create an observer object and when your DeltaY will be changed, the parent view will be notified and the view will update accordingly.
First, Create an ObservableObject final class DeltaHeight: ObservableObject & pass to the child view.
Add .offset(x: 0, y: 0) to your ScrollView
Here is the tested code:
import SwiftUI
struct ScaleDemoView_Previews: PreviewProvider {
static var previews: some View {
ScaleDemoView()
}
}
struct ScaleItem: View {
#ObservedObject var delta: DeltaHeight
static let defaultHeight: CGFloat = 200.0
#State var heightDiff: CGFloat = 0.0
#State var currentHeight: CGFloat = ScaleItem.defaultHeight
var resizingButton: some View {
VStack {
VStack {
Spacer(minLength: 15)
HStack {
Spacer()
Image(systemName: "arrow.up.and.down.square")
.background(Color.white)
Spacer()
}
}
Spacer()
.frame(height: 11)
}
.background(Color.clear)
}
var body: some View {
ZStack {
VStack {
Spacer()
HStack {
Spacer()
Text("Sample")
Spacer()
}
Spacer()
}
.background(Color.red)
.overlay(
RoundedRectangle(cornerRadius: 5.0)
.strokeBorder(Color.black, lineWidth: 1.0)
.shadow(radius: 3.0)
)
.padding()
.frame(
minHeight: self.currentHeight + heightDiff,
idealHeight: self.currentHeight + heightDiff,
maxHeight: self.currentHeight + heightDiff,
alignment: .top
)
resizingButton
.gesture(
DragGesture()
.onChanged({ gesture in
print("Changed")
let location = gesture.location
let startLocation = gesture.startLocation
let deltaY = location.y - startLocation.y
self.heightDiff = deltaY
print("deltaY: ", deltaY)
self.delta.delta = deltaY
})
.onEnded { gesture in
print("Ended")
let location = gesture.location
let startLocation = gesture.startLocation
let deltaY = location.y - startLocation.y
self.currentHeight = max(ScaleItem.defaultHeight, self.currentHeight + deltaY)
self.heightDiff = 0
print(deltaY)
print(String(describing: gesture))
})
}
}
}
struct ScaleDemoView: View {
#ObservedObject var delta = DeltaHeight()
var body: some View {
ScrollView(.vertical) {
ForEach(0..<3) { _ in
ScaleItem(delta: self.delta)
}
}.offset(x: 0, y: 0)
.background(Color.green)
}
}
final class DeltaHeight: ObservableObject {
#Published var delta: CGFloat = 0.0
}

Resources