【iOS 客户端专场 学习资料三】第四届字节跳动青训营

785 阅读42分钟

第四届字节跳动青训营讲师非常用心给大家整理了课前、中、后的学习内容,同学们自我评估,选择性查漏补缺,便于大家更好的跟上讲师们的节奏,祝大家学习愉快,多多提问交流~

第七节:iOS App 入门与 UIKit 简介

iOS App

1、App的入口

\

main函数

main函数中会初始化一个UIApplication实例以及一个AppDelegate代理

\

UIApplicationMain

main函数中,关键方法为UIApplicationMain,此方法的官方解释为

Creates the application object and the application delegate and sets up the event cycle.

创建application对象,和application的代理,设置事件循环

此方法包含4个参数,对应的参数表为:


@param argc 参数个数
@param argv 参数
@param principalClassName 根据该参数初始化一个UIApplication或其子类的对象并开始接收事件(传入nil, 意味使用默认的UIApplication)
@param delegateClassName 该参数指定AppDelegate类作为委托, delegate对象主要用于监听, 类似于生命周期的回调函数
@return 返回值为int, 但是并不会返回(runloop), 会一直在内存中 直到程序终止

我们重点关注最后两个参数

  • principalClassName(主要类名):如果传空,会尝试从info.plist获取值,如果info.plist也没有,则默认为UIApplication。pricipalClass类除了管理整个App的生命周期之外什么也不做,只负责监听事件,然后将其交给delegateClass去处理
  • delegateClassName(委托类名):将在工程新建时实例化一个对象NSStringFromClass([AppDelegate class])

\

\

\

AppDelegate

AppDelegate对象实际上也是一个单例实例对象,在App推出前永远也不会被释放,AppDelegate在程序启动时就不断跟踪App的状态变化,处理包括从其他应用跳转、启动应用、后台运行等动作。

AppDelegate作为App的根对象,主要有以下功能:

  • 在启动时运行用以初始化App的代码
  • 响应App在各个生命周期状态的指定方法
  • 接受推送通知或者本地通知启动App的情况
  • 处理低内存警告
  • 处理App的状态保存和恢复
  • 响应由App处理的事件
  • 处理数据的存储

\

2、App的生命周期

App从启动到退出的过程中,iOS应用程序不断从系统接收各种事件,如:用户点击了屏幕、用户点击了Home键,并对这些事件进行响应。接收事件是UIApplication对象的工作,但是,响应事件需要由程序员编写的代码来处理。为了理解事件响应需要在哪里处理,就必须对iOS应用程序的整个生命周期和事件周期有所了解。

我们在ppt中讲述了一个App的生命周期包含了5种状态:

  • Not Running
  • Inactive
  • Active
  • Background
  • Suspended

而App的生命周期,就是根据这些状态之间进行变化而在UIApplicationDelegate中响应的回调方法

最常用的回调方法分为以下7种

  • application:willFinishLaunchingWithOptions: 在App启动时调用表示应用加载进程已经开始,常用来处理应用状态的存储和恢复
  • application:didFinishLaunchingWithOptions: 表示App将从未运行状态进入运行状态,用于对App的初始化操作
  • applicationDidBecomeActive: 当应用即将进入前台运行时调用
  • applicationWillResignActive: 当应用即将进从前台退出时调用
  • applicationDidEnterBackground: 当应用开始在后台运行的时候调用
  • applicationWillEnterForeground: 当程序从后台将要重新回到前台(但是还没变成Active状态)时候调用
  • applicationWillTerminate: 当前应用即将被终止,在终止前调用的函数。通常是用来保存数据和一些退出前的清理工作。如果应用当前处在suspended,此方法不会被调用。 该方法最长运行时限为5秒,过期应用即被kill掉并且移除内存。

\

\

UIKit

\

1、什么是UIKit

The UIKit framework provides the required infrastructure for your iOS or tvOS apps. It provides the window and view architecture for implementing your interface, the event-handling infrastructure for delivering Multi-Touch and other types of input to your app, and the main run loop needed to manage interactions among the user, the system, and your app

\

UIKit提供的类

\

2、视图(UIView)与图层(CALayer)

iOS中所有的视图都是由UIView的基类派生来的

UIView可以处理触摸事件,可以支持基于CoreGraphics绘图,可以做仿射变换,或者做滑动或者渐变动画

CALayer类被UIView所封装(iOS上,在macOS上是被NSView所封装),CALayer不处理用户的交互,他只负责管理子涂层位置,包含一些方法和属性用来做动画和变换

CALayer不清楚具体响应链,不能响应事件

\

\

UIView不能做,但CALayer可以做的:

  • 阴影、圆角、带颜色边框
  • 3D变换
  • 非矩形范围
  • 透明遮罩
  • 多级非线性动画

\

3、视觉效果

圆角

如果想创建有些圆角有些直角的图层或视图的时候,可能需要用到图层蒙版或者是CAShapeLayer

\

图层边框

CALayer的两个属性borderWidth和borderColor

默认borderWidth是0. ,borderColor是黑色

borderColor是CGColorRef类型,而不是UIColor,不是Cocoa的内置对象

边框不会把寄宿图或者是子图层的形状计算进来,如果涂层的子图层超过了边界,边框依然会沿着涂层的边界绘制出来

\

阴影

shadowOpacity,一个必须在0.0(不可见)和1.0(完全不透明)之间的浮点数

可以使用CALayer的另外三个属性改变阴影表现:shadowColor、shadowOffset、shadowRadius

shadowRadius越大,阴影越模糊,看上去深度就会更明显

\

阴影裁剪

涂层的阴影继承自内容的外形,而不是根据边界和角半径确定的

计算阴影时,CoreAnimation会将寄宿图(包括子视图)考虑在内,这就是我们在课上所举的例子,我们只为红色方块添加阴影,蓝色方块作为红色方块的子视图,也同时被添加上了阴影

\

shadowPath属性

如果预知了阴影的形状,可以通过提供一个CGPathRef给shadowPath属性,单独于涂层的形状之外指定阴影的形状

\

图层蒙版

maskToBounds可以沿着边界裁剪图形,如果想要展现自定义的框架,可以用CALayer的mask属性解决问题,这个属性本身也是一个CALayer类型

\

蒙版图层真正历代的地方在于蒙版图不局限于静态图,任何有图层构成的都可以作为mask属性,这意味着蒙版甚至可以通过代码甚至动画实时生成

\

拉伸过滤

当图片需要展示不同大小的时候,有一种叫做拉伸过滤的算法起作用了

minification(缩小图片)和magnification(放大图片)默认的过滤器都是kCAFilterLinear,这个过滤器采用双线性滤波算法,它在大多数情况下都表现良好

CALayer提供三种拉伸过滤方法:

  • kCAFilterLinear 双线性过滤
  • kCAFilterNearest 最近邻过滤
  • kCAFilterTrilinear 三线过滤

