Pages

Wednesday 15 February 2012

Embed Youtube Native player in a UIWebview

No, the iPhone still doesn't do Flash, but if you haven't already noticed, the Safari browser on the iPhone is clever enough to turn any YouTube embed into a clickable thumbnail that launches the native YouTube player app on the phone. You can take advantage of this feature in your app by using a UIWebView. Here's how:

1. Set up a UIWebView in your app. You can make it part of a xib or create it programmatically. Size the UIWebView according to how large you want the clickable thumbnail to be.

2. Grab the video url using the same method as the one described above.

3. Call the loadHTMLString:baseURL: method on the UIWebView instance with some carefully constructed HTML that contains the YouTube embedded player code snippet and some supporting HTML to make sure that the video thumbnail appears correctly. Set the base URL to the URL of your website (it doesn't do anything here -- ordinarily UIWebView uses it to handle relative URL links correctly).

The best way to illustrate this is with a code snippet. Note the use of the viewport HTML meta parameter and the consistent use of width and height parameters throughout.

// webView is a UIWebView, either initialized programmatically or loaded as part of a xib.

NSString *htmlString = @"<html><head>
<meta name = \"viewport\" content = \"initial-scale = 1.0, user-scalable = no, width = 212\"/></head>
<body style=\"background:#F00;margin-top:0px;margin-left:0px\">
<div><object width=\"212\" height=\"172\">
<param name=\"movie\" value=\"http://www.youtube.com/v/oHg5SJYRHA0&f=gdata_videos&c=ytapi-my-clientID&d=nGF83uyVrg8eD4rfEkk22mDOl3qUImVMV6ramM\"></param>
<param name=\"wmode\" value=\"transparent\"></param>
<embed src=\"http://www.youtube.com/v/oHg5SJYRHA0&f=gdata_videos&c=ytapi-my-clientID&d=nGF83uyVrg8eD4rfEkk22mDOl3qUImVMV6ramM\"
type=\"application/x-shockwave-flash\" wmode=\"transparent\" width=\"212\" height=\"172\"></embed>
</object></div></body></html>";

[webView loadHTMLString:htmlString baseURL:[NSURL URLWithString:@"http://www.your-url.com"]];

One of the biggest benefits of this approach is that your app does not have to quit in order for the video to start playing. In fact, the iPhone will keep your app running in the background while it fires up the YouTube player to play the video. After the video finishes playing (or when the user hits "Done"), the user is automatically taken back to your app. This experience is very similar to watching embedded YouTube videos in the iPhone Safari browser and is just as seamless.


Happy coding and don't forget to list your app in the YouTube Project Gallery once it goes live in the App Store!

Note: Above method work only on an actual device, not in the iPhone simulator, since it does not have a YouTube player.
Courtesy : Youtube API Blog

Tuesday 8 November 2011

Creating Circular and Infinite UIScrollview.

Editor’s Note: This post was written by Jacob Haskins, Director of Mobile Development at Accella. Thanks to Jon Stroz of Accella for reaching out to me to share this idea.
When creating paging functionality for iPhone apps, there may be times that an infinite page loop would be desired. For example, if you have a small gallery of photos you are displaying, you may want to swipe through the set and have it start back at the beginning once you reach the end. The user would be able to continue swiping as much as they wanted in one direction to continue to view the content. Here are two strategies for achieving this result:
Duplicate End Caps
The first option is best suited for smaller loops. Suppose you have ten photos to display. When the user is on photo one and swipes left, it will take the user to photo ten. When the user is on photo ten and swipes right, it will take the user to photo one. The logic we will follow here is to add photos in order, but place an duplicate of photo ten to the left of photo one and a duplicate of photo one to the right of photo ten.
Now when the user scrolls to the end, we reposition the content offset of our UIScrollView. By having a duplicate photo at the end and repositioning the offset without using animation, we create a seamless experience for the user.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender 
{
    // The key is repositioning without animation
    if (scrollView.contentOffset.x == 0) {
        // user is scrolling to the left from image 1 to image 10.
        // reposition offset to show image 10 that is on the right in the scroll view
        [scrollView scrollRectToVisible:CGRectMake(3520,0,320,480) animated:NO];
    }
    else if (scrollView.contentOffset.x == 3840) {
        // user is scrolling to the right from image 10 to image 1.
        // reposition offset to show image 1 that is on the left in the scroll view
        [scrollView scrollRectToVisible:CGRectMake(320,0,320,480) animated:NO];
    }
}
3 Pages Only
There may be times when you want an infinite page loop, but don’t want to load in a lot of content. For example, You may have a lot of content to display inside the UIScrollView. Loading large amounts of data there would not be the ideal approach to the situation.
The logic that we can use there is to keep the UIScrollView at just three pages. Data would load on each page and the user would always be looking at the data in the middle page. When the user scrolled to a new page, the content for each page would be reset and the offset would go back the user is back to viewing the middle page. That way even though you may have a large amount of data to scroll through, it’s not all loaded at once. Only three pages are ever loaded at one time.
 
- (void)viewDidLoad 
{
  [super viewDidLoad];
 
  documentTitles = [[NSMutableArray alloc] init];
 
  // create our array of documents
  for (int i = 0; i < 10; i++) {
    [documentTitles addObject:[NSString stringWithFormat:@"Document %i",i+1]];
  }
 
  // create placeholders for each of our documents
  pageOneDoc = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
  pageTwoDoc = [[UILabel alloc] initWithFrame:CGRectMake(320, 0, 320, 44)];
  pageThreeDoc = [[UILabel alloc] initWithFrame:CGRectMake(640, 0, 320, 44)];
 
  pageOneDoc.textAlignment = UITextAlignmentCenter;
  pageTwoDoc.textAlignment = UITextAlignmentCenter;
  pageThreeDoc.textAlignment = UITextAlignmentCenter;
 
  // load all three pages into our scroll view
  [self loadPageWithId:9 onPage:0];
  [self loadPageWithId:0 onPage:1];
  [self loadPageWithId:1 onPage:2];
 
  [scrollView addSubview:pageOneDoc];
  [scrollView addSubview:pageTwoDoc];
  [scrollView addSubview:pageThreeDoc];
 
  // adjust content size for three pages of data and reposition to center page
  scrollView.contentSize = CGSizeMake(960, 416);
  [scrollView scrollRectToVisible:CGRectMake(320,0,320,416) animated:NO];
}
 
- (void)loadPageWithId:(int)index onPage:(int)page 
{
  // load data for page
  switch (page) {
    case 0:
      pageOneDoc.text = [documentTitles objectAtIndex:index];
      break;
    case 1:
      pageTwoDoc.text = [documentTitles objectAtIndex:index];
      break;
    case 2:
      pageThreeDoc.text = [documentTitles objectAtIndex:index];
      break;
  }
}
 
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender 
{
  // All data for the documents are stored in an array (documentTitles).
  // We keep track of the index that we are scrolling to so that we
  // know what data to load for each page.
  if(scrollView.contentOffset.x > scrollView.frame.size.width) 
  {
    // We are moving forward. Load the current doc data on the first page.
    [self loadPageWithId:currIndex onPage:0];
 
    // Add one to the currentIndex or reset to 0 if we have reached the end.
    currIndex = (currIndex $gt;= [documentTitles count]-1) ? 0 : currIndex + 1;
    [self loadPageWithId:currIndex onPage:1];
 
    // Load content on the last page. This is either from the next item in the array
    // or the first if we have reached the end.
    nextIndex = (currIndex $gt;= [documentTitles count]-1) ? 0 : currIndex + 1;
 
    [self loadPageWithId:nextIndex onPage:2];
  }
  if(scrollView.contentOffset.x $lt; scrollView.frame.size.width) {
    // We are moving backward. Load the current doc data on the last page.
    [self loadPageWithId:currIndex onPage:2];
 
    // Subtract one from the currentIndex or go to the end if we have reached the beginning.
    currIndex = (currIndex == 0) ? [documentTitles count]-1 : currIndex - 1;
    [self loadPageWithId:currIndex onPage:1];
 
    // Load content on the first page. This is either from the prev item in the array
    // or the last if we have reached the beginning.
    prevIndex = (currIndex == 0) ? [documentTitles count]-1 : currIndex - 1;
 
    [self loadPageWithId:prevIndex onPage:0];
  }     
 
  // Reset offset back to middle page
  [scrollView scrollRectToVisible:CGRectMake(320,0,320,416) animated:NO];
}

Download the Source Code


Wednesday 26 October 2011

Apple's App Store-safe iBook page flip animation



  Even if its usefulness is questionable, the page curl has become one of the signature effects of Apple’s iOS devices so it is no surprise that many developers would like to implement this effect in their apps.

iBooks screenshot during page curl
iBooks on the iPad doing a page curl

Apple uses private APIs

The problem is that the page curl animation used by Apple is not exposed in a public and documented API. Steven Troughton-Smith did a great job at documenting how Apple’s implementation works in his post Apple’s iBooks Dynamic Page Curl. Although Steven’s sample code is a bit rough (as he admits himself), the inner workings become clear: Apple has written a custom Core Image filter that is accessible with the undocumented kCAFilterPageCurl constant. (Yeah I know, Apple actually tells us in the documentation that Core Image is not available in iPhone OS. They lied.) This filter accepts two input values, inputAngle and inputTime, to control the angle from which the layer is curled up and the magnitude of the curl. For a page curl animation, we would animate inputTime from 0.0f to1.0f. To attach the filter to a layer, simply add it to an array and assign the array to the layer’s filtersproperty (ignoring that the documentation says this leads to undefined behavior. In this case, undefined behavior is exactly what we want.). From Steven’s code (edited for clarity):
@class CAFilter;
extern NSString *kCAFilterPageCurl; // From QuartzCore.framework

static CAFilter *filter = nil;

...

// In -touchesMoved:
filter = [[CAFilter filterWithType:kCAFilterPageCurl] retain];
[filter setDefaults];
[filter setValue:[NSNumber numberWithFloat:((NSUInteger)fingerDelta)/100.0] forKey:@"inputTime"];

CGFloat _angleRad = angleBetweenCGPoints(currentPos, lastPos);
[filter setValue:[NSNumber numberWithFloat:_angleRad] forKey:@"inputAngle"];
pageView.layer.filters = [NSArray arrayWithObject:filter];

The App Store-safe way

I hope Apple makes this public in the future (and if you want to have it, too, you should file a bug and request it). In the meantime, Tom Brow has written Leaves, a simple component that achieves a page curl effect through a very smart combination of mirrored and shaded layers (for translucent pages) and gradient layers (for shadows). Basically, Tom adds to the layer that contains the page content (topPage):
  1. an overlay to shade the page during the curl animation (topPageOverlay),
  2. a gradient layer that acts as the top page’s shadow during the curl (topPageShadow),
  3. a mirrored image of the page that will be displayed on the back of the topPage layer during the curl (topPageReverseImage),
  4. a nearly-white overlay to soften the topPageReverseImage,
  5. and the page below the current page that will become the new topPage after the curl has finished.
The end result is not quite as stunning as Apple’s solution but it is a very good workaround. As I played around with Tom’s code (I encourage you to take a look at it, it is very clean), I noticed thatLeavesView did not support displaying two pages side by side in landscape mode, so I modified Tom’s code accordingly. At first, I planned to duplicate the entire layer hierarchy for the second page, but then I noticed that even in the side-by-side view it is enough if only the page on the right is animated. It was enough to add a leftPage layer, modify the page skipping algorithm (skip two pages instead of one) and the display of the topPageReverseImage layer (display an image of the next page instead a mirrored image of the current page). This is what you get:
Leaves project page curl screenshot
Page curl in the Leaves project in side-by-side view
The code is not yet perfect: the topPageShadow is not aligned correctly and I struggled a bit with Tom’s implementation of the page image cache so the code in that section is quite rough.