How to animate color change swiftui - swiftui

I am trying to animate the color transition between green, yellow, and red. How would I go about this? I have tried using animation() on the Circle view, but this created weird bugs in which the StrokeStyle seems to be ignored and the whole circle fills with color during the transition.
import SwiftUI
import UserNotifications
struct CircleTimer_Previews: PreviewProvider {
static var previews: some View {
Group {
CircleTimer() }
}
}
struct CircleTimer : View {
#State var to: CGFloat = 1;
#State var start = false
#State var count = 15
let defaultTime = 15
#State var time = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View{
ZStack{
Color.black.opacity(0.06).edgesIgnoringSafeArea(.all)
VStack{
ZStack{
Circle()
.trim(from: 0, to: 1)
.stroke(Color.black.opacity(0.09), style: StrokeStyle(lineWidth: 35, lineCap: .round))
.frame(width: 280, height: 280)
Circle()
.trim(from: 0, to: self.to)
.stroke(self.count > 10 ? Color.green : self.count > 5 ? Color.yellow : Color.red, style: StrokeStyle(lineWidth: 35, lineCap: .round))
.frame(width: 280, height: 280)
.rotationEffect(.init(degrees: -90))
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
VStack{
Text("\(self.count)")
.font(.system(size: 72))
.fontWeight(.bold)
}
}
TimerButtons(start: $start, to: $to, count: $count)
}
}
.onAppear(perform: {
UNUserNotificationCenter.current().requestAuthorization(options: [.badge,.sound,.alert]) { (_, _) in
}
})
.onReceive(self.time) { (_) in
if self.start{
if self.count > 0 {
self.count -= 1
print(self.count)
withAnimation(.default){
self.to = CGFloat(self.count) / 15
print(self.to)
}
}
else{
self.start.toggle()
}
}
}
}
}

As suggested by #Yrb, you must create three separate views of three different colors.

Related

How can I change button status in SwiftUI?

I am trying to change the color programmatically of some buttons in SwiftUI.
The buttons are stored in a LazyVGrid. Each button is built via another view (ButtonCell).
I'm using a #State in the ButtonCell view to check the button state.
If I click on the single button, his own state changes correctly, just modifying the #State var of the ButtonCell view. If I try to do the same from the ContentView nothing is happening.
This is my whole ContentView (and ButtonCell) view struct:
struct ContentView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
var body: some View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(0..<10) { number in
ButtonCell(value: number + 1)
}
}
}
Button(action: {
ButtonCell(value: 0, isEnabled: true)
ButtonCell(value: 1, isEnabled: true)
ButtonCell(value: 1, isEnabled: true)
}){
Rectangle()
.frame(width: 200, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("Change isEnabled state").foregroundColor(.white)
)
}
}
struct ButtonCell: View {
var value: Int
#State var isEnabled:Bool = false
var body: some View {
Button(action: {
print (value)
print (isEnabled)
isEnabled = true
}) {
Rectangle()
.foregroundColor(isEnabled ? Color.red : Color.yellow)
.frame(width: 50, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("\(value)").foregroundColor(.white)
)
}
}
}
}
How I may change the color of a button in the LazyVGrid by clicking the "Change isEnabled state" button?
You need a different approach here. Currently you try to change the State of ButtonCell from the outside. State variables should always be private and therefore should not be changed from outside. You should swap the state and parameters of ButtonCell into a ViewModel. The ViewModels then are stored in the parent View (ContentView) and then you can change the ViewModels and the child views automatically update. Here is a example for a ViewModel:
final class ButtonCellViewModel: ObservableObject {
#Published var isEnabled: Bool = false
let value: Int
init(value: Int) {
self.value = value
}
}
Then store the ViewModels in the ContentView:
struct ContentView: View {
let buttonViewModels = [ButtonCellViewModel(value: 0), ButtonCellViewModel(value: 1), ButtonCellViewModel(value: 2)]
private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
var body: some View {
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
ScrollView {
LazyVGrid(columns: columns, spacing: 0) {
ForEach(0..<3) { index in
ButtonCell(viewModel: buttonViewModels[index])
}
}
}
Button(action: {
buttonViewModels[0].isEnabled.toggle()
}){
Rectangle()
.frame(width: 200, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("Change isEnabled state").foregroundColor(.white)
)
}
}
}
And implement the ObservedObject approach in ButtonCell.
struct ButtonCell: View {
#ObservedObject var viewModel: ButtonCellViewModel
var body: some View {
Button(action: {
print (viewModel.value)
print (viewModel.isEnabled)
viewModel.isEnabled = true
}) {
Rectangle()
.foregroundColor(viewModel.isEnabled ? Color.red : Color.yellow)
.frame(width: 50, height: 50)
.cornerRadius(10)
.shadow(color: .black, radius: 3, x: 1, y: 1)
.padding()
.overlay(
Text("\(viewModel.value)").foregroundColor(.white)
)
}
}
}

