When using drag and drop with a UICollectionView embedded in a UIPageViewController, the delegate immediately triggers didExit when paging.
Begin Drag and Drop gesture
Page to new view
CollectionDropDelegate immediately fires:
newCV.didEnter
oldCV.didExit
newCV.didExit
newCV.didUpdate is never called. If we let go of the drop at this point it cancels. My PageViewController is not fullscreen, so if I move the drag outside and back in I can still perform the drop after paging, but it's a bad user experience.
Notes:
Not using UICollectionViewController
CollectionView gets added to the UIViewController hierarchy in viewDidLoad
Any ideas?
I was able to resolve this by toggling collectionView.isUserInteractionEnabled. Figuring out where in the lifecycle to do this was a bit challenging, and ultimately I ended up using the UIPageViewControllerDelegate didFinishAnimating...
So in my ContentViewController.viewDidLoad, I set collectionView.isUserInteractionEnabled = false and then in the delegate, I need to conditionally enable/disable the collection views.
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed {
guard let showing = pageViewController.viewControllers?.first as? DayViewController else {
assertionFailure()
return
}
// Disable All
previousViewControllers.forEach {
if let dayVC = $0 as? DayViewController {
dayVC.collectionView?.isUserInteractionEnabled = false
}
}
// Enable Showing
showing.collectionView?.isUserInteractionEnabled = true
} else {
guard let showing = previousViewControllers.first as? DayViewController else {
assertionFailure()
return
}
// Disable All
viewControllers?.forEach {
if let dayVC = $0 as? DayViewController {
dayVC.collectionView?.isUserInteractionEnabled = false
}
}
// Enable Showing
showing.collectionView?.isUserInteractionEnabled = true
}
}
Related
I have UISearchController in the navigationItem.searchController and I want to make it focus when the user selects "Search" from the menu.
So shortly, when the user is tapping on the "Search" option in the menu (UITableViewCell) it's getting the view controller that have the searchController in it and calling:
guard let navigationVC = presentingViewController as? UINavigationController else { return }
guard let documentsVC = navigationVC.topViewController as? DocumentsViewController else { return }
documentsVC.searchController.searchBar.becomeFirstResponder()
Then, the UISearchBar is getting focus, the keyboard is appearing and then it's immediately disappearing, and I don't have any code that would make it disappear (like view.endEditing()).
1 GIF is worth more than 1,000 words:
So, after many tries I got some way to make it work, but I'm sure there is a much more elegant ways to do this, so if someone think that they have better way, please post it here and I may use it and mark your answer as the correct one.
Create the function focusOnSearchBar() in YourViewController:
func focusOnSearchBar() {
let searchBar = searchController.searchBar
if searchBar.canBecomeFirstResponder {
DispatchQueue.main.async {
searchBar.becomeFirstResponder()
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.focusOnSearchBar()
}
}
}
What it actually do is use itself recursively and check (every 0.1 sec) if searchBar.canBecomeFirstResponder. This is the problematic/not elegant thing.
Then, add this to viewDidAppear():
if focusOnSearch {
searchController.isActive = true
}
Don't forget to add extension to your ViewController for UISearchControllerDelegate (and of course, set searchController.delegate = self) and implement didPresentSearchController (that will be invoke by setting searchController.isActive = true):
extension YourViewController: UISearchControllerDelegate {
func didPresentSearchController(_ searchController: UISearchController) {
if focusOnSearch {
focusOnSearchBar()
}
}
}
Now all you have to do is to set focusOnSearch = true in the prepare(for segue:sender:).
*Note: if you want to focusOnSearchBar while you are in the same viewController of the searchBar, just set:
focusOnSearch = true
searchController.isActive = true
And it will work by itself.
Make your searchbar first responder in the viewDidLoad method. That will make sure everything is ready before focusing the search bar.
I'm trying to use the Z gesture to dismiss a UIAlertController. I have a very simple app. It has a single view with 1 button. Tapping the button presents an alert. I have implemented
- (BOOL)accessibilityPerformEscape {
NSLog(#"Z gesture");
return YES;
}
With VoiceOver on, scrubbing the screen prints out "Z gesture," but when I press the button and the alert is visible, scrubbing the screen does nothing, the method is not called and nothing is printed. What do I have to do to get this to function while the alert is on screen?
Thanks...
To get the desired result on your alert view thanks to the scrub gesture, override accessibilityPerformEscape() in the alert view itself.
A solution could be to implement this override in an UIView extension as follows :
extension UIView {
override open func accessibilityPerformEscape() -> Bool {
if let myViewController = self.findMyViewController() as? UIAlertController {
myViewController.dismiss(animated: true,
completion: nil)
}
return true
}
private func findMyViewController() -> UIViewController? {
if let nextResponder = self.next as? UIViewController {
return nextResponder
} else if let nextResponder = self.next as? UIView {
return nextResponder.findMyViewController()
} else {
return nil
}
}
}
The code is short enough to be understood without further explanation. If it's not clear, don't hesitate to ask.
The function findMyViewController has been found here.
I try the following approach found here
extension UIPageViewController {
func goToNextPage(){
guard let currentViewController = self.viewControllers?.first else { return }
guard let nextViewController = dataSource?.pageViewController( self, viewControllerAfter: currentViewController ) else { return }
setViewControllers([nextViewController], direction: .forward, animated: true, completion: nil)
}
}
It works, but there is one issue:
When the page is turned programatically, the indicator does not move. It seems that they move only when user turns. page with swipe
that's how indicators should look like after programmatic turn is performed:
instead they remain unchanged
Which leads to issue that hierarchy shown by indicators is rather [2,0,1] instead of [0,1,2]
This is how I implement indicators:
func presentationCount(for PageViewController:UIPageViewController) -> Int {
return 3
}
func presentationIndex(for PageViewController:UIPageViewController) -> Int {
return 0
}
How to make dots indicators move when the page is turned programatically?
Unfortunately you can't update UIPageControl embedded in UIPageViewController. However, you can have your own UIPageControl in UIPageViewController in order to get full control. Then you can update UIPageControl property programmatically upon updating your Page. Have a look at this article.
There is a workaround get the subviews of UIPageViewController, set the value to currentPage.
for subView in self.view.subviews{
if let pageControl = subView as? UIPageControl,
pageControl.numberOfPages > currentIndex{
pageControl.currentPage = currentIndex
}
}
You can, all you need to do is maintain the page index in a variable, let's call it currentPageIndex and use the following method:
// Part of UIPageViewControllerDelegate
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return currentPageIndex;
}
// In your Button action, set this variable
#IBAction func nextPage(_ sender: Any) {
var index = (pageViewController?.viewControllers?.last as! SinglePageViewController).pageIndex
currentPageIndex = index
}
That's it!! Your page indicator should work now.
I was stuck in a same situation as yours.
I'm writing a series of settings / setup screens using uiPageViewController (images at the bottom). The user configures stuff, and swipe to the next screen, and so on. But I would like to lock / disable / block the forward swipe until the settings has been accomplished by the user in the current screen.
I tried:
1) uiPageViewController.view.userInteractionEnabled = false. It blocks everything in the screen, including the backward swipe. I want to block only the forward.
2)
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
if !forwardSwipeEnabled {
return nil
}
var index = contentViewControllers.indexOf(viewController)
index += 1
return contentViewControllers[index]
}
to return nil when forwardSwipeEnabled is set to false, and it works, but the forward swipe remains blocked even after I change forwardSwipeEnabled = true because UIPageViewController already called this function when the scene showed up on the screen.
If I go back and forward again, the function gets called again and it works.
----- edit -----
Calling pageViewController!.setViewControllers(viewControllers, direction: .Forward, animated: false, completion: nil) with the current view controller doesn't refresh or call the function again.
3) Only append the next screen (UIViewcontroller), after the user finishes, but the same problem as 2) occurs.
----- edit -----
4) I can get the gestureRecognizers with view.subviews.first?.gestureRecognizers, but setting them gestureRecognize.enabled = false blocks both forward and reverse. There aren't different gestureRecognizers for each to block selectively.
5) Intercepting the gesture and eating it when direction is forward or letting it run, with the bottom code when backward doesn't work because the user can start the swipe backward, trigger the function return and finish forward.
override func viewDidLoad() {
super.viewDidLoad()
for view: UIView in pageViewController!.view.subviews {
if (view is UIScrollView) {
let scrollView = (view as! UIScrollView)
self.scrollViewPanGestureRecognzier = UIPanGestureRecognizer()
self.scrollViewPanGestureRecognzier.delegate = self
scrollView.addGestureRecognizer(scrollViewPanGestureRecognzier)
}
}
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == scrollViewPanGestureRecognzier {
guard let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { return false }
let velocity = panGestureRecognizer.velocityInView(view)
let translation = panGestureRecognizer.translationInView(view)
if translation.x < 0 {
return true
}
if velocity.x < 0 {
return true
}
return false
}
return false
}
Example screen:
just as an suggestion you may reload datasource with setViewControllers:direction:animated:completion after your job is done. This should fix your next page.
Its obj-c code. it should has same code in swift.
Did you find the solution for these years?
I've found only one: immediately go back if forward is forbidden:
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
let pageContentViewController = pageViewController.viewControllers![0]
let currentPage = orderedViewControllers!.index(of: pageContentViewController)!
if !nextButton.isEnabled && currentPage > pageControl.currentPage {
DispatchQueue.main.asyncAfter(deadline: .now()+0.05, execute: {
self.setViewControllers([self.orderedViewControllers![self.pageControl.currentPage]], direction: .reverse, animated: false, completion: nil)
})
}
}
Regarding to this tutorial by AppCoda about how to implement a app with UIPageViewController I'd like to use a custom page control element on top of the pages instead of at the bottom.
When I just put a page control element on top of the single views which will be displayed, the logical result is that the control elements scrolls with the page view away from the screen.
How is it possible to put the control element on top of the views so the page views are full screen (like with an image) so the user can see the views underneath the fixed control element?
I attached an example screenshot - credits to AppCoda and Path:
I didn't have the rep to comment on the answer that originated this, but I really like it. I improved the code and converted it to swift for the below subclass of UIPageViewController:
class UIPageViewControllerWithOverlayIndicator: UIPageViewController {
override func viewDidLayoutSubviews() {
for subView in self.view.subviews as! [UIView] {
if subView is UIScrollView {
subView.frame = self.view.bounds
} else if subView is UIPageControl {
self.view.bringSubviewToFront(subView)
}
}
super.viewDidLayoutSubviews()
}
}
Clean and it works well. No need to maintain anything, just make your page view controller an instance of this class in storyboard, or make your custom page view controller class inherit from this class instead.
After further investigation and searching I found a solution, also on stackoverflow.
The key is the following message to send to a custom UIPageControl element:
[self.view bringSubviewToFront:self.pageControl];
The AppCoda tutorial is the foundation for this solution:
Add a UIPageControl element on top of the RootViewController - the view controller with the arrow.
Create a related IBOutlet element in your ViewController.m.
In the viewDidLoad method you should then add the following code as the last method you call after adding all subviews.
[self.view bringSubviewToFront:self.pageControl];
To assign the current page based on the pageIndex of the current content view you can add the following to the UIPageViewControllerDataSource methods:
- (UIPageViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
// ...
index--;
[self.pageControl setCurrentPage:index];
return [self viewControllerAtIndex:index];
}
- (UIPageViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
// ...
index++;
[self.pageControl setCurrentPage:index];
// ...
return [self viewControllerAtIndex:index];
}
sn3ek Your answer got me most of the way there. I didn't set the current page using the viewControllerCreation methods though.
I made my ViewController also the delegate of the UIPageViewController. Then I set the PageControl's CurrentPage in that method. Using the pageIndex maintained I'm the ContentViewController mention in the original article.
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
APPChildViewController *currentViewController = pageViewController.viewControllers[0];
[self.pageControl setCurrentPage:currentViewController.pageIndex];
}
don't forget to add this to viewDidLoad
self.pageViewController.delegate = self;
To follow up on PropellerHead's comment the interface for the ViewController will have the form
#interface ViewController : UIViewController <UIPageViewControllerDataSource, UIPageViewControllerDelegate>
The same effect can be achieved simply by subclassing UIPageViewController and overriding viewDidLayoutSubviews as follows:
-(void)viewDidLayoutSubviews {
UIView* v = self.view;
NSArray* subviews = v.subviews;
if( [subviews count] == 2 ) {
UIScrollView* sv = nil;
UIPageControl* pc = nil;
for( UIView* t in subviews ) {
if( [t isKindOfClass:[UIScrollView class]] ) {
sv = (UIScrollView*)t;
} else if( [t isKindOfClass:[UIPageControl class]] ) {
pc = (UIPageControl*)t;
}
}
if( sv != nil && pc != nil ) {
// expand scroll view to fit entire view
sv.frame = v.bounds;
// put page control in front
[v bringSubviewToFront:pc];
}
}
[super viewDidLayoutSubviews];
}
Then there is no need to maintain a seperate UIPageControl and such.
You have to implement a custom UIPageControl and add it to the view. As others have mentioned, view.bringSubviewToFront(pageControl) must be called.
I have an example of a view controller with all the code on setting up a custom UIPageControl (in storyboard) with UIPageViewController
There are 2 methods which you need to implement to set the current page indicator.
func pageViewController(pageViewController: UIPageViewController, willTransitionToViewControllers pendingViewControllers: [UIViewController]) {
pendingIndex = pages.indexOf(pendingViewControllers.first!)
}
func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed {
currentIndex = pendingIndex
if let index = currentIndex {
pageControl.currentPage = index
}
}
}
Here is a RxSwift/RxCocoa answer I put together after looking at the other replies.
let pages = Variable<[UIViewController]>([])
let currentPageIndex = Variable<Int>(0)
let pendingPageIndex = Variable<(Int, Bool)>(0, false)
let db = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
pendingPageIndex
.asDriver()
.filter { $0.1 }
.map { $0.0 }
.drive(currentPageIndex)
.addDisposableTo(db)
currentPageIndex.asDriver()
.drive(pageControl.rx.currentPage)
.addDisposableTo(db)
}
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
if let index = pages.value.index(of: pendingViewControllers.first!) {
pendingPageIndex.value = (index, false)
}
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
pendingPageIndex.value = (pendingPageIndex.value.0, completed)
}
Here's the Swifty 2 answer very much based on #zerotool's answer above. Just subclass UIPageViewController and then add this override to find the scrollview and resize it. Then grab the page control and move it to the top of everything else. You also need to set the page controls background color to clear. Those last two lines could go in your app delegate.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
var sv:UIScrollView?
var pc:UIPageControl?
for v in self.view.subviews{
if v.isKindOfClass(UIScrollView) {
sv = v as? UIScrollView
}else if v.isKindOfClass(UIPageControl) {
pc = v as? UIPageControl
}
}
if let newSv = sv {
newSv.frame = self.view.bounds
}
if let newPc = pc {
self.view.bringSubviewToFront(newPc)
}
}
let pageControlAppearance = UIPageControl.appearance()
pageControlAppearance.backgroundColor = UIColor.clearColor()
btw - I'm not noticing any infinite loops, as mentioned above.