blank collection view in user login for the first time - swift

Good day everyone,
I have root view controller set up as my HomeViewController, in this project i am using token based authentication ( which i am storing in user defaults ) and i am using token for all my API calls.
I have a check in my viewWillAppear method to check if there is access token present and then i make the api call in viewDidAppear to populate the collection view, and this works perfectly fine at all times except the first time.
If I log in for the first time it hits the viewWillAppear, viewDidAppear and then login screen pops up and once i authenticate the user and save it in the UserDefaults, dismiss the login screen and in the HomeViewController all i get is a spinner ( which means that viewDidAppear is also been called ) but if i close the app and open it again it all works fine.
What can i change in my code to make it work in the first time please and thank you!!
class HomeViewController: UIViewController {
// MARK: - Properties
let refreshControl = UIRefreshControl()
var publishedReportList: [ReportListDetail] = []
private let reportsCollectionView: UICollectionView = {
let viewLayout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: viewLayout)
collectionView.register(ReportsCollectionViewCell.self, forCellWithReuseIdentifier: ReportsCollectionViewCell.identifier)
collectionView.backgroundColor = .systemBackground
return collectionView
}()
// MARK: - Initialisation
override func viewDidLoad() {
super.viewDidLoad()
reportsCollectionView.delegate = self
reportsCollectionView.dataSource = self
print(publishedReportList.count)
refreshControl.tintColor = .blue
refreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
reportsCollectionView.addSubview(refreshControl)
reportsCollectionView.alwaysBounceVertical = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// check auth status
handleNotAuthenticated()
}
override func viewDidAppear(_ animated: Bool) {
// call for reports
getReportUserLayout()
}
// MARK: - Handlers
#objc func pullToRefresh() {
// Code to refresh table view
getReportUserLayout()
}
fileprivate func getReportUserLayout() {
// publishedReportList.removeAll()
whySuchEmptyLabel.isHidden = true
spinner.show(in: view)
spinner.textLabel.text = "Loading Reports.."
DispatchQueue.main.async {
ReportsManager.shared.getReportData { [weak self] (listOfReports) in
guard let strongSelf = self else { return }
strongSelf.publishedReportList = listOfReports
if listOfReports.count == 0 {
strongSelf.whySuchEmptyLabel.isHidden = false
}
strongSelf.reportsCollectionView.reloadData()
strongSelf.spinner.dismiss()
strongSelf.refreshControl.endRefreshing()
}
}
}
private func handleNotAuthenticated() {
if UserDefaults.standard.string(forKey: "accessToken") == nil {
// show login view controller
let loginVC = LoginViewController()
loginVC.modalPresentationStyle = .overCurrentContext
present(loginVC, animated: false)
}
}
}

You are in the HomeViewController and presenting loginVC, so it will not trigger viewDidAppear or viewWillAppear because it is not disappeared from the app. You have to use closure or delegate or notification to communicate back to the HomeViewController. You can also use the Combine framework and save the state. Here is an example of using delegate.
// Add protocol
protocol ViewControllerDelegate {
func loggedIn()
}
class HomeViewController: UIViewController, ViewControllerDelegate {
private func handleNotAuthenticated() {
if UserDefaults.standard.string(forKey: "accessToken") == nil {
let loginVC = LoginViewController()
loginVC.modalPresentationStyle = .overCurrentContext
loginVC.viewDelegate = self
present(loginVC, animated: false)
}
}
}
class LoginViewController: UIViewController {
var viewDelegate: ViewControllerDelegate? = nil
func userLoggedIn() {
self.viewDelegate?.loggedIn()
}
}

Related

Testing tableview.reloadData()

