Having trouble creating a star animation in swiftUI - swift

I'm trying to create a star animation using blur, but my stars end up disappearing and I can't figure out why.
Through debugging a bit I'm pretty sure this has to do with how I'm using onAppear. I'm just trying to make sure the stars blur and unblur on the screen forever - I always want the stars to be visible though.
Could anyone help me fix this problem (attached code below) and any design tips would be appreciated haha.
struct ContentView: View {
#State private var radius = 2
private var opacity = 0.25
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack {
ForEach(0..<8) {_ in
HStack {
ForEach(0..<5) { _ in
Circle().fill(Color.white)
.frame(width: 3, height: 3)
.blur(radius: CGFloat(self.radius))
.animation(Animation.easeInOut(duration: 6).
repeatForever(autoreverses: true))
.padding(EdgeInsets(top: self.calculateRandom(), leading: 0,
bottom: 0, trailing: self.calculateRandom()))
.onAppear() {
self.radius += 2
}
}
}
}
}
}
}
func calculateRandom() -> CGFloat {
return CGFloat(Int.random(in: 30..<150))
}
}

To have animation activated you need to toggle some values, so animator has range to animate in between.
Here is fixed code. Tested with Xcode 12 / iOS 14.
struct ContentView: View {
#State private var run = false // << here !!
private var opacity = 0.25
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack {
ForEach(0..<8) {_ in
HStack {
ForEach(0..<5) { _ in
Circle().fill(Color.white)
.frame(width: 3, height: 3)
.blur(radius: run ? 4 : 2) // << here !!
.animation(Animation.easeInOut(duration: 6).repeatForever(autoreverses: true))
.padding(EdgeInsets(top: self.calculateRandom(), leading: 0,
bottom: 0, trailing: self.calculateRandom()))
.onAppear() {
self.run = true // << here !!
}
}
}
}
}
}
}
func calculateRandom() -> CGFloat {
return CGFloat(Int.random(in: 30..<150))
}
}
Update: variant with static star positions (movement animation is caused by layout in V/H/Stacks as soon as new elements added, so to avoid this it needs to remove those inner stacks and layout manually in ZStack with .position modifier)
struct BlurContentView: View {
#State private var run = false
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
GeometryReader { gp in
ForEach(0..<8) {_ in
ForEach(0..<5) { _ in
Circle().fill(Color.white)
.frame(width: 3, height: 3)
.position(x: calculateRandom(in: gp.size.width),
y: calculateRandom(in: gp.size.height))
.animation(nil) // << no animation for above modifiers
.blur(radius: run ? 4 : 2)
}
}
}
.animation(Animation.easeInOut(duration: 6)
.repeatForever(autoreverses: true), value: run) // animate one value
.onAppear() {
self.run = true
}
}
}
func calculateRandom(in value: CGFloat) -> CGFloat {
return CGFloat(Int.random(in: 10..<Int(value) - 10))
}
}

Related

How to implement scrolling bottom down when we got to the beginning of the content, inside the scrollview using pure SwiftUI?

Here is my attempt to implement this functionality, I also tried to solve it through UIKit, it worked, but I ran into problems with dynamically changing the content of SwiftUI, which was inside UIScrollView. More precisely, the problem was in changing the height of the container
https://imgur.com/a/6du73pt
import SwiftUI
struct ScrollViewOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct ContentView: View {
#State private var offset: CGFloat = 300
var body: some View {
ZStack {
Color.yellow.ignoresSafeArea()
ScrollView(.vertical) {
ForEach(0..<100, id: \.self) { _ in
Color.red
.frame(width: 250, height: 125, alignment: .center)
}
.overlay(
GeometryReader { proxy in
let offset = proxy.frame(in: .named("scroll")).minY
Color.clear.preference(key: ScrollViewOffsetPreferenceKey.self, value: offset)
.frame(width: 0, height: 0, alignment: .center)
})
}
.coordinateSpace(name: "scroll")
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in
if value >= 0 {
offset = value + 300
}
}
.gesture(DragGesture()
.onChanged({ value in
print("scrooll")
print(value)
})
)
}
.offset(y: offset)
.gesture(DragGesture(minimumDistance: 25, coordinateSpace: .local)
.onChanged({ value in
offset = value.translation.height + 300
}))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Below is an example of how you can lock the ScrollView when you are at the top, and then allow the DragGesture to operate instead of scroll. I removed your PreferenceKey as it was not necessary. I also used frame reader to determine where in the scroll view the top cell was. Code is extensively commented.
struct ScrollViewWithPulldown: View {
#State private var offset: CGFloat = 300
#State private var scrollEnabled = true
#State private var cellRect: CGRect = .zero
// if the top of the cell is in view, origin.y will be greater than or equal to zero
var topInView: Bool {
cellRect.origin.y >= 0
}
var body: some View {
ZStack {
Color.yellow.ignoresSafeArea()
ScrollView {
ForEach(0..<100, id: \.self) { id in
Color.red
.id(id)
.frame(width: 250, height: 125, alignment: .center)
// This is inspired by https://www.fivestars.blog/articles/swiftui-share-layout-information/
.copyFrame(in: .named("scroll"), to: $cellRect)
.onChange(of: cellRect) { _ in
if id == 0 { // insure the first view however you need to
if topInView {
scrollEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
scrollEnabled = true
}
} else {
scrollEnabled = true
}
}
}
}
}
.disabled(!scrollEnabled)
.coordinateSpace(name: "scroll")
}
.offset(y: offset)
.gesture(DragGesture()
.onChanged({ value in
// Scrolling down
if value.translation.height > 0 && topInView {
scrollEnabled = false
print("scroll locked")
print(value)
} else { // Scrolling up
scrollEnabled = true
print("scroll up")
print(value)
}
})
.onEnded({ _ in
scrollEnabled = true
})
)
}
}
A view extension inspired by FiveStar Blog:
extension View {
func readFrame(in space: CoordinateSpace, onChange: #escaping (CGRect) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: FrameInPreferenceKey.self, value: geometryProxy.frame(in: space))
}
)
.onPreferenceChange(FrameInPreferenceKey.self, perform: onChange)
}
func copyFrame(in space: CoordinateSpace, to binding: Binding<CGRect>) -> some View {
self.readFrame(in: space) { frame in
binding.wrappedValue = frame
}
}
}

