2015年7月6日 星期一

iOS的多線程設計,NSThread與NSRupLoop的應用

最近工作需要多線程技術,我也趁機研究了網路上許多關於iOS的相關教學,我把學到的概念和查到的資源放在這和大家分享。
  1. Grand Central Dispatch In-Depth,介紹GCD和thread的基本概念,這篇文章出自於Raywenderlich,除了GCD API的使用以外也介紹了何謂thread,對我這個半路出家的開發者來說很有幫助。
  2. How To Use NSOperations and NSOperationQueues,這篇文章同樣出自Raywenderlich,這篇文章帶著讀者一步步寫出客製化main函式。
  3. Run Loops in Thread Programming Guide of Apple,這篇是Apple官方的教學文件,裡面講到如何讓thread接收外來的資訊進行相應的動作。
這篇文章主要在談NSThread,如上面所列除了NSThread以外,Apple提供了其他的API,操作起來比NSThread簡單,但是我遇到的問題是:

  • 我希望可以控制Thread在使用者做某些動作的當下馬上開始或結束。
因此,我不能使用GCD(無法在任務執行到一半時結束動作)和NSOperation(無法保證動作會馬上開始執行,因為iOS會視系統狀況調整目前的concurrent thread數量,在一般情況下是比較安全的選擇)我的做法直接開啟NSThread,同時我會接收系統memory warning的訊息,當系統提示記憶體不足,我會在GUI提示使用者,並且關閉最近開啟的NSThread。



我把所學的概念寫成Sample Code也有Swift版本),接下來,我將會講解Sample Code的內容,還有Swift和Objc-C兩種語言在一些小地方的差異。

首先,進入App之後會看到以下的畫面,有兩個Label分別是$counter1, $counter2,按下Thread1之後$counter1就會開始出現遞增的數字,按下Thread2之後$counter2也會出現遞增的數字。這兩個Label遞增的動作分別是在兩個Thread中完成,等等會有進一步的說明。



按下中間的Thread_Safety會出現下面的Log,我會用兩個Thread對同一個Array塞資料,一個Thread在Array尾端加入10,另外一個在Array的開端加入0。(Obj-C版本的sample code,只有在Thread1Thread2都開啟的狀態下按下Thread_Safety才能正常動作)






接下來我將會用說明在sample code中,我做了哪些事情:
  • 建立一個NSThread與如何結束一個NSThread
  • 讓一個NSThread持續等待外界呼叫在其內部執行方法(Obj-C only)
  • Thread Safety,如何處理兩個Thread爭搶資源的方法


建立一個NSThread

請打開ViewController.m或是ViewController.swift檔,viewDidLoad的方法內我初始化了兩個NSThread。
- (void)viewDidLoad {
    [super viewDidLoad];
    /*
     Setup _thread1 and _thread2 with their enter method
    */
    _thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(thread1Routine) object:nil];
    _thread2 = [[NSThread alloc]initWithTarget:self selector:@selector(thread2Routine) object:nil];
    
    _arr = [NSMutableArray new];
}
Objective C
override func viewDidLoad() {
        super.viewDidLoad()
        self.thread1 = NSThread(target: self, selector: "thread1Routine", object: nil);
        self.thread2 = NSThread(target: self, selector: "thread2Routine", object: nil);
       
    }
Swift

上面兩段程式碼分別是Objective-C與Swift的sample code的片段,我在viewDidLoad方法裡面初始化兩個NSThread,並且指定好target和thread進入點所要執行的方法與參數。在這邊target的就是告訴系統,等等在thread中如果出現"self",這個"self"是誰。

接著要是定義thread進入點所要執行的方法,這個概念有點類似C的程式進入點,這個thread在創造之後會執行這個方法,這個進入點方法內所呼叫到的方法會在這個thread執行,以下就用thread1的進入點方法為例。

-(void)thread1Routine{
    //Create a autoReleasePool for _thread1
    @autoreleasepool {
        //Hold the pointer of current thread, which means _thread1
        NSThread * curThread = [NSThread currentThread];
        //Hold the pointer of current runloop
        NSRunLoop * curRunloop = [NSRunLoop currentRunLoop];
        
        int counter = 0;
        //Run the loop while current thread is not cancelled
        while (curThread.isCancelled == NO) {
            counter += 1;
            
            //Back to main thread to update GUI
            dispatch_async(dispatch_get_main_queue(), ^{
                _counter1Lb.text = [NSString stringWithFormat:@"%d",(counter * 2) - 1];
            });
            
            //Force current thread to sleep 0.5 sec
            usleep(500000);
            
            //Wait input source
            if ([curRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
                
            }
        }
        [NSThread exit];
    }
}

Objective C
func thread1Routine(){
        autoreleasepool{
            let curThread = NSThread.currentThread()
            let curRunLoop = NSRunLoop.currentRunLoop()
            var counter = 0
            
            while curThread.cancelled == false {
                counter += 1
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    
                    self.counter1Lb.text = "\(counter * 2)"
                });
                
                usleep(500000);
               
            }
            
            NSThread.exit()
        }
    }

Swift

由於我現在自行創造了自己的thread,所以要幫它做個autoreleasepool(請參考這裡),這樣幫助thread裡面的記憶體更有效率回收。

我利用NSThread的類別方法currentThread來抓住currentThread的指標,在這個例子裡我抓住的就是thread1。

利用NSRunLoop的類別方法,我抓住了這個thread中的runloop指標。

接著我讓thread跑一個while迴圈,在thread結束之前,它會改變counter1Lb的內容,然後休眠0.5秒。(注意!所有UI的更新都要切回main thread,否則你會無法預期UI什麼時候會被更新!)

當while迴圈結束時,結束這個thread。


NSThread等待外界Input

也許你已經察覺到Obj-C的程式碼和Swift的程式碼有些不同,Obj-C的程式碼中有這麼一段:

