Is a callback in a lazy var okay to do in Swift? - ios

lazy var headerView: WatchlistModifierHeaderView = {
let view = WatchlistModifierHeaderView()
view.translatesAutoresizingMaskIntoConstraints = false
view.heightAnchor.constraint(equalToConstant: HEADER_VIEW_HEIGHT).isActive = true
view.tapEventer.handler = { [unowned self] in
print("HeaderView tapped")
}
return view
}()
Here is an example. I like this style because everything pertaining to the view is captured in the lazy var. However, I am curious if the variable won't deinit because of the callback.
At the end of the day, I need to read up on memory, init, and deinit.

That looks fine to me. If you actually end up using self in the callback as long as you keep that unowned or add a weak, it shouldn't cause a retain cycle and will deinit correctly.

Related

Why am I getting nil for tableview outlet?

When the callback for the TaskListDataSource gets called it reloads both the todayVC and the reviewVC because they are UITableViewControllers. However the plannerVC is not and the tableview property is an outlet.
#IBOutlet weak var tableView: UITableView!
Why is it that when the callback runs it crashes saying it is nil. If I am somehow able to scroll across in the page view however and and view the plannerVC it will never crash as the tableview has been loaded into memory. But why doesn't it do it initially?
override func viewDidLoad() {
super.viewDidLoad()
let taskListDataSource = TaskListDataSource {
self.todayVC.tableView.reloadData()
self.plannerVC.tableView.reloadData()
self.reviewVC.tableView.reloadData()
}
todayVC = storyboard!.instantiateViewController(identifier: "TodayViewController", creator: { coder in
return TodayViewController(coder: coder, taskListDataSource: taskListDataSource)
})
plannerVC = storyboard!.instantiateViewController(identifier: "PlannerViewController", creator: { coder in
return PlannerViewController(coder: coder, taskListDataSource: taskListDataSource)
})
reviewVC = storyboard!.instantiateViewController(identifier: "ReviewViewController", creator: { coder in
return ReviewViewController(coder: coder, taskListDataSource: taskListDataSource)
})
addVC = storyboard!.instantiateViewController(identifier: "AddViewController")
setViewControllers([todayVC], direction: .forward, animated: false)
dataSource = self
print(plannerVC.tableView) // Console is printing nil
}
When you call instantiateInitialViewController(creator:), the UIViewController is initiated, but its view (and all subviews, including then all the IBOutlet) aren't loaded in memory.
So when, you try to do self.someIBoutlet (in your case self.plannerVC.tableView.reloadData(), it crashes.
A solution, would be to force the view to load, with loadViewIfNeeded().
Since loading the view can be heavy, it's usually used when the ViewController will be shown shortly after (for instance, in a didSet of some property that access outlet in it, because it will be shown on screen in a few instants, so the view will be loaded anyway, just a few moment after).
Since you are loading 3 UIViewController, could it be that you aren't showing them, but prematurely loading them?
If that's the case, you might rethink your app architecture (all your UIViewController don't need to be initialized and in memory, and less to have their view loaded).
Still, you can check beforehand if the view has been loaded, and that you can access the outlets with isViewLoaded.
I'dd add for that a method in PlannerVC:
func refreshData() {
guard isViewLoaded else { return }
tableView.reloadData()
}
Side note, it could be a protocol (and even more, complexe, like adding var tableView { get set }, and have a default implementation of refreshData(), but that's going further, not necessary)...
protocol Refreshable {
func refreshData()
}
let taskListDataSource = TaskListDataSource {
self.todayVC.refreshData()
self.plannerVC.refreshData()
self.reviewVC.refreshData()
}
Side note, I would check if there isn't memory retain cycles, I would have use a [weak self] in the closure of TaskListDataSource, and also would have made it a property of the VC.

MVVM: binding the View with the ViewModel with closures, unowned or weak self?

I'm implementing a simple Master-Detail app where the Master viewController manages a table view which shows the results of a call to a REST service. The Detail viewController manages a view where I show more information about the item selected in the Master. Common scenario.
I'm trying to apply MVVM pattern. In the Master viewController, I create and initialize its viewModel this way:
lazy private var viewModel: ListViewModel = {
return ListViewModel()
}()
override func viewDidLoad() {
super.viewDidLoad()
initViewModel()
}
private func initViewModel() {
viewModel.onModelChange = { [weak self] () in
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
viewModel.fetchData()
}
My question is: in the closure provided to the viewModel, should self be weak or unowned instead? I found an example implementing an scenario similar to mine that was setting it to weak, and another one setting it to unowned, so I'm not completely clear.
[unowned self]. This tells your model not to hold a strong reference to ViewController
Apple document states it clearly that:
“Like weak references, an unowned reference does not keep a strong
hold on the instance it refers to. Unlike a weak reference, however,
an unowned reference is assumed to always have a value”.
In your case, there is always be ViewController. So benefit of unowned reference is it’s nonoptional. No unwrap required each time it is used
Unowned is used when you 100% sure the object won't be deallocated.
weak you then need to take care of its reference count issues.
viewModel.onModelChange = { [weak self] () in
guard let strongSelf = self else { return }
strongSelf.tableView.reloadData()
}
I generally do it like this. You then hold a strong reference of self to avoid it being allocated during the block is running.
The difference between unowned and weak is that weak is declared as an Optional while unowned isn't. If you know that self will not be nil use unowned, if you don't know: Use weak

In Swift, isn't it best practice to instantiate objects outside of functions?

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.
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.
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.
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.

DisposeBag memory leak?

I inherited a project that uses disposeBags everywhere, but disposeBag seems to be a massive memory leak. None of the view controllers utilizing the bag ever get deallocated, which leads to subscriptions piling up. I'm
class TestViewController: UIViewController
{
#IBOutlet weak var testLabel: UILabel!
var basePresenter: BasePresenter = BasePresenter()
var disposeBag: DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindPresenter()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
//self.disposeBag = DisposeBag() <------------
}
func bindPresenter() {
//super.bindPresenter()
basePresenter.testVariable.asDriver().drive(onNext: { test in
if !test.id.isEmpty {
self.testLabel.text = "Test text" //<------------
}
}).addDisposableTo(disposeBag)
}
deinit{
print("TestView was dealloc'd")
}
}
The key issue is the reference in the handler to "self."
My theory is that self is a strong reference, which is leading to a situation where even when the view controller gets popped and there are no other references to the view controller, it still doesn't get deallocated because the bag has a strong reference to it. Circular logic where the bag doesn't get disposed because the VC doesn't dealloc and the VC doesn't dealloc because the bag doesn't get disposed.
The commented out line
//self.disposeBag = DisposeBag()
when called allows for the view to properly dealloc.
On top of the memory leak, the issue that I'm facing is that I don't want to dispose the bag on viewWillDisappear, but rather when the view is popped. I need it to stick around if I add a view on top in case I pop back to this view.
Any help would be greatly appreciated!
Your theory is correct. You need to use a weak or unowned reference to self in your subscribe methods rather than a strong reference. And get rid of the assignment to disposeBag in the viewWillDissapear. The disposeBag will properly dispose of your subscribers when the object gets deinted.
You setup a weak reference to self like this:
basePresenter.testVariable.asDriver().drive(onNext: { [weak self] test in
if !test.id.isEmpty {
self?.testLabel.text = "Test text" // no longer a strong reference
}
}).disposed(by: disposeBag)

iOS - Weak var can still cause retain cycle?

Here is my real code:
#IBOutlet weak var contentTextView: SmartTextView! {
didSet {
self.contentTextView.onDidBeginEditing = {
$0.layer.borderColor = Util.green.CGColor
}
self.contentTextView.onDidEndEditing = {
$0.layer.borderColor = Util.gray.CGColor
}
self.contentTextView.layer.borderWidth = 1 / Util.screenScale
self.contentTextView.layer.borderColor = Util.gray.CGColor
self.contentTextView.minHeight = 148
self.contentTextView.maxHeight = 148
self.contentTextView.onChange = { [unowned self] text in
var content = text.stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: "\n\t"))
self.contentLenthLabel.text = "\(self.MAX_CONTENT - count(content))"
}
}
}
If I remove [unowned self] statement, I can see a retain cycle problem in Instruments.
Is the KVO or something else make a weak var can still cause retain cycle?
The weak reference is a red herring; it has nothing to do with the story here. Without the [unowned self], you are retaining this view and this view is retaining you. That's a retain cycle:
UIViewController retains its view
View retains its subviews; one of those subviews is the SmartTextView
SmartTextView retains the onChange function
Function retains self (the UIViewController) unless you say unowned self.

Resources