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()
}
}
Related
I am trying to automatically scroll slides in my scroll view. I am actually doing this for my onboarding screens.
However I want the timer to pause for a while when user touches the slides and start the same again when he removes his finger.
I am doing something like this but this doesn't work for the case I am asking:
in viewDidLoad -
timer = Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(autoScroll), userInfo: nil, repeats: true)
#objc func autoScroll() {
let totalPossibleOffset = CGFloat(topImages.count - 1) * self.view.bounds.size.width
if offSet == totalPossibleOffset {
//offSet = 0 // come back to the first image after the last image
//timer.invalidate()
}
else {
offSet += self.view.bounds.size.width
}
DispatchQueue.main.async() {
UIView.animate(withDuration: 0.3, delay: 0, options: UIViewAnimationOptions.curveLinear, animations: {
self.scrollView.contentOffset.x = CGFloat(self.offSet)
}, completion: nil)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let page = scrollView.contentOffset.x / scrollView.frame.size.width
pageControl.currentPage = Int(page)
self.offSet = page * scrollView.frame.size.width // this updates offset value so that automatic scroll begins from the image you arrived at manually
}
Also I have a second question: How do I start the timer interval again when user manually slides to other other. Right now, when the user slides to another slide before 4 seconds (as 4 seconds is required time to slide to another slide), say 2 seconds, he will slide to next page there after 4-2 = 2 seconds instead of 4 seconds as expected.
I think you should add some flag like
var isSlideTouched = false
In gesture recognizer add
isSlideTouched = true
And some code in autoScroll()
#objc func autoScroll() {
if isSlideTouched {
isSlideTouched = false
return
}
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"
})
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.
my ViewController is still sending an array update, even if I'm in another View, what can I do? Here is my code:
import UIKit
import CoreBluetooth
class ViewController: UIViewController {
var audioVibe : AudioVibes!
var superpowered:Superpowered!
var displayLink:CADisplayLink!
var layers:[CALayer]!
var magnitudeArray : [UInt16] = [0, 0, 0, 0, 0, 0, 0]
override func viewDidLoad() {
super.viewDidLoad()
// Setup 8 layers for frequency bars.
let color:CGColorRef = UIColor(red: 0, green: 0.6, blue: 0.8, alpha: 1).CGColor
layers = [CALayer(), CALayer(), CALayer(), CALayer(), CALayer(), CALayer(), CALayer(), CALayer()]
for n in 0...7 {
layers[n].backgroundColor = color
layers[n].frame = CGRectZero
self.view.layer.addSublayer(layers[n])
}
superpowered = Superpowered()
// A display link calls us on every frame (60 fps).
displayLink = CADisplayLink(target: self, selector: #selector(ViewController.onDisplayLink))
displayLink.frameInterval = 1
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
}
// Gets triggered when you leave the ViewController.
override func viewWillDisappear(animated: Bool) {
superpowered.togglePlayback();
superpowered.moritz();
//delete(superpowered);
}
func onDisplayLink() {
// Get the frequency values.
let magnitudes = UnsafeMutablePointer<Float>.alloc(8)
superpowered.getMagnitudes(magnitudes)
// Wrapping the UI changes in a CATransaction block like this prevents animation/smoothing.
CATransaction.begin()
CATransaction.setAnimationDuration(0)
CATransaction.setDisableActions(true)
// Set the dimension of every frequency bar.
let originY:CGFloat = self.view.frame.size.height - 20
let width:CGFloat = (self.view.frame.size.width - 47) / 5
var frame:CGRect = CGRectMake(20, 0, width, 0)
for n in 0...4 {
frame.size.height = CGFloat(magnitudes[n]) * 2000
frame.origin.y = originY - frame.size.height
layers[n].frame = frame
frame.origin.x += width + 1
}
// Set the magnitudes in the array.
for n in 0...6 {
magnitudeArray[n] = UInt16(magnitudes[n] * 32768)
}
// Update the array in the audioVibe class to trigger the sending command.
audioVibe.magnitudeArray = magnitudeArray
CATransaction.commit()
// Dealloc the magnitudes.
magnitudes.dealloc(8)
}
}
I want him to stop doing things like audioVibe.magnitudeArray = magnitudeArray while I'm not in his view, what can I do?
Thanks!
Looks like you display link isn't getting paused when you transition to different views.
You could pause your display link when transitioning away from this view and resume it when coming back to this view using the paused property on CADisplayLink. You would pause it in viewWillDisappear and resume in viewWillAppear.
In your viewDidLoad method you're creating a CADisplayLink and adding it to the run loop.
If you don't do anything, that display link will stay active when you push another view controller on top of it.
You should move the code that creates the display link and adds it to the run loop to your viewWillAppear method.
Then you need to add code in viewWillDisappear that removes the display link from the run loop, invalidates it, and nils it out.
That way, you'll start your display link code when the view appears, and stop it when the view disappears.
Im trying to make a small game. And there is some problem with the animation. Im new to Swift. So lets take a look. I create a UIImageView picture and want to do animation of this picture appear in a different places on a screen. I believe that the algorithm will look like this:
Infinite loop{
1-GetRandomPlace
2-change opacity from 0 to 1 and back(with smooth transition)
}
Looks simple, but I can't understand how to do it correctly in Xcode.
Here is my test code but it looks useless
Thank you for help and sorry if there was already this question, I can't find it.
class ViewController: UIViewController {
#IBOutlet weak var BackgroundMainMenu:UIImageView!
#IBOutlet weak var AnimationinMenu: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// MoveBackgroundObject(AnimationinMenu)
// AnimationBackgroundDots(AnimationinMenu, delay: 0.0)
// self.AnimationinMenu.alpha = 0
//
// UIImageView.animateWithDuration(3.0,
// delay: 0.0,
// options: UIViewAnimationOptions([.Repeat, .CurveEaseInOut]),
// animations: {
// self.MoveBackgroundObject(self.AnimationinMenu)
// self.AnimationinMenu.alpha = 1
// self.AnimationinMenu.alpha = 0
// },
// completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(animated: Bool) {
AnimationBackgroundDots(AnimationinMenu, delay: 0.0)
}
func ChangeOpacityto1(element: UIImageView){
element.alpha = 0
UIView.animateWithDuration(3.0) {
element.alpha = 1
}
}
func ChangeOpacityto0(element: UIImageView){
element.alpha = 1
UIView.animateWithDuration(3.0){
element.alpha = 0
}
}
func AnimationBackgroundDots(element: UIImageView, delay: Double){
element.alpha = 0
var z = 0
while (z<4){
MoveBackgroundObject(AnimationinMenu)
UIImageView.animateWithDuration(3.0,
animations: {
element.alpha = 0
element.alpha = 1
element.alpha = 0
},
completion: nil)
z++
}
}
func MoveBackgroundObject(element: UIImageView) {
// Find the button's width and height
let elementWidth = element.frame.width
let elementHeight = element.frame.height
// Find the width and height of the enclosing view
let viewWidth = BackgroundMainMenu.superview!.bounds.width
let viewHeight = BackgroundMainMenu.superview!.bounds.height
// Compute width and height of the area to contain the button's center
let xwidth = viewWidth - elementWidth
let yheight = viewHeight - elementHeight
// 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.
element.center.x = xoffset + elementWidth / 2
element.center.y = yoffset + elementHeight / 2
}
}
The problem is in AnimationBackgroundDots.
You are immediately creating 4 animations on the same view but only one can run at a time. What you need to do is wait until one animation is finished (fade in or fade out) before starting a new one.
Also, the animations closure is for setting the state you want your view to animate to. It looks at how your view is at the start, runs animations, then looks at the view again and figures out how to animate between the two. In your case, the alpha of the UIImageView starts at 0, then when animations runs, the alpha ends up being 0 so nothing would animate. You can't create all the steps an animation should take that way.
Want you need to do it move your view and start the fade in animation. The completion closure of fading in should start the fade out animation. The completion closure of the fading out should then start the process all over again. It could look something like this.
func AnimationBackgroundDots(element: UIImageView, times: Int) {
guard times > 0 else {
return
}
MoveBackgroundObject(element)
element.alpha = 1
// Fade in
UIView.animateWithDuration(3.0, animations: {
element.alpha = 1
}, completion: { finished in
// Fade Out
UIView.animateWithDuration(3.0, animations: {
element.alpha = 0
}, completion: { finished in
// Start over again
self.AnimationBackgroundDots(element, times: times-1)
})
})
}
You called also look at using keyframe animations but this case is simple enough that theres no benefit using them.
Also as a side note. The function naming convention in swift is to start with a lowercase letter, so AnimationBackgroundDots should be animationBackgroundDots.