Posts match “ iOS ” tag:

使用UIAlert, UIActionSheet 與Modal View的時機

UIAlert

給使用者非常重要資訊會影響使用者的後續操作,通常用於顯示非預期狀況,告訴使用者目前遇到的問題或改變目前狀態,需要使用者做決定。

ActionSheet

給使用者在目前當下正進行的動作予以額外的選擇,使用者學習去預期它的出現(ex.刪除所有的來電記錄),或是某個動作可以用不同的方式進行(ex .讓使用者在多個目的地中選擇其中一個)。

Modal View

可以視其圍子任務,必須完成當下這個子任務才能回到你原本的狀態下繼續進行(ex. 登入: 可以在需要權限時在開支線任務給User)

Reference
iphone - When to use a UIAlertView vs. UIActionSheet - Stack Overflow

XCode SVN 版本控管設定

UserInterfaceState.xcuserstate 檔案會一直更新,*.a靜態檔案也可以不用加到SVN中,因此可以修改該Repostory中的config, 並忽略上述檔案而不commit。

  1. 開啟config檔案: 開啟terminal輸入
    vi ~/.subversion/config
    
  2. 搜尋[miscellany]
    /miscellany
    
  3. global-ignores下一行新增

    global-ignores = *~ #*# .#* .*.swp .DS_Store .xcuserstate
    
  4. 存檔, 關閉

    :wq
    

這次專案是用SVN來做版本控管,下次如果用git就把它加入.gitignore就搞定囉。

Reference

[iOS x Design Pattern] ViewController之間傳遞參數 Delegate Pattern

在iOS中頁面的參數傳遞有幾種作法,最常碰到的是相鄰的兩個頁面傳遞,其中又以把參數往下一頁傳遞的狀況較多,比較少的情況是需要把參數傳遞給前一頁

題外話,一個月前我還什麼都不會的時候,跑去參加iOS Bootcamp 2013那時候真的搞不懂iOS的delegate,本來還以為跟匿名函式一樣:P 但是完全不是,許多用字也與過去的不同,像是interface在iOS中是用Protocol,總之,天下武功大逕相同,殊途同歸,只要了解語言特性去習慣它,勿故步自封便能無招勝有招。

言歸正傳

情境

ViewControllerA -> ViewControllerB

[傳遞給下一頁]

Passing Data Forward

ViewControllerB *viewControllerB = [[ViewControllerB alloc] initWithNib:@"ViewControllerB" bundle:nil];
viewControllerB.isSomethingEnabled = YES;
[self pushViewController:viewControllerB animated:YES];

Passing Data Forward using Segue's

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    if([segue.identifier isEqualToString:@"showDetailSegue"]){
        ViewControllerB *controller = (ViewControllerB *)segue.destinationViewController;
        controller.isSomethingEnabled = YES;
    }
}

tips:你不能直接把值設定給UI物件,因為在prepare segue階段該物件尚未產生實體,會導致你傳資料給一個不存在的物件。

[傳遞給前一頁]

透過Protocol限制delegate的物件

  1. 建立Protocol

ViewControllerB.h

@protocol ViewControllerBDelegate <NSObject>
- (void)addItemViewController:(ViewControllerB *)controller didFinishEnteringItem:(NSString *)item;
@end
@interface ViewControllerB : UIViewController
@property (nonatomic, weak) id <ViewControllerBDelegate> delegate;
@end

ViewControllerB.m

#import "ViewControllerA.h"
//When you need viewControllerA
- (void) setDataBack{
  [self.delegate didFinishEnteringItem:YES];
}

set delegate
加上介面ViewControllerBDelegate
ViewControllerA.h

@interface ViewControllerA : UIViewController <ViewControllerBDelegate>

ViewControllerA.m

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    if([segue.identifier isEqualToString:@"showDetailSegue"]){
        ViewControllerB *controller = (ViewControllerB *)segue.destinationViewController;
        controller.delegate = self;
    }
}

更簡單的方式,把ViewControllerA當成物件傳進來

