在UIkit中创建iOS自定义视图

最初 Andres 在2017年10月17日 发布在 www.scalablepath.com 上。

我刚从大学毕业就开始使用iOS。 使用不熟悉的平台和编程语言是一项挑战。 真正使我紧张的是什么? 是创意团队要完成的设计。 出色的设计可以为网站注入新的活力,但它越独特,就越有可能需要在UIKit中创建iOS自定义视图。 作为一个新开发人员,这可能会令人生畏。

本文的目的是解释如何将任何设计转换为功能用户界面元素。

iOS定制视图

视图是应用程序用户界面的基本构建块。 通常,您可以通过将故事板中的视图从库中拖动到画布来在其故事板上创建视图。 但是,有时您需要使用UIKit中的标准“标签”或“按钮”元素创建一个不可用的元素。 这是您需要自定义视图的时间。

顺便说一句,如果本文中的任何术语听起来都不熟悉,您可能需要查看Apple的UIKit文档。 在本文的其余部分,我将假设您具有UIKit和Swift的使用知识。

初始设计

让我们从选择无法在标准UIKit视图中重新创建的设计开始。 我找到了由耿高设计的“圆形进度栏”,它非常适合该法案。

此元素由两个文本标签(标题和副标题)和一个圆形完成指示符组成,该指示符旨在在任务进行时填充灰色轨道。 我们将从创建一个新的Xcode项目并将其命名为CircularProgressBar开始。

文件>新建>项目>单视图应用程序

当我们将创建自定义UIView时,我们需要使用新类CircularProgressBar扩展UIKit的默认UIView。

 导入UIKit 
class CircularProgressBar:UIView {
}

有两种向设计中添加标签的方式:通过代码或使用.xib文件。

为了避免进一步的混乱,术语“ xib”和“ nib”通常可以互换使用。 NIB来自“ NeXTSTEP Interface Builder”,这是苹果现已停产的操作系统。 虽然.nib文件已替换为.xib文件,但开发人员仍将其称为“ nibs”。

我喜欢使用.xib文件创建我的iOS自定义视图,因为它们需要较少的编码并且更容易进行更改。 因此,我们执行此操作,并将其命名为CircularProgressBar。

文件>新建>文件,然后选择查看

我们将在导航器中选择CircularProgressBar.xib,然后将.xib的文件所有者定义为我们的类扩展名:CircularProgressBar

等一下,什么是文件所有者? StackOverflow提供了比以往任何时候都更优雅的解释: “文件所有者是一个实例化的运行时对象,在加载.nib时,它拥有.nib的内容及其出口/动作。 它可以是您喜欢的任何类的实例。”

有了这些了解,我们将隐藏状态栏,并将.xib的大小设置为“自由格式”。 这样,我们可以更改尺寸,以便视图具有与设计相似的尺寸。 在这种情况下为300 x 300像素。

为了清楚起见,让我们将背景设为红色以使其突出。 我们还将添加标题和字幕标签。

然后,我们需要编写以下代码,以便CircularProgressBar加载刚刚创建的.xib文件:

 导入UIKit 
 打开类CircularProgressBar:UIView { 
var view:UIView!
必需的公共初始化?(编码器aDecoder:NSCoder){
super.init(编码器:aDecoder)
loadViewFromNib()
}
 覆盖init(frame:CGRect){ 
super.init(frame:框架)
loadViewFromNib()
}
  func loadViewFromNib(){ 
let bundle = Bundle(for:type(of:self))
let nib = UINib(nibName:String(描述:type(of:self)),bundle:bundle)
让view = nib.instantiate(withOwner:self,选项:nil)。 UIView
view.frame =界限
view.autoresizingMask = [
UIViewAutoresizing.flexibleWidth,
UIViewAutoresizing.flexibleHeight
]
addSubview(view)
self.view =视图
}
}

接下来,将UIView标记为@IBDesignable并将其添加到Main.storyboard中以查看其呈现方式。 如果您不确定IBDesignable是什么,请查看NSHipster上的这篇很棒的文章。

