0

In Swift, it appears we infer types within the class however outside of functions. I understand that if a variable is only declared within a function then it will only live within that given scope. Isn't it best practice to instantiate objects outside of functions so that we can reference the same object as we program a viewController while also avoiding the possibility of crashes? And if not, then what is the purpose of inferring variables at the top of viewControllers and then instantiating the object within a function?

Here is my example code I'm following from a tutorial. Notice how mapView is inferred at the top of the viewController but instantiated in the loadView method. Wouldn't this make the mapView object only accessible to the loadView function but not to other methods:

import Foundation
import UIKit
import MapKit
import CoreLocation

class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {

    var mapView: MKMapView!
    var problemChild: Int!

    override func viewWillDisappear(_ animated: Bool) {
        print("the view disappeared")
    }

    override func loadView() {

        mapView = MKMapView()

        view = mapView

        mapView.delegate = self

        mapView.isPitchEnabled = true

       // let atlLongLat = MKCoordinateRegion.init(center: CLLocationCoordinate2D.init(latitude: CLLocationDegrees.init(33.7490), longitude: CLLocationDegrees.init(84.3880)), span: MKCoordinateSpan.init(latitudeDelta: 33.7490, longitudeDelta: 84.3880))

        //mapView.setRegion(atlLongLat, animated: true)

        mapView.showsPointsOfInterest = true
        mapView.showsBuildings = true
        mapView.showsCompass = true
        mapView.showsTraffic = true
        let locationManager = CLLocationManager()
        locationManager.delegate = self
        let locationAuthStatus = CLLocationManager.authorizationStatus()
        if locationAuthStatus == .notDetermined {
            locationManager.requestWhenInUseAuthorization()
        }
        mapView.showsUserLocation = true


        let segmentedControl = UISegmentedControl.init(items: ["Standard", "Hybrid", "Satellite"])
        segmentedControl.selectedSegmentIndex = 0
        segmentedControl.translatesAutoresizingMaskIntoConstraints = false
        segmentedControl.backgroundColor = UIColor.yellow
        view.addSubview(segmentedControl)

        let zoomButtonFrame = CGRect.init(x: 0, y: 0, width: view.bounds.width, height: 400)
        let zoomButton = UIButton.init(frame: zoomButtonFrame)
        zoomButton.backgroundColor = UIColor.green
        zoomButton.setTitle("Where Am I?", for: .normal)
        zoomButton.setTitleColor(UIColor.black, for: .normal)
        zoomButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(zoomButton)

        let guide = view.safeAreaLayoutGuide
        let topConstraint = segmentedControl.topAnchor.constraint(equalTo: guide.topAnchor, constant: 8)
        let zoomButtonTopConstraint = zoomButton.topAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 559)
        let margins = view.layoutMarginsGuide
        let zoomButtonLeadingConstraint = zoomButton.leadingAnchor.constraint(equalTo: margins.leadingAnchor)
        let leadingConstraint = segmentedControl.leadingAnchor.constraint(equalTo: margins.leadingAnchor)
        let trailingConstraint = segmentedControl.trailingAnchor.constraint(equalTo: margins.trailingAnchor)
        let zoomButtonTrailingConstraint = zoomButton.trailingAnchor.constraint(equalTo: margins.trailingAnchor)
        topConstraint.isActive = true
        leadingConstraint.isActive = true
        trailingConstraint.isActive = true
        zoomButtonTopConstraint.isActive = true
        zoomButtonLeadingConstraint.isActive = true
        zoomButtonTrailingConstraint.isActive = true


        segmentedControl.addTarget(self, action:#selector(mapTypeChanged(segControl:)), for: .valueChanged)

        zoomButton.addTarget(self, action: #selector(zoomButtonTapped(zoomButt:)), for: .touchUpInside)
    }

    @objc func mapTypeChanged(segControl: UISegmentedControl) {
        switch segControl.selectedSegmentIndex {
        case 0:
            mapView.mapType = .standard
        case 1:
            mapView.mapType = .mutedStandard
        case 2:
            mapView.mapType = .satelliteFlyover
        default:
            break
        }
    }

    @objc func zoomButtonTapped(zoomButt: UIButton){
        let b: Int = problemChild
        print(b)

        for _ in 1...5 {
            print("Pinging Your Location...")
            if zoomButt.backgroundColor == UIColor.green{
                print("this button's background color is green man.")
            }
        }

    }

    func mapViewWillStartLocatingUser(_ mapView: MKMapView) {
        //adding this here to get used to the idea of protocols
    }


}

Thank you in advance and I apologize for sounding like a noob but I'd really like to understand.

Laurence Wingo
  • 3,912
  • 7
  • 33
  • 61
  • 1
    I think your uses of "infer" (and its conjugations) should be "declare". – rmaddy Nov 28 '17 at 17:58
  • Please review https://stackoverflow.com/questions/24006975/why-create-implicitly-unwrapped-optionals – rmaddy Nov 28 '17 at 17:59
  • 1
    *"Wouldn't this make the mapView object only accessible to the loadView function but not to other methods"* - **No**, simple as that. The variable is visible according to the scope in which it is declared, where you assign its value does not matter – luk2302 Nov 28 '17 at 18:01
  • @luk2302 are you saying that since it was instantiated in the loadView method then it will always remain in memory and the other functions will refer to this same object if they call on it? – Laurence Wingo Nov 28 '17 at 18:06

3 Answers3

1

The scope of a variable is set by its definition, not its assignment. mapView is a property of MapViewController. Therefore it is accessible everywhere in MapViewController. That's unrelated to when it is assigned.

View controllers are a bit unusual because they are often initialized from storyboards, but some pieces cannot be initialized until viewDidLoad (because they reference pieces from the storyboard). That said, this is not the best code. It would have been better written this way:

class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {

