Look! UIAlertView is dating UITableView!

As you may have guessed, this is about UIAlertView containing a UITableView. Since I started playing around with GameKit, I had the issue that I couldn’t use the PeerPicker with Client/Server stuff..
I can’t really get into the stuff we are doing and where we are using it – but I can offer you some trick and code to have a UIAlertView displayed with a fully controllable UITableView.

I started off with making it a decorator.. After 20′ I had to give up, because it was just too complicated to decorate objects where you don’t really know what’s going on. So I had to subclass it – unfortunately, but anyway..

Let’s take a look at what you would probably like to have..

Client

UIAlertTableView *alert = [[UIAlertTableView alloc] initWithTitle:@"Select Option"
    message:@"select option or create one"
    delegate:self
    cancelButtonTitle:@"Cancel"
    otherButtonTitles:@"Create", nil];
alert.tableDelegate = self;
alert.dataSource = self;
alert.tableHeight = 120;

And this should be the result:

Alright, so, we just add a UITableView to the UIAlertView as a Subview, right? Hold on, Tiger :)
First of all, if we set the message to nil, we want to have this:

And if we rotate, we want to have the nice effects! Like this:

Rotate Animation

If you only want to grab the code without BlahBlah: UIAlertTableView on Bitbucket.org (btw. I switched to bitbucket.org – but that’s another story)

So, let’s look at the code a bit:

UIAlertTableView.h


#import <UIKit/UIKit.h>

@class UIAlertView;

@interface UIAlertTableView : UIAlertView {
	// The Alert View to decorate
	UIAlertView *alertView;

	// The Table View to display
	UITableView *tableView;

	// Height of the table
	int tableHeight;

	// Space the Table requires (incl. padding)
	int tableExtHeight;

	id dataSource;
	id tableDelegate;
}

@property (nonatomic, assign) id dataSource;
@property (nonatomic, assign) id tableDelegate;

@property (nonatomic, readonly) UITableView *tableView;
@property (nonatomic, assign) int tableHeight;

- (void)prepare;

@end

You see: we subclass UIAlertView, we have a UITableView, we have a delegate which needs to implement the protocol as well as a dataSource. Straight forward, I’d say…

Now, as for the implementation, one would think: just overload the “show” method and insert the TableView as a subview etc. Well, that works – NOT. First of all: you have to resize your AlertView, then you have to move the buttons down and then you have to place the tableView somewhere (esoteric?).
OkOk, just overwrite the drawRect then? You are getting closer!
But, first, let’s have a look at the prepare method.

UIAlertTableView.m:prepare


- (void)prepare {
	if (tableHeight == 0) {
		tableHeight = 150.0f;
	}

	// Calculate the TableViewHeight with padding
	tableExtHeight = tableHeight + 2 * kTablePadding;

	tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
	tableView.delegate = tableDelegate;
	tableView.dataSource = dataSource;	

	// Insert it as the first subview
	[self insertSubview:tableView atIndex:0];
}

This code creates the TableView but does not set a real frame yet. It sets the given DataSource and Delegate. To be totally correct, there should be a custom setTableDelegate / setDataSource method which changes them in the tableView – but this is left as an exercise to the reader :)
After the creation, we insert the tableView as the very first subview of the alertView – so we know where to find it again and that nothing is hidden because of the tableView.

Now comes the tricky part: drawing.
For that, we use a private API call to the AlertView, called layoutAnimated:(BOOL)animated.
We overload it in our custom subclass because the initial drawing and the drawing on setNeedsLayout goes through that method.
After that method is called, all the elements that belong to the AlertView (title, message, buttons …) are arranged, so we can use those values for our next computations.

So, this is how it goes:

UIAlertTableView.m:layoutAnimated


- (void)layoutAnimated:(BOOL)fp8 {
	[super layoutAnimated:fp8];
	[self setFrame:CGRectMake(self.frame.origin.x, self.frame.origin.y - tableExtHeight/2, self.frame.size.width, self.frame.size.height + tableExtHeight)];

	// We get the lowest non-control view (i.e. Labels) so we can place the table view just below
	UIView *lowestView = [self.subviews objectAtIndex:0];
	int i = 0;
	while (![[self.subviews objectAtIndex:i] isKindOfClass:[UIControl class]]) {
		UIView *v = [self.subviews objectAtIndex:i];
		if (lowestView.frame.origin.y + lowestView.frame.size.height < v.frame.origin.y + v.frame.size.height) {
			lowestView = v;
		}

		i++;
	}

	// TODO: calculate this value
	CGFloat tableWidth = 262.0f;

	tableView.frame = CGRectMake(11.0f, lowestView.frame.origin.y + lowestView.frame.size.height + 2 * kTablePadding, tableWidth, tableHeight);

	for (UIView *sv in self.subviews) {
		// Move all Controls down
		if ([sv isKindOfClass:[UIControl class]]) {
			sv.frame = CGRectMake(sv.frame.origin.x, sv.frame.origin.y + tableExtHeight, sv.frame.size.width, sv.frame.size.height);
		}
	}
}

