Checking approach: Managing UIPageViewController page data using UITableView cells - swift

I've implemented a UIPageViewController that allows the user to add/remove/delete new "pages" via a separate UITableView, similar to how Apple implements Cities/Weather Locations in the iPhone Weather app. Project is posted below. There is no weather or fancy UI in it at this point – I'm just trying to focus on the UIPageViewController management below. Hopefully this is useful if anyone wondering how to implement UIPageViewController with pages managed via UITableView:
https://github.com/gallaugher/PageViewControllerDemo
It seems to work fine, but I'm new to this & quite uncertain if I've done this using recommended approaches or if it's "Swifty".
Right now I have:
PageViewController [Initial View Controller]
- Sets delegate & data source to self
- Instantiates first UIViewController (imagine a CityViewController for local weather, even though details & UI not added in this example) & sets initial values.
- Cities (weather locations) are kept in a [String] array: citiesArray
In CityViewController - when "Cities" button is clicked in lower-right (created in interface builder), like Apple Weather, it opens a UITableView (CityListViewController).
- To get to this CityListViewController, I trigger a segue drawn directly via the interface builder from the "Cities" button in CityViewController to the CityListViewController, presenting modally.
- preapareForSegue passes citiesArray to the destination CityListViewController (UITableView).
In CityListViewController (the UITableView)
- User can add cities, move, delete in UITableView updating tableView & array
- Clicking a tableView row (a city's name or "Local Weather") triggers a perform segue, unwinding to CityViewController, getting source CityListViewController and using this to pass data back to the CityViewController (e.g. citiesArray = controller.citiesArray).
- This #IBAction unwind function calls an unwind to the PageViewController (really does nothing more than pass data through from the TableView in CityListViewController to the UIPageViewController PageViewController).
Unwind in PageViewController
- Grabs source (as CityViewController)
- Passes key data back (e.g. citiesArray = controller.citiesArray)
- Calls a function to instantiate view controller for the current page & set PageControl index, etc.
Q1:
While this seems to work & I haven't managed to break it during testing, is it a sound approach to go from UITableView, unwinding to a ViewController that simply triggers another unwind to the UIPageViewController, with nothing done other than pass data through?
Q2:
I've implemented the UIPageControl by building it programmatically in the PageViewController, but the button that segues to the CityListViewController (the UITableView) was created in the CityViewController using Interface Builder. Is this the proper approach? I couldn't seem to get both of these created within the same VC.
Thanks so much for those who had the patience to wade through this convoluted explanation. Still trying to get a handle on data passing among VCs, and how this relates to PageControllers & the TableViews.

For this interested in the solution I've posted to GitHub at:
https://github.com/gallaugher/PageViewControllerDemo
I've posted with more generic names - PageVewController, DetailViewController, and ListViewController, so this is easier to reuse and perhaps follow.
Thanks to Sazan Dauti for answering this question outside of StackOverflow. It is possible to segue directly between the PageController & the ListController that contains a TableView for managing pages an array of similar pages (e.g. like Apple does in the Weather app where you can add/delete/move cities), keeping the Detail (Cities in original example) free of any knowledge that it's in a PageView.
- Drag a segue directly from the UIPageViewController to the UIViewController with the TableView that contains, in my case, a list of locations (not the detail that's managed by the PageView). The segue should present modally. Key variables should be passed to the ListViewController in a prepare for segue in the PageViewController like this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ToListViewController" {
let controller = segue.destination as! ListViewController
controller.listArray = listArray
controller.currentPage = currentPage
}
}
Be sure the values passed (listArray & currentPage in the case above) are declared in ListViewController.
Add an Unwind method to the PageViewController similar to this:
#IBAction func unwindFromListViewController(sender: UIStoryboardSegue) {
pageControl.numberOfPages = listArray.count
pageControl.currentPage = currentPage
setViewControllers([createDetailViewController(currentPage)], direction: .forward, animated: false, completion: nil)
}
setViewControllers above refers to the function written in the PageViewController that contains the logic to instantiate the DetailViewController for the current page.
Return segue is created by dragging from a TableViewCell in the ListView to the "Exit' button (far right of the three buttons at the top/title view of this UIViewController). You'll be asked the name for an unwind method that should be in the PageViewController (in the example above I've called it unwindFromListViewController), so be sure to add this method before trying to make the segue.
Add a prepare for segue function to the ListViewController:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ToPageViewController" {
let controller = segue.destination as! PageViewController
controller.currentPage = currentPage
controller.listArray = listArray
}
}
and in tableView didSelectRowAt, trigger the perform segue:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
currentPage = indexPath.row // set current page to row selected
performSegue(withIdentifier: "ToPageViewController", sender: self)
}
I couldn't find any decent examples online demonstrating how this was done, so hopefully this is understandable and efficient. Any corrections are welcome. Cheers!

Related

CollectionView within a CollectionView: Using didSelectItemAt to performSegue