//Wait input source
            if ([curRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
                
            }
這段程式之所以被放在while迴圈中是因為我希望在runloop在這個迴圈中等待外界的input,當thread收到外界的input時runMode:beforeDate:將會回傳YES,此時runloop會run一次然後回到這一行並且block住直到我剛剛給的時間點或是它再次接收到外界的input,但我給的時間點是極遙遠的未來,所以基本上它會等到外界再次input。
(未完待續...)

2015年3月4日 星期三

Storyboard使用技巧:分割Storyboard與AutoLayout (Split Storyboard and AutoLayout with Ratio)

這篇網誌可能是目前為止打的code最少的網誌,因為這次想分享的是storyboard使用的小技巧。之前在使用它的時候總覺得不好用,現在發現其實是我自己不會用。如標題所寫,這篇網誌有兩大主題:

  1. 分割Storyboard:在大一點的專案,有時需要多人開發,或是自己開發的時候專案越長越大,這時候就需要使用這樣的技巧,把Storyboard分開管理。
  2. 按照比例來AutoLayout:之前學習AutoLayout時沒有很用心,我只會用pin來設定間距,頂多用到對齊。現在才發現原來AutoLayout可以設定View和SuperView之間的比例關係。
在開始前各位可以先下載我準備的sample project,用模擬器打開來跑看看。

分割Storyboard

首先請先打開Main.storyboard,你應該會看到像下圖的畫面。

假設今天你的專案分成三大功能區塊(我用顏色來代表不同的功能區塊),分別用TabBar來切換,這三個功能區塊又分別使用三個NavigationController來管理。為了避免storyboard日後越長越肥大,又或者為了將程式分派給三個不同的工程師開發,這時候就需要分出三個storyboard。


專案視窗的左手邊可以看到我另外還有Blue、Red、Green三個storyboard,基本上跳轉的方式都一樣,我就用GreenVC來舉例,請看到GreenVC.m檔裡面的IBAction。
- (IBAction)change:(id)sender {
    //在main bundle中找到檔名為Green的storyboard並實體化
    UIStoryboard * green = [UIStoryboard storyboardWithName:@"Green" bundle:nil];
    //在剛剛實體化的storyboard中找到storyboardID為"green1"的ViewController並實體化
    UIViewController * vc = [green instantiateViewControllerWithIdentifier:@"green1"];
    //利用自己所屬的NavigationController進行push的動作
    [self.navigationController pushViewController:vc animated:YES];
}
這段程式碼做的工作就如我的註解一樣,值得一提的是我另外分出來的storyboard沒有NavigationController,而Run App的時後會發現NavigationController還能跨storyboard幫忙管理這些ViewController(真開心!)

依照比例AutoLayout
我用Green.storyboard的最右手邊的ViewController來舉例。在App執行時手機轉向它的畫面會像下圖




白色View的原點X座標為SuperView寬度的1/10,原點Y座標為NavigationBar寬度的兩倍。如果用我以前熟悉的pin來訂距離只能設定常數,無法依照比例作出調整。作法如下面的步驟:



在storyboard左手邊的Menu裡面點選白色的View,按住右鍵(或是control+左鍵)把藍線連到它的SuperView上。




放手後選擇Leading Space to Container Margin



接著在storyboard左側的Menu可以看到我們剛剛新增的限制條件,請點選它。



在視窗右側的工具列可以看到一些欄位,這些欄位可以幫助我們設定比例條件。先解釋一下名詞:

First Item欄位裡面的View.Leading表示我們要設定的View的最左側X座標。

Relation欄位裡面的Equal表示等於,當然你可以選大於或小於,在這個sample裡面我們選等於。

Second Item欄位裡面的SuperView.Tailing Margin表示SuperView最右邊的X座標。

下面還有三個欄位,整個條件可以用這六個欄位表達:

First Item =(這是我們剛剛選的relation) Second Item x multiplie + constant。

換言之:
白色View的原點X座標 = SuperView的寬度 x 1/10 + 0

而Priority表示這個條件的優先順位,在本篇中不會操作到。

接著我們設定Y座標:



同樣的拉線方式,這次選擇Top Space to Top Layout Guide。



看出來我設的條件是什麼嗎?

白色View原點Y座標 = SuperView上方列(此時是NavigationBar)的下緣Y座標 x 2 + 0

因為我的App有使用NavigationController所以這時候Top Layout Guid就是NavigationBar,否則它會是大家熟悉的電量與手機訊號顯示工具列。

最後再補上一個條件,這次拉線的目標是自己(白色的View拉給白色的View)


條件選擇Aspect Ratio就大功告成囉!




如果想要練習的話,我建議自己開一個Single View的project來從頭到尾自己做一次。這些小技巧不難,但是可以省下很多寫code的時間喔!

2015年2月14日 星期六

iOS程式設計模式(iOS Design Pattern)

公司的前輩推薦我一篇在RAYWENDERLICH發表的iOS Design Pattern教學,正好我維護的專案也是這位前輩用同樣的design pattern寫出來的,所以我花了一些時間研究了一下,我發現這真是不可多得的好教學文章,正好補足了之前學習的盲點,所以我徵求了作者和網站主人的同意翻譯這篇文章。(該網站規定翻譯文章以十篇為上限,所以我這次用掉了一篇的額度 XD)原文連結:http://www.raywenderlich.com/46988/ios-design-patterns

言歸正傳,以下是我翻譯的內容,如果有翻譯錯誤的地方,請各位指正:

"iOS Design Pattern"一個你可能聽過但卻不完全瞭解的字眼,儘管許多的開發者都認為design pattern非常的重要,但是相關的文章卻非常少,而且開發者有時會忽略它。

Design Pattern使指在軟體設計常見的問題中可重複利用的模式,也可以說是一種設計範本,協助你創做容易理解和重複使用的程式碼,同時也能增加程式碼的彈性,有利於往後維護與修改。

若你不熟悉所謂的design pattern,我必須告訴你一些好消息!第一,拜Cocoa原始的設計所賜,你在無意中已經用了一堆的design pattern了!第二,跟隨這份教學的腳步,你將快速上手iOS主要的幾個design pattern。

這邊文章將會分成幾個段落,每個段落講述一個pattern並且說明:


  • What    這個pattern是什麼
  • Why     為什麼要使用這個pattern
  • How     如何使用,在什麼情況下用,在使用時常見的陷阱

在過程中,你會寫出一個專輯瀏覽App,它將能展示你收藏的專輯與專輯相關的資訊,同時你將會熟悉以下幾種常見的設計模式:
  • 創造類(Creational)Singleton and Abstract Factory
  • 結構類(Structural)    :MVC, Decorator, Adapter, Facade and Composite
  • 行為類(Behavioral)Observer, Memento, Chain of Responsibility and Command
千萬別以為這是一篇關於設計的理論,上述的設計模式都會在你將創作的專輯瀏覽app中使用,完成後你的App將會看起來像這樣:


開始動手

下載starter project,解壓縮後用Xcode打開BlueLibray.xcodeproj。裡面並沒有很多東西,只有一個原生的ViewController和一個未實做的simple Http Client類別。
Note:
你知道當你創造一個全新的Xcode project時,你的程式碼就已經充斥著design pattern了嗎?MVC, Protocol, Singleton - 全都免費奉送喔!!
編注:
下載的檔案基本上是一個SingleView Project加上一個simple Http Client 類別,有興趣的人可以打開這個類別的.m檔看看,基本上裡面只實做了一個方法,之後我們會用這個方法從網路下載圖片。

在你開始鑽研第一個design pattern之前,你需要創建兩個類別來儲存和呈現專輯的資料。
點選"File/New/File..."(或是使用快捷鍵Command+N),選擇iOS > Cocoa Touch Class > Next。設定Class name為Album繼承自NSObjct,按下Next創造類別。

打開Album.h把以下的屬性和方法加到@interface與@end之間。


@property (nonatomic,copy,readonly)NSString *title, *artist, *genre, *coverUrl, *year;
-(id)initWithTitle:(NSString*)title artist:(NSString*)artist coverUrl:(NSString*)coverUrl year:(NSString*)year;

請注意,所有的屬性都是唯讀的,因為我們在創造物件時就把這些物件的資料填入,而且之後也沒有必要修改。這個方法是物件的建構子,當你創造一個新的Album物件時,你會填入專輯名稱,歌手,專輯封面的URL和年份。

現在,請打開Album.m我們來完成剛剛的方法,請把以下的程式碼加到@implementation和@end之間。

-(id)initWithTitle:(NSString *)title artist:(NSString *)artist coverUrl:(NSString *)coverUrl year:(NSString *)year{
    //1
    self = [super init];
    //2
    if (self) {
        _title = title;
        _artist = artist;
        _coverUrl = coverUrl;
        _year = year;
        _genre = @"Pop";
    }
    //3
    return self;
}

這邊沒什麼特殊的地方,只是一個簡單的init方法將Album物件實體化。

編注:
以上是一個很基本的init方法,之後會很常見到,我先解釋這些程式碼在幹什麼:
1: 呼叫父類別的init方法,把一個基本的NSObject(我們繼承的類別)做出來指派給self
2: 當成功實體化後,把這個類別在生成時要做的所有特殊的事情(父類別沒做的事情)在這做完,在這邊我們把Album特有的屬性塞進傳入的值。
3: 所有事情都完成了,把指標送出去供以後使用。

依照之前的方式,新增一個類別AlbumView,不過這次要繼承UIView!

Note:
如果你覺得快捷鍵好用,可以參考一下:
Command+N會新增一個檔案, Command+Option+N會新增一個group, Command+B會build你的project, Command+R會run你的project。

打開AlbumView.h,把以下程式碼加到@interface與@end之間。

-(instancetype)initWithFrame:(CGRect)frame albumCover:(NSString*)albumCover;
接著請打開AlbumView.m把@implementation之後的程式碼改成以下的程式碼:
@implementation AlbumView
{
    UIImageView * coverImage;
    UIActivityIndicatorView * indicator;
}

-(instancetype)initWithFrame:(CGRect)frame albumCover:(NSString *)albumCover{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor blackColor];
        //the coverImage has a 5 pixels margin from its frame
        coverImage = [[UIImageView alloc]initWithFrame:CGRectMake(5, 5, frame.size.width-10, frame.size.height-10)];
        [self addSubview:coverImage];
        
        indicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
        indicator.center = self.center;
        [indicator startAnimating];
        [self addSubview:indicator];
    }
    return self;
}

