David Janes' Code Weblog

April 9, 2010

Djolt-obj: Django-like templates for Objective-C

code fragments,djolt,iphone · David Janes · 7:51 am ·

My company, Discover Anywhere Mobile, has open sourced a software component that many iPhone developers may be interested in: Djolt (Django-like Templates) for Objective C. The objective of this project is to be able to take NSObjects – particularly NSDictionary, NSArray, NSString and NSNumber – and be able to feed them to a text template, Django style, and get text output. This will be fantastic for working with APIs that deliver data via JSON or XML.

It’s dependent on RegexKit library for now, though if the iPhone OS 4 NSRegularExpression is powerful enough we may transition to that.

The code is being tested against the Django template test cases, with the goal of having as complete as possible (given different language idioms) compatibility. We only cover a small subset of Django yet – the stuff we need – but if you’re interested in working on the project, please by all means join in.

Code

NSDictionary* d = [[NSData dataWithContentsOfFile:@"test.json"] yajl_JSON];

DjoltTemplate* t = [[DjoltTemplate alloc] initWithTemplateFile:@"test.djolt"];
NSString* r = [t render:d];

test.json

{
 "title" : "Test 1",
 "items" : [
  {
   "href" : "http://www.example.com/1",
   "title" : "Example 1"
  },
  {
   "href" : "http://www.example.com/2",
   "title" : "Example 2 & Example \"2.5\""
  },
  {
   "href" : "http://www.example.com/3",
   "title" : "Example 3"
  }
 ]
}

test.djolt

<html>
<head>{{ title }}</head>
<body>
{% if fred or items or bleck %}
<ul>
{% for itemd in items %}
<li><a href='{{ itemd.href }}'>{{ itemd.title|safe }}</a> ({{ forloop.counter }})</li>
{% endfor %}
</ul>
{% endif %}
<p>
{{ nothing|default:"something" }}
</p>
</body>
</html>

Output

<html>
<head>Test 1</head>
<body>

<ul>

<li><a href='http://www.example.com/1'>Example 1</a> (1)</li>

<li><a href='http://www.example.com/2'>Example 2 & Example "2.5"</a> (2)</li>

<li><a href='http://www.example.com/3'>Example 3</a> (3)</li>

</ul>

<p>
something
</p>
</body>
</html>

March 1, 2010

Key Preserving YAJL

code fragments,iphone · David Janes · 8:35 am ·

Of all the JSON parsing libraries for the iPhone, the one I like the most is YAJL-obj, which is based on the “event driven/SAX style” YAJL. When I first started creating iPhone applications, I used a lot of Plists; I’ve now switched this all over to JSON not only because it’s more standards-y, but also because I can use the same data sets with my Python/Django and Android applications.

The JSON data I’m working with looks something like a dump of a SQL Table: lots of rows using the same column names — i.e. dictionary keys – over and over. If you’re loading a couple of thousand rows of data, each with 20 or or 30 columns/keys, the shear size of the key strings can add up — even though really there’s only a couple of dozen keys in total really in use. All these duplications waste a lot of memory – which is especially tight on a 8Gb iPod Touch – so I decided it might help out to actually reuse keys in my JSON loaders.

Here’s the changes I made to YAJL:

YAJLDocument.h

@interface YAJLDocument : NSObject  {
...
    NSMutableDictionary *keyStore_;
}
@property(nonatomic,retain) NSMutableDictionary *keyStore_;

YAJLDocument.m

- (id)initWithParserOptions:(YAJLParserOptions)parserOptions {
	if ((self = [super init])) {
...
		keyStore_ = [[NSMutableDictionary alloc] initWithCapacity:16];
	}
	return self;
}

- (void)parser:(YAJLParser *)parser didMapKey:(NSString *)key {
#if 1 // DPJ 2010-03-01
	key_ = [keyStore_ objectForKey:key];
	if (!key_) {
		[keyStore_ setObject:key forKey:key];
		key_ = key;
	}
#else
	key_ = key;
#endif
	[keyStack_ addObject:key_]; // Push
}

To rebuild your library:

  • select build target Device – 3.0 | Release | Combine Libs
  • the output file is Project-IPhone/build/libYAJLIPhone-*.zip
  • just add all the things in that ZIP file to your iPhone project and you’re good to go

Final thoughts:

  • whether this happens or not should be controlled by the YAJLParserOptions
  • I’m probably going keep modifying this code, adding a version with a “scrubbing delegate” that allows dictionaries to be scrubbed on the spot of data not needed in the application

December 25, 2009

Make INPUT fields select dates using the YUI Calendar Control

