Hello, I'm Steven Veltema: a programmer, founder of Labosu K.K., maker, and longtime resident of Japan.

Changing UITextField Placeholder Font and Color

There are no settings available in a UITextField to make the placeholder font different (color, smaller, fontface etc) from the font or textColor. You would think that it would be possible with UITextField attributedPlaceholder, but you would be wrong. The way to get around this is to sublclass UITextField and override drawPlaceholderInRect. Occasionally, this will cause placeholder text to be pinned to the top of the UITextField, which can be fixed by vertically centering the draw rect.

Generating strings.xml from JSON at Build in Android Studio

One project that I'm working on right now shares it's assets across several platforms and development teams. The client maintains all of the string data in Excel and generates several simple JSON files from a worksheet. These sheets are then used by other teams for building the web interfaces and by myself for making the iOS and Android apps. Using the raw JSON files on iOS is not a problem because I use a custom cache backed localized string manager and not the Interface Builder localisation tooling or the default NSLocalizedString macro.

In Android, string handling and localisation are highly integrated into the tools making custom solutions more difficult. Additionally, strings in Android projects are defined in a strings.xml file residing in the read-only res folder and thus are not writeable from within the app itself. The only way then to use the standard IDE etc and still use common set of JSON files for localised strings, is to generate a proper strings.xml file from the various JSON files as part of the build process. Since I am developing in Android Studio, this meant fiddling around with embedded build tool, Gradle. Being relatively unfamiliar with both Gradle and Groovy, the language used in the build.gradle file, it took me a bit of fiddling around but I was finally able to generate a proper strings.xml as part of the build.

The localised JSON string file is just a simple map of keys and strings similar to below:

{
"Localize_back_button":"戻る",
"Localize_html_title":"タイトル",
}

The additions to build.gradle file are as follows:

In the new generateStrings task, the JSON file containing the localised string data is read in and a new xml MarkupBuilder is created to receive the new string data. Next we iterate over the JSON map and add the keys and values as string elements to the xml MarkupBuilder, writing the new strings.xml file when finished.

The trick to get this to be run on build, is to insert the new generateStrings task into the build process. This is done by adding the generateStrings task as a dependency to the preBuild task, ensuring that the strings.xml file is recreated before every build.

gradle.projectsEvaluated {
    preBuild.dependsOn('generateStrings')
}

Possible Improvements

  • The strings.xml file is recreated on every build and as such slows down the build process. There should be a way to only regenerate the strings file when the JSON has been altered
  • It may be desirable to not overwrite the file, but read in the current xml file and only add in the changes from the JSON assets. Could lead to a bloated strings file though.

Restart

Restart

with a few posts from the past

NSDateFormatter woes

NSDateFormatter has some really obscure behavior that I'm sure trips up a lot of people. A case in point. I have an app where a date is sent from the server in the format: "yyyy-MM-dd'T'HH:mm:ssZZ". You would expect that the following code would work just fine:

NSDateFormatter dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setTimeStyle:NSDateFormatterFullStyle];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZ"];

but you would be wrong. It appears that

  1. for certain locales (say Japan)
  2. when the phone is set to 12 hour mode

The date formatter ignores the date format and inserts its own localized AM/PM string and date string parsing fail. Now in Japan, the standard and thus majority of users use a 24 hour clock, so you will only get intermittent reports of errors/bugs which can be quite frustrating.

To fix this, instead of solely relying on setDateFormat, you need to set the locale. In a rather obscure Q&A, Apple recommends en_US_POSIX.

NSDateFormatter dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[[[NSLocale alloc]initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]];
[dateFormatter setTimeStyle:NSDateFormatterFullStyle];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZ"];

I'm sure there are many apps, particularly ones developed with only an English speaking user in mind that contain this bug.

Resizing a UIWebView to fit with a fixed width

Formatting ScreenshotUIWebView is great for displaying hightly formated content, but sizing the frame correctly can be tough at times. Recently, I was using a UIWebView to display formated content and I needed to resize it display all of it's contents. The standard way to automatically resize a UIView to fit its contents is to call sizeToFit. Calling sizeToFit on the UIWebView in the -(void)webViewDidFinishLoad:(UIWebView *)webView UIWebViewDelegate method works nicely... almost.

My problem was that I wanted the width to be fixed and change only the height but sizeToFit automatically changes both, causing the content to run off the edge of the screen.

However, there is a neat little trick.

By calling the UIWebView's& stringByEvaluatingJavaScriptFromString method it is possible to interact with the content of the UIWebView. From there it is easy to get the content size and resize the UIWebView's frame accordingly.