SwiftUI Custom View repeat forever animation show as unexpected

I created a custom LoadingView as a Indicator for loading objects from internet. When add it to NavigationView, it shows like this
enter image description here
I only want it showing in the middle of screen rather than move from top left corner
Here is my Code
struct LoadingView: View {
#State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.primaryDota, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
and my content view
struct ContentView: View {
var body: some View {
NavigationView {
LoadingView()
.frame(width: 30, height: 30)
}
}
}
This looks like a bug of NavigationView: without it animation works totally fine. And it wan't fixed in iOS15.
Working solution is waiting one layout cycle using DispatchQueue.main.async before string animation:
struct LoadingView: View {
#State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.red, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
}
This is a bug from NavigationView, I tried to kill all possible animation but NavigationView ignored all my try, NavigationView add an internal animation to children! here all we can do right now!
struct ContentView: View {
var body: some View {
NavigationView {
LoadingView()
}
}
}
struct LoadingView: View {
#State private var isLoading: Bool = Bool()
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.blue, lineWidth: 5.0)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: isLoading)
.onAppear { DispatchQueue.main.async { isLoading.toggle() } }
}
}

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

How to get bounce animation in SwiftUI?

How do I achieve the bounce animation similar to the one on macOS dock when an application needs attention and it bounces on the dock. SwiftUI seems to only have ease-curves and spring, which don't really emphasize the way a bounce does.
I've attempted various spring animations and combinations of ease curves and timingCurves to try and get the bounce animation working but nothing really did the job.
The closest to a bounce animation is interpolating spring but the main problem with these animations is that they overshoot during the animation, whereas bounce animations don't.
struct ContentView: View {
#State var bounce = false
#State private var initialVelocity:Double = 1
#State private var damping:Double = 1
#State private var stiffness:Double = 1
var body: some View {
VStack {
Circle().fill(Color.red).frame(width:50,height:50)
.offset(y: bounce ? 0 : -80)
.animation(.interpolatingSpring(stiffness: self.stiffness, damping: self.damping, initialVelocity: self.initialVelocity))
HStack(){
Text("stiffness")
Slider(value: $stiffness, in: 0...100)
}
HStack(){
Text("damping")
Slider(value: $damping, in: 0...100)
}
HStack(){
Text("initialVelocity")
Slider(value: $initialVelocity, in: 0...100)
}
Button("Animate" ){
self.bounce.toggle()
}
}.padding(.horizontal)
}
}
What I'm looking for is a bounce animation that replicates gravity, it's a pretty common animation that is available in a lot of games and software
What's about .interpolatingSpring(...)?
Consider the following example but keep in mind, that you might have to play around with the values for stiffness, etc.:
struct ContentView: View {
#State var test = false
var body: some View {
VStack {
Text("Animatable")
.offset(y: test ? 0 : -80)
.animation(.interpolatingSpring(stiffness: 350, damping: 5, initialVelocity: 10))
Button(action: {self.test.toggle()}) {
Text("Animate")
}
}
}
}
Using multiple offsets, delays and easing I was able to get fairly close to replicating that specific animation.
struct ContentView: View {
#State var bounceHeight: BounceHeight? = nil
func bounceAnimation() {
withAnimation(Animation.easeOut(duration: 0.3).delay(0)) {
bounceHeight = .up100
}
withAnimation(Animation.easeInOut(duration: 0.04).delay(0)) {
bounceHeight = .up100
}
withAnimation(Animation.easeIn(duration: 0.3).delay(0.34)) {
bounceHeight = .base
}
withAnimation(Animation.easeOut(duration: 0.2).delay(0.64)) {
bounceHeight = .up40
}
withAnimation(Animation.easeIn(duration: 0.2).delay(0.84)) {
bounceHeight = .base
}
withAnimation(Animation.easeOut(duration: 0.1).delay(1.04)) {
bounceHeight = .up10
}
withAnimation(Animation.easeIn(duration: 0.1).delay(1.14)) {
bounceHeight = .none
}
}
var body: some View {
VStack {
Text("☝️")
.font(.system(size: 200))
.multilineTextAlignment(.center)
.minimumScaleFactor(0.2)
.lineLimit(1)
}
.padding(8)
.frame(width: 72, height: 72)
.background(.purple.opacity(0.4))
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.shadow(radius: 3)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(.purple, lineWidth: 2)
)
.offset(y: bounceHeight?.associatedOffset ?? 0)
.onTapGesture {
bounceAnimation()
}
}
}
enum BounceHeight {
case up100, up40, up10, base
var associatedOffset: Double {
switch self {
case .up100:
return -100
case .up40:
return -40
case .up10:
return -10
case .base:
return 0
}
}
}

