In the settings for my app, there is a switch allowing the user to turn iPhone's flash on or off (flash is used to indicate certain points in app logic while it's running). What I want to implement is this: when the user toggles this switch on, I want it to, well, flash for a split second to indicate its 'ON' state.
Now, I know how to set torchMode on or off - this is implemented in the app itself, but I'm not sure how to correctly make it 'blink' for settings purpose. One of the ways I thought of is to use following code (toggleFlash() is a static method for toggling torchMode implemented in main code):
UIView.animate(withDuration: 1.0, animations: {
ViewController.toggleFlash(on: true)
}, completion: { (_) in
ViewController.toggleFlash(on: false)
})
This does make it 'blink', but only for a moment - not 1 second. Besides, I'm not so sure if it's at all correct to use animate for this purpose. Another idea is to use Thread.sleep, but this looks like an even worse practice.
Can someone recommend better solutions?
You could use a timer.
func flashForOneSecond() {
ViewController.toggleFlash(on: true)
flashOffTimer = Timer.scheduledTimer(timeInterval:1, target:self, selector:#selector(self.switchFlashOff), userInfo:nil, repeats:false)
}
#objc func switchFlashOff() {
ViewController.toggleFlash(on: false)
}
Probably something like this:
func flash() {
ViewController.toggleFlash(on: true)
let time = DispatchWallTime.now() + DispatchTimeInterval.seconds(1)
DispatchQueue.main.asyncAfter(wallDeadline: time) {
ViewController.toggleFlash(on: false)
}
}
wallDeadline is reliable and the solution is packed in one function.
Related
I am using a for loop coupled with a DispatchQueue with async to incrementally increase playback volume over the course of a 5 or 10-minute duration.
How I am currently implementing it is:
for i in (0...(numberOfSecondsToFadeOut*timesChangePerSecond)) {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)/Double(timesChangePerSecond)) {
if self.activityHasEnded {
NSLog("Activity has ended") //This will keep on printing
} else {
let volumeSetTo = originalVolume - (reductionAmount)*Float(i)
self.setVolume(volumeSetTo)
}
}
if self.activityHasEnded {
break
}
}
My goal is to have activityHasEnded to act as the breaker. The issue as noted in the comment is that despite using break, the NSLog will keep on printing over every period. What would be the better way to fully break out of this for loop that uses DispatchQueue.main.asyncAfter?
Updated: As noted by Rob, it makes more sense to use a Timer. Here is what I did:
self.fadeOutTimer = Timer.scheduledTimer(withTimeInterval: timerFrequency, repeats: true) { (timer) in
let currentVolume = self.getCurrentVolume()
if currentVolume > destinationVolume {
let volumeSetTo = currentVolume - reductionAmount
self.setVolume(volumeSetTo)
print ("Lowered volume to \(volumeSetTo)")
}
}
When the timer is no longer needed, I call self.fadeOutTimer?.invalidate()
You don’t want to use asyncAfter: While you could use DispatchWorkItem rendition (which is cancelable), you will end up with a mess trying to keep track of all of the individual work items. Worse, a series of individually dispatch items are going to be subject to “timer coalescing”, where latter tasks will start to clump together, no longer firing off at the desired interval.
The simple solution is to use a repeating Timer, which avoids coalescing and is easily invalidated when you want to stop it.
You can utilise DispatchWorkItem, which can be dispatch to a DispatchQueue asynchronously and can also be cancelled even after it was dispatched.
for i in (0...(numberOfSecondsToFadeOut*timesChangePerSecond)) {
let work = DispatchWorkItem {
if self.activityHasEnded {
NSLog("Activity has ended") //This will keep on printing
} else {
let volumeSetTo = originalVolume - (reductionAmount)*Float(i)
self.setVolume(volumeSetTo)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)/Double(timesChangePerSecond), execute: work)
if self.activityHasEnded {
work.cancel() // cancel the async work
break // exit the loop
}
}
Here is my situation. (Using Swift 2.2)
I have a list of coordinates (CLLocation). I need to call the reverseGeocodeLocation to fetch the corresponding Place/City. If I try to loop through the elements there is a chance for some calls to fails as Apple suggest to send one call in a second. So I need to add a delay between each calls as well.
Is there any way to achieve this? Any help is appreciated.
(If we have multiple items with same lat, long we only call the api once)
This code declares a set of locations and looks them up one by one, with at least 1 second between requests:
var locations = Set<CLLocation>()
func reverseGeocodeLocation() {
guard let location = locations.popFirst() else {
geocodingDone()
return
}
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
//Do stuff here
//Dispatch the next request in 1 second
_ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
self.reverseGeocodeLocation()
}
}
}
func geocodingDone() {
//Put your finish logic in here
}
FYI I used the block syntax for the Timer, but that only works on iOS 10. If you are using iOS 9 or earlier just use the selector version and it works the same way.
I'm looking for a way to exit a function without using guard. After extensive searching, I cannot find a way to exit a function and call the next at the same time when a button it pressed.
The button calls a repeat using a selector:
#IBAction func BottomLeft(sender: AnyObject) {
NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(0.01), target: self, selector: "bottomLeftMovement", userInfo: nil, repeats: true)
}
This is when the code button is pressed, and the ball will follow this movement:
func bottomLeftMovement() {
Ballx = Ballx - 0.6125
Bally = Bally + 1.2
self.Ball.center.x = Ballx
self.Ball.center.y = Bally
}
I am looking to make it so that when a different button is pressed, that this function will be exited and the corresponding one will be called.
I cannot hard program into the first function, as it is an interchangeable thing, as opposed to a function chain.
Any help would be much appreciated.
What your code is doing is creating a new timer every time you tap on the button. This is not the correct way to do this. I'd recommend you do some research/reading on game development.
But basically, if you're gonna do things this way (which again, is not a good thing (tm)). You need to keep track of your timer in a property, and invalidate it (to stop it). Read about timers too (https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/)
So, in short:
- don't do this
- if you must, track your timer in a property
- invalidate() your timer and start a new one as required
Note: Track and invalidate the timer like this:
(note I'm doing this from memory, not tested, but something along the lines of...)
class someClass {
var myTimer = NSTimer?
func myFunction() {
if let timer = myTimer {
timer.invalidate()
}
timer = NSTimer(...
Are awakeWithContext, willActivate, didDeactivate the same as viewDidLoad, viewWillAppear, viewDidAppear in terms of functionality?
I am porting code from a Swift Apple Watch tutorial that was created back when people had to add their own watch AppViewController file to test their watch apps.
The included files and things have changed with the official watch release of Xcode obviously so I’m wondering where to put where.
For example there is some code in the older AppViewController file and so I just copy/pasted it into the new InterfaceController. I put code that was in viewDidLoad, viewWillAppear, viewDidAppear into awakeWithContext, willActivate, didDeactivate respectively.
It seems the methods are different. I got 1 error saying that setText doesn’t exist:
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)"
…and 2 errors saying view doesn’t exist:
iconLabel.frame = self.view.bounds
self.view.insertSubview(iconLabel, atIndex: 1)
It’s like WatchKit doesn’t use some of the normal property methods or something.
Error Messages:
http://i.imgur.com/wXMdt3c.png
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
self.view.insertSubview(iconLabel, atIndex: 1) // Xcode error
}
override func willActivate() {
super.willActivate()
iconLabel.frame = self.view.bounds // Xcode error
iconLabel.textAlignment = .Center
iconLabel.font = UIFont.boldSystemFontOfSize(132)
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
newBeat()
NSTimer.scheduledTimerWithTimeInterval(8,
target: self,
selector: Selector("newBeat"),
userInfo: nil,
repeats: true)
beat()
}
func newBeat() {
// 1
if ++currentBeatPatternIndex == beatPatterns.count {
currentBeatPatternIndex = 0
}
// 2
currentBeatPattern = beatPatterns[currentBeatPatternIndex]
// 3
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)" // Xcode error
iconLabel.text = currentBeatPattern.icon
}
func beat() {
// 1
UIView.animateWithDuration(currentBeatPattern.duration / 2,
delay: 0.0,
options: .CurveEaseInOut,
animations: {
// 2
self.iconLabel.transform = CGAffineTransformScale(
self.iconLabel.transform, self.shrinkFactor, self.shrinkFactor)
},
completion: { _ in
// 3
UIView.animateWithDuration(self.currentBeatPattern.duration / 2,
delay: 0.0,
options: .CurveEaseInOut,
animations: {
// 4
self.iconLabel.transform = CGAffineTransformScale(
self.iconLabel.transform, self.expandFactor, self.expandFactor)
},
completion: { _ in
// 5
self.beat()
}
)
}
)
}
}
You are right that awakeWithContext, willActivate, and didDeactivate are very similar to the existing UIViewController methods like viewDidLoad, viewWillAppear, and viewDidUnload. The errors you're seeing however are related to the way WatchKit currently works. In order for a watch app to run, all the code is executed on an iPhone but the Apple Watch itself assumes responsibility for the UI elements. What that means is that any views that constitute your watch app MUST be included on your watch app's storyboard. Also, as a direct result, views cannot be instantiated and added to a parent view. All UI elements must be included in your watch app's storyboard and the watch will lay them out based on their arrangement in interface builder. This means you cannot call addSubview and you cannot set an element's frame. You can only adjust its size and set its hidden property to hide or show it on the watch. As far as this method goes –
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)"
You are calling the method wrong. In swift the parameters are included in the parentheses. You can call it this way if it's what you mean
bpmLabel.setText("\(currentBeatPattern.bpm)")
but setText is a method that takes a string parameter and cannot be assigned with =
As far as the animations go, I think you're out of luck. Watch apps currently are more like widgets than iOS apps and things like UIView animations and frame math are not available you. You should definitely read
up on WatchKit because there's no way you'll be able to port an iOS app directly to the watch like this.
Newbie to IOS programming - learning through Swift. I'm writing a simple "slot machine / dice game".
I'm trying to show the user a flashing sequence of rolls before the "actual" roll appears.
func doFancyDiceRoll() {
for x in 1...100 {
Die1.image = PipsImg[RollOne()]
Die2.image = PipsImg[RollOne()]
Die3.image = PipsImg[RollOne()]
}
}
Die1, etc., are defined as generic UIImage views.
In any case, I'm not seeing the 100x iterations, just the images at the end of the loop. I'm assuming that either it redraws "too fast" or that IOS is trying to be smart, and only draws the last images so as to conserve resources.
I'll wildly guess that I need to either implement some kind of delay here, or, IOS needs to be told to explicitly draw out my images, and not try to outthink my intent.
For the delay, I've seen something about NSTimer, but nothing I saw seems to simply say something like "pause for .05" second, and the whole construct was unclear as they were ObjC examples/conversions.
(Note: I've simplified things here --- normally, I would store the value of RollOne() so I can use it later. I also would like to make an array (or collection?) like Die[1].image, but that is another question.)
========== Edit =======
OK, so I'm following up with more of my original code, merged in with that of #doctordoder so we can discuss a bit better. Hopefully that is kosher. (If this appended question is too long, please advise me on the best way to post a lengthy follow-up directly.)
import UIKit
class ViewController: UIViewController {
//( stripping out stuff unneeded for discussion )
// refers to same label below - works but kosher??
#IBOutlet var btnRoll_x: UIView
#IBAction func btnRoll(sender: AnyObject) {
triggerRoll()
}
var timer : NSTimer? = nil
var rolls : Int = 0
func triggerRoll() {
//hiding is bad UI, but until i know how to "disable & dim"
btnRoll_x.hidden = true
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: "doFancyDiceRoll", userInfo: nil, repeats: true);
}
func doFancyDiceRoll() {
Die1.image = PipsImg[randomInt(6)]
Die2.image = PipsImg[randomInt(6)]
Die3.image = PipsImg[randomInt(6)]
if (++rolls > 10)
{
timer?.invalidate()
timer = nil
rolls = 0 // DCB added this presumed missing line
btnRoll_x.hidden = false //again, need to do this better
}
}
}
Hopefully, my implementation of the code is what would have been intended. I made some minor adjustments for (hopeful) clarity.
Good news is I have working code. I have just enough understanding to get in place, but I'm fuzzy on some details.
Here is what I (think I) know...
We declare an NSTImer object, and a roll counter at the main level of the class. I note that in my original version, I had the roll counter scoped within the rolling function itself. Took me a while to understand why it could not live in the DiceRoll loop itself, but now I do. I'm going to express it poorly, but since the timer is going to call DiceRoll multiple instances, it needs to live outside the function.
The button btnRoll gets touched, and invokes triggerRoll().
To prevent the user from touching the button while we are in progress, which put us into a state where the roll counter never got to zero, I hide the button. (I'll figure how to properly put in in disabled state later.)
The timer is set. It fires every .1 second (within limits), and is set to repeat. (until .invalidate ). And it "calls" the function doFancyDiceRoll via the selector: attribute.
So, the big change as previously noted is that doFancy..Roll() no longer loops. It excites a single instance up updating the images. It checks the counter, and if we reach the limit, we kill the timer, which stops the timer (invalidate). (And I unhide the button, making it available again.)
So, a few things I am concerned/wondering about: I get the value of timers for other things that need to happen periodically (take health away every second, check a GPS position every 10 seconds, etc.). It's seems a odd construct to force a screen refresh.
Frankly, I would have expected to see see something like this:
func doFancyDiceRoll() {
for x in 1...100 {
Die1.image = PipsImg[RollOne()] // and 2 and 3 of course.....
VIewController.forceRedraw <<=== something like this, or maybe
ViewController.wait(.05) <<== this?? I dunno ;-)
}
}
instead we end up with about 20 extra lines or so. I'd be interested in knowing if there other approaches that could work keeping the loop intact.
Anyway, assuming this is the one true way to go, I guess my followup to this is how do I pass parameters, since this is not a "real" function call. Trying
selector: "doFancyDiceRoll(40)"
was not objected to by the IDE, but failed in execution.
I had exactly same problem back in days, entire loop is finished before the view is refreshed as #doctordoder mentioned. Solved with using NSTimer
var rollCount = 0
let arrayOfImages = ["image01", "image02", "image03"]
var timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("doFancyDiceRoll"), userInfo: nil, repeats: true)
func doFancyDiceRoll() {
if rollCount == 100 {
timer.invalidate
rollCount = 0
{
else {
//get images from array arrayOfImages[rollCount]
rollCount++
}
}
there could be typos, since I have no Xcode right now.
I have basically the same answer as above :(, but I thought I'd post it anyway.
var timer : NSTimer? = nil
var rolls : Int = 0
func doFancyDiceRoll() {
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: "roll", userInfo: nil, repeats: true);
}
func roll() {
println("rolling")
Die1.image = PipsImg[RollOne()]
Die2.image = PipsImg[RollOne()]
Die3.image = PipsImg[RollOne()]
if (++rolls > 100)
{
timer?.invalidate()
timer = nil
}
}
Rather than NSTimer and invalidating, you can use dispatch_after to do the work for you.
func rollDice(howManyTimes: Int) {
die1.image = PipsImg[RollOne()]
die2.image = PipsImg[RollOne()]
die3.image = PipsImg[RollOne()]
if howManyTimes > 0 {
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) / 10.0))
dispatch_after(delayTime, dispatch_get_main_queue()) {
self.rollDice(howManyTimes - 1)
}
}
}
This will run the code for the number of times specified, delaying each time by 0.1 seconds. It works like this: First it sets the images on each die, then, if there are more iterations, it does a dispatch_after to call itself with rollDice(howManyTimes - 1)
With this, you don't need to maintain a NSTimer and it is pretty self contained.