SwiftUI Animation onLongPressGesture(minimumDuration: 0.5) automatically starts - swift

I've created a circular button which I specified to show scale animation on .onLongPressGesture(minimumDuration: 0.5)
the code is bellow:
struct PlayerTouch: View {
#State private var animationAmount: CGFloat = 1
#State var animationEffect = false
var outerColor: Color
var innerColor: Color
let touchpulse = Animation.linear.repeatForever(autoreverses: true)
private var outerBorderCirle: some View {
let side: CGFloat = Device.model == device.Six_6S_7_8_SE ? 108.6 : 146
return Circle()
.stroke(outerColor, lineWidth: 6)
.frame(width: side, height: side)
.shadow(color: Color.black.opacity(0.3) ,radius: 8 , x: 0, y: 0)
}
private var innerBorderCirle: some View {
let side: CGFloat = Device.model == device.Six_6S_7_8_SE ? 80.3 : 108
return Circle()
.stroke(innerColor, lineWidth: 20)
.frame(width: side, height: side)
.scaleEffect(animationEffect ? 0.9 : 1)
.opacity(animationEffect ? 0.7 : 1)
.animation(touchpulse)
}
private var touchCirle: some View {
let side: CGFloat = Device.model == device.Six_6S_7_8_SE ? 71.7 : 96
return Circle()
.foregroundColor(colors.darkCream.value)
.shadow(color: colors.touchCream.value, radius: 18, x: 0, y: 6)
.frame(width: side, height: side)
.onLongPressGesture(minimumDuration: 0.5) {
self.animationEffect = true
}
}
}
var body: some View {
ZStack{
outerBorderCirle
innerBorderCirle
touchCirle
}
}}
and I've created 4 instances from this view in my main view like bellow:
struct GameScene: View {
#Binding var numberOfPeople: Int
#State var offset = 0
private var touch1: some View {
PlayerTouch(outerColor: outerColors[0], innerColor: innerColors[0])
}
private var touch2: some View {
PlayerTouch(outerColor: outerColors[1], innerColor: innerColors[1])
}
private var touch3: some View {
PlayerTouch(outerColor: outerColors[2], innerColor: innerColors[2])
}
private var touch4: some View {
PlayerTouch(outerColor: outerColors[3], innerColor: innerColors[3])
}
var body: some View {
VStack(spacing: Device.model! == .X_Xs_11Pro ? 20 : 40){
HStack{
Spacer()
touch1
touch2
.offset(y: Device.model! == .Six_6S_7_8_SE &&
numberOfPeople > 2 ? -40 : -50)
Spacer()
}
HStack{
if numberOfPeople > 2{
Spacer()
touch3
.offset(x: numberOfPeople == 3 ? -40 : 0, y: numberOfPeople == 3 ? -40 : 0)
}
if numberOfPeople > 3 {
touch4
.offset(y:-50)
Spacer()
}
}
}.frame(height: height)
.padding(.leading, 10)
.padding(.trailing, 10)
}}
and I have a button to add numberofpeople which leads to show instances on the view base on that.
my problem is animation automatically triggers after adding to numberofpeople
here is the gif of showing the problem:
The animation problem shown
the right way of showing animation is after I touching it for 0.5 seconds
Animation should be like this and should be triggered after long tap

Make animation active explicit per-value change, like
return Circle()
.stroke(innerColor, lineWidth: 20)
.frame(width: side, height: side)
.scaleEffect(animationEffect ? 0.9 : 1)
.opacity(animationEffect ? 0.7 : 1)
.animation(touchpulse, value: animationEffect) // << here !!

Related

SwiftUI WatchOS: Animation behaves strangely when placed inside a list?

