16

I have a multi-line UILabel that I want to enable zooming on.

I embedded it with a UIScrollView and set min zoom to .25 and max zoom to 4. This works well, however my UILabel's font looks rather gross at any zoom level other than 1.

I can handle this method:

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale

in order to re-size the font of my UILabel to something larger, but the view is still zoomed in, so it always looks awful.

Is there any way to make the label's text re-render one I'm done zooming?

It is important that the users current scrolled position in the text not be lost.

(To get a feel for what I'm going for, notice how in Mobile Safari when you zoom the text is scaled/anti-aliased for a split second then it clears up to render well at your current zoom scale)

Ben Scheirman
  • 40,531
  • 21
  • 102
  • 137

8 Answers8

18

The UIScrollView's built-in scaling only applies a transform to your content view, which results in blurriness at anything above a scale factor of 1.0. For truly sharp rendering, you'll need to handle the scaling yourself. I describe a chunk of the process in this answer.

You'll need to keep track of the scale factor of the content view manually, then in the -scrollViewDidEndZooming:withView:atScale: delegate method you'll apply that scale. For your UILabel, that will mean changing the font size to reflect the new scale.

In order to maintain the correct scroll position, you'll need to grab the contentOffset of the UIScrollView within the above delegate method and calculate what position that corresponds to in the newly scaled UILabel. You then set the contentSize of the scroll view to match the new size of the UILabel and use -setContentOffset:animated: to set the newly calculated content offset (with animated set to NO).

It's a little tricky to get the math right, but I do this when scaling text in one of my applications, which can be seen in the video demonstration of that application (at about the 1/3 mark).

Community
  • 1
  • 1
Brad Larson
  • 170,088
  • 45
  • 397
  • 571
  • Thanks for the detailed answer. does this mean I need to abandon the built-in zooming feature of the scrollview? Or when I'm done zooming I just re-render the surface at the new scale factor? What does this do to your min/max zoom settings? – Ben Scheirman Jan 02 '10 at 18:25
  • You use the built-in zooming while the pinch gesture is ongoing, then re-render when the gesture is finished. This is the only way to get decent pinch zooming performance. The UIScrollView keeps track of the overall zoom scale (which is why you need to override the transform accessor on the content view to counteract this), so it will still observe the min / max zoom settings. – Brad Larson Jan 02 '10 at 21:39
  • Staring to make more sense. So if I zoom to 150%, I can change the font size of my label to 1.5 * currentFontSize, re-calculate their exact scroll position, and re-render my label. Then I reset the zoom to 1 and re-calculate my min/max? Do I have this right? – Ben Scheirman Jan 04 '10 at 19:33
  • This is amazingly good stuff, and apparently you're the only guy who has published something about this (here on SO :)) – Dan Rosenstark Jul 23 '10 at 18:55
  • @Yar - Glad it was helpful. You might want to check the update I added to the linked answer, because some of this zooming behavior changed in iPhone OS 3.2+. – Brad Larson Jul 23 '10 at 19:06
  • Hello might be a little late but I tried resizing my font but it stil looked blurry . Any idea? I am using a textview. – user281300 Nov 02 '11 at 23:58
  • @user281300 - Are you sure that you resized the font, and didn't apply any sort of scaling transform to the view? If so, check to make sure that the view still has its origin aligned to an integer pixel boundary. One way to test this is to use the Core Animation instrument, with its option to color misaligned layers. – Brad Larson Nov 03 '11 at 14:27
  • okay thanks. Ye I tried resizing the font and it did not look great. There is no transform. Just using the default zoom and then changing the size. – user281300 Nov 07 '11 at 19:17
8

Thanks valvoline & Scrimmers for your answers. My solution was a mix between yours.

Subclass UILabel like this:

UILabelZoomable.h

+ (Class) layerClass;

UILabelZoomable.m

+ (Class) layerClass
{    
    return [CATiledLayer class];
}

Now, when you use your fancy new UILabelZoomable in your app, remember to do this:

YourViewController.m

CATiledLayer *tiledLayer = (CATiledLayer*)textoLabel.layer;
tiledLayer.levelsOfDetail = 10;
tiledLayer.levelsOfDetailBias = 10;

Remember to add the QuartzCore framework!

#import <QuartzCore/QuartzCore.h>

Enjoy sharp and a beautiful rendered text:

[UIView animateWithDuration:1 animations:^
     {
         yourLabel.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(200, 200), CGAffineTransformMakeRotation(1));
     }];
JEzu
  • 2,949
  • 1
  • 16
  • 6
8

iOS 4+

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
    scrollView.contentScaleFactor = scale;
    for (UIView *subview in scrollView.subviews) {
        subview.contentScaleFactor = scale;
    }
}

If this gets too intensive and performance is slow, just try setting the contentScaleFactor for only your labels.

kabucey
  • 2,418
  • 3
  • 18
  • 7
4

Here is something that I have come up with:

func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView?, atScale scale: CGFloat) {
        label.font = UIFont(name: "YourFont", size: fontSize * scale)
        label.transform = CGAffineTransformConcat(CGAffineTransformMakeScale(1 / scale, 1 / scale), CGAffineTransformMakeRotation(0))
    }

Updated for Swift 4.0:

func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
    label.font = label.font.withSize(fontSize * scale)
    label.transform = CGAffineTransform(scaleX: 1 / scale, y: 1 / scale).concatenating(CGAffineTransform(rotationAngle: 0))
}

And also remember that the label should be large enough to show the whole text. It sometimes takes a tiny bit to redraw, but this method is fairly simple.

robinCTS
  • 5,746
  • 14
  • 30
  • 37
Snacks
  • 513
  • 4
  • 22
  • This is my preferred way to solve the problem, you do not need the affine rotation transform (`CGAffineTransform(rotationAngle: 0)`), though. – Wizard Jan 23 '19 at 14:42
3

I implemented Scrimmers's solution by subclassing UILabel as DetailedUILabel with overriding methods like this;

import QuartzCore

#import <QuartzCore/QuartzCore.h>

override init method, initWithFrame whichever you want.

- (id)init
{
    self = [super init];
    if (self) {
        CATiledLayer *tiledLayer = (CATiledLayer *)self.layer;
        tiledLayer.levelsOfDetailBias = 4;
        tiledLayer.levelsOfDetail = 4;
        self.opaque = YES;
    }
    return self;
}

and layerClass, class method.

+ (Class)layerClass {
    return [CATiledLayer class];
}
Eralp Karaduman
  • 304
  • 1
  • 5
  • 10
2

I tested JEzu's code in iOS 5.1 and 6 beta, it is working for a const text label, but cause other normal label require updating text content fail, i.e. the text is not changed...

I subclass the UILabel as ZoomableLabel with JEzu's answer, and apply the class as the label's custom class and it works fine.

BillChan
  • 85
  • 1
  • 3
2

the correct way to substitute a CALayer with a CATiledLayer is as follow:

+ (Class)layerClass {
    return [CATiledLayer class]; 
}

apart, you've to set your bias detail and whatever you need.

valvoline
  • 7,737
  • 3
  • 47
  • 52
1

There is a simpler way to do this..

Replace the layerClass of the UILabel with a CATiledLayer and set the level of detail appropriately.

+ (Class)layerClass
{
    CATiledLayer *layerForView = (CATiledLayer *)self.layer;
    layerForView.levelsOfDetailBias = 2;
    layerForView.levelsOfDetail = 2;
    return [CATiledLayer class];
}

Job done

Scrimmers
  • 458
  • 2
  • 8