打造所有iOS版本皆適用的ActionSheet

之前寫過如何在iOS的ActionSheet加上圖片,雖然很簡單~卻要針對新舊版(以iOS 8劃分)寫二種code,且添加上去的圖片位置很醜XD

既然不好用又醜,乾脆手工打造一個可以適用所有iOS版本又漂亮的ActionSheet吧
(8之後已改用UIAlertController,為了方便本文統一稱呼ActionSheet)

如果對專案實作方式沒有興趣,只想使用~請到我的GitHub吧 :)

How To Customize ?
準備要打造的ActionSheet,希望能符合下列需求
1.  選項要可以帶文字&圖片
2.  樣式編排要仿照目前最新版iOS 8,樣式要相同有幾點要注意
     2.1  要有Title
     2.2  除了ActionSheet以外,整個背景都要帶半透明灰底
     2.3  ActionSheet要整個展開,不可出現TableView的scroll形式
3.  行為模式也要仿照,要做到相同效果有幾點要注意
     3.1  ActionSheet要以"滑出"的效果出現
     3.2  若未選中任何選項,而是touch半透明灰底背景,必須以往下收闔的動畫效果關閉ActionSheet
     3.3  ActionSheet被選中的選項要能觸發事件。觸發要透過delegate傳遞到前端,不可寫出高耦合性的view

接下來的實作,會產生三個一組的h、m、xib檔案,這一組以後就是可以重複使用且適用所有版本的ActionSheet


● xib file
觀察前述需求,xib需要灰背景、放選項清單的TableView、能偵測灰背景被按到的gesture

View:選上圖的View,設定Background(需求2.2)


TableView:設定delegate與dataSources(藍框),建立table的連結至h檔的"*actionView"(紅框)


Gesture:將gesture連結至前面設灰背景的View(需求3.2)



● h file
h檔程式碼如下

首先看Line17~25
Line 17:用來當作ActionSheet列表的Table要塞資料,且按下不同item要觸發事件,故此處需實作UITableViewDelegate, UITableViewDataSource
Line 18:method 'setData' 是為了賦予TableView資料,也就是ActionSheet所呈現的選項
Line 19:method 'setTitle' 用來設定ActionSheet標題
Line 21:property '*actionView' 是透過IB(Interface Builder)設定的TableView連結
Line 22:property '*actionItems' 用來存放'setData'傳進來的值
Line 24:property 'delegateOfAction' 是為了委派,簡單來說其他外部class想要透過CustomActionSheet去幫他做事,甚至取得後續資訊,那麼就要將自己指定為 'delegateOfAction' 這個動態型別(如何指定會在之後談到)
ps:如果delegate的概念對你來說太吃力,請參考這篇iOS學習_實作自己的delegate來傳值

再來看Line12~15
Line 14:剛剛提到透過委派(delegate)可以請別人做事,甚至傳遞資訊 。本文的ActionSheet會將被選中item資訊返回,所以定義一個回傳資料id或index的method  'actionSelected:(NSString*) itemIdOrIndex'


● m file
1. 實作View的進場效果,與按下View觸發gesture event後的退場效果(需求3.1 & 3.2)
gesture當然是透過委派

 
viewWillAppear作二件事:註冊gesture,讓畫面進場(參考稍後的 'slideIn' method)

撰寫進場/退場method,特別注意的是退場method 'slideOut' 會用在二個情境,按下灰背景View與選中TableView選項

2. 塞資料
指派TableView資料(Line 74~76)與Title資料(Line 78~80)


3. 為滿足需求2.3,必須動態指定TableView高度,且Title(需求2.1)我打算用TableView Header來實踐(Header高度也必須算進去)。以下一併實現需求2.1 & 2.3
在viewDidLayoutSubViews呼叫method 'adjustHeight' 依照資料筆數計算高度並指定給TableView


計算時依據是否有Title(Line 127)決定高度是否要加上Header高度


4. 實作UITableViewDataSource二個required method