\

组透明

常用alpha控制透明

当你显示一个50%透明度的图层时,如果图层包含一个同样显示50%透明的子图层时,你所看到的视图,50%来自子视图,25%来了图层本身的颜色,另外的25%则来自背景色

如果你希望包含了整个图层树像一个整体一样呈现透明效果,可以在info.plist的UIViewGroupOpacity设置为YES来实现这个效果,CALayer的shouldRasterize实现组透明效果

shouldRasterize和UIViewGroupOpacity一起使用时,会出现性能问题

\

\

3、Core Graphics(核心图形)

Core Graphics也被称为Quartz 2D,是一个先进的二维绘图引擎,是iOS的核心图形库

iOS本身提供两套绘图框架,即UIBezierPath和Core Graphics,前者属于UIKit,其实是对Core Graphics框架关于path的进一步封装,所以使用起来更加简单

当然,Core Graphics更接近底层,所以更加强大

\

Quartz2D是使用CPU绘制,在基于View的绘图过程中,view的bound和center被改动时会触发drawRect:来重新绘制位图。这种方式需要CPU在主线程执行,比较耗时(并不明显,除非有CPU密集绘制操作)

\

支持绘制

  • 贝塞尔线条,基本几何图形
  • 文字
  • 绘制、生成图片
  • 读取、生成PDF
  • 裁剪图片
  • 自定义UI控件

API由C语言写成,不受ARC管控

\

更多系统性的介绍,可以参考

southpeak.github.io/2014/11/10/…

\

4、Core Animation(核心动画)

具体参考@刘锦泉 同学的核心动画学员手册

\

常用组件

\

普通控件

TextField、TextView输入框

它是一个文本域的编辑视图,可以在该区域上进行编辑(包括删除、剪贴、复制、修改等),它与文本框UITextField的不同之处是:当它里面的每一行内容超出时,可以自动换行,而且带有滚动条,可以滚动查看其他无法显示的内容。

参考:

\

\

\

DatePicker 日期选择器

 // 日期转为字符串
  NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
  formatter.dateFormat = @"yyyy年MM月dd日"; // HH:mm:ss 时分秒
  NSString *str = [formatter stringFromDate:date];

UIScrollView 滚动视图

功能: 滚动视图,参考: www.jianshu.com/p/bcaf5cdfa…my.oschina.net/u/2340880/b…

概述: UIScrollView是用来在屏幕上显示那些在有限区域内放不下的内容。例如,在手机屏幕上显示内容丰富的网页或者很大的图片。在这种情况下,需要用户对屏幕内容进行拖动或缩放来查看屏幕或窗口区域外的内容。

\

UITableView表格视图

概念: 把内容按行分组显示的控件,适合于内容不断加载的场景,比如今日头条的Feed流。

\

关于单元格cell

UITableView的每行数据都是一个UITableViewCell:

  1. 每个Cell使用IndexPath来表示位置
  1. IndexPath又分为section和 row ,每个节section为一组,其中可以包含多个行row。

UIKIt里给NSIndexPath写了个 UITableView的分类。深度为两个节点 indexPath with two nodes,就是section和row。比如path:9-0就是 第9节第0行。

\

系统Cell的 UI 和布局

样式

右侧的访问器

UITableViewCell右侧可以显示不同的图标,在iOS中称之为访问器,点击可以触发不同的事件,例如设置功能:

要设置这些图标只需要设置UITableViewCell的accesoryType属性:

typedef NS_ENUM(NSInteger, UITableViewCellAccessoryType) {     
    UITableViewCellAccessoryNone,                   // 不显示任何图标     
    UITableViewCellAccessoryDisclosureIndicator,    // 跳转指示图标    
    UITableViewCellAccessoryDetailDisclosureButton, // 内容详情图标和跳转指示图标    
    UITableViewCellAccessoryCheckmark,              // 勾选图标     
    UITableViewCellAccessoryDetailButton NS_ENUM_AVAILABLE_IOS(7_0) // 内容详情图标 
}; 

\

\

\

用户界面显示的行数有限,比如现在显示了7行,然后向下滚动时,上面的 行row就会被“销毁”并放到回收池pool,之后滚动回来可以重用。

\

UICollectionView 集合视图

是可以自定义样式的tableview,最常用的是FlowLayout流式布局方式

4个组成部分:

  • Cell : 单元格
  • section:节
  • 补充视图:节的header和footer
  • 装饰视图:集合视图的背景视图。

集合视图单元格(自定义继承自UICollectionViewCell)

单元格本质上是一个视图,不是vc没有viewDidLoad方法,得在视图的构造函数中实例化属性对象

可重用单元格标识identifier:通过可重用单元格标识符可以从集合视图中获取可重用的单元格。

委托协议和数据源协议

\

\

推荐阅读

iOS生命周期

iOS App生命周期理解 - 掘金

developer.apple.com/documentati…

iOS核心动画高级技巧Gitbook

第八节:Swift 入门与实践

上一节课,我们学习了UIKit, 了解了如何去构建一个UIKit app, 这节课,我们会来学习Swift的一些基础语法和使用,并学习Swift UI的使用

思维导图

image.png

Swift与OC的区别

  • 速度更快
  • 安全性更高

基础语法

  • 变量、不变量、Int、String、double

• 变量定义

可变变量 用var,不可变变量用 let

// 可变变量
var name: String = "Michael"
// 不可变变量
let age: Int = 20    
age = 18   // Cannot assign to value: 'age' is a 'let' constant
  • 类型推断

Swift可以自动推断类型,如上面的name,age,赋值的时候可以自动推断出来是String,Int, let a

因此上面的类型声明可以省略

var name = "Michael"
let age = 20
  • 类型集合

    • 数组

      • 数组以有序的方式来储存相同类型的值。相同类型的值可以在数组的不同地方多次出现。
    • 字典

      • 无序的互相关联的同一类型的键和同一类型的值的集合。每一个值都与唯一的相关联
    • 合集

      • 合集将同一类型且不重复的值无序地储存在一个集合当中
    • // 数组
      var names: [String] = ["Tom", "Jony"]
      // 字典
      var classes: [String: [String]] = ["class 1": ["Tom", "Jony"],
                   "class 2": ["Peter", "Egg"]
                  ]
      // 合集
      var id: Set<String> = ["1", "2", "3", "4"]
      print(id) // 1, 2, 3
      
  • 可选值:nil

可选类型 用? 表示( 最好不要用!强制解包,容易crash)

可以用is as转换类型

let name: String?
name = "Tom"
if let name = name as? String {
    print(name) // Tom, String类型
}
  • 控制流:for,while
for xx in {

}

while() {
}
  • 驼峰式命名法

\

let ruleStatus: String = "passed"

