Starter Step

Posts Tagged ‘iphone

Another UI component we found missing in Titanium Mobile was a TabStrip, like the one you would find in Facebook’s Three20 project for the iPhone where you can have as many tabs as you like and have them fit nicely on the top of the screen because they are scrollable.

After spending some time we came up with a solution that we believe works really well. It takes in any number of labels and will dynamically size the horizontal scroll width and provide arrow indicators that more tabs exist in either direction. Just pass in an onselect function and you can get the tab index for a selected tab – with this you can change the data on the screen. The code below uses the days of the week as an example and will swap out the table view’s data based on the tab selected.

function createTabStrip(options) {
 options = options || {};
 params = {
   labels: options.labels || [],
   onselect: options.onselect || null,
   top: options.top || .1,
   height: options.height || 40,
   backgroundColor: options.backgroundColor || '#000',
   gradientColor: options.gradientColor || '#444',
   selectedColor: options.selectedColor || '#fff',
   unselectedColor: options.unselectedColor || '#999',
   fontSize: options.fontSize || 14
 }

 var labelViews = [];
 var lastSelectedLabel = null;
 var totalWidth = 0;

 var containerView = Titanium.UI.createView({
   top:params.top,
   height:params.height,
   width:320,
   backgroundColor:params.backgroundColor,
   backgroundGradient: {
     type:'linear',
     colors:[
       {color:params.gradientColor,position:0.0},
       {color:params.backgroundColor,position:0.50},
       {color:params.gradientColor,position:1.0}
     ]
   }
 });

 var leftArrow = Ti.UI.createLabel({
   text:String.fromCharCode(171),
   font:{fontSize:params.height / 2,fontWeight:'bold'},
   color:params.selectedColor,
   height:params.height,
   width:15,
   top:params.top,
   left:2,
   textAlign:'left',
   visible:false
 });
 containerView.add(leftArrow);

 var scrollView = Titanium.UI.createScrollView({
   layout:'horizontal',
   top:params.top,
   left:17,
   height:params.height,
   width:286
 });
 containerView.add(scrollView);

 var rightArrow = Ti.UI.createLabel({
   text:String.fromCharCode(187),
   font:{fontSize:params.height / 2,fontWeight:'bold'},
   color:params.selectedColor,
   height:params.height,
   width:15,
   top:params.top,
   right:2,
   textAlign:'right',
   visible:false
 });
 containerView.add(rightArrow);

 scrollView.addEventListener('scroll', function(e) {
   leftArrow.visible = e.x > 5;
   rightArrow.visible = e.x < scrollView.contentWidth - scrollView.width;
 });

 containerView.labels = function(labels) {
   params.labels = labels;
   resetLabels();
 }

 containerView.selectTab = function(index) {
   select(labelViews[index]);
 }

 function resetLabels() {
   totalWidth = 0;
   labelViews = [];
   var oldLabels = scrollView.children;
   if (oldLabels) {
     for (var i = 0,count = oldLabels.length; i < count; i++) {
       scrollView.remove(oldLabels[i]);
     }
   }

   var labels = params.labels;
   for (var i = 0,count = labels.length; i < count; i++) {
      var button = createButton(labels[i], i);
      scrollView.add(button);
   }
   scrollView.contentWidth = totalWidth;
   rightArrow.visible = totalWidth > scrollView.width;
 }

 function createButton(title, index) {
   var buttonView = Ti.UI.createView({
     top:params.top,
     height:params.height
   });

   var label = Ti.UI.createLabel({
     text:title,
     font:{fontSize:params.fontSize,fontStyle:'bold'},
     width:'auto',
     textAlign:'center',
     height:params.height,
     touchEnabled:false
   });
   label.index = index;
   labelViews.push(label);
   buttonView.add(label);
   showAsUnSlected(label);
   buttonView.addEventListener('click', function(e) {
     select(e.source.children[0]);
   });
   buttonView.width = label.size.width + 20;
   totalWidth += buttonView.width;

   if (index == 0) {
     showAsSelected(label);
   }
   return buttonView;
 }

 function select(label) {
   if (lastSelectedLabel) {
     showAsUnSlected(lastSelectedLabel);
   }
   showAsSelected(label)
   if (params.onselect) {
     params.onselect(label.index);
   }
 }

 function showAsSelected(label) {
   label.color = params.selectedColor;
   label.getParent().borderWidth = 1;
   lastSelectedLabel = label;
 }

 function showAsUnSlected(label) {
   label.color = params.unselectedColor;
   label.getParent().borderWidth = 0;
 }

 resetLabels();
 return containerView;
}


var tableData = [
 [{title:'Monday Data'}],
 [{title:'Tuesday Data'}],
 [{title:'Wednesday Data'}],
 [{title:'Thursday Data'}],
 [{title:'Friday Data'}],
 [{title:'Saturday Data'}],
 [{title:'Sunday Data'}]
];