I have this pulse animation below which works well by itself, but when I place it inside of a List then the animation of the circles pulsing is correct but all of the circles also move vertically from the top to the center of the screen as well? Outside of a list the circles remain in the center. Why is a list causing this and how to get around?
import SwiftUI
struct HeartRatePulseView: View {
#State var animate = false
#Environment(\.scenePhase) private var scenePhase
func circlesColor() -> Color {
Color.blue
}
var body: some View {
VStack(spacing: -3) {
ZStack {
ZStack {
GeometryReader { geometry in
ZStack {
Circle().fill(circlesColor().opacity(0.25)).frame(width: geometry.size.width, height: geometry.size.height).scaleEffect(self.animate ? 1 : 0.01)
Circle().fill(circlesColor().opacity(0.35)).frame(width: geometry.size.width * 0.79, height: geometry.size.height * 0.79).scaleEffect(self.animate ? 1 : 0.01)
Circle().fill(circlesColor()).frame(width: geometry.size.width * 0.60, height: geometry.size.height * 0.60)
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
.onAppear { self.animate = true }
.onChange(of: scenePhase, perform: { newValue in
if newValue == .active {
self.animate = true
} else {
self.animate = false
}
})
.animation(animate ? Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true) : .default)
}
}
.frame(height: 145)
}
}
struct HeartRatePulseView_Previews: PreviewProvider {
static var previews: some View {
List {
HeartRatePulseView()
}
.listStyle(.carousel)
}
}
Ok, if you change the animation method slightly it works,
Note the value argument, it seems this is the new way to animate
.animation(animate ? Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true) : nil, value: animate)
import SwiftUI
struct HeartRatePulseView: View {
#State var animate = false
#Environment(\.scenePhase) private var scenePhase
func circlesColor() -> Color {
Color.blue
}
var body: some View {
VStack(spacing: -3) {
ZStack {
ZStack {
GeometryReader { geometry in
ZStack {
Circle().fill(circlesColor().opacity(0.25)).frame(width: geometry.size.width, height: geometry.size.height).scaleEffect(self.animate ? 1 : 0.01)
Circle().fill(circlesColor().opacity(0.35)).frame(width: geometry.size.width * 0.79, height: geometry.size.height * 0.79).scaleEffect(self.animate ? 1 : 0.01)
Circle().fill(circlesColor()).frame(width: geometry.size.width * 0.60, height: geometry.size.height * 0.60)
}
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
.onAppear { self.animate = true }
.onChange(of: scenePhase, perform: { newValue in
if newValue == .active {
self.animate = true
} else {
self.animate = false
}
})
.animation(animate ? Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true) : nil, value: animate)
}
}
.frame(height: 145)
}
}
struct HeartRatePulseView_Previews: PreviewProvider {
static var previews: some View {
List {
HeartRatePulseView()
HeartRatePulseView()
}
.listStyle(.carousel)
}
}

SwiftUI Create a Animating Circle

I'd like to create some content like this, a blinking green circle, and it works in the single preview mode
But when I put the View inside a List, the Green circle start moving left and right
struct DotView: View {
#State var delay: Double = 0 // 1.
#State var scale: CGFloat = 0.5
var body: some View {
Circle()
.frame(width: 6, height: 6)
.foregroundColor(Color.green)
.scaleEffect(scale)
.animation(Animation.easeInOut(duration: 0.6).repeatForever().delay(delay)) // 2.
.onAppear {
withAnimation {
self.scale = 1
}
}
}
}
Using inside a navigation view
List {
VStack {
HStack {
Text(server.name)
.fontWeight(.bold)
.foregroundColor(Color.primary)
.fontWeight(.light)
.foregroundColor(.gray)
Spacer()
DotView()
}
}
}
As I wrote in comments it is not reproducible for me, but try the following...
struct DotView: View {
var delay: Double = 0 // 1. << don't use state for injectable property
#State private var scale: CGFloat = 0.5
var body: some View {
Circle()
.frame(width: 6, height: 6)
.foregroundColor(Color.green)
.scaleEffect(scale)
.animation(
Animation.easeInOut(duration: 0.6)
.repeatForever().delay(delay), value: scale // 2. << link to value
)
.onAppear {
self.scale = 1 // 3. << withAnimation no needed now
}
}
}

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
}
)
)
}
}
}

Problem with position() / offset() swiftUI

