swift: defer non-optional object initialization - ios

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

Related

Access variable in a function which is called from another class

I have a class1 which is having many variable and also having one function within a class1.I am calling the function from another class class2.function triggered but i cannot access variable of class1.
here is the sample code
class ViewController: UIViewController {
var Flat:String?
var Flong:String?
var Tlat:String?
var Tlong:String?
override func viewDidLoad() {
super.viewDidLoad()
Flat = "flat value";
Flong="flong value";
Tlat="Tlat value";
Tlong="tlong value";
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func calculation()
{
print("origin_lat new\(Flat)")
print("origin_lng new\(Flong)")
print("dest_lat new\(Tlat)")
print("dest_lng new\(Tlong)")
}
}
I am calling calculation method from another class Collectionviewcell click function
var mycontroller : ViewController = ViewController()
mycontroller.calculation()
Why i could not access the values anyone help me?
You can also reach other controller's variables with defining global variables like this way:
class Class1ViewController: UIViewController {
struct GlobalVariables{
static var Flat:String?
static var Flong:String?
static var Tlat:String?
static var Tlong:String?
}
override func viewDidLoad() {
super.viewDidLoad()
Flat = "flat value";
Flong="flong value";
Tlat="Tlat value";
Tlong="tlong value";
}
...
}
And you can use these variables in another view controller:
class Class2ViewController: UIViewController
{
...
print(Class1ViewController.GlobalVariables.Flat)
print(Class1ViewController.GlobalVariables.Flong)
print(Class1ViewController.GlobalVariables.Tlat)
print(Class1ViewController.GlobalVariables.Tlong)
...
}
Actually, the "viewDidLoad()" function is not called. It's will be called when you display the viewController, for example, a UINavigationController push it. In your case, you just created the viewController, not displayed it. If you want to init these variables without displaying the viewController, you need to do this:
class ViewController: UIViewController {
var Flat:String?
var Flong:String?
var Tlat:String?
var Tlong:String?
required init?(coder:NSCoder) {
super.init(coder:coder)
self.customInit()
}
override init(nibName: String?, bundle: Bundle?) {
super.init(nibName: nibName, bundle: bundle)
self.customInit()
}
func customInit() {
Flat = "flat value";
Flong="flong value";
Tlat="Tlat value";
Tlong="tlong value";
}
//.......
}
What your trying to achieve is a kind of model class. I would suggest you create a simple model class which is not subclass of UIViewcontroller like so.
class Model: NSObject {
var fLat: String?
var fLong: String?
var tLat: String?
var tLong: String?
override init() {
super.init()
fLat = "flat value"
fLong = "flong value"
tLat = "tlat value"
tLong = "tlong value";
}
// for print I have forced unwrapped so remember to check before force unwrapping smi)e
func calculation() {
print("orgin_lat new\(fLat!)")
print("origin_lng new\(fLong!)")
print("origin_lat new\(tLat!)")
print("origin_lng new \(tLong!)")
}
}
Now inside your main View controller you can initialize and call the function like so
let model = Model()
model.calculation()

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

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)

Property value prints but when assigned to label it shows nil

