2010年5月19日 星期三

[iPad/iPhone] Javascript + jQuery + ePub + WebKit + HTML5 + iBooks ?


圖片來源 - http://webkit.org/


電子書用 iBooks 看,沒有什麼稀奇,但 iBooks 的閱讀器是用修修改改的 WebKit 做,那可就好玩了!因為 ePub 裡的格式幾乎就像 XHTML 一般,如果呈現它的閱讀軟體可以支援網頁上豐富的互動,那電子書就不再枯燥乏味了!況且 WebKit 有支援 HTML5 與 Javascript 耶,那是不是電子書的內容就可變動了呢?


上週老闆說,iPad 裡頭的 iBooks 是用 WebKit 實作的,當下就聽到他弄出 HTML5 的特效出來,這週開始來測測上頭的 WebKit 到底有甚麼可以玩的,首先擺於上傳自己的電子書,方法很簡單,就是建立出 epub 格式,再把他拖進 iTunes ,那就會出現"書櫃"的項目,接著再切換到 device 上頭,跑去該項目按一下同步!資料就傳送到 iPad 上,並且可以用 iBooks 去瀏覽。細節可以參考這篇:[iPhone/iPad] 匯入自製的 ePub 電子書 至 iBooks


目前測試 Javascript 的結果,可以用 alert 對 document 跟 window 物件測試一下,代表有此物件可以用,所以接下來測試 document.getElementById 也是 OK 的,當然,去改某個物件的 innerHTML 也是可以成功啦!接著,野心更大就是丟個 jQuery 進去吃吃,發現也 ok 啦!


之前是先寫一個簡單的 HTML 檔案,然後用 Calibre 把他轉成 ePub 來用,後來測試幾次後,發現 script 會被它轉爛,當時還一直以為是 WebKit 的功能被縮減掉(但真的有其他的功能被拿掉了,如 js 的 document.write),浪費不少時間測試,最後,就變成自己手動去建 ePub 啦!建議可以先用 Calibre 建立一個範例檔,然後把它解開來,除了確認內文是否正常,也可以當作一個範本,以後修改完內文就可以快速打包囉!


指令:


$ cd epub
$ ls
META-INF        cover_image.jpg        jquery.html        stylesheet.css        toc.ncx
content.opf        jquery-1.4.2.min.js    mimetype        titlepage.xhtml

打包指令:
$ zip -0Xq ../test.epub mimetype
$ zip -Xr9Dq ../test.epub *


把所在目錄的東西, 打包成 test.epub 並擺到上一層目錄裡


以下是 jquery.html 程式碼:


<?xml version='1.0' encoding='utf-8'?>
<html xmlns="http://www.w3.org/1999/xhtml">
        <head>
                <title>jQuery on local</title>
                <script language="JavaScript" TYPE="text/javascript" src="jquery-1.4.2.min.js"></script>
                <script language="JavaScript" TYPE="text/javascript">
                function updateByjQuery()
                {
                        var curr = new Date();
                        var y = curr.getFullYear();
                        var m = curr.getMonth() + 1;
                        var d = curr.getDate();
                        var h = curr.getHours();
                        var min = curr.getMinutes();
                        var sec = curr.getSeconds();
                        $( '#1' ).html( 'By jQuery: '+y+'/'+m+'/'+d+' '+h+':'+min+':'+sec );
                }
                function updateBygetElementById()
                {
                        var curr = new Date();
                        var y = curr.getFullYear();
                        var m = curr.getMonth() + 1;
                        var d = curr.getDate();
                        var h = curr.getHours();
                        var min = curr.getMinutes();
                        var sec = curr.getSeconds();
                        document.getElementById( '1' ).innerHTML = 'By getElementById: ' +y+'/'+m+'/'+d+' '+h+':'+min+':'+sec;
                }
                function ajaxByjQuery()
                {
                        $.ajax({
                                async: false,
                                type: "GET",
                                dataType: "html",
                                url: "http://tw.yahoo.com",


                                success: function(data) {
                                        alert( data ) ;
                                }
                        });
                }
                function dumpObj(obj, name, indent, depth) {    // modified from http://geekswithblogs.net/svanvliet/archive/2006/03/23/simple-javascript-object-dump-function.aspx
                        if (depth > 1)
                                return ( indent + name + ": 'Maximum Depth Reached'\n" );
                        if(typeof obj == "function") {
                                return "func: " + name + "\n";
                        } else if (typeof obj == "object") {
                                var child = null;
                                var output = indent + name + "\n";
                                indent += "\t";
                                for (var item in obj) {
                                        try {
                                                child = obj[item];
                                        } catch (e) {
                                                child = "'UnableToEvaluate'";
                                        }
                                        if(typeof child == "function") {
                                                output += indent + " func: " + item + "\n";
                                        } else if (typeof child == "object") {
                                                //output += dumpObj(child, item, indent, depth + 1);
                                        } else {
                                                //output += indent + item + ": " + child + "\n";
                                        }
                                }
                                return output;
                        } else {
                                //return obj;
                        }
                }
                </script>
                <meta content="http://www.w3.org/1999/xhtml; charset=utf-8" http-equiv="Content-Type"/>
        </head>
        <body>
                <div id="main">
                        <p id="1">Hello World</p>
                </div>
                <hr/>
                <p>
                        <span onclick="alert(window);">alert(window)</span><br />
                        <span onclick="alert(document);">alert(document)</span><br />
                        <span onclick="alert(document.write);">alert(document.write)</span><br />
                        <span onclick="alert(jQuery);">alert(jQuery)</span><br />
                        <span onclick="alert($);">alert($)</span><br />
                        <span onclick="var curr = new Date();var y = curr.getFullYear();var m = curr.getMonth() + 1;var d = curr.getDate();var h = curr.getHours();var min = curr.getMinutes();var sec = curr.getSeconds();$( '#1' ).html( 'By inline:'+y+'/'+m+'/'+d+' '+h+':'+min+':'+sec );">update by inline (use jQuery)</span><br />
                        <span onclick="updateByjQuery();">update by func (use jQuery)</span><br />
                        <span onclick="updateBygetElementById();">update by func (use document.getElementById)</span><br />
                        <span onclick="$.ajax({async: false,type: 'GET',dataType: 'html',url: 'http://tw.yahoo.com', success: function(data) {alert( data ) ;}});">inline ajax call (use jQuery)</span><br />
                        <span onclick="ajaxByjQuery();">ajax call by func</span><br />
                        <span onclick="alert( dumpObj(document, 'document', '\t', 0) );">dump document</span><br />
                </p>
        </body>
</html>


心得:



  1. 若用 <script language="JavaScript" TYPE="text/javascript" src="http://code.jquery.com/jquery-1.4.2.min.js"></script> 這種方式, 則打開書的時候, 整個會空白, 但改用以下方式就沒問題:

    • <script language="JavaScript" TYPE="text/javascript" src="jquery-1.4.2.min.js"></script>

    • 新增 content.opf 的 manifest


      • <item href="jquery-1.4.2.min.js" id="jquery" media-type="text/javascript"/>



    • 最後,記得把 jquery-1.4.2.min.js 也擺進去打包



  2. 有些地方, 如在寫 inline 時, 若碰到 '<' 符號會被判斷成語法錯誤, 所以得要設法避開

  3. 在 head 定義 js func 時, 不要用 <!-- 與 // --> 去處理, 這樣定義出來不能使用, 即:

    <script language="JavaScript" TYPE="text/javascript"><!--
    func hehe() {}
    // -->
    </script>


一些成果:


測試 jQuery
jquery


錯誤訊息
error


列出 document 相關函數
document_func


2010年5月18日 星期二

iOS 開發教學 - 使用 UIScrollView 筆記

在網路上打滾多時,後來才發現其實 Apple Dev Center 上的範例就很好用了!真搞不懂我在亂花時間做什麼


