深入探究Views

视图(其类是UIView或UIView的子类的对象)知道如何将自己绘制到界面的矩形区域中。多亏了视图,你的应用程序才有一个可见的界面; 用户看到的一切最终都是因为视图。视图的创建和配置非常简单:“设置完后就无需再关注。” 例如,你可以在nib编辑器中配置UIButton; 当应用程序运行时,按钮出现,并正常工作。但您也可以实时地以强大的方式操纵视图。您的代码可以完成视图自身的部分或全部绘图; 它可以使视图出现和消失,移动,调整自身大小,并显示许多其他物理变化,可能跟随着动画一起。

视图也是一个responder (UIView是UIResponder的子类)。 这意味着视图受到用户交互的影响,比如点击和滑动。 因此,视图不仅是用户看到的界面的基础,也是用户触摸的界面的基础。组织你的视图,以便使正确的视图对给定的触摸作出反应,这允许您整洁而高效地配置代码。

视图层次结构是视图组织的主要模式。 一个视图可以有子视图; 子视图只有一个直接父视图。 这样就有了视图树。 这个层次结构允许视图来来去去 。 如果一个视图从界面被移除,它的子视图将被移除; 如果一个视图被隐藏(使其不可见),则它的子视图被隐藏; 如果一个视图被移动,它的子视图也随之移动; 视图中的其他更改同样与它的子视图共享。 视图层次结构也是responder chain的基础,尽管它与responder chain并不相同。

视图可以在nib创建,也可以在代码中创建。 总的来说,任何一种创建方法都不比另外方法更优;这取决于您的需求和偏好,以及应用程序的总体架构。

窗口和根视图

视图层次结构的顶部是应用程序的窗口。 它是UIWindow的一个实例(或者你自己的子类),它是UIView的子类。 你的应用程序应该只有一个主窗口。 它是在启动时创建的,不会被销毁或替换。 它是所有其他可见视图的背景,也是最终的父视图。 之所以其他视图是可见的,因为它们是应用程序窗口的某个深度的子视图。

如果你的应用可以在外部屏幕上显示视图,你将创建一个额外的UIWindow来包含这些视图; 但在这本书中,我假设只有一个屏幕,设备自己的屏幕,只有一个窗口。

应用程序如何启动

你的应用的根视图控制器是你的应用的UIWindow和它包含的界面之间的纽带。 在初始视图控制器被实例化之后,该实例将被分配给窗口的rootViewController属性。 一旦成为应用的根视图控制器,它的视图从此就占据了整个窗口。 根视图控制器本身将是视图控制器层次结构的顶部。

但是,你的应用程序在启动时,首先是如何拥有一个主窗口的,以及这个窗口是如何被填充和显示的? 如果你的应用使用一个主故事板,就会自动有一个被填充并显示的主窗口。 你的应用程序最终由一个对UIApplicationMain函数的调用组成。 (与Objective-C项目不同,一个典型的Swift项目不会在代码中显式地调用这个调用; 它在幕后为你被调用。) 以下是这个调用首先要做的一些事情:

  1. UIApplicationMain实例化UIApplication并保留这个实例,将作为共享的应用程序实例,您的代码稍后可以通过UIApplication.shared来引用这个实例。 然后实例化app delegate类。 (它知道哪个类是app delegate,因为它被标记为@UIApplicationMain。) 它保留app delegate实例,从而确保它将在应用程序的生命周期中持续存在,并将其分配为应用程序实例的delegate属性。

  2. UIApplicationMain查看应用程序是否使用主故事板。 它通过查看info.plist的键值“Main stroyboard file base name”(UIMainStoryboardFile)知道你是否在使用主情节串连板,以及它的名称。 如果需要,可以通过编辑target和在General窗格中更改Deployment Info中的主界面值来轻松编辑此键。 默认情况下,一个新的iOS项目有一个名为
    Main.storyboard的主故事板 ,并且主界面值是Main。

  3. 如果您的应用程序使用一个主要的故事板,UIApplicationMain实例化UIWindow并分配改实例给app delegate的window属性,保留它,从而确保窗口将持续应用程序的生命周期,还调整窗口大小,以便它最初将填充设备的屏幕。 这是通过将窗口的frame设置为屏幕的bounds来确保的。

  4. 如果你的应用使用一个主故事板,UIApplicationMain实例化那个故事板的初始视图控制器。 然后它将这个视图控制器实例分配给窗口的rootViewController属性,后者保留它。 当一个视图控制器成为主窗口的根视图控制器时,它的主视图(它的视图)成为你的主窗口的唯一的直接子视图——主窗口的根视图。 主窗口中的所有其他视图都是根视图的子视图。

    因此,根视图是视图层次结构中用户通常看到的最高对象。 在某些情况下,用户可能只会瞥见根视图后面的窗口;因此,您可能希望为主窗口分配一个合理的backgroudColor。 一般来说,您没必要改变窗口本身的任何其他内容。

  5. UIApplicationMain调用app delegate的application(_:didFinishLaunchingWithOptions:)。

  6. 应用程序的界面在包含它的窗口成为应用程序的关键窗口之前是不可见的。 因此,如果你的应用使用主故事板,UIApplicationMain调用窗口的实例方法makeKeyAndVisible。

也有可能编写一个没有主故事板的应用,或者有一个主故事板,但在某些情况下,通过覆盖部分或全部的自动UIApplicationMain行为,在启动时有效地忽略它。 这样的应用程序只需在代码中完成——通常是在application(_:didFinishLaunchingWithOptions:)——UIApplicationMain在应用程序有主故事板时自动完成的所有事情:

  1. 实例化UIWindow并将其指定为app delegate的window属性。 通过一些巧妙的编码,如果UIApplicationMain已经这样做了,我们可以避免这样做:
	self.window = self.window ??UIWindow()
  1. 实例化视图控制器,根据需要配置它,并将其分配为窗口的rootViewController属性。 如果UIApplicationMain已经分配了一个根视图控制器,这个视图控制器会替换它。

  2. 调用makeKeyAndVisible在窗口中显示它。 即使有主故事板,这也没有任何不好的影响,因为UIApplicationMain随后不会重复这个调用。

现在没有主故事板的应用并不常见,但是有时会覆盖一些自动UIApplicationMain行为的混合应用是。 一个频繁的场景是,我们的应用程序有一个登录或注册屏幕,如果用户没有登录,它

会在启动时出现,但在用户登录后的后续启动中不会出现。

为了实现这一点,我们只实现步骤2,让UIApplicationMain执行其他步骤。 在步骤2中,我们查看UserDefaults(或一些类似的存储),看看用户是否已经登录:

  • 如果是,我们跳过故事板的初始视图控制器跳到故事板中的下一个视图控制器。

  • 如果没有,我们什么也不做,留下UIApplicationMain来实例化故事板的初始视图控制器并使它成为窗口的rootViewController。

下面是总体思路:

func application(_ application:UIApplication, didFinishLaunchingWithOptions . launchOptions: [UIApplication]) LaunchOptionsKey: Any]?) -> Bool {
	if let rvc = self.window?.rootViewController{
		let ud = userdefault.standard
		if ud.string(forKey:"username") != nil {// user已经登陆
			let vc = rvc.storyboard!.instantiateViewController(withIdentifier:"userHasLoggedIn")
			self.window!.rootViewController = vc
		}
	}
	return true
}

子类化UIWindow

一个可能的变化是子类化UIWindow。 这在今天是不常见的,尽管它可能是一种干预命中测试(hit-testing)或目标动作(target–action)机制的方法。 要使用UIWindow子类的一个实例作为应用程序的主窗口,你需要阻止UIApplicationMain将一个普通的UIWindow实例指定为你的app delegate的window。 规则是,在UIApplicationMain实例化app delegate之后,它要求app
delegate实例获取其window属性的值:

  • 如果该值为nil UIApplicationMain实例化UIWindow并将该实例分配给app delegate的window属性
  • 如果那个值不是nil, UIApplicationMain不处理它。

因此,要使你的应用的主窗口成为你的UIWindow子类的一个实例,你所要做的就是将这个实例赋值为app delegate的window属性的默认值:

@UIApplicationMain
class AppDelegate: UIResponder UIApplicationDelegate {
	var window: UIWindow? = MyWindow()
/ /……
}

引用窗口

一旦应用程序运行,你的代码有多种方法来引用窗口:

  • 如果UIView在界面中,它会自动通过它自己的window属性引用窗口。 你的代码可能会在主视图的视图控制器中运行,self.view. window通常是引用窗口的最佳方式。

    你也可以使用UIView的window属性来询问它是否嵌入在窗口中; 如果不是,它的window属性是nil。 window属性为nil的UIView用户无法看到。

  • app delegate实例通过其window属性维护对窗口的引用。 您可以通过共享应用程序delegate属性从其他地方获得对app delegate的引用,并通过该引用可以引用window:

    let w = uiapplication.share.delegate!.window!!
    

    如果你更喜欢不那么通用的东西(并且需要较少极端的选项展开),将委托显式地转换到你的app委托类:

    let w = (uiapplication .share .delegate as!) AppDelegate).window!
    
  • 共享应用程序通过其key- window属性维护对窗口的引用:

    let w = uiapplication.share.keywindow!
    

    然而,这个引用有一点不稳定,因为系统可以创建临时的窗口,并将它们作为应用程序的关键窗口。

视图实战

在本章和后续章节中,您可能希望在自己的项目中试验视图。 如果你用单一视图应用模板开始你的项目,它会给你一个可能的最简单的应用——一个包含一个场景的主故事板,这个场景由一个视图控制器实例及其主视图组成。 就像我在前一节中描述,当应用程序运行时,视图控制器将成为应用程序的主窗口的rootViewController,其主视图将成为窗口的根视图。 因此,如果你能让你的视图成为那个视图控制器的主视图的子视图,它们会在app启动时出现在app的界面中。

在nib编辑器中,你可以将一个视图作为子视图从库中拖拽到主视图中,当应用程序运行时,它将在界面中被实例化。 然而,我的初始示例都将创建视图并在代码中将它们添加到界面中。 那么代码应该放在哪里呢? 最简单的地方是视图控制器的viewDidLoad方法,它由项目模板代码作为存根提供; 它在视图第一次出现在界面之前运行一次。

viewDidLoad方法可以通过self.view引用视图控制器的主视图。 在我的代码示例中,每当我说self.view时。 视图,你可以假设我们在一个视图控制器中和那个self.view 是这个视图控制器的主视图。 例如:

override func viewDidLoad() {
	super.viewDidLoad() //这是模板代码
	let v = UIView(frame:CGRect(x:100, y:100, width:50, height:50)) 
	v.backgroundColor = .red //小红方块
	self.view.addSubview(v) //将其添加到主视图中
}

试一试! 从单一视图应用模板创建一个新项目,并使视图控制器类的viewDidLoad看起来像这样。运行应用程序。你会在运行应用程序的界面上看到一个小的红色方块。

子视图,父视图

很久以前,也就是不太久远以前,一个视图拥有它自己的独特矩形区域。任何不是这个视图的子视图的视图的任何部分都不能出现在它里面,因为当这个视图重绘它的矩形时,它会擦除另一个视图的重叠部分。 这个视图的任何子视图的任何部分都不能出现在它的外部,因为视图只对它自己的矩形负责。

然而,这些规定逐渐放宽,从OS X 10.5开始,苹果推出了一个全新的视图绘制架构,完全取消了这些限制。 iOS视图绘制就是基于这个修改后的架构。 在iOS中,一个子视图的一部分或全部可以出现在它的父视图之外,一个视图可以与另一个视图重叠,可以在它的前面部分或全部绘制,而不需要成为它的子视图。

例如,图1-1显示了三个重叠的视图。 所有三个视图都有一个背景色,因此每个视图都完全由一个彩色矩形表示。 从这个可视化表示中,您无法确切地知道视图是如何关联的

在这里插入图片描述
图1 - 1 重叠的视图

在这里插入图片描述
图1 - 2 在nib编辑器中显示的视图层次结构

在视图层次结构中。 实际上,视图1是视图2的兄弟视图(它们都是根视图的直接子视图),而视图3是视图2的子视图。

在nib中创建视图时,您可以检查nib编辑器的文档大纲中的视图层次结构,以了解它们的实际关系(图1-2)。 在代码中创建视图时,您知道它们的层次关系,因为您创建了该层次结构。但是可视界面不会告诉你,因为视图重叠是非常灵活的。

然而,视图在视图层次结构中的位置是非常重要的。 首先,视图层次结构规定了绘制视图的顺序。 相同父视图的兄弟子视图有一个明确的顺序:一个在另一个之前绘制,因此如果它们重叠,它将看起来在它的兄弟视图之后。 类似地,父视图在它的子视图之前绘制,因此如果它们重叠,它将显示在它们后面。

如图1-1所示。 视图3是视图2的子视图,并绘制在其上。 视图1是视图2的兄弟姐妹,但它是后面的兄弟姐妹,因此它是在视图2和视图3的上面绘制的。 视图1不能出现在视图3后面,而是出现在视图2前面,因为视图2和3是子视图和父视图,它们是一起绘制的——它们都是在视图1之前或之后绘制的,这取决于兄弟视图的顺序。

通过在文档大纲中排列视图,可以在nib编辑器中管理这个分层顺序。 (如果您单击画布,你可以使用Editor→Arrange菜单的菜单项代替——Send to Front,Send to Back,Send Forward,Send Backward。) 在代码中,有一些方法来排列视图的兄弟顺序,我们稍后会讲到。

下面是视图层次结构的一些其他效果:

  • 如果一个视图被从它的父视图中移除或在父视图中移动,它的子视图也会随之移动。

  • 视图的透明度程度由它的子视图继承。

  • 视图可以有选择地限制子视图的绘图,这样视图之外的任何部分都不会显示出来。 这称为剪切,并使用视图的clipsToBounds属性进行设置。

  • 父视图拥有它的子视图,在内存管理的意义上,就像数组拥有它的元素一样; 当子视图不再是它的子视图时(它从这个视图的子视图集合中删除),或者当父视图本身不存在时,它负责释放子视图。

  • 如果视图的大小发生了变化,它的子视图可以自动调整大小。

UIView有一个superview属性(一个UIView)和一个subviews属性(一个UIView对象数组,前后顺序),允许你在代码中跟踪视图层次结构。 还有一个isDescendant(of:)方法允许您检查一个视图在任何深度上是否是另一个视图的子视图。

如果您需要对特定视图的引用,您可能会预先将其安排为属性,可能通过outlet。 或者,一个视图可以有一个数字标记(它的标记属性),然后可以通过在视图层次结构中发送viewWithTag(_:)消息来引用它。 要看到所有感兴趣的标记在其层次结构的区域内都是唯一的,这取决于您。

在代码中操作视图层次结构很容易。 这是赋予iOS应用动态质量的原因之一,它弥补了一个基本仅有一个窗口的事实。 您的代码完全有理由在用户眼前从父视图中提取整个视图层次结构并替换另一个视图! 你可以直接这样做; 你可以结合动画; 你可以通过视图控制器来控制它。

addSubview(_:)方法使一个视图成为另一个视图的子视图; removeFromSuperview从父视图的视图层次结构中取出一个子视图。 在这两种情况下,如果父视图是可见界面的一部分,子视图将在那一刻分别出现或消失; 当然,这个视图本身可能具有与之相关的子视图。 只要记住从父视图中删除子视图就会释放它; 如果你

要想在以后重用该子视图,首先需要保留它(通常是通过分配给属性)。

视图这些动态更改将会有事件通知。 要响应这些事件,需要子类化。 然后你就可以覆盖这些方法:

  • willRemoveSubview (), didAddSubview (: )

  • didMoveToSuperview willMove(toSuperview:)

  • didMoveToWindow willMove (toWindow:)

当调用addSubview(_:)时,该视图在其父视图的子视图中位于最后; 因此它被画在最后,意思是它出现在最前面。 这可能不是你想要的。 视图的子视图被索引,从0开始,这是最后面的,并且有方法在给定的索引处插入子视图,或者在特定视图的下面(后面)或上面(前面)插入子视图; 按索引交换两个同级视图; 对于将子视图移动到它的兄弟视图的前面或后面:

  • insertSubview(_:: )

  • insertSubview(:belowSubview:),insertSubview(:aboveSubview:)

  • exchangeSubview(withSubviewAt:)

  • bringSubviewToFront(),sendSubviewToBack(: )

奇怪的是,没有命令一次性删除视图的所有子视图。 然而,视图的子视图数组是内部子视图列表的不可变副本,因此循环遍历它并一次删除一个子视图是合法的:

myView.subviews.forEach {$0.removeFromSuperview()}

可见性和透明度

视图可以通过将isHidden属性设置为true而变得不可见,通过将其设置为false而再次可见。 隐藏视图将它(当然还有它的子视图)从可见j界面中取出,而不需要从视图层中实际删除它。一个隐藏的视图(通常)不会接收触摸事件,所以对于用户来说,它实际上就像视图不存在一样。 但是它是存在的,所以它仍然可以在代码中操作。

视图可以通过它的backgroundColor属性来分配背景颜色。 颜色是UIColor。 背景颜色为nil(默认值)的视图具有一个透明背景。 如果这样的视图没有自己附加的绘图,它将是不可见的! 然而,这种观点是完全合理的; 例如,具有透明背景的视图可以作为其他视图的方便的父视图,使它们一起工作。

