Background
I have a workoutVM view model that would hold most of my view models, which I want to pass down to all other view controllers in my app. My app also has a tabbar controller, which I have used to store some data such as user information, etc.
Problem 1
Even though I have created the workoutVM view model (with values) in the MyTrainingViewController ("Home" tab), I am unable to pass the view model (with values) to the next ExerciseSetsViewController("Workout" tab) by using
Method 1 - By using Delegate MyTrainingViewControllerDelegate and/or
Method 2 - By instantiating ExerciseSetsViewController and loading the view.
The codes/action to pass workoutVM view model are run when user selects a particular tableview cell.
I am really not sure why either one of the methods work because similar approach worked for many other scenarios. Below is the Xcode debugger showing that my codes to utilize both methods didn't pass the workoutVM view model to the ExerciseSetsViewController
Problem 2
As a result, I found a workaround (Method 3 that were commented out in the below codes) to utilize tabbar to store the workoutVM view model (again relying on tabbar to pass & share data across multiple view controllers).
At this point, I am afraid that my app is practically using tabbar as a "singleton", even though I "sort of" understand that it is not quite "singleton".
I think, ideally, the view models should serve as some sort of data models, which are to be shared/manipulated/passed across multiple view controllers without the need to have tabbar as a middle layer. Wouldn't that be correct? Or is this the best/good practice that I am adopting by utilizing the tabbar?
protocol MyTrainingViewControllerDelegate {
func passWorkoutVM(workoutVM: WorkoutViewModel)
}
class MyTrainingViewController: UIViewController {
var workoutVM: WorkoutViewModel?
var delegate: MyTrainingViewControllerDelegate!
#IBOutlet weak var dayProgramTableView: UITableView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
dayProgramTableView.delegate = self
dayProgramTableView.dataSource = self
}
}
extension MyTrainingViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let tabbar = tabBarController as! MainTabBarController
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let ExerciseSetsViewController = storyBoard.instantiateViewController(withIdentifier: "ExerciseSetsView") as! ExerciseSetsViewController
guard let workoutVM = self.workoutVM else {
return
}
print("printing workoutViewModel dayprogram no.of exercise at HomeView \(workoutVM.dayprograms[indexPath.row].dayprogram.dayIntensity)")
//1.Instantiate view via storyboard Method
ExerciseSetsViewController.loadViewIfNeeded()
ExerciseSetsViewController.workoutVM = workoutVM
//2.Delegate Method
self.delegate?.passWorkoutVM(workoutVM: workoutVM)
//3.Tabbar Method
// tabbar.workoutVM = workoutVM
tabbar.selectedIndex = 1
}
}
class ExerciseSetsViewController: UIViewController {
var workoutVM: WorkoutViewModel?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
// ** To do
//create workoutViewModel
//3.Tabbar Method
// self.workoutVM = tabbar.workoutVM
print("printing workoutViewModel to check if workoutVM had been passed from MyTrainingView \(String(describing: self.workoutVM))")
}
}
extension ExerciseSetsViewController: MyTrainingViewControllerDelegate {
func passWorkoutVM(workoutVM: WorkoutViewModel) {
self.workoutVM = workoutVM
print("passWorkoutDelegate Method executed")
}
}
class MainTabBarController: UITabBarController {
var workoutVM: WorkoutViewModel?
override func viewDidLoad() {
super.viewDidLoad()
}
}
With some external help, I was able to identify the source of the 1st problem and rectify them. I was also able to hear necessary advice for the 2nd problem.
Problem 1
The issue was that the exerciseSetsViewController that I "instantiated via storyboard" was different from the exerciseSetsViewController that would have been shown from TabbarController's 2nd tab (Workout Tab). Hence, the workoutVM was not passed to correct viewcontroller. Hence, below corrected codes needed to be used if I wanted to use either
Method 1 - By using Delegate MyTrainingViewControllerDelegate and/or
Method 2 - By instantiating ExerciseSetsViewController and loading
the view.
The corrected code ensured that the exerciseSetsViewController that was instantiated was placed as the TabbarController's 2nd tab.
protocol MyTrainingViewControllerDelegate {
func passWorkoutVM(workoutVM: WorkoutViewModel)
}
class MyTrainingViewController: UIViewController {
var workoutVM: WorkoutViewModel?
var delegate: MyTrainingViewControllerDelegate!
#IBOutlet weak var dayProgramTableView: UITableView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
dayProgramTableView.delegate = self
dayProgramTableView.dataSource = self
}
}
extension MyTrainingViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let tabbar = tabBarController as! MainTabBarController
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let ExerciseSetsViewController = storyBoard.instantiateViewController(withIdentifier: "ExerciseSetsView") as! ExerciseSetsViewController
guard let workoutVM = self.workoutVM else {
return
}
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let exerciseSetsViewController = storyBoard.instantiateViewController(withIdentifier: "ExerciseSetsView") as! ExerciseSetsViewController
guard let nav = tabbar.viewControllers?[1] as? UINavigationController, let exerciseSetsViewController = nav.viewControllers.first as? ExerciseSetsViewController else { return }
print("printing workoutViewModel dayprogram no.of exercise at MyTrainingView \(workoutVM.dayprograms[indexPath.row].dayprogram.dayIntensity)")
// 1.Instantiate view via storyboard Method
exerciseSetsViewController.loadViewIfNeeded()
exerciseSetsViewController.workoutVM = workoutVM
// 2.Delegate Method
self.delegate?.passWorkoutVM(workoutVM: workoutVM)
}
Problem 2
I was advised that my approach to utilize the "tabbar" is literally the same as using "Singleton" because there is one shared source of data where multiple views are accessing. Likewise, my approach to utilize view models that can be accessed via multiple view controller is same as using "global" variable, which has similar repercussion as using "Singletons".
While this approach can be acceptable in certain cases, it is not the "best practice" and I will need to change some of my codes/approach, where each view controller would have their own set of data/view models.
Related
I'm building a simple movie app by fetching some data from an API. My app navigation pattern looks like this: NavigationController as InitialViewController > TabBarController. TabBarController includes two ViewControllers: HomeViewController and FavouriteMoviesViewController. I have a button inside HomeViewController which pushes the SeachViewController page using navigation. I also have DetailsViewController. Whenever I press movie poster whether in HomeViewController or SearchViewController CollectionViewCell, it presents me DetailsPageViewController. I'm using delegation pattern(i set FavouriteMoviesViewController as a delegate of the DetailsViewController when I present DetailsViewController from HomeViewController) to update data inside FavouriteMoviesViewController from DetailsViewController, but the problem is that I can't set delegate when I access DetailsViewController from SearchViewController because these two ViewControllers are not related in any way. I tried to set an observer inside FavouriresViewController, but it won't work until I access that viewController
here's my code when i present DetailsViewController from HomeViewController and pass fetched data
// Cell action
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Instantiate DetailsViewController and set it's delegate to FavouritesViewController to pass data
let targetVC = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: DetailsViewController.identifier) as! DetailsViewController
let favMoviesScreen = self.tabBarController?.viewControllers?[0] as! FavouritesViewController
targetVC.changeToFavouriteDelegate = favMoviesScreen
switch collectionView {
case self.trendingMoviesCollectionView:
let selectedMovie = trendingMovies[indexPath.row]
targetVC.movie = selectedMovie
targetVC.genreIDs = selectedMovie.genreIDs
self.navigationController?.present(targetVC, animated: true, completion: nil)
case self.upcomingMoviesCollectionView:
let selectedMovie = upcomingMovies[indexPath.row]
targetVC.movie = selectedMovie
targetVC.genreIDs = selectedMovie.genreIDs
self.navigationController?.present(targetVC, animated: true, completion: nil)
case self.topRatedMoviesCollectionView:
let selectedMovie = topRatedMovies[indexPath.row]
targetVC.movie = selectedMovie
targetVC.genreIDs = selectedMovie.genreIDs
self.navigationController?.present(targetVC, animated: true, completion: nil)
default: break
}
}
everything's good here.
here's DetailsViewController where i show movie details, i mark favourite movie here and add/remove it in my FavouriteMoviesViewController, this is delegate method:
// MARK: - Button Actions
#objc private func markAsFavourite() {
guard let favouriteMovie = movie else {
print("invalid movie object")
return
}
// check whether the movie is already added into favourite movies array
if !DetailsViewController.favouriteMovies.contains(where: { favouriteMovie.movieId == $0.movieId }) {
addToFavouritesButton.tintColor = .appRedColor
DetailsViewController.favouriteMovies.append(favouriteMovie)
changeToFavouriteDelegate?.addMovie(favouriteMovie)
} else {
addToFavouritesButton.tintColor = .white
DetailsViewController.favouriteMovies.removeAll(where: { $0.movieId == favouriteMovie.movieId})
changeToFavouriteDelegate?.removeMovie(favouriteMovie)
}
}
now the problem is here, in my SearchViewController,i guess because of the fact that this ViewController doesnt' belong to tabBar
// Cell action
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let targetVC = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: DetailsViewController.identifier) as! DetailsViewController
let tabBar = (self.navigationController?.viewControllers[0]) as! UITabBarController
let favMoviesController = tabBar.viewControllers![0] as! FavouritesViewController
targetVC.changeToFavouriteDelegate = favMoviesController
let selectedMovie = filteredMovies[indexPath.row]
targetVC.movie = selectedMovie
targetVC.genreIDs = selectedMovie.genreIDs
self.navigationController?.present(targetVC, animated: true, completion: nil)
}
this just doesn't work...
targetVC.changeToFavouriteDelegate = favMoviesController
Is there anything I can do to solve this problem?
This might not answer your question completely however my intention is to point you in the right direction.
The issue could be one of several and it will be hard to debug without being able to run your code. Here are some possibilities:
Your delegate could be set correctly but because the Favorites View is not on the screen, the UI updates are not executed and you do not do any reloading / refreshing when you go back there
Maybe you are not accessing the desired instance of the Favorites ViewController
One reason notifications would have not worked, maybe you aren't listening to it
etc
It's hard to say easily.
In my opinion, the implementation to achieve what you want is quite complex.
I would recommend to simplify in the following ways:
Simplest solution - Maintain a struct or a class of favorite movies which should be passed around between the classes
One step further would be to look at MVVM as one of the commenters posted and using that with observers (Notifications) to update UI. I recommend checking this stanford lecture which I liked
Might be an overkill at this stage but can read up on CoreData to see if it suits your needs
This is my code. It is an NSObject class and implementing Table View in this to show menus. By clicking on a table view cell I want to navigate to another view controller.
class SideMenuController: NSObject, UITableViewDelegate, UITableViewDataSource {
private weak var navigationController: UINavigationController?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let window: UIWindow = UIApplication.shared.keyWindow!
//print(indexPath.row)
if indexPath.row == 1 {
//print("1 is")
let storybord = UIStoryboard(name: "Main", bundle: nil)
//let nextvc = storybord.instantiateViewController(withIdentifier: "manageprofilevcid") as! ManageProfileViewController
let viewController = storybord.instantiateViewController(withIdentifier: "manageprofilevcid") as! ManageProfileViewController
//let navi = UINavigationController(rootViewController: viewController)
navigationController?.pushViewController(viewController, animated: true)
handleDismiss()
//let ll = navigation
}
}
}
But I'm unable to navigate to another view controller. Is there any code that will work in this NSObject class for perform navigation to specific view controller?
In your NSObect class of SideMenuController you have created a variable navigationController which you are using for navigationController?.pushViewController.
In the main controller you must have created the variable of SideMenuController.
In that add :
self.sideMenuController.navigationController = self.navigationController
and before using navigationController?.pushViewController check if navigationController is nil or not. you have to remove the private declaration to use navigationController variable in main view controller.
You haven't added the code of main view controller so I have answered according to the assumptions. Hope it helps.
You can also go with Protocols created in SideMenuController and write the code of segues or navigation in main view controller.
Verify the following points:
1. Inheriting from NSObject doesn't make any sense. UIViewController already does that.
class SideMenuController: UIViewController, UITableViewDelegate, UITableViewDataSource {
2. Check if you've set the tableView's delegate and dataSource.
tableView.dataSource = self
tableView.delegate = self
3. Where is code for instantiating UINavigationController? In case your SideMenuController is already embedded in a navigationController, there is no need to make a new instance.
Do clarify the use of private weak var navigationController: UINavigationController? here.
I have a Tabbed App with two tabs... the first tab has the main Action, and the second has Settings that can be updated. I am trying to pass some variables data from Settings to the Action tab. Based on some suggestions, I have used the following code for the Update button:
#IBAction func updateBut(_ sender: Any) {
let myVC = self.storyboard?.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
myVC.totTime = totalTime
myVC.timeInt = intTime
self.present(myVC, animated: true, completion: nil)
}
The data does pass to the first view controller, however, the tabs have disappeared on this view now. So, how can I get the tabs back on the screen? I am quite the beginner to any form of app development, and am just trying to learn by doing... the Tabbed App has been created using one of the Xcode New Project templates. Thanks.
try this way
(self.tabBarController?.viewControllers?[0] as! FirstViewController).totTime = totalTime
(self.tabBarController?.viewControllers?[0] as! FirstViewController).timeInt = intTime
self.tabBarController?.selectedIndex = 0
self.tabBarController?.tabBar.isHidden = false try this
A much better way to pass data by using protocols, you can define a protocol like below
protocol PassDataDelegate{
func updateFirstVc(totalTime:String)
}
and in your SecondViewController class have a delegate property like below
class SecondViewController:UIViewController{
myDelegate:PassDataDelegate?//declaration part
#IBAction func updateBut(_ sender: Any) {
myDelegate.updateFirstVc(totalTime:totalTime)
}
}
And finally in your UITabController class implement the protocol
class myTabController:UITabController,PassDataDelegate{
var firstController:FirstViewController? //declaration part
var secondController:SecondViewController?
override func viewDidLoad(){
super.viewDidLoad()
//initialize your view controller here
self.firstViewController = FirstViewController()
self.secondViewController = SecondViewController()
self.secondViewController.myDelegate = self //assign delegate to second vc
self.viewcontrollers = [firstController, secondController]
}
updateFirstVc(totalTime:totalTime){
self.firstViewController?.totTime = totalTime
self.selectedIndex = 0// here change the tab to first vc if you want to switch the tab after passing data
}
}
I am new to iOS correct me If I am wrong,
The reason why the TabBar is not visible since you are instantiating new FirstViewController which is present on top of your TabBar.
TabBar by default does this Job or Add the new viewController to the TabBar Stack.
tabBarController.viewControllers.append(myVC)
For passing the data TabBar holds the reference of all its ViewControllers. So you can set or get in each other ViewControllers like this
var yourData{
set{
let yourVC = self.tabBarController?.viewController[0] as? FirstViewController ?? ErrorClass
yourVC.dataObj = newValue
}
get{
let yourVC = self.tabBarController?.viewController[0] as? FirstViewController ?? ErrorClass
return yourVC.dataObj
}
I have an mainViewcontroller in that one button called get value.Then i am calling dataviewcontroller to select any item in collection view cell.Once user select any cell.That particular dataviewcontroller will dismiss and while dismiss it will have the user selected item name and it will display in mainViewcontroller .Now the view controller is not dismissing.
Here the code :
In my mainViewcontroller:
var SelectedName: String? = ""
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
currentTF.text = SelectedName
}
Now dataviewcontroller :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView.cellForItem(at: indexPath) != nil {
selectedNamesss = allitems[indexPath.row] as String
}
calldismiss()
print(selectedNamesss)
}
func calldismiss() {
if let presenter = presentingViewController as? mainViewcontroller {
presenter.SelectedName = selectedNamesss
}
dismiss(animated: true, completion: nil)
}
Now how can i solve this.My viewcontroller is not dismising and values also not showing.
Thanks in advance ~
My Suggestion is to use "UnwindSegue" technique, which is simply works like "PerformSegue", So you can send back values very easily using prepare for segue and will be back to main controller. Here is tutorial for unwind segue. Or you can find more from google.
For The problem where data is not showing, problem is with this condition:
if let presenter = presentingViewController as? mainViewcontroller {
presenter.SelectedName = selectedNamesss
}
this condition will never be true as the presentingViewController would always be a UINavigationController. so you need to change that may be something like follows:
if let presenter = (presentingViewController as! UINavigationController).viewControllers.first! as? mainViewcontroller {
presenter.SelectedName = name
}
Here i am using viewControllers.first! in condition as mainViewcontroller was at first index of the viewControllers array. Check the viewControllers array and find the index of mainViewcontroller and make changes accordingly.
I made this changes and the code is working for me. I am getting the selected name on the mainViewcontroller.
Also i would like to mention that this is not the right way to transfer data backwards. You can use delegates, blocks or unwind segue to achieve this.
Though this is working i am attaching a gif to show this working.
Regarding the issue where your controller is not being dismissed, i am presenting the controller in following way:
let dataVC = self.storyboard?.instantiateViewController(withIdentifier: "dataviewcontroller") as! dataviewcontroller
self.present(vc, animated: true, completion: nil)
Try this and see if this helps you :)
Suppose I have three view controllers in a Main.storyboard. Two of the three, vc_login and vc_studyDesc load the other view controller using a UIButton with 'present modally' option.
The other one vc_signup has a UIButton, which may go back to the previous controller. To implement this, I used the following methods:
vc_studyDesc has an identifier of studyDesc; I let it pass its identifier to vc_signup. In the same way, vc_login has login as an identifier.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if sender as! UIButton == qnaSignUp {
let signup = segue.destinationViewController as! vc_signup
signup.latestVC = "studyDesc"}}
This one is in the UIViewController class for vc_signup. By referencing a string latestVC, the method determines which VC to move on.
#IBAction func backBtnClick(sender: UIButton) {
print("latestVS: \(latestVC)")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier(latestVC)
vc.modalTransitionStyle = UIModalTransitionStyle.CrossDissolve
print("check check")
self.presentViewController(vc, animated: true, completion: nil)}
The problem I have is that the app gets terminated when vc_studyDesc is called by vc_signup. I found that this is because I missed a significant variable which must be loaded in vc_signup.
vc_studyDesc has some data to be referenced from Firebase when it is loaded. I did this by loading a variable postID from the prior vc to vc_studyDesc; which is vc_list.
So I just saved postID using NSUserdefaults.standardUserDefaults(). It is solved but I'm wondering if there's any way to pass data using the way I used in vc_signup.
As far as I see, I cannot find any way to pass the data into vc_studyDesc.swift; for the vc is chosen by its identifier..
Can I pass the variable I want in the way I want?? And adding tags would be appreciated!
So there are a couple problems with this design.
When you instantiate a viewController you are creating a new instance of that class, and presenting it adds it to the stack. Think of the stack like a deck of cards, you start with one card and then add or remove them, the top card being the visible vc. When you are going back to studyDesc you are instantiating and presenting it so you will have 3 VCs in your stack, of which two are studyDesc (the one you started with and the one you add when you try to go back)
To remove a VC from the stack you can use
dismissViewController(animated: true, completion: nil)
or if you have the VCs in a navigation controller you can use
popViewControllerController(animated: true, completion: nil)
in terms of passing information between viewControllers, if the info is in the VC you use to present your new controller you can use prepareForSegue like you already have. To pass information back you should use a delegate pattern. So to implement a delegate pattern in this case you would do the following:
Declare a protocol (not inside your classes, above there but below your import's)
protocol SignUpDelegate {
signInCompleted(infoToPass: AnyObject)
}
Then have your studyDesc class conform to this protocol and implement the function signInCompleted
StudyDescVC: UIViewController, SignUpDelegate {
func signInCompleted(infoToPass: AnyObject) {
// do what you want with the info here
}
}
Then in your signUpVc add a var delegate (which will be used to call the signInCompeleted function)
class SignInVC: UIViewController {
var delegate: SignUpDelegate!
func finishedSigningIn() {
delegate.signInCompleted(infoToPass: //yourinfo)
self.dismissViewControllerAnimated(true, completion: nil)
}
And then in your prepareForSegue set the delegate
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if sender as! UIButton == qnaSignUp {
let signup = segue.destinationViewController as! vc_signup
signup.delegate = self
}
}
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier(latestVC) as! YOUR_VIEW_CONTROLLER_NAME
vc.modalTransitionStyle = UIModalTransitionStyle.CrossDissolve
vc.name = "Andrew"
print("check check")
self.presentViewController(vc, animated: true, completion: nil)
//set a variale or property to your viewController
class YOUR_VIEW_CONTROLLER_NAME: UIViewController {
var name: String?
}