code fragments,javascript,tips,yui · David Janes · 3:57 pm ·

Here’s a lengthy Javascript code segment that can be dropped into any of your existing forms that provide for a date selection range. It has the following functionality:

  • clicking a date field pops up a YUI Calendar control to change the date
  • the date field is made read only by this code
  • pressing ESC while a Calendar is displaying dismisses it
  • pressing DEL while a Calendar is displaying clears the input field
  • there is an Input field for the start date and an Input field for the end date
  • you cannot make the end date before the start date, and you cannot make the start date after the end date
  • that the dates are optional
  • dates are displayed in the standard ISO YYYY-MM-DD format

This code sample demonstrates:

  • the YUI Calendar control, including
    • setting the date in the control
    • getting the date from the control (after selection)
    • dynamically displaying and hiding the control
    • manipulating ISO dates into the format that YUI requires
    • setting min and max dates on the fly – specifically, this shows you how to manipulate YUI “config” parameters which the documentation implies are readonly (look for functions in source code named like configMinDate)
    • simple YUI animations (to show that a field value has been changed)
    • detecting keystrokes (DEL and ESC) using YUI
    • adding functionality using Javascript, after the DOM loads

    This assumes:

    • you have an Input field for the start date with DOM id="id_event_start"
    • you have an Input field for the end date with DOM id="id_event_end"
    • that the Form (or a parent element of the Form, up to Body) has DOM class="yui-skin-sam"

    Add the following Script inclusions to your HTML (note: not all of these are probably needed, and there’s more efficient ways to include YUI JS, especially for production deployments):

    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/json/json-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/event/event-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/connection/connection-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/dom/dom-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/element/element-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/button/button-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/dragdrop/dragdrop-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/container/container-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/yuiloader/yuiloader-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/container/container_core-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/calendar/calendar-min.js"></script>
    <script type="text/javascript"
     src="http://yui.yahooapis.com/2.8.0r4/build/animation/animation-min.js"></script>
    

    Add the following CSS inclusions to your HTML:

    <link rel="stylesheet" type="text/css"
     href="http://yui.yahooapis.com/2.8.0r4/build/button/assets/skins/sam/button.css" />
    <link rel="stylesheet" type="text/css"
     href="http://yui.yahooapis.com/2.8.0r4/build/container/assets/skins/sam/container.css" />
    <link rel="stylesheet" type="text/css"
     href="http://yui.yahooapis.com/2.8.0r4/build/calendar/assets/skins/sam/calendar.css" />
    

    Add the following CSS to your HTML:

    <style type="text/css">
    
    #id_calendar {
     position: absolute;
     z-index: 15;
    }
    .yui-skin-sam .yui-panel .bd, .yui-skin-sam .yui-panel .ft {
     background-color:#FFFFFF;
    }
    </style>
    

    Add the following HTML fragment:

    <div id="id_calendar_wrapper">
     <div id="id_calendar"></div>
    </div>
    

    And finally, here’s the JS that makes it all work

    <script type="text/javascript">
    calendar_js = {
     c : null,
     start_e : null,
     end_e : null,
     current_e : 0,
     supress : 0,
    
     create : function() {
     calendar_js.start_e = YAHOO.util.Dom.get("id_event_start");
     calendar_js.end_e = YAHOO.util.Dom.get("id_event_end");
    
     YAHOO.util.Event.addListener(calendar_js.start_e, "click", calendar_js.onclick, this);
     YAHOO.util.Event.addListener(calendar_js.end_e, "click", calendar_js.onclick, this);
    
     YAHOO.util.Dom.get(calendar_js.start_e).readOnly = true;
     YAHOO.util.Dom.get(calendar_js.end_e).readOnly = true;
    
     (new YAHOO.util.KeyListener(document, { keys:27 }, { fn:calendar_js.onesc })).enable();
     (new YAHOO.util.KeyListener(document, { keys:8 }, { fn:calendar_js.ondel })).enable();
     },
    
     onesc : function() {
     calendar_js.current_e = null;
     if (calendar_js.c) {
     calendar_js.c.hide();
     }
    
     },
    
     ondel : function() {
     if (calendar_js.current_e) {
     calendar_js.current_e.value = "";
     calendar_js.current_e = null;
     }
     if (calendar_js.c) {
     calendar_js.c.hide();
     }
    
     },
    
     toydate : function(d) {
     if (!d) {
     return null;
     }
     if (d.value) {
     d = d.value;
     }
     if (d && d.length) {
     d = d.replace(/(\d\d\d\d)-(\d\d)-(\d\d)/, "$2/$3/$1");
     if (d.indexOf('/') == 2) {
     return d;
     }
     }
     return null;
     },
    
     onclick : function(evt, obj) {
     var e_e = YAHOO.util.Event.getTarget(evt);
    
     if (calendar_js.c == null) {
     calendar_js.c = new YAHOO.widget.Calendar(null, "id_calendar");
     calendar_js.c.render();
    
     calendar_js.c.selectEvent.subscribe(calendar_js.ondate, calendar_js.c, true);
     }
    
     var c_e = YAHOO.util.Dom.get("id_calendar");
     if (calendar_js.current_e == e_e) {
     calendar_js.current_e = null;
     calendar_js.c.hide();
     } else {
     YAHOO.util.Dom.setAttribute(c_e, "display", "block");
     calendar_js.current_e = e_e;
    
     var region = YAHOO.util.Region.getRegion(e_e);
    
     calendar_js.c.show();
    
     if (e_e.value.length) {
     var d = calendar_js.toydate(e_e);
     if (d) {
     calendar_js.supress = 1;
     calendar_js.c.select(d);
     }
     }
    
     if (e_e == calendar_js.start_e) {
     var d = calendar_js.toydate(calendar_js.end_e);
     if (d) {
     calendar_js.c.configMaxDate(null, [ d ], null);
     calendar_js.c.configMinDate(null, [ "01/01/1970" ], null);
     }
     } else {
     var d = calendar_js.toydate(calendar_js.start_e);
     if (d) {
     calendar_js.c.configMinDate(null, [ d ], null);
     calendar_js.c.configMaxDate(null, [ "01/01/2200" ], null);
     }
     }
    
     calendar_js.c.render();
     YAHOO.util.Dom.setXY(c_e, [ region.right + 2, region.top ]);
     }
     },
    
     ondate : function(type, args, obj) {
     if (calendar_js.supress) {
     calendar_js.supress = 0;
     return;
     }
    
     var date = this.toDate(args[0][0]);
    
     var year = "" + date.getFullYear();
     var month = "" + ( 1 + date.getMonth() );
     while (month.length < 2) {
     month = "0" + month;
     }
     var day = "" + date.getDate();
     while (day.length < 2) {
     day = "0" + day;
     }
    
     var animate = new YAHOO.util.ColorAnim(calendar_js.current_e,
     {backgroundColor: { from: '#ffff99', to: '#FFFFFF' } }
     );
     animate.duration = 0.75;
     animate.method = YAHOO.util.Easing.easeOut;
     animate.animate();
    
     calendar_js.current_e.value = year + "-" + month + "-" + day;
     calendar_js.c.hide();
     calendar_js.current_e = null;
     },
    
     end : 0
    };
    YAHOO.util.Event.onDOMReady(calendar_js.oninit);
    </script>
    

    Notes:

    • no apologies for verboseness – I write code so I can understand it in 24 months time
    • my original code is a lot more “tabby” – I flattened to a single space so they’d fit in the space constraints here
    • the code should be fairly self explanatory if you step through it – the hard stuff is probably YUI weirdness and you can probably just accept that “that’s just the way it is”

