Trying to understand button behaviour - ios

this is my first project, and i try to set a button title for some time and after it should reset.
This my code:
The first (test) snippet simply tries to set immediately to "tapped" and then after 2 secs to reset to "tap me".
What i see is disappering the button for 2sec and then only "tap me" again!?
#IBOutlet weak var button1: UIButton!
#IBAction func button1Tapped(sender: UIButton) {
sender.setTitle("tapped", forState: .Highlighted)
NSThread.sleepForTimeInterval(2.0)
sender.setTitle("tap me", forState: .Normal)
}
The second example is more what i really want!
But for some reason I immediately fail here, because the alert simply does not show up!?
Any hints?
#IBOutlet weak var button2: UIButton!
#IBAction func button2Tapped(sender: AnyObject) {
}
func isOK(sender:UIAlertAction!){
button2.setTitle("tapped", forState: .Normal)
NSThread.sleepForTimeInterval(2.0)
button2.setTitle("tap me", forState: .Normal)
}
func isCancel(sender:UIAlertAction!){
}
#IBAction func button2Tapped(sender: UIButton) {
let alert = UIAlertController(title: "Test", message: "button", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title:"OK", style: UIAlertActionStyle.Default, handler: isOK))
alert.addAction(UIAlertAction(title:"Cancel", style: UIAlertActionStyle.Cancel, handler: isCancel))
self.presentViewController(alert, animated: true, completion: nil)
}
OK,
i changed to this:
#IBOutlet weak var button1: UIButton!
#IBAction func button1Tapped(sender: UIButton) {
sender.selected = true
NSThread.sleepForTimeInterval(2.0)
sender.selected = false
}
override func viewDidLoad() {
super.viewDidLoad()
button1.setTitle("tapped", forState: .Selected)
button1.setTitle("tap me", forState: .Normal)
}
but it shows the same behaviour.
Actually it shows "tapped" but only when i keep pressing!?

Firstly, delete all the code you have written. Instead, use this code that I have taken from answer of Matt. Credits goes to him. This function is needed to "delay" the action of something.
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
Now create an #IBAction connection:
#IBAction func button(sender: AnyObject) {
// Okay, the button has been tapped
button.setTitle("tapped", forState: .Normal)
// after two seconds...
delay(2.0) {
// change the button's title:
button.setTitle("tap here!", forState: .Normal)
}
}

One of the problems with the first way that you attempted is that you have simply set the title for two different states of sender: .Highlighted and .Normal. If you never change the state of sender, you'll only ever see the title associated with its current state (.Normal by default).
You could update the title for the same state twice to get the behaviour you want, or you might consider using the .Selected state of the button, depending on your application's implementation and usage of the button.
// Call these two setTitle lines wherever you are instantiating your button
// or otherwise setting up your view:
sender.setTitle("tapped", forState: .Selected)
sender.setTitle("tap me", forState: .Normal)
// Somewhere else, you can call either of these:
sender.selected = YES // sender now shows "tapped"
sender.selected = NO // sender now shows "tap me"
Secondly, we need to address your threading issues.
At some point, the main thread has been scheduled to run the code contained within your isOK method. It also has work scheduled to update the UI. Your main thread's queue might look something like this:
(0): __ execute isOK(..)
(1): __ do some other stuff
(2): __ update the UI
(3): __ do some other stuff
If you update your UI in (0) and then update it again in (0), the first update won't be seen because the queue hasn't been allowed to progress to (2) in between. When you NSThread.sleepForTimeInterval() in (0), you are delaying UI updates and freezing your app.
Instead, you'll want to put your sleep into a different queue and then inject your following commands back onto the main thread after your duration has elapsed.
You can do this in a few ways, but the easiest way is to use GCD (Grand Central Dispatch) and dispatch_after.
#IBAction func button1Tapped(sender: UIButton) {
sender.selected = YES
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(2.0 * Double(NSEC_PER_SEC))),
dispatch_get_main_queue(), ^{
sender.selected = NO
});
}

Related

Swift: textField listener not responding to text change

