你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 自定義系統控件的外觀:UIApearance

自定義系統控件的外觀:UIApearance

編輯:IOS開發基礎

makeup-791303_640.jpg

文章開頭先援引一下Mattt Thompson大神在UIApearance裡的一句話吧:

Users will pay a premium for good-looking software.

就如同大多數人喜歡看帥哥美女一樣,一款App能不能被接受,長得怎樣很重要。雖然大家都明白“人不可貌相”這個理,但大多數人其實還是視覺動物。用戶體驗用戶體驗,如果都讓用戶看得不爽了,又何談用戶體驗呢?所以…所以…哎,我也只能在這默默地碼字了。

在iOS 5以前,我們想去自定義系統控件的外觀是一件麻煩的事。如果想統一地改變系統控件的外觀,我們可能會想各種辦法,如去繼承現有的控件類,並在子類中修改,或者甚至於動用method swizzling這樣高大上的方法。不過,蘋果在iOS 5之後為我們提供了一種新的方法:UIAppearance,讓這些事簡單了不少。在這裡,我們就來總結一下吧。

UIApearance是作用

UIApearance實際上是一個協議,我們可以用它來獲取一個類的外觀代理(appearance proxy)。為什麼說是一個類,而不明確說是一個視圖或控件呢?這是因為有些非視圖對象(如UIBarButtonItem)也可以實現這個協議,來定義其所包含的視圖對象的外觀。我們可以給這個類的外觀代理發送一個修改消息,來自定義一個類的實例的外觀。

我們以系統定義的控件UIButton為例,根據我們的使用方式,可以通過UIAppearance修改整個應用程序中所有UIButton的外觀,也可以修改某一特定容器類中所有UIButton的外觀(如UIBarButtonItem)。不過需要注意的是,這種修改只會影響到那些執行UIAppearance操作之後添加到我們的視圖層級架構中的視圖或控件,而不會影響到修改之前就已經添加的對象。因此,如果要修改特定的視圖,先確保該視圖在使用UIAppearance後才通過addSubview添加到視圖層級架構中。

UIAppearance的使用

如上面所說,有兩種方式來自定義對象的外觀:針對某一類型的所有實例;針對包含在某一容器類的實例中的某一類型的實例。講得有點繞,我把文檔的原文貼出來吧。

for all instances, and for instances contained within an instance of a container class.

為此,UIAppearance聲明了兩個方法。如果我們想自定義一個類所有實例的外觀,則可以使用下面這個方法:

// swift
static func appearance() -> Self

// Objective-C
+ (instancetype)appearance

例如,如果我們想修改UINavigationBar的所有實例的背影顏色和標題外觀,則可以如下實現:

UINavigationBar.appearance().barTintColor = UIColor(red: 104.0/255.0, green: 224.0/255.0, blue: 231.0/255.0, alpha: 1.0)

UINavigationBar.appearance().titleTextAttributes = [
    NSFontAttributeName: UIFont.systemFontOfSize(15.0),
    NSForegroundColorAttributeName: UIColor.whiteColor()
]

我們也可以指定一類容器,在這個容器中,我們可以自定義一個類的所有實例的外觀。我們可以使用下面這個方法:

+ (instancetype)appearanceWhenContainedIn:(Class)ContainerClass, ...

如,我們想修改導航欄中所有的按鈕的外面,則可以如下處理:

[[UIBarButtonItem appearanceWhenContainedIn:[UINavigationBar class], nil]
   setBackgroundImage:myNavBarButtonBackgroundImage forState:state barMetrics:metrics];

[[UIBarButtonItem appearanceWhenContainedIn:[UINavigationBar class], [UIPopoverController class], nil]
    setBackgroundImage:myPopoverNavBarButtonBackgroundImage forState:state barMetrics:metrics];

[[UIBarButtonItem appearanceWhenContainedIn:[UIToolbar class], nil]
    setBackgroundImage:myToolbarButtonBackgroundImage forState:state barMetrics:metrics];