要将CircularProgressBar添加到Main.storyboard中的ViewController,我们必须添加一个标准的UIView,然后将其类更改为CircularProgressBar。 如果一切顺利,您应该在ViewController中看到呈现的自定义视图:

您总是可以在我们的存储库上查看以下标记的提交:阶段1:界面构建器中的渲染视图。

创建图层

到目前为止,我们已经创建了一个自定义UIView,该UIView加载.xib文件并在界面生成器中进行呈现。 现在,我们开始介绍一些很酷的东西:核心动画层。

CALayer是Core Animation的简写:该框架提供渲染图形和动画所需的所有工具。 这个庞大的框架有时可能不堪重负且不切实际,因此Apple在其之上构建了更简单的UIKit。 UIKit是创建简单视图(如UILabels,UITextViews等)的简便方法。

在创建自定义设计时,我们需要通过CALayers利用Cora Animation框架的功能。 让我们从简单地画一个圆的圆周开始。 创建一个新层并将其命名为BorderLayer。

File> New> File> Cocoa Touch Class

让我们向CircularProgressBar添加一个新层。 为此,我们将创建一个名为commonInit()的方法,并在初始化程序的loadViewFromNib()之后立即调用它。 在commonInit()中,创建一个新的BorderLayer实例,并将其添加到视图层。

 打开函数commonInit(){ 
让borderLayer = BorderLayer()
self.layer.addSublayer(borderLayer)
}

如果您运行该应用程序,您会发现似乎没有任何更改。 图层在那里,但是我们看不到它,因为它没有尺寸,颜色和图纸。 因此,让我们添加这些属性!

1.创建BorderLayer的实例,并将其存储在名为“ darkBorderLayer”的常量中。

2.将“ darkBorderLayer”添加到视图的图层。

3.这将覆盖layoutSubviews()。 解析所有大小后,将调用该方法,而视图的大小将为其最终大小。 因此,这是设置图层大小的最佳时间。

4.调用setNeedsDisplay()通知系统该图层的内容需要重绘。

仅供参考:下面的代码段中评论了上述每个步骤。

 打开类CircularProgressBar:UIView { 
var view:UIView!
// 1
让darkBorderLayer = BorderLayer()



// 2
打开函数commonInit(){
self.layer.addSublayer(darkBorderLayer)
}
// 3
覆盖open func layoutSubviews(){
super.layoutSubviews()
darkBorderLayer.frame = self.bounds
// 4
darkBorderLayer.setNeedsDisplay()
}
}

