How to properly implement Navigator pattern - swift

I am following John Sundell's post to implement a Navigator pattern (https://www.swiftbysundell.com/posts/navigation-in-swift). The basic idea is that, in contrast to Coordinator pattern, each view controller could simply call navigator.navigate(to: .someScreen) without having to know other view controllers.
My question is that, since in order to construct a view controller I need a navigator, to construct a navigator I need a navigation controller, but I want to make the view controller the root of the navigation controller, what's the best way to resolve this circular dependency in a way that respects the best practices of dependency injection?
Below is the idea of Navigator pattern as illustrated by Sundell
Navigator
protocol Navigator {
associatedtype Destination
func navigate(to destination: Destination)
}
class LoginNavigator: Navigator {
enum Destination {
case loginCompleted(user: User)
case signup
}
private weak var navigationController: UINavigationController?
private let viewControllerFactory: LoginViewControllerFactory
init(navigationController: UINavigationController,
viewControllerFactory: LoginViewControllerFactory) {
self.navigationController = navigationController
self.viewControllerFactory = viewControllerFactory
}
func navigate(to destination: Destination) {
let viewController = makeViewController(for: destination)
navigationController?.pushViewController(viewController, animated: true)
}
private func makeViewController(for destination: Destination) -> UIViewController {
switch destination {
case .loginCompleted(let user):
return viewControllerFactory.makeWelcomeViewController(forUser: user)
case .signup:
return viewControllerFactory.makeSignUpViewController()
}
}
}
View Controller
class LoginViewController: UIViewController {
private let navigator: LoginNavigator
init(navigator: LoginNavigator) {
self.navigator = navigator
super.init(nibName: nil, bundle: nil)
}
private func handleLoginButtonTap() {
navigator.navigate(to: .loginCompleted(user: user))
}
private func handleSignUpButtonTap() {
navigator.navigate(to: .signup)
}
}
Now in AppDelegate I want to do something like
let factory = LoginViewControllerFactory()
let loginViewController = factory.makeLoginViewController()
let rootNavigationController = UINavigationController(rootViewController: loginViewController)
window?.rootViewController = rootNavigationController
But I somehow have to pass the rootNavigationController into the factory in order for the loginViewController to be properly constructed right? Because it needs a navigator, which needs the navigation controller. How to do that?

I also was recently trying to implement Sundell's Navigator pattern and ran into this same circular dependency. I had to add some additional behavior to the initial Navigator to handle this odd bootstrap issue. I believe subsequent Navigators in your app can perfectly follow the blog's suggestion.
Here is the new initial Navigator code using JGuo's (the OP) example:
class LoginNavigator: Navigator {
enum Destination {
case loginCompleted(user: User)
case signup
}
private var navigationController: UINavigationController?
// This ^ doesn't need to be weak, as we will instantiate it here.
private let viewControllerFactory: LoginViewControllerFactory
// New:
private let appWindow: UIWindow?
private var isBootstrapped = false
// We will use this ^ to know whether or not to set the root VC
init(appWindow: UIWindow?, // Pass in your app's UIWindow from the AppDelegate
viewControllerFactory: LoginViewControllerFactory) {
self.appWindow = appWindow
self.viewControllerFactory = viewControllerFactory
}
func navigate(to destination: Destination) {
let viewController = makeViewController(for: destination)
// We'll either call bootstrap or push depending on
// if this is the first time we've launched the app, indicated by isBootstrapped
if self.isBootstrapped {
self.pushViewController(viewController)
} else {
bootstrap(rootViewController: viewController)
self.isBootstrapped = true
}
}
private func makeViewController(for destination: Destination) -> UIViewController {
switch destination {
case .loginCompleted(let user):
return viewControllerFactory.makeWelcomeViewController(forUser: user)
case .signup:
return viewControllerFactory.makeSignUpViewController()
}
}
// Add these two new helper functions below:
private func bootstrap(rootViewController: UIViewController) {
self.navigationController = UINavigationController(rootViewController: rootViewController)
self.appWindow?.rootViewController = self.navigationController
}
private func pushViewController(_ viewController: UIViewController) {
// Setup navigation look & feel appropriate to your app design...
navigationController?.setNavigationBarHidden(true, animated: false)
self.navigationController?.pushViewController(viewController, animated: true)
}
}
And inside the AppDelegate now:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let factory = LoginViewControllerFactory()
let loginViewController = factory.makeLoginViewController()
loginViewController.navigate(to: .signup) // <- Ideally we wouldn't need to signup on app launch always, but this is the basic idea.
window?.makeKeyAndVisible()
return true
}
...
}