建議先下載範例程式跑一下,看看這個東西的功能是不是你想要的,以下是一些筆記


  1. 將圖片做適當的縮放並擺在 UIScrollView 來呈現,並且可以放大縮小,此範例僅寫在 YourAppDelegate 中,以及讓 YourAppDelegate 要去回應 UIScrollViewDelegate 和新增一個函數 viewForZoomingInScrollView,並且請準備一張圖擺到 Resources 中(此例是 Googlelogo.png):

    @interface YourAppDelegate : NSObject <UIApplicationDelegate, UIScrollViewDelegate> {
        UIWindow *window;
        UIImageView *imageView ;
    }
    @property (nonatomic, retain) IBOutlet UIWindow *window;
    @end

    @implementation YourAppDelegate
    @synthesize window;

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
        //UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Googlelogo.png"]];

        imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Googlelogo.png"]];
        UIScrollView *scrollView = [[UIScrollView alloc] init];
        [scrollView setFrame:[window frame]];
        [scrollView addSubview:imageView];
        [window addSubview:scrollView];
        //[imageView release];
        [scrollView release];

        CGFloat widthRatio = [scrollView frame].size.width / [imageView frame].size.width;
        CGFloat heightRatio = [scrollView frame].size.height / [imageView frame].size.height;
        CGFloat initialZoom = (widthRatio > heightRatio) ? heightRatio : widthRatio;
      
        [scrollView setZoomScale:initialZoom];
        [scrollView setMinimumZoomScale:initialZoom];
        [scrollView setMaximumZoomScale:2.0];

        CGSize imageViewAdjustSize = CGSizeMake([imageView frame].size.width * initialZoom, [imageView frame].size.height * initialZoom);
        [scrollView setContentSize:imageViewAdjustSize];
      
        [imageView setFrame:CGRectMake(0, 0, imageViewAdjustSize.width, imageViewAdjustSize.height)];
        [imageView setCenter:[scrollView  center]];
       
        [scrollView setDelegate:self];


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

    - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
    {
        return imageView;
    }


    - (void)dealloc {
        [imageView release];
        [window release];
        [super dealloc];
    }
    @end

    uiscrollview

  2. 在某些情境下,希望使用 UIScrollView 來提供 Paging 的效果,例如翻頁的效果,其觀念就是把 UIScrollView 的 Content Width 設成你要的 Page * Width 的寬度,接著設定一開始執行時要停在哪個 Page ,最後則是每一頁的內容要擺什麼,以下是修改 (1) 的程式碼,以 3 Page 為例,並把 UIScrollView 起始擺在第 2 頁,而真正的呈現的圖片擺在第 3 頁,所以執行的效果是,一開始畫面是空白的,但可以往右移過去,及翻到第 3 頁並看到圖片,接著往左可以移兩頁,但因為沒有設定內容,所以看到的都是空白的:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  

        //UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Googlelogo.png"]];
        imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Googlelogo.png"]];
        UIScrollView *scrollView = [[UIScrollView alloc] init];
        [scrollView setFrame:[window frame]];
        [scrollView addSubview:imageView];
        [scrollView setDelegate:self];
        [window addSubview:scrollView];
        //[imageView release];
        [scrollView release];
      
        CGFloat widthRatio = [scrollView frame].size.width / [imageView frame].size.width;
        CGFloat heightRatio = [scrollView frame].size.height / [imageView frame].size.height;
        CGFloat initialZoom = (widthRatio > heightRatio) ? heightRatio : widthRatio;
      
        [scrollView setZoomScale:initialZoom];
        [scrollView setMinimumZoomScale:initialZoom];
        [scrollView setMaximumZoomScale:2.0];
      
        [imageView setFrame:CGRectMake(0, 0, [imageView frame].size.width * initialZoom, [imageView frame].size.height * initialZoom)];
      
        int TotalPageCount = 3;
        int MainImagePageOffset = 2;
        int CureentPageOffset = 1;

        // content width of scrollView = TotalPageCount * width of scrollView => paging
        CGSize imageViewAdjustSize = CGSizeMake( [scrollView frame].size.width * TotalPageCount, [scrollView frame].size.height );
        [scrollView setContentSize:imageViewAdjustSize];
      
        CGPoint imageOffsetOnScrollViewContent = [scrollView center];
        imageOffsetOnScrollViewContent.x += [imageView frame].size.width * MainImagePageOffset;
        [imageView setCenter:imageOffsetOnScrollViewContent];
      
        [scrollView setContentOffset:CGPointMake( [imageView frame].size.width * CureentPageOffset, 0 )];
        [scrollView setPagingEnabled:YES];


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

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 ,如此一來程式就會不正常結束,這都需要小心使用。

