How to update UIPageViewController's page control count - ios

I have a UIPageViewController with a page control. In the delegate, I implement the following methods:
presentationCountForPageViewController:
presentationIndexForPageViewController:
This works well, but the total number of pages can change, and the number of dots displayed in the page control doesn't change in this case.
How do I tell the page view controller to call presentationCountForPageViewController: to update the total number of dots when this happens?

So it turns out this was a result of using NSFetchedResultsController as the source of the data for pages. The count for NSFetchedResultsController was not getting updated before I called setViewControllers:direction:animated:completion: on the UIPageViewController.
The fix was to call the following function in controllerDidChangeContent: to force an update to the page control.
func refreshPageController() {
let controllers = pageController.viewControllers
if controllers.count > 0 {
pageController.setViewControllers(controllers, direction: .Forward, animated: false, completion: nil)
}
}
Update:
Though the solution above worked, it broke the animation I was using to scroll to the next page when a page was removed.
A better solution: just wait until the page removal is committed to Core Data before calling setViewControllers:direction:animated:completion:.

I had the same problem, and the workaround I found is to reset the uipageviewcontroller view every time I change my dataSoure and reload the whole pageViewController with the new dataSource. This way I'm able to have the correct number of dots (with the dataSource and delegate functions correctly implemented). I do this with the following function :
func reloadPageViewController() {
if pageContentView.subviews.count > 1 {
for _ in 1...pageContentView.subviews.count - 1 {
pageContentView.subviews.last?.removeFromSuperview()
}
}
guard let pageViewController = storyboard?.instantiateViewController(identifier: String(describing: PageViewController.self)) as? PageViewController else {
return
}
pageViewController.removeFromParent()
self.configurePageViewController()
}

Set the dataSource property again to a new instance of a data source object that has the new number of objects.

Related

Load UITable Data from another View Controller

I have two Views:
UITableViewController (View A)
UIViewController (View B)
I was wondering, if it's possible to load and setup the table from View B and then segue to View A, when the loading is done. I need this, since the Table View loads Data from Core Data and that takes some time; I would then show a Loading Animation or something. I have a function called loadData() in View A, which fetches all Elements from Core Data and then calls tableView.reloadData().
Does anyone know, how I could implement this? Or should I somehow show the loading View directly from View A with a SubView or something?
Remember to not think about the specifics but instead, think generally:
You want to move from one VC to another and you have some data that needs to be fetched asynchronically. Let's assume you can't know how long it will take.
My suggestion is to contain all data fetching related to a VC inside that VC itself (or services/facades related to it). So basically you should present the UITableViewController and then have it fetch the data while showing skeleton-cells/spinner/etc.
You want to have separation of concerns which means you don't want your ViewController to handle data related to another view controller.
Think about the following use-case: if you have code to fetch data in the previous VC, before presenting the TVC, what happens when you need to re-fetch the data or refresh something? You will have to duplicate the code in both the VC and the TVC.
That's why it's suggested to keep data fetching inside the view controller that needs it.
If, for some reason, you still want to have your answer for this specific question:
You can have the initial VC create the TVC, but not present it yet, call its methods to fetch the data, and have it send a callback (closure/delegate/etc) when it's done fetching. When the fetching is done, present the TVC.
Here is a quick example:
class MyTableVC: UITableViewController {
private var myData: [Int] = []
public func fetchData(completion: () -> Void) {
//Fetch data asyncly
myData = [1, 2 ,3]
completion()
}
}
class MyVC: ViewController {
private func loadTableVC() {
let tableVC = MyTableVC()
tableVC.fetchData { [weak self] in
self?.present(tableVC, animated: true, completion: nil)
}
}
}
Again, I wouldn't use this due to having tight coupling between the 2 view controllers, but it's always up to you to decide how to design your code.

IOS swift UIBarButtonItem action

I've a table view with navigation controller embedded in. I've added a UIBarButtonItem (add) button. When I click this button it opens a new view where user enters the data and submits it (which makes a web service call) and returns back to the previous view. This navigation happens as shown below,
func addTapped(_ sender:UIBarButtonItem) {
print("Called Add")
let vc = (storyboard?.instantiateViewController( withIdentifier: "newNote")) as! newNoteVC
self.navigationController?.pushViewController(vc, animated: true)
}
And in new view I do following,
#IBAction func saveButton(_ sender: UIButton) {
if (self.noteDescription.text?.isEmpty)! {
print("Enter missing note description")
return
} else {
let desc = self.noteDescription.text
self.uploadNote(noteText: desc!, noteDate: self.dateInMilliseconds)
self.navigationController?.popViewController(animated: true)
}
}
This way a record gets saved and a view gets popped from the navigation controller stack but only thing I don't how to do is refresh the table view data in the parent view (where I might need to make a new http service call to read all the records).
I hope I'm able to explain the issue? Any help is appreciated.
As mentioned in the comments, making a service call just to update the tableview might be a overkill. However, if this is the business scenario which needs to be implemented, you can do the same in
func viewWillAppear
in the parent view controller. You can make the service call in this method and reload the table view with the data.
You would also need to check the overall navigation of the application as making service calls in viewWillAppear method is not a good approach as these gets called everytime the view is shown. For Ex: If coming back from a navigated view then also the method is called.

Empty a UIPageViewController (remove ViewControllers inside)