Does this solve it? in AppDelegate:
let factory = LoginViewControllerFactory()
let navController = UINavigationController()
let loginNavigator = LoginNavigator(navigationController: navController, viewControllerFactory: factory)
loginNavigator.navigate(to: .signup) // The example doesn't have a .login Destination, but it can easily be added to the factory, so using .signup instead
window?.rootViewController = navController

Instead of having the rootViewController as a property of the LoginViewControllerFactory, I would suggest to pass it as an argument when calling the 'make' functions:
return viewControllerFactory.makeWelcomeViewController(forUser: user, with: rootViewController)

Related

How to pass data properly when two VC is on same screen

In my app, it has two ViewControllers on below same screen.
ViewControllerA is an UIViewController, which includes a tableView.
ViewControllerB is a containerView, which is on top of UITabBarController(root VC) as a child.
For now, I load data(songs array, index, etc.) in viewWillAppear of ViewControllerA, that would be in background queue. Meanwhile, ViewControllerB requires the same data when ViewControllerA's data loading is complete.
Currently, I'm using Dependency Injection to keep a unique data source. Declare modelController reference in ViewController A and B, and use getter and setter to binding the data changes.
The problem is the songs array fetched from modelController in ViewControllerB is nil. Because ViewControllerA will set data on modelController when loading is completed. And ViewControllerB will use it from modelController in its viewWillAppear. But these two VC are on the same screen, how could I guaranty which one will go first? Also data loading is in background queue, so it is suppose to be delayed. Then if ViewControllerB needs that data in first place, it can't be.
My current thought is to load data on ViewControllerB too, so it will not rely on ViewControllerA's result. Or add some observer/listener to message ViewControllerB when data is ready. I think it is kind of architecture stuff, I need advices from experienced guys, any hints are appreciated!
Create and inject the ModelController
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
guard let rootViewController = window?.rootViewController as? TabBarController else {
fatalError("Unexpected Root View Controller")
}
// create dependency injection here
rootViewController.modelController = ModelController()
AudioManager.shared.modelController = rootViewController.modelController
}
ModelController
enum PlayMode: String {
case cycle = "repeat"
case cycleOne = "repeat.1"
case shuffle = "shuffle"
}
class ModelController {
var songs: [Song]? = nil
var position: Int = 0
var playMode = PlayMode.cycle
}
Getter and Setter
var modelController: ModelController!
var songs: [Song]?
{
get { modelController.songs }
set { modelController.songs = newValue }
}
var position: Int
{
get { modelController.position }
set { modelController.position = newValue }
}
Load data in ViewControllerA.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
loadData()
}
func loadData() {
self.showSpinner()
songs?.removeAll()
DispatchQueue.global(qos: .background).async { [weak self] in
self?.songs = DataFetcher.shared.fetchMetaData().0
DispatchQueue.main.async {
print("data load complete")
self?.table.reloadData()
self?.removeSpinner()
}
}
}
so what you said is also possible
observer/listener to message ViewControllerB when data is ready
You can have a shared data class which both the vcA and vcB use. For example
class SharedData {
static let shared = SharedData()
// Use didLoad as a flag for both ViewControllers
var didLoad: Bool {
return self.data != nil
}
var data: SomeData!
private init() {
fetchData()
}
}
In this answer there is a function called fetchData which is in the same class. You can invoke the fetchData function in the background, and use the didLoad or data as flags for displaying/calling any other function you require. This class is also a singleton meaning there will be one instance of this class throughout your applications - So you can use this anywhere in your application.
This is merely an example. You can extend this to your needs such as adding a delegate to listen for callbacks when data is loaded so you can do something in both VCs.
Or you can simply use a protocol from vcA (assuming vcA triggers the network call) and once the data is loaded you can pass it to vcB

