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

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

Related

Swift - How to pass data from VC A to B after VC B is presented?

I have 2 ViewControllers, A and B. ViewController A uses sockets and updates its data whenever changes occur.
How can I pass those updated data to ViewController B when it's already presented (programmatically) by A?
I'm thinking to pass ViewController A's update handler class to ViewController B and take advantage of the fact that classes are reference-type, so any change would happen to A's handler, it would also happen to B's. Is it a valid architectural choice?
You just need to maintain a reference to ViewControllerB in ViewControllerA when you're presenting ViewControllerB in ViewControllerA, here's how:
class ViewControllerA: UIViewController {
var viewControllerB: ViewControllerB?
func presentViewControllerB() {
if let viewControllerB = viewControllerB {
present(viewControllerB, animated: true)
} else {
viewControllerB = ViewControllerB()
presentViewControllerB()
}
}
func passDataToViewControllerB() {
viewControllerB?.someData = "Data from ViewControllerA."
}
}
class ViewControllerB: UIViewController {
var someData = ""
}

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 properly implement Navigator pattern

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)

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

Trying to pass information between controllers fails, yet works from AppDelegate to controller

I have two UIViewControllers, vc1, and vc2. vc1 is embedded in a UIViewController which is embedded in a UITabBarController, but vc2 is not embedded in either.
How do I pass information from vc2 to vc1? After a user performs an action the data is saved and vc2 simply closes, so there isn't a segue to pass information. Obviously I can't reference vc1 through the Navigation stack or the TabController.
I could save to the AppDelegate, but I've read this isn't a good practice.
This is the code I use to pass information from AppDelegate to vc1 I tried it in vc2, but obviously it failed.:
let tabBarController = window!.rootViewController as! UITabBarController
if let tabBarViewControllers = tabBarController.viewControllers {
let navPostViewController = tabBarViewControllers[0] as! UINavigationController
let user = User(context: managedObjectContext)
if user.userID != nil {
print("User is loggedIn")
isUserLoggedIn = true
} else {
print("User is not loggedIn")
isUserLoggedIn = false
}
let postViewController = navPostViewController.topViewController as! PostViewController
postViewController.managedObjectContext = managedObjectContext
}
First off, I've never got into the habit of using segue to pass information. What i would recommend is that you implement the delegate pattern whenever you need to pass data between two objects. Its a lot cleaner.
For instance lets say you wanted to pass data between LoginViewController and PostViewController:
protocol LoginViewControllerDelegate:NSObjectProtocol{
func userDidLogin(data:String)
}
class LoginViewController:UIViewController {
weak var delegate:LoginViewControllerDelegate?
...
#IBAction func loginButtonPressed(sender:UIButton) {
//Perform login logic here
//If successful, tell the other controller or the 'delegate'
self.delegate?.userDidLogin(data:"Some data....")
}
}
class PostViewController:UIViewController, LoginViewControllerDelegate {
func userDidLogin(data:String) {
print("Got data from login controller: \(data)")
}
}
//How you might use this
loginViewController.delegate = postViewController
One caveat to remember is to never try to have strong references between two objects i.e. do not have the objects hold onto each other or this will cause a memory leak.