Archives for category: UIWebView


Last week WordPress founding developer Matt Mullenweg was interviewed by John Battelle at SXSW where he was startlingly candid about the shortcomings of WordPress’s iPhone app

“Twitter inspired us to start taking mobile seriously,” he added. “You open it [Twitter’s app] at any time and instantly start reading your friends’ tweets. If you open our app you get a blank screen.”
Mullenweg was startlingly candid about the shortcomings of WordPress’s iPhone app, which he described as “not good yet.” His engineers are working to improve it, he added.

Not Good Yet

I’m a big WordPress fan, having used it for years on my personal blog and more recently here on iDevRecipes. I tried the WordPress iPhone app a while back and I’d wholeheartedly agree with Matt that it’s “not good yet.”

Now just as Matt said, the WordPress Mobile team is working on it. Just yesterday, they released version 2.7 of the app with over 100 bug fixes and UI improvements like pull to refresh.

Making it Great

But Matt’s comments got me thinking on how to make the WordPress app really great. Most iPhone apps initially focus on content consumption with a splash of account management. The thinking is that users are much more likely to consume on these devices.

The WordPress app is actually open sourced and you can see that it uses the WordPress XML-RPC API which predated the iPhone to let you manage your blog.

I think this XML-RPC API nudged the WordPress iPhone app towards a full blown account management app. But where is the content consumption? (reading the comments on your posts doesn’t count!)

Reimagined Around Content Consumption

So my first thought was to reimagine the app around content consumption. WordPress.com has a bunch of great curated content. They highlight posts in their Freshly Pressed section and they categorize blogs based on tags.

A little sketching, thinking and poking around WordPress.com and here is what I came up with:

  • First convert the app to a tab based app
  • Fill the first tab with the Freshly Pressed content
  • Fill the second tab with Tags, letting you explore the tags and then see blogs associated with each tag
  • In the third tab put Posts I like. You’d be able to add to this list either on WordPress.com or by liking posts from within the app
  • In the fourth tab put Subscriptions, letting you see the newest posts from the blogs that you subscribe to (Subscriptions are like RSS feeds for non-techies)
  • Take the existing app and put it under a My Blogs tab at the end of the tab bar

Prototype Implementation

Of course this is an iOS development blog, and we can’t have a post without code! So I implemented the first two items in the list. Using our previous Twitter custom tab bar recipe I converted the main view of the app to be tab bar based app. Next I added the Freshly Pressed content to the first tab.

Freshly Pressed

Normally when displaying existing content in an iPhone app, you can’t go wrong with RSS feeds. Freshly Pressed has an RSS feed, but it just displays the first page of content. Also, the image for each featured post is offset using a background-position CSS property that the RSS feed doesn’t expose.

Scraping

So I did what seemed reasonable at the time: I wrote a ruby script that scraped the Freshly Pressed pages and put some JSON up on S3. Now keep in mind that we’re just experimenting here and we can easily swap this scraping out for a real Freshly Pressed API.

Some interesting things about the ruby script: The images uses either a WordPress image server to resize the original images or an imgpress WordPress service to convert the blog home page to an image, and in both cases I change the width from the 223 pixels the web site uses to 320 pixels so the images look nice on the iPhone. After increasing the size from 223 to 320, I also have to increase the pixel offset for each image. For example, if the offset was -30 pixels for the original image, then the offset for the new image is -30 * (320/223).

Without a real design, my first cut iteration of the table view cells is to mirror the look of the web site, including the colors, fonts and general layout

Tapping on each cell loads the blog post in a UIWebView

What do you think?

If you were to imagine a really great WordPress iPhone app, what would you come up with?
Are there things about this design/implementation you like/don’t like?

Let me know in the comments!

Full Source code: https://github.com/boctor/idev-recipes/tree/master/WordPressReimagined

Tweet This!Hacker NewsShare on Facebook

Advertisement

Full Source code: https://github.com/boctor/idev-recipes/tree/master/VerticalSwipeArticles

Problem:

The Reeder iPhone App lets you pull up to see the title of the next article. If you pull up far enough the arrow rotates and the next article animates into view. We want to recreate this UI.

Solution:

If you don’t pull up far enough, the article bounces back into view and that is a very strong clue that we are dealing with a UIScrollView.