I`m trying to move this circle to the edges of the screen then determine if it is at the border of the screen and show alert.
This is what I already tried: Tried offset(), now am playing with position() without luck. Tried using geometryReader didnt help
I can hard code to a value of minus something which is working but want to know how to detect the edges of the screen and also to understand why this logic isnt working.
To know edges of the screen I tried also using UIScreen.main.boundries
I`m doing SwiftUI exersises to get "comfortable" with SwiftUI which is a real pain.
import SwiftUI
struct ContentView: View {
let buttons1 = ["RESET","UP","COLOR"]
let buttons2 = ["<","DOWN",">"]
#State private var colorSelector = 0
let circleColors:[Color] = [.red,.green,.blue,.black]
#State private var ballOffset = CGPoint.init(x: 80, y: 80)
#State private var circleOpacity = 0.5
#State private var alertActive = false // wall problem
var body: some View {
ZStack{
Color.init("Grayish")
VStack{
Circle()
.fill(circleColors[colorSelector]).opacity(circleOpacity)
.position(ballOffset)
.frame(width: 160, height: 160)
}
VStack(spacing:10){
Spacer()
Slider(value: $circleOpacity)
HStack{
ForEach(0..<buttons1.count,id:\.self){ text in
Button(action: {
switch text{
case 0:
colorSelector = 0
ballOffset = .zero
case 1:
ballOffset.y -= 3
case 2:
if colorSelector == circleColors.count - 1{
colorSelector = 0
}
else {
colorSelector += 1
}
default: break
}
}, label: {
Text(buttons1[text])
.foregroundColor(text == 1 ? Color.white:Color.accentColor)
})
.buttonModifier()
.background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor).opacity(text == 1 ? 1 : 0))
}
}.padding(.horizontal,5)
HStack{
ForEach(0..<buttons2.count,id:\.self){text in
Button(action:{
switch text{
case 0:
ballOffset.x -= 3
case 1:
ballOffset.y += 3
case 2:
ballOffset.x += 3
default:break
}
},
label:{
Text(buttons2[text])
.foregroundColor(Color.white)
})
.buttonModifier()
.background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
}
}.padding([.bottom,.horizontal],5)
}
.alert(isPresented: $alertActive, content: {
Alert(title: Text("out of bounds"), message: Text("out"), dismissButton: .default(Text("OK")))
}) // alert not working have to figure out wall problem first
}.edgesIgnoringSafeArea(.all)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ButtonModifier: ViewModifier{
func body(content: Content) -> some View {
content
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, idealHeight: 44, maxHeight: 44)
.padding(.horizontal)
.foregroundColor(Color.accentColor)
.background(RoundedRectangle(cornerRadius: 10)
.stroke(Color.accentColor))
}
}
extension View{
func buttonModifier() -> some View{
modifier(ButtonModifier())
}
}
Here is a solution using GeometryReader, which is dynamic and will change with the device orientation. It checks the future ball position is within the boundaries before moving the ball and shows the alert on failure.
struct ContentView: View {
let buttons1 = ["RESET","UP","COLOR"]
let buttons2 = ["<","DOWN",">"]
#State private var colorSelector = 0
let circleColors:[Color] = [.red,.green,.blue,.black]
let ballSize = CGSize(width: 160, height: 160)
#State private var ballOffset: CGSize = .zero
#State private var circleOpacity = 0.5
#State private var alertActive = false
var body: some View {
ZStack {
Color.green.opacity(0.4)
.edgesIgnoringSafeArea(.all)
Circle()
.fill(circleColors[colorSelector])
.opacity(circleOpacity)
.frame(width: ballSize.width, height: ballSize.height)
.offset(ballOffset)
GeometryReader { geometry in
VStack(spacing:10) {
Spacer()
Slider(value: $circleOpacity)
HStack {
ForEach(0..<buttons1.count,id:\.self){ text in
Button(action: {
switch text{
case 0:
colorSelector = 0
ballOffset = .zero
case 1:
handleMove(incrementY: -3, geometryProxy: geometry)
case 2:
colorSelector = colorSelector == (circleColors.count - 1) ? 0 : colorSelector + 1
default:
break
}
}, label: {
Text(buttons1[text])
.foregroundColor(text == 1 ? Color.white:Color.accentColor)
})
.buttonModifier()
.background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor).opacity(text == 1 ? 1 : 0))
}
}.padding(.horizontal,5)
HStack{
ForEach(0..<buttons2.count,id:\.self){text in
Button(action:{
switch text{
case 0:
handleMove(incrementX: -3, geometryProxy: geometry)
case 1:
handleMove(incrementY: 3, geometryProxy: geometry)
case 2:
handleMove(incrementX: 3, geometryProxy: geometry)
default:break
}
},
label:{
Text(buttons2[text])
.foregroundColor(Color.white)
})
.buttonModifier()
.background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
}
}.padding([.bottom,.horizontal],5)
}
.background(Color.orange.opacity(0.5))
}
.alert(isPresented: $alertActive, content: {
Alert(title: Text("out of bounds"), message: Text("out"), dismissButton: .default(Text("OK")))
})
}
}
func handleMove(incrementX: CGFloat = 0, incrementY: CGFloat = 0, geometryProxy: GeometryProxy) {
let newOffset = CGSize(width: ballOffset.width + incrementX, height: ballOffset.height + incrementY)
let standardXOffset =
(geometryProxy.size.width) / 2 // width (excluding safe area)
- (ballSize.width / 2) // subtract 1/2 of ball width (anchor is in the center)
let maxXOffset = standardXOffset + geometryProxy.safeAreaInsets.trailing // + trailing safe area
let minXOffset = -standardXOffset - geometryProxy.safeAreaInsets.leading // + leading safe area
let standardYOffset =
(geometryProxy.size.height / 2) // height (excluding safe area)
- (ballSize.height / 2) // subtract 1/2 of ball height (anchor is in the center)
let maxYOffset = standardYOffset + geometryProxy.safeAreaInsets.bottom // + bottom safe area
let minYOffset = -standardYOffset - geometryProxy.safeAreaInsets.top // + top safe area
if
(newOffset.width >= maxXOffset) ||
(newOffset.width <= minXOffset) ||
(newOffset.height >= maxYOffset) ||
(newOffset.height <= minYOffset) {
alertActive.toggle()
} else {
ballOffset = newOffset
}
}
}