2010年5月13日 星期四

[iPhone/iPad] 匯入自製的 ePub 電子書 至 iBooks


圖片來源 - http://itunes.apple.com/us/app/ibooks/id364709193


不曉得 iBooks 嗎?這是一套免費的電子書閱讀軟體,可以在 iTunes App Store 下載!透過 iBooks 除了可以購買一些要付費的電子書外,其實也可以自製一些免費的電子書傳到 iPad 或 iPhone 上觀看,下則就是教學影片。









其教學影片,則是把想要製成電子書的內容,透過一些網站來製作,然而,其實已經有許多不錯的工具可以用囉,目前有一個統一的格式 ePub
正在慢慢地推行,可以透過 Calibre 這免費的軟體,可以幫你把 PDF 轉成 ePub 格式,甚至 HTML 也能轉喔!



圖片來源 - http://en.wikipedia.org/wiki/File:Calibre_main_screenshot.png


使用 Calibre 也不用擔心,安裝時可以選擇語言,有中文可以選囉,之後只需要點選最左上的按鈕,把你的 PDF 匯入到 Calibre 後,接著在點選左上角第二或第三個按鈕,有一個可以幫你轉格式,記得選匯出 ePub 就行了。


至於匯入 iBooks 的方式,其實就只是透過 iTunes 的同步來處理,把你製作好的 ePub 檔案,拉到 iTunes 視窗內(一開始可從"音樂")試試,不久之後就會看到新增的"書櫃"項目,可以看到你上傳的自製電子書,最後就再用同步的方式傳到 iPhone 或 iPad ,用 iBooks 觀看整個感覺就是不一樣!


2010年5月12日 星期三

[Objective C] NSMutableString in NSMutableDictionary - setString: 'Attempt to mutate immutable object with setString:'

有時候會想用 *.plist 來儲存資料,這時候若是從檔案內讀進資料建立 NSMutableDictionary 時,某個資料是
NSMutableString 時,想要對他 setString 時,就會出現 'Attempt to mutate immutable
object with setString:' 的錯誤訊息,程式也就不正常結束了。


猜是可能是從檔案讀進來建的資料結構有問題,如只轉成 NSString 而已,雖然我有用 [obj isKindOfClass:[NSMutableString class] ] 來判斷出來是 NSMutableString
狀態,但在這個情境下還是會出錯。


測試程式如下:


UntitledAppDelegate.h


#import <UIKit/UIKit.h>

@interface UntitledAppDelegate : NSObject <UIApplicationDelegate> {

    UIWindow *window;

}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@end


UntitledAppDelegate.m


#import "UntitledAppDelegate.h"



@implementation UntitledAppDelegate

@synthesize window;



- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    



  
 NSArray *paths =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);

    NSString *documentsPath = [paths objectAtIndex:0];

    NSString *db_path = [[NSString alloc] initWithString:[documentsPath
stringByAppendingPathComponent:@"test.plist"]];



    NSMutableDictionary *db;

    if( [[NSFileManager defaultManager] fileExistsAtPath:db_path] )

       db = [[NSMutableDictionary alloc]
initWithContentsOfFile:db_path];

    else

       db = [[NSMutableDictionary alloc] init];

    

    if( [db objectForKey:@"test"] == nil )

    {

        NSLog( @"Init test" );

        [db setValue:[[NSMutableString alloc] initWithString:@"1"]
forKey:@"test"];

    }

    else if( [[db objectForKey:@"test"] isKindOfClass:[NSMutableString
class] ] )

    {

        NSLog( @"set test from disk data" );

        [[db objectForKey:@"test"] setString:@"2"];

    }

    else

    {

        NSLog(@"ERROR");

    }

    if( [db objectForKey:@"test"] && [[db objectForKey:@"test"]
isKindOfClass:[NSMutableString class] ] )

    {

        NSLog( @"set test from memory" );

        [[db objectForKey:@"test"] setString:@"2"];

    }


    

    [db writeToFile:db_path atomically:YES];

    

    // Override point for customization after application launch

    [window makeKeyAndVisible];

    return YES;

}



