almost 4 years ago

最近帶iOS新人常要回答一些既有設計上的問題,有時候雖能解釋,但也不見得能百分之百確定自己說的東西,像是專案中有許多客製化的UI元件完成某些特殊的需求(嘿~這應該不能說吧),這些元件依賴某些屬性是需要透過XCode的User Defined Runtime Attributes (例如Figure 1右下角的shadowColor、shadowOffset和someKey),解釋時,會解釋為何要設定這些屬性以及如何運作,但有時新人忘記設定(自己有時也會忘記),畫面上的元件就不太正確。

最近趁機找了一些網路文章,最後看了一下Resource Programming Guide中關於The Nib Object Life Cycle的敘述及Key-Value Coding Programming Guide後發現:可以從很多framework找到相似的設計(通常這也被稱為是好的設計或pattern),首先,Key-Value Coding和JavaBeans是相似的設計,讓外部不需實際呼叫getter或setter而是直接用屬性的名稱修改或讀取屬性的值。因此在XCode中設定的User Defined Runtime Attributes就是在載入nib後透過Key-Value機制將值設定給物件的某個屬性,然後就可以在程式中使用這些值了。

直接看例子吧!原有UITextField沒有屬性能設定文字的陰影,所以Code List 1和Code List 2提供了一個客製化的ShadowedTextField。UI元件從nib檔載入的順序是先呼叫initWithCoder:初始化物件(雖然UIView的designated initializer是initWithFrame:,但若是從nib載入的話,並不會呼叫initWithFrame:而是呼叫initWithCoder:),接著透過setValue:forKey:將User Defined Runtime Attributes設定給初始化過的UI元件,然後呼叫awakeFromNib告知客製化的元件說已經從nib檔中被喚醒了。因此,ShadowedTextFieldinitWithCoder:中設定陰影顏色和偏移量的初始值,然後在awakeFromNib套用陰影。

Code List 1 - ShadowedTextField Header
#import <UIKit/UIKit.h>

@interface ShadowedTextField : UITextField

@property (nonatomic) CGSize shadowOffset;
@property (nonatomic) UIColor* shadowColor;

@end
Code List 2 - ShadowedTextField Implementation
#import "ShadowedTextField.h"

@implementation ShadowedTextField

@dynamic shadowColor;
@dynamic shadowOffset;

- (instancetype)initWithCoder:(NSCoder*)aDecoder {
    if(self = [super initWithCoder:aDecoder]) {
        [self initializeDefaultValues];
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
    if(self = [super initWithFrame:frame]) {
        [self initializeDefaultValues];
    }
    return self;
}

- (void)initializeDefaultValues {
    self.shadowColor = [UIColor grayColor];
    self.shadowOffset = CGSizeMake(1.0f, 1.0f);
}

- (void)awakeFromNib {
    [super awakeFromNib];
    [self applyShadow];
}

- (void)setValue:(id)value forUndefinedKey:(NSString*)key {
    NSLog(@"undefined key-value: %@-%@", key, value);
}

- (void)applyShadow {
    NSShadow* shadow = [[NSShadow alloc] init];
    shadow.shadowColor = self.shadowColor;
    shadow.shadowOffset = self.shadowOffset;
    id attributes = @{
        NSShadowAttributeName: shadow,
        NSFontAttributeName: self.font
    };
    self.attributedText = [[NSAttributedString alloc] initWithString:self.text attributes:attributes];
}
@end

使用時,在Interface Builder中拉進一個UITextField,接著將該元件的Class設為客製化的ShadowedTextField,然後如Figure 1新增shadowColorshadowOffset兩筆屬性,執行後就可以看到Figure 2的樣子,陰影顏色被改成紫色(Code List 2的initializeDefaultValues將預設的shadowColor設為灰色,但執行後會被User Defined Runtime Attribues的值取代)。雖然說這兩個屬性不像原生的屬性那樣有相對應的編輯器可以編輯屬性值,但還算是能在Interface Builder中設定了。

Figure 1 - Specify the user defined runtime attributes

Figure 2 - The actual result

問題來了,執行環境(runtime)在呼叫setValue:forKey:時會尋找該物件是否有相對應的屬性,例如:在執行 [person setValue:@1982 forKey:@"birthYear"];時會檢查person物件是否有birthYear屬性,若無對應屬性會呼叫setValue: forUndefinedKey:讓該物件處理這特殊情況,NSObject預設的實作是拋出NSUndefinedKeyException例外讓程式終止,所以,若要客製化UI元件時,可以像Code List 2覆寫setValue: forUndefinedKey:處理這狀況,所以Console畫面上應該會印出類似2014-06-07 22:30:00.359 CustomAttributes[13452:60b] set undefined key-value: someKey-23的字樣。

看似問題都解決了,其實還是有些小問題,寫例子時發現幾個有趣現象。大家可以試試看,第一,將shadowColorshadowOffset兩屬性的accessors生成方法從@dynamic改成@synthesize,如果nib中沒有給text field的文字初始值,執行時輸入的文字其實並不會有陰影,但改回@dynamic就會有。第二,在accessors生成方法是@dynamic的情況下,若第一次點擊空白的text field後卻不輸入任何文字就收回鍵盤,第二次再點擊該text field後輸入的文字也不會有陰影,但如果有輸入文字,即使再次編輯將全部文字刪除,之後輸入的文字還是有陰影。上述現象我暫時還沒找到答案,知道答案的麻煩跟我說一聲XD

← About Android App Architecture About Custom View and Runtime Attributes →