while using a MockTableView this code still not calling reloadData() from the mock,
please i wanna know what is wrong here.
following this book: Test-Driven IOS Development with Swift 4 - Third Edition
page 164, i was as an exercise
full code repo - on github
ItemListViewController.swift
import UIKit
class ItemListViewController: UIViewController, ItemManagerSettable {
#IBOutlet var tableView: UITableView!
#IBOutlet var dataProvider: (UITableViewDataSource & UITableViewDelegate &
ItemManagerSettable)!
var itemManager: ItemManager?
override func viewDidLoad() {
super.viewDidLoad()
itemManager = ItemManager()
dataProvider.itemManager = itemManager
tableView.dataSource = dataProvider
tableView.delegate = dataProvider
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
#IBAction func addItem(_ sender: UIBarButtonItem) {
if let nextViewController =
storyboard?.instantiateViewController(
withIdentifier: "InputViewController")
as? InputViewController {
nextViewController.itemManager = itemManager
present(nextViewController, animated: true, completion: nil)
}
}
}
ItemListViewControllerTest.swift
import XCTest
#testable import ToDo
class ItemListViewControllerTest: XCTestCase {
var sut: ItemListViewController!
var addButton: UIBarButtonItem!
var action: Selector!
override func setUpWithError() throws {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier:
"ItemListViewController")
sut = vc as? ItemListViewController
addButton = sut.navigationItem.rightBarButtonItem
action = addButton.action
UIApplication.shared.keyWindow?.rootViewController = sut
sut.loadViewIfNeeded()
}
override func tearDownWithError() throws {}
func testItemListVC_ReloadTableViewWhenAddNewTodoItem() {
let mockTableView = MocktableView()
sut.tableView = mockTableView
guard let addButton = sut.navigationItem.rightBarButtonItem else{
XCTFail()
return
}
guard let action = addButton.action else{
XCTFail()
return
}
sut.performSelector(onMainThread: action, with: addButton, waitUntilDone: true)
guard let inputViewController = sut.presentedViewController as?
InputViewController else{
XCTFail()
return
}
inputViewController.titleTextField.text = "Test Title"
inputViewController.save()
XCTAssertTrue(mockTableView.calledReloadData)
}
}
extension ItemListViewControllerTest{
class MocktableView: UITableView{
var calledReloadData: Bool = false
override func reloadData() {
calledReloadData = true
super.reloadData()
}
}
}
You inject a MockTableview Then you call loadViewIfNeeded(). But because this view controller is storyboard-based and the table view is an outlet, the actual table view is loaded at this time. This replaces your MockTableview.
One solution is:
Call loadViewIfNeeded() first
Inject the MockTableview to replace the actual table view
Call viewDidLoad() directly. Even though loadViewIfNeeded() already called it, we need to repeat it now that we have a different tableview in place.
Another possible solution:
Avoid MockTableview completely. Continue to use a real table view. You can test whether it reloads data by checking whether the number of rows matches the changed data.
Yet another solution:
Avoid storyboards. You can do this with plain XIBs (but these lack table view prototype cells) or programmatically.
By the way, I see all your tearDownWithError() implementations are empty. Be sure to tear down everything you set up. Otherwise you will end up with multiple instances of your system under test alive at the same time. I explain there here: https://qualitycoding.org/xctestcase-teardown/

How to push controller after dismiss presented controller in swift?

