I made the following PulsatingView:
struct PulsatingView: View {
#State private var opacity = 0.7
#State private var scale: CGFloat = 0.5
var body: some View {
VStack {
ZStack {
Circle()
.fill(Color.black.opacity(opacity))
.animation(.easeInOut(duration: 2).delay(1.5).repeatForever(autoreverses: false))
.frame(width: 7, height: 7)
.scaleEffect(scale)
.animation(.easeIn(duration: 2.5).delay(1).repeatForever(autoreverses: false))
Circle()
.fill(Color.black)
.frame(width: 7, height: 7)
}
}
.onAppear {
opacity = 0
scale = 5
}
}
}
It produces the following result: (click to see animation; it's a .gif)
However, when I add it to a view, inside a VStack for example, this happens: (click to see animation; it's a .gif)
I have no idea why this happens or how I can fix it. I've already tried adding modifiers such as .fixedSize(horizontal: true, vertical: true) or .frame(width: 50, height: 50) to it but that doesn't help.
Does anyone know what's going on?
Try using animation with joined value, like
Circle()
.fill(Color.black.opacity(opacity))
.animation(.easeInOut(duration: 2).delay(1.5).repeatForever(autoreverses: false), value: opacity) // << here !!
.frame(width: 7, height: 7)
.scaleEffect(scale)
.animation(.easeIn(duration: 2.5).delay(1).repeatForever(autoreverses: false), value: scale) // << here !!
Important: if it is started in NavigationView then animatable state should be activated postponed. The reason and investigation details was provided in https://stackoverflow.com/a/66643096/12299030.
This issue is not about your code it is because NavigationView to fix it we should use DispatchQueue.main.async I wanted refactor your code as well, hope help you, also you can avoid hard coded value as much as possible, for example you can apply the frame in outside of view:
struct ContentView: View {
var body: some View {
CircleView(lineWidth: 0.5, color: .red, scale: 5.0)
.frame(width: 7, height: 7)
}
}
struct CircleView: View {
let lineWidth: CGFloat
let color: Color
let scale: CGFloat
#State private var startAnimation: Bool = Bool()
var body: some View {
Circle()
.strokeBorder(style: .init(lineWidth: startAnimation ? .zero : lineWidth)).opacity(startAnimation ? .zero : 1.0)
.scaleEffect(startAnimation ? scale : 1.0)
.overlay( Circle() )
.foregroundColor(color)
.onAppear() { DispatchQueue.main.async { startAnimation.toggle() } }
.animation(.easeIn(duration: 2.5).delay(1).repeatForever(autoreverses: false), value: startAnimation)
}
}
I finally managed to find a solution to this on my own project - I found the only solution to get the animated view working within a NavigationView was to move the animation inside the onAppear call, like this:
Circle()
.fill(Color.black.opacity(opacity))
.onAppear {
withAnimation(.easeInOut(duration: 2).repeatForever(autoreverses: false)) {
opacity = 0
}
}
Hope this can help someone else if they get stuck on the same issue I ended up having!
Related
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() } }
}
}
I am relatively new to SwiftUI and I am trying to implement animated loader to my app. It works fine until I scroll my content inside scroll view down and then move back to trigger pull to refresh. Circles started to jump though they should only increase and decrease their size one by one.
My code is
struct ActivityIndicator: View {
var isShowing: Bool
#State private var shouldAnimate = false
init(isShowing: Bool) {
self.isShowing = isShowing
}
var body: some View {
HStack(alignment: .center) {
Circle()
.fill(Color.mainAccent)
.frame(width: 20, height: 20)
.scaleEffect(shouldAnimate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 0.5).repeatForever())
Circle()
.fill(Color.mainAccent)
.frame(width: 20, height: 20)
.scaleEffect(shouldAnimate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 0.5).repeatForever().delay(0.3))
Circle()
.fill(Color.mainAccent)
.frame(width: 20, height: 20)
.scaleEffect(shouldAnimate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 0.5).repeatForever().delay(0.6))
}
.opacity(self.isShowing ? 1 : 0)
.onAppear {
self.shouldAnimate = true
}
}
}
I have read few articles related to my case and it seems that I might had to use withAnimation (Explicit) instead .animation (Implicit) but I can't make it work properly.
Btw I connect my Activity Indicator to the scrollView using Loading View modifier and it looks like this
struct LoadingView: ViewModifier {
#Binding var isShowing: Bool
func body(content: Content) -> some View {
ZStack(alignment: .center) {
content
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
ActivityIndicator(isShowing: isShowing)
}
}
}
Any ideas and suggestions are appreciated I am really stuck. Thanks
Try to link all your animations to related state, like
var body: some View {
HStack(alignment: .center) {
Circle()
.fill(Color.mainAccent)
.frame(width: 20, height: 20)
.scaleEffect(shouldAnimate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 0.5).repeatForever(),
value: shouldAnimate) // << here !!
If you have any problems with the previous answer, try removing the .animation and replace it with withAnimation:
https://developer.apple.com/documentation/swiftui/withanimation(_:_:)
The advantage of that is that it does not affect other animations.
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
}
}
}
I have a problem regarding behavior of an animated loading view. The loading view shows up while the network call. I have an isLoading #Published var inside my viewModel and the ActivityIndicator is shown inside a ZStack in my view. The Activityindicator is a custom view where I animate a trimmed circle - rotate it. Whenever the activity indicator is shown inside my mainView it has a weird transition when appearing- it is transitioned from the top left corner to the center of the view. Does anyone know why is this happening? I attach the structs with the code and 3 pictures with the behavior.
ActivityIndicator:
struct OrangeActivityIndicator: View {
var style = StrokeStyle(lineWidth: 6, lineCap: .round)
#State var animate = false
let orangeColor = Color.orOrangeColor
let orangeColorOpaque = Color.orOrangeColor.opacity(0.5)
init(lineWidth: CGFloat = 6) {
style.lineWidth = lineWidth
}
var body: some View {
ZStack {
CircleView(animate: $animate, firstGradientColor: orangeColor, secondGradientColor: orangeColorOpaque, style: style)
}.onAppear() {
self.animate.toggle()
}
}
}
struct CircleView: View {
#Binding var animate: Bool
var firstGradientColor: Color
var secondGradientColor: Color
var style: StrokeStyle
var body: some View {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [firstGradientColor, secondGradientColor]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.transition(.opacity)
.animation(Animation.linear(duration: 0.7) .repeatForever(autoreverses: false), value: animate)
}
}
The view is use it in :
struct UserProfileView: View {
#ObservedObject var viewModel: UserProfileViewModel
#Binding var lightMode: ColorScheme
var body: some View {
NavigationView {
ZStack {
VStack(alignment: .center, spacing: 12) {
HStack {
Text(userProfileEmail)
.font(.headline)
.foregroundColor(Color(UIColor.label))
Spacer()
}.padding(.bottom, 16)
SettingsView(userProfile: $viewModel.userProfile, isDarkMode: $viewModel.isDarkMode, lightMode: $lightMode, location: viewModel.locationManager.address, viewModel: viewModel)
ButtonsView( userProfile: $viewModel.userProfile)
Spacer()
}.padding([.leading, .trailing], 12)
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
// .animation(nil)
}
}
}
}
}
I also tried with animation nil but it doesn't seem to work.
Here are the pictures:
Here is a possible solution - put it over NavigationView. Tested with Xcode 12.4 / iOS 14.4
NavigationView {
// .. your content here
}
.overlay( // << here !!
VStack {
if isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
)
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.