Quickly updating UIView background color in Swift? - ios

I have an array of times for when the background color of a UIView should change colors. The array holds values from 0-10000ms, where each index is the duration between the first change and the current change. I have implemented a loop to execute the tasks at the scheduled times, and using print statements, it seems to be working. However, the background color only changes to the last color instead of changing continuously.
I believe this is because the background color does not update until the loop is done. Is this correct, and if so, how can I fix this?
sendingView.backgroundColor = UIColor.black
let startingTime = getCurrentMillis()
var found = false
for time in receivingMsgData {
while(!found) {
let curDuration = getCurrentMillis() - startingTime
if curDuration > time {
if(sendingView.backgroundColor == UIColor.black) {
sendingView.backgroundColor = UIColor.white
print("playing at \(getCurrentMillis()-startingTime) turning white")
} else {
sendingView.backgroundColor = UIColor.black
print("playing at \(getCurrentMillis()-startingTime) turning black")
}
found = true
}
}
found = false
}

Try something like this
DispatchQueue.global(qos: .userInitiated).async {
let delay:TimeInterval = 0.01 // Seconds
for index in 0..<100 {
if index % 2 == 0 {
Thread.sleep(forTimeInterval: delay)
DispatchQueue.main.async {
self.view.backgroundColor = UIColor.white
print("white")
}
}else{
Thread.sleep(forTimeInterval: delay)
DispatchQueue.main.async {
self.view.backgroundColor = UIColor.black
print("black")
}
}
}
}
Do your action in async thread and change the color on main thread
You doing to much in main thread causing it to hang, aslo put some delay so that user can view the animation
try something like this in your case
let delay:TimeInterval = 0.01 // Seconds
sendingView.backgroundColor = UIColor.black
let startingTime = getCurrentMillis()
var found = false
DispatchQueue.global(qos: .userInitiated).async {
for time in receivingMsgData {
while(!found) {
let curDuration = getCurrentMillis() - startingTime
if curDuration > time {
if(sendingView.backgroundColor == UIColor.black) {
Thread.sleep(forTimeInterval: delay)
DispatchQueue.main.async {
sendingView.backgroundColor = UIColor.white
print("playing at \(getCurrentMillis()-startingTime) turning white")
}
} else {
Thread.sleep(forTimeInterval: delay)
DispatchQueue.main.async {
sendingView.backgroundColor = UIColor.black
print("playing at \(getCurrentMillis()-startingTime) turning black")
}
}
found = true
}
}
found = false
}
}

The code you posted either blocks the main thread or the operation is not on the main thread when setting the color. Both of the situations will most likely result in no changes until the execution of the whole loop is done or after some time even.
When you are dealing with data that are scheduled at some time which may be irrelevant to the speed of the screen update you need to give the color on demand when the screen will refresh.
In your case I would suggest using a display link CADisplayLink which is designed to trigger whenever the screen should refresh. You can even get the current time value since the beginning from the display link itself. That elapsed time can be used the same way you are already using curDuration in your code. Assuming that the rest of the code works fine that is.

I believe this is because the background color does not update until the loop is done. Is this correct
Yes, it is. Drawing doesn't happen until all your code on the main thread finishes and the CATransaction is committed. You would need to do one of two things:
Run your time-consuming code on a background thread, coming to the main thread only to talk to the interface and set the color.
Use some form of delay to get off the main thread long enough to let drawing take place before continuing with your "loop".

Related

Labels displaying countdown sometimes out of sync after pausing. Rounding errors?