5. 實作TableView項目被選中
TableView被選中(也就是user認為的ActionSheet)除了讓整個View退場,還要透過h檔宣告的 'actionSelected:(NSString*) itemIdOrIndex' method將選中的資訊回傳(Line 65~72)



以上,已成功實作適用所有iOS版本的ActionSheet,接著會用一個空白頁面示範如何透過NavigationBar叫出ActionSheet...

How To Use ?
想要使用CustomActionSheet的Class必須遵循下列事項
1.  宣告實作CustomActionSheetDelegate   (h file)
2.  完成method  'actionSelected:(NSString*) itemIdOrIndex'   (m file)
3.  藉由addSubview將CustomActionSheet.xib附加於畫面上   (m file)

● xib file
本文叫出CustomActionSheet是透過NavigationBar,故畫面上對擺放何種UI Element無特殊需求


● h file
宣告實作CustomActionSheetDelegate



● m file
宣告等一下要使用的property


m檔其他程式碼如下

Line 23:initial
Line 27~30:NavigationBar呼叫method 'showCustomActionsheet'把畫面叫出來
Line 33~41:CustomActionsheet需要的資料與設定都在此完成
Line 83~107:撰寫CustomActionSheetDelegate下的 'actionSelected:(NSString*) itemIdOrIndex' method

Done !
按下NavigationBar,畫面出現帶灰底背景的CustomActionSheet

選擇第三個選項TXT,因為有委派機制~所以可以在主畫面抓到被選中的id為3

參考資料
Implementing a Custom UIActionSheet
Display a custom UIView like a UIActionSheet

Xib not Filling Screen

這陣子練寫iOS,呈現ViewController時都是用push的方式把頁面推出來,今天改用present的方式然出現畫面無法全螢幕的窘境@@
其實跟ViewController或iOS SDK版本都無關!!!
真正的問題如下圖紅框處
解決方式如下圖綠框處,改為Use Asset Caalog即可
此時,再回頭去檢查Images.xcassets,就會看到雖然我們沒有真的去做icon圖片給app,但是已經產生所缺少的空白圖片,上述問題也就解決了
為什麼會這樣呢?引述參考來源的文字如下
The problem is that you don't have a launch image for the 4-inch screen, so the iPhone 5s is treating this app as a 3.5-inch app (iPhone 4) and letterboxing it.

參考資料

如何在iOS的ActionSheet加上圖片

本文紀錄新舊版中,Action要帶圖片的作法
1. 舊版UIActionSheet
2. 新版(iOS 8.0) UIAlertController

1. 舊版UIActionSheet
UIActionSheet *actionSheet = [[UIActionSheet alloc]
    initWithTitle:@"我是8.0以下的Action(UIActionSheet)"
    delegate:self
    cancelButtonTitle:NSLocalizedString(@"取消",nil)
    destructiveButtonTitle:nil
    otherButtonTitles:
                              NSLocalizedString(@"選項一",nil),
                              NSLocalizedString(@"選項二",nil),
                              nil];
     
 // 讓第1個action帶圖片
[[[actionSheet valueForKey:@"_buttons"] objectAtIndex:0] setImage:[UIImage imageNamed:@"yourimagename.png"] forState:UIControlStateNormal];
     
actionSheet.actionSheetStyle = UIActionSheetStyleDefault;
[actionSheet showInView:self.view];


2. 新版(iOS 8.0) UIAlertController
UIAlertController * actionSheetNew = [UIAlertController
    alertControllerWithTitle:@"我是8.0以上的Action(UIAlertAction)"
    message:nil
    preferredStyle:UIAlertControllerStyleActionSheet];

// 定義第1個action
UIAlertAction* action1 = [UIAlertAction
    actionWithTitle:@"選項一"
    style:UIAlertActionStyleDefault
    handler:^(UIAlertAction * action)
    {
     //Do some thing here
     [actionSheetNew dismissViewControllerAnimated:YES completion:nil];
     NSLog(@"選項一..");
    }];
 // 讓第1個action帶圖片
