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 一樣啦!還有,排測檢驗是要考證照的,在這之前我還以為每一間機車行都行哩