A UIScrollView is used to display content that is larger than the application’s window. You tell it the contentSize of your content and it manages scrolling within the content. When you get to the edge of the content, the scroll view bounces to let you know you’ve reached the edge.

The header and footer views

Normally when you pull up at the edge of a scroll view empty space appears, but in the Reeder app, the title of the next article appears along with an arrow. To recreate this we’ll create a scroll view with a contentSize that is the same as the scroll view. Then we’ll tell the scroll view to alwaysBounceVertical. This causes a view that bounces vertically when you pull up or down.

Next we’ll add a header view as the subview of the scroll view and set it’s frame to be right above the scroll view and we’ll add a footer view as the subview of the scroll view and set it’s frame to be right below the scroll view. The header and footer are offscreen but when you pull up or down, they get pulled into view.

Subclassing UIScrollView

In addition to trying to figure out how to recreate the feature, we need to also figure out how to structure our code. The most reusable part of this feature is the ability to swipe up and down to see another article while seeing a preview of the previous/next article. We’ve already determined that we’ll be using a scroll view that we’ve customized so it seems logical that we would create a subclass of UIScrollView.

Animating the header and footer views

The arrows in the header and footer rotate to let the user know that when they lift their finger, the previous/next article will be shown. When the view has been scrolled past some distance we need to trigger this arrow rotation.

To accomplish this we will listen to the UIScrollViewDelegate’s scrollViewDidScroll message and check the scroll view’s contentOffset. This means that our UIScrollView subclass will have itself as its delegate. This sounds odd but works just fine.

Our subclass will send out 4 messages:

  • headerLoadedInScrollView
  • headerUnloadedInScrollView
  • footerLoadedInScrollView
  • footerUnloadedInScrollView

A header/footer is loaded when the user pulls down or up past the height of the header/footer. It is unloaded when they pull back and hide part of the header/footer. So with one arrow image, this is how we animate the arrow rotation:

- (void) rotateImageView:(UIImageView*)imageView angle:(CGFloat)angle
{
  [UIView beginAnimations:nil context:nil];
  [UIView setAnimationDuration:0.2];
  imageView.transform = CGAffineTransformMakeRotation(DegreesToRadians(angle));
  [UIView commitAnimations];
}
-(void) headerLoadedInScrollView:(VerticalSwipeScrollView*)scrollView
{
  [self rotateImageView:headerImageView angle:0];
}

-(void) headerUnloadedInScrollView:(VerticalSwipeScrollView*)scrollView
{
  [self rotateImageView:headerImageView angle:180];
}

-(void) footerLoadedInScrollView:(VerticalSwipeScrollView*)scrollView
{
  [self rotateImageView:footerImageView angle:180];
}

-(void) footerUnloadedInScrollView:(VerticalSwipeScrollView*)scrollView
{
  [self rotateImageView:footerImageView angle:0];
}

Animating the previous and next page

The UIScrollViewDelegate’s scrollViewDidEndDragging message lets us know when the user has lifted their finger after dragging. To animate the next page, we place the page below the footer and inside of an animation block place it on screen. This results in a nice up animation.

if (_footerLoaded) // If the footer is loaded, then the user wants to go to the next page
{
  // Ask the delegate for the next page
  UIView* nextPage = [externalDelegate viewForScrollView:self atPage:currentPageIndex+1];
  // We want to animate this new page coming up, so we first
  // Set its frame to the bottom of the scroll view
  nextPage.frame = CGRectMake(0, nextPage.frame.size.height + self.contentOffset.y, self.frame.size.width, self.frame.size.height);
  [self addSubview:nextPage];

  // Start the page up animation
  [UIView beginAnimations:nil context:nextPage];
  [UIView setAnimationDuration:0.2];
  [UIView setAnimationDelegate:self];
  [UIView setAnimationDidStopSelector:@selector(pageAnimationDidStop:finished:context:)];
  // When the animation is done, we want the next page to be front and center
  nextPage.frame = self.frame;
  // We also want the existing page to animate to the top of the scroll view
  currentPageView.frame = CGRectMake(0, -(self.frame.size.height + headerView.frame.size.height), self.frame.size.width, self.frame.size.height);
  // And we also animate the footer view to animate off the top of the screen
  footerView.frame = CGRectMake(0, -footerView.frame.size.height, footerView.frame.size.width, footerView.frame.size.height);
  [UIView commitAnimations];

  // Increment our current page
  currentPageIndex++;
}

