《iOS UI 開發(fā)捷徑 利用 Interface Builder 高效、優(yōu)雅地開發(fā) UI》 讀書筆記

第1章 Interface Bundle 概要


Bundle

一種標(biāo)準(zhǔn)化的層次結(jié)構(gòu),保存了可執(zhí)行代碼及代碼所需要的資源。

nib

Next Interface Builder

Interface Builder 的優(yōu)點(diǎn)

  1. 開發(fā)和維護(hù)效率高
  2. 減少大量的 UI 代碼和“膠水代碼”
  3. 適配變得十分簡(jiǎn)單
  4. IB 也可以做一些非 UI 的事情
  5. 利用 IB 學(xué)習(xí)控件可以達(dá)到事半功倍的效果

Interface Builder 的缺點(diǎn)

  1. IB 的執(zhí)行效率沒有純代碼高
  2. 使用 IB 開發(fā)的過(guò)程中容易出現(xiàn)一些小問(wèn)題
  3. 有一定的學(xué)習(xí)成本
  4. 文件易沖突
  5. 沒有代碼表達(dá)清晰
  6. 不利于代碼的封閉和工程架構(gòu)的組織

Interface Builder 學(xué)習(xí)的特點(diǎn)

  1. 簡(jiǎn)單,容易入門
  2. 容易犯錯(cuò)誤
  3. 有很多的“坑”,需要積累屬于自己的經(jīng)驗(yàn)

Interface Builder 的發(fā)展

xib -> sb -> AutoLayout -> LaunchScreen.storyboard

蘋果越來(lái)越重視 IB。

CocoaPods

# 1 最新版本
pod 'AFNetworking'

# 2 最新的2.x版本
pod 'AFNetworking', '~>2.5.3'

# 3 指定版本
pod 'AFNetworking', '2.5.3' 

試用

$ pod try AFNetworking

Podfile文件與CocoaPods的三種依賴方式

  • 遠(yuǎn)程依賴
# master
pod 'Alamofile', :git => 'https://github.com/Alamofile/Alamofile.git'

# branch
pod 'Alamofile', :git => 'https://github.com/Alamofile/Alamofile.git', :branch -> 'dev'

# commit
pod 'Alamofile', :git => 'https://github.com/Alamofile/Alamofile.git', :commit => '0f506b1c45'
  • 本地依賴
pod 'AFNetworking', :path => '../externals/libs/AFNetworking'
  • 私有依賴
pod 'AFNetworking', :podspec => '../externals/libs/AFNetworking.podspec'

“:podspec=>”用于指定本地的一個(gè)xxx.podspec文件。

podspec文件

$ pod init
$ cat Podfile

第2章 使用 Interface Builder


解決沖突

  1. 解決普通IB文件沖突

Open As -> Source Code

<<<<<<<

=======

>>>>>>>

編輯好,再刪除這三行就可以了。

  1. 解決 Xcode 8 引起的 IB 文件沖突

如果用 Source Code 不能打開,就用文件編輯器(vim, etc)打開,把systemVersion等沖突解決。

關(guān)聯(lián) xib 文件與源文件

  1. 關(guān)聯(lián) xib 文件與 UIView 子類的源文件
  • 新建一個(gè)空的xib文件,拖一個(gè)UIView上去
  • 新建一個(gè)繼承自UIView的源文件
  • 選中xib文件里的View,把class改為上面

@IBOutlet 與 @IBAction

連線

設(shè)計(jì)模式之MVC

mvc

理解 File's Owner

關(guān)聯(lián) xib 文件與 UIViewController 子類的源文件

  • 自定義一個(gè) VC 的 View 的兩種方法
  1. 在 IB 文件中選中 VC 所在的 View,在 Show the Identity inspector 中設(shè)置 Class 標(biāo)簽的值為自定義的 View 的類名。
  2. 在源文件的 loadView() 方法里設(shè)置該 VC 的 View 屬性為這個(gè)自定義的 View。
override func loadView() {
    self.view = CustomView.init(frame: UIScreen.main.bounds)
}
  • File's Owner 指向 VC

  • VC 的 View 連線

xib 既可以與 UIView 關(guān)聯(lián),也可以與 UIViewController 關(guān)聯(lián),也可以同時(shí)關(guān)聯(lián) UIView 與 UIViewController

使用 xib

  1. 通過(guò) Bundle 方式加載
  2. 通過(guò) UINib 方式加載

使用與UIView子類源文件關(guān)聯(lián)的xib

Bundle

