How to animate views in turn - swiftui

I want to animate the arriving of views on the screen, in turn, one by one. Now my application draws circles and they arrive on the screen at the same time. But I would like it if the first circle will take its position and only after this the second circle will start its animation and etc. What solutions does this problem have?
import SwiftUI
struct ContentView: View {
var body: some View {
GenrealView()
}
}
struct GenrealView: View {
#State var hide = false
var body: some View {
giveViewForBody()
}
func giveViewForBody() -> some View {
ZStack {
drawCircles()
Button(action: {
self.hide.toggle()
}) {
Text(hide ? "Show circles" : "Hide circles")
}.padding(50)
}
}
func drawCircles(times: Int = 4) -> some View {
ForEach(0..<times) { _ in
Circle()
.fill(Color.green)
.frame(width: 100, height: 100)
.position(x: -100, y: -100)
.offset(x: CGFloat(hide ? 0 : 100 + Int.random(in: 100...300)),
y: CGFloat(hide ? 0 : 200 + Int.random(in: 50...600)))
.animation(.easeIn(duration: 2.0))
}
}
}

Add a .delay to each Circle() and make the delay larger for each successive one. Add index in to your ForEach loop and then make the delay .delay(2.0 * Double(index)):
func drawCircles(times: Int = 4) -> some View {
ForEach(0..<times) { index in
Circle()
.fill(Color.green)
.frame(width: 100, height: 100)
.position(x: -100, y: -100)
.offset(x: CGFloat(hide ? 0 : 100 + Int.random(in: 100...300)),
y: CGFloat(hide ? 0 : 200 + Int.random(in: 50...600)))
.animation(Animation.easeIn(duration: 2.0).delay(2.0 * Double(index)))
}
}

Related

SwiftUI: How to center a rectangle relative to another view that has a custom anchor scaling effect?

The rectangle should always be centered in ContainerView no matter what scale offset or anchor point innerContainerView has.
What offset is needed to place the rectangle in the center of the ContainerView?
let innerContainerSize = CGSize(width: 100, height: 100)
struct innerContainerView: View {
#Binding var ia: [si]
var body: some View {
ZStack() {
ForEach(ia) { i in
Rectangle()
.fill(.green)
.scaleEffect(i.scale)
.offset(x: i.frame.origin.x, y: i.frame.origin.y)
.frame(width: 500 * 0.7, height: 500 * 0.7)
}
}
}
}
struct ContainerView: View {
#Binding var ia: [si]
#Binding var fscale: CGFloat
#Binding var foffset: CGSize
var body: some View {
innerContainerView(ia: $ia)
.frame(width: innerContainerSize.width, height: innerContainerSize.height)
.background(Color.yellow)
.scaleEffect(fscale, anchor: .init(x: (250 - foffset.width) / 500, y: (250 - foffset.height) / 500))
.offset(foffset)
}
}
struct ContentView: View {
#State var ia = [si]()
#State var fscale: CGFloat = 1
#State var foffset: CGSize = .zero
var body: some View {
VStack(alignment: .center) {
ContainerView(ia: $ia, fscale: $fscale, foffset: $foffset)
.frame(width: 500, height: 500)
.background(Color.blue)
.clipped()
.offset(x: 50)
}
}
}
you can use .alignmentGuide() with GeometryReader.
Possible example :
struct ContentView: View {
let color = [Color.red, .green, .yellow, .blue, .orange, .gray]
let number = [5,4,3,2,1]
var body: some View {
GeometryReader { proxy in
ZStack {
Rectangle()
.fill(.red)
.scaleEffect(CGSize(width: 3, height: 2))
.frame(width: 100,height: 100)
.border(Color.blue, width: 4)
Rectangle()
.frame(width: 250,height: 100)
.offset(x: 50, y: 75)
.alignmentGuide(HorizontalAlignment.center) { viewDimension in
viewDimension[HorizontalAlignment.center] + 50 // offset of x
}
.alignmentGuide(VerticalAlignment.center, computeValue: { viewDimension in
viewDimension[VerticalAlignment.center] + 75 // offset of y
})
.border(Color.gray, width: 4)
.position(x: proxy.size.width/2 - 50, y: proxy.size.height/2 - 75)
// adding this once is enough
Rectangle()
.fill(Color.yellow)
.frame(width: 50,height: 30)
.offset(x: 100, y: 150)
.alignmentGuide(HorizontalAlignment.center) { viewDimension in
viewDimension[HorizontalAlignment.center] + 100 // offset of x
}
.alignmentGuide(VerticalAlignment.center, computeValue: { viewDimension in
viewDimension[VerticalAlignment.center] + 150 // offset of y
})
.border(Color.gray, width: 4)
}
.border(.gray)
}
}
}