Pass data from tabBarController to uiviewcontroller embedded in navigationcontroller

I have a custom tab bar controller. each tab contains a viewcontroller embedded in a navigation controller. I get a value for pro_user from the database in appledelegate and set it there. then before CustomTabBarController gets launched (in appledelegate), i set it's "pro_user" property to true or false (this works and CustomTabBarController receives the value from appledelegate).
Now i'm trying to pass this same value to the ViewControllers (ViewController1 and ViewController2). each view controller also has a "pro_user" property.I'm doing this by creating the viewcontroller instances and then setting their pro_user property before embedding each viewcontroller in a navigationcontroller. but neither viewcontroller is actually receiving the value of pro_user which i'm setting in CustomTabBarController. I hope this is clear. How do i pass the value of pro_user from CustomTabBarController to each of the view controllers? programmatically (i'm not using storyboards)
class AppDelegate: UIResponder, UIApplicationDelegate {
var pro_user = true
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame:UIScreen.main.bounds)
window?.makeKeyAndVisible()
let customTabBarController = CustomTabBarController()
customTabBarontroller.pro_user = pro_user
self.window?.rootViewController = customTabBarController
return true
}
}
class CustomTabBarController:UITabBarController{
var pro_user : Bool?
override func viewDidLoad(){
super.viewDidLoad()
let viewController1 = ViewController1()
viewController1.pro_user = pro_user //doesn't work
let firstNavigationController = UINavigationController(rootViewController: viewController1)
let viewController2 = ViewController2()
viewController2.pro_user = pro_user //doesn't work
let secondNavigationController = UINavigationController(rootViewController:viewController2)
viewControllers=[firstNavigationController,secondNavigationController]
}
It looks like you're setting a global setting. If so you may want to consider using UserDefaults.
E.g. with a nice extension:
extension UserDefaults {
var isProUser: Bool {
get {
return bool(forKey: "isProUser")
}
set {
set(newValue, forKey: "isProUser")
}
}
}
Then anywhere in your app you can set it:
UserDefaults.standard.isProUser = true
And get it:
let isProUser = UserDefaults.standard.isProUser
The value is also saved between launches.

Pass data using TabBarController

I know this question has been ask a lot on stack overflow. So I have a TabBarController that has 2 NavigationController, which both NavigationController have a TableViewController. I am using firebase to get a user, saving the user into a variable called currentUser. Now my problem starts here, I want to set the 2nd Navigation/Tableview controller title to the user's name. I know how to pass data using the prepare for segue, however there is no segue in TabBarController.
I've found a solution, not sure if its good or bad. What I did was make the first controller to be the delegate of the tab bar. Then I added tabBarController did select method. Here is the code.
class FirstTableVC: UITableViewController, UITabBarControllerDelegate {
var currentUser: User?
override func viewDidLoad() {
super.viewDidLoad()
tabBarController?.delegate = self
}
//Code that saves user
func code() {
...
...
...
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if viewController == tabBarController.viewControllers![1] {
let navController = tabBarController.viewControllers![1] as? UINavigationController
let secondTableVC = navController?.topViewController as! SecondTableVC
secondTableVC.currentUser = currentUser
}
}
}
class SecondTableVC: UITableViewController {
var currentUser: User?
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title =currentUser?.name
}
}
This works but not sure if this is a good way to do. I was wondering if there is a better way or a more efficient way. Thanks :)
Added:
Okay read this article about passing data using tabController. The author says that we should pass data using the state of the app. I am not really sure what he means by this. This is what I though he meant.
Example code:
class Person {
var name: String
var email: String
static var currentPerson: Person?
init(name: String, email: String) {
self.name = name
self.email = email
}
}
Can some please help me clarify . Thanks.
There's nothing wrong with this solution. Another way would be to have an abstracted class responsible for the user login object (instead of FirstTableVC having responsibility) that is accessible from both FirstTableVC and SecondTableVC
Add this in your FirstTableVC
func code() {
...
...
...
self.changeTabbarTitle()
}
func changeTabbarTitle() {
if let items = self.tabBarController?.tabBar.items {
items[1].title = currentUser?.name
}
}
You can use with delegate protocol
create NavigationTitle Protocol in FirstViewController
protocol NavigationTitle{
func setTitle(name:String)
}
class FirstViewController: UITableViewController,UITabBarControllerDelegate{
var delegate: NavigationTitle?
func setCurrentUser(){
delegate.setTitle(name:self.currentUser?.name)
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if viewController == tabBarController.viewControllers![1] {
let navController = tabBarController.viewControllers![1] as? UINavigationController
let secondTableVC = navController?.topViewController as! SecondTableVC
self.delegate = secondTableVC.self
}
}
}
implement protocol in SecondVC
class SecondVC: UITableViewController,NavigationTitle{
func setTitle(name:String){
navigationItem.title = name
}
}