视图可以通过alpha属性部分或完全透明:

1.0表示不透明,0.0表示透明,值可以介于两者之间

视图的alpha属性值影响其背景颜色的透明度和内容的透明度。 例如,如果一个视图显示了一幅图像,并且有一个背景色,并且它的alpha值小于1,那么背景色就会渗透到图像中(而视图后面的任何东西都会渗透到这两种颜色中)。 此外,视图的alpha属性会影响其子视图的视交叉‑! 如果父视图的alpha值为0.5,那么它的子视图的不透明度都不可能超过0.5,因为无论它们的alpha值是多少,都是相对于0.5绘制的。 一个完全透明(或非常接近它)的视图就像一个isHidden为真的视图:它及其子视图是不可见的,并且(通常)不能被触摸。

(更复杂的是,颜色也有一个alpha值。 例如,一个视图的alpha值为1.0,但背景仍然是透明的,因为它的backgroundColor的alpha值小于1.0。)

另一方面,视图的isOpaque属性是完全另一回事; 它对视图的外观没有影响。 相反,这个属性是对绘图系统的提示。 如果一个视图完全被不透明的材质填充,alpha值为1.0,因此该视图没有有效的透明度,那么如果您通过将其isOpaque设置为true来告知绘图系统这一事实,那么它可以被绘制得更高效(对性能的拖累更小)。 否则,应该将其isOpaque设置为false。 当你设置一个视图的背景色或alpha时,isOpaque值不会为你改变! 正确设置完全取决于您; 也许令人惊讶的是,默认情况是true。

框架(Frame)

一个视图的frame属性,一个CGRect,是它的矩形在父视图中的位置,在父视图的坐标系中。 默认情况下,父视图的坐标系统的原点在左上角,x坐标向右正增长,y坐标向下正增长。

将视图的框架设置为不同的CGRect值将重新定位视图,或调整其大小,或两者兼而有。 如果视图是可见的,那么该更改将在此时在界面中得到明显的反映。 另一方面,您还可以在视图不可见时设置视图的框架——例如,在代码中创建视图时。 在这种情况下,框架描述了当它被给定父视图时,视图将被定位在它的父视图中的什么位置。

UIView的指定初始化器是init(frame:),你通常会这样分配一个框架,特别是因为默认框架可能是CGRect.zero,这不是你想要的。 在代码中创建视图时忘记为它分配框架,然后想知道为什么添加到父视图时它没有出现,这是初学者常见的错误。 一个零大小框架的视图实际上是不可见的

(虽然可能仍然会看到它的子视图,但这可能会增加混乱)。 如果一个视图有一个你想要它采用的标准尺寸,特别是关于它的内容(就像一个UIButton关于它的标题),可以选择调用它的sizeToFit方法。

我们现在可以以编程方式生成如图1-1所示的界面:

let v1 = UIView(frame:CGRect(113, 111, 132, 194))
v1.backgroundColor = UIColor(red:1,green:0.4,blue:1,alpha: 1)
let v2 = UIView(frame:CGRect(41, 56, 132, 194))
v2.backgroundColor = UIColor(red:0.5,green:1,blue:0,alpha: 1)
let v3 = UIView(frame:CGRect(43, 197, 160, 230))
v3.backgroundColor = UIColor(red:1,green:0,blue:0,alpha: 1) self.view.addSubview(v1)
v1.addSubview(v2)self.view.addSubview(v3)

这段代码以及所有后续代码使用了一个没有参数标签的定制CGRect初始化器。
在这段代码中,我们通过使用addSubview(_:)将v1和v3(中间和左边的视图,它们是兄弟视图)插入视图层的顺序来确定它们的分层顺序。

当一个UIView从nib实例化时,它的init(frame:)不会被调用-取而代之的是init(coder:)。 (在UIView子类中实现init(frame:),然后想知道为什么视图从nib实例化时代码没有被调用,这是一个常见的初学者错误。)

边界(Bounds)和中心(Center)

假设我们有一个父视图和一个子视图,这个子视图嵌入(inset)10个点,如图1-3所示。 像insetBy(dx:dy:)这样的CGRect方法可以很容易地从另一个矩形派生出一个作为inset的矩形。 但是我们应该从哪个矩形中嵌入(inset)呢? 不是父视图的框架; 框架表示一个视图在它的父视图中的位置,以及在那个父视图的坐标中。 我们寻找一个CGRect,该CGRect在它自己的坐标中描述我们父视图的矩形,因为在该CGRect坐标中子视图的框架将被表达。 在视图自身坐标中描述视图矩形的CGRect是视图的bounds属性。

核心图形初始值设定项

从Swift 3开始,对核心图形方便构造函数(如CGRectMake)的访问被封堵了。 你不能再说:

let v1 = UIView(frame:CGRectMake(113, 111, 132, 194)) //编译错误

相反,您必须使用带标记参数的初始化器,如下所示:

let v1 = UIView(frame:CGRect(x:113, y:111, width:132, height:194))

我发现这很冗长,所以我编写了一个CGRect扩展,它添加了一个参数没有标签的初始化器。 因此,我可以继续简洁地说话,就像CGRectMake允许我做的那样:

let v1 = UIView(frame:CGRect(113, 111, 132, 194)) //感谢我的扩展

我使用这个扩展,以及类似的扩展对CGPoint, CGSize,和CGVector。 如果我的代码不能在您的机器上编译,可能是因为您需要添加这些扩展。

在这里插入图片描述
图1 - 3 从父视图插入的子视图

因此,生成图1-3的代码如下:

let v1 = UIView(frame:CGRect(113, 111, 132, 194))
v1.backgroundColor = UIColor(red:1,green:0.4,blue:1,alpha: 1)
let v2 = UIView(frame:v1.bounds).insetBy(dx: 10, dy: 10)
v2.backgroundColor = UIColor(red:0.5, green:1,blue:0,alpha: 1)
self.view.addSubview(v1)
v1.addSubview(v2)

你会经常这样使用视图的边界。 当你需要在视图中放置内容的坐标时,无论是手动绘制还是放置子视图,你都需要参考视图的边界。如果你改变一个视图的边界大小,你就改变了它的框架。 视图框架的变化是围绕着它的中心周围来发生的,中心保持不变。 举个例子:

在这里插入图片描述
图1 - 4 子视图完全覆盖它的父视图

let v1 = UIView(frame:CGRect(113, 111, 132, 194))
v1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1)
let v2 = UIView(frame:v1.bounds.insetBy(dx: 10, dy: 10))
v2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1)
self.view.addSubview(v1)
v1.addSubview(v2)
v2.bounds.size.height += 20
v2.bounds.size.width += 20

出现的是一个矩形; 子视图完全覆盖它的父视图,它的框架与父视图的边界相同。 对insetBy的调用从父视图的边界开始,并从左、右、上、下各削减10个点,以设置子视图的框架(图1-3) 。 但是随后我们在子视图的边界高度和宽度上添加了20个点,因此在子视图的框架高度和宽度上也添加了20个点(图1-4)。 子视图的中心没有移动,所以我们有效地将10个点放回子视图框架的左、右、上、下。

如果你改变一个视图的边界原点(origin),你移动它的内部坐标系的原点(origin)。 当你创建一个UIView时,它的边界坐标系的零点(0,0)在它的左上角。 因为子视图相对于它的父视图的坐标系被定位在它的父视图中,父视图边界原点的改变将改变子视图的表观位置。 为了说明这一点,我们再次从子视图的父视图中均匀地嵌入开始,然后改变父视图的边界原点:

let v1 = UIView(frame:CGRect(113, 111, 132, 194))

v1.backgroundColor = UIColor(red:1,green:0.4,blue:1,alpha: 1)

let v2 = UIView(frame:v1.bounds).insetBy(dx: 10, dy: 10) 

v2.backgroundColor = UIColor(red:0.5,green:1,blue:0,alpha: 1) 

self.view.addSubview(v1)

v1.addSubview(v2) 

v1.bounds.origin.x += 10

v1.bounds.origin.y += 10

在这里插入图片描述
图1 - 5 父视图的边界原点已经移动

父视图的大小和位置没有变化。 但是子视图已经向上和向左移动,因此它与父视图的左上角齐平(图1-5)。基本地,我们所做的是对父视图说,“不要调用左上角的点(0.0,0.0),而是调用那个点(10.0,10.0)。 因为子视图的框架原点本身位于(10.0,10.0),所以子视图现在会接触到父视图的左上角。改变视图边界原点的效果似乎是反向的——我们在正方向上增加了父视图的原点,但是子视图在负方向上移动——但是这样想:视图的边界原点与它的框架的左上角重合。

我们已经看到改变视图的边界大小会影响它的框架大小。反过来也是正确的:改变视图的框架大小会影响它的边界大小。不受视图边界大小影响的是视图的中心。 这个属性,就好像frame属性那样,在父视图的坐标系中,表示子视图在父视图中的位置; 特别地,正是子视图自身边界中心的父视图中的位置,从这些边界得到的点如下:

let c = CGPoint(theView.bounds.midX, theView.bounds.midY)

因此,视图的中心是在视图的边界和它的父视图的边界之间建立位置关系的单个点。

改变视图的边界并不会改变它的中心; 改变视图的中心并不会改变它的边界。 因此,视图的边界和中心是正交的(不相交的),并且完全描述了视图的大小和它在父视图中的位置。 因此视图的框架是多余的! 事实上,frame属性仅仅是center值和bounds值的转换表达式。 在大多数情况下,这对你来说并不重要; 不管怎样,你会用到frame属性。 当您第一次从头创建视图时,指定的初始化器是init(frame:)。 你可以改变框架,边界的大小和中心也会随之改变。 你可以改变边界的大小或中心,框架也会相应改变。然而,在父视图中定位和调整视图大小的正确和最可靠的方法是使用它的边界和中心,而不是它的框架; 在某些情况下,框架是没有意义的(或者至少表现得非常奇怪),但是边界和中心总是有效的。

我们已经看到,每个视图都有它自己的坐标系,由它的边界表示,视图的坐标系与它的父视图的坐标系统有明确的关系,由它的中心表示。对于窗口中的每个视图都是如此,因此可以在同一个窗口中的任意两个视图的坐标之间进行转换。 提供了方便的方法来对CGPoint和CGRect执行此转换:

  • convert(_:to:)

  • convert(_:from:)

第一个参数是CGPoint或CGRect。 第二个参数是UIView; 如果第二个参数为nil,则将其视为窗口。 接收者是另一个UIView; CGPoint或CGRect在其坐标和第二个视图的坐标之间进行转换。

例如,如果v1是v2的父视图,那么为了使v2在v1中居中,你可以说:

v2.center= v1.convert (v1.center,from:v1.superview)

然而,更常见的方法是将子视图的中心放在父视图的边界中心,如下所示:

v2.center = CGPoint(v1.bounds.midX, v1.bounds.midY)

这是很常见的做法,我已经编写了一个扩展,它将CGRect的中心作为其中心属性,允许我这样说:

v2.center= v1.bounds.center

注意,以下不是正确的方式来让子视图v2在父视图v1中居中:

v2.center= v1.center//那行不通!

试图像那样让一个视图在另一个视图中居中是初学者常见的错误。 它不可能成功,并且会有不可预知的结果,因为两个中心值在不同的坐标系中。

当通过设置其中心来设置视图的位置时,如果视图的高度或宽度不是一个整数(或者single-resolution屏幕上,甚至不是一个整数),视图可以最终偏差:它的点值在一个或两个尺寸的方式位于在屏幕像素之间。 这会导致视图显示不正确;例如,如果视图包含文本,则文本可能是模糊的。可以通过检查发现这种情况,在模拟器上 Debug →Color Misaligned Images。 一个简单的解决方案是将视图的框架设置为它自己的integral。

变换(Transform)

视图的transform属性改变了视图的绘制方式——例如,它可以改变视图的表观大小和方向——而不影响其边界和中心。 转换后的视图继续正常工作:旋转的按钮,例如,仍然是一个按钮,可以在其明显的位置和方向上点击。

转换值是一个CGAffineTransform,它是一个结构体,代表一个3×3变换矩阵的9个值中的6个值(其他3个值是常数,所以没有必要在结构体代表他们)。 你可能已经忘记了大学的线性代数, 所以你可能不记得什么是变换矩阵。 有关更多细节,请参阅官方文档中的Apple’s Quartz 2D Programming Guide的“Transforms”一章,特别是“The Math Behind the Matrices”一节。 但是你并不需要知道这些细节,因为初始化器(initializer)是用来创建三种基本类型的转换的:旋转、缩放和转换(例如改变视图的表观位置)。 第四种基本的转换类型,倾斜或剪切,是没有初始化器(initializer)。

默认情况下,视图的转换矩阵是CGAffineTransform.identity,恒等转变。它没有可见的效果,所以你对它没有感知。 您所做的任何转换都是围绕视图的中心进行的,该中心保持不变。下面是一些代码来演示转换的使用:

let v1 = UIView(frame:CGRect(113, 111, 132, 194))

v1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1)

let v2 = UIView(frame:v1.bounds.insetBy(dx: 10, dy: 10))

v2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1)

self.view.addSubview(v1)

v1.addSubview(v2)

v1.transform = CGAffineTransform(rotationAngle: 45 * .pi/180)

print(v1.frame)

视图v1的transform属性设置为旋转变换。结果(图1-6)是视图似乎是顺时针旋转45度。 (我用的是角度,但Core Graphics用的是弧度,所以我的代码必须转换。) 观察视图的center属性不受影响,因此旋转似乎是围绕视图的中心发生的。 此外,视图的边界属性是不受影响的; 内部坐标系不变,因此子视图相对于其父视图在相同的位置绘制。 然而,视图的框架现在是无用的,因为不仅仅矩形可以描述父视图表面被视图占据的区域; 框架的实际值大约(63.7,92.7230.5230.5)描述了围绕视图的表面位置的最小边界矩形。 规则是如果一个视图的变换不是恒等变换(identity transform),你不应该设置它的框架; 此外,本章后面讨论的子视图的自动调整大小要求父视图的转换是恒等变换。

在这里插入图片描述
图1 - 6 旋转变换
在这里插入图片描述
图1 - 7 尺寸变换

假设不是旋转变换,而是缩放变换,就像这样:

v1. transform = CGAffineTransform(scaleX:1.8, y:1)

v1视图的bounds属性仍然不受影响,因此子视图相对于它的父视图仍然在相同的位置绘制; 这意味着这两个视图似乎是水平拉伸在一起的(图1-7)。 没有边界或中心因此转换运用产生不利影响!

提供了用于转换现有transform的方法。 这个操作不是可交换的; 也就是说,顺序很重要。 (你现在开始回想起大学时的数学了,不是吗?) 如果你从一个将视图向右平移的变换开始,然后应用一个45度的旋转,旋转后的视图会出现在它原来位置的右边; 另一方面,如果您从一个将视图旋转45度的转换开始,然后向右应用一个平移,则“right”的含义已经更改,因此旋转后的视图从其原始位置向下显示45度。 为了演示不同之处,我将从一个子视图开始,它与父视图完全重叠:

在这里插入图片描述
图1 - 8 平移,然后旋转

在这里插入图片描述
图1 - 9 旋转,然后平移

let v1 = UIView(frame:CGRect(20, 111, 132, 194))

v1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1)

let v2 = UIView(frame:v1.bounds)

v2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1)

self.view.addSubview(v1)

v1.addSubview(v2)

然后我将对子视图应用两个连续的转换,让父视图显示子视图的原始位置。 在这个例子中,我平移然后旋转(图1-8):

v2.transform = CGAffineTransform(translationX:100, y:0).rotated(by: 45 * .pi/180)

在这个例子中,我旋转然后平移(图1-9):

v2.transform =CGAffineTransform(rotationAngle: 45 * .pi/180).translatedBy(x: 100, y: 0)

在这里插入图片描述
图1 - 10 旋转,然后平移,然后反转

串联方法是利用矩阵多重迭加的方法将两个变换矩阵串联起来。 同样,这个操作不是可交换的。 顺序与链转换时的顺序相反。 这段代码给出了与前一个示例相同的结果(图1-9):

let r = CGAffineTransform(rotationAngle: 45 * .pi/180)

let t = CGAffineTransform(translationX:100, y:0)

v2.transform = t.concatenating(r) // not r.concatenating(t)

要从多个变换的组合中删除一个变换,应用它的逆转。 逆转(inverted)方法允许您获得给定仿射变换的逆转。 再次,顺序很重要。 在本例中,我旋转子视图并将其移到“右边”,然后删除旋转(图1-10):

let r = CGAffineTransform(rotationAngle: 45 * .pi/180)

let t = CGAffineTransform(translationX:100, y:0)

v2.transform = t.concatenating(r)

v2.transform = r.inverted().concatenating(v2.transform)

最后,由于没有用于创建倾斜(剪切)转换的初始化器,我将通过手动创建一个来说明,无需进一步解释(图1-11):

v1.transform = CGAffineTransform(a:1, b:0, c:-0.2, d:1, tx:0, ty:0)

转换非常有用,特别是作为临时的可视指示器。 例如,您可以通过应用稍微放大的转换来引起对视图的注意,然后应用恒等转换将其恢复到原来的大小,并使这些更改具有动画效果(第4 章)。