How to correctly do up an adjustable split view in SwiftUI?

This is my first time trying out SwiftUI, and I am trying to create a SwiftUI view that acts as a split view, with an adjustable handle in the center of the two views.
Here's my current code implementation example:
struct ContentView: View {
#State private var gestureTranslation = CGSize.zero
#State private var prevTranslation = CGSize.zero
var body: some View {
VStack {
Rectangle()
.fill(Color.red)
.frame(height: (UIScreen.main.bounds.height / 2) + self.gestureTranslation.height)
RoundedRectangle(cornerRadius: 5)
.frame(width: 40, height: 3)
.foregroundColor(Color.gray)
.padding(2)
.gesture(DragGesture()
.onChanged({ value in
self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)
})
.onEnded({ value in
self.gestureTranslation = CGSize(width: value.translation.width + self.prevTranslation.width, height: value.translation.height + self.prevTranslation.height)
self.prevTranslation = self.gestureTranslation
})
)
Rectangle()
.fill(Color.green)
.frame(height: (UIScreen.main.bounds.height / 2) - self.gestureTranslation.height)
}
}
}
How it looks like now:
[
This kinda works, but when dragging the handle, it is very glitchy, and that it seems to require a lot of dragging to reach a certain point.
Please advice me what went wrong. Thank you.
See How to change the height of the object by using DragGesture in SwiftUI? for a simpler solution.
My version of that:
let MIN_HEIGHT = CGFloat(50)
struct DragViewSizeView: View {
#State var height: CGFloat = MIN_HEIGHT
var body: some View {
VStack {
Rectangle()
.fill(Color.red)
.frame(width: .infinity, height: height)
HStack {
Spacer()
Rectangle()
.fill(Color.gray)
.frame(width: 100, height: 10)
.cornerRadius(10)
.gesture(
DragGesture()
.onChanged { value in
height = max(MIN_HEIGHT, height + value.translation.height)
}
)
Spacer()
}
VStack {
Text("my o my")
Spacer()
Text("hoo hah")
}
}
}
}
struct DragTestView: View {
var body: some View {
VStack {
DragViewSizeView()
Spacer() // If comment this line the result will be as on the bottom GIF example
}
}
}
struct DragTestView_Previews: PreviewProvider {
static var previews: some View {
DragTestView()
}
}
From what I have observed, the issue seems to be coming from the handle being repositioned while being dragged along. To counteract that I have set an inverse offset on the handle, so it stays in place. I have tried to cover up the persistent handle position as best as I can, by hiding it beneath the other views (zIndex).
I hope somebody else got a better solution to this question. For now, this is all that I have got:
import PlaygroundSupport
import SwiftUI
struct SplitView<PrimaryView: View, SecondaryView: View>: View {
// MARK: Props
#GestureState private var offset: CGFloat = 0
#State private var storedOffset: CGFloat = 0
let primaryView: PrimaryView
let secondaryView: SecondaryView
// MARK: Initilization
init(
#ViewBuilder top: #escaping () -> PrimaryView,
#ViewBuilder bottom: #escaping () -> SecondaryView)
{
self.primaryView = top()
self.secondaryView = bottom()
}
// MARK: Body
var body: some View {
GeometryReader { proxy in
VStack(spacing: 0) {
self.primaryView
.frame(height: (proxy.size.height / 2) + self.totalOffset)
.zIndex(1)
self.handle
.gesture(
DragGesture()
.updating(self.$offset, body: { value, state, _ in
state = value.translation.height
})
.onEnded { value in
self.storedOffset += value.translation.height
}
)
.offset(y: -self.offset)
.zIndex(0)
self.secondaryView.zIndex(1)
}
}
}
// MARK: Computed Props
var handle: some View {
RoundedRectangle(cornerRadius: 5)
.frame(width: 40, height: 3)
.foregroundColor(Color.gray)
.padding(2)
}
var totalOffset: CGFloat {
storedOffset + offset
}
}
// MARK: - Playground
let splitView = SplitView(top: {
Rectangle().foregroundColor(.red)
}, bottom: {
Rectangle().foregroundColor(.green)
})
PlaygroundPage.current.setLiveView(splitView)
Just paste the code inside XCode Playground / Swift Playgrounds
If you found a way to improve my code please let me know.