Swift - How to use a closure to fire a function in a view model?

I am watching the video series
Swift Talk #5
Connecting View Controllers
url: https://talk.objc.io/episodes/S01E05-connecting-view-controllers
In this video series they remove all the prepareForSegue and use an App class to handle the connection between different view controllers.
I want to replicate this, but specifically only in my current view model; but what I don't get is how to connect view controllers through a view model (or even if you're meant to)
In their code, at github: https://github.com/objcio/S01E05-connecting-view-controllers/blob/master/Example/AppDelegate.swift
They use do this within their view controller
var didSelect: (Episode) -> () = { _ in }
This runs;
func showEpisode(episode: Episode) {
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
detailVC.episode = episode
navigationController.pushViewController(detailVC, animated: true)
}
In the same way, I want to use my ViewController to use my ViewModel for a menu button press (relying on tag).
My code follows;
struct MainMenuViewModel {
enum MainMenuTag: Int {
case newGameTag = 0
}
func menuButtonPressed(tag: Int) {
guard let tagSelected = MainMenuTag.init(rawValue: tag) else {
return
}
switch tagSelected {
case .newGameTag:
print ("Pressed new game btn")
break
}
}
func menuBtnDidPress(tag: Int) {
print ("You pressed: \(tag)")
// Do a switch here
// Go to the next view controller? Should the view model even know about navigation controllers, pushing, etc?
}
}
class MainMenuViewController: UIViewController {
#IBOutlet var mainMenuBtnOutletCollection: [UIButton]!
var didSelect: (Int) -> () = { _ in }
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func mainMenuBtnPressed(_ sender: UIButton) {
let tag = (sender).tag
self.didSelect(tag)
}
}
What I don't understand is how do I connect the command
self.didSelect(tag)
to the function
func menuButtonPressed(tag: Int)
within my ViewModel
As I understand it, according to the swift talk video is that the idea is that the view controller are "plain" and that the view model handles all the major stuff, like menu button presses and then moving to different view controllers as necessary.
How do I connect the didSelect item to my viewModel function?
Thank you.
You should set didSelect property for your controller like here:
func showEpisode(episode: Episode) {
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
detailVC.episode = episode
detailVC.didSelect = { episode in
// do whatever you need
// for example dismiss detailVC
self.navigationController.popViewController(animated: true)
// or call the model methods
self.model.menuButtonPressed(episode)
}
navigationController.pushViewController(detailVC, animated: true)
}