Currently working with two view controllers and a swift file dealing with the details of a store such as the phone number. There is a main ViewController and a DetailsViewController.
I currently acquire data from google places api and am able to successfully store the values in a PlaceDetails Class. Testing out the data - I am able to print to the console. However, when I try to assign a value taken from the API to a UILabel the application crashes and shows that the value of the property is nil. I am not sure why this is the case. I feel in the two view controllers I am instantiating the PlaceDetails class correctly and accessing it's properties correctly but there is something I am not aware of that is going wrong.
class ViewController: UIViewController
{
let placeDetails = PlaceDetails()
let detailsVC = DetailsViewController()
func tapLabel( sender: UITapGestureRecognizer )
{
// print statement successfully prints out the stored value as - Optional("1 888-555-5555")
print(placeDetails.phoneNumber)
// assigning value to label causes a crash stating value is nil
detailsVC.phoneNumberLabel.text = placeDetails.phoneNumber!
self.performSegueWithIdentifier("showDetailsVC", sender: self)
}
}
class DetailsViewController: UIViewController
{
#IBOutlet weak var phoneNumberLabel : UILabel!
let placeDetails = PlaceDetails()
override func viewDidLoad()
{
super.viewDidLoad()
//This approach does not cause a crash but outputs nil to the console for both the print statement and the assignment statement
print(placeDetails.phoneNumber)
phoneNumberLabel.text = placeDetails.phoneNumber!
}
}
class PlaceDetails
{
override init()
{
super.init()
}
var phoneNumber : String? //viewcontroller actions give this class property its value
}
You need to assign placeDetails to your destination view controller in prepareForSegue. I know you aren't doing this as you have created placeDetails as a let constant rather than a variable so it can't ever change from the empty PlaceDetails you originally assign.
You should declare it as an optional variable and then unwrap it properly when you use it;
class ViewController: UIViewController
{
let placeDetails = PlaceDetails()
func tapLabel( sender: UITapGestureRecognizer )
{
self.performSegueWithIdentifier("showDetailsVC", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "showDetailsVC") {
let destVC = segue.destinationViewController as! DetailsViewController
destVC.placeDetails = self.placeDetails
}
}
}
class DetailsViewController: UIViewController
{
#IBOutlet weak var phoneNumberLabel : UILabel!
var placeDetails: PlaceDetails?
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
if let placeDetails = self.placeDetails {
if let phoneNumber = placeDetails.phoneNumber {
self.phoneNumberLabel.text = phoneNumber
}
}
}
}
You can't use the value in viewDidLoad as this method will execute before the property has been set; the view controller is loaded before prepareForSegue is called, so viewWillAppear is a better place.
Try to cast your phoneNumber in a string.
detailsVC.phoneNumberLabel.text = String(placeDetails.phoneNumber!)
Also, the nil value could come from the encoding method of the response of the API.
EDIT
I think you have to set the text of your UILabel in the viewDidLoad() method of your showDetailsVC. Declare a string variable in showDetailVC, and then pass your placeDetails.phoneNumber variable to the string you just declare. (Instead of directly set the text in the tapLabel() method). Then in your
showDetailVC set the text to your UILabel in the viewDidLoad()

Getting input from text field in ViewController and using in another class

In ViewController.swift, I have a text field whose input I would like to use and manipulate in my other class, Conjugate.swift. I am capturing the input at the same time the keyboard is hidden, like so:
import UIKit
class MainViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var mainTextField: UITextField!
var input: String!
override func viewDidLoad() {
super.viewDidLoad()
mainTextField.delegate = self
}
/* KEYBOARD HIDE */
func textFieldShouldReturn(textField: UITextField) -> Bool {
input = self.mainTextField.text
textField.resignFirstResponder()
return true;
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
And then in my other class:
class Conjugate {
var infinitive: String!
var isEndingAr = false
func conjugate() {
// gets verb from text field
infinitive = MainViewController().input
// checks verb ending and sets value to booleans
if (infinitive.hasSuffix("ar")) {
isEndingAr = true
}
}
}
Here is the error I'm getting:
fatal error: unexpectedly found nil while unwrapping an Optional value
I know that input in MainViewController.swift is still nil. I just don't know why. What am I doing wrong?
When you do infinitive = MainViewController().input, MainViewController() just creates a new instance of MainViewController. That new instance never calls textFieldShouldReturn and therefore its input will be nil. Instead, add a reference from Conjugate to MainViewController.
var mainViewController: MainViewController?
Then in textFieldShouldReturn create a Conjugate and assign its property:
let conjugate = Conjugate()
conjugate.mainViewController = self
Then in the conjugate() method, instead of creating a new view controller, refer to the property:
func conjugate() {
// gets verb from text field
infinitive = self.mainViewController!.input!
// checks verb ending and sets value to booleans
if (infinitive.hasSuffix("ar")) {
isEndingAr = true
}
}
}
Also, like #emresancaktar said, input should be optional, since it might be nil. However, infinitive does not have to be optional.

#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