How to animate a view in a circular motion using its real-time position coordinates?

I'm currently working on a SwiftUI project, and in order to detect intersections/collisions, I need real-time coordinates, which SwiftUI animations cannot offer. After doing some research, I came across a wonderful question by Kike regarding how to get the real-time coordinates of a view when it is moving/transitioning. And Pylyp Dukhov's answer to that topic recommended utilizing CADisplayLink to calculate the position for each frame and provided a workable solution that did return the real time values when transitioning.
But I'm so unfamiliar with CADisplayLink and creating custom animations that I'm not sure I'll be able to bend it to function the way I want it to.
So this is the animation I want to achieve using CADisplayLink that animates the orange circle view in a circular motion using its position coordinates and repeats forever:
Here is the SwiftUI code:
struct CircleView: View {
#Binding var moveClockwise: Bool
#Binding var duration: Double // Works as speed, since it repeats forever
let geo: GeometryProxy
var body: some View {
ZStack {
Circle()
.stroke()
.frame(width: geo.size.width, height: geo.size.width, alignment: .center)
//MARK: - What I have with SwiftUI animation
Circle()
.fill(.orange)
.frame(width: 35, height: 35, alignment: .center)
.offset(x: -CGFloat(geo.size.width / 2))
.rotationEffect(.degrees(moveClockwise ? 360 : 0))
.animation(
.linear(duration: duration)
.repeatForever(autoreverses: false), value: moveClockwise
)
//MARK: - What I need with CADisplayLink
// Circle()
// .fill(.orange)
// .frame(width: 35, height: 35, alignment: .center)
// .position(CGPoint(x: pos.realTimeX, y: realTimeY))
Button("Start Clockwise") {
moveClockwise = true
// pos.startMovement
}.foregroundColor(.orange)
}.fixedSize()
}
}
struct ContentView: View {
#State private var moveClockwise = false
#State private var duration = 2.0 // Works as speed, since it repeats forever
var body: some View {
VStack {
GeometryReader { geo in
CircleView(moveClockwise: $moveClockwise, duration: $duration, geo: geo)
}
}.padding(20)
}
}
This is what I have currently with CADisplayLink, I added the coordinates to make a circle and that’s about it & it doesn’t repeat forever like the gif does:
Here is the CADisplayLink + real-time coordinate version that I’ve tackled and got lost:
struct Point: View {
var body: some View {
Circle()
.fill(.orange)
.frame(width: 35, height: 35, alignment: .center)
}
}
struct ContentView: View {
#StateObject var P: Position = Position()
var body: some View {
VStack {
ZStack {
Circle()
.stroke()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width, alignment: .center)
Point()
.position(x: P.realtimePosition.x, y: P.realtimePosition.y)
}
Text("X: \(P.realtimePosition.x), Y: \(P.realtimePosition.y)")
}.onAppear() {
P.startMovement()
}
}
}
class Position: ObservableObject, Equatable {
struct AnimationInfo {
let startDate: Date
let duration: TimeInterval
let startPoint: CGPoint
let endPoint: CGPoint
func point(at date: Date) -> (point: CGPoint, finished: Bool) {
let progress = CGFloat(max(0, min(1, date.timeIntervalSince(startDate) / duration)))
return (
point: CGPoint(
x: startPoint.x + (endPoint.x - startPoint.x) * progress,
y: startPoint.y + (endPoint.y - startPoint.y) * progress
),
finished: progress == 1
)
}
}
#Published var realtimePosition = CGPoint.zero
private var mainTimer: Timer = Timer()
private var executedTimes: Int = 0
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
displayLink.add(to: .main, forMode: .default)
return displayLink
}()
private let animationDuration: TimeInterval = 0.1
private var animationInfo: AnimationInfo?
private var coordinatesPoints: [CGPoint] {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
// great progress haha
let radius: Double = Double(screenWidth / 2)
let center = CGPoint(x: screenWidth / 2, y: screenHeight / 2)
var coordinates: [CGPoint] = []
for i in stride(from: 1, to: 360, by: 10) {
let radians = Double(i) * Double.pi / 180 // raiments = degrees * pI / 180
let x = Double(center.x) + radius * cos(radians)
let y = Double(center.y) + radius * sin(radians)
coordinates.append(CGPoint(x: x, y: y))
}
return coordinates
}
// Conform to Equatable protocol
static func ==(lhs: Position, rhs: Position) -> Bool {
// not sure why would you need Equatable for an observable object?
// this is not how it determines changes to update the view
if lhs.realtimePosition == rhs.realtimePosition {
return true
}
return false
}
func startMovement() {
mainTimer = Timer.scheduledTimer(
timeInterval: 0.1,
target: self,
selector: #selector(movePoint),
userInfo: nil,
repeats: true
)
}
#objc func movePoint() {
if (executedTimes == coordinatesPoints.count) {
mainTimer.invalidate()
return
}
animationInfo = AnimationInfo(
startDate: Date(),
duration: animationDuration,
startPoint: realtimePosition,
endPoint: coordinatesPoints[executedTimes]
)
displayLink.isPaused = false
executedTimes += 1
}
#objc func displayLinkAction() {
guard
let (point, finished) = animationInfo?.point(at: Date())
else {
displayLink.isPaused = true
return
}
realtimePosition = point
if finished {
displayLink.isPaused = true
animationInfo = nil
}
}
}
Inside Position you're calculating position related to whole screen. But .position modifier requires value related to the parent view size.
You need to make your calculations based on the parent size, you can use such sizeReader for this purpose:
extension View {
func sizeReader(_ block: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometry in
Color.clear
.onAppear {
block(geometry.size)
}
.onChange(of: geometry.size, perform: block)
}
)
}
}
Usage:
ZStack {
Circle()
.stroke()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width)
Point()
.position(x: P.realtimePosition.x, y: P.realtimePosition.y)
}
.sizeReader { size in
P.containerSize = size
}
Also CADisplayLink is not used in the right way. The whole point of this tool is that it's already called on each frame, so you can calculate real time position, so your animation is gonna be really smooth, and you don't need a timer or pre-calculated values for only 180(or any other number) positions.
In the linked answer timer was used because a delay was needed between animations, but in your case the code can be greatly simplified:
class Position: ObservableObject {
#Published var realtimePosition = CGPoint.zero
var containerSize: CGSize?
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(displayLinkAction))
displayLink.add(to: .main, forMode: .default)
displayLink.isPaused = true
return displayLink
}()
private var startDate: Date?
func startMovement() {
startDate = Date()
displayLink.isPaused = false
}
let animationDuration: TimeInterval = 5
#objc func displayLinkAction() {
guard
let containerSize = containerSize,
let timePassed = startDate?.timeIntervalSinceNow,
case let progress = -timePassed / animationDuration,
progress <= 1
else {
displayLink.isPaused = true
startDate = nil
return
}
let frame = CGRect(origin: .zero, size: containerSize)
let radius = frame.midX
let radians = CGFloat(progress) * 2 * .pi
realtimePosition = CGPoint(
x: frame.midX + radius * cos(radians),
y: frame.midY + radius * sin(radians)
)
}
}
I've tried to make more simplified the implementation, here is the SwiftUI code,
struct RotatingDotAnimation: View {
#State private var moveClockwise = false
#State private var duration = 1.0 // Works as speed, since it repeats forever
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 4)
.foregroundColor(.white.opacity(0.5))
.frame(width: 150, height: 150, alignment: .center)
Circle()
.fill(.white)
.frame(width: 18, height: 18, alignment: .center)
.offset(x: -63)
.rotationEffect(.degrees(moveClockwise ? 360 : 0))
.animation(.easeInOut(duration: duration).repeatForever(autoreverses: false),
value: moveClockwise
)
}
.onAppear {
self.moveClockwise.toggle()
}
}
}
It'll basically create animation like this,
enter image description here