ViewControllerB.h

@property (nonatomic, strong) id previousViewController;

ViewControllerB.m

#import "ViewControllerA.h"
//When you need viewControllerA
- (void) setDataBack{
  ViewControllerA *controllerA = (ViewControllerA)self.previousViewController;
  [controllerA setText:@"set somthing data"];
}

別忘了在前一頁的時候要把自己傳過去

ViewControllerA.m

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    if([segue.identifier isEqualToString:@"showDetailSegue"]){
        ViewControllerB *controller = (ViewControllerB *)segue.destinationViewController;
        controller.previousViewController = self;
    }
}

這樣也就不用寫什麼Protocol,就懶人來說是比較容易的方式XD

結論

比較後面這兩種方式,一種是正規的Delegate Pattern,一種是偷吃步懶人寫法,前者有較嚴謹的規範,你可以定義好前一頁該做什麼事情並且不用管前一頁是誰(即便是ViewControllerC),只要是符合Protocol的人都能當他的前一頁; 而後面偷吃步的作法,好處是可以偷懶不用寫Protocol,若你今天要回傳的資料很多,寫起來也是挺麻煩的,但如果你是跟別人合作,或是你的ViewControllerB是好幾頁的子頁面,就比較建議用正規寫法囉。

另外值得一提的是,如果你要傳遞的頁面距離該頁面有些距離,比如你要傳遞給你的曾祖父資訊,那就要動用到觀察者模式Observer pattern了,會在下一篇介紹給大家。

refernce:Passing Data between View Controllers

[iOS x Design Pattern] 觀察者模式 Observer Pattern ( I )

無論在任何程式語言中,觀察者模式都是使用率最高的一種設計模式,在此模式中會有一個主題(subject)物件讓其他物件可以訂閱,而其他的訂閱者就稱為觀察者(Observer),當主題有更新資料時,會主動通知所有的訂閱者,因此訂閱者不需要在費心處理資料有沒有更新這回事。

定義一對多的物件依存關係,讓物件狀態一有變動,就自動通知其他相依物件做該做的更新動作。--Design patterns : Elements of Reusable Object-Orientied Software

Wiki - Observer pattern

問題

在一般生活中也有顯而易見的觀察者模式,

你習慣看某個Blog,但這個作者很有個性,不是固定時間更新Blog,
有的時候一天發三篇文章,有的時候又一個月才發一篇。

你總不可能三天兩頭一直去check這個Blog,這樣太浪費時間了。

這時候你就會想,要是有個東西可以在這個Blog更新的時候通知我,那該有多好。

於是RSS就是這個問題的最佳解決方案,RSS協定讓User可以藉由RSS reader自動檢查這個Blog,並且一有更新就通知你。

iOS的觀察者模式

在iOS當中也有這樣的問題存在,於是官方實作了兩項有解決方案分別是NotificationKVO

[Notification]

iOS提供一個NotifivationCenter,你可以在任何一個地方去訂閱發送通知

  1. 訂閱:告訴NotificationCenter,我要訂閱

    [[NSNotificationCenter defaultCenter] addObserver: self
    selector: @selector(blogUpdated)
    name: @"部落格更新"
    object: nil];
    
  2. 定義Callbak:收到更新後我要做什麼事情

    - (void)blogUpdated{
        NSLog(@"部落格更新了,現在就去看");
    }
    
  3. 更新: 在任何一處都可以更新並通知User

    - (void)didWhiteNewBlog
    {
        [[NSNotificationCenter defaultCenter] postNotificationName:@"部落格更新" object:self];
    }
    

Notification的優點是,訂閱者不用知道主題(subject)的實體物件在哪裡,只管聆聽某一個name,有點像網路協定中,我們講好我們用某某port來做溝通,subject只管往那個port送資料,而訂閱者只管去那個port收資料。
而這個方法的缺點是,主題(subject)與觀察者(Observer)必須事先協議好溝通的key,如果兩端都是同一個人實作那問題還小,因為你自己定key,假如你沒有主題(subject)的主控權,又要如何訂閱呢? 這就要請到下一位KVO大大了。

