I'm learning Programmatic UI and am a little bit obsessed with clean code.
I'm currently building a TabBarVC so that I can manage all of my VC's but I get an error message while doing this.
import UIKit
class MainTabBarVC: UITabBarController {
let firstVC = FirstVC()
let secondVC = SecondVC()
let firstNavVC = UINavigationController(rootViewController: firstVC) // Cannot use instance member 'firstVC' within property initializer; property initializers run before 'self' is available
let secondNavVC = UINavigationController(rootViewController: secondVC) // Cannot use instance member 'secondVC' within property initializer; property initializers run before 'self' is available
let viewControllers = [firstNavVC, secondNavVC]
override func viewDidLoad() {
super.viewDidLoad()
self.setupViews()
}
func setupViews() {
// Nav Configs
self.firstVC.view.backgroundColor = .systemBackground
self.firstVC.navigationItem.title = "First Nav"
self.secondVC.view.backgroundColor = .systemBackground
self.secondVC.navigationItem.title = "Second Nav"
// Tab Configs
self.firstNavVC.tabBarItem.title = "First TAB"
self.secondNavVC.tabBarItem.title = "Second TAB"
}
}
I know if I put firtNavVC, secondNavVC, and viewcontrollers inside the setupViews it is gonna work but I don't like it when one function has too many lines of codes especially when it gets bigger.
So except for my question, are there any extension or enum functions that I can easily manage all of my UINavigationController, UITabBarController, and UIViewController such as enumeration that can call the function whenever I need to call or add a new VC.
You could change your lets into lazy vars.
class MainTabBarVC: UITabBarController {
lazy var firstVC = FirstVC()
lazy var secondVC = SecondVC()
lazy var firstNavVC = UINavigationController(rootViewController: firstVC)
lazy var secondNavVC = UINavigationController(rootViewController: secondVC)
lazy var viewControllers = [firstNavVC, secondNavVC]
override func viewDidLoad() {
super.viewDidLoad()
self.setupViews()
}
However, I think your impulse to maintain instance property references to all these view controllers is mistaken. What I would do is just move the lets into setupViews. You don't need a permanent reference to any of these objects; you just need to create them, configure them, and assign the view controllers as children of your tab bar controller (your code is missing that crucial step, by the way).
Related
I am implementing coordinator pattern to handle navigation in my app. In theory when users choose different category I want to set the splitViewController to replace the existing navigationController for that category by a new one.
When app starts the coordinator operate as expected, and when I pop or push on the same navigationController implemented at start also works fine, my only problem is when I try to replace the whole navigationController of the splitviewcontroller.
ISSUE: adding new navigationController is not displayed to the user
here is my implementation.
class Coordinator: Navigable, DataCommunicator{
//MARK: - Navigable Conformable
typealias UIController = SplitController
var viewController: UIController
var childCoordinators: [Coordinatable] = []
//MARK: - Root Custom setup
weak var parentCoordinator: RootCoordinator?
//MARK: - Init
init(viewController: UIController) {
self.viewController = viewController
}
func start() {
let categoryNavigationController = CategoryNavigationController()
let categoryNavigationCoordinator = CategoryNavigationCoordinator(viewController: noteNavigationController)
categoryNavigationCoordinator.start()
childCoordinators.append(categoryNavigationCoordinator)
categoryNavigationController.coordinator = self
viewController.viewControllers = [categoryNavigationController]
}
func startSearchCategory() {
childCoordinators.removeLast()
viewController.navigationController?.popToRootViewController(animated: false)
viewController.viewControllers.removeLast()
let searchNavigationController = SearchNavigationController()
let searchCoordinator = SearchNavigationCoordinator(viewController:searchNavigationController)
searchCoordinator.start()
childCoordinators.append(searchCoordinator)
searchNavigationController.coordinator = self
searchCoordinator.parentCoordinator = self
viewController.viewControllers = [searchNavigationController]
}
}
Update:
I think I reached the desired behavior with a different approach, still I am curious why I can't display different navigationController for the masterController in the UISplitViewController and display it.
But my approach helped my code to be more modular. I added in my Coordinator protocol the following function
func stopChild<T: Coordinatable>(coordinator: T, callback: CoordinatorCallBack?)
and implemented the function as the following:
override func stopChild<T>(coordinator: T, callback: CoordinatorCallBack?) where T : Coordinatable {
childCoordinators = childCoordinators.filter({$0 !== coordinator})
// Calling parent to stop the child coordinator to roll back to the rootController
parentCoordinator?.stopChild(coordinator: self, callback: nil)
}
Rolling back helped me to instantiate the full stack I desire without trying to add custom modifying code for the splitViewController, instead I am replacing the whole splitViewController with the one corresponding to the module I am working with, which is prettier for generic use.
Since in my call back I can send to the root coordinator the desired module the user will be interested in next.
I've been looking through a Coordinator tutorial and it brought up a problem with code I've written in the past.
Namely, when reusing a view controller I've used a property to be able to display different elements depending on which view controller the user arrived from. This is described in the above tutorial as a hack.
For example I segue to labelviewcontroller using
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "label" {
let vc = segue.destination as! LabelViewController
vc.originalVC = self
}
}
and then on labelViewController have a property
var originalVC: ViewController?
which I then change the items in viewDidLoad() through
override func viewDidLoad() {
super.viewDidLoad()
if originalVC != nil {
label.text = "came direct"
imageView.isHidden = true
}
else {
label.text = "button"
imageView.isHidden = false
}
}
I've a working example project here: https://github.com/stevencurtis/ReusibilityIssues
Now, I know the answer might be use the Coordinator tutorial, but is there any other method that I can use to simple reuse a viewController for two different circumstances, rather than using a property or is there anyway to clean this up to be acceptable practice?
You can do that without passing originalVC just by checking parent type if you are pushing it inside a navigation controller like this :
if let p = parent {
if p.isKind(of: OriginalViewController.self){
//it pushed in navigation controller stack after OriginalViewController
}
}
but is there any other method that I can use to simple reuse a viewController for two different circumstances
If the "two different circumstances" you describe are very different (by this I mean "require very different lines of code to be run"), then you should create two different view controller classes, because otherwise you would be violating the Single Responsibility Principle.
If your "two different circumstances" are different, but also quite related, then you can just have all the information that the VC needs to know as properties. You certainly don't need a whole ViewController.
For example, if your LabelViewController will show a "foo" button only if it is presented by ViewControllerFoo.
You can add a showFooButton property in LabelViewController:
var showFooButton = false
override func viewDidLoad() {
fooButton.isHidden = !showFooButton
}
And then in ViewControllerFoo.prepareForSegue:
if segue.identifier == "label" {
let vc = segue.destination as! LabelViewController
vc.showFooButton = true
}
I wouldn't call this a hack. This is the recommenced way described in this post and they didn't call it a hack.
In my AppDelegate's applicationDidFinishLaunching I need to create an object using data read from disk, and then pass this object to the initial view controller for display. What would be the best way to do this?
Right now I'm loading the storyboard programatically like so:
func applicationDidFinishLaunching(_ aNotification: Notification) {
importantThing = ImportantThing()
importantThing.load(url: URL(fileURLWithPath: "..."))
let storyboard = NSStoryboard(name: "Main", bundle: nil)
myWindowController = storyboard.instantiateController(withIdentifier: "MyWindowController") as! NSWindowController
(myWindowController.contentViewController as? MyViewController)?.importantThing = importantThing
myWindowController.showWindow(self)
}
But this feels clunky. For one, the property is being set after viewDidLoad, so now view setup is weird.
There must be a better way to do this. If possible, I would like to not resort to using a singleton, because I actually need to set up a few interconnected objects (two objects with important state that have references to each other, but it doesn't make sense for either to contain the other). What would be a good way to solve this?
What you're doing in the app delegate is correct. As for what you should do in the view controller, Apple's Master-Detail app template shows you the correct pattern (I've added a few comments):
// the interface
#IBOutlet weak var detailDescriptionLabel: UILabel!
// the property
var detailItem: NSDate? {
didSet {
self.configureView()
}
}
func configureView() {
// check _both_ the property _and_ the interface
if let detail = self.detailItem { // property set?
if let label = self.detailDescriptionLabel { // interface exists?
label.text = detail.description
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// at this point, its _certain_ that the interface exists
self.configureView()
}
If you think about it, you'll see that the interface is updated correctly regardless of the order of events — that is, regardless of whether viewDidLoad or the setting of the property comes first. Just follow that pattern.
When I define a variable in the first tab I can then make it available in the usual way to the other tabs. But what if I define a variable in the third tab and I want it to be available in the first tab? Here is the usual set up for the variable in the first tab:
class AddCardViewController: UIViewController {
var cards = [Card]()
override func viewDidLoad() {
super.viewDidLoad()
cards = [1,2,3,4]
let barViewController = self.tabBarController?.viewControllers
let svc1 = barViewController![1] as! LearningViewController
svc1.cards = self.cards
}
in the second viewController I can then easily use the cards array
class LearningViewController: UIViewController {
var cards = [Card]()
override func viewDidLoad() {
super.viewDidLoad()
print(cards) ////works all fine
Now I want to define a variable in the second tab and make it available to the first tab. So, the other way round. I tried to use the same setup, but when I start the app it crashes because the variable is nil in the first tab - makes sense. Is there any way to do this or do I just have to define all variables in the first viewController?
I think the easiest solution is to force the second tab to load before calling the variable in question. The most brute force way to do this is to create a func in AddCardViewController that loads all the tabs in the tabBarController e.g.
func loadAllTabs() {
if let viewControllers = self.tabBarController?.viewControllers {
for viewController in viewControllers {
viewController.view
}
}
}
and call it in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
loadAllTabs()
}
I know that this has to be a simple fix, but can't seem to understand why my code is not working. Basically I am trying to send a value from a text field in 1 view to a 2nd view's label.
ViewController.swift
#IBOutlet var Text1st: UITextField
#IBAction func Goto2ndView(sender: AnyObject) {
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2
//view2.Label2nd.text=text;
self.navigationController.pushViewController(view2, animated: true)
}
MyView2.swift
#IBOutlet var Label2nd: UILabel
override func viewDidLoad() {
super.viewDidLoad()
var VC = ViewController()
var string = (VC.Text1st.text) //it doesn't like this, I get a 'Can't unwrap Option.. error'
println(string)
}
-------EDITED UPDATED CODE FROM (drewag)-------
ViewController.swift
let text = "text"
var sendString = Text1st.text
println(sendString) //successfully print it out.
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2
view2.Label2nd.text=sendString;
self.navigationController.pushViewController(view2, animated: true)
MyView2.swift
#IBOutlet var Label2nd: UILabel
override func viewDidLoad() {
super.viewDidLoad()
var VC = ViewController()
var string = self.Label2nd.text
println(string) //still getting the error of an unwrap optional.none
}
var VC = ViewController() creates a new instance of ViewController. Unless there is a default value, you are not going to get any value out of VC.Text1st.text. You really should use a string variable on your second view controller to pass the data to it.
Also, a note on common formatting:
Class names should start with a capital letter (as you have)
Method / function names should start with a lower case letter
UIViewController subclasses should have "Controller" included in their name, otherwise, it looks like it is a subclass of UIView which is an entirely different level of Model View Controller (the architecture of all UIKit and Cocoa frameworks)
Edit:
Here is some example code:
class ViewController1 : UIViewController {
...
func goToSecondView() {
var viewController = ViewController2()
viewController.myString = "Some String"
self.navigationController.pushViewController(viewController, animated: true)
}
}
class ViewController2 : UIViewController {
var myString : String?
func methodToUseMyString() {
if let string = self.myString {
println(string)
}
}
...
}
Note, I am not creating ViewController2 using a storyboard. I personally prefer avoiding storyboards because they don't scale well and I find editing them to be very cumbersome. You can of course change it to create the view controller out of the storyboard if you prefer.
jatoben is correct that you want to use optional binding. IBOutlets are automatically optionals so you should check the textfield to see if it is nil.
if let textField = VC.Text1st {
println(textField.text)
}
This should prevent your app from crashing, but it will not print out anything because your text field has not yet been initialized.
Edit:
If you want to have a reference to your initial ViewController inside your second you're going to have to change a few things. First add a property on your second viewcontroller that will be for the first view controller:
#IBOutlet var Label2nd: UILabel //existing code
var firstVC: ViewController? //new
Then after you create view2, set it's firstVC as the ViewController you are currently in:
let view2 = self.storyboard.instantiateViewControllerWithIdentifier("view2") as MyView2 //already in your code
view2.firstVC = self //new
Finally in your viewDidLoad in your second view controller, use firstVC instead of the ViewController you recreated. It will look something like this:
override func viewDidLoad() {
super.viewDidLoad()
if let textField = firstVC?.Text2nd {
println(textField.text)
}
}
Use optional binding to unwrap the property:
if let string = VC.Text1st.text {
println(string)
}