学习完基础知识之后,我们知道了怎么构造基本类型,接下来我们学习自定义和调用一个函数

函数

定义和调用函数

定义函数

下面是三种函数类型的greet

第一个是传参的greet,会传入一个person的String类型,然后返回一个对该person打招呼的字符串 Hello,{person} !

第二个是传参,前面加了一个"_"下划线

第二个不传参,但返回一个String数组

// 传参
func greet(person: String) -> String {
    let greeting = "Hello, " + person + "!"
    return greeting
}

func greet2(_ person: String) -> String {
    let greeting = "Hello, " + person + "!"
    return greeting
}

Swift使用func关键字作为前缀,用->来明确函数返回的类型, 当不想输入参数名时,可以前面加 “_ ”表示省略该字符

\

调用函数

Swift中可以直接输入函数名,和所需参数,来调用一个函数,若参数前面带了下划线,则参数名可以省略

print(greet(person: "Tom"))
// Hello, Tom!
print(greet("Tom"))
// Hello, Tom!

Inout

在Swift中,用inout参数表示参数传递是引用传递,比如下面的例子

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var a = 8.0
var b = 7.0
swapTowDoubles(&a, &b) // 加上&表示引用
print(a) // 7.0
print(b) // 8.0


func swapTwoDoubles(_ a: Double, _ b: Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

swapTowDoubles(a, b)
print(a) // 8.0
print(b) // 7.0

在没有用inout修饰参数的函数调用时,是复制了一份传参供函数内部使用,函数内部对参数的处理不会影响到原来的参数

类的基本定义与使用

类的定义与集成

类是一种通用而又灵活的结构。你可以使用定义常量、变量和函数的语法,定义属性、添加方法。

一个类可以继承另一个类的方法,属性和其它特性。当一个类继承其它类时,继承类叫子类,被继承类叫超类(或父类)。不继承于其它类的类,称之为基类。类的继承语法结构如下

 class SomeClass {
     var name: String
     // code
    init(name: String) {
        self.name = name
    }
 }
 // 继承 ":"后面跟超类
class SomeClassInherited: SomeClass {
     // code
}

类的实例化

类能使用初始化器语法来生成新的实例。初始化器语法最简单的是在类或结构体名字后面接一个空的圆括号。新实例属性的初始化值可以通过属性名称传递到成员初始化器中。若类中含有未被初始化的变量属性,在初始化类时,会要求实现初始化语法,对变量进行赋值

class SomeClass {
     var name: String
     // 要求实现初始化语法
     init(name: String) {
        self.name = name
    }
}

// init
let someClass = SomeClass(name: "Anna")

class SomeClass2 {
    // 变量已经赋值,无需初始化语法
     var name: String = ""
}

// init
let someClass2 = SomeClass2()

类的访问

点语法来访问一个实例的属性。在点语法中,你只需在实例名后面书写属性名,用( .)来分开,无需空格, 也可以用点语法赋值一个值到类的变量属性中

print(someClass.name)    // Anna
someClass.name = "Tom"

类的扩展

和其他语言一样,类也可以用extension对类进行扩展

extension SomeClass {
    // new functionality to add to SomeType goes here }

扩展为现有的类、结构体、枚举类型、或协议添加了新功能。这也包括了为无访问权限的源代码扩展类型的能力(即所谓的逆向建模

Swift 中的扩展可以:

  • 添加计算实例属性和计算类型属性;
extension Double {
    var km: Double { return self * 1_000.0 }
    var m: Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.28084 }
}
  • 定义实例方法和类型方法;
extension Int {
    func repetitions(task: () -> Void) {
        for _ in 0..<self {
            task()
        }
    }
}
  • 提供新初始化器;
extension Rect {
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

类是引用类型

引用类型被赋值到一个常量,变量或者本身被传递到一个函数的时候它是不会被拷贝的。相对于拷贝,这里使用的是同一个对现存实例的引用。形如以下代码,a与b指向的是同一块内存地址

var someClass = SomeClass(name: "Anna")
var someClassCopy = someClass
someClassCopy.name = "Tom"
print(someClass.name)
// "Tom"

\

例子:定义随机数类

这里将lastRandom,m,a,c作为成员变量,func random作为成员函数。其中lastRandom要为可变型,这样才能保证random的值一直在变化

class RandomSelect {     var lastRandom = 42.0     let m = 139968.0     let a = 3877.0     let c = 29573.0     func random() -> Double {     //lastRandom*a+c再对m取余         lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))     //取余之后再除以m必定是一个0-1的数字         return lastRandom / m     } }  // 调用 let generator = RandomSelect() print(generator.random())

这只是一种伪随机数生成算法,类似的算法还有很多,我们可以指定一种规则,只要类遵循了该规则,就表示它实现了类似的伪随机数算法。这就是协议

协议

协议的基本定义与使用

协议为方法、属性、以及其他特定的任务需求或功能定义蓝图。协议可被类、结构体、或枚举类型采纳以提供所需功能的具体实现。满足了协议中需求的任意类型都叫做遵循了该协议。

除了指定遵循类型必须实现的要求外,你可以扩展一个协议以实现其中的一些需求或实现一个符合类型的可以利用的附加功能

协议的语法结构

protocol SomeProtocal {
    //protocal definition goes here
}

协议可以要求所有遵循该协议的类型提供特定名字和类型的实例属性或类型属性。协议并不会具体说明属性是储存型属性还是计算型属性——它只具体要求属性有特定的名称和类型。协议同时要求一个属性必须明确是可读的或可读的和可写的。

若协议要求一个属性为可读和可写的,那么该属性要求不能用常量存储属性或只读计算属性来满足。若协议只要求属性为可读的,那么任何种类的属性都能满足这个要求,而且如果你的代码需要的话,该属性也可以是可写的。

属性要求定义为变量属性,在名称前面使用 var 关键字。可读写的属性使用 { get set } 来写在声明后面来明确,使用 { get } 来明确可读的属性。

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

协议的使用

协议可以在很多其他类型可以使用的地方使用,包括:

  • 在函数、方法或者初始化器里作为形式参数类型或者返回类型;
  • 作为常量、变量或者属性的类型;
  • 作为数组、字典或者其他存储器的元素的类型。
protocol SomeProtocol { 
    //protocol definition goes here 
}

func someFunction (protocol: SomeProtocol) {}

var someProtocol: SomeProtocol

var someProtocols: [SomeProtocol]

我们可以创建一个伪随机数的算法协议RandomNumberGenerator, 协议要求所有采用该协议的类型都必须有一个实例方法 random ,而且要返回一个 Double 的值,只要遵循该协议的类,都要生成一个伪随机数。具体定义如下

protocol RandomNumberGenerator {
    func random() -> Double
}

扩展extension

如果一个类型已经遵循了协议的所有需求,但是还没有声明它采纳了这个协议,你可以让通过一个空的扩展来让它采纳这个协议。例如LinearCongruentialGenerator,除了上面那种形式,还可以直接用extension表示它已经遵循了该协议

extension RandomSelectStudent: RandomNumberGenerator { } 

\

上文中提到的RandomSelectStudent类就是一种伪随机数算法,遵循RandomNumberGenerator类,可以写成如下形式

// 1
class RandomSelectStudentRandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
    //lastRandom*a+c再对m取余
        lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
    //取余之后再除以m必定是一个0-1的数字
        return lastRandom / m
    }
}

// 2
class RandomSelectStudent {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
    //lastRandom*a+c再对m取余
        lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
    //取余之后再除以m必定是一个0-1的数字
        return lastRandom / m
    }
}

extension RandomNumberGeneratorRandomNumberGenerator { }

协议的其他变型

  • mutating:表示该方法可以改变所属的实例
  • required :实现指定初始化器或便捷初始化器来使遵循该协议的类满足协议的初始化器要求。在这两种情况下,你都必须使用 required 关键字修饰初始化器的实现,
  • required & override:如果一个子类重写了父类指定的初始化器,并且遵循协议实现了初始化器要求,那么就要为这个初始化器的实现添加 required 和 override 两个修饰符,但如果子类带final那就不用再用required修饰来。
  • 协议的多个使用:直接用逗号分隔或& 这里SomeSuperClass ,SomeProtocal是两个协议,Named & Aged也是两个协议
class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass    required override init() {
        // initializer implementation goes here    }
}

func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, (celebrator.name), you're (celebrator.age)!")
}

到这里为止,我们遵循了RandomNumberGenerator协议,实现了RandomSelectStudent类和其中的random方法

那假如有一天,老师突然说,我想给学生们都安排一个学号,抽取学生的时候,能够同时返回学号和姓名。那该怎么办呢?要重写一个关于randomSelectStudent的函数吗?如果以后要新增身高、体重呢?要重写另外两个同名的函数吗?其实不用,我们可以通过泛型,去指定randomSelectStudent返回一个指定的类型。

泛型

我们先来看两个例子

swapTwoInts(::) 函数把 b 原本的值给 a ,把 a 原本的值给 b 。你可以调用这个函数来交换两个 Int 变量的值。swapTowStrings() 交换两个String值。 这里的inout指的是值传递,若不指定该值,则函数执行完a,b 仍是原来的值

func swapTwoInts(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}
  • 泛型是为所存储或使用的一个或多个类型具有占位符(类型形参)的类、结构、接口和方法。 泛型集合类可以将类型形参用作其存储的对象类型的占位符;类型形参呈现为其字段的类型和其方法的参数类型。 泛型方法可将其类型形参用作其返回值的类型或用作其形参之一的类型。

泛型的使用

// 泛型版本 
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

泛型版本的函数用了一个占位符类型名(这里叫做 T ),而不是一个实际的类型名(比如 Int 、 String 或 Double )。占位符类型名没有声明 T 必须是什么样的,但是它确实说了 a 和 b 必须都是同一个类型 T ,或者说都是 T 所表示的类型。替代 T 实际使用的类型将在每次调用 swapTwoValues(::) 函数时决定。

其他的区别是泛型函数名( swapTwoValues(::) )后面有包在尖括号( )里的占位符类型名( T )。尖括号告诉Swift, T 是一个 swapTwoValues(::) 函数定义里的占位符类型名。因为 T 是一个占位符,Swift 不会查找真的叫 T 的类型。

泛型类型

除了泛型函数,Swift允许你定义自己的泛型类型。它们是可以用于任意类型的自定义类、结构体、枚举

我们把学生数组也收纳到RandomSelectStudent里,此时students是一个String类型的数组,这是代码没有用泛型的样子

class RandomSelectStudent {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    // students是一个String数组
    var students: [String] = ["王1", "王2", "王3","王4", "王5", "王6","王7","王8", "王9", "王10"]
    func random() -> Double {
        //lastRandom*a+c再对m取余
        lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
        //取余之后再除以m必定是一个0-1的数字
        return lastRandom / m
    }
    
    func randomSelectStudent() -> String {
        let count = students.count
        // 类型转换
        let index = Int(random() * Double(count)) + 1
        return students[index]
    }
}

对类使用泛型,占位符写在类名后的<>,泛型类型一般用Element表示,用一个叫做 Element 的类型形式参数代替了实际的 String 类型。

class RandomSelectStudent<Element> {     var lastRandom = 42.0     let m = 139968.0     let a = 3877.0     let c = 29573.0     // students是一个Element数组     var students: [Element] = []     func random() -> Double {     //lastRandom*a+c再对m取余         lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))     //取余之后再除以m必定是一个0-1的数字         return lastRandom / m     }     
    // 返回一个Element类型变量     func randomSelectStudent() -> Element {         let count = students.count         // 类型转换         let index = Int(random() * Double(count)) + 1         return students[index]     } }

// 调用
let generator = RandomSelectStudent<String>()

\

闭包

闭包是可以在你的代码中被传递和引用的功能性独立代码块。Swift 中的闭包和 C 以及 Objective-C 中的 blocks 很像,还有其他语言中的匿名函数也类似。

但是Swift相对于其他语法,Swift 的闭包表达式拥有简洁的风格,鼓励在常见场景中实现简洁,无累赘的语法。常见的优化包括:

  • 利用上下文推断形式参数和返回值的类型;
  • 单表达式的闭包可以隐式返回;
  • 简写实际参数名;
  • 尾随闭包语法。

Swfit的闭包格式为

{ (parameters) -> (return type) in
    statements
}

下面这个例子展示一个sorted闭包表达版本,这也是我们本轮重点要优化的函数sorted(by:)

let studentsSorted = students.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

sorted(by:) 会根据你提供的排序闭包将已知类型的数组的值进行排序。一旦它排序完成, sorted(by:) 方法会返回与原数组类型大小完全相同的一个新数组,该数组的元素是已排序好的。原始数组不会被 sorted(by:) 方法修改。下面s1,s2 代表先后两个相邻的String类型元素,然后->后面跟着这个闭包的返回值,是一个Bool型,后面跟着表达式,表示当s1 > s2的时候,就返回true。

利用上下文推断形式参数和返回值的类型

由于排序闭包为实际参数来传递给方法,Swift 就能推断它的形式参数类型和返回类型。 sorted(by:) 方法是在字符串数组上调用的,所以它的形式参数必须是一个 (String, String) -> Bool ******类型的函数。这意味着 (String, String)和 Bool 类型不需要写成闭包表达式定义中的一部分。因为所有的类型都能被推断,返回箭头 ( ->) 和围绕在形式参数名周围的括号也能被省略。

let studentsSorted = students.sorted(by: { (s1, s2) -> Bool in
    return s1 > s2
})

单表达式闭包

单表达式闭包能够通过从它们的声明中删掉 return 关键字来隐式返回它们单个表达式的结果

let studentsSorted = students.sorted(by: { s1, s2 in s1 > s2 })

简写实际参数名

Swift 自动对行内闭包提供简写实际参数名,你也可以通过 0,0 , 1 , $2 等名字来引用闭包的实际参数值。

如果你在闭包表达式中使用这些简写实际参数名,那么你可以在闭包的实际参数列表中忽略对其的定义,并且简写实际参数名的数字和类型将会从期望的函数类型中推断出来。 in 关键字也能被省略,因为闭包表达式完全由它的函数体组成:

let studentsSorted = students.sorted(by: { $0 > $1 })

Swift 的 String 类型定义了关于大于号( >)的特定字符串实现,让其作为一个有两个 String 类型形式参数的函数并返回一个 Bool 类型的值。这正好与 sorted(by:) 方法的第二个形式参数需要的函数相匹配。因此,你能简单地传递一个大于号,并且 Swift 将推断你想使用大于号特殊字符串函数实现:

let studentsSorted = students.sorted(by: >)

尾随闭包语法

如果你需要将一个很长的闭包表达式作为函数最后一个实际参数传递给函数且闭包表达式很长,使用尾随闭包将增强函数的可读性。尾随闭包是一个被书写在函数形式参数的括号外面(后面)的闭包表达式,但它仍然是这个函数的实际参数。当你使用尾随闭包表达式时,不需要把第一个尾随闭包写对应的实际参数标签。函数调用可包含多个尾随闭包,但这里我们暂时只学习单一尾随闭包的写法:

下面这个例子,是一个函数someFunctionThatTakesAClosure,然后他参数为一个closure闭包,返回Void值,然后下面是正常闭包和尾随闭包的写法

func someFunctionThatTakesAClosure(_ closure:() -> Void) {
   //function body goes here
}

// 正常闭包写法
someFunctionThatTakesAClosure({
    // closure body
})

// 尾随闭包
someFunctionThatTakesAClosure() {
    // closure body
}

这一小节,我们学习了如何使用闭包,去优化一个简单的sorted函数。并且学习了尾随闭包的使用。

SwiftUI

SwiftUI 以一种创新且极度简单的方式,透过Swift 的力量,让使用者建立横跨所有Apple 平台的使用者界面。只要一套工具与API,即能建立适用所有Apple 装置的使用者界面。

- Apple ( developer.apple.com/xcode/swift… )

SwiftUI的优点

  • 实时加载界面
  • 先进的布局设计
  • 能与UIKit兼容

如何生成一个SwiftUI

如图是SwiftUI项目创建成功后的视图,其中LandmarksAppApp是驱动程序,ContentView是布局视图

其中有两个结构体,第一个ContentView符合View协议并描述视图的内容和布局,第二个遵循PreviewProvider,提供该视图的预览。

而这里展示了一个Hello world!的例子,其中Text.padding()的意思是,向此视图的每个边缘添加不同的填充量。(添加空格)

\

栈布局

我们一般一个页面上有很多个元素,比如,按钮,输入框,文字标题,正文等,但我们只有一个页面,那么如何去管理页面上的元素,让他布局在合适的位置呢?这里就要用到栈,栈有三种VSTack,ZSTack,HSTack,分别是垂直布局,覆盖布局和水平布局

// Hello, world!
// Hello, world!
VStack {
    Text("Hello, world!")
    .padding()
    Text("Hello, world!")
    .padding()
}

// Hello, world!    Hello, world!
HStack {
    Text("Hello, world!")
    .padding()
    Text("Hello, world!")
    .padding()
}

ZStack {
    Text("Hello, world!")
    .padding()
    Spacer()
    Text("Hello, world!")
    .padding()
    .offset(x: 10, y: 20)
}

\

\

你可以通过多个栈嵌套(Embed)去实现将元素通过你想要的方式去堆砌排列,如下面的代码,VSTack里面有一个Hello world!的文本框,有一个HSTack的堆栈,他们两个将垂直布局,而HSTack中又包含了两个Text文本框,它们两个将水平布局

VStack {
    Text("Hello, world!")
    .padding()
    HStack {
        Text("Hello, world!")
        .padding()
        Text("Hello, world!")
        .padding()
    }
}

这小节我们学习了如何创建一个SwiftUI工程,然后了解了堆栈的使用方式,以及堆栈嵌套

当然,SwiftUI除了文本框之外,还有很多的控件,供开发者们去开发,但因为时间运用,我们就不再赘述,大家有兴趣的可以去看一下苹果官方的SwiftUI教程,我们今天的讲课就到这里结束了。接下来是答疑环节,大家可以提问

developer.apple.com/tutorials/s…

第九节:iOS 交互、手势与动画编程

课程大纲

image.png

手势识别器

UIGestureRecognizer

iOS 系统提供了一些常用的手势(UIGestureRecognizer 的子类),开发者可以直接使用他们进行手势操作。

  • UIPanGestureRecognizer(拖动)
  • UIPinchGestureRecognizer(捏合)
  • UIRotationGestureRecognizer(旋转)
  • UITapGestureRecognizer(点按)
  • UILongPressGestureRecognizer(长按)
  • UISwipeGestureRecognizer(轻扫)

另外,可以通过继承 UIGestureRecognizer 类,实现自定义手势(手势识别器类)。

自定义手势时,需要 #import <UIKit/UIGestureRecognizerSubclass.h>,一般需实现如下方法:

- (void)reset;
 
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

UIGestureRecognizer继承图:

手势状态

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    UIGestureRecognizerStatePossible,   // 尚未识别是何种手势操作(但可能已经触发了触摸事件),默认状态
    UIGestureRecognizerStateBegan,      // 手势已经开始,此时已经被识别,但是这个过程中可能发生变化,手势操作尚未完成
    UIGestureRecognizerStateChanged,    // 手势状态发生转变
    UIGestureRecognizerStateEnded,      // 手势识别操作完成(此时已经松开手指)
    UIGestureRecognizerStateCancelled,  // 手势被取消,恢复到默认状态
    
    UIGestureRecognizerStateFailed,     // 手势识别失败,恢复到默认状态
    
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // 手势识别完成,同UIGestureRecognizerStateEnded
};

使用手势的步骤

  • 使用initWithTarget:action:初始化方法来创建一个手势识别器。initWithTarget:action:是所有手势识别器的基类初始化方法。
  • 设置手势识别器对象实例的相关属性(delegate等)
  • 使用addGestureRecognizer给UIView添加手势,addGestureRecognizer是所有UIView的基类方法。

事件响应机制

iOS事件类型

  1. 触摸事件
  1. 加速计事件
  1. 远程操控事件

触摸事件UITouch类

UITouch父类是NSObject, 存放在NSSet中,无序且不能重复,通过anyObject来访问某个元素。通过forin循环来遍历NSSet中的每一个元素,它的数量表示了这次事件是几根手指的操作,目前iOS设备支持的多点操作手指数最多是5。

  • began:当用户用手指触摸屏幕时,会创建一个与手指相关联的UITouch集合(一根手指对应一个UITouch对象),其中,touch对象 保存着跟本次手指触摸相关的信息,比如触摸的位置、时间、阶段。
  • moved:手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。
  • ended:当手指离开屏幕时,系统会销毁相应的UITouch对象。

\

1、属性:(demo中自己试试)

1)触摸产生时所处的窗口, 即[[UIApplication sharedApplication] keyWindow];和view.window

