2010年5月17日 星期一

iOS 開發教學 - 使用 UIWebView 和 NSOperation(NSThread) 的一些筆記

有些設計,為了讓使用者有更好的體驗,常常會用到的技巧就是 Asynchronous Operation ,採用非同步處理的模式,就像 Web 這幾年來很熱門的 Ajax 使用方式。在此以 NSOperation 與 UIWebView 做個筆記,前者是類似 Thread(NSThread) ,但他還可以設定相依性,例如兩個工作,必須第一個做完才能做第二個等,但在此僅簡單使用 Thread 的功能;後者只是瀏覽網頁用的,他除了可以直接給 URL 來源,也可以從檔案。

此範例主要是呈現一個非同步取得網頁資料的方式,當資料在下載時,先呈現一個 loading 的狀態,等資料取得後再更新。

裡頭呈現 loading 狀態的程式碼,參考:11-ThreadedFlickrTableView.zip, 2010 Winter, CS 193P iPhone Application Development

程式碼:

AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
    WebBrowserViewController *t = [[[WebBrowserViewController alloc] initWithNibName:nil bundle:nil] autorelease];
    [window addSubview:t.view];

    // Override point for customization after application launch
    [window makeKeyAndVisible];
    return YES;
}

WebGetOperation.h

#import <Foundation/Foundation.h>

@interface WebGetOperation : NSOperation {
    NSString *url;
    NSString *indexData;
    NSString *indexURL;
    id target;
    SEL action;
}
-(id)initWithWebURL:(NSString *)theURL indexData:(NSString *)theIndexData indexURL:(NSString *)theIndexURL target:(id)theTarget action:(SEL)theAction;
@end

WebGetOperation.m

#import "WebGetOperation.h"

@implementation WebGetOperation

-(id)initWithWebURL:(NSString *)theURL indexData:(NSString *)theIndexData indexURL:(NSString *)theIndexURL target:(id)theTarget action:(SEL)theAction
{
    if( ( self = [super init] ) )
    {
        url = [theURL retain];
        indexData = [theIndexData retain];
        indexURL= [theIndexURL retain];
        target = [theTarget retain];
        action = theAction;
    }
    return self;
}

- (void)delloc
{
    [url release];
    [target release];
    [super dealloc];
}

- (void)main
{
    NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]];
    NSDictionary *result = [NSDictionary dictionaryWithObjectsAndKeys:data, indexData, url, indexURL, nil];
    [target performSelectorOnMainThread:action withObject:result waitUntilDone:NO];
    [data release];
}
@end

WebBrowserViewController.h

#import <UIKit/UIKit.h>

@interface WebBrowserViewController : UIViewController {
    UIActivityIndicatorView *spinner;
    UILabel *loadingLabel;
  
    NSOperationQueue *opQueue;
  
    UIWebView *webView;

}

@end

WebBrowserViewController.m

#import "WebBrowserViewController.h"
#import "WebGetOperation.h"

@implementation WebBrowserViewController

- (void)waitForLoading
{
    if(!spinner)    // from CS193P - 2010 Winter, 11-ThreadedFlickrTableView.zip
    {
        spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
        [spinner startAnimating];
      
        loadingLabel = [[UILabel alloc] initWithFrame:CGRectZero];
        loadingLabel.font = [UIFont systemFontOfSize:20];
        loadingLabel.textColor = [UIColor grayColor];
        loadingLabel.text = @"Loading...";
        [loadingLabel sizeToFit];
      
        static CGFloat bufferWidth = 8.0;
      
        CGFloat totalWidth = spinner.frame.size.width + bufferWidth + loadingLabel.frame.size.width;
      
        CGRect spinnerFrame = spinner.frame;
        spinnerFrame.origin.x = (self.view.bounds.size.width - totalWidth) / 2.0;
        spinnerFrame.origin.y = (self.view.bounds.size.height - spinnerFrame.size.height) / 2.0;
        spinner.frame = spinnerFrame;
        [self.view addSubview:spinner];
      
        CGRect labelFrame = loadingLabel.frame;
        labelFrame.origin.x = (self.view.bounds.size.width - totalWidth) / 2.0 + spinnerFrame.size.width + bufferWidth;
        labelFrame.origin.y = (self.view.bounds.size.height - labelFrame.size.height) / 2.0;
        loadingLabel.frame = labelFrame;
        [self.view addSubview:loadingLabel];
    }
}