视图的环境

视图位于更大的环境中。 最终的超视图是窗口; 窗口之外是屏幕,屏幕本身有一个坐标系。 此外,这种环境是动态的。

在这里插入图片描述
图1 - 11 斜(剪切)

该应用程序可以随着用户旋转设备而旋转,并可以在iPad多任务环境中调整大小; 屏幕,窗口和应用的主视图可以改变大小。 本节讨论视图如何与它们的整体环境相关联。

窗口坐标和屏幕坐标

设备屏幕没有框架,但是有边界。 主窗口没有父视图,但它的框架是根据屏幕的边界设置的:

let w = UIWindow(frame: UIScreen.main.bounds)

您可以省略frame参数; 效果是一样的:

let w = UIWindow()

因此,窗口开始时填充屏幕,通常会继续填充屏幕,因此,在大多数情况下,窗口坐标就是屏幕坐标。

在ios7和之前,屏幕坐标是不变的。 transform属性奠定一个iOS应用程序旋转界面的能力的核心:窗口的框架和边界被锁定到屏幕,然后一个应用程序通过对根视图应用旋转变换来使界面旋转,以响应设备朝向的变化,以便它的原点移到现在的用户所看到的视图的左上角。

但iOS 8引入了一个重大变化:当应用程序旋转以响应设备的旋转时,屏幕(以及与之一起的窗口)就会旋转。 故事中的任何视图——无论是窗口、根视图还是它的任何子视图——在应用程序的界面旋转时都不会接收到旋转转换。相反,屏幕边界的尺寸有一个移位(窗口边界的尺寸和它的根视图边界的尺寸也有一个相应的移位):在纵向方向上,尺寸大于宽度,但在横向方向上,尺寸大于高。

因此,实际上有两组屏幕坐标。 每一个都通过UICoordinateSpace报告,UICoordinateSpace是一个协议(也被UIView采用),它提供了一个bounds属性:

UIScreen coordinateSpace属性

这个坐标空间旋转。 当应用程序旋转时,它的边界高度和宽度被调换,以响应设备方向的变化; 它的原点在应用程序的左上角。

UIScreen fixedCoordinateSpace属性

这个坐标空间是不变的。 它的边界原点位于物理设备的左上角,始终与设备的硬件按钮保持相同的关系,而不管设备本身是如何被持有的。为了帮助您在坐标空间之间进行转换,UICoordinateSpace提供了与我在上一节中列出的坐标转换方法平行的方法:

  • convert(_:from:)

  • convert(_:to:)

第一个参数是CGPoint或CGRect。 第二个参数是UICoordinateSpace,它可以是UIView或UIScreen; 接受者也是如此。例如,假设我们的界面中有一个UIView v,我们想知道它在固定设备坐标中的位置。 我们可以这样做:

let screen = UIScreen.main.fixedCoordinateSpace

let r = v.superview!.convert(v.frame, to: screen)

假设我们有一个主视图的子视图,在主视图的左上角。 当设备和应用处于纵向时,子视图的左上角在窗口坐标和屏幕fixedCoordinateSpace坐标中为(0.0,0.0)。 当设备向左旋转到横屏方向时,如果应用旋转进行响应,窗口也会旋转,所以从用户的角度来看子视图仍然在左上角,在窗口坐标中仍然在左上角。 但是在屏幕fixedCoordinateSpace坐标中,子视图的左上角的x坐标将有一个大的正值,因为原点现在在左下方,它的x向上正增长。

然而,你需要这些信息的场合是很少的。 实际上,我的经验是,甚至很少考虑窗口坐标。 将让你感兴趣的是:应用程序的所有可视的行动发生在你的根视图控制器的主视图范围内,该主视图的边界可能是最高的坐标系统,当应用旋转来响应设备方向变化时为你自动调整。

特征集合(Trait Collection)和尺寸类(Size Classs)

关于应用旋转等的一个突出事实不是旋转本身而是应用尺寸比例的变化。 例如,考虑根视图的子视图,当设备处于纵向显示时,位于屏幕右下角。 如果根视图的边界宽度和边界高度被有效地转换了,那么这个可怜的旧子视图现在将超出边界高度,因此不在屏幕上——除非你的应用程序以某种方式响应这个变化来重新定位它。(这样的回答被称为布局,这个主题将占据本章的大部分内容。)

环境的维度特征体现在一组size classs中,这些size classs由视图的traitCollection属性提供。 从窗口向下的界面中的每个视图,以及视图是界面一部分的任何视图控制器,都从环境中继承其traitCollection属性的值,该属性通过实现UITraitEnvironment协议而具有。

traitCollection是UITraitCollection,一个值类。 ios8引入了UITraitCollection; 从那时起,它一直在增长,现在承载着大量描述环境的属性。 除了它的displayScale(屏幕分辨率)和userInterfaceIdiom(一般设备类型,iPhone或iPad), traitCollection现在还会报告设备的强制触摸能力和显示范围等信息。 但就一般观点而言,我们只特别关注两个性质:

horizontalSizeClass verticalSizeClass

UIUserInterfaceSizeClass值是.regular或.compact。

这些是size classs。 结合起来,当你的应用占据整个屏幕时,它们通常有以下含义:

  • 水平和垂直size classs都是.regular

    我们在iPad上运行。

  • 水平size classs是.compact,垂直size classs是.regular

    我们在iPhone上运行,app是纵向的。

  • 水平size classs是.regular,垂直size classs是.compact

    我们在iPhone 6/7/8 Plus、iPhone XR或iPhone XS Max(“大”iPhone)上运行,应用程序是横向的。

  • 水平和垂直size classs都是.compact

    我们运行在iPhone(不是大iPhone)上,应用是横向的。

(你的应用程序可能不会因为iPad多任务处理而占据整个屏幕。)
当应用程序运行时,size classs的traitCollection属性可以改变。通常,iPhone上的size classs反映了应用程序的方向——随着应用程序的旋转,应用程序的方向会随着设备方向的变化而变化。因此,无论是在应用程序启动时,还是在应用程序运行时traitCollection发生变化时,traitCollectionDidChange(_:)消息都将沿着UITraitEnvironments的层级传播(对于我们的目的,主要是指视图控制器和视图); 提供旧的traitCollection(如果有的话)作为参数,新的traitCollection可以作为self.traitCollection来检索。

可以自己构建一个traitCollection。 (在下一章中,我将举例说明为什么它可能有用。) 但奇怪的是,您不能直接设置任何traitCollection属性; 相反,您可以通过一个只确定一个属性的初始化器来形成一个traitCollection,如果您想添加更多的属性设置,您必须通过调用init(traitsFrom:)和一个traitCollection数组来组合traitCollection。 例如:

let tcdisp = UITraitCollection(displayScale: UIScreen.main.scale)

let tcphone = UITraitCollection(userInterfaceIdiom: .phone)

let tcreg = UITraitCollection(verticalSizeClass: .regular)

let tc1 = UITraitCollection(traitsFrom: [tcdisp, tcphone, tcreg])

init(traitsFrom:)数组的工作原理类似于继承:有序的交集是按顺序形成的。 如果将两个traitCollection组合起来,并且它们都设置了相同的属性,那么稍后出现在数组中或者继承层次结构中更靠后的traitCollection优先。如果一个设置了属性,而另一个没有,那么设置属性的那个优先。因此,如果您创建一个traitCollection,那么如果traitCollection发现自己在继承层次结构中,那么任何未指定属性的值都将被继承。

要比较traitCollection,调用containsTraits(in:)。 如果参数traitCollection的每个指定属性的值与这个traitCollection的值匹配,那么返回true。

不能仅通过设置视图的traitCollection就将traitCollection直接插入继承层次结构; traitCollection不是一个可设置的属性。 相反,您将使用一个特殊的overrideTraitCollection属性或方法;

布局

我们已经看到当父视图的边界原点改变时,子视图就会移动。 但是当父视图的边界(或框架)大小改变时,子视图会发生什么呢?自然而然地,什么也没发生。子视图的边界和中心没有改变,父视图的边界原点没有移动,所以子视图相对于它的父视图的左上角保持相同的位置。然而,在现实生活中,这往往不是你想要的。 当子视图的父视图的边界大小发生变化时,需要对它们进行调整和重新定位。 这叫做布局。

下面是一些动态调整父视图大小的方法:

  • 你的应用程序可能会响应用户旋转设备90度,通过旋转设备本身,使其顶部移动到屏幕的新顶部,匹配其新参数,从而改变其边界的宽度和高度值。

  • iPhone应用程序可能会在不同纵横比的屏幕上启动:例如,iPhone 5s的屏幕相对较晚型号的iPhone短,应用程序的界面可能需要适应这种差异。

  • 一个通用的应用程序可能会在iPad或iPhone上发布。 应用程序的界面可能需要适应它所在屏幕的大小。

  • 从nib实例化的视图,例如视图控制器的主视图或表视图单元,可能会被调整大小以适应它所在的界面。

  • 视图可能会响应其周围视图中的更改。 例如,当动态显示或隐藏导航栏时,剩余的界面可能会缩小或增大以补偿,从而填充可用的空间。

  • 用户可能会改变iPad上应用程序窗口的宽度,作为iPad多任务界面的一部分。

在上述任何一种情况下,都可能需要布局。大小发生变化的视图的子视图将需要进行移位、更改大小、重新分布或以其他方式进行补偿,以使界面看起来仍然良好并保持可用。布局主要有三种方式:

手动布局

父视图在调整大小时向layoutSubviews发送消息; 因此,要手动布局子视图,请提供您自己的子类并重写layoutSubviews。很明显,这可能需要很多工作,但这意味着你可以做任何你喜欢的事情。

Autoresizing

自动调整大小是自动执行布局的最古老方法。当它的父视图的调整大小时,子视图将按照由autoresizingMask属性值规定的规则进行响应,该规则它描述了子视图和它的父视图之间的调整大小关系。

Autolayout

Autolayout依赖于视图的约束。 约束(NSLayoutConstraint的一个实例)是一个功能完备的对象,它的数值描述了视图的大小或位置的某些方面,通常根据其他视图来描述; 它比autoresizingMask更复杂、更具描述性和更强大。 多个约束可以应用于单个视图,并且它们可以描述任意两个视图(不仅仅是子视图及其父视图)之间的关系。 自动布局是在layoutSubviews里面并在后台来实现的;实际上,约束允许您无需代码就可以编写复杂的layoutSubviews功能。

你的布局策略可以包括这些的任何组合。 人工布局的需求很少,但如果你需要,它就会出现; 从某种意义上说,autoresizing是默认的,在某种场景,可以伴随autolayout作为一种可选的选择,使用autolayout的视图可以与使用autoresizing的视图共存。 然而,在现实生活中,您的所有视图都很可能选择autolayout,因为它非常强大,最适合帮助您的界面适应各种屏幕大小。

视图的默认布局行为取决于它是如何创建的:

在代码中

默认情况下,代码创建并添加到界面的视图使用autoresizing,而不是autolayout。 这意味着,如果您希望这样的视图使用autolayout,就必须有意避免使用autoresizing,本章稍后将对此进行解释。

在nib文件中

所有新的.storyboard和.xib文件选择autolayout。 要查看这一点,在Project导航中选择文件,显示File检查器,并检查“Use Auto Layout”复选框。 这意味着它们的视图已经为autolayout做好了准备。 但是nib编辑器中的视图仍然可以使用autoresizing,即使勾选了“Use Auto Layout”,我将在后面解释。

自动调整尺寸(Autoresizing)

Autoresizing从一个概念上来说可看做是分配子视图“弹簧(spings)和支柱(struts)”的问题。 弹簧可以伸展; 一个支撑不能。 弹簧和支柱可以配置在内部或外部,水平或垂直。 因此,您可以指定(使用内部spring和struts)视图是否以及如何调整大小,以及(使用外部spring和struts)视图是否以及如何重新定位。 例如:

  • 想象一个子视图在它的父视图中居中,并保持居中,但是随着父视图的大小调整它自己。 它会有外部的支柱和内部的弹簧。

  • 想象一个子视图在它的父视图中居中并保持居中,而不是在父视图调整大小时调整它自己。 它外部有弹簧内部有支柱。

  • 假设有一个OK按钮,它将停留在父视图的右下角。 它的内部有支板,右侧和底部有支柱,顶部和左侧有弹簧。

  • 想象一个文本字段停留在它的父视图的顶部。 它随着父视图的扩展而变宽。 它外部有支柱,但底部有弹簧; 它内部有一个垂直支柱和一个水平弹簧。

在代码中,通过视图的autoresizingMask属性(UIView.AutoresizingMask)设置spring和struts的组合,该属性是位掩码(UIView.AutoresizingMask),以便您可以组合选项。 选项代表弹簧; 没有指定的是支柱。 默认值是空集,显然是指所有的struts,但它当然不能是所有的struts,因为如果父视图被调整了大小,就需要更改某些东西,因此在实际情况下,一个空的autoresizingMask与.flexbleRightMargin和.flexbleBottomMargin是一样的。

在调试中,当您将UIView的日记输出到控制台时,它的autoresizingMask将使用单词“autoresize”和一系列弹簧(springs)来报告。边距为LM、RM、TM、BM;内部尺寸是W和H, 例如:
autoresize = LM+TM表示左边距和上边距是灵活;
autoresize = W+BM 表示宽度和下边距灵活的。

为了演示自适应大小,我将从一个视图和两个子视图开始,一个位于顶部,另一个位于右下角(图1-12):

let v1 = UIView(frame:CGRect(100, 111, 132, 194))

v1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1)

let v2 = UIView(frame:CGRect(0, 0, 132, 10))

v2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1)

let v1b = v1.bounds

let v3 = UIView(frame:CGRect(v1b.width-20, v1b.height-20, 20, 20))

v3.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)

self.view.addSubview(v1)

v1.addSubview(v2)

v1.addSubview(v3)

对于这个例子,我将在两个子视图中添加应用spring和struts的代码,使它们的行为类似于前面我假设的文本字段和OK按钮:

v2.autoresizingMask = .flexibleWidth

v3.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin]

现在我将调整父视图的大小,从而实现自动调整大小; 如您所见(图1-13),子视图仍然固定在正确的相对位置:

在这里插入图片描述
图1-12 autoresizing之前
在这里插入图片描述
图1-13 autoresizing之后

v1.bounds.size.width += 40
v1.bounds.size.height -= 50

如果自动调整大小还不够复杂,无法实现您想要的效果,那么您有两个选择:

• 将其与layoutSubviews中的手动布局相结合。 自动大小是在调用layoutsubview之前发生的,因此您的layoutSubviews代码可以自由进入并整理autoresizing不太正确的内容。

• 使用autolayout。 这实际上是相同的解决方案,因为autolayout实际上是一种将功能注入layoutsubview的方法。 但是使用自动布局要比编写自己的layoutSubviews代码容易得多!

自动布局(Autolayout)和约束(Constraints)

Autolayout是一种可选的嵌入技术,在每个单独视图的层面上。 您可以使用自动大小和自动布局在同一界面的不同区域-甚至同一视图的不同子视图。 一个同级视图可以使用自动布局, 而另一个同级视图可以不使用,一个父视图可以使用自动布局,而它的一些或所有子视图可以不使用。

但是,autolayout是通过父视图链实现的,因此如果一个视图使用autolayout,那么它的所有父视图也会自动使用autolayout; 几乎可以肯定的是:如果其中一个视图是视图控制器的主视图,视图控制器就会接收与autolayout相关的事件。

但是视图如何选择使用autolayout呢? 仅仅通过参与一个约束。 约束是告诉autolayout引擎希望它在这个视图上执行布局以及希望视图如何布局的方式。

autolayout约束,或简称约束,是一个NSLayoutConstraint实例,它描述一个视图的绝对宽度或高度,或者描述一个视图的属性与另一个视图的属性之间的关系。 在后一种情况下,两个属性不需要是相同的,并且两个视图不需要是同级的(同一父视图的子视图)或父子(父视图和子视图),唯一的要求就是,他们共享一个共同的祖先(某个父视图的视图层次)。

以下是NSLayoutConstraint的主要属性:

firstItem,firstAttribute,secondItem,secondAttribute

这两个视图及其各自的属性(NSLayoutConstraint.Attribute)参与到这个约束中。 如果约束描述的是视图的绝对高度或宽度,则第二个视图将为nil,第二个属性将为. notanAttribute。除此之外,可能的属性值为:

• .width,.height

• .top,.bottom

• .left,.right,.leading,.trailing

• .centerX, .centerY

• .firstBaseline, .lastBaseline

.firstBaseline主要适用于多行标签,是从标签顶部向下一段距离; . lastbaseline是从标签底部向上的一段距离。

其他属性的含义在直观上是显而易见的,除了你可能想知道.lead和.trailing是什么意思:它们是.left和.right的国际对等的,在你的应用程序是本地化的,并且其语言是从右到左编写的系统 上自动颠倒它们的含义。 在这样的系统中,整个界面是自动反转的——但只有在您使用了.leading和.trailing约束的情况下,该界面才能正常工作。

乘数(multiplier), 常数(constant)

