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"];