var window = Titanium.UI.createWindow({
 fullscreen:false,
 backgroundColor:'#fff',
 title:'TabStrip'
});

var tabStripView = createTabStrip({
 labels:['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'],
 onselect: function(index) {
    //This is so you don't get flickering
    tableView.hide();
    tableView.setData(tableData[index]);
    setTimeout(function() {
        tableView.show();
    }, 300);
 }
});

window.add(tabStripView);

var tableView = Ti.UI.createTableView({
 top:tabStripView.height
});
window.add(tableView);

var navGroup = Ti.UI.iPhone.createNavigationGroup({
 window:window
});

var main = Ti.UI.createWindow();
main.add(navGroup);
main.open();

tabStripView.selectTab(0);

We have started looking at Appcelerator’s Titanium Mobile SDK for developing iPhone and Android apps using Javascript…unfortunately they still have a ways to go with the promise of being cross-platform in supporting iPhone and Android with a single code base.  However if you are just targeting iPhone, then you must take a look at the latest Titanium 1.6 SDK.

We did however find a vital UI component missing from Titanium – the ability to view remote images (thumbnails) in a grid-like fashion with built in paging.  We knew that this would be a showstopper for us so I took a stab at building my own by wiring together pieces of the current Titanium UI components.  To try it out just follow the getting started instructions on Appcelerator’s site and then copy and paste the code below into the app.js file that is generated for you.  Please note that this only works on a Mac since it’s targeting the iPhone.  And it’s not the prettiest code and could use some refactoring before going prime-time.

The thumbnail viewer requires a plixi user id.  It uses this id to call the plixi api and retrieve a user’s uploaded photos for viewing as thumbnails.  The example below uses Arsenio Hall’s plixi id 🙂

function createThumbnailViewer(props) {

 var that = {};
 var xhr = Titanium.Network.createHTTPClient();

 var photosWindow = Titanium.UI.createWindow({
   fullscreen:false,
   backgroundColor:'#fff',
   title:props.title
 });
 that.window = photosWindow;

 var refreshButton = Ti.UI.createButton({
   title:'Refresh',
   width:40,
   height:30
 });
 var photosContainer = Titanium.UI.createView();

 var imageUrlLists = [];
 var index = -1;
 var total = 0;

 var heightSpacer = 3.5;
 var widthSpacer = 8;
 var topOffset = heightSpacer;
 var imageDim = 70;
 var pageSize = 20;
 for (var i = 0,count = pageSize; i < count; i += 4) {
   var leftOffset = widthSpacer;
   for (var j = 0; j < 4; j++) {
     var photoView = Ti.UI.createImageView({
       top:topOffset,
       left:leftOffset,
       height:imageDim,
       width:imageDim
     });
     photosContainer.add(photoView);
     leftOffset += imageDim + widthSpacer;
   }
   topOffset += imageDim + heightSpacer;
 }

 photosWindow.add(photosContainer);

 var flexSpace = Titanium.UI.createButton({
   systemButton:Titanium.UI.iPhone.SystemButton.FLEXIBLE_SPACE
 });

 var previousButton = Ti.UI.createButton({
   title:'Prev',
   width:50,
   height:10
 });
 previousButton.addEventListener('click', function() {
   previous();
 });
 var nextButton = Ti.UI.createButton({
   title:'Next',
   width:50,
   height:10
 });
 nextButton.addEventListener('click', function() {
   next();
 });
 var nativespinner = Titanium.UI.createButton({
   systemButton:Titanium.UI.iPhone.SystemButton.SPINNER
 });

 var labelContainer = Ti.UI.createView({width:100,height:20});

 var label = Ti.UI.createLabel({
 color:'#fff'
 });

 labelContainer.add(label);

 photosWindow.toolbar = [previousButton, flexSpace, labelContainer, flexSpace, nextButton];

 refreshButton.addEventListener('click', function() {
   that.start();
 });

 that.start = function() {
   index = -1;
   imageUrlLists = [];
   total = 0;
   next();
 }

 photosWindow.rightNavButton = refreshButton;

 function next() {
   index++;
   if (index >= imageUrlLists.length) {
     load();
   } else {
     applyImageUrls();
   }
 }

 function previous() {
   index--;
   if (index >= 0) {
     applyImageUrls();
   }
 }

 function applyImageUrls() {
   var imageUrlList = imageUrlLists[index];
   label.text = (index * pageSize + imageUrlList.length) + " of " + total;
   var photos = photosContainer.children;
   var i = 0,count = imageUrlList.length;
   while (i < count) {
     photos[i].image = imageUrlList[i];
     i++;
   }
   if (count < pageSize) {
     while (i < pageSize) {
       photos[i].image = null;
       i++;
     }
   }
   previousButton.enabled = index > 0;
   nextButton.enabled = imageUrlLists.length * pageSize <= total;
 }

 function load() {
   xhr.onload = function() {
     var json = JSON.parse(this.responseText);
     var list = json.List;
     total = json.CollectionCount;
     var imageUrlList = [];
     var i = 0, count = list.length;
     while (i < count) {
       imageUrlList[i] = list[i].ThumbnailUrl;
       i++;
     }
     imageUrlLists[index] = imageUrlList;
     photosWindow.toolbar = [previousButton, flexSpace, labelContainer, flexSpace, nextButton];
     applyImageUrls();
   };
   previousButton.enabled = false;
   nextButton.enabled = false;
   photosWindow.toolbar = [previousButton, flexSpace, nativespinner, flexSpace, nextButton];
   var url = "http://api.plixi.com/api/tpapi.svc/json/users/"+props.plixiUserId+"/photos?getcount=true&ps=" + pageSize + "&ind=" + (index * pageSize);
   Ti.API.info("Calling url  = " + url);
   xhr.open("GET", url);
   xhr.send();
 }
 return that;
}

