Random time Swift - iphone

So when a UIButton is pressed I want a random 1 of 4 images to pop up but I want one of the images to pop up at a random time after the button has been pressed. I'm using Xcode and swift 2. Your help is greatly appreciated

you don't need NSTimer (and all the tricky selector based stuff) at all. With help of matt's delay function (somewhere in stackowerflow, i am lazy to find the link :-), it could be as simple as
import Foundation
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
func randomDelay(range: Range<UInt32>, completion: ()->()) {
if let first = range.first,
let last = range.last {
let d = arc4random_uniform(last) + first
print("now: ",NSDate(),"delay:",d)
delay(Double(d)) {
completion()
}
}
}
let range:Range<UInt32> = 3..<10
// run this on button press
randomDelay(range) {
// update your image here
print("complete:", NSDate())
}
it prints something like
now: 2016-04-02 07:33:24 +0000 delay: 9
complete: 2016-04-02 07:33:34 +0000
and on another 'press'
now: 2016-04-02 07:39:55 +0000 delay: 5
complete: 2016-04-02 07:40:00 +0000

Related

Swift, iOS: How to detect GCController button being held when only given a pressedChangedHandler?

My solution is hacky and frankly doesn't even work because the spin loop halts other simultaneous button actions from being recognized. I'd love to be able to use a long press gesture recognizer, but that 's not available on GCController.
let dPadLeftButtonPressedChangedHandler = {
(dPadLeftButton: GCControllerButtonInput, value: Float, pressed: Bool) -> Void in
if dPadLeftButton.isPressed {
// No longer nil
self.moveLeft()
self.nanoTimer = DispatchTime.now()
while (dPadLeftButton.isPressed) {
let nanoTimerNow = DispatchTime.now()
// Thousandths of a second
let timeDifference = (nanoTimerNow.uptimeNanoseconds - self.nanoTimer!.uptimeNanoseconds) / UInt64(1000000)
// 200 / 1000 is .2 seconds
if timeDifference > 250 {
self.hardLeft()
self.nanoTimer = nil
break
}
}
}
}
controller?.extendedGamepad?.dpad.left.pressedChangedHandler = dPadLeftButtonPressedChangedHandler
I solved this by initialzing a CADisplayLink object to call the function I want repeated via the selector argument. Then on the button release, I invalidate the CADisplayLink.

Delay loop calling animation process