这些数字将应用于第二个属性的值,以确定第一个属性的值。 multiplier乘以第二个属性值;常数加到乘积中。 将第一个属性设置为结果。 基本上,你在写一个等式a1 = m*a2 + c,其中a1和a2是两个属性,m和c是乘数和常数。 因此,在简并(degenerate)的情况下,第一个属性的值等于第二个属性的值,乘数为1,常数为0。 如果你绝对地描述一个视图的宽度或高度,乘数将是1,常数将是宽度或高度值。

关系(relation)

两个属性值如何相互关联,由乘数和常数进行修改。 这是一个运算符,它位于我在上一段公式中放置等号的位置。可能的值为(nslayoutconstraint . relationship):

  • equal
  • lessThanOrEqual
  • greaterThanOrEqual

优先级(priority)

优先级值从1000 (required)到1不等,某些标准行为具有标准优先级。 约束可以有不同的优先级,决定它们的应用顺序。 从ios11开始,优先级不是一个数字,而是一个UILayoutPriority结构体,它将数值包装成它的rawValue,可以用init(rawValue:)初始化。
约束属于视图。视图可以有很多约束:UIView有约束属性,以及下面这些实例方法:

  • addConstraint (: ), addConstraints (: )
  • removeConstraint (: ), removeConstraints (: )

接下来的问题是,给定的约束应该属于哪个视图。 答案是:从约束中涉及的两个视图中最接近视图层次的视图。 如果可能,它应该是这些视图之一。 例如,如果约束规定了一个视图的绝对宽度,那么它就属于那个视图; 如果它将视图的顶部与其父视图的顶部相关联,那么它就属于该父视图; 如果它对齐两个兄弟视图的顶部,它就属于它们的公共父视图。

但是,您可能永远不会调用这些方法中的任何一个! 从ios8开始,不需要显式地向特定视图添加约束,您可以使用NSLayoutConstraint类方法activate(:)激活约束,该方法接受一个约束数组。 激活的约束会自动添加到正确的视图中,从而使您不必确定将是哪个视图。 还有一个方法deactivate(: ),它从视图中删除约束。 此外,约束具有isActive属性; 您可以将其设置为激活或禁用单个约束,另外它还会告诉您此时给定的约束是否是界面的一部分。

NSLayoutConstraint属性是只读的,但是优先级、常量和isActive除外。 如果您希望更改现有约束的任何其他内容,则必须删除该约束并添加一个新约束。

一旦您使用显式约束来定位和大小视图,不要设置它的框架(或边界和中心); 单独使用约束。否则,当调用layoutSubview时,该视图将跳转回其约束位置。 (不过,您可以在layoutSubviews的实现中设置视图的框架,这样做非常正常。)

隐式Autoresizing约束

单个视图可以选择自动布局的机制会突然涉及自动布局中其他视图,即使这些其他视图以前没有使用自动布局。 因此,当这样一个视图涉及到自动布局时,需要有一种方法来为它生成约束——约束确定该视图的位置和大小,与frame和autoresizingMask确定它们的方式相同。 autolayout引擎会帮你解决这个问题:它会读取视图的frame和autoresizingMask设置,并将它们转换成隐式的约束(类 NSAutoresizingMaskLayoutConstraint)。 autolayout引擎只有在将视图的translatesAutoresizingMaskIntoConstraints属性设置为true时才会以这种特殊的方式处理视图,而这恰好是默认值。

为了演示,我将分两个阶段构造一个示例。 在第一阶段,我在代码中向界面添加了一个不使用autolayout的UILabel。 我将确定这个视图的位置在屏幕右上方附近。 为了使它保持在接近右上角的位置,它的自动大小掩码将是 [.flexibleLeftMargin .flexibleBottomMargin]:

let lab1 = UILabel(frame:CGRect(270,20,42,22))
lab1.autoresizingMask = [.flexibleLeftMargin, .flexibleBottomMargin]
lab1.text = "Hello"
self.view.addSubview(lab1)

如果我们现在旋转设备(或模拟器窗口),应用程序旋转到静止状态,通过自动调整大小,标签保持在右上角附近的正确位置。

但是,现在我将添加第二个使用autolayout的label—特别是,我将通过一个约束将其附加到第一个标签上(这段代码的含义将在后面几节中阐明; 现在就接受吧)

let lab2 = UILabel()
lab2.translatesAutoresizingMaskIntoConstraints = false
lab2.text = "Howdy"
self.view.addSubview(lab2)
NSLayoutConstraint.activate([
	lab2.topAnchor.constraint(equalTo: lab1.bottomAnchor, constant: 20),
	lab2.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20)
])

这将导致自动布局中包含第一个标签。 因此,第一个标签魔法般地获得了4个自动生成的隐式约束,例如,当它的父视图被调整大小时,赋予它相同的大小和位置,以及之前以frame和autoresizingMask方式配置时相同的行为:

<NSAutoresizingMaskLayoutConstraint:0x6000002818b0 h=&-- v=--&
	UILabel:0x7f9d3820bf80'Hello'.midX == UIView:0x7f9d383079d0.width-29>
<NSAutoresizingMaskLayoutConstraint:0x60000009fe50 h=&-- v=--&
	UILabel:0x7f9d3820bf80'Hello'.midY == 31>
<NSAutoresizingMaskLayoutConstraint:0x60000009fef0 h=&-- v=--&
	UILabel:0x7f9d3820bf80'Hello'.width == 42>
<NSAutoresizingMaskLayoutConstraint:0x6000002821c0 h=&-- v=--&
	UILabel:0x7f9d3820bf80'Hello'.height == 22>

但在这种有用的自动行为中隐藏着一个陷阱。 假设一个视图获得了自动生成的隐式约束,然后假设您继续向该视图附加更多的约束,显式地设置其位置或大小。 那么,几乎可以肯定的是,你的显性约束和隐性约束之间存在冲突。 解决方案是将视图的translatesAutoresizingMaskIntoConstraints属性设置为false,这样就不会生成隐式约束,视图唯一的约束就是显式约束。

在nib中勾选了“Use Auto Layout”,就没有这方面的问题了。 一旦添加了可能导致问题的约束,nib编辑器本身就会将视图的translatesAutoresizingMaskIntoConstraints属性切换为false。 问题很可能会出现在您代码中创建一个视图,然后使用约束来定位或调整该视图的位置或大小,但是您却忘记了还需要将它的translateAutoresizingMaskIntoConstraints属性设置为false。 如果发生这种情况,您将得到约束之间的冲突。 (说实话,我通常会忘记,只有在遇到约束之间的冲突时才会被提醒。)

在代码中创建约束

现在,我们准备编写一些创建约束的代码! 我将首先使用NSLayoutConstraint初始化器:

  • init(item:attribute:relatedBy:toItem:attribute:multiplier:constant:)

正如我刚才描述的,这个初始化器设置了约束的所有属性——除了优先级之外,优先级默认为.required(1000),如果需要,可以稍后设置。

我将生成与图1-12和图1-13中相同的视图、子视图和布局行为,但是使用约束。 首先,我将创建视图并将它们添加到界面。 注意,在创建子视图v2和v3时,我没有费心分配它们的显式frame,因为约束将负责定位它们。 此外,我记得(只有一次)将它们的translatesAutoresizingMaskIntoConstraints属性设置为false,这样它们就不会产生额外的隐式NSAutoresizingMaskLayoutConstraints:

let v1 = UIView(frame:CGRect(100, 111, 132, 194))
v1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1)
let v2 = UIView()
v2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1)
let v3 = UIView()
v3.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
self.view.addSubview(v1)
v1.addSubview(v2)
v1.addSubview(v3)
v2.translatesAutoresizingMaskIntoConstraints = false
v3.translatesAutoresizingMaskIntoConstraints = false
//现在来看看约束条件: 
v1.addConstraint(
	NSLayoutConstraint(item: v2,
		attribute: .leading,
		relatedBy: .equal,
		toItem: v1,
		attribute: .leading,
		multiplier: 1, constant: 0)
)
v1.addConstraint(
	NSLayoutConstraint(item: v2,
		attribute: .trailing,
		relatedBy: .equal,
		toItem: v1,
		attribute: .trailing,
		multiplier: 1, constant: 0)
)
v1.addConstraint(
	NSLayoutConstraint(item: v2,
		attribute: .top,
		relatedBy: .equal,
		toItem: v1,
		attribute: .top,
		multiplier: 1, constant: 0)
)
v2.addConstraint(
	NSLayoutConstraint(item: v2,
		attribute: .height,
		relatedBy: .equal,
		toItem: nil,
		attribute: .notAnAttribute,
		multiplier: 1, constant: 10)
)
v3.addConstraint(
	NSLayoutConstraint(item: v3,
		attribute: .width,
		relatedBy: .equal,
		toItem: nil,
		attribute: .notAnAttribute,
		multiplier: 1, constant: 20)
)
v3.addConstraint(
	NSLayoutConstraint(item: v3,
		attribute: .height,
		relatedBy: .equal,
		toItem: nil,
		attribute: .notAnAttribute,
		multiplier: 1, constant: 20)
)
v1.addConstraint(
	NSLayoutConstraint(item: v3,
		attribute: .trailing,
		relatedBy: .equal,
		toItem: v1,
		attribute: .trailing,
		multiplier: 1, constant: 0)
)
v1.addConstraint(
	NSLayoutConstraint(item: v3,
		attribute: .bottom,
		relatedBy: .equal,
		toItem: v1,
		attribute: .bottom,
		multiplier: 1, constant: 0)
)

现在,我知道你在想什么了。 你在想:“ 你是什么,傻瓜? 那是一堆代码! ” (只不过你可能用了另外一个四个字母的单词,而不是“船”。) 我认为我们在这里所做的实际上比使用显式frames和autoresizing创建图1-12所使用的代码更简单。

毕竟,我们只是在8个简单的命令中创建了8个约束。 (我将每个命令分成多行,但这只是格式化而已。) 它们非常冗长,但是它们是使用不同参数重复的相同命令,因此创建它们是这样的简单 此外,我们的8个约束决定了我们的两个子视图的位置、大小和布局行为,因此我们得到了很多有用的东西。

更能说明问题的是,这些约束比设置frame和autoresizingMask更能清楚地表达应该发生什么。 我们的子视图的位置被一劳永逸地描述了,既像它们最初显示的那样,也像它们在父视图被调整大小时显示的那样。 我们不需要用任意的数学。 回想一下我们之前说过的:

let v1b = v1.bounds
let v3 = UIView(frame:CGRect(v1b.width-20, v1b.height-20, 20, 20))

从父视图的边界高度和宽度中减去视图的高度和宽度来定位视图的做法是令人困惑和容易出错的。 有了约束,我们可以直接说真话; 我们的约束条件简单明了地说“v3是宽20和高20,与v1的右下角齐平 ”。此外,约束可以表达自动调整大小所不能表达的东西。 例如,不是对v2应用一个绝对高度,我们可以要求它的高度恰好是v1高度的十分之一,不管v1如何调整大小。 要在不使用自动布局的情况下做到这一点,您必须在代码中实现layoutSubviews并手动执行它。

锚(Anchor)的符号

NSLayoutConstraint(item:…)初始化器相当冗长,但它具有奇点的优点:一个方法可以创建任何约束。 我刚才做的每一件事都有另一种方法,用一种更紧凑的表示法将它们添加到相同的视图中,这种表示法采用了相反的方法:它专注于简洁,但牺牲了独特性。 紧凑表示法不关注约束,而是关注约束所关联的属性。 这些属性表示为UIView的锚点属性:

  • widthAnchor,heightAnchor
  • topAnchor,bottomAnchor
  • leftAnchor, rightAnchor, leadingAnchor, trailingAnchor
  • centerXAnchor,centerYAnchor
  • firstBaselineAnchor,lastBaselineAnchor

锚值是NSLayoutAnchor子类的实例。 约束形成方法是锚实例方法,有许多合法组合,您的选择取决于您需要表达多少信息。 你可以提供另一个锚,另一个锚和一个常数,另一个锚和一个乘数,另一个锚和一个常数和一个乘数,或者单独一个常数(适用于绝对宽度或高度限制)。

如果省略常数,则为0; 如果忽略乘数,它是1。 在每种情况下,都有三种可能的关系:

  • constraint(equalTo:)
  • constraint(greaterThanOrEqualTo:)
  • constraint(lessThanOrEqualTo:)
  • constraint(equalTo:constant:)
  • constraint(greaterThanOrEqualTo:constant:)
  • constraint(lessThanOrEqualTo:constant:)
  • constraint(equalTo:multiplier:)
  • constraint(greaterThanOrEqualTo:multiplier:)
  • constraint(lessThanOrEqualTo:multiplier:)
  • constraint(equalTo:multiplier:constant:)
  • constraint(greaterThanOrEqualTo:multiplier:constant:)
  • constraint(lessThanOrEqualTo:multiplier:constant:)
  • constraint(equalToConstant:)
  • constraint(greaterThanOrEqualToConstant:)
  • constraint(lessThanOrEqualToConstant:)

在ios10中,添加了一个方法,该方法生成的不是约束,而是一个新的宽度或高度锚,表示两个锚之间的距离; 其思想是您可以将视图的宽度或高度锚点设置为相等

  • anchorWithOffset(to:)

从ios11开始,其他方法基于运行时提供的常量值创建约束。 这有助于获取视图之间的标准间距,在垂直连接文本基线时尤其有价值,因为系统间距将根据文本大小而变化:

  • constraint(equalToSystemSpacingAfter:multiplier:)
  • constraint(greaterThanOrEqualToSystemSpacingAfter:multiplier:)
  • constraint(lessThanOrEqualToSystemSpacingAfter:multiplier:)
  • constraint(equalToSystemSpacingBelow:multiplier:)
  • constraint(greaterThanOrEqualToSystemSpacingBelow:multiplier:)
  • constraint(lessThanOrEqualToSystemSpacingBelow:multiplier:)

当我描述它的时候,所有这些听起来都很复杂,但是当你看到它的实际应用时,你会立即欣赏到这种简洁符号的好处:它很容易写 (特别感谢Xcode的代码完成),易于阅读,易于维护。锚点表示法在与activate(:)关联时特别方便,因为我们不必担心指定每个约束应该添加到什么视图中。

在这里,我们生成的约束条件与前面的例子中完全相同:

NSLayoutConstraint.activate([
	v2.leadingAnchor.constraint(equalTo:v1.leadingAnchor),
	v2.trailingAnchor.constraint(equalTo:v1.trailingAnchor),
	v2.topAnchor.constraint(equalTo:v1.topAnchor),
	v2.heightAnchor.constraint(equalToConstant:10),
	v3.widthAnchor.constraint(equalToConstant:20),
	v3.heightAnchor.constraint(equalToConstant:20),
	v3.trailingAnchor.constraint(equalTo:v1.trailingAnchor),
	v3.bottomAnchor.constraint(equalTo:v1.bottomAnchor)
])

这是8行代码中的8个约束—加上将这些约束放入界面的激活调用。 没有必要一次激活所有的约束,但是最好尝试这样做,因为这允许autolayout引擎更有效地响应。

视觉格式符号

另一种简化约束创建的方法是使用一种基于文本的、称为可视格式的简写。 它的优点是允许您同时描述多个约束条件,特别是在水平或垂直排列一系列视图时。 我将从一个简单的例子开始:

“V:|(v2(10))

在该表达式中,V:表示正在讨论垂直尺寸; 另一种选择是H:,它也是默认值(因此可以略过)。 视图的名称出现在方括号中,管道(|)表示父视图,因此我们将v2的上边缘描述为它与父视图的上边缘的碰撞。 数值尺寸出现在圆括号中,视图的名称旁边的数值尺寸值设置了视图的尺寸,因此我们也将v2的高度设置为10。

要使用可视化格式,必须提供一个字典,将可视化格式字符串提到的每个视图的字符串名称映射到实际视图。 例如,前面表达式的字典可能是[“v2”:v2]。

下面是另一种表达上述示例的方法,使用4个命令而不是8个命令生成完全相同的8个约束,这要归功于可视化格式简写:

let d = ["v2":v2,"v3":v3]
NSLayoutConstraint.activate([
	NSLayoutConstraint.constraints(withVisualFormat:
		"H:|[v2]|", metrics: nil, views: d),
	NSLayoutConstraint.constraints(withVisualFormat:
		"V:|[v2(10)]", metrics: nil, views: d),
	NSLayoutConstraint.constraints(withVisualFormat:
		"H:[v3(20)]|", metrics: nil, views: d),
	NSLayoutConstraint.constraints(withVisualFormat:
		"V:[v3(20)]|", metrics: nil, views: d)
].flatMap{$0})

(The constraints(withVisualFormat:…) 类方法生成一个约束数组,因此我的文字数组是一个约束数组的数组。 但是activate(_:)需要一个约束数组,因此我将我的文字数组扁平化。 )

下面是使用可视化格式语法生成约束时需要知道的一些进一步的事情:

  • metrics: 参数是一个具有数值的字典。 这允许您在需要数字值的可视格式字符串中使用名称。
  • 在前面的示例中省略的options: 参数是一个位掩码(NSLayoutConstraint.FormatOptions),主要允许您指定要应用于可视格式字符串中提到的所有视图的对齐方式。
  • 要指定两个连续视图之间的距离,可以使用数字值周围的连字符,例如:“[v1]-20-[v2]”。 数值也可以选择用圆括号括起来。
  • 圆括号中的数值可以在等号或不等式运算符之前,也可以在具有优先级的at符号之后。 多个以逗号分隔的数值值可以一起出现在圆括号中。 例如:“(v1(20 @400 > =,< = 30)]”。