Step by step: first, we call the superclass - so everything gets arranged.
Then, we set a new frame for the AlertView, which is the same frame + tableHeight + padding. But we also need to rearrange the frame - which is half of that additional height.
After that, we compute the lowest "sitting" normal view - no control object - in the alert view. We loop through it, until we find the first control element - which are usually the buttons, because they are added at the end. You could loop through all views and get the lowest from any non-UIControl objects. But this works :)
This position is used to place the TableView at the correct position - whether you have a message, a title or whatever there.. but it needs to be above the buttons - UI standards.
To be above the buttons, we need to rearrange the buttons, and this is what happens: all UIControl object in the AlertView are moved down by the size we added to the AlertView frame - the complete TableHeight. And this is it.
We resize the AlertView, calculate the position where the TableView is supposed to be inserted at and then move the buttons. If we rotate, this gets calculated again and nicely rearranged.

Note: you can apply the very same method for any other UIView object you want to insert into an UIAlertView.. Be it TextField or ImageView or whatever. (For textField, you should use their private APIs though)

Again, code is here: Code on Bitbucket.org

Use pastie.org with LaunchBar

Since a while, I’m having senile insomnia, so I thought, I should use it for good. One thing that always bugs me at work is the work with pastie (or any other paste-thing) because of the copy – paste – copy thing.
So I wrote a very little script to use it with LaunchBar.

What does it do? It takes your clipboard, pastes it to pastie and pastes the returned URL back into the clipboard..

Here is the script:

pastie.scpt

set pastieURL to "http://pastie.org/pastes"
try
set responseURL to (do shell script "curl http://pastie.caboo.se/pastes/create -H 'Expect:' -F 'paste[parser]=plaintext' -F 'paste[body]=" & (the clipboard) & "' -F 'paste[authorization]=burger' -s -L -o /dev/null -w '%{url_effective}'")
set the clipboard to responseURL
end try

Save this to “~/Library/Application Support/LaunchBar/Actions/” (works with LaunchBar 5) and it will detect it automatically. Type the name of your script and you’re done. So much for my insomnia :)

UPDATE: Just found out about Spark which let’s you define shortcuts for Apple Scripts – which is another nice way.

A short note on CLLocation / CLLocationManager

**** DEPRECATED **** Use http://liip.to/ilocator

Well.. I’ve been testing this Locator quite a while now. Today I had my final attempt, to find out, why I always got either a cached location or an extremely inaccurate location.

Today, I went out with my notebook (yes, there where the sun is..) to debug.

The example of apple says, to check the timestamp of the newLocation. (It should be less than 5..). Yeah well, guess what – it is sometimes 0 in a cached location. And somehow it’s sometimes just a bit over 5 and you’ll have to wait forever to get a new location.


- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation
{
    NSDate* eventDate = newLocation.timestamp;
    NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];

    if (howRecent < -0.0 && howRecent > -10.0) {
        [manager stopUpdatingLocation];

        // USE THE FORCE OF THE LOCATION!
    }
}

This one should work. Maybe that location stuff is working in the U.S. but it wasn’t in Switzerland..

P.S.: I had another issue with a pwned iPhone today. NSXMLParser throw me errors back and I didn’t know why.. I still don’t know why, I guess it had to do with the libxml2 update I did. Workaround? Restore, works fine now.

Make NSXMLParser your friend..

As promised, here is a little How-I-did-it / How-To.

First off: I am not an experienced SAX-User.. So this approach might be packing the problem at it’s tail, but this is how DOM-Users feel comfortable with ;)

Let’s assume we want to parse the following XML:

tranist.xml

<root>
    <schedules>
        <schedule id="0">
            <from>SourceA</from>
            <to>DestinationA</to>
            <links>
                <link id="0">
                    <departure>2008-01-01 01:01</departure>
                    <arrival>2008-01-01 01:02</arrival>
                    <info>With food</info>
                    <parts>
                        <part id="0">
                            <departure>2008-01-01 01:01</departure>
                            <arrival>2008-01-01 01:02</arrival>
                            <vehicle>Walk</vehicle>
                        </part>
                        <part id="1">
                            <departure>2008-01-01 01:01</departure>
                            <arrival>2008-01-01 01:02</arrival>
                            <trackfrom>1</trackfrom>
                            <trackto>2</trackto>
                            <vehicle>Train</vehicle>
                        </part>
                    </parts>
                </link>
                <link id="1">
                    ...
                </link>
                <link id="2">
                    ...
                </link>
            </links>
        </schedule>
        <schedule id="1">
            ...
        </schedule>
        <schedule id="2">
            ...
        </schedule>
    </schedules>