var thumbnailViewer = createThumbnailViewer({
 plixiUserId:'6236650',
 title:'Aresenio Hall'
});

var navGroup = Ti.UI.iPhone.createNavigationGroup({
 window:thumbnailViewer.window
});

var main = Ti.UI.createWindow();
main.add(navGroup);
main.open();

thumbnailViewer.start();

Last night Apple’s latest commercial premiered with our mobile travel app, TripCase, as the first application demoed in the advertisement.

It ran during Heroes, House, and Gossip Girl, and Apple plans on running it for the next several months in various time slots.

If you haven’t seen it yet you can view it on Apple’s website here:

http://www.apple.com/iphone/gallery/ads/#dine-large

I have just added simple scripting support to UISpec. It allows you to send a String as a script and have it run dynamically. This really opens up the possibility of using UISpec with other languages like Ruby, Java, etc. The following is taken from the documentation found here.

UIScript’s goal is to provide a simple lightweight scripting language that supports all the functionality of UISpec, but without having to write Objective-C code. Instead you write UIScript which is very similar to Smalltalk.

Currently UIScript only supports straight messages, which means there is no support for things like variables and loops or decisions (for, while, if else, etc…). And right now UISpec recognizes only Strings, Integers, and BOOLs.

You can run a script in your test by calling the C function:

UIQuery * $(NSString *script, …);

Messages

Like in Objective-C, UIScript is based on sending messages to objects. The only real difference in writing UIScript vs Objective-C is that you take out all the dot notation and brackets.

$(@”navigationButton touch”);
$(@”tableView tableViewCell all should not be selected”);

As you can see it’s very similar to what you are used to writing, except without all the syntactic sugar, making the script even more readable.

Strings

Strings in UISpec are just quoted values like ‘Hello’ or ‘Good Bye’, and there is no need to use the @ symbol as a prefix like you do in Objective-C.

$(@”navigationButton label text:’Save’ touch”);