- (void)dealloc {

    [window release];

    [super dealloc];

}



@end



執行結果:


第一次:


[Session started at 2010-05-12 09:06:16 +0800.]
2010-05-12 09:06:17.170 Untitled[1017:207] Init test
2010-05-12 09:06:17.171 Untitled[1017:207] set test from memory


再跑第二次:


[Session started at 2010-05-12 09:06:22 +0800.]
2010-05-12 09:06:23.488 Untitled[1022:207] set test from disk data
2010-05-12 09:06:23.489 Untitled[1022:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with setString:'
2010-05-12 09:06:23.494 Untitled[1022:207] Stack: (
    29287515,
    2479863049,
    29371451,
    29371290,
    723866,
    9974,
    2721159,
    2759094,
    2747188,
    2729599,
    2756705,
    37383513,
    29072256,
    29068360,
    2723349,
    2760623,
    9252,
    9106
)


目前暫時的解法,只好把它刪除再重新設定了!


原本:


[[db objectForKey:@"test"] setString:@"2"];


更新:


[db removeObjectForKey:@"test"];
[db setValue:[[NSMutableString alloc] initWithString:@"2"] forKey:@"test"];


2010年5月10日 星期一

iOS 開發教學 - 兩個 UITableViewController 共用一個 dataSource

UILog

有時候呈現多個 View 時,用的資料會有相關性,此例以兩個 UITableViewController 為例,他們的 dataSource 是同一個來源,當透過底下的 TabBar 進行接換時,會自動增加 dataSource 的資料,接著呈現最新的清單列表,並且在 Console 上印出從哪個 View 新增的資料,以及目前 dataSource 的個數。此作法是共用記憶體資料,另一種作法是每次讀資料都從 databases 或 file 更新。

程式碼:

DataConnectAppDelegate.h

#import <UIKit/UIKit.h>

@interface DataConnectAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    NSMutableArray *dataSource;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@end

DataConnectAppDelegate.m

#import "DataConnectAppDelegate.h"
#import "MyTableViewController.h"

@implementation DataConnectAppDelegate

@synthesize window;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  

    // Override point for customization after application launch
    dataSource = [[NSMutableArray alloc] init];
  
    MyTableViewController *a = [[MyTableViewController alloc] init];
    a.dataSource = dataSource;
    a.title = @"View A";
  
    MyTableViewController *b = [[MyTableViewController alloc] init];
    b.dataSource = dataSource;
    b.title = @"View B";
  
    UITabBarController *tabBar = [[UITabBarController alloc] init];

    tabBar.viewControllers = [NSArray arrayWithObjects:a, b, nil];
    [window addSubview:tabBar.view];
    [window makeKeyAndVisible];

  
    return YES;
}

- (void)dealloc {
    [dataSource release];
    [window release];
    [super dealloc];
}

@end

 MyTableViewController.h

#import <UIKit/UIKit.h>

@interface MyTableViewController : UITableViewController {
    NSMutableArray *dataSource;
}

@property (nonatomic ,assign) NSMutableArray *dataSource;


@end

 MyTableViewController.m

#import "MyTableViewController.h"

@implementation MyTableViewController

@synthesize dataSource;
#pragma mark -
#pragma mark Initialization

#pragma mark -
#pragma mark View lifecycle

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog( @"In,%3@, %d" , self.title , [self.dataSource count]);
    [self.dataSource addObject:[NSString stringWithFormat:@"%d) %@" , [self.dataSource count] , self.title ]];
    [[self tableView] reloadData];

}

