UIPageControl and delay in appearing - iphone

How do I make an instance of UIPageControl appear immediately? (I have set defersCurrentPageDisplay to NO.)
I have an instance of UIPageControl which is configured (number of pages, current page and then updated) when my view appears. However there is a short, fixed delay before it appears to the user. I'd like it to appear right away.
Otherwise it's working fine.

The problem is I'm performing a lengthy background process and I've inadvertently and ultimately called updateCurrentPageDisplay etc. from this secondary thread. UIKit is not thread-safe and blocks this call until it can move it to the main thread, hence the delay.
To solve this, I've subclassed UIPageControl creating "wrapper" methods that push calls to super onto the main thread. I can then safely forget about this every time I need to speak with my page controls.
For example:
- (void) updateCurrentPageDisplay
{
#synchronized(self)
{
if ([UIDevice currentDeviceSupportsGrandCentralDispatch] == YES)
{
dispatch_async(dispatch_get_main_queue(), ^{
[super updateCurrentPageDisplay];
});
}
else
{
[super performSelectorOnMainThread:#selector(updateCurrentPageDisplay)
withObject:nil
waitUntilDone:NO];
}
}
}

I have fixed the delay issue by adding the UIPageViewController delegate's willTransitionToViewControllers and setting the pageController's index there:
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers {
for (MyContentPageViewController *contentController in pendingViewControllers) {
if ([contentController isKindOfClass:[MyContentPageViewController class]]) {
NSUInteger newIndex = contentController.pageIndex;
[self.pageControl setCurrentPage:newIndex];
}
}
Then, to avoid bugs in cases where swipe is not completed, add the following delegate method:
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed{
if(finished){
for (MyContentPageViewController *contentController in previousViewControllers) {
if ([contentController isKindOfClass:[MyContentPageViewController class]]) {
NSUInteger currentIndex = MIN(MAX(0, contentController.pageIndex), _allPages.count- 1);
[self.pageControl setCurrentPage:currentIndex];
}
}
} else {
for (MyContentPageViewController *contentController in previousViewControllers) {
if ([contentController isKindOfClass:[MyContentPageViewController class]]) {
NSUInteger currentIndex = MIN(MAX(0, contentController.pageIndex), _allPages.count);
[self.pageControl setCurrentPage:currentIndex];
}
}
}
}

Related

Two Independent Delegate Methods in a Class

I have two independent delegate methods in a class.
- (void)delegateMethod1:(id)data {
self.data = data;
}
- (void)delegateMethod2 {
[someClass sendData:self.data];
}
Now, this works fine sometimes but the other times, delegateMethod2 gets called before delegateMethod1.
I need to know how to manage this elegantly so that the line: [someClass sendData:self.data]; gets called only when both delegateMethod1 and delegateMethod2 have been called.
I know I can do it by using a variable to set to something on each delegate call but there has to be an elegant way to do this.
Any help?
Remembering which delegate has been called seems the easiest and cleanest solution to me.
But you can make it symmetric by moving the check to a separate method, so that
is does not matter which delegate is called first:
- (void)checkIfDataCanBeSent {
if (self.method1called && self.method2called) {
[someClass sendData:self.data];
}
}
- (void)delegateMethod1:(id)data {
self.method1called = YES;
// ...
[self checkIfDataCanBeSent];
}
- (void)delegateMethod2 {
self.method2called = YES;
// ...
[self checkIfDataCanBeSent];
}
(I have assumed that all delegate methods are called on the main thread, otherwise
one would have to add some synchronization.)
I believe, using a indicative variable to be the most elegant way to get over this. But this variable has to be kept in the delegate caller object.
Pseudo-type explanation
#interface DelegateCaller
{
BOOL hasCalled1stMethod;
}
#property(nonatomic,weak) id delegate;
#end
#implementation DelegateCaller
-(void)in_some_process_1
{
[self.delegate delegateMethod1]; //call
hasCalled1stMethod = YES; //set indicator
}
-(void)in_some_process_2
{
if(hasCalled1stMethod)
{
[self.delegate delegateMethod2]; //call
hasCalled1stMethod = NO; //reset indicator for reuse, if required.
}
}
#end
This way you'll not have to maintain any variable in the delegate itself, because the regulation of calling is maintained in the caller-object itself.
Another case:
If the delegateMethod1 is called from some object1 and the delegateMethod2 is called from some other object2, then again the indicative variable method is the most elegant way (in this limited scenario)
Pseudo-type explanation:
#interface ClassDelegateObject //aka the callee
{
BOOL hasCalledMethod1;
}
#end
#implementation ClassDelegateObject
-(void)delegateMethod1:(NSData*)data
{
self.data = data;
hasCalledMethod1 = YES; //set the indicator.
}
-(void)delegateMethod2
{
//here relying on the self.data!=nil will not be fruitful
//in case the self.data is not nil and hold some previous garbage data then
//this logic will fail.
if(hasCalledMethod1)
{
[someClass sendData:self.data];
hasCalledMethod1 = NO; //reset the variable for reuse if required.
}
}
#end
I would suggest that you rethink how the code works. Maybe you can check if there is no data and if so send it once it is ready:
- (void)delegateMethod1:(id)data {
self.data = data;
if (self.dataShouldBeSentWhenReady) {
[self sendData];
}
}
- (void)delegateMethod2 {
if (self.data) {
[self sendData];
} else {
[self setDataShouldBeSentWhenReady:YES];
}
}
- (void)sendData {
[self setDataShouldBeSentWhenReady:NO];
[someClass sendData:self.data];
}

Make scrollbar always visible on UIScrollView?

I need to make a scrollbar always visible on viewDidLoad so that the user can understand that there is content to scroll. I did the following:
[myscrollView flashScrollIndicators];
But then the scrollbars only appear for some time after viewDidLoad and disappear again only to reappear when the user touches the screen..
I need to make scrollbars always visible. How can I do it?
Apple indirectly discourage constantly displaying scroll indicators in their iOS Human Interface Guidelines but guidelines are just guidelines for a reason, they don't account for every scenario and sometimes you may need to politely ignore them.
The scroll indicators of any content views are UIImageView subviews of those content views. This means you can access the scroll indicators of a UIScrollView as you would any of its other subviews (i.e. myScrollView.subviews) and modify the scroll indicators as you would any UIImageView (e.g. scrollIndicatorImageView.backgroundColor = [UIColor redColor];).
The most popular solution appears to be the following code:
#define noDisableVerticalScrollTag 836913
#define noDisableHorizontalScrollTag 836914
#implementation UIImageView (ForScrollView)
- (void) setAlpha:(float)alpha {
if (self.superview.tag == noDisableVerticalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (self.frame.size.width < 10 && self.frame.size.height > self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.height < sc.contentSize.height) {
return;
}
}
}
}
if (self.superview.tag == noDisableHorizontalScrollTag) {
if (alpha == 0 && self.autoresizingMask == UIViewAutoresizingFlexibleTopMargin) {
if (self.frame.size.height < 10 && self.frame.size.height < self.frame.size.width) {
UIScrollView *sc = (UIScrollView*)self.superview;
if (sc.frame.size.width < sc.contentSize.width) {
return;
}
}
}
}
[super setAlpha:alpha];
}
#end
Which is originally credited to this source.
This defines a category for UIImageView that defines a custom setter for the alpha property. This works because at some point in the underlying code for the UIScrollView, it will set its scroll indicator's alpha property to 0 in order to hide it. At this point it will run through our category and, if the hosting UIScrollView has the right tag, it will ignore the value being set, leaving it displayed.
In order to use this solution ensure your UIScrollView has the appropriate tag e.g.
If you want to display the scroll indicator from the moment its UIScrollView is visible simply flash the scroll indicators when the view appears .e.g
- (void)viewDidAppear:(BOOL)animate
{
[super viewDidAppear:animate];
[self.scrollView flashScrollIndicators];
}
Additional SO references:
UIScrollView - showing the scroll bar
UIScrollView indicator always show?
Scroll Indicators Visibility
Make scrollbars always visible in uiscrollview
I want to offer my solution. I don't like the most popular variant with category (overriding methods in category can be the reason of some indetermination what method should be called in runtime, since there is two methods with the same selector).
I use swizzling instead. And also I don't need to use tags.
Add this method to your view controller, where you have scroll view (self.categoriesTableView in my case)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Do swizzling to turn scroll indicator always on
// Search correct subview with scroll indicator image across tableView subviews
for (UIView * view in self.categoriesTableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, [AlwaysOpaqueImageView class]);
break;
}
}
}
}
}
// Ask to flash indicator to turn it on
[self.categoriesTableView flashScrollIndicators];
}
Add new class
#interface AlwaysOpaqueImageView : UIImageView
#end
#implementation AlwaysOpaqueImageView
- (void)setAlpha:(CGFloat)alpha {
[super setAlpha:1.0];
}
#end
The scroll indicator (vertical scroll indicator in this case) will be always at the screen.
Update November, 2019
Starting from iOS 13 UIScrollView subclasses are changed. Now scroll indicators are inherited from UIView and has their own private class called _UIScrollViewScrollIndicator. This means, that they are not subclasses of UIImageView now, so old method won't work anymore.
Also we are not able to implement subclass of _UIScrollViewScrollIndicator because it is private class and we don't have access to it. So the only solution is to use runtime. Now to have support for iOS 13 and earlier implement the next steps:
Add this method to your view controller, where you have scroll view (self.categoriesTableView in my case)
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Do swizzling to turn scroll indicator always on
// Search correct subview with scroll indicator image across tableView subviews
for (UIView * view in self.categoriesTableView.subviews) {
if ([view isKindOfClass:[UIImageView class]]) {
if (view.alpha == 0 && view.autoresizingMask == UIViewAutoresizingFlexibleLeftMargin) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, [AlwaysOpaqueImageView class]);
break;
}
}
}
} else if ([NSStringFromClass(view.class) isEqualToString:#"_UIScrollViewScrollIndicator"]) {
if (view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width) {
if (self.categoriesTableView.frame.size.height < self.categoriesTableView.contentSize.height) {
// Swizzle class for found scroll indicator, (be sure to create AlwaysOpaqueScrollIndicator in runtime earlier!)
// Current implementation is in AlwaysOpaqueScrollTableView class
object_setClass(view, NSClassFromString(#"AlwaysOpaqueScrollIndicator"));
break;
}
}
}
}
// Ask to flash indicator to turn it on
[self.categoriesTableView flashScrollIndicators];
}
Add new class (this is for iOS earlier than 13)
#interface AlwaysOpaqueImageView : UIImageView
#end
#implementation AlwaysOpaqueImageView
- (void)setAlpha:(CGFloat)alpha {
[super setAlpha:1.0];
}
#end
Add these methods somewhere in you code (either the same view controller as in step 1, or to the desired UIScrollView subclass).
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Create child class from _UIScrollViewScrollIndicator since it is private
Class alwaysOpaqueScrollIndicatorClass = objc_allocateClassPair(NSClassFromString(#"_UIScrollViewScrollIndicator"), "AlwaysOpaqueScrollIndicator", 0);
objc_registerClassPair(alwaysOpaqueScrollIndicatorClass);
// Swizzle setAlpha: method of this class to custom
Class replacementMethodClass = [self class];
SEL originalSelector = #selector(setAlpha:);
SEL swizzledSelector = #selector(alwaysOpaque_setAlpha:);
Method originalMethod = class_getInstanceMethod(alwaysOpaqueScrollIndicatorClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(replacementMethodClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(alwaysOpaqueScrollIndicatorClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(alwaysOpaqueScrollIndicatorClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)alwaysOpaque_setAlpha:(CGFloat)alpha {
[self alwaysOpaque_setAlpha:1.0];
}
This step creates the subclass of _UIScrollViewScrollIndicator called AlwaysOpaqueScrollIndicator in runtime and swizzle setAlpha: method implementation to alwaysOpaque_setAlpha:.
Do not forget to add
#import <objc/runtime.h>
to the files you've inserted this code. Thanks to #Smartcat for reminder about this
I dont know whether this will work or not. But just a hint for you.
Scrollbar inside the Scrollview is a Imageview. Which is a subview of UIScrollview
So get the Scrollbar Imageview of the UIscrollview. Then try to set that image property hidden to NO or Change Alpha value
static const int UIScrollViewHorizontalBarIndexOffset = 0;
static const int UIScrollViewVerticalBarIndexOffset = 1;
-(UIImageView *)scrollbarImageViewWithIndex:(int)indexOffset
{
int viewsCount = [[yourScrollview subviews] count];
UIImageView *scrollBar = [[yourScrollview subviews] objectAtIndex:viewsCount - indexOffset - 1];
return scrollBar;
}
-(void) viewDidLoad
{
//Some Code
//Get Scrollbar
UIImageView *scrollBar = [self scrollbarImageViewWithIndex: UIScrollViewVerticalBarIndexOffset];
//The try setting hidden property/ alpha value
scrollBar.hidden=NO;
}
Got reference from here
This is Swift version of #Accid Bright's answer:
class AlwaysOpaqueImageView: UIImageView {
override var alpha: CGFloat {
didSet {
alpha = 1
}
}
static func setScrollbarToAlwaysVisible(from scrollView: UIScrollView) {
// Do swizzling to turn scroll indicator always on
// Search correct subview with scroll indicator image across tableView subviews
for view in scrollView.subviews {
if view.isKind(of: UIImageView.self),
view.alpha == 0 && view.autoresizingMask == UIView.AutoresizingMask.flexibleLeftMargin,
view.frame.size.width < 10 && view.frame.size.height > view.frame.size.width,
scrollView.frame.size.height < scrollView.contentSize.height {
// Swizzle class for found imageView, that should be scroll indicator
object_setClass(view, AlwaysOpaqueImageView.self)
break
}
}
// Ask to flash indicator to turn it on
scrollView.flashScrollIndicators()
}
}
One difference is that setting scrollbar is extracted out as a static method.

UIPageViewController, how do I correctly jump to a specific page without messing up the order specified by the data source?

I've found a few questions about how to make a UIPageViewController jump to a specific page, but I've noticed an added problem with jumping that none of the answers seem to acknowledge.
Without going into the details of my iOS app (which is similar to a paged calendar), here is what I'm experiencing. I declare a UIPageViewController, set the current view controller, and implement a data source.
// end of the init method
pageViewController = [[UIPageViewController alloc]
initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:nil];
pageViewController.dataSource = self;
[self jumpToDay:0];
}
//...
- (void)jumpToDay:(NSInteger)day {
UIViewController *controller = [self dequeuePreviousDayViewControllerWithDaysBack:day];
[pageViewController setViewControllers:#[controller]
direction:UIPageViewControllerNavigationDirectionForward
animated:YES
completion:nil];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
NSInteger days = ((THDayViewController *)viewController).daysAgo;
return [self dequeuePreviousDayViewControllerWithDaysBack:days + 1];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
NSInteger days = ((THDayViewController *)viewController).daysAgo;
return [self dequeuePreviousDayViewControllerWithDaysBack:days - 1];
}
- (UIViewController *)dequeuePreviousDayViewControllerWithDaysBack:(NSInteger)days {
return [[THPreviousDayViewController alloc] initWithDaysAgo:days];
}
Edit Note: I added simplified code for the dequeuing method. Even with this blasphemous implementation I have the exact same problem with page order.
The initialization all works as expected. The incremental paging all works fine as well. The issue is that if I ever call jumpToDay again, the order gets jumbled.
If the user is on day -5 and jumps to day 1, a scroll to the left will reveal day -5 again instead of the appropriate day 0. This seems to have something to do with how UIPageViewController keeps references to nearby pages, but I can't find any reference to a method that would force it to refresh it's cache.
Any ideas?
Programming iOS6, by Matt Neuburg documents this exact problem, and I actually found that his solution feels a little better than the currently accepted answer. That solution, which works great, has a negative side effect of animating to the image before/after, and then jarringly replacing that page with the desired page. I felt like that was a weird user experience, and Matt's solution takes care of that.
__weak UIPageViewController* pvcw = pvc;
[pvc setViewControllers:#[page]
direction:UIPageViewControllerNavigationDirectionForward
animated:YES completion:^(BOOL finished) {
UIPageViewController* pvcs = pvcw;
if (!pvcs) return;
dispatch_async(dispatch_get_main_queue(), ^{
[pvcs setViewControllers:#[page]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO completion:nil];
});
}];
So I ran into the same problem as you where I needed to be able to 'jump' to a page and then found the 'order messed up' when I gestured back a page. As far as I have been able to tell, the page view controller is definitely caching the view controllers and when you 'jump' to a page you have to specify the direction: forward or reverse. It then assumes that the new view controller is a 'neighbor' to the previous view controller and hence automagically presents the previous view controller when you gesture back. I found that this only happens when you are using the UIPageViewControllerTransitionStyleScroll and not UIPageViewControllerTransitionStylePageCurl. The page curl style apparently does not do the same caching since if you 'jump' to a page and then gesture back it delivers the pageViewController:viewController(Before/After)ViewController: message to the data source enabling you to provide the correct neighbor view controller.
Solution:
When performing a 'jump' to page you can first jump to the neighbor page to the page (animated:NO) you are jumping to and then in the completion block of that jump, jump to the desired page. This will update the cache such that when you gesture back, the correct neighbor page will be displayed. The downside is that you will need to create two view controllers; the one you are jumping to and the one that should be displayed after gesturing back.
Here is the code to a category that I wrote for UIPageViewController:
#implementation UIPageViewController (Additions)
- (void)setViewControllers:(NSArray *)viewControllers direction:(UIPageViewControllerNavigationDirection)direction invalidateCache:(BOOL)invalidateCache animated:(BOOL)animated completion:(void (^)(BOOL finished))completion {
NSArray *vcs = viewControllers;
__weak UIPageViewController *mySelf = self;
if (invalidateCache && self.transitionStyle == UIPageViewControllerTransitionStyleScroll) {
UIViewController *neighborViewController = (direction == UIPageViewControllerNavigationDirectionForward
? [self.dataSource pageViewController:self viewControllerBeforeViewController:viewControllers[0]]
: [self.dataSource pageViewController:self viewControllerAfterViewController:viewControllers[0]]);
[self setViewControllers:#[neighborViewController] direction:direction animated:NO completion:^(BOOL finished) {
[mySelf setViewControllers:vcs direction:direction animated:animated completion:completion];
}];
}
else {
[mySelf setViewControllers:vcs direction:direction animated:animated completion:completion];
}
}
#end
What you can do to test this is create a new 'Page-Based Application' and add a 'goto' button that will 'jump' to a certain calendar month and then gesture back. Be sure to set the transition style to scroll.
I use this function (I'm always in landscape, 2 page mode)
-(void) flipToPage:(NSString * )index {
int x = [index intValue];
LeafletPageContentViewController *theCurrentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
NSUInteger retreivedIndex = [self indexOfViewController:theCurrentViewController];
LeafletPageContentViewController *firstViewController = [self viewControllerAtIndex:x];
LeafletPageContentViewController *secondViewController = [self viewControllerAtIndex:x+1 ];
NSArray *viewControllers = nil;
viewControllers = [NSArray arrayWithObjects:firstViewController, secondViewController, nil];
if (retreivedIndex < x){
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
} else {
if (retreivedIndex > x ){
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:NULL];
}
}
}
Here is my Swift solution to be used for subclasses of UIPageViewController:
Assume you store an array of viewControllers in viewControllerArray and the current page index in updateCurrentPageIndex.
private func slideToPage(index: Int, completion: (() -> Void)?) {
let tempIndex = currentPageIndex
if currentPageIndex < index {
for var i = tempIndex+1; i <= index; i++ {
self.setViewControllers([viewControllerArray[i]], direction: UIPageViewControllerNavigationDirection.Forward, animated: true, completion: {[weak self] (complete: Bool) -> Void in
if (complete) {
self?.updateCurrentPageIndex(i-1)
completion?()
}
})
}
}
else if currentPageIndex > index {
for var i = tempIndex - 1; i >= index; i-- {
self.setViewControllers([viewControllerArray[i]], direction: UIPageViewControllerNavigationDirection.Reverse, animated: true, completion: {[weak self] (complete: Bool) -> Void in
if complete {
self?.updateCurrentPageIndex(i+1)
completion?()
}
})
}
}
}
Swift version of djibouti33's answer:
weak var pvcw = pageViewController
pageViewController!.setViewControllers([page], direction: UIPageViewControllerNavigationDirection.Forward, animated: true) { _ in
if let pvcs = pvcw {
dispatch_async(dispatch_get_main_queue(), {
pvcs.setViewControllers([page], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
})
}
}
It's important to note that this is no longer the case in iOS 10 and you no longer have to use the accepted answer solution. Just continue as always.
I can confirm this issue, and that it only happens when using UIPageViewControllerTransitionStyleScroll and not UIPageViewControllerTransitionStylePageCurl.
Workaround: Make a loop and call UIPageViewController setViewControllers for each page turn, until you reach the desired page.
This keeps the internal datasource index in UIPageViewController in sync.
This is only solution
-(void)buyAction
{
isFromBuy = YES;
APPChildViewController *initialViewController = [self viewControllerAtIndex:4];
viewControllers = [NSArray arrayWithObject:initialViewController];
[self.pageController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
-(NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
if (isFromBuy) {
isFromBuy = NO;
return 5;
}
return 0;
}
I had a different approach, should be possible if your pages are designed to be updated after init:
When a manual page is selected I update a flag
- (void)scrollToPage:(NSInteger)page animated:(BOOL)animated
{
if (page != self.currentPage) {
[self setViewControllers:#[[self viewControllerForPage:page]]
direction:(page > self.currentPage ?
UIPageViewControllerNavigationDirectionForward :
UIPageViewControllerNavigationDirectionReverse)
animated:animated
completion:nil];
self.currentPage = page;
self.forceReloadNextPage = YES; // to override view controller automatic page cache
}
}
- (ScheduleViewController *)viewControllerForPage:(NSInteger)page
{
CustomViewController * scheduleViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"CustomViewController"];
scheduleViewController.view.tag = page; // keep track of pages using view.tag property
scheduleViewController.data = [self dataForPage:page];
if (self.currentViewController)
scheduleViewController.calendarLayoutHourHeight = self.currentViewController.calendarLayoutHourHeight;
return scheduleViewController;
}
and then force the the next page to reload with the correct data:
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
{
CustomViewController * nextViewController = [pendingViewControllers lastObject];
// When manual scrolling occurs, the next page is loaded from UIPageViewController cache
// and must be refreshed
if (self.forceReloadNextPage) {
// calculate the direction of the scroll to know if to load next or previous page
NSUInteger page = self.currentPage + 1;
if (self.currentPage > nextViewController.view.tag) page = self.currentPage - 1;
nextViewController.data = [self dataForPage:page];
self.forceReloadNextPage = NO;
}
}
If you do not need to animate to the new page, as I didn't, the following code worked for me, called on "Value Changed" in the storyboard. Instead of changing between view controllers, I change the data associated with the current view controller.
- (IBAction)pageControlCurrentPageDidChange:(id)sender
{
self.currentIndex = self.pageControl.currentPage;
MYViewController *currentPageViewController = (MYViewController *)self.pageViewController.viewControllers.firstObject;
currentPageViewController.pageData = [self.pageDataSource dataForPage:self.currentIndex];
[currentPageViewController updateDisplay];
}
currentIndex is there so I can update the pageControl's currentPage when I swipe between pages.
pageDataSource dataForPage: returns an array of data objects that are displayed by the pages.
Here is an up-to-date Swift 3+ version of the answer by #djibouti33 with cleaned-up syntax.
weak var weakPageVc = pageVc
pageVc.setViewControllers([page], direction: .forward, animated: true) { finished in
guard let pageVc = weakPageVc else {
return
}
DispatchQueue.main.async {
pageVc.setViewControllers([page], direction: .forward, animated: false)
}
}
let orderedViewControllers = [UIViewController(),UIViewController(), UIViewController()]
let pageViewController = UIPageViewController()
let pageControl = UIPageControl()
func jump(to: Int, completion: #escaping (_ vc: UIViewController?) -> Void){
guard orderedViewControllers.count > to else{
//index of bounds
return
}
let toVC = orderedViewControllers[to]
var direction: UIPageViewController.NavigationDirection = .forward
if pageControl.currentPage < to {
direction = .forward;
} else {
direction = .reverse;
}
pageViewController.setViewControllers([toVC], direction: direction, animated: true) { _ in
DispatchQueue.main.async {
self.pageViewController.setViewControllers([toVC], direction: direction, animated: false){ _ in
self.pageControl.currentPage = to
completion(toVC)
}
}
}
}
USAGE:
self.jump(to: 5) { (vc) in
// you can do anything for new vc.
}
I was struggling with this issue for a long time myself. For me I had a UIPageViewController (I called it PageController) load from storyboard and on it I add a UIViewController 'ContentVC'.
I let the ContentVC takes care of the data to be loaded on to the content area and let PageController takes care of the sliding/goto/PageIndicator updates. The ContentVC has an ivar CurrentPageIndex and sends that value to PageController so PageController knows which page it's on. In my .m file that has PageController I have these two methods.
Note that I used set to 0 and so every time PageVC reloads it goes to the first page which I don't want, [self viewControllerAtIndex:0].
- (void)setPageForward
{
ContentVC *FirstVC = [self viewControllerAtIndex:[CurrentPageIndex integerValue]];
NSArray *viewControllers = #[FirstVC];
[PageController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
This second method is PageViewController's DataSource method. presentationIndexForPageViewController will set the highlighted dot to the right page (the page you want). Note that if we return 0 here the page indicator will highlight the first dot which indicates the first page and we don't want that.
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
return [CurrentPageIndex integerValue];
}

Facebook loginViewFetchedUserInfo is called twice

I am using facebook SDK 3.0 in my app. The delegate method is called twice when after logging to facebook.
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView
user:(id<FBGraphUser>)user {
//loginThroughFb=TRUE;
NSString *userId=[[NSString alloc] initWithString:[user id]];
[self soapCallForLogin:#"" password:#"" deviceId:#"" fbid:userId];
NSLog(#"%#",userId);
[userId release];
}
I tried 'HelloFacebookSample' project and the method is called only once.
So I guess the best solution for such case is to keep a reference to the last user object and compare it to the new object you get the next call, and if they're equal you can just ignore that call.
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user {
if (![self isUser:cachedUser equalToUser:user]) {
cachedUser = user;
/// Do something
}
}
- (BOOL)isUser:(id<FBGraphUser>)firstUser equalToUser:(id<FBGraphUser>)secondUser {
return
[firstUser.objectID isEqual:secondUser.objectID] &&
[firstUser.name isEqual:secondUser.name] &&
[firstUser.first_name isEqual:secondUser.first_name] &&
[firstUser.middle_name isEqual:secondUser.middle_name] &&
[firstUser.last_name isEqual:secondUser.last_name] &&
...
}
I also had this problem. I managed to fix it with an ugly hack, but it works. I keep a counter in the FBLoginView delegate. When the fetchedUserInfo is called, I check the counter. If it is greater than zero, return. Otherwise, do two things -
1. increment the message counter
2. Fire a delayed event that zeroes the message counter again.
So your fetchedUserInfo method will look like this:
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView
user:(id<FBGraphUser>)user {
if ([self messageCounter] >0)
return;
else
{
self.messageCounter++;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_current_queue(), ^{
[self setMessageCounter:0];
});}
// Do whatever you were going to do }
Fixed in FB SDK 3.8 released on Sept 18 2013. The delegate methods are now called once per login regardless of how many times the repeated logging out and back in occur.
I was also able to reproduce this on FB SDK 3.7.1 and within their own sample program "Scrumptious"
As mentioned (at least for me) this only happens after:
Logging in once
Logging out
Logging back in (Now it happens)
What is interesting is the order of calls on re-logins:
On the first login I the calls I see are:
- (void)loginViewShowingLoggedInUser:(FBLoginView *)loginView;
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user;
On the 2nd (and later) logins I see:
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user;
- (void)loginViewShowingLoggedInUser:(FBLoginView *)loginView;
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user;
Which gives a handy little workaround of setting a flag in the middle method like so:
- (void)loginViewShowingLoggedInUser:(FBLoginView *)loginView {
// Set flag
self.isFirstLoginDone = YES;
}
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user {
// Check
if(self.isFirstLoginDone) {
// Execute code I want to run just once
NSLog(#"fetched");
}
// Don't forget to clear the flag (I guess it shouldn't matter if everything is cleaned up)
self.isFirstLoginDone = NO;
}
There could be another reason, which i jsut faced.
My situation:
ViewController A has a login (With fbloginview and its delegate set)
User chooses to register, moves to ViewController B with another fbloginview and its delegate set.
The above makes the delegate fire twice.
I have fixed this by setting delegate to nil on ViewWillDisappear in ViewController A.
-(void) viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
fbLoginButton.delegate=self;
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
fbLoginButton.delegate=nil;
}
I used this simple trick :
(Define an int facebookCounter in your interface)
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView
user:(id<FBGraphUser>)user {
if (self.facebookCounter==0) {
self.facebookCounter++;
return;
}
//Do stuff here
}
I needed to add thread safety in this method. A simple class variable did not work. The following two options will work, depending on the use case-
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView user:(id<FBGraphUser>)user {
//self.executedOnce = NO; in the init method of this class
#synchronized(self){
if(!self.executedOnce) {
//do something once per init of this class
self.executedOnce = YES;
}
}
//OR- This will only execute once in the lifetime of the app, thus no need for the executedOnce flag
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//do something once per lifetime of the app
});
}
just in the loginViewFetchedUserInfo method set the delegate of the loginView to nil. then it can never be called. and if you need the login again, set the delegate to the correct object.

Searching for the Right Pattern (iPhone/Objective C)

EDIT: It was suggested to me that I implement the strategy pattern (http://en.wikipedia.org/wiki/Strategy_pattern), which I think I would do as several objects that implement a delegate protocol in Objective-C. This accomplishes the encapsulation I want while still allowing me to have a generic view controller in memory.
I have a class called DetailViewController that displays information about various types of data - waypoints, trails, maps, photos.
Right now, this class is 1400 lines long and it has some messy switch statements. For example:
- (void) changeMiniView:(id)sender {
if (self.track) {
[self changeTrackMiniView:[sender selectedSegmentIndex]];
} else if (self.waypoint) {
[self changeWaypointMiniView:[sender selectedSegmentIndex]];
} else if (self.photo) {
[self changePhotoMiniView:[sender selectedSegmentIndex]];
} else if (self.map) {
[self changeMapMiniView:[sender selectedSegmentIndex]];
}
}
This would be a lot neater if I made subclasses of DetailViewController, but my conundrum is I would like to keep the viewController in memory and just change certain elements, so I can have crisp transitions, particularly on 3G phones.
I feel like if I want my code to be neat, I have to take a performance hit.
Have the current view in a field in your object (rather than one field for every type of miniview you have), and implement changeMiniView for each of them.
Then your method would look like:
- (void) changeMiniView: (id)sender {
[self.currentMiniView changeMiniView: [sender selectedSegmentIndex]];
}
How about using selector?
- (void)viewDidLoad {
if (self.track) {
sel = #selector(changeTrackMiniView:);
} else if (self.waypoint) {
sel = #selector(changeWaypointMiniView:);
} else if (self.photo) {
sel = #selector(changePhotoMiniView:);
} else if (self.map) {
sel = #selector(changeMapMiniView:);
}
}
- (void)changeTrackMiniView:(id)sender {
....
}
- (void)changeMiniView:(id)sender {
[self performSelector:sel withObject:sender];
}