有关可视化格式语法的正式细节,请参阅文档档案中苹果Auto Layout Guide附录中的“Visual Format Syntax”。

当多个视图在同一尺寸上按相对关系排列时,可视格式语法会发挥最大的优势; 在这种情况下,您可以通过一个压缩的可视格式字符串生成许多约束。 然而,在最近的iOS版本中,它还没有更新,因此有一些重要类型的约束是可视格式语法无法表达的(例如将视图固定到安全区,将在本章后面讨论)。

约束作为对象

到目前为止的例子涉及到创建约束并将它们直接添加到界面——然后忘记它们。 但它通常是有用去组成约束,并将它们保留在手边,以备将来使用,通常在属性中使用。 一个常见的用例是,您打算在将来的某个时候以某种激进的方式更改界面,例如插入或删除视图; 您可能会发现手边有多个约束集是很方便的,每个约束集都适合于界面的特定配置。 因此,将约束与它们所影响的视图一起换到界面里面或者外面是很简单的。

在本例中,我们在主视图(self.view)中创建三个视图v1、v2和v3,它们分别是红色、黄色和蓝色矩形。 由于某些原因,我们稍后将在应用程序运行时动态删除黄色视图(v2),将蓝色视图移动到黄色视图所在的位置; 然后,稍后,我们将再次插入黄色视图(图1-14)。 我们有两个交替的视图构型。

为此,我们创建了两组约束条件,一个描述v1、v2和v3都存在时的位置,另一个描述v1和v3存在而v2不存在时的位置。 为了维护这些约束集,我们已经准备了两个属性,constraintsWith和constraintsWithout,初始化为NSLayoutConstraint的空数组。 我们还需要一个对v2的强引用,这样当我们从界面中删除它时它就不会消失:

var v2 : UIView!
var constraintsWith = [NSLayoutConstraint]()
var constraintsWithout = [NSLayoutConstraint]()

下面是创建视图的代码:

let v1 = UIView()
v1.backgroundColor = .red
v1.translatesAutoresizingMaskIntoConstraints = false
let v2 = UIView()
v2.backgroundColor = .yellow
v2.translatesAutoresizingMaskIntoConstraints = false
let v3 = UIView()
v3.backgroundColor = .blue
v3.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(v1)
self.view.addSubview(v2)
self.view.addSubview(v3)
self.v2 = v2 // retain

现在我们创建约束条件。 在下面的例子中,c1、c3和c4在这两种情况下都是相同的(v2存在或不存在),因此我们只需一次性地激活它们。 我们将剩下的约束存储在两组中,一组用于这两种情况:

//构造约束 
let c1 = NSLayoutConstraint.constraints(withVisualFormat:
	"H:|-(20)-[v(100)]", metrics: nil, views: ["v":v1])
let c2 = NSLayoutConstraint.constraints(withVisualFormat:
	"H:|-(20)-[v(100)]", metrics: nil, views: ["v":v2])
let c3 = NSLayoutConstraint.constraints(withVisualFormat:
	"H:|-(20)-[v(100)]", metrics: nil, views: ["v":v3])
let c4 = NSLayoutConstraint.constraints(withVisualFormat:
	"V:|-(100)-[v(20)]", metrics: nil, views: ["v":v1])
let c5with = NSLayoutConstraint.constraints(withVisualFormat:
	"V:[v1]-(20)-[v2(20)]-(20)-[v3(20)]", metrics: nil,
	views: ["v1":v1, "v2":v2, "v3":v3])
let c5without = NSLayoutConstraint.constraints(withVisualFormat:
	"V:[v1]-(20)-[v3(20)]", metrics: nil, 
	views: ["v1":v1, "v3":v3])
// 应用公共约束
NSLayoutConstraint.activate([c1, c3, c4].flatMap{$0})
// 第一组约束(当v2出现时)
self.constraintsWith.append(contentsOf:c2)
self.constraintsWith.append(contentsOf:c5with)
// 第二组约束(当v2不存在时)
self.constraintsWithout.append(contentsOf:c5without)

视图和约束的替代集
图1-14 视图和约束的替代集

现在我们准备开始在constraintsWith和constraintsWithout之间转换。 我们从v2存在开始,所以我们一开始要激活的是constraintsWith:

//应用第一组
NSLayoutConstraint.activate(self.constraintsWith) 

这些准备工作似乎都非常详细,最后当切换v2进出界面时,同时切换相应的约束:

if self.v2.superview != nil {
	self.v2.removeFromSuperview()
	NSLayoutConstraint.deactivate(self.constraintsWith)
	NSLayoutConstraint.activate(self.constraintsWithout)
} else {
	self.view.addSubview(v2)
	NSLayoutConstraint.deactivate(self.constraintsWithout)
	NSLayoutConstraint.activate(self.constraintsWith)
}

 在该代码中,我在激活新约束之前先禁用旧约束。 一定要按这个顺序进行;有效的旧约束与激活的新约束一起使用将导致冲突(如我将在本章后面解释的那样)。

边距(Margins)和指引(Guides)

到目前为止,我一直假设约束的锚点表示视图的边缘和中心。 然而,有时您希望视图显示一组辅助边,其他视图可以根据这些边定位。 例如,您可能希望子视图与父视图的边缘保持最小距离,而父视图应该能够指定最小距离。
辅助边的概念有两种不同的表达方式:

边缘内嵌(Edge insets)

视图以UIEdgeInsets的形式显示辅助边,这是一个由四个浮点数组成的结构,表示从顶部开始并逆时针方向(顶部、左侧、底部、右侧)执行的inset值。 当您需要将辅助边作为数值进行交互时(例如设置它们或执行手动布局),这是非常有用的。

布局指引(Layout guides)

UILayoutGuide类将辅助边表示为一种伪视图。 它有一个与声明它的视图相关的框架(它的layoutFrame),但是它的重要属性是它的锚,这与视图是一样的。 显然,这对自动布局是有用的。

安全区域(Safe area)

一组重要的辅助边(从ios11开始)是安全区。 这是UIView的一个特性,但它是由管理这个视图的UIViewController强加的。 需要安全区域的一个原因是,界面的顶部和底部经常被一个栏(状态栏、导航栏、工具栏、选项卡栏——参见第12章)所占据。 子视图的布局通常会占用这些栏之间的区域。 但这并不简单,因为:

  • 视图控制器的主视图通常会垂直扩展到窗口的边缘,在那些栏的后面。
  • 这些栏可以动态变化,可以改变高度。 例如,默认情况下,在iPhone应用程序中,当应用程序处于纵向时状态栏会出现,但当应用程序处于横向时状态栏会消失; 类似地,当应用处于纵向时导航栏比处于横向时要高。

因此,除了视图控制器的主视图的顶部和底部之外,您还需要其他东西来锚定定位其子视图的垂直约束—动态移动以反映栏的当前位置的东西。 否则,在某些情况下看起来正确的界面在其他情况下也会看起来错误。
例如,考虑一个视图,它的顶部被严格限制在视图控制器的主视图的顶部,也就是它的父视图:

let arr = NSLayoutConstraint.constraints(withVisualFormat:
	"V:|-0-[v]", metrics: nil, views: ["v":v])

当应用程序是横向的,默认情况下状态栏被删除,这个视图将直接对着屏幕的顶部,这没问题。 但在纵向中,此视图仍将直接对着屏幕顶部——这看起来可能很糟糕,因为状态栏会重新出现并与之重叠。 例如,如果这个视图是一个标签,状态栏现在重叠它的文本。

为了解决这个问题,UIViewController把安全区加到它的主视图上,描述主视图中被状态栏和其他栏重叠的区域。 安全区顶部与最低顶部栏的底部匹配,如果没有顶部栏,则与主视图的顶部匹配; 安全区的底部与底部栏的顶部相匹配,如果没有底部栏,则与主视图的底部相匹配。 安全区随着情况的变化而变化——当顶部栏或底部栏改变高度或完全消失时。

因此,在现实生活中,您将特别关注视图控制器的主视图的子视图相对于主视图的安全区的位置。 你的视图被限制在主视图的安全区域内,将避免被条形栏重叠,并将移动以跟踪主视图可见区域的边缘。 但任何视图都可以参与安全区。 当一个视图执行布局时,它将安全区加到它自己的子视图上,描述每个子视图被它自己的安全区重叠的区域; 因此,每个视图都“知道”条形栏的位置。 (我省略了一些额外的信息,因为出于实际目的,你可能不会遇到它们。)

要检索作为边缘内嵌的视图安全区域,请获取其safeAreaInsets。 要检索作为布局指引的视图安全区,请获取其safeAreaLayoutGuide。 您可以通过重写safeAreaInsetsDidChange来了解子类化视图的安全区已经改变,或者通过重写视图控制器的viewSafeAreaInsetsDidChange来了解视图控制器的主视图的安全区已经改变; 然而,在现实生活中,使用autolayout,您可能不需要这些信息——您只需要允许固定在安全区域布局指引上的视图随着安全区域的变化而移动。

在这个例子中,v是一个视图控制器的主视图,v1是它的子视图; 我们在v1顶部和主视图安全区顶部之间构造一个约束:

let c = v1.topAnchor.constraint(equalTo: v.safeAreaLayoutGuide.topAnchor)

视图控制器可以进一步内嵌强加在主视图上的安全区; 设置其additionalSafeAreaInsets。 顾名思义,这是添加到自动安全区。 它是一个UIEdgeInsets。 例如,如果你设置一个视图控制器的additionalSafeAreaInsets到一个带有top值为50的UIEdgeInsets,如果状态栏显示并且没有其它顶部栏,因为默认安全区顶部是20,所以现在是70。 如果你的主视图的边缘有必须保持可见的物体,additionalSafeAreaInsets是很有用的。

在没有边框的设备(如iPhone X)上,安全区域内嵌的重要性日益增加,在该设备上,内嵌有助于使您的视图远离屏幕的圆形轮廓,并防止传感器和home指示器(无论是纵向还是横向)干扰它们。

边距(Margins)

一个视图也有它自己的边距。 与从视图控制器向下传播视图层次结构的安全区不同,您可以自由设置单个视图的边距。 同样,我们的想法是子视图的位置可能与其父视图的边距有关,特别是通过一个自动布局约束。 默认情况下,视图的所有四条边的边距都是8。

通过layoutMarginsGuide属性,视图的边距可为一个UILayoutGuide。这是子视图的leading边和父视图leading边距之间的约束:

let c = v.leadingAnchor.constraint(equalTo:
	self.view.layoutMarginsGuide.leadingAnchor)

在可视化格式语法中,一个视图使用一个连字符固定在父视图的边,没有显式的距离值,被解释为对父视图边距的约束:

let arr = NSLayoutConstraint.constraints(withVisualFormat:
	"H:|-[v]", metrics: nil, views: ["v":v])

layoutMarginsGuide属性是只读的。 为了允许你设置视图的边距,UIView有一个layoutMargin属性,一个可写的UIEdgeInsets。 然而,从iOS 11开始,苹果希望你设置directionalLayoutMargin属性; 它的特点是,当您的界面在为了应用程序本地化的right-to-left系统语言中反转时,其leading值和trailing值的行为是正确的(从left-to-right的leading值变为从right-to-left的leading值)。 它表示为NSDirectionalEdgeInsets结构体,其属性为top、lead、bottom和tail。

或者,视图的布局边距可以向下传播到它的子视图,如下所示:与父视图边距重叠的子视图可以获得重叠量作为其自身的最小边距。 要打开此选项,请将子视图的preservesSuperviewLayoutMargin设置为true。 例如,假设我们将父视图的directionalLayoutMargin设置为一个leading值为40的NSDirectionalEdgeInsets。 并且假设子视图被固定在距离父视图leading边10个点的位置上,这样子视图就会与父视图的leading边距重叠30个点。然后,如果子视图的preservesSuperviewLayoutMargin为true,子视图的leading边距为30。

默认情况下,视图的边距值被视为来自安全区域的内嵌。 例如,假设视图的上边距为8。 假设这个视图位于整个状态栏的下方,因此获得了安全区域顶部有20。那么它的有效顶边距值是28——这意味着一个顶部被精确地固定在这个视图的顶边距上的子视图将出现在这个视图的顶边下方28个点。 如果您不喜欢这种行为(可能是因为您的代码先于安全区的存在),您可以通过将视图的insetsLayoutMarginsFromSafeArea属性设置为false来关闭它; 上边距值8表示有效上边距值8。

视图控制器还具有systemMinimumLayoutMargin属性; 它将这些边距作为最小值强加在它的主视图上,这意味着您可以将主视图的边距增加到超过这些限制的地方,但是尝试减少低于这些限制的边距将会失败。 然而,你可以通过设置视图控制的viewRespectsSystemMinimumLayoutMargins属性为false来规避这一限制。 systemMinimumLayoutMargin默认值是在较小的设备上的上边距和下边距为0,侧边距为16,在较大的设备上的侧边距为20。

第二组边距是UIView的readableContentGuide (UILayoutGuide),你不能改变它,它强化了这样一种观点:一个由文本组成的子视图在横向上不应该像ipad那样宽,因为它太宽了,不容易阅读,特别是当文本很小的时候。 通过水平地约束子视图到父视图的readableContentGuide,可以确保不会发生这种情况。

自定义布局指引

你可以添加你自己的自定义UILayoutGuide对象到一个视图中,以满足您的任何需要。 它们构成视图的layoutGuides数组,通过调用addLayoutGuide(:)或removeLayoutGuide(:)进行管理。 每个自定义布局引导对象必须完全使用约束进行配置。

你为什么要这么做? 好吧,你可以通过它的锚约束视图到UILayoutGuide。 因此,由于UILayoutGuide是由约束配置的,而且其他视图也可以被约束到它,所以它可以像子视图一样参与布局——但它不是子视图,因此它避免了UIView所具有的所有开销和复杂性。

例如,考虑如何在父视图中平均分配视图的问题。 这在一开始很容易排列,但是如何设计当父视图调整大小时仍然保持均匀间距的等间距视图,那就不明显了。 问题是,约束描述的是视图之间的关系,而不是约束之间的关系; 当父视图被调整大小时,没有办法将视图之间的间距约束自动保持相等。
平均分配
图1-15 平均分配

另一方面,您可以限制视图的高度或宽度以保持彼此相等。 因此,传统的解决方案是诉诸于将isHidden设置为true的间隔视图。 但间隔视图就是视图; 不管隐藏与否,它们在绘图、内存、触摸检测等方面都增加了开销。 定制UILayoutGuide解决了这个问题; 它们可以服务于与间隔视图相同的目的,但它们不是视图。

我将演示。 假设我有四个视图,它们将保持均匀分布。 我约束它们的左右边缘,它们的高度,第一个视图的顶部和最后一个视图的底部。 这就留下了一个问题:我们将如何确定两个中间视图的垂直位置; 它们必须以这样一种方式移动:即始终与垂直邻居保持等距离(图1-15)。

为了解决这个问题,我在真实视图之间引入了三个UILayoutGuide对象。 一个自定义UILayoutGuide对象被添加到一个UIView中,所以我将把我的添加到我的四个真实视图的父视图中:

let guides = [UILayoutGuide(), UILayoutGuide(), UILayoutGuide()]
for guide in guides {
	self.view.addLayoutGuide(guide)
}

然后在布局中加入三个布局引导。 记住,它们必须完全使用约束进行配置(三个布局指引通过我的指引数组引用,四个视图通过另一个数组视图引用):

