UILabel doesn't update after button click - ios

Background Xcode Version 8.2 (8C38)
ios 10.2 for iPhone 7 plus - Simulator
I got this working in objective-c and I'm now trying to get it to work in swift. The code compiles and runs, but when the buttons are clicked the label doesn't change. If the label was updating I would post it on CR rather than here.
It started out as an interview take home question, but the objective-c portion got me the onsite interview. I'm pursing this to learn more about swift. Interview question
/*
* Create a static library or iOS Framework using Objective-C that performs the following 3 functions:
* - Collects the GPS location (latitude and longitude) of the user at a point in time
* - Collects the battery state and returns whether or not the device is plugged in and what percentage of life is left.
* - Accesses any publicly available, free API to collect the data of your choice and returns it
* (this should be a network call)
*
* Build a simple application with 3 buttons and a label where text can be displayed. Each button should call into the
* three functions of the library described above and output the response to the label. Your application should consist
* of two tabs, one written in Objective-C and one written in Swift. Both tabs should call into the same Objective-C
* library and perform the same function.
*
* Only use Apple frameworks to complete this task. Fully comment your code explaining your logic and choices where multiple
* choices are available. For example, Apple provides numerous ways to retrieve a network resource, document why you choose
* the solution you did.
*/
This may be a duplicate question, but I'm not sure I have looked at
The following questions:
Swift Update Label (with HTML content) takes 1min
Concurrent vs serial queues in GCD
How to create dispatch queue in Swift 3
Swift 3 warning for dispatch async
I'm not sure if the problem is in the async call I make, in my creation of the button itself, or the property/var declarations at the top.
import UIKit
class ViewController: UIViewController {
var getGPSLongitudeAndLatitudeWithTimeStamp : UIButton?
var getBatteryLevelAndState : UIButton?
var getNextorkImplementation : UIButton?
var displayButtonAction : UILabel?
// The following variables are used in multiple functions. They are constant during the display of the super view
// and control the size of the subviews
var selfWidth : CGFloat = 0.0
var buttonHeight : CGFloat = 0.0
var viewElementWidth : CGFloat = 0.0
var buttonYCenterOffset : CGFloat = 0.0 // The y center should be half the value of the height
var buttonXCenter : CGFloat = 0.0 // Center the button title relative to the width of the button and the width of the super view
var buttonXInit : CGFloat = 0.0
var buttonVerticalSeparation : CGFloat = 0.0
var startingVerticalLocation : CGFloat = 0.0
var displayLabelHeight: CGFloat = 50.0
func initFramingValuesOfMyDisplay() {
selfWidth = self.view.bounds.size.width
buttonHeight = 20.0 // This should be programmable in relative to self.view.bounds.size.height
viewElementWidth = 0.8 * selfWidth;
buttonYCenterOffset = buttonHeight / 2.0; // The y center should be half the value of the height
buttonXCenter = selfWidth / 2.0; // Center the button title relative to the width of the button and the width of the super view
buttonXInit = 0.0;
buttonVerticalSeparation = buttonHeight + buttonYCenterOffset;
startingVerticalLocation = 430.0; // 430 was chosen based on experimentation in the simulator
}
func setLabelWithGPSLatitudeAndLongitudeWithTimeStampData()
{
var actionString : String = "Testing Label Text"
actionString = "GPS Button Action Failure: Data Model not created";
DispatchQueue.global().async {
self.displayButtonAction?.text = actionString
}
}
func setLabelWithBatteryLevelAndState() {
var actionString : String = "Get Battery Level and State";
actionString = "Battery Button Action Failure: Data Model not created"
DispatchQueue.global().async {
self.displayButtonAction?.text = actionString
}
}
func setLabelActionNetwork() {
var actionString :String = "Fake Button set to American Express Stock Price";
actionString = "Network Button Action Failure: Data Model not created";
DispatchQueue.global().async {
self.displayButtonAction?.text = actionString
}
}
func makeAButton(yButtonStart : CGFloat, buttonTitle: String, underSubview: UIButton?) -> UIButton
{
let thisButton = UIButton.init(type: .system)
thisButton.frame = CGRect(x: buttonXInit, y: yButtonStart, width: viewElementWidth, height: buttonHeight)
thisButton.setTitle(buttonTitle, for:UIControlState.normal)
thisButton.backgroundColor = UIColor.yellow
thisButton.setTitleColor(UIColor.black, for: UIControlState.normal)
if ((underSubview) == nil) {
self.view.addSubview(thisButton)
}
else {
self.view.insertSubview(thisButton, belowSubview:underSubview!)
}
return thisButton;
}
func makeALabel(yLabelStart : CGFloat, underSubview: UIButton?) -> UILabel
{
let thisLabel = UILabel.init()
thisLabel.frame = CGRect(x: selfWidth * 0.1, y: yLabelStart, width: viewElementWidth, height: displayLabelHeight)
thisLabel.font = thisLabel.font.withSize(12)
thisLabel.lineBreakMode = .byWordWrapping;
thisLabel.numberOfLines = 0;
thisLabel.textAlignment = NSTextAlignment.center;
thisLabel.textColor = UIColor.black;
if ((underSubview) == nil) {
self.view.addSubview(thisLabel)
}
else {
self.view.insertSubview(thisLabel, belowSubview:underSubview!)
}
return thisLabel;
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = UIColor.white
initFramingValuesOfMyDisplay()
addButtonAndLabels()
}
func addButtonAndLabels() -> Void {
if (selfWidth < 1.0) {
return;
}
var viewElementVerticalLocation: CGFloat = startingVerticalLocation;
// var touchUpInside : UIControlEvents = touchUpInside;
let getGPSLongitudeAndLatitudeWithTimeStamp : UIButton = makeAButton(yButtonStart: viewElementVerticalLocation, buttonTitle: "Get GPS Location with TimeStamp", underSubview: nil)
getGPSLongitudeAndLatitudeWithTimeStamp.addTarget(self, action: #selector(setLabelWithGPSLatitudeAndLongitudeWithTimeStampData), for: .touchUpInside)
viewElementVerticalLocation += buttonVerticalSeparation
let getBatteryLevelAndState : UIButton = makeAButton(yButtonStart: viewElementVerticalLocation, buttonTitle: "Get Battery Level and State", underSubview: getGPSLongitudeAndLatitudeWithTimeStamp)
getBatteryLevelAndState.addTarget(self, action: #selector(setLabelWithBatteryLevelAndState), for: .touchUpInside)
viewElementVerticalLocation += buttonVerticalSeparation
let getNextorkImplementation : UIButton = makeAButton(yButtonStart: viewElementVerticalLocation, buttonTitle: "Get Battery Level and State", underSubview: getBatteryLevelAndState)
getNextorkImplementation.addTarget(self, action: #selector(setLabelWithBatteryLevelAndState), for: .touchUpInside)
viewElementVerticalLocation += buttonVerticalSeparation
let displayButtonAction = makeALabel(yLabelStart: viewElementVerticalLocation, underSubview: getNextorkImplementation)
displayButtonAction.text = "No Action Yet"
}
required init(coder aDecoder: NSCoder) {
super.init(nibName: nil, bundle: nil)
initFramingValuesOfMyDisplay()
}
}

Your displayButtonAction is in fact nil. Notice in your method addButtonAndLabels() you are creating a label and populating the text only within the scope of that method here:
let displayButtonAction = makeALabel(yLabelStart: viewElementVerticalLocation, underSubview: getNextorkImplementation)
In your makeALabel method, you are inserting that label as a subview of your view which is why it shows up when you run the application. Nowhere are you assigning that label to displayButtonAction.
In summary, You are creating a local scope UILabel, populating it's placeholder text, inserting it as a subview of your view, then discarding it, then trying to populate the text of your displayButtonAction label on button press of which is nil.
Within addButtonAndLabels(), assigning the instantiated label to your main label in the main scope of the view controller via:
self.displayButtonAction = displayButtonAction
will get you started in the right direction.
Using the debugger tools, breakpoints, and po are your friends in situations like these.

The main thing I notice is that you are trying to set the text for the label on a background queue. You should never make UI changes on anything but the main queue, so change that to the main queue like so:
DispatchQueue.main.async {
self.displayButtonAction?.text = actionString
}
You should be getting warnings in the debugger telling you this too.
You should also throw a breakpoint inside that function to verify that displayButtonAction is not nil, and that it has a frame that is actually on screen and has a non-zero size. Make good use of that debugger for these.

You can't update UI elements on background (async) threads. You need to get the main thread to do that.
DispatchQueue.main.async(execute: {
self.label.text = "some text"
})

Related

swift multiple views to tap gestures in array

I have an image slider where multiple images are added to a scrollview. Each is stored in an array of my custom data type (named Slide). I cycle through them like this:
for (index, slide) in slides.enumerated() {
let imageView = UIImageView()
imageView.image = UIImage(named: slide.image) //set the imageView with the imageName in this slide
imageView.contentMode = .scaleAspectFit
let xPos = self.view.frame.width * CGFloat(index) //calculate x depending upon slide index
imageView.frame = CGRect(x: xPos, y: -200, width: self.SVSlider.frame.width, height: self.SVSlider.frame.height)
SVSlider.contentSize.width = SVSlider.frame.width * CGFloat(index + 1) //add slide image to scrollview
SVSlider.addSubview(imageView)
//add tap function to slide
imageView.isUserInteractionEnabled = true
imageView.tag = index
let tapImage = UITapGestureRecognizer(target: self, action: #selector(slideAction(_:)))
imageView.addGestureRecognizer(tapImage)
}
Each slide image is attached to the method "slideAction", below:
#objc func slideAction(_ sender: UITapGestureRecognizer) {
let selName = slides[(sender.view?.tag)!].target
NSObject.perform(Selector(selName))
}
But the application throws an error when I click an image. By debugging I can see that the "tag" is correctly set to the slide index number. I want to perform the target action held in the slide object. (Currently the target is a String of the method name - ending with ':').
How can I get the "slideAction" to goto the method in the slide object?
Try this ,create target method in the custom class callMethodDirectly and call it
#objc func slideAction(_ sender: UITapGestureRecognizer) {
let selName = slides[(sender.view?.tag)!]
selName.callMethodDirectly()
}
class slide:NSObjcet
{
func callMethodDirectly()
{
// implement target code here
}
}

Is swift by default autorelease? What causes memory growth when the app is idle?

While debugging the code in this question UILabel doesn't update after button clickl I noticed that the simulated memory usage kept growing, even when I was in the debugger and not viewing the app.
I modified the addButtonsAndLabels() function to this to prevent memory loss, is this a bad practice?
func addButtonAndLabels() -> Void {
// If the width of the screen hasn't been used as a base for the size of the sub-views then
// this function is not ready to generate the sub-views.
if (selfWidth < 1.0) {
return;
}
var viewElementVerticalLocation: CGFloat = startingVerticalLocation;
// To prevent memory leaks only create the UIView object if it hasn't already been created
if (self.getGPSLongitudeAndLatitudeWithTimeStamp == nil) {
self.getGPSLongitudeAndLatitudeWithTimeStamp = makeAButton(yButtonStart: viewElementVerticalLocation, buttonTitle: "Get GPS Location with TimeStamp", underSubview: nil)
self.getGPSLongitudeAndLatitudeWithTimeStamp?.addTarget(self, action: #selector(setLabelWithGPSLatitudeAndLongitudeWithTimeStampData), for: .touchUpInside)
viewElementVerticalLocation += buttonVerticalSeparation
}
// ... more of the above ...
}
Is this code interfering with ARC? Is this code unnecessary?
I also noticed that viewDidLoad() was called before init(), since I had breakpoints in both functions.
If this is a duplicate please point me in the correct direction.
EDIT
import UIKit
class ViewController: UIViewController {
var getGPSLongitudeAndLatitudeWithTimeStamp : UIButton?
var getBatteryLevelAndState : UIButton?
var getNextorkImplementation : UIButton?
var displayButtonAction : UILabel?
var displayDataModel : PCI7DataModelLibrary?
// The following variables are used in multiple functions. They are constant during the display of the super view
// and control the size of the subviews. They should change when the orientation changes
var selfWidth : CGFloat = 0.0
var buttonHeight : CGFloat = 0.0
var viewElementWidth : CGFloat = 0.0
var buttonYCenterOffset : CGFloat = 0.0 // The y center should be half the value of the height
var buttonXCenter : CGFloat = 0.0 // Center the button title relative to the width of the button and the width of the super view
var buttonXInit : CGFloat = 0.0
var buttonVerticalSeparation : CGFloat = 0.0
var startingVerticalLocation : CGFloat = 0.0
var displayLabelHeight: CGFloat = 75.0
// TODO: This function should be altered so that all values are calculated on screen height and screen width,
// this will allow for changes in orientation.
func initFramingValuesOfMyDisplay() {
selfWidth = self.view.bounds.size.width
buttonHeight = 20.0 // This should be programmable in relative to self.view.bounds.size.height
viewElementWidth = 0.8 * selfWidth;
buttonXCenter = selfWidth / 2.0; // Center the button title relative to the width of the button and the width of the super view
buttonXInit = selfWidth * 0.1; // 10 percent margin on the left leaves a 10% margin on the right as well
buttonYCenterOffset = buttonHeight / 2.0; // The y center should be half the value of the height
buttonVerticalSeparation = buttonHeight + buttonYCenterOffset;
startingVerticalLocation = 430.0; // 430 was chosen based on experimentation in the simulator
}
// This function is called when the getGPSLongitudeAndLatitudeWithTimeStamp button is receives the touchUpInside event.
func setLabelWithGPSLatitudeAndLongitudeWithTimeStampData()
{
var actionString : String = "Testing Label Text"
if (self.displayDataModel != nil) {
actionString = (self.displayDataModel?.provideGPSLocationData())!
}
else {
actionString = "GPS Button Action Failure: Data Model not created"
}
DispatchQueue.main.async {
self.displayButtonAction?.text = nil
self.displayButtonAction?.text = actionString
}
}
// This function is called when the getBatteryLevelAndState button is receives the touchUpInside event.
func setLabelWithBatteryLevelAndState() {
var actionString : String = "Get Battery Level and State";
if (self.displayDataModel != nil) {
actionString = (self.displayDataModel?.provideBatteryLevelAndState())!
}
else {
actionString = "Battery Button Action Failure: Data Model not created"
}
DispatchQueue.main.async {
self.displayButtonAction?.text = nil
self.displayButtonAction?.text = actionString
}
}
// This function is called when the getNextorkImplementation button is receives the touchUpInside event.
func setLabelActionNetwork() {
var actionString :String = "Fake Button set to American Express Stock Price"
if (self.displayDataModel != nil) {
actionString = (self.displayDataModel?.provideNetworkAccessData())!
}
else {
actionString = "Network Button Action Failure: Data Model not created"
}
DispatchQueue.main.async {
self.displayButtonAction?.text = nil
self.displayButtonAction?.text = actionString
}
}
func makeAButton(yButtonStart : CGFloat, buttonTitle: String, underSubview: UIButton?) -> UIButton
{
let thisButton = UIButton.init(type: .system)
thisButton.frame = CGRect(x: buttonXInit, y: yButtonStart, width: viewElementWidth, height: buttonHeight)
thisButton.setTitle(buttonTitle, for:UIControlState.normal)
thisButton.backgroundColor = UIColor.yellow
thisButton.setTitleColor(UIColor.black, for: UIControlState.normal)
if ((underSubview) == nil) {
self.view.addSubview(thisButton)
}
else {
self.view.insertSubview(thisButton, belowSubview:underSubview!)
}
return thisButton;
}
func makeALabel(yLabelStart : CGFloat, height: CGFloat, underSubview: UIButton?) -> UILabel
{
let thisLabel = UILabel.init()
thisLabel.frame = CGRect(x: buttonXInit, y: yLabelStart, width: viewElementWidth, height: height)
thisLabel.font = thisLabel.font.withSize(12) // Reduce the size of the text so that more output fits on a single line
thisLabel.lineBreakMode = .byWordWrapping;
thisLabel.numberOfLines = 0; // Allow the label to grow as necessary
thisLabel.textAlignment = NSTextAlignment.center;
thisLabel.textColor = UIColor.black;
if ((underSubview) == nil) {
self.view.addSubview(thisLabel)
}
else {
self.view.insertSubview(thisLabel, belowSubview:underSubview!)
}
return thisLabel;
}
override func viewDidLoad() {
super.viewDidLoad()
// rather than assume a particular background color, set the background color so that everything can be seen.
self.view.backgroundColor = UIColor.white
initFramingValuesOfMyDisplay()
if (self.displayDataModel == nil) {
self.displayDataModel = PCI7DataModelLibrary.init()
}
addButtonAndLabels()
}
func addButtonAndLabels() -> Void {
// If the width of the screen hasn't been used as a base for the size of the sub-views then
// this function is not ready to generate the sub-views.
if (selfWidth < 1.0) {
return;
}
var viewElementVerticalLocation: CGFloat = startingVerticalLocation;
// To prevent memory leaks only create the UIView object if it hasn't already been created
if (self.getGPSLongitudeAndLatitudeWithTimeStamp == nil) {
self.getGPSLongitudeAndLatitudeWithTimeStamp = makeAButton(yButtonStart: viewElementVerticalLocation, buttonTitle: "Get GPS Location with TimeStamp", underSubview: nil)
self.getGPSLongitudeAndLatitudeWithTimeStamp?.addTarget(self, action: #selector(setLabelWithGPSLatitudeAndLongitudeWithTimeStampData), for: .touchUpInside)
viewElementVerticalLocation += buttonVerticalSeparation
}
if (self.getGPSLongitudeAndLatitudeWithTimeStamp == nil) {
self.getBatteryLevelAndState = makeAButton(yButtonStart: viewElementVerticalLocation, buttonTitle: "Get Battery Level and State", underSubview: getGPSLongitudeAndLatitudeWithTimeStamp)
self.getBatteryLevelAndState?.addTarget(self, action: #selector(setLabelWithBatteryLevelAndState), for: .touchUpInside)
viewElementVerticalLocation += buttonVerticalSeparation
}
if (self.getNextorkImplementation == nil) {
self.getNextorkImplementation = makeAButton(yButtonStart: viewElementVerticalLocation, buttonTitle: "Get American Express Stock Price", underSubview: getBatteryLevelAndState)
self.getNextorkImplementation?.addTarget(self, action: #selector(setLabelActionNetwork), for: .touchUpInside)
viewElementVerticalLocation += buttonVerticalSeparation
}
if (self.displayButtonAction == nil) {
self.displayButtonAction = makeALabel(yLabelStart: viewElementVerticalLocation, height: displayLabelHeight, underSubview: getNextorkImplementation)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
required init(coder aDecoder: NSCoder) {
super.init(nibName: nil, bundle: nil)
if (self.displayDataModel == nil) {
self.displayDataModel = PCI7DataModelLibrary.init()
}
initFramingValuesOfMyDisplay()
}
}
It's a little difficult to answer this question because I fear you have a lot of misunderstandings about how Swift memory management works and how iOS view controller life cycles work. They're not surprising misunderstandings; it's just hard to know where to start here. I may cover things you're already aware of.
First, start with studying the Automatic Reference Counting section of the Swift manual. The key to understanding ARC is that it is based on strong references. If two things have strong references to each other, that's a retain loop and until it's broken, neither object will be deallocated.
It isn't really meaningful to ask "is Swift by default autorelease." Autorelease is a slightly more advanced concept that you probably don't have to understand for basic Swift. (I've worked with it so long that I hope I'm not being overly dismissive of whether people need to understand it, but I think you can avoid thinking about it for now.) Autorelease has to do with objects that you want to exist for the rest of the current event loop, but would otherwise have a zero retain count. While it still exists in ARC, it's mostly for manual retain counting, and you rarely need to think too much about it in Swift, particularly as a beginner.
I also noticed that viewDidLoad() was called before init(), since I had breakpoints in both functions.
It's not possible for viewDidLoad to be called before init on a given object. Your problem, almost certainly, is that there are more than one of this object, and that's surprising you. Generally if you print(self) (or p self in the debugger) it will show you the address of the object. If it has a different address, it's a different object. You may want to implement deinit so you can put a breakpoint there as well and explore the life cycle of the view controller.
I assume somewhere there's an addSubview call on this button. What's almost certainly happening is that you're calling addButtonAndLabels multiple times (perhaps because you are confused about view lifecycle), and each call adds the button to the view. So you get more and more buttons in the view, and each takes memory. You need to make sure you only add things once if you only want one. You can see this problem with p view.subviews in the debugger (or print(view.subviews)). You'll probably see a ton of buttons. This isn't a leak. These are just objects you're creating that you didn't mean to create.

How do you setup a loop with an interval/delay?

I'm trying to make a Wack-a-Mole game. The way I am doing so is by having one button randomly appear and disappear around the screen after someone has tapped it. If they do not tap the button within one second after it reappears, then it will disappear and find a new position, then reappear and wait one second and repeat the above steps. However, whenever I run this code, it doesn't do that. It moves positions only if I get rid of the 'while' statement and the 'if else' statement. Why won't it loop, disappear, reappear, etc?
Delay is this: https://stackoverflow.com/a/24318861/5799228
#IBAction func moveButton(button: UIButton) {
while self.WaffleButton.hidden == true || false {
if self.WaffleButton.hidden == false {
self.WaffleButton.hidden = true
delay(3) {
// Find the button's width and height
let buttonWidth = button.frame.width
let buttonHeight = button.frame.height
// Find the width and height of the enclosing view
let viewWidth = button.superview!.bounds.width
let viewHeight = button.superview!.bounds.height
// Compute width and height of the area to contain the button's center
let xwidth = viewWidth - buttonWidth
let yheight = viewHeight - buttonHeight
// Generate a random x and y offset
let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))
// Offset the button's center by the random offsets.
button.center.x = xoffset + buttonWidth / 2
button.center.y = yoffset + buttonHeight / 2
self.WaffleButton.hidden = false
self.delay(1) {
self.WaffleButton.hidden = true
}
}
} else { delay(3) {
// Find the button's width and height
let buttonWidth = button.frame.width
let buttonHeight = button.frame.height
// Find the width and height of the enclosing view
let viewWidth = button.superview!.bounds.width
let viewHeight = button.superview!.bounds.height
// Compute width and height of the area to contain the button's center
let xwidth = viewWidth - buttonWidth
let yheight = viewHeight - buttonHeight
// Generate a random x and y offset
let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))
// Offset the button's center by the random offsets.
button.center.x = xoffset + buttonWidth / 2
button.center.y = yoffset + buttonHeight / 2
self.WaffleButton.hidden = false
self.delay(1) {
self.WaffleButton.hidden = true
}
}
}
}
}
You are using while in a way it is not meant to be used.
This is how you should use while :
var someCondition = true
while someCondition {
// this will loop as fast as possible untill someConditionIsTrue is no longer true
// inside the while statement you will do stuff x number of times
// then when ready you set someCondition to false
someCondition = false // stop
}
This is how you are using while :
let someConditionThatIsAlwaysTrue = true
while someConditionThatIsAlwaysTrue {
// condition is always true, so inifinite loop...
// this creates a function that is executed 3 seconds after the current looping pass of the while loop.
// while does not wait for it to be finished.
// while just keeps going.
// a fraction of a second later it will create another function that will execute 3 seconds later.
// so after 3 seconds an infite amount of functions will execute with a fraction of a second between them.
// except they won't, since the main thread is still busy with your infinite while loop.
delay(3) {
// stuff
}
}
How to do it the right way :
don't ever use while or repeat to "plan" delayed code execution.
Split up the problem in smaller problems:
Issue 1 : Creating a loop
A loop is created by have two functions that trigger each other.
I will call them execute and executeAgain.
So execute triggers executeAgain and executeAgain triggers execute and then it starts all over again -> Loop!
Instead of calling execute and executeAgain directly, you also create a start function. This is not needed but it is a good place to setup conditions for your looping function. start will call execute and start the loop.
To stop the loop you create a stop function that changes some condition.
execute and executeAgain will check for this condition and only keep on looping if the check is successful. stop makes this check fail.
var mustLoop : Bool = false
func startLoop() {
mustLoop = true
execute()
}
func execute() {
if mustLoop {
executeAgain()
}
}
func executeAgain() {
if mustLoop {
execute()
}
}
func stop() {
mustLoop = false
}
Issue 2: Delayed Execution
If you need a delay inside a subclass of NSObject the most obvious choice is NSTimer. Most UI classes (like UIButton and UIViewController) are subclasses of NSObject.
NSTimer can also be set to repeat. This would also create a loop that executes every x seconds. But since you actually have 2 alternating actions it makes more sense to adopt the more verbose looping pattern.
An NSTimer executes a function (passed as Selector("nameOfFunction")) after x amount of time.
var timer : NSTimer?
func planSomething() {
timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector("doSomething"), userInfo: nil, repeats: false)
}
func doSomething() {
// stuff
}
If you need a delay in another class/struct (or you don't like NSTimer) you can use the delay function that matt posted.
It will execute whatever you enter in the closure after x amount of time.
func planSomething() {
delay(3) {
doSomething()
}
}
func doSomething() {
// stuff
}
Combining the two solutions:
By using the loop pattern above you now have distinct functions. Instead of calling them directly to keep the loop going. You insert the delay method of your choice and pass the next function to it.
So NSTimer will have a Selector pointing to execute or executeAgain and with delay you place them in the closure
How to implement it elegantly:
I would subclass UIButton to implement all this. Then you can keep your UIViewController a lot cleaner. Just choose the subclass in IB and connect the IBOutlet as usual.
This subclass has a timer attribute that will replace your delay.
The button action wacked() is also set in the init method.
From your UIViewController you call the start() func of the button. This will start the timer.
The timer will trigger appear() or disappear.
wacked() will stop the timer and make the button hide.
class WackingButton : UIButton {
var timer : NSTimer?
var hiddenTime : NSTimeInterval = 3
var popUpTime : NSTimeInterval = 1
override init(frame: CGRect) {
super.init(frame: frame)
self.addTarget(self, action: "wacked", forControlEvents: UIControlEvents.TouchUpInside)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addTarget(self, action: "wacked", forControlEvents: UIControlEvents.TouchUpInside)
}
func start() {
timer = NSTimer.scheduledTimerWithTimeInterval(hiddenTime, target: self, selector: Selector("appear"), userInfo: nil, repeats: false)
}
func appear() {
self.center = randomPosition()
self.hidden = false
timer?.invalidate()
timer = NSTimer.scheduledTimerWithTimeInterval(popUpTime, target: self, selector: Selector("dissappear"), userInfo: nil, repeats: false)
}
func dissappear() {
self.hidden = true
timer?.invalidate()
timer = NSTimer.scheduledTimerWithTimeInterval(hiddenTime, target: self, selector: Selector("appear"), userInfo: nil, repeats: false)
}
func wacked() {
self.hidden = true
timer?.invalidate()
}
func randomPosition() -> CGPoint {
// Find the width and height of the enclosing view
let viewWidth = self.superview?.bounds.width ?? 0 // not really correct, but only fails when there is no superview and then it doesn't matter anyway. Won't crash...
let viewHeight = self.superview?.bounds.height ?? 0
// Compute width and height of the area to contain the button's center
let xwidth = viewWidth - frame.width
let yheight = viewHeight - frame.height
// Generate a random x and y offset
let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))
// Offset the button's center by the random offsets.
let x = xoffset + frame.width / 2
let y = yoffset + frame.height / 2
return CGPoint(x: x, y: y)
}
}
Your UIViewController :
class ViewController: UIViewController {
#IBOutlet weak var button1: WackingButton!
override func viewDidAppear(animated: Bool) {
button1.start()
}
}

Adding UIButton dynamically: action not getting registered

I'm creating pages dynamically, each page contains a Navigation Controller and UIViewController.
Inside each page, there are components like link, images, texts.
Each component is a class like the following:
class link: Component, ComponentProtocol {
var text: String
var url: String
func browseURL(sender: UIButton!){
let targetURL = NSURL(fileURLWithPath: self.url)
let application = UIApplication.sharedApplication()
application.openURL(targetURL!)
}
func generateView() -> UIView?{
var result: UIView?
var y = CGRectGetMinY(frame)
var linkBtn = UIButton(frame: CGRect(x: 0, y:30 , width:300 , height: 50)
linkBtn.setTitle(self.text, forState: UIControlState.Normal)
linkBtn.setTitleColor(UIColor.blueColor(), forState: UIControlState.Normal)
linkBtn.titleLabel?.font = linkBtn.titleLabel?.font.fontWithSize(15)
// this doesn't seem to be registered properly
linkBtn.addTarget(self, action: "browseURL:", forControlEvents: UIControlEvents.TouchUpInside)
result = UIView()
result?.addSubview(linkBtn)
return result
}
Then in the ViewDidLoad of the page ViewController method I would have this to initialise the page components:
for component in components!{
if let acomponent:ComponentProtocol = component as? ComponentProtocol {
if let res = acomponent.generateView(innerFrame) {
if let view = res.view {
self.view.addSubview(view)
}
}
}
}
The button is showing, but when I touch nothing happens. When I debugged, the browseURL is no triggered at all.
What's wrong with my code? I'm guessing because I registered the action in link class and not in the ViewController of the page?
UPDATE
This could be a similar issue, but the answer is not so straightforward and I actually have the component reference in my ViewController: (Target: Object) not working for setting UIButton from outside viewController
I'm not at a Mac right now so I can't test, but does this work:
linkBtn.target = self
linkBtn.action = "browseURL:"
I think if you have selectors in Swift you have to be careful with colons and specifying #objc if your class doesn't inherit from NSObject, it could also be something to do with that?

UIView class for drop down list with UIButtons - delegate/protocol issue?

I am trying to create a custom drop down list in a ViewController. There are going to be 5 drop down lists and each list will have 4 options. Because of the number of lists, I decided to make a UIView that has the four choices in the form of UIButtons for each of the lists. Right now I am just trying to get one down; therefore, the following code is for ONE drop down list with FIVE options (including the one selected, which I will explain further below).
Essentially what I want is to have a button showing the selected value (or a default value at launch) and then when you click on that value then the UIView that contains 4 buttons (aka the drop down list) is shown below the original button. When the user clicks on one of the buttons I want the the button with the selected value to have the title of the button that was clicked on.
I am having the following issues:
I want to be able to pass the titles of the four buttons from the ViewController to the UIView because I want to use this UIView multiple times with different values for the titles of the four buttons. I don't know how to pass values to a UIView class.
When a choice from the drop down list (ie a UIButton) is clicked I can't figure out how to pass the value of the title of the button from the UIView back to UIViewController. I tried setting the title to a variable in the ViewController but that didn't work (showed up as nil).
Thank you so much in advance - I know this is a long questions and I am really unsure if this is even a good approach to take for what I am trying to do but it made sense in my head.
Here is my code for the ViewController
var buttonsLeft: buttonsView = buttonsView() // this is the UIView subclass
var time = UIButton.buttonWithType(UIButtonType.System) as! UIButton
override func viewWillAppear(animated: Bool) {
//hidden drop down list
self.buttonsLeft.frame = CGRect(x: self.view.bounds.width*(1/6) - 50, y:120, width:100, height: 135)
self.buttonsLeft.hidden = true
//button with selection showing or the default value at launch
self.time.frame = CGRectMake(self.view.bounds.width * (1/6) - 50, 90, 100, 30)
self.time.setTitle("1 DAY", forState: UIControlState.Normal)
self.time.addTarget(self, action: "showLeft", forControlEvents: UIControlEvents.TouchUpInside)
self.time.hidden = false
self.view.addSubview(self.time)
}
//this function shows the list
func showLeft(){
self.view.addSubview(self.buttonsLeft)
self.buttonsLeft.hidden = false
}
Here is the code for the UIView buttonsView:
import UIKit
class buttonsView: UIView {
var option1 = UIButton()
var option2 = UIButton()
var option3 = UIButton()
var option4 = UIButton()
var buttons: Array<UIButton> = Array()
var title:String = String()
override init(frame: CGRect) {
super.init(frame: frame)
self.buttons = [option1, option2, option3, option4]
self.option1.setTitle("1 DAY", forState: UIControlState.Normal)
self.option2.setTitle("1 MONTH", forState: UIControlState.Normal)
self.option3.setTitle("1 YEAR", forState: UIControlState.Normal)
self.option4.setTitle("LONGER", forState: UIControlState.Normal)
var yStep = 35
for var i:Int = 0; i < 4; ++i {
var totalY:CGFloat = CGFloat(i*yStep)
buttons[i].frame = CGRectMake(0, totalY, 100, 30)
buttons[i].addTarget(self, action: "choseOption:", forControlEvents: UIControlEvents.TouchUpInside)
buttons[i].hidden = false
self.addSubview(buttons[i])
}
}
func choseOption(sender:UIButton){
self.title = sender.titleLabel!.text!
MyView().parentTitle = sender.titleLabel!.text! // my attempt at assigning to variable in View Controller
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Delegation will help you to pass value to UIViewController.
Here are the way you can implement delegate in swift.
Step 1 : Declare protocol in class which is used to sending data. here is buttonsview.
#objc protocol MyButtonDelegate{
optional func didSelectButton(text:String)
}
Step 2 : Now declare delegate in sending class. here is buttonsview.
class buttonsView: UIView {
var delegate:MyButtonDelegate?
[other stuf......]
}
Step 3: now use delegate to send data to 'UIViewController'.
func choseOption(sender:UIButton){
delegate!.didSelectButton(text: sender.titleLabel!.text!)
}
Step 4 : adopt protocol in receiving class.
class ViewController: UIViewController,MyButtonDelegate {
Step 5: implement delegate method in receiving class.
func didSelectButton(text: String) {
parentTitle = "The Buttons title is " + text
}
Step 6: now set delegate
override func viewDidLoad() {
super.viewDidLoad()
buttonsLeft.delegate = self
}
Hope this help you.

Resources