KalyanChakravarthy.net

Thoughts, stories and ideas.

Detect touches on words in UILabels

Detecting touches on UIViews is quite trivial. But trying to recognize touches on individual words or attributed strings can be not so much, which was exactly what I wanted to do.

There are 2 approaches one can take

  • CoreText and compute the frames of all glyphs
  • TextKit - which is not exactly kit per se, but are bunch of classes part of UIKit

TextKit

TextKit is designed based on MVC, with:

  • M being NSTextStorage
  • V being NSTextContainer
  • C being NSLayoutManager

NSTextStorage acts as the data provider and can own multiple NSLayoutManagers. For the layout manager to display text, it needs an instance of NSTextContainer, which essentially defines the area of visible text.

NSTextContainer has 2 main properties defining the layout - the bounds and the exclusion paths. Bounds is the outline rect, this is required. Optionally one can pass a bezier path to define which areas are not to be layouted - allowing one flow text around images.

Here is some code that detects touch on "Read More" string of a UILabel*.

:::objc

/*
	Some initialization
*/
- (void)viewDidLoad {
	// The full string
	NSMutableDictionary *attributesForString = [[NSMutableDictionary alloc] init];
	attributesForString[ NSFontAttributeName ] = [UIFont systemFontOfSize:13];
	self.attrString = [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor set amit"];
	
	// The "Read More" string that should be touchable
	attributesForString[ NSFontAttributeName ] = [UIFont boldSystemFontOfSize:13];
	self.moreString = [[NSAttributedString alloc] initWithString:@"READ MORE" attributes:attributesForString];
	[self.attrString appendAttributedString:self.moreString];
	
	// Store range of chars we want to detect touches for
	self.moreStringRange = [self.attrString.string rangeOfString:self.moreString.string];
	
	self.textLabel.attributedString = self.attrString;

	UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self selector:@selector(didTap:)];
	[self.textLabel addGestureRecgonizer:tapRecognizer];
}

/*
	Simple touch recognition.
	Could be setup during initialisation.
*/
- (void)didTap:(UITapGestureRecognizer *)gesture {
	// Storage class stores the string, obviously
	NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attrString];
	
	// The storage class owns a layout manager
	NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
	[textStorage addLayoutManager:layoutManager];
	
	// Layout manager owns a container which basically
	// defines the bounds the text should be contained in
	NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.textLabel.frame.size];
	
	// For labels the fragment padding should be 0
	textContainer.lineFragmentPadding = 0;
	
	// Begin computation of actual frame
	// Glyph is the final display representation
	// Eg: Ligatures have 2 characters but only 1 glyph.
	NSRange glyphRange;
	
	// Extract the glyph range
	[layoutManager characterRangeForGlyphRange:self.moreStringRange actualGlyphRange:&glyphRange];
	
	// Compute the rect of glyph in the text container
	CGRect glyphRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
	
	// Final rect relative to the textLabel.
	NSLog( @"%@", glyphRect );
	
	// Now figure out if the touch point is inside our rect
	CGPoint touchPoint = [gesture locationOfTouch:0 inView:self.textLabel];
	
	if( CGRectContainsPoint(glyphRect, touchPoint) ) {
		NSLog( @"User tapped on Read More. So show something more");
	}
}

Sketch Plugin to resize iOS app icon to all resolutions

Most common use-case of designing an iOS App Icon, simply involves designing the main [email protected] and then resizing it to all other resolutions. Since the default template does not come with a smart symbol available across all artboards, you have to create one and copy it over and resize it manually.

Since I am too lazy to do it, I wrote a sketch plugin in javascript.

Steps:

  1. Open Sketch
    • Create New From Template
    • iOS App Icon
  2. Design in [email protected] Artboard
  3. Make sure all design happens inside one single layer group in the art board.
  4. Open Plugins menu option and select Custom Plugin
  5. Copy Paste code from below
  6. Run & Profit!

Code:

:::javascript
var resizeToAllCanvases = function(context) {

	// Current board
	var artboards = context.document.artboards();
	var mainBoard;

	for( i=0; i<artboards.count(); i++) {
		if( artboards[i].name().containsString("iTunesArtwork") )
			mainBoard = artboards[i];
	}

	var mainShape = mainBoard.layers().firstObject();
	var mainName= mainBoard.name();

	// Go through all the art boards
	// and copy the layer group
	for( i=0; i<artboards.count(); i++) {
		if( artboards[i].name() != mainBoard.name() )  {
			var targetBoard = artboards[i]

			// Resize
			mainRect = mainShape.rect()
			mainSize = mainRect.size;
			newRect = CGRectZero;
			newRect.size = targetBoard.rect().size;
			newSize = newRect.size;
			
			// Copy the Layer item
			mainShapeCopy = mainShape.copy()
			mainShapeCopy.frame().width = newSize.width;
			mainShapeCopy.frame().height = newSize.height;

			// Clear out the art board and paste layer
			targetBoard.layers().removeAllObjects()
			targetBoard.layers().addObject( mainShapeCopy )
		}
	}
};

resizeToAllCanvases(context);

Convert DXF to SVG

After discovering a local makerspace which has free open days, I wanted to try their laser cutter, for which I needed to provide a file in SVG format.

One of the challenges I set myself up for was to be able to create model and have series of scripts to do the rest of conversion for me.

I started with a simple sliced model in OpenSCAD, which unfortunately only exported to DXF format

module side() {
	difference() {
		cube([50, 50, 10], center = true);
		translate([-10,0,0]) cube([20, 20, 10], center = true);
	}
}

projection(cut=true) side();

So I first converted to DXF

$ ~/Applications/OpenSCAD/Contents/MacOS/OpenSCAD -o ex1.dxf ex1.scad 