@end
你應該會注意到有一個物件變數叫做coverImage,你將用它來呈現專輯的封面圖片。第二個變數是一個indicator,它會在我們download專輯封面時旋轉。

在實做建構子時,你把背景設定為黑色,創造了一個image view距離邊界5 pixle並且加入了一個activity indicator。


Note: 
你會許會好奇為什麼我們把變數定義在implementation區段而不是在.h檔的interface區段,這是因為別的物件不需要知道這幾個變數的存在,它們只會用在implementation區段的方法中。當你在寫library或是framework給其他開法者使用時,這麼做是非常重要的。

Build你的project(command+B)確認到目前為止的程式碼都沒有錯誤。如果都對,準備好迎接你用到的第一個design pattern :)

MVC - Design Pattern之王

Model View Controller(MVC)無疑的是Cocoa中最常被使用的design pattern。它將你的app分成三種角色,並有助於將程式碼乾淨的切割開來。

三種角色分別是:


  • Model: 這類的物件負責存放和定義如何處理你的app中用到的資料。例如:我們剛剛做的Album類別。



  • View:這類的物件負責呈現Model和與使用者互動,基本上UIView和其子類別都算是View。例如:我們剛剛做的AlbumView類別。



  • Controllercontroller負責協調所有運作,它會從model中存取資料在view上顯現,偵聽事件,如果需要的話也會編輯資料。猜猜看你的專案中哪個類別屬於controller的角色?沒錯,就是ViewController。


理想的狀態下,在你的app中所有的物件都會屬於三種角色的其中一種。

Veiw和Model之間的溝通必須透過Controller,如下圖:



Model會通知Controller資料改變,然後Controller更新View的資料。View會通知Controller使用者觸發了什麼事件,Controller會更新Model或是做出任何相應的處置。

你或許會好奇,為什麼不能把所有的東西都寫在Controller裡面呢?這樣看起來不是單純許多嗎?

之所以這麼做,完全是為了把程式碼切開使之容易閱讀 、維護與重複使用。理想情況下,所有的View都必須完全和Model分開,如此一來我們可以重複使用同一個View去呈現不同的Model。

例如,在未來你想要為你的app增加電影與書籍的資料,你可以使用AlbumView來呈現電影與書籍的圖片。甚或,你想要寫另外一個app處理音樂專輯相關的事物,你也可以重複使用Album這個類別,畢竟他可以為任何的View提供資料。這就是MVC的強大之處!

如何使用MVC Pattern

首先,你必須先確認你project中的所有類別分屬於Model、View或Controller。專案進行到此,你創造了Album類別與AlbumView類別。

接著,在project中為這三種角色創造group來放置屬於他們的類別檔。

點選"File\New\Group(或使用快捷鍵command + Option + N)來建立新的group,並將三個group分別命名為Model、View與Controller 。

現在,將Album.h與Album.m放到Model group,把AlbumView.h和AlbumView.m放到View group,最後把ViewController.h和ViewController.m放到Controller group裡面。


完成之後,你的project看起來應該像這樣:



少了四散各處的檔案,你的專案目前看起來好多了。當然,你的專案中還是可以有其他的group和類別,不過整個app的核心都會放在這三個group中。

現在你的所有元件都被分門別類的安置好了,你需要從某處拿到專輯的資料,接著你將會創造一個API類別,用它在你的程式中管理所有的資料,這是個絕佳的機會讓我們來介紹你將學會的另一個design pattern——Singleton。

Singleton設計模式

Singleton確保該類別只有一個實體物件存在,通常會用一個lazy loading的方法去創造第一個實體物件。

Note: 
Apple也用了很多Singleton,例如:[NSUserDefaults standardUserDefaults], [UIApplication shareApplication], [UIScreen mainScreen], [NSFileManager defaultManager]全都會回傳一個Singleton的物件。

你可能會問,為什麼你需要確保只有一個實體存在,記憶體很便宜不是嗎?

這是因為有些特殊的情況下這麼做是非常合理的,例如:當你需要編輯Log檔時,你不會想要多個Logger實體,除非你想一次編輯多個Log檔。較理想的方法是用一個全域的類別來控制它,這樣我們可以避開多個實體物件同時存取一個Log檔的情況。

如何使用Singleton Pattern

請參考下圖:

上圖表示一個Logger類別有一個單一屬性(那就是他那獨一無二的實體),和兩個方法:shareInstanceinit

第一次呼叫shareInstance時,instance屬性尚未被初始化,所以你會創造一個物件實體指派給instance屬性。

