TLDR: This blog post provides a detailed tutorial on how to
implement OmniGroup’s OUIEditableFrame
in an iOS project to
render text with proper kerning, which cannot be achieved with standard UIKit
controls due to a bug. The guide walks through creating and configuring an
Xcode workspace, cloning the OmniGroup project from GitHub, adding necessary
frameworks and dependencies, and writing the required view controller code.
Learn to implement OmniGroup's
OUIEditableFrame
for proper text kerning in iOS projects with
this step-by-step Xcode tutorial.
Table of Contents:
- Introduction
- Create and Configure Xcode Workspace
- Clone the OmniGroup Project from GitHub
- Add the FixStringsFile Project
- Add OmniBase
- Add the Rest of the Frameworks
- Write View Controller Code
- Making It Work in iOS 5
- Get Rid of the Static Analyzer Message
If you need a UITextField
that renders text with proper kerning
you don’t have many of options. Writing all the CoreText code is a major
undertaking and not for the average iOS programmer. One third-party option is
EGOTextView but it
hasn’t been updated for a couple of years and should be considered abandoned.
While iOS started supporting attributed strings for UIKit controls a
bug in UITextView
prevented it from kerning properly.
The only other real option is OmniGroup’s OUIEditableFrame
which
is part of their
iOS/macOS X open source frameworks. Omni uses these frameworks in their own highly respected products. However,
it’s not exactly easy to incorporate into your own project. There are several
places where dependencies need to be specified and the Xcode workspace needs
to be set up a particular way.
I haven’t seen any documentation on how to use the OmniGroup framework which
is why I created this tutorial. This will serve as a reference to myself in
the future and hopefully others will find it useful as well. The goal of this
tutorial is to create a project which uses OUIEditable
frame in
the simplest possible manner.
DISCLAIMER: There is no warranty that the approach to a possible solution is the best one.
Create and Configure Xcode Workspace
Create a new Xcode project using the Single View Application template for iOS.
Target iPad. Use Storyboards and Automatic Reference Counting. Name the
project SimpleEditableFrame
.
Close the project with File | Close Project but leave Xcode
running. Create a workspace with File | New Workspace and
call it SimpleEditableFrame
. Save it in the
SimpleEditableFrame
directory which Xcode created in the previous
step.
Add SimpleEditableFrame.xcodeproj
to the workspace. I drag the
.xcodeproj
file from Finder into Xcode’s navigation (left-most)
pane. If you usually use a different method to add projects to a workspace
feel free to use that.
Clone the OmniGroup project from GitHub
We don’t want to mix the OmniGroup git clone with the git repository for our
own project so we’re going to clone it into a directory that’s completely
separate from our project. On my system I keep the OmniGroup files in
~/dev/Omni/
. Create this directory on your system and clone the
git repository.
cd ~/dev/Omni/
git clone git://github.com/omnigroup/OmniGroup
cd OmniGroup
git submodule update --init
When the git clone is completed you can copy the OmniGroup directory to your
project directory (omiting the .git
directory). The project
directory should now contain the following
OmniGroup/
SimpleEditableFrame/
SimpleEditableFrame.xcodeproj
SimpleEditableFrame.xcworkspace
Add the FixStringsFile Project
Add OmniGroup/Tools/FixStringsFile/FixStringsFile.xcodeproj
to
the workspace. Make sure it’s at the same level as
SimpleEditableFrame and not contained within it. The
workspace should now contain two projects at the same level:
SimpleEditableFrame and FixStringsFile.
In the FixStringsFile project open
Configurations/Target-Mac-Common.xcconfig
. Comment out the
following line:
OMNI_MAC_CODE_SIGN_IDENTITY = Mac Developer:
If you are developing only for iOS this line is unnecessary and will likely cause problems when building.
Edit the Scheme (⌘<) and choose the Build action. Add the
FixStringsFile
target using the + button at the
bottom of the list. Drag FixStringsFile
to the top of the list.
Uncheck Parallelize Build and
Find Implicit Dependencies.
Build (⌘B). The project should build cleanly at this point.
Add OmniBase
I find it helpful to add just OmniBase
at first and get the
project to build with only that framework. Then I add the rest of the Omni
frameworks. This requires a bit of extra work but if you’re just getting
started it’s a good idea to minimize the amount you’re trying to change at any
one time. If something goes wrong there are fewer things that could be the
cause of the problem and the changes required to make fixes are easier since
you’re only dealing with one framework instead of several.
Add the OmniGroup/Configurations
directory to the
SimpleEditableFrame project by right-clicking the project and
choosing Add Files to “SimpleEditableFrame”… from the context
menu. Make sure
Copy groups into destination group’s folder (if needed) is
unchecked. The files already exist in the project directory. We just need to
create references to them in the project.
In Configurations/Configurations/Target-Mac-Common.xcconfig
the
OMNI_MAC_CODE_SIGN_IDENTITY
line is probably already commented
out. Double check to ensure it is.
Right-click on the project and choose New Group. Name the new
group OmniFrameworks
. Add
OmniGroup/Frameworks/OmniBase/OmniBase.xcodeproj
to the
OmniFrameworks
group. I use drag-n-drop from Finder. You can use
what works best for you.
Select the SimpleEditableFrame project in the navigator and
go to Build Phases for the
SimpleEditableFrame target. In the
Link Binary with Libraries section add
libOmniBase.a
. In the
Target Dependencies section add OmniBaseTouch
.
Build (⌘B). The project should build cleanly at this point. This is a major milestone in the process.
Add the Rest of the Frameworks
Add these system frameworks in the Link Binary with Libraries section of the Build Phases tab.
CoreText.framework
QuartzCore.framework
MobileCoreServices.framework
MessageUI.framework
Security.framework
SystemConfiguration.framework
AssetsLibrary.framework
ImageIO.framework
libz.dylib
libxml2.dylib
In the navigator drag these into the Frameworks group to keep things organized.
Add the remaining OmniGroup frameworks to the workspace. Again, I use drag-n-drop from Finder to Xcode but you can use what works best for you. Just be careful to ensure they land in the OmniFrameworks group.
OmniFoundation.xcodeproj
OmniQuartz.xcodeproj
OmniAppKit.xcodeproj
OmniFileStore.xcodeproj
OmniFileExchange.xcodeproj
OmniUI.xcodeproj
OmniUIDocument.xcodeproj
OmniUnzip.xcodeproj
Add the following to Link Binary with Libraries in the Build Phases tab.
libOmniFoundation.a
libOmniQuartz.a
libOmniAppKit.a
libOmniFileStore.a
libOmniFileExchange.a
libOmniUnzip.a
libOmniUI.a
libOmniUIDocument.a
Add the following to Target Dependencies in the Build Phases tab.
- OmniFoundationTouch (OmniFoundation)
- OmniFoundationTouchTests (OmniFoundation)
- OmniQuartzTouch (OmniQuartz)
- OmniAppKitTouch (OmniAppKit)
- OmniAppKitTouchTests (OmniAppKit)
- OmniUnzipTouch (OmniUnzip)
- OmniFileStoreTouch (OmniFileStore)
- OmniUITouch (OmniUI)
- OmniUIDocument (OmniUIDocument)
Edit the Scheme (⌘<) and choose the Build action. Add the following libraries as targets. I’m not sure if the order is important but just to be safe I kept these in the same order as shown in Omni’s example TextEditor application.
- OmniBaseTouch
- OmniFoundationTouch
- OmniUnzipTouch
- OmniFileStoreTouch
- OmniAppKitTouch
- OmniQuartzTouch
- OmniFileExchangeTouch
- OmniUITouch
Go back to the Build Phases screen. Select Add Build Phase and then Add Run Script.
Paste the following line as the contents of the script.
OmniGroup/Scripts/CopyLibraryResources
Build (⌘B). The project should build with one warning from the static analyzer. This is okay. If you want to get rid of the warning see the section at the end of this tutorial. You are now building the project with all of the required Omni frameworks classes. This is another major milestone.
Write View Controller Code
In the SimpleEditableFrame project open ViewController.m
.
Import the OUIEditableFrame header.
#import <OmniUI/OUIEditableFrame.h>
Create some defines for the font information we’ll use in the app. Add these after the imports.
#define FONT_NAME @"HelveticaNeue-Light"
#define FONT_WEIGHT 36.0
#define TEST_STRING @"AVAVA"
Add the following to viewDidLoad
so it looks like this.
- (void)viewDidLoad
{
[super viewDidLoad];
// Create attributed string for use in the OUIEditableFrame
CTFontRef fontFace = CTFontCreateWithName((__bridge CFStringRef)(FONT_NAME), FONT_WEIGHT, NULL);
NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
[attributes setObject:(__bridge id)fontFace forKey:(NSString*)kCTFontAttributeName];
[attributes setObject:[UIColor blackColor] forKey:(NSString*)kCTForegroundColorAttributeName];
NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:TEST_STRING];
// Create the OUIEditableFrame and add it to the view
OUIEditableFrame *omniEditableFrame = [[OUIEditableFrame alloc] initWithFrame:CGRectMake(60, 100, 648, 200)];
omniEditableFrame.defaultCTFont = fontFace;
omniEditableFrame.backgroundColor = [UIColor lightGrayColor];
omniEditableFrame.textColor = [UIColor blackColor];
omniEditableFrame.attributedText = attrStr;
[self.view addSubview:omniEditableFrame];
// For comparison create a regular UITextField and add it to the view
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(60, 340, 648, 200)];
textField.backgroundColor = [UIColor lightGrayColor];
textField.font = [UIFont fontWithName:FONT_NAME size:FONT_WEIGHT];
textField.text = TEST_STRING;
[self.view addSubview:textField];
}
Build and Run (⌘R). You should notice the the
OUIEditableFrame
has less space between the characters. It’s
rendering with CoreText instead of WebKit which the
UITextField
uses under the hood. CoreText honors the font’s
built-in kerning.
Of course, all this will likely be unecessary in iOS 7 but for projects which need to support older versions of iOS this is practically the only way to get CoreText font rendering in an editable text field.
Making It Work in iOS 5
If you try to run this in the iOS 5 simulator and get errors like the following you’ll need to do a little extra configuring in Xcode.
dyld: Symbol not found: _objc_setProperty_nonatomic
This is usually the case when the deployment targets for dependent libraries are higher than the deployment target for the main project. In this case I set the project’s iOS Deployment Target to 5.1. I also had to manually set the iOS Deployment Target for the Omni libraries to 5.1 as well. First select each of the Omni projects in the navigator and set the iOS Deployment Target to 5.1 in the Project Info screen.
You’ll need to do this for each of the included frameworks. Next, select the library targets for each of the included Omni frameworks and set the iOS Deployment Target to 5.1 in the Build Settings screen.
Do this for each library target in each of the included Omni frameworks. Build & Run (⌘R) and it should run in the iOS 5 simulator.
Get Rid of the Static Analyzer Message
Chances are good you see the following message when building.
Instance variable used while 'self' is not set to the result of '[(super or self) init...]
If you examine the code in question you can see that
[super init]
is indeed being called and the code is entirely
safe. This is a case of Xcode’s static analyzer getting tripped up and
reporting a false positive. The easiest way to turn off this warning is to
wrap the problem code in a __clang_analyzer__
directive. For
example:
#ifndef __clang_analyzer__
// Code the analyzer should ignore
#endif
Because the problem code in this case spans a couple of methods we’ll need to
be careful about where to place the directives. It should cover both the
mutableCopyWithZone
and
initWithParagraphStyle
methods. Here’s what this section of the
file looks like for me.
#ifndef __clang_analyzer__
- (id)mutableCopyWithZone:(NSZone *)zone;
{
return [[OAMutableParagraphStyle alloc] initWithParagraphStyle:self];
}
@end
@implementation OAMutableParagraphStyle
+ (OAParagraphStyle *)defaultParagraphStyle;
{
// Return a mutable instance when sent to the mutable subclass
return [[[super defaultParagraphStyle] mutableCopy] autorelease];
}
- initWithParagraphStyle:(OAParagraphStyle *)original;
{
if (!(self = [super init]))
return nil;
if (original) {
memcpy(&_scalar, &original->_scalar, sizeof(_scalar));
_tabStops = [[NSArray alloc] initWithArray:original->_tabStops];
}
return self;
}
#endif
This is a completely optional step but if you get tired of seeing that little blue icon on each build this is how to safefly supress it.
You can also check out the source code on GitHub.
Comments