[chat setValue:[UIImage imageNamed:@"yourimagename"] forKey:@"image"];

// 定義第2個action
UIAlertAction* action2 = [UIAlertAction
    actionWithTitle:@"選項二"
    style:UIAlertActionStyleDefault
    handler:^(UIAlertAction * action)
    {
     //Do some thing here
     [actionSheetNew dismissViewControllerAnimated:YES completion:nil];
     NSLog(@"選項二..");
    }];

// 定義第3個action
UIAlertAction* action3 = [UIAlertAction
    actionWithTitle:@"取消"
    style:UIAlertActionStyleDefault
    handler:^(UIAlertAction * action)
    {
     //Do some thing here
     [actionSheetNew dismissViewControllerAnimated:YES completion:nil];
     NSLog(@"取消..");
    }];

[actionSheetNew addAction:action1];
[actionSheetNew addAction:action2];
[actionSheetNew addAction:action3];
[self presentViewController:actionSheetNew animated:YES completion:nil];


參考資料
Adding Images to UIActionSheet buttons as in UIDocumentInteractionController
UIActionSheet is deprecated in iOS 8
除了添加圖片,也可以嘗試修改文字顏色,請參考UIAlertController custom font, size, color

UITableView無資料時該如何呈現

Android的ListView都是用setEmptyView,然後去設定塞進EmptyView的TextView,例如顏色、字體大小等。
且xml檔都要記得放android:id="@+id/empty"的TextView

iOS的做法如下
作法1:使用UITableView自身來呈現
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (self.myDataItems.count == 0) // 資料為空, 只需1個cell呈現"無資料"資訊, 故個數是1
        return 1;
    else
        return self.myDataItems.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    if (self.myDataItems.count == 0) {
        UITableViewCell *cell = [UITableViewCell new];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
        cell.textLabel.textAlignment = NSTextAlignmentCenter;
        cell.textLabel.textColor = [UIColor lightGrayColor];
        cell.textLabel.text = @"沒有資料";
        return cell;
    }

    // 若資料不為空, 就自行實踐cell的呈現
    static NSString *CellIdentifier = @"MyCustomCellIdentifier";
    MyCustomCell *cell = (MyCustomCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    // 略...
 
    return cell;
}
作法2:使用UILable客製化呈現
作法1因為用cell呈現,樣式有點難隨心所欲,例如想把"沒有資料"放在畫面正中央,這時候自行new一個UILabel會是比較快的選擇
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
      // 不需要額外撰寫 if (self.myDataItems.count == 0)要怎麼處理
     return self.myDataItems.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 不需要額外撰寫 if (self.myDataItems.count == 0)要怎麼處理, 本method只要專心處理有資料的cell要怎麼呈現即可

    // 若資料不為空, 就自行實踐cell的呈現
    static NSString *CellIdentifier = @"MyCustomCellIdentifier";
    MyCustomCell *cell = (MyCustomCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    // 略...
 
    return cell;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    if([self.myDataItems count] == 0) {
        //create a lable size to fit the Table View
        UILabel *messageLbl = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width, tableView.bounds.size.height)];
        //set the message
        messageLbl.text = @"沒有資料";
        //center the text
        messageLbl.textAlignment = NSTextAlignmentCenter;
        //auto size the text
        [messageLbl sizeToFit];
        
        //set back to label view
        tableView.backgroundView = messageLbl;
        //no separator
        tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
        
        return 0;
    }
    return 1;
}
參考資料 UITableView: Display Message In Empty Table View

UINavigationController的一些設定值紀錄

●指定ViewController自己的Title, 當下層View有NavigationBar時, Back旁邊的文字就是Title
self.title = @"Main";


●本頁面自己是否隱藏NavigationBar, 要寫在本頁的viewWillAppear
self.navigationController.navigationBarHidden = true;
//[self.navigationController setNavigationBarHidden:true animated:true];


●指定背景色
float iOSversion = [[[ UIDevice currentDevice ] systemVersion ] floatValue ];
if(iOSversion <7)
    self.navigationController.navigationBar.barTintColor = [UIColor colorWithRed:0 green:0 blue:1.0 alpha:1.0];