</root>

In human readable format, this means: We have multiple schedules with from/to etc. These schedules consist of multiple links (different connections for the same route) with departure/arrival etc. These links consist then of multiple parts/sections with various elements which are not sure to be there..

With the let’s find the element called ‘part’ – approach, you won’t get anywhere..

The Basics

So what do we want to achieve? We want a list/array of Schedules, which have the given members. On member is a list/array of Links, also consisting of the given members and a list/array of parts with the respective members.

This is also the basic idea behind my approach: for every new node-container, use a new class/object (an array will also work, but it’s kinda crap..)

Now we have a Schedule class, a Link class and a Part class.

This is an example of the Link class interface:

Link.h

#import "Part.h"

@interface Link : NSObject {
    NSString *departure;
    NSString *arrival;
    NSString *info;
    NSMutableArray *parts;
}

@property (nonatomic, retain) NSString *departure;
@property (nonatomic, retain) NSString *arrival;
@property (nonatomic, retain) NSString *info;
@property (readonly, retain) NSMutableArray *parts;

- (void)addPart:(Part *)part;

@end

We use an accessor method for the parts, because it just feels better when dealing with arrays. (Instead of later using [foo.myArray addObject:..] we have [foo addMe:..])

Also we make it easier for us, using retain properties..

The Parser setup

A short introduction into SAX:

The parsing goes node by node and is not nesting-sensitive. That means that first we get root, then schedules, then schedule, then from, then to, then links, then link, then departure etc. As soon as the parser returns you the node for example, you don’t know anymore in what schedule you were. As long as you have a clearly defined structure where always every element must be present, you could do this using a counter, but as soon as you have multiple nodes with no defined count, you have a problem.

What we do is known as recursive parsing. What does this mean? We implement some kind of memory.

In our parser, we have 4 members and 1 method (to make actual use of the parser..):

@property (nonatomic, retain) NSMutableString *currentProperty;
@property (nonatomic, retain) Schedule *currentSchedule;
@property (nonatomic, retain) Link *currentLink;
@property (nonatomic, retain) Part *currentPart;
@property (nonatomic, readonly) NSMutableArray *schedules;

- (void)parseScheduleData:(NSData *)data parseError:(NSError **)error;

(Yes, this needs to be a NSMutableString..)

Your parseScheduleData method should look similar to the following:

parseJourneyData

- (void)parseJourneyData:(NSData *)data parseError:(NSError **)err {
    NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];

    self.schedules = [[NSMutableArray alloc] init]; // Create our scheduler list

    [parser setDelegate:self]; // The parser calls methods in this class
    [parser setShouldProcessNamespaces:NO]; // We don't care about namespaces
    [parser setShouldReportNamespacePrefixes:NO]; //
    [parser setShouldResolveExternalEntities:NO]; // We just want data, no other stuff

    [parser parse]; // Parse that data..

    if (err && [parser parserError]) {
        *err = [parser parserError];
    }

    [parser release];
}

Now we need those delegate methods.

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string

This function is called by the parser, when it reads something between nodes. (Text that is..) Like with blah it would read “blah”. It is possible, that this method is called multiple times in one node. As you will see later, we define the property “currentProperty” only if we find a node, we care about. That’s why we test it against this property to make sure, that we need this property. This will then look something like this:

Parser

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    if (self.currentProperty) {
        [currentProperty appendString:string];
    }
}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict

This is called, when the parser finds an opening element. In this case, we have a few cases, we need to distinguish. These are:

It’s standard property in the schedule (like <form> etc.) or it’s a deeper nested node (like <links>), the same for all the other nodes.

How to? We define, that we only set a member, if we are in that node. That means, only when we have entered a <part>, then currentPart is set, otherwise it’s nil. The same with the others.

We do then need to check them in reverse order of their nesting level.. Why? Because if we would check for currentLink before currentPart, currentLink would also evaluate to YES/True and hence we will have a problem if their are elements with the same name. If we aren’t in any node, then there is probably a new main node comming -> in the else..

When we hit a nested node, we need to allocate the respective member of our class, so we can use it when the parser gets deeper into it.

This will look like this:

Parser

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
    if (qName) {
        elementName = qName;
    }

    if (self.currentPart) { // Are we in a
        // Check for standard nodes
        if ([elementName isEqualToString:@"departure"] || [elementName isEqualToString:@"arrival"] || [elementName isEqualToString:@"vehicle"] || [elementName isEqualToString:@"trackfrom"] || [elementName isEqualToString:@"trackto"] ) {
            self.currentProperty = [NSMutableString string];
        }
    } else if (self.currentLink) { // Are we in a
        // Check for standard nodes
        if ([elementName isEqualToString:@"departure"] || [elementName isEqualToString:@"arrival"] || [elementName isEqualToString:@"info"]) {
            self.currentProperty = [NSMutableString string];
        // Check for deeper nested node
        } else if ([elementName isEqualToString:@"part"]) {
            self.currentPart = [[Part alloc] init]; // Create the element
        }
    } else if (self.currentSchedule) { // Are we in a  ?
        // Check for standard nodes
        if ([elementName isEqualToString:@"from"] || [elementName isEqualToString:@"to"]) {
            self.currentProperty = [NSMutableString string];
        // Check for deeper nested node
        } else if ([elementName isEqualToString:@"link"]) {
            self.currentLink = [[Link alloc] init]; // Create the element
        }
    } else { // We are outside of everything, so we need a
        // Check for deeper nested node
        if ([elementName isEqualToString:@"schedule"]) {
            self.currentSchedule = [[Schedule alloc] init];
        }
    }
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName

Basically, the same things apply as for didStartElement above. This time, we need to clean things up and assign them if they are set :) This is a bit a pitty, since it’s a lot of code.. *(for not so much)

It’s the same checker-structure..

If we are in a deeper nested node (like <Link>) and we hit an ending element of that nested node (like </Link>), Then we need to add this element to the parent (like <Schedule>) and set it to nil

See yourself:

Parser

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
    if (qName) {
        elementName = qName;
    }

    if (self.currentPart) { // Are we in a
        // Check for standard nodes
        if ([elementName isEqualToString:@"departure"]) {
            self.currentPart.departure = self.currentProperty;
        } else if ([elementName isEqualToString:@"arrival"]) {
            self.currentPart.arrival = self.currentProperty;
        } else if ([elementName isEqualToString:@"vehicle"]) {
            self.currentPart.vehicle = self.currentProperty;
        } else if ([elementName isEqualToString:@"trackfrom"]) {
            self.currentPart.trackfrom = self.currentProperty;
        } else if ([elementName isEqualToString:@"trackto"]) {
            self.currentPart.trackto = self.currentProperty;
        // Are we at the end?
        } else if ([elementName isEqualToString:@"part"]) {
            [currentLink addPart:self.currentPart]; // Add to parent
            self.currentPart = nil; // Set nil
        }
    } else if (self.currentLink) { // Are we in a
        // Check for standard nodes
        if ([elementName isEqualToString:@"departure"]) {
            self.currentLink.departure = self.currentProperty;
        } else if ([elementName isEqualToString:@"arrival"]) {
            self.currentLink.arrival = self.currentProperty;
        } else if ([elementName isEqualToString:@"info"]) {
            self.currentLink.info = self.currentProperty;
        // Are we at the end?
        } else if ([elementName isEqualToString:@"link"]) {
            [currentSchedule addPart:self.currentLink]; // Add to parent
            self.currentLink = nil; // Set nil
        }
    } else if (self.currentSchedule) { // Are we in a  ?
        // Check for standard nodes
        if ([elementName isEqualToString:@"from"]) {
            self.currentSchedule.from = self.currentProperty;
        } else if ([elementName isEqualToString:@"to"]) {
            self.currentSchedule.to = self.currentProperty;
        // Are we at the end?
        } else if ([elementName isEqualToString:@"schedule"]) { // Corrected thanks to Muhammad Ishaq
             [schedules addObject:self.currentSchedule]; // Add to the result node
             self.currentSchedule = nil; // Set nil
        }
    }

    // We reset the currentProperty, for the next textnodes..
    self.currentProperty = nil;
}

Finally..

Well, that’s it. You can expand / shrink this principle as you like. You can also add a maxElements counter, like in the SeismicXML example of the iPhone SDK to get only a certain number of elements. You can abort the parser with [parser abortParsing]; It is important, that you don’t abort while in a deeper nested node, because this could lead to inconsistencies. You will need to skip them..

Please note, that I wrote this, while watching TV, so you may need to fix some syntax errors ;) But I hope you get the idea..

© 2010 some rights reserved by codesofa
Design by Stefan Sicher
Powered by WordPress