So I'm working on an App right now and one of the specs of this app is to have a swipe between views functionality on specific pages kind of like the camera on Instagram.
My current solution is to set the initial view controller as a UIPageViewController which has two view controllers one is the main tab bar controller (first image) for the whole app and the second view controller is the one shown in pink in the image above, and then to enable and disable sliding functionality for the PageViewController depending on whether the current view is meant to have access to the second 'pink' view controller. (PS open to a totally different architecture than this if anyone knows of one, this is just the best I could do given my limited knowledge on iOS)
Normally every thing works fine. However, when I move my finger's in a very particular pattern, the entire app stops functioning. The pattern is shown below:
The pattern is basically:
Swipe slowly to the left a little bit until part of the second 'pink' view controller is showing (image 2 above)
Swipe quickly to the right causing the empty space to the left of the main view controller to show (image 3)
Quickly Let go and let the main view controller fall back into place (image 4)
(*edit - Probably worth noting that if I do this same pattern slowly instead of quickly sliding right and letting go everything works just fine)
(PS if there's a way to upload a screen recording let me know)
Anyways, If I do this just one time, my entire app stops working. Basically every page stops loading data, and any time I click on a button on the main page (eg. the likes button shown in the images) I get an "Unbalanced calls to begin/end appearance transitions for AppName.ViewController" and the new view controller shows up with no data.
Furthermore, as soon as I swipe over to the second page 'pink' view controller and then back to the main app, everything works again.
I don't really know what code is relevant to this problem (spent about 5 hours trying to figure that out with no luck), so I'm just gonna post my UIPageViewController class for now, if you think the problem is coming from somewhere else let me know and I'll post that code.
// MARK: -> Properties
class PageViewController: UIPageViewController {
var pages = [UIViewController]()
}
// MARK: -> Lifecycle
extension PageViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
setViewController(withIdentifier: "MainTabController")
setViewController(withIdentifier: "SecondaryPage") // The 'Pink' screen
setViewControllers([pages.first!], direction: UIPageViewControllerNavigationDirection.forward, animated: false, completion: nil)
}
}
// MARK: -> Helpers
fileprivate extension PageViewController {
func setViewController(withIdentifier storyboardIdentifier: String){
let page: UIViewController! = storyboard?.instantiateViewController(withIdentifier: storyboardIdentifier)
storyboard?.configure(viewController: page)
self.pages.append(page)
}
}
// MARK: -> UIPageController Data Source
extension PageViewController: UIPageViewControllerDataSource {
func presentationIndex(for pageViewController: UIPageViewController)-> Int {
return pages.count
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let cur = pages.index(of: viewController)!
if cur == 0 { return nil }
let prev = abs((cur - 1) % pages.count)
return pages[prev]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let cur = pages.index(of: viewController)!
if cur == (pages.count - 1) { return nil }
let nxt = abs((cur + 1) % pages.count)
return pages[nxt]
}
}
Update
Not sure what exactly this means but I'm sure it's relevant. I added this extension and method to my PageViewController
extension PageViewController: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
print(completed)
}
}
(and then obviously set the delegate to self in viewDidLoad)
This printed true and false as you would expect except when I swiped in the pattern I described above, in which case it never even fired the method and printed nothing at all.
Related
There are many SO questions talk about preload but seems none of them discuss how to do this during initialize.
I want to preload the next page when UIPageViewController is initialized.
For example, I initial UIPageViewController with the following code.
// [firstVC, secondVC]
setViewControllers([firstVC], direction: .forward, animated: true)
Unfortunately, datasource delegate function won't be called at the moment.
They will be called when user interaction involve.
In another word, when user swipe page.
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?
I tried to "scroll" view programmatically.
It shows blank, there is nothing on the scrollView at the region.
// self is a UIPageViewController
if let scrollView = self.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView {
var offset = scrollView.contentOffset
offset.x += 200
scrollView.setContentOffset(offset, animated: true)
}
Also tried to set 2 viewControllers when initialize but app crash.
setViewControllers([firstVC, secondVC], direction: .forward, animated: true)
Can I preload without user interaction?
Update.
So when user open the pageVC, I want to do a small swipe to user to demo this new feature, like the left image, since user doesn't know they can swipe so no user interaction at the moment.
If I can't preload the next view, it will look like the right image (white means nothing in this sample), empty view will make user confused, they could think this is a bug.
Update:
Github
Problem: UIPageViewController navigation between pages works fine but:
1) Navigation dots view not showing up
2) Some dataSource methods not called (probably cause for 1.)
I have following UI setup (basically UIPageViewController in UITableViewCell)
UITableViewCell <- UIViewController <- ContainerView <- UIPageViewController
Problem:
CALLED OK:
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?
NOT CALLED:
func presentationCount(for pageViewController: UIPageViewController) -> Int
func presentationIndex(for pageViewController: UIPageViewController) -> Int
All setup done after viewDidLoad:
dataSource = self
setViewControllers([viewController], direction: .reverse, animated: false, completion: nil)
Note: I'm using another UIPageViewController somewhere else in the app directly on the content view -> there all delegate methods are working fine. Another (probably directly related) problem is that navigation dot view is not visible. (Again works fine in case where not embedded).
Anybody experience anything like this?
Why would some dataSource delegate methods be called while others would not called?
I've been looking at this code and all I could find in google for hours but can't move forward.
UIPageViewController Implementation
A page indicator will be visible if both methods are implemented, transition style is 'UIPageViewControllerTransitionStyleScroll', and navigation orientation is 'UIPageViewControllerNavigationOrientationHorizontal'.
Both methods are called in response to a 'setViewControllers:...' call, but the presentation index is updated automatically in the case of gesture-driven navigation.
According to Apple Documentation you need to implement both methods and make sure that the controller's transition style is "scroll". Then the Page Indicator will be visible and the functions are called. Please check this.
for the visibility of the pageindicator. Add below code in viewdidload
var pageControl = UIPageControl.appearance()
pageControl.pageIndicatorTintColor = UIColor.lightGray
pageControl.currentPageIndicatorTintColor = UIColor.white
pageControl.backgroundColor = UIColor.black
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return orderedViewControllers.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
if let currentVC = self.viewControllers!.first {
let currentIndex = orderedViewControllers.index(of: currentVC)
return currentIndex!
}else{
return 0
}
}
According to Apple Documentation you need to implement both methods and make sure that the controller's transition style is "scroll". Then the Page Indicator will be visible and the functions are called. Please check this.
Actually Vishal stated this, but not very clear :)
I have a viewcontroller (FirstVC.swift) with a container view, in the upper part, that has an embedded pageviewcontroller (SecondPageVC.swift), and a view that has 3 buttons in the lower part.
At first, the only button visible is the middle button and other two is hidden. If the user reaches the last page, the other two buttons should appear. How do I pass the bool value that will make the buttons appear?
In my SecondPageVC.swift
func pageViewController(pageViewController: UIPageViewController,
viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
if index == myViewControllers.count-1{ //if last page
hide = true
}
Define a protocol called PageDelegate like this :
protocol PageDelegate { func hasReachLastPage(hasReached: Bool) }
Create a delegate var in your SecondPageVC like this : weak var delegate: PageDelegate?
On your pageViewController func called your func hasReachLastPage(hasReached: Bool) like this :
func pageViewController(pageViewController: UIPageViewController,
viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
if index == myViewControllers.count-1{ //if last page
delegate?.hasReachLastPage(true)
}
On your FirstVC makes it adopt your protocol that you just create like this : FirstVC : PageDelegate
Then, on your FirstVC when you have an instance of your SecondPageVC you are going to set your delegate variable to that FirstVC like this : SecondPageVC.delegate = self
You are saying that FirstVC is going to handle SecondPageVC delegate methods
Finally always on your FirstVC you have to implement the body of func hasReachLastPage(hasReached: Bool) by hidding or not your two buttons like this :
func hasReachLastPage(hasReached: Bool) {
if hasReached {
// Unhide two buttons
} else {
// Hide buttons or whatever
}
}
This is how you implement the delegate pattern. It's something very used and very useful to pass data between view controllers.
So, if I understand correctly, you just want to pass this hide property to the instance of FirstVC? If so, I would advise you to define a protocol SecondPageVCDelegate with a method secondPageVCDidReachLastPage:. Assigning the delegate can be done in prepare for segue method of the FirstVC.
I have a UIPageViewController (custom one) inside a Container located in a regular UIViewController. i need to be able to call an event with each Page Change but ONLY if it really did change and not only half way or anything of that sort.
using:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController?
is unreliable and not called each time for some reason.
if your answer contains anything about willTransitionToViewControllers or didFinishAnimating please elaborate and not just mention them, since i already know they exist but dont understand the proper way to use them.
Thank you
Use didFinishAnimating it has a completed and finished property so you know the page has actually changed. From the pageViewController you can get the currently displayed page, then get the position of this VC in your model.
First make sure your ViewController adopts UIPageViewControllerDelegate
Set the delegate (e.g. in viewDidLoad)
pageViewController.delegate = self
Then implement the following function:
func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if (completed && finished) {
if let currentVC = pageViewController.viewControllers?.last {
let index = myViewControllers.indexOf(currentVC)
//do something with index
}
}
}
I am looking to create a PageViewController very similar to SnapChat's one, whereby you can swipe from the UIImagePickerController to another VC. To do this, I have my initial VC which displays the imagepickercontroller, and a second VC (a caption VC) which I want to come after this initial VC. To encapsulate my PageViewController, I have created another VC class (shown below) which I have now set as my initial VC, and I am trying to handle the PageVC data source.
For whatever reason, it is not working and the error - 'fatal error: unexpectedly found nil while unwrapping an Optional value' occurs. Is this because you can't contain an imagePickerController in a PageVC (doubtful as SnapChat do). I created a simpler template which contained two simple VCs perfectly - why can I not do this here? The other one I did, I contained all the below code in the initial VC that the project starts with, whereas here I created an additional VC and manually changed it to make it the 'initial view controller'.
NB. the project compiles fine without the pageVC so it is nothing to do with any bad code in the other VCs.
I am very stuck and would hugely appreciate some help to this tricky issue. Thanks!
class PageViewController: UIViewController, UIPageViewControllerDataSource {
private var pageViewController: UIPageViewController?
private let VCarray = [ViewController(), CaptionViewController()]
override func viewDidLoad() {
super.viewDidLoad()
createPageViewController()
}
private func createPageViewController() {
let pageController = self.storyboard!.instantiateViewControllerWithIdentifier("PageController") as! UIPageViewController
pageController.dataSource = self
if VCarray.count > 0 {
pageController.setViewControllers([ViewController()], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
pageViewController = pageController
addChildViewController(pageViewController!)
self.view.addSubview(pageViewController!.view)
pageViewController!.didMoveToParentViewController(self)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
if viewController.isKindOfClass(CaptionViewController) {
let pvc = self.storyboard?.instantiateViewControllerWithIdentifier("CameraVC")
return pvc
}
return nil
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
if viewController.isKindOfClass(ViewController) {
let pvc = self.storyboard?.instantiateViewControllerWithIdentifier("CaptionVC")
return pvc
}
return nil
}
The only optional unwrapping I see is on the first line. Is that where the exception is being thrown? Are you sure your main controller has a storyboard associated with it? If so, are you sure that the storyboard contains a controller named "PageController"?