View Controller lifecycle methods are not called after init() - ios

I'm facing a strange problem. I'm calling my CommonListViewController in the following way from my main tab bar screen:
self?.navigator.go(module: CommonListBuilder(mode: .vacation), mode: .push(animated: false))
This is my build() method:
func build() -> BaseViewController {
CommonListViewController(interactor: CommonListInteractor(mode: mode), mode: mode)
}
This method is called, then CommonListViewController's init is called:
init(
interactor: CommonListInteractorProtocol,
mode: CommonListMode
) {
self.interactor = interactor
self.mode = mode
self.adapter = CommonListTableAdapter(mode: mode)
self.searchAdapter = CommonListSearchAdapter()
super.init()
interactor.sendMetrics(event: .openScreen, params: mode.paramsForOpenScreen)
}
But then nothing happens. Loadview(), viewDidLoad() (I'm using breakpoints) and other lifecycle methods are not called, so CommonListViewController is not opening. What can be the reason?
IMPORTANT: This happens not on the first launch, but only after switching between tabs.

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.

'button.setImage' is not working when ViewController (containing it) is in background

I'm using the delegation pattern for my CoreBluetooth based app. I have a main ViewController that is a delegate to my BLEHandler class. I'm updating a button based on the response I get from following delegate method:
func acIsOn(error: NSError?) {
if error == nil{
pushButton.setImage(UIImage(named: "state4"), for: .normal)
}
}
It works fine when my delegate controller class is in the foreground but when I move to another ViewController and calls a method of handler class that in response calls the delegate method above, the button on the image is not updated.
Here's what I have already tried:
Wrapping the statement in DispatchQueue.main.async{}
Calling pushButton.setNeedsLayout() and setNeedsDisplay()
But none of it worked. ,
Also I made sure that this method was being called when the ViewController is not in the foreground.
Edit 1: I'm more interested in learning about the limitation that is not allowing this to happen, I'm not looking for hacks/tricks to bypass this.
Edit 2: As mentioned by Shoazab, button.setBackgroundImage() is working when the VC is in background. Still curious why button.setImage doesn't work in background but it does in when VC is on top.
You are trying to changed an UI element on a ViewController which is not currently displayed. It will update values but no refresh on UI will be executed.
I think that calling SetNeedsDisplay on method viewWillAppear on your ViewController will fix your problem.
You can also use a variable Image and update the button Image when the controller is displayed again.
Move the UI update code to the viewWillAppear like this:
class MainViewController: UIViewController, BLEHandler {
var isUIUpdateNeeded = false
//Define your UI outlets or proeprties
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewWillAppear(_ animated: Bool) {
if isUIUpdateNeeded {
updateUI()
}
}
func acIsOn(error: NSError?) {
if error == nil{
pushButton.setImage(UIImage(named: "state4"), for: .normal)
}
}
func updateUI() {
//do your UI changes here
pushButton.setImage(UIImage(named: "state4"), for: .normal)
}
}
Call btn.setBackgroundImage() it will set it

Memory leak when adding subview

When adding a subview, the view controller seems to leak.
Why does the following print 'What'
import UIKit
final class ViewController: UIViewController {
private lazy var mySwitch: UISwitch = {
let mySwitch = UISwitch()
mySwitch.tintColor = .blue
return mySwitch
}()
func setup() {
view.addSubview(mySwitch)
}
#objc func switchChangedState() {
}
deinit {
print("what")
}
}
var controller: ViewController? = ViewController()
controller = nil
But the following does not
var controller: ViewController? = ViewController()
controller?.setup()
controller = nil
Edit: adding GIF
Xcode Version 9.4.1 (9F2000)
Your code is good. controller?.setup() will not cause a leak. Please make sure the code in test case 2 is really called or not. (No calling no "what" printed)
There's nothing wrong with your code. There's no retain cycle here.
The problem appears to be something to do with the playground. It could be a bug, or the playground may be retaining your view controller for some reason.
If you execute your code in an actual Xcode project (either in the iOS simulator or on a device), the initializer is executed in both cases:

