How to change color of a shape after a time x? - time

Sorry if it's a simple fix, I am a still bit new to swift/swiftui
I have been trying to accomplish a change of color after a second.
struct ContentView: View {
#State private var hasTimeElapsed = false
var colorCircle: [Color] = [.red, .green]
var body: some View {
Circle()
.fill(self.colorCircle.randomElement()!)
.frame(width: 100,
height: 100)
.onAppear(perform: delayCircle)
}
private func delayCircle() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.hasTimeElapsed = true
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

There's no need to use the hasTimeElapsed variable. Instead you can add a #State variable to store your current colour and set it in delayCircle():
struct ContentView: View {
var colorCircle: [Color] = [.red, .green]
#State var currentColor = Color.red
var body: some View {
Circle()
.fill(currentColor)
.frame(width: 100,
height: 100)
.onAppear(perform: delayCircle)
}
private func delayCircle() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.currentColor = self.colorCircle.randomElement()!
}
}
}
If you want to start with a random colour (instead of #State var currentColor = Color.red) you can set it in init:
#State var currentColor: Color
init() {
_currentColor = .init(initialValue: self.colorCircle.randomElement()!)
}
UPDATE due to OP's comment
If you want to set your circle colour in a loop you can use a Timer:
struct ContentView: View {
var colorCircle: [Color] = [.red, .green]
#State var currentColor: Color = .red
// firing every 1 second
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Circle()
.fill(currentColor)
.frame(width: 100,
height: 100)
Button("Stop timer") {
self.timer.upstream.connect().cancel() // <- stop the timer
}
}
.onReceive(timer) { _ in // <- when the timer fires perfom some action (here `randomizeCircleColor()`)
self.randomizeCircleColor()
}
}
private func randomizeCircleColor() {
self.currentColor = self.colorCircle.randomElement()!
}
}

Related

Tracking scroll position in a List SwiftUI

So in the past I have been tracking the scroll position using a scroll view but I've fallen into a situation where I need to track the position using a List. I am using a List because I want some of the built in real estate to create my views such as the default List styles.
I can get the value using PreferenceKeys, but the issue is when I scroll to far upwards, the PreferenceKey value will default back to its position 0, breaking my show shy header view logic.
This is the TrackableListView code
struct TrackableListView<Content: View>: View {
let offsetChanged: (CGPoint) -> Void
let content: Content
init(offsetChanged: #escaping (CGPoint) -> Void = { _ in }, #ViewBuilder content: () -> Content) {
self.offsetChanged = offsetChanged
self.content = content()
}
var body: some View {
List {
GeometryReader { geometry in
Color.clear.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("ListView")).origin)
}
.frame(width: 0, height: 0)
content
.offset(y: -10)
}
.coordinateSpace(name: "ListView")
.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged)
}
}
private struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { }
}
And this is my ContentView:
struct ContentView: View {
#State private var contentOffset = CGFloat(0)
#State private var offsetPositionValue: CGFloat = 0
#State private var isShyHeaderVisible = false
var body: some View {
NavigationView {
ZStack {
TrackableListView { offset in
withAnimation {
contentOffset = offset.y
}
} content: {
Text("\(contentOffset)")
}
.overlay(
ZStack {
HStack {
Text("Total points")
.foregroundColor(.white)
.lineLimit(1)
Spacer()
Text("20,000 pts")
.foregroundColor(.white)
.padding(.leading, 50)
}
.padding(.horizontal)
.padding(.vertical, 8)
.frame(width: UIScreen.main.bounds.width)
.background(Color.green)
.offset(y: contentOffset < 50 ? 0 : -5)
.opacity(contentOffset < 50 ? 1 : 0)
.transition(.move(edge: .top))
}
.frame(maxHeight: .infinity, alignment: .top)
)
}
.navigationTitle("Hello")
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarTitleDisplayMode(.inline)
.frame(maxHeight: .infinity, alignment: .top)
.background(AccountBackground())
}
}
}
The issue was that Other Views in my hierarchy (probably introduced by SwiftUI) could be sending the default value, which is why you start getting zero sometimes.
What I needed was a way to determine when to use or forward the new value, versus when to ignore it.
To do this I had to make my value optional GCPoint, with a nil default value, then in your reduce method when using PreferenceKeys you have to do:
if let nextValue = nextValue() {
value = nextValue
}
Then make sure your CGPoint is an optional value.