FYI: I am a JS developer, and recently started learning Swift. So I am not sure I am using the right terminology here.
In my app, I have a simple textField:
It is linked to this outlet:
#IBOutlet weak var activationCodeTextField1: UITextField!
The "listener" is implemented as follows:
self.activationCodeTextField1.addTarget(self, action: #selector(self.textField1DidChange(_:)), for: UIControl.Event.editingChanged)
self.activationCodeTextField1.delegate = self
textField1DidChange function:
#IBAction func textField1DidChange(_ sender: AnyObject) {
print("TextField1DidChange")
}
But, when I type something in the TextField, nothing gets printed.
So maybe textField1DidChange does not get triggered for some reason?
But, I really do not why.
Update: Here is the sample code for you to test in playgrounds.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let textField = UITextField()
textField.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
textField.placeholder = "enter text here"
textField.center = view.center
view.addSubview(textField)
textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
}
#objc func textChanged(_ sender: UITextField) {
print(sender.text)
}
}
PlaygroundPage.current.setLiveView(ViewController())
The event valueChanged should be used to detect to see if the text has changed. editingChanged detects if the state of UITextField has changed between editing or idle states.
Replace:
self.activationCodeTextField1.addTarget(self, action: #selector(self.textField1DidChange(_:)), for: UIControl.Event.editingChanged)
With:
activationCodeTextField1.addTarget(self, action: #selector(self.textField1DidChange), for: .valueChanged)
Add-on: You don't need to put self unless you're inside a closure. And don't need the entire UIControl.Event.valueChanged, you can just put .valueChanged and it'll work fine.
Remove these delegates and target method just use action for editing changed.

Keep AVSpeechSynthesizer playing if another view is presented modally

I am trying to utilize the AVSpeechSynthesizer in swift/xcode to read out some text. I have it working for the most part. I have it set so that if they go back to the previous screen or to the next screen, the audio will stop. But in my instance, I want the speech to continue if another view is presented modally. As an example, I have an exit button that once clicked presents an "Are you sure you want to exit? y/n" type screen, but I want the audio to continue until they click yes and are taken away. I also have another view that can be presented modally, again, wanting the audio to continue if this is the case.
Does anyone have an idea of how I can keep the speech playing when a view is presented modally over top but stop playing when navigating to another view entirely?
Here is my code so far:
//Press Play/Pause Button
#IBAction func playPauseButtonAction(_ sender: Any) {
if(isPlaying){
//pause
synthesizer.pauseSpeaking(at: AVSpeechBoundary.immediate)
playPauseButton.setTitle("Play", for: .normal)
} else {
if(synthesizer.isPaused){
//resume playing
synthesizer.continueSpeaking()
} else {
//start playing
theUtterance = AVSpeechUtterance(string: audioTextLabel.text!)
theUtterance.voice = AVSpeechSynthesisVoice(language: "en-UK")
synthesizer.speak(theUtterance)
}
playPauseButton.setTitle("Pause", for: .normal)
}
isPlaying = !isPlaying
}
//Press Stop Button
#IBAction func stopButtonAction(_ sender: Any) {
if(isPlaying){
//stop
synthesizer.stopSpeaking(at: AVSpeechBoundary.immediate)
playPauseButton.setTitle("Play", for: .normal)
isPlaying = !isPlaying
}
}
//Leave Page
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
synthesizer.stopSpeaking(at: .immediate)
}
The problem lies in your viewWillDisappear. Any kind of new screen will trigger this, so your code indeed synthesizer.stopSpeaking(at: .immediate) will be invoked, thus stopping your audio. And that includes presentation or pushing a new controller.
Now, how to improve that? You've mentioned this:
I have it set so that if they go back to the previous screen or to the
next screen, the audio will stop
First off, if they go back to the previous screen:
You'd want to execute the same stopping of audio code line inside your
deinit { } method. That will let you know that your screen or controller is being erased from the memory, meaning the controller is gone in your controller stacks (the user went back to the previous screen). This should work 100% fine as long as you don't have retain cycle count issue.
Next, to the next screen, easily, you could include the same code line of stopping your audio inside your function for pushing a new screen.
After a lot of research I was able to get the desired functionality. As suggested by Glenn, the correct approach was to get the stopSpeaking to be called in deinit instead of viewWillDisappear. The issue was that using AVSpeechSynthesizer/AVSpeechSynthesizerDelegate normally would create a strong reference to the ViewController and, therefore, deinit would not be called. To solve this I had to create a custom class that inherits from AVSpeechSynthesizerDelegate but uses a weak delegate reference to a custom protocol.
The custom class and protocol:
import UIKit
import AVFoundation
protocol AudioTextReaderDelegate: AnyObject {
func speechDidFinish()
}
class AudioTextReader: NSObject, AVSpeechSynthesizerDelegate {
let synthesizer = AVSpeechSynthesizer()
weak var delegate: AudioTextReaderDelegate!
//^IMPORTANT to use weak so that strong reference isn't created.
override init(){
super.init()
self.synthesizer.delegate = self
}
func startSpeaking(_ toRead: String){
let utterance = AVSpeechUtterance(string: toRead)
synthesizer.speak(utterance)
}
func resumeSpeaking(){
synthesizer.continueSpeaking()
}
func pauseSpeaking(){
synthesizer.pauseSpeaking(at: .immediate)
}
func stopSpeaking() {
synthesizer.stopSpeaking(at: .immediate)
}
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
self.delegate.speechDidFinish()
}
}
And then inheriting and using it in my ViewController:
import UIKit
import AVFoundation
class MyClassViewController: UIViewController, AudioTextReaderDelegate{
var isPlaying = false
let audioReader = AudioTextReader()
let toReadText = "This is the text to speak"
#IBOutlet weak var playPauseButton: UIButton!
#IBOutlet weak var stopButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
self.audioReader.delegate = self
}
//Press Play/Pause Button
#IBAction func playPauseButtonAction(_ sender: Any) {
if(isPlaying){
//Pause
audioReader.synthesizer.pauseSpeaking(at: .immediate)
playPauseButton.setTitle("Play", for: .normal)
} else {
if(audioReader.synthesizer.isPaused){
//Resume Playing
audioReader.resumeSpeaking()
} else {
audioReader.startSpeaking(toReadText)
}
playPauseButton.setTitle("Pause", for: .normal)
}
isPlaying = !isPlaying
}
//Press Stop Button
#IBAction func stopButtonAction(_ sender: Any) {
if(isPlaying){
//Change Button Text
playPauseButton.setTitle("Play", for: .normal)
isPlaying = !isPlaying
}
//Stop
audioReader.stopSpeaking()
}
//Finished Reading
func speechDidFinish() {
playPauseButton.setTitle("Play", for: .normal)
isPlaying = !isPlaying
}
//Leave Page
deinit {
audioReader.stopSpeaking()
playPauseButton.setTitle("Play", for: .normal)
isPlaying = !isPlaying
}
#IBAction func NextStopButton(_ sender: Any) {
audioReader.stopSpeaking()
playPauseButton.setTitle("Play", for: .normal)
isPlaying = !isPlaying
}
}
I hope this helps someone in the future, because this was driving me up the wall.