There are no tools out there to ease the conversion to SVG (on mac at least). Inkscape does support it, but getting it to run on Mac was a hassle. So I resorted to using FreeCAD which has nice python bindings, to write an export script

:::python
# exporter.py 
import sys
ifile = sys.argv[0]
ofile = sys.argv[1]

import importDXF
import importSVG

obj = importDXF.open( ifile )
importSVG.export( obj.Objects, ofile )

And then converted the DXF to SVG using this

:::shell
$ ~/Applications/FreeCAD.app/Contents/bin/FreeCADCmd exporter.py ex1.dxf ex1.svg

Here is the final result

Drawing anti-aliased unicode text with python

For an app I was hacking during the weekend, I needed to generate images for all characters of Berber alphabet and had to figure out several things

converting hexcode to python unicode

Since there were a lot of characters and they were contiguous, I could loop through them - for this, I had to figure out how to convert hex code into python unicode character

:::python
hexCode = '0x2d62'                # U+2d62
intValue = int( hexCode, 16 )     # hex is base 16

# using unicode(intValue) won't work
pyUnicodeChar = unichr( intValue ) 

drawing on the image

Next came the part of drawing it on a ImageDraw surface -

:::python
# Load the True Type font for anti-aliasing
font = ImageFont.truetype( 'T_I_UNICODE.ttf', 400 )

# Init the drawing surface
image = Image.new( 'RGBA', (500,500) )
drawPad = ImageDraw.Draw(image)

# Draw pyUnicodeChar string at origin in black with opacity=1
drawPad.text( (0,0), pyUnicodeChar, fill=(0,0,0,225) )

computing font size for centring

Note: ImageFont or ImageDraw, do not provide functionality to centre the text and has to be done manually by computing its size, which can be done this way

:::python
# using ImageDraw instance
textSize = drawPad.textsize(pyUnicodeChar, font=font)

# Or, using ImageFont instance
textSize = font.getsize( pyUnicodeChar )

python implementation

Here is the full code to generate the image shown above

:::python
# -*- coding: utf-8 -*-
# Above comment is needed if we use unicode chars directly in code

from PIL import Image, ImageDraw, ImageFont
import sys, os

# Constants
fontFile = 'T_I_UNICODE.ttf'

# Unicode characters from the Berber alphabet system
hexcodeChars = ['0x2d62', '0x2d65', '0x2d4b', '0x2d3e']

# Load the font
font = ImageFont.truetype(fontFile, 400)

# Convert the hexCodes to python unicode strings
unicodeChars = map( lambda c: unichr( int(c, 16) ), hexcodeChars )

# Compute the font sizes, to derive an optimal image size
unicodeCharSizes = map( lambda c: font.getsize(c), unicodeChars )
maxCharDimension = max( map( lambda s: max(s), unicodeCharSizes ) )

layoutPadding = 10
gridSize = maxCharDimension*2 + layoutPadding*3

# Initalize the image
theImage = Image.new( 'RGB', (gridSize,gridSize), color='white' )
theDrawPad = ImageDraw.Draw(theImage)

i = 0
for char, size in zip(unicodeChars, unicodeCharSizes):
	x, y = ( i % 2, i / 2 )
	xSize, ySize = size

	# get the grid cell position, and then center the font in that cell
	# as only way to do it is to compute size of char & position it manually
	xPos = (maxCharDimension * x) + (maxCharDimension-xSize)/2.0
	yPos = (maxCharDimension * y) + (maxCharDimension-ySize)/2.0

	theDrawPad.text( (xPos, yPos), char, font=font, fill='black' )
	
	i += 1

theImage.save('antialiased-berber-chars.png')

note: PIL & Pillow

  • For using the Image/ImageDraw/ImageFont library, install PIL using
    pip install Pillow
  • the original PIL is quite buggy and is not maintained to by knowledge
    • for example font size returned for true-type fonts is wrong and inconsistent.
  • Pillow is the drop-in replacement/fork for it, which addresses all the issues and is actively maintained.
  • If you have older PIL installed, remove it using pip uninstall PIL

Flask custom template loaders

Flask is amazing. The templating system called jinja2 packed with it is equally amazing.

The default behaviour of flask app is to look for template files specified in app.template_folder directory. Although this will work in most use-cases, sometimes you need additional control over this - for example if you have user specific templates or if you want to load templates from a database, dynamically.

This can be accomplished, by initialising app.jinja_loader with a custom Loader.

Jinja2 ships with serveral different loaders by default. The easiest one is DictLoader, which simply loads tempalte sfrom a dictionary.

:::python
from flask import Flask
import jinja2

app = Flask(__name__)
app.jinja_loader = jinja2.DictLoader({
		'index.html' : """
			{% extends 'base.html' %}
			{% block text %}
			Super cool!
			{% endblock %}
		""",

		'base.html' : """
			<b>{{ self.text() }}</b>
		"""

	})
	
@app.route('/')
def doHome():
	return render_template('index.html')

The above example was fairly simple - it still pre-loads all templates. If there is a non-trivial requirement where you want to load them from database, then you can use FunctionLoader

:::python
def load_template(template_name):
	is_uptodate = True
	if template_name == 'index.html':
		return ("""
			{% extends 'base.html' %}
			{% block text %}
			Super cool!
			{% endblock %}
		""", None, is_uptodate)

	if template_name == 'base.html':
		return ("""
			<b>{{ self.text() }}</b>
		""", None, is_uptodate)


app.jinja_loader = jinja2.FunctionLoader(load_template)

Jinja2 ships with the following loaders, all of which can be used with flask

  • FileSystemLoader
  • PackageLoader
  • DictLoader
  • FunctionLoader
  • PrefixLoader
  • ChoiceLoader
  • ModuleLoader