NSLayoutConstraint.activate([ 
	// guide left是任意的,比如superview margin。 我约束布局指引的leading边(任意一个都到父视图的leading边)和宽度(任意一个)。
	guides[0].leadingAnchor.constraint(equalTo:self.view.leadingAnchor),
	guides[1].leadingAnchor.constraint(equalTo:self.view.leadingAnchor),
	guides[2].leadingAnchor.constraint(equalTo:self.view.leadingAnchor),
	//guide widths是任意的,假设10。
	guides[0].widthAnchor.constraint(equalToConstant:10),
	guides[1].widthAnchor.constraint(equalToConstant:10),
	guides[2].widthAnchor.constraint(equalToConstant:10),
	//每个视图的底部是下面的guide的顶部。我将每个布局指引约束在它上面视图的底部和下面视图的顶部。 
	views[0].bottomAnchor.constraint(equalTo:guides[0].topAnchor),
	views[1].bottomAnchor.constraint(equalTo:guides[1].topAnchor),
	views[2].bottomAnchor.constraint(equalTo:guides[2].topAnchor),
	//每个视图的顶部是前一个guide视图底部
	views[1].topAnchor.constraint(equalTo:guides[0].bottomAnchor),
	views[2].topAnchor.constraint(equalTo:guides[1].bottomAnchor),
	views[3].topAnchor.constraint(equalTo:guides[2].bottomAnchor),
	//guid高度相等。最后,我们的全部目的是均匀地分布视图,因此布局指引的高度必须彼此相等。
	guides[1].heightAnchor.constraint(equalTo:guides[0].heightAnchor),
	guides[2].heightAnchor.constraint(equalTo:guides[0].heightAnchor),


(在这段代码中,我可以(也应该)将每一组约束作为循环生成,从而使这种方法适用于任意数量的分布式视图; 但是为了示例的目的,我特意展开了这些循环)。

然而,在现实生活中,您不太可能直接使用这种技术,因为您将使用UIStackView,并让UIStackView为您生成所有代码— 我稍后会解释。

内部内容大小(Content Size)和对齐矩形(Alignment Rects)

在使用autolayout时,一些内置界面对象在一个或两个尺寸中都具有固有的大小。 例如:

  • UIButton,默认情况下,有一个标准的高度,它的宽度由它的标题决定。
  • 默认情况下,UIImageView采用它所显示的图像的大小。
  • 默认情况下,如果UILabel由多行组成,且宽度受限,则采用足以显示所有文本的高度。

这个固有的大小就是对象的固有内容大小。 内部内容大小用于隐式地生成约束(类NSContentSizeLayoutConstraint的约束)。

因此,内置界面对象(按钮的标题、图像视图的图像、标签的文本或字体等)的特征或内容的更改可能会导致其内部内容大小要更改。 这反过来又会改变你的布局。 您将需要配置您的autolayout约束,以便您的界面能够优雅地响应这些更改。

您不必提供显式约束来配置视图的尺寸,视图的固有内容大小配置该尺寸。 但是你可能! 当你这么做的时候,界面对象将其自身大小调整为其固有内容大小的趋势不应该与它对显式约束的服从相冲突。 因此,由视图的固有内容大小生成的约束具有较低的优先级,并且只有在没有更高优先级的约束阻止它们时才生效。 下面的方法允许您访问这些优先级(参数是一个NSLayoutConstraint.Axis, .horizontal 或 .vertical):

contentHuggingPriority(for:)

视图在此尺寸中抵抗增长大于其内部大小。 实际上,有一个不等式约束:视图在这个尺寸上的大小应该小于或等于它的内部大小。 默认的优先级是通常为.defaultLow(250),不过如果在nib中初始化,一些界面类将默认为更高的值。

contentCompressionResistancePriority (for:)

视图在此尺寸中抵抗收缩小于其内部大小。 实际上,有一个不等式约束:视图在这个尺寸上的大小应该大于或等于它的内部大小。 默认优先级通常是.defaultHigt(750)。

这些方法是getter; 有相应的setter,并且存在需要更改优先级的情况。 例如,以下是可视化格式配置——将两个水平相邻的标签(lab1和lab2)固定在父视图上,并相互连接:

"V:|-20-[lab1]"
"V:|-20-[lab2]"
"H:|-20-[lab1]"
"H:[lab2]-20-|"
"H:[lab1(>=100)]-(>=20)-[lab2(>=100)]"

这些不等式确保,当父视图变得更窄或标签的文本变得更长时,在两个标签中仍然可以看到合理数量的文本。 同时,一个标签将被压缩到100个点的宽度,而另一个标签将被允许增长,以填充剩余的水平空间。 问题是:哪个标签是哪个? 你需要回答这个问题。 要做到这一点,只需将其中一个标签的抗压优先级比另一个标签的抗压优先级提高一个点(请参阅附录B,了解允许将数字添加到UILayoutPriority中的扩展名):

let p = lab2.contentCompressionResistancePriority(for: .horizontal)
lab1.setContentCompressionResistancePriority(p+1, for: .horizontal)

你可以通过在你自己的UIView子类中重写intrinsicContentsize提供一个内部尺寸。 显然,只有当视图的大小在某种程度上取决于其内容时,才应该这样做。 如果你需要运行时再次询问你的intrinsicContentSize,因为视图内容已经改变,视图需要重新布局,那么您可以调用视图的invalidateIntrinsicContentSize方法。

自定义UIView子类可能关心的另一个问题是:另一个视图与它对齐意味着什么。 它可能意味着与视图的框架边缘对齐,但也可能不是。 一个可能的例子是一个视图,它在内部绘制一个带有阴影的矩形; 你可能想要对齐这个矩形,而不是阴影的外部。 要确定这一点,您可以重写视图的alignmentRectInsets属性(或者更确切地说,重写视图的alignmentRect(forFrame:)和frame(forAlignmentRect:)方法)。

在更改视图的alignmentRectants时要小心,因为对于涉及这些边的所有约束,您实际上是在更改视图的边的位置。例如,如果一个视图的对齐矩形的左内嵌为30,那么涉及该视图的.leading属性或leadingAnchor的所有约束都将从该内嵌中被计算。

同样的,你可能想要能够将你的自定义UIView与另一个视图按照它们的基线对齐。 这里的假设是,您的视图有一个子视图,其中包含本身具有基线的文本。 自定义视图将在实现forFirstBaselineLayout或forLastBaselineLayout时返回该子视图。

堆栈视图(Stack Views)

堆栈视图(UIStackView)是一个视图,它的主要任务是为它的一些或所有子视图生成约束。 这些是它被安排的子视图。 特别是,当子视图在水平行或垂直列中线性配置时,堆栈视图解决了提供约束的问题(可能不需要使用可视化格式语言来做同样的事情)。 实际上,许多布局可以表示为简单的子视图行和列的排列(可能是嵌套的)。 因此,您可能会求助于堆栈视图,以使布局更易于构造和维护。
您可以通过调用它的初始化器init(arrangedSubviews:)来提供带有被安排的子视图的堆栈视图。 被安排的子视图成为堆栈视图的arrangedSubviews只读属性。 您还可以使用以下方法管理安排好的子视图:

  • addArrangedSubview (_)
  • insertArrangedSubview (_:at:)
  • removeArrangedSubview (_)

arrangedSubviews数组与堆栈视图的子视图不同,但它是后者的一个子集。 堆栈视图可以有未安排的子视图(您必须自己提供约束); 另一方面,如果您将一个视图设置为一个安排好的子视图,而它还不是一个子视图,那么堆栈视图将在此时将其作为一个子视图。

arrangement子视图的顺序独立于子视图的顺序; 子视图的顺序,你记得,决定了子视图绘制的顺序,但是arrangedSubviews的顺序决定了堆栈视图将如何定位这些子视图。

使用它的属性,您可以配置堆栈视图来告诉它应该如何排列它的被安排的子视图:

轴(axis)

如何排列被安排的子视图? 你的选择是(NSLayoutConstraint.Axis):

  • .horizontal
  • .vertical

对齐(alignment)

这描述了被安排的子视图相对于其他尺寸维度如何布局。
你的选择是(UIStackView.Alignment):

  • .fill
  • .leading(或.top)
  • .center
  • .trailing(或.bottom)
  • .firstBaseline或.lastBaseline(如果轴为.horizontal)

如果轴是.vertical,通过将堆栈视图的isBaselineRelativeArrangement设置为true,您仍然可以在子视图的间距中包含它们的基线。

分布(distribution)

被安排的子视图应该如何沿轴定位? 这就是你来这里的原因! 之所以从一开始就使用堆栈视图,是因为你希望此定位为你执行。 您的选择是(UIStackView.Distribution):

  • .fill
    被安排的子视图可以具有实际大小约束,或者沿被安排的尺寸的内部内容大小。 使用这些大小,被安排的子视图将从头到尾填充堆栈视图。 但是必须至少有一个视图没有实际大小限制,这样才能调整它的大小以填充其他视图没有占用的空间。 如果多个视图缺少实际大小约束,这些视图则必须有一个是具有降低的内容吸附(content hugging,即指如果拉伸,不想被拉伸能力)或降低的压缩阻力(content compression resistance,即指如果压缩,不想被压缩能力),以便堆栈视图知道要调整哪个视图的大小。
  • .fillEqually
    没有视图可以沿着被安排的尺寸维度有实际的大小约束。 将被安排的子视图在被安排的尺寸维度中被设置为相同的大小,以填充堆栈视图。
  • .fillProportionally
    所有被安排的子视图都必须有一个内部内容大小,并且沿着被安排的尺寸维度没有实际大小的约束。 然后,视图将填充堆栈视图,根据其内部内容大小的比例调整大小。
  • .equalSpacing
    被安排的子视图可以具有实际大小约束,也可以具有沿着被安排的尺寸维度的内部内容大小。 使用这些大小,所安排的子视图将用每个相邻对之间相等的空间从头到尾填充堆栈视图。
  • .equalCentering
    被安排的子视图可以具有实际大小约束,也可以具有沿着被安排的尺寸维度的内部内容大小。 使用这些大小,排列好的子视图将以每个相邻对的中心之间相等的距离从头到尾填充堆栈视图。

堆栈视图的间距属性决定了所有视图之间的间距(或最小间距)。 从ios11开始,您可以通过调用setCustomSpaces (_:after:)来设置单个视图的间距; 如果您需要为视图关闭单独的间距,恢复到总体间距属性值,那么将自定义间距设置为UIStackView.spacingUseDefault。 要设置系统通常设置的间距,请将间距设置为UIStackview.spacingUseSystem。

isLayoutMarginsRelativeArrangement

如果为真,则堆栈视图的内部layoutMargins参与其被安排的子视图的定位。 如果为false(默认值),则使用堆栈视图的各个边缘。

不要手动添加约束来定位被安排的子视图! 添加这些约束正是堆栈视图的工作。 您的约束将与堆栈视图创建的约束冲突。另一方面,必须约束堆栈视图本身(除非堆栈视图本身是包含堆栈视图的排列视图); 否则,布局引擎不知道该做什么。尝试不受约束地使用堆栈视图是初学者常见的错误。

为了说明这一点,我将重写本章前面的平等分布代码(图1-15)。 我有四个视图,有高度限制。 我想在主视图中垂直分布它们。 这一次,我将有一个堆栈视图为我做所有的工作:

//给出堆栈视图安排的子视图 
let sv = UIStackView(arrangedSubviews: views)
//配置堆栈视图
sv.axis = .vertical
sv.alignment = .fill
sv.distribution = .equalSpacing
//约束堆栈视图
sv.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(sv)
let marg = self.view.layoutMarginsGuide
let safe = self.view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
	sv.topAnchor.constraint(equalTo:safe.topAnchor),
	sv.leadingAnchor.constraint(equalTo:marg.leadingAnchor),
	sv.trailingAnchor.constraint(equalTo:marg.trailingAnchor),
	sv.bottomAnchor.constraint(equalTo:self.view.bottomAnchor),
]) 

检查得到的约束,您可以看到堆栈视图正在为我们有效地做我们之前所做的事情(生成UILayoutGuide对象并将它们用作间隔符)。 但是让堆栈视图来做要容易得多!

UIStackView的另一个很好的特性是它能智能地响应变化。 例如,使用前面的代码配置了一些东西之后,如果我们随后让我们被安排的子视图之一不可见(通过将其isHidden设置为true),堆栈视图将通过均匀地分布其余的子视图来响应,就好像隐藏的子视图不存在一样。 类似地,我们可以实时更改堆栈视图本身的属性。 这种灵活性可以非常有用,可以让整个界面区域随意地进行重新排列。

国际化

当应用程序在应用程序本地化且语言是right-to-left的系统上运行时,应用程序的整个界面及其行为将发生逆转。无论在何处使用leading约束和trailing约束替代left约束和right约束,或者如果约束是由堆栈视图生成的,或者是使用视觉格式语言构建的,则应用程序的布局或多或少会自动参与这种反转。

然而,也可能有例外。苹果给出了一个在CD播放器上模拟按钮的水平传输控件行的例子:您不希望倒带按钮和快进按钮颠倒,仅仅因为用户的语言right-to-left读取。因此,UIView被赋予一个semanticContentAttribute属性,说明是否应该翻转;默认值为.unspecified,但值为.playback或.spatial将阻止翻转,您还可以使用.forceLeftToRight或.forceRightToLeft强制绝对方向。也可以在NIB编辑器中设置此属性(使用Attributes检查器中的Semantic弹出菜单)。

界面方向性是一个特性,一个特性集合(trait collection)的.layoutDirection;并且一个UIView有一个有效的effectiveUserInterfaceLayoutDirection属性, 该属性报告用于布局内容的方向,并且如果您以代码方式构造一个视图的子视图,您可以参考它。

通过将scheme的Run option Application Language更改为 Right to Left的伪语言,您可以轻松测试应用程序的从right-to-left行为。

约束错误

正如我在本章中所做的,手动创建约束会招致错误。您的全部约束构成了视图布局的指令,而且只要多于一个或两个以上的视图被涉及到,就很容易生成错误的指令。你可以(并且将要)犯两种主要的有限制的错误:

冲突

您已经应用了不能同时满足的约束。这将在控制台中报告(非常详细)。

不确定/歧义(ambiguity)

视图使用自动布局,但您没有提供足够的信息来确定其大小和位置。这是一个更加隐蔽的问题,因为也许没有不好的事情发生。如果你幸运的话,这个视图至少不会显示,或者会显示在一个不想要的地方,这个问题会有警告。

只有.required约束(priority 1000)可以导致冲突,因为运行时可以自由地忽略它不能达标的较低优先级约束。具有不同优先级的约束不会相互冲突。具有相同优先级的非必需约束可能导致歧义。

在正常情况下,在代码完成运行之前,不会执行布局——即使如此,也只有在需要的时候。歧义布局在实际发生布局之前不会歧义的;临时地导致歧义布局是完全合理的,前提是在调用layoutSubviews之前解决了歧义。另一方面,一个冲突的约束在添加时会发生冲突。这就是为什么在代码中替换约束时,您应该先deactivate然后再activate,而不是反过来。

让我们从产生冲突开始。在本例中,我们返回到小红块(图1-12)右下角的小红块,并附加一个对立约束:

let d = ["v2":v2,"v3":v3] 
NSLayoutConstraint.activate([

	NSLayoutConstraint.constraints(withVisualFormat: "H:|[v2]|", metrics: nil, views: d),
	
	NSLayoutConstraint.constraints(withVisualFormat: "V:|[v2(10)]", metrics: nil, views: d),
	
	NSLayoutConstraint.constraints(withVisualFormat: "H:[v3(20)]|", metrics: nil, views: d),
	
	NSLayoutConstraint.constraints(withVisualFormat: "V:[v3(20)]|", metrics: nil, views: d),
	
	NSLayoutConstraint.constraints(withVisualFormat: "V:[v3(10)]|", metrics: nil, views: d) // *

].flatMap{$0}) 

v3的高度不能同时为10和20。运行时报告冲突,并告诉您是哪些约束导致了冲突:
无法同时满足约束。可能下面列表中至少有一个约束是您不想要的…

<nslayoutConstraint:0x60008b6d0 uiview:0x7ff45e803.height=+20(active)><nslayoutConstraint:0x60008bae0 uiview:0x7ff45e803.height=+10(active)>

可以为约束(或UILayoutGuide)分配标识符字符串;这可以更容易确定在冲突报告中哪个约束是哪个。

现在,我们将生成一个歧义。在这里,我们忽略了给我们的小红块一个高度:

let d = ["v2":v2,"v3":v3] 
NSLayoutConstraint.activate([

	NSLayoutConstraint.constraints(withVisualFormat: "H:|[v2]|", metrics: nil, views: d),

	NSLayoutConstraint.constraints(withVisualFormat: "V:|[v2(10)]", metrics: nil, views: d),
	
	NSLayoutConstraint.constraints(withVisualFormat: "H:[v3(20)]|", metrics: nil, views: d)

].flatMap{$0})

没有控制台消息提醒我们出错。然而幸运的是,v3未能出现在界面中,所以我们知道出了问题。如果你的视图没有显示,就怀疑歧义。
在这里插入图片描述
图1-16 视图调试

在不太幸运的情况下,这种视图可能会显示,但(如果幸运的话)是错误的。在一个真正不幸的情况下,这种视图可能显示在正确的位置,但并不总是持续的。
怀疑歧义是一回事;追踪并证明它是另一回事。幸运的是,视图调试器将立即报告歧义(图1-16)。在应用程序运行时,选择Debug → View Debugging → Capture View Hierarchy,或单击调试栏中的Debug View Hierarchy按钮。左侧调试导航器中的感叹号告诉我们,此视图(不显示在画布中)的布局不明确;此外,运行时窗格中的问题导航器更明确地告诉我们:“高度和垂直位置对于UIView来说不明确。”

另一个有用的技巧是在调试器中暂停,并在控制台中给出以下神秘命令:

(lldb) expr -l objc -O -- [[UIWindow keyWindow] _autolayoutTrace]

其结果是一个图形化的树,描述视图层次结构并标记任何不明确地布局的视图:

UIWindow:0x7fe8d0d9dbd0

|UIView:0x7fe8d0c2bf00

| | +UIView:0x7fe8d0c2c290

| | | *UIView:0x7fe8d0c2c7e0

| | | *UIView:0x7fe8d0c2c9e0- AMBIGUOUS LAYOUT

UIView还具有hasAmbiguousLayout属性。我发现设置一个工具方法非常有用,它允许我在任何深度检查一个视图及其所有子视图是否有歧义;