#pragma mark -
#pragma mark Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // Return the number of sections.
    return 1;
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    // Return the number of rows in the section.
    if( dataSource == nil )
        return 0;
    return [dataSource count];

}

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  
    static NSString *CellIdentifier = @"Cell";
  
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }
  
    // Configure the cell...
    cell.textLabel.text = (NSString*)[dataSource objectAtIndex:indexPath.row];
    return cell;
}

#pragma mark -
#pragma mark Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // Navigation logic may go here. Create and push another view controller.
    /*
     <#DetailViewController#> *detailViewController = [[<#DetailViewController#> alloc] initWithNibName:@"<#Nib name#>" bundle:nil];
     // ...
     // Pass the selected object to the new view controller.
     [self.navigationController pushViewController:detailViewController animated:YES];
     [detailViewController release];
     */
}

#pragma mark -
#pragma mark Memory management

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

- (void)viewDidUnload {
    // Relinquish ownership of anything that can be recreated in viewDidLoad or on demand.
    // For example: self.myOutlet = nil;
}

- (void)dealloc {
    [super dealloc];
}

@end

成果:

UI1 UI2
左邊一開始執行程式時,由於預設是進入 View A ,故一開始是新增一筆 View A 資料,接著點選下方的 TabBar 切換到 View B 時,則會新增另一筆資料 View B ,接著再 Table View 就會呈現出兩筆資料,最後,不斷掉交替切換到 View A 或 View B 那則會呈現以下的資料列表

UI3

此例在 UITableViewController 中的 - (void)viewWillAppear:(BOOL)animated; 進行實作的,要記得呼叫 [[self tableView] reloadData]; 才會更新 Table View 的資料,否則不斷地切換就只會顯示上面兩個小圖而已,那就是因為沒有叫 tableView 更新資料。

2010年5月6日 星期四

iOS 開發教學 - Regular Expression 之使用 RegexKitLite

處理字串很容易就會用到 Regular Expression 啦!在 iPhone SDK 中雖然可以用 regex ,但卻少了物件導向的方便性。所幸,有方便的 library 可以用,那就是 RegexKitLite - Lightweight Objective-C
Regular Expressions for Mac OS X using
the ICU Library
囉!

用法不難,網路上也滿多相關文章的,簡單筆記:


  • 下載 RegexKitLite-4.0.tar.bz2 (139.1K)

  • 把裡頭兩個檔案 RegexKitLite.h 和 RegexKitLite.m 拉到你的專案下,並且增加編譯程式的參數

    • [Xcode]->[Project]->[Edit Project Settings]->[Linking]->[Other Linker Flags]-> 增加 '-licucore' 即可

簡單的範例

片段程式碼:

NSString *list    = @"<a href='t1.html'>t1</a><a href='t2.html'>t2</a><a href='t2.html'>t2</a>";
NSArray *listItems = [list arrayOfCaptureComponentsMatchedByRegex:@"href=['\"](.*?)['\"]"];
  
NSLog( @"%@" , listItems );

輸出:

(
        (
        "href='t1.html'",
        "t1.html"
    ),
        (
        "href='t2.html'",
        "t2.html"
    ),
        (
        "href='t2.html'",
        "t2.html"
    )
)

剩下的使用可以參考 RegexKitLite-4.0.pdf

2010年5月4日 星期二

遠端桌面 登入 Mac OS X (Remote Desktop) @ Windows 7

由於我的 Mac 是用我 Windows 機器分出去的 IP 享用網路的,也就是 Private IP 啦,雖然已經有 KVM switch 但剛剛突然想要試試從 Windows 7 遠端登入 Mac OSX 囉!找了一下相關文章,結果竟然沒成功:[教學] XP跟Mac互相遠端桌面遙控


原來是少設定密碼了,記錄一下:


[Mac OS X 10.6.2]->[系統偏好設定]->[共享]->[遠端管理]


"電腦設定"


勾選 "VNC 檢示程式可以使用密碼來控制螢幕" ,並且填寫一下密碼吧


"選項"


可以全勾看看(我在Private IP比較無憂無慮XD)