How to trigger SwiftUI animation via change of non-State variable

It's easy to have an animation begin when a view appears, by using .onAppear(). But I'd like to perform a repeating animation whenever one of the view's non-State variables changes. An example:
Here is a view that I would like to "throb" whenever its throbbing parameter, set externally, is true:
struct MyCircle: View {
var throbbing: Bool
#State var scale = 1.0
var body: some View {
Circle()
.frame(width: 100 * scale, height: 100 * scale)
.foregroundColor(.blue)
.animation(.easeInOut.repeatForever(), value: scale)
.onAppear { scale = 1.2 }
}
}
Currently the code begins throbbing immediately, regardless of the throbbing variable.
But imagine this scenario:
struct ContentView: View {
#State var throb: Bool = false
var body: some View {
VStack {
Button("Throb: \(throb ? "ON" : "OFF")") { throb.toggle() }
MyCircle(throbbing: throb)
}
}
}
It looks like this:
Any ideas how I can modify MyCircle so that the throbbing starts when the button is tapped and ends when it is tapped again?
You can use onChange to watch throbbing and then assign an animation. If true, add a repeating animation, and if false, just animate back to the original scale size:
struct MyCircle: View {
var throbbing: Bool
#State private var scale : CGFloat = 0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.scaleEffect(1 + scale)
.foregroundColor(.blue)
.onChange(of: throbbing) { newValue in
if newValue {
withAnimation(.easeInOut.repeatForever()) {
scale = 0.2
}
} else {
withAnimation {
scale = 0
}
}
}
}
}
There're two interesting things that i've just found when trying to achieve yours target.
Animation will be added when the value changed
Animation that added to the view cannot be removed, and we need to drop the animated view from the view hiearachy by remove its id, new view will be created with zero animations.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var throbbling = false
#State var circleId = UUID()
var body: some View {
VStack {
Toggle("Throbbling", isOn: $throbbling)
circle.id(circleId)
.scaleEffect(throbbling ? 1.2 : 1.0)
.animation(.easeInOut.repeatForever(), value: throbbling)
.onChange(of: throbbling) { newValue in
if newValue == false {
circleId = UUID()
}
}
}.padding()
}
#ViewBuilder
var circle: some View {
Circle().fill(.green).frame(width: 100, height: 100)
}
}
PlaygroundPage.current.setLiveView(ContentView())

Reload SwiftUI view from within it

I have some text inside a View and I want this text to change it's position on timer. I have the following:
struct AlphabetView: View {
#State var timer: Publishers.Autoconnect<Timer.TimerPublisher> = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
#State var refreshMe: Bool = false // this is a hack to refresh the view
var relativePositionX: CGFloat {
get { return CGFloat(Float.random(in: 0...1)) }
}
var relativePositionY: CGFloat {
get { return CGFloat(Float.random(in: 0...1)) }
}
var body: some View {
GeometryReader { geometry in
Text("Hello World!")
.position(x: geometry.size.width * self.relativePositionX, y: geometry.size.height * self.relativePositionY)
.onReceive(timer) { _ in
// a hack to refresh the view
self.refreshMe = !self.refreshMe
}
}
}
}
I suppose that the View should be reloaded every time self.refreshMe changes, because it is #State. But what I see is that the text position changes only when the parent view of the view in question is reloaded. What am I doing wrong (except hacking around)? Is there a better way to do this?
Instead of hack just update position directly.
Here is corrected variant. Tested with Xcode 12 / iOS 14.
struct AlphabetView: View {
let timer: Publishers.Autoconnect<Timer.TimerPublisher> = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
#State var point: CGPoint = .zero
var relativePositionX: CGFloat {
get { return CGFloat(Float.random(in: 0...1)) }
}
var relativePositionY: CGFloat {
get { return CGFloat(Float.random(in: 0...1)) }
}
var body: some View {
GeometryReader { geometry in
Text("Hello World!")
.position(self.point)
.onReceive(timer) { _ in
self.point = CGPoint(x: geometry.size.width * self.relativePositionX, y: geometry.size.height * self.relativePositionY)
}
}
}
}

