Updating a UILabel via protocol results in crash (found nil) - ios

I want to implement the MVP pattern for a new app. So the View shouldn't have any logic besides one that exclusively concerns UI elements. Therefore I want to request initial data from an "Interpreter" (interpreting user input in later code), which in turn requests data from my model and gives it to the "Presenter". The presenter holds a protocol with functions of the view.
The problem is: Calling updateUIData() from the presenter results in a
fatal error: unexpectedly found nil while unwrapping an Optional value
while calling the function from within the View at the same position is working just fine.
I suspect the error comes from the initialization of the specific MainViewController in the init of the presenter, but I don't know how to resolve this, if my guess is right.
Here's my (relevant) code:
MainViewController:
class MainViewController: UIViewController {
lazy var interpreter = Interpreter() // lazy needed b/c Interpreter holds Presenter which holds MainViewController
#IBOutlet var dateLabel: UILabel!
#IBOutlet var totalTimeLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// updateUIData()
requestData()
}
func requestData() {
interpreter.requestData()
}
}
extension MainViewController: MainViewSetters {
func updateUIData() {
dateLabel.text = "Data"
totalTimeLabel.text = "loaded"
}
}
MainViewSetters (Protocol):
protocol MainViewSetters {
func updateUIData()
}
Interpreter:
class Interpreter {
let presenter = Presenter()
func requestData() {
// normally: get data from model and pass it to presenter
presenter.presentData()
}
}
Presenter:
class Presenter {
let mainView: MainViewSetters
init(withMainViewController mainVC: MainViewSetters = MainViewController()) {
mainView = mainVC
}
func presentData() {
mainView.updateUIData()
}
}