@property(nonatomic,readonly,retain) UIWindow *window;

2)触摸产生时所处的视图

@property(nonatomic,readonly,retain) UIView *view;

3)快速点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击

@property(nonatomic,readonly) NSUInteger tapCount;

4)触摸事件产生或变化时的时间,单位是秒

@property(nonatomic,readonly) NSTimeInterval timestamp;

5)获取当前触摸事件所处的状态phase

  • @property(nonatomic,readonly) UITouchPhase phase;
    •   触摸事件在屏幕上有一个周期,即触摸开始、触摸点移动、触摸结束,还有中途取消。而通过phase可以查看当前触摸事件在一个周期中所处的状态
@property(nonatomic,readonly) UITouchPhase  phase;
UITouchPhase 枚举:
UITouchPhaseBegan 开始触摸
UITouchPhaseMoved 移动
UITouchPhaseStationary 停留
UITouchPhaseEnded 触摸结束
UITouchPhaseCancelled 触摸中断

\

6)触摸类型

@property(nonatomic,readonly) UITouchType type;

@property(nonatomic,readonly) UITouchType type;
UITouchType 枚举:
UITouchTypeDirect 垂直的触摸类型
UITouchTypeIndirect 非垂直的触摸类型
UITouchTypeStylus 水平的触摸类型

