Determine UIScreen half in SwiftUI - swift

I have a gesture applied to my text. Right now, it is sitting at the bottom of the screen and if I drag it to anywhere and let go, it goes back to the bottom. But if I drag it to the top half of my device screen, it should stay at the top.
The feature is almost ready to go, it just needs the correct function to split using UIScreen.main.bounds.
struct SwiftUIView: View {
#State var offset: CGSize = .zero
#State var isOnTop = false
var body: some View {
Text("Hello!")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isOnTop == true ? .top : .bottom)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
withAnimation(.spring()) {
offset = value.translation
}
}
.onEnded { value in
withAnimation(.spring()) {
offset = .zero
// if dragged to the top half of the screen, set true
//isOnTop = true
}
}
)
}
}

This is how I will do, I use GeometryReader to have the size of my screen and I will apply logic to my offset when my gesture is ended
struct SwiftUIView: View {
#State private var yOffset: CGFloat = 0.0
#State private var xOffset: CGFloat = 0.0
#State private var height: CGFloat = 0
#State private var lastXOffset: CGFloat = 0.0
#State private var lastYOffset : CGFloat = 0.0
var body: some View {
GeometryReader{ geo in
VStack{
Spacer()
Text("Hello!")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.offset(x: xOffset, y: yOffset >= 0 ? 0: yOffset)
.gesture(DragGesture().onChanged({ gesture in
self.xOffset = gesture.translation.width + lastXOffset
self.yOffset = gesture.translation.height + lastYOffset
}).onEnded({ _ in
withAnimation{
lastXOffset = xOffset
if yOffset <= -height/2 {
yOffset = -height + geo.safeAreaInsets.top
///save last offset
lastYOffset = yOffset
} else {
yOffset = 0
///save last offset
lastYOffset = yOffset
}
}
}))
}
.onAppear{
height = geo.size.height
}
}
}
}

Related

How to show an overlay on swipe/drag gesture

I want to show an overlay that displays a different image depending on which side the card is dragged towards. I tried setting a default image but that way the image never updated, so I am trying with a string of the imageName instead. I implemented a tinder like swiping already:
class CardViewModel : ObservableObject {
#Published var offset = CGSize.zero
}
struct CardView: View {
#State var offset = CGSize.zero
#StateObject var cardVM = CardViewModel()
#State var imageName = ""
#State var isDragging = false
func swipeCard(width: CGFloat) {
switch width {
case -500...(-150):
offset = CGSize(width: -500, height: 0)
self.imageName = "nope"
case 150...500:
offset = CGSize(width: 500, height: 0)
self.imageName = "bid"
default:
offset = .zero
}
}
var body: some View {
VStack {
}.overlay(
isDragging ? Image(imageName) : Image("")
)
.offset(x: offset.width, y: offset.height * 0.4)
.rotationEffect(.degrees(Double(offset.width / 40)))
.animation(.spring())
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
isDragging = true
} .onEnded { _ in
withAnimation {
swipeCard(width: offset.width)
isDragging = false
}
}
).offset(x: cardVM.offset.width, y: cardVM.offset.height * 0.4)
.rotationEffect(.degrees(Double(cardVM.offset.width / 40)))
.animation(.spring())
}
}

SwiftUI: Adjust bounds of ScrollView

I am trying to scale a view horizontally using scaleEffect and MagnificationGesture.
It is almost working as I want with the exception that the ScrollView does not resize when its child view resizes.
Is there a fix for this? Any solution would be greatly appreciated.
To reproduce:
run the code below
scale up the image horizontally with the pinch gesture
notice that the ScrollView scrolls as if the image still has the same size.
struct ContentView: View {
#State private var currentAmount: CGFloat = 0
#State private var finalAmount: CGFloat = 1
var body: some View {
ScrollView(.horizontal) {
Image(systemName: "star")
.resizable()
.scaledToFit()
.scaleEffect(x: finalAmount + currentAmount, y: 1)
.gesture(
MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
}
.onEnded { amount in
self.finalAmount += self.currentAmount
self.currentAmount = 0
}
)
}
.frame(maxHeight: 300)
}
}
This view does it.
struct HorizontalScaleView<Content: View>: View {
#ViewBuilder var content: Content
#State private var currentAmount: CGFloat = 0
#State private var finalAmount: CGFloat = 1
var body: some View {
GeometryReader { geo in
ScrollView(.horizontal) {
content
.scaledToFit()
.scaleEffect(x: finalAmount + currentAmount, y: 1)
.frame(width: (finalAmount + currentAmount) * geo.size.width, height: geo.size.width)
.gesture(
MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
}
.onEnded { _ in
if self.finalAmount + self.currentAmount >= 1 {
self.finalAmount += self.currentAmount
} else {
self.finalAmount = 1
}
self.currentAmount = 0
}
)
}
}
}
}