extension NSLayoutConstraint {
	class func reportAmbiguity (_ v:UIView?) {
		var v = v
		if v == nil {
			v = UIApplication.shared.keyWindow
		}
		for vv in v!.subviews {
			print("\(vv) \(vv.hasAmbiguousLayout)")
			if vv.subviews.count > 0 {
				self.reportAmbiguity(vv)
			}
		}
	}
	class func listConstraints (_ v:UIView?) {
		var v = v
		if v == nil {
			v = UIApplication.shared.keyWindow
		}
		for vv in v!.subviews {
			let arr1 = vv.constraintsAffectingLayout(for:.horizontal)
			let arr2 = vv.constraintsAffectingLayout(for:.vertical)
			let s = String(format: "\n\n%@\nH: %@\nV:%@", vv, arr1, arr2)
			print(s)
			if vv.subviews.count > 0 {
				self.listConstraints(vv)
			}
		}
	}
}

要获取负责将特定视图定位到其父视图中的约束的完整列表,请记录调用UIView实例方法constraintsAffectingLayout(for:)的结果。参数是一个轴(NSLayoutConstraint.Axis),可以是.horizontal或.vertical。这些约束不一定属于这个视图(输出不会告诉您它们属于哪个视图)。如果视图不参与自动布局,则结果将为空数组。同样,上面工具方法也可以派上用场;

考虑到冲突和歧义的概念,更容易理解优先级是什么。假设所有约束都按降序放置在框中,其中每个框都是优先级值。现在假设我们是运行时,按照这些约束执行布局。我们如何继续?

第一个框(.required,1000)包含所有必需的约束,因此我们首先遵守它们。(如果它们发生冲突,那是很糟糕的,我们会在日志中报告这一点。)如果仍然没有足够的信息来执行明确的布局,仅考虑到所需的优先级,我们会将约束从下一个框中拉出来,并尝试服从它们。如果我们能够,与我们已经做的一致,那么很好;如果我们不能,或者如果歧义依旧,那么我们将在下一个框中查找——以此类推。

对于第一个框之后的框,我们不关心是否严格遵守它所包含的约束;如果歧义依旧,我们可以使用较低优先级的约束值来为我们提供目标,解决歧义,而不完全遵守较低优先级约束的需求。例如,不等式是一种歧义,因为无限多的值可以满足它;较低优先级的等式可以告诉我们要选择什么值,从而解决歧义,但即使我们不能完全实现该首选值,也没有冲突。

在NIB中配置布局

到目前为止,讨论的重点一直是在代码中配置布局。然而,这通常是不必要的;相反,您将使用NIB编辑器在NIB中设置布局。不是很严格来说,在NIB中你完全可以做任何你可以在代码中做的事情,但是NIB编辑器无疑是配置布局的一种非常强大的方法(如果它不够,你可以用一些额外的代码来补充它)。

在File inspector中,当选择.storyboard或.xib文件时,可以通过复选框作出与布局相关的三个主要选择。默认情况下,这些复选 框是选中的,我建议您将其保留为:

使用自动布局(Use Auto Layout)

如果未选中,则无法在NIB编辑器中创建约束:必须完全使用自动调整大小来配置视图的布局。

使用特性变量(Use Trait Variations)

如果选中,NIB编辑器中的各种设置,例如约束constant值,可以使其在运行时取决于环境的size classes。

使用安全区域布局指引(Use Safe Area Layout Guides)

如果选中,则存在安全区域,并且可以构造固定到该区域的约束。默认情况下,只有视图控制器的主视图的安全区域可以有固定的约束,但您可以更改它(稍后我将解释)。

您在NIB编辑器画布中实际看到的内容还取决于Editor → Canvas层次菜单中选中的菜单项。例如,如果未选中Show Layout Rectangles, 将看不到安全区域的轮廓(但仍可以对其构造约束)。如果未选中Show Constraints,则不会看到任何约束(但仍可以构造它们)。

在NIB中自动调整大小

当您从库中将视图拖到画布中时,默认情况下它使用自动调整大小,并且将继续执行此操作,除非您通过添加影响它的约束使其参与自动布局。
编辑使用自动调整大小的视图时,可以在大小检查器中为其指定弹簧和支柱。外部实线表示支柱;内部实线表示弹簧。一个有帮助的动画显示了当父视图调整大小时对视图位置和大小的影响。

创建约束

NIB编辑器提供了两种创建约束的主要方法:

拖动操作(Control-drag)

拖动操作从一个视图到另一个视图。会出现一个HUD(弹出显示框),列出可以创建的约束(图1-17)。视图可以在画布中,也可以在文档大纲中。若要创建内部宽度或高度约束,拖动操作从一个视图到自身。
在画布中拖动操作时,拖动方向用于取消显示在Hud中的选项;例如,如果在画布中的视图中水平拖动操作,则HUD将列出宽度而不是高度。
在查看HUD时,您可能希望切换选项键以查看某些替代项;例如,这可能会使边缘或安全区域约束与基于边界的约束之间产生差异。按住SHIFT键可以同时创建多个约束。
在这里插入图片描述
图1-17 通过拖动操作创建约束

在这里插入图片描述
图1-18 从布局栏创建约束

布局栏按钮

单击画布下方布局栏右端的“对齐或添加新约束”按钮。这些按钮会弹出一个小对话框,您可以在其中选择要创建的多个约束(如果事先选择了多个视图,则可能是多个视图),并为它们提供数值(图1-18)。在单击底部的Add Constraints之前,不会实际添加约束!

您在nib中创建的约束不必在创建后马上完美!随后您将能够编辑约束并进一步配置它,正如我将在下一节中解释的那样。

如果创建约束,然后移动或调整受这些约束影响的视图的大小,则约束不会自动更改。这意味着约束不再符合视图的描述方式;如果约束现在定位视图,它们就不会将其放在您所放置的位置。NIB编辑器将提醒您注意这种情况(视图错误的问题),并可以随时为您解决它,但除非您明确要求,否则它不会改变任何东西。
在这里插入图片描述
图1-19 显示在NIB中的视图约束

Size inspector中还有其他视图设置。若要显式设置视图的布局边距,请将Layout Margins弹出菜单更改为Fixed(或更好的设置为Language Directional)。若要使视图的布局边距行为为readableContentGuide边距,请选中Follow Readable Width。要允许对视图的安全区域(不是视图控制器的主视图)构造约束,请检查其Safe Area Layout Guide。

查看和编辑约束

nib中的约束是完整的对象。它们可以被选择、编辑和删除。此外,您可以为约束创建一个outlet(而且您可能希望这样做是有原因的)。
nib中的约束在三个位置可见(图1-19):

在文档大纲中

约束列在它们所属的视图下的特殊类别“约束”中。(如果给视图添加有意义的标签,那么区分这些约束就容易多了!)

在画布上

当选择受约束影响的视图时,约束以图形方式显示为尺寸线(除非取消选中Editor → Canvas → Show Constraints)。

在Size inspector中

选择受约束影响的视图时,Size inspector将列出这些约束,以及以图形方式显示视图约束的网格。单击网格中的约束将过滤其下面列出的约束。
在文档大纲或画布中选择约束时,可以在Attributes或Size inspector中查看和编辑其值。使用Inspector可以访问约束的几乎所有特征:约束中涉及的锚(第一个
项目和第二个项目弹出菜单),它们之间的关系,常数constant和乘数multiplier,以及优先级。您也可以在这里设置标识符(如我前面提到的,调试时很有用)。

第一项和第二项弹出菜单可以列出可选约束类型;因此,例如,宽度约束可以更改为高度约束。这些弹出菜单还可以列出要约束到的可选对象,例如其他同级视图、父视图和安全区域。此外,这些弹出菜单可能有一个“Relative to margin”选项,您可以选中或取消选中该选项,在基于边缘和基于边距的约束之间切换。
因此,如果意外地创建了错误的约束,或者在创建时无法指定所需的约束,则编辑通常会允许您修复问题。例如,当将子视图约束到视图控制器的主视图时,HUD不提供约束到主视图边缘的方法;您的选择是约束到主视图的安全区域(默认)或其边界(如果保留选项)。但是,在将子视图限制在主视图的边距之后,您可以在弹出菜单中取消选中“Relative to margin”。

要简单编辑约束的常量、关系、优先级和乘数,请双击画布中的约束以调用一个弹出对话框。当一个约束在视图的Size inspector中列出时,双击它在自己的inspector中进行编辑,或者单击它的Edit按钮来调起小的弹出对话框。

视图的Size inspector还提供对其内容吸附和内容压缩阻力优先级设置的访问。在这些下面,有一个固有的大小弹出菜单。这里的想法是,您的自定义视图可能有一个内部大小,但NIB编辑器不知道这一点,因此当您无法提供您知道实际上不需要的宽度约束时,它将报告一个歧义;选择Placeholder来提供一个内部大小,并减轻NIB编辑器的担忧。

在约束的Attributes或Size inspector中,有一个Placeholder选框(“Remove at build time”)。如果选中此复选框,则在加载NIB时不会实例化正在编辑的约束:实际上,当从NIB实例化视图和约束时,会故意生成歧义的布局。您之所以这样做,可能是因为希望在NIB编辑器中模拟布局,但您打算在代码中提供不同的约束;也许您无法在NIB中描述此约束,或者约束取决于运行时才知道的环境。

不幸的是,只能在代码中创建和配置自定义UILayoutGuide。如果要在NIB编辑器中完全配置布局,并且此配置需要使用间隔视图,并且不能由UIStackView构建,则必须使用间隔视图-不能将其替换为UILayoutGuide对象,因为NIB编辑器中没有UILayoutGuide对象。

Nib约束问题

我已经说过,在代码中手动生成约束很容易出错。但在NIB编辑器中不容易出错!NIB编辑器知道它是否包含有问题的约束。如果一个视图受到任何约束的影响,XcodeNIB编辑器将允许它们歧义或冲突,但那也将是有用地发牢骚。你应该注意这些发牢骚!Nib编辑会在不同的地方引起您的注意:

Canvas画布

当您选择受其影响的视图时,在画布中绘制的约束将使用颜色编码来表示其状态:

  • 满意的约束
    画成蓝色。
  • 问题的约束
    画成红色。
  • 错位约束
    用橙色绘制;这些约束是有效的,但它们与您对视图施加的框架不一致。我将在下一段讨论放错地方的视图。

文档大纲

如果存在布局问题,文档大纲将以红色或橙色圆圈显示右箭头。单击它可以查看问题的详细列表(图1-20)。将鼠标悬停在标题上可查看信息按钮,您可以单击该按钮了解有关此问题性质的更多信息。右边的图标是按钮:单击其中一个按钮可查看NIB编辑器为您修复问题提供的操作列表。主要问题是:

  • 冲突的约束
    约束之间的冲突。
  • 缺少的约束
    不明确的布局
  • 错位的视图
    如果手动更改受约束(包括其内部大小)影响的视图的框架,则画布显示该视图的方式可能与遵守当前约束时的实际显示方式不同。画布中还描述了错位的视图情况:
    • 画布中以橙色绘制的约束显示其值与视图框架之间的数字差。
    • 画布中的虚线轮廓可以显示如果遵守现有约束,将在何处绘制视图。

在这里插入图片描述
图1-20 文档大纲中的布局问题

可以关闭特定视图的歧义检查;使用视图大小检查器中的歧义弹出菜单。这意味着您可以略过一个需要的约束,NIB编辑器不会通知您存在问题。显然,您需要在代码中生成缺少的约束,否则布局将不明确。

NIB编辑器警告您布局有问题后,还提供了修复这些问题的工具。

布局栏(或Editor → Update Frames)中的Update Frames按钮可更改所选视图或所有视图在画布中的绘制方式,以显示在其所处的约束条件下,在正在运行的应用程序中实际显示的内容。或者,如果调整了已使用内部大小约束(如按钮或标签)的视图的大小,并且希望它根据这些内部大小约束恢复其大小,请选择该视图,然后选择“Editor → Size to Fit Content。

对 Update Frames要注意:如果约束不明确,这可能导致视图消失。

布局栏中的Resolve Auto Layout Issues按钮(或Editor → Resolve Auto Layout Issues层次菜单)提供涉及到影响选定视图或所有视图的所有约束的大规模移动:

更新约束常量(Update Constraint Constants)

选择此菜单项以数字方式更改影响视图的所有现有约束,以匹配画布当前绘制视图框架的方式。

添加缺少的约束(Add Missing Constraints)

创建新的约束,以便视图有足够的约束来明确地描述其框架。添加的约束与画布当前绘制视图框架的方式相对应。

这个命令可能不会做你最终想要做的事情;你应该把它当作一个起点。毕竟,NIB编辑读不懂你的想法!例如,它不知道您是否认为某个视图的宽度应该由内部宽度约束确定,或者通过将其固定到其父视图的左右两侧来确定;并且它可能会与您从未打算使用的其他视图生成对齐约束。

重置为建议的约束(Reset to Suggested Constraints)

这就好像选择了清除约束(Clear Constraints)后再添加缺少的约束:它会删除影响视图的所有约束,并用一组自动生成的约束替换它们, 这些约束描述画布当前绘制视图框架的方式。

清除约束(Clear Constraints)

删除影响视图的所有约束。

改变屏幕大小

约束的目的通常是设计一个布局,以响应应用程序在不同大小的设备上启动的可能性,并可能随后被旋转。很难想象这将如何在现实生活中工作,并且您可能怀疑您在NIB编辑器中配置约束时是否正确。别害怕:Xcode是来帮忙的。
在画布的左下角有一个View As按钮。单击它可以显示(如果尚未显示)表示各种设备类型和方向的菜单或按钮。选择一个,画布的主视图将相应地调整大小。当这种情况发生时,立即遵循由约束所决定的布局。因此,您可以在画布中的不同屏幕大小下尝试约束的效果。

只有当Size inspector中视图控制器的Simulated Size弹出菜单显示Fixed时,此功能才起作用。如果显示为Freeform,则单击设备类型或方向按钮时,视图将不会调整大小。

条件界面设计

画布左下角的View As按钮使用符号如wC hR,来表示当前所选设备和方向的size classes(参见“Trait Collections和Size Classes”)。W和H代表“宽度”和“高度”,分别对应于特性集合的.horizontalSizeClass和. verticalSizeClass;R和C代表.regular和.compact。

您得到这些信息的原因是:您可能希望NIB编辑器中约束和视图的配置取决于在运行时有效的size classes。您可以在NIB编辑器中为你的app界面安排,以检测traitCollectionDidChange通知并对其作出响应。因此,例如:

  • 当iPhone应用程序旋转以响应设备方向的变化时,您可以直接在界面中设计复杂的界面重新排列。
  • 一个.storyboard或.xib文件可以用来设计一个通用应用程序的界面,尽管ipad界面和iphone界面可能有很大的不同。

构造条件界面时的想法是,首先为最一般的情况设计。当您这样做了,并且当您想要为特定的size classes情况做一些不同的事情时,您将在Attributes或Size inspector中描述这种差异,或者在画布中设计这种差异:

在Attributes或Size inspector中

在Attributes或Size inspector中查找值的左侧的加号。这是一个可以根据环境在运行时的size class有条件地改变的值。加号是一个按钮!单击它可以看到一个弹出窗口,从中可以选择一个专门的size class组合。当您这样做时,该值现在出现两次:一次用于一般情况,一次用于使用wC hR表示法标记的特殊情况。现在,您可以为这两种情况提供不同的值。

在画布上

单击device types按钮右侧的Vary for Traits按钮。将显示两个复选框,允许您指定要与当前size class的宽度或高度size class(或两者)匹配。您现在在画布中所做的任何设计都将只应用于宽度或高度size class(或两者),还可以根据需要修改Attributes或Size inspector。
我将通过一个小教程来说明这些方法。你需要有一个现成的示例项目;确保它是一个Universal应用。

在inspectors中的Size classes

假设我们在画布上有一个按钮,我们只希望这个按钮在iPad上有一个黄色的背景。(这是不可能的,但很戏剧性。)您可以在Attributes inspector,中直接配置,如下所示:

  1. 在界面中选择按钮。
  2. 切换到Attributes inspector,,并在inspector的视图部分找到Background弹出菜单。
  3. 单击加号按钮,弹出指定size classes的弹出菜单。一个iPad有宽度(水平)size class规则和高度(垂直)size class规则,所以改变前两个弹出菜单,使他们都说规则。单击Add Variation。
  4. 出现第二个Background弹出菜单,标记为wR hR。将其更改为黄色(或任何所需颜色)。

这个按钮现在在iPad上有一个彩色背景,但在iPhone上没有。要了解这一点,在不在不同设备类型上运行应用程序的情况下,可以使用画布左下角的View As按钮和设备按钮在不同的屏幕大小之间切换。当你点击一个ipad按钮时,画布上的按钮有一个黄色的背景。单击iPhone按钮时,画布中的按钮具有默认的清晰背景。

既然您知道了加号按钮的含义,那么请查看Attributes和Size inspectors.。任何带有加号按钮的内容都可以根据size class环境进行更改。例如,按钮的文本可以是不同的字体和大小;这是有意义的,因为您可能希望文本在iPad上更大。按钮的Hidden复选框对于不同size classes可能不同,因此在某些设备类型上按钮是不可见的。Attributes inspector的底部是Installed复选框;对于特定size class组合取消选中此复选框会导致按钮完全不在界面中。

在画布中的Size classes