Hey I am showing a controller (A) in main controller with presentation style (not push), and I want to button tapped and push another controller (B) after dismiss this (A) controller, this situation occurred in main controller. I am using protocol for this situation. Any idea for that ? Code like below.`
//this is dismiss button action
var segueDelegate: segueFromController?
#objc func dismissController() {
self.dismiss(animated: true) {
self.segueDelegate?.segueFromController()
}
//and this one is protocol function in main controller
func segueFromController() {
let contProfile = ContViewController(collectionViewLayout: UICollectionViewFlowLayout())
navigationController?.pushViewController(contProfile, animated: true)
}
// and I am making "self" this protocol in main controller's didload
let aCont = AController()
override func viewDidLoad() {
super.viewDidLoad()
AController.segueDelegate = self
}
// protocol
protocol segueFromController {
func segueFromController()
}
// this is presenting (A) controller code in main page
func openController() {
let preController = AController()
preController.modalPresentationStyle = .fullScreen
self.present(preController, animated: true, completion: nil)
}
First you need to make this segueDelegate weak
protocol segueFromController : class {
func segueFromController()
}
weak var segueDelegate: segueFromController?
func openController() {
let preController = AController()
preController.segueDelegate = self
preController.modalPresentationStyle = .fullScreen
self.present(preController, animated: true, completion: nil)
}
Try to dismiss without animation
self.dismiss(animated: false) {
self.segueDelegate?.segueFromController()
}

Can not remove child coordinator because transitionCoordinator is nil in navigationController delegate method

Brief :
I have implemented Soroush's Coordinators architecture. Everything works fine except the removing part which is needed to remove previous(child) coordinators.
Scenario :
I have two ViewController named HomeViewController and MyGroupsViewController. Each has its own coordinator named HomeCoordinator and MyGroupsCoordinator respectively.
User taps a button on HomeViewController which triggers gotoMyGroupsTapped function and gets the user to MyGroupsViewController, Then the user taps on another button on MyGroupsViewController which get the user back to HomeViewController by triggering gotoHomePage().
Pretty simple! : HomeVC -> MyGroupsVC -> HomeVC
But the Problem is :
navigationController.transitionCoordinator? is nil in func navigationController(..., didShow viewController: UIViewController...) in both coordinators and I can not remove child coordinators in each transition.
Is it correct to set navigationController.delegate = self in start() func of both coordinators?
Should I use navigationController?.popViewController(animated: false ) in my backToHomePage() func? because Paul Hudson has only used pushViewController.
My Codes [Simplified Versions]:
HomeCoordinator.swift
import Foundation
import UIKit
class HomeCoordinator: NSObject,Coordinator,UINavigationControllerDelegate {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
weak var parentCoordinator : Coordinator?
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
// Transition here is nil
print(" Transition : ",navigationController.transitionCoordinator)
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
print("Unknown fromViewController!")
return
}
// Removing a child coordinator
}
func gotoMyGroups (){
let groupsCoordinator = GroupsCoordinator(navigationController: navigationController)
childCoordinators.append(groupsCoordinator)
groupsCoordinator.parentCoordinator = self
groupsCoordinator.start()
}
func start() {
let vc = HomeViewController.instantiate()
vc.coordinator = self
navigationController.delegate = self
navigationController.pushViewController(vc, animated: false)
navigationController.setNavigationBarHidden(true, animated: false)
}
}
MyGroupsCoordinator.swift
import Foundation
import UIKit
class MyGroupsCoordinator: NSObject,Coordinator,UINavigationControllerDelegate {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
weak var parentCoordinator : Coordinator?
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
// Transition here is nil
print(" Transition : ",navigationController.transitionCoordinator)
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
print("Unknown fromViewController!")
return
}
// Removing a child coordinator
}
func start() {
let vc = MyGroupViewController.instantiate()
vc.coordinator = self
navigationController.delegate = self
navigationController.pushViewController(vc, animated: false)
navigationController.setNavigationBarHidden(true, animated: false)
}
}
MyGroupViewController.magik
class MyGroupViewController : UIViewControllerWithCoordinator,UITextFieldDelegate,Storyboarded{
#IBAction func gotoHomePage(_ sender: Any) {
if let coord = coordinator as? GroupsCoordinator {
coord.parentCoordinator?.start()
}
}
}
HomeViewController.swift
class HomeViewController: UIViewControllerWithCoordinator,Storyboarded {
#IBAction func gotoMyGroupsTapped(_ sender: Any) {
guard let acoordinator = coordinator as? HomeCoordinator else {
return
}
acoordinator.gotoMyGroups()
}
It looks to me there is a confusion around Coordinator pattern usage here.
From your expected flow HomeVC -> MyGroupsVC -> HomeVC, if you mean in the sense level1 -> level2 -> level3, then GroupsCoordinator should create a new HomeCoordinator instance with its own new HomeVC.
So instead of your previous code
class MyGroupViewController ... {
#IBAction func gotoHomePage(_ sender: Any) {
if let coord = coordinator as? GroupsCoordinator {
coord.parentCoordinator?.start()
}
}
}
I would change it to
class MyGroupViewController ... {
#IBAction func gotoHomePage(_ sender: Any) {
if let coord = coordinator as? GroupsCoordinator {
coord.goToHome()
}
}
}
class MyGroupsCoordinator ... {
func goToHome() {
let homeCoordinator = HomeCoordinator(navigationController: navigationController)
childCoordinators.append(homeCoordinator)
groupsCoordinator.parentCoordinator = self
groupsCoordinator.start()
}
}
This will allow you to create a brand new page as you describe there HomeVC -> MyGroupsVC -> HomeVC.
However, if you meant in this approach level1 -> level2 -> (back) level1, then you'll need to terminate MyGroupsCoordinator and remove from the parent while navigating back.
As you noticed, to do so, you'll need to use UINavigationControllerDelegate to be able to be notified when the user navigate back (either pop in code, or with classic back button).
One solution I found is to use a Router to handle all this navigation when a UIViewController is removed from it to also notify via closures the right coordinator to be removed. You can read more about it here.
Hope it helps

How can I get an updated value from UserDefaults?

I have an Onboarding controller, when a user has completed Onboarding I am writing a value in UserDefaults and then dismissing the OnboardingViewController.
My base viewcontroller however is pushing them back to the Onboarding flow as UserDefaults.standard.bool(forKey: "ONBOARDING|COMPLETE") is returning false.
If I restart my app however is will return true and they are pushed to the correct VC.
My Base ViewController
class ViewController: UIViewController {
var onboardingComplete: Bool {
get {
return UserDefaults.standard.bool(forKey: "ONBOARDING|COMPLETE")
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(UserDefaults.standard.bool(forKey: "ONBOARDING|COMPLETE"))
print(onboardingComplete)
setBaseViewController()
}
fileprivate func setBaseViewController() {
if onboardingComplete {
print("has completed onboarding")
} else {
let layout = UICollectionViewFlowLayout()
navigationController?.present(OnboardingController(collectionViewLayout: layout), animated: true, completion: { })
}
}
}
My Onboarding controller has a method in which I am using to set the value as complete
#objc func handleCompleteOnboarding() {
dismiss(animated: true) {
DispatchQueue.main.async {
UserDefaults.standard.set(true, forKey: "ONBOARDING|COMPLETE")
}
}
}
You are setting the value in the completion handler.
Move this outside of dismiss.
#objc func handleCompleteOnboarding() {
UserDefaults.standard.set(true, forKey: "ONBOARDING|COMPLETE")
dismiss(animated: true)
}
You have essentially created a race condition between you setting the value and your viewDidAppear method being called.

Why delegate event is not received swift?

I would like to pass data from EditPostViewController to NewsfeedTableViewController using delegates, but func remove(mediaItem:_) is never called in the adopting class NewsfeedTableViewController. What am I doing wrong?
NewsfeedTableViewController: UITableViewController, EditPostViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
//set ourselves as the delegate
let editPostVC = storyboard?.instantiateViewController(withIdentifier: "EditPostViewController") as! EditPostViewController
editPostVC.delegate = self
}
//remove the row so that we can load a new one with the updated data
func remove(mediaItem: Media) {
print("media is received heeeee")
// it does't print anything
}
}
extension NewsfeedTableViewController {
//when edit button is touched, send the corresponding Media to EditPostViewController
func editPost(cell: MediaTableViewCell) {
let editPostVC = storyboard?.instantiateViewController(withIdentifier: "EditPostViewController") as? EditPostViewController
guard let indexPath = tableView.indexPath(for: cell) else {
print("indexpath was not received")
return}
editPostVC?.currentUser = currentUser
editPostVC?.mediaReceived = cell.mediaObject
self.navigationController?.pushViewController(editPostVC!, animated: true)
}
protocol EditPostViewControllerDelegate: class {
func remove(mediaItem: Media)
}
class EditPostViewController: UITableViewController {
weak var delegate: EditPostViewControllerDelegate?
#IBAction func uploadDidTap(_ sender: Any) {
let mediaReceived = Media()
delegate?.remove(mediaItem: mediaReceived)
}
}
The objects instantiating in viewDidLoad(:) and on edit button click event are not the same objects. Make a variable
var editPostVC: EditPostViewController?
instantiate in in viewDidLoad(:) with delegate
editPostVC = storyboard?.instantiateViewController(withIdentifier: "EditPostViewController") as! EditPostViewController
editPostVC.delegate = self
and then present it on click event
navigationController?.pushViewController(editPostVC, animated: true)
or
present(editPostVC, animated: true, completion: nil)
you can pass data from presenter to presented VC before or after presenting the VC.
editPostVC.data = self.data
I suggest having a property in NewsfeedTableViewController
var editPostViewController: EditPostViewController?
and then assigning to that when you instantiate the EditPostViewController.
The idea is that it stops the class being autoreleased when NewsfeedTableViewController.viewDidLoad returns.