- (void)webViewDidFinishLoad:(UIWebView *)webView {
        CGFloat contentHeight = [[webView stringByEvaluatingJavaScriptFromString:
            "document.documentElement.scrollHeight"] floatValue];
        webView.frame = CGRectMake(webView.frame.origin.x, webView.frame.origin.y, 
            webView.frame.size.width, contentHeight);

And away you go.

Displaying PDF documents on iOS

iOS has a very nice library for the displaying of PDFs, but because of their large size, take up significant memory and often are unable to be displayed all at once.

In order to display large PDFs you must first know some UIView basics. The UIView display area is generally composed of any number of CALayers. There are several different types of CALayers which can be interchanged depending on what you want to display. For the display of heavy PDFs or very large images, we will use the CATiledLayer. This is a special layer which draws the layer content as a series of tiles, allowing the display of very large or memory intensive content such as huge images, PDFs, etc.

First we need to get a CGPDFPageRef from the PDF file. To do that we first get the PDF CGPDFDocumentRef, using the CGPDFDocumentCreateWithURL() function. From there we can retrieve the CGPDFPageRef by calling CGPDFDocumentGetPage(documentRef, pageNumber).

With the CGPDFPageRef in hand, we can now create our CATiledLayer. Below, is an example of the drawLayer method from a subclassed CATiledLayer:

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
        CGPDFPageRef pageRef = myPageRef
        if (pageRef != nil && ctx != nil) {
            [(NSObject*)pageRef retain];
            //prevent releasing while drawing
            CGPDFPageRetain(pageRef);
            CGContextRetain(ctx);
            CGContextSaveGState(ctx);

            CGRect bounding = layer.bounds;            
            CGContextSetRGBFillColor(ctx, 1.0, 1.0, 1.0, 1.0);
            CGContextFillRect(ctx, CGContextGetClipBoundingBox(ctx));
            CGContextTranslateCTM(ctx, 0.0, bounding.size.height);
            CGContextScaleCTM(ctx, 1.0, -1.0); 
            CGContextConcatCTM(ctx, CGPDFPageGetDrawingTransform(pageRef, kCGPDFCropBox, bounding, 0, true));
            CGContextDrawPDFPage(ctx, pageRef);
            CGContextRestoreGState(ctx);
            CGContextRelease(ctx);
            CGPDFPageRelease(pageRef);
            [(NSObject*)pageRef release];
        }

}

First we retain both the pageRef and the CGContextRef. (It is not strictly necessary to retain the pageRef but due to the multithreaded nature of CATiledLayer rendering a little defensive programming doesn't hurt) and save the CGContextRef state.

Next, using the layer bounds the pdf background color (white) is filled in. Since the iPhone coordinate system is the reverse of that of the Mac, we must translate and scale the context to flip the coordinate system and force rendering from the bottom left corner. After concacting the context to the PDF rendering rect, we finally draw the PDF to the context.After drawing, restore and release the context state, and if your done with the CGPDFPageRef and CGPDFDocumentRef, release them with CGPDFPageRelease and CGPDFDocumentRelease respectively.

This will get you a basic tiled PDF display.

One unfortunate drawback is that display of a tiled layer is not exactly speedy, and the user will be left staring at a black page while the various tiles are being drawn. So how do we get something to be displayed immediately when the UIView is presented? The easiest way is to create another CALayer behind the CATiledLayer and use it to draw a low resolution image of the pdf content. Something like this in your UIView:

//set up the background image layer
self.imageLayer = [CALayer layer];
//set the size and contents with the image     
self.imageLayer.contents = (id) yourUIImage.CGImage;
//add the layers
[self.layer addSublayer:self.imageLayer];

But PDF display is still slow, memory intensive, etc and can create problems when paging quickly through a PDF document. Since the true value of displaying a PDF is for the display of clean text while zooming, one way to limit the damage is in the CATiledLayer to only draw the pdf when zoomed in above a threshold and use images for content at lower zoom scales. This will also limit exposure of your app to PDF rendering memory leaks that can be found in certain iOS versions. For example:

{
    if(self.zoomScale < 2.0 ){
        CGImageRef  cgImage = yourUIImage.CGImage;
        if( cgImage != nil )
        {        
            CGContextSaveGState( context );
            CGRect bounding = self.bounds;
            CGContextTranslateCTM(ctx, 0.0, bounding.size.height);
            CGContextScaleCTM(ctx, 1.0, -1.0); 
            CGContextDrawImage( context, bounding, cgImage );
            CGContextRestoreGState( context );
        }
    }else {
        CGPDFPageRef pageRef = myPageRef
        ...
    }
}

While this doesn't cover all of the details, it should be enough to get you started.