Slide a Scrollview over another view

Here is what i am trying to achieve :
Here is what i currently have :
I cannot seem to get the my ScrollView to slide over the green header view (schedulerHeaderView) like in the first example. Here is my code
struct CreateShiftView: View {
#EnvironmentObject private var createShiftViewModel: CreateShiftViewModel
var body: some View {
GeometryReader { geo in
VStack {
schedulerHeaderView
Spacer()
}
.background(
Color(createShiftViewModel.isNavbarTitleHidden ? ThemeManager.current().greenery : .white)
.ignoresSafeArea(.all, edges: .top)
)
.overlay(
ScrollView {
VStack(spacing: 42) {
addEmployeeSection
shiftInfoSection
addBreakSection
addResourceSection
addJustificationSection
savePublishSection
}
.background(Color.white)
.cornerRadius(20, corners: [.topLeft, .topRight])
.padding(.horizontal, 24)
}
.frame(minWidth: UIScreen.main.bounds.width)
.offset(y: geo.size.height * 0.155)
)
}
}
}
I'm guessing i need to dynamically change the offset or position of the scrollView with a DragGesture but i haven't been able to get a working example so far. Any help would be appreciated. Thanks in advance.
I have found the solution to my question. In summary, i need to keep track of the last offset (using state) and use DragGesture's .onChanged and .onEnded to set a new offset depending on the value of the drag translation. Here is my updated code which now works
struct CreateShiftView: View {
#EnvironmentObject private var createShiftViewModel: CreateShiftViewModel
#State private var offsets = (top: CGFloat.zero, bottom: CGFloat.zero)
#State private var offset: CGFloat = .zero
#State private var lastOffset: CGFloat = .zero
#State private var isAtTopOffset = false
#State private var dragging = false
var body: some View {
GeometryReader { geo in
VStack {
schedulerHeaderView
Spacer()
}
.background(
Color(createShiftViewModel.isNavbarTitleHidden ? ThemeManager.current().greenery : .white)
.ignoresSafeArea(.all, edges: .top)
)
.overlay(
SlideOverView(cardviewInitialPosition: geo.size.height * 0.30, viewModel: createShiftViewModel) {
VStack(spacing: 42) {
informationSection
shiftInfoSection
addEmployeeSection
addBreakSection
addResourceSection
addJustificationSection
savePublishSection
}
.background(Color.white)
.cornerRadius(20, corners: [.topLeft, .topRight])
.padding(.horizontal, 24)
}
.onAppear {
self.offsets = (
top: .zero,
bottom: geo.size.height * 0.155
)
self.offset = self.offsets.bottom
self.lastOffset = self.offset
}
.offset(y: self.offset)
.animation(dragging ? nil : {
Animation.interpolatingSpring(stiffness: 250.0, damping: 40.0, initialVelocity: 5.0)
}())
.simultaneousGesture(
DragGesture(minimumDistance: createShiftViewModel.isNavbarTitleHidden ? 5 : 100, coordinateSpace: .local)
.onChanged { v in
dragging = true
let newOffset = self.lastOffset + v.translation.height
if (newOffset > self.offsets.top && newOffset < self.offsets.bottom) {
self.offset = newOffset
}
}
.onEnded{ v in
dragging = false
if (self.lastOffset == self.offsets.top && v.translation.height > 0) {
self.offset = self.offsets.bottom
createShiftViewModel.cardAtBottom()
} else if (self.lastOffset == self.offsets.bottom && v.translation.height < 0) {
self.offset = self.offsets.top
createShiftViewModel.cardAtTop()
}
self.lastOffset = self.offset
}
)
)
}
}
}

shaky view when changing offset through a drag gesture

edit: it looks even worse on physical device, and simulator vs gif
I have a grid of rectangles. These are nested in HStacks and VStacks. This grid is put into a ZStack. I attatch the gesture to the nested Grid and see what happens when I drag with my implementation
import SwiftUI
import UIKit
struct ContentView: View {
#State var numCell: CGFloat = 20
#State var cellSize: CGFloat = 50
var body: some View {
return ZStack() {
Grid(numCell: $numCell, cellSize: $cellSize)
}.position(x: 0, y: 0).frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity).background(Color.blue)
}
}
struct Grid: View {
#Binding var numCell: CGFloat
#Binding var cellSize: CGFloat
#State var gridDraggedX: CGFloat = 0
#State var gridDraggedY: CGFloat = 0
#State var accumulatedGridDraggedX: CGFloat = 0
#State var accumulatedGridDraggedY: CGFloat = 0
var body: some View {
let drag = DragGesture().onChanged({ value in
self.gridDraggedX = value.translation.width + self.accumulatedGridDraggedX
self.gridDraggedY = value.translation.height + self.accumulatedGridDraggedY
}).onEnded({ value in
self.gridDraggedX = value.translation.width + self.accumulatedGridDraggedX
self.gridDraggedY = value.translation.height + self.accumulatedGridDraggedY
self.accumulatedGridDraggedX = self.gridDraggedX
self.accumulatedGridDraggedY = self.gridDraggedY
})
return HStack(spacing: 2) {
ForEach(0..<Int(numCell) - 1) { _ in
VStack(spacing: 2) {
ForEach(0..<Int(self.numCell) - 1) { _ in
Rectangle()
.fill(Color.red)
.frame(width: self.cellSize,
height: self.cellSize)
}
}
}
}.gesture(drag).background(Color.green).offset(x: gridDraggedX, y: gridDraggedY)
}
}
via GIPHY
DragGesture(coordinateSpace: .global)

