2

I'm currently trying to read in a RSS feed and separate the long paragraphed text into a UIScrollView with paging enabled. I need to separate the text into different pages aka fit what will fit on each page and separate the string as such. I'm not sure if there is a standard what of doing this and I assume that this is how most RSS reading apps separate their information on multiple pages. Does anyone know how to tackle this? I did not want to look letter by letter until the text didn't fit and continue.

edit:

This is a good start, but the example code pretty much runs into the problems I was trying to avoid and don't know how to get around. This range calculates incorrectly for the UITextView. I change the font and such as seen below. Everything is attempting to being calculated within - (NSRange)visibleRangeOfTextView:(UITextView *)textView. This method is called by -(void)adjustTextDisplay which is called by an external class after setting the text for the UITextView. I have no idea why setting the content size to the frame size of the screen does not restrict the view (as shown below) nor do I know why this method is returning the full string length as the range.

TheCodingArt
  • 3,436
  • 4
  • 30
  • 53

2 Answers2

3

Instead of heavy iterated calculations, I'd set complete (starting from the previous page end of course) text to the textView and get the last displayed character position. Then it is easy to perform a fast backward search to truncate the word/sentence.


I've got the following solution, a little tricky part of skipping the last partially displayed line in order to avoid scrolling and make it look nicer. You still need to move endCutIndex to make it word- or sentence-wrapping.

The base project with the pager but not text-views is taken from here

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString * fullText = @"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.";

    pageControlBeingUsed = NO;


    int pageNumber = 0;
    UIFont * theFont = [UIFont boldSystemFontOfSize:30];
    const CGSize charSize = [@"A" sizeWithFont:theFont];

    while (fullText.length > 0) {
        CGRect frame;
        frame.origin.x = self.scrollView.frame.size.width * (pageNumber++);
        frame.origin.y = 0;
        frame.size = self.scrollView.frame.size;

        UIView *subview = [[UIView alloc] initWithFrame:frame];

        UITextView * textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
        textView.font = theFont;
        [subview addSubview:textView];
        [textView release];


        textView.text = fullText;

        CGRect bounds = textView.bounds;
        // - charSize.height to skip a partially visible line
        // - charSize.width*2 to skip annoying character still displayed at partially visible line
        CGPoint endPoint = CGPointMake(CGRectGetMaxX(bounds) - charSize.width*2, CGRectGetMaxY(bounds) - charSize.height);
        UITextPosition *start = [textView characterRangeAtPoint:bounds.origin].start;
        UITextPosition *end = [textView characterRangeAtPoint:endPoint].end;

        const int startCutIndex = [textView offsetFromPosition:textView.beginningOfDocument toPosition:start];
        const int endCutIndex =   [textView offsetFromPosition:textView.beginningOfDocument toPosition:end];

        NSString * cutText = [fullText substringToIndex:endCutIndex];
        textView.text = cutText;
        fullText = [fullText substringFromIndex:endCutIndex];

        [self.scrollView addSubview:subview];
        [subview release];

        NSLog(@"Page (1-total) %d, start text index %d, end text index %d \ntext:%@", pageNumber, startCutIndex, endCutIndex, cutText);
    }

    const int totalPages = pageNumber;

    self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * totalPages, self.scrollView.frame.size.height);

    self.pageControl.currentPage = 0;
    self.pageControl.numberOfPages = totalPages;
}

Here's a bit of the .h file:

@interface FCContentViewController : UIViewController <UIScrollViewDelegate, UITextViewDelegate>{
    UIPageControl *pageControl;   
    NSString *trunkedString;
    UITextView *bodyText;
}
@property (nonatomic, retain) UIScrollView *scrollView;
@property (nonatomic, retain) NSString *bodyTextString;
Community
  • 1
  • 1