Here is a section of my code where I am trying to delay a function called dropText that drops a name from the top of the screen. I tried using a delay function but it delays then drops them all at once. What am I missing, or is this method just plain wrong? Thanks in advance:
func delay(_ delay:Double, closure:#escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
//New group choose method
func groupChoose()
{
//loop through the players
for x in 0...players - 1{
//drop the name in from the top of the screen
delay(2.0) {
self.dropText(playing[x])
}
}
This issue is because you are delaying all of them at once! You should try to assign different delay time to each one:
for x in 1...players {
//drop the name in from the top of the screen
delay(2.0 * x) {
self.dropText(playing[x-1])
}
Refactored
Try to not call array elements by index:
for playing in playing.enumerated() {
// drop the name in from the top of the screen
let player = playing.offset + 1
delay(2.0 * player) {
self.dropText(playing.element)
}
Look at the loop. You are calling asyncAfter almost immediately one after another. So the text is dropped after the delay almost immediately one after another, too.
I recommend to use a Timer
func delay(_ delay: Double, numberOfIterations: Int, closure:#escaping (Int) -> Void) {
var counter = 0
Timer.scheduledTimer(withTimeInterval: delay, repeats: true) { timer in
DispatchQueue.main.async { closure(counter-1) }
counter += 1
if counter == numberOfIterations { timer.invalidate() }
}
}
and
func groupChoose()
{
delay(2.0, numberOfIterations: players) { counter in
self.dropText(playing[counter])
}
}

current time after AVPlayer seek is incorrect

func seekFullRead(seconds: Float64, completion: (() -> ())? = nil) {
let targetTime:CMTime = CMTimeMakeWithSeconds(seconds, preferredTimescale: 60000)
fullReadPlayer?.currentItem?.seek(to: targetTime, toleranceBefore: .zero, toleranceAfter: .zero, completi[enter image description here][1]onHandler: { (finish) in
if finish {
completion?()
}
})
}
fullReadTimeObserver = fullReadPlayer?.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main, using: { [weak self](time) in
guard let self = self else { return }
if self.fullReadPlayer?.status == .readyToPlay {
self.delegate?.audioPlayer(self, didUpdateCurrentTime: time.seconds, teach: nil)
}
})
When I seek to 4.57 seconds, the correct current time will be displayed first, then the current time will be 0.2 seconds forward, but playback will start after the current time will be 0.2 seconds forward.
Logs:
current time: 1.30104062
current time: 1.401042787
seek to : 4.579999923706055
current time: 1.498295786
current time: 4.579983333333334
current time: 4.319330793
current time: 4.319642834
current time: 4.401050459
current time: 4.501045084
current time: 4.601038959
I think NSGangster had the point. Observer timer and seek are on different processes. What this means to me is I will have to handle the discrepancies by myself.
I did not find an answer on the internet and had no luck in finding out a way to make this correct, but I did manage to find a workaround: I could use a variable like 'isSeekInProgress' to mark whether to update the progress bar UI. Illustrated below:
var isSeekInProgress: Bool = false // Marker
let targetTime:CMTime = CMTimeMakeWithSeconds(seconds, preferredTimescale: 60000)
isSeekInProgress = true // Mark
fullReadPlayer?.currentItem?.seek(to: targetTime, toleranceBefore: .zero, toleranceAfter: .zero, completi[enter image description here][1]onHandler: { (finish) in
if finish {
isSeekInProgress = false // Unmark
completion?()
}
})
Some people pause the player while seeking then resume playback in the completion block, but this won't work in the case you want the playback to keep going while scrubbing. The above example is fairly painless, and all you have to do is check:
if !playbackManager.isSeekInProgress {
progressSlider.value = playbackManager.progress
}
And during the next periodic observer notification, your progress will be updated.

Smooth animation with timer and loop in iOS app

I have ViewController with stars rating that looks like this (except that there are 10 stars)
When user opens ViewController for some object that have no rating I want to point user's attention to this stars with very simple way: animate stars highlighting (you could see such behaviour on some ads in real world when each letter is highlighted one after another).
One star highlighted
Two stars highlighted
Three stars highlighted
......
Turn off all of them
So this is the way how I am doing it
func delayWithSeconds(_ seconds: Double, completion: #escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
completion()
}
}
func ratingStarsAnimation() {
for i in 1...11 {
var timer : Double = 0.6 + Double(i)*0.12
delayWithSeconds(timer) {
ratingStars.rating = (i < 10) ? Double(i) : 0
}
}
}
What is going on here? I have function called delayWithSeconds that delays action and I use this function to delay each star highlighting. And 0.6 is initial delay before animation begins. After all stars are highlighted - last step is to turn off highlighting of all stars.
This code works but I can't say that it is smooth.
My questions are:
How can I change 0.6 + Double(i)*0.12 to get smooth animation feel?
I think that my solution with delays is not good - how can I solve smooth stars highlighting task better?
Have a look at the CADisplaylink class. Its a specialized timer that is linked to the refresh rate of the screen, on iOS this is 60fps.
It's the backbone of many 3rd party animation libraries.
Usage example:
var displayLink: CADisplayLink?
let start: Double = 0
let end: Double = 10
let duration: CFTimeInterval = 5 // seconds
var startTime: CFTimeInterval = 0
let ratingStars = RatingView()
func create() {
displayLink = CADisplayLink(target: self, selector: #selector(tick))
displayLink?.add(to: .main, forMode: .defaultRunLoopMode)
}
func tick() {
guard let link = displayLink else {
cleanup()
return
}
if startTime == 0 { // first tick
startTime = link.timestamp
return
}
let maxTime = startTime + duration
let currentTime = link.timestamp
guard currentTime < maxTime else {
finish()
return
}
// Add math here to ease the animation
let progress = (currentTime - startTime) / duration
let progressInterval = (end - start) * Double(progress)
// get value =~ 0...10
let normalizedProgress = start + progressInterval
ratingStars.rating = normalizedProgress
}
func finish() {
ratingStars.rating = 0
cleanup()
}
func cleanup() {
displayLink?.remove(from: .main, forMode: .defaultRunLoopMode)
displayLink = nil
startTime = 0
}
As a start this will allow your animation to be smoother. You will still need to add some trigonometry if you want to add easing but that shouldn't be too difficult.
CADisplaylink:
https://developer.apple.com/reference/quartzcore/cadisplaylink
Easing curves: http://gizma.com/easing/

Implementing NSTimer into Code - Swift

I am having a bit of trouble using / implementing NSTimer into code. So far, i have this:
//Delay function from http://stackoverflow.com/questions/24034544/dispatch-after-gcd-in-swift/24318861#24318861
func delay(delay:Double, closure:()->()) {
dispatch_after(
//adds block for execution at a specific time
dispatch_time(
//creates a dispatch time relative to default clock
DISPATCH_TIME_NOW,
//Indicates that something needs to happen imediately
Int64(delay * Double(NSEC_PER_SEC))
//a 64bit decleration that holds the delay time times by the amount of nanoseconds in one seconds, inorder to turn the 'delay' input into seconds format
),
dispatch_get_main_queue(), closure)
}
#IBAction func computerTurn(){
if(isFirstLevel){levelLabel.text = ("Level 1"); isFirstLevel = false}
else{ level++ }
var gameOrderCopy = gameOrder
var randomNumber = Int(arc4random_uniform(4))
gameOrder.append(randomNumber)
var i = 0
var delayTime = Double(1)
println(Double(NSEC_PER_SEC))
for number in self.gameOrder{
if number == 0{
delay(delayTime++){self.greenButton.highlighted = true}
self.delay(delayTime++){
self.greenButton.highlighted = false
}
}
else if number == 1{
delay(delayTime++){self.redButton.highlighted = true}
self.delay(delayTime++){
self.redButton.highlighted = false
}
}
else if number == 2{
delay(delayTime++){self.yellowButton.highlighted = true}
self.delay(delayTime++){
self.yellowButton.highlighted = false
}
}
else if number == 3{
delay(delayTime++){self.blueButton.highlighted = true}
self.delay(delayTime++){
self.blueButton.highlighted = false
}
}
println(delayTime)
}
}
What i need to do, is replace the timer function, or get rid of it, and do the same as whats happening here, but using NSTimer.
Thanks
NSTimer can be a little clunky in this context because it requires using either target-action or NSInvocation. However, NSTimer is toll-free bridged with CFRunLoopTimer, which you can call with a block:
func delay(delay:Double, closure:()->()) {
let fireDate = delay + CFAbsoluteTimeGetCurrent()
let timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, fireDate, 0, 0, 0) { _ in
closure()
}
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes)
}
I would suggest:
Setting up some class property to maintain the "current" numeric index (which you'd start at 0);
Start your repeating timer;
The timer's handler would use the index to figure out which button to change and then do one "turn highlight on" and then do a dispatch_after for the "turn highlight back off";
Then, this handler would look at the index and determine if you're at the end of the list or not. If you are, then cancel the timer and you're done. If you're not at the end of the list, then increment the "current index" and wait for the next scheduled timer fire off.