We also register a callback for when this animation is done and make sure our header and footer are in place for the next time the user pulls the scroll view up or down.

UIWebViews are unique

Our UIScrollView subclass calls the delegate’s viewForScrollView:atPage to get the actual pages. Life would be simple if we could return a static page like say an image, but in the real world it is more likely that you will be returning a UIWebView to accommodate things like titles that may wrap.

The sample app uses a JSON feed of the top paid apps in the App Store and uses a UIWebView to display each page.

No matter how simple the html that you are displaying in a UIWebView, the rendering will not be instantaneous and there will always be an overhead of setting up the UIWebView. If every time viewForScrollView:atPage is called you created a new  UIWebView with html, then as this page is getting animated into view, the rendering will not have completed. The net result will be that the scroll animation will show a blank white page instead of the actual content.

To deal with this the sample app keeps around a previousPage and nextPage UIWebViews. When asked for page 1, the sample preloads previousPage with page 0 and nextPage with page 2. If there are other caching techniques you think would work here, please share your thoughts in the comments.

Full Source code: https://github.com/boctor/idev-recipes/tree/master/VerticalSwipeArticles

Tweet This!Hacker NewsShare on Facebook

Full Source code: http://github.com/boctor/idev-recipes/tree/master/TransparentUIWebViews

Problem:

UIWebViews have a built in gradient at the top and bottom.

If you set the UIWebView’s background color to clearColor, this gradient is still visible.

How can we turn off this gradient to make the UIWebView completely transparent?

Solution:

The UIWebView API doesn’t expose this gradient, but we can be sneaky and look at it’s view hierarchy.

If we put a breakpoint in the debugger where we have access to a UIWebView then in the console’s gdb prompt, we can take a look at the view hierarchy:

(gdb) po [webView recursiveDescription]
<UIWebView: 0x68220e0; frame = (0 0; 320 460); >
| <UIScrollView: 0x4b2bee0; frame = (0 0; 320 460); >
|    | <UIImageView: 0x4b2dca0; frame = (0 0; 54 54); >
|    | <UIImageView: 0x4b2da20; frame = (0 0; 54 54); >
|    | <UIImageView: 0x4b2d9c0; frame = (0 0; 54 54); >
|    | <UIImageView: 0x4b12030; frame = (0 0; 54 54) >
|    | <UIImageView: 0x4b11fd0; frame = (-14.5 14.5; 30 1); >
|    | <UIImageView: 0x4b11f70; frame = (-14.5 14.5; 30 1); >
|    | <UIImageView: 0x4b11f10; frame = (0 0; 1 30); >
|    | <UIImageView: 0x4b11eb0; frame = (0 0; 1 30); >
|    | <UIImageView: 0x4b11e50; frame = (0 430; 320 30); >
|    | <UIImageView: 0x4b2d0c0; frame = (0 0; 320 30);  >
|    | <UIWebBrowserView: 0x6005800; frame = (0 0; 320 460); >

So a UIScrollView contains a UIBrowserView filling up the UIWebView which we can assume is the actual web view and then a bunch of UIImageViews at the top and bottom are used to show the gradients. So how do we hide them?

This hierarchy may well change in future iOS versions, so the less assumptions  we make the better. We shouldn’t assume that there is a UIScrollView with embedded UIImageViews, but we do have to make at least one assumption: That the only UIImageViews are the ones used for the gradients.

So how do we find all the UIImageViews and hide them? A view can contain any number of subviews, so we need to walk the view hierarchy and hide any UIImageViews we find. To be safe, we should do this before we load the UIWebView with any content.

- (void)viewDidLoad
{
  [super viewDidLoad];
  [webView setBackgroundColor:[UIColor clearColor]];
  [self hideGradientBackground:webView];
}

- (void) hideGradientBackground:(UIView*)theView
{
  for (UIView* subview in theView.subviews)
  {
    if ([subview isKindOfClass:[UIImageView class]])
      subview.hidden = YES;

    [self hideGradientBackground:subview];
}

Full Source code: http://github.com/boctor/idev-recipes/tree/master/TransparentUIWebViews

If you think think this is something that Apple should provide an API for, then please fill out an enhancement request at http://bugreport.apple.com