I have a collectionView within a collectionView and I want to be able to select a cell in the 2nd collectionView to perform a segue to another ViewController.
Currently, when I select a cell, I get the following message:
" Receiver ... has no segue with identifier 'ToVC2'. "
However, I have used this segue/identifier from other UIButtons and it works.
I have two ViewControllers: ViewController1 and ViewController2.
On ViewController1, there is a collectionView ("categoryCollectionView") which has vertical scrolling.
Within categoryCollectionView, there is another collectionView ("eventCollectionView") which allows horizontal scrolling.
The two collectionViews are set up and working correctly for numberOfItemsInSection and cellForItemAt. I now want to be able to select a cell within eventCollectionView, and cause a segue from ViewController1 to ViewController2.
I have added a function in ViewController1:
func segueToViewController2(event: Event){
performSegue(withIdentifier: "ToVC2", sender: event)
}
Within eventCollectionView's didSelectItemAt, I have tried the following:
var viewController1: ViewController1? = ViewController1()
viewController1.segueToViewController2(event: eventSelected)
When I select a cell, I get the following error message:
'Receiver () has no segue with identifier 'ToVC2''
However, this function performs the segue correctly if called from a regular UIButton on ViewController1 (therefore I know the issue is not that there is no segue / the identifier is wrong.) I believe the issue is that the function is being called from a collectionView within a collectionView.
Please help!!!!!
I believe the problem is that when you call var viewController1: ViewController1? = ViewController1(), you are initializing a new instance of ViewController1. This is probably not what you are meaning to do. Based on what you have said, you should pass a reference down the hierarchy of collection views you have created so that your didSelect can call the segue function on the original instance of ViewController1. Ideally, you would use a design pattern like delegation to do this.

Programmatically press back button for UIViewController with UITableView iOS swift

I have a UIViewController that implements UITableViewDelegate, UITableViewDataSource and that contains a UITableView as a member variable. When a user click on one of the rows of that table, the app performs a storyboard segue to open the detail view controller. That detail view controller of course has a button in the top left of the screen that is the "back" button to go back up to the UIViewController with the UIViewTable.
So, suppose that I want to programmatically "click" that back button. How exactly would I do that in swift? This is the most recent version of swift (swift 4?) in XCode 10.1.
UPDATE:
So here is how I solved this. As the answers below show, it is possible to use self.navigationController?.popViewController(animated: true) to just return to the previous view controller. What I discovered I also wanted to do, however, was to call a specific method in that view controller so that it executed a certain behavior once it got shown. It turns out that is also possible, but in my case it was a bit tricky, since that prior view controller was actually a UITabBarController. Therefore I had to get the ViewController that I was interested in from the UITabBarController. I did it like this:
let numvc = navigationController!.viewControllers.count
let tvc:UITabBarController = navigationController!.viewControllers[numvc-2] as! UITabBarController
let my_vc: MyCustomVC = tvc.viewControllers![0] as! MyCustomVC
my_vc.some_function()
Here of course MyCustomV is my custom view controller class and some_function() is the method I want to call on that class. Hope this helps someone.
When You run a segue you perform a "pushViewController" method to the next view, so if you want to go back to the previous view programmatically you just have to do is pop the last view like so:
self.navigationController?.popViewController(animated: true)
UPDATE
You just need the if statement if you have multiple segues from that viewController, if not, you can delete and just cast the next view as you wish and set the properties, let the autocomplete write the *prepare(for segue... * method for you, so You don't run into any problems
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "yourSegueName" {
let destinationVC = segue.destination as! CustomViewController
destinationVC.labelExample.text = "Some text I'm sending"
}
}
Are you sure you need to "click" the button?
If all you need is to dismiss details view controller, you can just call navigationController?.popViewController(animated: true)
Or if you want to deal directly with button, you can tell it to send its actions: backButton.sendActions(for: .touchUpInside)
Or if you absolutely need to show button clicking animation, then you will need something like this (you should play and choose suitable delay):
backButton.isHighlighted = true
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
backButton.isHighlighted = false
backButton.sendActions(for: .touchUpInside)
}

UISplitViewController - Lost reference to detail view controller on resize

I have seen plenty of questions similar to this - but not quite the same..
So I have a UISplitViewController, and the detail view controller has a lot of heavy drawing in it, so I don't want to reinit it every time I select a new row on the master view. So I observe when the user selects a row, and only perform a segue if it doesn't see a detail view controller.
Here's where the problem lies... On the iPad, if I resize the view via multi-tasking, eventually the UISplitViewController only shows the master. But when I select a row, it thinks the detail view controller is nil and allocates a new detail view controller (performs the segue). The thing is that the detail view controller still exists, It just doesn't show up on the split view controller's childViewControllers. I want to perform a segue when in the compact size, but I don't want it to recreate a new view - I want it to use the view that the split view controller is holding.
Sorry if this seems confusing.
Thanks
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// try to get my custom detail view controller
if let myVC = splitViewController?.detailViewController?.childViewControllers.first as? MyViewController {
let selectedRow = indexPath.row
myVC.doSomething(selectedRow)
} else {
// perform segue if it's not available
performSegue(withIdentifier: "showMyVC", sender: self)
}
}