How can I remove all the viewControllers inside a UIPageViewController in Swift?
Those calls:
pageViewController.setViewControllers(nil,....)
pageViewController.setViewControllers([UIViewController(),....)
both make my app crash with the following message:The number of view controllers provided (0) doesn't match the number required (1) for the requested transition
A UIPageViewController does not "hold view controllers" in the sense you are thinking of. All it does is display a view controller returned by its dataSource ... and its dataSource is your code.
If you simply do this:
[pageViewController setViewControllers:#[UIViewController.new] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
That will replace the current "page" with an empty UIViewController - but it won't do anything else. You'll still be able to scroll, and depending on your next/previous logic, that may result in other errors.
What you'll want to do is "clear" the current page with that line, but then also prevent scrolling / swiping. This is often done by setting the UIPageViewController's dataSource to nil.
What Don explained, for 2020:
As #DonMag has explained. Really all you can do is
Simply replace the current VC you are seeing, with, a blank one.
Set the datasource of the page view controller to null
Important caveats ..
• What does it really mean to have a "blank" VC in your app? It could be, you make as special view controller (which is just "gray clouds" or "an explanation that there is no data" or something) which you use.
• But you can in fact safely just use UIViewController() and it will be blank, nothingness.
• With point 2, don't forget to set it back!
In practice... something like...
var vcs: [UIViewController] = []
func reloadPages() {
print("reload pages in your page view controller")
let k = yourData.count
if k == 0 {
dataSource = nil
setViewControllers([UIViewController()], direction: .forward, animated: false)
return
}
dataSource = self
vcs = []
for i in 0..<k {
let vc = _sb("SomePageOfYours") as! SomePageOfYours
vcs.append(vc)
}
setViewControllers([vcs[0]], direction: .forward, animated: false)
}
In the last line of code, don't forget it's an array of only the first item, not the whole array.
Aside - the call '_sb' is just something that loads your view controller from it's storyboard. Example,
func _sb(_ s: String) -> UIViewController {
// assume the storyboard id is set as ClassNameID
return UIStoryboard(name: s, bundle: nil)
.instantiateViewController(withIdentifier: s + "ID")
}

ViewDidLoad is always called

I just recently started to use Swift and am facing a "weird" bug with the viewDidLoad method.
My very simple app currently only has 2 viewcontrollers:
MainViewController, which is the Apps entry point and serves as an overview for the data which has already been created. It also provides the option to add new data, which would trigger a segue to
DataViewController, which provides the UI to create new data, after which it goes back to the MainViewController
Now my issue is that the viewDidLoad method of MainViewController is always called whenever the MainViewController appears (At the start of the app and every time the DataViewController disappears). Particularly, the msg "MainViewController newly created" is always printed.
Even worse, it seems that my app is "secretly" resetting. To show this I have defined the class variable "createView" in my MainViewController which is true by default, and is set to false during the viewDidLoad (the only place where this variable is called/set). However the msg "MVC newly created" is still always printed in the output after the MainViewController shows up. How can that be? Why / how is createView reset to true?
Hope this snippet is enough to find the issue. Otherwise, let me know if something is missing.
Thanks for your help!
override func viewDidLoad()
{
super.viewDidLoad()
if (createView)
{
determineArraySize()
createDataArray()
print("MainViewController newly created")
createView = false
}
else {print("Nothing happened")}
}
As #moritz mentioned in the comments, check out the way you present the DataViewController in your storyboard.
If the view is presented modally, you should call:
dismiss(animated: true, completion: nil)
If the view is presented using a show seque, you should call:
_ = navigationController?.popViewControllerAnimated(true)

UIPageViewController initial swipe called twice

The below function is how I display different views when a user swipes right where
self.viewControllerAtIndex() is my own custom function that returns a view. The problem is that the first swipe outputs "---------swipe Right before 0" twice. And then works perfectly like expected afterwards.
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
print("---------swipe Right before " + String(index))
index += 1
print("---------swipe Right " + String(index))
if index == (products!.count) {
index = 0
}
return self.viewControllerAtIndex(index:index)
}
=====CONSOLE OUTPUT=====
---------swipe Right before 0
---------swipe Right 1
---------swipe Right before 0
---------swipe Right 1
func viewControllerAtIndex(index: Int) -> ViewController{
return ViewController.init(id: index)
}
For some reason, every other swipe after the first works as expected. The initial swipe is what causes the console output above. This causes my view sequence to look like below (2 views)
First Swipe
View1
Second Swipe
View1
Third Swipe
View2
Fourth Swipe
View1
Fifth Swipe
View2
I'm also initiating my uiPageViewController like so
let array = [ViewController](repeating: ViewController.init(videos: (self.videos![self.products![self.index].id]!)), count: 1)
self.UIViewPageController?.setViewControllers(array, direction: UIPageViewControllerNavigationDirection.forward, animated: false, completion: nil)
So I'm creating a new ViewController on viewDidLoad and then when a user swipes, I'm creating new ViewControllers
I think you should avoid doing index state management in this method. Apple does not guarantee the circumstances under which this method is called. If I had to paraphrase, I would say that while the framework is asking you for the view controller that should come after the provided one, you are answering a different question: Which view controller comes after the one you previously told us about?
To get around this, you need to use your original array of view controllers, find the index of the passed-in view controller in that array, and return the view controller that comes next (or nil if you are at the end of the array).
Too long for a comment ...
Chris Trahey's is the correct answer.
I had the same problem, based on the misunderstanding that the viewControllerBefore/viewControllerAfter methods meant "What should happen if the user swipes right/left". Like you said, these methods only answer the question: "Which view controller comes before/after a given view controller?" They should only answer that question, without any side effects.
A solution for some scenarios would be to add an index property to the view controller class, and set that index on creation. That's useful if you're not working directly with an array of view controllers, but create them on the fly. Then, instead of saying array.index(of: vc), you can say vc.index.
See here: https://stackoverflow.com/questions/25795093/swift-uipageviewcontroller-always-repeats-second-viewcontroller#=
EDIT:
Oh, and if you want to actually react to a page turn, you can do that in pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:).

Resources