2012年8月31日 星期五

iOS 開發筆記 - 使用 Interface Builder 建立專屬的 UITableViewCell

IBStudy--


雖然內建的 UITableViewController 已經有不少還不錯用的 Cell Style (UITableViewCellStyleDefault, UITableViewCellStyleValue1, UITableViewCellStyleValue2, UITableViewCellStyleSubtitle),但客製化自己 Cell 還是一種必學的技能。然而,客製化自己的 layout 大概可以分成兩種方式,一種是純程式碼的撰寫(覆寫 layoutSubViews 或對 contentView 加東西),例如把 ImageView 擺在哪個 (x,y) 座標並設定大小為 (w,h) 等,另一種則是使用 Interface Builder 對元件的拖拉,再跟 Class 進行相對應的設定。由於之前比較熱愛 Coding ,所以一直沒用 Interface Builder,這次就來摸個幾把美美的 UI 操作吧 *誤*


使用 Interface Builder 仍需搭配 Class 物件來使用。例如建立自己的 MyTableViewCell(繼承UITableViewCell) 排版畫面 MyCell.xib 後,需搭配物件使用,當物件初始化時,可以指定採用 MyCell.xib 來初始化,整個過程等同於跟系統要了記憶體來使用,所以有一些人認為透過 Iterface Builder 所建立的 layout 不太好進行資源管理,並且當程式不穩時,很難除錯。


以 UITalbViewController 為例,建立自己的 MyTalbeViewCell,其粗略的使用方式:



  1. 建立 XIB (MyCell.xib),規劃 UI 呈現部分

  2. 建立對應的 UIView Class (MyTableViewCell 並繼承 UITableViewCell),並新增所需的 IBOutlet

  3. 更新 XIB 的 Custom Class 及對 XIB 上的元件進行連結 (outlets)

  4. 在 UITableViewController - (void)viewDidLoad 中,進行 XIB 的註冊

  5. 在 UITableViewController - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 中,進行 cell 的初始化及設定使用

  6. 在 UITableViewController -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 中,回傳 Cell 的高度


用法:[New File] -> [User Interface] -> 選 View or Empty 都行 -> 選擇 iPhone or iPad -> 取名 MyCell (此名稱跟之後XIB使用有關)


IBStudy00


點一下 MyCell.xib 開啟設定頁面,接著可在右下角 Object Library 之下方 Search 欄位中,填入想要的元件並拖拉到畫面進行 layout 安排,如使用 UITableViewCell、UILabel、UIImageView、UITextView 等元件


IBStudy01


IBStudy03


接著建立自己的 UITableViewCell 物件,此例為 MyTableViewCell,並將 XIB 中用到的元件進行宣告及實作(透過 Objective-C @property 語法可以請編譯器處理實作)


@interface MyTableViewCell : UITableViewCell


@property (nonatomic, strong) IBOutlet UIImageView *mImageView;
@property (nonatomic, strong) IBOutlet UILabel *mTitle;
@property (nonatomic, strong) IBOutlet UILabel *mSubtitle;
@property (nonatomic, strong) IBOutlet UILabel *mDescription;


@end


接著再切回 MyCell.XIB 進行元件的連結,先點選左邊 Objects 下的 UITableViewCell 後,在右邊選單上,切到 Identity inspector 可以進行 Custom Class 的更名,請更改成剛剛建立的 Class name (MyTableViewCell),如此一來切到 Connections inspector 可以進行 Outlets 連結,並可以看到 mImageView、mTitle、mSubtitle 和 mDescription 等項目。


IBStudy04 IBStudy05


將滑鼠移至項目旁邊的圓形框中,可以看到 + 號,點選按著後,拖到左邊畫面上的對應元件,放開後則完成連結。


IBStudy06


另外也可以在左邊 Objects 下,按著 Ctrl 點一下 My Table View Cell 後,也會出現 Connections 清單,其連結方式一樣。


IBStudy08


一切的 Connections 設定完後,即可存檔關閉,在此僅 demo 呈現的部份,若有其他點擊互動則需要設定 IBAction。此外,點選 MyTableViewCell.h 中,可以看到左邊都可以看 IBOutlet 都有圓點的連結圖示,亦可用來 debug 觀看自己有沒有漏掉什麼連結。