I have an app that does a countdown with a Timer. The countdown tracks multiple steps (all at the same intervals) as well as the total time left, and updates 2 separate UILabels accordingly. Occasionally, the labels will be out of sync.
I can't say for sure, but I think it might be only happening when I pause the countdown sometimes, and usually on steps later than the first step. It's most apparent on the last step when the two labels should be displaying the same exact thing, but will sometimes be 1 second off.
The other tricky thing is that sometimes pausing and resuming after the time has gone out of sync will get it back in sync.
My guess is I'm getting something weird happening in the pause code and/or the moving between steps, or maybe the calculating and formatting of TimeIntervals. Also I'm using rounded() on the calculated TimeIntervals because I noticed only updating the timer every 1s the labels would freeze and skip seconds a lot. But I'm unsure if that's the best way to solve this problem.
Here's the relevant code. (still need to work on refactoring but hopefully it's easy to follow, I'm still a beginner)
#IBAction func playPauseTapped(_ sender: Any) {
if timerState == .running {
//pause timer
pauseAnimation()
timer.invalidate()
timerState = .paused
pausedTime = Date()
playPauseButton.setImage(UIImage(systemName: "play.circle"), for: .normal)
} else if timerState == .paused {
//resume paused timer
guard let pause = pausedTime else { return }
let pausedInterval = Date().timeIntervalSince(pause)
startTime = startTime?.addingTimeInterval(pausedInterval)
endTime = endTime?.addingTimeInterval(pausedInterval)
currentStepEndTime = currentStepEndTime?.addingTimeInterval(pausedInterval)
pausedTime = nil
startTimer()
resumeAnimation()
timerState = .running
playPauseButton.setImage(UIImage(systemName: "pause.circle"), for: .normal)
} else {
//first run of brand new timer
startTimer()
startProgressBar()
startTime = Date()
if let totalTime = totalTime {
endTime = startTime?.addingTimeInterval(totalTime)
}
currentStepEndTime = Date().addingTimeInterval(recipeInterval)
timerState = .running
playPauseButton.setImage(UIImage(systemName: "pause.circle"), for: .normal)
currentWater += recipeWater[recipeIndex]
currentWeightLabel.text = "\(currentWater)g"
}
}
func startTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
}
#objc func runTimer() {
let currentTime = Date()
guard let totalTimeLeft = endTime?.timeIntervalSince(currentTime).rounded() else { return }
guard let currentInterval = currentStepEndTime?.timeIntervalSince(currentTime).rounded() else { return }
//end of current step
if currentInterval <= 0 {
//check if end of recipe
if recipeIndex < recipeWater.count - 1 {
//move to next step
totalTimeLabel.text = totalTimeLeft.stringFromTimeInterval()
currentStepEndTime = Date().addingTimeInterval(recipeInterval)
startProgressBar()
currentStepTimeLabel.text = recipeInterval.stringFromTimeInterval()
stepsTime += recipeInterval
recipeIndex += 1
//update some ui
} else {
//last step
currentStepTimeLabel.text = "00:00"
totalTimeLabel.text = "00:00"
timer.invalidate()
//alert controller saying finished
}
} else {
//update time labels
currentStepTimeLabel.text = currentInterval.stringFromTimeInterval()
totalTimeLabel.text = totalTimeLeft.stringFromTimeInterval()
}
}
extension TimeInterval {
func stringFromTimeInterval() -> String {
let time = NSInteger(self)
let seconds = time % 60
let minutes = (time / 60) % 60
return String(format: "%0.2d:%0.2d",minutes,seconds)
}
}
EDIT UPDATE: I tried a few different things but still kept having the same issue. I started testing with printing the TimeInterval and the formatted string to compare and see what's off. It's definitely some sort of rounding error.
Total - 173.50678288936615 / 02:54
Step - 39.00026595592499 / 00:39
Total - 172.5073879957199 / 02:53
Step - 38.00087106227875 / 00:38
Total - 171.1903439760208 / 02:51
Step - 36.68382704257965 / 00:37
Total - 170.19031596183777 / 02:50
Step - 35.683799028396606 / 00:36
As you can see, the total time skips from 2:53 to 2:51, but the step timer remains consistent. The reason is the TimeInterval for total goes from 172.5 which gets rounded up, to 171.19 which gets rounded down.
I've also watched the timer count down without touching pause, and it remains in sync reliably. So I've narrowed it down to my pause code.
Fixed my issue and posting here for posterity. I ended up making my totalTimeLeft and currentInterval global properties. Then, on pause and resume, instead of tracking the paused time and adding it to endTime, I just used the totalTimeLeft and currentInterval values that are still stored from the last Timer firing and doing endTime = Date().addingTimeInterval(totalTimeLeft) and the same with the interval time. This got rid of the paused time adding weird amounts that would mess up the rounding.