[Key-Value-Observer (KVO)]

KVO的實作比較困難一點點,但帶來的好處就是被訂閱的主題(subject)完全無感,就好像你在監聽他一樣(不過完全合法),只要該物件的property修改,就會馬上通知你。

參考Apple官方的KVO Programming Guide,其中指出KVO常用於model and controller layer尤其是從controller去綁定(觀察)model

controller訂閱model

model <- controller

iOS把KVO實作在基礎類別NSObject,所以你可以任意訂閱任一物件,你就是老大不用擔心被告違法(監聽),接著告訴大家該怎麼做:

  1. 了解要訂閱的物件
//Subject.h
@interface Subject : NSObject
@property (strong, nonatomic) NSString *lastBlogPostDate;
@end

我們要訂閱subject物件中lastBlogsPostDate這個property,在他更新部落格文章時,接收第一手的資訊。

  1. 訂閱與接收通知
//Observer.m
#import "Subject.h"

//訂閱
- (void)subscribeSubject:(Subject *)subject
{
    /* 向subject物件增加一個觀察者(self),
  並且我要觀察'lastBlogPostDate'這個屬性,
  options: 當該值更新時把新舊值都傳給我(放在dictionary中)
  */
    [subject addObserver:self forKeyPath:@"lastBlogPostDate" 
  options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
    context:nil];
}

//接收通知
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if([keyPath isEqualToString:@"lastBlogPostDate"])
    {
        NSLog(@"%@",change);
    }
}

其中option的部份有下面幾項:

  • NSKeyValueObservingOptionNew : 通知時要告訴我新的值
  • NSKeyValueObservingOptionOld : 通知時要告訴我舊的值
  • NSKeyValueObservingOptionInitial : 訂閱時會先通知一次
  • NSKeyValueObservingOptionPrior : 變更數值前會先通知一次,變更數值後再通知一次(通知兩次)

KVO的優點是可以更改subject的情況下訂閱(也意味著被訂閱的subject完全不用理會通知observer這件事情),缺點是observer必須要知道subject的實體位置(在訂閱時需要該本體)。

KVO與notification各有好處,就看大家怎麼去用囉:)

後記

為了寫文章反而學到更多,真是不錯,缺點是每天都在熬夜阿XD

[iOS x Design Pattern] 觀察者模式 Observer Pattern ( II )

既上回介紹Notification與KVO後又發現了一個KVO的用法KVO Compliance

前言

事情是這樣的,在與Johnny分享KVO的pattern後,建議他可以用KVO觀察某個陣列的數量來被動更新畫面,這麼一來就可以不用每次都加一個物件就再多加一段updateView,當天晚上我就回家再實作了一次,發現竟然沒有接收到通知!於是我立馬展開調查(撞牆)

錯誤示範

@interface LYObserverViewController ()
@property (strong,nonatomic) NSMutableArray *dataSource;
@end