接著下載 TightVNC (VNC-Compatible Free Remote Control Software) ,就可以輸入 Mac OS 的 IP 登入囉!原先也有試著用 RealVNC 免費版,但一直不成功,最後才看到這張表 http://www.realvnc.com/vnc/features.html,才發現只有 Enterprise Edition 才有支援 Mac OSX (x86 and PPC) 囉


TightVNC


只不過有一些好奇的問題,假設我沒有勾選 "VNC 檢示程式可以使用密碼來控制螢幕" ,結果會登入不了,錯誤訊息:


Server did not offer supported security type


原先是想用內建帳密的,但看來不適合(不會用)這個情境下,其他的方式有的在 Mac OS 上架設 VNC Server ,目前還沒有那種需求囉。測試的心得,連上線後,擺一陣子不動就會斷線了?不知是不是我沒設定好,暫時僅用來 Demo 用而已。


最後一提,反而是 Windows 與 Mac 的資料共享還滿重要的,請參考 [分享]Windows & Mac Mini的雙向網路磁碟共享之經驗分享(圖文並茂版)


2010年5月3日 星期一

機車 排氣檢驗 二行程


騎車的經驗差不多有半年了!我機車的車齡已經超過 15 年了!是一台可以自己加機油的二行程機車。


今天有補假,下午就騎車到附近的檢測站,大概只花 3 分鐘就到了!測試的結果 -- 有兩項不合格!


一氧化碳(CO)


數值 5.xx / 排放標準 4.5


碳氫化合物(HC)


數值 1xxxx / 排放標準 9000


二氧化碳(CO2) 


數值 3.30 / 排放標準 3.00


其中只有第三項 (CO2) 是有合格的。機車老闆就說我這車老了,要換東換西,什麼化油器還啥的,大概要兩千多吧!當下我心裡也有個底,好吧!那就去大家推薦的機車行問一下價錢吧!騎了 20 來分,終於到了,也跟老闆聊聊我的問題,原本想問他換東西要多少錢,結果他馬上幫我拆下板子調整一下,還問我有沒有印出檢測的單子借他看一下,沒幾下,他就跟我說,請我去附近一家的檢測站,報他的車行名字去。


對於路況不熟的我,還是上路了,也幸運地找到囉!這次幫我檢驗排氣的是一位熟女,當下我馬上想到 FF7 的 Tifa ,上面那張圖片是 Google 到的 Tifa Cosplay 囉!跟她閒聊後,她跟我說了不少重要的事:



  • 二行程的車子,若溫度不夠高那測試的結果一定是不合格的

  • 可以看 CO2 的指標,若只是剛過 3 的標準,那其實排氣管就還不夠熱

  • 二行程的機車,政府有補助報廢,但要看各縣市的名額還有沒有,有的話有 1500 元,另外,原先都會有環保局 300 元補助,所以最多可以領到 1800 補助費,但各縣市的名額有限,還得去問問就是了


再次測試的結果:


一氧化碳(CO)


數值 0.x / 排放標準 4.5


碳氫化合物(HC)


數值 5xxx / 排放標準 9000


二氧化碳(CO2)


數值 7.xx / 排放標準 3.00


難怪,一開始我覺得幫我測的老闆臉上有一點點奸笑的感覺,再加上我把車子熄火,對我說話有點點不耐煩,不過有一點我也誤會了,像第一位老闆有操我的機車一下,我那時還有點不悅,現在想起來,他大概是要幫我熱車,只是測試失敗時,他竟然沒跟我說一些良心話,在測試過程中就不停囔囔,說這台不會過囉!這也讓我有點起疑心,彷彿還沒測試他早已知道結果了,所幸,還是有碰到好人囉!壞人的存在是基本款,我倒已習慣,更要好好珍惜好人啦~


碰到好店家就是不一樣!當下不是請你花錢買設備,而是很好心地先要幫你調一調!真好!後來我是去展業,體驗好心老闆的服務!但測排氣是到台鈴測試(從寶山路往西大路騎過去,與南大路交界前 50 m 處),不過並不是老闆娘超正啦,只是碰過壞人之後,對於認真的女性,感覺就像看到 Tifa 一樣啦!還有,排測檢驗是要考證照的,在這之前我還以為每一間機車行都行哩