Animated view floats into place weirdly SwiftUI - ios

So I have a circular progress bar that is declared like this
struct ProgressBar: View {
var progress: CGFloat
var body: some View {
let gradient = LinearGradient(...)
ZStack {
Circle()
.stroke(lineWidth: 25)
.opacity(0.3)
.foregroundColor(Color.secondary)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(gradient ,style: StrokeStyle(lineWidth: 25.0, lineCap: .round, lineJoin: .round))
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
}
}
}
The variable progress is passed into the view and is just simple division of value/total where I have two buttons to reduce or increase value and I use the animation to update the progress cleanly.
I call this view in another view called DetailView
However for some reason the second secondView floats in from the top when I navigate to DetailView from another view. What is happening?
Im not sure if this is a common issue but I can share a video if that might help you.
As requested here is an example.
import SwiftUI
struct ContentView: View {
#State var bookData: [Book] = load("list")
#State private var selectedBook: Book? = nil
#State var showOnboarding = false
var body: some View {
NavigationView {
Form {
Section{
ForEach(bookData){ bookDetail in
NavigationLink(
destination: PageDetail(bookData: $bookData, book: bookDetail),
label: {
BookView(book: bookDetail)
})
}
}
}
}
}
import SwiftUI
struct PageDetail: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#Binding var bookData: [Book]
#State var count = 0
var progress: CGFloat{
let page = value
let total = Int(book.total) ?? 1
let answer = CGFloat(page)/CGFloat(total)
return answer
}
var value: Int{
let page = Int(book.page) ?? 1
let cnt = count
let calc = page + cnt
return calc
}
var book: Book{
var body: some View {
ZStack{
LinearGradient(...)
VStack {
VStack(spacing: -25){
ProgressBar(page: value,total: book.total ,progress: progress)
.frame(width: 250, height: 300)
.padding(.horizontal, 20)
HStack {
Button(action: {
self.count -= 1
}, label: {
Image(systemName: "minus.circle")
})
Button(action: {
self.count += 1
}, label: {
Image(systemName: "plus.circle")
})
}
}
}
}
}
struct ProgressBar: View {
var page: Int
var total: String
var progress: CGFloat
var body: some View {
let gradient = LinearGradient(...)
ZStack {
Circle()
.stroke(lineWidth: 25)
.opacity(0.3)
.foregroundColor(Color.secondary)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(gradient ,style: StrokeStyle(lineWidth: 25.0, lineCap: .round, lineJoin: .round))
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
}
}
}
struct PageDetail_Previews: PreviewProvider {
#State static var previewed = testData
static var previews: some View {
PageDetail(bookData: $previewed, book: previewed[0])
}
}

Only idea - try to make animation value dependent, like below
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(gradient ,style: StrokeStyle(lineWidth: 25.0, lineCap: .round, lineJoin: .round))
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear, value: progress) // << here !!

Related

SwiftUI sheet reappears after dismissed on Apple Watch

I'm developing a fitness app for the Apple Watch that lets you choose an intensity before starting a workout. I want to present the intensity picker before the workout starts, so I tried presenting it as a sheet before navigating to the actual workout view. The problem is that when I try to dismiss the sheet, it is dismissed but it comes right back. I'm using Xcode 14.2 and watchOS 9.1.
This is the main view and also the view that presents the said sheet (the first one, controlled by showingZonePickerView):
import SwiftUI
#main
struct Wise_Watch_AppApp: App {
#StateObject private var workoutManager = WorkoutManager()
#SceneBuilder var body: some Scene {
WindowGroup {
NavigationView {
WorkoutView()
}
.sheet(isPresented: $workoutManager.showingZonePickerView, onDismiss: {
workoutManager.showingZonePickerView = false
}) {
WorkoutLevelPickerView(total: 5, completed: 1)
.toolbar(.hidden)
}
.sheet(isPresented: $workoutManager.showingSummaryView) {
SummaryView()
}
.environmentObject(workoutManager)
}
}
}
This is the view where the user can pick the preferred workout:
import SwiftUI
import HealthKit
struct WorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#State var linkActive: Bool = false
var workoutTypes: [HKWorkoutActivityType] = [.cycling, .running, .walking]
var workoutDictionary: Dictionary<String, HKWorkoutActivityType> = [
"figure.outdoor.cycle" : .cycling,
"figure.run" : .running,
"figure.walk" : .walking
]
var body: some View {
List(Array(workoutDictionary.keys), id: \.self) { workoutType in
NavigationLink(
destination: SessionPagingView(),
tag: workoutDictionary[workoutType]!,
selection: $workoutManager.selectedWorkout
) {
Label("\(workoutDictionary[workoutType]!.name)", systemImage: workoutType)
.font(.subheadline)
.fontWeight(.semibold)
.padding()
}
.padding(EdgeInsets(top: 15, leading: 5, bottom: 15, trailing: 5))
}
.listStyle(.carousel)
.navigationBarTitle("Workouts")
.onAppear {
workoutManager.requestAuthorization()
}
}
}
This is the view to which the app navigates when a NavigationLink is pressed:
import SwiftUI
import WatchKit
import ConfettiSwiftUI
struct SessionPagingView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#State private var selection: Tab = .metrics
#State private var counter: Int = 0
#State private var isViewHidden: Bool = true
enum Tab {
case metrics, nowPlaying, milestone
}
var body: some View {
if(isViewHidden) {
sessionView.hidden()
} else {
sessionView
}
}
var sessionView: some View {
TabView(selection: $selection) {
ForEach(workoutManager.tabItems) { item in
VStack {
Text("Congrats!")
.font(.title2)
.fontWeight(.bold)
.confettiCannon(counter: $counter, num: 40, radius: 200)
Text("You just reached")
.font(.subheadline)
.foregroundColor(.cyan)
.tag(Tab.milestone)
Text("\(item.value) km")
.font(.subheadline)
.foregroundColor(.cyan)
.tag(Tab.milestone)
}
}
MetricsView().tag(Tab.metrics)
NowPlayingView().tag(Tab.nowPlaying)
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.onAppear {
workoutManager.showingZonePickerView = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isViewHidden = false
}
}
.onChange(of: workoutManager.running) { _ in
displayMetricsView()
}
.onChange(of: workoutManager.tabItems) { _ in
if(workoutManager.tabItems.count > 0) {
displayMilestoneView()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: {
displayMetricsView()
})
}
}
.ignoresSafeArea()
.animation(.easeInOut, value: self.selection)
}
private func displayMetricsView() {
withAnimation {
selection = .metrics
}
}
private func displayMilestoneView() {
withAnimation {
selection = .milestone
WKInterfaceDevice.current().play(.notification)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: {
counter += 1
})
}
}
}
This is the actual view that I'm presenting inside the sheet:
import SwiftUI
struct WorkoutLevelPickerView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#Environment(\.dismiss) private var dismiss
let total: Int
#State var lineWidth: CGFloat = 16
#State var color: Color = .green
#State var completed: Double = 1.0
#State var currentZone: HeartRateZone = zones[0]
#State var isScrolling: Bool = false
var body: some View {
VStack {
ZStack {
CircleLabelView(
radius: 30,
tracking: 0,
size: .init(width: 120, height: 120),
text: currentZone.intensity.uppercased()
)
.font(.headline)
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: Double(currentZone.labelRotationAngleModifier * currentZone.intensity.count / 2)))
.opacity(isScrolling ? 0 : 1)
.animation(.easeInOut, value: isScrolling)
WorkoutLevelPickerBackgroundView(total: total, lineWidth: lineWidth)
withAnimation(.spring()) {
WorkoutLevelPickerProgressView(total: total, completed: Int(completed), lineWidth: lineWidth, zone: currentZone)
}
VStack {
Button {
dismiss()
} label: {
Image(systemName: currentZone.iconName)
.padding()
.font(.title2)
}
.frame(width: 70, height: 70)
}
}
.frame(width: 150, height: 150)
.focusable()
.digitalCrownRotation(
detent: $completed.animation(.spring()),
from: 1.0,
through: 5.0,
by: 1.0,
sensitivity: .low,
isContinuous: false,
isHapticFeedbackEnabled: true,
onChange: { _ in
isScrolling = true
},
onIdle: {
isScrolling = false
}
)
.digitalCrownAccessory(.hidden)
.onChange(of: completed) {_ in
if(Int(completed) != currentZone.id) {
currentZone = zones[Int(completed) - 1]
print(currentZone.tint.description)
}
}
}
}
}
This is a video of the flow that generates my problem:
I tried presenting the sheet from other views. I also tried dismissing the sheet through a binding, not through the dismiss action. On all these changes, the outcome was the same as before.
Edit:
Forgot to mention that, when the sheet is presented, pressing the side button will dismiss it and the app will work as intended until the user tries to start a new workout.
As Paulw11 mentioned in his comment, the problem was that I was constantly setting showingZonePickerView on true when the sheet was dismissed and the view reappeared.