SwiftUI - How to get notified when dragging and releasing on an view

I have two circles on my screen (top and bottom). I want the user to press down on the top circle, and drag to the bottom one.
I'm not dragging and dropping (I don't want the UI to change).
I just want to know that the user started on the top one, and released their finger on the bottom circle. When the user releases their finger on the bottom one.
I haven't been able to find my answer from other questions.
Current code
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Circle()
.fill()
.foregroundColor(.blue)
.frame(width: 100, height: 100)
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .named("mySpace"))
.onChanged { value in
}
.onEnded { value in
// if value.location == endPoint {
// print("user started press on blue and ended on green")
// }
}
)
Spacer()
Circle()
.fill()
.foregroundColor(.green)
.frame(width: 100, height: 100)
}.coordinateSpace(name: "mySpace")
}
}
Screenshot
Would appreciate any help!
Here I could find a way for get notified wether this 2 Circles are in some part inside each other:
import SwiftUI
struct ContentView: View {
typealias OffsetType = (offset: CGSize, lastOffset: CGSize)
#State private var objects: [OffsetType] = [(offset: CGSize(width: 0.0, height: -200.0), lastOffset: CGSize(width: 0.0, height: -200.0)),
(offset: CGSize(width: 0.0, height: 200.0), lastOffset: CGSize(width: 0.0, height: 200.0))]
var body: some View {
ZStack {
CircleView(color: Color.blue)
.offset(objects[0].offset)
.gesture(dragGesture(indexOfObject: 0))
CircleView(color: Color.green)
.offset(objects[1].offset)
.gesture(dragGesture(indexOfObject: 1))
}
.animation(Animation.easeInOut(duration: 0.1))
}
func dragGesture(indexOfObject: Int) -> some Gesture {
DragGesture(minimumDistance: 0.0, coordinateSpace: .global)
.onChanged() { value in
objects[indexOfObject].offset = CGSize(width: objects[indexOfObject].lastOffset.width + value.translation.width,
height: objects[indexOfObject].lastOffset.height + value.translation.height)
}
.onEnded() { value in
objects[indexOfObject].lastOffset = CGSize(width: objects[indexOfObject].lastOffset.width + value.translation.width,
height: objects[indexOfObject].lastOffset.height + value.translation.height)
objects[indexOfObject].offset = objects[indexOfObject].lastOffset
distance()
}
}
func distance() {
if pow(pow((objects[1].offset.width - objects[0].offset.width), 2.0) + pow((objects[1].offset.height - objects[0].offset.height), 2.0), 0.5) <= 100 { print("same place!") }
}
}
struct CircleView: View {
let color: Color
var body: some View {
Circle()
.fill(color)
.frame(width: 100, height: 100)
}
}