Fullscreen video in UIWebView interferes with UIViewController's input accessory view

I have a view which I set up as input accessory view for view controller the following way:
#IBOutlet private weak var bottomPane: UIView!
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView {
return bottomPane
}
Everything works just fine until I try to view YouTube video in fullscreen mode (video is loaded in UIWebView). When video enters fullscreen mode, keyboard and my input accessory view disappear (which is normal, I guess), but when I exit fullscreen mode, they do not appear. If I keep the reference to bottomPane weak, it becomes nil and application crashes, if I change it to strong, input accessory view remains hidden until the keyboard appears next time.
Can anybody explain what's going on and how to fix this?
Here's what's going on.
When user interacts with UIWebView, it becomes first responder and inputAccessoryView provided by view controller disappears (no idea why behavior in this case is different from, say, UITextField). Subclassing UIWebView and overriding inputAccessoryView property does not work (never gets called). So I block interaction with UIWebView until user loads video.
private func displayVideo(URL: String) {
if let video = Video(videoURL: URL) {
// load video in webView
webView.userInteractionEnabled = true
} else {
webView.userInteractionEnabled = false
}
}
When user loads video, the only way to detect that user has entered/exited fullscreen mode is to listen to UIWindowDidBecomeKeyNotification and UIWindowDidResignKeyNotification and detect when our window loses/gains key status:
//in view controller:
private func windowDidBecomeKey(notification: NSNotification!) {
let isCurrentWindow = (notification.object as! UIWindow) == view.window
if isCurrentWindow {
// this restores our inputAccessoryView
becomeFirstResponder()
}
}
private func windowDidResignKey(notification: NSNotification!) {
let isCurrentWindow = (notification.object as! UIWindow) == view.window
if isCurrentWindow {
// this hides our inputAccessoryView so that it does not obscure video
resignFirstResponder()
}
}
And, of course, since inputAccessoryView can be removed at some point, we should recreate it if needed:
//in view controller:
override var inputAccessoryView: UIView {
if view == nil {
// load view here
}
return view
}

Is it possible to call a block completion handler from another function in iOS?

I have a custom UIView with a UITapGestureRecognizer attached to it. The gesture recognizer calls a method called hide() to remove the view from the superview as such:
func hide(sender:UITapGestureRecognizer){
if let customView = sender.view as? UICustomView{
customView.removeFromSuperview()
}
}
The UICustomView also has a show() method that adds it as a subview, as such:
func show(){
// Get the top view controller
let rootViewController: UIViewController = UIApplication.sharedApplication().windows[0].rootViewController!!
// Add self to it as a subview
rootViewController.view.addSubview(self)
}
Which means that I can create a UICustomView and display it as such:
let testView = UICustomView(frame:frame)
testView.show() // The view appears on the screen as it should and disappears when tapped
Now, I want to turn my show() method into a method with a completion block that is called when the hide() function is triggered. Something like:
testView.show(){ success in
println(success) // The view has been hidden
}
But to do so I would have to call the completion handler of the show() method from my hide() method.
Is this possible or am I overlooking something?
Since you are implementing the UICustomView, all you need to do is store the 'completion handler' as part of the UICustomView class. Then you call the handler when hide() is invoked.
class UICustomView : UIView {
var onHide: ((Bool) -> ())?
func show (onHide: (Bool) -> ()) {
self.onHide = onHide
let rootViewController: UIViewController = ...
rootViewController.view.addSubview(self)
}
func hide (sender:UITapGestureRecognizer){
if let customView = sender.view as? UICustomView{
customView.removeFromSuperview()
customView.onHide?(true)
}
}
Of course, every UIView has a lifecycle: viewDidAppear, viewDidDisappear, etc. As your UICustomView is a subclass of UIView you could override one of the lifecycle methods:
class UICustomView : UIView {
// ...
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear (animated)
onHide?(true)
}
}
You might consider this second approach if the view might disappear w/o a call to hide() but you still want onHide to run.

Resources