Run and Pause an ARSession in a specified period of time

I'm developing ARKit/Vision iOS app with gesture recognition. My app has a simple UI containing single UIView. There's no ARSCNView/ARSKView at all. I'm putting a sequence of captured ARFrames into CVPixelBuffer what then I use for VNRecognizedObjectObservation.
I don't need any tracking data from a session. I just need currentFrame.capturedImage for CVPixelBuffer. And I need to capture ARFrames at 30 fps. 60 fps is excessive frame rate.
preferredFramesPerSecond instance property is absolutely useless in my case, because it controls frame rate for rendering an ARSCNView/ARSKView. I have no ARViews. And it doesn't affect session's frame rate.
So, I decided to use run() and pause() methods to decrease a session's frame rate.
Question
I'd like to know how to automatically run and pause an ARSession in a specified period of time? The duration of run and pause methods must be 16 ms (or 0.016 sec). I suppose it might be possible through DispatchQueue. But I don't know how to implement it.
How to do it?
Here's a pseudo-code:
session.run(configuration)
/* run lasts 16 ms */
session.pause()
/* pause lasts 16 ms */
session.run(session.configuration!)
/* etc... */
P.S. I can use neither CocoaPod nor Carthage in my app.
Update: It's about how ARSession's currentFrame.capturedImage is retrieved and used.
let session = ARSession()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
session.delegate = self
let configuration = ARImageTrackingConfiguration() // 6DOF
configuration.providesAudioData = false
configuration.isAutoFocusEnabled = true
configuration.isLightEstimationEnabled = false
configuration.maximumNumberOfTrackedImages = 0
session.run(configuration)
spawnCoreMLUpdate()
}
func spawnCoreMLUpdate() { // Spawning new async tasks
dispatchQueue.async {
self.spawnCoreMLUpdate()
self.updateCoreML()
}
}
func updateCoreML() {
let pixelBuffer: CVPixelBuffer? = (session.currentFrame?.capturedImage)
if pixelBuffer == nil { return }
let ciImage = CIImage(cvPixelBuffer: pixelBuffer!)
let imageRequestHandler = VNImageRequestHandler(ciImage: ciImage, options: [:])
do {
try imageRequestHandler.perform(self.visionRequests)
} catch {
print(error)
}
}
If what you want is to reduce the frame rate from 60 to 30, you should use the preferredFramesPerSecond property of SCNView. I'm assuming you're using an ARSCNView, which is a subclass of SCNView.
Property documentation.
I don't think the run() and pause() strategy is the way to go because the DispatchQueue API is not designed for realtime accuracy. Which means there will be no guarantee that the pause will be 16ms every time. On top of that, restarting a session might not be immediate and could add more delay.
Also, the code you shared will at most capture only one image and as session.run(configuration) is asynchronous will probably capture no frame.
As you're not using ARSCNView/ARSKView the only way is to implement the ARSession delegate to be notified of every captured frame.
Of course the delegate will most likely be called every 16ms because that's how the camera works. But you can decide which frames you are going to process. By using the timestamp of the frame you can process a frame every 32ms and drop the other ones. Which is equivalent to a 30 fps processing.
Here is some code to get you started, make sure that dispatchQueue is not concurrent to process your buffers sequentially:
var lastProcessedFrame: ARFrame?
func session(_ session: ARSession, didUpdate frame: ARFrame) {
dispatchQueue.async {
self.updateCoreML(with: frame)
}
}
private func shouldProcessFrame(_ frame: ARFrame) -> Bool {
guard let lastProcessedFrame = lastProcessedFrame else {
// Always process the first frame
return true
}
return frame.timestamp - lastProcessedFrame.timestamp >= 0.032 // 32ms for 30fps
}
func updateCoreML(with frame: ARFrame) {
guard shouldProcessFrame(frame) else {
// Less than 32ms with the previous frame
return
}
lastProcessedFrame = frame
let pixelBuffer = frame.capturedImage
let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
do {
try imageRequestHandler.perform(self.visionRequests)
} catch {
print(error)
}
}
If I understand It correctly, you can achieve it via DispatchQueue. If you run below code, It prints HHH first then waits for 1 second then prints ABC. You can put your own functions to make it work for you. Of course change time interval from 1 to your desired value.
let syncConc = DispatchQueue(label:"con",attributes:.concurrent)
DispatchQueue.global(qos: .utility).async {
syncConc.async {
for _ in 0...10{
print("HHH - \(Thread.current)")
Thread.sleep(forTimeInterval: 1)
print("ABC - \(Thread.current)")
}
}
PS: I'm still not sure If Thread.sleep will block your process, If It is I'll edit my answer.

How to stop a Function from Running in Swift 3.0

I have a function that starts playing an animation that is running asynchronously (in the background). This animation is called indefinitely using a completion handler (see below). Is there a way to close this function upon pressing another button?
Here is my code:
func showAnimation() {
UIView.animate(withDuration: 1, animations: {
animate1(imageView: self.Anime, images: self.animation1)
}, completion: { (true) in
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
self.showAnimation() // replay first showAnimation
}
})
}
Then upon pressing another button we closeout the above function
showAnimation().stop();
Thanks
You can add a property to the class to act as a flag indicating whether the animation should be run or not.
var runAnimation = true
func showAnimation() {
if !runAnimation { return }
UIView.animate(withDuration: 1, animations: {
animate1(imageView: self.Anime, images: self.animation1)
}, completion: { (true) in
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
if runAnimation {
self.showAnimation() // replay first showAnimation
}
}
})
}
Then in the button handler to stop the animation you simply do:
runAnimation = false
Note that this does not stop the currently running 1 second animation. This just prevent any more animations.
There are a lot of ways to do this. The simplest is to have a Boolean property (which you should make properly atomic) that you check in your asyncAfter block, and don't just don't call showAnimation() again if it's true.
Another thing you can do, and what I like to do for more complex tasks, is to use OperationQueue instead of DispatchQueue. This allows you to cancel operations, either individually or all at once, or even suspend the whole queue (obviously don't suspend the main queue or call removeAllOperations() on it, though, since there may be other operations in there unrelated to your code).
You can provide a variable outside of your function, then observe its value and handle your task. I can give you a solution:
class SomeClass {
private var shouldStopMyFunction: Bool = false // keep this private
public var deadline: TimeInterval = 0
func stopMyFunction() {
shouldStopMyFunction = true
}
func myFunction(completionHanlder: #escaping (String)->()) {
// -------
var isTaskRunning = true
func checkStop() {
DispatchQueue.global(qos: .background).async {
if self.shouldStopMyFunction, isTaskRunning {
isTaskRunning = false
completionHanlder("myFunction is forced to stop! 😌")
} else {
//print("Checking...")
checkStop()
}
}
}
checkStop()
// --------
// Start your task from here
DispatchQueue.global().async { // an async task for an example.
DispatchQueue.global().asyncAfter(deadline: .now() + self.deadline, execute: {
guard isTaskRunning else { return }
isTaskRunning = false
completionHanlder("My job takes \(self.deadline) seconds to finish")
})
}
}
}
And implement:
let anObject = SomeClass()
anObject.deadline = 5.0 // seconds
anObject.myFunction { result in
print(result)
}
let waitingTimeInterval = 3.0 // 6.0 // see `anObject.deadline`
DispatchQueue.main.asyncAfter(deadline: .now() + waitingTimeInterval) {
anObject.stopMyFunction()
}
Result with waitingTimeInterval = 3.0: myFunction is forced to stop! 😌
Result with waitingTimeInterval = 6.0: My job takes 5.0 seconds to finish

How can I call a function after a time delay in the main thread in Swift 3?

I have a UIControl that calls a function after 0.5 seconds depending on how many times the user presses it.
(Eg 1 press calls f1(), 2 presses calls f2(), 3 presses calls f3())
So basically I need to set a timer when a user presses the Control. If the Control is not pressed for 0.5 seconds then create a dialog. I have tried using a DispatchQueue, but when it gets to the point of making the dialog, it takes several seconds. I think it is because it is being called concurrently instead of on the main thread (apologies if poor terminology).
self.operationQueue.cancelAllOperations() //To cancel previous queues
self.mainAsyncQueue = DispatchQueue(label: "bubblePressed" + String(describing: DispatchTime.now()), qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent)
let time = DispatchTime.now()
self.currentTime = time
self.mainAsyncQueue!.asyncAfter(deadline: time + 0.5){
guard self.currentTime == time else {
return
}
let tempOperation = BlockOperation(block:{
self.displayDialog()
})
self.operationQueue.addOperation(tempOperation)
}
operationQueue and mainAsycQueue are defined in viewDidLoad as
self.currentTime = DispatchTime.now()
self.operationQueue = OperationQueue()
How can I call my function displayDialog() in the main thread so that it loads faster?
Based on the question title, answer is:
let deadlineTime = DispatchTime.now() + .seconds(1)
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
//update UI here
self.displayDialog()
}
or
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.displayDialog()
}
I don't think it needs to be anywhere near that complicated. You can just use a Timer;
class MyClass: UIViewController {
var tapCount = 0
var tapTimer: Timer?
#IBAction tapped(_ sender: Any) {
if tapCount < 3 {
tapCount += 1
tapTimer?.invalidate()
tapTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { (timer) in
switch (self.tapCount) {
case 1:
self.f1()
case 2:
self.f2()
case 3:
self.f3()
default:
// Hmm, shouldn't happen
}
self.tapCount = 0
})
}
}
The timer will be scheduled on the main queue by default, so there is no need to dispatch anything on the main queue specifically
Use below func, it executes the func in the main thread and no other action will perform during this execution.
DispatchQueue.main.async {
self.displayDialog()
}