November 28, 2009

UIImage imageNamed: and caching

code fragments,iphone · David Janes · 7:07 am ·

There may be some temptation to improve caching for UIImage imageNamed:

static UIImage* starred = nil;
if (starred == nil) starred = [UIImage imageNamed:@"starred.png"];

Don’t do this, it’s really stupid. It’s already being held by a cache, and maybe dropped out of it at any time, leading to miraculous crashes.

November 23, 2009

Animated Slideshow on Android

android,code fragments,java · David Janes · 7:20 pm ·

After a brutal day of running into many Android bugs and misdocumentations, I’ve finally figured out how to create a slideshow of images for Android, with each imaging fading to the next. This is not unlike AnimationDrawable, except with a smoother and slower transition between images.

First, you need a resource file with something like this in it. The FrameLayout is the clever bit: it stacks everyone of its children on top of each other.

<FrameLayout
 android:id="@+id/frame"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
>
 <ImageView
 android:id="@+id/slide_1"
 android:layout_gravity="center_vertical"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 />
 <ImageView
 android:id="@+id/slide_2"
 android:layout_gravity="center_vertical"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 />
</FrameLayout>

Then you need code that looks like this:

public class TopListActivity
  extends Activity
{
  private static class AnimationAlphaTimer
    extends TimerTask
    implements Animation.AnimationListener
  {
    TopListActivity topList;
    Vector<BitmapDrawable> images;
    int count = 0;

    public AnimationAlphaTimer(TopListActivity _topList)
    {
      this.topList = _topList;

      this.images = new Vector<BitmapDrawable>();
      for (int i = 0; ; i++) {
      // LOAD IMAGES HERE
      }

      if (this.images.size() > 0) {
        this.topList.slide_0.setBackgroundDrawable(this.images.get(0));

        if (this.images.size() > 1) {
          this.topList.slide_1.setBackgroundDrawable(this.images.get(1));
        }
      }

      this.count = 1;
    }

    public void launch()
    {
      if (this.images.size() >= 2) {
        (new Timer(false)).schedule(this, 100);
      }
    }

    @Override
    public void run()
    {
      this.doit();
      this.cancel();
    }

    private void doit()
    {
      if ((this.count % 2) == 0) {
        AlphaAnimation animation = new AlphaAnimation(1.0f, 0.0f);
        animation.setStartOffset(3000);
        animation.setDuration(3000);
        animation.setFillAfter(true);
        animation.setAnimationListener(this);

        this.topList.slide_1.startAnimation(animation);
      } else {
        AlphaAnimation animation = new AlphaAnimation(0.0f, 1.0f);
        animation.setStartOffset(3000);
        animation.setDuration(3000);
        animation.setFillAfter(true);
        animation.setAnimationListener(this);

        this.topList.slide_1.startAnimation(animation);
      }
    }

    public void onAnimationEnd(Animation animation)
    {
      if ((this.count % 2) == 0) {
        this.topList.slide_1.setBackgroundDrawable(
          this.images.get((this.count + 1) % (this.images.size()))
        );
      } else {
        this.topList.slide_0.setBackgroundDrawable(
          this.images.get((this.count + 1) % (this.images.size()))
        );
      }

      this.count++;
      this.doit();
    }

    public void onAnimationRepeat(Animation animation)
    {
    }
    public void onAnimationStart(Animation animation)
    {
    }
  }

  @Override
  public void onResume()
  {
    super.onResume();

    (new AnimationAlphaTimer(this)).launch();
  }
}