[[UIBarButtonItem appearanceWhenContainedIn:[UIToolbar class], [UIPopoverController class], nil]
    setBackgroundImage:myPopoverToolbarButtonBackgroundImage forState:state barMetrics:metrics];

注意這個方法的參數是一個可變參數,因此,它可以同時設置多個容器。

我們仔細看文檔,發現這個方法沒有swift版本,至少我在iOS 8.x的SDK中沒有找到對應的方法。呵呵,如果想在iOS 8.x以下的系統用swift來調用appearanceWhenContainedIn,那就乖乖地用混編吧。

不過在iOS 9的SDK中(記錄一下,今天是2015.07.18),又把這個方法給加上了,不過這回參數換成了數組,如下所示:

@available(iOS 9.0, *)
static func appearanceWhenContainedInInstancesOfClasses(containerTypes: [AnyObject.Type]) -> Self

嗯,這裡有個問題,我在Xcode 7.0 beta 3版本上測試swift版本的這個方法時,把將其放在啟動方法裡面,如下所示:

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

    // 此處會崩潰,提示EXC_BAD_ACCESS
    let barButtonItemAppearance = UIBarButtonItem.appearanceWhenContainedInInstancesOfClasses([UINavigationBar.self])

    let attributes = [
        NSFontAttributeName: UIFont.systemFontOfSize(13.0),
        NSForegroundColorAttributeName: UIColor.whiteColor()
    ]

    barButtonItemAppearance.setTitleTextAttributes(attributes, forState: .Normal)

    return true
}

程序崩潰了,在appearanceWhenContainedInInstancesOfClasses這行提示EXC_BAD_ACCESS。既然是內存問題,那就找找吧。我做了如下幾個測試:

1.拆分UIBarButtonItem.appearanceWhenContainedInInstancesOfClasses,在其前面加了如下幾行代碼:

let appearance = UIBarButtonItem.appearance()

let arr: [AnyObject.Type] = [UINavigationBar.self, UIToolbar.self]

print(arr)

可以看到除了appearanceWhenContainedInInstancesOfClasses自身外,其它幾個元素都是沒問題的。

2.將這段拷貝到默認的ViewController中,運行。同樣崩潰了。

3.在相同環境下(Xcode 7.0 beta 3 + iOS 9.0),用Objective-C對應的方法試了一下,如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [UIBarButtonItem appearanceWhenContainedInInstancesOfClasses:@[[UINavigationBar class]]];

    return YES;
}

程序很愉快地跑起來了。

額,我能把這個歸結為版本不穩定的緣故麼?等到穩定版出來後再研究一下吧。

支持UIAppearance的組件

從iOS 5.0後,有很多iOS的API都已經支持UIAppearance的代理方法了,Mattt Thompson在UIApearance中,給我們提供了以下兩行腳本代碼,可以獲取所有支持UI_APPEARANCE_SELECTOR的方法(我們將在下面介紹UI_APPEARANCE_SELECTOR):

$ cd /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk/System/Library/Frameworks/UIKit.framework/Headers