IBStudy10


最後則是挑一個 UITableViewController 來把玩吧:


- (void)viewDidLoad
{
    [super viewDidLoad];


    [self.tableView registerNib:[UINib nibWithNibName:@"MyCell" bundle:nil]
forCellReuseIdentifier:@"MyCellIdentifier"]; // "MyCell" for MyCell.xib
}
- (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.
    return 5;
}


-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 150; // 可在 XIB 檔案,點選 My Talbe View Cell 從 Size inspector 得知
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    //if( cell == nil)
    // cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"Cell"] autorelease];
    //cell.imageView.image = [UIImage imageNamed:@"icon_chrome.png"];


    MyTableViewCell *cell = (MyTableViewCell *)[tableView dequeueReusableCellWithIdentifier:@"MyCellIdentifier"];
    cell.mTitle.text = [NSString stringWithFormat:@"Row: %d", indexPath.row];
    cell.mSubtitle.text = [NSString stringWithFormat:@"section: %d", indexPath.section];
    cell.mDescription.text = @"My Description";
    switch ((indexPath.row%4)) {
        case 1:
            cell.mImageView.image = [UIImage imageNamed:@"icon_chrome.png"];
            break;
        case 2:
            cell.mImageView.image = [UIImage imageNamed:@"icon_firefox.png"];
            break;
        case 3:
            cell.mImageView.image = [UIImage imageNamed:@"icon_safari.png"];
            break;
         default:
            cell.mImageView.image = [UIImage imageNamed:@"icon_ie.png"];
            break;
    }
    return cell;
}


成果:


IBStudy99


2012年8月29日 星期三

iOS 開發筆記 - UITableViewController 之 dataSource 更新顯示

最近開始複習 iOS SDK,過去使用 UITableViewController 時,每次資料新增變動時,都是透過 [self.tableView reloadData] 來更新畫面,效果就是把整個 UITableView 都重畫一次,只是若碰到資料更新很頻繁時,就會發現有些 touch event 因過度 reloadData 而出現短暫不能被處理。最近看 UITableView 相關書籍時,才發現原來早在 iOS SDK 3.0 時,就可以指定某個 Section 的某個 Row 更新就好!這招當然就可以用在新增資料及更新資料,實在太讚了!不曉得是不是太早接觸 iPhone SDK 哩,真的是太晚發現了 Orz


From UITableView Class Reference



  • reloadData (iOS SDK 2.0)

  • reloadRowsAtIndexPaths:withRowAnimation: (iOS SDK 3.0)

  • reloadSections:withRowAnimation: (iOS SDK 3.0)

  • reloadSectionIndexTitles (iOS SDK 3.0)


早期寫法(iOS 2.0):



  1. 更新 dataSource

  2. 呼叫 [self.tableView reloadData]