The “create a Timer trick” in onResume is courtesy of Diego Torres. If you just try to run the animation, it’s likely just to choke.

November 20, 2009

How to use XCode for Android Projects

android,code fragments,ideas,macintosh · David Janes · 6:07 pm ·

Let’s assume you already have an Android project on your Mac.

Create the XCode Project

  • start XCode
  • select File > New Project…
  • select External Build System
  • go to the parent directory of your Android Project
  • in the Save As: field, enter the directory name of your Android Project
  • select the scarily-misnamed Replace option [not in XCode 4 -- thanks Jusin]

Add Files

In your new XCode project:

  • select first item in the left hand column, which is the name of your project
  • right-click, select Add > Existing Files…
  • select add files (don’t select the Copy option)
  • organize as desired (I like to do a lot of grouping). You should be probably adding at least your Java files and your Layout resources.

Configure your Build Target

In your new XCode project:

  • look for Targets
  • inside will be a target for your project’s name
  • double click on it
    • change Build Tool to ant
    • change Arguments to install

Clicking ⌘B should now compile your project.

Note: if you figure out how to have a Build vs. Build & Install (e.g. ⌘ENTER) please let me know!.

Getting XCode to Recognize Java errors

  • Reconfigure the Build Target, changing ant to ./xant
  • Make a file xant in the project’s home directory, using the code below
  • do (from a Terminal) chmod a+x xant
#!/usr/bin/env python

import sys
import re
import subprocess

av = list(sys.argv)
av[0] = "ant"

p = subprocess.Popen(av, stdout = subprocess.PIPE)

javac_rex = re.compile(" +[[]javac[]] +")
line_rex = re.compile("[.]java:[\d]+:")

pending = ""
while True:
    d = p.stdout.read(128)
    if not d:
        break

    d = pending + d

    nx = d.rfind('\n')
    if nx == -1:
        pending = d
        continue
    else:
        d, pending = d[:nx + 1], d[nx + 1:]

        d = javac_rex.sub("", d)
        d = line_rex.sub(r"\g error: ", d)
        sys.stdout.write(d)
        sys.stdout.flush()

sys.stdout.write(pending)
p.wait()
sys.exit(p.returncode)

Note: this code has been updated from the original post. It now reads little chunks and outputs them immediately rather than post-processing the ant output.

November 16, 2009

Django 1.1 and ImageField

code fragments,django,python,tips · David Janes · 7:42 am ·