how to infinite rotation in circular way by SwiftUI?

I could not rotate the green part in the circular path.
I want to set dynamically infinite time.
I created a struct for ProgressView and want to set it as ProgressView in my project.
how to implement the red and black colour curve design in the rotation
path
struct CircularProgressView: View {
#State var progressValue: Float = 0.0
var body: some View {
ZStack {
ProgressBar(progress: self.$progressValue)
.frame(width: 150.0, height: 150.0)
.padding(40.0)
}.onAppear{
self.incrementProgress()
}
}
func incrementProgress() {
let randomValue = Float([0.012, 0.022, 0.034, 0.016, 0.11, 0.012].randomElement()!)
self.progressValue += randomValue
}
}
struct ProgressBar: View {
#Binding var progress: Float
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.red)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 2.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.green)
.rotationEffect(Angle(degrees: 270))
.animation(.linear)
}
}
}
This is the Preview struct.
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
CircularProgressView()
}
}
Thanks in advance for helping me.
struct CircularProgressView: View {
#State var progressValue: Float = 0.0
#State private var isLoaderVisible: Bool = false
var degree = 90
var body: some View {
ZStack {
ProgressBar(progress: self.$progressValue)
.frame(width: 150.0, height: 150.0)
.padding(40.0)
}.overlay(LoaderView(showLoader: isLoaderVisible))
.onAppear {
isLoaderVisible.toggle()
}
}
}
struct ProgressBar: View {
#Binding var progress: Float
#State var degree:Double = 90
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20.0)
.foregroundColor(Color.green)
}
}
}
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
CircularProgressView()
}
}
// enum for LoaderView
public enum LoaderAnimation {
case low, medium, high
var animationSpeed: Double {
switch self {
case .low: return 1.0
case .medium: return 0.8
case .high: return 10.2
}
}
}
This this the LoaderView which we set on overlay and animate
public struct LoaderView: View {
var loaderAnimationSpeed: LoaderAnimation? = .high
var showLoader: Bool = false
public var body: some View {
GeometryReader { reader in
ZStack {
Circle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.yellow)
Circle()
.trim(from: 0.6, to: 1)
.stroke(.green, lineWidth: 20)
.rotationEffect(.degrees(showLoader ? 360 : 0))
.animation(Animation.easeInOut(duration: showLoader ? loaderAnimationSpeed?.animationSpeed ?? 0.0 : 1).repeatForever(autoreverses: false), value: showLoader)
}
}
}
public init( loaderAnimationSpeed: LoaderAnimation? = .medium, showLoader: Bool) {
self.loaderAnimationSpeed = loaderAnimationSpeed
self.showLoader = showLoader
}
}

