《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 文件进行 连线

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容