视图(其类是UIView或UIView的子类的对象)知道如何将自己绘制到界面的矩形区域中。 多亏了视图,你的应用程序才有一个可见的界面; 用户看到的一切最终都是因为视图。 视图的创建和配置非常简单:“设置完后就无需再关注。” 例如,你可以在nib编辑器中配置UIButton; 当应用程序运行时,按钮出现,并正常工作。 但您也可以实时地以强大的方式操纵视图。 您的代码可以完成视图自身的部分或全部绘图; 它可以使视图出现和消失,移动,调整自身大小,并显示许多其他物理变化,可能跟随着动画一起。
视图也是一个responder (UIView是UIResponder的子类)。 这意味着视图受到用户交互的影响,比如点击和滑动。 因此,视图不仅是用户看到的界面的基础,也是用户触摸的界面的基础。组织你的视图,以便使正确的视图对给定的触摸作出反应,这允许您整洁而高效地配置代码。
视图层次结构是视图组织的主要模式。 一个视图可以有子视图; 子视图只有一个直接父视图。 这样就有了视图树。 这个层次结构允许视图来来去去 。 如果一个视图从界面被移除,它的子视图将被移除; 如果一个视图被隐藏(使其不可见),则它的子视图被隐藏; 如果一个视图被移动,它的子视图也随之移动; 视图中的其他更改同样与它的子视图共享。 视图层次结构也是responder chain的基础,尽管它与responder chain并不相同。
视图可以来自nib,也可以在代码中创建它。 总的来说,两种方法都不另外方法更优; 这取决于您的需求和偏好,以及应用程序的总体架构。
窗口和根视图
视图层次结构的顶部是应用程序的窗口。 它是UIWindow的一个实例(或者你自己的子类),它是UIView的子类。 你的应用程序应该只有一个主窗口。 它是在启动时创建的,不会被销毁或替换。 它是所有其他可见视图的背景,也是最终的父视图。 之所以其他视图是可见的,因为它们是应用程序窗口的某个深度的子视图。
如果你的应用可以在外部屏幕上显示视图,你将创建一个额外的UIWindow来包含这些视图; 但在这本书中,我假设只有一个屏幕,设备自己的屏幕,只有一个窗口。
应用程序如何启动
你的应用的根视图控制器是你的应用的UIWindow和它包含的界面之间的纽带。 在初始视图控制器被实例化之后,该实例将被分配给窗口的rootViewController属性。 一旦成为应用的根视图控制器,它的视图从此就占据了整个窗口。 根视图控制器本身将是视图控制器层次结构的顶部。
但是,你的应用程序在启动时,首先是如何拥有一个主窗口的,以及这个窗口是如何被填充和显示的? 如果你的应用使用一个主故事板,就会自动有一个被填充并显示的主窗口。 你的应用程序最终由一个对UIApplicationMain函数的调用组成。 (与Objective-C项目不同,一个典型的Swift项目不会在代码中显式地调用这个调用; 它在幕后为你被调用。) 以下是这个调用首先要做的一些事情:
-
UIApplicationMain实例化UIApplication并保留这个实例,将作为共享的应用程序实例,您的代码稍后可以通过UIApplication.shared来引用这个实例。 然后实例化app
delegate类。 (它知道哪个类是app delegate,因为它被标记为@UIApplicationMain。) 它保留app delegate实例,从而确保它将在应用程序的生命周期中持续存在,并将其分配为应用程序实例的delegate属性。 -
UIApplicationMain查看应用程序是否使用主故事板。 它通过查看info.plist的键值“Main stroyboard file base name”(UIMainStoryboardFile)知道你是否在使用主情节串连板,以及它的名称。 如果需要,可以通过编辑target和在General窗格中更改Deployment Info中的主界面值来轻松编辑此键。 默认情况下,一个新的iOS项目有一个名为
Main.storyboard的主故事板 ,并且主界面值是Main。 -
如果您的应用程序使用一个主要的故事板,UIApplicationMain实例化UIWindow并分配改实例给app delegate的window属性,保留它,从而确保窗口将持续应用程序的生命周期,还调整窗口大小,以便它最初将填充设备的屏幕。 这是通过将窗口的frame设置为屏幕的bounds来确保的。
-
如果你的应用使用一个主故事板,UIApplicationMain实例化那个故事板的初始视图控制器。 然后它将这个视图控制器实例分配给窗口的rootViewController属性,后者保留它。 当一个视图控制器成为主窗口的根视图控制器时,它的主视图(它的视图)成为你的主窗口的唯一的直接子视图——主窗口的根视图。 主窗口中的所有其他视图都是根视图的子视图。
因此,根视图是视图层次结构中用户通常看到的最高对象。 在某些情况下,用户可能只会瞥见根视图后面的窗口;因此,您可能希望为主窗口分配一个合理的backgroudColor。 一般来说,您没必要改变窗口本身的任何其他内容。
-
UIApplicationMain调用app delegate的application(_:didFinishLaunchingWithOptions:)。
-
应用程序的界面在包含它的窗口成为应用程序的关键窗口之前是不可见的。 因此,如果你的应用使用主故事板,UIApplicationMain调用窗口的实例方法makeKeyAndVisible。
也有可能编写一个没有主故事板的应用,或者有一个主故事板,但在某些情况下,通过覆盖部分或全部的自动UIApplicationMain行为,在启动时有效地忽略它。 这样的应用程序只需在代码中完成——通常是在application(_:didFinishLaunchingWithOptions:)——UIApplicationMain在应用程序有主故事板时自动完成的所有事情:
- 实例化UIWindow并将其指定为app delegate的window属性。 通过一些巧妙的编码,如果UIApplicationMain已经这样做了,我们可以避免这样做:
self.window = self.window ??UIWindow()
-
实例化视图控制器,根据需要配置它,并将其分配为窗口的rootViewController属性。 如果UIApplicationMain已经分配了一个根视图控制器,这个视图控制器会替换它。
-
调用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而再次可见。 隐藏视图将它(当然还有它的子视图)从可见接口中取出,而不需要从视图层中实际删除它。 一个隐藏的视图(通常)不会接收触摸事件,所以对于用户来说,它实际上就像视图不存在一样。 但是它是存在的,所以它仍然可以在代码中操作。
视图可以通过它的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。 零,这不是你想要的。 在代码中创建视图时忘记为它分配框架,然后想知道为什么添加到父视图时它没有出现,这是初学者常见的错误。 一个零大小框架的视图实际上是不可见的
(虽然可能仍然会看到它的子视图,但这可能会增加混乱)。 如果一个视图有一个你想要它采用的标准尺寸,特别是关于它的内容(就像一个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乘以第二个属性值
网友评论