@implementation LYObserverViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.dataSource = [[NSMutableArray alloc] init];
    
    //觀察陣列dataSource的數量
    [self.dataSource addObserver:self forKeyPath:@"count"
                         options:NSKeyValueObservingOptionNew
                         context:@"dataSourceSizeChanged"];
    //陣列加入物件(應該會驅動observe)
    [self.dataSource addObject:@"first"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == @"dataSourceSizeChanged") {
        //印出結果
        NSLog(@"%@", change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

顯然這其中一定有什麼誤會,不然沒道理不會動。

Key-Value Observing

後來查了蠻久都找不到原因observe NSMutableArray count,最後參考官方文件Key-Value Observing Programming Guide,文中指出只要用Key-value coding compliant方式實作的物件都會自動發出通知,並且提供兩種手動機制來觸發通知:

1. Automatic Change Notification (自動通知)

從下面這段程式碼可以大約猜出,如果要Observe NSMutableArray的話要透過物件,並且在透過mutableArrayValueForKey取得的實體才會支援自動通知。

// Call the accessor method.
[account setName:@"Savings"];
 
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
 
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
 
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

2. Manual Change Notification (手動通知)

willChangeValueForKey:didChangeValueForKey給埋在Setter裡面,就可以當做發出通知的訊號,當然,你也可以故意做成setA然後Notify B,這邊的key你是可以自已定的。或者你自己用一個覆合式的通知,完全自定:)

- (void)setOpeningBalance:(double)theBalance {
    if (theBalance != _openingBalance) {
        [self willChangeValueForKey:@"openingBalance"];
        _openingBalance = theBalance;
        [self didChangeValueForKey:@"openingBalance"];
    }
}

解決方法

只要從mutableArrayValueForKey取的實體,在對它觀察就可以囉。

@implementation LYObserverViewController
- (void)viewDidLoad
{
    [super viewDidLoad];    
    self.dataSource = [[NSMutableArray alloc] init];

    //觀察陣列dataSource的數量
    [self addObserver:self forKeyPath:@"dataSource"
                         options:NSKeyValueObservingOptionNew
                         context:@"dataSourceSizeChanged"];
                         
    //從mutableArrayValueForKey取的實體
    //陣列加入物件(應該會驅動observe)
    NSMutableArray *array = [self mutableArrayValueForKey:@"dataSource"];
    [array addObject:@"first"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == @"dataSourceSizeChanged") {
        //印出結果
        NSLog(@"%@", change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

後記

其實一開始只是想學好iOS的MVC架構,並且將Observer Pattern運用在上面(iOS好像本來就該這麼做),卻意外學到了怎麼用手動觸發通知,也算是不錯的經驗:)

顯示物件詳細資訊

用 NSLog 把物件給印出來只會得到記憶體位置:

2013-11-21 15:27:06.323 GraceGift[19972:70b] <GGUser: 0x8e805c0>

像 java 的 toString 一樣,Obj-C 也有可以轉成字串顯示的 method

- (NSString *)description
{
    NSString *desc = @"format your information you want";
  return desc
}

- (NSString *)debugDescription
{
    NSString *desc = @"format your information you want";
  return desc
}

執行Shell Script打包.ipa檔案

最近學習用Jenkins把iOS開發整個串起來,奇怪的是Jenkins的XCode plugin一直沒辦法打包.ipa檔案,所以就改用執行Shell Script的方式來做。

目標是只用最少檔案產生.ipa檔案

  1. source code
  2. adhoc.mobileprovision
  3. adhoc.sh

原理

因為.mobileprovision檔案裡面有完整的描述,包含所有Build & Archive會用到的參數

  1. PROFILE_NAME
  2. UUID
  3. CODE_SIGN_IDENTITY

檔案配置

因為下列原因,所以把.mobileprovision與build script跟source code放在一起

1. Bundle id與帳號轉換

為了未來置換.mobileprovision檔案(從企業版帳號換到一般帳號)

2. 測試Build script功能

要先確定在local端執行script可以正確的Build出能安裝的.ipa檔案

遇到的問題

1. Build過但是不能安裝

之前有試著把CODE_SIGN_IDENTITY這個參數拿掉,我原本以為Xcodebuild會聰明的拿.mobileprovision內的資料自動去比對,因為他Build沒出錯但最後的.ipa卻不能安裝。

2. 從.mobileprovision抓取CODE_SIGN_IDENTITY

因為這個是一個base64的data,所以在抓取過程中遇到許多挫折,後來找到一個gist卻因為看不動複雜的linux指定而放棄,後來我試著改用application-identifier作為CODE_SIGN_IDENTITY竟然成功了!

剩下就是Build script了~

adhoc.sh

#!/bin/bash

# 使用者參數
PROJECT_NAME="jenkinsdemo"
PROFILE="adhoc.mobileprovision"

function NSLog(){
    clear
    MESSAGE=$1
    echo "$(tput setaf 220)\n ${MESSAGE} \n$(tput sgr 0)"
}


## 確認參數
NSLog "** CHECK PARAMETERS **"

# 沒給ProjectName則用FolderName
if [[(-z "$PROJECT_NAME")]]
then
    PROJECT_NAME=${PWD##*/}
fi

# 沒給ProvisioningProfile從檔案拿
if [[(-z "${PROFILE}")]]
then
    echo "\n$(tput setaf 1)  缺少ProvisioningProfile\n"
    exit 0
fi

## 系統參數
BUILD_PATH="JenkinsBuild"
IPA_FILE="${PROJECT_NAME}.ipa"
ARCHIVE_FILE="${PROJECT_NAME}.xcarchive"

# Error Log檔案
BUILD_ERROR_FILE=build_errors.log

ARCHIVE_PATH="./${BUILD_PATH}/${ARCHIVE_FILE}"
EXPORT_PATH="./${BUILD_PATH}/${IPA_FILE}"

# Provisioning Profile
PROFILE_DATA=$(security cms -D -i ${PROFILE})
PROVISIONING_PROFILE_NAME=$(/usr/libexec/PlistBuddy -c 'Print :Name' /dev/stdin <<< $PROFILE_DATA)
UUID=$(/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $PROFILE_DATA)
APP_ID_PREFIX=$(/usr/libexec/PlistBuddy -c 'Print :ApplicationIdentifierPrefix:0' /dev/stdin <<< $PROFILE_DATA)
CODE_SIGN_IDENTITY=$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:application-identifier' /dev/stdin <<< $PROFILE_DATA)

# Copy 來源的 Provisioning Profile 至 OS
cp -rf $PROFILE ~/Library/MobileDevice/Provisioning\ Profiles/$UUID.mobileprovision

echo "PROJECT_NAME: ${PROJECT_NAME}"
echo "PROVISIONING_PROFILE: ${PROFILE}"
echo "PROFILE_NAME: ${PROVISIONING_PROFILE_NAME}"
echo "UUID: ${UUID}"
echo "CODE_SIGN_IDENTITY: ${CODE_SIGN_IDENTITY}"
echo "APP_ID_PREFIX: ${APP_ID_PREFIX}"
echo ""


## 建置前清除 
NSLog "** CLEAN BEFORE BUILD **"

xcodebuild -alltargets clean
rm -rf ${BUILD_PATH}
rm $BUILD_ERROR_FILE
mkdir ${BUILD_PATH}

## 建置與封裝
NSLog "** BUILD & ARCHIVE **"
# 可換成xcodebuild
xcodebuild \
 -scheme $PROJECT_NAME \
 archive \
 PROVISIONING_PROFILE="$UUID" \
 CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" \
 -archivePath ${ARCHIVE_PATH} \
 CONFIGURATION_BUILD_DIR=${BUILD_PATH} \
 2>${BUILD_ERROR_FILE} \


## 檢查建置錯誤
NSLog "** CHECK BUILD ERROR ** "

errors=`grep -wc "The following build commands failed" ${BUILD_ERROR_FILE}`
if [ "$errors" != "0" ]
then
    echo "$(tput setaf 1)BUILD FAILED. Error Log:"
    cat ${BUILD_ERROR_FILE}
    echo "$(tput sgr 0)"
    exit 0
else
    rm $BUILD_ERROR_FILE
fi

# ... continue


## 輸出IPA檔案
NSLog "** EXPORT IPA FILE **"

xcodebuild \
 -exportArchive \
 -exportFormat IPA \
 -exportProvisioningProfile $PROVISIONING_PROFILE_NAME \
 -archivePath ${ARCHIVE_PATH} \
 -exportPath ${EXPORT_PATH} \


##移除多餘檔案 
NSLog "** REMOVE UNUSED FILES **"

rm "./${BUILD_PATH}/${PROJECT_NAME}.app"
rm -rf "./${BUILD_PATH}/${PROJECT_NAME}.app.dSYM"
rm -rf ${ARCHIVE_PATH}

# ... finish all