Your problem here is that you are not passing the reference to MainViewController to your instance of Presenter.
This code :
lazy var interpreter = Interpreter()
Should be more like this : (Type is needed here because with lazy the compiler can't infer properly)
lazy var interpreter: Interpreter = Interpreter(for: self)
You then have to create a special initializer in Interpreter which will pass the viewController instance to its presenter property :
class Interpreter {
let presenter: Presenter
init(for viewController: MainViewSetters) {
presenter = Presenter(withMainViewController: viewController)
}
func requestData() {
// normally: get data from model and pass it to presenter
presenter.presentData()
}
}
I also highly suggest you to remove the default value to Presenter's init method, it's very unlikely you'll want to assign a random instance of MainViewController as mainView of any Presenter object.
Finally, please note that this code is creating a retain cycle and neither your MainViewController instance nor your Presenter instance will be deallocated. This is due to the fact the Presenter class holds a strong reference to the MainViewController instance with its property mainView. To fix this you have to mark the mainView as weak as well as making it optional.
Please see the fixed implementation below :
class Presenter {
weak var mainView: MainViewSetters?
init(withMainViewController mainVC: MainViewSetters) {
mainView = mainVC
}
func presentData() {
mainView?.updateUIData()
}
}
For weak to be acceptable on a property of type MainViewSetters (which is not a real type but only a protocol) you have to specify that its a protocol that will only be applied to classes :
protocol MainViewSetters: class {
func updateUIData()
}

You are initializing interpreter passing a default MainViewController().
Change that code from:
lazy var interpreter = Interpreter()
to
lazy var interpreter = Interpreter(withMainViewController: self)

Related

How to pass data to the final view controller

I am new to Swift and am building an app to learn. Right now I am making the registration section of the app.
I thought the UX would be better if there were multiple VC's asking a single question, i.e. one for your name, one for your birthdate, etc as opposed to jamming all that into a single view controller. The final view controller collects all of that information and sends a dictionary as FUser object to be saved on Firebase.
I figured I could instantiate the final view controller on each of the previous five view controllers and pass that data directly to the end. I kept getting errors and figured out that the variables were nil. It works just fine if I pass the data directly to the next view controller but it doesn't seem to let me send it several view controllers down. Obviously there's a nuance to how the memory is being managed here that I'm not tracking.
Is there a way to do what I am trying to do or do I have to pass the data through each view controller along the way?
import UIKit
class FirstViewController: UIViewController {
//MARK: - IBOutlets
#IBOutlet weak var firstNameTextField: UITextField!
//MARK: - ViewLifeCycle
override func viewDidLoad() {
super.viewDidLoad()
}
//MARK: - IBActions
#IBAction func continueToMiddleViewController(_ sender: Any) {
let vcFinal = storyboard?.instantiateViewController(withIdentifier:
"finalVC") as! finalViewController
vcFinal.firstName = firstNameTextField.text
let vc = storyboard?.instantiateViewController(withIdentifier:
"middleVC") as! middleViewController
vc.modalPresentationStyle = .fullScreen
present(vc, animated: false)
}
...
}
import UIKit
class FinalViewController: UIViewController {
var firstName: String?
...
//MARK: - ViewLifeCycle
override func viewDidLoad() {
super.viewDidLoad()
}
...
}
TL;DR: The fastest one that would solve your problem is creating a singleton
There are many strategies for this. For a starter, it might be a good idea to read some begginer articles, like this one. I can update this answer if you don't find it useful, but it'd look just like the article
Viewcontroller's variable can't be initiated until any of the init method is called.
There are detailed answers on this thread.
Passing Data between ViewControllers
Another way to approach this problem could be to make use of closures. Note that personally I've moved away from using storyboards but I'll try to explain still. Closures are also referred to as callbacks, blocks, or in some context like here - completions.
You can declare a closure like let onSubmitInfo: (String?) -> Void below, it stores a reference to a block of code that can be executed at a later stage just like a function and it takes an optional string as a parameter just like a function can.
The closures are specified in the initialisers where a block of code is passed into the respective classes below and the closures are then called in the IBActions that will trigger the block of code that is defined where the below classes are initialised:
class First: UIViewController {
// MARK: - IBOutlets
#IBOutlet weak var firstNameTextField: UITextField!
// MARK: - Properties
private let onSubmitInfo: (String?) -> Void
init(onSubmitInfo: (String?) -> Void) {
self.onSubmitInfo = onSubmitInfo
}
// MARK: - IBActions
#IBAction func continue(_ sender: Any) {
onSubmitInfo(firstNameTextField.text)
}
}
class Second: UIViewController {
// MARK: - IBOutlets
#IBOutlet weak var lastNameTextField: UITextField!
// MARK: - Properties
private let onSubmitInfo: (String?) -> Void
init(onSubmitInfo: (String?) -> Void) {
self.onSubmitInfo = onSubmitInfo
}
// MARK: - IBActions
#IBAction func continue(_ sender: Any) {
onSubmitInfo(lastNameTextField.text)
}
}
To manage showing the above views and collecting the values returned by their closures (i.e. onSubmitInfo) we create a FlowController class that will also show the next view when the closure is called.
In FlowController we define the closures or blocks of code to be executed when it is called inside the IBAction in the respective First and Second classes above.
The optional string that is provided in the respective First and Second classes is used as the (firstName) and (secondName) closure properties below:
class FlowController: UIViewController {
private var fistName: String?
private var lastName: String?
...
private func showFirstView() {
let firstViewController = First(onSubmitInfo: { (firstName) in
self.firstName = firstName
showSecondView()
})
navigationController?.pushViewController(
firstViewController,
animated: true)
}
private func showSecondView() {
let secondViewController = Second(onSubmitInfo: { (lastName) in
self.lastName = lastName
showFinalView()
})
navigationController?.pushViewController(
secondViewController,
animated: true)
}
private func showFinalView() {
let finalViewController = Final(
firstName: firstName,
lastName: lastName)
navigationController?.pushViewController(
finalViewController,
animated: true)
}
}
The FlowController finally shows the Final view controller after it has collected the firstName form the First view controller and the lastName form the Second view controller in the showFinalView function above.
class Final: UIViewController {
let firstName: String
let lastName: String
...
}
I hope this is a shove in the right direction. I have moved away from storyboards because I find creating views in code is more verbose and clear on peer reviews and it was also easier for me to manage constraints and just to manage views in general.

Weak delegate becomes nil