Update UITabBarController bar item from NSObject class

I have NSObject class listening for a specific event from my server.
When this specific event happens, I would like to update the badge value of an existing tabBar item from my UITabBarController called TabBarController.
How can I access it from the NSObject class?
Below is the NSOBject class listening for the event.
The function connectedToSocketIo() is launched when the application is launched.
The print("Event is working") is displayed in the terminal so everything is working.
The only thing I need now is to be able to update the badge of a specific bar item.
import Foundation
import UIKit
import SwiftyJSON
class SocketIOManager: NSObject{
func connectedToSocketIo(){
socket.on("post-channel:App\\Events\\contact\\newContactRequest"){ (data, ack) -> Void in
let json = JSON(data)
if json[0]["id"].string! == self.defaults.stringForKey("user_id")! {
print("event is working")
// I want to change the tab bar item badge here
} else {
print("no event")
}
}
}
}
You should try to get a reference to the UITabBarController in your SocketIOManager class. Once you have a reference the tab bar controller you can change the badge value of the desired UITabBarItem.
import Foundation
import UIKit
import SwiftyJSON
class SocketIOManager: NSObject {
/*
var tabBarController: UITabBarController!
*/
// When the tabBarController gets set the connectedToSocketIO function gets automatically called.
var tabBarController: UITabBarController! {
didSet {
connectedToSocketIO()
}
}
init() {
super.init()
}
// Either call this function
init(tabBarController: UITabBarController) {
super.init()
self.tabBarController = tabBarController
connectedToSocketIO()
}
// Or create a setter
func setTabBarController(tabBarController: UITabBarController) {
self.tabBarController = tabBarController
}
func connectedToSocketIo() {
socket.on("post-channel:App\\Events\\contact\\newContactRequest"){ (data, ack) -> Void in
let json = JSON(data)
if json[0]["id"].string! == self.defaults.stringForKey("user_id")! {
print("event is working")
// Set the desired tab bar item to a given value
tabBarController!.tabBar.items![0].badgeValue = "1"
} else {
print("no event")
}
}
}
}
EDIT
class CustomTabBarController: UITabBarController {
var socketIOManager: SocketIOManager!
viewDidLoad() {
super.viewDidLoad()
socketIOManager = SocketIOManager(tabBarController: self)
}
}
Hope this helps!
#Jessy Naus
I removed:
the socket connection from the app delegate,
the override init function inside the socketIOManager so the init(UITabBarController)
and added the socket.connect() (from socket.io library) function inside the init function linked to the tab bar controller as follow:
init(tabBarController: UITabBarController) {
super.init()
self.tabBarController = tabBarController
socket.connect()
self.listeningToSocketEvent()
}
I have replaced "self.connectedToSocketIo()" by "listeningToSocketEvent()" has the meaning of this function is more clear.
All together following your instructions mentioned above = Works perfectly. So I put your answer as the good one.
Really not easy concept. Will still need some time to assimilate it and apply it to other components of the UI.
Thanks a lot for your help on this!
actually, I found another way which avoid touching my socket.io instance.
Source:
Swift 2: How to Load UITabBarController after Showing LoginViewController
I just make the link with my tab bar controller as follow:
In my SocketIOManager
//"MainTabBarController" being the UITabBarController identifier I have set in the storyboard.
//TabBarController being my custom UITabBarController class.
// receivedNotification() being a method defined in my custom TabBarController class
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController: TabBarController = mainStoryboard.instantiateViewControllerWithIdentifier("MainTabBarController") as! TabBarController
tabBarController.receivedNotification(1)
In my TabBarController class:
func receivedNotification(barItem: Int){
if let actualValue = self.tabBar.items![barItem].badgeValue {
let currentValue = Int(actualValue)
self.tabBar.items![barItem].badgeValue = String(currentValue! + 1)
} else {
self.tabBar.items![barItem].badgeValue = "1"
}
// Reload tab bar item
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController = self
}