只缺少一件事:实际上在BorderLayer中绘制了一些东西! 这是通过重写方法draw(在ctx:CGContext中)完成的。 我喜欢将CGContext视为可以在其中进行绘图的白板。

  class BorderLayer:CALayer { 
覆盖功能绘制(在ctx中:CGContext){
让lineWidth:CGFloat = 2.0
让中心= CGPoint(x:bounds.width / 2,y:bounds.height / 2)
ctx.beginPath()
ctx.setStrokeColor(UIColor.blue.cgColor)
ctx.setLineWidth(lineWidth)
ctx.addArc(
中心:
半径:bounds.height / 2-lineWidth,
startAngle:0,
endAngle:2.0 * CGFloat.pi,
顺时针:false

ctx.drawPath(使用:.stroke)
}
}

运行该应用程序时,您应该看到两个标签被一个蓝色圆圈包围。

如果事情看起来不太正确,请不要担心。 您可以在存储库中查看到目前为止我们已经完成的所有工作。 提交称为:阶段2:将第一层添加到视图中。

原始设计由两个重叠的圆圈组成:绿色圆圈下方的灰色圆圈。 后者显示了任务进度。 为了达到这种效果,我们需要再创建一层并将其存储在progressBorderLayer中。 顺便说一句,您可以根据需要添加任意多个图层。 您只需要记住,各层是堆叠在一起的,因此最新标签将覆盖以前的所有标签。

最后,我们将重构BorderLayer,以便我们可以从类外部设置颜色,大小,起始角度和终止角度:

 导入UIKit 
  class BorderLayer:CALayer { 
var lineColor:CGColor = UIColor.blue.cgColor
var lineWidth:CGFloat = 2.0
var startAngle:CGFloat = 0.0
@NSManaged var endAngle:CGFloat = 0.0
覆盖功能绘制(在ctx中:CGContext){
让中心= CGPoint(x:bounds.width / 2,y:bounds.height / 2)
ctx.beginPath()
ctx.setStrokeColor(lineColor)
ctx.setLineWidth(lineWidth)
ctx.addArc(
中心:
半径:bounds.height / 2-lineWidth,
startAngle:startAngle,
endAngle:endAngle,
顺时针:false

ctx.drawPath(使用:.stroke)
}
}

在Swift中,@ NSManaged是告诉编译器这实际上是@dynamic Objective-c变量的方式。 本质上,@dynamic告诉Core Animation跟踪属性更改,然后从我们的层调用不同的方法。 更深入的解释超出了本文的范围,因此您只需要信任我!

现在,让我们将progressBorderLayer添加到我们的视图中,并设置设计模板中使用的正确颜色。

 导入UIKit 
  @IBDesignable 
 打开类CircularProgressBar:UIView { 
var view:UIView!
让darkBorderLayer:BorderLayer!
var progressBorderLayer:BorderLayer!



覆盖open func layoutSubviews(){
super.layoutSubviews()
darkBorderLayer.frame = self.bounds
progressBorderLayer.frame = self.bounds
progressBorderLayer.setNeedsDisplay()
darkBorderLayer.setNeedsDisplay()
}
 打开函数commonInit(){ 
darkBorderLayer = BorderLayer()
darkBorderLayer.lineColor = UIColor(
红色:134/255,
绿色:133/255,
蓝色:148/255,
阿尔法:1
).cgColor
darkBorderLayer.startAngle = 0
darkBorderLayer.endAngle = 2.0 * CGFloat.pi
self.layer.addSublayer(darkBorderLayer)
progressBorderLayer = BorderLayer()
progressBorderLayer.lineColor = UIColor(
红色:168/255,
绿色:207/255,
蓝色:45/255,
阿尔法:1
).cgColor
progressBorderLayer.startAngle = 0
progressBorderLayer.endAngle = CGFloat.pi
self.layer.addSublayer(progressBorderLayer)
}
}

在匹配了背景颜色和字体之后,您应该看到事情真的开始融合起来。

现在,我们将为标题和副标题创建@IBInspectable变量,然后在界面构建器中更改其值。

  @IBOutlet弱var titleLabel:UILabel! 
  @IBOutlet弱var subtitleLabel:UILabel! 
  @IBInspectable变量标题:String =“” { 
didSet {
titleLabel.text =标题
}
}
  @IBInspectable var副标题:String =“” { 
didSet {
subtitleLabel.text =字幕
}
}

我在这里创建另一个提交,以便您可以完成到目前为止的所有工作。 该提交称为:阶段3:添加第二层并应用样式。

更新进度栏

就快到了! 我们将在视图中更改进度栏的值。 这是通过更新绿线层来完成的。 我们还将在主故事板上添加一个UISlider,以便我们可以测试进度指示器的行为。

将UISlider连接到“值已更改”操作,并将滑块的最小值和最大值设置为0和100。我在图像中标记了我们希望绿线开始填充灰色轨迹的位置:这是我们的0值。

您可能已经注意到,在BorderLayer中,我们使用一种名为addArc的方法绘制圆的边界。 此方法接收两个我们需要特别注意的参数:startAngle和endAngle。 您可能会猜测它们的名称,但重要的是要知道它们接受的弧度值。 弧度只是测量角度的一种方法。 要使用它们,我们需要构建一个函数,该函数接收一个数字并以弧度形式返回该数字的等效值。 例如,此函数会将30%转换为其等效的弧度。 一旦有了弧度值,就可以在addArc中使用它来绘制弧线:

 静态让startAngle = 3/2 * CGFloat.pi 
 静态让endAngle = 7/2 * CGFloat.pi 
 内部类函数radianForValue(_ value:CGFloat)-> CGFloat { 
让realValue = CircularProgressBar.sanitizeValue(value)
return(realValue * 4/2 * CGFloat.pi / 100)+ CircularProgressBar.startAngle
}

 内部类func sanitizeValue(_ value:CGFloat)-> CGFloat { 
var realValue =值
如果值<0 {
realValue = 0
}如果值> 100 {
realValue = 100
}
返回realValue
}

我们还将使用两个新的常量startAngle和endAngle来更改图层的startAngle和endAngle。 转到CircularProgressBar中的commonInit()方法并更改角度。

 打开函数commonInit(){ 
darkBorderLayer.lineColor = UIColor(
红色:134/255,
绿色:133/255,
蓝色:148/255,
阿尔法:1
).cgColor
darkBorderLayer.startAngle = CircularProgressBar.startAngle
darkBorderLayer.endAngle = CircularProgressBar.endAngle
self.layer.addSublayer(darkBorderLayer)
progressBorderLayer.lineColor = UIColor(
红色:168/255,
绿色:207/255,
蓝色:45/255,
阿尔法:1
).cgColor
progressBorderLayer.startAngle = CircularProgressBar.startAngle
progressBorderLayer.endAngle = CircularProgressBar.endAngle
self.layer.addSublayer(progressBorderLayer)
}

让我们来回顾一下:我们有滑块,一个将CGFloat值转换为弧度和视图的函数。 现在,我们所需要做的就是更新progressBorderLayer的endAngle属性:

  @IBInspectable var进度:CGFloat = 0.0 { 
  didSet { 
progressBorderLayer.endAngle = CircularProgressBar.radianForValue(进度)
}
}

上面创建了一个@IBInspectable变量,称为progress。 我们将使用didSet观察器(在存储新值之后立即调用它)来更新层的endAngle。

UISlider应该更新我们视图的progress属性。 为此,请为CircularProgress栏创建一个@IBOutlet变量,并将其连接到Main.storyboard主视图。 将其命名为circularProgressBar。 然后,我们将创建一个函数,该函数将在用户每次移动滑块时调用。 该函数接收具有当前值(0到100之间)的UISlider对象作为参数。 然后,我们认为该值会发送到progressproperty。

  @IBOutlet弱var CircularProgressBar:CircularProgressBar! 
  @IBAction func slideAction(_ sender:UISlider){ 
self.circularProgressBar.progress = CGFloat(sender.value)
}

CALayers很懒惰,他们不喜欢自己重画。 这是一种内置的优化,可以避免在图层的属性没有更改的情况下重绘图层。 因此,如果希望每次endAngle属性更新时都重新绘制progressBorderLayer,则需要向BorderLayer添加以下方法:

 覆盖类func needsDisplay(forKey键:String)-> Bool { 
如果key ==“ endAngle” {
返回真
}
返回super.needsDisplay(forKey:key)
}

该函数只是简单地说:“如果endAngle属性发生更改,那么我们需要更新图层并调用重绘”。

现在一切都已设置好,让我们运行该应用程序!

我创建了对项目存储库的最终提交:阶段4:更新进度。

结论

希望本教程为您提供了在UIKit中创建自己的iOS自定义视图的工具和知识。

在UIView扩展中加载.xib文件本身节省了很多时间。 它使您可以使用已经熟悉的UI工具来编写视图,同时仍将视图逻辑保留在自定义类中。 然后,通过在视图控制器中保留用于将用户交互连接到视图属性的逻辑,可以提高应用程序的可维护性,并能够在任何地方重用自定义视图。