iOS Xib Storyboard
iOS Xib & Storyboard
- InterfaceBuilder
- Xib文件
- Xib解析方式
- 模擬示例
- Storyboard
- storyboard分析
- storyboard的啟動
在iOS開發中,我們會經常接觸到的 xib 文件,還能聽到它的另一個名字 nib,其實它們倆差不多是指代同一個東西,只不過 xib 是編譯前,nib是編譯后, 還有后來的 storyboard,它們其實都xml文件,通過右鍵這些文件然后 open as > source code 就可以看到文件的源碼。
這次主要從以下幾點分析Xib & Storyboard
- InterfaceBuilder簡介
- Xib文件格式分析
- Xib解析方式猜測
- 簡單模擬ViewControll加載xib
- 平時使用xib方式的注意事項
- 自定義View關聯xib的兩種方式
- Storyboard簡單介紹
- Storyboard的啟動
InterfaceBuilder
Interface Builder(縮寫:IB)是用于蘋果公司Mac OS X操作系統的軟件開發程序,是Xcode套件的一部分。Cocoa開發者可以使用Interface Builder來創建和修改應用程序的圖形用戶界面。其數據以XML的形式被儲存在.xib文件中。
Xib文件
如果你仔細比對xib和storyboard的xml的文件內容,你會發現差別很小,其中兩個重要的差別是:
- storyboard的type是com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB
- xib的type是com.apple.InterfaceBuilder3.CocoaTouch.XIB
- storyboard相對于xib多了一個scene的概念, 所有xml里會有一個頂層標簽是scenes而xib里的頂層標簽是objects
xib和storyboard就像一個配置文件,在圖形化界面里將想要的界面搭建好,然后調用系統提供的方法來讀取這些文件來構建一個個對象。
最常用的就是從xib里面初始化ViewController了。在創建ViewController的時候,Xcode會詢問是否創建一個xib,如果選擇是那么和這個ViewController同名的xib將會被創建。
ViewController加載xib流程
- (instancetype)init; //該方法會轉調下面的方法 - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil;以下均可正常展示
1. Test1ViewController *viewController = [[Test1ViewController alloc] init]; [self.navigationController pushViewController:viewController animated:YES];2. Test1ViewController *viewController = [[Test1ViewController alloc] initWithNibName:@"Test1ViewController" bundle:[NSBundle mainBundle]]; [self.navigationController pushViewController:viewController animated:YES];3. Test1ViewController *viewController = [[Test1ViewController alloc] initWithNibName:@"Test2ViewController" bundle:[NSBundle mainBundle]]; [self.navigationController pushViewController:viewController animated:YES];默認情況下ViewController會找對應類名的xib文件,需要注意的是,xib中如果有connections*(用IB拖出來的線會在conections標簽下生成相關數據)*則ViewController一定要有相對應的IBOutlet,否則會因找不到相應的selector造成crash。
分析一下xib文件
<?xml version="1.0" encoding="UTF-8"?> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"><device id="retina4_7" orientation="portrait"><adaptation id="fullscreen"/></device><dependencies><deployment identifier="iOS"/><plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/><capability name="Safe area layout guides" minToolsVersion="9.0"/><capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/></dependencies><objects><placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="TestCustom"><connections><outlet property="titleLabel" destination="HBy-2B-FHf" id="Aqm-dl-WXp"/><outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/></connections></placeholder><placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/><view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT"><rect key="frame" x="0.0" y="0.0" width="375" height="667"/><autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/><subviews><label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Test for ViewController" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HBy-2B-FHf"><rect key="frame" x="100" y="323" width="175" height="21"/><fontDescription key="fontDescription" type="system" pointSize="17"/><nil key="textColor"/><nil key="highlightedColor"/></label></subviews><color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/><constraints><constraint firstItem="HBy-2B-FHf" firstAttribute="centerX" secondItem="i5M-Pr-FkT" secondAttribute="centerX" id="3Jb-mF-KMo"/><constraint firstItem="HBy-2B-FHf" firstAttribute="centerY" secondItem="i5M-Pr-FkT" secondAttribute="centerY" id="7fQ-gk-QU8"/></constraints><viewLayoutGuide key="safeArea" id="Q5M-cg-NOt"/></view></objects> </document>xib文件使用的xml格式其中除了objects標簽內數據其他均是配置信息,我們主要分析objects標簽。
File's Owner 標簽
我們用Xcode創建ViewController時勾選xib, 其中Xcode會自動設置為對應的ViewController類, 表現為File’s Owner對應標簽的customClass為ViewController的類名(如下HomeViewController);并且會有一個property="view"的outlet標簽,該標簽和ViewController.view對象關聯(如下outlet標簽)。
<objects><placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="HomeTabViewController"><connections><outlet property="view" destination="iN0-l3-epB" id="HvV-il-KfN"/></connections></placeholder><placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/><view contentMode="scaleToFill" id="iN0-l3-epB">...</view> </objects>outlet標簽指定了iN0-l3-epB,而iN0-l3-epB就是view的id。
在ViewController中可以通過如下方式拿到對應的view
- (void)viewDidLoad {[super viewDidLoad];self.view }如果手動刪除xib中view這個outlet標簽,self.view就是nil
其實xib被解析時,如果有outlet則調用對應class的selector,而調用那個selector則是由property這一屬性值決定。
view subviews label 等標簽是布局相關的 constraints標簽是配置約束關系
了解這幾個標簽后,初步知道xib是被解析然后綁定到ViewController上的,接下來更加細化的分析和驗證一下.
Xib解析方式
xib文件的解析是 NSBundle(UINibLoadingAdditions) 完成的,h文件如下
#import <Foundation/Foundation.h> #import <UIKit/UIKitDefines.h>NS_ASSUME_NONNULL_BEGINtypedef NSString * UINibOptionsKey NS_TYPED_ENUM;UIKIT_EXTERN UINibOptionsKey const UINibExternalObjects NS_AVAILABLE_IOS(3_0);@interface NSBundle(UINibLoadingAdditions) - (nullable NSArray *)loadNibNamed:(NSString *)name owner:(nullable id)owner options:(nullable NSDictionary<UINibOptionsKey, id> *)options; @end@interface NSObject(UINibLoadingAdditions) - (void)awakeFromNib NS_REQUIRES_SUPER; - (void)prepareForInterfaceBuilder NS_AVAILABLE_IOS(8_0); @endUIKIT_EXTERN NSString * const UINibProxiedObjectsKey NS_DEPRECATED_IOS(2_0, 3_0) __TVOS_PROHIBITED;NS_ASSUME_NONNULL_ENDimport了這個h文件后,任何對象就會新增兩個方法 awakeFromNib prepareForInterfaceBuilder xib加載好 (不包含布局好,這時候控件的位置和大小還未完全確定) 就會調對應Class的這兩個方法。
由于看不到源碼,所以只能猜了,下面就根據猜測模擬一下
模擬示例
簡單模擬ViewController加載
TestCustom.h
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface TestCustom : UIResponder@endNS_ASSUME_NONNULL_ENDTestCustom.m
#import <UIKit/UIKit.h>#import "TestCustom.h"@interface TestCustom()//這兩行可以手動加,也可以改一下xib中File's owner后鼠標拖出 @property (weak, nonatomic) IBOutlet UIView *view; @property (weak, nonatomic) IBOutlet UILabel *titleLabel;@end@implementation TestCustom- (instancetype)init {self = [super init];if (self) {[self setup];}return self; }- (void)setup {//調用loadNibName:::方法, owner設置成self,這樣解析outlet時就會調owner對應的方法[[NSBundle mainBundle] loadNibNamed:@"TestCustom" owner:self options:nil];NSLog(@"TestCustom, setup, view = %@, titleLabel = %@", self.view, self.titleLabel); }- (void)awakeFromNib {NSLog(@"TestCustom, awakeFromNib"); }@endLog:
TestCustom, setup
view = <UIView: 0x7fcf84f53f10;...>
titleLabel = <UILabel: 0x7fcf84f6c4c0;...>
TestCustom.xib文件
<objects><placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="TestCustom"><connections><outlet property="titleLabel" destination="HBy-2B-FHf" id="Aqm-dl-WXp"/><outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/></connections></placeholder><placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/><view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT"><rect key="frame" x="0.0" y="0.0" width="375" height="667"/><autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/><subviews><label opaque="NO" ...... id="HBy-2B-FHf"><rect key="frame" x="60" y="48" width="92" height="21"/><fontDescription key="fontDescription" type="system" pointSize="17"/><nil key="textColor"/><nil key="highlightedColor"/></label></subviews>......</view> </objects>解析 <outlet property="titleLabel" ..."/> 和 <outlet property="view" ..."/> 時調了TestCustom設置titleLabel的selector和設置view的selector,如果刪除TestCustom中的 IBOutlet UIView *view 和 IBOutlet UILabel *titleLabel 就會出crash,如下
*** Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[<TestCustom 0x600002709660> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key titleLabel.’
*** Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[<TestCustom 0x600000662370> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key view.’
如果再將view放到window上那一個可見的界面就產生了,同時TestCustom繼承了UIResponder, 不僅能看到界面還能處理用戶操作,如果再維護一個NavigationController就完完全全是一個ViewController了,當然肯定還有其他很多的細節.
###自定義View實現方式
第一種:
#import "Test1View.h"@interface Test1View()@property (weak, nonatomic) IBOutlet UILabel *titleLabel;@end@implementation Test1View+ (instancetype)view {return [[[NSBundle mainBundle] loadNibNamed:@"Test1" owner:nil options:nil] firstObject]; }- (void)awakeFromNib {[super awakeFromNib];NSLog(@"Test1View, awakeFromNib"); }- (void)layoutSubviews {[super layoutSubviews];NSLog(@"Test1View, layoutSubviews"); }@end對應的xib文件
<objects><placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/><placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/><view contentMode="scaleToFill" id="iN0-l3-epB" customClass="Test1View">...<connections><outlet property="titleLabel" destination="Hcy-Sp-Hbg" id="znM-fY-xwR"/></connections></view> </objects>log都正常
xib中 File's Owner 沒有指定 customClass , 代碼上 owner 為空
xib中objects下一層也沒有outlet;
xib中view的下一層添加了outlet 同時指定了view是由Test1View實現的,xib解析時就會創建Test1View并調Test1View的titleLabel的設置方法。
這種方式需要注意view的customClass一定要有對應的IBOutlet,否則會Crash,正常Xcode手動操作不會出問題,切記手動修改類名或者xib文件名.
第二種:
#import "Test2View.h"@interface Test2View()@property (weak, nonatomic) IBOutlet UIView *view; @property (weak, nonatomic) IBOutlet UILabel *titleLabel;@end@implementation Test2View+ (instancetype)view {return [[Test2View alloc] init]; }- (instancetype)init {self = [super init];if (self) {[[NSBundle mainBundle] loadNibNamed:@"Test2" owner:self options:nil];[self addSubview:self.view];NSLog(@"Test2View, init, view = %@, titleLabel = %@", self.view, self.titleLabel);}return self; }- (void)awakeFromNib {[super awakeFromNib];NSLog(@"Test2View, awakeFromNib"); }- (void)layoutSubviews {[super layoutSubviews];NSLog(@"Test2View, layoutSubviews"); }@end對應的xib文件
<objects><placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="Test2View"><connections><outlet property="titleLabel" destination="Hcy-Sp-Hbg" id="3Mf-4P-MnA"/><outlet property="view" destination="iN0-l3-epB" id="HvV-il-KfN"/></connections></placeholder><placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/><view contentMode="scaleToFill" id="iN0-l3-epB">。。。</view> </objects>log中沒有awakeFromNib, 從xib解析中實例化的view才會有awakeFromNib的回調,這一點可以通過在xib中view的實現者來驗證
xib中 File's Owner 指定了 customClass , 代碼上 owner 指定了 self
xib中objects有outlet,對應owner有相關的IBOutlet,xib解析時就會owner的titleLabel和view的設置方法,
這種方式需要注意owner一定要有對應的IBOutlet,否則會Crash,正常xcode手動操作不會出問題,切記手動修改類名或者xib文件名。
第一種和第二種的區別:
綜上并結合ViewController的xib內容,可以斷定ViewController用的就是第二種方式,并且ViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil; 方法中調了 [[NSBundle mainBundle] loadNibNamed:@"XXX" owner:self options:nil]
Storyboard
storyboard相對于xib多了一個scene的概念,xml里會有一個頂層標簽是scenes而xib里的頂層標簽是objects。
storyboard分析
要介紹Storyboard是什么,從這張圖講起:
上面是Main.storyboard的內容,從左到右依次是 Navigation Controller Scenc View Controller Scenc Tab2 View Controller Scene Tab1 View Controller Scene
Tab1ViewController和Tab2ViewController對應的線分別是MainButton1和MainButton2,這樣點擊Button1就會跳轉到Tab1ViewController點擊Button2就會跳到Tab2ViewController
看一下Main.stroyboard文件
... <scene sceneID="tne-QT-ifu"><objects><viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController"><view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">...<subviews><label ...><button .../><connections><segue destination="D7z-E4-gRL" kind="push" id="1Of-px-wpF"/></connections></button><button ...></subviews>...</view><navigationItem key="navigationItem" id="jgj-Ur-zGs"/></viewController>...</objects><point key="canvasLocation" x="1199" y="167"/> </scene> ... ... <!--Tab1 View Controller--> <scene sceneID="zkj-DY-y2h"><objects><viewController id="D7z-E4-gRL" customClass="Tab1ViewController" sceneMemberID="viewController"><view key="view" contentMode="scaleToFill" id="exE-0e-ynF">...</view><navigationItem key="navigationItem" id="s1g-wa-3DJ"/></viewController>...</objects><point key="canvasLocation" x="2703" y="54"/> </scene> ... ... <!--Tab2 View Controller--> <scene sceneID="7uO-pe-oCU"><objects><viewController id="VPd-VG-xrk" customClass="Tab2ViewController" sceneMemberID="viewController"><view key="view" contentMode="scaleToFill" id="7CO-RC-wiG">...</view></viewController>...</objects><point key="canvasLocation" x="2004" y="495"/> </scene> ...上面分析xib時我們知道xib文件中objects標簽包含的為主要內容,storyboard是以scenes標簽頁包含的為主要內容,每一個scene下包含objects. storyboard相當于多個xib的集合。
其中點擊button跳轉ViewController在segue標簽內聲明。button點擊自動跳轉其實是storyboard解析時給button添加了targets, 如下是加斷點打印出button的targets
2019-03-08 11:15:13.892396+0800 LayoutTest[13748:366986] libMobileGestalt MobileGestalt.c:890: MGIsDeviceOneOfType is not supported on this platform. (lldb) po self.tab1Button.allTargets {(<UIStoryboardPushSegueTemplate: 0x600003724fc0> )}(lldb)storyboard和xib的本質都一樣:在解析中執行代碼
storyboard的啟動
默認情況下storyboard是這樣啟動
info.plist 中 Main storyboard file base name 這一項指定了app啟動的storyboard,其實也是執行代碼,把這個配置刪掉用代碼如下:
AppDelegate.h
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];UIStoryboard * storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];self.window.rootViewController = [storyBoard instantiateInitialViewController];self.window.backgroundColor = [UIColor whiteColor];[self.window makeKeyAndVisible];return YES; }UIStoryboard.h
#import <Foundation/Foundation.h> #import <UIKit/UIKitDefines.h>@class UIViewController;NS_ASSUME_NONNULL_BEGINNS_CLASS_AVAILABLE_IOS(5_0) @interface UIStoryboard : NSObject { }+ (UIStoryboard *)storyboardWithName:(NSString *)name bundle:(nullable NSBundle *)storyboardBundleOrNil;- (nullable __kindof UIViewController *)instantiateInitialViewController; - (__kindof UIViewController *)instantiateViewControllerWithIdentifier:(NSString *)identifier;@endNS_ASSUME_NONNULL_ENDstoryboard是UIStoryboard類解析的,并且storyboard的啟動其實就是指定一個ViewController作為RootViewController。
storyboard相比于xib, 其實是一個包含的關系,storyboard只是多出了scenes這個,至于這些標簽和xib思路是想通的,具體什么含義就不再贅述。
使用Storyboard可以更好地了解App中所有的視圖以及它們之間的關聯的概況。掌控全局更加容易,因為所有的設計都包含在一個文件中,而不是分散在很多單獨的xib文件中,當然這也是storyboard的一個弊端,都集中在一個文件不利于團隊開發,所以在平時的開發還是要以具體情況分析。
總結
以上是生活随笔為你收集整理的iOS Xib Storyboard的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 技术沙龙|这期我们聊聊软件工程师的区块链
- 下一篇: 轻松实现富文本编辑器