How to pass data between unrelated ViewControllers - ios

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

Related

Access same View Model across multiple tabs in Swift

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.

Push New VC from Subview Table View Controller

I have an Arranged Subview (a Table View) within a parent View Controller.
Let's call the parent VC CollectionViewController and the subview (Table VC) ResultsTableViewController.
I've added the table view to my stackview (in CollectionViewController) like this:
let hitsTableController = ResultsTableViewController()
stackView.addArrangedSubview(hitsTableController.tableView)
Then I've implemented didSelectRowAt in the ResultsTableViewController (the subview):
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let hit = hitsSource?.hit(atIndex: indexPath.row)
print("The ID is " + hit?.id)
let vc = self.storyboard?.instantiateViewController(withIdentifier: "ItemDetailViewController") as? ItemDetailViewController
self.navigationController?.pushViewController(vc!, animated: true)
}
}
I'm able to print information about the cell I select (the ID in this case), but I can't figure out how to navigate to the Details View Controller when a cell is pressed.
I've tried this with no luck:
let vc = self.storyboard?.instantiateViewController(withIdentifier: "ItemDetailViewController") as? ItemDetailViewController
self.navigationController?.pushViewController(vc, animated: true)
The code runs, but nothing happens. Any help would be appreciated.
SOLUTION
Add this
addChild(hitsTableController)
hitsTableController.didMove(toParent: self)
before this
stackView.addArrangedSubview(hitsTableController.tableView)
As Frankenstein mentioned below, the problem was that ResultsTableViewController had no access to the navigation stack. That's because I had to make it a child of the parent VC, CollectionViewController. Now I'm able to call this without any problems.
self.navigationController?.pushViewController(vc!, animated: true)
try to force unwrap your objects if the app crash then give us the crash details
let vc = self.storyboard!.instantiateViewController(withIdentifier: "ItemDetailViewController") as! ItemDetailViewController
self.navigationController!.pushViewController(vc, animated: true)
Update: With the details that you've added it has become evident that the issue was that the view controller isn't embedded in a UINavigationController. You see that navigationController is an optional and it get allocated only if the controller is embedded in a UINavigationController. Try this:
let hitsTableController = UINavigationController(rootViewController: ResultsTableViewController())
stackView.addArrangedSubview(hitsTableController.tableView)
Seems the storyboard either has no ItemDetailViewController in it or you've forgotten to set the identifier which seems unlikely in your scenario. Hence I conclude that the ItemDetailViewController is created programmatically. So you should be initializing ItemDetailViewController with the empty initializer for presenting it.
let vc = ItemDetailViewController()
self.navigationController?.pushViewController(vc, animated: true)

Dismiss the view controller which have navigation controller and pass the value back to main view controller

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

How can I pass data to the next view controller when it is loaded programmatically?

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

Attempt to present whose view is not in the window hierarchy when a cell is tapped

I'm currently working on an app with a UITableViewController. The idea is that when a cell is tapped on, an info page will pop up showing some information about it. So when tapped on, I attempt to present another ViewController in another class. However, the warning shows up and I'm not sure why.
I've tried looking at questions similar to mine on Stackoverflow and other websites, but they don't make much sense to me. Can anyone explain the problem behind this warning and how to fix it?
Some of my code:
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
print("showing item info")
let cell = tableView.cellForRow(at: indexPath)
currentInfo = (cell?.textLabel?.text)!
currentDes = descriptions[indexPath.row]
currentBool = itemBools[indexPath.row]
currentIndexPath = indexPath
showInfo()
}
func showInfo() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ShowInfoViewController") as! ShowInfoViewController
self.present(vc, animated: true, completion: nil)
}
Warning message is:
Attempt to present <Things_to_Do.ShowInfoViewController: 0x100b2fe20> on <Things_to_Do.TableViewController: 0x100b05eb0> whose view is not in the window hierarchy!
Try to add presentation style
Swift 2
vc.modalPresentationStyle = .FormSheet
vc.modalTransitionStyle = .CoverVertical
You are trying to present the ViewController from a class which is not there in the window hierarchy. You need to check self.present(vc, animated: true, completion: nil) whether the self in this line is a ViewController which is added to the window hierarchy before you execute that line.
I found my problem. The problem was cause I had created a segue on the storyboard, connecting a table cell to the new ViewController. By writing code to present the same ViewController, XCode got confused.

Resources