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")
}
}
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 am refactoring my codes and want to give Coordinators a try.
However, I am not quite comfortable with UINavigationController in charge, cause it creates a Back button on child coordinates as well as an unwanted top bar which I don’t want.
Is there anyway I could use ordinary UIViewController instead of UINavigationController?
P.S : it is a sample code I found [Here][1]
protocol Coordinator {
func start()
}
protocol FlowACoordinatable {
var coordinator: FlowACoordinator
}
class FlowACoordinator: Coordinator {
private var navigationController: UINavigationController
private var firstVC: FirstViewController
private var secondVC: SecondViewController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() { ... }
func present(_ viewController: (FlowACoordinatable & UIViewController)) {
viewController.coordinator = self
self.navigationController.pushViewController(viewController, animated: true)
}
...
}
class FirstViewController: UIViewController, FlowACoordinatable {
var coordinator: FlowACoordinator?
func buttonTapped() {
self.coordinator?.goToNextStep()
}
}
Answering my own question...
Coordinators architecture is based on UINavigationController and can not be replaced by UIViewController.
But still, it is possible to hide the buttom bar pane using Object Inspection and check Hide buttom bar... or do it by code.
I have issues with using protocols to send data back to previous controller. I have studied SO questions and guides, but for some reason my data doesn't get transferred back.
In my second class I create data, that is later being sent back to first class:
protocol ImageEditorDelegate {
func sendImage(image: UIImage, id: String)
}
class PhotoEditorViewController: UIViewController {
var delegate: ImageEditorDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func didPressSave(_ sender: UIButton) {
delegate?.sendImage(image: finalImage, id: imageThatWasSelected)
self.dismiss(animated: true, completion: nil)
}
}
And in my receiving class I have:
class NewProductViewController: UIViewController, ImageEditorDelegate {
var imageEditor: PhotoEditorViewController?
override func viewDidLoad() {
super.viewDidLoad()
imageEditor?.delegate = self
}
func sendImage(image: UIImage, id: String) {
print("Receiving images", image, id)
switch id {
case "1":
selectedImages[1] = image
productImage1.image = image
case "2":
selectedImages[2] = image
productImage2.image = image
case "3":
selectedImages[3] = image
productImage3.image = image
default:
break
}
}
}
But nothing happens, this func never gets called. I think my delegate is nil, or so, but how could I fix this issue? I have Also, I'm using VIPER as architecture with slightly customized segues, may this be the issue? I have tried simple segues, but had same issue.
I understand that this is rather simple question, but I couldn't understand what I doing wrong after I have read articles about protocols.
Thanks for your help!
What you're doing is very wrong. You have two view controllers with property references to one another:
class PhotoEditorViewController: UIViewController {
var delegate: ImageEditorDelegate?
}
class NewProductViewController: UIViewController, ImageEditorDelegate {
var imageEditor: PhotoEditorViewController?
}
Those are not weak references, so if you ever do get this to work — that is, if you ever arrange things so that the NewProductViewController's imageEditor is a PhotoEditorViewController whose delegate is that NewProductViewController — you will have a nasty retain cycle and a memory leak.
This suggests that you have not understood the protocol-and-delegate pattern. Only the presented view controller should have a delegate property pointing back to the presenter, and it should be weak. The presenter does not need any property pointing to the presented view controller, because it presents it.
you need to instantiate your photoEditor, like
photoEditor = PhotoEditorViewController()
before attempting to set its delegate.
you dont' have to do this next part, but I'd suggest making the delegate variable a weak variable to avoid any retain issues, like so
weak var delegate: ImageEditorDelegate?
and you'll need to mark the protocol as class like so
protocol ImageEditorDelegate : class {
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
}
}
Sorry in advance that I can’t explain myself very well. I’m really new to programming and the topic of delegation still eludes me. I had some great help with this once before, but now I am trying to use a delegate in a different situation and I can’t get it right. I pieced together a bit of code that doesn’t work, and no matter how much I search I can’t find a way to fix it.
I have a view controller (MainController) with and embedded view controller (EmbeddedController) in a container view. I am trying to have a button in the embedded controller manipulate the container view (containerView).
EmbeddedController:
protocol ControllerDelegate {
func hideContainerView()
}
class EmbeddedController: UIViewController {
var delegate: VControllerDelegate?
#IBAction func button(sender: AnyObject) {
delegate?.hideContainerView()
}
}
MainController:
class MainController: UIViewController, ControllerDelegate {
#IBOutlet var containerView: UIView!
func hideContainerView() {
containerView.hidden = true
}
override func viewDidLoad() {
super.viewDidLoad()
var vc = EmbeddedController()
vc.delegate = self
}
}
Does anyone have any idea what I am doing wrong? And why this isn’t working?
What I ended up doing is adding this to the MainController:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "mySegue") {
let vc = segue.destinationViewController as! EmbeddedController
vc.delegate = self
}
}
In storyboard I selected the segue from the MainController to the EmbeddedController, and set the identifier to "mySegue".
Without the code above the delegate kept returning nil. I didn't look into this solution at first as I thought segues were only for transitioning between view controllers, and in my mind I didn't see the embedded controller as a transition. Maybe someone more knowledgable than me (which is practically anyone on here at this point) can explain how this is all fitting together.
In any case, this is how I solved my issue and hopefully someone else can benefit from this as well :)
First of all, to avoid strong reference cycles:
protocol ControllerDelegate: class {
func hideContainerView()
}
class EmbeddedController: UIViewController {
weak var delegate: ControllerDelegate?
And you haven't added your newly instantiated VC view to container view, and not added it as a child VC:
let vc = EmbeddedController()
vc.delegate = self
containerView.addSubview(vc.view)
self.addChildViewController(vc)
vc.didMoveToParentViewController(self)