7)获取手指与屏幕的接触半径

@property(nonatomic,readonly) CGFloat majorRadius;

8)获取触摸手势

@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers;

9)获取触摸压力值,一般压力值为1.0

@property(nonatomic,readonly) CGFloat force;

10)获取最大触摸压力值

@property(nonatomic,readonly) CGFloat maximumPossibleForce;

\

2、方法

1)返回在指定view所处坐标系,当前触摸点的位置。注: 这里返回的位置是针对view的坐标系(以view的左上角为圆点(0,0)),调用时传入view参数为nil的话,返回的是触摸点在UIWindow的位置。

- (CGPoint)locationInView:(nullable UIView *)view;

\

2)返回在指定view所处坐标系中,前一个触摸点的位置

- (CGPoint)previousLocationInView:(nullable UIView *)view;

\

3)在指定view所处坐标系的精确坐标

- (CGPoint)preciseLocationInView:(nullable UIView *)view;

\

4)当前触摸对象的前置坐标

- (CGPoint)precisePreviousLocationInView:(nullable UIView *)view;

触摸事件UIEvent类

一个完整的触摸过程对应一个UIEvent的实例

UIEvent:成为事件对象,记录产生的时刻和类型,事件对象中包含: 当前多点触摸序列相对应的所有触摸对象,还可以提供与特定视图或窗口相关联的触摸对象。