How to give condition in SwiftUI while using parent and child view?

I have custom view as below which i am using in another view
import SwiftUI
public struct TopSheet<Content >: View where Content : View {
private var content: () -> Content
#State private var arrowOffset: Double = 0
public init(#ViewBuilder content: #escaping () -> Content) { self.content = content }
public func expandRatio() -> Double { return max((currentHeight - minHeight) / contentHeight, 0) }
public var body: some View {
HStack(alignment: .center) {
Arrow(offset: arrowOffset)
.stroke(Color.pink, style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
.frame(width: 30, height: 4)
Spacer()
}
}
}
I am using above view in below view
import SwiftUI
struct PassengerView: View {
#State private var passengers: [String] = ["Joe Black", "Eva Green", "Jared Leto"]
var body: some View {
TopSheet {
VStack {
ForEach($passengers, id: \.self) { passenger in
HStack {
Text(passenger.wrappedValue)
Spacer()
}
}
}
.padding(.horizontal, .afklPaddingL)
}
}
}
Here I want to give one condition Arrow() from Topsheet should visible only if passenger count is greater than 1.
I am not sure how should i give this condition as both are in diff view.
Try this:
Add a var to your TopSheet:
var count: Int
Change the constructor to:
public init(count: Int, #ViewBuilder content: #escaping () -> Content) {
self.count = count
self.content = content
}
and your body to:
public var body: some View {
HStack(alignment: .center) {
if count > 1 {
Arrow(offset: arrowOffset)
.stroke(Color.pink, style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
.frame(width: 30, height: 4)
}
Spacer()
}
}
call it like:
TopSheet(count: passengers.count) {
VStack {
........
As passengers is a #State variable it will reavaluate your views as it changes.

How to make a view's height animate from 0 to height in SwiftUI?

I'd like to make this bar's height (part of a larger bar graph) animate on appear from 0 to its given height, but I can't quite figure it out in SwiftUI? I found this code that was helpful, but with this only the first bar (of 7) animates:
struct BarChartItem: View {
var value: Int
var dayText: String
var topOfSCaleColorIsRed: Bool
#State var growMore: CGFloat = 0
#State var showMore: Double = 0
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(getBarColor())
.frame(width: 40, height: growMore)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.linear(duration: 7.0)) {
growMore = CGFloat(value)
showMore = 1
}
}
}.opacity(showMore)
Text(dayText.uppercased())
.font(.caption)
}
}
And this is the chart:
HStack(alignment: .bottom) {
ForEach(pastRecoveryScoreObjects) { pastRecoveryScoreObject in
BarChartItem(value: pastRecoveryScoreObject.recoveryScore, dayText: "\(pastRecoveryScoreObject.date.weekdayName)", topOfSCaleColorIsRed: false)
}
}
This works: you can get the index of the ForEach loop and delay each bar's animation by some time * that index. All the bars are connected to the same state variable via a binding, and the state is set immediately on appear. You can play with the parameters to tune it to your own preferences.
struct TestChartAnimationView: View {
let values = [200, 120, 120, 90, 10, 80]
#State var open = false
var animationTime: Double = 0.25
var body: some View {
VStack {
Spacer()
HStack(alignment: .bottom) {
ForEach(values.indices, id: \.self) { index in
BarChartItem(open: $open, value: values[index])
.animation(
Animation.linear(duration: animationTime).delay(animationTime * Double(index))
)
}
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
open = true
}
}
}
}
struct BarChartItem: View {
#Binding var open: Bool
var value: Int
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(Color.blue)
.frame(width: 20, height: open ? CGFloat(value) : 0)
Text("Week".uppercased())
.font(.caption)
}
}
}
Edit: ForEach animations are kind of weird. I'm not sure why, but this works:
struct ContentView: View {
let values = [200, 120, 120, 90, 10, 80]
var body: some View {
HStack(alignment: .bottom) {
ForEach(values, id: \.self) { value in
BarChartItem(totalValue: value)
.animation(.linear(duration: 3))
}
}
}
}
struct BarChartItem: View {
#State var value: Int = 0
var totalValue = 0
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(Color.blue)
.frame(width: 20, height: CGFloat(value))
Text("Week".uppercased())
.font(.caption)
}.onAppear {
value = totalValue
}
}
}
Instead of applying the animation in the onAppear, I applied an implicit animation inside the ForEach.
Result:

