《iOS UI 開發捷徑 利用 Interface Builder 高效、優雅地開發 UI》 讀書筆記

第1章 Interface Bundle 概要


Bundle

一種標準化的層次結構,保存了可執行代碼及代碼所需要的資源。

nib

Next Interface Builder

Interface Builder 的優點

  1. 開發和維護效率高
  2. 減少大量的 UI 代碼和“膠水代碼”
  3. 適配變得十分簡單
  4. IB 也可以做一些非 UI 的事情
  5. 利用 IB 學習控件可以達到事半功倍的效果

Interface Builder 的缺點

  1. IB 的執行效率沒有純代碼高
  2. 使用 IB 開發的過程中容易出現一些小問題
  3. 有一定的學習成本
  4. 文件易沖突
  5. 沒有代碼表達清晰
  6. 不利于代碼的封閉和工程架構的組織

Interface Builder 學習的特點

  1. 簡單,容易入門
  2. 容易犯錯誤
  3. 有很多的“坑”,需要積累屬于自己的經驗

Interface Builder 的發展

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

蘋果越來越重視 IB。

CocoaPods

# 1 最新版本
pod 'AFNetworking'

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

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

試用

$ pod try AFNetworking

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

  • 遠程依賴
# 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=>”用于指定本地的一個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等沖突解決。

關聯 xib 文件與源文件

  1. 關聯 xib 文件與 UIView 子類的源文件
  • 新建一個空的xib文件,拖一個UIView上去
  • 新建一個繼承自UIView的源文件
  • 選中xib文件里的View,把class改為上面

@IBOutlet 與 @IBAction

連線

設計模式之MVC

mvc

理解 File's Owner

關聯 xib 文件與 UIViewController 子類的源文件

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

  • VC 的 View 連線

xib 既可以與 UIView 關聯,也可以與 UIViewController 關聯,也可以同時關聯 UIView 與 UIViewController

使用 xib

  1. 通過 Bundle 方式加載
  2. 通過 UINib 方式加載

使用與UIView子類源文件關聯的xib

Bundle

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

Parameters 參數

  • 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,一定傳其實例對象,否則傳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 子類源文件關聯的 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

圖片上傳失敗請。參考 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章 全面學習 xib


Autoresizing

Autoresizing

默認選中左,上。

對應代碼為:

testView.autoresizingMask = [.flexibleRightMargin, .flexibleBottomMargin]

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

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

也可以看右側的動畫查看顯示效果。

第4章 在 Interface Builder 中使用 Auto Layout


在 IB 中使用 Auto Layout 的優缺點

  • 設置約束十分簡單

  • 如果約束不恰當,IB 提供很好的實時反饋

  • 如果約束不恰當,IB 可以幫忙改正。改正操作十分簡單、方便、快捷。

  • 難以理解

約束

Auto Layout 的數學公式

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
}

約束關系

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

約束的優先級

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 固有尺寸

默認設置了 Width 和 Height

Content Compression Resistance 壓縮阻力

Content Hugging 內容吸附

NSLayoutConstraint 與 @IBOutlet 連線

設置約束的方法

  1. 在 IB 中設置 - 推薦
  2. 蘋果原生 API - 最復雜,強烈不推薦
  3. 用 VFL (Visual Format Language) 設置約束 - 不推薦
  4. 第三方庫(Masonry等)設置約束 - 代碼設置最簡單、最常用 推薦(如果不熟悉 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 需要設置 6 個約束

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

設置 ScrollView 的子 View 約束時一定要讓系統確定 ScrollView 的 contentSize。

第5章 storyboard 全面學習


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 進階


Use Trait Variations

設備 豎屏 橫屏
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 里面可以對應 Int、Double、Float。
在 Objective-C 里面可以對應 NSInteger、NSNumber 等。

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

IB 文件的加載過程

Bundle 和 UINib

  1. 將 nib 加載到內存
  2. 解固化并實例化 nib 文件里對應的對象
  3. 建立 connections (outlet、action)
  4. 調用 awakeFromNib() 方法
  5. 將 nib 中可見的控件顯示出來

本地化

兩種策略

  1. App 本地化跟隨系統語言
  2. App 內部有一個可以設置語言的選項

本地化介紹

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 點擊 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 點擊 localize...,替換 zh-Hans.lproj 中的資源文件。

App 內設置語言的本地化

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 重構 "神VC"

代碼量龐大、結構臃腫、可維護性差的 VC。

使用 Object

  • 通常 VC 會成為很多對象的 delegate,需要處理很多回調。用 Object 替 VC 實現 delegate。
  • 將一些能用需求或交互模塊化在對應的 Object 里。將需求或交互與 VC 解耦。

用 External Object 重構 VC

只能在于 xib

IB 中的關鍵字總結

Swift

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

Objective-C

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

@IBDesignable

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

prepareForInterfaceBuild()

只需要將實時渲染的代碼放到 prepareForInterfaceBuild() 方法中就可以了,該方法并不會在程序運行時調用。

@IBInspectable

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

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

第7章 在 Interface Builder 開發中的技巧和 Bug


調整 View 的尺寸,使它與顯示內容的尺寸相適應

comment + =

查看各個 View 之間的距離

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

在 IB 中添加參考線

Editor -> Guides -> Add Horizontal Line

command + -

Editor -> Guides -> Add Vertical Line

command + ctrl + |

快速調整底層被擋住的 View 的位置

快速查看 View 的 UI 層次關系

command + shift + right click

連線小技巧

兩個窗口

使用吸管快速設置顏色

IB 中的復制與粘貼

command + c

command + v

利用 Media Library 快速設置圖片

IB 開發中遇到的一些小 bug

最好的做法就是重啟 Xcode。

  • 無法連線

IB 文件是否與源文件關聯

  • @IBAction 紅色提示

先在源文件中定義好方法,再從源文件 到 IB 文件進行 連線

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容