- (NSArray *)loadNibNamed:(NSString *)name 
                    owner:(id)owner 
                  options:(NSDictionary *)options;
func loadNibNamed(_ name: String, 
            owner: Any?, 
          options: [AnyHashable : Any]? = nil) -> [Any]?

Parameters 參數(shù)

  • name
    The name of the nib file, which need not include the .nib extension.
    nib名稱
  • owner
    The object to assign as the nib’s File's Owner object.
    如果xib文件有File's Owner,一定傳其實(shí)例對(duì)象,否則傳nil
  • options
    A dictionary containing the options to use when opening the nib file. For a list of available keys for this dictionary, see UIKit Nib Loading Options.
Bundle Demo
let testView = Bundle.main.loadNibNamed("TestView", owner: nil, options: nil)?[0] as! UIView
view.addSubview(testView)

UINib

NS_CLASS_AVAILABLE_IOS(4_0) @interface UINib : NSObject 

// If the bundle parameter is nil, the main bundle is used.
// Releases resources in response to memory pressure (e.g. memory warning), reloading from the bundle when necessary.
+ (UINib *)nibWithNibName:(NSString *)name bundle:(nullable NSBundle *)bundleOrNil;

// If the bundle parameter is nil, the main bundle is used.
+ (UINib *)nibWithData:(NSData *)data bundle:(nullable NSBundle *)bundleOrNil;

// Returns an array containing the top-level objects from the NIB.
// The owner and options parameters may both be nil.
// If the owner parameter is nil, connections to File's Owner are not permitted.
// Options are identical to the options specified with -[NSBundle loadNibNamed:owner:options:]
- (NSArray *)instantiateWithOwner:(nullable id)ownerOrNil options:(nullable NSDictionary *)optionsOrNil;
@end
@available(iOS 4.0, *)
open class UINib : NSObject {

    
    // If the bundle parameter is nil, the main bundle is used.
    // Releases resources in response to memory pressure (e.g. memory warning), reloading from the bundle when necessary.
    public /*not inherited*/ init(nibName name: String, bundle bundleOrNil: Bundle?)

    
    // If the bundle parameter is nil, the main bundle is used.
    public /*not inherited*/ init(data: Data, bundle bundleOrNil: Bundle?)

    
    // Returns an array containing the top-level objects from the NIB.
    // The owner and options parameters may both be nil.
    // If the owner parameter is nil, connections to File's Owner are not permitted.
    // Options are identical to the options specified with -[NSBundle loadNibNamed:owner:options:]
    open func instantiate(withOwner ownerOrNil: Any?, options optionsOrNil: [AnyHashable : Any]? = nil) -> [Any]
}
UINib Demo
let testViewNib = UINib.init(nibName: "TestView", bundle: Bundle.main)
func loadTestView() {
    let testView = UINib.instantiate(withOwner: nil, options: nil)[0] as! UIView
    view.addSubview(testView)
}

使用與 UIViewController 子類源文件關(guān)聯(lián)的 xib