更有效率得寫法(iOS 3.0),新增資料(前頭):



  1. [dataSource insertObject:newItem atIndex:0];

  2. [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject: [NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];


更有效率得寫法(iOS 3.0),新增資料(後頭):



  1. [dataSource addObject:newItem];

  2. [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject: [NSIndexPath indexPathForRow:[dataSource count]-1 inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];


如果,不幸一開始使用卻會蹦出 Exception:


Invalid update: invalid number of sections.  The number of sections contained in the table view after the update (1) must be equal to the number of sections contained in the table view before the update (0), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).


那可能跟我的情況很像,簡單的說,假設你的 numberOfSectionsInTableView 判斷是 return dataSource && [dataSource count] ? 1 : 0 ; 的話,那一開始因為沒資料而 sections 個數為 0,所以更新指定 section 時會出錯,解法有兩個,一個是一開始 numberOfSectionsInTableView 就會回傳固定數值(非0),這樣就不會出錯;另一個則是第一次新增資料時,呼叫 [self.tableView reloadData] 處理,之後才用 insertRowsAtIndexPaths 處理:


[dataSource addObject:item];
if( [dataSource count] == 1 )
        [self.tableView reloadData];
else
        [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject: [NSIndexPath indexPathForRow:[dataSource count]-1 inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];


此外,在新增資料在前頭後,如果更新太頻繁時將會碰到 'NSInternalInconsistencyException', reason: 'Attempt to create two animations for cell' 而程式 crash,例如:


for( int i=0 ; i<50 ; i++ ) {
         [dataSource insertObject:[NSString stringWithFormat:@"%d",i] atIndex:0]; // 每次塞在 index = 0 的位置
         [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject: [NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic]; // 每次都對 index = 0 的 cell 更新
}


解法:


for( int i=0 ; i<50 ; i++ ) {
        [dataSource insertObject:[NSString stringWithFormat:@"%d",i] atIndex:0];
        [self.tableView beginUpdates];
        [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject: [NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationAutomatic];
        [self.tableView endUpdates];
}


或統一更新:


NSMutableArray *indexPathSet = [[NSMutableArray alloc] init];
for( int i=0 ; i<50 ; i++ ) {
        [dataSource insertObject:[NSString stringWithFormat:@"%d",i] atIndex:0];
        [indexPathSet addObject:[NSIndexPath indexPathForRow:i inSection:0]]; // 依序累積 50 個 rows
}
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:indexPathSet withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
[indexPathSet release];


故比較好得方式就是在對 cell 處理更新特效時(除了reloadData用法外),一律採用 [self.tableView beginUpdates]; 和 [self.tableView endUpdates]; 包起來(有點lock味道),並且在目前很流行 Event-Driven 或 Async 架構更須如此。更多詳情,請參考 UITableView Class Reference 囉。


2012年8月24日 星期五

Android 開發筆記 - APP 的上架與更新

ExportSignedApplicationPackage


嘗試將小試身手的程式上架看看,初步的心得,覺得 Android app 上架比 iOS app 還要快,大概資料填妥後,約兩小時內就可以在 Google Play 搜尋到了!


上架前,需準備的資料:



  • keystore

  • Signed Application Package (apk)

  • ic_Launcher.png

  • 512x512 icon

  • 2 images


軟體更新需準備的資料:



  • 更新 AndroidManifest.xml 的 Version code 跟 Version name 

  • 上次使用的 keystore

  • Signed Application Package (apk)


所謂的 keystore 可以想成對 apk 進行簽名,透過這個 keystore ,可以驗證此 apk 是由同一人所送出的。特別在 Android Developer Console 中,可允許多位開發人員,假設有兩個團隊分別上架兩個 app 時,這時每個團隊各自用自己的 keystore 維護,除了可避免內鬥被人亂換掉 apk 外,也是種不錯的保護機制。當然,如果連 keystore 弄丟的話,似乎…只好砍掉重來了 XD


過去產生 keystone 時,都是透過 command line 來進行,現在 Eclipse+ADT 的環境已經可以透過視窗介面進行啦!並且產生完 keystore 後,連續動作還可以順便產生可以上架的 apk 


在 Eclipse 之 Package Explorer 裡點選 Your App -> 按右鍵選擇 Android Tools -> 選 Export Signed Application -> Create new keystone -> 輸入相關資料及儲存位置 -> 順便輸出簽證後的 apk


CreateKeystore1


CreateKeystore2


接著回到 Android app 上架流程,拿著剛簽好的 apk 往 Android Developer Console 上去後,會蹦出一些表單資料的填寫,其中需要的額外資料:



  • 至少 2 張圖片

  • 一張 512x512 圖片

  • 程式顯示名稱(在Google Play上)

  • 程式簡介

  • 程式分類

  • 程式類型


如此填好後,就可以按儲存或是發佈了,十分快速。至於兩張使用圖片,可以用 Eclipse 之 DDMS 去拍兩張程式在裝置上的使用截圖即可,而 512x512 基本上就是 App icon 的圖,所以做 App icon 時,別忘了先做張 512x512 的圖,之後縮成 72x72 來當 ic_Launcher.png 使用即可。


如果要更新已上架的軟體,姑且不論程式碼有沒更新,至少需更新 AndroidManifest.xml 的 Version code 跟 Version name 即可,其中 Version code 是流水號,讓 Google Play 系統判別用途,而 Version name 是顯示給使用者的版本,如 1.0, 1.0.1, 1.1 等用途。接著一樣用 Android Tools -> Export Signed Application -> 選擇 keystone path -> 輸入密碼 -> 選擇 Alias 及密碼後 -> 輸出成功


UseExistingKeystore1


UseExistingKeystore2


最後到 Android Developer console 上,選擇要更新的 app 後,切換到 APK 分頁,點選 "上載APK" 後,再把前一版本停用,啟用新版本,就算完成更新,更細膩的部份則是切換到"產品詳細資料"裡,填寫此版本更新的項目資訊。約莫兩小時就可以在 Google Play 上搜尋到新版資訊。


免費申請鄧白氏環球編碼(D-U-N-S Number)

最近正在幫公司申請 iOS Developer Program for Company 版本,結果有一欄是 D-U-N-S Number,詢問一些前輩才發現這是今年五月底或六月初才新增的資料欄位,可能是要提高申請 Company 的門檻補充 Company 資料吧,看到要多填的資料時,大多都會想改用 Individual 版本吧。


順便一提的,鄧白氏環球編碼(D-U-N-S Number)常用在貿易公司,鄧白氏大概算公認的第三方評鑑公司,故申請到D-U-N-S Number可促進貿易過程的流暢。總之,為了符合 Apple 的要求,著手進行申請吧!就跑去逛 www.dnb.com 後,發現該網站免費申請的表格是僅限美國地區的(或是與美國政府單位有來往的才可以申請),就輾轉逛到 www.dnb.com.tw 後,發現沒有所謂的免費,申請一組就要價一萬六(年費) 並且在 CocoaChina 看到一堆人哀號 XD  大多都花錢買了一組(人民幣1500以上吧),正當自己意志力薄弱時,就順手播電話給 Apple Developer 台灣區客服(0800-022-237),得知是可以免費申請的。


首先在 https://developer.apple.com/ios/enroll/dunsLookupForm.action 填好公司資料進行查詢(D-U-N-S Number申請前都是先查詢),查沒資料就可以按 Submit 去申請啦!大概一到兩天,就會收到 dnb.com 寄來的 D-U-N-S Number Request/Update Confirmation 信件,上頭會顯示大概約一個月左右才會完成申請,過程將會有專人聯絡。然而,這次的經驗是在7天內就收到台灣區的電話確認,確認完資料後,大概一天內就會收到 D-U-N-S Number Request/Update Completed,也就獲取到一組 D-U-N-S Number 啦。


D-U-N-S Profile Lookup


Legal Entity Name: YourCompanyName Inc.
Tradestyle or DBA: (可空白)


Headquarters Address (公司地址)
Street Address:
City: 
State/Province:
Postal Code:
Phone Number:
Country:Taiwan


Mailing Address (同上)


Work Information
Full Name: 負責人名字
Job Title: 負責人職稱
Phone Number: 負責人電話
Work Email: 負責人信箱


不過,在 https://developer.apple.com/support/D-U-N-S/ 網站上有提到,仍須 14 個工作天 Apple 才能跟 DNB 同步資料,所以還須等等囉。


補充一下:當初沒透過 https://developer.apple.com/ios/enroll/dunsLookupForm.action 申請的原因是...底下的 captcha image 根本看不到...當然也就沒試了,所幸寄信給 Apple 後,就可以使用囉。


2012年8月16日 星期四

Android 開發筆記 - 更新 ListView 之 AndroidRuntime FATAL EXCEPTION 之 UI Thread 問題

一直以來都知道更新 UI 時,要使用 UI Thread 來進行,有幾種方式:



  • 使用 Handler.post

  • 使用 YourActivity.this.runOnUiThread

  • 使用 mListView.post 


然而今天練習時卻一直蹦出錯誤訊息:


AndroidRuntime FATAL EXCEPTION: main
java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread.


因為我在背景跑了 Thread 來更新資料,更新完資料在請 Handler 或 runOnUiThread 來更新 ListView,卻依舊碰到上述的錯誤訊息,最後才發現...那就是 ListView 的 Adapter 一開始在初始化時,有指定一個 ListAdapter 給他,那時有綁定一個 List/ArrayList 結構來記錄要顯示的資料(在此簡稱 mItemList),我一直以為只要 mSimpleAdapter.notifyDataSetChanged 、 mListView.requestLayout 在 UIThread 做就好,沒想到那個 mItemList 的增減也必須在 UIThread 做才行。這個 bug 不是每次都會出現,但出現的比例不低啦 :P


總之,解法就把 mItemList 的更新也都在 UIThread 做就行了 :P