Testing method in UITabBarController sets title correctly - swift

I am currently trying to understand Unit Testing in Swift 4.
I have a class, with a method that setups my view controllers.
I would like to ensure that this method sets the title on the ViewController correctly.
However I cannot understand how to write this test?
This is my code and test so far.
Currently my tests fails with:
XCTAssertEqual failed: ("nil") is not equal to ("Optional("Favourites")") -
How is it possible to test this behaviour? Any help would be much appreciated.
Controller
class MainTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
setupTabBar()
setupViewControllers()
}
// MARK:- Setup
fileprivate func setupViewControllers() {
viewControllers = [
generateNavigationController(with: UIViewController(), title: "Favourites", image: UIImage(imageLiteralResourceName: "favorites")),
generateNavigationController(with: UIViewController(), title: "Search", image: UIImage(imageLiteralResourceName: "search")),
generateNavigationController(with: UIViewController(), title: "Downloads", image: UIImage(imageLiteralResourceName: "downloads"))
]
}
fileprivate func setupTabBar() {
tabBar.tintColor = .purple
}
// MARK:- Helpers
fileprivate func generateNavigationController(with rootViewController: UIViewController, title: String, image: UIImage) -> UIViewController {
let controller = UINavigationController(rootViewController: rootViewController)
controller.tabBarItem.title = title
controller.tabBarItem.image = image
rootViewController.navigationItem.title = title
navigationController?.navigationBar.prefersLargeTitles = true
return controller
}
}
Tests
import Foundation
import XCTest
#testable import Podcasts
class MainTabBarControllerTests: XCTestCase {
func testTheInitialViewControllerShouldHaveTitleFoo() {
let sut = MainTabBarController()
let _ = sut.viewDidLoad()
XCTAssertEqual(sut.navigationItem.title, "Favourites")
}
}

There are some issues with accessing proper views in your code snippet. Btw I wrote an additional test for your tab bar controller, hope it will help :) Try this to pass the test:
class MainTabBarControllerTests: XCTestCase {
func testFirstTabTitleIsCorrectAfterInitialSetup() {
// Given
let sut = MainTabBarController()
// When
sut.viewDidLoad()
// Then
let viewController = sut.viewControllers?.first
let title = viewController?.tabBarItem.title
XCTAssertEqual(title, "Favourites")
}
func testNavigationTitleIsCorrectAfterInitialSetup() {
// Given
let sut = MainTabBarController()
// When
sut.viewDidLoad()
// Then
let viewController = sut.viewControllers?.first as? UINavigationController
let title = viewController?.viewControllers.first?.navigationItem.title
XCTAssertEqual(title, "Favourites")
}
}
It's good that you've already used Given-When-Then, but it would even great if you improve naming of the test. There are a lot of conventions, for example, I prefer something like test_SubjectUnderTest_doSomething_whenConditionsAreCorrect.
Here is another tip. Try to figure out how to separate the logic between view and view controller. It you get your hands dirty into at least MVP (Model-View-Presenter), then you'll figure out that its testability is better.
By the way, instead of this kind of tests it's more reasonable to consider UI tests. UI tests mostly rely on accessibility IDs. The most popular tools for test automation are Appium or XCUITests/Earlgrey if you prefer to go with native.

Related

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

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

How to pass the func () value of the view element to another Controller

First I'll start with what I did, and in the end I will describe the problem, so it will be clearer. In general, after Android Studio, working with view elements in Xcode is still a task. I understand that a good programmer will not write the same code twice, so that each time in different Controller does not describe each time the view elements - so I wrote this function in the main Controller
class func designForButton (button: UIButton){
button.layer.masksToBounds = true
button.layer.cornerRadius = 8
}
Then you can access it in different Controller
class RegisterViewController: UIViewController {
#IBOutlet weak var buttonBack: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
ViewController.designForButton(button: buttonBack)
}
Now, what is not clear. I loaded the framework to work with Toast. And I described its values in the main Controller
func designForToast(message: String){
let style = CSToastStyle.init(defaultStyle: {
}())
_ = style?.backgroundColor = UIColor.gray
_ = style?.titleColor = UIColor.cyan
_ = style?.messageColor = UIColor.darkGray
self.view.makeToast(message, duration: 2, position: self.bottomLayoutGuide, title: "title", image: UIImage (named: "logo.jpg"), style: style ) { (success: Bool) in
}
}
But the matter is that in this case I can address to it exclusively in the same Controller
_=self.designForToast(message: "Its a Toast")
As soon as I want to make func () - like class func () to work in another Controller, Xcode starts to highlight that it's impossible, and due to my small experience, I can not fix it myself.
I suggest that you add a viewController parameter in the method so that it looks like this:
static func showToast(message: String, on viewController: UIViewController){ // I also renamed the method as well
let style = CSToastStyle.init(defaultStyle: {
}())
_ = style?.backgroundColor = UIColor.gray
_ = style?.titleColor = UIColor.cyan
_ = style?.messageColor = UIColor.darkGray
viewController.view.makeToast(message, duration: 2, position: viewController.bottomLayoutGuide, title: "title", image: UIImage (named: "logo.jpg"), style: style ) { (success: Bool) in
}
}
I also suggest that you put these "helper functions" in a designated class, like StyleHelper or something like that.
Then you can use it like this in a View Controller:
StyleHelper.showToast(message: "Hello", on: self)
Or better yet, make this an extension method!
extension UIViewController {
func showMyToast(message: String){
let style = CSToastStyle.init(defaultStyle: {
}())
_ = style?.backgroundColor = UIColor.gray
_ = style?.titleColor = UIColor.cyan
_ = style?.messageColor = UIColor.darkGray
self.view.makeToast(message, duration: 2, position: self.bottomLayoutGuide, title: "title", image: UIImage (named: "logo.jpg"), style: style ) { (success: Bool) in
}
}
}
You can then use it like this in a View Controller:
self.showMyToast(message: "Hello")
Side Note:
For views that conform to UIAppearance protocol, you can access the appearance() property to change its style globally. For example, you can do this in didFinishLaunchingWithOptions:
UIButton.appearance().tintColor = .red
And every UIButton you create will be red.
I think you need to define a protocol , and an extension to UIViewController to provide a default implementation for your method.....if this was the question...:P
protocol ToastProtocol: class {
func showToast(message:String)
}
extension ToastProtocol where Self: UIViewController {
func showToast(message:String){
///yourcode
}
}
After that you can youse your showToast method in any UIViewController

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
}