之後你再次呼叫shareInstance時,他會檢查instance是否存在,若存在就直接回傳instance,不再做任何實體化的動作。

你將會創造一個Singleton的類別來管理所有專輯的資料。

你可能有發現project裡面有一個group叫做API,你將會把所有為app提供服務的類別放進去。在這個group裡面創造一個繼承NSObjcct的類別,命名為LibraryAPI
編注: 
這邊翻譯的有些繞口,其實API group裡面放的類別是協助你的app和外界溝通或存取資料的類別,像是寫入檔案、存取資料庫等,將這些動作放在一些類別內執行以免這些程式碼散落在project各處。

打開LibraryAPI.h把以下的程式碼填入:

@interface LibraryAPI : NSObject

+(LibraryAPI*)shareInstance;

@end

接著,打開LibrayAPI.m在@interface後插入以下程式碼:

+(LibraryAPI *)shareInstance{
    //1
    static LibraryAPI * _shareInstance = nil;
    
    //2
    static dispatch_once_t oncePredicate;
    
    //3
    dispatch_once(&oncePredicate, ^{
        _shareInstance = [[LibraryAPI alloc]init];
    });
    return _shareInstance;
}

這次我們遇到很多陌生的語法,以下將一一解釋:

  1. 宣告一個靜態變數來存放這個類別的實體(這就像上面圖示的+instance:Logger)
  2. 宣告一個靜態的dispatch_once_t類別的變數,我們之後會利用它來檢查實體化的方法有沒有被執行過。
  3. 利用Grand Central Dispatch(GCD)來執行一個block以實體化LibraryAPI。這是一個典型的Singleton design pattern,當實體建立後,建構子永遠不會被呼叫。

當你在次呼叫shareInstancedispatch_one的block將不會被執行,你將會直接收到之前實體化的物件指標。

Note: 
如果你對GCD的細節有興趣,你可以參考Multithreading and Grand Central DispatchHow To Use Blocks的教材。


編注:
你或許注意到我們使用dispatch_once_t類別的變數時沒有給他一個初值!這是因為它算是一個旗標,當我們執行dispatch_onece的block前,會判斷dispatch_once_t的記憶體位置是否有值,如果沒有值就會執行block並起塞一個值進去dispatch_once_t的記憶體位置。下次再次執行這個方法時,dispatch_once_t的記憶體位置就有值了,block就不會被執行,直接回傳_shareInstance。這也是我們為什麼要使用靜態變數的原因。

你現在有了一個Singleton的物件作為管理專輯資料的單一窗口,接著進一步創造一個類別來管理app資料。

在API group中創造一個繼承自NSObjct的類別,命名為PersistencyManager

打開PersistencyManager.h檔,在開頭的部份增加下面的code來import Album.h檔。
#import "Album.h"
接著,在@interface之後增加以下的程式碼:
-(NSArray*)getAlbums;
-(void)addAlbum:(Album*)album atIndex:(int)index;
-(void)deleteAlbumAtIndex:(int)index;
你宣告了三個方法來處理專輯資料

打開PersistencyManager.m@implementation之前增加以下程式碼:
@interface PersistencyManager ()
{
    //an array of all albums;
    NSMutableArray * albums;
}

@end
以上的程式碼增加一個extension,這是另一種增加私有方法、變數或屬性的方式。你在這裡宣告了一個NSMutableArray來存放專輯資料(裡面放的是Album類別的物件)

