2

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()
    }

}
Community
  • 1
  • 1
pacmaninbw
  • 439
  • 1
  • 10
  • 22
  • If you had code that was creating views and adding them as subviews over and over, that would indeed cause your memory use to leak. You haven't provided enough code for us to really tell what's going on however. You have a variable `self.getGPSLongitudeAndLatitudeWithTimeStamp` which holds a button, but you don't show what you're doing with it. – Duncan C Apr 26 '17 at 14:19
  • I'm not going to invalidate the answer by changing the question now. All of the code pertaining to this is in http://stackoverflow.com/questions/43617580/uilabel-doesnt-update-after-button-clickl. – pacmaninbw Apr 26 '17 at 14:29
  • Editing your question does not invalidate the answer. Add an `##Edit` section at the bottom, of your answer and provide additional information. – Duncan C Apr 26 '17 at 14:30
  • I have been programming in Swift for less than a week and using Xcode for less than 2 weeks. I am undoubtedly asking stupid questions trying to learn. – pacmaninbw Apr 26 '17 at 14:31
  • @DuncanC all the code is added. – pacmaninbw Apr 26 '17 at 14:41

1 Answers1

11

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.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you, I've been programming in swift less than a week, objective-c and iOS less than 2 weeks. I have a lot of learning still to do. – pacmaninbw Apr 26 '17 at 14:27
  • 2
    Good, comprehensive answer. I just threw up my hands and posted a "you didn't provide enough info" comment. I applaud your patience and thoroughness. (Voted). – Duncan C Apr 26 '17 at 14:30