SwiftUI Circle View animation glitch

import SwiftUI
struct CircularProgressView: View {
#Binding var progress: Float
private let strokeStyle = StrokeStyle(lineWidth: 30.0, lineCap: .round, lineJoin: .round)
private let rotation = Angle(degrees: 270.0)
var body: some View {
ZStack {
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: strokeStyle)
.opacity(0.2)
.foregroundColor(Color.black)
.rotationEffect(rotation)
.offset(x: 0, y: 10)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: strokeStyle)
.opacity(1)
.foregroundColor(Color.white)
.rotationEffect(rotation)
}
.animation(.linear(), value: progress)
}
}
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
OtherView(progress: 0.6)
}
struct OtherView : View {
#State var progress : Float = 0.0
var body: some View {
ZStack {
Color.yellow
VStack {
CircularProgressView(progress: self.$progress)
.padding(50)
Button(action: {
if(progress >= 1) {
progress = 0
} else {
progress += 0.1
}
}) {
Text("try me")
.frame(width: 200, height: 50)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.blue, lineWidth: 2)
)
.padding(.bottom, 100)
}
}
}.ignoresSafeArea()
}
}
}
What could be the reason?
It is unclear definitely, probably due to undefined size of shapes as a nature... Anyway, seems using drawing group fixes the issue.
Here is a fixed code. Tested with Xcode 13 / iOS 15.
struct CircularProgressView: View {
#Binding var progress: Float
private let strokeStyle = StrokeStyle(lineWidth: 30.0, lineCap: .round, lineJoin: .round)
private let rotation = Angle(degrees: 270.0)
var body: some View {
ZStack {
Group {
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: strokeStyle)
.opacity(0.2)
.foregroundColor(Color.black)
.rotationEffect(rotation)
.offset(x: 0, y: 10)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: strokeStyle)
.opacity(1)
.foregroundColor(Color.white)
.rotationEffect(rotation)
}
.padding() // << compensate offset within own bounds
}
.drawingGroup() // << here !!
.animation(.linear, value: progress)
}
}
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
OtherView(progress: 0.6)
}
struct OtherView : View {
#State var progress : Float = 0.0
var body: some View {
ZStack {
Color.yellow
VStack {
CircularProgressView(progress: self.$progress)
.padding()
Button(action: {
if(progress >= 1) {
progress = 0
} else {
progress += 0.1
}
}) {
Text("try me")
.frame(width: 200, height: 50)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.blue, lineWidth: 2)
)
.padding(.bottom, 100)
}
}
}.ignoresSafeArea()
}
}
}

SwiftUI DragGesture blocking List vertical scroll?