delay/sleep in Swift is not working

I have a problem with the sleep function in Swift code. I'm using import Darwin and usleep(400000). Some actions before reaching the sleep are blocked and I dont know why. Here a short example from my code:
#IBAction func Antwort4Button(_ sender: Any) {
if (richtigeAntwort == "4"){
Antwort4.backgroundColor = UIColor.green
Ende.text = "Richtig!"
NaechsteFrage()
}
else {
Ende.text = "Falsch!"
//NaechsteFrage()
}
}
func NaechsteFrage() {
usleep(400000)
Antwort1.backgroundColor = UIColor.red
Antwort2.backgroundColor = UIColor.red
Antwort3.backgroundColor = UIColor.red
Antwort4.backgroundColor = UIColor.red
Ende.text = ""
FragenSammlung()
}
This lines will be not executed:
Antwort4.backgroundColor = UIColor.green
Ende.text = "Richtig!"
Why is calling sleep blocking these actions? If I delete the import Darwin and the sleep, my code works fine. Has anyone an idea? Sorry for my bad english :P
like #jcaron said
here the code to do it:
func delay(delayTime: Double, completionHandler handler:#escaping () -> ()) {
let newDelay = DispatchTime.now() + delayTime
DispatchQueue.main.asyncAfter(deadline: newDelay, execute: handler)
}
Edited: you can create a viewController extension to use in any viewControllers like this:
extension UIViewController {
func delay(delayTime: Double, completionHandler handler:#escaping () -> ()) {
let newDelay = DispatchTime.now() + delayTime
DispatchQueue.main.asyncAfter(deadline: newDelay, execute: handler)
}
}
So in your viewController you just call like this:
delay(delayTime: 2, completionHandler: {
_ in
// do your code here
})
Others alrady answered the question, I just wanted to provide some additional information (cannot comment yet).
You said that Antwort4.backgroundColor = UIColor.green is not executed. To clarify, this is executed, but you don't see the result becuase you call sleep, which is blocking the UI. Here is what happens:
set background color of Antwort4 to green
sleep: block the UI which prevents the app from actually showing the green background
set the background color of Antwort4 to red again
To solve the problem at hand you can use Apples Displatch API. So instead of sleept, you could use:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.Antwort1.backgroundColor = UIColor.red
self.Antwort2.backgroundColor = UIColor.red
self.Antwort3.backgroundColor = UIColor.red
self.Antwort4.backgroundColor = UIColor.red
self.Ende.text = ""
self.FragenSammlung()
}
The usleep() function will block your UI. If you don't want to have this behavior better use the Timer class.

Resources