Demo HomeViewController.swift <==> HomeController.xib
let homeVC = HomeViewController()
class HomeViewController {
    var aName: String?

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: "HomeController", bundle: nil)
    }

    init(aName: String) {
        name = aName
        super.init(nibName: "HomeController", bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

嵌套 xib

https://github.com/iOSDevLog/iOSDevLog/tree/master/228.%20NestedXib

圖片上傳失敗請(qǐng)。參考 http://iosdevlog.com/ios/2017/12/19/ios-ui-interface-builder.html

使用 storyboard

@available(iOS 5.0, *)
open class UIStoryboard : NSObject {

    
    public /*not inherited*/ init(name: String, bundle storyboardBundleOrNil: Bundle?)

    
    open func instantiateInitialViewController() -> UIViewController?

    open func instantiateViewController(withIdentifier identifier: String) -> UIViewController
}

storyboard Demo

extension UIViewController {
    class func storyboardID() -> String {
        return String(describing: self)
    }
}

let userStoryBoard = UIStoryboard(name: "User", bundle: nil)
let profileVC = userStoryBoard.instantiateInitialViewController(withIdentifier: ProfileController.storyboardID())

第3章 全面學(xué)習(xí) xib


Autoresizing

Autoresizing

默認(rèn)選中左,上。

對(duì)應(yīng)代碼為:

testView.autoresizingMask = [.flexibleRightMargin, .flexibleBottomMargin]

外框的 上、下、左、右如果選中,則UIView的邊框與父View邊框距離保持不變。

中間帶箭頭的選中表示UIView邊框是隨屏幕尺寸變化的。
否則UIView大小保持不變。

也可以看右側(cè)的動(dòng)畫查看顯示效果。

第4章 在 Interface Builder 中使用 Auto Layout


在 IB 中使用 Auto Layout 的優(yōu)缺點(diǎn)

  • 設(shè)置約束十分簡(jiǎn)單

  • 如果約束不恰當(dāng),IB 提供很好的實(shí)時(shí)反饋

  • 如果約束不恰當(dāng),IB 可以幫忙改正。改正操作十分簡(jiǎn)單、方便、快捷。

  • 難以理解

約束

Auto Layout 的數(shù)學(xué)公式

item1.attribute1 = multiplier * item2.attribute2 + constant

約束屬性

public enum NSLayoutAttribute : Int {
    case left
    case right
    case top
    case bottom
    case leading
    case trailing
    case width
    case height
    case centerX
    case centerY
    case lastBaseline
    @available(iOS 8.0, *)
    case firstBaseline
    @available(iOS 8.0, *)
    case leftMargin
    @available(iOS 8.0, *)
    case rightMargin
    @available(iOS 8.0, *)
    case topMargin
    @available(iOS 8.0, *)
    case bottomMargin
    @available(iOS 8.0, *)
    case leadingMargin
    @available(iOS 8.0, *)
    case trailingMargin
    @available(iOS 8.0, *)
    case centerXWithinMargins
    @available(iOS 8.0, *)
    case centerYWithinMargins
    case notAnAttribute
}

約束關(guān)系

public enum NSLayoutRelation : Int {
    case lessThanOrEqual
    case equal
    case greaterThanOrEqual
}

約束的優(yōu)先級(jí)

public struct UILayoutPriority : RawRepresentable, Equatable, Hashable {
    public init(_ rawValue: Float)
    public init(rawValue: Float)
}

extension UILayoutPriority {
    @available(iOS 6.0, *)
    public static let required: UILayoutPriority

    @available(iOS 6.0, *)
    public static let defaultHigh: UILayoutPriority // This is the priority level with which a button resists compressing its content.

    @available(iOS 6.0, *)
    public static let defaultLow: UILayoutPriority // This is the priority level at which a button hugs its contents horizontally.

    @available(iOS 6.0, *)
    public static let fittingSizeLevel: UILayoutPriority // When you send -[UIView systemLayoutSizeFittingSize:], the size fitting most closely to the target size (the argument) is computed.  UILayoutPriorityFittingSizeLevel is the priority level with which the view wants to conform to the target size in that computation.  It's quite low.  It is generally not appropriate to make a constraint at exactly this priority.  You want to be higher or
    lower.
}

Instrinsic Size 固有尺寸

默認(rèn)設(shè)置了 Width 和 Height

Content Compression Resistance 壓縮阻力

Content Hugging 內(nèi)容吸附

NSLayoutConstraint 與 @IBOutlet 連線

設(shè)置約束的方法

  1. 在 IB 中設(shè)置 - 推薦
  2. 蘋果原生 API - 最復(fù)雜,強(qiáng)烈不推薦
  3. 用 VFL (Visual Format Language) 設(shè)置約束 - 不推薦
  4. 第三方庫(kù)(Masonry等)設(shè)置約束 - 代碼設(shè)置最簡(jiǎn)單、最常用 推薦(如果不熟悉 Auto Layout)

UIStackView

Axis

  • Vertical: 豎直布局
  • Horizontal: 水平布局

Alignment

  • Fill
  • Top
  • Leading
  • Center
  • Bottom
  • Trailing
  • First Baseline
  • Last Base Line

Distribution

  • Fill
  • Fill Equal
  • Fill Proportionally
  • Equal Spacing
  • Equal Centering

Space

Baseline Relative

FDStackView

NSUUID https://gist.github.com/OliverLetterer/4643294

Auto Layout 的異類 - UIScrollView

UIScrollView 的子 View 需要設(shè)置 6 個(gè)約束

scrollView.contentSize.width = subView.leading + subView.width + subView.trailing;
scrollView.contentSize.height = subView.top + subView.height+ subView.bottom;

設(shè)置 ScrollView 的子 View 約束時(shí)一定要讓系統(tǒng)確定 ScrollView 的 contentSize。

第5章 storyboard 全面學(xué)習(xí)


Extra View

segue

Embed Segue

Unwind Segue

Launch Screen

https://github.com/iOSDevLog/iOSDevLog/tree/master/229.%20ScreenLunch

@interface AppDelegate ()
            
@property (nonatomic, strong) UIWindow* launchWindow;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.launchWindow = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
    self.launchWindow.hidden = NO;
    self.launchWindow.windowLevel = UIWindowLevelNormal + 1;
    self.launchWindow.rootViewController = [UIStoryboard storyboardWithName:@"Launch Screen" bundle:nil].instantiateInitialViewController;
    [UIView animateWithDuration:3 delay:0.5 options: UIViewAnimationOptionCurveEaseInOut animations:^{
        self.launchWindow.alpha = 0;
    } completion:^(BOOL finished) {
        self.launchWindow.hidden = YES;
        self.launchWindow.windowLevel = UIWindowLevelNormal - 1;
        self.launchWindow.alpha = 1;
    }];
    
    return YES;
}
@end
var launchWindow: UIWindow?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    launchWindow = UIWindow(frame: UIScreen.main.bounds)
    launchWindow?.windowLevel = UIWindowLevelNormal + 1
    launchWindow?.isHidden = false
    launchWindow?.rootViewController = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()
    UIView.animate(withDuration: 0.5, delay: 3, options: .curveEaseInOut, animations: {
        self.launchWindow?.alpha = 0
    }) { (finish) in
        if finish {
            self.launchWindow?.isHidden = true
            self.launchWindow?.windowLevel = UIWindowLevelNormal - 1
            self.launchWindow?.alpha = 1
        }
    }
    return true
}