In my app I'm using delegates, so that I can read the data when ever it's ready.
I'm calling a delegate from two classes. Here is my code
protocol MyDelegate: class {
func getData()
}
class MyDelegateCalss {
weak var delegate: MyDelegate?
func loadData() {
// doing some actions
if(self.delegate != nil){
self.delegate!.getData!()
}
}
}
In one class I'm loading this method in tableview numberOfSections delegate method.
class A: UIViewController, MyDelegate {
func somefunc(){
let mydelegatecls : MyDelegateCalss = MyDelegateCalss()
mydelegatecls.delegate = self
mydelegatecls.loadData()
}
func getData(){
// Doing some actions
}
}
This method I'm loading from another calss.
class B: UIViewController, MyDelegate {
open func testfunc(){
let mydelegatecls : MyDelegateCalss = MyDelegateCalss()
mydelegatecls.delegate = self
mydelegatecls.loadData()
}
func getData(){
// doing some other stuff
}
}
class C: UIViewController {
func testfunc(){
let b : B = B()
b.testfunc()
}
}
Here from class A my delegate is working fine. and I'm able to see getData method is calling .
from Class B, the delegate becomes nil and unable to see getData method is called
If I make the delegate reference its working fine. But that will cause memory leak.
How can handle this case ?
Your delegate var is declared as weak. If nothing keep a strong reference on the object you assign as the delegate (implementing MyDelegate), your delegate will pass to nil as soon as the object is released (eg. the end of the scope where you instantiate it).
Some good read: https://cocoacasts.com/how-to-break-a-strong-reference-cycle/

instantiated class is nil when called inside viewdidload()