Why UIButton requires two clicks to change its image

I am new to iOS app development using Swift 4. I used the code below to change the image of button2 by running it in the iOS simulator:
#IBAction func button2(_ sender: Any) {
button2.setImage(UIImage(named: "wrong_answer"), for: .normal)
}
However, button2 was highlighted when I first click on it without changing its image. Then after the second click, the image has been changed in button2.
My question is why the image was not changed in button2 after the first click?
What can I do to change the image after the first click instead of twice? Is this a bug in the iOS simulator of Xcode or it is normal?
You probably have an issue related to UIButton states that is causing this problem.
I don't think it is a simulator bug.
By the way, a good practice you should follow is to name the outlet different than the #IBAction. Let's say:
#IBAction func buttonTapped(_ sender: Any) {
button.setImage(UIImage(named: "image"), for: .normal)
}
Try this:
override func viewDidLoad() {
super.viewDidLoad()
button.setImage(UIImage(named: "image"), for: .selected)
}
#IBAction func buttonTapped(_ sender: Any) {
button.isSelected = !button.isSelected
}
And then the image will be updated automatically when you tap on the button. You can change it to button.isSelected = true if you want to keep the image after the first tap.
Rename your method/action so it differs from the button/property.
Change Any to UIButton since you know its class.
#IBAction func buttonTapped(_ buttonTapped: UIButton) {
buttonTapped. button.isSelected = !button.isSelected
}
Make sure that you are receiving the button callbacks by declaring your view controller a UIButtonDelegate and set the button's delegate property to self.
it's simulator bug. it worked on a real device
#IBAction func button2(_ sender: UIButton) {
button2.setImage(UIImage(named: "wrong_answer"), for: .normal)
}

Disable timer button after validation Swift - Not working

