I edited some code based on what others have pointed out, but I keep getting the error stated above, saying I sent an "unrecognised selector". The selector for my timer, originally the error, has been amended, but Xcode is still complaining.
Here is my code:
import UIKit
class ViewController: UIViewController {
#IBOutlet var instructionsNew: UILabel!
#IBOutlet var lockStatusNew: UIImageView!
#IBOutlet var timerText: UILabel!
#IBAction func hackLockButton(sender: AnyObject){
var counter = 0
let timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "updateCounter", userInfo: nil, repeats: true)
func updateCounter() {
timerText.text = String(counter++)
}
while(timerText.text == "1") {instructionsNew.text = "loading"}
while(timerText.text == "2"){instructionsNew.text = "loading."}
while(timerText.text == "3") {instructionsNew.text = "loading.."}
while(timerText.text == "4"){instructionsNew.text = "loading..."}
while(timerText.text == "5") {instructionsNew.text = "hack successful!"
lockStatusNew.image = UIImage(named: "unlocked.png")
timer.invalidate()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Please help me spot the problem. Thanks!
The issue is, you added the updateCounter inside the hackLockButton function.
You should place the method outside that function and it will work.
#IBAction func hackLockButton(sender: AnyObject)
{
// Code here
}
func updateCounter()
{
timerText.text = String(counter++)
}
Suggestion:
You don't want to write while(timerText.text == "1") {instructionsNew.text = "loading"} for showing that label. It can cause an infinite loop and hang your UI. Instead use a switch case like:
switch(counter)
{
case 1: instructionsNew.text = "loading"
// Write other cases too
}
I think you really, really need to get your head around how a timer works.
Your application has a run loop. When the user does anything, the run loop will call the appropriate code in your program, runs the code, and finish when that code is run. For example when you tap on a button, the run loop will call your button callback function, wait for it to finish, and then it can wait for the next thing to happen.
A scheduled timer inserts calls into that run loop. So every second the run loop calls updateCounter. updateCounter should do some stuff, and then return. It's not supposed to wait in a while loop at all. The while () inside it is badly, badly wrong.
You also do some other things upside down. You use your timerText label to control things. That's wrong. The label should display things. The updateCounter can update the counter, but then all other actions should depend on the value of the counter, not on the value of a user interface label! Imagine your boss tells you to display not 1, 2, 3, 4, 5 but one, two, three, four, five. You obviously change what goes into the label. But with your code you have to change code everywhere that reads the text of the label. Now imagine you don't want one, two, three, but the right text in the user's language...
Related
I am trying to learn how to use "ON" and "OFF" buttons and the basics of swift in general. Everything is going well but when I try to create an infinite loop of vibration using the "ON" UIButton, the loop keeps reiterating and I can't press the "OFF" button to stop it.
I tried looking up ways to stop it but none of them mention how to apply the code. I am still new and learning how to use swift. I read about "UIViewAnimationOptionAllowUserInteraction" but I don't know how to put it into my code.
#IBOutlet weak var label: UILabel!
#IBAction func onSwitch(_ sender: UIButton) {
label.text = "ON"
vibrate()
}
#IBAction func offSwitch(_ sender: UIButton) {
label.text = "OFF"
vibrate()
}
func vibrate() {
while label.text == "ON" {
AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
}
}
Your code is continuously re-running tasks in while loop. This is happening on a main thread, so your application is unable to catch off button tap, because every tick is consumed to re-run the AudioServicesPlayAlertSound().
The While Loop is wrong choice here !
You only vibrate is once by replacing the While With if condition to test your code correctly
OR
you can use Timer to vibrate every 3-5 Seconds
let timer = Timer.scheduledTimer(timeInterval: 0.4, target: self, selector: #selector(self.vibrate), userInfo: nil, repeats: true)
// must be internal or public.
#objc func vibrate() {
// Something cool
}
In a single VC app,
a info label is used to display textually the number of clicks of a button,
idea is to toggle the text on even/ uneven number of clicks. I did this ok,
but the problem came when I tried to link couple ( 9 ) UIButtons to the single IBAction where count and label updates happen. Here the label update lags one step behind the real count ( verified by print(counter) on Console).
May someone help why this happens?
I can do it of course with separate IBAction for each UIButton,
but one action and buttons tags works fine, so it would be nice to keep the code less by single IBAction.
import UIKit
class ViewController: UIViewController {
var player:Player = .cross
#IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.label.text = "Player \(player.rawValue) is on the move"
}
#IBAction func Move(_ sender: UIButton) {
if player == .cross {
player = .circle
} else {
player = .cross
}
print(sender.tag)
print("Player \(player.rawValue) is on the move")
self.label.text = "Player \(player.rawValue) is on the move"
}
#IBAction func button(_ sender: UIButton) {
if player == .cross {
player = .circle
} else {
player = .cross
}
print(sender.tag)
print("Player \(player.rawValue) is on the move")
self.label.text = "Player \(player.rawValue) is on the move"
}
}
// the move function is the common IBAction, which lags the label update
// the func button is action for a separate button, doing label updates correctly
// debug progress: similar code worked fine without lag on other desktop,
moreover working code there clean, lags when i run it on my desktop.
I have doubts it might be simulator issue.
For the record, my xcode did it's latest amend few days ago.
In swift, how can I run some code 1 second after a user stops typing into a textfield? There is textFieldDidChange but that will run code immediately after a new character is typed.
Try with this custom UITextField, you need setup a timer to 1 second every time a user put a character in your UITextField, and invalidate the timer before re-schedule again, I added the action as closure to allow any kind of action, giving full freedom in you implementation
Added Inspectable property for delay customization
import UIKit
#IBDesignable
class AfterOneSecondTextField: UITextField {
#IBInspectable var delayValue : Double = 1.0
var timer:Timer?
var actionClosure : (()->Void)?
override func awakeFromNib() {
super.awakeFromNib()
self.addTarget(self, action: #selector(changedTextFieldValue), for: .editingChanged)
}
func changedTextFieldValue(){
timer?.invalidate()
//setup timer
timer = Timer.scheduledTimer(timeInterval: delayValue, target: self, selector: #selector(self.executeAction), userInfo: nil, repeats: false)
}
func executeAction(){
actionClosure?()
}
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
}
Use it
You only need to set the class, and pass a closure to your desired action in my case is debugPrint but you can do whatever you need
class ViewController: UIViewController {
#IBOutlet weak var tfText: AfterOneSecondTextField!
override func viewDidLoad() {
super.viewDidLoad()
tfText.actionClosure = {
debugPrint("Test")
}
}
This works!, was tested
Hope this helps
What I have done is use a cancelable closure to run code after some delay; that way as the user types the closure keeps getting cancelled, but when they pause long enough or finish the code to search or fetch is actually run after a brief delay.
I used the cancelable closure code from here:
Cancel a timed event in Swift?
Then the code that goes into textFieldDidChange looks like:
fileprivate var delayedSearchClosure : dispatch_cancelable_closure?
open func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if let searchClosure = delayedSearchClosure {
searchClosure(true)
delayedSearchClosure = .none
}
delayedSearchClosure = delayClosure(0.8) { [weak self] in
self?.doSearch(searchText)
self?.delayedSearchClosure = .none
}
}
To explain what is happening here - in the callback for the textfield as the user types, I first terminate the existing closure created to run at a specific time. Then in the next block of code, it makes a new delayedSearchClosure that will execute 0.8 seconds from now and call "doSearch()", then toss away the closure.
Thus as the user types, closures are being thrown away that no longer matter. Eventually the user will pause or stop typing, at that point the code you want run is actually executed (in this case a search is performed based on the text). This gives you a search form that feels responsive but does not waste time with an added search launched for every character a user types (which can add overhead and start to impact the UI performance).
I am trying to print a value from a slider at regular intervals. But only print the value if it is different to that last printed. I also do not want to miss any of the output values from the slider.
To do this I have created an array and added an element to the start of that array if it is different to the one already at the start. I have then used a repeating NSTimer to regularly call a function that prints the last element in the array before removing it from the array.
What happens when I run the app is the NSTimer stops anything being printed for it's set time, but then all of the elements print at once and more than one of each print. I've tried messing about with lots of different things - this is the closest I have got to making it work.
If you need to know any more info let me know.
I really appreciate any help given, thanks very much.
var sliderArray: [Float] = []
var timer: NSTimer!
let step: Float = 1
#IBAction func sliderValueChanged(sender: AnyObject)
{
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
if sliderArray.first != slider.value
{
sliderArray.insert(slider.value, atIndex: 0)
}
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(sendSliderPosition), userInfo: nil, repeats: true)
}
func sendSliderPosition()
{
if sliderArray.count > 0
{
print(self.sliderArray.last)
sliderArray.removeLast()
}
}
I would suggest using CADisplayLink. A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display. Which is perfect for your slider case.
This will also not trigger unnecessary call when the slider or the UI is at rest.
class C: UIViewController {
var displayLinkTimer: CADisplayLink?
#IBOutlet weak var slider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
displayLinkTimer = CADisplayLink(target: self, selector: #selector(printSliderValue))
let runLoop = NSRunLoop.mainRunLoop()
displayLinkTimer?.addToRunLoop(runLoop, forMode: runLoop.currentMode ?? NSDefaultRunLoopMode )
displayLinkTimer?.paused = true
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
displayLinkTimer?.paused = true
}
deinit {
displayLinkTimer?.invalidate()
}
func printSliderValue()
{
let step: Float = 1
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
print(roundedValue)
}
}
The basic idea is this:
--> Every time the screen needs to redraw (this will happen at max around 60 frames per second taking into consideration this is fps rate), we get a chance to perform function.
--> to do so, we add the displayLink to the Run Loop. (Run lopp processes input/ refreshes UI and time slices)
--> NOTE This method wont be called if there is no redraw needed on the screen. This is not a timer per say that fires periodically. It fires when redraw is needed. In Sliders case, we want this to fire when we move slightest of the slider too.
For more info on how it actually works try it out and see the apple documentation. Make sure to invalidate before deinitializing the ViewController.
Figured out the answer, thank to everyone for the help and suggestions:
override func viewDidLoad()
{
super.viewDidLoad()
NSTimer.scheduledTimerWithTimeInterval(0.02, target: self, selector: #selector(sendSliderPosition), userInfo: nil, repeats: true)
}
#IBAction func sliderValueChanged(sender: AnyObject)
{
let step: Float = 1
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
if sliderArray.first != slider.value
{
sliderArray.insert(slider.value, atIndex: 0)
}
}
func sendSliderPosition()
{
if sliderArray.count > 1
{
let end1 = sliderArray.count-2
print(sliderArray[end1])
sliderArray.removeLast()
}
}
Explanation:
If the new slider value is different to the one already in the array then add it to the array at the start. Use an NSTimer to repeatedly call the sendSliderPosition function from viewDidLoad. The function will only be performed if there is more than one element in the array. If there is, print the element before the last one and remove the last. This always ensures that there is one element in the array so the function does not always run and that the element printed is the most recent one that hasn't already been printed.
So I'm doing little timer app in swift and I just have 2 buttons. One to start timer, and one to stop it and reset value to 0. I've figured out everything, and I have this function called timer which increases value for one each second for variable "Time". Problem is that when I click the STOP button, it resets the value to 0 but it keeps counting again.
Question is how do I stop that function from running.
Here is some code
var time = 0
func result() {
time++
print(time)
}
#IBAction func clickToStart(sender: AnyObject) {
result()
}
#IBAction func clickToStop(sender: AnyObject) {
time = 0
print(time)
}
override func viewDidLoad() {
super.viewDidLoad()
var timer = NSTimer()
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("result"), userInfo: nil, repeats: true)
}
Make your timer a member variable and call timer.invalidate() on it
Change your variable timer to be an instance variable. Make it weak, since the system owns it, and when you stop the timer it will be deallocated automatically.
In your clickToStop method, call timer.invalidate().
As others have pointed out using the timer.invalidate() functions, but you can also use flag variables in case you are doing something else not related to time or want another actions to stop.
Basically, you can just create a bool variable and when the user stops the actions, make the bool true. In the other function, make it that if bool is true then don't do the actions unless it's false. This works well for a mute button.
Like in my game, when it's a game over, I have a bool variable touchesInvalid that is at the very top of the touchesBegan function. If the touchesInvalid bool is true, then the user can't do any more actions that involved touch.