A-Live
  • 8,904
  • 2
  • 39
  • 74
  • I attempted dancing around this with no success. Take a look of my code above. This kind of solution is exactly what I was looking for, but it's not working and I've tried altering the code numerous ways for my liking. It is grabbing the last characters from the text overall within the UITextView. Which means it grabs all text truncated into the scrolling portion of the UITextView. The body text string always comes out to the returned length. – TheCodingArt Oct 23 '12 at 17:57
  • It must be `[self.textView offsetFromPosition:self.textView.beginningOfDocument toPosition:end]` as the second argument of created range to return. This method works fine for me. Now there's a problem appeared that partially visible text is detected by `characterRangeAtPoint:`, but definitely that should not always be the last textView.text character. – A-Live Oct 23 '12 at 19:26
  • Arg, just tried that (which does make sense and I did try it before) with no success. I added my console output above with the corrected code. I also added a screen with the text, note that the text almost leads into a full extra paragraph (it's not just a line). It's still the value for end that is returning the wrong word though, so this problem occurs far before the returned input. – TheCodingArt Oct 23 '12 at 21:37
  • I did notice something strange now, I can't seem to set the content size of the UITextView. I wanted to see if that was the problem, and I am setting it (as the console reports) to bodyText frame size: x:270.000000 y:494.000000 , bounds size: x:270.000000 y:494.000000. Yet the view is still scrolling with the text.... – TheCodingArt Oct 23 '12 at 22:19
  • I dont think the content size should be set manually, but let me check your code later. – A-Live Oct 24 '12 at 01:53
  • Thanks, any luck looking into it? This is considerably driving me nuts. I want to dynamically create the UIText View based on it's bounds (I completely want it to ignore scrolling capabilities... kinda like a UILabel but with all of the methods a UITextView). I really did not want to subclass it either. Essentially, if this is set up properly, I can add labels above and below, images above and below, and everything will resize and text will continue in the next UITextView which will typically be on the next page in the UIScrollView it's in. – TheCodingArt Oct 24 '12 at 15:42
  • Yeah, please see the answer update. Please let me know if it helps you to move forward (the code might be optimized of course, it is here only to show the concept). – A-Live Oct 24 '12 at 19:46
  • Astounding answer. This is definitely trudging me in the right direction! Thank you for that! – TheCodingArt Oct 24 '12 at 23:44
  • The only problem I'm having now is updating the PagControl * sigh *. For some reason, no matter what the actual counter say they are, nor how many pages PageController states it has... the pages are stacking. Say you open up a two page text segment. Everything 2 pages and under will always have 2 dots at the bottom. Then if you open up a 3 paged segment, everything else with 3 pages and less will have 3 dots. – TheCodingArt Oct 26 '12 at 01:45
  • I don't quite understand, are you using the same page-control for several scroll-views ? – A-Live Oct 26 '12 at 08:31
  • I'm using the same one note in the above code. Should only be one overal. Shouldn't be within any scroll views (just below them). The UITextView is in a UIScroll view but that shouldn't cause conflict should it? I update the pages when more than half the view has been scrolled and that seems right. For some reason, the actual number of dots appearing is wrong ( not the page count returned by the scroll view ) – TheCodingArt Oct 26 '12 at 13:36
  • Sorry, it's hard to get what's happening. I've checked your `scrollViewDidScroll` - it looks good. Can you please make a screenshot highlighting the problem ? – A-Live Oct 26 '12 at 13:56
  • I'm sorry about the mess, just really appreciative to have someone helping out. I updated the info with the images above. Let me know if anything else is needed. I'm still digging into it... but can't find out why. – TheCodingArt Oct 26 '12 at 15:52
  • I also added a bit of the .h file above. – TheCodingArt Oct 26 '12 at 17:56
  • SOLVED. I had to move pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(0 , 0, [self view].bounds.size.width, pageControlHeight)]; into the init method. I'm not %100 sure why yet.. but that did it! I'm guessing consistently initializing it was causing some conflict because the ARC wasn't releasing it right away. – TheCodingArt Oct 28 '12 at 20:08
  • If it was the memory issue you would most likely get crashes, as you don't have the indicators count problem any more I can guess there was a problem that you set the number of pages to the older pageControl instance. – A-Live Oct 29 '12 at 02:17
  • That'e pretty much what I concluded the issue to be. – TheCodingArt Nov 14 '12 at 18:50
1

As of iOS 7, there's a much more elegant solution to this using TextKit that I've included in sample code below. The idea is to let TextKit's layout manager handle separating the glyphs and lay everything out that way properly. This prevents cutting off words mid way and a ton of flexibility:

class BookView: UIScrollView {

    var bookMarkup: NSAttributedString!
    private let layoutManager = NSLayoutManager()

    override func layoutSubviews() {
        super.layoutSubviews()

        if layoutManager.textContainers.count == 0 {
            buildFrames()
        }
    }

    func buildFrames() {
        let textStorage = NSTextStorage(attributedString: bookMarkup)

        textStorage.addLayoutManager(layoutManager)

        var range = NSMakeRange(0, 0)
        var containerIndex = 0

        while NSMaxRange(range) < layoutManager.numberOfGlyphs {
            let textViewRect = frameForViewAtIndex(containerIndex)
            let containerSize = CGSizeMake(CGRectGetWidth(textViewRect), CGRectGetHeight(textViewRect) - 16) //UITextView adds an 8 margin above and below the container so we take that into consideration here with the 16. heightTracksTextView causes a performance hit when adding multiple containers... so we don't do that instead
            let textContainer = NSTextContainer(size: containerSize)
            layoutManager.addTextContainer(textContainer)

            let textView = UITextView(frame: textViewRect, textContainer: textContainer)

            addSubview(textView)

            containerIndex++

            range = layoutManager.glyphRangeForTextContainer(textContainer)
        }

        contentSize = CGSize(width: CGRectGetWidth(bounds) / 2 * CGFloat(containerIndex), height: CGRectGetHeight(bounds))

        pagingEnabled = true
    }

    private func frameForViewAtIndex(index: Int) -> CGRect {

        var textViewRect = CGRect(origin: CGPointZero, size: CGSize(width: CGRectGetWidth(bounds)/2, height: CGRectGetHeight(bounds)))
        textViewRect = CGRectInset(textViewRect, 10, 20)
        textViewRect = CGRectOffset(textViewRect, CGRectGetWidth(bounds) / 2 * CGFloat(index), 0)
        return textViewRect
    }
}
TheCodingArt
  • 3,436
  • 4
  • 30
  • 53