I have this repeating animation where a backgroundColor phases back and forth between two options
private func phase() {
let duration = TimeInterval.random(in: 1.2...2.6)
let delay = TimeInterval.random(in: 0.2...3.0)
let now = Date()
UIView.animate(withDuration: duration, delay: delay, options: [.beginFromCurrentState, .curveEaseInOut]) {
self.backgroundColor = self.useAlt ? .red : .blue
} completion: { finished in
let delta = abs(now.timeIntervalSinceNow)
let dd = abs(duration + delay)
print("Finished with discrepancy:", delta - dd)
self.useAlt.toggle()
self.phase()
}
}
var useAlt = false
Normally when I run this I get logs like these
Finished with discrepancy: 0.022582966519481662
Finished with discrepancy: 0.018295981202324896
Finished with discrepancy: 0.0265824227200131
Finished with discrepancy: 0.02452599005364453
Finished with discrepancy: 0.014387094118363919
However go into the phone settings and in Accessibility, turn on Bold Text which pertains to traitCollection.legibilityWeight the delay and duration is completely ignored, the colour flashes wildly between the two states and the logs read
Finished with discrepancy: -4.234233809309864
Finished with discrepancy: -2.6556281772023143
Finished with discrepancy: -3.2806602018580384
Finished with discrepancy: -2.1561019119347353
Finished with discrepancy: -2.098664491903789
The duration and delay is completely ignored.
I can fix with guard traitCollection.legibilityWeight != .bold else { return } however, what is going on here? I'm a bit worried the other animations in my app will be impacted but I haven't found any sign of it actually... its very strange.
Related
I have the following function on iOS that performs UI work on a TableView, and the challenge is that if it begins to take longer then 0.5sec. a spinner should be displayed to the user so that the screen doesn't look like it froze.
func updateForm(with rowItems: [RowItem]) {
self.tableView.beginUpdates() // performance tweak.
let viewControllerName = String.init(describing: self.classForCoder) // id
var defaultSection = Form.createSectionWith(tag: viewControllerName, in: form)
// MARK: - update rows
let allRows = self.form.allRows
let startTime = CFAbsoluteTimeGetCurrent()
var showSpinner = false
for (index, item) in rowItems.enumerated() {
.
.
.
<TableView processing work on 100's of rows>
.
.
.
// Evaluate our running time for this process loop, and display spinner if we're past a threshold of seconds.
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
if timeElapsed > 0.5 && showSpinner == false {
showSpinner = true
self.showActivityIndicator(withStatus: "processing") // {NEVER GETS DISPLAYED}
}
} // for (index, item) ...
Of course, when I make the call to showActivityIndicator, it never actually gets displayed.
How can I interrupt and pause the UI work to have the showActivityIndicator spinner animation show-up, then let the loop continue?
You need to get off the main thread in order to allow the run loop to cycle and the redraw moment to occur. A simple delay after showing the spinner before proceeding with your work will solve the problem. But you will have to reorganize your code to allow for that. The easiest solution is to give up your 0.5 second idea and just show the spinner before starting in the first place.
I think there are some more important architectural questions that you need to be addressing with that code, but as a solution to the question asked...
before you start processing the data, create a timer with a delay that will load the spinner when it fires
let delayInSeconds = 5.0
let timer = Timer.scheduledTimer(withTimeInterval: delayInSeconds, repeats: false){
//closure that loads the spinner view on top of the current view
}
then when the processing activity is complete
timer?.invalidate() //cancels the timer if it hasn't fired
and then unload the spinner view
I do push notifications. If there is no data in the database, they downloaded before display. I did a time interval using Grand Central Dispatch, but it is not right because for example if the user is a weak Internet application falls.
How delay with barrier of GCD the display of the data before download data?
This code with time interval:
if equal == false {
let url = "https://****.**/json/post/\(data)"
self.download.getLastestNews(url)
}
let when = DispatchTime.now() + 2
DispatchQueue.main.asyncAfter(deadline: when) {
let newsCategory = self.realm.objects(News.self).filter("newsID == \(self.id)")
vc.titleText = (newsCategory.first?.newsTitle)!
vc.fullText = (newsCategory.first?.newsFullText)!
vc.imageLink = (newsCategory.first?.newsImage)!
if let tabBarController = self.window?.rootViewController {
tabBarController.present(vc, animated: true, completion: nil)
}
}
Platform iOS 9.*, iOS 3
Delaying the queue won't always work because as you said, sometimes Internet connection is too slow and there might be no data received, so the program may fail with error. While downloading anything from Internet, check for completion handling methods. Inside them use the code you gave in DispatchQueue closure.
I have a problem with my animated image.
In my page, I have a label in the center, initialised with a text "Start dictation" and an Image at the bottom initialised without image
There is my code :
func dictation() {
let seconds = 1.0
let delay = seconds * Double(NSEC_PER_SEC) // nanoseconds per seconds
let dispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
self.label.setText("")
self.myImage.setImageNamed("frame-")
self.myImage.startAnimatingWithImagesInRange(NSMakeRange(0, 15), duration: 0.5, repeatCount: 0)
})
presentTextInputControllerWithSuggestions([], allowedInputMode: .Plain, completion: { (selectedAnswers) -> Void in
if ((selectedAnswers != nil) && (selectedAnswers!.count>0) ){
if selectedAnswers![0] is String {
self.myImage.stopAnimating()
self.myImage.setImageNamed("")
self.label.setText((selectedAnswers![0] as! String))
}
}
})
}
When my dictation is finished, there is a time before the displaying of my text. So, I tried to add animation to see that it's in progress.
Here, I want to start my dictation, start in background my animation and clear my text. And, when my speech is ready to be display, I want to stop and clear the animation and print my text.
My problem is : sometimes, when I come back on my page after dictation, I found my first text "Start dictation" and not my animation.
I tried with debug mode and I added breakpoints and logs in all my code. All is executed in the good order but the result is really random..
I saw also that my animation doesn't stop when I use stopAnimating() and doesn't clear when I use setImageNamed("").
Could you help me ?
When I started animation, I wasn't on the main page so, the code was executed but "self" was not my main page.
To solve this, I just call my animation in the willActivate function when I come back on the main page
I'm totally new to iOS development and am working on an iPhone cooking app that gives the user the choice of three 'timer' options. The first timer runs for 6 mins, the second for 8.5 mins and the last for 11 mins.
Once the timer finishes counting down it plays an audio file and displays a message within the app screen. Everything works perfectly, except that I've discovered in testing that the timer stops running while the user goes to another app (e.g. checking email, using Safari, etc). Obviously, this defeats the purpose of the app as the user needs to know when the timer is finished so they can do the next step (e.g. remove a saucepan from the stove).
I've researched background modes and am getting confused. It seems that I literally have no reason (according to Apple) to run this app in the background (i.e. it's not playing music, using locations services, etc). I also keep reading that there's a 10 min limit to running in the background otherwise.
I also come across the idea of local and remote notifications, but the page I was referred to no longer exists on Apple's developer site. I'm now at a loss and confused.
Is there a way for me to actually get this app to work in the background for up to 11 minutes? If so, how?
Here's an update. I've been trying to get my head around Local Notifications and Background Tasks.
LOCAL NOTIFICATIONS
This showed some promise, but I'm not sure how I would implement this in my scenario? How would I ensure the right amount of time passes before the notification appears/plays a sound?
For example, the user selects the button for 'soft boiled eggs' at exactly 12:00:00pm and the app starts a counter for 6 mins. At 12:01:20pm the user reads an email, taking 30 seconds before putting the phone down at 12:01:50 to read the paper. Let's assume at 12:02:50 the phone goes into lock mode, how do I ensure the local notification triggers 3mins and 10secs later to make up the whole 6mins and play the sound file notifying the user their eggs are ready.
BACKGROUND TASKS
This may work for my scenario if I can start and restart background tasks to allow my timer to complete before playing the sound.
Below is a snippet of my code (relating to the eggs example above) that I hope will help put my app in context:
#IBAction internal func ButtonSoft(sender: UIButton) {
counter = 360
TimerDisplay.text = String("06:00")
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: "Eggs done!!", repeats: true)
ButtonSoft.alpha = 0.5
ButtonMedium.alpha = 0.5
ButtonHard.alpha = 0.5
ButtonSoft.enabled = false
ButtonMedium.enabled = false
ButtonHard.enabled = false
}
#IBAction internal func ButtonMedium(sender: UIButton) {
counter = 510
TimerDisplay.text = String("08:30")
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: "Eggs done!!", repeats: true)
ButtonSoft.alpha = 0.5
ButtonMedium.alpha = 0.5
ButtonHard.alpha = 0.5
ButtonSoft.enabled = false
ButtonMedium.enabled = false
ButtonHard.enabled = false
}
#IBAction internal func ButtonHard(sender: UIButton) {
counter = 660
TimerDisplay.text = String("11:00")
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: "Eggs done!!", repeats: true)
ButtonSoft.alpha = 0.5
ButtonMedium.alpha = 0.5
ButtonHard.alpha = 0.5
ButtonSoft.enabled = false
ButtonMedium.enabled = false
ButtonHard.enabled = false
}
func stopTimer() {
if counter == 0 {
timer.invalidate()
}
}
func updateCounter() {
counter--
let seconds = counter % 60
let minutes = (counter / 60) % 60
let strMinutes = minutes > 9 ? String(minutes) : "0" + String(minutes)
let strSeconds = seconds > 9 ? String(seconds) : "0" + String(seconds)
if seconds > 0 {
TimerDisplay.text = "\(strMinutes):\(strSeconds)"
}
else {
stopTimer()
TimerDisplay.text = String("Eggs done!!")
SoundPlayer.play()
}
}
#IBAction func ButtonReset(sender: AnyObject) {
timer.invalidate()
stopTimer()
TimerDisplay.text = String("Choose your eggs:")
ButtonSoft.alpha = 1.0
ButtonMedium.alpha = 1.0
ButtonHard.alpha = 1.0
ButtonSoft.enabled = true
ButtonMedium.enabled = true
ButtonHard.enabled = true
}
In terms of running background tasks, I've come across the following example of code.
To create the background task:
func someBackgroundTask(timer:NSTimer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { () -> Void in
println("do some background task")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
println("update some UI")
})
})
}
And the below line of code to (I think) use a timer to run the above function:
var timer = NSTimer(timeInterval: 1.0, target: self, selector: "someBackgroundTask:", userInfo: nil, repeats: true)
And the below code to stop it all:
timer.invalidate()
So, how would I adapt this for my scenario? If this isn't possible, how would I use local notifications in my app?
Or do I just give up on the iPhone version of this app (my Apple Watch version seems to work fine).
In a word, no. You can ask for background time, but recent versions of iOS give you 3 minutes.
If you are a background sound playing app or navigation app you are allowed to run in the background for longer, but you have to ask for those permissions and the app review board will check.
The bottom line is that third parties can't really do a timer app that counts down an arbitrary time longer than 3 minutes.
You might want to use timed local notifications. You can make those play a sound when they go off. Search in the Xcode docs on UILocalNotification.
Edit in Aug 2020: I would no longer recommend this approach.
I have had some success by starting a background task, then setting a timer for just under a minute. When the timer fires, I start a new background task and end the old one. I just keep rolling over the background task, creating a new one every minute.
I have a strange problem with my countdown timer. It fires off normally when my start button is hit, and is reinstantiated correctly when I close the app and relaunch it again. However, when I select a different tab and stay there for a while, it stops counting down, then resumes counting down from where it left off when I show the countdown tab again.
For example, if the timer is now at 00:28:00 (format is HH:MM:SS), select some other tab, stay there for 5 minutes, and then go back to the timer tab, it's only at the 27:52 mark. When I close the app (double tap the home button, swipe up my app) and reopen it, it starts off at a more reasonable 22:50 mark.
I've posted the relevant code from the class to show how I'm setting up the timer, but a summary of what it does:
I have plus (+) and minus (-) buttons somewhere that, when tapped, call recalculate().
recalculate() fires off a CalculateOperation.
A CalculateOperation computes for the starting HH:MM:ss based on the addition/removal of a new record. The successBlock of a CalculateOperation executes in the main thread.
A CalculateOperation creates the NSTimer in the successBlock if the countdownTimer hasn't been created yet.
The NSTimer executes decayCalculation() every 1 second. It reduces the calculation.timer by 1 second by calling tick().
Code:
class CalculatorViewController: MQLoadableViewController {
let calculationQueue: NSOperationQueue // Initialized in init()
var calculation: Calculation?
var countdownTimer: NSTimer?
func recalculate() {
if let profile = AppState.sharedState.currentProfile {
// Cancel all calculation operations.
self.calculationQueue.cancelAllOperations()
let calculateOperation = self.createCalculateOperation(profile)
self.calculationQueue.addOperation(calculateOperation)
}
}
func decayCalculation() {
if let calculation = self.calculation {
// tick() subtracts 1 second from the timer and adjusts the
// hours and minutes accordingly. Returns true when the timer
// goes down to 00:00:00.
let timerFinished = calculation.timer.tick()
// Pass the calculation object to update the timer label
// and other things.
if let mainView = self.primaryView as? CalculatorView {
mainView.calculation = calculation
}
// Invalidate the timer when it hits 00:00:00.
if timerFinished == true {
if let countdownTimer = self.countdownTimer {
countdownTimer.invalidate()
}
}
}
}
func createCalculateOperation(profile: Profile) -> CalculateOperation {
let calculateOperation = CalculateOperation(profile: profile)
calculateOperation.successBlock = {[unowned self] result in
if let calculation = result as? Calculation {
self.calculation = calculation
/* Hide the loading screen, show the calculation results, etc. */
// Create the NSTimer.
if self.countdownTimer == nil {
self.countdownTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("decayCalculation"), userInfo: nil, repeats: true)
}
}
}
return calculateOperation
}
}
Well, if I leave the app in some other tab and not touch the phone for a while, it eventually goes to sleep, the app resigns active, and enters the background, which stops the timer.
The solution was to set my view controller as a listener to the UIApplicationWillEnterForegroundNotification and call recalculate to correct my timer's countdown value.