假设您的界面有一个按钮固定在它的父视图的左上角。假设仅在iPad设备上,您希望将此按钮固定在其父视图的右上角。(同样,这是不可能的,但很戏剧性。)这意味着按钮的leading约束将只存在于iPhone设备上,而将被iPad设备上的trailing约束所取代。约束是不同的对象。为不同size classes配置不同对象的方法是使用Vary for Traits按钮,如下所示:

  1. 在device type按钮中,单击其中一个iPhone按钮(最右边)。配置按钮,使其由主视图的顶部和左上方固定。

  2. 在device type按钮中,单击其中一个iPad按钮(最左边)。size classes现在列为wR hR。

  3. 单击Vary for Traits.。在出现的小弹出框中,选中这两个框:我们希望仅当宽度size class和高度size class与当前的size class(两者都应为.regular)匹配时,才应用即将进行的更改。整个布局栏变为蓝色,表示我们在特殊的条件设计模式下工作。

  4. 进行所需的更改:选择界面中的按钮;选中left约束;删除left约束;将按钮滑动到界面右侧;拖动操作按钮向右并创建新的trailing约束。如有必要,单击Update Frames按钮,使橙色Misplaced Views警告符号消失。

  5. 单击Done Varying。布局栏不再是蓝色。

我们已经创建了一个条件约束。要查看这是否正确,请单击iPhone设备按钮,然后单击iPad设备按钮。正如您所做的,界面中的按钮在界面的左侧和右侧之间跳跃。其位置取决于设备类型!

这个按钮的inspectors和我们刚做的更改相符。要查看是否为真,请单击按钮,选择trailing或leading约束(取决于设备类型),然后查看Attributes或Size inspector.。约束有两个Installed复选框,一个用于一般情况,一个用于wR hR。只选中其中一个复选框;约束在一种情况下存在,但在另一种情况下不存在。

在文档大纲中,没有为当前size classes集安装的约束或视图将以暗淡图标列出。

Xcode视图特征

本节总结了Xcode的一些与视图相关的其他特性,这些特性值得了解。

视图调试器(View Debugger)

要进入视图调试程序,请选择Debug → View Debugging → Capture View Hierarchy,,或单击调试栏中的Debug View Hierarchy按钮。结果是分析和显示app当前视图层次结构(图1-21):

  • 在Debug导航器左侧,视图及其约束按层次列出。(视图控制器也作为层次结构的一部分列出。)

在这里插入图片描述
图1-21 视图调试(再次)

  • 在中心、画布中,以图形方式显示视图及其约束。窗口从前面开始,就像你在看运行应用程序的屏幕一样;但是如果你在画布上侧滑一点(或者点击画布底部的Orient to 3D按钮,或者选择Editor → Orient to 3D),窗口旋转,其子视图显示在它的前面,在层中。您可以通过各种方式调整您的视角;例如:
    • 左下角的滑块改变层之间的距离。
    • 右下角的双滑块可以从分层顺序(或两者)的前面或后面消除视图显示。
    • 你可以双击一个视图来聚焦它,从显示器上消除它的父视图。双击视图外部以退出焦点模式。
    • 您可以切换到线框模式。
    • 您可以显示当前选定视图的约束。
  • 在右侧,Object inspector和Size inspector会告诉您有关当前所选对象(视图或约束)的详细信息。

在Debug导航器或画布中选择视图时,Size inspector将列出其边界和确定这些边界的所有约束。这与视图的分层图形显示以及画布中的约束一起,可以帮助您找出任何约束相关困难的原因。

预览界面

在Xcode中显示NIB编辑器时,显示assistant pane。它的Tracking菜单(其跳转栏中的第一个组件)包括Preview选项。选择它可以查看当前所选视图控制器视图的预览(或在.xib文件中,顶级视图)。

在左下角,加号按钮允许您为不同的设备和设备大小添加预览;因此,您可以同时比较不同设备上的界面。在每个预览的底部,使用旋转按钮可以切换其方向。预览考虑了约束和条件接口。

在右下角,语言弹出菜单允许您将应用程序的文本(按钮和标签)切换为已本地化应用程序的另一种语言,或切换为人工的“double-length”语言。

Designable视图和Inspectable属性

您的自定义视图可以在NIB编辑器画布中正确绘制并预览,即使它是在代码中配置的。要利用此功能,需要声明为@IBDesignable的UIView子类。

如果这个UIView子类的一个实例出现在nib编辑器中,那么它的自配置方法如willMove(toSuperview:)将在nib编辑器准备描绘视图时编译并运行。此外,您的视图可以实现特殊的方法prepareForInterfaceBuilder来执行可视化配置,专门针对如何在NIB编辑器中描述它。通过这种方式,您甚至可以在NIB编辑器中描述出一个特性,您的视图将在app的后期采用该特性。例如,如果你的视图里面包含一个创建和配置为空但最终将包含文本的UILabel,则可以实现
prepareForInterfaceBuilder,为标签提供一些要在NIB编辑器中显示的示例文本。

在图1-22中,我重构了一个熟悉的示例。我们的视图子类为自己提供了一个洋红色的背景,以及两个子视图,一个横跨顶部,另一个位于右下角—— 都是用代码设计的。NIB包含这个视图子类的一个实例。当app运行时,将调用
willMove(toSuperview:),代码也将运行,并且子视图也将出现。但由于willMove(toSuperview:)也被NIB编辑器调用,因此子视图也显示在NIB编辑器中:

@IBDesignable class MyView: UIView {
	func configure() {
		self.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1)
		let v2 = UIView()
		v2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1)
		let v3 = UIView()
		v3.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
		v2.translatesAutoresizingMaskIntoConstraints = false
		v3.translatesAutoresizingMaskIntoConstraints = false
		self.addSubview(v2)
		self.addSubview(v3)
		NSLayoutConstraint.activate([
			v2.leftAnchor.constraint(equalTo:self.leftAnchor),
			v2.rightAnchor.constraint(equalTo:self.rightAnchor),
			v2.topAnchor.constraint(equalTo:self.topAnchor),
			v2.heightAnchor.constraint(equalToConstant:20),
			v3.widthAnchor.constraint(equalToConstant:20),
			v3.heightAnchor.constraint(equalTo:v3.widthAnchor),
			v3.rightAnchor.constraint(equalTo:self.rightAnchor),
			v3.bottomAnchor.constraint(equalTo:self.bottomAnchor),
		])
	}
	override func willMove(toSuperview newSuperview: UIView!) {
		self.configure()
	}
} 

在这里插入图片描述
图1-22 可设计视图

此外,可以直接在NIB编辑器中配置自定义视图属性。为此,UIView子类需要一个属性,声明为@IBInspectable,并且此属性的类型需要是inspectable属性类型的有限列表之一(我稍后将告诉您它们是什么)。现在让我们假设在NIB中有一个这个UIView子类的实例:这个属性将在视图的Attributes inspector的顶部得到它自己的一个字段,在那里你可以在NIB编辑器中设置这个属性的初始值,而不必在代码中设置它。(此功能实际上相当于在Identity inspector中设置NIB对象User Defined Runtime Attributes。)
在这里插入图片描述
图1-23 具有inspectable属性的designable视图

inspectable属性类型为:Bool, number, String, CGRect, CGPoint, CGSize, NSRange, UIColor, 或UIImage。对于类(UIColor和UIImage),属性类型可以是Optional。除非显式声明属性的类型,否则该属性将不会显示在Attributes inspector中。您可以在代码中指定默认值;Attributes inspector不会将此值描述为默认值,但您可以告诉它使用默认值,方法是将字段留空(或者,如果已经输入了一个值,则通过删除该值方式来实现)。

在运行init(coder:)和willMove(toSuperview:)之后,才会应用在NIB编辑器中设置的IBInspectable属性值。您的最早代码可以在awakeFromNib中检索这个值。

@IBDesignable和@IBInspectable是无关的,但前者知道后者。因此,可以使用inspectable属性更改NIB编辑器界面显示。

在这个例子中,我们使用@IBDesignable和@IBInspectable来解决NIB编辑器的一个恼人的限制。通过设置layer的borderWidth,UIView可以自动绘制自己的边框(第3章)。但这只能在代码中配置。视图的Attributes inspector中没有任何东西可以让您设置layer的borderWidth,而特殊的layer配置通常不会在画布中描绘出来。用@IBDesignable和@IBInspectable来解围:

@IBDesignable class MyButton: UIButton 
	@IBInspectable var borderWidth: Int {
		set {
			self.layer.borderWidth = CGFloat(newValue) 
		}
		get {
			return Int(self.layer.borderWidth)
		}
	}
}

结果是,在NIB编辑器中,按钮的Attributes inspector有一个Border Width自定义属性,当我们更改Border Width属性设置时,该按钮将使用该Border Width重新绘制(图1-23)。此外,我们在NIB中设置了这个属性,所以当应用程序运行并加载NIB时,按钮在运行的应用程序中确实有这个边界宽度。

布局事件

本节总结了与布局相关的主要UIView事件。这些事件可以通过在UIView子类中重写它们来接收和响应。您可能希望在布局复杂的情况下这样做——例如,当您需要用代码中的手动布局补充autoresizing或autolayout时,或者当布局配置需要根据更改的条件进行更改时。(与这些UIView事件密切相关的是一些与布局相关的事件,您可以在UIViewController接收和响应)

updateConstraints

如果您的接口涉及自动布局和约束,那么当运行时认为您的代码可能需要配置约束时,updateConstraints传播到层次中,从 最深的子视图开始。您可能会重写updateConstraints,因为您有一个UIView子类能够改变它自己的约束。如果你这样做 了,你必须通过调用super来完成,否则应用程序将崩溃(显示一条有用的错误消息)。

updateConstraints在 启 动 时 调 用 , 但 很 少 在 启 动 之 后 调 用 , 除 非 您 导 致 调 用 它 。 您 不 应 该 直 接 调 用updateConstraints。若要立即调用updateConstraints,请发送一个视图updateConstraintsFeeded消息。要强制将updateConstraints发送到特定视图,请向其发送setNeedSupdateConstraints消息。

traitCollectionDidChange(_: )

在启动时,如果环境的trait collection此后发生更改,则traitCollectionDidChange(_:)消息将沿着UITraitEnvironments的层次结构向下传播。传入参数是旧trait collection;若要获取新特性集合,请调用self.traitCollection。

因此,如果您的界面需要响应trait collection中的更改——通过更改约束、添加或删除子视图或您所拥有的内容——则可以重写traitCollectionDidChange。

例如,在本章的前面,我展示了一些用于将视图交换到界面中或交换出界面的代码,以及布置该界面的整个约束集。但是,我没有讨论我们希望发生这种交换的条件;traitCollectionDidChange可能是一个合适的时机(因为我们希望在应用程序旋转时更改界面)。然后,典型的实现将检查新的trait collection,并根据其水平或垂直size class做出响应。

layoutSubviews

layoutSubviews消息是布局本身发生的!它沿着层次结构向下传播,从顶部(通常是根视图)开始,一直向下传播到最深的子视图。即使trait collection没有更改,也可以触发布局;例如,可能更改了约束,或者更改了标签的文本,或者父视图的大小发生了更改。

您可以重写UIView子类中的layoutSubviews,以便参与布局过程。如果您不使用autolayout,则默认情况下,layoutSubviews不执行任 何操作;layoutSubviews是您在autoresizing后执行手动布局的机会。如果您正在使用autolayout,则必须调用super,否则app将崩溃(并显示一条有用的错误消息)。

您不应该直接调用layoutSubviews;要立即调用layoutSubviews,请发送一个视图layoutIfNeeded消息(这可能会导致整个视图树的布局,不仅位于此视图的下方,也位于此视图的上方),或者在代码运行完成后,稍后发送setNeedsLayout触发对layoutSubviews的 调用,此时布局将正常地发生了。

对安全区域或视图布局边距的更改可以触发布局。在这种情况下,layoutMarginsDidChange和safeAreaInsetsDidChange通常在layoutSubviews之前调用,但您可能不应该在那些依赖于以任何特定顺序发生的事情的方法中执行任何操作。

布局驱动UI

调用setNeedsLayout来触发布局和实现layoutSubviews来执行布局的组合产生了一个编程范例,苹果在2018年的WWDC视频中 调用了布局驱动UI。例如,假设界面中的子视图需要移动到不同的位置。当代码发现这一事实时,它不会直接当场更改其中心,而是在子视图的父视图上调用setNeedsLayout。最终,父视图的layoutSubviews被调用,此时,父视图将布局其子视图。因此,对其子视图放置位置的控制被封装在父视图及其layoutSubviews实现中。

举例来说,假设我们有一个基本的纸牌游戏应用程序,用户可以在纸牌组中点击一张纸牌,将其放入“手”中。(这基本上是WWDC视频提出的示例。)为了简化操作,我将让每个卡都是一个名为Card的UIButton子类的实例,当它被点击时会发出通知:

class Card : UIButton {
	static let tappedNotification = Notification.Name("tapped") 
	required init?(coder aDecoder: NSCoder) {
		super.init(coder:aDecoder)
		self.addTarget(self, action: #selector(tapped), for: .touchUpInside)
	}
	@objc func tapped(_ sender:UIGestureRecognizer) {
	 	NotificationCenter.default.post( 
			name: Card.tappedNotification, object: self)
	}
}

卡片的父视图是一个叫做GameBoard的UIView子类的实例。它记录由点击tapped Card发出的通知:

class GameBoard : UIView {
	required init?(coder aDecoder: NSCoder) {
		super.init(coder:aDecoder)
		NotificationCenter.default.addObserver(
			self, selector: #selector(tapped),
			name: Card.tappedNotification, object: nil)
	}
	// ...
} 

有趣的部分来了。当一张卡被点击时,它需要被移出甲板区域,进入用户的“手”区域。然而,在其抽头方法中,所有游戏板都是将抽头卡添加到自己的手阵列中。它实际上不会移动卡!相反,hand有一个setter observer,它通过触发布局来响应自身的修改:

var hand = [Card]() {
	didSet {
		self.setNeedsLayout()
	}
}
@objc func tapped(_ n:Notification) {
	guard let v = n.object as? Card else {return}
	if let ix = self.hand.firstIndex(of:v) {
		// do nothing
	} else {
		self.hand.append(v)
	}
}

卡什么时候动?在layoutSubviews中!这就是我们看到封装的好处的地方。比如说,用户“手”中的卡片应该出现在界面“手”区域的left-to-right的一行中,由称为handView的子视图划分。(在本例中,我将保持简单,并假设“手”中的所有卡片都可以放在一个水平行中。)layoutSubviews表示这个——并通过在构成手阵列的卡片和这些卡片的位置之间建立简单的对应关系来实施:

override func layoutSubviews() {
	super.layoutSubviews()
	for (ix,v) in self.hand.enumerated() {
		let p = CGPoint(
			self.handView.frame.minX + 20 + v.bounds.width / 2.0 +
			(10 + v.bounds.width) * CGFloat(ix),
			self.handView.frame.minY + 10 + v.bounds.height / 2.0
		)
		v.center = p
	}
}

这种方法在任何有布局的时候都会被调用,包括当一个Card被点击时。它的表达之美在于,layoutSubviews不知道或不关心什么Card被点击,或是否有任何Card被点击。它只知道一件事:“手”中的牌必须按顺序出现在handView区域中。就布局而言,这就是它在“手”中的含义。可能是在调用layoutSubviews时,手组中的大多数或甚至所有Cards都已处于正确的位置。没关系!声明他们的正确位置也没有什么坏处,即使他们已经占据了正确的位置。但是,如果手上的卡没有占据正确的位置,那么在调用layoutSubviews之后,手上的卡就会占据它。

调整Autolayout

当使用自动布局时,layoutSubviews中会发生什么?运行时会检查并处理了影响此视图子视图的所有约束,并计算了它们的center 和bounds值,现在只需分配center和bounds值给它们。换句话说,layoutSubviews执行手动布局!

知道了这一点,您可能会在使用autolayout时重写layoutSubview,以便调整结果。一个典型的结构是:首先调用super,使所有 子视图采用它们的新框架;然后检查这些框架;如果您不喜欢结果,可以更改内容;最后调用super,以获得新的布局结 果。正如我前面提到的,在layoutSubviews中显式地设置视图的frame(或bounds或center)是非常好的,即使这个视图使用autolayout;毕竟,autolayout引擎本身就是这样做的。但是,请记住,您必须与自动布局引擎合作。不调用setNeedsUpdateConstraints——那一刻已经过去了——不要偏离这个视图的子视图。(不遵守这些规则会导致应用程序挂起。)

模拟Autolayout

可以根据视图的约束和子视图的约束来模拟视图的布局。如果autolayout引擎此时执行布局,这对于提前发现视图的大小非常有用。

向视图发送systemLayoutSizeFitting(_:)消息。系统将尝试以非常低的优先级达到或至少接近您指定的大小。这个调用相对缓慢且昂贵,因为必须创建临时的autolayout引擎,将其设置为工作然后丢弃。但有时,这是获取所需信息的最佳方式。

很可能您会指定UIView.layoutFittingCompressedSize或UIView.layoutFittingExpandedSize,这取决于您所追求的是视图可以合法达 到的最小或最大尺寸。这里的想法是视图的尺寸在内部受约束于其子视图的约束。在一些情况下,iOS运行时实际上会以这种方式调整视图的大小(尤其是在UITableViewCells和UIBarButtonItems方面)。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值