I am trying to learn the VIPER architecture model and one thing I can't figure out is when I do the following:
Instantiate promotionPresenter class
Instantiate promotionsViewController
assign promotionsViewController.presenter = (instantiated promotionPresenter class from step 1)
try to access the instantiated presenter class from inside viewdidload() function within promotionviewController class.
presenter is nil.
Why is presenter nil? I already instantiated it.
import UIKit
/*
* The Router responsible for navigation between modules.
*/
class PromotionsWireframe : PromotionsWireframeInput {
// Reference to the ViewController (weak to avoid retain cycle).
var promotionsViewController: PromotionsViewController!
var promotionsPresenter: PromotionsPresenter!
var rootWireframe: RootWireframe!
init() {
let promotionsInteractor = PromotionsInteractor()
// Presenter is instantiated
promotionsPresenter = PromotionsPresenter()
promotionsPresenter.interactor = promotionsInteractor
promotionsPresenter.wireframe = self
promotionsInteractor.output = promotionsPresenter
}
func presentPromotionsIntefaceFromWindow(_ window: UIWindow) {
//view controller is instantiated
promotionsViewController = promotionsViewControllerFromStoryboard()
//presenter of view controller is assigned to instantiaed class
promotionsViewController.presenter = promotionsPresenter
promotionsPresenter.view = promotionsViewController
}
private func promotionsViewControllerFromStoryboard() -> PromotionsViewController {
let storyboard = UIStoryboard(name: "PromotionsStoryboard", bundle: nil )
let viewController = storyboard.instantiateViewController(withIdentifier: "promotionsViewController") as! PromotionsViewController
return viewController
}
}
import UIKit
class PromotionsViewController : UIViewController, PromotionsViewInterface {
// Reference to the Presenter's interface.
var presenter: PromotionsModuleInterface!
var promotions: [Promotion]!
/*
* Once the view is loaded, it sends a command
* to the presenter asking it to update the UI.
*/
override func viewDidLoad() {
super.viewDidLoad()
// getting error because presenter is unwrapped as nil
self.presenter.updateView()
}
func showPromotionsData(_ promotions: [Promotion]) {
// need to implement
}
}
import Foundation
class PromotionsPresenter : PromotionsModuleInterface, PromotionsInteractorOutput {
// Reference to the View (weak to avoid retain cycle).
var view: PromotionsViewInterface!
// Reference to the Interactor's interface.
var interactor: PromotionsInteractorInput!
var wireframe: PromotionsWireframe!
func updateView() {
self.interactor.fetchLocalPromotions()
}
func PromotionsFetched(_promotions: [Promotion]) {
// need to implement
}
}
I suggest you take this boilerplate (https://github.com/CheesecakeLabs/Boilerplate_iOS_VIPER) and read this post (https://www.ckl.io/blog/best-practices-viper-architecture/) in order to learn not only how to correctly initialize your VIPER modules, but also how to automate VIPER files creation and initiazlization

swift: defer non-optional object initialization

When dialing with CocoaTouch, it often happens that UIView(Controller) subclass properties can't be initialized in init method (ex. we need view already loaded), but logically they are non-optional and even non-var. In such cases the property must be optional to compile without errors, what looks pretty ugly - the code is fulfilled with !.
Is there any way to solve this problem? I would imagine some deferred initialization. Perfectly if such property can compile without initial value and crash at runtime if it's accessed prior to be initialized.
Some code sample to describe the issue:
class MyVC: UIViewController {
#IBOutlet var someLabel: UILabel!
let viewBasedParam: CustomClass // how to keep it non-optional if it can be initialized after view has been loaded?
override func viewDidLoad() {
super.viewDidLoad()
self.viewBasedParam = CustomClass(self.someLabel.text)
}
}
P.S. CustomClass can't have default initializer, because it requires data from view.
In your MyVC you can have a convenience init method where you can initialize the let variables. Try this and let me know if it works for you:
class MyVC: UIViewController {
let viewBasedParam: CustomClass
convenience init() {
self.init(nibName:nil, bundle:nil)
self.viewBasedParam = CustomClass(self.someLabel.text)//else just initialize with empty string here and then assign actual value in viewDidLoad
}
}
As far as I've discovered the workaround solution may be following:
class MyVC: UIViewController {
#IBOutlet var someLabel: UILabel!
private var _viewBasedParam: CustomClass? = nil
var viewBasedParam: CustomClass {
get { return self._viewBasedParam! } // always unwrap private optional
set { self._viewBasedParam = newValue }
}
override func viewDidLoad() {
super.viewDidLoad()
self.viewBasedParam = CustomClass(self.someLabel.text)
}
}

#obj protocol delegate method not working in second class. Swift

Im trying to call protocol delegate in an additional class. The first class (ViewController) works but the second one I have implemented it in doesn't show any data. I added it with the autofill option so its not giving errors. It just doesn't do anything.
sender class
#objc protocol BWWalkthroughViewControllerDelegate{
#objc optional func walkthroughPageDidChange(pageNumber:Int) // Called when current page changes
}
#objc class BWWalkthroughViewController: UIViewController, UIScrollViewDelegate, ViewControllerDelegate {
// MARK: - Public properties -
weak var delegate:BWWalkthroughViewControllerDelegate?
var currentPage:Int{ // The index of the current page (readonly)
get{
let page = Int((scrollview.contentOffset.x / view.bounds.size.width))
return page
}
}
// MARK: - Private properties -
let scrollview:UIScrollView!
var controllers:[UIViewController]!
var lastViewConstraint:NSArray?
var shouldCancelTimer = false
var aSound: AVAudioPlayer!
var isForSound: AVAudioPlayer!
var alligatorSound: AVAudioPlayer!
var audioPlayer = AVAudioPlayer?()
var error : NSError?
var soundTrack2 = AVAudioPlayer?()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var audioPlayerAnimalSound = AVAudioPlayer?()
var audioPlayerAlphabetSound = AVAudioPlayer?()
var audioPlayerFullPhraze = AVAudioPlayer?()
var audioPlayerPhonics = AVAudioPlayer?()
Code removed to save space carries on:
/**
Update the UI to reflect the current walkthrough situation
**/
private func updateUI(){
// Get the current page
pageControl?.currentPage = currentPage
// Notify delegate about the new page
delegate?.walkthroughPageDidChange?(currentPage)
}
receiver class
class BWWalkthroughPageViewController: UIViewController, BWWalkthroughPage, ViewControllerDelegate, BWWalkthroughViewControllerDelegate {
Function in second Class.
func walkthroughPageDidChange(pageNumber: Int) {
println("current page 2 \(pageNumber)")
}
walkthroughPageDidChange does work in the viewController class however. Please can you help me see what is wrong?
Your weak var delegate:BWWalkthroughViewControllerDelegate? is an optional variable and it is not assigned anywhere in your presented code, hence it will be nil. You have to instantiate it somewhere for it to work.
A good way to do so is:
class BWWalkthroughViewController {
let bwWalkthroughPageViewController = BWWalkthroughPageViewController()
var bwWalkthroughViewControllerDelegate : BWWalkthroughViewControllerDelegate!
init() {
bwWalkthroughViewControllerDelegate = bwWalkthroughPageViewController as BWWalkthroughViewControllerDelegate
}
}
A thing to note is that it is often good practice to name the delegates with meaningful names. The keyword "delegate" is often used for many classes and is restricted. A more verbose name will eliminate the chance of overriding an original delegate and will help identify each one if you have more than one.

Resources