Coordinator Pattern to replace UINavigationController in SplitViewController - swift

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.

Related

How to Organize my ViewScreens on Swift Programmatic UI

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

Access NSCache across view controllers in a tab view controller

I want to access NSCache from more than one place in my APP, as I'm using it to cache images from an API endpoint.
For example table view 4 and viewcontroller 6 in the diagram below use the same images, so I do not want to download them twice.
Candidate solutions:
Singleton
class Cache {
private static var sharedCache: NSCache<AnyObject, AnyObject>?
static public func getCache () -> NSCache<AnyObject, AnyObject> {
if sharedCache == nil {
self.sharedCache = NSCache()
}
return sharedCache!
}
}
Seems to work fine, but "Singletons are bad" so...
Store the cache in TabViewController
This will tightly couple the views to the view controller so...
Store in the AppDelegate somehow. But isn't this the same as 1? So...
Use dependency injection. But we're in a tab view controller, so isn't this the same as 2?
I'm not sure the right strategy here, so am asking whether there is another method that can be used here.
What I've done Created an App with an example using a NSCache, and explored a singleton solution. Ive tried to use dependency injection but think that it doesn't make sense. I've looked at Stack overflow and documentation, but for this specific circumstance I have found no potential solutoins.
What I've given A minimal example, with a diagram and tested solution that I'm dissatisfied with.
What is not helpful are answers that say NSCache is incorrect, or to use libraries. I'm trying to use NSCache for my own learning, this is not homework and I want to solve this specific instance of this problem in this App structure.
What the question is How to avoid using a singleton in this instance, view controllers in a tab view controller.
First up. Singletons are not inherantly bad. They can make your code hard to test and they do act as dependancy magnets.
Singletons are good for classes that are tools e.g NSFileManager aka FileManger, i.e something that does not carry state or data around.
A good alternative is dependancy injection but with view controllers and storyboards it can be hard and feel very boilerplate. You end up passing everything down the line in prepareForSegue.
One possible method is to declare a protocol that describes a cache like interface.
protocol CacheProtocol: class {
func doCacheThing()
}
class Cache: CacheProtocol {
func doCacheThing() {
//
}
}
Then declare a protocol that all things that wish to use this cache can use.
protocol CacheConsumer: class {
var cache: CacheProtocol? { get set }
func injectCache(to object: AnyObject)
}
extension CacheConsumer {
func injectCache(to object: AnyObject) {
if let consumer = object as? CacheConsumer {
consumer.cache = cache
}
}
}
Finally create a concrete instance of this cache at the top level.
/// Top most controller
class RootLevelViewController: UIViewController, CacheConsumer {
var cache: CacheProtocol? = Cache()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
injectCache(to: segue.destination)
}
}
You could pass the cache down the line in prepareForSegue.
Or you can use subtle sub-classing to create conformance.
class MyTabBarController: UITabBarController, CacheConsumer {
var cache: CacheProtocol?
}
Or you can use delegate methods to get the cache object broadcast downhill.
extension RootLevelViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
injectCache(to: viewController)
}
}
You now have a system where any CacheConsumer can use the cache and pass it downhill to any other object.
If you use the coordinator pattern you can save the cache in the coordinator for your navigation flow and access it from there/init with the cache. It also works nicely since when the navigation flow is removed the cache is also removed.
final class SomeCoordinator: NSObject, Coordinator {
var rootViewController: UINavigationController
var myCache = NSCache<AnyObject, AnyObject>()
override init() {
self.rootViewController = UINavigationController()
super.init()
}
func start() {
let vc = VC1(cache: myCache)
vc.coordinator = self
rootViewController.setViewControllers([vc], animated: false)
parentCoordinator?.rootViewController.present(rootViewController, animated: true)
}
func goToVC2() {
let vc = VC2(cache: myCache)
vc.coordinator = self
rootViewController.pushViewController(vc, animated: true)
}
func goToVC3() {
let vc = VC3(cache: myCache)
vc.coordinator = self
rootViewController.present(vc, animated: true)
}
func goToVC4() {
let vc = VC4(cache: myCache)
vc.coordinator = self
rootViewController.present(vc, animated: true)
}
deinit {
print("✅ Deinit SomeCoordinator")
}
}

How to access a referencing outlet from a different view controller?