How do I randomly change the color of a circle?

I'm beginning to try out SwiftUI. I want the Circle is randomly changing the color from red or green.
struct ContentView: View {
#State var colorCircle = [".red", ".green"]
var body: some View {
ZStack {
Circle()
.fill(Color.colorCircle.randomElement())
.frame(width: 100,
height: 100)
.position(x: 250,
y: 320)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First make your colorCircle array to contain Colors and not Strings.
#State var colorCircle: [Color] = [.red, .green]
Also note you're trying to access colorCircle from inside the Color struct:
Color.colorCircle.randomElement()
As you declared colorCircle in the ContentView you can use it like:
self.colorCircle.randomElement()
or (shorter):
colorCircle.randomElement()
Then you can use randomElement:
struct ContentView: View {
#State var colorCircle: [Color] = [.red, .green]
var body: some View {
ZStack {
Circle()
.fill(colorCircle.randomElement()!)
.frame(width: 100,
height: 100)
.position(x: 250,
y: 320)
}
}
}
Note that randomElement() returns an optional so you can force-unwrap it (!) if you're sure your array is not empty.
However, as a good practice, I recommend not to use ! and provide default values if possible:
.fill(colorCircle.randomElement() ?? .red)

SwiftUI: using RotationGesture with changeable blur effect

I am trying to combine the RotationGesture with changeable blur effect. So if you turn the ocular right, you get the picture more sharp like in binoculars. Turn left, it will be blurred again. The code below is basically doing it, but not very smoothly. Have you any ideas to improve the effect?
struct ContentView: View {
#State var blurring: CGFloat = 20
#State var sharping: CGFloat = 0
#State private var rotateState: Double = 0
var body: some View {
ZStack {
Image("cat")
.resizable()
.frame(width: 200, height: 200)
.blur(radius: blurring)
.clipShape(Circle())
Image("ocular_rk2")
.resizable()
.frame(width: 350, height: 350)
.rotationEffect(Angle(degrees: self.rotateState))
.gesture(RotationGesture()
.onChanged { value in
self.rotateState = value.degrees
self.changeBlur()
}
.onEnded { value in
self.rotateState = value.degrees
print(self.rotateState)
}
)
}
}
func changeBlur() {
switch rotateState {
case -10000...10000: sharping = 0.1
default:
print("# Mistake in Switch")
}
if rotateState < 0 {
blurring += sharping
} else {
blurring -= sharping
}
}
}
check this out (i tested it, it works)
maybe you should think about changing some values because you have to rotate a while until you see the text.
by the way: it is always a good manner to post compilable reproducable code (we do not have your images)...
struct ContentView: View {
#State var blurring: CGFloat = 20
#State var sharping: CGFloat = 0
#State private var rotateState: Double = 0
var body: some View {
ZStack {
Text("cat")
// .resizable()
.frame(width: 200, height: 200)
.clipShape(Circle())
.background(Color.yellow)
Text("ocular_rk2")
// .resizable()
.blur(radius: blurring)
.frame(width: 350, height: 350)
.background(Color.red)
.rotationEffect(Angle(degrees: self.rotateState))
.gesture(RotationGesture()
.onChanged { value in
self.rotateState = value.degrees
self.changeBlur()
}
.onEnded { value in
self.rotateState = value.degrees
print(self.rotateState)
}
)
}
}
func changeBlur() {
switch rotateState {
case -10000...10000: sharping = 0.1
default:
print("# Mistake in Switch")
}
if rotateState < 0 {
blurring = blurring + sharping
} else {
blurring = blurring - sharping
print(blurring)
}
}
}

Resources