I'm developing an iOS App that uses a PageViewController to wrap several "Chat rooms". I've a TextField in each page and when I swipe in order to move to another page I wanna change the focus from the current page TextField to the new page TextField without losing the keyboard.
I've tried different approaches but I cannot do that, every time I call becomeFirstResponder in the new page TextField the keyboard goes down, then goes up immediately and then the textfields becomes the first responder.
Can you help me?
Thank you!
I was able to find a solution for this problem:
Using a variable in each page
var firstResponderIsLocked: Bool = false
To check if the TextField should end editing
func textFieldShouldEndEditing(textField: UITextField) -> Bool {
return !firstResponderIsLocked
}
And handling it when I swipe between pages:
func pageViewController(pageViewController: UIPageViewController, willTransitionToViewControllers pendingViewControllers: [UIViewController]) {
if let viewControllerToShow = pendingViewControllers[0] as? LiveMessagingPageContentViewController {
pendingViewControllerIndex = viewControllerToShow.index
lockFirstResponder(pendingViewControllerIndex!)
}
}
func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed && pendingViewControllerIndex != nil {
takeFirstResponder(pendingViewControllerIndex!)
}
pendingViewControllerIndex = nil
}
I make my app work as needed.
Put the text field on the page view controller's view, instead of on each child page. From the page view controller, call self.view.bringSubviewToFront(textField) to put it on top.
I just tested this in a new project (using the "Page-Based Application" template) and it works fine.
Related
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.
I have a UIPageViewController that only ever has two UIViewControllers in. There is a UISegmentControl above the UIPageViewController that changes between the segments "new" and "hot" depending on which page the UIPageViewController is on. This works about 90% of the time, however, if I swipe as indicated in this question, the UIPageViewController gets confused on which page it is on. This results in the UISegmentControl saying "new" when actually the "hot" page is being displayed.
Each view controller is assigned a page index (0 or 1) when it's first instantiated. Then the following code changes the segmented control index:
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let index = (pageViewController.viewControllers?.last as? MyCollectionViewController)?.pageIndex,
let pageType = PageType(rawValue: index) {
currentPage = pageType
segmentedControl.selectedSegmentIndex = index
}
}
What's weird is that when the bug is occurring, I print out the pageIndex of the pageViewControllers last viewController and it agrees with the segmentedControl index. Yet on screen, it is 100% not the viewController it's claiming is being shown...
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'm scrolling between different views, which i on button click from the rootViewController would like to render to a image. I'm wondering what is the best approach for this? is it possible to retrieve the visible view from my contentViewController or would i need to recreate the view somehow by getting the visible view index and then access the object equal to this index?
To answer 'is it possible to retrieve the visible view from my contentViewController', not really. You need to set a pageIndex property in your CustomContentViewController. Keep trace of it in the didFinishAnimating method (You can find more robust way in other posts). Or you can directly trace the currentContentViewController
This is just a very simple example
func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
self.currentContentViewController = pageViewController.viewControllers!.last as! CustomContentViewController
self.currentPageIndex = self.currentContentViewController.pageIndex
}
If you want to set a image, then do
#IBAction func buttonTapped(sender: UIButton) {
self.currentContentViewController.imageView.image = UIImage("MyImage.png")
}
I have a UIPageViewController which I am providing page data for using an implementation of UIPageControllerDelegate and UIPageControllerDataSource.
It's all working fine, but I want to be able to add items to the page data and reorder the page data.
If a user has already got to the last of the pages, and then I add an item, they can't get to the next page because viewControllerAfterViewController: has already been called. If they scroll back one and then forward two they can get to the new page fine, so the data is setup correctly. How can I tell the UIPageViewController to refresh its store of what comes next?
Similarly I would like to reorder the collection that is backing the page view. But if I do this I'll get the same problem - the page view will think the next page is still what it was last time the current page was loaded.
I guess I'm looking for something similar to reloadData: on UITableView.
I found a workaround to force UIPageViewController to forget about cached view controllers of neighboring pages that are currently not displayed:
pageViewController.dataSource = nil;
pageViewController.dataSource = self;
I do this everytime I change the set of pages. Of course this doesn't affect the currently displayed page.
With this workaround I avoid the caching bug and can still use animated:YES in setViewControllers:direction:animated:completion:.
You need to call setViewControllers:direction:animated:completion:.
Also, in iOS 6, watch out if you're using UIPageViewControllerTransitionStyleScroll style, as there is a major caching bug if animated: is YES (see my discussion here: UIPageViewController navigates to wrong page with Scroll transition style).
Here's my complete implementation of #ortwin-gentz's answer in the didFinish delegate method in Swift 2, which works perfectly for me:
func pageViewController(
pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool
) {
if !completed { return }
dispatch_async(dispatch_get_main_queue()) {
pageViewController.dataSource = nil
pageViewController.dataSource = self
}
}
Swift 4 Version for reload of data of pageviewcontroller, to #theory's version.
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if !completed { return }
DispatchQueue.main.async() {
self.dataSource = nil
self.dataSource = self
}
}