Using Storyboard and custom View Controller init - swift

When pushing view controller programatically, one can easily do some dependency injection through the init method :
let dummyVC = DummyVC(dummyManager: DummyManager())
self.pushViewController(dummyVC, animated: true)
Using destination controller :
class DummyVC: UIViewController {
private let dummyManager: DummyManager
init(dummyManager: DummyManager) {
self.dummyManager = dummyManager
super.init(nibName: nil, bundle: nil)
}
}
The previous code is fine because it encapsulates the attribute correctly and clearly show dependencies to external APIs.
When working with Storyboards we cannot choose the init method being called (a custom init method is being called).
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let dummyVC = mainStoryboard.instantiateViewControllerWithIdentifier("DummyVC") as! DummyVC
dummyVC.dummyManager = DummyManager() // ERROR: would require dummyManager to have public scope
Is there any way to inject dependencies the same way while keeping attributes private and constants (let) ?

View controllers in storyboards are always initialised using
init?(coder aDecoder: NSCoder)
there's no way around that.
An alternative would be to have…
class DummyVC: UIViewController {
private var dummyManager: DummyManager!
func configure(dummyManager: DummyManager) {
self.dummyManager = dummyManager
}
}
and then…
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let dummyVC = mainStoryboard.instantiateViewControllerWithIdentifier("DummyVC") as! DummyVC
dummyVC.configure(dummyManager: DummyManager())
or
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.destination {
case let dummyVC as DummyVC:
dummyVC.configure(dummyManager: DummyManager())
default:
break
}
}
Whilst not perfect (using let rather than var) the property being private and an implicitly unwrapped optional means it must be set (or the app will crash on use), and that can only happen from within the containing class.
I've adopted this throughout my apps, and find it quite a nice way to ensure all properties are set. Just remember to update the configure func when a property is added to a class.

Related

Xcode Moving from Storyboard

I have a Xcode project using storyboards.
I have the typical setup,
Segue in storyboard
Call self.perform("..."....) from view controller
This then calls 'prepare(for segue: UIStoryboardSegue, sender: Any?)' allowing me to write variables in the new view controller before 'viewdidload' is called
Now the project is growing making using storyboards impractical.
We are taking a code approach loading views from code. The issue we are facing is how to pass data to new view before 'viewdidload' is called or what's the correct process to use.
We are loading view using
let nib = UINib(nibName: "TestView", bundle: nil)
let view = nib.instantiate(withOwner: self, options: nil).first as! TestViewController
view.delegate = self
view.dataModel = TestViewDataModel()
present(view, animated: true, completion: nil)
Issue we have is 'instantiate' calls viewdidload but ideally it needs data from 'datamodel'
Thanks
Add a new initializer to TestViewController that takes accepts whatever data you need as parameters.
From within it, call super.init(nibName: "TestView", bundle: nil). UIViewController's initializer will take care of finding the nib, instantiating itself from it, and settings itself as the owner.
class TestViewController: UIViewController {
let dataModel: TestViewDataModel
init(dataModel: TestViewDataModel) {
super.init(nibName: "TestView", bundle: nil)
self.dataModel = dataModel
}
//...
}

Swift: Can't use navigationController.pushViewController inside a function

I am transitioning between views programatically using the code below and it gets repeated quite a lot so I wanted to create a global function but I can't seem to be able to get the hang of it.
The code works when called inside a ViewController class, so I suppose the problem is that my function doesn't know which VC do I want to call navigationController.pushViewController on, but I don't know how to reference the VC either as an argument passed to the function, or better yet with something like .self to take the current VC class the function is called in.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ExamplesControllerVC")
self.navigationController?.pushViewController(vc, animated: true)
The error I get if I try to run that as a function in a separate file is:
Use of unresolved identifier 'navigationController'; did you mean 'UINavigationController'?
So the function I'd like to create and call is something like:
showVC("ExamplesControllerVC")
Any ideas?
Whatever function this code is in needs to be updated to take a parameter of type UIViewController.
func showMain(on vc: UIViewController) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ExamplesControllerVC")
vc.navigationController?.pushViewController(vc, animated: true)
}
Now you can call this as:
showMain(on: someViewController)
Or add this function to an extension on UIViewController then your use of self works just fine.
extension UIViewController {
func showMain() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ExamplesControllerVC")
self.navigationController?.pushViewController(vc, animated: true)
}
}
Do you want to do something like this?:
extension UIViewController {
func presentView(withIdentifier: String) {
if let newVC = self.storyboard?.instantiateViewController(withIdentifier: withIdentifier) {
self.present(newVC, animated: true, completion: nil)
}
}
}
You can call it this way:
self.presentView(withIdentifier: "yourIdentifier")

Page View Controller viewDidLoad() Issues