第6章 Interface Builder 進(jìn)階


Use Trait Variations

設(shè)備 豎屏 橫屏
3.5 iPhone wC hR wC hC
4.0 iPhone wC hR wC hC
4.7 iPhone wC hR wC hC
5.5 iPhone wC hR wR hC
iPad wR hR wR hR

User Define Runtime Attribute

IB 中的類型 Swift Objective-C
Boolean Bool BOOL
Number + +
String String NSString
Localized String String NSString
Point CGPoint CGPoint
Size CGSize CGSize
Rect CGRect CGRect
Range Range NSRange
Color UIColor UIColor
Image UIImage UIImage
Nil Nil Nil

Number 在 Swift 里面可以對(duì)應(yīng) Int、Double、Float。
在 Objective-C 里面可以對(duì)應(yīng) NSInteger、NSNumber 等。

extension UIView {
    var borderColor: UIColor {
        set {
            self.layer.borderColor = newValue.cgColor
        }
        get {
            return UIColor.init(cgColor: self.layer.borderColor!)
        }
    }
}

IB 文件的加載過(guò)程

Bundle 和 UINib

  1. 將 nib 加載到內(nèi)存
  2. 解固化并實(shí)例化 nib 文件里對(duì)應(yīng)的對(duì)象
  3. 建立 connections (outlet、action)
  4. 調(diào)用 awakeFromNib() 方法
  5. 將 nib 中可見的控件顯示出來(lái)

本地化

兩種策略

  1. App 本地化跟隨系統(tǒng)語(yǔ)言
  2. App 內(nèi)部有一個(gè)可以設(shè)置語(yǔ)言的選項(xiàng)

本地化介紹

Base

文本的本地化

  1. 利用 NSLocalizedString。 新建 Localizable.strings 文件

Localizable.strings(English)

"test" = "hello world";

Localizable.strings(Chinese(Simplified))

"test" = "你好,世界";
override func viewDidLoad() {
    super.viewDidLoad()
    // Localizable.string
    testLabel.text = NSLocalizedString("test", comment: "")
    // Home.strings
    // testLabel.text = NSLocalizedString("test", tableName: "Home", comment: "")
}

Info.plist 的本地化

新建 InfoPlist.strings,在 Show the File inspector 點(diǎn)擊 Localize...

InfoPlist.strings(English)

CFBundleName = "hello world";
CFBundleDisplayName = "hello world";

InfoPlist.strings(Chinese(Simplified))

CFBundleName = "你好,世界";
CFBundleDisplayName = "你好,世界";

圖片資源的本地化

  • 方法1

Localizable.strings(English)

"testImageName" = "1";

Localizable.strings(Chinese(Simplified))

