Programmatically turns the page, indicators does not change - ios

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.

Related

CollectionView Drag and Drop when embedded in PageViewController

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
}
}

How to Focus Accessibility On A Particular Segment in A UISegmentedControl

OK. This answer helps a lot. I can select an accessibility item when a screen is shown. I simply add
UIAccessibility.post(notification: .layoutChanged, argument: <a reference to the UI item to receive focus>)
to the end of my viewWillAppear() method, and the item receives focus.
However, in one of my screens, the item I want to receive focus is a UISegmentedControl, and, when focused, it always selects the first item, no matter which one is selected. Since I followed the excellent suggestion here, I have an accessibility label for each item in the control, and I'd like my focus to begin on whichever segment is selected.
Is there a way to do this? As a rule, I try to avoid "hacky" solutions (like the one I just referenced), but I'm willing to consider anything.
Thanks!
UPDATE: Just to add insult to injury, I am also having an issue with the item I want selected being selected, then a second later, the screen jumps the selection to the first item. That's probably a topic for a second question.
I created a blank project as follows to reproduce the problem:
The solution is taking the selectedIndex to display the selected segment and providing the appropriate segment object for the VoiceOver notification: easy, isn't it?
I naively thought that getting the subview in the segmented control subviews array with the selectedIndex would do the job but that's definitely not possible because the subviews can move inside this array as the following snapshot highlights (red framed first element for instance):
The only way to identify a unique segment is its frame, so I pick up the segmented control index and the frame of the selected segment to pass them to the previous view controller.
That will allow to display (index) and read out (frame that identifies the object for the notification) the appropriate selected segment when this screen will appear after the transition.
Hereafter the code snippets for the view controller that contains the 'Next Screen' button:
class SOFSegmentedControl: UIViewController, UpdateSegmentedIndexDelegate {
var segmentIndex = 0
var segmentFrame = CGRect.zero
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let segueName = segue.identifier {
if (segueName == "SegmentSegue") {
if let destVC = segue.destination as? SOFSegmentedControlBis {
destVC.delegate = self
destVC.segmentIndex = segmentIndex
destVC.segmentFrame = segmentFrame
}
}
}
}
#IBAction func buttonAction(_ sender: UIButton) { self.performSegue(withIdentifier: "SegmentSegue", sender: sender) }
func updateSegmentIndex(_ index: Int, withFrame frame: CGRect) {
segmentIndex = index
segmentFrame = frame
}
}
... and for the view controller that displays the segmented control:
protocol UpdateSegmentedIndexDelegate: class {
func updateSegmentIndex(_ index: Int, withFrame frame: CGRect)
}
class SOFSegmentedControlBis: UIViewController {
#IBOutlet weak var mySegmentedControl: UISegmentedControl!
var delegate: UpdateSegmentedIndexDelegate?
var segmentFrame = CGRect.zero
var segmentIndex = 0
var segmentFrames = [Int:CGRect]()
override func viewDidLoad() {
super.viewDidLoad()
mySegmentedControl.addTarget(self,
action: #selector(segmentedControlValueChanged(_:)),
for: .valueChanged)
mySegmentedControl.selectedSegmentIndex = segmentIndex
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(mySegmentedControl.subviews)
let sortedFrames = mySegmentedControl.subviews.sorted(by: { $0.frame.origin.x < $1.frame.origin.x})
for (index, segment) in sortedFrames.enumerated() { segmentFrames[index] = segment.frame }
if (self.segmentFrame == CGRect.zero) {
UIAccessibility.post(notification: .screenChanged,
argument: mySegmentedControl)
} else {
mySegmentedControl.subviews.forEach({
if ($0.frame == self.segmentFrame) {
UIAccessibility.post(notification: .screenChanged,
argument: $0)
}
})
}
}
#objc func segmentedControlValueChanged(_ notif: NSNotification) {
delegate?.updateSegmentIndex(mySegmentedControl.selectedSegmentIndex,
withFrame: segmentFrames[mySegmentedControl.selectedSegmentIndex]!) }
}
The final result is as follows:
Double tap to go to the next screen.
Select the next element to focus the second segment.
Double tap to select the focused element.
Get back to the previous screen thanks to the Z gesture natively known by iOS with the navigation controller. The delegate passes the index and the frame of the selected segment.
Double tap to go to the next screen.
The segment that was formerly selected is read out by VoiceOver and still selected.
You can now Focus Accessibility On A Particular Segment in A UISegmentedControl following this rationale.
I try to avoid "hacky" solutions (like the one I just referenced), but I'm willing to consider anything.
Unfortunately, this solution is a hacky one... sorry. However, it works and I couldn't find another one anywhere else: see it as a personal fix unless you get a cleaner one to share? ;o)
UPDATE... That's probably a topic for a second question.
I can't reproduce the behavior of your update: if you create a dedicated topic for this problem, please add the most detailed code and context so as to provide the most accurate solution.
i think this works~!
class VC {
let segment = UISegmentedControl()
func fucusSegment(index: Int) {
let item = segment.accessibilityElement(at: index )
UIAccessibility.post(notification: .layoutChanged, argument: item)
}
}

Keyboard is appearing and immediately disappearing when UISearchBar.becomeFirstResponder() is getting called

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.

Disable temporarily forward swipe only in uiPageViewController

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)
})
}
}

How to put the UIPageControl element on top of the sliding pages within a UIPageViewController?

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.

Resources