    let mapView = MKMapView()
    ...

And the line mapView = MKMapView() should be removed from loadView. The way it's written works, but it's not as clear or as safe as it should be. (loadView is only called one time in a view controller's life cycle. This wasn't always true, but has been true for longer than Swift has been around.)

When I say that this is not "as safe as it should be," I mean that if something were to access mapView between init and loadView (which can happen for various reasons), this would crash. By carefully managing things, you can avoid that, but it's safer just to avoid ! types when you can.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
1

Variables declared outside of functions the way mapView is are instance variables. The scope of the variable is all the code that's available to the instance, so it's acceptable to reference the object from other functions.

By initializing it inside loadView, the object reference is only valid after that assignment executes but that's different from visibility of the variable.

Phillip Mills
  • 30,888
  • 4
  • 42
  • 57
  • When you made the statement, "By initializing it inside loadView, the object reference is only valid after that assignment executes but that's different from visibility of the variable.", does this mean that once the assignment has executed, the instantiated object remains in memory and can be used again by another function without explicitly saying mapView = MKMapView() again? – Laurence Wingo Nov 28 '17 at 18:14
  • 1
    Yes. `mapView = MKMapView()` would create a new instance of the view and replace the existing one, which is generally not what you would want. – Phillip Mills Nov 28 '17 at 18:25
1

You asked:

Isn't it best practice to instantiate objects outside of functions so that we can reference the same object as we program a viewController ... ?

If you’re asking the question of “don’t we favor properties over local variables”, the answer is “no”. In defensive programming, we favor local variables except in those cases where we actually need view controller level scope, in which case we use a property. Local variables are free of unintended data sharing/mutation. Properties, because they are shared amongst all the methods, entail a (admittedly modest) risk that it might be accidentally changed elsewhere. Now, where you need to reference the object in a variety of methods, such as the case with mapView, then a property is what you need. But if we don’t need to reference it elsewhere, such as the locationManager, then we stick with local variables if we can.

And if not, then what is the purpose of inferring variables at the top of viewControllers and then instantiating the object within a function?

First, we’re not “inferring variables” at the top of the view controller. We’re simply “declaring properties”, declaring that this will accessible throughout the view controller, regardless of where it is ultimately instantiated.

Regarding the practice of declaring a property up front, but only later instantiating the object and setting the property later within a function like viewDidLoad (or in your example, in loadView), this is not an unreasonable practice. It keeps the instantiation and configuration all together in one place.

If this really bothered you, then yes, you could go ahead and instantiate the object up where you declare the property. But if you were doing that, I might move the configuration of that object there, too, using a closure to both instantiate and configure the object:

class MapViewController: UIViewController {

    lazy var mapView: MKMapView = {
        let mapView = MKMapView()
        mapView.delegate = self
        mapView.isPitchEnabled = true
        mapView.showsPointsOfInterest = true
        mapView.showsBuildings = true
        mapView.showsCompass = true
        mapView.showsTraffic = true
        mapView.showsUserLocation = true
        return mapView
    }()

    ...

    override func loadView() {    
        view = mapView
    
        if CLLocationManager.authorizationStatus() == .notDetermined {
            CLLocationManager().requestWhenInUseAuthorization()
        }    

        ...
    }
}

The only trick is that because the closure initializing mapView refers to self, we need to instantiate it lazily.

All of that having been said, I’m not concerned as others about the practice of just declaring the property up front, but instantiating/configuring it later in viewDidLoad/loadView. In fact, this is very common. In storyboards, for example, we decouple the process into three phases:

  • we declare of the property in the view controller and hook up the outlet in Interface Builder;
  • when the storyboard scene is instantiated, the storyboard instantiates the map view for us; and
  • we’ll do any additional programmatic configuration of the map view in viewDidLoad.

You said:

Here is my example code I'm following from a tutorial. Notice how mapView is inferred at the top of the view controller but instantiated in the loadView method. Wouldn't this make the mapView object only accessible to the loadView function but not to other methods:

class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {

    var mapView: MKMapView!

    ...


    override func loadView() {
        mapView = MKMapView()
        view = mapView
        ...
        let locationManager = CLLocationManager()
        ...
    }
}

No, just because we instantiated mapView in loadView does not limit its scope to that method. In fact, because we declared it as a property earlier, the opposite is true. It is accessible throughout the view controller methods. Do not conflate the declaration of a variable/property with its instantiation.

In this example, locationManager is a local variable (because it’s only used locally within this method), but mapView is a property of the view controller class. The mapView is instantiated and configured in loadView, but we make it a property of the view controller because the mapTypeChanged method needs access to it.


By the way, this technique of programmatically setting the root view of the view controller in loadView is a very uncommon practice nowadays. That is only for programmatically create views. Instead, often we use a storyboard (or in rare cases, a NIB/XIB), add the map view there, hook up the @IBOutlet in Interface Builder, and then viewDidLoad (not to be confused with loadView) could reference that outlet property:

class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {

    @IBOutlet var mapView: MKMapView!

    ...


    override func viewDidLoad() {
        super.viewDidLoad()

        // perhaps reference the map view created in the storyboard to configure it, e.g.

        mapView.isPitchEnabled = true

        ...
        let locationManager = CLLocationManager()
        ...
    }
}

For more information, see Displaying and Managing Views with a View Controller. Also see the View Controller Programming Guide.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044