i am trying to add swipe inside list cell , swipe to show more options such as Delete, archive etc .
The swipe is working just fine , but the List ( vertical scroll ) is no longer scrolling up down .
Cell Bite :
import SwiftUI
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
//Swipe to custom options ,by "Jack" this option not yet available in SwiftUI
let drag = DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged {
if (self.offset.width > 0 ){ return }
self.offset.width = $0.translation.width
}.onEnded {
if $0.translation.width < -100 {
self.offset = .init(width: -100, height: 0)
} else {
self.offset = .zero
}
}
ZStack{
Rectangle().foregroundColor(.blue).offset(x: offset.width, y: offset.height)
.gesture(drag)
.animation(.easeIn, value: offset)
Text("test").foregroundColor(.white)
}.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 100,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
struct cl_task_Previews: PreviewProvider {
static var previews: some View {
cl_task().previewLayout(.sizeThatFits)
}
}
List main view :
struct Item {
let uuid = UUID()
let value: String
}
struct w_tasks: View {
#State private var items = [Item]()
var body: some View {
ZStack {
List(self.items, id: \.uuid) {item in
cl_task()
}
.simultaneousGesture(DragGesture().onChanged({ value in
//Scrolled
}))
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
self.items.append(Item(value: "Item"))
}, label: {
Text("+")
.font(.system(size: 50))
.frame(width: 77, height: 70)
.foregroundColor(Color.white)
.padding(.bottom, 7)
})
.background(Color(hex : "#216D94"))
.cornerRadius(38.5)
.padding()
.shadow(color: Color.black.opacity(0.3),
radius: 3,
x: 3,
y: 3)
}
}
}
}
}
struct w_tasks_Previews: PreviewProvider {
static var previews: some View {
w_tasks()
}
}
I've posted my question after spending hours solving this issue as i am new to SwiftUI , any advice how to solve it ?
The solution is to give different distance for swipe, like below
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
// give 25 distance makes vertical scroll enabled !!
let drag = DragGesture(minimumDistance: 25, coordinateSpace: .local)
.onChanged {
Tested with Xcode 12.4 / iOS 14.4

Why the text appear at loading with animation?

I want to hide some text behind the NavigationBarTitle and show it when user press a button, it's working fine. The only problem is with the animation. At loading we see the text moving behind the NavigationBarTitle and that's not what i want.
How can i fix this?
I tried with offset and position but it's not working...
Code :
import SwiftUI
struct TestZStackNavigationView: View {
#State var isShowed = false
let screenSize: CGRect = UIScreen.main.bounds
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 0){
Text("Im the hidden text")
.fontWeight(.bold)
.foregroundColor(Color.white)
.frame(width: screenSize.width, height: 40, alignment: .center)
.background(Color.red)
// .offset(y: self.isShowed ? -315 : -355)
.position(x: screenSize.width / 2, y: self.isShowed ? 20 : -40)
.animation(.easeIn(duration: 0.5))
Button(action: {
self.isShowed.toggle()
}) {
Text("click me")
}
.navigationBarTitle(Text("Navigation Bar Title"), displayMode:.inline)
}
}
}
}
struct TestZStackNavigationView_Previews: PreviewProvider {
static var previews: some View {
TestZStackNavigationView()
}
}
There are two possibilities
make animation per-state (and no other changes)
.animation(.easeIn(duration: 0.5), value: isShowed)
replace implicit animation as modifier and add explicit animation in action
.position(x: screenSize.width / 2, y: self.isShowed ? 20 : -40)
// .animation(.easeIn(duration: 0.5)) // remove from here
Button(action: {
withAnimation(Animation.easeIn(duration: 0.5)) { // << add this
self.isShowed.toggle()
}
}) {
Text("click me")
}
I want to use it now with an ObservedObject but i got that same behavior as first code. Why?
struct TestZStackNavigationView: View {
// #State var isShowed = false
let screenSize: CGRect = UIScreen.main.bounds
#ObservedObject var online = NetStatus()
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 0){
Text("Im the hidden text")
.fontWeight(.bold)
.foregroundColor(Color.white)
.frame(width: screenSize.width, height: 40, alignment: .center)
.background(Color.red)
.position(x: screenSize.width / 2, y: online.connected ? -40 : 20)
.animation(.easeIn(duration: 0.5), value: online.connected)
.navigationBarTitle(Text("Navigation Bar Title"), displayMode:.inline)
}
}
}
}

iOS 14 weird behavior with Timer.publish