$ grep -H UI_APPEARANCE_SELECTOR ./* | sed 's/ __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_5_0) UI_APPEARANCE_SELECTOR;//'

大家可以試一下,我這裡列出部分輸出:

./UIActivityIndicatorView.h:@property (readwrite, nonatomic, retain) UIColor *color NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
./UIAppearance.h:/* To participate in the appearance proxy API, tag your appearance property selectors in your header with UI_APPEARANCE_SELECTOR.
./UIAppearance.h:#define UI_APPEARANCE_SELECTOR __attribute__((annotate("ui_appearance_selector")))
./UIBarButtonItem.h:- (void)setBackgroundImage:(UIImage *)backgroundImage forState:(UIControlState)state barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
./UIBarButtonItem.h:- (UIImage *)backgroundImageForState:(UIControlState)state barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
./UIBarButtonItem.h:- (void)setBackgroundImage:(UIImage *)backgroundImage forState:(UIControlState)state style:(UIBarButtonItemStyle)style barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;
./UIBarButtonItem.h:- (UIImage *)backgroundImageForState:(UIControlState)state style:(UIBarButtonItemStyle)style barMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;
./UIBarButtonItem.h:- (void)setBackgroundVerticalPositionAdjustment:(CGFloat)adjustment forBarMetrics:(UIBarMetrics)barMetrics NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR; 
......

大家還可以在這裡查看iOS 7.0下的清單。

自定義類實現UIAppearance

我們可以自定義一個類,並讓這個類支持UIAppearance。為此,我們需要做兩件事:

  1. 讓我們的類實現UIAppearanceContainer協議

  2. 如果是在Objective-C中,則將相關的方法用UI_APPEARANCE_SELECTOR來標記。而在Swift中,需要在對應的屬性或方法前面加上dynamic。

當然,要讓我們的類可以使用appearance(或appearanceWhenContainedInInstancesOfClasses)來獲取自己的類,則還需要實現UIAppearance協議。

在這裡,我們來定義一個帶邊框的Label,通過UIAppearance來設置它的默認邊框。實際上,UIView已經實現了UIAppearance和UIAppearanceContainer協議。因此,我們在其子類中不再需要顯式地去聲明實現這兩個接口。

我們的Label的聲明如下:

// RoundLabel.h

@interface RoundLabel : UILabel

@property (nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR;
@property (nonatomic, assign) CGFloat cornerRadius UI_APPEARANCE_SELECTOR;
@property (nonatomic, assign) UIColor *borderColor UI_APPEARANCE_SELECTOR;

@end

具體的實現如下:

@implementation RoundLabel

- (void)drawRect:(CGRect)rect {

    [super drawRect:rect];

    self.layer.borderColor = _borderColor.CGColor;
    self.layer.cornerRadius = _cornerRadius;
    self.layer.borderWidth = _borderWidth;
}

- (void)setBorderWidth:(CGFloat)borderWidth {

    _borderWidth = borderWidth;
}

- (void)setCornerRadius:(CGFloat)cornerRadius {

    _cornerRadius = cornerRadius;
}

- (void)setRectColor:(UIColor *)rectColor {

    _borderColor = rectColor;
}

@end

我們在drawRect:設置Label的邊框,這樣RoundLabel的所有實例就可以使用默認的邊框配置屬性了。

然後,我們可以在AppDelegate或者其它某個位置來設置RoundLabel的默認配置,如下所示:

UIColor *color = [UIColor colorWithRed:104.0/255.0 green:224.0/255.0 blue:231.0/255.0 alpha:1.0f];

[RoundLabel appearance].cornerRadius = 5.0f;
[RoundLabel appearance].borderColor = color;
[RoundLabel appearance].borderWidth = 1.0f;

當然,我們在使用RoundLabel時,可以根據實際需要再修改這幾個屬性的值。

Swift的實現就簡單多了,我們只需要如下處理:

class RoundLabel: UILabel {

    dynamic func setBorderColor(color: UIColor) {
        layer.borderColor = color.CGColor
    }

    dynamic func setBorderWidth(width: CGFloat) {
        layer.borderWidth = width
    }

    dynamic func setCornerRadius(radius: CGFloat) {
        layer.cornerRadius = radius
    }
}

在UIAppearanceContainer的官方文檔中,有對支持UIAppearance的方法作格式限制,具體要求如下:

// Swift
func propertyForAxis1(axis1: IntegerType, axis2: IntegerType, axisN: IntegerType) -> PropertyType
func setProperty(property: PropertyType, forAxis1 axis1: IntegerType, axis2: IntegerType)

// OBJECTIVE-C
- (PropertyType)propertyForAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 … axisN:(IntegerType)axisN;
- (void)setProperty:(PropertyType)property forAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 … axisN:(IntegerType)axisN;

其中的屬性類型可以是iOS的任意類型,包括id, NSInteger, NSUInteger, CGFloat, CGPoint, CGSize, CGRect, UIEdgeInsets或UIOffset。而IntegerType必須是NSInteger或者NSUInteger。如果類型不對,則會拋出異常。

我們可以以UIBarButtonItem為例,它定義了以下方法:

setTitlePositionAdjustment:forBarMetrics:

backButtonBackgroundImageForState:barMetrics:

setBackButtonBackgroundImage:forState:barMetrics:

這些方法就是滿足上面所提到的格式。

Trait Collection

我們查看UIAppearance的官方文檔,可以看到在iOS 8後,這個協議又新增了兩個方法:

// Swift
static func appearanceForTraitCollection(_ trait: UITraitCollection) -> Self

// Objective-C
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait

+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait
                         whenContainedIn:(Class)ContainerClass, ...

這兩個方法涉及到Trait Collection,具體的內容我們在此不過多的分析。

一些深入的東西

了解了怎麼去使用UIApearance,現在我們再來了解一下它是怎麼運作的。我們跟著UIAppearance for Custom Views一文的思路來走。

我們在以下實現中打一個斷點:

- (void)setBorderWidth:(CGFloat)borderWidth {

    _borderWidth = borderWidth;
}

然後運行程序。程序啟動時,我們發現雖然在AppDelegate中調用了

[RoundLabel appearance].borderWidth = 1.0f;

但實際上,此時程序沒有到在此斷住。我們再進到Label所在的視圖控制器,這時程序在斷點處停住了。在這裡,我們可以看看方法的調用棧。

在調用棧裡面,我們可以看到_UIAppearance這個東東,我們從iOS-Runtime-Headers可以找到這個類的定義:

@interface _UIAppearance : NSObject {
    NSMutableArray *_appearanceInvocations;
    NSArray *_containerList;
    _UIAppearanceCustomizableClassInfo *_customizableClassInfo;
    NSMapTable *_invocationSources;
    NSMutableDictionary *_resettableInvocations;
}

其中_UIAppearanceCustomizableClassInfo存儲的是外觀對應的類的信息。我們可以看看這個類的聲明:

@interface _UIAppearanceCustomizableClassInfo : NSObject {
    NSString *_appearanceNodeKey;
    Class _customizableViewClass;
    Class _guideClass;
    unsigned int _hash;
    BOOL _isCustomizableViewClassRoot;
    BOOL _isGuideClassRoot;
}

@property (nonatomic, readonly) NSString *_appearanceNodeKey;
@property (nonatomic, readonly) Class _customizableViewClass;
@property (nonatomic, readonly) Class _guideClass;
@property (nonatomic, readonly) unsigned int _hash;

+ (id)_customizableClassInfoForViewClass:(Class)arg1 withGuideClass:(Class)arg2;

- (id)_appearanceNodeKey;
- (Class)_customizableViewClass;
- (Class)_guideClass;
- (unsigned int)_hash;
- (id)_superClassInfo;
- (void)dealloc;
- (id)description;
- (unsigned int)hash;
- (BOOL)isEqual:(id)arg1;

@end

在_UIAppearance中,還有一個_appearanceInvocations變量,我們可以在Debug中嘗試用以下命令來打印出它的信息:

po [[NSClassFromString(@"_UIAppearance") _appearanceForClass:[RoundLabel class] withContainerList:nil] valueForKey:@"_appearanceInvocations"]

我們可以得到以下的信息:

<__NSArrayM 0x7fd44a5c1f80>(

return value: {v} void
target: {@} 0x10b545ae0
selector: {:} setCornerRadius:
argument 2: {d} 0.000000
,

return value: {v} void
target: {@} 0x10b545ae0
selector: {:} setBorderColor:
argument 2: {@} 0x7fd44a5bbb80
,

return value: {v} void
target: {@} 0x10b545ae0
selector: {:} setBorderWidth:
argument 2: {d} 0.000000

)

可以看到這個數組中存儲的實際上是NSInvocation對象,每個對象就是我們在程序中設置的RoundLabel外觀的方法信息。

在Peter Steinberger的文章中,有提到當我們設置了一個自定義的外觀時,_UIAppearanceRecorder會去保存並跟蹤這個設置。我們可以看看_UIAppearanceRecorder的聲明:

@interface _UIAppearanceRecorder : NSObject {
    NSString *_classNameToRecord;
    NSArray *_containerClassNames;
    NSMutableArray *_customizations;
    Class _superclassToRecord;
    NSArray *_unarchivedCustomizations;
}

不過有點可惜的是,我沒有從這裡找到太多的信息。我用runtime檢查了一下這個類中的數據,貌似沒有太多東西。可能是姿勢不對,我把代碼和結果貼出來,大家幫我看看。

unsigned int outCount = 0;

Class recorderClass = NSClassFromString(@"_UIAppearanceRecorder");

id recorder = [recorderClass performSelector:NSSelectorFromString(@"_sharedAppearanceRecorderForClass::whenContainedIn:") withObject:[RoundLabel class] withObject:nil];

NSLog(@"_UIAppearanceRecorder instance : %@", recorder);

Ivar *variables = class_copyIvarList(recorderClass, &outCount);

for (int i = 0; i < outCount; i++) {
    Ivar variable = variables[i];

    id value = object_getIvar(recorder, variable);
    NSLog(@"variable's name: %s, value: %@", ivar_getName(variable), value);
}

free(variables);

打印結果:

UIAppearanceExample2[7600:381708] _UIAppearanceRecorder instance : <_UIAppearanceRecorder: 0x7fa29a718960>
UIAppearanceExample2[7600:381708] variable's name: _classNameToRecord, value: RoundLabel
UIAppearanceExample2[7600:381708] variable's name: _superclassToRecord, value: (null)
UIAppearanceExample2[7600:381708] variable's name: _containerClassNames, value: (null)
UIAppearanceExample2[7600:381708] variable's name: _customizations, value: (
)
UIAppearanceExample2[7600:381708] variable's name: _unarchivedCustomizations, value: (null)

我們回過頭再來看看_UIAppearance的_appearanceInvocations,我們是否可以這樣猜測:UIAppearance是否是通過類似於Swizzling Method這種方式,在運行時去更新視圖的默認顯示呢?求解。

遺留問題

這一小篇遺留下了兩個問題:

  1. 在swift中如何正確地使用appearanceWhenContainedInInstancesOfClasses方法?我在stackoverflow中沒有找到答案。

  2. iOS內部是如何用UIAppearance設置的信息來在運行時替換默認的設置的?

如果有答案,還請告知。

小結

使用UIAppearance,可以讓我們方便地去修改一些視圖或控件的默認顯示。同樣,如果我們打算開發一個視圖庫,也可能會用到相關的內容。我們可以在庫的內部自定義一些UIAppearance的規則來代替手動去修改視圖外觀。這樣,庫外部就可以方便的通過UIAppearance來整體修改一個類中視圖的外觀了。

我在github中搜索UIAppearance相關的實例時,找到了UISS這個開源庫,它提供了一種便捷的方式來定義程序的樣式。這個庫也是基於UIAppearance的。看其介紹,如果我們想自定義一個UIButton的外觀,可以使用以下方式:

{
    "UIButton":{
        "titleColor:normal":["white", 0.8],
        "titleColor:highlighted":"white",
        "backgroundImage:normal": ["button-background-normal", [0,10,0,10]],
        "backgroundImage:highlighted": ["button-background-highlighted", [0,10,0,10]],
        "titleEdgeInsets": [1,0,0,0],
        "UILabel":{
            "font":["Copperplate-Bold", 18]
        }
    }
}

看著像JSON吧?

具體的我也還沒有看,回頭抽空再研究研究這個庫。

補充:文章中的示例代碼已放到github中,可以在這裡查看(不保證在iOS 9.0以下能正常進行,嘿嘿)

參考

  • UIApearance

  • UIAppearance Protocol Reference

  • UIAppearanceContainer Protocol Reference

  • UIAppearance for Custom Views

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved