前几节课过于基础,所以没做笔记,“五”指第5节课
「written by Talaxy on 2020/2/18」
抛出异常
func save() throws
do {
try context.save()
} catch let error {
// ...
throw error
}
如果你能够确定语句不会报错,可以:
try! context.save()
如果不确定,可以:
let x = try? context.save()
其中x是Int?类型。如果try语句报错,则返回nil。
Any & AnyObject
Any & AnyObject 通常是为了兼容OC中的API所使用。
因为swift是强类型语言,所以方法也可以推断为一个Any类型。
Any类型无法使用任何属性方法,所以平常中我们不要去使用Any。但我们会需要将一个Any类型转为一个具体的类型,可以使用 as? 语句:
let unknown: Any = ...
if let foo = unknown as? MyType {
...
...
}
大部分情况MyType会是目标类型的父类或者协议。但这种情况下,foo也会被转为MyType。所以foo中的MyType子类方法也会因此不可用。
其他一些有趣的类
NSObject:
NSObject在OC中是所有类的父类,但在swift中没有要求所有类必须是NSObject的子类。
NSNumber:
NSNumber是个class,所以是引用类型。这个类型用来存储数字类型:
Int, Double, Float, Bool, ...
可以如此构造及使用:
let n = NSNumber(35.5) or
let m: NSNumber = 35.5
let intified: Int = n.intValue
// also doubleValue, boolValue, ...
Date(and Calendar, DateFormatter, DateComponents):
在UI中会使用到Date,并且需要注意不同时区会用不同的时间系统。
Data:
Data是一包二进制数,在传输中经常用到。通过Data可以获知数据的格式,比如 jpg, png, json, ...
Views
我们会在MVC中使用view(V代表Views)。
一个view(UIView类型)代表一个矩形区域,用来绘制或者处理触控事件。
view是分层的(hierarchy)
一个view只能有一个父级view:
var superview: UIView?
但可以有许多子级view:
var subviews: [UIView]
并且subviews是根据view的添加顺序排序
在Xcode中,我们会手动编辑一个view,但其实我们可以用代码去添加一个view:
// 添加一个subview
func addSubview(_ view: UIView)
// 告诉superview把自己去除
func removeFromSuperview()
在顶层的view是Controller的属性:
var view: UIView
也是每个iOS应用的根view
创建一个UIView
如果是在代码中创建一个view,则使用:
init(frame: CGRect)
如果是在storyboard创建一个vieww,则使用:
init(coder: NSCoder)
如果你需要为一个view创建一个构造方法,你必须实现以上两个构造方法:
func setup() { ... }
override init(frame: CGRect) {
super.init(frame: CGRect)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
尽量避免去创建一个构造方法
创建一个构造方法的替代方法是awakeFromNib()。awakeFromNib是个函数,只有当storyboard有view时才会调用,并且每一个view都会调用一次,调用顺序不确定。
NIB是界面构造器文件的一个非常古老的名称。
坐标系统数据结构
CG: Core Graphics
CGFloat:
用来取代有关坐标系统的Double/Float数据(因此,绘制方法中一般不能用Double/Float传参)。可以通过Double/Float构造:
let cgf = CGFloat(12.34)
CGPoint:
含有两个CGFloat类型(x, y)的结构:
var point = CGPoint(x: 37.0, y: 55.2)
point.y -= 30
CGSize:
含有两个CGFloat类型(宽, 长)的结构:
var size = CGSize(100.0, height: 50.0)
size.width += 42.5
size.height += 75
CGRect:
含有一个CGPoint和一个CGSize的结构:
struct CGRect {
var origin: CGPoint
var size: CGSize
}
let rect = CGRect(origin: aCGPoint, size: aCGSize)
// 还有一些其他的构造方法
CGRect还有许多的便捷的属性和方法:
// 矩形中x的最小值
var minX: CGFloat
// 中点y值
var midY: CGFloat
// 自身是否和这个CGRect有相交
intersects(CGRect) -> Bool
// 取交集
intersect(CGRect)
// 这个CGPoint是否在自身中
contains(CGPoint) -> Bool
...
还有许多的方法在文档,需要去记忆
界面坐标系统
一个view中:
Origin是view的左上角,Units是点,而不是像素(像素是材质的)。
大部分情况下每个点有两个像素,可以通过属性:
var contentScaleFactor: CGFloat
来查询
view的边界(boundary):
var bounds: CGRect
一些UIView属性可以知道view在superview的位置:
var center: CGPoint
var frame: CGRect
(注意,这些数据都是对于superview的)
bounds和frame的(width,height)值不一定是相同的:
view是可以旋转的(或者缩放/转移),而frame是包含view的最小的标准矩形。所谓标准,是其边平行于x, y轴。
创造界面
大部分情况下,你的view在storyboard创建
Xcode的控件板中有一个通用的UIView供使用。但在这创建之后,你需要通过Identify Inspector去阐明其具体类型(通常是自己创建的一个类型)
如果是通过代码去创建了一个view,可以:
let newView = UIView(frame: meViewFrame) or
// 这个 newView.frame == CGRect.zero,CGRect.zero即四个属性皆为0
let newView = UIView()
举个例子:
// 假设view是根界面
let labelRect = CGRect(x: 20, y: 20, width:100, height: 50)
// UILabel是UIView的子类
let label = UILabel(frame:labelRect)
label.text = "Hello"
view.addSubView(label)
自定义的界面
当我们想自定义绘制方法或者特殊地去处理触控事件,就需要去自定义一个界面。
对于绘图,只需要继承UIView,然后覆盖draw(_:)方法:
override func draw(_ rect: CGRect)
你可以在rect的外部绘制,但一般不会得到系统优化。只有view的bounds才是绘图边界。
注意,千万不要去手动调用draw(_:) 如果你想让系统重绘,只需要如此来告诉系统:
setNeedsDisplay() or
setNeedsDisplay(_ rect: CGRect)
如何去实现draw方法?有两种方法:
- 可以获得一个上下文并且告诉它怎么绘制...
- 使用UIBezierPath类来创建一个绘图路径
Core Graphics构想(第一种方法)
- 你需要上下文来绘制。所谓上下文,就是绘制前的准备,一般是各种属性。全局函数UIGraphicsGetCurrentContext()提供一个上下文,你可以使用它
- 创建路径(线, 弧, ...)
- 设置绘图属性,比如colors, fonts, textures(材质), linewidths, linecaps(线端), ...
- 笔触(stroke)是绘制图形线边轮廓,填充(fill)是绘制图形内部填充
UIBezierPath(Bezier:贝塞尔)(第二种方法)
和第一种一样,但是通过一个UIBezierPath实例来获得所有的绘制要求。UIBezierPath自动在当前上下文绘制
定义一个路径
创建一个UIBezierPath:
let path = UIBezierPath()
移动,给路径加边/弧:
path.move(to: CGPoint(80, 50))
path.addLine(to: CGPoint(140, 150))
path.addLine(to: CGPoint(10, 150))
合上路径:
path.close()
设置绘图属性:
UIColor.green.setFill()
UIColor.red.setStroke()
path.linewidth = 3.0
填充,线条:
path.fill()
path.stroke()
可以用UIBezierPath画出一些普遍的图形:
// 圆角矩形
let roundRect = UIBezierPath(roundedRect: CGRect, cornerRadius: CGFloat)
// 椭圆形
let oval = UIBezierPath(ovalIn: CGRect)
添加图形边界:
// 调用方法后,将不再绘制超出填充范围外的图形
// Clip意为修剪
addClip()
访问目标点是不是在闭合路径中:
// 在调用该方法前必须调用close方法
func contains(_ point: CGPoint) -> Bool
更多的方法需要查阅文档
UIColor
我们用UIColor来设置颜色。UIColor设置了一些类属性来获得基础颜色:
let green = UIColor.green
或者我们自定一个颜色(RGB, HSB, 图案)
UIColor构造方法详见文档
UIView的背景颜色属性:
var backgroundColor: UIColor
颜色拥有透明度(Alpha):
let semitransparentYellow = UIColor.yellow.withAlphaComponent(0.5)
Alpha的定义域是[0.0, 1.0],从完全透明到完全不透明
如果你想在view中绘制透明图案,需要设置:
var isOpaque = false
因为默认情况下view是不透明的。同时,也可以设置透明度:
var alpha = CGFloat(0.5)
图层
在UIView底下有个CALayer绘图机制
CA: Core Animation
view的绘制其实是由Core Animation完成的
你可以访问你的view的图层:
var layer: CALayer
一般情况下我们不回去访问layer属性,但是CALayer能做些很特别的事:
// 通过设置拐角半径,使得背景是个圆角矩形
var cornerRadius: CGFloat
// 设置view的边界
var borderWidth: CGFloat
// 设置边界颜色,注意不是UIColor
// 因为CA机制在CG之上,但在UIKit工具包层之下
// 所以CA不能使用UIColor类型
var borderColor: CGColor?
界面透明度
如果各个view互相重叠/有透明度,那么会发生什么呢?
如果互相重叠,则按照superview的subviews列表迭代顺序从下往上显示(subviews是可以设置的,因此可以手动控制图层顺序)
你可以隐藏一个view而非删除通过设置:
var isHidden: Bool
一个隐藏的view将不会被绘制,以及接受触控事件
绘制文字
通常我们会用UILabel去显示文字。但有时候,我们会需要在view的draw方法显示文字:
// 通常我们还会为text设置属性
let text = NSAttributedString(string: "hello")
// 或者draw(in: CGRect)
text.draw(at: aCGPoint)
我们可以访问size属性来获得text所占空间:
let textSize: CGSize = text.size
通过NSRange来访问/设置NSAttributedString部分范围字符属性:
let pizzaJoint = "café pesto"
// 创建一个可变的NSAttributedString
var attrString = NSMutableAttributedString(string: pizzaJoint)
// 获得目标范围
let firstWordRange = pizzaJoint.startIndex..<pizzaJoint.indexOf(" ")!
// 用NSRange封装该范围
let nsrange = NSRange(firstWordRange, in pizzaJoint)
// 添加为nsrange范围下属性(线条颜色:橘色)
attrString.addAttribute(.strokeColor, value: UIColor.orange, range: nsrange)
字体
通常会在UIButton, UILabel等UI组件中设置字体
简单的方式去获得一个字体:
static func preferredFont(forTextStyle: UIFont.TextStyle) -> UIFont
其中,UIFont.TextStyle的静态属性有:
- .headline
- .body
- .footnote
或者更高级的方式:
let font = UIFont(name: "Helvetiva", size: 36.0)
也可以使用UIFontDescripter类型
有的时候,人们会调节iOS系统中的字体大小。但在默认情况下,简单字体大小不会随系统设置而变化。可以通过这个方式来使得字体随系统设置变化:
let metrics = UIFontMetrics(forTextStyle: .body)
let fontToUse = metrics.scaledFont(for: font)
还有一些"系统字体",这些经常出现在按钮等控件上:
static func systemFont(ofSize: CGFloat) -> UIFont
static func boldSystemFont(ofSize: CGFloat) -> UIFont
但是不要将这种字体用在用户内容里。对于普通的用户内容,请使用上面提到的preferredFont
绘制图片
有个和UILabel等价的控件:UIImageView
但是,和字体一样,我们可能需要在draw方法中去绘制图片
我们可以先创建一个UIImage对象:
// 注意这里的构造方法返回一个可选类型
let image = UIImage(named: "foo")
你需要将 foo.jpg 文件放在工程中的Assets.xcassets文件夹中,并且Xcode可能会要求你对不同分辨率的屏幕适配不同的图片
或者可以通过文件系统去创建一个UIImage:
// 通过提供照片的文件路径
let image = UIImage(contentOfFile: pathString)
// 通过提供一个存了照片的Data值
let image = UIImage(data: aData)
还可以使用全局函数UIGraphicsBeginImageContext(CGSzie)来绘制自定义图片 (详细见文档)
一旦你获得了UIImage,你就可以直接在draw方法绘制:
let image: UIImage = ...
// 固定在某点
image.draw(at point: aCGPoint)
// 缩放到一个矩形里
image.draw(in rect: aCGRect)
// 平铺图片(反复重复图像来填充矩形)
image.drawAsPattern(in rect: aCGRect)
在边界(bounds)改变时重绘
在bounds变化的时候,系统默认不会去重绘view
有时候边界改变后会使得布局糟糕。幸运的是,有个属性可以控制布局:
var contentMode: UIViewContentMode
这个也可以在Xcode中设置
UIViewContentMode
如果不想缩放,仅仅需要将view放在...:
- .left
- .right
- .top
- .bottom
- .topRight
- .topLeft
- .bottomRight
- .bottomLeft
- .center
如果想要缩放:
- .scaleToFill
- .scaleAspectFill
- .scaleAspectFit
如果想要重新绘制:
- .redraw
当bounds变化时,我们也需要考虑subviews的布局。
通常情况下,你会用Xcode的自动布局(Autolayout constraints)来处理subviews,但你也可以手动的处理布局,通过覆盖layoutSubviews方法:
override func layoutSubviews() {
super.layoutSubviews()
...
...
}
当你的view的bounds值变化时,就会自动调用这个方法