else
    self.navigationController.navigationBar.tintColor = [UIColor colorWithRed:0 green:0 blue:1.0 alpha:1.0];

使用Storyboard打造UICollectionView

1. 建立一個不含xib的h&m檔案
OfficeViewController.h、OfficeViewController.m


2. 建立一個storyboard
OfficeStoryboard.storyboard
3. 從object library拉View Controller進去StoryBoard


4. 承上選擇View,並從object library拉Collection View Controller進去,接著修改Collection View的Background為White Color


5. 選擇Collection View Cell,將Size由Default改為Custom並設定高度與寬度,接著設定Idenrifier為"officeCell"(名稱可自行決定)



6. 依據需求拉一些需要的控件至Cell裡面,本範例放了UIImageView和Label各一(且分別設定Attributes inspector的Tag為101與100)



7. 設定ViewController的CustomClass為第1步驟時新增的OfficeViewController



8. 選擇Collection View,設定outlets如下圖


9. 在h檔增加用來存放資料的NSMutableArray,並且宣告將實作的protocol
@interface OfficeViewController : UIViewController
@property (weak, nonatomic) IBOutlet UICollectionView *collectionView; //這是第8步驟用IBOutlet設定connection所產生的
@property (nonatomic,strong) NSMutableArray *officeItems;
@end


10. 實作m檔
實作方法如同前篇文章如何不使用Storyboard打造UICollectionView


11. 設定StoryBoard的'Storyboard ID'(網路上很多人會說Tag...)
本設定是為了達到這個效果:從起始頁面MainViewController跳到OfficeViewController,且畫面要使用剛設計的OfficeStoryboard.storyboards

設定完之後,在MainViewController.m加入下列程式,即可前往透過Storyboard打造的OfficeViewController
UIStoryboard *sbOffice = [UIStoryboard storyboardWithName:@"OfficeStoryboard" bundle:nil];
OfficeViewController *viewController = [sbOffice instantiateViewControllerWithIdentifier:@"OfficeSBIdentifier"];
[self.navigationController pushViewController:viewController animated:YES];


參考資料
UICollectionView Tutorial Part 1: Getting Started
What's the difference between instantiateInitialViewController and instantiateViewControllerWithIdentifier:?

如何不使用Storyboard打造UICollectionView

網路、工具書大多使用Storyboard打造,這篇記錄一下使用獨立xib檔案作CollectionCell,且完全不用Storyboard的方法~就像是UITableView

1. 建立呈現CollectionView的檔案
File → New → File → Source → Cocoa Touch Class → 建立一個繼承UIViewController的Class
建立完成應該要有.h、.m、.xib三個檔案


2. h檔
修改h檔如下
@interface MyViewController : UIViewController<UICollectionViewDelegate, UICollectionViewDataSource>
@end

並新增一個property等下用來放資料
@property (nonatomic,strong) NSMutableArray *dataItems;


3. xib檔
拖曳一個Collection View至步驟1的xib檔案,並設定layout屬性,例如背景色、大小。
接著用工具拖曳的方式Insert Outlet至h檔,並替這個IBOutlet取名為collectionView
h檔會因此自動產生如下連結
@property (nonatomic, strong) IBOutlet UICollectionView *collectionView;

然後最重要的...設定dataSource和delegate!!
如果不透過畫面設定,那就記得在m檔寫好
[self.collectionView setDataSource:self];
[self.collectionView setDelegate:self];


4. m檔
開始撰寫implement UICollectionViewDelegate, UICollectionViewDataSource必須要實作的method

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
 
    // 取資料
    if(self.dataItems == nil) {    // 初始化data array
        self.dataItems = [[NSMutableArray alloc] init];
    }
    [self getMyData]; // 自訂method, 用來向API取資料
 
    // 註冊給CollectionCell用的xib檔
    UINib *cellNib = [UINib nibWithNibName:@"SaasCell" bundle:nil];
    [self.collectionView registerNib:cellNib forCellWithReuseIdentifier:@"saasCell"];
}