現在把下面的程式碼加到PersistencyManager.m@implemetation之後:
- (id)init
{
    self = [super init];
    if (self) {
     // a dummy list of albums
        albums = [NSMutableArray arrayWithArray:
                 @[[[Album alloc] initWithTitle:@"Best of Bowie" artist:@"David Bowie" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png" year:@"1992"],
                 [[Album alloc] initWithTitle:@"It's My Life" artist:@"No Doubt" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png" year:@"2003"],
                 [[Album alloc] initWithTitle:@"Nothing Like The Sun" artist:@"Sting" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png" year:@"1999"],
                 [[Album alloc] initWithTitle:@"Staring at the Sun" artist:@"U2" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png" year:@"2000"],
                 [[Album alloc] initWithTitle:@"American Pie" artist:@"Madonna" coverUrl:@"http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png" year:@"2000"]]];
    }
    return self;
}
init方法中,你將albums陣列創造出來並且填入了五個Album(還記得我們的Model嗎!)類別的資料。如果你不喜歡這五張專輯,你也可以換成你喜歡的專輯喔!

接著,把下面的方法加入PersistencyManager.m檔案中:
-(NSArray *)getAlbums{
    return albums;
}

-(void)addAlbum:(Album *)album atIndex:(int)index{
    if ([albums count]>=index) {
        [albums insertObject:albums atIndex:index];
    }else{
        [albums addObject:album];
    }
}

-(void)deleteAlbumAtIndex:(int)index{
    [albums removeObjectAtIndex:index];
}
這些方法可以讓你新增、刪除與查詢專輯的資料。

Build你的project確保目前為止沒有什麼問題。

進行到這邊,你也許會好奇,PersistencyManager類別並不是一個singleton的類別,那它和LibraryAPI類別又有什麼關係呢?我們又為什麼要疊床架屋把單純的NSMutableArray包在一個類別裡面,寫額外的方法存取它呢?答案將會在下一個段落揭曉,你將會學習到Facade設計模式。

Facade Pattern



Facade設計模式避免你將複雜的子系統曝露出來,取而代之的是簡單的單一窗口。

下圖解釋這個設計模式的概念:



API的使用者(在這個範例中你就是API的使用者,但如果是合作開發,使用者可能就是別的開發者囉!)完全不會察覺到底層有複雜的子系統。需要和大量其他類別互動時,Facade設計模式可以發揮強大的作用,也可用在把一些艱澀難懂的類別包裝起來,方便其他人使用。

Facade有助於將程式碼切割開,也能減少API使用者與底層系統的相依性。日後若需要維護程式碼,能有效地控制影響範圍。舉例來說:如果日後你需要更換後台的伺服器,你不需要更改使用API的程式碼,Facade有效地把更動的範圍縮減在這個API底層。

如何使用Facade Pattern

目前你已經有PersistencyManager儲存本機的專輯資料、HTTPClient處理遠端通訊。project中其他的類別不應該察覺到這些處理邏輯。

為了實現Facade設計模式,只有LibraryAPI需要有PersistencyManagerHTTPClient的實體物件。而LibraryAPI將會把其他類別不需要知道的處理邏輯隱藏起來,只曝露出簡單的API來存取這些物件。


Note:
通常singleton物件的存續時間應該和app一樣,所以你不該保留太多的strong指標在singleton物件內,因為到這個app結束前,這些指標都不會被release。

這個設計模式看起來像下圖:



如之前所說,只有LibraryAPI會被其他程式碼使用,HTTPClientPersistencyManager會被藏在LibraryAPI後頭。

打開LibraryAPI.h把下面的程式碼加到頂端:
#import "Album.h"
接著把下面的方法定義在LibraryAPI.h內:
-(NSArray *)getAlbums;
-(void)addAblum:(Album*)album atIndex:(int)index;
-(void)deleteAlbumAtIndex:(int)index;
這是你目前會暴露給其他類別使用的方法。

打開LibraryAPI.m增加以下程式碼:
#import "PersistencyManager.h"
#import "HTTPClient.h"
之所以把import指令放在.m檔,是因為你的API是底層複雜子系統的單一窗口。

現在增加一些私有的物件變數在.m檔extension中(加在@implementation之前):
@interface LibraryAPI ()
{
    PersistencyManager * persistencyManager;
    HTTPClient * httpClient;
    BOOL isOnline;
}

@end
isOnline判斷伺服器是否更新資料,像是新增或是刪除專輯。

你需要在init方法中初始化這些變數,把下面的程式碼增加到LibraryAPI.m檔中:
-(id)init{
    self = [super init];
    if (self) {
        persistencyManager = [[PersistencyManager alloc]init];
        httpClient = [[HTTPClient alloc]init];
        isOnline = NO;
    }
    return self;
}
在這個範例中httpClient不會真的和後端的伺服器溝通,它只會被用來展示Facade如何應用,所以isOnline將會被設定為NO
接著把下面的方法加入LibraryAPI.m檔中:

-(NSArray *)getAlbums{
    return [persistencyManager getAlbums];
}

-(void)addAblum:(Album *)album atIndex:(int)index{
    [persistencyManager addAlbum:album atIndex:index];
    if (isOnline) {
        [httpClient postRequest:@"/api/addAlbum" body:[album description]];
    }
}

-(void)deleteAlbumAtIndex:(int)index{
    [persistencyManager deleteAlbumAtIndex:index];
    if (isOnline) {
        [httpClient postRequest:@"/api/deleteAlbum" body:[@(index) description]];
    }
}
我們來看看addAlbum:atIndex:這個方法,LibraryAPI會先更新本機資料,如果有網路連線就會接著更新伺服器的資料。這就是Facade的強大之處!當其他的類別需要增加一個專輯時,它不會知道也不需要知道背後的複雜過程。


Note:
當使用Facade來包裝你的子系統時,請記得你無法預防使用者(其他開發者)存取你想包覆住的類別。不要吝嗇使用防禦型的設計方式,也不要假設使用者會依照Facade使用這些類別的方式使用它們。

Run你的app,你會看到一片漆黑,就像下圖一樣



你需要再花點功夫來把專輯資料顯示到螢幕上,剛好給了我們應用下一個design pattern——Decorator的機會。

Decorator Pattern

Decorator設計模式可以動態增加一個物件的行為而且不用修改類別的原始碼。除了繼承之外,你還可以用這種方式為一個類別增加額外的行為或是將兩個類別的行為合併。

在Objective-C裡面,有兩種常見的方法來達到Decorator的效果:CategoryDelegation

Category 

Category是個極度強大的機制,可以讓你在不用繼承的狀態下為一個類別增加方法。這個新的方法會在compile時被加入該類別,使用起來就像一般的方法。


Note:
Category除了可以擴充你自製的類別,你也可以用來擴充Cocoa內建的類別喔!

如何使用Category 

想像你要把Album類別的物件用TableView來顯示:



專輯的名稱要從哪來呢?Album是一個Model物件,所以它並不會管你怎麼呈現資料。在不修改Album類別的前提下(之前說過Model類別不能和View扯上關係!),你需要增加額外的方法來把資料輸出成你要的格式。

你將會擴充你的Album類別,增加一個新方法讓Album輸出的資料能夠輕易的用在UITableView上。


輸出的資料結構會像這樣:



點選File\New\Objective-C File...選擇Objective-c category,不要選成Objective-C Class!將category命名為TableRepresentation。


Note:
你有沒有注意到檔案名稱?Album+TableRepresentation代表你正擴張Album類別。這個慣例非常重要,因為它讓你還有其他開發者可以一眼看出這是個category。

打開Album+TableRepresentation.h檔宣告以下的方法:
-(NSDictionary*)tr_tableRepresentation;
請注意我們在方法的開頭增加tr_這是category名稱的縮寫。這個習慣可以幫助我們分辨category的方法和原本的方法(也可以避免命名衝突)。


Note:
當category內的方法名稱和原來類別內的方法名稱重複,或是和原類別(或是其父類別)的其他category的方法名稱重複,呼叫方法的行為會是undefined。當category中重複定義了Cocoa類別的方法時,這個機制能避免嚴重的問題發生。

打開Album+TableRepresentation.m檔並增加以下的方法:
-(NSDictionary *)tr_tableRepresentation{
    return @{@"titles":@[@"Artist"  ,@"Album"   ,@"Genre" ,@"Year"],
             @"values":@[self.artist,self.title,self.genre,self.year]};
}
現在來看看這個強大的pattern做了什麼:
  • 你在裡面用了Album的屬性。
  • 你在未使用繼承的狀態下就為Album類別增加了方法,當然如果使用繼承也能達到相同的效果。
  • 你沒修改Album類別的程式碼卻能傳回一個方便UITableView讀取的資料型態。

Apple用了大量的Category在Foundation類別中。你可以打開NSString.h檔,找到@interface NSString,你可以看到裡面包含了三個category:NSStringExtensionMethods, NSExtendedStringPropertyListParsingNSStringDeprecated。這些category讓這些方法可以被分門別類地擺到個個區段內。

Delegation 

另一個Decorator design pattern——Delegation,它可以讓一個物件支援另一個物件。例如,當你使用UITableView時,你必須要實作tableView:numberOfRowInSection:

你不能期望你的UITableView會知道你想在一個section裡面放多少個row,因此計算要放多少row的工作自然會落到UITableView dataSource上,這可以讓UITableView獨立於它將呈現的資料(又是另一個有助於你將Model與View切割的技巧!)。

下圖是當你創造一個UITableView時它和ViewController之間的互動:



UITableView物件會負責顯示一個表格清單,但它終究需要得到一些它未知的資訊(所以它會去詢問delegate,在這個案例中ViewController擁有這個delegate)。在Objective-C中實作delegate pattern時,需要delegate的類別會宣告一個protocol定義optional和required的方法,你將會在這個教學中學到如何製作自己的protocol。


Note:
這是個很重要的design pattern。Apple在大部分的UIKit類別中都有使用它,例如:UITableView, UITextView, UITextField, UIWebView, UIAlertView⋯⋯你會不斷的遇到它!

如何使用Delegate 

打開ViewController.m把匯入下列的類別:
#import "LibraryAPI.h"
#import "Album+TableRepresentation.h"
現在,增加一些物件變數:
@interface ViewController ()
{
    UITableView * dataTable;
    NSArray * allAlbums;
    NSDictionary * currentAlbumData;
    int currentAlbumIndex;
}
@end
接著把@interface那一行改成以下的程式碼:
@interface ViewController ()<UITableViewDataSource, UITableViewDelegate>
這邊宣告這個類別將會實作這兩個protocol定義的方法,這樣一來UITableView就能夠在這個類別中拿到它所需要的方法。

把viewDidLoad:的程式碼改成以下的樣子:
- (void)viewDidLoad
{
    [super viewDidLoad];
 //1
    self.view.backgroundColor = [UIColor colorWithRed:0.76f green:0.81f blue:0.87f alpha:1];
    currentAlbumIndex = 0;
    
    //2
    allAlbums = [[LibraryAPI shareInstance] getAlbums];
    
    //3
    //the uitableview that present the album data
    dataTable = [[UITableView alloc]initWithFrame:CGRectMake(0, 120, self.view.bounds.size.width, self.view.bounds.size.height-120) style:UITableViewStyleGrouped];
    dataTable.delegate = self;
    dataTable.dataSource = self;
    dataTable.backgroundView = nil;
    [self.view addSubview:dataTable];
}
以上的程式碼做了這些事:


  1. 背景顏色變為海軍藍。
  2. 從API取得所有專輯的資料,注意你不會在這接觸到PersistencyManager!
  3. 這段程式碼創造一個UITableView物件,把delegatedatasource指定給目前的view controller。
接著在VeiwController.m中新增以下方法:
-(void)showDataForAlbumAtIndex:(int)albumIndex{
    
    //防禦機制,確保albumIndex小於allAlbums的元素個數
    if (albumIndex < [allAlbums count]) {
        
        //取得專輯資料
        Album * album = allAlbums[albumIndex];
        
        //用字典的形式儲存album的資料
        currentAlbumData = [album tr_tableRepresentation];
    }else{
        currentAlbumData = nil;
    }
    
    //拿到了我們想要的資料後重新整理我們的tableView
    [dataTable reloadData];
}
showDataForAlbumAtIndex:從專輯陣列中提取我們需要的資料。當你想要刷新tableView的資料只要呼叫reloadData方法即可,它會讓UITableView重新呼叫它的delegatedatasource重新呈現tableView裡面的資料。

將下面這行程式碼加到viewDidLoad:的最後一行
[self showDataForAlbumAtIndex:currentAlbumIndex];
這會讀取app目前顯示的專輯資料,而我們剛剛把currentAlbumIndex設定為0,所以它會呈現專輯陣列裡的第一個元素。

Run你的project,如果沒有意外,你的app會當機,並且顯示以下的錯誤訊息:



發生了什麼事?因為你剛剛稱你的ViewController是一個UITableView的dataSource和delegate,但是你卻沒有實作它們所需的方法,例如:tableView:numberOfRowsInSection:方法。

將以下的程式碼加到ViewController.m中的@implementation和@end之間。
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return [currentAlbumData[@"titles"] count];
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (cell == nil) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"];
    }
    
    cell.textLabel.text = [currentAlbumData[@"titles"] objectAtIndex:indexPath.row];
    cell.detailTextLabel.text = [currentAlbumData[@"values"] objectAtIndex:indexPath.row];
    
    return cell;
}
tableView:numberOfRowsInSection:回傳tableView裡共有多少列。這邊我們回傳currentAlbumData的title數。

tableView:cellForRowAtIndexPath:將會創造並且回傳tableView中顯示的UITableViewCell物件。

Run你的app你將會看到以下的畫面出現在你的螢幕上:




回頭看看我們的第一張圖片,在這個tableView上面應該要有一個水平方向的scroller讓使用者可以選擇不同的專輯。與其專為這個app製作一個scroller,不如做一個可以重複使用的scroller!


為了讓這個scroller可以重複使用,我們必須讓別的物件為它提供顯示的內容——delegate,就像tableView的datasource一樣,我們的scroller必須宣告一些方法讓它的delegate去實作,為我們提供需要的資料。我們將會在下一個design pattern中討論並實作這個scroller元件。

Adapter Pattern

Adapter讓不同的類別可以一起作業,它把自己和別的物件綁在一起,並且提供其他物件一個標準的互動方式。

如果你熟悉Adapter Pattern你會發現Apple應用它的手法有一點點特殊,Apple用protocol來達到目的。也許你很熟悉這些protocol:UITableViewDelegate UIScrollViewDelegateNSCoding 和NSCopying。舉例來說,如果任何類別遵守NSCopy protocol,它就提供一個標準的copy方法。

如何使用Adapter Pattern

之前提到的水平scroller會像下面的圖片:



在你的View group裡面新增一個繼承自UIView的類別,命名為HorizontalScroller

打開HorizontalScroller.h檔,把下面的程式碼加到@end之後。
@protocol HorizontalScrollerDelegate <NSObject>
//在這宣告方法
@end
就像你之前宣告新的類別一樣,你剛剛宣告了一個繼承自NSObject protocol的protocol並且命名為HorizontalScrollerDelegate。讓自定義的protocol遵守NSObject protocol是一個很重要的動作,這可以讓自定義的protocol藉由NSObject protocol傳送資訊,之後我們將會讓你知道為什麼這很重要。

在@protocol和@end之間加入以下的程式碼,以定義delegate必須要實作的方法和選擇性實作的方法。
@required
//向delegate詢問它想要放多少個View在HorizontalScroller裡面
-(NSInteger)numberOfViewsForHorizontalScroller:(HorizontalScroller*)scroller;

//要求delegate回傳各個index要顯示的view
-(UIView*)horizontalScroller:(HorizontalScroller*)scroller viewAtIndex:(int)index;

//通知delegate哪個view被使用者點選
-(void)horizontalScroller:(HorizontalScroller*)scroller clickedViewAtIndex:(int)index;

@optional

//詢問delegate在一開始時要顯示哪個view,這是選擇性實作的方法,如果沒有實作就會顯示第0個view
-(NSInteger)initialViewIndexForHorizontalScroller:(HorizontalScroller*)scroller;
上面的程式碼宣告了delegate必須實作的方法與選擇性實作的方法,必須實作的方法通常包含這個類別所需要的資料,沒有這些資料這個類別就無法運作。對HorizontalScroller來說,知道要顯示幾個view和view的內容是什麼以及告訴delegate哪個view被按了都是必備的方法。選擇性實作的方法負責告訴HorizontalScroller一開始要顯示哪個view,如果沒有實作就顯示第一個view。

接著我們要為HorizontalScroller新增一個delegate屬性,但是我們的protocol在@interface之後宣告,HorizontalScroller這時候並不『知道』protocol的存在,為了解決這個問題,我們在@interface之前增加以下程式碼。
@protocol HorizontalScrollerDelegate;
在HorizontalScroller.h檔的@interface和@end之間加上下面的程式碼。
@property (weak) id<HorizontalScrollerDelegate>delegate;

-(void)reload;
請注意,在宣告delegate時必須要加入(weak)的描述,這是用來避免retain cycle造成memory leak。當一個類別持有一個strong的delegate指標,而它的delegate也持有這個類別的strong指標,你的app就會發生memory leak,因為這兩個類別都不會release彼此的指標,記憶體無法被回收。

id用來表示delegate只能被指派給已經宣告要實作HorizontalScrollerDelegate的類別。

reload這個方法就像UITableViewreloadData方法一般,它會將HorizontalScroller所需的資料重新載入。




(未完待續)

2015年2月1日 星期日

自製彈出選單Part I

和大家分享,如何製作類似UIAlertView的彈出式選單。相信開發者們都有用過apple內建的UIAlertView,apple為了某些原因,沒有開放一些屬性可對這個UI元件進一步客製化,例如:改變底色或為按鈕加入圖片等等。這篇文章就利用自製一個類似UIAlertView的元件來練練手,做出來的元件如下圖,下載連結在此!

在開始動手之前,必須先想想這個元件要怎麼包裝:要讓使用者(其他開發者)應用哪些屬性和方法,以此元件為例我想讓使用者能自由設定選項的(請看CTPopoutMenu.h):

  1. 選項的title和image
  2. tintColor與backgroundColor
  3. 字型和對齊方式
另外還有一個簡單的實體化方法

@class CTPopoutMenu;

@interface CTPopoutMenuItem : NSObject

@property (nonatomic,readonly) NSString * title;
@property (nonatomic,readonly) UIImage * image;
@property (nonatomic) UIColor * tintColor, * backgroundColor;
//default tintColor is whiteColor
//default backgroundColor is clearColor
@property (nonatomic) NSTextAlignment textAligment;
//default textAligment is NSTextAligmentCenter
@property (nonatomic) UIFont * font;
//defaut font is system font with size 14
-(instancetype)initWithTitle:(NSString*)title image:(UIImage*)image;
@end

看到這邊,也許有些人會有疑問,為什麼我在CTPopoutMenu class裡面寫了CTPopoutMenuItem的interface區段,又為什麼它是繼承NSObject類別呢?

這其實是一個常用的手法,把東西包起來,這樣使用者就不用import另外一個header檔就能使用這個類別。為什麼它繼承NSObject類別呢?不是UIView或是其他關於UI的類別?這典型的MVC設計方式,我等等在實作時就可以針對使用者的設定的"M"做出不同的"V",例如使用者沒有image則在itemView實體化時就不用放imageView。

接著就是我們的主角彈出視窗了!

@protocol CTPopoutMenuDelegate 

-(void)menu:(CTPopoutMenu*)menu willDismissWithSelectedItemAtIndex:(NSUInteger)index;
-(void)menuwillDismiss:(CTPopoutMenu *)menu ;

@end

@interface CTPopoutMenu : UIViewController

@property (nonatomic,readonly) NSString * titleText, * messageText;
//the title and message of the menu
@property (nonatomic)UIFont * titleFont, * messageFont;
//the font of title and message
@property (nonatomic)NSTextAlignment textAligment;
//default is NSTextAligmentCenter
@property (nonatomic,readonly) NSArray * items;
//the buttons of the menu
@property (nonatomic,readonly) UIView * menuView;
//the menuView of the PopoutMenu
@property (nonatomic) UIActivityIndicatorView * activityIndicator;
//ActivityIndicatorView of menuView default style is UIActivityIndicatorViewStyleWhite
@property (nonatomic) UIColor * backgroundColor, * highlightColor, *tintColor;
//backgroundColor of menuView, the default color is black with alpha 0.75
//highlightColor of items, the default color is white with alpha 0.5
@property (nonatomic) CGColorRef borderColor;
//borderColor of menuView, default color is white
@property (nonatomic) CGFloat blurLevel, borderRadius, borderWidth;
//blurRadius of backgroundView, default value is 3.5(0~4)
//borderRadius of menuView, default is 5
@property (nonatomic) PopoutMenuStyle menuStyle;
@property (nonatomic) iddelegate;

-(instancetype)initWithTitle:(NSString *)title message:(NSString *)message items:(NSArray *)items;
-(instancetype)initWithTitle:(NSString *)title message:(NSString *)message images:(NSArray *)images;
-(instancetype)initWithTitle:(NSString *)title message:(NSString *)message itemTitles:(NSArray *)itemTitles;

-(void)showMenuInParentViewController:(UIViewController*)parentVC withCenter:(CGPoint)center;
-(void)dismissMenu;

@end

首先介紹自定義的protocal: CTPopoutMenuDelegate
使用者可以藉由實作這個delegate接收到PopoutMenu即將dismiss的訊息,如果PopoutMenu是點到某個選項結束的,則會觸發:
-(void)menu:(CTPopoutMenu*)menu willDismissWithSelectedItemAtIndex:(NSUInteger)index;
使用者可以接收到選項的index藉此產生對應的動作,相反的,若是藉由觸碰PopoutMenu以外的區域使得dismiss的動作產生,則會呼叫:
-(void)menuwillDismiss:(CTPopoutMenu *)menu ;

interface的區段,各位應該可發現這個PopoutMenu是繼承UIViewController的元件,不單單只是一個UIView,這是因為我會為他加入模糊化的背景,並希望在使用時可以藉由點選選單外的區域dismiss選單。

接下來的屬性若沒有標示readonly則是使用者可以額外設定的屬性,包括:字型、對齊方式、背景色、主題色等等。標示了readyonly的屬性則是我不希望使用者額外修改的,像是在初始化方法中可以設定的標題文字、訊息文字、選項,另外整個MenuView我會在實作時自行layout所以我也不希望使用者可以存取他,所以我也選擇標上了readonly。

另外為這個選單提供三種初始化的方式,使用者可以選擇先做出放有CTPopoutMenuItem的NSArray來實體化,或是只提供選項的圖片 、標題來初始化選單,這三個方法:

-(instancetype)initWithTitle:(NSString *)title message:(NSString *)message items:(NSArray *)items;
-(instancetype)initWithTitle:(NSString *)title message:(NSString *)message images:(NSArray *)images;
-(instancetype)initWithTitle:(NSString *)title message:(NSString *)message itemTitles:(NSArray *)itemTitles;

初始化物件後,使用者可以呼叫
-(void)showMenuInParentViewController:(UIViewController*)parentVC withCenter:(CGPoint)center;
來彈出選單,也可以呼叫
-(void)dismissMenu;
自行結束選單。

以上為本次介紹的內容,詳細實作的方法留到後面再介紹,有任何的問題歡迎在留言中提出,又或者使用元件時發現bug也可以在留言中回報!

2015年1月25日 星期日

在iOS中按比例佈置UI (UI layout with ratio In iOS)


iOS和Android在UI佈局上最大的不同點就是iOS有座標,座標可以幫助開發者精確的定位,但是隨著apple產品的螢幕尺寸多樣化,座標就不再好用。像是這個網頁提到的,目前光是座標就有4種不同大小。

好在apple推出了size class和auto layout協助開發者處理座標上的差異,不過這些工具依然沒有辦法將所有的UI做到等比例縮放,因為他們的定位基本上還是建立在座標之上。要做到將UI在不同大小的螢幕上等比例縮放,甚至在旋轉後還能等比例縮放我目前知道的唯一辦法就是用程式寫。

之前我用程式寫,在程式中有許多計算比例的算式和CGRectMake...導致可讀性降低,比較好的處理方式是將計算位置的方法寫在方法裡面,我們可以用上次提到的Category,實作一個UIView Category加入計算的方法,或是直接寫個物件方法或是函示(C Function),解決的方法很多種,難度也不高。之所以寫在這個網誌是要提醒自己,不要在程式中遺留ㄧ堆計算位置的code,程式亂到我自己看都會怕。

開始看程式碼之前,先看看執行的效果,不管螢幕的尺寸改變或是旋轉螢幕,這些View相對於superView的長寬比例都不會改變。


如果用C寫的話可以參考以下的程式碼:
這個函示需要兩個參數:
第一個是superView,要計算UIView在superView中的相對位置需要superView的size。
第二的參數是一個CGRect,我用一個CGRect的結構來表達UIView.frame之於superView的比例。
#pragma mark Function

CGRect layoutInSuperViewwithRatios(UIView * superView, CGRect rectRatios){
    CGSize superViewSize = superView.bounds.size;
    CGFloat originX = rectRatios.origin.x*superViewSize.width;
    CGFloat originY = rectRatios.origin.y*superViewSize.height;
    CGFloat width = rectRatios.size.width * superViewSize.width;
    CGFloat height = rectRatios.size.height * superViewSize.height;

    return CGRectMake(originX, originY, width, height);
}
或者選擇用 Category可以在檔案的開頭寫下以下的程式碼:
計算的方式基本上和上面介紹的函示一模一樣。

#pragma mark Category

@implementation UIView (RelativeLayout)

+(CGRect)relativeLayoutinView:(UIView*)superView ratios:(CGRect)rectRatios{
    CGSize superViewSize = superView.bounds.size;
    CGFloat originX = rectRatios.origin.x*superViewSize.width;
    CGFloat originY = rectRatios.origin.y*superViewSize.height;
    CGFloat width = rectRatios.size.width * superViewSize.width;
    CGFloat height = rectRatios.size.height * superViewSize.height;
     CGRectMake(originX, originY, width, height);
    return CGRectMake(originX, originY, width, height);
}

@end
接下來就是ViewController部分的程式碼:
為了方便,我將這些UIView都宣告為物件變數,我在viewDidLoad方法中將這些UIView實體化,然後在viewWillLayoutSubviews方法中計算他們的位置和長寬。為了Demo前兩個View用了函示後兩個View用了類別方法,但基本上作用一模一樣,甚至你想把類別方法直接改成物件方法也行。

#pragma mark ViewController

@interface ViewController ()
{
    UIView * testView1, * testView2, * testView3, * testView4;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    testView1 = [[UIView alloc]init];
    testView1.backgroundColor = [UIColor darkGrayColor];
    [self.view addSubview:testView1];
    
    testView2 = [[UIView alloc]init];
    testView2.backgroundColor = [UIColor blueColor];
    [self.view addSubview:testView2];
    
    testView3 = [[UIView alloc]init];
    testView3.backgroundColor = [UIColor redColor];
    [self.view addSubview:testView3];
    
    testView4 = [[UIView alloc]init];
    testView4.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:testView4];

}

-(void)viewWillLayoutSubviews{
    [super viewWillLayoutSubviews];
    testView1.frame = layoutInSuperViewwithRatios(self.view,(CGRect){0.1,0.1,0.4,0.9});
    testView2.frame = layoutInSuperViewwithRatios(self.view,(CGRect){0.5,0.1,0.4,0.4});
    testView3.frame = [UIView relativeLayoutinView:self.view ratios:(CGRect){0.5,0.5,0.4,0.25}];
    testView4.frame = [UIView relativeLayoutinView:self.view ratios:(CGRect){0.5,0.75,0.4,0.25}];
}

@end

結論:計算UI位置的方式百百種,重要的是提高程式的可讀性,往後在維護時不會一頭霧水。
使用時機:UI設計師出的設計圖只給比例不給座標或是懶得用storyboard or Xib時。

2015年1月23日 星期五

Core Image Filter 以模糊影像為例

一般常見的UI特效之一就是模糊背景讓使用中的UI更明顯,像是下圖:
彈出選單未啟動時的畫面

彈出選單啟動,背景霧化
這樣的處理會讓UI看來更有質感。以往這類的效果會牽扯到GPU或CPU的運算,開發者必須使用OpenGL這類C++的函式庫來處理,在iOS5之後apple提供了簡單的用具來協助使用者處理這些細節。詳細的資料請閱讀(CoreImage的官方文件

apple提供了數十種不同的濾鏡效果供開發者選擇,詳細的濾鏡效果和參數請參閱(Core Image Filter的官方文件)廢話不多說,開始寫code吧:

首先,我建議各位把霧化效果的方法放在UIImage的Category區段裡面,這是為了程式的可讀性。所以我們先來做個Category區段:
@implementation UIImage (Blur_and_Color_Filter)
@end
這樣我們就可以用UIImage呼叫在Category區段的方法,剩下的工作就是選擇適合濾鏡和輸入參數了,apple提供的模糊濾鏡有很多種,有些是專屬OS X的,在這邊我們將使用高斯模糊"Gaussian Blur",其原理請參考這個網頁。在使用高斯模糊後,我們會產生一些問題:

  1. 影像會擴張,這是由於高斯模糊是把像素周圍的RGBA值取平均,所以本來圖片周圍不該有像素的地方也會被填入像素。
  2. 影像周圍的模糊效果較不明顯,這也是因為原本邊緣的像素被取平均後的後遺症,這的狀況會隨著模糊半徑越大越明顯。

為了解決問題我們在使用濾鏡的時候必須注意:

  1. 模糊後必須把圖片做適當的裁切,通常是input多大的圖,output的圖就裁剪成多大。
  2. 模糊半徑不宜設得太大,範例圖的模糊半徑是4。

接著我們把方法加入區段裡面吧:

@implementation UIImage (Blur_and_Color_Filter)
-(UIImage*)blurWithRadius:(CGFloat)radius{
    CIImage * inputImage = [CIImage imageWithCGImage:self.CGImage];
    CIFilter * blurFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
    [blurFilter setValue:inputImage forKey:@"inputImage"];
    [blurFilter setValue:[NSNumber numberWithFloat:radius] forKey:@"inputRadius"];
    CIImage * outputImage = [blurFilter outputImage];
    outputImage = [outputImage imageByCroppingToRect:[inputImage extent]];
    UIImage * blurImage = [UIImage imageWithCIImage:outputImage];
    return blurImage;
}
@end
完成這個區段後,我們在同一個project內就可以直接把UIImage用物件方法直接模糊化,輕鬆又方便。之後我打算介紹如何做一個自己的UIAlertView取代apple內建的AlertView(由UIViewController做起)。