Positioning Views in ForEach SwiftUI

I would like to add animating views to a parent view. I know that the parent view needs to position the children but I'm having trouble coming up with the formula to implement. I have the first couple of views right but once I get to 4 and up its a problem! I would like the views to appear in a grid with 3 columns.
Here is some reproducible code ready to be copy and pasted.
import SwiftUI
struct CustomView: View, Identifiable {
#State private var startAnimation = false
let id = UUID()
var body: some View {
Circle()
.frame(width: 50, height: 50)
.scaleEffect(x: startAnimation ? 2 : 1,
y: startAnimation ? 2 : 1)
.animation(Animation.interpolatingSpring(mass: 2, stiffness: 20, damping: 1, initialVelocity: 1))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.startAnimation = true
}
}
}
}
struct StartView: View {
#State private var userSelection: [CustomView] = []
var body: some View {
VStack(spacing: -20) {
Button("Add View") {
self.userSelection.append(CustomView())
}
LazyVGrid(columns: gridStyle) {
ForEach(Array(userSelection.enumerated()), id: \.0 ){ index, equip in
CustomView()
.position(x: widthBasedOn(index: index), y: heightBasedOn(index: index))
}
.padding([])
}
.frame(width: UIScreen.main.bounds.width * 0.5,
height: UIScreen.main.bounds.height * 0.8)
}
}
let gridStyle = [
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50),
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50),
GridItem(.flexible(minimum: 0, maximum: 100), spacing: -50)
]
private func widthBasedOn(index: Int) -> CGFloat {
if index % 3 != 0 {
if index > 3 {
let difference = index - 4
return CGFloat(index * difference * 100)
}
let answer = CGFloat(index * 100)
print("\(index) width should be: \(answer)")
return answer
}
return 0
}
private func heightBasedOn(index: Int) -> CGFloat {
if index > 3 && index < 6 {
return 100
}
return 200
}
}
struct EquipmentSelectionView_Previews: PreviewProvider {
static var previews: some View {
StartView()
}
}
Since most of your question is somewhat vague, and I am not sure about the specifics, this is my solution. Feel free to respond, and I will be glad to answer your question further with more tailored solution.
I removed many of your code that was unnecessary or overly-complicated. For example, I removed the widthBasedOn and heightBasedOn methods. I also changed the array property var userSelection: [CustomView] to var numberOfViews = 0.
Note: Both your original code and my solution cause all the circles to wiggle up and down, whenever a new circle is added.
I suggest that you copy paste this code snippet, run it in Xcode, and see if this is what you want.
struct CustomView: View, Identifiable {
#State private var startAnimation = false
let id = UUID()
var body: some View {
Circle()
//Changing the frame size of the circle, making it bigger or smaller
.frame(width: startAnimation ? 100 : 50, height: startAnimation ? 100 : 50)
.animation(Animation.interpolatingSpring(mass: 2, stiffness: 20, damping: 1, initialVelocity: 1))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.startAnimation = true
}
}
}
}
struct StartView: View {
//View will display this number of circles
#State private var numberOfViews = 0
var body: some View {
VStack() {
Button("Add View") {
self.numberOfViews += 1
}
.padding(.top, 100)
Spacer()
LazyVGrid(columns: gridStyle) {
//Add a new circle CustomView() to the LazyVGrid for each number of views
ForEach(0..<numberOfViews, id: \.self ){view in
CustomView()
}
}
}
}
//3 columns, flexible spacing for elments. In this case, equal amount of spacing.
let gridStyle = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
]
}
struct EquipmentSelectionView_Previews: PreviewProvider {
static var previews: some View {
StartView()
}
}
Limiting number of circles
To limit the number of circles:
if numberOfViews < 9 {
self.numberOfViews += 1
}
Positioning the button
To position the button, you can add padding:
Button("Add View") {
if numberOfViews < 9 {
self.numberOfViews += 1
}
}
.padding(.top, 100)
Overlap vs. No Overlap
Using there .frame modifier will not have any overlap:
.frame(width: startAnimation ? 100 : 50, height: startAnimation ? 100 : 50)
But if you do want overlap, use .scaleEffect:
.scaleEffect(x: startAnimation ? 2 : 1,
y: startAnimation ? 2 : 1)
P.S. Unfortunately, I can't show you the results with GIF images because Stackoverflow keep giving me upload errors.