How to make a swipeable view with SwiftUI

I tried to make a SWIFTUI View that allows card Swipe like action by using gesture() method. But I can't figure out a way to make view swipe one by one. Currently when i swipe all the views are gone
import SwiftUI
struct EventView: View {
#State private var offset: CGSize = .zero
#ObservedObject var randomView: EventViewModel
var body: some View {
ZStack{
ForEach(randomView.randomViews,id:\.id){ view in
view
.background(Color.randomColor)
.cornerRadius(8)
.shadow(radius: 10)
.padding()
.offset(x: self.offset.width, y: self.offset.height)
.gesture(
DragGesture()
.onChanged { self.offset = $0.translation }
.onEnded {
if $0.translation.width < -100 {
self.offset = .init(width: -1000, height: 0)
} else if $0.translation.width > 100 {
self.offset = .init(width: 1000, height: 0)
} else {
self.offset = .zero
}
}
)
.animation(.spring())
}
}
}
}
struct EventView_Previews: PreviewProvider {
static var previews: some View {
EventView(randomView: EventViewModel())
}
}
struct PersonView: View {
var id:Int = Int.random(in: 1...1000)
var body: some View {
VStack(alignment: .center) {
Image("testBtn")
.clipShape(/*#START_MENU_TOKEN#*/Circle()/*#END_MENU_TOKEN#*/)
Text("Majid Jabrayilov")
.font(.title)
.accentColor(.white)
Text("iOS Developer")
.font(.body)
.accentColor(.white)
}.padding()
}
}
With this piece of code, when i swipe the whole thing is gone
Basically your code tells every view to follow offset, while actually you want only the top one move. So firstly I'd add a variable that'd hold current index of the card and a method to calculate it's offset:
#State private var currentCard = 0
func offset(for i: Int) -> CGSize {
return i == currentCard ? offset : .zero
}
Secondly, I found out that if we just leave it like that, on the next touch view would get offset of the last one (-1000, 0) and only then jump to the correct location, so it looks just like previous card decided to return instead of the new one. In order to fix this I added a flag marking that card has just gone, so when we touch it again it gets right location initially. Normally, we'd do that in gesture's .began state, but we don't have an analog for that in swiftUI, so the only place to do it is in .onChanged:
#State private var didJustSwipe = false
DragGesture()
.onChanged {
if self.didJustSwipe {
self.didJustSwipe = false
self.currentCard += 1
self.offset = .zero
} else {
self.offset = $0.translation
}
}
In .onEnded in the case of success we assign didJustSwipe = true
So now it works perfectly. Also I suggest you diving your code into smaller parts. It will not only improve readability, but also save some compile time. You didn't provide an implementation of EventViewModel and those randomViews so I used rectangles instead. Here's your code:
struct EventView: View {
#State private var offset: CGSize = .zero
#State private var currentCard = 0
#State private var didJustSwipe = false
var randomView: some View {
return Rectangle()
.foregroundColor(.green)
.cornerRadius(20)
.frame(width: 300, height: 400)
.shadow(radius: 10)
.padding()
.opacity(0.3)
}
func offset(for i: Int) -> CGSize {
return i == currentCard ? offset : .zero
}
var body: some View {
ZStack{
ForEach(currentCard..<5, id: \.self) { i in
self.randomView
.offset(self.offset(for: i))
.gesture(self.gesture)
.animation(.spring())
}
}
}
var gesture: some Gesture {
DragGesture()
.onChanged {
if self.didJustSwipe {
self.didJustSwipe = false
self.currentCard += 1
self.offset = .zero
} else {
self.offset = $0.translation
}
}
.onEnded {
let w = $0.translation.width
if abs(w) > 100 {
self.didJustSwipe = true
let x = w > 0 ? 1000 : -1000
self.offset = .init(width: x, height: 0)
} else {
self.offset = .zero
}
}
}
}