$(@”textField placeholder:’Username’ setText:’bkuser'”);
//If you need to include a String dynamically just use NSString formatting
$(@”textField placeholder:’Username’ setText:’%@'”, [Config userName]);

Integers

Currently Integers are the only number support in UIScript. We hope to have support for decimals very soon.

$(@”tableView should have numberOfSections:3″);

$(@”label text:’Returns’ parent tableViewCell should have accessoryType:3″);
//Or you can use NSString formatting
$(@”label text:’Returns’ parent tableViewCell should have accessoryType:%d”, UITableViewCellAccessoryCheckmark);

BOOLs

The BOOLs YES and NO are also supported in UIScript.

$(@”tableView tableViewCell first should have isSelected:YES”);
$(@”tableView tableViewCell last should have isSelected:NO”);

//Or you can use NSString formatting
int rows = $(@”tableView numberOfRowsInSection:0″);
$(@”tableView should have:%@”, rows==3);

Example

Here is the itShouldUpdateUserRoles example from the demo rewritten using UIScript

-(void)itShouldUpdateUserRoles {
[self addTestUser];

$(@”label with text:’Brian Knorr’ touch”);
$(@”label text:’User Roles’ touch”);

$(@”tableView scrollToBottom”);
$(@”label text:’Returns’ touch”);
$(@”label text:’Returns’ parent tableViewCell should be selected”);
$(@”label text:’Returns’ parent tableViewCell should have accessoryType:%d”, UITableViewCellAccessoryCheckmark);

$(@”label text:’Returns’ touch”);
$(@”label text:’Returns’ parent tableViewCell should have accessoryType:%d”, UITableViewCellAccessoryNone);

$(@”view:’UINavigationItemButtonView’ touch”);
$(@”view:’UINavigationItemButtonView’ touch”);

[self deleteTestUser];
}

Over the weekend I did some work on UISpec…mostly getting the “redo” feature working.  Redo let’s you rerun a chained set of queries that will return a result based on what is on the screen right now.  Internally the redo feature is being used for traversals and filtering, and I thought it might come in handy in your test scripts.  Here is a simple example:

//First let's grab the last UILabel under the first
//UITableViewCell on the screen
UIQuery *label = app.tableViewCell.label.last;

//Next we will check that the label's text
//is equal to "Start"
[label.should.have text:"Start"];

Now let’s say we execute some code that reloads the screen or goes to a new one, and afterward we want to check the text of a label in the same place as before but on the new screen.

We could find the label again with label = app.tableViewCell.label.last; OR we can do the following instead:

[label.redo.should.have text:"Finish"];

Calling label.redo will actually rerun the code that created it in the first place: “app.tableViewCell.label.last” except now it’s against the new screen. Acts kinda like a template to find the label. It’s not really a template though since you have to run it at least once before a redo can be called. Now it wouldn’t be too difficult to add finder templates to UISpec. Hmmm…maybe I will work on that next weekend:)

Over the weekend I made some small changes to the way traversals work in UISpec. As a result the following method names have changed:

  • descendants is now descendant
  • parents is now parent
  • children is now child

Calling these methods return exactly what you would except them to. The method parent finds the parent view, child gets you the first child view, and descendant retrieves the first descendant view. Of course each result is wrapped in a UIQuery object. And if you want all the views and not just the first, call the method all afterward. Here are some examples

//Finds the first child of the
//first UITableViewCell
app.tableViewCell.child;

//Finds the all child views under
//the first UITableViewCell
app.tableViewCell.child.all;

//Touches all the child views under the first UITableView
app.tableView.child.all.touch

//Both of these will find the first child UILabel
//under UITableViewCell
app.tableViewCell.child.label;
[app.tableViewCell.child view:@"UILabel"];

//Both of these will find all the child UILabels
//under UITableViewCell
app.tableViewCell.child.label.all;
[app.tableViewCell.child view:@"UILabel"].all;

//And of course the default way still works the same

//Finds the first descendant UILabel 
//under UITableViewCell
app.tableViewCell.label; 

//Finds all the descendant UILabels
//under UITableViewCell
app.tableViewCell.label.all; 

Also the documentation has been updated to reflect these changes.

For UISpec I needed a an easy way to create and cache a method call on an object. I know that NSInvocation is the class to go with, but it’s very cumbersome to create and use. Luckily I found the blog post Callable Objects in Cocoa, which pointed me in the right direction, so I took what they suggested one step further and made it dynamic using message forwarding.

Say you have an address book object that models a list of addresses and you want to “record” a call that does a find by first and last name and “play” the call later.


//Create a Recordable instance with the addressBook as its target
Recordable *dvd = [Recordable withTarget:addressBook];

//Record the call you want to make
[dvd findAddressByFirstName:@"Joe" lastName:@"Smith"];

//Play it to get the results of the call
Address *address = dvd.play;

Pretty nifty huh? Here is the source code. Enjoy!

Recordable.h


@interface Recordable : NSObject {
	id target, play;
	NSInvocation *invocation;
}

@property(nonatomic, retain) id target;
@property(nonatomic, retain) NSInvocation *invocation;
@property(nonatomic, readonly) id play;

+(id)withTarget:(id)target;

@end

Recordable.m

<ol>
  <li>import "Recordable.h"</li>
</ol>

@implementation Recordable

@synthesize target, invocation;

+(id)withTarget:(id)target {
	return [[[self alloc] initWithTarget:target] autorelease];
}

-(id)initWithTarget:(id)_target {
	if (self = [super init]) {
		self.target = _target;
	}
	return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
	return [target methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation setTarget:target];
    [anInvocation retainArguments];
    [anInvocation setReturnValue:&self];
    self.invocation = anInvocation;
}

-(id)play {
	id value;
	[invocation invoke];
	NSString *returnType = [NSString stringWithFormat:@"%s", [[invocation methodSignature] methodReturnType]];
	if (![returnType isEqualToString:@"v"]) {
		id value;
		[invocation getReturnValue:&value];
		return value;
	} else {
		return nil;
	}
}

-(void)dealloc {
	self.target = nil;
	self.invocation = nil;
	[super dealloc];
}

@end

Tags: ,


  • Dave: I can tell you're a ruby guy because you forgot the 'return' keyword. Thanks for the tip though!
  • Chandrashekhar H M: Hi, Thanks its working fine in iOS 6 but not in iOS 7.0. Any Suggestion on this.
  • Coeur: To change a rootViewController, without all this TVNavigationController : myNewRoot = [[UIViewController alloc] init]; myNavigationController.view