How can I have 2 Gestures in SwiftUI?

I have a CircleView() which is movable! I want minimumDistance for DragGesture be Zero for this View and in the other hand I defined another Gesture called onTapGesture, it is working but not in the way I wanted! because of onTapGesture the minimumDistance became 10 and with dragging you can see that SwiftUI think minimumDistance is 10, how can I have both Gestures working fine with minimumDistance = 0 ?
my goal: I want have an onTapGesture and a DragGesture with minimumDistance = 0
import SwiftUI
struct ContentView: View {
var body: some View {
CircleView()
}
}
struct CircleView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
var body: some View {
Circle()
.fill()
.frame(width: 100, height: 100, alignment: .center)
.position(x: location.width + translation.width + 100, y: location.height + translation.height + 100)
.onTapGesture { print("onTapGesture!") } // << Until here: minimumDistance: 10
.gesture(DragGesture(minimumDistance: 0) // << After here: minimumDistance: 0 But also it does not Help! SwiftUI thinks that minimumDistance is 10!
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
})
}
}
One option is to compose a combination of two simultaneous gestures, like this:
import SwiftUI
struct ContentView: View {
var body: some View {
CircleView()
}
}
struct CircleView: View {
#State private var location: CGSize = CGSize()
#GestureState private var translation: CGSize = CGSize()
var body: some View {
let tapDrag = DragGesture(minimumDistance: 0)
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
location = CGSize(width: location.width + value.translation.width, height: location.height + value.translation.height)
}
.simultaneously(with: TapGesture()
.onEnded{ print("onTapGesture!") } )
Circle()
.fill()
.frame(width: 100, height: 100, alignment: .center)
.position(x: location.width + translation.width + 100, y: location.height + translation.height + 100)
.gesture(tapDrag)
}
}