How to drag a working slider using SwiftUI

I would like to drag a slider and of course have it slide as well.
I can do one or the other, but I cannot do both.
How can I drag and have a working slider?
I also tried to find a way to remove a gesture, but I could not find a way to do that.
Also tried the "Sequenced Gesture States" code from Apple "Composing SwiftUI Gestures" docs,
and introduce a flag to turn the dragging on/off with same results, drag or slide not both.
I also tried to put the slider in a Container (VStack) and attach the drag gesture to that,
but that did not work either.
import SwiftUI
struct ContentView: View {
#State var pos = CGSize.zero
#State var acc = CGSize.zero
#State var value = 0.0
var body: some View {
let drag = DragGesture()
.onChanged { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
}
.onEnded { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
self.acc = self.pos
}
return Slider(value: $value, in: 0...100, step: 1)
.frame(width: 250, height: 40, alignment: .center)
.overlay(RoundedRectangle(cornerRadius: 25).stroke(lineWidth: 2).foregroundColor(Color.black))
.offset(x: self.pos.width, y: self.pos.height)
.simultaneousGesture(drag, including: .all) // tried .none .gesture, .subviews
// also tried .gesture(flag ? nil : drag)
}
}
With "simultaneousGesture" I was expecting to have both gestures operating at the same time.
This is working. Basically I needed to set the flag in an outside observable object for it to update the view so that it could take effect. When the value changes the flag is set to false, but then after a tenth of a second it becomes draggable. Working pretty seamlessly.
struct ContentView: View {
#State var pos = CGSize.zero
#State var acc = CGSize.zero
#State var value = 0.0
#ObservedObject var model = Model()
var body: some View {
let drag = DragGesture()
.onChanged { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
}
.onEnded { value in
self.pos = CGSize(width: value.translation.width + self.acc.width, height: value.translation.height + self.acc.height)
self.acc = self.pos
}
return VStack {
Slider(value: $value, in: 0...100, step: 1) { _ in
self.model.flag = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.model.flag = true
}
}
}
.frame(width: 250, height: 40, alignment: .center)
.overlay(RoundedRectangle(cornerRadius: 25).stroke(lineWidth: 2).foregroundColor(Color.black))
.offset(x: self.pos.width, y: self.pos.height)
.gesture(model.flag == true ? drag : nil)
}
}
class Model: ObservableObject {
#Published var flag = false
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}