1、属性

1)获取事件类型

@property(nonatomic,readonly) UIEventType type;
UIEventType枚举:
UIEventTypeTouches 触摸事件
UIEventTypeMotion 加速事件
UIEventTypeRemoteControl 远程控制事件
UIEventTypePresses 按压事件

2)获取远程控制事件

@property(nonatomic,readonly) UIEventSubtype  subtype;
UIEventSubtype 枚举:
// 不包含任何子事件类型
UIEventSubtypeNone                              = 0,
// 摇晃事件(从iOS3.0开始支持此事件)
UIEventSubtypeMotionShake                       = 1,
//远程控制子事件类型(从iOS4.0开始支持远程控制事件)
//播放事件【操作:停止状态下,按耳机线控中间按钮一下】
UIEventSubtypeRemoteControlPlay                 = 100,
//暂停事件
UIEventSubtypeRemoteControlPause                = 101,
//停止事件
UIEventSubtypeRemoteControlStop                 = 102,
//播放或暂停切换【操作:播放或暂停状态下,按耳机线控中间按钮一下】
UIEventSubtypeRemoteControlTogglePlayPause      = 103,
//下一曲【操作:按耳机线控中间按钮两下】
UIEventSubtypeRemoteControlNextTrack            = 104,
//上一曲【操作:按耳机线控中间按钮三下】
UIEventSubtypeRemoteControlPreviousTrack        = 105,
//快退开始【操作:按耳机线控中间按钮三下不要松开】
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
//快退停止【操作:按耳机线控中间按钮三下到了快退的位置松开】
UIEventSubtypeRemoteControlEndSeekingBackward   = 107,
//快进开始【操作:按耳机线控中间按钮两下不要松开】
UIEventSubtypeRemoteControlBeginSeekingForward  = 108,
//快进停止【操作:按耳机线控中间按钮两下到了快进的位置松开】
UIEventSubtypeRemoteControlEndSeekingForward    = 109

\

3)获取触摸产生或变化的时间戳

@property(nonatomic,readonly) NSTimeInterval timestamp;

\

2、方法

1)获取触摸点的集合,可以判断多点触摸事件

(nullable NSSet <UITouch *> *)allTouches;

2)获取指定窗口里的触摸点

(nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;

3)获取指定视图里的触摸点

(nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;

4)获取手势对象

(nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture;

UIResponder类

处理事件的类

每个响应者都是一个UIResponder对象,即所有派生自UIResponder的对象,本身都具备响应事件的能力。因此以下类的实例都是响应者:

  • UIView
  • UIViewController
  • UIApplication
  • AppDelegate

响应者之所以能响应事件,因为其提供了4个处理触摸事件的方法:

//手指触碰屏幕,触摸开始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移动
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指离开屏幕,触摸结束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸结束前,某个系统事件中断了触摸,例如电话呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

事件传递和响应链

  1. 发生触摸事件后,系统会将事件加入到 UIApplication管理的事件队列中;UIApplication从事件队列中取出最前面的事件,并将其分发下去处理,通常先发送事件给应用程序的主窗口keyWindowkeyWindow 会在视图层次结构中, 找到一个最适合的视图Firstresponder来处理触摸事件(命中测试)。
  1. app从操作系统接受点击信息,并转化为 UITouch对象(包含View,window,point点击位置),封装为UIEvent作为事件的消息载体在responder chain上传递,第一响应者处理完后交给Next responder,响应时从最上的view依次往下抛到最底下的UIWindow ---> UIApplication(响应链传递)。

第一步 ,通过Hit Test确定用户触摸的是哪一个UIView。这个步骤通过-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法(UIView的基类方法)来完成。hitTest:方法内部的参考实现如下:

第二步 ,hitTest找到被触摸的第一响应值View之后:

  • 如果它能够响应用户事件,调用touches响应函数 做事件处理。
  • 默认情况下,这些touches方法会将事件顺着响应者链向上传递,将事件交给上一个响应者UIResponder对象进行处理。

触摸屏幕后事件传递的步骤可分为:

  1. 事件派发:HitTest && PointInside,方向与响应链相反:通过「命中测试hittest和posInside」来找到「第一响应者」
  1. 由「第一响应者」来确定「响应链」
  1. 将事件沿「响应链」传递
  1. 事件被某个响应者接收,或没有响应者接收从而被丢弃

tip 这些步骤都是建立在不使用 UIGestureRecognizer 的基础上的。

触摸事件responder和手势的关系:

当触摸发生或者触摸的状态发生变化时,Window都会传递事件寻求响应。

  • Window先将绑定了触摸对象的事件传递给触摸对象上绑定的手势识别器,再发送给触摸对象对应的hit-tested view。
  • 手势识别器识别手势期间,若触摸对象的触摸状态发生变化,事件都是先发送给手势识别器再发送给hit-test view。
  • 手势识别器若成功识别了手势,则通知Application取消hit-tested view对于事件的响应,并停止向hit-tested view发送事件;
  • 若手势识别器未能识别手势,而此时触摸并未结束,则停止向手势识别器发送事件,仅向hit-test view发送事件。
  • 若手势识别器未能识别手势,且此时触摸已经结束,则向hit-tested view发送end状态的touch事件以停止对事件的响应。

手势识别器影响与UIResponder对于事件响应的联系的三个属性:

@property(nonatomic) BOOL cancelsTouchesInView;
@property(nonatomic) BOOL delaysTouchesBegan;
@property(nonatomic) BOOL delaysTouchesEnded;

cancelsTouchesInView 默认为YES。表示当手势识别器成功识别了手势之后,会通知Application取消响应链对事件的响应,并不再传递事件给hit-test view。若设置成NO,表示手势识别成功后不取消响应链对事件的响应,事件依旧会传递给hit-test view。

UIView动画

Block动画

UIView提供了Block动画接口,调用方式简洁,日常开发中也使用Block动画,最简洁的Block动画:包含时间和动画:

[UIView animateWithDuration:(NSTimeInterval)  //动画持续时间
              animations:^{
              //执行的动画
 }];

带有动画提交回调的Block动画

 [UIView animateWithDuration:(NSTimeInterval)  //动画持续时间
              animations:^{
            //执行的动画
 }                completion:^(BOOL finished) {
            //动画执行提交后的操作
 }];

可以设置延时时间和过渡效果的Block动画

[UIView animateWithDuration:(NSTimeInterval) //动画持续时间
                   delay:(NSTimeInterval) //动画延迟执行的时间
                 options:(UIViewAnimationOptions) //动画的过渡效果
              animations:^{
               //执行的动画
 }                completion:^(BOOL finished) {
               //动画执行提交后的操作
 }];

UIViewAnimationOptions的枚举值如下,可组合使用: optiona | optionb

UIViewAnimationOptions的枚举值:
UIViewAnimationOptionLayoutSubviews            //进行动画时布局子控件
 UIViewAnimationOptionAllowUserInteraction      //进行动画时允许用户交互
 UIViewAnimationOptionBeginFromCurrentState     //从当前状态开始动画
 UIViewAnimationOptionRepeat                    //无限重复执行动画
 UIViewAnimationOptionAutoreverse               //执行动画回路
 UIViewAnimationOptionOverrideInheritedDuration //忽略嵌套动画的执行时间设置
 UIViewAnimationOptionOverrideInheritedCurve    //忽略嵌套动画的曲线设置
 UIViewAnimationOptionAllowAnimatedContent      //转场:进行动画时重绘视图
 UIViewAnimationOptionShowHideTransitionViews   //转场:移除(添加和移除图层的)动画效果
 UIViewAnimationOptionOverrideInheritedOptions  //不继承父动画设置

 UIViewAnimationOptionCurveEaseInOut            //时间曲线,慢进慢出(默认值)
 UIViewAnimationOptionCurveEaseIn               //时间曲线,慢进
 UIViewAnimationOptionCurveEaseOut              //时间曲线,慢出
 UIViewAnimationOptionCurveLinear               //时间曲线,匀速

 UIViewAnimationOptionTransitionNone            //转场,不使用动画
 UIViewAnimationOptionTransitionFlipFromLeft    //转场,从左向右旋转翻页
 UIViewAnimationOptionTransitionFlipFromRight   //转场,从右向左旋转翻页
 UIViewAnimationOptionTransitionCurlUp          //转场,下往上卷曲翻页
 UIViewAnimationOptionTransitionCurlDown        //转场,从上往下卷曲翻页
 UIViewAnimationOptionTransitionCrossDissolve   //转场,交叉消失和出现
 UIViewAnimationOptionTransitionFlipFromTop     //转场,从上向下旋转翻页
 UIViewAnimationOptionTransitionFlipFromBottom  //转场,从下向上旋转翻页

弹簧动画

Spring动画ios7.0以后新增了Spring动画(IOS系统动画大部分采用Spring Animation, 适用所有可被添加动画效果的属性)

性)

self.redView.alpha = 0.0;
[UIView animateWithDuration:(NSTimeInterval)//动画持续时间
                   delay:(NSTimeInterval)//动画延迟执行的时间
  usingSpringWithDamping:(CGFloat)//震动效果,范围0~1,数值越小震动效果越明显
   initialSpringVelocity:(CGFloat)//初始速度,数值越大初始速度越快
                 options:(UIViewAnimationOptions)//动画的过渡效果
              animations:^{
                 //执行的动画
                self.redView.alpha = 1.0;
        self.redView.frame = CGRectMake(200, 350, 140, 140);
 }
                  completion:^(BOOL finished) {
                 //动画执行提交后的操作
 }];

\

关键帧动画

IOS7.0后新增了关键帧动画,支持属性关键帧,不支持路径关键帧
 [UIView animateKeyframesWithDuration:(NSTimeInterval)//动画持续时间
                            delay:(NSTimeInterval)//动画延迟执行的时间
                          options:(UIViewKeyframeAnimationOptions)//动画的过渡效果
                       animations:^{
                     //执行的关键帧动画
 }
                       completion:^(BOOL finished) {
                     //动画执行提交后的操作
 }];

关键帧选项UIViewKeyframeAnimationOptions枚举值如下,可组合使用:

UIViewAnimationOptionLayoutSubviews           //进行动画时布局子控件
UIViewAnimationOptionAllowUserInteraction     //进行动画时允许用户交互
UIViewAnimationOptionBeginFromCurrentState    //从当前状态开始动画
UIViewAnimationOptionRepeat                   //无限重复执行动画
UIViewAnimationOptionAutoreverse              //执行动画回路
UIViewAnimationOptionOverrideInheritedDuration //忽略嵌套动画的执行时间设置
UIViewAnimationOptionOverrideInheritedOptions //不继承父动画设置

UIViewKeyframeAnimationOptionCalculationModeLinear     //运算模式 :连续
UIViewKeyframeAnimationOptionCalculationModeDiscrete   //运算模式 :离散
UIViewKeyframeAnimationOptionCalculationModePaced      //运算模式 :均匀执行
UIViewKeyframeAnimationOptionCalculationModeCubic      //运算模式 :平滑
UIViewKeyframeAnimationOptionCalculationModeCubicPaced //运算模式 :平滑均匀

各种运算模式(选项) 的效果对比图:

\

\

增加关键帧方法:

[UIView addKeyframeWithRelativeStartTime:(double)//动画开始的时间(占总时间的比例)
                     relativeDuration:(double) //动画持续时间(占总时间的比例)
                           animations:^{
                         //执行的动画
 }];

\

转场动画

双视图:从旧视图到新视图的动画效果

[UIView transitionFromView:(nonnull UIView *) toView:(nonnull UIView *) duration:(NSTimeInterval) options:(UIViewAnimationOptions) completion:^(BOOL finished) {
                 //动画执行提交后的操作
 }];

在该动画过程中,fromView 会从父视图中移除,并将 toView 添加到父视图中,注意转场动画的作用对象是父视图(过渡效果体现在父视图上)。调用该方法相当于执行下面两句代码

[fromView.superview addSubview:toView];
[fromView removeFromSuperview];

核心动画

CAAnimation类继承关系

核心动画类中可以直接使用的类有:

  • CABasicAnimation
  • CAKeyframeAnimation
  • CATransition
  • CAAnimationGroup
  • CASpringAnimation

使用核心动画时需要注意的点:

  • 所有的动画类都遵守CAMediaTiming
  • CAAnimation和CAPropertyAnimation都是抽象类,不具有动画效果,携带动画类共有的一些属性
  • 动画组由多个动画对象组成
  • 基础动画只有初始态和最终态
  • 关键帧动画保存多个状态或路径点,逐个展示
  • 通过KeyPath设置动画属性

核心动画类的常用属性

  • KeyPath:可以指定 KeyPath 为 CALayer 的属性值,并对它修改,注意部分属性是不支持动画的
  • duration:动画的持续时间
  • repeatCount: 动画的重复次数
  • timingFunction:动画曲线(时间节奏控制)
  • fillMode:视图在非Active时的行为
  • removedOnCompletion:动画执行完毕后是否从图层上移除,默认为YES(视图会恢复到动画前的状态),可设置为NO(图层保持动画执行后的状态,前提是fillMode设置为kCAFillModeForwards)
  • beginTime:动画延迟执行时间(通过CACurrentMediaTime() + your time 设置)
  • delegate:代理

CABasicAnimation——基本动画

通过属性控制动画的参数,只要有初始状态fromValue和结束状态toValue就可实现动画效果,只能针对属性进行单一值变化的动画。

CABasicAnimation有三个比较重要的属性:fromValue,toValue,byValue,都是可选的,但不能同时多于两个为非空,最终都是为了确定animation变化的起点和终点,中间的值是通过插值方式计算出来的。插值计算的结果由timingFunction指定,默认timingFunction为nil:线性liner均匀变化的。

关键属性:

  • keyPath表示需要变化的属性的名称
  • fromValue初始状态值
  • toValue结束状态值
  • byValue 在当前值的基础上累加 的偏移值。

动画使用:

  • fromValue和toValue不为空, 动画的值从fromValue变化到toValue
  • fromValue和byValue不为空,动画的值从fromValue变化到fromValue + byValue
  • byValue和toValue不为空,动画的值从toValue - byValue变化到toValue
  • 只有fromValue不为空,动画的值从fromValue变化到layer的当前值
  • 只有toValue不为空,动画的值从layer的当前值变化到toValue
  • 只有byValue不为空,动画的值从layer的当前值变化到layer的当前值 + byValue

\

CAKeyframeAnimation——关键帧动画

CAKeyframeAnimation和CABasicAnimation都是CAPropertyAnimation的子类,前者可以控制动画的全过程,后者可理解成只关注起点和终点的CAKeyframeAnimation。

通过属性控制动画的参数,但与基础动画不同的是有多个控制状态,并且可以通过path来实现动画。

关键属性:

  • values:需要变化到的值,每个值对应一个关键帧
  • path:让图层跟随路径移动,只针对anchorPoint、postion生效。(比如CGPathRef和bezierPath)
  • keyTimes:对应关键帧的时间点
  • calculateMode:关键帧之间进行差值计算的方式。

动画使用:

设置values:

设置path:

CAAnimationGroup-动画组

一种组合动画,可以通过动画组来进行所有动画行为的统一控制,组中动画效果可以并发执行。

关键属性:

  • animations:添加到动画组的其他动画
  • duration:动画组执行的时间间隔

动画使用:

Lottie动画

一种跨平台复杂动画的实现,支持iOS、android、web、react native等。通过JSON来描述动画过程。

简介:

  • 很多复杂的动画是我们使用自带动画API无法实现的。
  • lottie是一个第三方的开源库,在使用时需要先通过CocoaPods接入lottie库。
  • 创建lottie动画只需要调用lottie api,注入描述动画的json文件即可。

lottie动画的使用

lottie动画的创建:

LOTAnimationView *lottieView = [LOTAnimationView animationNamed:@"fire"];

一行代码搞定,fire是lottie动画json文件名,这是最常用的一种创建lottie的方式,除此之外还有:

创建完lottie之后,需要记得把lottieView添加到父视图上:

lottie动画的控制:

  • 在 Lottie 的 API 中我们可以看到,只需要简单的 [lottieView play] 即可播放动画。
  • 有时候需要在动画播放完毕时,做一些事情,那么可以使用playWithCompletion
  • 播放过程中,可以调用pause或者stop方法暂停或者停止。

lottie动画也给我们提供了一些可以设置的属性,包括是否循环,动画执行时间,查看动画进度等等。

完整的使用过程:

推荐阅读

iOS | 事件传递及响应链 - 掘金

iOS | 响应链及手势识别 - 掘金

iOS动画开发-核心动画编程[CoreAnimation] - 掘金

iOS自定义转场动画 - 掘金

iOS动画全面解析 - 掘金