I did my best to pull out only a small part of my larger project that displays this odd behavior. The intention is for one random number to be added to the array and displayed every 3 seconds. In iOS 13 each number slides in from the left every 3 seconds and everything works as expected. What I see in iOS 14 is that 4 numbers are added every 3 seconds. Does anyone understand why this would be happening? Thanks in advance!
import SwiftUI
struct ContentView: View {
#State private var calledNumbers = CalledNumbers()
#State private var timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
#State private var inProgress = false
var body: some View {
Button(action: {
if !self.inProgress {
self.calledNumbers.startOver()
print("Start timer")
self.timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
}
else {
print("Stop timer")
self.timer.upstream.connect().cancel()
}
self.inProgress.toggle()
})
{
if(self.inProgress == false) {
Text("S T A R T")
.font(.system(size: 22))
.fontWeight(.heavy)
.frame(width: 200, height: 35, alignment: .center)
.background(Capsule()
.fill(Color.green))
.cornerRadius(35)
.foregroundColor(.white)
.padding(.bottom, 2)
}
else {
Text("S T O P")
.font(.system(size: 22))
.fontWeight(.heavy)
.frame(width: 200, height: 35, alignment: .center)
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(35)
.onReceive(self.timer) { _ in
self.timer.upstream.connect().cancel()
print("CALL NEXT NUMBER")
self.calledNumbers.callNextNumber()
self.timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
}
}
}
ZStack {
RoundedRectangle(cornerRadius: 35)
.frame(width: UIScreen.main.bounds.size.width - 19, height: 40, alignment: .center)
.foregroundColor(.clear)
.padding(.bottom, 2)
HStack {
ForEach(self.calledNumbers.calledNumberList.reversed().filter {self.checkCount(number: $0)}, id: \.self) { number in
Text("\(String(number))")
.font(.custom("Menlo", size: 20))
.fontWeight(.black)
.frame(width: 40, height: 40, alignment: .center)
.background(Color.red)
.clipShape(Circle())
.foregroundColor(.white)
.transition(AnyTransition.offset(x: (number == self.calledNumbers.calledNumberList.last) ? -250 : 250))
.animation(Animation.linear(duration: 1).repeatCount(1))
}
}
}
}
func checkCount(number: Int) -> Bool {
let count = self.calledNumbers.calledNumberList.count
if (count <= 8) {
return true
}
else {
guard let index = self.calledNumbers.calledNumberList.firstIndex(of: number) else { return false }
if (count - index > 8) { return false }
else { return true }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This file was added by xcode 12 (I named this project Test2):
import SwiftUI
#main
struct Test2App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Class for calledNumbers
//
// class.swift
// Test2
//
import Foundation
class CalledNumbers {
#Published var calledNumberList: [Int]
init() {
calledNumberList = [Int]()
}
func callNextNumber() {
var tempNumber = Int.random(in: 1...75)
while calledNumberList.contains(tempNumber) {
tempNumber = Int.random(in: 1...75)
}
calledNumberList.append(tempNumber)
print("Number added \(tempNumber)")
}
func startOver() {
calledNumberList.removeAll()
}
}
The problem seems to be caused by having the .onReceived() attached to the code inside of the Button. Moving .onReceived() to the Button as a whole solves the issue.
Also, you were doing more timer manipulation than is necessary. I removed stopping and restarting the timer from .onReceive().
calledNumbers should be an #ObservableObject.
struct ContentView: View {
#ObservedObject var calledNumbers = CalledNumbers()
#State private var timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
#State private var inProgress = false
var body: some View {
Button(action: {
if !self.inProgress {
self.calledNumbers.startOver()
print("Start timer")
self.timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
}
else {
print("Stop timer")
self.timer.upstream.connect().cancel()
}
self.inProgress.toggle()
})
{
Text(inProgress ? "STOP" : "START")
.font(.system(size: 22))
.fontWeight(.heavy)
.frame(width: 200, height: 35, alignment: .center)
.cornerRadius(35)
.foregroundColor(.white)
.background(Capsule().fill(inProgress ? Color.red : .green))
.padding(.bottom, 2)
}
}
.onReceive(self.timer) { _ in
print("CALL NEXT NUMBER")
self.calledNumbers.callNextNumber()
}
.onAppear {
// Cancel the initial timer
self.timer.upstream.connect().cancel()
}
ZStack {
RoundedRectangle(cornerRadius: 35)
.frame(width: UIScreen.main.bounds.size.width - 19, height: 40, alignment: .center)
.foregroundColor(.clear)
.padding(.bottom, 2)
HStack {
ForEach(self.calledNumbers.calledNumberList.reversed().filter {self.checkCount(number: $0)}, id: \.self) { number in
Text("\(String(number))")
.font(.custom("Menlo", size: 20))
.fontWeight(.black)
.frame(width: 40, height: 40, alignment: .center)
.background(Color.red)
.clipShape(Circle())
.foregroundColor(.white)
.transition(AnyTransition.offset(x: (number == self.calledNumbers.calledNumberList.last) ? -250 : 250))
.animation(Animation.linear(duration: 1).repeatCount(1))
}
}
}
}
func checkCount(number: Int) -> Bool {
let count = self.calledNumbers.calledNumberList.count
if (count <= 8) {
return true
}
else {
guard let index = self.calledNumbers.calledNumberList.firstIndex(of: number) else { return false }
if (count - index > 8) { return false }
else { return true }
}
}
}
Also, your CalledNumbers class should be an ObservableObject so that Published works correctly:
import Foundation
class CalledNumbers: ObservableObject {
#Published var calledNumberList: [Int]
init() {
calledNumberList = [Int]()
}
func callNextNumber() {
var tempNumber = Int.random(in: 1...75)
while calledNumberList.contains(tempNumber) {
tempNumber = Int.random(in: 1...75)
}
calledNumberList.append(tempNumber)
print("Number added \(tempNumber)")
}
func startOver() {
calledNumberList.removeAll()
}
}