Cannot convert value of type 'Int?' to expected argument type 'Binding<Int>' SwiftUI

I created a circularprogress view to be able to show a progress bar according to the steps data. But for some reason I can not reach to the step.count inside my stepView file.
This is my StepView
struct StepView: View {
private var healthStore: HealthStore?
#State private var presentClipboardView = true
#State private var steps: [Step] = [Step]()
init() {
healthStore = HealthStore()
}
private func updateUIFromStatistics(_ statisticsCollection: HKStatisticsCollection) {
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
statisticsCollection.enumerateStatistics(from: startOfDay, to: now) { (statistics, stop) in
let count = statistics.sumQuantity()?.doubleValue(for: .count())
let step = Step(count: Int(count ?? 0), date: statistics.startDate, wc: Double(count ?? 0 / 1000 ))
steps.append(step)
}
}
var body: some View {
VStack {
ForEach(steps, id: \.id) { step in
VStack {
HStack{
Text("WC")
Text("\(step.wc)")
}
HStack {
Text("\(step.count ?? 0)")
Text("Total Steps")
}
Text(step.date, style: .date)
.opacity(0.5)
CircularProgress(steps: step.count) //ERROR
Spacer()
}
}
.navigationBarBackButtonHidden(true)
}
.onAppear() {
if let healthStore = healthStore {
healthStore.requestAuthorization { (success) in
if success {
healthStore.calculateSteps { (statisticsCollection) in
if let statisticsCollection = statisticsCollection {
updateUIFromStatistics(statisticsCollection)
}
}
}
}
}
}
.onDisappear() {
self.presentClipboardView.toggle()
}
}
}
and this is my circularprogress view
struct CircularProgress: View {
var steps: Binding<Int>
var body: some View {
ZStack {
Color.progressBarColor
.edgesIgnoringSafeArea(.all)
VStack {
ZStack {
Label()
Outline(steps: steps)
}
}
}
}
}
struct Label: View {
var percentage: CGFloat = 20
var body : some View {
ZStack {
Text(String(format: "%.0f", percentage))
.font(Font.custom("SFCompactDisplay-Bold", size: 56))
}
}
}
struct Outline: View {
var steps: Binding<Int>
var percentage: CGFloat = 20
var colors : [Color] = [Color.trackProgressBarColor]
var body: some View {
ZStack {
Circle()
.fill(Color.clear)
.frame(width: 250, height: 250)
.overlay(
Circle()
.trim(from: 0, to: percentage * 0.01)
.stroke(style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.fill(AngularGradient(gradient: .init(colors: colors), center: .center, startAngle: .zero, endAngle: .init(degrees: 360)))
).animation(.spring(response: 2.0, dampingFraction: 1.0, blendDuration: 1.0))
}
}
}
I am getting this error at stepview WHILE CALLING CIRCULARPROGRESS inside the stepview. I guess I am trying to get the data in the wrong way.
I don't see necessity of binding here, so just replace corresponding places with simple Int:
struct CircularProgress: View {
var steps: Int
and
struct Outline: View {
var steps: Int

Resources