I'm learning Swift/Xcode and trying to create an app with three pages that you can swipe back and forth using a Page View Controller. The issues I'm having lie in the UIViewController subclass, more specifically the viewDidLoad() function. I'm getting errors like "value of type 'NameOfMyClass' has no member datasource/delegate" and "use of unresolved identifier "setViewControllers". I've followed many tutorials and checked other posts but no one seems to be having these issues. When I attempted to run this I would get a black screen, and now I get a terminated due to signal 15 error.
Here's the relevant code where the errors are popping up:
import UIKit
class RootPageViewController: UIViewController,
UIPageViewControllerDataSource, UIPageViewControllerDelegate {
lazy var viewControllerList:[UIViewController] = {
return [self.VCInstance(name: "MissionOne"),
self.VCInstance(name: "MissionTwo"),
self.VCInstance(name: "MissionThree")]
}()
private func VCInstance(name: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle:
nil).instantiateViewController(withIdentifier: name)
}
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self //...has no member 'dataSource' error
self.delegate = self //...has no member 'delegate' error
if let MissionOne = viewControllerList.first {
setViewControllers([MissionOne], direction: .forward, animated:
true, completion: nil)
//use of unresolved indentifier 'setViewControllers' error
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
The other issue I'm having, and it may be related, is that the Page View Controller is not accepting/recognizing my class file to set as the custom class in the story board.
Thanks in advance for any insight or suggestions.
your RootPageViewController is subclass of UIViewController, not a UIPageViewController
I looks to me that you are mixing two different controllers here.
What you want to do is add 3 RootPageViewController to a UIPageViewController, yet here you are trying to add 3 RootPageViewControllers to a RootPageViewController!
One way to fix it is to have a RootPageViewController and a separate UIPageViewController which will contain all RootPageViewControllers like so:
// This is a UIPageViewController containing all RootPages
class PagesViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
lazy var viewControllerList:[UIViewController] = {
return [RootPageViewController.VCInstance(name: "MissionOne"),
RootPageViewController.VCInstance(name: "MissionTwo"),
RootPageViewController.VCInstance(name: "MissionThree")]
}()
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
if let MissionOne = viewControllerList.first {
setViewControllers([MissionOne], direction: .forward, animated:
true, completion: nil)
}
}
}
And the RootPageViewController which will be added in the UIPageViewController above:
// This is a UIViewController representing a single page
class RootPageViewController: UIViewController {
static func VCInstance(name: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: name)
}
// Do what you need a page to do...
}
With such a construction you will have all your errors solved and you will also be able to use PagesViewController as custom class in your Storyboard!

Having issues setting delegate with Observer Pattern

I'm trying to realize the Observer Pattern and I'm experiencing some difficulty as my delegate doesn't seem to be setting properly.
In my Main.storyboard I have a ViewController with a container view. I also have an input box where I'm capturing numbers from a number keypad.
Here's my storyboard:
I'm trying to implement my own Observer Pattern using a protocol that looks like this:
protocol PropertyObserverDelegate {
func willChangePropertyValue(newPropertyValue:Int)
func didChangePropertyValue(oldPropertyValue:Int)
}
My main ViewController.swift
class ViewController: UIViewController {
#IBOutlet weak var numberField: UITextField!
// observer placeholder to be initialized in implementing controller
var observer : PropertyObserverDelegate?
var enteredNumber: Int = 0 {
willSet(newValue) {
print("//Two: willSet \(observer)") // nil !
observer?.willChangePropertyValue(5) // hard coded value for testing
}
didSet {
print("//Three: didSet")
observer?.didChangePropertyValue(5) // hard coded value for testing
}
}
#IBAction func numbersEntered(sender: UITextField) {
guard let inputString = numberField.text else {
return
}
guard let number : Int = Int(inputString) else {
return
}
print("//One: \(number)")
self.enteredNumber = number // fires my property observer
}
}
My ObservingViewController:
class ObservingViewController: UIViewController, PropertyObserverDelegate {
// never fires!
func willChangePropertyValue(newPropertyValue: Int) {
print("//four")
print(newPropertyValue)
}
// never fires!
func didChangePropertyValue(oldPropertyValue: Int) {
print("//five")
print(oldPropertyValue)
}
override func viewDidLoad() {
super.viewDidLoad()
print("view loads")
// attempting to set my delegate
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController") as! ViewController
print("//six \(pvc)")
pvc.observer = self
}
}
Here's what my console prints:
What's happening
As you can see when my willSet fires, my observer is nil which indicates that I have failed to set my delegate in my ObservingViewController. I thought I set my delegate using these lines:
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController") as! ViewController
print("//six \(pvc)")
pvc.observer = self
However, I must be setting my delegate incorrectly if it's coming back nil.
Question
How do I properly set my delegate?
You are calling into the storyboard to instantiate a view controller and setting it as the observer, however that instantiates a new instance of that view controller, it doesn't mean that it is referencing the one single "view controller" that is in the storyboard. ObservingViewController needs another way to reference the ViewController that has already been created.
So #Chris did reenforce my suspicions which helped me to figure out a solution for assigning my delegate to my view controller properly.
In my ObservingViewController I just need to replace the code in my viewDidLoad with the following:
override func viewDidLoad() {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let vc = app.window?.rootViewController as! ViewController
vc.observer = self
}
Rather than creating a new instance of my view controller, I'm now getting my actual view controller.

Performing a method that uses variables that come from optional

If I need to perform a method whose multiple parameters' original source are optional, is doing multiple optional binding before performing the method the cleanest way to go about this?
e.g. UIStoryboardSegue's sourceViewController and destionationViewController are both AnyObject? and I need to use source's navigationController to perform something.
override func perform() {
var svc = self.sourceViewController as? UIViewController
var dvc = self.destinationViewController as? UIViewController
if let svc = svc, dvc = dvc {
svc.navigationController?.pushViewController(dvc, animated: true)
}
}
If the view controllers are part of a designed segue in Interface Builder and you actually know that they are not nil, you can unwrap them
override func perform() {
var svc = self.sourceViewController as! UIViewController
var dvc = self.destinationViewController as! UIViewController
svc.navigationController!.pushViewController(dvc, animated: true)
}
otherwise if the source controller could be nil, the push command will only be executed if the controller is not nil, it's like sending a message to nil in Objective-C
override func perform() {
var svc = self.sourceViewController as? UIViewController
var dvc = self.destinationViewController as? UIViewController
svc.navigationController?.pushViewController(dvc, animated: true)
}
It seems unnecessary to create two vars, if you really want to be sure that the optional values are not nil you could use:
override func perform() {
if let svc = self.sourceViewController as? UIViewController,
dvc = self.destinationViewController as? UIViewController {
svc.navigationController?.pushViewController(dvc, animated: true)
}
}