- (void)didFinishWithResult:(NSDictionary *)result
{
    if( spinner )
    {
        //NSLog(@"%@",[[NSString alloc] initWithData:[result objectForKey:@"data"] encoding:NSASCIIStringEncoding]);
        //[webView loadHTMLString:[[NSString alloc] initWithData:[result objectForKey:@"data"] encoding:NSASCIIStringEncoding] baseURL:[NSURL URLWithString:[result objectForKey:@"url"]]];
        //[webView loadData:[result objectForKey:@"data"] MIMEType:@"text/html" textEncodingName:nil baseURL:[NSURL URLWithString:[result objectForKey:@"url"]]];

        [webView loadData:[result objectForKey:@"data"] MIMEType:@"image/png" textEncodingName:@"binart" baseURL:[NSURL URLWithString:[result objectForKey:@"url"]]];
      
        // from CS193P - 2010 Winter, 11-ThreadedFlickrTableView.zip - Begin
        [spinner stopAnimating];
        [spinner removeFromSuperview];
        [spinner release];
        spinner = nil;
      
        [loadingLabel removeFromSuperview];
        [loadingLabel release];
        loadingLabel = nil;
        // End - from CS193P - 2010 Winter, 11-ThreadedFlickrTableView.zip
    }
}


 // The designated initializer.  Override if you create the controller programmatically and want to perform customization that is not appropriate for viewDidLoad.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) {
        // Custom initialization
        opQueue = [[NSOperationQueue alloc] init];
        [opQueue setMaxConcurrentOperationCount:1];
        webView = [[UIWebView alloc] init];
        [webView setFrame:[[self view] frame]];
        [[self view] addSubview:webView];

    }
    return self;
}

- (void)dealloc {
    [webView release];
    [opQueue release];

    [super dealloc];
}

- (void)viewDidLoad {
    [super viewDidLoad];
  
    // sync loading...
    //[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]]];

    // async loading
    //WebGetOperation *getData = [[WebGetOperation alloc] initWithWebURL:@"http://www.gogole.com/" indexData:@"data" indexURL:@"url" target:self action:@selector(didFinishWithResult:)];
    WebGetOperation *getData = [[WebGetOperation alloc] initWithWebURL:@"http://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Googlelogo.png/300px-Googlelogo.png" indexData:@"data" indexURL:@"url" target:self action:@selector(didFinishWithResult:)];
    [opQueue addOperation:getData];
    [getData release];
}


- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self waitForLoading];
}


- (void)didReceiveMemoryWarning {
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];
  
    // Release any cached data, images, etc that aren't in use.
}

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

@end

呈現:

此例為下載一個圖檔:http://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Googlelogo.png/300px-Googlelogo.png

@ - (void)didFinishWithResult:(NSDictionary *)result
[webView loadData:[result objectForKey:@"data"] MIMEType:@"image/png" textEncodingName:@"binart" baseURL:[NSURL URLWithString:[result objectForKey:@"url"]]];

getWeb1

此例為下載一個網頁:http://www.google.com,但使用 loadHTMLString 時,碰到編碼的問題,這是因為抓回來的資料是 Big5 編碼,但 iPhone 呈現是以 UTF-8 為主體,有一種修正的方式是把抓到的資料在轉成 UTF-8 ,但這樣太累了,且編碼我是從抓回來的 HTML code 看到的,並不是每個網頁都是 Big5 喔

@ - (void)didFinishWithResult:(NSDictionary *)result
[webView loadHTMLString:[[NSString alloc] initWithData:[result objectForKey:@"data"] encoding:NSASCIIStringEncoding] baseURL:[NSURL URLWithString:[result objectForKey:@"url"]]];

getWeb2

此例為下載一個網頁:http://www.google.com,改用 loadData 並指定 MIME Type 為 text/html 即可處理,這才是正解囉!

@ - (void)didFinishWithResult:(NSDictionary *)result
[webView loadData:[result objectForKey:@"data"] MIMEType:@"text/html" textEncodingName:nil baseURL:[NSURL URLWithString:[result objectForKey:@"url"]]];

getWeb3

其他的筆記:


  • 若只是純粹想用 UIWebView 把指定的 URL 取出來看看,並且不需上述的非同步方式,那其實很簡單,只需覆蓋掉以下函數:

    - (void)viewDidLoad {
        [super viewDidLoad];
      
        // sync loading...
        [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]]];
    }

    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
    }

    以上就不會出現 loading 的等待字樣

  • 在使用 UIWebView 時,如果只是純粹用 [view addSubview:webView]; 時,結果仍是一片空白,那是因為沒有指定呈現的 frame ,可以試試這個:

    [webView setFrame:[[self view] frame]];
    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]]];
    [[self view] addSubview:webView];

  • 在使用 NSOperation 時,要留意 target 是否仍存在,也就是晚點等 main 跑完後會做類似的動作:

     [target performSelectorOnMainThread:action withObject:result waitUntilDone:NO];

    這時候,要留意 target 是否還存在。當初有試過一個物件,類似當它在 init 時就會用 NSOperation 去做事,然後再測試這段程式時,很快地用以下的方式:

    TestNSOperation *t = [[TestNSOperation alloc] init];
    [t release];

    當 t 裡頭使用 NSOperation 做事花較多時間時,將導致做完後沒辦法把結果丟回 t ,如此一來程式就會不正常結束,這都需要小心使用。

沒有留言:

張貼留言