Having recently upgraded to Django 1.1, I suddenly started getting the error messages that look like:

  File "/Library/Python/2.6/site-packages/django/db/models/fields/related.py", line 257, in __get__
    rel_obj = QuerySet(self.field.rel.to).get(**params)
  File "/Library/Python/2.6/site-packages/django/db/models/query.py", line 300, in get
    num = len(clone)
  File "/Library/Python/2.6/site-packages/django/db/models/query.py", line 81, in __len__
    self._result_cache = list(self.iterator())
  File "/Library/Python/2.6/site-packages/django/db/models/query.py", line 251, in iterator
    obj = self.model(*row[index_start:aggregate_start])
  File "/Library/Python/2.6/site-packages/django/db/models/base.py", line 324, in __init__
    signals.post_init.send(sender=self.__class__, instance=self)
  File "/Library/Python/2.6/site-packages/django/dispatch/dispatcher.py", line 166, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/Library/Python/2.6/site-packages/django/db/models/fields/files.py", line 368, in update_dimension_fields
    (self.width_field and not getattr(instance, self.width_field))
AttributeError: 'Icon' object has no attribute 'width'

The issue turns out to be that you can’t just define the ImageField in your model, you also have to explicitly define the fields that will store the width and height fields for the image field. The sql generation tools for Django don’t do it for you.

For various reasons, I can’t do that this at this moment so I made the following I hack which I strongly recommend you don’t use (for efficiency reasons, as with this the height & width have to be computed every time you access the image). This is added to site-packages/django/db/models/fields around line 367.

if self.width_field and not hasattr(instance, self.width_field):
     dimension_fields_filled = False
else:
     dimension_fields_filled = not(
          (self.width_field and not getattr(instance, self.width_field))
          or (self.height_field and not getattr(instance, self.height_field))
     )

The proper solutions probably involve:

  • not adding the hack above and explicitly adding the fields, as per here
  • updating the documentation (here and here) to say “you also have to add the fields to the DB”
  • making syncdb/sql automatically generate the width & height fields

October 24, 2009

Fizz Buzz in one line of Python

code fragments,python · David Janes · 5:58 am ·

Since Libin points to “Fizz Buzz” in one line of Ruby, I feel it’s only fair to do it in one line of Python:

print [ not i % 15 and "Fizz Buzz" or not i % 5 and "Buzz" or not i % 3 and "Fizz" or i for i in xrange(1, 101) ]

My preference is to really have a few more brackets in there, for clarity but apparently terseness is considered a virtue in and off itself sometimes. There’s other implementations of this in one line of Python:

September 26, 2009

UIPageControl + UIScrollView

code fragments,iphone,tips · David Janes · 7:58 am ·

This is an example of how to combine a UIPageControl and a UIScrollView together to create a “snap to page”-like effect that is seen on the iPhone’s home screen. This sample is partially based on Apple’s UIPageControl example.

XCode Sample Project

March 23, 2009

iPhone How To: create a Simple Table

code fragments,iphone · David Janes · 8:08 am ·

If you have not created a XIB, do the following:

  • select Resources in the Interface Building
  • press ⌘N
  • select User Interfaces in iPhone OS
  • select View XIB … and create in the normal way

To create the Simple Table in the Interface Builder

  • open the XIB in Resources
  • drag the Table View from the Library to the View … it should resize to the full extent of the View
  • select the Table View object and press ⌘2
  • from the Outlets section, drag
    • dataSource to File’s Owner
    • delegate to File’s Owner
  • save

Make your Controller.h a data source and delegate for the table:

@interface ThemesController : UIViewController
<UITableViewDelegate, UITableViewDataSource>
{
}

Add the following basic code to the Controller.m file:

#pragma mark -
#pragma mark Table View Data Source Methods

- (NSInteger) tableView:(UITableView*) tableView numberOfRowsInSection:(NSInteger) section
{
  return  ... the number of rows ...;
}

- (UITableViewCell*) tableView:(UITableView*) tableView
  cellForRowAtIndexPath:(NSIndexPath*) indexPath
{
  static NSString *SimpleTableIdentifier = @"SimpleTableIdentifier";

  UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:SimpleTableIdentifier];
  if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero
      reuseIdentifier: SimpleTableIdentifier] autorelease];
  }

  NSUInteger row = [indexPath row];
  cell.text = ... an NSString containing what to display for row row ...;
  // cell.image = [UIImage imageNamed:@"star.png"];

  return  cell;
}

#pragma mark -
#pragma mark Table Delegate Methods

- (void) tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath
{
}

Compile and run.

« Newer Posts · Older Posts »

Powered by WordPress

Switch to our mobile site