I am new to Swift and Xcode. I am building an Financial Expense ios app.
In my first view controller, I created a referencing outlet for a label called expenseNum.
In my second view controller, I have a function for a button called Add Expense. When it is clicked, I need it to update the expenseNum variable with the amount of the expense.
What is the best way to go about this? I had created an object of the first view controller class and accessed it like "firstviewcontroller.expenseNum" but this will create a new instance of the class and I need it to be all the same instance so it can continuously add to the same variable. Thanks for the help!
You need a delegate
protocol SendManager {
func send(str:String)
}
In first
class FirstVc:UIViewcontroller , SendManager {
func send(str:string) {
self.expenseNum.text = str
}
}
when you present SecondVc
let sec = SecondVc()
sec.delegate = self
// present
In second
class SecondVc:UIViewcontroller {
var delegate:SendManager?
#IBAction func btnClicked(_ sender:UIButton) {
delegate?.send(str:"value")
}
}
// setting delegate
in viewDidLoad of SecondVc
if let first = self.tabBarController.viewControllers[0] as? FirstVc {
self.delegate = first
}
There are several ways you can pass data from ViewController2 to another ViewController1
The best way here is Protocol Delegates
Please follow below steps to pass data
In Your SecondViewController from where you want to send data back declare a protocol at the top of class declaration
protocol SendDataBack: class {
func sendDataFromSecondVCtoFirstVC(myValue: String)
}
Now in the class , declare a object of your protocol in same ViewController
weak var myDelegateObj: SendDataBack?
And now in your Add Expense button action just call the delegate method
myDelegateObj?.sendDataFromSecondVCtoFirstVC(myValue: yourValue)
Now go to your first ViewController
the place from where you have pushed/present to SecondViewController you must have taken the object of SecondVC to push to push from first
if let secondVC = (UIStoryboard.init(name: "Main", bundle: nil)).instantiateViewController(withIdentifier: "secondVCID") as? SecondViewController {
vc?.myDelegateObj = self
self.navigationController?.pushViewController(secondVC, animated: true)
**OR**
self.present(secondVC, animated: true, completion: nil)
}
now in your FirstViewController make an extension of FirstViewVC
extension FirstViewController: SendDataBack {
func sendDataFromSecondVCtoFirstVC(myValue: String) {
}
}
I think you can make a variable in your properties in second ViewController (before viewDidLoad method)
var delegate: FirstViewController? = nil
and use from the properties of the first view controller anywhere of the second view controller.
delegate!.mainTableView.alpha=1.0
//for example access to a tableView in first view controller
The simplest way to achieve this is to use a public var. Add a new Swift file to your project, call it Globals. Declare the public variable in Globals.swift like so:
public var theValue: Int = 0
Set its required value in the first ViewController, and you'll find you can read it in the second with ease.

make variable available to first tab when var is defined in second tab

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

Swift – Using popViewController and passing data to the ViewController you're returning to

I have an optional bool variable called showSettings on my first view controller which is called ViewController, and I'm popping from SecondViewController back to ViewController.
Before I pop, I want to set the bool to true. Seems wrong to instantiate another view controller since ViewController is in memory.
What's the best way to do this? I'm not using storyboards, if that's important for your answer.
Thanks for your help
So I figured it out, based mostly from this post – http://makeapppie.com/2014/09/15/swift-swift-programmatic-navigation-view-controllers-in-swift/
In SecondViewController, above the class declaration, add this code:
protocol SecondVCDelegate {
func didFinishSecondVC(controller: SecondViewController)
}
Then inside of SecondViewContoller add a class variable:
var delegate: MeditationVCDelegate! = nil
Then inside of your function that your button targets, add this:
self.navigationController?.popViewControllerAnimated(true)
delegate.didFinishSecondVC(self)
What we're doing here is doing the pop in SecondViewController, and not passing any data, but since we've defined a protocol, we're going to use that in ViewController to handle the data.
So next, in ViewController, add the protocol you defined in SecondViewController to the list of classes ViewController inherits from:
class ViewController: UIViewController, SecondVCDelegate { ... your code... }
You'll need to add the function we defined in the new protocol in order to make the compiler happy. Inside of ViewController's class, add this:
func didFinishSecondVC(controller: SecondViewController) {
self.myBoolVar = true
controller.navigationController?.popViewControllerAnimated(true)
}
In SecondViewController where we're calling didFinishSecondVC, we're calling this method inside of the ViewController class, the controller we're popping to. It's similar to if we wrote this code inside of SecondViewController but we've written it inside of ViewController and we're using a delegate to manage the messaging between the two.
Finally, in ViewController, in the function we're targeting to push to SecondViewController, add this code:
let secondVC = secondViewController()
secondVC.delegate = self
self.navigationController?.pushViewController(secondVC, animated: true)
That's it! You should be all set to pass code between two view controllers without using storyboards!
_ = self.navigationController?.popViewController(animated: true)
let previousViewController = self.navigationController?.viewControllers.last as! PreviousViewController
previousViewController.PropertyOrMethod
I came across this while looking for a way to do it. Since I use Storyboards more often, I found that I can get the array of controllers in the navigation stack, get the one just before the current one that's on top, check to see if it's my delegate, and if so, cast it as the delegate, set my methods, then pop myself from the stack. Although the code is in ObjC, it should be easily translatable to swift:
// we need to get the previous view controller
NSArray *array = self.navigationController.viewControllers;
if ( array.count > 1) {
UIViewController *controller = [array objectAtIndex:(array.count - 2)];
if ( [controller conformsToProtocol:#protocol(GenreSelectionDelegate)]) {
id<GenreSelectionDelegate> genreDelegate = (id<GenreSelectionDelegate>)controller;
[genreDelegate setGenre:_selectedGenre];
}
[self.navigationController popViewControllerAnimated:YES];
}
Expanding upon the answer by Abdul Baseer Khan:
For cases where the current view controller may have been loaded by different types of previous view controller, we can use the safer as? call instead of as!, which will return nil if the controller is not what we were looking for:
let previousVC = self.navigationController?.viewControllers.last as? AnExampleController
previousVC?.doSomething()
Although, you would need to repeat that for each different view controller that could load the current view controller.
So, you may want to, instead, implement a protocol to be assigned to all the possible previous view controllers:
protocol PreviousController: UIViewController {
func doSomething()
}
class AnExampleController: UIViewController, PreviousController {
// ...
func doSomething() {}
}
class AnotherController: UIViewController, PreviousController {
// ...
func doSomething() {}
}
class CurrentController: UIViewController {
// ...
func goBack() {
let previousVC = self.navigationController?.viewControllers.last as? PreviousController
previousVC?.doSomething()
}
}