# pragma mark-UICollectionViewDataSource
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {  
    return [self.dataItems count];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {  
    NSDictionary *saasDict = [self.dataItems  objectAtIndex:indexPath.row];
 
    static NSString *cellIdentifier = @"saasCell";
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
    
    NSURL *avatarURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@",[saasDict objectForKey:@"SaaSPhoto"]]];
    UIImageView *photoImage = (UIImageView*) [cell viewWithTag:101];
    [photoImage setImageWithURL:avatarURL placeholderImage:[UIImage imageNamed:@"img_user.png"]];    
    UILabel *titleLabel = (UILabel *)[cell viewWithTag:100];
    [titleLabel setText:[saasDict objectForKey:@"SolutionName"]];
    return cell;
}


5. 新增xib檔,主要用來設計CollectionCell
File → New → File → User Interface → View,xib的樣貌如下
這張圖也說明了第4點的黃色highlight為何要如撰寫


參考資料 A Simple UICollectionView Tutorial

change background of CollectionView

網路上很多人都在問為何Collection View背景色總是黑色,然後寫一堆程式去動態修改也沒用,其實是設定有遺漏。

以最簡單的xib來看,畫面除預設的View外,只有一個Collection View,觀察一下這二項的Background屬性,分別是這樣的
View: White
Collection View: Default→問題出在這,這邊也要改為White

Collection View從Object Library被拉進畫面時,Background預設值為Default,不像Table View預設是White,所以要記得改!

back to previous event

Objective-C(5.0+)
- (void)willMoveToParentViewController:(UIViewController *)parent;

Android
onBackPressed();

如果要在預設的back傳值或觸發其他事件,可override上述event

Back to previous view controller

Go to previous view controller
[self.navigationController popViewControllerAnimated:YES];

Go to root view controller
[self.navigationController popToRootViewControllerAnimated:YES];

Go to any view controller
[self.navigationController popToViewController:viewControllerObject animated:YES];

How to use Android Volley

Volley其實老在Google I/O 2013就被介紹了
但我問遍google,無論是自己包裝或使用第三方已封裝好的jar都是舊版做法,以下提供最新作法

環境&工具:Android Studio 1.1.0、jcenter


1. Download volley 
使用git command從google下載volley專案(請記得安裝Git)
前往Git的位置
cd C:\Program Files (x86)\Git\cmd
執行下載指令
git clone https://android.googlesource.com/platform/frameworks/volley


2. Make volley.aar
是的,你沒看錯,是.aar (Android Archive Library) !!!當然你要堅持用jar也是可以ˋ(′~‵")ˊ

目前,Android Studio似乎還無法直接做aar,我是隨意新增一個android專案,再把前一步驟下載回來的volley匯入並compile即可得到aar file~
假設專案根目錄在D:\AndroidStudioProject\MyApplication
aar產出位置就會是D:\AndroidStudioProject\MyApplication\volley\build\outputs\aar


3. Add volley.aar to your project
在"app"增加aar資料夾,複製步驟2製作的aar至此(就像以前用Eclipse複製jar到lib資料夾那樣)
此時,"app"底下的build.gradle檔案會自動替你補上相依性描述
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile(name: 'volley', ext: 'aar')
}
或是透過Project Structure來看


接著修改root directory下的build.gradle,加入粉紅色highlight處(因為剛剛把檔案放在aar這個自訂資料夾)
allprojects {
    repositories {
        jcenter()
        flatDir {
            dirs 'aar' //代表有個資料夾名稱叫做aar
        }
    }
}


4. Have fun to use volley
ya~開始在app專案使用
例如
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;

public class MainActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RequestQueue mQueue = Volley.newRequestQueue(this);
    }

    //略...
}

進階用法請參考

跟第3點有關的參考資訊

因環境與工具的相異浪費了一些時間@@  希望這篇能幫助近期有需要的人