4

I'm working on a multi-page reading application backed by TextKit, based off of the "Advanced Text Layouts and Effects with Text Kit" session from WWDC 2013 (but with some code reconstructed from incomplete example). The basic structure is you calculate the number of pages needed for your text upfront, then create an NSTextContainer for each page and add it to the NSLayoutManager. Whenever the UIPageViewController asks for the next or previous page, you create a new UITextView and set its backing text container by selecting the correct one out of the NLayoutManger's array of NSTextContainers.

Unfortunately, I have a problem where the text gets reflowed both on the first page, and the first time that I page back to any given page. Here's what it look like:

animation of reflowing text

It's not the most glaring effect (if you missed it, pay attention to the top of the screen when paging back), but it is a little disorienting, and I'd like to eliminate it if possible. Given that the text containers should be calculated up front, I don't understand why it's reflowing the text, or how to prevent it. Does anyone know what the problem is?

EDIT: Adding a code sample.

@interface ReaderViewController () <UIPageViewControllerDataSource>

@property (nonatomic, assign) NSUInteger numberOfPages;

@property (nonatomic, retain) UIPageViewController *pageViewController;
@property (nonatomic, retain) NSTextStorage *currentDocument;
@property (nonatomic, retain) NSLayoutManager *layoutManager;

@end

@implementation ReaderViewController

- (instancetype)initWithDocument:(NSTextStorage *)document {
  if ((self = [super init])) {
    _currentDocument = document;
  }
  return self;
}

- (void)viewDidLoad
{
  [super viewDidLoad];

  _layoutManager = [[NSLayoutManager alloc] init];
  [self.layoutManager setTextStorage:self.currentDocument];
  self.layoutManager.delegate = self;

  _pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
                                                        navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
                                                                      options:nil];
  self.pageViewController.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  self.pageViewController.dataSource = self;
  [self addChildViewController:self.pageViewController];
  [self.view addSubview:self.pageViewController.view];

  // the numberOfPages accessor lazily calculates the number of pages needed to contain the document
  for (int i = 0; i < self.numberOfPages; ++i) {
    NSTextContainer *container = [[NSTextContainer alloc] init];
    container.size = [self _textFrame].size;
    [self.layoutManager addTextContainer:container];
  }

  [self.pageViewController setViewControllers:@[[self viewControllerForPageNumber:0]] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}

- (UIViewController *)viewControllerForPageNumber:(NSUInteger)pageNumber {
  if (pageNumber >= self.numberOfPages) {
    return nil;
  }

  // SinglePageViewController is a lightweight view controller that has a UITextView and a page number
  SinglePageViewController *vc = [[SinglePageViewController alloc] init];

  UITextView *textView = [[UITextView alloc] initWithFrame:[self _textFrame] textContainer:[self.layoutManager.textContainers objectAtIndex:pageNumber]];
  textView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  textView.scrollEnabled = NO;
  textView.editable = NO;
  [textView setFont:[UIFont fontWithName:@"IowanOldStyle-Roman" size:20.f]];
  [vc.view addSubview:textView];

  vc.textView = textView;
  vc.pageNumber = pageNumber;

  return vc;
}

#pragma mark - UIPageViewControllerDataSource

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
  NSUInteger currentPage = [(SinglePageViewController *)viewController pageNumber];
  if (currentPage >= self.numberOfPages) {
    return nil;
  }

  return [self viewControllerForPageNumber:currentPage + 1];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
  NSUInteger currentPage = [(SinglePageViewController *)viewController pageNumber];
  if (currentPage == 0) {
    return nil;
  }

  return [self viewControllerForPageNumber:currentPage - 1];
}

#pragma mark - Private

- (CGRect)_textFrame {
  return CGRectInset(self.view.bounds, 5., 0.);
}

@end
Aaron
  • 173
  • 2
  • 13
  • Are you working on storyboards for the controllers? If so, can you tell us about your `constraints`? – Dunes Buggy Jul 12 '14 at 08:00
  • No, I'm doing the layout all programmatically. – Aaron Jul 12 '14 at 13:06
  • I went ahead and implemented the NSLayoutManagerDelegate protocol to see if it gave any hints as to what the cause was. from `layoutManager:textContainer:didChangeGeometryFromSize:` I found out that the textContainer is changing geometry when I page back, shrinking the width of the container. That answers why the the text is reflowing on all but the first page, but I'm still not sure what's causing the new geometry. – Aaron Jul 13 '14 at 07:18
  • Can you put some code? Your Page View Controller implementation. Possibly I can explain with that. – Dunes Buggy Jul 13 '14 at 07:21
  • Added a code sample; tried to strip out what unnecessary content that I could to shorten it but it's still fairly long, sorry! – Aaron Jul 13 '14 at 15:54
  • @Aaron, how do u calculate: `numberOfPages` – Chanchal Raj Mar 23 '17 at 09:56
  • @ChanchalRaj This project and question is fairly old, so I can't go into specifics anymore, but when I created this view, I manually laid out frames of text (each the size of the screen) until I reached the end of the text; then I simply counted the number of frames that I had generated. – Aaron Mar 24 '17 at 18:15

2 Answers2

3

I faced the same issue. My solution is to put the scrollEnabled after addSubview will force the textContainer re-calculate the size. See below code:

vc.view.addSubview(textView)
textView.scrollEnabled = false;

Update. Finally, I think I found the right answer... We need to reset the container size in the didChangeGeometryFromSize callback. Please correct me if it is not true :)

func layoutManager(layoutManager: NSLayoutManager, 
                   textContainer: NSTextContainer, 
                   didChangeGeometryFromSize oldSize: CGSize) {

    textContainer.size = self.textFrame().size;
    println(textContainer.size.width)
    println(textContainer.size.height)
}
Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
ytll21
  • 850
  • 6
  • 12
0

Try to set automaticallyAdjustsScrollViewInsets of your view controller to NO.

Ahmed Ashour
  • 5,179
  • 10
  • 35
  • 56
Rodion
  • 1
  • 1