looking for some help for this, I have a timer that works after submitting a password which is great, but I then need to disable the button after the timer starts and is disabled for a period of time, (in the code I have entered a nominal 90 seconds)
however the button is not disabling.
if anybody could show me where I am going wrong that would be awesome.
import UIKit
class appiHour: UIViewController {
var timer = Timer()
var counter = 60
var password_Text: UITextField?
func enableButton() {
self.timerStartButton.isEnabled = true
}
#IBOutlet weak var timerLabel: UILabel!
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.
}
#IBAction func timerStartButton(_ sender: Any) {
var password_Text: UITextField?
let alertController = UIAlertController(title: "To start your own 2 Cocktails for £10 APPi Hour", message: "get a memeber of the team to enter the password, but use it wisely, as you can only use it once per day, with remember great power comes great responsability", preferredStyle: UIAlertControllerStyle.alert)
let tickoff_action = UIAlertAction(title: "let the APPiness commence", style: UIAlertActionStyle.default) {
action -> Void in
self.timerStartButton.isEnabled = false
Timer.scheduledTimer(timeInterval: 90, target: self, selector: #selector(appiHour.enableButton), userInfo: nil, repeats: false)
if let password = password_Text?.text{
print("password = \(password)")
if password == "baruba151" {
self.counter = 60
self.timerLabel.text = String(self.counter)
self.timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(appiHour.updateCounter), userInfo: nil, repeats: true)
}
} else {
print("No password entered")
}
}
alertController.addTextField { (txtpassword) -> Void in
password_Text = txtpassword
password_Text!.isSecureTextEntry = true
password_Text!.placeholder = ""
}
alertController.addAction(tickoff_action)
self.present(alertController, animated: true, completion: nil)
}
#IBOutlet weak var timerStartButton: UIButton!
func updateCounter() {
counter -= 1
timerLabel.text = String(counter)
if counter == 0{
timer.invalidate()
counter = 0
}
}
}
As a secondary question is it possible to run the timer while the app is in the background? i know apple frowns on this aside for Sat Nav, Music apps etc. But is there a method in which the timer is held and a notification is sent locally letting the user know the timer has ended?
thanks in advance.
I suspect that your action may not be hooked up to your button. I just tried the following code with no issues. The button gets disabled, and then enabled 5 seconds later:
class ViewController: UIViewController {
#IBOutlet weak var myButton: UIButton!
#IBAction func ButtonPressed(_ sender: Any) {
myButton.isEnabled = false
Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(myTimerTick), userInfo: nil, repeats: false)
}
func myTimerTick() {
myButton.isEnabled = true
}
}
So make sure your outlets and actions are hooked up to the button correctly. If you right click on your button, you should see the dots filled in next to the outlet and action. You should see similarly filled in dots in your code.
You can further verify it is hooked up by placing a breakpoint in your "timerStartButton" method and making sure that breakpoint is hit.
Edit to further clarify: You need to connect your code to your Interface build objects. See this article from Apple for a complete tutorial on how to do that.
I'm not 100% sure if this is what you mean. But this would at least satisfy the first part of your request: disable a button whilst a timer is running, and re-enable it once the timer stops.
#IBOutlet weak var myButton: UIButton!
#IBOutlet weak var timerCount: UILabel!
#IBAction func buttonPressed(_ sender: UIButton) {
var count = 0
sender.isEnabled = false
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [unowned self] timer in
count += 1
if count == 5 {
count = 0
sender.isEnabled = true
timer.invalidate()
}
self.timerCount.text = "\(count)"
}
}
Here's a couple of screenshots of what you get.
It's enabled when the user starts off, disabled whilst the count is going then reverts back to its original state with counter at 0 and button enabled. Is that what you're going for?
As far as your second question, what do you mean by
the timer is held
Do you want the timer to keep running whilst the app is in the background, then update the user once the timer has elapsed? If so, take a look at this answer which should point you in the right direction: Continue countdown timer when app is running in background/suspended

How to make dynamically changing text in a button in swift?

When user taps "download" button i need to change the title of a button to
"loading..." but the number of dots must dynamically change from 1 to 3. How can i make it for certain amount of time(5-10 sec) ? Or do i just need to setTitle multiple times ?
button.setTitle("loading.", forState: .Normal)
You need to use
button.setTitle("loading.", forState: .Normal)
button.setTitle("loading..", forState: .Normal)
button.setTitle("loading...", forState: .Normal)
whenever you want to change the number of dots.
Call one of the above after a delay or whenever you need to update the button.
EDIT: To prevent your button's title from blinking (as pointed out by teamnorge) use:
UIView.performWithoutAnimation {
button.setTitle("loading...", forState: .Normal)
}
#IBAction func renameClassButton(sender: AnyObject) {
classTopButton.text = "\(classTopTextField)"
}
You can d it this way:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var loading: UIButton!
//Create an array for your button name
let buttonNameArray = ["loading.","loading..","loading..."]
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func loadingButton(sender: AnyObject) {
//Set delay time for changing name
var delayTime = 1.0
for name in buttonNameArray {
self.delay(delayTime++){
self.loading.setTitle(name, forState: .Normal)
}
}
}
//Function for delay
func delay(delay:Double, closure:()->()) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,Int64(delay * Double(NSEC_PER_SEC))),dispatch_get_main_queue(), closure)
}
}
And your result will be:
Hope this will help.
button.setTitle("my text here", forState: .Normal)

Resources