"testImageName" = "2";
testImageView.image = UIImage.init(named: NSLocalizedString("testImageName", comment: "")
  • 方法2

選中圖片,Show the File inspector 點(diǎn)擊 localize...,替換 zh-Hans.lproj 中的資源文件。

App 內(nèi)設(shè)置語(yǔ)言的本地化

https://github.com/iOSDevLog/iOSDevLog/tree/master/230.%20Localizations

extension Bundle {
    class func loadLocalizableString(languageBundleName: String, key: String) -> String? {
        let languageBundlePath = Bundle.main.path(forResource: languageBundleName, ofType: "lproj")
        
        guard languageBundlePath != nil else {
            return nil
        }
        
        let languageBundle = Bundle.init(path: languageBundlePath!)
        guard languageBundle != nil else {
            return nil
        }
        
        let value = languageBundle?.localizedString(forKey: key, value: "", table: "")
        
        return value
    }
}

func changeLanguage() {
    let kTestKey = "testKey"
    
    switch selectIndex {
    case 0:
        testLabel.text = Bundle.loadLocalizableString(languageBundleName: Language.simplifiedChinese.rawValue, key: kTestKey)
        break
    case 1:
        testLabel.text = Bundle.loadLocalizableString(languageBundleName: Language.english.rawValue, key: kTestKey)
        break
    case 2:
        testLabel.text = Bundle.loadLocalizableString(languageBundleName: Language.japanese.rawValue, key: kTestKey)
        break
    case 3:
        testLabel.text = Bundle.loadLocalizableString(languageBundleName: Language.korea.rawValue, key: kTestKey)
        break
    default:
        break
    }
}

Storyboard Reference 的使用

使用 RBStoryboardLink https://github.com/rob-brown/RBStoryboardLink

用 Object 重構(gòu) "神VC"

代碼量龐大、結(jié)構(gòu)臃腫、可維護(hù)性差的 VC。

使用 Object

  • 通常 VC 會(huì)成為很多對(duì)象的 delegate,需要處理很多回調(diào)。用 Object 替 VC 實(shí)現(xiàn) delegate。
  • 將一些能用需求或交互模塊化在對(duì)應(yīng)的 Object 里。將需求或交互與 VC 解耦。

用 External Object 重構(gòu) VC

只能在于 xib

IB 中的關(guān)鍵字總結(jié)

Swift

  • @IBAction
  • @IBOutlet
  • @IBDesignable
  • @IBInspectable

Objective-C

  • IBAction
  • IBOutlet
  • IB_DESIGNABLE
  • IBInspectable
  • IBOutletCollection(ClassName)

@IBDesignable

可以不運(yùn)行程序的情況下把源文件中的一些代碼實(shí)時(shí)渲染到 IB 中,但是源文件必須是 UIView 或者 NSView 的子類。

prepareForInterfaceBuild()

只需要將實(shí)時(shí)渲染的代碼放到 prepareForInterfaceBuild() 方法中就可以了,該方法并不會(huì)在程序運(yùn)行時(shí)調(diào)用。

@IBInspectable

@IBInspectable 修飾的屬性會(huì)顯示在 IB 的 Show the Attributes inspector。

extension UIView {
    private struct AssociatedKeys {
        static var name: String?
    }
    
    // 存儲(chǔ)屬性
    @IBInspectable var name: String {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.name) as! String
        }
        set {
            objc_setAssociatedObject(self, &AssociatedKeys.name, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }
    
    
    // 計(jì)算屬性
    @IBInspectable var borderColor: UIColor {
        set {
            self.layer.borderColor = newValue.cgColor
        }
        get {
            return UIColor.init(cgColor: self.layer.borderColor!)
        }
    }
}

第7章 在 Interface Builder 開發(fā)中的技巧和 Bug


調(diào)整 View 的尺寸,使它與顯示內(nèi)容的尺寸相適應(yīng)

comment + =

查看各個(gè) View 之間的距離

選中 View, 按住 option,懸停在其它 View 上。

在 IB 中添加參考線

Editor -> Guides -> Add Horizontal Line

command + -

Editor -> Guides -> Add Vertical Line

command + ctrl + |

快速調(diào)整底層被擋住的 View 的位置

快速查看 View 的 UI 層次關(guān)系

command + shift + right click

連線小技巧

兩個(gè)窗口

使用吸管快速設(shè)置顏色

IB 中的復(fù)制與粘貼

command + c

command + v

利用 Media Library 快速設(shè)置圖片

IB 開發(fā)中遇到的一些小 bug

最好的做法就是重啟 Xcode。

  • 無(wú)法連線

IB 文件是否與源文件關(guān)聯(lián)

  • @IBAction 紅色提示

先在源文件中定義好方法,再?gòu)脑次募? 到 IB 文件進(jìn)行 連線

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容