I'm trying to animate Slider smoothly. My original problem was animating it for 1 second every second for a small value (building an audio player scrubber), but the animation was a split second instead of a full second.
To isolate a problem, I built a playground where I'm trying to animate Slider change from 0 to maxValue for maxValue number of seconds. However, whatever maxValue is, the animation happens in a fraction of a second.
Here is the code:
struct SliderTest: View {
#State private var sliderValue = 0.0
let maxValue = 30.0
var body: some View {
VStack(spacing: 10) {
Slider(value: $sliderValue, in: 0...maxValue)
Button("Animate Slider") {
withAnimation(.linear(duration: maxValue-sliderValue)) {
sliderValue = maxValue
}
}
Button("Reset to Random") {
sliderValue = Double.random(in: 0..<maxValue)
}
Button("Reset") {
sliderValue = 0
}
}
}
}
Here you can get the code with a preview for Swift Playgrounds:
https://gist.github.com/OgreSwamp/6e6423d6ef2d26425e3f993042ac208d
First of all, animation is not working here because when it comes into withAnimation it will assign sliderValue to maxValue immediately. That causes the reason above like you said.
For the problem, I will separate the slider just for change UI depends on sliderValue only. Then building sliderTimer for handle the change value of slider and everytime the timer is excuted it will automatically increase sliderValue by step.
The code will be like this
struct ContentView: View {
#State private var sliderValue = 0.0
private var maxValue = 30.0
private var sliderTimeAnimation = 2.0
private var step = 1.0
#State private var sliderTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack(spacing: 10) {
Slider(
value: $sliderValue,
in: 0...maxValue
).onReceive(sliderTimer) {
_ in
sliderValue += step
if (sliderValue > maxValue) {
stopTimer()
}
}
.onAppear() {
self.stopTimer()
}
Button("Animate Slider") {
startTimer()
}
Button("Reset to Random") {
stopTimer()
sliderValue = Double.random(in: 0..<maxValue)
}
Button("Reset") {
stopTimer()
sliderValue = 0
}
}
}
func startTimer() {
self.sliderTimer = Timer.publish(every: sliderTimeAnimation * step / maxValue, on: .main, in: .common).autoconnect()
}
func stopTimer() {
self.sliderTimer.upstream.connect().cancel()
}
}
In this case, I set sliderTimeAnimation is 2.0, step is 1.0 (if you need faster animation you can set step is 0.5 or lower) and the max value is 30.0 like your code.
The result is like this.
Related
New to Switch. I need to implement an Analog Clock with auto-updating every second to the current time (like a typical analog clock).
Drawing the Views etc is no problem. But I'm having troubles auto-updating the time.
View
struct ClockView: View {
var viewModel: ClockViewModel
var clock:ClockViewModel.TransformedClock
var size: CGFloat
#State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Circle()
.fill(.white)
.frame(width: size, height: size)
.overlay{
GeometryReader{ geometry in
let radius = geometry.size.width/2
let midX = geometry.safeAreaInsets.top + radius
let midY = geometry.safeAreaInsets.leading + radius
ClockFace(size: size, radius: radius, x: midX, y: midY)
ClockHand(type: .second, angle: clock.secondAngle, x: midX, y: midY, length: radius-size*0.04, size: size)
.onReceive(timer){ _ in updateAngleHere}
}
}
}
}
struct ClockHand:View{
let type: ClockHandType
#State var angle: Double
var x: CGFloat
var y: CGFloat
var length: CGFloat
var size: CGFloat
var body: some View {
RoundedRectangle(cornerRadius: 5)
.fill(type.getColor())
.frame(width: size*type.getWidth(), height: length )
.position(x: x, y: y)
.offset(y: -length/2)
.rotationEffect(Angle.degrees(angle))
}
mutating func update(angle: Double) {
print("calling \(self.angle)")
self.angle = angle
}
}
ViewModel
class ClockViewModel: ObservableObject {
#Published private var model: ClockModel
init(){
model = ClockViewModel.initClocks()
}
static func initClock()->ClockModel {
let places = [
"Europe/Bern",
]
return ClockModel(number: places.count){ index in
places[index]
}
}
var activeClock: TransformedClock{
TransformedClock(clock: model.activeWorldClock!)
}
struct TransformedClock:Identifiable{
var id:Int
var secondAngle:Double
var clock:ClockModel.WorldClock
init(clock:ClockModel.WorldClock){
let second = clock.calendar.component(.second, from: Date())
self.clock = clock
self.id = clock.id
self.secondAngle = Double(second)/60.0*360
}
}
}
Model
struct ClockModel{
var worldClocks: [WorldClock]
init(number: Int, timeFactory: (Int)->String){
worldClocks = [WorldClock]()
for index in 0..<number{
let place = timeFactory(index)
var calendar = Calendar.current
worldClocks.append(
WorldClock(
id: index,
calendar: calendar,
place: formatPlaceString(place)
)
)
}
}
struct WorldClock:Identifiable {
var calendar: Calendar
var place: String
}
}
Find my code above. First of all, I just would like to move the ClockHand for seconds to different positions. For this, I've been told to use a timer and make use of onReceive. My first idea was to call a mutating func of that struct in the closure - but since I can't seem to specifically call it on that struct, I guess it's the wrong option.
So, I need to find a way to teach my second-ClockFace struct to update / redraw itself, whenever I call the mutating func.
Examples I've found only shown functions outside a struct...
Any inut would be much appreciated.
Background
In this learning app, I've followed and excellent tutorial from Hacking with Swift on generating a wave-like animation. I've modified this app further adding some functionalities:
Providing Start/Stop mechanism for the wave animation
Perpetually generating random numbers for the duration of the animation
Modifying animation if an "interesting" number is found. Initially, I've implemented logic that defines even numbers as interesting but that could be easily changes to flag prime numbers, etc.
Problem
After stopping the animation does not "run" again. This is demonstrated in the gif below.
After stopping the animation does not restart.
Code
//
// ContentView.swift
// WaveExample
//
// Created by Konrad on 28/07/2021.
// Original tutorial: https://www.hackingwithswift.com/plus/custom-swiftui-components/creating-a-waveview-to-draw-smooth-waveforms
//
import SwiftUI
/**
Creates wave shape object
- Parameter strength: How tall the wave should be
- Parameter frequency: How densly the wave should be packed
- returns: Shape
*/
struct Wave: Shape {
// Basic wave characteristics
var strength: Double // Height
var frequency: Double // Number of hills
var phase: Double // Offsets the wave, can be used to animate the view
// Required to define that animation relates to moving the wave from left to right
var animatableData: Double {
get { phase }
set { self.phase = newValue }
}
// Path drawing function
func path(in rect: CGRect) -> Path {
let path = UIBezierPath()
// Basic waveline characteristics
let width = Double(rect.width)
let height = Double(rect.height)
let midWidth = width / 2
let midHeight = height / 2
let wavelength = width / frequency
let oneOverMidWidth = 1 / midWidth
// Path characteristics
path.move(to: CGPoint(x: 0, y: midHeight))
// By determines the nmber of calculations, can be decreased to run faster
for xPosition in stride(from: 0, through: width, by: 1) {
let relativeX = xPosition / wavelength // How far we are from the start point
let distanceFromMidWidth = xPosition - midWidth // Distance from the middle of the space
let normalDistance = distanceFromMidWidth * oneOverMidWidth // Get values from -1 to 1, normalize
// let parabola = normalDistance // Small waves in the middle
let parabola = -(normalDistance * normalDistance) + 1 // Big wave in the middle
let sine = sin(relativeX + phase) // Offset based on phase
let yPosition = parabola * strength * sine + midHeight // Moving this halfway
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
return Path(path.cgPath)
}
}
struct Line: Shape {
func path(in rect: CGRect) -> Path {
// Positioning
let midHeight = rect.height / 2
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: midHeight))
path.addLine(to: CGPoint(x: rect.width, y: midHeight))
return Path(path.cgPath)
}
}
struct ContentView: View {
#State private var phase = 0.0 // Used to animate the wave
#State private var waveStrength: Double = 10.0 // How tall, change for interesting numbers
#State private var waveFrequency: Double = 10.0 // How frequent, change for interesting numbers
#State var isAnimating: Bool = false // Currently running animation
#State private var randNum: Int16 = 0 // Random number to keep generating while animating
#State private var isNumberInteresting: Bool = false // Will take 'true' of the random number has some interesting properties
// Timer publisher reflecting frequent animation changes
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
// Stop timer
func stopTimer() {
self.timer.upstream.connect().cancel()
}
// Start timer
func startTimer() {
self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
}
// Check if number is interesting
func checkNumber(num: Int16) -> Bool {
var isInteresting: Bool = false
if num % 2 == 0 {
isInteresting.toggle()
}
return isInteresting
}
var body: some View {
VStack {
if self.isAnimating {
VStack {
Button("Stop") {
self.isAnimating = false
stopTimer()
}
.font(.title)
.foregroundColor(Color(.blue))
Text("Random number: \(String(randNum)), interesting: \(String(isNumberInteresting))")
.onReceive(timer, perform: { _ in
randNum = Int16.random(in: 0..<Int16.max)
isNumberInteresting = checkNumber(num: randNum)
})
}
} else {
Button("Start") {
self.isAnimating = true
startTimer()
}
.font(.title)
.foregroundColor(Color(.red))
}
if self.isAnimating {
// Animation
ZStack {
ForEach(0..<10) { waveIteration in
Wave(strength: waveStrength, frequency: waveFrequency, phase: phase)
.stroke(Color.blue.opacity(Double(waveIteration) / 3), lineWidth: 1.1)
.offset(y: CGFloat(waveIteration) * 10)
}
}
.onReceive(timer) { _ in
// withAnimation requires info on how to animate
withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
self.phase = .pi * 2 // 180 degrees of sine being calculated
if isNumberInteresting {
waveFrequency = 50.0
waveStrength = 50.0
} else {
waveFrequency = 10.0
waveStrength = 10.0
}
}
}
.frame(height: UIScreen.main.bounds.height * 0.8)
} else {
// Static line
ZStack {
Line()
.stroke(Color.blue)
}
.frame(height: UIScreen.main.bounds.height * 0.8)
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Side notes
In addiction to the problem above any good practice pointers on working with Swift are always welcome.
I made your project works, you can see the changed code // <<: Here!, the issue was there that you did not show the Animation the changed value! you showed just one time! and after that you keep it the same! if you see your code in your question you are repeating self.phase = .pi * 2 it makes no meaning to Animation! I just worked on your ContentView the all project needs refactor work, but that is not the issue here.
struct ContentView: View {
#State private var phase = 0.0
#State private var waveStrength: Double = 10.0
#State private var waveFrequency: Double = 10.0
#State var isAnimating: Bool = false
#State private var randNum: Int16 = 0
#State private var isNumberInteresting: Bool = false
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var stringOfText: String = String() // <<: Here!
func stopTimer() {
self.timer.upstream.connect().cancel()
phase = 0.0 // <<: Here!
}
func startTimer() {
self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(500)) { phase = .pi * 2 } // <<: Here!
}
func checkNumber(num: Int16) -> Bool {
var isInteresting: Bool = false
if num % 2 == 0 {
isInteresting.toggle()
}
return isInteresting
}
var body: some View {
VStack {
Button(isAnimating ? "Stop" : "Start") { // <<: Here!
isAnimating.toggle() // <<: Here!
isAnimating ? startTimer() : stopTimer() // <<: Here!
}
.font(.title)
.foregroundColor(isAnimating ? Color.red : Color.blue) // <<: Here!
ZStack {
if isAnimating {
ForEach(0..<10) { waveIteration in
Wave(strength: waveStrength, frequency: waveFrequency, phase: phase)
.stroke(Color.blue.opacity(Double(waveIteration) / 3), lineWidth: 1.1)
.offset(y: CGFloat(waveIteration) * 10)
}
}
else {
Line().stroke(Color.blue)
}
}
.frame(height: UIScreen.main.bounds.height * 0.8)
.overlay(isAnimating ? Text(stringOfText) : nil, alignment: .top) // <<: Here!
.onReceive(timer) { _ in
if isAnimating { // <<: Here!
randNum = Int16.random(in: 0..<Int16.max)
isNumberInteresting = checkNumber(num: randNum)
stringOfText = "Random number: \(String(randNum)), interesting: \(String(isNumberInteresting))" // <<: Here!
if isNumberInteresting {
waveFrequency = 50.0
waveStrength = 50.0
} else {
waveFrequency = 10.0
waveStrength = 10.0
}
}
else {
stopTimer() // For safety! Killing Timer in case! // <<: Here!
}
}
.animation(nil, value: stringOfText) // <<: Here!
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) // <<: Here!
.id(isAnimating) // <<: Here!
}
}
}
I have a line slider that looks like this:
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 200 * (timeRemaining), y: 0))
}
.stroke(Color.orange, lineWidth: 4).opacity(0.5)
The timeRemaining will go from 2s, 1s, 0s making the line slider to decrease from right to left. How can I change this implementation so that the slider will decrease more like this:
400,399,398...2,1,0
which will make the line slider looks smoother.
Here is the timer declaration:
#State var timeRemaining = 2
#State var timer: Timer.TimerPublisher = Timer.publish (every: 1,
on: .main, in: .common)
.onReceive(timer) { time in
if timeRemaining > 0 {
timeRemaining -= 1
}
I think I need to make some changes in the onReceive part.
To have a finer granularity and a smoother animation you simply need to scale down the update interval. Since 2 / 400 is 0.005 you should use this for your timer and your count down
#State var timeRemaining = 2.0 // Use Double
#State var timer: Timer.TimerPublisher = Timer.publish (every: 0.005, on: .main, in: .common)
...
.onReceive(timer) { time in
if timeRemaining > 0 {
timeRemaining -= 0.005
}
I'm trying to implement a simple animation using CADisplayLink. A black square should move slowly across the screen.
But the square appears stationary because the body of Squares doesn't get updated when step() increments testX and testY.
From what I can gather (I'm new to Swift), I'm using the #ObservedObject paradigm correctly, so I don't understand why the view won't refresh.
How do I get Squares's body to update along with the animation?
My code is below, thanks for your help!
class MyAnimations: NSObject, ObservableObject {
#Published var testX: Double = 0
#Published var testY: Double = 0
func createDisplayLink() {
let displaylink = CADisplayLink(target: self, selector: #selector(step))
displaylink.add(to: .current, forMode: RunLoop.Mode.default)
}
#objc func step(link: CADisplayLink) {
testX += 0.5
testY += 0.5
}
}
struct Squares: View {
#ObservedObject var animation = MyAnimations()
var body: some View {
Path { path in
path.addRect(CGRect(x: self.animation.testX, y: self.animation.testY, width: 50, height: 50))
}
.fill(Color.black)
}
}
struct SomeOtherView: View {
var body: some View {
Squares()
}
}
Update: retested with Xcode 13.4 / iOS 15.5 - works as-is
This is because your displaylink is created on stack, so destroyed right after quit from function. You need to make it property.
Here is modified tested code. Tested with Xcode 12 / iOS 14.
class MyAnimations: NSObject, ObservableObject {
#Published var testX: Double = 0
#Published var testY: Double = 0
private var displaylink: CADisplayLink! // << here !!
func createDisplayLink() {
if nil == displaylink {
displaylink = CADisplayLink(target: self, selector: #selector(step))
displaylink.add(to: .current, forMode: RunLoop.Mode.default)
}
}
#objc func step(link: CADisplayLink) {
testX += 0.5
testY += 0.5
}
}
struct Squares: View {
#ObservedObject var animation = MyAnimations()
var body: some View {
Path { path in
path.addRect(CGRect(x: self.animation.testX, y: self.animation.testY, width: 50, height: 50))
}
.fill(Color.black)
.onAppear {
animation.createDisplayLink()
}
}
}
How can I change the brightness of the screen programmatically using iPhone SDK?
[[UIScreen mainScreen] setBrightness: yourvalue];
Requires iOS 5.0 or later. yourvalue is a float between 0.0 and 1.0.
UPDATE: For Swift 3
UIScreen.main.brightness = YourBrightnessValue
Here's the swift answer to perform this
UIScreen.mainScreen().brightness = YourBrightnessValue
YourBrightnessValue is a float between 0.0 and 1.0
I had some problems with changing the screen brightness in viewDidLoad/viewWillDisappear so I created a singleton class to handle all the action. This is how I do it:
import Foundation
import UIKit
final class ScreenBrightnessHelper {
private var timer: Timer?
private var brightness: CGFloat?
private var isBrighteningScreen = false
private var isDarkeningScreen = false
private init() { }
static let shared = ScreenBrightnessHelper()
func brightenDisplay() {
resetTimer()
isBrighteningScreen = true
if #available(iOS 10.0, *), timer == nil {
brightness = UIScreen.main.brightness
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { (timer) in
UIScreen.main.brightness = UIScreen.main.brightness + 0.01
if UIScreen.main.brightness > 0.99 || !self.isBrighteningScreen {
self.resetTimer()
}
}
}
timer?.fire()
}
func darkenDisplay() {
resetTimer()
isDarkeningScreen = true
guard let brightness = brightness else {
return
}
if #available(iOS 10.0, *), timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { (timer) in
UIScreen.main.brightness = UIScreen.main.brightness - 0.01
if UIScreen.main.brightness < brightness || !self.isDarkeningScreen {
self.resetTimer()
self.brightness = nil
}
}
timer?.fire()
}
}
private func resetTimer() {
timer?.invalidate()
timer = nil
isBrighteningScreen = false
isDarkeningScreen = false
}
}