MVVM RxSwift way to send data from main view to detail view controller?

I have a view model that has an element that returns an observable array after calling an API.
I then find that result to a table view to display it. The problem I am having is how to call the detail view controller on the specific cell that is clicked. I bound the results with:
let queryResults = eventsViewModel.mainTableItems
queryResults
.bind(to: collectionView.rx.items) { collectionView, row, item in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: IndexPath(row: row, section: 0)) as! EventCell
cell.heroID = "heroCellID"
cell.restaurantNameLabel.text = item.name
cell.restaurantDetailLabel.text = item.location
cell.timeLabel.text = item.date
cell.restaurantImageView.kf.setImage(with: URL(string: item.image))
return cell
}
.addDisposableTo(disposeBag)
I have no way to access the specific element in this observable array that was clicked. It says an Observable array cannot have subscript.
This is the code that says that:
vc.festival = queryResults.value[indexPath.row]
I am still new to RxSwift and I am struggling to understand this.
This really is the million dollar question, and something that has a lot of different answers and none are really simple, even without using RxSwift. The short answer is, it depends on how your app is architected.
First thing, if you haven't already realized, is that you find out which item was selected with tableView.rx.itemSelected.
IMHO, view controllers should be independent of each other so the one thing you don't want to do is create or segue to the detail view controller from this one. There should be some sort of coordinator object that subscribes to itemSelected and is in charge of deciding where to go from there.
Here are some good articles to get you on the right track:
http://rasic.info/a-different-take-on-mvvm-with-swift/
In this article, Mr. Rasic talks about a class that he calls the Scene which is in charge of creating the view controller and its view mode, attaching them and then deciding where to go from there.
http://khanlou.com/2015/01/the-coordinator/
In this article, Mr. Khanlou talks about a class that he calls the Coordinator. He doesn't use Rx, instead he uses delegates, but it's pretty easy to see how it would relate.
https://talk.objc.io/episodes/S01E05-connecting-view-controllers
This is a video where the objc.io team creates an App class that takes care of view controller navigation. They use closures here instead of Rx, but again the correspondence should be obvious.
Depending on the complexity of your app, as Daniel suggested- creating a high level coordinator object may be the best option. One simple option is to create a property in eventsViewModel and assign a value to it from within tableView.rx.itemSelected. Then when you prepare for segue, you can retrieve object from eventsViewModel, like so:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "identifier" {
let detailVC = segue.destination as! DetailViewController
if let selectedItem = eventsViewModel.item {
detailVC.viewModel = DetailViewModel(item: selectedItem)
}
}
}
probably not the best solution but it does work as intended.

What is the best way to reload a child tableview contained within a parent VC that is being fed fresh data?

I have been playing with the concept of the parent/child view delegation for a few days now, and currently understand how to feed data from parent to child. However, now, I want a button in the parent (main VC) to reload the data presented in the child VC.
I'm trying to delegate a method that is activated in the child VC's class but is activated in the parent's navigation controller. So that when I press the button, the delegated method in the child VC is performed; in my case, that method would be reload table. Why am I getting so many errors when trying to set up this simple delegation relationship?
My parent/container View is currently delegating a method to the child, so I have it set up from child -> parent. But I want to set it up from parent -> child. Pretty much I have:
struct Constants {
static let embedSegue = "containerToCollectionView"
}
class ContainerViewController: UIViewController, CollectionViewControllerDelegate {
func giveMeData(collectionViewController: CollectionViewController) {
println("This data will be passed")
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == Constants.embedSegue {
let childViewController = segue.destinationViewController as! CollectionViewController
childViewController.delegate = self
}
}
FROM CHILD:
protocol CollectionViewControllerDelegate {
func giveMeData(collectionViewController: CollectionViewController)
}
class CollectionViewController: UIViewController {
var delegate:CollectionViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
self.delegate?.giveMeData(self)
// Do any additional setup after loading the view.
}
I think my trouble is the fact that I'm declaring the child delegate in a prepareforsegue, so that was straight forward, but now I want the reverse delegation. How do I set that up so that I can use a child-method from the parent VC?
The child view controller has no business supplying other controllers with data. It should actually not even have any data fetching logic that is so generic it is also used by other controllers. You should refactor the data methods out into a new class.
This pattern is called Model-View-Controller, or MVC, and is a very basic concept that you should understand and follow. Apple explains it pretty well.
In general, to send data to from a controller to a detail controller, use prepareForSegue to set properties, etc. To communicate back to the parent controller, you use delegate protocols, but usually these are called when the detail controller is finished with its work and just reports the result up to the parent.
If you want to update the detail VC with new data (without dismissing it and with the parent not visible) you should not put the logic to update it into the parent. Instead, use the structure suggested above.