注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Swift 方法

Swift 方法是与某些特定类型相关联的函数在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活的在你创建的类型(类/结构体/枚举)上定义方法。实例方法在 Swift 语言中,实例...
继续阅读 »

Swift 方法是与某些特定类型相关联的函数

在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活的在你创建的类型(类/结构体/枚举)上定义方法。


实例方法

在 Swift 语言中,实例方法是属于某个特定类、结构体或者枚举类型实例的方法。

实例方法提供以下方法:

  • 可以访问和修改实例属性

  • 提供与实例目的相关的功能

实例方法要写在它所属的类型的前后大括号({})之间。

实例方法能够隐式访问它所属类型的所有的其他实例方法和属性。

实例方法只能被它所属的类的某个特定实例调用。

实例方法不能脱离于现存的实例而被调用。

语法

func funcname(Parameters) -> returntype
{
Statement1
Statement2
……
Statement N
return parameters
}

实例

import Cocoa

class Counter {
var count = 0
func increment() {
count += 1
}
func incrementBy(amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
// 初始计数值是0
let counter = Counter()

// 计数值现在是1
counter.increment()

// 计数值现在是6
counter.incrementBy(amount: 5)
print(counter.count)

// 计数值现在是0
counter.reset()
print(counter.count)

以上程序执行输出结果为:

6
0

Counter类定义了三个实例方法:

  • increment 让计数器按 1 递增;
  • incrementBy(amount: Int) 让计数器按一个指定的整数值递增;
  • reset 将计数器重置为0。

Counter 这个类还声明了一个可变属性 count,用它来保持对当前计数器值的追踪。


方法的局部参数名称和外部参数名称

Swift 函数参数可以同时有一个局部名称(在函数体内部使用)和一个外部名称(在调用函数时使用

Swift 中的方法和 Objective-C 中的方法极其相似。像在 Objective-C 中一样,Swift 中方法的名称通常用一个介词指向方法的第一个参数,比如:with,for,by等等。

Swift 默认仅给方法的第一个参数名称一个局部参数名称;默认同时给第二个和后续的参数名称为全局参数名称。

以下实例中 'no1' 在swift中声明为局部参数名称。'no2' 用于全局的声明并通过外部程序访问。

import Cocoa

class division {
var count: Int = 0
func incrementBy(no1: Int, no2: Int) {
count = no1 / no2
print(count)
}
}

let counter = division()
counter.incrementBy(no1: 1800, no2: 3)
counter.incrementBy(no1: 1600, no2: 5)
counter.incrementBy(no1: 11000, no2: 3)

以上程序执行输出结果为:

600
320
3666

是否提供外部名称设置

我们强制在第一个参数添加外部名称把这个局部名称当作外部名称使用(Swift 2.0前是使用 # 号)。

相反,我们呢也可以使用下划线(_)设置第二个及后续的参数不提供一个外部名称。

import Cocoa

class multiplication {
var count: Int = 0
func incrementBy(first no1: Int, no2: Int) {
count = no1 * no2
print(count)
}
}

let counter = multiplication()
counter.incrementBy(first: 800, no2: 3)
counter.incrementBy(first: 100, no2: 5)
counter.incrementBy(first: 15000, no2: 3)

以上程序执行输出结果为:

2400
500
45000

self 属性

类型的每一个实例都有一个隐含属性叫做self,self 完全等同于该实例本身。

你可以在一个实例的实例方法中使用这个隐含的self属性来引用当前实例。

import Cocoa

class calculations {
let a: Int
let b: Int
let res: Int

init(a: Int, b: Int) {
self.a = a
self.b = b
res = a + b
print("Self 内: \(res)")
}

func tot(c: Int) -> Int {
return res - c
}

func result() {
print("结果为: \(tot(c: 20))")
print("结果为: \(tot(c: 50))")
}
}

let pri = calculations(a: 600, b: 300)
let sum = calculations(a: 1200, b: 300)

pri.result()
sum.result()

以上程序执行输出结果为:

Self 内: 900
Self 内: 1500
结果为: 880
结果为: 850
结果为: 1480
结果为: 1450

在实例方法中修改值类型

Swift 语言中结构体和枚举是值类型。一般情况下,值类型的属性不能在它的实例方法中被修改。

但是,如果你确实需要在某个具体的方法中修改结构体或者枚举的属性,你可以选择变异(mutating)这个方法,然后方法就可以从方法内部改变它的属性;并且它做的任何改变在方法结束时还会保留在原始结构中。

方法还可以给它隐含的self属性赋值一个全新的实例,这个新实例在方法结束后将替换原来的实例。

import Cocoa

struct area {
var length = 1
var breadth = 1

func area() -> Int {
return length * breadth
}

mutating func scaleBy(res: Int) {
length *= res
breadth *= res

print(length)
print(breadth)
}
}

var val = area(length: 3, breadth: 5)
val.scaleBy(res: 3)
val.scaleBy(res: 30)
val.scaleBy(res: 300)

以上程序执行输出结果为:

9
15
270
450
81000
135000

在可变方法中给 self 赋值

可变方法能够赋给隐含属性 self 一个全新的实例。

import Cocoa

struct area {
var length = 1
var breadth = 1

func area() -> Int {
return length * breadth
}

mutating func scaleBy(res: Int) {
self.length *= res
self.breadth *= res
print(length)
print(breadth)
}
}
var val = area(length: 3, breadth: 5)
val.scaleBy(res: 13)

以上程序执行输出结果为:

39
65

类型方法

实例方法是被类型的某个实例调用的方法,你也可以定义类型本身调用的方法,这种方法就叫做类型方法。

声明结构体和枚举的类型方法,在方法的func关键字之前加上关键字static。类可能会用关键字class来允许子类重写父类的实现方法。

类型方法和实例方法一样用点号(.)语法调用。

import Cocoa

class Math
{
class func abs(number: Int) -> Int
{
if number < 0
{
return (-number)
}
else
{
return number
}
}
}

struct absno
{
static func abs(number: Int) -> Int
{
if number < 0
{
return (-number)
}
else
{
return number
}
}
}

let no = Math.abs(number: -35)
let num = absno.abs(number: -5)

print(no)
print(num)

以上程序执行输出结果为:

35
5
收起阅读 »

Swift 属性

Swift 属性将值跟特定的类、结构或枚举关联。属性可分为存储属性和计算属性:存储属性计算属性存储常量或变量作为实例的一部分计算(而不是存储)一个值用于类和结构体用于类、结构体和枚举存储属性和计算属性通常用于特定类型的实例。属性也可以直接用于类型本身,这种属性...
继续阅读 »

Swift 属性将值跟特定的类、结构或枚举关联。

属性可分为存储属性和计算属性:

存储属性计算属性
存储常量或变量作为实例的一部分计算(而不是存储)一个值
用于类和结构体用于类、结构体和枚举

存储属性和计算属性通常用于特定类型的实例。

属性也可以直接用于类型本身,这种属性称为类型属性。

另外,还可以定义属性观察器来监控属性值的变化,以此来触发一个自定义的操作。属性观察器可以添加到自己写的存储属性上,也可以添加到从父类继承的属性上。


存储属性

简单来说,一个存储属性就是存储在特定类或结构体的实例里的一个常量或变量。

存储属性可以是变量存储属性(用关键字var定义),也可以是常量存储属性(用关键字let定义)。

  • 可以在定义存储属性的时候指定默认值

  • 也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值

import Cocoa

struct Number
{
var digits: Int
let pi = 3.1415
}

var n = Number(digits: 12345)
n.digits = 67

print("\(n.digits)")
print("\(n.pi)")

以上程序执行输出结果为:

67
3.1415

考虑以下代码:

let pi = 3.1415

代码中 pi 在定义存储属性的时候指定默认值(pi = 3.1415),所以不管你什么时候实例化结构体,它都不会改变。

如果你定义的是一个常量存储属性,如果尝试修改它就会报错,如下所示:

import Cocoa

struct Number
{
var digits: Int
let numbers = 3.1415
}

var n = Number(digits: 12345)
n.digits = 67

print("\(n.digits)")
print("\(n.numbers)")
n.numbers = 8.7

以上程序,执行会报错,错误如下所示:

error: cannot assign to property: 'numbers' is a 'let' constant
n.numbers = 8.7

意思为 'numbers' 是一个常量,你不能修改它。


延迟存储属性

延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。

在属性声明前使用 lazy 来标示一个延迟存储属性。

注意:
必须将延迟存储属性声明成变量(使用var关键字),因为属性的值在实例构造完成之前可能无法得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。

延迟存储属性一般用于:

  • 延迟对象的创建。

  • 当属性的值依赖于其他未知类

import Cocoa

class sample {
lazy var no = number() // `var` 关键字是必须的
}

class number {
var name = "Runoob Swift 教程"
}

var firstsample = sample()
print(firstsample.no.name)

以上程序执行输出结果为:

Runoob Swift 教程

实例化变量

如果您有过 Objective-C 经验,应该知道Objective-C 为类实例存储值和引用提供两种方法。对于属性来说,也可以使用实例变量作为属性值的后端存储。

Swift 编程语言中把这些理论统一用属性来实现。Swift 中的属性没有对应的实例变量,属性的后端存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。

一个类型中属性的全部信息——包括命名、类型和内存管理特征——都在唯一一个地方(类型定义中)定义。


计算属性

除存储属性外,类、结构体和枚举可以定义计算属性,计算属性不直接存储值,而是提供一个 getter 来获取值,一个可选的 setter 来间接设置其他属性或变量的值。

import Cocoa

class sample {
var no1 = 0.0, no2 = 0.0
var length = 300.0, breadth = 150.0

var middle: (Double, Double) {
get{
return (length / 2, breadth / 2)
}
set(axis){
no1 = axis.0 - (length / 2)
no2 = axis.1 - (breadth / 2)
}
}
}

var result = sample()
print(result.middle)
result.middle = (0.0, 10.0)

print(result.no1)
print(result.no2)

以上程序执行输出结果为:

(150.0, 75.0)
-150.0
-65.0

如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。


只读计算属性

只有 getter 没有 setter 的计算属性就是只读计算属性。

只读计算属性总是返回一个值,可以通过点(.)运算符访问,但不能设置新的值。

import Cocoa

class film {
var head = ""
var duration = 0.0
var metaInfo: [String:String] {
return [
"head": self.head,
"duration":"\(self.duration)"
]
}
}

var movie = film()
movie.head = "Swift 属性"
movie.duration = 3.09

print(movie.metaInfo["head"]!)
print(movie.metaInfo["duration"]!)

以上程序执行输出结果为:

Swift 属性
3.09

注意:

必须使用var关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let关键字只用来声明常量属性,表示初始化后再也无法修改的值。


属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,甚至新的值和现在的值相同的时候也不例外。

可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重载属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。

注意:
不需要为无法重载的计算属性添加属性观察器,因为可以通过 setter 直接监控和响应值的变化。

可以为属性添加如下的一个或全部观察器:

  • willSet在设置新的值之前调用
  • didSet在新的值被设置之后立即调用
  • willSet和didSet观察器在属性初始化过程中不会被调用
import Cocoa

class Samplepgm {
var counter: Int = 0{
willSet(newTotal){
print("计数器: \(newTotal)")
}
didSet{
if counter > oldValue {
print("新增数 \(counter - oldValue)")
}
}
}
}
let NewCounter = Samplepgm()
NewCounter.counter = 100
NewCounter.counter = 800

以上程序执行输出结果为:

计数器: 100
新增数 100
计数器: 800
新增数 700

全局变量和局部变量

计算属性和属性观察器所描述的模式也可以用于全局变量和局部变量。

局部变量全局变量
在函数、方法或闭包内部定义的变量。函数、方法、闭包或任何类型之外定义的变量。
用于存储和检索值。用于存储和检索值。
存储属性用于获取和设置值。存储属性用于获取和设置值。
也用于计算属性。也用于计算属性。

类型属性

类型属性是作为类型定义的一部分写在类型最外层的花括号({})内。

使用关键字 static 来定义值类型的类型属性,关键字 class 来为类定义类型属性。

struct Structname {
static var storedTypeProperty = " "
static var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

enum Enumname {
static var storedTypeProperty = " "
static var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

class Classname {
class var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

注意:
例子中的计算型类型属性是只读的,但也可以定义可读可写的计算型类型属性,跟实例计算属性的语法类似。


获取和设置类型属性的值

类似于实例的属性,类型属性的访问也是通过点运算符(.)来进行。但是,类型属性是通过类型本身来获取和设置,而不是通过实例。实例如下:

import Cocoa

struct StudMarks {
static let markCount = 97
static var totalCount = 0
var InternalMarks: Int = 0 {
didSet {
if InternalMarks > StudMarks.markCount {
InternalMarks = StudMarks.markCount
}
if InternalMarks > StudMarks.totalCount {
StudMarks.totalCount = InternalMarks
}
}
}
}

var stud1Mark1 = StudMarks()
var stud1Mark2 = StudMarks()

stud1Mark1.InternalMarks = 98
print(stud1Mark1.InternalMarks)

stud1Mark2.InternalMarks = 87
print(stud1Mark2.InternalMarks)

以上程序执行输出结果为:

97
87
收起阅读 »

Swift 类

Swift 类是构建代码所用的一种通用且灵活的构造体。我们可以为类定义属性(常量、变量)和方法。与其他编程语言所不同的是,Swift 并不要求你为自定义类去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类,系统会自动生成面向其它代码的外部接口。...
继续阅读 »

Swift 类是构建代码所用的一种通用且灵活的构造体。

我们可以为类定义属性(常量、变量)和方法。

与其他编程语言所不同的是,Swift 并不要求你为自定义类去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类,系统会自动生成面向其它代码的外部接口。

类和结构体对比

Swift 中类和结构体有很多共同点。共同处在于:

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义附属脚本用于访问值
  • 定义构造器用于生成初始化值
  • 通过扩展以增加默认实现的功能
  • 符合协议以对某类提供标准功能

与结构体相比,类还有如下的附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 解构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

语法:

class classname {
Definition 1
Definition 2
……
Definition N
}

类定义

class student{
var studname: String
var mark: Int
var mark2: Int
}

实例化类:

let studrecord = student()

实例

import Cocoa

class MarksStruct {
var mark: Int
init(mark: Int) {
self.mark = mark
}
}

class studentMarks {
var mark = 300
}
let marks = studentMarks()
print("成绩为 \(marks.mark)")

以上程序执行输出结果为:

成绩为 300

作为引用类型访问类属性

类的属性可以通过 . 来访问。格式为:实例化类名.属性名

import Cocoa

class MarksStruct {
var mark: Int
init(mark: Int) {
self.mark = mark
}
}

class studentMarks {
var mark1 = 300
var mark2 = 400
var mark3 = 900
}
let marks = studentMarks()
print("Mark1 is \(marks.mark1)")
print("Mark2 is \(marks.mark2)")
print("Mark3 is \(marks.mark3)")

以上程序执行输出结果为:

Mark1 is 300
Mark2 is 400
Mark3 is 900

恒等运算符

因为类是引用类型,有可能有多个常量和变量在后台同时引用某一个类实例。

为了能够判定两个常量或者变量是否引用同一个类实例,Swift 内建了两个恒等运算符:

恒等运算符不恒等运算符
运算符为:===运算符为:!==
如果两个常量或者变量引用同一个类实例则返回 true如果两个常量或者变量引用不同一个类实例则返回 true

实例

import Cocoa

class SampleClass: Equatable {
let myProperty: String
init(s: String) {
myProperty = s
}
}
func ==(lhs: SampleClass, rhs: SampleClass) -> Bool {
return lhs.myProperty == rhs.myProperty
}

let spClass1 = SampleClass(s: "Hello")
let spClass2 = SampleClass(s: "Hello")

if spClass1 === spClass2 {// false
print("引用相同的类实例 \(spClass1)")
}

if spClass1 !== spClass2 {// true
print("引用不相同的类实例 \(spClass2)")
}

以上程序执行输出结果为:

引用不相同的类实例 SampleClass
收起阅读 »

Swift 结构体

Swift 结构体是构建代码所用的一种通用且灵活的构造体。我们可以为结构体定义属性(常量、变量)和添加方法,从而扩展结构体的功能。与 C 和 Objective C 不同的是:结构体不需要包含实现文件和接口。结构体允许我们创建一个单一文件,且系统会自动生成面向...
继续阅读 »

Swift 结构体是构建代码所用的一种通用且灵活的构造体。

我们可以为结构体定义属性(常量、变量)和添加方法,从而扩展结构体的功能。

与 C 和 Objective C 不同的是:

  • 结构体不需要包含实现文件和接口。

  • 结构体允许我们创建一个单一文件,且系统会自动生成面向其它代码的外部接口。

结构体总是通过被复制的方式在代码中传递,因此它的值是不可修改的。

语法

我们通过关键字 struct 来定义结构体:

struct nameStruct { 
Definition 1
Definition 2
……
Definition N
}

实例

我们定义一个名为 MarkStruct 的结构体 ,结构体的属性为学生三个科目的分数,数据类型为 Int:

struct MarkStruct{
var mark1: Int
var mark2: Int
var mark3: Int
}

我们可以通过结构体名来访问结构体成员。

结构体实例化使用 let 关键字:

import Cocoa

struct studentMarks {
var mark1 = 100
var mark2 = 78
var mark3 = 98
}
let marks = studentMarks()
print("Mark1 是 \(marks.mark1)")
print("Mark2 是 \(marks.mark2)")
print("Mark3 是 \(marks.mark3)")

以上程序执行输出结果为:

Mark1  100
Mark2 78
Mark3 98

实例中,我们通过结构体名 'studentMarks' 访问学生的成绩。结构体成员初始化为mark1, mark2, mark3,数据类型为整型。

然后我们通过使用 let 关键字将结构体 studentMarks() 实例化并传递给 marks。

最后我们就通过 . 号来访问结构体成员的值。

以下实例化通过结构体实例化时传值并克隆一个结构体:

import Cocoa

struct MarksStruct {
var mark: Int

init
(mark: Int) {
self.mark = mark
}
}
var aStruct = MarksStruct(mark: 98)
var bStruct = aStruct // aStruct 和 bStruct 是使用相同值的结构体!
bStruct
.mark = 97
print(aStruct.mark) // 98
print(bStruct.mark) // 97

以上程序执行输出结果为:

98
97

结构体应用

在你的代码中,你可以使用结构体来定义你的自定义数据类型。

结构体实例总是通过值传递来定义你的自定义数据类型。

按照通用的准则,当符合一条或多条以下条件时,请考虑构建结构体:

  • 结构体的主要目的是用来封装少量相关简单数据值。
  • 有理由预计一个结构体实例在赋值或传递时,封装的数据将会被拷贝而不是被引用。
  • 任何在结构体中储存的值类型属性,也将会被拷贝,而不是被引用。
  • 结构体不需要去继承另一个已存在类型的属性或者行为。

举例来说,以下情境中适合使用结构体:

  • 几何形状的大小,封装一个width属性和height属性,两者均为Double类型。
  • 一定范围内的路径,封装一个start属性和length属性,两者均为Int类型。
  • 三维坐标系内一点,封装xyz属性,三者均为Double类型。

结构体实例是通过值传递而不是通过引用传递。

import Cocoa

struct markStruct{
var mark1: Int
var mark2: Int
var mark3: Int

init
(mark1: Int, mark2: Int, mark3: Int){
self.mark1 = mark1
self.mark2 = mark2
self.mark3 = mark3
}
}

print("优异成绩:")
var marks = markStruct(mark1: 98, mark2: 96, mark3:100)
print(marks.mark1)
print(marks.mark2)
print(marks.mark3)

print("糟糕成绩:")
var fail = markStruct(mark1: 34, mark2: 42, mark3: 13)
print(fail.mark1)
print(fail.mark2)
print(fail.mark3)

以上程序执行输出结果为:

优异成绩:
98
96
100
糟糕成绩:
34
42
13

以上实例中我们定义了结构体 markStruct,三个成员属性:mark1, mark2 和 mark3。结构体内使用成员属性使用 self 关键字。

从实例中我们可以很好的理解到结构体实例是通过值传递的。

收起阅读 »

Swift 枚举

枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:它声明在类中,可以通过实例化类来访问它的值。枚举也可以定义构造函数(ini...
继续阅读 »

枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。

Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:

  • 它声明在类中,可以通过实例化类来访问它的值。

  • 枚举也可以定义构造函数(initializers)来提供一个初始成员值;可以在原始的实现基础上扩展它们的功能。

  • 可以遵守协议(protocols)来提供标准的功能。

语法

Swift 中使用 enum 关键词来创建枚举并且把它们的整个定义放在一对大括号内:

enum enumname {
// 枚举定义放在这里
}

例如我们定义以下表示星期的枚举:

import Cocoa

// 定义枚举
enum DaysofaWeek {
case Sunday
case Monday
case TUESDAY
case WEDNESDAY
case THURSDAY
case FRIDAY
case Saturday
}

var weekDay = DaysofaWeek.THURSDAY
weekDay = .THURSDAY
switch weekDay
{
case .Sunday:
print("星期天")
case .Monday:
print("星期一")
case .TUESDAY:
print("星期二")
case .WEDNESDAY:
print("星期三")
case .THURSDAY:
print("星期四")
case .FRIDAY:
print("星期五")
case .Saturday:
print("星期六")
}

以上程序执行输出结果为:

星期四

枚举中定义的值(如 SundayMonday……Saturday)是这个枚举的成员值(或成员)。case关键词表示一行新的成员值将被定义。

注意: 和 C 和 Objective-C 不同,Swift 的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的DaysofaWeek例子中,SundayMonday……Saturday不会隐式地赋值为01……6。相反,这些枚举成员本身就有完备的值,这些值是已经明确定义好的DaysofaWeek类型。

var weekDay = DaysofaWeek.THURSDAY 

weekDay的类型可以在它被DaysofaWeek的一个可能值初始化时推断出来。一旦weekDay被声明为一个DaysofaWeek,你可以使用一个缩写语法(.)将其设置为另一个DaysofaWeek的值:

var weekDay = .THURSDAY 

weekDay的类型已知时,再次为其赋值可以省略枚举名。使用显式类型的枚举值可以让代码具有更好的可读性。

枚举可分为相关值与原始值。

相关值与原始值的区别

相关值原始值
不同数据类型相同数据类型
实例: enum {10,0.8,"Hello"}实例: enum {10,35,50}
值的创建基于常量或变量预先填充的值
相关值是当你在创建一个基于枚举成员的新常量或变量时才会被设置,并且每次当你这么做得时候,它的值可以是不同的。原始值始终是相同的

相关值

以下实例中我们定义一个名为 Student 的枚举类型,它可以是 Name 的一个字符串(String),或者是 Mark 的一个相关值(Int,Int,Int)。

import Cocoa

enum Student{
case Name(String)
case Mark(Int,Int,Int)
}
var studDetails = Student.Name("Runoob")
var studMarks = Student.Mark(98,97,95)
switch studMarks {
case .Name(let studName):
print("学生的名字是: \(studName)。")
case .Mark(let Mark1, let Mark2, let Mark3):
print("学生的成绩是: \(Mark1),\(Mark2),\(Mark3)。")
}

以上程序执行输出结果为:

学生的成绩是: 98,97,95。

原始值

原始值可以是字符串,字符,或者任何整型值或浮点型值。每个原始值在它的枚举声明中必须是唯一的。

在原始值为整数的枚举时,不需要显式的为每一个成员赋值,Swift会自动为你赋值。

例如,当使用整数作为原始值时,隐式赋值的值依次递增1。如果第一个值没有被赋初值,将会被自动置为0。

import Cocoa

enum Month: Int {
case January = 1, February, March, April, May, June, July, August, September, October, November, December
}

let yearMonth = Month.May.rawValue
print("数字月份为: \(yearMonth)。")

以上程序执行输出结果为:

数字月份为: 5。
收起阅读 »

Swift 闭包

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 匿名函数比较相似。全局函数和嵌套函数其实就是特殊的闭包。闭包的形式有...
继续阅读 »

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。

Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 匿名函数比较相似。

全局函数和嵌套函数其实就是特殊的闭包。

闭包的形式有:

全局函数嵌套函数闭包表达式
有名字但不能捕获任何值。有名字,也能捕获封闭函数内的值。无名闭包,使用轻量级语法,可以根据上下文环境捕获值。

Swift中的闭包有很多优化的地方:

  1. 根据上下文推断参数和返回值类型
  2. 从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略return)
  3. 可以使用简化参数名,如$0, $1(从0开始,表示第i个参数...)
  4. 提供了尾随闭包语法(Trailing closure syntax)
  5. 语法

    以下定义了一个接收参数并返回指定类型的闭包语法:

    {(parameters) -> return type in
    statements
    }

    实例

    import Cocoa

    let studname = { print("Swift 闭包实例。") }
    studname
    ()

    以上程序执行输出结果为:

    Swift 闭包实例。

    以下闭包形式接收两个参数并返回布尔值:

    {(Int, Int) -> Bool in
    Statement1
    Statement 2
    ---
    Statement n
    }

    实例

    import Cocoa

    let divide = {(val1: Int, val2: Int) -> Int in
    return val1 / val2
    }
    let result = divide(200, 20)
    print (result)

    以上程序执行输出结果为:

    10

    闭包表达式

    闭包表达式是一种利用简洁语法构建内联闭包的方式。 闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。


    sorted 方法

    Swift 标准库提供了名为 sorted(by:) 的方法,会根据您提供的用于排序的闭包函数将已知类型数组中的值进行排序。

    排序完成后,sorted(by:) 方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被 sorted(by:) 方法修改。

    sorted(by:)方法需要传入两个参数:

    • 已知类型的数组
    • 闭包函数,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    // 使用普通函数(或内嵌函数)提供排序功能,闭包函数类型需为(String, String) -> Bool。
    func backwards
    (s1: String, s2: String) -> Bool {
    return s1 > s2
    }
    var reversed = names.sorted(by: backwards)

    print(reversed)

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    如果第一个字符串 (s1) 大于第二个字符串 (s2),backwards函数返回true,表示在新的数组中s1应该出现在s2前。 对于字符串中的字符来说,"大于" 表示 "按照字母顺序较晚出现"。 这意味着字母"B"大于字母"A",字符串"S"大于字符串"D"。 其将进行字母逆序排序,"AT"将会排在"AE"之前。


    参数名称缩写

    Swift 自动为内联函数提供了参数名称缩写功能,您可以直接通过$0,$1,$2来顺序调用闭包的参数。

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    var reversed = names.sorted( by: { $0 > $1 } )
    print(reversed)

    $0和$1表示闭包中第一个和第二个String类型的参数。

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    如果你在闭包表达式中使用参数名称缩写, 您可以在闭包参数列表中省略对其定义, 并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字同样也可以被省略.


    运算符函数

    实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。

    Swift 的String类型定义了关于大于号 (>) 的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。 而这正好与sort(_:)方法的第二个参数需要的函数类型相符合。 因此,您可以简单地传递一个大于号,Swift可以自动推断出您想使用大于号的字符串函数实现:

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    var reversed = names.sorted(by: >)
    print(reversed)

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    尾随闭包

    尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。

    func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体部分
    }

    // 以下是不使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure
    ({
    // 闭包主体部分
    })

    // 以下是使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure
    () {
    // 闭包主体部分
    }

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    //尾随闭包
    var reversed = names.sorted() { $0 > $1 }
    print(reversed)

    sort() 后的 { $0 > $1} 为尾随闭包。

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    注意: 如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()省略掉。

    reversed = names.sorted { $0 > $1 }

    捕获值

    闭包可以在其定义的上下文中捕获常量或变量。

    即使定义这些常量和变量的原域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

    Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数。

    嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

    看这个例子:

    func makeIncrementor(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    一个函数makeIncrementor ,它有一个Int型的参数amout, 并且它有一个外部参数名字forIncremet,意味着你调用的时候,必须使用这个外部名字。返回值是一个()-> Int的函数。

    函数体内,声明了变量 runningTotal 和一个函数 incrementor。

    incrementor函数并没有获取任何参数,但是在函数体内访问了runningTotal和amount变量。这是因为其通过捕获在包含它的函数体内已经存在的runningTotal和amount变量而实现。

    由于没有修改amount变量,incrementor实际上捕获并存储了该变量的一个副本,而该副本随着incrementor一同被存储。

    所以我们调用这个函数时会累加:

    import Cocoa

    func makeIncrementor
    (forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    let incrementByTen = makeIncrementor(forIncrement: 10)

    // 返回的值为10
    print(incrementByTen())

    // 返回的值为20
    print(incrementByTen())

    // 返回的值为30
    print(incrementByTen())

    以上程序执行输出结果为:

    10
    20
    30

    闭包是引用类型

    上面的例子中,incrementByTen是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量值。

    这是因为函数和闭包都是引用类型。

    无论您将函数/闭包赋值给一个常量还是变量,您实际上都是将常量/变量的值设置为对应函数/闭包的引用。 上面的例子中,incrementByTen指向闭包的引用是一个常量,而并非闭包内容本身。

    这也意味着如果您将闭包赋值给了两个不同的常量/变量,两个值都会指向同一个闭包:

    import Cocoa

    func makeIncrementor
    (forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    let incrementByTen = makeIncrementor(forIncrement: 10)

    // 返回的值为10
    incrementByTen
    ()

    // 返回的值为20
    incrementByTen
    ()

    // 返回的值为30
    incrementByTen
    ()

    // 返回的值为40
    incrementByTen
    ()

    let alsoIncrementByTen = incrementByTen

    // 返回的值也为50
    print(alsoIncrementByTen())
收起阅读 »

Swift 函数

Swift 函数用来完成特定任务的独立的代码块。Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。函数声明: 告诉编译器函数的名字,返回类型及参数。函数定义: 提供了函数的实体。Swift 函数包含了参数类型...
继续阅读 »

Swift 函数用来完成特定任务的独立的代码块。

Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。

  • 函数声明: 告诉编译器函数的名字,返回类型及参数。

  • 函数定义: 提供了函数的实体。

Swift 函数包含了参数类型及返回值类型:


函数定义

Swift 定义函数使用关键字 func

定义函数的时候,可以指定一个或多个输入参数和一个返回值类型。

每个函数都有一个函数名来描述它的功能。通过函数名以及对应类型的参数值来调用这个函数。函数的参数传递的顺序必须与参数列表相同。

函数的实参传递的顺序必须与形参列表相同,-> 后定义函数的返回值类型。

语法

func funcname(形参) -> returntype
{
Statement1
Statement2
……
Statement N
return parameters
}

实例

以下我们定义了一个函数名为 runoob 的函数,形参的数据类型为 String,返回值也为 String:

import Cocoa

func runoob
(site: String) -> String {
return (site)
}
print(runoob(site: "www.runoob.com"))

以上程序执行输出结果为:

www.runoob.com

函数调用

我们可以通过函数名以及对应类型的参数值来调用函数,函数的参数传递的顺序必须与参数列表相同。

以下我们定义了一个函数名为 runoob 的函数,形参 site 的数据类型为 String,之后我们调用函数传递的实参也必须 String 类型,实参传入函数体后,将直接返回,返回的数据类型为 String。

import Cocoa

func runoob
(site: String) -> String {
return (site)
}
print(runoob(site: "www.runoob.com"))

以上程序执行输出结果为:

www.runoob.com

函数参数

函数可以接受一个或者多个参数,这些参数被包含在函数的括号之中,以逗号分隔。

以下实例向函数 runoob 传递站点名 name 和站点地址 site:

import Cocoa

func runoob
(name: String, site: String) -> String {
return name + site
}
print(runoob(name: "菜鸟教程:", site: "www.runoob.com"))
print(runoob(name: "Google:", site: "www.google.com"))

以上程序执行输出结果为:

菜鸟教程:www.runoob.com
Googlewww.google.com

不带参数函数

我们可以创建不带参数的函数。

语法:

func funcname() -> datatype {
return datatype
}

实例

import Cocoa

func sitename
() -> String {
return "菜鸟教程"
}
print(sitename())

以上程序执行输出结果为:

菜鸟教程

元组作为函数返回值

函数返回值类型可以是字符串,整型,浮点型等。

元组与数组类似,不同的是,元组中的元素可以是任意类型,使用的是圆括号。

你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。

下面的这个例子中,定义了一个名为minMax(_:)的函数,作用是在一个Int数组中找出最小值与最大值。

import Cocoa

func minMax
(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin
= value
} else if value > currentMax {
currentMax
= value
}
}
return (currentMin, currentMax)
}

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("最小值为 \(bounds.min) ,最大值为 \(bounds.max)")

minMax(_:)函数返回一个包含两个Int值的元组,这些值被标记为min和max,以便查询函数的返回值时可以通过名字访问它们。

以上程序执行输出结果为:

最小值为 -6 ,最大值为 109

如果你不确定返回的元组一定不为nil,那么你可以返回一个可选的元组类型。

你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如(Int, Int)?或(String, Int, Bool)?

注意
可选元组类型如(Int, Int)?与元组包含可选类型如(Int?, Int?)是不同的.可选的元组类型,整个元组是可选的,而不只是元组中的每个元素值。

前面的minMax(_:)函数返回了一个包含两个Int值的元组。但是函数不会对传入的数组执行任何安全检查,如果array参数是一个空数组,如上定义的minMax(_:)在试图访问array[0]时会触发一个运行时错误。

为了安全地处理这个"空数组"问题,将minMax(_:)函数改写为使用可选元组返回类型,并且当数组为空时返回nil

import Cocoa

func minMax
(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin
= value
} else if value > currentMax {
currentMax
= value
}
}
return (currentMin, currentMax)
}
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
print("最小值为 \(bounds.min),最大值为 \(bounds.max)")
}

以上程序执行输出结果为:

最小值为 -6,最大值为 109

没有返回值函数

下面是 runoob(_:) 函数的另一个版本,这个函数接收菜鸟教程官网网址参数,没有指定返回值类型,并直接输出 String 值,而不是返回它:

import Cocoa

func runoob
(site: String) {
print("菜鸟教程官网:\(site)")
}
runoob
(site: "http://www.runoob.com")

以上程序执行输出结果为:

菜鸟教程官网:http://www.runoob.com

函数参数名称

函数参数都有一个外部参数名和一个局部参数名。

局部参数名

局部参数名在函数的实现内部使用。

func sample(number: Int) {
println
(number)
}

以上实例中 number 为局部参数名,只能在函数体内使用。

import Cocoa

func sample
(number: Int) {
print(number)
}
sample
(number: 1)
sample
(number: 2)
sample
(number: 3)

以上程序执行输出结果为:

1
2
3

外部参数名

你可以在局部参数名前指定外部参数名,中间以空格分隔,外部参数名用于在函数调用时传递给函数的参数。

如下你可以定义以下两个函数参数名并调用它:

import Cocoa

func pow
(firstArg a: Int, secondArg b: Int) -> Int {
var res = a
for _ in 1..<b {
res
= res * a
}
print(res)
return res
}
pow
(firstArg:5, secondArg:3)

以上程序执行输出结果为:

125

注意
如果你提供了外部参数名,那么函数在被调用时,必须使用外部参数名。


可变参数

可变参数可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数,其数量是不确定的。

可变参数通过在变量类型名后面加入(...)的方式来定义。

import Cocoa

func vari
<N>(members: N...){
for i in members {
print(i)
}
}
vari
(members: 4,3,5)
vari
(members: 4.5, 3.1, 5.6)
vari
(members: "Google", "Baidu", "Runoob")

以上程序执行输出结果为:

4
3
5
4.5
3.1
5.6
Google
Baidu
Runoob

常量,变量及 I/O 参数

一般默认在函数中定义的参数都是常量参数,也就是这个参数你只可以查询使用,不能改变它的值。

如果想要声明一个变量参数,可以在参数定义前加 inout 关键字,这样就可以改变这个参数的值了。

例如:

func  getName(_ name: inout String).........

此时这个 name 值可以在函数中改变。

一般默认的参数传递都是传值调用的,而不是传引用。所以传入的参数在函数内改变,并不影响原来的那个参数。传入的只是这个参数的副本。

当传入的参数作为输入输出参数时,需要在参数名前加 & 符,表示这个值可以被函数修改。

实例

import Cocoa

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


var x = 1
var y = 5
swapTwoInts
(&x, &y)
print("x 现在的值 \(x), y 现在的值 \(y)")

swapTwoInts(_:_:) 函数简单地交换 a 与 b 的值。该函数先将 a 的值存到一个临时常量 temporaryA 中,然后将 b 的值赋给 a,最后将 temporaryA 赋值给 b。

需要注意的是,someInt 和 anotherInt 在传入 swapTwoInts(_:_:) 函数前,都加了 & 的前缀。

以上程序执行输出结果为:

x 现在的值 5, y 现在的值 1

函数类型及使用

每个函数都有种特定的函数类型,由函数的参数类型和返回类型组成。

func inputs(no1: Int, no2: Int) -> Int {
return no1/no2
}

inputs 函数类型有两个 Int 型的参数(no1、no2)并返回一个 Int 型的值。

实例如下:

import Cocoa

func inputs
(no1: Int, no2: Int) -> Int {
return no1/no2
}
print(inputs(no1: 20, no2: 10))
print(inputs(no1: 36, no2: 6))

以上程序执行输出结果为:

2
6

以上函数定义了两个 Int 参数类型,返回值也为 Int 类型。

接下来我们看下如下函数,函数定义了参数为 String 类型,返回值为 String 类型。

func inputstr(name: String) -> String {
return name
}

函数也可以定义一个没有参数,也没有返回值的函数,如下所示:

import Cocoa

func inputstr
() {
print("菜鸟教程")
print("www.runoob.com")
}
inputstr
()

以上程序执行输出结果为:

菜鸟教程
www
.runoob.com

使用函数类型

在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:

var addition: (Int, Int) -> Int = sum

解析:

"定义一个叫做 addition 的变量,参数与返回值类型均是 Int ,并让这个新变量指向 sum 函数"。

sum 和 addition 有同样的类型,所以以上操作是合法的。

现在,你可以用 addition 来调用被赋值的函数了:

import Cocoa

func sum
(a: Int, b: Int) -> Int {
return a + b
}
var addition: (Int, Int) -> Int = sum
print("输出结果: \(addition(40, 89))")

以上程序执行输出结果为:

输出结果: 129

函数类型作为参数类型、函数类型作为返回类型

我们可以将函数作为参数传递给另外一个参数:

import Cocoa

func sum
(a: Int, b: Int) -> Int {
return a + b
}
var addition: (Int, Int) -> Int = sum
print("输出结果: \(addition(40, 89))")

func another
(addition: (Int, Int) -> Int, a: Int, b: Int) {
print("输出结果: \(addition(a, b))")
}
another
(addition: sum, a: 10, b: 20)

以上程序执行输出结果为:

输出结果: 129
输出结果: 30

函数嵌套

函数嵌套指的是函数内定义一个新的函数,外部的函数可以调用函数内定义的函数。

实例如下:

import Cocoa

func calcDecrement
(forDecrement total: Int) -> () -> Int {
var overallDecrement = 0
func decrementer
() -> Int {
overallDecrement
-= total
return overallDecrement
}
return decrementer
}
let decrem = calcDecrement(forDecrement: 30)
print(decrem())

以上程序执行输出结果为:

-30
收起阅读 »

Swift 字典

Swift 字典用来存储无序的相同类型数据的集合,Swift 字典会强制检测元素的类型,如果类型不同则会报错。Swift 字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我...
继续阅读 »

Swift 字典用来存储无序的相同类型数据的集合,Swift 字典会强制检测元素的类型,如果类型不同则会报错。

Swift 字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。

和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。

Swift 字典的key没有类型限制可以是整型或字符串,但必须是唯一的。

如果创建一个字典,并赋值给一个变量,则创建的字典就是可以修改的。这意味着在创建字典后,可以通过添加、删除、修改的方式改变字典里的项目。如果将一个字典赋值给常量,字典就不可修改,并且字典的大小和内容都不可以修改。


创建字典

我们可以使用以下语法来创建一个特定类型的空字典:

var someDict =  [KeyType: ValueType]()

以下是创建一个空字典,键的类型为 Int,值的类型为 String 的简单语法:

var someDict = [Int: String]()

以下为创建一个字典的实例:

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

访问字典

我们可以根据字典的索引来访问数组的元素,语法如下:

var someVar = someDict[key]

我们可以通过以下实例来学习如何创建,初始化,访问字典:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var someVar = someDict[1]

print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

修改字典

我们可以使用 updateValue(forKey:) 增加或更新字典的内容。如果 key 不存在,则添加值,如果存在则修改 key 对应的值。updateValue(_:forKey:)方法返回Optional值。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var oldVal = someDict.updateValue("One 新的值", forKey: 1)

var someVar = someDict[1]

print( "key = 1 旧的值 \(oldVal)" )
print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 旧的值 Optional("One")
key
= 1 的值为 Optional("One 新的值")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

你也可以通过指定的 key 来修改字典的值,如下所示:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var oldVal = someDict[1]
someDict
[1] = "One 新的值"
var someVar = someDict[1]

print( "key = 1 旧的值 \(oldVal)" )
print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 旧的值 Optional("One")
key
= 1 的值为 Optional("One 新的值")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

移除 Key-Value 对

我们可以使用 removeValueForKey() 方法来移除字典 key-value 对。如果 key 存在该方法返回移除的值,如果不存在返回 nil 。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var removedValue = someDict.removeValue(forKey: 2)

print( "key = 1 的值为 \(someDict[1])" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 nil
key
= 3 的值为 Optional("Three")

你也可以通过指定键的值为 nil 来移除 key-value(键-值)对。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

someDict
[2] = nil

print( "key = 1 的值为 \(someDict[1])" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 nil
key
= 3 的值为 Optional("Three")

遍历字典

我们可以使用 for-in 循环来遍历某个字典中的键值对。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

for (key, value) in someDict {
print("字典 key \(key) - 字典 value \(value)")
}

以上程序执行输出结果为:

字典 key 2 -  字典 value Two
字典 key 3 - 字典 value Three
字典 key 1 - 字典 value One

我们也可以使用enumerate()方法来进行字典遍历,返回的是字典的索引及 (key, value) 对,实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

for (key, value) in someDict.enumerated() {
print("字典 key \(key) - 字典 (key, value) 对 \(value)")
}

以上程序执行输出结果为:

字典 key 0 -  字典 (key, value)  (2, "Two")
字典 key 1 - 字典 (key, value) (3, "Three")
字典 key 2 - 字典 (key, value) (1, "One")

字典转换为数组

你可以提取字典的键值(key-value)对,并转换为独立的数组。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

let dictKeys = [Int](someDict.keys)
let dictValues = [String](someDict.values)

print("输出字典的键(key)")

for (key) in dictKeys {
print("\(key)")
}

print("输出字典的值(value)")

for (value) in dictValues {
print("\(value)")
}

以上程序执行输出结果为:

输出字典的键(key)
2
3
1
输出字典的值(value)
Two
Three
One

count 属性

我们可以使用只读的 count 属性来计算字典有多少个键值对:

import Cocoa

var someDict1:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
var someDict2:[Int:String] = [4:"Four", 5:"Five"]

print("someDict1 含有 \(someDict1.count) 个键值对")
print("someDict2 含有 \(someDict2.count) 个键值对")

以上程序执行输出结果为:

someDict1 含有 3 个键值对
someDict2
含有 2 个键值对

isEmpty 属性

Y我们可以通过只读属性 isEmpty 来判断字典是否为空,返回布尔值:

import Cocoa

var someDict1:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
var someDict2:[Int:String] = [4:"Four", 5:"Five"]
var someDict3:[Int:String] = [Int:String]()

print("someDict1 = \(someDict1.isEmpty)")
print("someDict2 = \(someDict2.isEmpty)")
print("someDict3 = \(someDict3.isEmpty)")

以上程序执行输出结果为:

someDict1 = false
someDict2
= false
someDict3
= true
收起阅读 »

Swift 数组

iOS
Swift 数组Swift 数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。Swift 数组会强制检测元素的类型,如果类型不同则会报错,Swift 数组应该遵循像Array<Element>这样的形式,其中Elem...
继续阅读 »

Swift 数组

Swift 数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。

Swift 数组会强制检测元素的类型,如果类型不同则会报错,Swift 数组应该遵循像Array<Element>这样的形式,其中Element是这个数组中唯一允许存在的数据类型。

如果创建一个数组,并赋值给一个变量,则创建的集合就是可以修改的。这意味着在创建数组后,可以通过添加、删除、修改的方式改变数组里的项目。如果将一个数组赋值给常量,数组就不可更改,并且数组的大小和内容都不可以修改。


创建数组

我们可以使用构造语法来创建一个由特定数据类型构成的空数组:

var someArray = [SomeType]()

以下是创建一个初始化大小数组的语法:

var someArray = [SomeType](repeating: InitialValue, count: NumbeOfElements)

以下实例创建了一个类型为 Int ,数量为 3,初始值为 0 的空数组:

var someInts = [Int](repeating: 0, count: 3)

以下实例创建了含有三个元素的数组:

var someInts:[Int] = [10, 20, 30]

访问数组

我们可以根据数组的索引来访问数组的元素,语法如下:

var someVar = someArray[index]

index 索引从 0 开始,即索引 0 对应第一个元素,索引 1 对应第二个元素,以此类推。

我们可以通过以下实例来学习如何创建,初始化,访问数组:

import Cocoa

var someInts = [Int](repeating: 10, count: 3)

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 10
第二个元素的值 10
第三个元素的值 10

修改数组

你可以使用 append() 方法或者赋值运算符 += 在数组末尾添加元素,如下所示,我们初始化一个数组,并向其添加元素:

import Cocoa

var someInts = [Int]()

someInts
.append(20)
someInts
.append(30)
someInts
+= [40]

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 20
第二个元素的值 30
第三个元素的值 40

我们也可以通过索引修改数组元素的值:

import Cocoa

var someInts = [Int]()

someInts
.append(20)
someInts
.append(30)
someInts
+= [40]

// 修改最后一个元素
someInts
[2] = 50

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 20
第二个元素的值 30
第三个元素的值 50

遍历数组

我们可以使用for-in循环来遍历所有数组中的数据项:

import Cocoa

var someStrs = [String]()

someStrs
.append("Apple")
someStrs
.append("Amazon")
someStrs
.append("Runoob")
someStrs
+= ["Google"]

for item in someStrs {
print(item)
}

以上程序执行输出结果为:

Apple
Amazon
Runoob
Google

如果我们同时需要每个数据项的值和索引值,可以使用 String 的 enumerate() 方法来进行数组遍历。实例如下:

import Cocoa

var someStrs = [String]()

someStrs
.append("Apple")
someStrs
.append("Amazon")
someStrs
.append("Runoob")
someStrs
+= ["Google"]

for (index, item) in someStrs.enumerated() {
print("在 index = \(index) 位置上的值为 \(item)")
}

以上程序执行输出结果为:

 index = 0 位置上的值为 Apple
index = 1 位置上的值为 Amazon
index = 2 位置上的值为 Runoob
index = 3 位置上的值为 Google

合并数组

我们可以使用加法操作符(+)来合并两种已存在的相同类型数组。新数组的数据类型会从两个数组的数据类型中推断出来:

import Cocoa

var intsA = [Int](repeating: 2, count:2)
var intsB = [Int](repeating: 1, count:3)

var intsC = intsA + intsB

for item in intsC {
print(item)
}

以上程序执行输出结果为:

2
2
1
1
1

count 属性

我们可以使用 count 属性来计算数组元素个数:

import Cocoa

var intsA = [Int](count:2, repeatedValue: 2)
var intsB = [Int](count:3, repeatedValue: 1)

var intsC = intsA + intsB

print("intsA 元素个数为 \(intsA.count)")
print("intsB 元素个数为 \(intsB.count)")
print("intsC 元素个数为 \(intsC.count)")

以上程序执行输出结果为:

intsA 元素个数为 2
intsB
元素个数为 3
intsC
元素个数为 5

isEmpty 属性

我们可以通过只读属性 isEmpty 来判断数组是否为空,返回布尔值:

import Cocoa

var intsA = [Int](count:2, repeatedValue: 2)
var intsB = [Int](count:3, repeatedValue: 1)
var intsC = [Int]()

print("intsA.isEmpty = \(intsA.isEmpty)")
print("intsB.isEmpty = \(intsB.isEmpty)")
print("intsC.isEmpty = \(intsC.isEmpty)")

以上程序执行输出结果为:

intsA.isEmpty = false
intsB
.isEmpty = false
intsC
.isEmpty = true
收起阅读 »

Swift 字符(Character)

iOS
Swift 的字符是一个单一的字符字符串字面量,数据类型为 Character。以下实例列出了两个字符实例:import Cocoa let char1: Character = "A" let char2: Character = "B" print("...
继续阅读 »

Swift 的字符是一个单一的字符字符串字面量,数据类型为 Character。

以下实例列出了两个字符实例:

import Cocoa

let char1: Character = "A"
let char2: Character = "B"

print("char1 的值为 \(char1)")
print("char2 的值为 \(char2)")

以上程序执行输出结果为:

char1 的值为 A
char2
的值为 B

如果你想在 Character(字符) 类型的常量中存储更多的字符,则程序执行会报错,如下所示:

import Cocoa

// Swift 中以下赋值会报错
let char: Character = "AB"

print("Value of char \(char)")

以上程序执行输出结果为:

error: cannot convert value of type 'String' to specified type 'Character'
let char: Character = "AB"

空字符变量

Swift 中不能创建空的 Character(字符) 类型变量或常量:

import Cocoa

// Swift 中以下赋值会报错
let char1: Character = ""
var char2: Character = ""

print("char1 的值为 \(char1)")
print("char2 的值为 \(char2)")

以上程序执行输出结果为:

 error: cannot convert value of type 'String' to specified type 'Character'
let char1: Character = ""
^~
error
: cannot convert value of type 'String' to specified type 'Character'
var char2: Character = ""

遍历字符串中的字符

Swift 的 String 类型表示特定序列的 Character(字符) 类型值的集合。 每一个字符值代表一个 Unicode 字符。

Swift 3 中的 String 需要通过 characters 去调用的属性方法,在 Swift 4 中可以通过 String 对象本身直接调用,例如:

Swift 3 中:

import Cocoa

for ch in "Runoob".characters {
print(ch)
}

Swift 4 中:

import Cocoa

for ch in "Runoob" {
print(ch)
}

以上程序执行输出结果为:

R
u
n
o
o
b

字符串连接字符

以下实例演示了使用 String 的 append() 方法来实现字符串连接字符:

import Cocoa

var varA:String = "Hello "
let varB:Character = "G"

varA
.append( varB )

print("varC = \(varA)")

以上程序执行输出结果为:

varC  =  Hello G
收起阅读 »

Swift 字符串

iOS
Swift 字符串是一系列字符的集合。例如 "Hello, World!" 这样的有序的字符类型的值的集合,它的数据类型为 String。创建字符串你可以通过使用字符串字面量或 String 类的实例来创建一个字符串:import Cocoa //...
继续阅读 »

Swift 字符串是一系列字符的集合。例如 "Hello, World!" 这样的有序的字符类型的值的集合,它的数据类型为 String


创建字符串

你可以通过使用字符串字面量或 String 类的实例来创建一个字符串:

import Cocoa

// 使用字符串字面量
var stringA = "Hello, World!"
print( stringA )

// String 实例化
var stringB = String("Hello, World!")
print( stringB )

以上程序执行输出结果为:

Hello, World!
Hello, World!

空字符串

你可以使用空的字符串字面量赋值给变量或初始化一个String类的实例来初始值一个空的字符串。 我们可以使用字符串属性 isEmpty 来判断字符串是否为空:

import Cocoa

// 使用字符串字面量创建空字符串
var stringA = ""

if stringA.isEmpty {
print( "stringA 是空的" )
} else {
print( "stringA 不是空的" )
}

// 实例化 String 类来创建空字符串
let stringB = String()

if stringB.isEmpty {
print( "stringB 是空的" )
} else {
print( "stringB 不是空的" )
}

以上程序执行输出结果为:

stringA 是空的
stringB 是空的

字符串常量

你可以将一个字符串赋值给一个变量或常量,变量是可修改的,常量是不可修改的。

import Cocoa

// stringA 可被修改
var stringA = "菜鸟教程:"
stringA += "http://www.runoob.com"
print( stringA )

// stringB 不能修改
let stringB = String("菜鸟教程:")
stringB += "http://www.runoob.com"
print( stringB )

以上程序执行输出结果会报错,因为 stringB 为常量是不能被修改的:

error: left side of mutating operator isn't mutable: 'stringB' is a 'let' constant
stringB += "http://www.runoob.com"

字符串中插入值

字符串插值是一种构建新字符串的方式,可以在其中包含常量、变量、字面量和表达式。 您插入的字符串字面量的每一项都在以反斜线为前缀的圆括号中:

import Cocoa

var varA = 20
let constA = 100
var varC:Float = 20.0

var stringA = "\(varA) 乘于 \(constA) 等于 \(varC * 100)"
print( stringA )

以上程序执行输出结果为:

20 乘于 100 等于 2000.0

字符串连接

字符串可以通过 + 号来连接,实例如下:

import Cocoa

let constA = "菜鸟教程:"
let constB = "http://www.runoob.com"

var stringA = constA + constB

print( stringA )

以上程序执行输出结果为:

菜鸟教程:http://www.runoob.com

字符串长度

字符串长度使用 String.count 属性来计算,实例如下:

Swift 3 版本使用的是 String.characters.count

import Cocoa

var varA = "www.runoob.com"

print( "\(varA), 长度为 \(varA.count)" )

以上程序执行输出结果为:

http://www.runoob.com, 长度为 14

字符串比较

你可以使用 == 来比较两个字符串是否相等:

import Cocoa

var varA = "Hello, Swift!"
var varB = "Hello, World!"

if varA == varB {
print( "\(varA) 与 \(varB) 是相等的" )
} else {
print( "\(varA) 与 \(varB) 是不相等的" )
}

以上程序执行输出结果为:

Hello, Swift! 与 Hello, World! 是不相等的

Unicode 字符串

Unicode 是一个国际标准,用于文本的编码,Swift 的 String 类型是基于 Unicode建立的。你可以循环迭代出字符串中 UTF-8 与 UTF-16 的编码,实例如下:

import Cocoa

var unicodeString = "菜鸟教程"

print("UTF-8 编码: ")
for code in unicodeString.utf8 {
print("\(code) ")
}

print("\n")

print("UTF-16 编码: ")
for code in unicodeString.utf16 {
print("\(code) ")
}

以上程序执行输出结果为:

UTF-8 编码: 
232
143
156
233
184
159
230
149
153
231
168
139
UTF-16 编码:
33756
40479
25945
31243

字符串函数及运算符

Swift 支持以下几种字符串函数及运算符:

序号函数/运算符 & 描述
1

isEmpty

判断字符串是否为空,返回布尔值

2

hasPrefix(prefix: String)

检查字符串是否拥有特定前缀

3

hasSuffix(suffix: String)

检查字符串是否拥有特定后缀。

4

Int(String)

转换字符串数字为整型。 实例:

let myString: String = "256"
let myInt: Int? = Int(myString)

5

String.count

Swift 3 版本使用的是 String.characters.count

计算字符串的长度

6

utf8

您可以通过遍历 String 的 utf8 属性来访问它的 UTF-8 编码

7

utf16

您可以通过遍历 String 的 utf8 属性来访问它的 utf16 编码

8

unicodeScalars

您可以通过遍历String值的unicodeScalars属性来访问它的 Unicode 标量编码.

9

+

连接两个字符串,并返回一个新的字符串

10

+=

连接操作符两边的字符串并将新字符串赋值给左边的操作符变量

11

==

判断两个字符串是否相等

12

<

比较两个字符串,对两个字符串的字母逐一比较。

13

!=

比较两个字符串是否不相等。

收起阅读 »

Swift 实战技巧

iOS
Swift实战技巧从OC转战到Swift,差别还是蛮大的,本文记录了我再从OC转到Swift开发过程中遇到的一些问题,然后把我遇到的这些问题记录形成文章,大体上是一些Swift语言下面的一些技巧,希望对有需要的人有帮助OC调用方法的处理给OC调用的方法需要添加...
继续阅读 »

Swift实战技巧

从OC转战到Swift,差别还是蛮大的,本文记录了我再从OC转到Swift开发过程中遇到的一些问题,然后把我遇到的这些问题记录形成文章,大体上是一些Swift语言下面的一些技巧,希望对有需要的人有帮助

  • OC调用方法的处理

给OC调用的方法需要添加@objc标记,一般的action-target的处理方法,通知的处理方法等需要添加@objc标记

@objc func onRefresh(){
self.refreshCallback?()
}

  • 处理SEL选择子

使用方法型如 #selector(方法名称)
eg.

`#selector(self.onRefresh))`  

更加详细的介绍可以看这篇文章: http://swifter.tips/selector/

下面是使用MJRefresh给mj_headermj_footer添加回调处理函数的例子

self.mj_header.setRefreshingTarget(self, refreshingAction: #selector(self.onRefresh))
self.mj_footer.setRefreshingTarget(self, refreshingAction: #selector(self.onLoadMore))

  • try关键字的使用

可能发生异常的方法使用try?方法进行可选捕获异常

let jsonStr=try?String(contentsOfFile: jsonPath!)

  • 类对象参数和类对象的参数值

AnyClass作为方法的形参,类名称.self(modelClass.self)作为实参

    func registerCellNib(nib:UINib,modelClass:AnyClass){
self.register(nib, forCellReuseIdentifier: String(describing:modelClass.self))
}

...
self.tableView?.registerCellNib(nib: R.nib.gameCell(), modelClass: GameModel.self)

  • 线程间调用

主线程使用DispatchQueue.main,全局的子线程使用DispatchQueue.global(),方法可以使用syncasyncasyncAfter等等

下面是网络请求线程间调用的例子

let _  = URLSession.shared.dataTask(with: url, completionHandler: { [weak self] (data, response, error) in
guard let weakSelf = self else {
return
}
if error == nil {
if let json = try? JSONSerialization.jsonObject(with: data!, options: .mutableContainers) {
let data = json as! [Any]
DispatchQueue.main.async {
weakSelf.suggestions = data[1] as! [String]
if weakSelf.suggestions.count > 0 {
weakSelf.tableView.reloadData()
weakSelf.tableView.isHidden = false
} else {
weakSelf.tableView.isHidden = true
}
}
}
}
}).resume()

  • 闭包中使用weak防止循环引用的语法
URLSession.shared.dataTask(with: requestURL) {[weak self] (data, response, error) in
guard let weakSelf = self else {
return
}
weakSelf.tableView.reloadData()
}

  • 逃逸闭包和非逃逸闭包
    逃逸闭包,在方法执行完成之后才调用的闭包称为逃逸闭包,一般在方法中做异步处理耗时的任务,任务完成之后把结果使用闭包进行回调处理使用的闭包为逃逸闭包,需要显示的使用@escaping关键字修饰
    非逃逸闭包,在方法执行完成之前调用的闭包称为逃逸闭包,比如snapkit框架使用的闭包是在方法执行完成之后就已经处理完毕了
    Swift3之后闭包默认都是非逃逸(@noescape,不能显示声明),并且这种类型是不能显示使用@noescape关键字修饰的
    // 模拟网络请求,completion闭包是异步延迟处理的,所以需要添加`@escaping`进行修饰
class func fetchVideos(completion: @escaping (([Video]) -> Void)) {
DispatchQueue.global().async {
let video1 = Video.init(title: "What Does Jared Kushner Believe", channelName: "Nerdwriter1")
let video2 = Video.init(title: "Moore's Law Is Ending. So, What's Next", channelName: "Seeker")
let video3 = Video.init(title: "What Bill Gates is afraid of", channelName: "Vox")
var items = [video1, video2, video3]
items.shuffle()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.init(uptimeNanoseconds: 3000000000), execute: {
completion(items)
})
}
}

  • Notification.Name的封装处理

swift3中Notification的名字是一种特殊的Notification.Name类型,下面使用enum进行封装处理,并且创建一个NotificationCenter的扩展,处理通知消息的发送

// 定义Notification.Name枚举
enum YTNotification: String {
case scrollMenu
case didSelectMenu
case openPage
case hideBar

var stringValue: String {
return "YT" + rawValue
}

// 枚举成员返回对应的Notification.Name类型
var notificationName: NSNotification.Name {
return Notification.Name.init(stringValue)
}
}

extension NotificationCenter {
func yt_post(custom notification: YTNotification, object anObject: Any?, userInfo aUserInfo: [AnyHashable : Any]? = nil) {
self.post(name: notification.notificationName, object: anObject, userInfo: aUserInfo)
}
}

使用方法
添加通知观察者使用的是YTNotification枚举成员的notificationName返回的Notification.Name类型的值
发送消息使用的是YTNotification枚举成员

// 添加通知观察
NotificationCenter.default.addObserver(self, selector: #selector(self.changeTitle(notification:)), name: YTNotification.scrollMenu.notificationName, object: nil)

// 发送消息
NotificationCenter.default.yt_post(custom: YTNotification.scrollMenu, object: nil, userInfo: ["length": scrollIndex])

lazy惰性加载属性,只有在使用的时候才初始化变量

    // 闭包的方式
let menuTitles = ["History", "My Videos", "Notifications", "Watch Later"]
lazy var menuItems : [MenuItem] = {
var tmenuItems = [MenuItem]()
for menuTitle in menuTitles {
let menuItem = MenuItem(iconImage: UIImage.init(named: menuTitle)!, title: menuTitle)
tmenuItems.append(menuItem)
}
return tmenuItems
}()

// 普通方式,
lazy var titles = ["A", "B"]

  • 类型判断

使用is判断类型以及使用if-let和as?判断类型

// MARK:- 类型检查例子
let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]

var chemCount = 0
var mathsCount = 0
for item in sa {
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。 相当于isKindOfClass
if item is Chemistry {
chemCount += 1
} else if item is Maths {
mathsCount += 1
}
}

// 使用if-let和as?判断类型
for item in sa {
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。 相当于isKindOfClass
if let _ = item as? Chemistry {
chemCount += 1
} else if let _ = item as? Maths {
mathsCount += 1
}
}

使用switch-case和as判断类型

// Any可以表示任何类型,包括方法类型
var exampleany = [Any]()
exampleany.append(12)
exampleany.append(3.14159)
exampleany.append("Any 实例")
exampleany.append(Chemistry(physics: "固体物理", equations: "兆赫"))

// 使用switch-case和as判断类型
for item2 in exampleany {
switch item2 {
case let someInt as Int:
print("整型值为 \(someInt)")
case let someDouble as Double where someDouble > 0:
print("Pi 值为 \(someDouble)")
case let someString as String:
print("\(someString)")
case let phy as Chemistry:
print("主题 '\(phy.physics)', \(phy.equations)")
default:
print("None")
}
}

  • Swift使用KVC,执行KVC操作的变量需要添加@objc标记
class Feed: NSObject, HandyJSON  {
// 使用KVC添加@objc关键字
@objc var id = 0
var type = ""
var payload: PayLoad?
var user: PostUser?

required override init() {}
}

  • swift中CGRect类型的操作

swift中简化了CGRect类型的操作,比如有一个CGRect的类型实例为frame,以下例举了OC中对应的在swift下的语法

OCSwift
CGRectGetMaxX(frame)frame.maxX
CGRectGetMinY(frame)frame.minY
CGRectGetMidX(frame)frame.midX
CGRectGetWidth(frame)frame.width
CGRectGetHeight(frame)frame.height
CGRectContainsPoint(frame, point)frame.contains(point)
  • Swift中指针的处理

详细的介绍可以查看这篇文章:http://swifter.tips/unsafe/
下面是一个使用OC库RegexKitLite中的一个例子,block中返回值是指针类型的,需要转换为对应的swift对象类型

func composeAttrStr(text: String) -> NSAttributedString {
// 表情的规则
let emotionPattern = "\\[[0-9a-zA-Z\\u4e00-\\u9fa5]+\\]";
// @的规则
let atPattern = "@[0-9a-zA-Z\\u4e00-\\u9fa5-_]+";
// #话题#的规则
let topicPattern = "#[0-9a-zA-Z\\u4e00-\\u9fa5]+#";
// url链接的规则
let urlPattern = "\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^[:punct:]\\s]|/)))";
let pattern = "\(emotionPattern)|\(atPattern)|\(topicPattern)|\(urlPattern)"

var textParts = [TextPart]()

(text as! NSString).enumerateStringsMatched(byRegex: pattern) { (captureCount: Int, capString: UnsafePointer<NSString?>?, capRange: UnsafePointer<NSRange>?, stop: UnsafeMutablePointer<ObjCBool>?) in
let captureString = capString?.pointee as! String
let captureRange = capRange?.pointee as! NSRange

let part = TextPart()
part.text = captureString
part.isSpecial = true
part.range = captureRange
textParts.append(part)
}
// ...
}

  • 只有类才能实现的protocol 有一种场景,protocol作为delegate,需要使用weak关键字修饰的时候,需要指定delegate的类型为ptotocol型,这个ptotocol需要添加class修饰符,比如下面的这个protocol,因为类类型的对象才有引用计数,才有weak的概念,没有引用计数的struct型是没有weak概念的
/// ImageViewer和ImageCell交互使用的协议
protocol YTImageCellProtocol : class {
// Cell的点击事件,处理dismiss
func imageCell(_ imageCell: YTImageCell, didSingleTap: Bool);
}
收起阅读 »

iOS 上的 WebSocket 框架 Starscream

iOS
Starscream实现Websocket通讯1.Starscream 简介2.Starscream 使用2.1 Starscream基本使用2.2 Starscream高阶使用2.2.1 判断是否连接2.2.2 自定义头文件2.2.3 自定义HTTP方法2....
继续阅读 »

Starscream实现Websocket通讯
1.Starscream 简介
2.Starscream 使用
2.1 Starscream基本使用
2.2 Starscream高阶使用
2.2.1 判断是否连接
2.2.2 自定义头文件
2.2.3 自定义HTTP方法
2.2.4 协议
2.2.5 自签名 SSL
2.2.5.1 SSL引脚
2.2.5.2 SSL密码套件
2.2.6 压缩扩展
2.2.7 自定义队列
2.2.8 高级代理
3.Starscream 使用Demo
1.Starscream 简介

Starscream的特征:

Conforms to all of the base Autobahn test suite.
Nonblocking. Everything happens in the background, thanks to GCD.
TLS/WSS support.
Compression Extensions support (RFC 7692)
Simple concise codebase at just a few hundred LOC.
什么是websocket:
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。
在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

HTTP 第一次出现是 1991 年,它设计为一种请求/响应式的通讯机制。Web 浏览器用这种机制工作良好,用户请求 web 页,服务器返回内容。但某些时候,需要有新数据时不经过用户请求就通知用户——也就是,服务器推。
HTTP 协议无法很好地解决推模型。在 websocket 出现前,web 服务通过一系列浏览器刷新机制来实现推模型,但效率无法让人满意。
webSocket 实现了服务端推机制。新的 web 浏览器全都支持 WebSocket,这使得它的使用超级简单。通过 WebSocket 能够打开持久连接,大部分网络都能轻松处理 WebSocket 连接。
WebSocket 通常应用在某些数据经常性或频繁改变的场景。例如 Facebook 中的 web 通知、Slack 中的实时聊天、交易系统中的变化的股票价格
socket通讯过程:


集成Websocket:

开发中推荐使用Starscream框架。通过pod 方式导入:

pod 'Starscream'
1
Starscream 使用swift版本为4.2
2.Starscream 使用
2.1 Starscream基本使用
import UIKit
import Starscream
@objc public protocol DSWebSocketDelegate: NSObjectProtocol{
/**websocket 连接成功*/
optional func websocketDidConnect(sock: DSWebSocket)
/**websocket 连接失败*/
optional func websocketDidDisconnect(socket: DSWebSocket, error: NSError?)
/**websocket 接受文字信息*/
func websocketDidReceiveMessage(socket: DSWebSocket, text: String)
/ **websocket 接受二进制信息*/
optional func websocketDidReceiveData(socket: DSWebSocket, data: NSData)
}
public class DSWebSocket: NSObject,WebSocketDelegate {
var socket:WebSocket!
weak var webSocketDelegate: DSWebSocketDelegate?
//单例
class func sharedInstance() -> DSWebSocket
{
return manger
}
static let manger: DSWebSocket = {
return DSWebSocket()
}()

//MARK:- 链接服务器
func connectSever(){
socket = WebSocket(url: NSURL(string: 你的URL网址如:ws://192.168.3.209:8080/shop))
socket.delegate = self
socket.connect()
}

//发送文字消息
func sendBrandStr(brandID:String){
socket.writeString(brandID))
}
//MARK:- 关闭消息
func disconnect(){
socket.disconnect()
}

//MARK: - WebSocketDelegate
//客户端连接到服务器时,websocketDidConnect将被调用。
public func websocketDidConnect(socket: WebSocket){
debugPrint("连接成功了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidConnect!(self)
}
//客户端与服务器断开连接后,将立即调用 websocketDidDisconnect。
public func websocketDidDisconnect(socket: WebSocket, error: NSError?){
debugPrint("连接失败了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidDisconnect!(self, error: error)
}
//当客户端从连接获取一个文本框时,调用 websocketDidReceiveMessage。
//注:一般返回的都是字符串
public func websocketDidReceiveMessage(socket: WebSocket, text: String){
debugPrint("接受到消息了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidReceiveMessage!(self, text: text)
}
public func websocketDidReceiveData(socket: WebSocket, data: NSData){
debugPrint("data数据")
webSocketDelegate?.websocketDidReceiveData!(self, data: data)
}
}

编写一个pong框架
writePong方法与writePing相同,但发送一个pong控制帧。
socket.write(pong: Data()) //example on how to write a pong control frame over the socket!
1
Starscream会自动响应传入的ping 控制帧,这样你就不需要手动发送 pong。

但是,如果出于某些原因需要控制这个 prosses,你可以通过禁用 respondToPingWithPong 来关闭自动 ping 响应。

socket.respondToPingWithPong=false//Do not automaticaly respond to incoming pings with pongs.
1
当客户端从连接获得一个pong响应时,调用 websocketDidReceivePong。 你需要实现WebSocketPongDelegate协议并设置一个额外的委托,例如: socket.pongDelegate = self

funcwebsocketDidReceivePong(socket: WebSocketClient, data: Data?) {
print("Got pong! Maybe some data: (data?.count)")
}

2.2 Starscream高阶使用
2.2.1 判断是否连接
if socket.isConnected {
// do cool stuff.
}

2.2.2 自定义头文件
你可以使用自己自定义的web socket标头覆盖默认的web socket标头,如下所示:
var request = URLRequest(url: URL(string: "ws://localhost:8080/")!)
request.timeoutInterval = 5
request.setValue("someother protocols", forHTTPHeaderField: "Sec-WebSocket-Protocol")
request.setValue("14", forHTTPHeaderField: "Sec-WebSocket-Version")
request.setValue("Everything is Awesome!", forHTTPHeaderField: "My-Awesome-Header")
let socket = WebSocket(request: request)

2.2.3 自定义HTTP方法
你的服务器在连接到 web socket时可能会使用不同的HTTP方法:
var request = URLRequest(url: URL(string: "ws://localhost:8080/")!)
request.httpMethod = "POST"
request.timeoutInterval = 5
let socket = WebSocket(request: request)

2.2.4 协议
如果需要指定协议,简单地将它的添加到 init:
//chat and superchat are the example protocols here
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
socket.delegate = self
socket.connect()

2.2.5 自签名 SSL
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])

//set this if you want to ignore SSL cert validation, so a self signed SSL certificate can be used.
socket.disableSSLCertValidation = true

2.2.5.1 SSL引脚
Starscream还支持SSL固定。
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
let data = ... //load your certificate from disk
socket.security = SSLSecurity(certs: [SSLCert(data: data)], usePublicKeys: true)
//socket.security = SSLSecurity() //uses the .cer files in your app's bundle

你可以加载证书的Data 小区,否则你可以使用 SecKeyRef,如果你想要使用 public 键。 usePublicKeys bool是使用证书进行验证还是使用 public 键。 如果选择 usePublicKeys,将自动从证书中提取 public 密钥。

2.2.5.2 SSL密码套件
要使用SSL加密连接,你需要告诉小红你的服务器支持的密码套件。
socket = WebSocket(url: URL(string: "wss://localhost:8080/")!, protocols: ["chat","superchat"])

// Set enabled cipher suites to AES 256 and AES 128
socket.enabledSSLCipherSuites = [TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]

如果你不知道服务器支持哪些密码套件可以查看:SSL Labs

2.2.6 压缩扩展
Starscream支持压缩扩展( RFC 7692 )。 默认情况下,压缩是启用的,但是只有当服务器支持压缩时才会使用压缩。 你可以通过 .enableCompression 属性启用或者禁用压缩:
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!)
socket.enableCompression = false

如果应用程序正在传输已经压缩。随机或者其他uncompressable数据,则应禁用压缩。
2.2.7 自定义队列
调用委托方法时可以指定自定义队列。 默认使用 DispatchQueue.main,因此使所有委托方法调用都在主线程上运行。 重要的是要注意,所有 web socket处理都是在后台线程上完成的,只有修改队列时才更改委托方法。 实际的处理总是在后台线程上,不会暂停你的应用程序。
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
//create a custom queue
socket.callbackQueue = DispatchQueue(label: "com.vluxe.starscream.myapp")

2.2.8 高级代理
socket.advancedDelegate = self

websocketDidReceiveMessage
func websocketDidReceiveMessage(socket: WebSocketClient, text: String, response: WebSocket.WSResponse) {
print("got some text: \(text)")
print("First frame for this message arrived on \(response.firstFrame)")
}

websocketDidReceiveData
func websocketDidReceiveData(socket: WebSocketClient, data: Date, response: WebSocket.WSResponse) {
print("got some data it long: \(data.count)")
print("A total of \(response.frameCount) frames were used to send this data")
}

websocketHttpUpgrade
当发送HTTP升级请求后,会返回下面回调

func websocketHttpUpgrade(socket: WebSocketClient, request: CFHTTPMessage) {
print("the http request was sent we can check the raw http if we need to")
}

func websocketHttpUpgrade(socket: WebSocketClient, response: CFHTTPMessage) {
print("the http response has returned.")
————————————————

原文链接:https://blog.csdn.net/kyl282889543/article/details/100655005

收起阅读 »

iOS 15-适配要点

iOS
增加UISheetPresentationController,通过它可以控制 Modal 出来的 UIViewController 的显示大小,且可以通过拖拽手势在不同大小之间进行切换。只需要在跳转的目标 UIViewController 做如下处理:if ...
继续阅读 »

  1. 增加UISheetPresentationController,通过它可以控制 Modal 出来的 UIViewController 的显示大小,且可以通过拖拽手势在不同大小之间进行切换。只需要在跳转的目标 UIViewController 做如下处理:

    if let presentationController = presentationController as? UISheetPresentationController {
    // 显示时支持的尺寸
    presentationController.detents = [.medium(), .large()]
    // 显示一个指示器表示可以拖拽调整大小
    presentationController.prefersGrabberVisible = true
    }
  2. UIButton支持更多配置。UIButton.Configuration是一个新的结构体,它指定按钮及其内容的外观和行为。它有许多与按钮外观和内容相关的属性,如cornerStyle、baseForegroundColor、baseBackgroundColor、buttonSize、title、image、subtitle、titlePadding、imagePadding、contentInsets、imagePlacement等。

    // Plain
    let plain = UIButton(configuration: .plain(), primaryAction: nil)
    plain.setTitle("Plain", for: .normal)
    // Gray
    let gray = UIButton(configuration: .gray(), primaryAction: nil)
    gray.setTitle("Gray", for: .normal)
    // Tinted
    let tinted = UIButton(configuration: .tinted(), primaryAction: nil)
    tinted.setTitle("Tinted", for: .normal)
    // Filled
    let filled = UIButton(configuration: .filled(), primaryAction: nil)
    filled.setTitle("Filled", for: .normal)

    Snipaste_2021-07-11_15-26-55.png16259886795922.png

  3. 推出CLLocationButton用于一次性定位授权,该内容内置于CoreLocationUI模块,但如果需要获取定位的详细信息仍然需要借助于CoreLocation

    let locationButton = CLLocationButton()
    // 文字
    locationButton.label = .currentLocation
    locationButton.fontSize = 20
    // 图标
    locationButton.icon = .arrowFilled
    // 圆角
    locationButton.cornerRadius = 10
    // tint
    locationButton.tintColor = UIColor.systemPink
    // 背景色
    locationButton.backgroundColor = UIColor.systemGreen
    // 点击事件,应该在在其中发起定位请求
    locationButton.addTarget(self, action: #selector(getCurrentLocation), for: .touchUpInside)
  4. URLSession 推出支持 async/await 的 API,包括获取数据、上传与下载。

    // 加载数据
    let (data, response) = try await URLSession.shared.data(from: url)
    // 下载
    let (localURL, _) = try await session.download(from: url)
    // 上传
    let (_, response) = try await session.upload(for: request, from: data)

  5. 系统图片支持多个层,支持多种渲染模式。

    // hierarchicalColor:多层渲染,透明度不同
    let config = UIImage.SymbolConfiguration(hierarchicalColor: .systemRed)
    let image = UIImage(systemName: "square.stack.3d.down.right.fill", withConfiguration: config)
    // paletteColors:多层渲染,设置不同风格
    let config2 = UIImage.SymbolConfiguration(paletteColors: [.systemRed, .systemGreen, .systemBlue])
    let image2 = UIImage(systemName: "person.3.sequence.fill", withConfiguration: config2)
  6. UINavigationBar、UIToolbar 和 UITabBar 设置颜色,需要使用 UIBarAppearance APIs。

    // UINavigationBar
    let navigationBarAppearance = UINavigationBarAppearance()
    navigationBarAppearance.backgroundColor = .red
    navigationController?.navigationBar.scrollEdgeAppearance = navigationBarAppearance
    navigationController?.navigationBar.standardAppearance = navigationBarAppearance
    // UIToolbar
    let toolBarAppearance = UIToolbarAppearance()
    toolBarAppearance.backgroundColor = .blue
    navigationController?.toolbar.scrollEdgeAppearance = toolBarAppearance
    navigationController?.toolbar.standardAppearance = toolBarAppearance
    // UITabBar
    let tabBarAppearance = UITabBarAppearance()
    toolBarAppearance.backgroundColor = .purple
    tabBarController?.tabBar.scrollEdgeAppearance = tabBarAppearance
    tabBarController?.tabBar.standardAppearance = tabBarAppearance
  7. UITableView 新增了属性 sectionHeaderTopPadding,会给每一个section 的 header 增加一个默认高度。

    tableView.sectionHeaderTopPadding = 0
  8. UIImage 新增了几个调整尺寸的方法。

    // preparingThumbnail
    UIImage(named: "sv.png")?.preparingThumbnail(of: CGSize(width: 200, height: 100))
    // prepareThumbnail,闭包中直接获取调整后的UIImage
    UIImage(named: "sv.png")?.prepareThumbnail(of: CGSize(width: 200, height: 100)) { image in
    // 需要回到主线程更新UI
    }
    // byPreparingThumbnail
    await UIImage(named: "sv.png")?.byPreparingThumbnail(ofSize: CGSize(width: 100, height: 100))

文中代码已在 Xcode 13 Beta3 中测试通过, 案例源代码下载地址

收起阅读 »

iOS Runtime (四)Runtime的消息机制

iOS
引言 iOS的消息转发机制,在我们开发中有时候忘记实现某个声明的方法,从而在运行过程中调用该方法出现崩溃, 当然这类问题是可以解决的,在当前对象或者父类对象中添加对象的方法实现,再重新运行,调用该方法就能解决这个问题,又或者在我们运行的时候动态的去添加接收者中...
继续阅读 »

引言


iOS的消息转发机制,在我们开发中有时候忘记实现某个声明的方法,从而在运行过程中调用该方法出现崩溃,


当然这类问题是可以解决的,在当前对象或者父类对象中添加对象的方法实现,再重新运行,调用该方法就能解决这个问题,又或者在我们运行的时候动态的去添加接收者中未知方法实现,这就是这篇重点要学习的内容。


错误异常实例


创建Game对象代码

#import <Foundation/Foundation.h>
@interface Game : NSObject
- (void)Play;
- (void)DoThings:(NSString *)Str Num:(NSInteger)num;
@end

#import "Game.h"
@implementation Game
- (void)Play{
NSLog(@"the game is play");
}
@end

调用该对象中没有实现的方法 DoThings: Num:


Game *game = [[Game alloc]init];
[game DoThings:@"wodenidetade" Num:10];

当我们运行的时候会报当调用的对象的方法不存在,即便是消息转发过后还是不存在的时候,就会抛出这个异常


解决方案


第一种方式

遇到这种情况通常第一个想法就是在该对象或继承树中的实现文件中添加该方法并实现,这种形式,就是你必须要去实现方法,需要开发者主动去写代码。


第二种方式

消息转发在运行时:


1.动态方法解析

+(BOOL)resolveInstanceMethod:(SEL)sel 实例方法解析
+(BOOL)resolveClassMethod:(SEL)sel 类方法解析

当运用消息转发运行时,根据调用的方法类型调用这两个方法其中一个,返回值BOOL类型,告诉系统该消息是否被处理,YES处理 NO 未处理



  • resolveInstanceMethod实例方法调用

  • resolveClassMethod类方法调用


这样的作用是: 当接受者接受到的消息方法并没有找到的情况下,系统会调用该函数,给予这个对象一次动态添加该消息方法实现的机会,如果该对象动态的添加了这个方法的实现,就返回YES,告诉系统这个消息我已经处理完毕。再次运行该方法。


注意:

当这个对象在实现了resolveInstanceMethod,resolveClassMethod两个方法,并没有对该对象消息进行处理,那么该方法会被调用两次:


一次是没有找到该方法需要对象解析处理;第二次是告诉系统我处理完成需要再次调用该方法但实际上并没有处理完成,所以会调用第二次该方法崩溃


2.后备接收者对象

-(id)forwardingTargetForSelector:(SEL)aSelector

在消息转发第一次方法解析中没有处理方法,并告诉系统本对象无法处理,需另寻办法,那么系统会给予另外一个办法,就是让别的对象B来处理该问题,如果对象B能够处理该消息,那么该消息转发结束。


将未知SEL作为参数传入,寻找另外对象处理,如果可以处理,返回该对象


3.以其他形式实现该消息方法

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation

当我们在前面两个步骤都没有处理该未知SEL时,就会到第三个步骤,上述两个方法是最后寻找IML的机会



  • 将未知SEL作为参数传入methodSignatureForSelector,在该方法中处理该消息,一旦能够处理,返回方法签名(自由的修改方法签名,apple签名),让后续forwardInvocation来进行处理



  • forwardInvocation中我们可以做很多的操作,这个方法比forwardingTargetForSelector更灵活



    • 也可以做到像forwardingTargetForSelector一样的结果,不同的是一个是让别的对象去处理,后者是直接切换调用目标,也就是该方法的Target

    • 我们也可以修改该方法的SEL,就是重新替换一个新的SEL

    • ....




4.直到最后未处理,抛出异常

-(void)doesNotRecognizeSelector:(SEL)aSelector

作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。


虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。


流程图


代码实现

/*
* 第一步 实例方法专用 方法解析
**/
+ (BOOL)resolveInstanceMethod:(SEL)sel{

NSLog(@"%@",NSStringFromSelector(sel));

if(sel == @selector(DoThings:Num:)){
class_addMethod([self class], sel, (IMP)MyMethodIMP, "v@:");
return YES;
}

return [super resolveInstanceMethod:sel];
}

/*
* 第二步 如果第一步未处理,那么让别的对象去处理这个方法
**/
-(id)forwardingTargetForSelector:(SEL)aSelector{
if([NSStringFromSelector(aSelector) isEqualToString:@"DoThings:Num:"]){
return [[Tools alloc]init];
}
return [super forwardingTargetForSelector:aSelector];
}

/*
* 第三步 如果前两步未处理,这是最后处理的机会将目标函数以其他形式执行
**/
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSString *SelStr = NSStringFromSelector(aSelector);
if([SelStr isEqualToString:@"DoThings:Num:"]){
[NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
//改变消息接受者对象
[anInvocation invokeWithTarget:[[Tools alloc]init]];

//改变消息的SEL
anInvocation.selector = @selector(flyGame);
[anInvocation invokeWithTarget:self];
}

- (void)flyGame{
NSLog(@"我要飞翔追逐梦想!");
}

/*
* 作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。
* 虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。
*
***/
- (void)doesNotRecognizeSelector:(SEL)aSelector{

}


作者:响彻天堂
链接:https://www.jianshu.com/p/019bce1e6253

上一篇链接:https://www.imgeek.org/article/825358865

收起阅读 »

iOS Runtime (三)Runtime的消息机制

iOS
消息发送 消息机制就是向接收者发送消息,并带有参数,根据接收者对象的数据结构,找到相关发放实现,最后达到这个消息的目的。 objc_msgSend是Runtime的核心,Objective-C中调用对象方法就是消息传递。 objc_msgSend并不是直接调用...
继续阅读 »

消息发送


消息机制就是向接收者发送消息,并带有参数,根据接收者对象的数据结构,找到相关发放实现,最后达到这个消息的目的。


objc_msgSendRuntime的核心,Objective-C中调用对象方法就是消息传递。

objc_msgSend并不是直接调用方法实现(IMP)而是发送消息,让类的结构体去动态查到方法实现,所以在为查找到方法实现之前我们可以动态的去修改这个方法的实现


在Object-C中,我们其实可以直接调用C的代码也就是Runtime的C语言代码,需要添加message.h头文件。

#import 
#import

编写Runtime的时候会遇到没有提示的尴尬,那是因为在Xcode5.0以后的版本,Apple不建议我们写比较底层的代码,So,在target->info搜索msgYES改成NO,然后可以尽情的使用Runtime代码


Objc-msgSend所做的事情



1,找到方法的实现,由于通过单独的类以不同方式创建相同的方法,因此这个方法的实现的确定取决于接收消息的类对象,也即是说多个实例类对戏那个可以创建同样的方法,每个实例对象中的该方法都是独立存在的。

2,调用该方法实现,将接收消息类指针,以及该方法的参数传递给这个类。

3,最后将过程的返回值作为自己的返回值传递



消息传递的发送过程和关键要素



1,指向superclass的指针。消息发送给对象时,消息传递函数遵循对象的isa指针指向类结构的指针,在该结构中它查询结构体变量methodLists中的方法SEL(方法选择器).

2,会有一个SEL方法实现的地址(这个地址是基于独立的类)关联的表

    当创建一个新的对象时,分配内存,初始化变量,对象变量中的第一个是指向该类结构的指针,这个名字为isa的指针能让对象可以访问它的类,并通过该类访问它继承的所有类

    isa指针是对象使用Objective-C运行时系统所必需的,在结构中定义的任何字段中,对象需要与结构体objc_object(objc/objc.h中的定义)"等效",日常开发中很少有创见自己的根对象的这种情况,一般从NSObject或者NSProxy继承的对象会自动拥有isa变量

    如在isa指向的类结构中找不到SEL(方法选择器),Objc_msgSend会跟随指向Supercalss(父类)指针并再次尝试查找该SEL。如连续失败直到NSObject类,它的superclass也就是它自己本身。一旦找到SEL,该函数就会调用methodLists的方法并将接收对象的指针传给它。



加速消息发送




  • 1,有的时候在一个类会有继承关系,Objective-C中大部分对象都是继承于NSObject、自己自定义类,在这种继承体系当中有很多的方法,这些方法有可能不会用到,在向类发送消息的时候,去methodLists中查找无疑会拖慢程序的运行速度,所以Apple在开发的时候加入了缓存cache的概念,也就是缓存。

  • 2,在每个类中都会有一个单独的缓存cache,它可以包含继承过来的方法SEL以及自定义的SEL,在搜索methodLists之前,消息传递程序会检查接受者对象的告诉缓存cache,如果找到,就不会在去搜索庞大的methodLists列表,一旦在缓存当中存在你需要的SEL,这样以后也就比函数调用稍微慢一点。

  • 3,理论上cache缓存的是一些会再次调用的SEL,当写的程序预热足够时间,那么所有发送过的SEL都会在cache中找到

  • 4,cache会动态增长,容纳新的消息,知道程序中所有调用的SEL运行一遍为止

  • 5,原理时:好比是通常小圈子找人总比大圈子找人要快



Runtime的发送消息隐藏的参数


每次当我们向一个对象发送消息时,也就是Objective-C调用方法的时候,传递的所有参数,还包括两个隐藏的参数:



接收者对象

调用的方法SEL _cmd



这两个参数没有在定义中声明,而是在编译代码时插入方法实现的。

/*
* _cmd 就是你调用的方法的SEL
**/
NSLog(@"%@",NSStringFromSelector(_cmd));

规避动态绑定的方法,获取方法地址


代码正常编译的时候,需要使用消息传递Objc-msgSend才能找到方法的IMP中间就有了这个消息传递的过程。

有时候我们不希望调用消息传递的,或者节省消息传递的开销,就需要我们拿到方法的IMP,代码直接使用IMP中的方法。

下面的示例显示了如何调用实现setFilled:方法的过程:

@interface ViewController (){
NSInteger num;
}
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[self methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(self, @selector(setFilled:), YES);
}

- (void)setFilled:(NSInteger)number{
NSLog(@"%ld",++num);
}

传递给方法实现的前两个参数是: 接收对象(self)方法选择器对象(SEL),这些参数隐藏在方法的语法中,方法作为函数调用时必须使它显式化。


使用methodForSelector绕过动态绑定可以节省消息传递的大部分时间,在特定的消息多次重复的情况下才会节省的更加显著


methodForSelector是由Cocoa运行时系统提供,它并不是Objective-C语言本身的一个特性


3人点赞


链接:https://www.jianshu.com/p/04760fc66276
收起阅读 »

iOS Runtime (二) Runtime底层详解

iOS
Runtime的定义? 为了更好的认识类是怎么工作的,我们将要将一段Object-C的代码用clang看下底层的C/C++的写法。 在Object-C中的NSObject对象中@interface NSObject <NSObject> { ...
继续阅读 »

Runtime的定义?


为了更好的认识类是怎么工作的,我们将要将一段Object-C的代码用clang看下底层的C/C++的写法


在Object-C中的NSObject对象中

@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

由此可见可以看到id是指向objc_object的一个指针

objc_class结构体中的定义如下:

struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

runtime使用当中,我们经常需要用到的字段,它们的定义


isaClass对象,指向objc_class结构体的指针,也就是这个Class的MetaClass(元类)

类的实例对象的 isa 指向该类;该类的 isa 指向该类的MetaClassMetaCalssisa对象指向RootMetaCalss


super_class Class对象指向父类对象



  • 如果该类的对象已经是RootClass,那么这个super_class指向nil


  • MetaCalssSuperClass指向父类的MetaCalss


  • MetaCalssRootMetaCalss,那么该MetaClassSuperClass指向该对象的RootClass


ivars 类中所有 属性的列表,使用场景:我们在字典转换成模型的时候需要用到这个列表找到属性的名称,去取字典中的值,KVC赋值,或者直接Runtime赋值


methodLists 类中 所有的方法的列表,使用场景:如在程序中写好方法,通过外部获取到方法名称字符串,然后通过这个字符串得到方法,从而达到外部控制App已知方法。


cache 主要用于 缓存常用方法列表,每个类中有很多方法,我平时不用的方法也会在里面,每次运行一个方法,都要去 methodLists遍历得到方法,如果类的方法不多还行,但是基本的类中都会有很多方法,这样势必会影响程序的运行效率,所以 cache在这里就会被用上,当我们使用这个类的方法时先判断 cache是否为空,为空从 methodLists找到调用,并保存到 cache,不为空先从 cache中找方法,如果找不到在去 methodLists,这样提高了程序方法的运行效率。


protocols故名思义,这个类中都遵守了 哪些协议,使用场景:判断类是否遵守了某个协议上


在介绍runtime的时候,需要了解下类的本质。


类底层代码、类的本质?


为了更好的认识类是怎么工作的,我们将要将一段Object-C的代码用clang看下底层的C/C++的写法

typedef enum : NSUInteger {
ThisRPGGame = 0,
ThisActionGame = 1,
ThisBattleFlagGame = 2,
} ThisGameType;

@interface Game : NSObject
@property (copy,nonatomic)NSString *Name;
@property (assign,nonatomic)ThisGameType Type;
@end

@implementation Game
@synthesize Name,Type;

- (void)GiveThisGameName:(NSString *)name{
Name = name;
}

- (void)GiveThisGameType:(ThisGameType)type{
Type = type;
}
@end

使用命令,在当前文件夹中会出现Game.cpp的文件

# clang -rewrite-objc Game.m

由于生成的文件很庞大,可以仔细去研读,受益匪浅

/*
* 顾名思义存放property的结构体
* 当我们使用perproty的时候,会生成这样一个结构体
* 具体存储的数据为
* 实际内容:"Name","T@\"NSString\",C,N,VName"
* 原型:@property (copy,nonatomic)NSString *Name;
* 这个具体是怎么实现的,我会在后面继续深入研究,本文主要来理解runtime的理解
**/
struct _prop_t {
const char *name; //名字
const char *attributes; //属性
};

/*
*类中方法的结构体,cmd和imp的关系是一一对应的关系
*创建对象生成isa指针,指向这个对象的结构体时
*同时生成了一个表"Dispatch table"通过这个_cmd的编号找到对应方法
*使用场景:
*例如方法交换,方法判断。。。
**/
struct _objc_method {
struct objc_selector * _cmd; //SEL 对应着OC中的@selector()
const char *method_type; //方法的类型
void *_imp; //方法的地址
};


/*
* method_list_t 结构体:
* 原型:
* - (void)GiveThisGameName:(NSString *)name;
* 实际存储的方式:
* {(struct objc_selector *)"GiveThisGameName:", "v24@0:8@16", (void *)_I_Game_GiveThisGameName_}
* 其主要目的是存储一个数组,基本的数据类型是 _objc_method
* 扩展:当然这其中有你的属性,自动生成的setter、getter方法
**/

static struct _method_list_t {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[6];
}

/*
* 表示这个类中所遵守的协议对象
* 使用场景:
* 判断类是否遵守这个协议,从而动态添加、重写、交换某些方法,来达到某些目的
*
**/

struct _protocol_t {
void * isa; // NULL
const char *protocol_name;
const struct _protocol_list_t * protocol_list; // super protocols
const struct method_list_t *instance_methods; // 实例方法
const struct method_list_t *class_methods; //类方法
const struct method_list_t *optionalInstanceMethods; //可选的实例方法
const struct method_list_t *optionalClassMethods; //可选的类方法
const struct _prop_list_t * properties; //属性列表
const unsigned int size; // sizeof(struct _protocol_t)
const unsigned int flags; // = 0
const char ** extendedMethodTypes; //扩展的方法类型
};

/*
* 类的变量的结构体
* 原型:
* NSString *Name;
* 存储内容:
* {(unsigned long int *)&OBJC_IVAR_$_Game$Name, "Name", "@\"NSString\"", 3, 8}
* 根据存储内容可以大概了解这些属性的工作内容
**/
struct _ivar_t {
unsigned long int *offset; // pointer to ivar offset location
const char *name; //名字
const char *type; //属于什么变量
unsigned int alignment; //未知
unsigned int size; //大小
};


/*
* 这个就是类中的各种方法、属性、等等信息
* 底层也是一个结构体
* 名称、方法列表、协议列表、变量列表、layout、properties。。
*
**/
struct _class_ro_t {
unsigned int flags;
unsigned int instanceStart;
unsigned int instanceSize;
unsigned int reserved;
const unsigned char *ivarLayout; //布局
const char *name; //名字
const struct _method_list_t *baseMethods;//方法列表
const struct _objc_protocol_list *baseProtocols; //协议列表
const struct _ivar_list_t *ivars; //变量列表
const unsigned char *weakIvarLayout; //弱引用布局
const struct _prop_list_t *properties; //属性列表
};

/*
* 类本身
* oc在创建类的时候都会创建一个 _class_t的结构体
* 我的理解是在runtime中的object-class结构体在底层就会变成_class_t结构体
**/
struct _class_t {
struct _class_t *isa; //元类的指针
struct _class_t *superclass; //父类的指针
void *cache; //缓存
void *vtable; //表信息、未知
struct _class_ro_t *ro; //这个就是类中的各种方法、属性、等等信息
};


/*
* 类扩展的结构体
* 在OC中写的分类
**/
struct _category_t {
const char *name; //名称
struct _class_t *cls; //这个是哪个类的扩展
const struct _method_list_t *instance_methods; //实例方法列表
const struct _method_list_t *class_methods; //类方法列表
const struct _protocol_list_t *protocols; //协议列表
const struct _prop_list_t *properties; //属性列表
};

类就是多个结构体组合的一个集合体,类中的行为、习惯、属性抽象,按照机器能懂的数据存储到我们底层的结构体当中,在我们需要使用的时候直接获取使用。


那么就开始研究一下,类是如何使用,类的基本使用过程以及过程中runtime所做的事情。


类底层是如何调用方法?


了解了类的组成,那么类是通过什么样的形式去获取方法属性并得到应用?

在Object-C开发中我们经常会说到,对象调用方法,其本质就是想这个对象发送消息,为什么会有这么一说?下面我们来验证一下。

Object-C代码

int main(int argc, char * argv[]) {

Game *game = [Game alloc];
[game init];
[game Play];
return 0;
}

底层代码的实现

int main(int argc, char * argv[]) {

Game *game = ((Game *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Game"), sel_registerName("alloc"));
game = ((Game *(*)(id, SEL))(void *)objc_msgSend)((id)game, sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)game, sel_registerName("Play"));
return 0;
}

代码中使用了



objc_msgSend 消息发送

objc_getClass 获取对象

sel_registerName 获取方法的SEL



因为目前重点是objc_msgSend,其他的Runtime的方法会在后面继续一一道来, So 一个对象调用其方法,在`=Object-C中就是向这个对象发送一条消息,消息的格式

objc_msgSend("对象","SEL","参数"...)
objc_msgSend( id self, SEL op, ... )


收起阅读 »

iOS Runtime (一) 什么是Runtime?

iOS
一:Runtime是什么? 1,运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时。 2,Runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。 3,平时编写的OC代码,在程序运行过程中,其实最终会...
继续阅读 »

一:Runtime是什么?



1,运行时(Runtime)是指将数据类型的确定由编译时推迟到了运行时

2,Runtime是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。

3,平时编写的OC代码,在程序运行过程中,其实最终会转换成Runtime的C语言代码,Runtime是Object-C的幕后工作者

4,Object-C需要Runtime来创建对象,进行消息发送转发



二:Runtime用在哪些地方?



1,在程序运行过程中,动态的创建类,动态添加、修改这个类的属性方法

2,遍历一个类中所有的成员变量、属性、以及所有方法

2,消息传递、转发



三:Runtime具体应用?



1,创建类,给类添加属性、方法

2,方法交换

3,获取对象的属性、私有属性

4,字典转换模型

5,KVC、KVO

6,归档(编码、解码)

7,NSClassFromString class<->字符串

8,block



常见用法


1,使用objc_allocateClassPair可在运行时创建新的类

2,使用class_addMethodclass_addIvar可向类中增加方法实例变量

3,最后使用objc_registerClassPair注册后,就可以使用此类了。

这体现了OC作为运行时语言的强大之一:在代码运行中动态创建并添加方法变量


a.使用objc_allocateClassPair创建一个类Class

const char * className = "Calculator";
Class kclass = objc_getClass(className);
if (!kclass)
{
Class superClass = [NSObject class];
kclass = objc_allocateClassPair(superClass, className, 0);
}


b.使用class_addIvar添加一个成员变量

  NSUInteger size;
NSUInteger alignment;
NSGetSizeAndAlignment("*", &size, &alignment);
class_addIvar(kclass, "expression", size, alignment, "*");

注:



1.type定义参考

2."*"星号代表字符( )iOS字符为4位,并采用4位对齐kclass



c.使用class_addMethod添加成员方法

    class_addMethod(kclass, @selector(setExpressionFormula:), (IMP)setExpressionFormula, "v@:@");
class_addMethod(kclass, @selector(getExpressionFormula), (IMP)getExpressionFormula, "@@:");

static void setExpressionFormula(id self, SEL cmd, id value)
{
NSLog(@"call setExpressionFormula");
}

static void getExpressionFormula(id self, SEL cmd)
{
NSLog(@"call getExpressionFormula");
}

注:



1.type定义参考

2."v@:@",解释v-返回值void类型,@-self指针id类型,:-SEL指针SEL类型,@-函数第一个参数为id类型。

3."@@:",解释@-返回值id类型,@-self指针id类型,:-SEL指针SEL类型。



d.注册到运行时环境

objc_registerClassPair(kclass);

e.实例化类

id instance = [[kclass alloc] init];

f.给变量赋值

object_setInstanceVariable(instance, "expression", "1+1"); 

g.获取变量值

void * value = NULL;
object_getInstanceVariable(instance, "expression", &value);

h.调用函数

[instance performSelector:@selector(getExpressionFormula)];

说明:objc_allocateClassPair函数的作用是创建一个新类newClass及其元类,三个参数依次为newClass的父类newClass的名称,第三个参数通常为0。然后可向newClass中添加变量及方法,注意若要添加类方法,需用objc_getClass(newClass)获取元类,然后向元类中添加类方法。接下来必须把newClass注册到运行时系统,否则系统是不能识别这个的。



链接:https://www.jianshu.com/p/e7586587ccf7
收起阅读 »

iOS swiftUI 创建 macos图片 1.1

第六节 组合列表视图与过滤器视图创建一个组列过滤器和列表的视图。为过滤器提供新的状态信息,同时绑定地标选择到主视图的父视图上。步骤1 项目中添加一个新的SwiftUI视图,命名为NavigationPrimary.swift。步骤2 声明一...
继续阅读 »

第六节 组合列表视图与过滤器视图

创建一个组列过滤器和列表的视图。为过滤器提供新的状态信息,同时绑定地标选择到主视图的父视图上。

section 6

步骤1 项目中添加一个新的SwiftUI视图,命名为NavigationPrimary.swift

步骤2 声明一个FilterType状态。这个状态会被绑定到过滤器和列表视图中。

section 6 step2

步骤3 添加过滤器视图并绑定FilterType状态。现在预览是失败的,因为过滤器依赖环境中的用户数据,下一步会处理这块儿。

section 6 step3

步骤4 注入用户数据对角到环境中。导航主视图是不直接需要用户数据的,但它的子视图需要。为了可以进行预览,把用户数据作为环境对象注入到导航主视图中。

section 6 step4

步骤5 添加一个绑定到当前选中地标的关系。

步骤6 添加地标列表视图,并把它绑定到选中的地标和过滤器状态上。预览视图中选中第二个选项,因为输入数据是landmarkData[1]作为用户选中的地标输入数据。

section 6 step6

步骤7 限制导航视图的宽度,防止用户让它变的太宽或太窄。

section 6 step7

第七节 复用CircleImage

有时只需要经过稍微修改,就可以跨平台复用一些视图。当构建macOS平台的地标详情页视图时,会复用iOS版地标应用中的CircleImage视图。为了适配macOS平台下的不同布局要求,会添加一个参数来控件阴影半径。

section 7

步骤1 在项目导航栏中选中Landmarks -> Supporting Views并选择CircleImage.swift文件。

section 7 step1

步骤2 把CircleImage.swift文件添加到时MacLandmarks编译目标。

section 7 step2

步骤3 在CircleImage.swift文件中,修改结构体,使用新的阴影半径参数。通过给新参数提供默认值,可以确保iOSwatchOS平台的应用都能与原来保持一致,同时还能在macOS平台上使用。

section 7 step3

第八节 为macOS扩展MapView

类似于CircleImage,这里要在macOS上复用MapView。然而,MapView要做更大的改动,因为MapView使用的是MapKit依赖于UIKit框架。在macOS平台上使用MapKit需要依赖于AppKit框架,所以需要添加编译器指令,让编译过程在macOS目标上进行正确的依赖。

section 8

步骤1 在项目导航器中,选择Landmarks -> Supporting Views,选中MapView.swift文件。

步骤2 把MapView.swift文件添加到MacLandmarks编译目标上。此时Xcode会报错,因为MapView使用了UIViewRepresentable协议,这个协议在macOS SDK里是没有的。下面的步骤中,会使用NSViewRepresentable协议来扩展MapView,让它能在macOS平台上使用。

section 8 step2

步骤3 插入条件编译指令,用来指定特定平台行为。用条件编译的两个条件分支把协议UIViewRepresentableNSViewRepresentable协议的遵循分开。

步骤4 使用条件编译,把在iOS平台上要实现的协议UIViewRepresentable及协议方法makeUIViewupdateUIView放在MapView的扩展实现中,这样就把MapKit的平台依赖性解耦了。

步骤5 添加在macOS平台上的NSViewRepresentable协议遵循。与UIViewRepresentable协议一样,NSViewRepresentable协议的实现也可以使用主类中的方法。

section 8 step5

第九节 构建详情视图

详情视图展示用户选中的地标信息。创建一个类似iOS平台地标应用的地标详情视图,不同之处在于,macOS平台有不同的数据表示方法,这就需要针对macOS平台对详情视图作一些裁剪,复用一些之前调整过的视图。

section 9

步骤1 项目中添加一个新的视图,命名为NavigationDetail.swift,并添加一个landmark属性。初始化详情视图时会使用landmark属性来指定详情页展示的地标信息。

步骤2 在NavigationDetail.swift内部创建一个滚动视图,滚动视图中包含一个VStackVStack中又包含一个HStack,HStack中展示关于地标的图片CircleImageText地标文本信息。通过设置VStack的最大最小宽度,确保展示的内容保持一定的宽度,以适合用户阅读。跨平台复用视图是非常方便的,定制一下CircleImage视图,以满足当前的布局要求。

section 9 step2

步骤3 把输入的图片变为可缩放,并设置图片按视图大小展示,这样可以让CircleImage视图的大小与Text块文本的大小看上去比较匹配。这种修改方法不需要调整CircleImage的内部实现。

section 9 step3

步骤4 调整阴影半径,以匹配更小的图片。这个修改依赖之前对CircleImage视图所作的参数化改造。

section 9 step4

用户使用按钮标记一个地标是否被收藏。为了让这个动作生效,需要访问用户数据中的对应变量。

步骤5 添加用户数据对应的环境对象,并创建一个基于当前选中地标的存储属性landmarkIndex

section 9 step5

步骤6 添加一个按钮,水平方式对齐地标名称,使用星星图标,并在点击时可以切换用户对这个地标的收藏状态。当用户修改地标数据时,在用户数据中查找被修改的地标数据,并用最新的数据更新原来的数据,让数据保持最新状态。

section 9 step6

步骤7 在分割区载下再添加一个地标的信息,对应数据中新增的字段description

section 9 step7

预览视图中标题块会被挤到左边,因为描述内容比较多,把水平方向的宽度撑满了。

步骤8 在详情视图顶部插入地图,调整地图的偏移,让地图和其它内容有一定区域的重叠。地图占满视图全宽,因此会把详情文本挤到预览视图的底部看不到的位置,但它实际上是存在的。

section 9 step8

步骤9 导入MapKit并添加一个Open in Maps的按钮,当按钮被点击时,打开地图应用并定位到地标位置。

section 9 step9

步骤10 把Open in Maps按钮叠放在地图的右下角。

section 9 step10

第十节 把主视图和详情视图组合起来

已经构建了所有的视图元素,把主视图和详情视图组合起来,共同构成ContentView

section 10

步骤1 在MacLandmarks文件夹中,选择ContentView.swift文件。

步骤2 为选中的地标设置对应的属性selectedLandmark,并用@State属性标识为状态属性。使用可选类型定义selectedLandmark,可以不用为它设置默认值。因此,无论是预览视图还是应用初始化时,都可以不需要用户选中地标进行渲染。

步骤3 把用户数据作为环境对象注入。ContentView本身不会直接依赖用户数据,但它的子视图需要访问用户数据。对于预览视图来说,为了正常预览和编译成功,ContentView需要获取用户数据。

section 10 step3

步骤4 在AppDelegate.swift中,为ContentView注入环境对象,这样可以让它的子视图访问到用户数据,应用也可以编译成功。

section 10 step4

步骤5 在ContentView中添加NavigationView作为顶级视图,并设置一个最小尺寸。

section 10 step5

步骤6 添加主视图,展示选中的地标。当用户选中地标列表中的某个地标时,被选中的地标数据就会被赋值到selectedLandmark属性上。

section 10 step6

步骤7 添加详情视图,详情视图不接收可选地标数据, 因些传入详情视图的地标数据需要确保不为空。用户选中地标前,地标详情视图不会渲染,这就是为会预览视图没有任何改变,还是和之前一样。

section 10 step7

步骤8 构建并运行应用。尝试改变过滤器的设置,或者点击详情页中的收藏按钮,观察视图内容的变化。

section 10 step8


收起阅读 »

iOS swiftUI 创建 macos图片 1.0

创建MACOS应用创建了watchOS平台的Landmarks应用后,下一步就是把Landmarks带到MacOS平台上。运用之前学到的所有知识,完成在iOS、watchOS及macOS的全平台应用。在项目工程中添加macOS编译目标,复用在iOS应用中的代码...
继续阅读 »

创建MACOS应用

创建了watchOS平台的Landmarks应用后,下一步就是把Landmarks带到MacOS平台上。运用之前学到的所有知识,完成在iOSwatchOSmacOS的全平台应用。

在项目工程中添加macOS编译目标,复用在iOS应用中的代码和资源,使用SwiftUI创建macOS平台上的列表和详情视图。

按照步骤来编译工程,或者下载工程查看完成后的代码。


第一节 项目中添加macOS编译目标

项目中添加macOS编译目标,Xcode会自动添加一个文件组与一些初始文件,还会生成一个编译运行方案。

section 1

步骤1 选择File->New->Target,模板选择页面出现后,选择macOS选项卡,选中App模板并点击Next。这个模板会添加一个新的macOS编译目标到项目里。

section 1 step1

步骤2 在信息表中,输入MacLandmarks作为项目的名称,设置编程语言为Swift,界面构建方法为SwiftUI,然后点击Finish

section 1 step2

步骤3 设置运行方案为MacLandmarks -> My Mac。这样就可以编译并运行macOS应用。

section 1 step3

这个应用的运行依赖一些特性,这些特性在早期的macOS上是不支持的,所以可能需要改变部署目标。

步骤4 在项目导航器中,选择顶部的Xcode项目,在可用编译运行目标栏中,选择部署目标为10.15

section 1 step4

步骤5 在MacLandmarks文件夹中,选择ContentView.swift文件,打开预览画布,点击恢复(Resume),查看预览。SwiftUI会提供main视图和它的预览视图提供者,就像iOS应用,可以预览应用的主窗口。

section 1 step5

第二节 共享数据和资源

下一步,复用来自iOS应用的模型和资源文件到macOS应用中。

section 2

步骤1 在项目导航器中,打开Landmarks文件夹并选中所有ModelsResources文件夹。landmarkData.json文件包含在教程的启动项目,里面包含了一个新的description字段,这是之前的教程中所没有的内容。

section 2 step1

步骤2 在文件检查器中,为选中的文件设置目标成员关系为MacLandmarks项目。应用编译时需要访问这些共享资源。要使用新的description字段,需要在Landmark结构体中添加一个对应的字段。

section 2 step2

步骤3 打开Landmark.swift文件,添加一个description属性。因为载入的数据遵循Codable协议,只需要确保属性名称和json文件中对应的字段名称一致就可以导入新增的字段数据了。

section 2 step3

第三节 创建行视图

对于使用SwiftUI来构建视图,一般是自底向上的方式,先创建小视图,然后用小视图组合成更大的视图。下面将创建一个列表的行视图。这个行视图包含地标的名称、地理位置、图片以及一个可选的标记,表标这个地标是否被收藏。

section 3

步骤1 在MacLandmarks文件夹下添加一个新的SwiftUI视图,命名为LandmarkRow.swiftiOS应用下也有一个与之同名的文件,重名文件可以通过设置文件的目标成员为适合的App来解决重名的问题。

section 3 step1

步骤2 添加一个landmark属性到LandmarkRow结构体中,并更新预览视图,让新创建的视图可以在预览视图中展示出来。

section 3 step2

步骤3 用VStack包裹的地标图片视图替换占位文本Text视图。

section 3 step3

步骤4 添加一个包裹在VStack中的描述地标的文本视图。

section 3 step4

步骤5 添加一个收藏指示视图,把它和其它现有的内容用一个Spacer分割开。Spacer会把已有的视图推向左边,但是收藏指示视图要放在右边,目前是不可见状态,因为此时还没有图片资源与之对应。

section 3 step5

步骤6 从Resources文件夹下拖动star-filled.pdfstar-empty.pdf文件到macOS应用的Assets.xcassets文件内。

section 3 step6

步骤7 给行视图添加内边距,现在就能够把黄色的收藏标记显示出来了。行视图的内边距可以提高可读性,当把多个行视图集合到列表视图内时,这一点就能很明显的看出来了。

section 3 step7

第四节 把行视图组合进列表视图中

使用上一节创建的行视图,创建一个列表视图,用来展示用户了解的所有地标。当showFavoritesOnly属性为真时,列表中只展示那些被用户收藏的地标。

section 4

步骤1 添加一个名为LandmarkList.swift的新的SwiftUI视图

section 4 step1

步骤2 添加userData属性作为环境注入对象,并更新预览视图。这样就可以让视图访问全局用户地标数据。

section 4 step2

步骤3 创建一个列表,行使用使用landmarkRow定义的类型。

section 4 step3

步骤4 让列表的行可以被用户选中,需要给列表提供一个绑定可选地标成员的关系,并用地标数据自己来标识行。之后会使用这个被选中的地标来展示地标详情页。

section 4 step4

步骤5 根据showFavoritesOnly的状态值以及地标数据是否被用户标记为收藏来决定列表中展示的行的内容。

section 4 step5

第五节 创建过滤器来管理列表的展示内容

因为用户可以标记地标为收藏状态,所以需要提供方式让用户只看到自己收藏过的地标。现在要创建一个过滤器视图,使用Toggle控件给用户提供一个勾选设置,让用户选择是否过滤列表中的非收藏地标,只展示收藏过的地标。

为了让用户可以快速筛选出自己喜欢的地标,这里会添加一下选择器弹出按钮,让用户可以根据地标的不同类别,选择过滤展示自己收藏的地标数据。

section 5

步骤1 添加一个名为Filter.swiftSwiftUI视图。

步骤2 添加userData属性作为环境注入对象,并更新预览视图。

步骤3 用Toggle控件来展示布尔值showFavoritesOnly属性,并给它一个恰当的标签文本。

section 5 step3

当用户选择勾选框时,列表视图也会跟着一起刷新展示,因为它们都绑定了同一上环境注入对象中的值showFavoritesOnly。除此之外,还可以使用地标的类别来定义额外的过滤条件。

步骤4 创建FilterType类型,用来存放地标的类别以及类别对应的名称。确保FilterType遵循Hashable协议,这样FilterType就可以被用在选择器。FilterType中的名称属性可以展示在选择器中,让用户选择过滤哪一种类别的地标。

section 5 step4

步骤5 定义一个all类型用来表示不使用任何地标类别过滤。这个额外的过滤类别要求FilterType有一个特殊的初始构建器,用来处理类别为空的初始化场景。

section 5 step5

遵循CaseIterableIdentifiable协议,让FilterType可以做为ForEach的初始化入参,之后就可以使用这个FilterType类型了。

步骤6 遵循CaseIterable协议,给列表提供所有可能的类别。

section 5 step6

步骤7 遵循Identifiable协议并定义一个id属性。

section 5 step7

步骤8 在Filter.swift中,给Filter视图添加一个选择器,选择器使用一个FilterType的绑定用来记录用户选择,FilterType的名称用来表示用户在选择器菜单中的选项。使用FilterType的绑定关系可以让父视图观察到用户的选择。

section 5 step8

步骤9 返回到列表视图,添加FilterType绑定关系。对于过滤器视图来说,这允许它和父视图共享变量filter

步骤10 更新列表行的创建逻辑,让它包含类别过滤功能。查找那些与用户选中的过滤类别相匹配的地标类别,或者任何用户选择的特色类别地标。

section 5 step10


收起阅读 »

iOS SwiftUI 框架集成 1.1

第三节 在SwiftUI视图的状态下跟踪页面如果要添加一个自定义的UIPageControl控件,就需要一种方式能够在PageView中跟踪当前展示的页面。这就需要在PageView中声明一个@State属性,并传递一个针对该属性的绑定关系给PageViewC...
继续阅读 »

第三节 在SwiftUI视图的状态下跟踪页面

如果要添加一个自定义的UIPageControl控件,就需要一种方式能够在PageView中跟踪当前展示的页面。这就需要在PageView中声明一个@State属性,并传递一个针对该属性的绑定关系给PageViewController视图,在PageViewController中通过绑定关系更新状态属性,来反映当前展示的页面。

section 3

步骤1 在PageViewController中添加一个绑定属性currentPage。除了使用关键字@Binding声明属性为绑定属性外,还需要更新一下函数setViewControllers(_:direction:animated:),给它传入currentPage绑定属性

section 3 step 1

做到这一步还不能正常运行,继续进行下一步。

步骤2 在PageView中声明@State变量,并在创建PageViewController时把绑定属性传入。注意使用$语法创建一个针对状态变量的绑定关系。

section 3 step 2

步骤3 通过改变PageView视图中的currentPage初始值来测试绑定关系是否正常生效。也可以做一个测试按钮,点击按钮时让第二个页面展示出来

section 3 step 3

步骤4 添加一个TextView控件来展示状态变量currentPage的值,拖动页面切换时观察TextView上的值,目前不会发生变化。因为PageViewController内部没有在切换页面的过程中更新currentPage的值。

section 3 step 4

步骤5 在PageViewController.swift中让coordinator作为UIPageViewController的代理,并添加pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool) 方法。因为SwiftUI在页面切换动画完成时会调用这个方法,这样就可以这个方法内部获取当前正在展示的页面的下标,并同时更新绑定属性currentPage的值。

section 3 step 5

步骤6 coordinator除了是UIPageViewController数据源外,再把它赋值为UIPageViewController的代理。由于绑定关系是双向的,所以当页面切换时,PageView视图上的Text就会实时展示当前的页码。

section 3 step 6

section 3 step 6 gif

第四节 添加一个自定义PageControl

我们已经为包裹在UIViewRepresentable视图中的子视图上添加了一个自定义UIPageControl

section 4

步骤1 创建一个新的SwiftUI视图,命名为PageControl.swift,并使用PageControl类型遵循UIViewRepresentable协议。UIViewRepresentableUIViewControllerRepresentable类型有相同的生命周期,在UIKit类型中都有对应的生命周期方法。

section 4 step 1

步骤2 在PageView中用PageControl替换Text,并把VStack换成ZStack。因为总页数和当前页面都已经传入PageControl,所以PageControl已经可以正确的显示。

section 4 step 2

下一步要处理PageControl与用户的交互,让它可以被用户点击任意一边进行页面间的切换。

步骤3 在PageControl中创建一个嵌套类型Coordiantor,添加一个makeCoordinator()方法创建并返回一个coordinator实例。因为UIControl子类(包括UIPageControl)使用Target-Action模式,Coordinator实现一个@objc方法来更新currentPage绑定属性的值。

section 4 step 3

步骤4 把coordinator作为PageControl值改变事件的目标处理器,并指定updateCurrentPage(sender:)方法为处理函数

section 4 step 4

步骤5 现在就可以尝试PageControl的各种交互来切换页面,PageView展示了SwiftUIUIKit视图如何混合使用。

section 4 step 5 gif

检查是否理解

问题1 下面哪个协议可以用来把UIKit中的视图控件器桥接进SwiftUI

  •  UIViewRepresentable
  •  UIHostingController
  •  UIViewControllerRepresentable

问题2 对于UIViewControllerRepresentable类型,下面哪个方法可以为它创建一个代理或数据源?

  •  在makeUIViewController(context:)方法中创建UIViewController实例的地方
  •  在UIViewControllerRepresentable类型的初始化器中
  •  在makeCoordinator()方法中
收起阅读 »

iOS SwiftUI 框架集成 1.0

框架集成混合使用SwiftUI框架和平台相关的其它UI框架(视图和视图控制器)包含章节与UIKit交互创建watchOS应用创建macOS应用与UIKIT交互SwiftUI可以在苹果全平台上无缝兼容现有的UI框架。例如,可以在SwiftUI视图中嵌入UIKit...
继续阅读 »

框架集成

混合使用SwiftUI框架和平台相关的其它UI框架(视图和视图控制器)

framework and integeration

包含章节

与UIKIT交互

SwiftUI可以在苹果全平台上无缝兼容现有的UI框架。例如,可以在SwiftUI视图中嵌入UIKit视图UIKit视图控制器,反过来在UIKit视图UIKit视图控制器中也可以嵌入SwiftUI视图。

本篇教程展示如何把landmark应用的主页混合使用UIPageViewControllerUIPageControl。使用UIPageViewController来展示由SwiftUI视图构成的轮播图,使用状态变量和绑定来操作用户界面数据的更新。

跟着教程一步步走,可以下载工程文件进行实践。


第一节 创建一个用来展示UIPageViewController的SwiftUI视图

为了在SwiftUI视图中展示UIKit视图和UIKit视图控制器,需要创建遵循UIViewRepresentableUIViewControllerRepresentable协议的类型。创建的自定义视图类型,用来创建和配置所要展示的UIKit类型,SwiftUI框架来管理UIKIt类型的生命周期并在适当的时机更新它们。

section 1

步骤1 创建一个新的SwiftUI视图文件,命名为PageViewController.swift,并且声明PageViewController类型遵循UIViewControllerRepresentable。这个页面视图控制器存放一个UIViewController实例数组,数组中的每一个元素代表在地标滚动过程中的一页视图。

section 1 step 1

下一步添加UIViewControllerRepresentable协议的两个实现, 目前因为协议方法没有完成实现,会有报错提示。

步骤2 添加一个makeUIViewController(context:)方法,方法内部以指定的配置创建一个UIPageViewControllerSwiftUI会在准备显示视图时调用一次makeUIViewController(context:)方法创建UIViewController实例,并管理它的生命周期。

section 1 step 2

由于还缺少一个协议方法没有实现,所以目前还是会报错。

步骤3 添加updateUIViewController(_:context:)方法,这个方法里调用setViewControllers(_:direction:animated:)方法展示数组中的第一个视图控制器

section 1 step 3

创建另一个SwiftUI视图展示遵循UIViewControllerRepresentable协议的视图

步骤4 创建一个名为PageView.swift的视图,声明一个PageViewController作为子视图。初始化时使用一个视图数组来初始化,并把每一个视图都嵌入在一个UIHostingController中。UIHostingController是一个UIViewController的子类,用来在UIKit环境中表示一个SwiftUI视图。

section 1 step 4

步骤5 更新预览视图,并传入视图数组,预览视图就会开始工作了

section 1 step 5

步骤6 在继续下面的步骤前,先把PageView的预览视图固定住,以避免在文件切换时不能实现预览到PageView的改变。

section 1 step 6

第二节 创建视图控制器的数据源

短短几个步骤就做了很多事,PageViewController使用UIPageViewController去展示来自SwiftUI内容。现在是时候添加挥动手势进行页面之间的翻动了。

section 2

一个展示UIKit视图控制器的SwiftUI视图可以定义一个Coordinator类型,这个Coordinator类型由SwitUI管理,用来作为视图展示的环境

步骤1 在PageViewControlelr中定义一个嵌套类型CoordiantorSwiftUI管理UIViewController Representable类型的coordinator,并在调用方法时把它作为环境的一部分。

section 2 step 1

步骤2 在PageView Controller中添加另一个方法,创建coordinatorSwiftUI在调用makeUIViewController(context:)前会先调用makeCoordinator()方法,因此在配置视图控制器时是可以访问到coordiantor对象的。可以使用coordinator为实现通用的Cocoa模式,例如:代理模式数据源以及目标-动作

section 2 step 2

步骤3 让Coordinator类型添加UIPageViewControllerDataSource协议遵循,并且实现两个必要方法。这两个必要方法会建立起视图控制器之间的联系,因此可以实现页面之前的前后切换。

section 2 step 3

步骤4 把coordiantor作为UIPageViewController的数据源

section 2 step 4

步骤5 打开实时预览,并测试一下前后页面切换的功能是否正常

swipe landmarks

收起阅读 »

iOS SwiftUI 应用设计与布局 1.2

玩转UI控件在Landmarks应用中,用户可以创建一个简介来描述他们自已的个人情况。为了让用户可以编辑自己的简介,我们需要添加一个编辑模式并设计一个偏好设置界面。这里使用多种通用控件来展示用户的各种数据,并在用户保存他们所做的数据修改时更新地标数据模型。按照...
继续阅读 »

玩转UI控件

Landmarks应用中,用户可以创建一个简介来描述他们自已的个人情况。为了让用户可以编辑自己的简介,我们需要添加一个编辑模式并设计一个偏好设置界面。

这里使用多种通用控件来展示用户的各种数据,并在用户保存他们所做的数据修改时更新地标数据模型。

按照步骤在下面的项目工程中一步步进行实践。


第一节 展示用户简介

Landmarks应用在本地存储了一些配置和用户偏好设置。在用户编辑这些数据前,会被展示在一个没有编辑按钮的概要视图上。

secion 1

步骤1

在项目文件导航栏的Landmarks文件组下面新建一个名为Profile的文件组,并在这个新建的文件组下面添加一个新视图ProfileHost, 这个新视图包含一个TextView,用来展示用户名称。ProfileHost将会展示静态概要信息,同时支持编辑模式

secion 1 step 2

步骤2 用步骤1创建的ProfileHost替换Home.swift中的静态文本Text视图。现在主页中的profile按钮点击时可以调起一个用户简介页面了

secion 1 step 2

步骤3 创建一个新的视图命名为ProfileSummary,它会持有一个Profile实例,并显示一些用户的基本信息。Profile概要视图持有一个Profile对像的原因是,因为它的父视图ProfileHost管理着视图的状态,它不能与Profile进行绑定。

secion 1 step 3

步骤4 更新ProfileHost文件,显示新的概要视图

secion 1 step 4

步骤5 创建一个名为HikeBadge的新视图,这个新视图由Badge视图和一些描述性文字构成。Badge仅仅是一个图形,在HikeBadge视图中的文本与accessibility(label:)属性修改器一起,可以让这个徽章对用户更加清晰。注意frame(width:height:)的两种不同的用法用来配置徽章以不同的缩放尺寸显示。

secion 1 step 5

步骤6 更新ProfileSummary文件,添加几个不同的徽章代表用户得到的不同徽章

secion 1 step 6

步骤7 把HikeView包含在ProfileSummary页面中后,就完成了第一节的实践内容了。

secion 1 step 7

第二节 添加编辑模式

用户需要能够在浏览模式和编辑模式之间进行切换来查看或者修改用户简介的信息。通过在ProfileHost上添加一个Edit Button,然后创建一个用来编辑简介信息的页面。

secion 2

步骤1 添加一个Enviornment视图属性,用来使用\.edit模式。可以使用这个属性来读写当前编辑模式。

secion 2 step 1

步骤2 创建一个编辑按钮,可以切换编辑模式

secion 2 step 2

步骤3 更新UserData类,包含一个Profile实例,即使用户简介页面消失后也可以存储编辑后的信息

secion 2 step 3

步骤4 从环境变量中读取用户简介信息,并把数据传递给ProfileHost视图的控件上进行展示。为了在编辑状态下修改简介信息后确认修改前避免更新全局状态(例如在编辑用户名的过程中),编辑视图在一个备份属性中进行相应的修改操作,确认修改后,才把备份属性同步到全局应用状态中。

secion 2 step 4

步骤5 添加一个条件视图,可以用来显示静态用户简介视图或者是用户简介视图的编辑模式。当前的编辑模式只支持静态文本框的编辑。

secion 2 step 5

第三节 定义简介编辑器

用户简介编辑器包含几个单独的控件用来修改对应简介信息。在简介中,一些项例如徽章是不可以编辑修改的,所以它们不会出现在简介编辑器中。为了保持简介在编辑模式和浏览模式的一致性,需要按照简介页面各项相同的顺序进行添加。

步骤1 创建一个名为ProfileEditor的新视图,并绑定用户简介中的草稿。视图中的第一个控件是TextField,用来更新用户名字段值。创建TextField时要提供一个标签和一个绑定字符串。

secion 3 step 1

步骤2 更新ProfileHost中的条件内容,让它包含条件编辑器并把简单的绑定关系传递给简介编辑器。现在当你点击Edit按钮,简介视图就会变成编辑模式了。

secion 3 step 2

步骤3 添加一个切换开关,用来设置用户是否接收相关地标事件的推送通知。这个Toggle控件打开和关闭正好对应着布尔值的truefalse

secion 3 step 3

步骤4 把一个Picker和一个Text放在VStack结构里,让这个地标可以选择不同季节。

secion 3 step 4

步骤5 最后,在季节图片选择器下方添加一个DatePicker,用来修改地标的目标浏览日期

secion 3 step 5

第四节 延迟编辑传播

在编辑模式时,使用用户简介信息的备份进行修改,当用户确认进行修改后,再用修改的备份信息覆盖真正的用户信息。直到用户退出编辑模式前都不让编辑的备份生效。

secion 4

步骤1 在ProfileHost视图上添加一个取消按钮。不像编辑模式按钮提供的完成按钮,取消按钮不会应用修改后的简介备份信息到实际的简介数据上。

secion 4 step 1

步骤2 当用户点击完成按钮后,使用onAppear(perform:)onDisappear(perform:)来更新或保存用户简介数据。下一次进入编辑模式时,使用上一次的用户简介数据来展示。

secion 4 step 2

检查是否理解

问题1 编辑状态改变时,怎样更新一个视图,例如,当用户编辑了用户简介信息后点击完成按钮的情况下,是怎么更新一个视图的

  • problem 1 answer 1
  • problem 1 answer 2
  • problem 1 answer 3

问题2 什么情况下需要添加一个accessiblity标签,使用accessibility(label:)修改器?

  •  在应用的每一个视图都添加一个accessibility标签
  •  当可以让用户界面元素对用户变的更清晰时,添加一个accessibility标签
  •  只有当你没有给视图清加tag时才可以使用accessibility(label:)

问题3 模态和非模态视图展示有什么差别?

  •  当模态展示一个视图时,源视图设置目标视图的编辑模式
  •  当非模态展示一个视图时,目标视图会盖住源视图并且替代当前的导航栈
  •  当模态展示一个视图时,目标视图盖住源视图并替换当前导航栈
收起阅读 »

iOS SwiftUI 应用设计与布局 1.1

第四节 组合首页Landmarks应用的首页在用户点击查看地标详情前需要先把地标的一些简单信息展示出来。复用之前创建的视图构建具体某一类别地标的行视图步骤1 在CategoryRow.swift文件中,与CategoryRow类型并列,创建一个新的自...
继续阅读 »

第四节 组合首页

Landmarks应用的首页在用户点击查看地标详情前需要先把地标的一些简单信息展示出来。复用之前创建的视图构建具体某一类别地标的行视图

section 4

步骤1 在CategoryRow.swift文件中,与CategoryRow类型并列,创建一个新的自定义视图类型CategoryItem,用这个新的视图类型替换CategoryRow的地标名称Text控件

section 4 step 1

步骤2 在CategoryHome.swift中,添加一个名为FeaturedLandmarks的简单视图,这个视图用来显示地标数据中isFeatured属性为真的那些地标。在之后的教程中,会把FeaturedLandmarks这个视图修改成一个交互式轮播图。目前,这个视图仅仅展示一张缩放和剪裁后的地标图片。

section 4 step 2

步骤3 把视图的边距设置为0,让展示内容可以尽量贴着屏幕边沿

section 4 step 3

第五节

现在所有类别的地标都可以在首页视图中展示出来,用户还需要能够进入应用其它页面的方法。使用页面导航和相关API来实现用户从应用首页到地标详情页、收藏列表页及用户个人中心页的跳转。

section 5

步骤1 在CategoryRow.swift中,把CategoryItem视图包裹在NavigationLink视图中。CategoryItem这时做为跳转按钮的内容,destination指定点击NavigationLink按钮时要跳转的目标视图。

section 5 step 1

section 5 step 1 gif

步骤2 使用renderingMode(_:)foregroundColor(_:)这两个属性修改器来改变地标类别项的导航样式。做为NavigationLink标签的CategoryItem中的文本会使用Environment中的强调颜色,图片可能以模板图片的方式渲染,这些都可以使用属性修改器来调整,达到最佳效果。

section 5 step 2

步骤3 在CategoryHome.swift中,添加一个模态展示的用户信息展示页,点击了用户图标时弹出展示。当状态showProfile被置为true时,展示用户信息页,当showProfile状态置为false时,用户信息页消失。

section 5 step 3

步骤4 在导航条上添加一个按钮,用来切换showProfile状态的值:true或者false

section 5 step 4

section 5 step 4 gif

步骤5 在CategoryHome.swift中添加一个跳转链接,点击时跳转到全部地标的筛选页面。

section 5 step 5

section 5 step 5 gif

步骤6 把LandmarkList.swift中的把包裹地标列表视图的NavigationView移动到对应的预览视图中。因为在应用中,LandmarkList总是会被展示在CategoryHome.swift定义的导航视图中。

section 5 step 6

检查是否理解

问题1 对于Landmarks这个应用来说,哪一个视图是它的根视图?

  •  SceneDelegate
  •  Landmarks
  •  CategoryHome

问题2 CategoryHome这个视图是如何与应用的其它视图联动起来的

  •  在不同地标之间复用图片资源
  •  与其它视图使用一致的命名规范和属性修改器语法
  •  使用导航结构把地标应用中所有视图连接在一起


收起阅读 »

iOS SwiftUI 应用设计与布局 1.0

应用设计与布局深入了解使用SwiftUI创建的复杂的用户界面的结构和布局包含章节组合复杂用户界面组合复杂用户界面Landmarks应用的首页是一个纵向滚动的地标类别列表,每一个类别内部是一个横向滑动列表。随后将构建应用的页面导航,这个过程中可以学习到如果组合各...
继续阅读 »

应用设计与布局

深入了解使用SwiftUI创建的复杂的用户界面的结构和布局

app design and layout

包含章节

组合复杂用户界面

Landmarks应用的首页是一个纵向滚动的地标类别列表,每一个类别内部是一个横向滑动列表。随后将构建应用的页面导航,这个过程中可以学习到如果组合各种视图,并让它们适配不同的设备尺寸和设备方向。


第一节 添加一个首页视图

已经创建了所有在Landmarks应用中需要的视图,现在给应用创建一个首页视图,把之前创建的视图整合起来。首页不仅仅包含之前创建的视图,它还提供页面间导航的方式,同时也可以展示各种地标信息。

section 1

步骤1 创建一个名为CategoryHome.swift的自定义视图文件

section 1 step 1

步骤2 把应用的场景代理(scene delegate)的根视图从之前的地标列表视图更改为新创建的首页视图。现在应用启动后的每一个页面就是首页了,所以还需要添加从首页导航跳转到其它页面的方法。

section 1 step 2

步骤3 添加NavigationView,这个NavigationView将会容纳Landmarks应用中其它不同的视图。配合使用NavigationViewNavigationLink及相关的修改器,就可以构建出应用的页面间导航结构

section 1 step 3

步骤4 设置导航栏标题为Featured

section 1 step 4

第二节 创建地标类别列表

Landmarks应用为了便于用户浏览各种类别的地标,将地标按类别竖向排列形成列表视图,对于每一个类别内的具体地标,又把它们按照水平方向排列,形成横向列表。组合使用垂直栈(vertical statck)和水平栈(horizontal stack)并给列表添加滚动

section 2

步骤1 使用Dictionary结构体的初始化方法init(grouping:by:),把地标数据的类别属性category传入作为分组依据,可以把地标数据按类别分组。工程文件中已经为每一个地标样本数据预定义了类别。

section 2 step 1

步骤2 使用List显示地标数据的类别。Landmark.Category是枚举类型,它的值标识列表中每一种类别,可以保证类别不会有重复定义

section 2 step 1

第三节 添加针对单个类别的地标行列表

Landmarks应用对每个类别下的地标采用横向滑动的行进行展示。添加一个新的视图类型用来表示这样一个地标行,然后使用这个新创建的行类型具体展示某一具体类型上的所有地标。

section 3

步骤1 定义一个新的视图类型,用来展示地标类别行的内容。新建行视图需要存放地标具体类别的展示数据

section 3 step 1

步骤2 更新CategoryHome.swift的代码,把地标类别信息传给新建的行视图类型

section 3 step 2

步骤3 在CategoryRow.swift中使用一个HStack展示类别下的地标内容

section 3 step 3

步骤4 为行内容指定一个高度,并把行内容嵌入到ScrollView中,以支持横向滑动。预览视图时,可以多增加几个地标数据,用来查看列表的滑动是否正常。

section 3 step 4


收起阅读 »

iOS swiftUI 视图动画和转场 1.1

第二节 把视图的状态改态转化成动画效果已经学会了给单个视图添加动画的方法,现在可以学习怎么在视图的状态发生改变时添加动画效果。当用户点击按钮时会切换showDetail状态的值,在视图变化过程中添加动画效果。步骤1 把showDetail.toggl...
继续阅读 »

第二节 把视图的状态改态转化成动画效果

已经学会了给单个视图添加动画的方法,现在可以学习怎么在视图的状态发生改变时添加动画效果。当用户点击按钮时会切换showDetail状态的值,在视图变化过程中添加动画效果。

state change

步骤1 把showDetail.toggle()包裹在withAnimation函数调用块中。showDetail的改变影响了视图HikeDetail和详情切换按钮,在显示/隐藏详情的过程中都有了过滤动画效果。

with_animation block

放慢动画速度,可以观察SwiftUI动画在被中断下是怎么运作的

步骤2 给withAnimation传入一个时长4秒的基本动画参数.easeInOut(duration:4),可以指定动画过程时长,给withAnimation传入的动画参数与.animation(_:)修改器可用参数一致。

with animation duration block

步骤3 在动画过程进行中点击按钮切换视图状态,查看对应的动画被中断时的效果

with animation interrupt

步骤4 读下一节之前,把动画时长参数(.easeInOut(duration: 4))去掉,让动画不再缓慢进行。

第三节 定制视图转场动画

默值情况下,视图离屏和入屏时的动画效果是渐隐/渐现, 这个默认的转场效果可以使用transition(_:)修改器进行定制。

transitions

步骤1 给HikeView视图添加transition(_:)修改器,并定制转场参数为.slide,转场动画为滑入/滑出

transition slide

步骤2 可以把滑入/滑出这种转场动画封装起来,方便其它视图复用同样的转场效果

custom transition effect

步骤3 在moveAndFade转场效果的定义中使用move(edge:),让滑入/滑出从屏幕的同一边进行

move and fade custom

步骤4 使用asymmetric(insertion:removal:)修改器来定制视图显示/消失时的转场动画效果

custom move and fade slide scale

第四节 组合复杂的动画效果

点击图表下面的三个按钮,会在三个不同的数据集间进行切换并展示。本节中会使用组合动画,让图表在不同数据集间切换时的转换动画流畅自然。

combine animation

步骤1 把showDetail的默认值改为true,并把HikeView的预览模式视图固定在画布上。这样可以在编辑其它文件时,依然看到动画效果的变化。

pin canvas

步骤2 在HikeGraph.swift中定义了一个新的波动动画,并把它与滑入/滑出动画一起应用到图表视图上。

hike graph ripple

步骤3 把动画切换为弹簧动画(spring),并设置弹簧阻尼系数为0.5,动画过程中产生了逐渐回弹效果

spring animation

步骤4 加速弹簧动画的执行速度,缩短切换图表的时间

spring animation speed

步骤5 以当条形在图表中的位置为参数,添加延迟效果,图表中的每个条形会顺序动起来

spring animation index based delay

步骤6 观察一下自定义波动(rippling)效果是怎么作用在视图转场中的

检查是否理解

问题1 怎样从一串动画效果调用中,去掉其中的一种动画效果。以下面的代码为例,怎样去掉旋转动画

problem 1

  • a1
  • a2
  • a3

问题2 当你开发动画的过程上,为什么要把预览视图固定在画布上?

  •  为了固定动画过程中的当前帧
  •  为了在多个设备配置开发中预览动画效果
  •  为了在切换到其它不同文件时,固定显示当前视图的预览

问题3 在视图状态改变时,如何快速测试一个动画在被中断时的表现

  •  在包含animation(_:)修改器的代码行上打一个断点,然后单步按动画帧进行测试
  •  调整动画的持续时长,让动画在足够长的时间内完成,这样就可以调整动画的细节
  •  重复的调用sleep(100)来减慢动画的执行
收起阅读 »

iOS SwiftUI 视图动画和转场

视图动画和转场使用SwiftUI可以把视图状态的改变转成动画过程,SwiftUI会处理所有复杂的动画细节在这篇中,会给跟踪用户徒步的图表视图添加动画。使用animation(_:)修改器给一个视图添加动画效果非常容易下载起步项目并跟着本篇教程一步步实践,或者查...
继续阅读 »

视图动画和转场

使用SwiftUI可以把视图状态的改变转成动画过程,SwiftUI会处理所有复杂的动画细节

在这篇中,会给跟踪用户徒步的图表视图添加动画。使用animation(_:)修改器给一个视图添加动画效果非常容易

下载起步项目并跟着本篇教程一步步实践,或者查看本篇完成状态时的工程代码去学习


第一节 给每个视图单独添加动画

在视图上使用animation(_:)修改器时,SwiftUI会在视图的任何可进行动画的属性发生改变时产生对应的动画效果。视图的颜色、不透明度、旋转角度、大小及一些其它属性都是可进行动画的

animate button

步骤1 在HikeView.swift中,打开实时预览,体验一下图表的打开和隐藏,此时的状态改变时是没有添加动画效果的。在本篇的实践中,保持实时预览一直打开,每一步修改的效果就可以实时的看到

live preview animation

步骤2 给显示/隐藏切换的箭头按钮添加旋转动画,会发现现在按钮点击时的旋转有一个动画过渡的效果了

rotate button animation

rotate button animation video

步骤3 当视图从隐藏到展示时,让切换按钮变大1.5倍

rotate button scale

rotate button scale video

步骤4 把动画的类型从easeInOut改为spring()。SwiftUI包含一些预设或可自定义的动画类型,像弹簧(spring)动画和类型液体(fluid)动画类型。可以调整动画开始前的等待时长、动画的速度也可以指定让动画循环重复的进行

rotate button spring

步骤5 如果只想让按钮具有缩放动画而不进行旋转动画,可以在scaleEffect添加animation(nil)来实现。可以在这里做一些实验,如果把其它的一些动画效果结合在一起,会怎么样

rotate button no rotate

步骤6 学下一节之前,把本节中添加的animation(_:)修改器都去掉

rotate button resume

收起阅读 »

iOS SwiftUI 创建和组合视图 4.2

第三节 绘制徽章符号地标徽章中心有一个以地标App图标中的山峰图形改造形成的标志。山峰这个符号由两个形状组成,一个是表示山顶被雪覆盖的部分,另一个是山体。这里会使用有一定间距的两个局部三角形形状绘制这个徽章符号步骤1 把之前的徽章视图形状抽出来单独形...
继续阅读 »

第三节 绘制徽章符号

地标徽章中心有一个以地标App图标中的山峰图形改造形成的标志。山峰这个符号由两个形状组成,一个是表示山顶被雪覆盖的部分,另一个是山体。这里会使用有一定间距的两个局部三角形形状绘制这个徽章符号

badge symbol

步骤1 把之前的徽章视图形状抽出来单独形成一个BadgeBackground视图,并生成一个新的视图文件BadgeBackground.swift

badge background

步骤2 把BadgeBackground放在Badgebody属性中。

refactor badge

步骤3 创建名为BadgeSymbol的自定义视图,这个视图是一个山峰的形状,把这个形状复制多次并按一定角度旋转多次拼成一个徽章的图案

badge symbol

步骤4 使用pathAPI来绘制徽章符号的上半部分,试着调节spacingtopWidthtopHeight的系数,观察这些系数是怎么影响图形绘制的结果的

badge symbol top

步骤5 绘制徽章图案的下半部分,使用move(to:)把绘图光标移到另一个图形绘制的起点,绘制新的形状

badge symbol bottom

步骤6 用紫色填充徽章符号

badge symbol fill

第四节 组合徽章的前景符号和背景形状

徽章设计思路是在背景形状上面再绘制多个有固定旋转角度的山峰符号。定义一个新的类型用于展示旋转一定角度的徽章符号,使用ForEach生成不同旋转角度的山峰符号,绘制在徽章背景上,从而形成最终的徽章。

badge combine

步骤1 创建RotatedBadgeSymbol视图封装旋转徽章符号,调整旋转的角度,并在预览视图中查看效果

badge symbol rotate 4

步骤2 在Badge.swift中,使用ZStack把徽章图标放在徽章背景层上面。此时会发现,徽章符号的尺寸相比徽章背景大了许多,这不符合最初设计的预期

badge symbols

步骤3 缩放符号尺寸到合适的大小

badge geometry scale

步骤4 使用ForEach复制多个徽章图标,按360度周解均分,每一个徽章符号都比前一个多旋转45度,这种就会形成一个类似太阳和徽章图标

badge symbol completed

检查是否理解

问题1 GeometryReader的作用是什么?

  •  GeometryReader可以把父视图分割成网格,便于在屏幕上布局视图
  •  GeometryReader可以动态的绘制、定位、缩放视图,不需要写死它们的尺寸。这样可以在不同尺寸的屏幕上复用已经写好的视图
  •  使用GeometryReader可以自动识别应用视图层级上形状的类型和位置,例如: (圆)Circle

问题2 下面代码段布局后是哪一个图?

problem 2

  • answer 1
  • answer 2
  • answer 3

问题3 下面代码绘制出哪个图?

problem 3

  • answer 1
  • answer 2
  • answer 3
收起阅读 »

iOS SwiftUI 创建和组合视图 4.1

绘制和动画学习绘制形状和路径,并创建徽章和添加动画包含章节绘制路径和形状视图动画和转场绘制路径和形状用户在浏览完一个地标后会得到一个徽章。但用户要得到徽章首先要先要创建一个徽章。本篇教程就是使用路径和形状创建徽章的过程,创建的徽章可以和其它图形组合形成位置标志...
继续阅读 »

绘制和动画

学习绘制形状和路径,并创建徽章和添加动画

drawing and animation

包含章节

  • 绘制路径和形状
  • 视图动画和转场
  • 绘制路径和形状

    用户在浏览完一个地标后会得到一个徽章。但用户要得到徽章首先要先要创建一个徽章。本篇教程就是使用路径和形状创建徽章的过程,创建的徽章可以和其它图形组合形成位置标志。

    如果想要针对不同种类的地标创建不同的徽章,可以尝试改变徽章基本组成符号的重复次数、角度或大小。

    跟着教程一步步走,可以下载工程文件进行实践。


    第一节 创建徽章视图

    创建徽章前需要使用SwiftUI的矢量绘画API创建一个徽章视图

    badge

    步骤1 选择文件->新建->文件,然后从iOS文件模板列表中选择SwiftUI View。点击下一步(Next),输入文件名Badge后点击创建(Create)

    create file

    name file

    步骤2 调整Badge视图,暂时先让它显示"Badge"文本,一会儿再绘制徽章的形状

    badge text

    第二节 绘制徽章背景

    使用SwiftUI的图形API绘制一个徽章形状

    badge background

    步骤1 查看在文件HexagonParameters.swift中的代码。HexagonParameters结构体定义了绘制徽章六边形形状的控制点参数。不需要修改这些绘制相关的数据,仅仅使用这些数据指定绘制徽章形状时,线段和曲线的控制点位置。

    hexagonal data

    步骤2 在Badge.swift文件中,绘制徽章的形状并使用fill修改器给六边形填充颜色,形成一个视图。使用路径可以把多条直线、曲线或其它绘制形状的基本笔划连成一个复杂的图形,就像形成徽章六边形背景这样.

    Path

    步骤3 给路径添加起点,move(to:)方法可以把绘图光标移动到绘图中的一点,准备绘制的起点

    path start point

    步骤4 使用六边形的绘制参数数据HexagonParameters,依次绘制六边形的边,形成大致轮廓.addLine(to:)方法会使用当前绘图光标所在点为起点,方法参数中指定的点为终点绘制直线。目前六边形看起来有点问题,不过不要担心,这是意料中的事,下面的步骤做完,六边形的形状就会和开头显示的徽章的六边形形状一致了

    path fill

    步骤5 使用addQuadCurve(to:control:)方法绘制贝塞尔曲线,让六边形的角变的更圆润些。

    badge hexagonal

    步骤6 把徽章路径包裹在一个Geometry Reader中,这样徽章可以使用容器的大小,定义自己绘制的尺寸,这样就不需要硬编码绘制尺寸了(100)。当绘制区域不是正方形时,使用绘制区域的最小边长(长宽中哪个最小使用哪个)作为绘制徽章背景的边长,并保持徽章背景的长宽比为1:1

    geometry reader

    步骤7 使用xScalexOffset参数调整变量,把徽章几何绘图区域居中绘制出来

    badge square

    步骤8 把黑色实心填充色改为渐变色,使徽章看上去和开始设计的样式一致

    badge gradient

    步骤9 渐变色上再使用aspectRatio(_:contentMode:)修改器,让渐变色按内容宽高比进行成比例渐变填充。保持1:1的长宽比,徽章背景可以保持居中在徽章视图中,不管徽章视图本身是不是正方形

    badge center

收起阅读 »

iOS SwiftUI 创建和组合视图 3.1

第四节 使用可观察对象来存储数据要实现用户标记哪个地标为自己喜爱的地标这个功能,需要使用可观察对象(observalble object)存放地标数据可观察对象是一种可以绑定到具体SwifUI视图环境中的数据对象。SwiftUI可以察觉它影响视图展示的任何变化...
继续阅读 »

第四节 使用可观察对象来存储数据

要实现用户标记哪个地标为自己喜爱的地标这个功能,需要使用可观察对象(observalble object)存放地标数据

可观察对象是一种可以绑定到具体SwifUI视图环境中的数据对象。SwiftUI可以察觉它影响视图展示的任何变化,并在这种变化发生后及时更新对应视图的展示内容

observable

步骤1 创建一个名为UserData.swift的文件

步骤2 声明一个遵循ObservableObject协议的新数据模型,ObservableObject协议来自响应式框架Combine。SwiftUI可以订阅可观察对象,并在数据发生改变时更新视图的显示内容

步骤3 添加存储属性showFavoritesOnlylandmarks,并赋予初始值。可观察对象需要对外公布内部数据的任何改动,因此订阅此可观察对象的订阅者就可以获得对应的数据改动信息

步骤4 给新建的数据模型的每一个属性添加@Published属性修饰词

combine

第五节 视图中适配数据模型对象

已经创建了UserData可观察对象,现在要改造视图,让它使用这个新的数据模型来存储视图内容数据

model

步骤1 在LandmarkList.swift文件中,使用@EnvironmentObject修饰的userData属性来替换原来的showFavoritesOnly状态属性,并对预览视图调用environmentObject(_:)修改器。只要environmentObject(_:)修改器应用在视图的父视图上,userData就能够自动获取它的值。

步骤2 替换原来使用showFavoritesOnly状态属性的地方,改为使用userData中的对应属性。与@State修饰的属性一样,也可以使用$前缀访问userData对象的成员绑定引用

步骤3 创建ForEach实例时使用userData.landmarks做为数据源

envrionment object

步骤4 在SceneDelegate.swift中,对LandmarkList视图调用environmentObject修改器,这样可以把UserData的数据对象绑定到LandmarkList视图的环境变量中,子视图可以获得父视图环境中的变量。此时如果在模拟器或者真机上运行应用,也可以正常展示视图内容

scene delegate

步骤5 更新LandmarkDetail视图,让它从父视图的环境变量中取要展示的数据。之后在更新地标的用户喜爱状态时,会用到landmarkIndex这个变量

landmark detail environment

步骤6 切换到LandmarkList.swift文件,并打开实时预览视图去验证所添加的功能是否正常工作

landmark list environment

第六节 为每一个地标创建一个喜爱按钮

Landmark这个应用可以在喜欢和不喜欢的地标列表间进行切换了,但喜欢的地标列表还是硬编码形成的,为了让用户可以自己标记哪个地标是自己喜欢的,需要在地标详情页添加一个标记喜欢的按钮

favorite button

步骤1 在LandmarkDetail.swiftHStack中添加地标名称的Text

步骤2 在地标名称的Text控件旁边添加一个新的按钮控件。使用if-else条件语句设置不同的图片显示状态表示这个地标是否被用户标记为喜欢。在Button的动作闭包中,使用了landmarkIndex去修改userData中对应地标的数据。

favorite star button

步骤3 切换到landmarkList.swift,并开启实时预览模式。当从列表页导航进入详情页后,点击喜欢按钮,喜欢的状态会在返回列表页后与列表中对应的地标喜欢状态保持一致,因为列表页和详情页的地标数据使用的是同一份,所以可以在不同页面间保持状态同步。

star button completed

检查是否理解

问题1 下列选项哪个可以把数据按视图层级关系传递下去?

  •  @EnvironmentObject属性
  •  environmentObject(_:)修改器

问题2 绑定(binding)的作用是什么?

  •  绑定是值和改变值的方法
  •  是一个视图连接在一起的方法,确保连续起来的视图接收同一份数据
  •  是一个临时固化值的方式,目的是在其它视图状态变化时,保持值不改变
收起阅读 »

iOS SwiftUI 创建和组合视图 3.0

处理用户输入在Landmark应用中,标记喜爱的地方,过滤地标列表,只显示喜欢的地标。要增加这些特性,首先要在列表上添加一个开关,用来过滤用户喜欢的地标。在地标上添加一个星标按钮,用户可以点击它来标记这个地标为自己喜欢的。下载工程文件并且跟着下面的教程实践&n...
继续阅读 »

处理用户输入

Landmark应用中,标记喜爱的地方,过滤地标列表,只显示喜欢的地标。要增加这些特性,首先要在列表上添加一个开关,用来过滤用户喜欢的地标。在地标上添加一个星标按钮,用户可以点击它来标记这个地标为自己喜欢的。

下载工程文件并且跟着下面的教程实践


第一节 标记用户最喜欢的地标

mark-favorite

给地标列表的每一行添加一个星标用来表示用户是否标记该地标为自己喜欢的

步骤1 打开工程项目,在项目导航下选择LandmarkRow.swift文件

步骤2 在空白占位后面添加一个if表达式,if表达式判断是否当前地标是用户喜欢的,如果用户标记当前地标为喜欢就显示星标。可以在SwitUI的代码块中使用if语句来条件包含视图

步骤3 由于系统图片是矢量类型的,可以使用foregroundColor(_:)来改变它的颜色。当地标landmark的isFavorite属性为真时,星标显示,稍后会讲怎么修改属性值。

star

第二节 过滤列表

可以定制地标列表,让它只显示用户喜欢的地标,或者显示所有的地标。要实现这个功能,需要给LandmarkList视图类型添加一些状态变量。

状态(State)是一个值或者一个值的集合,会随着时间而改变,同时会影响视图的内容、行为或布局。在属性前面加上@State修饰词就是给视图添加了一个状态值

state

步骤1 选择LandmarkList.swift文件,并给LandmarkList添加一个名为showFavoritesOnly的状态,初始值设置为false

步骤2 点击Resume按钮或快捷键Command+Option+P刷新画布。当对视图进行添加或修改属性等结构性改变时,需要手动刷新画布

步骤3 代码中通过检查showFavoritesOnly属性和每一个地标的isFavorite属性值来过滤地标列表所展示的内容

state favorite

第三节 添加控件来切换状态

为了让用户控制地标列表的过滤器,需要添加一个可以修改showFavoritesOnly值的控件,传递一个绑定关系给toggle控件可以实现

一个绑定关系(binding)是对可变状态的引用。当用户点击toggle控件,从开到关或从关到开,toggle控件会通过绑定关系对应的更新视图的状态

toggle state

步骤1 创建一个嵌套的ForEach组来把地标数据转换成地标行视图。在一个列表中组合静态和动态视图,或者组合两个甚至多个不同的动态视图组,使用ForEach类型动态生成而不是给列表传入数据集合生成列表视图

步骤2 添加一个Toggle视图作为列表的每一个子视图,传入一个showFavoritesOnly的绑定关系。使用$前缀来获得一个状态变量或属性的绑定关系

步骤3 实时预览模式下,点击Toggle控件来验证过滤器的功能

toggle binding

live preview


收起阅读 »

iOS SwiftUI 创建和组合视图 2.2

第七节 子视图传入数据LandmarkDetail视图目前还是使用写死的数据进行展示,与LandmarkRow视图一样,LandmarkDetail视图及它内部的子视图也需要传入landmark数据,并使用它来进行实际的展示从LandmarkDetail的子视...
继续阅读 »

第七节 子视图传入数据

LandmarkDetail视图目前还是使用写死的数据进行展示,与LandmarkRow视图一样,LandmarkDetail视图及它内部的子视图也需要传入landmark数据,并使用它来进行实际的展示

LandmarkDetail的子视图(CircleImageMapView)开始,需要把它们都改造成为使用传入的数据进行展示,而不是在布局代码中写死数据展示

pass data

步骤1 在CircleImage.swift文件中,添加一个存储属性,命名为image。这是一种在构建SwiftUI视图中很常用的模式,常常会包裹或封装一些属性修改器。

circle image data

步骤2 更新CirleImage的预览结构体,并传入Turtle Rock这个图片进行预览

circle image preview

步骤3 在MapView.swift中添加一个coordinate属性,并使用这个属性来替换写死的经纬度坐标

map view data

步骤4 更新MapView的预览结构体,并传入每一个地标的经纬度数据

map view preview

步骤5 在LandmarkDetail.swift中添加landmark属性。

步骤6 更新LandmarkDetail预览结构体,并传入第一个地标的数据

步骤7 把对应子视图的数据传入

landmark detail

步骤8 最后调用navigationBarTitle(_:displayMode:)修改器为地标详情页展示时在导航条上设置一个标题

landmark detail preview

步骤9 在SceneDelegate.swift中把应用的根视图替换为LandmarkList。应用在模拟器中独立启动时使用SceneDelegate的根视图做为第一个展示的视图

scene delegate root view

步骤10 在LandmarkList.swift中,传入当前行的地标数据到地标详情页LandmarkDetail

landmark list data

步骤11 切换到实时预览模式下去查看从地标列表页对应的行跳转到对应地标详情页是否正常

landmark list preview

第八节 动态生成预览视图

dynamic preivew

接下来要在不同尺寸设备上展示不同的预览视图,默认情况下,预览视图会选择当前Scheme选中的设备尺寸进行渲染,可以使用previewDevice(_:)修改器来改变预览视图的设备

步骤1 改变当前预览列表,让它渲染在iPhone SE设备上。可以使用Xcode Scheme菜单上的设备名称来指定渲染设备。

iPhone SE Preview

步骤2 在列表的预览视图中,还可以把LandmarkList嵌套进入ForEach实例中,使用设备数组名作为数据。ForEach运算作用在集合类型的数据上,就和列表使用集合类型数据一样,可以在子视图使用的任何场景下使用ForEach,例如:stacklistgroup等。当元素数据是简单值类型时(例如字符串类型),可以使用\.self作为keypath去标识

preiview multiple device

步骤3 使用previewDisplayName(_:)修改器可以给预览视图添加设备标签

步骤4 可以在画布上多设置几个设备进行预览,比较不同设备下视图的展示情况

preivew multiple devices

检查是否理解

问题1 除了List外,下面哪种类型可以从集合数据中展示动态列表视图

  •  Group
  •  ForEach
  •  UITableView

问题2 可以从遵循了Identifiable协议的集合数据创建列表视图。但如果集合数据不遵循Identifiable协议,还有什么办法可以创建列表视图?

  •  在集合数据上调用map(_:)方法
  •  在集合数据上调用sorted(by:)方法
  •  给List(_:id:)类型传入集合数据的同时,使用keypath指定一个唯一标识符字段

问题3 使用什么类型才能让列表的行实现点击跳转到其它视图页面?

  •  NavigationLink
  •  UITableViewDelegate
  •  NavigationView

问题4 下面哪种方式不是用来设置预览设备的?

  •  改变活动scheme中选中的模拟器
  •  在画面设置中设置一个不同的预览设备
  •  使用previewDevice(_:)指定一个或多个预览设备
  •  连接开发机并点击设备预览按钮
收起阅读 »

iOS SwiftUI 创建和组合视图 2.1

第四节 创建地标列表使用SwiftUI列表类型可以展示平台相关的列表视图。列表的元素可以是静态的,类似于栈内部的子视图,也可以是动态生成的视图,也可以混合动态和静态的视图。步骤1 创建SwiftUI视图,命名为LandmarkList.swift步骤...
继续阅读 »

第四节 创建地标列表

使用SwiftUI列表类型可以展示平台相关的列表视图。列表的元素可以是静态的,类似于栈内部的子视图,也可以是动态生成的视图,也可以混合动态和静态的视图。

landmark list

步骤1 创建SwiftUI视图,命名为LandmarkList.swift

步骤2 用List替换默认创建的Text,并将前两个LandmarkRow实例做为列表的子元素,预览视图中会以列表的形式展示出两个地标

landmark list file create

landmark list landmark list tow rows

第五节 创建动态列表

除了单独列出列表中的每个元素外,列表还可以从一个集合中动态的生成。

landmark list dynamic

创建列表时可以传入一个集合数据和一个闭包,闭包会针对每一个数据元素返回一个视图,这个视图就是列表的行视图。

步骤1 从列表中移除两个静态指定的行视图,给列表初始化器传入landmarkData数据,列表要配合可辨别的数据类型使用。想让数据变成可辨别的数据类型有两种方法:

  1. 传入一个keypath指定数据中哪一个字段用来唯一标识这个数据元素。

  2. 让数据遵循Identifiable协议

步骤2 在闭包中返回一个LandmarkRow视图,List初始化器中指定数据集合landmarkData和唯一标识符**keypath:**\.id,这样列表就会动态生成,如下图所示

keypath identifier list data

步骤3 切换到文件Landmark.swfit,声明Landmark类型遵循Identifiable协议,因为Landmark类型已经定义了id属性,正好满足Identifiable协议,所以不需要添加其它代码

identifiable data

步骤4 现在切换回文件LandmarkList.swift,移除keypath\.id,因为landmarkData数据集合的元素已经遵循了Identifiable协议,所以在列表初始化器中可以直接使用,不需要手动标明数据的唯一标识符了



第六节 设置从列表页到详情页的页面导航

地标列表可以正常渲染展示,但是列表的元素点击后没有反应,跳转不到地标详情页。现在就要给列表添加导航能力,把列表视图嵌套到NavigationView视图中,然后把列表的每一个行视图嵌套进NavigationLink视图中,就可以建立起从地标列表视图到地标详情页的跳转。

landmark list to detail

步骤1 把动态生成的列表视图嵌套进一个NavigationView视图中

embed in navigation view

步骤2 调用navigationBarTitle(_:)修改器设置地标列表显示时的导航条标题

landmark list navigation view

步骤3 在列表的闭包中,将每一个行元素包裹在NavigationLink中返回,并指定LandmarkDetail视图为目标视图

navigation link

步骤4 切换到实时预览模式下可以直接点击地标列表的任意一行,现在就可以跳转到地标详情页了。

list navigation

identifiable list

收起阅读 »

iOS SwiftUI 创建和组合视图 2.0

创建列表和导航地标详情页视图已经创建完成,我们需要提供一种方式让用户可以查看完整的地标列表,并且可以查看每一个地标的详情下面会创建一个可以展示任何地标信息的视图,并动态生成一个可滚动列表,用户可以点击列表项去查看地标的详细信息。优化视图显示时,可以使用Xcod...
继续阅读 »

创建列表和导航

地标详情页视图已经创建完成,我们需要提供一种方式让用户可以查看完整的地标列表,并且可以查看每一个地标的详情

下面会创建一个可以展示任何地标信息的视图,并动态生成一个可滚动列表,用户可以点击列表项去查看地标的详细信息。优化视图显示时,可以使用Xcode画布来渲染多个不同设备大小下的预览视图。

下载下面的工程文件,并跟着教程一步步学习构建列表和视图间导航


第一节 了解样本数据

前面的教程中,自定义视图所展示的信息都直接被写死在代码中,这篇教程中会学习给自定义视图传入样本数据进行展示

swiftui-building-list

步骤1 打开项目导航器,选择Models->Landmark.swift文件,这个文件中声明了需要在应用中展示一个地标所需要信息的结构化名称,并通过导入landmarkData.json文件中的数据,生成一个地标信息数组。

building list model

步骤2 在项目导航器中选择Resources->landmarkData.json,在后面的教程中我们都会使用这个样本数据文件

building list sample data

步骤3 注意,之前的ContentView视图,已经被改名为LandmarkDetail了,在本教程和后面的教程中,还会创建一些其它的视图

landmark detail

第二节 创建行视图

本教程中创建的第一个视图就是用来显示每个地标的行视图,行视图把地标的相关信息存储在一个属性中,一行就可以代表一个地标,稍后就会把这些行组合成为一个列表。

swiftui-building-list-landmark-row

步骤1 创建一个名为LandmarkRow.swift的SwiftUI视图

landmark row create

步骤2 如果预览视图没有出现,可以选择菜单编辑器->画布,打开画布,并点击Resume进行预览,或者使用Command+Option+Enter快捷键调出画面,再使用Command+Option+P快捷键开始预览模式

步骤3 添加landmark属性做为LandmarkRow视图的一个存储属性。当添加landmark属性后,预览视图可能会停止工作,因为LandmarkRow视图初始化时需要有一个landmark实例。要想修复预览视图,需要修改Preview Provider

步骤4 在LandmarkRow_Previews的静态属性previews中给LandmarkRow初始化器中传入landmark参数,这个参数使用landmarkData数组的第一个元素。预览视图当前显示Hello, World

landmark row layout

步骤5 在一个HStack中嵌入一个Text

步骤6 修改这个Text,让它使用landmark属性的name字段

步骤7 在Text视图前面添加一个图片视图,在Text视图后面添加Spacer视图

landmark layout 1

第三节 自定义行预览

Xcode的画布会自动识别当前代码编辑器中遵循PreviewProvider协议的类型,并将它们渲染并展示在画面上。一个视图预览提供者(preview provider)返回一个或多个视图,这些视图可以配置不同的大小和设备型号。

可以定制从preview provider中返回的视图被渲染在何种场景下。

row preivew

步骤1 在LandmarkRow_Previews中,把landmark参数更新为landmarkData数组的第二个元素,预览视图会立即刷新反映第二个元素的渲染情况

preivew row 2

步骤2 使用previewLayout(_:)修改器设置一个行视图在列表中显示的尺寸大小。可以使用Group的方式,返回多个不同场景下的预览视图

preview layout size

步骤3 把预览的行视图包裹在Group中,把之前的第一个行视图也加进去。Group是一个容器,它可以把视图内容组织起来,Xcode会把Group内的每个子视图当作画布内一个单独的预览视图处理

preview group size

步骤4 为了简化代码,可以把previewLayout(_:)这个修改器应用到外层的Group上,Group的每一个子视图会继承自己所处环境的配置。对preivew provider的修改只会影响预览画布的表现,对实际的应用不会产生影响。

preview group coniguration


收起阅读 »

iOS SwiftUI 创建和组合视图 1.3

第六节 组合地标详情页前面我们创建了个地标详情页所需要的各种子视图元素:名称、地点、圆形图片以及位置地图,现在可以把这些视图元素组合在一起形成地标详情页的整个视图在项目工程浏览器中选择ContentView.swift文件body属性中嵌入一个VStack视图...
继续阅读 »

第六节 组合地标详情页

前面我们创建了个地标详情页所需要的各种子视图元素:名称、地点、圆形图片以及位置地图,现在可以把这些视图元素组合在一起形成地标详情页的整个视图

swiftui combine view begin

  1. 在项目工程浏览器中选择ContentView.swift文件

  2. body属性中嵌入一个VStack视图,它内部包含另一个VStack视图,内部的VStack视图又包含三个Text视图

  3. 在外层VStack的顶部添加自定义的地图视图MapView,并使用frame(width:height:)设置视图大小。当只指定高度时,宽度会自动计算为父视图的宽度,在这里就是屏幕宽度

  4. 点击Live Preview按钮进入实时预览模式,查看地图渲染情况。在实时预览模式下可以编辑视图,最新的改动也可以实时的刷新出来。

  5. MapView后面再添加一个CircleImage视图

  6. 为了让图片视图叠放在地图视图的上面,可以设置图片视图的垂直偏移量为-130,图片视图的底部内边距也为-130,这个效果就是把图片垂直上移了130,同时和下面的文字区域留出了130的空白分隔区

  7. 在外层VStack内部的最下面加上Spacer,可以让上面的视图内容顶到屏幕的上边

  8. 为了让地图的视图内容显示在状态栏的下方,可以给MapView添加edgesIgnoringSafeArea(.top)修改器,这可以让它在布局时忽略顶部的安全区域边距

swiftui combine view completed

检查是否理解

问题1 在声明自定义SwiftUI视图时,视图布局要声明的在哪里?

  •  在视图初始化器中
  •  body属性中
  •  layoutSubviews方法中

View协议中要求实现body属性,每一个SwiftUI视图都遵循View协议

问题2 代码布局的视图是以下哪个?

swiftui combine view problem2

  • swiftui combine view problem2-1
  • swiftui combine view problem2-2
  • swiftui combine view problem2-3

问题3 下面哪种方法是从body属性中返回三个视图的正确方法?

  • swiftui combine view problem3-1
  • swiftui combine view problem3-2
  • swiftui combine view problem3-3

问题4 配置视图时,下面哪种是正确使用修改器的方式?

  • swiftui combine view problem4-1
  • swiftui combine view problem4-2
  • swiftui combine view problem4-3

修改器每次都是返回一个新的对象,所以多个修改器可以通过链式调用

收起阅读 »

iOS SwiftUI 创建和组合视图 1.2

第四节 创建自定义图像视图(Image)有了地标名称、地标位置及状态视图,下一步再添加一个地标图片视图。这个图片视图将自定义遮罩(mask)、边框(border)和阴影(shadow)从控件加中拖一个Image到画布,或直接写代码到代码编辑器中步骤1 ...
继续阅读 »

第四节 创建自定义图像视图(Image)

有了地标名称、地标位置及状态视图,下一步再添加一个地标图片视图。这个图片视图将自定义遮罩(mask)、边框(border)和阴影(shadow)

从控件加中拖一个Image到画布,或直接写代码到代码编辑器中

步骤1 在项目资源文件中找到turtlerock.png图片,把它拖入资源编辑器(asset catalog editor)中,Xcode会创建一个新的图片集来存放这个图片,然后创建一个SwiftUI视图

swiftui assets catalog editor

步骤2 选择文件->新建->文件,打开模板选择器。在用户界面(User Interface)板块下,选择SwiftUI View并点击下一步,命名为CircleImage.swift,并点击创建(Create)。现在你已经准备好插入图片并修改布局来满足设计目标

swiftui create swiftui file

swiftui create circle image

步骤3 用Image替换Text,并使用turtlerock图片初始化Image视图

步骤4 添加clipShape(Circle())修改器到Image,给图片添加圆形剪切效果。Circle是一个形状,它可以被用作遮罩、也可以是圆圈,还可以是圆形填充视图。

步骤5 创建另一个灰色的圆圈并把它作为一个浮层添加到图片上,相当于给图片加了一个灰色边框

步骤6 给视图添加半径为10的阴影

swiftui turtlerock overlay

步骤7 把圆形边框的颜色改成白色,就完成了自定义图片视图的创建。

swiftui circle image completed

第五节 UIKit视图与SwiftUI视图混合使用

现在要创建一个地图视图,可以使用MapKit中的MKMapView视图类来渲染地图。要在SwiftUI中使用UIView及其子类,需要把这些UIView包裹在一个遵循UIViewRepresentable协议的SwiftUI视图中,SwiftUI中也包含适配WatchKitAppKit的类似的协议。

swiftui uikit swiftui combine

首先需要创建一个自定义视图用来容纳和显示MKMapView

步骤1 选择文件->新建->文件,选择iOS平台,选择SwiftUI View模板,并点击下一步(Next),命名文件为MapView.swift,并点击创建(Create)

步骤2 代码中导入MapKit引用,声明MapView遵循UIViewRepresentable协议。UIViewRepresentable协议要求实现两个方法UIView(context:)updateUIView(_:context:),第一个方法用来创建MKMapView,第二个方法用来配置视图响应状态变化

步骤3 替换body,用makeUIView(context:)方法来代替,创建并返回一个空的MKMapView

步骤4 创建方法updateUIView(_:context:),在方法内部设置地图视图的坐标为Turle Rock的中心。在静态模式下预览时,只会渲染swiftUI视图的部分,因为MKMapViewUIView的子类,所以需要切换到实时预览模式下才能看到地图被完全渲染出来

swiftui mapview mkmapview wrapper

步骤5 点击Live Preview(实时预览)按钮,可能需要点击Try AgainResume按钮来激活预览模式的切换。切换到实时预览模式下不久就可以看到指定地标所在的地图位置了

swiftui mkmapview live preview


收起阅读 »

iOS SwiftUI 创建和组合视图 1.1

第三节 使用栈来组合视图上一节创建了标题视图,接下来要添加一些文本视图来描述地标所在州及所在公园的名称等其它详细信息创建SwiftUI视图就是在body属性中描述视图的内容、布局及行为,但body属性只返回单个视图,这时组合多个视图时可以把它们放入一个栈中,通...
继续阅读 »

第三节 使用栈来组合视图

上一节创建了标题视图,接下来要添加一些文本视图来描述地标所在州及所在公园的名称等其它详细信息

swiftui layout stack

创建SwiftUI视图就是在body属性中描述视图的内容、布局及行为,但body属性只返回单个视图,这时组合多个视图时可以把它们放入一个栈中,通过水平、垂直、前后嵌套多个视图完成视图组合,做为一个整体在body属性中返回

这一节中,使用一个垂直栈,把标题放在包含公园详情的水平栈的上方,在水平栈中,布局公园详情相关的内容

可以使用Xcode提供的结构化布局来把视图嵌套在容器视图中

步骤1 按下Command键的同时,点击Text视图的初始化代码打开结构化编辑弹窗,然后选择把控件嵌套在垂直栈中(Embed in VStack),在栈中添加Text View控件可以从组件中直接拖进栈中完成

swiftui view embed in vertical stack

步骤2 点击Xcode右上角的+号,托动一个Text控件到指定位置,代码立即就会在编辑器中补全

步骤3 把Text视图的占位文本修改为Joshua Tree Nation Park,视图会自动调整位置布局

步骤4 设置位置控件的字体为子标题样式

swiftui inspector add text view

步骤5 设置VStack初始化参数为左对齐内部的子视图。默认情况下,栈会把内部视图在自己的主轴上居中对齐,并自动计算各子视图的间距。下一步要添加一个Text控制用来描述公园的状态,它水平排列在位置信息的右边。

swiftui vstack leadng alignment

步骤6 在画布内,command按下的同时点击位置视图,在弹出的菜单中选择嵌入到水平栈中(Embed in HStack)

步骤7 在位置控件的后面加一个公园状态的Text视图,并把占位文字改为California,字体设置为子标题样式

步骤8 为了水平布局使用整个屏幕宽度,在位置控件和公园状态控件中间添加一个Spacer控件,用来填充两个控件中间的空白部分,并把两个控件分别顶向屏幕的两侧。Spacer是一个可以伸缩的空白控件,他负责占用其它控件布局完成后剩下的所有空间。

步骤9 使用padding()修改器给地标信息内容视图整体加内边距

swiftui embed in hstack

收起阅读 »

iOS SwiftUI 创建和组合视图 1.0

创建和组合视图这个教程指导你构建一个名为Landmarks(地标)的应用。这个应用的功能是可以发现并分享你喜欢的地标。首先从创建地标详情页开始。Landmarks使用栈来按层组合图片、文本等视图元素,从而布局页面。在视图中添加地图,需要引入MapKit组件,在...
继续阅读 »

创建和组合视图

这个教程指导你构建一个名为Landmarks(地标)的应用。这个应用的功能是可以发现并分享你喜欢的地标。首先从创建地标详情页开始。

Landmarks使用栈来按层组合图片、文本等视图元素,从而布局页面。在视图中添加地图,需要引入MapKit组件,在你布局页面的过程中, Xcode可以提供实时的反馈,让你所做的改动立即转化成对应的代码实现。


第一节 创建新项目并体验画布

创建SwiftUI项目工程,体验画布、预览模式和SwiftUI模板代码

要想在Xcode中预览画布中的视图或者与画布中的视图进行交互,要求你的Mac系统版本号不低于macOS Catalina 10.15

create new project

步骤1 打开Xcode,在启动页面点击创建新工程或者在菜单中选择文件->新建->项目

create new project xcode

create new project xcode menu

步骤2 在项目模板选择器中,选择iOS作为项目平台,选项单视图应用(Single View App)作为项目模板,并点击下一步(Next)

create new project app template

步骤3 输入Landmarks作为项目名称,选择SwiftUI作为用户界面的创建方式,并点击下一步(Next),在磁盘目录下选择一个位置用来存放新创建的工程项目

create new project info

步骤4 工程创建好并打开后,在文件导航器中,选择ContentView.swift文件,可以浏览一下SwiftUI视图的组成结构。默认情况下,SwiftUI的视图文件包含两个结构体(Struct) 第一个结构体遵循View协议,描述视图的内容和布局。第二个结构体声明为第一个视图的预览视图。

步骤5 在**画布(Canvas)上,点击恢复(Resume)**按钮可以显示预览视图,也可以使用快捷键Command+Option+P

如果工程中没有出现画布(Canvas),可以选择菜单:编辑器(Editor) -> 编辑器和画布(Editor and Canvas) 打开画布进行预览

create new project completed

步骤6 在body属性内部,修改文字Hello World为其它的不同的文字,当你在改变代码的同时,预览视图也会实时的更新对应的内容变化

creating and combining views

第二节 定制文本视图(Text View)

可能通过修改代码来改变一个视图的显示样式,也可以通过检查器获取视图可修改属性,然后再写对应的代码改变样式。在创建应用的过程中,可以同时使用源码编辑器、画布或者检查器,无论当前使用的是哪一个工具编辑视图,代码会保持和这些编辑器展示的样式一致

customize text view

下面我们使用检查器来定制视图的显示样式

步骤1 在预览视图中,按下Command键的同时点击控件,会弹出一个编辑弹层,然后选择检查器(Inspect), 编辑弹层显示所有可以定制的视图属性,选中的控件不同,可以定制的属性集合也不相同

swift preview inspectror

步骤2 使用检查器把文字更改为Turtle Rock,也就是在应用中显示的第一个地标的名称

swiftui preivew inspector change text

步骤3 改变字体修改器为Title,使用系统字体修饰文字,可以自动按照用户在设备中设置的字体偏好大小进行调整。定制SwiftUI视图所调用的方法被称为视图修改器(Modifiers),修改器在原视图的基础上修改部分显示样式和属性,返回一个新的视图,这样就可以让多个修改器串连进行,形成水平方向的链式调用,或者垂直方向的堆叠调用

swiftui preview inspector change font

步骤4 手动在代码中添加foregroundColor(.green) 属性修改器,就会把文字的颜色调整为绿色。代码是决定视图样式的根本,当我们使用检查器来改变或移除一个属性修改器时,Xcode也会在代码编辑器中同步改变或移除对应的修改器代码

swiftui code change foreground color

步骤5 在代码编辑器中,按下Command的同时点击Text单词也可以属性弹窗,从中选择检查器后,再点击Color弹出菜单,选择继承(Inherited),让文字的颜色恢复成原来的黑色

swiftui code inspector resume font

步骤6 当我们移除 foregroundColor(.green) 时,Xcode会自动更新你的代码来反映视图的实际显示状况

swiftui xcode resume


收起阅读 »

iOS 知识拓展

iOS
本期概要本期话题:什么是暗时间。Tips 带来了多个内容:Fastlane 用法总结、minimumLineSpacing 与 minimumInteritemSpacing 的区别以及一个定位 RN 发热问题的过程。面试解析:本期围绕 block 的变量捕获...
继续阅读 »

本期概要

  • 本期话题:什么是暗时间。
  • Tips 带来了多个内容:Fastlane 用法总结、minimumLineSpacing 与 minimumInteritemSpacing 的区别以及一个定位 RN 发热问题的过程。
  • 面试解析:本期围绕 block 的变量捕获机制展开说明。
  • 优秀博客带来了几篇编译优化的文章。
  • 学习资料带来了一个从 0 设计计算机的视频教程,还有 Git 和正则表达式的文字教程。
  • 开发工具介绍了两个代码片段整理的相关工具。

本期话题

@zhangferry:最近在看一本书:《暗时间》,初听书名可能有些不知所云,因为这个词是作者发明的,我们来看文中对“暗时间”的解释:

看书并记住书中的东西只是记忆,并没有涉及推理,只有靠推理才能深入理解一个事物,看到别人看不到的地方,这部分推理的过程就是你的思维时间,也是人一生中占据一个显著比例的“暗时间”。你走路、买菜、洗脸洗手、坐公交、逛街、出游、吃饭、睡觉,所有这些时间都可以成为暗时间,你可以充分利用这些时间进行思考,反刍和消化平时看和读的东西,这些时间看起来微不足道,但日积月累会产生巨大的效应。

这里对于暗时间的解释是思维时间,因为思维是人的”后台线程“,我们通常注意不到它,可它却实际存在且非常重要。但按思维时间来说其适用的范围就有点窄了,大多数情况我们并不会一直保持思考。我尝试把刘未鹏关于暗时间的概念进行扩展,除思维时间外,还包括那些零碎的,可以被利用但未被利用起来的时间。“明时间”,暗时间倘若都能利用起来,那定是极佳的。

目前我有两个关于暗时间应用的实践:

1、在上下班走路过程中是思考时间。我现在换了一条上下班路线,使得步行时间更长,一趟在 15 分钟左右。这段时间,我会尝试想下今天的工作内容,规划日常任务;或者回忆最近在看的某篇文章,脑海里进行推演然后尝试复述其过程;或者仅仅观察路过的行人,想象下如果我是他们,我在另一个视角观察到的自己是什么样子。总之,让大脑活跃起来。

2、等待的过程是运动时间。等人或者等红绿灯的时候,我会尝试让自己运动起来,比如小动作像垫垫脚,大一点的动作像跳一跳、跑一跑。运动是一项反人性的事情,所以它不能规划,一规划就要跟懒惰做斗争,所以干脆就随时有空就动两下。通常这种小型的运动体验,如果突然因为要开始干正事被打断了,还会有种意犹未尽的感觉。

当然还可以有别的尝试,重要的是我们要明白和感受到暗时间这个东西,然后再想办法怎么利用它。至少在我的一些尝试中会让一些本该枯燥的时间变得更有趣了些。

开发Tips

整理编辑:zhangferry

Fastlane 用法总结

图片来源:iOS-Tips

React Native 0.59.9 引发手机发烫问题解决思路

内容贡献:yyhinbeijing

问题出现的现象是:RN 页面放置久了,或者反复操作不同的 RN 页面,手机会变得很烫,并且不会自动降温,要杀掉进程才会降温,版本是 0.59.9,几乎不同手机不同手机系统版本均遇到了这个问题,可以确定是 RN 导致的,但具体哪里导致的呢,以下是通过代码注释定位问题的步骤,后面数值为 CPU 占用率:

1、原生:7.2%

2、无网络无 Flatlist:7.2%

3、网络 + FlatList :100%+

4、网络 + 无 FlatList:100%+

5、去掉 loading:2.6% — 30%,会降低

6、网络和 FlatList 全部放开,只关闭 loading 最低 7.2%,能降低,最高 63%

首先是发现网络导致 CPU 占用率很高,然后网络注释掉 RNLoading (我们自写的 loading 动画),发现内存占用不高了。就断定是 RNLoading 问题,查询发现:我们每次点击 tab 都会加载 loading,而 loading 又是动画,这样大量的动画引发内存问题。虽不是特例问题,但发现、定位、解决问题的过程仍然是有借鉴意义的,即确定范围,然后不断缩小范围。

面试解析

整理编辑:反向抽烟师大小海腾

面试解析会按照主题讲解一些高频面试题,本期面试题是 block 的变量捕获机制

block 的变量捕获机制

block 的变量捕获机制,是为了保证 block 内部能够正常访问外部的变量。

1、对于全局变量,不会捕获到 block 内部,访问方式为直接访问;作用域的原因,全局变量哪里都可以直接访问,所以不用捕获。

2、对于局部变量,外部不能直接访问,所以需要捕获。

  • auto 类型的局部变量(我们定义出来的变量,默认都是 auto 类型,只是省略了),block 内部会自动生成一个同类型成员变量,用来存储这个变量的值,访问方式为值传递auto 类型的局部变量可能会销毁,其内存会消失,block 将来执行代码的时候不可能再去访问那块内存,所以捕获其值。由于是值传递,我们修改 block 外部被捕获变量的值,不会影响到 block 内部捕获的变量值。
  • static 类型的局部变量,block 内部会自动生成一个同类型成员变量,用来存储这个变量的地址,访问方式为指针传递。static 变量会一直保存在内存中, 所以捕获其地址即可。相反,由于是指针传递,我们修改 block 外部被捕获变量的值,会影响到 block 内部捕获的变量值。
  • 对于对象类型的局部变量,block 会连同它的所有权修饰符一起捕获。
    • 如果 block 是在栈上,将不会对对象产生强引用
    • 如果 block 被拷贝到堆上,将会调用 block 内部的 copy(__funcName_block_copy_num)函数,copy 函数内部又会调用 assign(_Block_object_assign)函数,assign 函数将会根据变量的所有权修饰符做出相应的操作,形成强引用(retain)或者弱引用。
    • 如果 block 从堆上移除,也就是被释放的时候,会调用 block 内部的 dispose(_Block_object_dispose)函数,dispose 函数会自动释放引用的变量(release)。
  • 对于 __block(可用于解决 block 内部无法修改 auto 变量值的问题) 修饰的变量,编译器会将 __block 变量包装成一个 __Block_byref_varName_num 对象。它的内存管理几乎等同于访问对象类型的 auto 变量,但还是有差异。
    • 如果 block 是在栈上,将不会对 __block 变量产生强引用
    • 如果 block 被拷贝到堆上,将会调用 block 内部的 copy 函数,copy 函数内部又会调用 assign 函数,assign 函数将会直接对 __block 变量形成强引用(retain)。
    • 如果 block 从堆上移除,也就是被释放的时候,会调用 block 内部的 dispose 函数,dispose 函数会自动释放引用的 __block 变量(release)。
  • 被 __block修饰的对象类型的内存管理:
    • 如果 __block 变量是在栈上,将不会对指向的对象产生强引用
    • 如果 __block 变量被拷贝到堆上,将会调用 __block 变量内部的 copy(__Block_byref_id_object_copy)函数,copy 函数内部会调用 assign 函数,assign 函数又会根据变量的所有权修饰符做出相应的操作,形成强引用(retain)或者弱引用。(注意:这里仅限于 ARC 下会 retain,MRC 下不会 retain,所以在 MRC 下还可以通过 __block 解决循环引用的问题)
    • 如果 __block 变量从堆上移除,会调用 __block 变量内部的 dispose 函数,dispose 函数会自动释放指向的对象(release)。

掌握了 block 的变量捕获机制,我们就能更好的应对内存管理,避免因使用不当造成内存泄漏。

常见的 block 循环引用为:self(obj) -> block -> self(obj)。这里 block 强引用了 self 是因为对于对象类型的局部变量,block 会连同它的所有权修饰符一起捕获,而对象的默认所有权修饰符为 __strong。

self.block = ^{
NSLog(@"%@", self);
};
复制代码

为什么这里说 self 是局部变量?因为 self 是 OC 方法的一个隐式参数。

为了避免循环引用,我们可以使用 __weak 解决,这里 block 将不再持有 self。

__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"%@", weakSelf);
};
复制代码

为了避免在 block 调用过程中 self 提前释放,我们可以使用 __strong 在 block 执行过程中持有 self,这就是所谓的 Weak-Strong-Dance。

__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"%@", strongSelf);
};
复制代码

当然,我们平常用的比较多的还是 @weakify(self) 和 @strongify(self) 啦。

@weakify(self);
self.block = ^{
@strongify(self);
NSLog(@"%@", self);
};
复制代码

如果你使用的是 RAC 的 Weak-Strong-Dance,你还可以这样:

@weakify(self, obj1, obj2);
self.block = ^{
@strongify(self, obj1, obj2);
NSLog(@"%@", self);
};
复制代码

如果是嵌套的 block:

@weakify(self);
self.block = ^{
@strongify(self);
self.block2 = ^{
@strongify(self);
NSLog(@"%@", self);
}
};
复制代码

你是否会疑问,为什么内部不需要再写 @weakify(self) ?这个问题就留给你自己去思考和解决吧!

相比于简单的相互循环引用,block 造成的大环引用更需要你足够细心以及敏锐的洞察力,比如:

TYAlertView *alertView = [TYAlertView alertViewWithTitle:@"TYAlertView" message:@"This is a message, the alert view containt text and textfiled. "];
[alertView addAction:[TYAlertAction actionWithTitle:@"取消" style:TYAlertActionStyleCancle handler:^(TYAlertAction *action) {
NSLog(@"%@-%@", self, alertView);
}]];
self.alertController = [TYAlertController alertControllerWithAlertView:alertView preferredStyle:TYAlertControllerStyleAlert];
[self presentViewController:alertController animated:YES completion:nil];
复制代码

这里循环引用有两处:

  1. self -> alertController -> alertView -> handlerBlock -> self
  2. alertView -> handlerBlock -> alertView

避免循环引用:

TYAlertView *alertView = [TYAlertView alertViewWithTitle:@"TYAlertView" message:@"This is a message, the alert view containt text and textfiled. "];
@weakify(self, alertView);
[alertView addAction:[TYAlertAction actionWithTitle:@"取消" style:TYAlertActionStyleCancle handler:^(TYAlertAction *action) {
@strongify(self, alertView);
NSLog(@"%@-%@", self, alertView);
}]];
self.alertController = [TYAlertController alertControllerWithAlertView:alertView preferredStyle:TYAlertControllerStyleAlert];
[self presentViewController:alertController animated:YES completion:nil];
复制代码

另外再和你提一个小知识点,当我们在 block 内部直接使用 _variable 时,编译器会给我们警告:Block implicitly retains self; explicitly mention 'self' to indicate this is intended behavior

原因是 block 中直接使用 _variable 会导致 block 隐式的强引用 self。Xcode 认为这可能会隐式的导致循环引用,从而给开发者带来困扰,而且如果不仔细看的话真的不太好排查,笔者之前就因为这个循环引用找了半天,还拉上了我导师一起查找原因。所以警告我们要显式的在 block 中使用 self,以达到 block 显式 retain 住 self 的目的。改用 self->_variable 或者 self.variable

你可能会觉得这种困扰没什么,如果你使用 @weakify 和 @strongify 那确实不会造成循环引用,因为 @strongify 声明的变量名就是 self。那如果你使用 weak typeof(self) weak_self = self; 和 strong typeof(weak_self) strong_self = weak_self 呢?

优秀博客

整理编辑:皮拉夫大王在此我是熊大

本期主题:编译优化

1、iOS编译过程的原理和应用 -- 来自 CSDN:黄文臣

做编译优化前,先了解下编译原理吧!该作者通过 iOS 的视角,白话了编译原理,通俗易懂。

2、Xcode编译疾如风系列 - 分析编译耗时 -- 来自腾讯社区:小菜与老鸟

在进行编译速度优化前,一个合适的分析工具是必要的,它能告诉你哪部分编译时间较长,让你发现问题,从而解决问题,本文介绍了几种分析编译耗时的方式,助你分析构建时间。该作者还有其他相关姊妹篇,建议前往阅读。

3、iOS 微信编译速度优化分享 -- 来自云+社区:微信终端开发团队

文章对编译优化由浅入深做了介绍。作者首先介绍了常见的现有方案,利用现有方案以及精简代码、将模板基类改为虚基类、使用 PCH 等方案做了部分优化。文章精彩的部分在于作者并没有止步于此,而是从编译原理入手,结合量化手段,分析出编译耗时的瓶颈。在找到问题的瓶颈后,作者尝试人工进行优化,但是效率较低。最终在 IWYU 基础上,增加了 ObjC 语言的支持,高效地处理了一部分多余的头文件。

4、iOS编译速度如何稳定提高10倍以上之一 -- 来自掘金:Mr_Coder

美柚 iOS 的编译提效历程。作者对常见的优化做了分析,列举了各自的优缺点。有想做编译优化的可以参考这篇文章了解一下。对于业界的主流技术方案,别的技术文章往往只介绍优点,对方案的缺点谈的不够彻底。这篇文章从实践者的角度阐述了常见方案的优缺点,很有参考价值。文章介绍了双私有源二进制组件并与 ccache 做了对比,最后列出了方案支持的功能点。

5、iOS编译速度如何稳定提高10倍以上之二 -- 来自掘金:Mr_Coder

作为上文的姊妹篇,本文详细介绍了双私有源二进制组件的方案细节以及使用方法。对该方案感兴趣的可以关注下。

6、一款可以让大型iOS工程编译速度提升50%的工具 -- 来自美团技术团队:思琦 旭陶 霜叶

本文主要介绍了如何通过优化头文件搜索机制来实现编译提速,全源码编译效率提升 45%。文中涉及很多知识点,比如 hmap 文件的作用、Build Phases - Headers 中的 Public,Private,Project 各自是什么作用。文中详细分析了 podspec 创建头文件产物的逻辑以及 Use Header Map 失效的原因。干货比较多,可能得多读几遍。

学习资料

整理编辑:Mimosa

从 0 到 1 设计一台计算机

地址:https://www.bilibili.com/video/BV1wi4y157D3

来自 Ele实验室 的计算机组成原理课程,该系列视频主要目的是让大家对「计算机是如何工作的」有个较直观的认识,做为深入学习计算机科学的一个启蒙。观看该系列视频最好有一些数字电路和模拟电路的基础知识,Ele 实验室同时也有关于 数电 和 模电 的基础知识介绍供大家参考。

Git Cheat Sheet 中文版

地址:https://github.com/flyhigher139/Git-Cheat-Sheet

Git Cheat Sheet 让你不用再去记所有的 git 命令!对新手友好,可以用于查阅简单的 git 命令。

正则表达式 30 分钟入门教程

地址:https://deerchao.cn/tutorials/regex/regex.htm

30 分钟内让你明白正则表达式是什么,并对它有一些基本的了解。别被那些复杂的表达式吓倒,只要跟着我一步一步来,你会发现正则表达式其实并没有想象中的那么困难。除了作为入门教程之外,本文还试图成为可以在日常工作中使用的正则表达式语法参考手册。

工具推荐

整理编辑:zhangferry

SnippetsLab

地址:http://www.renfei.org/snippets-lab/

软件状态:$9.99

软件介绍

一款强大的代码片段管理工具,从此告别手动复制粘贴,SnippetsLab 的设计更符合 Apple 的交互习惯,支持导航栏快速操作。另外还可以同步 Github Gist 内容,使用 iCloud 备份。

CodeExpander

地址:https://codeexpander.com/

软件状态:普通版免费,高级版付费

软件介绍

专为开发者开发的一个集输入增强、代码片段管理工具,支持跨平台,支持云同步(Github/码云)。免费版包含 90% 左右功能,相对 SnippetsLab 来说其适用范围更广泛,甚至包括一些日常文本的片段处理。

关于我们

iOS 摸鱼周报,主要分享开发过程中遇到的经验教训、优质的博客、高质量的学习资料、实用的开发工具等。周报仓库在这里:https://github.com/zhangferry/iOSWeeklyLearning ,如果你有好的的内容推荐可以通过 issue 的方式进行提交。另外也可以申请成为我们的常驻编辑,一起维护这份周报。另可关注公众号:iOS成长之路,后台点击进群交流,联系我们,获取更多内容。


收起阅读 »

iOS RXSwift 9.1

iOS
学习资源书籍RxSwift - By Raywenderlich视频Learning Path: RxSwift from Start to Finish - By Realm 团队RxSwift in Practice - By...
继续阅读 »

学习资源

书籍

视频

博客

教程

开源项目

  • CleanArchitectureRxSwift - Example of Clean Architecture of iOS app using RxSwift

  • PinPlace - Routing app. Build with MVVM+RxSwift and ❤️.

  • RxTodo - iOS Todo Application using RxSwift and ReactorKit

  • Drrrible - Dribbble for iOS using ReactorKit

  • RxMarbles - RxMarbles iOS app


关于本文档

问题反馈

如果你发现文档存在问题,可以通过以下任意一种方式将问题反馈给作者:

  • (推荐)在存在问题页面,点击左上方的编辑页面按钮,对文档进行修正,最后提交 Pull Request
  • 前往 文档库 提 issues, 并注明文档哪些地方存在问题
  • 加入到 RxSwift QQ 交流群: 871293356,将问题反馈给整理人
  • 通过邮件将问题反馈给整理人:beeth0vendev@gmail.com

文档更新日志

文档变更将被记录在此文件内。


2.0.0

19年5月21日(RxSwift 5)


1.2.0

18年2月15日

  • 纠正错别字
  • 给 retry 操作符加入演示代码
  • 给 replay 操作符加入演示代码
  • 给 connect 操作符加入演示代码
  • 给 publish 操作符加入演示代码
  • 给 reduce 操作符加入演示代码
  • 给 skipUntil 操作符加入演示代码
  • 给 skipWhile 操作符加入演示代码
  • 给 skip 操作符加入演示代码

1.1.0

17年12月7日


1.0.0

17年10月18日(RxSwift 4)


0.2.0

17年10月9日

0.1.1

17年9月18日


0.1.0

17年9月4日


0.0.1

17年9月1日(RxSwift 3.6.1)

收起阅读 »

iOS RXSwift 8.1

iOS
 RxSwift 生态系统RxCocoa 给 UI框架 提供了 Rx 支持,让我们能够使用按钮点击序列,输入框当前文本序列等。不过 RxCocoa 也只是 RxSwift...
继续阅读 »

 RxSwift 生态系统

RxCocoa 给 UI框架 提供了 Rx 支持,让我们能够使用按钮点击序列,输入框当前文本序列等。不过 RxCocoa 也只是 RxSwift 生态系统 中的一员。RxSwift 生态系统还给其他框架提供了 Rx 支持:

RxDataSources

书写 tabelView 或 collectionView 的数据源是一件非常繁琐的事情,有一大堆的代理方法需要被执行。 RxDataSources 可以帮助你简化这一过程。你可以用它来布局多层级的列表页,并且它还可以提供动画支持。

你只需要几行代码就可以布局一个多 Section 的 tabelView

let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Int>>()
Observable.just([SectionModel(model: "title", items: [1, 2, 3])])
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)

你可以点击 RxDataSources 来了解更多信息。


RxAlamofire

Alamofire 是一个非常流行的网络请求框架。RxAlamofire 是用 RxSwift 封装的 Alamofire。它使得网络请求调用变得更加平滑,处理请求结果变得更简洁,更高效:

let stringURL = ""

// 使用 NSURLSession
let session = NSURLSession.sharedSession()

_ = session.rx
.json(.get, stringURL)
.observeOn(MainScheduler.instance)
.subscribe { print($0) }

// 使用 Alamofire 引擎

_ = json(.get, stringURL)
.observeOn(MainScheduler.instance)
.subscribe { print($0) }

// 使用 Alamofire manager

let manager = Manager.sharedInstance

_ = manager.rx.json(.get, stringURL)
.observeOn(MainScheduler.instance)
.subscribe { print($0) }

// URLHTTPResponse + Validation + String
_ = manager.rx.request(.get, stringURL)
.flatMap {
$0
.validate(statusCode: 200 ..< 300)
.validate(contentType: ["text/json"])
.rx.string()
}
.observeOn(MainScheduler.instance)
.subscribe { print($0) }

你可以点击 RxAlamofire 来了解更多信息。


RxRealm

Realm 是一个十分前卫的跨平台数据库,他想要替换 Core Data 和 SQLiteRxRealm 是用 RxSwift 封装的 Realm。它使我们可以用 Rx 的方式监听数据变化,或者将数据写入数据库。

监听数据:

let realm = try! Realm()
let laps = realm.objects(Lap.self)

Observable.collection(from: laps)
.map {
laps in "\(laps.count) laps"
}
.subscribe(onNext: { text in
print(text)
})

添加数据:

let realm = try! Realm()
let messages = [Message("hello"), Message("world")]

Observable.from(messages)
.subscribe(realm.rx.add())

删除数据:

let realm = try! Realm()
let messages = realm.objects(Message.self)

Observable.from(messages)
.subscribe(realm.rx.delete())

你可以点击 RxRealm 来了解更多信息。


 ReactiveX 生态系统

我们之前提到过 RxSwift 是 Rx 的 Swift 版本。而 ReactiveX(简写: Rx)是一个跨平台框架。它不仅可以用来写 iOS ,你还可以用它来写 AndroidWeb 前端后台。并且每个平台都和 RxSwift 一样有一套 Rx 生态系统。Rx 支持多种编程语言,如:Swift,Java,JS,C#,Scala,Kotlin,Go 等。只要你掌握了其中一门语言,你很容易就能够熟悉其他的语言。

Android

RxJava 是 Android 平台上非常流行的响应式编程框架,它也是 Rx 的 Java 版本。

我们还是用输入验证来做演示:

iOS(RxSwift) 版:

...
let usernameValid = usernameOutlet.rx.text.orEmpty
.map { $0.characters.count >= minimalUsernameLength }
.share(replay: 1)

let passwordValid = passwordOutlet.rx.text.orEmpty
.map { $0.characters.count >= minimalPasswordLength }
.share(replay: 1)

let everythingValid = Observable
.combineLatest(usernameValid, passwordValid) { $0 && $1 }
.share(replay: 1)

usernameValid
.bind(to: passwordOutlet.rx.isEnabled)
.disposed(by: disposeBag)

usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

passwordValid
.bind(to: passwordValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

everythingValid
.bind(to: doSomethingOutlet.rx.isEnabled)
.disposed(by: disposeBag)
...

Android(RxJava) 版:

...
final Observable<Boolean> usernameValid = RxTextView.textChanges(usernameEditText)
.map(text -> text.length() >= minimalUsernameLength)
.compose(Rx.shareReplay(1));

final Observable<Boolean> passwordValid = RxTextView.textChanges(usernameEditText)
.map(text -> text.length() >= minimalPasswordLength)
.compose(Rx.shareReplay(1));

final Observable<Boolean> everythingValid = Observable
.combineLatest(usernameValid, passwordValid, (isUsernameValid, isPasswordValid) -> isUsernameValid && isPasswordValid)
.compose(Rx.shareReplay(1));

disposables.add(usernameValid
.subscribe(RxView.enabled(passwordEditText)));

disposables.add(usernameValid
.subscribe(RxView.visibility(usernameValidTextView)));

disposables.add(passwordValid
.subscribe(RxView.visibility(passwordValidTextView)));

disposables.add(everythingValid
.subscribe(RxView.enabled(doSomethingButton)));
...

这两段代码的逻辑是一样的,一个是 iOS(RxSwift) 版本,另一个是 Android(RxJava) 版本。仔细对比以后,你会发现它们的书写方式都是差不多的。

这样一来,你就可以用同一套逻辑来写跨平台应用,而且这个应用是纯原生的。这不仅节省了开发时间,而且还提升了 App 的质量


Web 前端

RxJS 是 Web 前端 平台上非常流行的响应式编程框架,它也是 Rx 的 JS 版本。而且主流的前端框架都提供了 Rx 支持,如:jQueryRxJS-DOMAngularJSRxEmber等。

下面这个例子是用 RxJS 写的,它和 GitHub 搜索 十分相似,只不过他搜索的是维基百科:

var $input = $('#input'),
$results = $('#results');

Rx.Observable.fromEvent($input, 'keyup')
.map(e => e.target.value)
.filter(text => text.length > 2)
.throttle(500 /* ms */);
.distinctUntilChanged();
.flatMapLatest(searchWikipedia);
.subscribe(data => {
var res = data[1];
$results.empty();
$.each(res, (_, value) => $('<li>' + value + '</li>').appendTo($results));
}, error => {
$results.empty();
$('<li>Error: ' + error + '</li>').appendTo($results);
});

当用户输入一个稳定的关键字后,向维基百科请求搜索结果,然后显示出来。

即便你没有学过 Web 前端开发,但是只要你熟悉 Rx ,以上代码你也能够看懂。


总结

由于 Rx 支持多种后台语言,如:JavaJSGo。所以你也可以用它来写后台。

如果你已经能够熟练使用 RxSwift ,那么你就已经具有某种“天赋”,这种“天赋”可以帮助你快速上手其他平台。你只需要学习一些和平台相关的知识,就可以写出交互相当复杂的应用程序。因为你的 Rx 技巧是可以跨平台复用的。

另外,你的学习效率也会更高,如果你在 RxSwift 中学到了某些技巧,那么这个技巧通常也可以被应用到 Android 或者其他的平台。如果你在 RxJava 中学到了某些技巧,那么这个技巧通常也可以被应用到 iOS 平台。因此,你的学习资源也就不再局限于 RxSwift,你还可以浏览其他平台上关于 Rx 的教程。

下一章将提供一些关于 RxSwift 的学习资源


收起阅读 »

iOS RXSwift 7.4

iOS
     作者Jeon Suyeol 是 ReactorKit 的作者。他也发布了一些富有创造性的框架,如 Then,URLNavigator,SwiftyImage&n...
继续阅读 »

Swift CocoaPods Platform Build Status Codecov CocoaDocs

作者

Jeon Suyeol 是 ReactorKit 的作者。他也发布了一些富有创造性的框架,如 ThenURLNavigatorSwiftyImage 以及一些开源项目 RxTodoDrrrible。他也是多个组织的成员 RxSwiftCommunityMoyaSwiftKorea

介绍

ReactorKit 结合了 Flux 和响应式编程。用户行为和页面状态都是通过序列相互传递。这些序列都是单向的:页面只能发出用户行为,然而反应器(Reactor)只能发出状态。


View

View 用于展示数据。ViewController 和 Cell 都可以看作是 ViewView 将用户输入绑定到 Action 的序列上,同时将页面状态绑定到 UI 组件上。

定义一个 View 只需要让它遵循 View 协议即可。然后你的类将自动获得一个 reactor 属性。这个属性应该在 View 的外面被设置:

class ProfileViewController: UIViewController, View {
var disposeBag = DisposeBag()
}

profileViewController.reactor = UserViewReactor() // 注入 reactor

当 reactor 属性被设置时,bind(reactor:) 方法就会被调用。执行这个方法来进行用户输入绑定和状态输出绑定。

func bind(reactor: ProfileViewReactor) {
// action (View -> Reactor)
refreshButton.rx.tap.map { Reactor.Action.refresh }
.bind(to: reactor.action)
.disposed(by: self.disposeBag)

// state (Reactor -> View)
reactor.state.map { $0.isFollowing }
.bind(to: followButton.rx.isSelected)
.disposed(by: self.disposeBag)
}

Reactor

Reactor 是与 UI 相互独立的一层,主要负责状态管理。Reactor 最重要的作用就是将业务逻辑从 View 中抽离。每一个 View 都有对应的 Reactor 并且将所有的逻辑代理给 ReactorReactor 不需要依赖 View,所以它很容易被测试。

遵循 Reactor 协议即可定义一个 Reactor。这个协议需要定义三个类型:ActionMutation 和 State。它也需要一个 initialState 属性。

class ProfileViewReactor: Reactor {
// 代表用户行为
enum Action {
case refreshFollowingStatus(Int)
case follow(Int)
}

// 代表附加作用
enum Mutation {
case setFollowing(Bool)
}

// 代表页面状态
struct State {
var isFollowing: Bool = false
}

let initialState: State = State()
}

Action 代表用户行为,State 代表页面状态。Mutation 是 Action 和 State 的桥梁。Reactor 通过两步将用户行为序列转换为页面状态序列mutate() 和 reduce()

mutate()

mutate() 接收一个 Action ,然后创建一个 Observable<Mutation>

func mutate(action: Action) -> Observable<Mutation>

每种附加作用,如,异步操作,API 调用都是在这个方法内执行。

func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .refreshFollowingStatus(userID): // receive an action
return UserAPI.isFollowing(userID) // create an API stream
.map { (isFollowing: Bool) -> Mutation in
return Mutation.setFollowing(isFollowing) // convert to Mutation stream
}

case let .follow(userID):
return UserAPI.follow()
.map { _ -> Mutation in
return Mutation.setFollowing(true)
}
}
}

reduce()

reduce() 通过旧的 State 以及 Mutation 创建一个新的 State

func reduce(state: State, mutation: Mutation) -> State

这个方法是一个纯函数。它将同步的返回一个 State。不会产生其他的作用。

func reduce(state: State, mutation: Mutation) -> State {
var state = state // create a copy of the old state
switch mutation {
case let .setFollowing(isFollowing):
state.isFollowing = isFollowing // manipulate the state, creating a new state
return state // return the new state
}
}

transform()

transform() 转换每一种序列。有三种转换方法:

func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>

执行这些方法可以转换或者组合其他的序列。例如,transform(mutation:) 最适合用来组合一个全局事件,生成一个 Mutation 序列。

它也可用来做调试:

func transform(action: Observable<Action>) -> Observable<Action> {
return action.debug("action") // Use RxSwift's debug() operator
}

示例

下一节将用 Github Search 来演示如何使用 ReactorKit


Github Search(示例)

我们还是使用Github 搜索来演示如何使用 ReactorKit。这个例子是使用 ReactorKit 重构以后的版本,你可以在这里下载这个例子

简介

这个 App 主要有这样几个交互:

  • 输入搜索关键字,显示搜索结果
  • 当用户滑动列表到底部时,加载下一页
  • 当用户点击某一条搜索结果是,用 Safari 打开链接

Action

Action 用于描叙用户行为:

enum Action {
case updateQuery(String?)
case loadNextPage
}
  • updateQuery 搜索关键字变更
  • loadNextPage 触发加载下页

Mutation

Mutation 用于描状态变更:

enum Mutation {
case setQuery(String?)
case setRepos([String], nextPage: Int?)
case appendRepos([String], nextPage: Int?)
case setLoadingNextPage(Bool)
}
  • setQuery 更新搜索关键字
  • setRepos 更新搜索结果
  • appendRepos 添加搜索结果
  • setLoadingNextPage 设置是否正在加载下一页

State

这个是用于描述当前状态:

struct State {
var query: String?
var repos: [String] = []
var nextPage: Int?
var isLoadingNextPage: Bool = false
}
  • query 搜索关键字
  • repos 搜索结果
  • nextPage 下一页页数
  • isLoadingNextPage 是否正在加载下一页

我们通常会使用这些状态来控制页面布局。


mutate()

将 Action 转换为 Mutation

func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .updateQuery(query):
return Observable.concat([
// 1) set current state's query (.setQuery)
Observable.just(Mutation.setQuery(query)),

// 2) call API and set repos (.setRepos)
self.search(query: query, page: 1)
// cancel previous request when the new `.updateQuery` action is fired
.takeUntil(self.action.filter(isUpdateQueryAction))
.map { Mutation.setRepos($0, nextPage: $1) },
])

case .loadNextPage:
guard !self.currentState.isLoadingNextPage else { return Observable.empty() } // prevent from multiple requests
guard let page = self.currentState.nextPage else { return Observable.empty() }
return Observable.concat([
// 1) set loading status to true
Observable.just(Mutation.setLoadingNextPage(true)),

// 2) call API and append repos
self.search(query: self.currentState.query, page: page)
.takeUntil(self.action.filter(isUpdateQueryAction))
.map { Mutation.appendRepos($0, nextPage: $1) },

// 3) set loading status to false
Observable.just(Mutation.setLoadingNextPage(false)),
])
}
}
  • 当用户输入一个新的搜索关键字时,就从服务器请求 repos,然后转换成更新 repos 事件(Mutation)。
  • 当用户触发加载下页时,就从服务器请求 repos,然后转换成添加 repos 事件。

reduce()

reduce() 通过旧的 State 以及 Mutation 创建一个新的 State

func reduce(state: State, mutation: Mutation) -> State {
switch mutation {
case let .setQuery(query):
var newState = state
newState.query = query
return newState

case let .setRepos(repos, nextPage):
var newState = state
newState.repos = repos
newState.nextPage = nextPage
return newState

case let .appendRepos(repos, nextPage):
var newState = state
newState.repos.append(contentsOf: repos)
newState.nextPage = nextPage
return newState

case let .setLoadingNextPage(isLoadingNextPage):
var newState = state
newState.isLoadingNextPage = isLoadingNextPage
return newState
}
}
  • setQuery 更新搜索关键字
  • setRepos 更新搜索结果,以及下一页页数
  • appendRepos 添加搜索结果,以及下一页页数
  • setLoadingNextPage 设置是否正在加载下一页

bind(reactor:)

在 View 层进行用户输入绑定和状态输出绑定:

func bind(reactor: GitHubSearchViewReactor) {
// Action
searchBar.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.map { Reactor.Action.updateQuery($0) }
.bind(to: reactor.action)
.disposed(by: disposeBag)

tableView.rx.contentOffset
.filter { [weak self] offset in
guard let `self` = self else { return false }
guard self.tableView.frame.height > 0 else { return false }
return offset.y + self.tableView.frame.height >= self.tableView.contentSize.height - 100
}
.map { _ in Reactor.Action.loadNextPage }
.bind(to: reactor.action)
.disposed(by: disposeBag)

// State
reactor.state.map { $0.repos }
.bind(to: tableView.rx.items(cellIdentifier: "cell")) { indexPath, repo, cell in
cell.textLabel?.text = repo
}
.disposed(by: disposeBag)

// View
tableView.rx.itemSelected
.subscribe(onNext: { [weak self, weak reactor] indexPath in
guard let `self` = self else { return }
self.tableView.deselectRow(at: indexPath, animated: false)
guard let repo = reactor?.currentState.repos[indexPath.row] else { return }
guard let url = URL(string: "https://github.com/\(repo)") else { return }
let viewController = SFSafariViewController(url: url)
self.present(viewController, animated: true, completion: nil)
})
.disposed(by: disposeBag)
}
  • 将用户更改输入关键字行为绑定到用户行为上
  • 将用户要求加载下一页行为绑定到用户行为上
  • 将搜索结果输出到列表页上
  • 当用户点击某一条搜索结果是,用 Safari 打开链接

整体结构

我们已经了解 ReactorKit 每一个组件的功能了,现在我们看一下完整的核心代码:

GitHubSearchViewReactor.swift

final class GitHubSearchViewReactor: Reactor {
enum Action {
case updateQuery(String?)
case loadNextPage
}

enum Mutation {
case setQuery(String?)
case setRepos([String], nextPage: Int?)
case appendRepos([String], nextPage: Int?)
case setLoadingNextPage(Bool)
}

struct State {
var query: String?
var repos: [String] = []
var nextPage: Int?
var isLoadingNextPage: Bool = false
}

let initialState = State()

func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .updateQuery(query):
return Observable.concat([
// 1) set current state's query (.setQuery)
Observable.just(Mutation.setQuery(query)),

// 2) call API and set repos (.setRepos)
self.search(query: query, page: 1)
// cancel previous request when the new `.updateQuery` action is fired
.takeUntil(self.action.filter(isUpdateQueryAction))
.map { Mutation.setRepos($0, nextPage: $1) },
])

case .loadNextPage:
guard !self.currentState.isLoadingNextPage else { return Observable.empty() } // prevent from multiple requests
guard let page = self.currentState.nextPage else { return Observable.empty() }
return Observable.concat([
// 1) set loading status to true
Observable.just(Mutation.setLoadingNextPage(true)),

// 2) call API and append repos
self.search(query: self.currentState.query, page: page)
.takeUntil(self.action.filter(isUpdateQueryAction))
.map { Mutation.appendRepos($0, nextPage: $1) },

// 3) set loading status to false
Observable.just(Mutation.setLoadingNextPage(false)),
])
}
}

func reduce(state: State, mutation: Mutation) -> State {
switch mutation {
case let .setQuery(query):
var newState = state
newState.query = query
return newState

case let .setRepos(repos, nextPage):
var newState = state
newState.repos = repos
newState.nextPage = nextPage
return newState

case let .appendRepos(repos, nextPage):
var newState = state
newState.repos.append(contentsOf: repos)
newState.nextPage = nextPage
return newState

case let .setLoadingNextPage(isLoadingNextPage):
var newState = state
newState.isLoadingNextPage = isLoadingNextPage
return newState
}
}

...
}

GitHubSearchViewController.swift

class GitHubSearchViewController: UIViewController, View {
@IBOutlet var searchBar: UISearchBar!
@IBOutlet var tableView: UITableView!

var disposeBag = DisposeBag()

override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset.top = 44 // search bar height
tableView.scrollIndicatorInsets.top = tableView.contentInset.top
}

func bind(reactor: GitHubSearchViewReactor) {
// Action
searchBar.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.map { Reactor.Action.updateQuery($0) }
.bind(to: reactor.action)
.disposed(by: disposeBag)

tableView.rx.contentOffset
.filter { [weak self] offset in
guard let `self` = self else { return false }
guard self.tableView.frame.height > 0 else { return false }
return offset.y + self.tableView.frame.height >= self.tableView.contentSize.height - 100
}
.map { _ in Reactor.Action.loadNextPage }
.bind(to: reactor.action)
.disposed(by: disposeBag)

// State
reactor.state.map { $0.repos }
.bind(to: tableView.rx.items(cellIdentifier: "cell")) { indexPath, repo, cell in
cell.textLabel?.text = repo
}
.disposed(by: disposeBag)

// View
tableView.rx.itemSelected
.subscribe(onNext: { [weak self, weak reactor] indexPath in
guard let `self` = self else { return }
self.tableView.deselectRow(at: indexPath, animated: false)
guard let repo = reactor?.currentState.repos[indexPath.row] else { return }
guard let url = URL(string: "https://github.com/\(repo)") else { return }
let viewController = SFSafariViewController(url: url)
self.present(viewController, animated: true, completion: nil)
})
.disposed(by: disposeBag)
}
}

这是使用 ReactorKit 重构以后的 Github Search。ReactorKit 分层非常详细,分工也是非常明确的。当你在处理大型应用程序时,这可以帮助你更好的管理代码。

收起阅读 »

iOS RXSwift 7.3

iOS
RxFeedback    作者Krunoslav Zaher 是 RxFeedback 的作者。他也是 RxSwift 的创始人以及 ReactiveX 组织...
继续阅读 »

RxFeedback

Travis CI platforms pod Carthage compatible Swift Package Manager compatible

作者

Krunoslav Zaher 是 RxFeedback 的作者。他也是 RxSwift 的创始人以及 ReactiveX 组织 的核心成员。他有 16 年以上的编程经验( VR 引擎,BPM 系统,移动端应用程序,机器人等),最近在研究响应式编程。

介绍

RxSwift 最简单的架构

typealias Feedback<State, Event> = (Observable<State>) -> Observable<Event>

public static func system<State, Event>(
initialState: State,
reduce: @escaping (State, Event)
-> State,
feedback: Feedback<State, Event>...
) -> Observable<State>

为什么?

  • 直接
    • 已经发生 -> Event
    • 即将发生 -> Request
    • 执行 Request -> Feedback loop
  • 声明式
    • 首先系统行为被明确声明出来,然后在调用 subscribe 后开始运作 => 编译时就保证了不会有“未处理状态”
  • 容易调试
    • 大多数逻辑是 纯函数,可以通过 xCode 调试器调试,或者将命令打印出来
  • 适用于任何级别
    • 整个系统
    • 应用程序(state 被储存在数据库中,CoreData, Firebase, Realm)
    • view controller (state 被储存在 system 操作符)
    • 在 feedback loop 中(feedback loop 中 调用另一个 system 操作符)
  • 容易做依赖注入
  • 易测试
    • Reducer 是 纯函数,只需调用他并断言结果即可
    • 伴随 附加作用 的测试 -> TestScheduler
  • 可以处理循环依赖
  • 完全从附加作用中分离业务逻辑
    • 业务逻辑可以在不同平台之间转换

示例

Observable.system(
initialState: 0,
reduce: { (state, event) -> State in
switch event {
case .increment:
return state + 1
case .decrement:
return state - 1
}
},
scheduler: MainScheduler.instance,
feedback:
// UI is user feedback
bind(self) { me, state -> Bindings<Event> in
let subscriptions = [
state.map(String.init).bind(to: me.label.rx.text)
]

let events = [
me.plus.rx.tap.map { Event.increment },
me.minus.rx.tap.map { Event.decrement }
]

return Bindings(
subscriptions: subscriptions,
events: events
)
}
)

这是一个简单计数的例子,只是用于演示 RxFeedback 架构。

State

系统状态用 State 表示:

typealias State = Int
  • 这里的状态就是计数的数值

Event

事件用 Event 表示:

enum Event {
case increment
case decrement
}
  • increment 增加数值事件
  • decrement 减少数值事件

当产生 Event 时更新状态:

Observable.system(
initialState: 0,
reduce: { (state, event) -> State in
switch event {
case .increment:
return state + 1
case .decrement:
return state - 1
}
},
scheduler: MainScheduler.instance,
feedback: ...
)
  • increment 状态数值加一
  • decrement 状态数值减一

Feedback Loop

状态输出到 UI 页面上,或者将 UI 事件输入到反馈循环里面去:

Observable.system(
initialState: 0,
reduce: { ... },
scheduler: MainScheduler.instance,
feedback:
// UI is user feedback
bind(self) { me, state -> Bindings<Event> in
let subscriptions = [
state.map(String.init).bind(to: me.label.rx.text)
]

let events = [
me.plus.rx.tap.map { Event.increment },
me.minus.rx.tap.map { Event.decrement }
]

return Bindings(
subscriptions: subscriptions,
events: events
)
}
)
  • 将状态数值用 label 显示出来
  • 将增加按钮的点击,作为增加数值事件传入
  • 将减少按钮的点击,作为减少数值事件传入

安装

CocoaPods

CocoaPods 是一个 Cocoa 项目的依赖管理工具。你可以通过以下命令安装他:

$ gem install cocoapods

将 RxFeedback 整合到项目中来,你需要在 Podfile 中指定他:

pod 'RxFeedback', '~> 3.0'

然后运行以下命令:

$ pod install

Carthage

Carthage 是一个分散式依赖管理工具,他将构建你的依赖并提供二进制框架。

你可以通过以下 Homebrew 命令安装 Carthage:

$ brew update
$ brew install carthage

将 RxFeedback 整合到项目中来,你需要在 Cartfile 中指定他:

github "NoTests/RxFeedback" ~> 3.0

运行 carthage update 去构建框架,然后将 RxFeedback.framework 拖入到 Xcode 项目中来。由于 RxFeedback 对 RxSwift 和 RxCocoa 有依赖,所以你也需要将 RxSwift.framework 和 RxCocoa.framework 拖入到 Xcode 项目中来。

Swift Package Manager

Swift Package Manager 是一个自动分发 Swift 代码的工具,他已经被集成到 Swift 编译器中。

一旦你配置好了 Swift 包,添加 RxFeedback 就非常简单了,你只需要将他添加到文件 Package.swift 的 dependencies 的值中。

dependencies: [
.package(url: "https://github.com/NoTests/RxFeedback.swift.git", majorVersion: 1)
]

与其他架构的区别

  • Elm - 非常相似,feedback loop 用作 附加作用, 而不是 Cmd, 要执行的 附加作用 被编码到 state 中,并且通过 feedback loop 完成请求
  • Redux - 也很像,不过采用 feedback loops 而不是 middleware
  • Redux-Observable - observables 观察状态,与视图和状态之间的 middleware
  • Cycle.js - 一言难尽 :),请咨询 @andrestaltz
  • MVVM - 将状态和 附加作用 分离,而且不需要 View

示例

下一节将用 Github Search 来演示如何使用 RxFeedback

Github Search(示例)

这个例子是我们经常会遇见的Github 搜索。它是使用 RxFeedback 重构以后的版本,你可以在这里下载这个例子

简介

这个 App 主要有这样几个交互:

  • 输入搜索关键字,显示搜索结果
  • 当请求时产生错误,就给出错误提示
  • 当用户滑动列表到底部时,加载下一页

State

这个是用于描述当前状态:

fileprivate struct State {
var search: String {
didSet { ... }
}
var nextPageURL: URL?
var shouldLoadNextPage: Bool
var results: [Repository]
var lastError: GitHubServiceError?
}

...

extension State {
var loadNextPage: URL? { return ... }
}

我们这个例子(Github 搜索) 就有这样几个状态:

  • search 搜索关键字
  • nextPageURL 下一页的 URL
  • shouldLoadNextPage 是否可以加载下一页
  • results 搜索结果
  • lastError 搜索时产生的错误
  • loadNextPage 加载下一页的触发

我们通常会使用这些状态来控制页面布局。

或者,用被请求的状态,触发另外一个事件。


Event

这个是用于描述所产生的事件:

fileprivate enum Event {
case searchChanged(String)
case response(SearchRepositoriesResponse)
case startLoadingNextPage
}

事件通常会使状态发生变化,然后产生一个新的状态

extension State {
...
static func reduce(state: State, event: Event) -> State {
switch event {
case .searchChanged(let search):
var result = state
result.search = search
result.results = []
return result
case .startLoadingNextPage:
var result = state
result.shouldLoadNextPage = true
return result
case .response(.success(let response)):
var result = state
result.results += response.repositories
result.shouldLoadNextPage = false
result.nextPageURL = response.nextURL
result.lastError = nil
return result
case .response(.failure(let error)):
var result = state
result.shouldLoadNextPage = false
result.lastError = error
return result
}
}
}

当发生某个事件时,更新当前状态

  • searchChanged 搜索关键字变更

    将搜索关键字更新成当前值,并且清空搜索结果。

  • startLoadingNextPage 触发加载下页

    允许加载下一页,如果下一页的 URL 存在,就加载下一页。

  • response(.success(...)) 搜索结果返回成功

    将搜索结果加入到对应的数组里面去,然后将相关状态更新。

  • response(.failure(...)) 搜索结果返回失败

    保存错误状态。


Feedback Loop

Feedback Loop 是用来引入附加作用的。

例如,你可以将状态输出到 UI 页面上,或者将 UI 事件输入到反馈循环里面去:

override func viewDidLoad() {
super.viewDidLoad()

...

Driver.system(
initialState: State.empty,
reduce: State.reduce,
feedback:
// UI, user feedback
UI.bind(self) { me, state in
let subscriptions = [
state.map { $0.search }.drive(me.searchText!.rx.text),
state.map { $0.lastError?.displayMessage }.drive(me.status!.rx.textOrHide),
state.map { $0.results }.drive(searchResults.rx.items(cellIdentifier: "repo"))(configureRepository),
state.map { $0.loadNextPage?.description }.drive(me.loadNextPage!.rx.textOrHide),
]
let events = [
me.searchText!.rx.text.orEmpty.changed.asDriver().map(Event.searchChanged),
triggerLoadNextPage(state)
]
return UI.Bindings(subscriptions: subscriptions, events: events)
},
// NoUI, automatic feedback
...
)
.drive()
.disposed(by: disposeBag)
}

这里定义的 subscriptions 就是如何将状态输出到 UI 页面上,而 events 则是如何将 UI 事件输入到反馈循环里面去。


被请求的状态

被请求的状态是,用于发出异步请求,以事件的形式返回结果。

override func viewDidLoad() {
super.viewDidLoad()
...

Driver.system(
initialState: State.empty,
reduce: State.reduce,
feedback:
// UI, user feedback
... ,
// NoUI, automatic feedback
react(query: { $0.loadNextPage }, effects: { resource in
return URLSession.shared.loadRepositories(resource: resource)
.asDriver(onErrorJustReturn: .failure(.offline))
.map(Event.response)
})
)
.drive()
.disposed(by: disposeBag)
}

这里 loadNextPage 就是被请求的状态,当状态 loadNextPage 不为 nil 时,就请求加载下一页。


整体结构

现在我们看一下这个例子整体结构,这样可以帮助你理解这种架构。然后,以下是核心代码:

...
fileprivate struct State {
var search: String {
didSet {
if search.isEmpty {
self.nextPageURL = nil
self.shouldLoadNextPage = false
self.results = []
self.lastError = nil
return
}
self.nextPageURL = URL(string: "https://api.github.com/search/repositories?q=\(search.URLEscaped)")
self.shouldLoadNextPage = true
self.lastError = nil
}
}

var nextPageURL: URL?
var shouldLoadNextPage: Bool
var results: [Repository]
var lastError: GitHubServiceError?
}

fileprivate enum Event {
case searchChanged(String)
case response(SearchRepositoriesResponse)
case startLoadingNextPage
}

// transitions
extension State {
static var empty: State {
return State(search: "", nextPageURL: nil, shouldLoadNextPage: true, results: [], lastError: nil)
}
static func reduce(state: State, event: Event) -> State {
switch event {
case .searchChanged(let search):
var result = state
result.search = search
result.results = []
return result
case .startLoadingNextPage:
var result = state
result.shouldLoadNextPage = true
return result
case .response(.success(let response)):
var result = state
result.results += response.repositories
result.shouldLoadNextPage = false
result.nextPageURL = response.nextURL
result.lastError = nil
return result
case .response(.failure(let error)):
var result = state
result.shouldLoadNextPage = false
result.lastError = error
return result
}
}
}

// queries
extension State {
var loadNextPage: URL? {
return self.shouldLoadNextPage ? self.nextPageURL : nil
}
}

class GithubPaginatedSearchViewController: UIViewController {
@IBOutlet weak var searchText: UISearchBar?
@IBOutlet weak var searchResults: UITableView?
@IBOutlet weak var status: UILabel?
@IBOutlet weak var loadNextPage: UILabel?

private let disposeBag = DisposeBag()

override func viewDidLoad() {
super.viewDidLoad()

let searchResults = self.searchResults!

searchResults.register(UITableViewCell.self, forCellReuseIdentifier: "repo")

let triggerLoadNextPage: (Driver<State>) -> Driver<Event> = { state in
return state.flatMapLatest { state -> Driver<Event> in
if state.shouldLoadNextPage {
return Driver.empty()
}

return searchResults.rx.nearBottom.map { _ in Event.startLoadingNextPage }
}
}

func configureRepository(_: Int, repo: Repository, cell: UITableViewCell) {
cell.textLabel?.text = repo.name
cell.detailTextLabel?.text = repo.url.description
}

let bindUI: (Driver<State>) -> Driver<Event> = UI.bind(self) { me, state in
let subscriptions = [
state.map { $0.search }.drive(me.searchText!.rx.text),
state.map { $0.lastError?.displayMessage }.drive(me.status!.rx.textOrHide),
state.map { $0.results }.drive(searchResults.rx.items(cellIdentifier: "repo"))(configureRepository),
state.map { $0.loadNextPage?.description }.drive(me.loadNextPage!.rx.textOrHide),
]
let events = [
me.searchText!.rx.text.orEmpty.changed.asDriver().map(Event.searchChanged),
triggerLoadNextPage(state)
]
return UI.Bindings(subscriptions: subscriptions, events: events)
}

Driver.system(
initialState: State.empty,
reduce: State.reduce,
feedback:
// UI, user feedback
bindUI,
// NoUI, automatic feedback
react(query: { $0.loadNextPage }, effects: { resource in
return URLSession.shared.loadRepositories(resource: resource)
.asDriver(onErrorJustReturn: .failure(.offline))
.map(Event.response)
})
)
.drive()
.disposed(by: disposeBag)
}
}
...

这是使用 RxFeedback 重构以后的 Github Search。你可以对比一下使用 ReactorKit 重构以后的 Github Search 两者有许多相似之处。

收起阅读 »

iOS RXSwift 7.2

iOS
Github Signup这是一个模拟用户注册的程序,你可以在这里下载这个例子。简介这个 App 主要有这样几个交互:当用户输入户名时,验证用户名是否有效,是否已被占用,将验证结果显示出来。当用户输入密码时,验证密码是否有效,将验证结果显示出来。当用户输入重复...
继续阅读 »

Github Signup

这是一个模拟用户注册的程序,你可以在这里下载这个例子


简介

这个 App 主要有这样几个交互:

  • 当用户输入户名时,验证用户名是否有效,是否已被占用,将验证结果显示出来。
  • 当用户输入密码时,验证密码是否有效,将验证结果显示出来。
  • 当用户输入重复密码时,验证重复密码是否相同,将验证结果显示出来。
  • 当所有验证都有效时,注册按钮才可点击。
  • 当点击注册按钮后发起注册请求(模拟),然后将结果显示出来。

Service

// GitHub 网络服务
protocol GitHubAPI {
func usernameAvailable(_ username: String) -> Observable<Bool>
func signup(_ username: String, password: String) -> Observable<Bool>
}

// 输入验证服务
protocol GitHubValidationService {
func validateUsername(_ username: String) -> Observable<ValidationResult>
func validatePassword(_ password: String) -> ValidationResult
func validateRepeatedPassword(_ password: String, repeatedPassword: String) -> ValidationResult
}

// 弹框服务
protocol Wireframe {
func open(url: URL)
func promptFor<Action: CustomStringConvertible>(_ message: String, cancelAction: Action, actions: [Action]) -> Observable<Action>
}

这里需要集成三个服务:

  • GitHubAPI 提供 GitHub 网络服务
  • GitHubValidationService 提供输入验证服务
  • Wireframe 提供弹框服务

这个例子目前只提供了这三个服务,实际上这一层还可以包含其他的一些服务,例如:数据库,定位,蓝牙...


ViewModel

ViewModel 需要集成这些服务,并且将用户输入,转换为状态输出:

class GithubSignupViewModel1 {

// 输出
let validatedUsername: Observable<ValidationResult>
let validatedPassword: Observable<ValidationResult>
let validatedPasswordRepeated: Observable<ValidationResult>
let signupEnabled: Observable<Bool>
let signedIn: Observable<Bool>
let signingIn: Observable<Bool>

// 输入 -> 输出
init(input: ( // 输入
username: Observable<String>,
password: Observable<String>,
repeatedPassword: Observable<String>,
loginTaps: Observable<Void>
),
dependency: ( // 服务
API: GitHubAPI,
validationService: GitHubValidationService,
wireframe: Wireframe
)
) {
...

validatedUsername = ...

validatedPassword = ...

validatedPasswordRepeated = ...

...

self.signingIn = ...

...

signedIn = ...

signupEnabled = ...
}
}

集成服务:

  • API GitHub 网络服务
  • validationService 输入验证服务
  • wireframe 弹框服务

输入:

  • username 输入的用户名
  • password 输入的密码
  • repeatedPassword 重复输入的密码
  • loginTaps 点击登录按钮

输出:

  • validatedUsername 用户名校验结果
  • validatedPassword 密码校验结果
  • validatedPasswordRepeated 重复密码校验结果
  • signupEnabled 是否允许登录
  • signedIn 登录结果
  • signingIn 是否正在登录

在 init 方法内部,将输入转换为输出。


ViewController

ViewController 主要负责数据绑定:

...
class GitHubSignupViewController1 : ViewController {
@IBOutlet weak var usernameOutlet: UITextField!
@IBOutlet weak var usernameValidationOutlet: UILabel!

@IBOutlet weak var passwordOutlet: UITextField!
@IBOutlet weak var passwordValidationOutlet: UILabel!

@IBOutlet weak var repeatedPasswordOutlet: UITextField!
@IBOutlet weak var repeatedPasswordValidationOutlet: UILabel!

@IBOutlet weak var signupOutlet: UIButton!
@IBOutlet weak var signingUpOulet: UIActivityIndicatorView!

override func viewDidLoad() {
super.viewDidLoad()

let viewModel = GithubSignupViewModel1(
input: (
username: usernameOutlet.rx.text.orEmpty.asObservable(),
password: passwordOutlet.rx.text.orEmpty.asObservable(),
repeatedPassword: repeatedPasswordOutlet.rx.text.orEmpty.asObservable(),
loginTaps: signupOutlet.rx.tap.asObservable()
),
dependency: (
API: GitHubDefaultAPI.sharedAPI,
validationService: GitHubDefaultValidationService.sharedValidationService,
wireframe: DefaultWireframe.shared
)
)

// bind results to {
viewModel.signupEnabled
.subscribe(onNext: { [weak self] valid in
self?.signupOutlet.isEnabled = valid
self?.signupOutlet.alpha = valid ? 1.0 : 0.5
})
.disposed(by: disposeBag)

viewModel.validatedUsername
.bind(to: usernameValidationOutlet.rx.validationResult)
.disposed(by: disposeBag)

viewModel.validatedPassword
.bind(to: passwordValidationOutlet.rx.validationResult)
.disposed(by: disposeBag)

viewModel.validatedPasswordRepeated
.bind(to: repeatedPasswordValidationOutlet.rx.validationResult)
.disposed(by: disposeBag)

viewModel.signingIn
.bind(to: signingUpOulet.rx.isAnimating)
.disposed(by: disposeBag)

viewModel.signedIn
.subscribe(onNext: { signedIn in
print("User signed in \(signedIn)")
})
.disposed(by: disposeBag)
//}

let tapBackground = UITapGestureRecognizer()
tapBackground.rx.event
.subscribe(onNext: { [weak self] _ in
self?.view.endEditing(true)
})
.disposed(by: disposeBag)
view.addGestureRecognizer(tapBackground)
}
}

用户行为传入给 ViewModel

  • username 将用户名输入框的当前文本传入
  • password 将密码输入框的当前文本传入
  • ...

将 ViewModel 的输出状态显示出来:

  • validatedUsername 用对应的 label 将用户名验证结果显示出来
  • validatedPassword 用对应的 label 将密码验证结果显示出来
  • ...

整体结构

以下是全部的核心代码:

// ViewModel
class GithubSignupViewModel1 {
// outputs {

let validatedUsername: Observable<ValidationResult>
let validatedPassword: Observable<ValidationResult>
let validatedPasswordRepeated: Observable<ValidationResult>

// Is signup button enabled
let signupEnabled: Observable<Bool>

// Has user signed in
let signedIn: Observable<Bool>

// Is signing process in progress
let signingIn: Observable<Bool>

// }

init(input: (
username: Observable<String>,
password: Observable<String>,
repeatedPassword: Observable<String>,
loginTaps: Observable<Void>
),
dependency: (
API: GitHubAPI,
validationService: GitHubValidationService,
wireframe: Wireframe
)
) {
let API = dependency.API
let validationService = dependency.validationService
let wireframe = dependency.wireframe

/**
Notice how no subscribe call is being made.
Everything is just a definition.

Pure transformation of input sequences to output sequences.
*/


validatedUsername = input.username
.flatMapLatest { username in
return validationService.validateUsername(username)
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(.failed(message: "Error contacting server"))
}
.share(replay: 1)

validatedPassword = input.password
.map { password in
return validationService.validatePassword(password)
}
.share(replay: 1)

validatedPasswordRepeated = Observable.combineLatest(input.password, input.repeatedPassword, resultSelector: validationService.validateRepeatedPassword)
.share(replay: 1)

let signingIn = ActivityIndicator()
self.signingIn = signingIn.asObservable()

let usernameAndPassword = Observable.combineLatest(input.username, input.password) { ($0, $1) }

signedIn = input.loginTaps.withLatestFrom(usernameAndPassword)
.flatMapLatest { (username, password) in
return API.signup(username, password: password)
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(false)
.trackActivity(signingIn)
}
.flatMapLatest { loggedIn -> Observable<Bool> in
let message = loggedIn ? "Mock: Signed in to GitHub." : "Mock: Sign in to GitHub failed"
return wireframe.promptFor(message, cancelAction: "OK", actions: [])
// propagate original value
.map { _ in
loggedIn
}
}
.share(replay: 1)

signupEnabled = Observable.combineLatest(
validatedUsername,
validatedPassword,
validatedPasswordRepeated,
signingIn.asObservable()
) { username, password, repeatPassword, signingIn in
username.isValid &&
password.isValid &&
repeatPassword.isValid &&
!signingIn
}
.distinctUntilChanged()
.share(replay: 1)
}
}

// ViewController
class GitHubSignupViewController1 : ViewController {
@IBOutlet weak var usernameOutlet: UITextField!
@IBOutlet weak var usernameValidationOutlet: UILabel!

@IBOutlet weak var passwordOutlet: UITextField!
@IBOutlet weak var passwordValidationOutlet: UILabel!

@IBOutlet weak var repeatedPasswordOutlet: UITextField!
@IBOutlet weak var repeatedPasswordValidationOutlet: UILabel!

@IBOutlet weak var signupOutlet: UIButton!
@IBOutlet weak var signingUpOulet: UIActivityIndicatorView!

override func viewDidLoad() {
super.viewDidLoad()

let viewModel = GithubSignupViewModel1(
input: (
username: usernameOutlet.rx.text.orEmpty.asObservable(),
password: passwordOutlet.rx.text.orEmpty.asObservable(),
repeatedPassword: repeatedPasswordOutlet.rx.text.orEmpty.asObservable(),
loginTaps: signupOutlet.rx.tap.asObservable()
),
dependency: (
API: GitHubDefaultAPI.sharedAPI,
validationService: GitHubDefaultValidationService.sharedValidationService,
wireframe: DefaultWireframe.shared
)
)

// bind results to {
viewModel.signupEnabled
.subscribe(onNext: { [weak self] valid in
self?.signupOutlet.isEnabled = valid
self?.signupOutlet.alpha = valid ? 1.0 : 0.5
})
.disposed(by: disposeBag)

viewModel.validatedUsername
.bind(to: usernameValidationOutlet.rx.validationResult)
.disposed(by: disposeBag)

viewModel.validatedPassword
.bind(to: passwordValidationOutlet.rx.validationResult)
.disposed(by: disposeBag)

viewModel.validatedPasswordRepeated
.bind(to: repeatedPasswordValidationOutlet.rx.validationResult)
.disposed(by: disposeBag)

viewModel.signingIn
.bind(to: signingUpOulet.rx.isAnimating)
.disposed(by: disposeBag)

viewModel.signedIn
.subscribe(onNext: { signedIn in
print("User signed in \(signedIn)")
})
.disposed(by: disposeBag)
//}

let tapBackground = UITapGestureRecognizer()
tapBackground.rx.event
.subscribe(onNext: { [weak self] _ in
self?.view.endEditing(true)
})
.disposed(by: disposeBag)
view.addGestureRecognizer(tapBackground)
}
}

这里还有一个 Driver 版的演示代码,有兴趣的同学可以了解一下。

收起阅读 »

iOS RXSwift 7.1

iOS
MVVMMVVM 是 Model-View-ViewModel 的简写。如果你已经对 MVC 非常熟悉了,那么上手 MVVM 也是非常容易的。MVCMVC 是 Model...
继续阅读 »

MVVM

MVVM 是 Model-View-ViewModel 的简写。如果你已经对 MVC 非常熟悉了,那么上手 MVVM 也是非常容易的。


MVC

MVC 是 Model-View-Controller 的简写。MVC 主要有三层:

  • Model 数据层,读写数据,保存 App 状态
  • View 页面层,和用户交互,向用户显示页面,反馈用户行为
  • ViewController 逻辑层,更新数据,或者页面,处理业务逻辑

MVC 可以帮助你很好的将数据,页面,逻辑的代码分离开来。使得每一层相对独立。这样你就能够将一些可复用的功能抽离出来,化繁为简。只不过,一旦 App 的交互变复杂,你就会发现 ViewController 将变得十分臃肿。大量代码被添加到控制器中,使得控制器负担过重。此时,你就需要想办法将控制器里面的代码进一步地分离出来,对 APP 进行重新分层。而 MVVM 就是一种进阶的分层方案。


MVVM

MVVM 和 MVC 十分相识。只不过他的分层更加详细:

  • Model 数据层,读写数据,保存 App 状态
  • View 页面层,提供用户输入行为,并且显示输出状态
  • ViewModel 逻辑层,它将用户输入行为,转换成输出状态
  • ViewController 主要负责数据绑定

没错,ViewModel 现在是逻辑层,而控制器只需要负责数据绑定。如此一来控制器的负担就减轻了许多。并且 ViewModel 与控制器以及页面相独立。那么,你就可以跨平台使用它。你也可以很容易地测试它。


示例

这里我们将用 MVVM 来重构输入验证

重构前:

class SimpleValidationViewController : ViewController {

...

override func viewDidLoad() {
super.viewDidLoad()

...

let usernameValid = usernameOutlet.rx.text.orEmpty
.map { $0.characters.count >= minimalUsernameLength }
.share(replay: 1)

let passwordValid = passwordOutlet.rx.text.orEmpty
.map { $0.characters.count >= minimalPasswordLength }
.share(replay: 1)

let everythingValid = Observable.combineLatest(
usernameValid,
passwordValid
) { $0 && $1 }
.share(replay: 1)

usernameValid
.bind(to: passwordOutlet.rx.isEnabled)
.disposed(by: disposeBag)

usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

passwordValid
.bind(to: passwordValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

everythingValid
.bind(to: doSomethingOutlet.rx.isEnabled)
.disposed(by: disposeBag)

doSomethingOutlet.rx.tap
.subscribe(onNext: { [weak self] in self?.showAlert() })
.disposed(by: disposeBag)
}

...

}

ViewModel

ViewModel 将用户输入行为,转换成输出的状态:

class SimpleValidationViewModel {

// 输出
let usernameValid: Observable<Bool>
let passwordValid: Observable<Bool>
let everythingValid: Observable<Bool>

// 输入 -> 输出
init(
username: Observable<String>,
password: Observable<String>
) {

usernameValid = username
.map { $0.characters.count >= minimalUsernameLength }
.share(replay: 1)

passwordValid = password
.map { $0.characters.count >= minimalPasswordLength }
.share(replay: 1)

everythingValid = Observable.combineLatest(usernameValid, passwordValid) { $0 && $1 }
.share(replay: 1)

}
}

输入:

  • username 输入的用户名
  • password 输入的密码

输出:

  • usernameValid 用户名是否有效
  • passwordValid 密码是否有效
  • everythingValid 所有输入是否有效

在 init 方法内部,将输入转换为输出。


ViewController

ViewController 主要负责数据绑定:

class SimpleValidationViewController : ViewController {

...

private var viewModel: SimpleValidationViewModel!

override func viewDidLoad() {
super.viewDidLoad()

...

viewModel = SimpleValidationViewModel(
username: usernameOutlet.rx.text.orEmpty.asObservable(),
password: passwordOutlet.rx.text.orEmpty.asObservable()
)

viewModel.usernameValid
.bind(to: passwordOutlet.rx.isEnabled)
.disposed(by: disposeBag)

viewModel.usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

viewModel.passwordValid
.bind(to: passwordValidOutlet.rx.isHidden)
.disposed(by: disposeBag)

viewModel.everythingValid
.bind(to: doSomethingOutlet.rx.isEnabled)
.disposed(by: disposeBag)

doSomethingOutlet.rx.tap
.subscribe(onNext: { [weak self] in self?.showAlert() })
.disposed(by: disposeBag)
}

...

}

输入:

  • username 将输入的用户名传入 ViewModel
  • password 将输入的密码传入 ViewModel

输出:

  • usernameValid 用用户名是否有效,来控制提示语是否隐藏,密码输入框是否可用
  • passwordValid 用密码是否有效,来控制提示语是否隐藏
  • everythingValid 用两者是否同时有效,来控制按钮是否可点击

当 App 的交互变复杂时,你仍然可以保持控制器结构清晰。这样可以大大的提升代码可读性。将来代码维护起来也就会容易许多了。


示例

下一节将用 Github Signup 来演示如何使用 MVVM

注意⚠️:这里介绍的 MVVM 并不是严格意义上的 MVVM。但我们通常都管它叫 MVVM,而且它配合 RxSwift 使用起来非常方便。如需了解什么是严格意义上的 MVVM,请参考微软的 The MVVM Pattern

RxSwift 常用架构

RxSwift 是一个响应式编程的基础框架,它并不会强制要求你使用某种架构。它和多个应用程序架构完美适配,这一章将介绍几个常用的架构:

收起阅读 »

iOS RXSwift 6.2

iOS
Calculator - 计算器1 + 2 + 3 = 6这是一个计算器应用程序,你可以在这里下载这个例子。简介这里的计算器是用响应式编程写的,而且它还用到了 RxFeedback 架构。它比较适合有经验的 RxSwift 使用者...
继续阅读 »

Calculator - 计算器

1 + 2 + 3 = 6

这是一个计算器应用程序,你可以在这里下载这个例子


简介

这里的计算器是用响应式编程写的,而且它还用到了 RxFeedback 架构。它比较适合有经验的 RxSwift 使用者学习。接下来我们就来介绍一下这个应用程序是如何实现的。


整体结构

class CalculatorViewController: ViewController {

@IBOutlet weak var lastSignLabel: UILabel!
@IBOutlet weak var resultLabel: UILabel!

@IBOutlet weak var allClearButton: UIButton!
@IBOutlet weak var changeSignButton: UIButton!
@IBOutlet weak var percentButton: UIButton!

@IBOutlet weak var divideButton: UIButton!
@IBOutlet weak var multiplyButton: UIButton!
@IBOutlet weak var minusButton: UIButton!
@IBOutlet weak var plusButton: UIButton!
@IBOutlet weak var equalButton: UIButton!

@IBOutlet weak var dotButton: UIButton!

@IBOutlet weak var zeroButton: UIButton!
@IBOutlet weak var oneButton: UIButton!
@IBOutlet weak var twoButton: UIButton!
@IBOutlet weak var threeButton: UIButton!
@IBOutlet weak var fourButton: UIButton!
@IBOutlet weak var fiveButton: UIButton!
@IBOutlet weak var sixButton: UIButton!
@IBOutlet weak var sevenButton: UIButton!
@IBOutlet weak var eightButton: UIButton!
@IBOutlet weak var nineButton: UIButton!

override func viewDidLoad() {
let commands: Observable<CalculatorCommand> = Observable.merge([
allClearButton.rx.tap.map { _ in .clear },

changeSignButton.rx.tap.map { _ in .changeSign },
percentButton.rx.tap.map { _ in .percent },

divideButton.rx.tap.map { _ in .operation(.division) },
multiplyButton.rx.tap.map { _ in .operation(.multiplication) },
minusButton.rx.tap.map { _ in .operation(.subtraction) },
plusButton.rx.tap.map { _ in .operation(.addition) },

equalButton.rx.tap.map { _ in .equal },

dotButton.rx.tap.map { _ in .addDot },

zeroButton.rx.tap.map { _ in .addNumber("0") },
oneButton.rx.tap.map { _ in .addNumber("1") },
twoButton.rx.tap.map { _ in .addNumber("2") },
threeButton.rx.tap.map { _ in .addNumber("3") },
fourButton.rx.tap.map { _ in .addNumber("4") },
fiveButton.rx.tap.map { _ in .addNumber("5") },
sixButton.rx.tap.map { _ in .addNumber("6") },
sevenButton.rx.tap.map { _ in .addNumber("7") },
eightButton.rx.tap.map { _ in .addNumber("8") },
nineButton.rx.tap.map { _ in .addNumber("9") }
])

let system = Observable.system(
CalculatorState.initial,
accumulator: CalculatorState.reduce,
scheduler: MainScheduler.instance,
feedback: { _ in commands }
)
.debug("calculator state")
.share(replay: 1)

system.map { $0.screen }
.bind(to: resultLabel.rx.text)
.disposed(by: disposeBag)

system.map { $0.sign }
.bind(to: lastSignLabel.rx.text)
.disposed(by: disposeBag)
}

func formatResult(_ result: String) -> String {
if result.hasSuffix(".0") {
return result.substring(to: result.index(result.endIndex, offsetBy: -2))
} else {
return result
}
}
}

首先合成出一个命令序列,它是通过按钮点击转换过来的:

let commands: Observable<CalculatorCommand> = Observable.merge([
allClearButton.rx.tap.map { _ in .clear },

changeSignButton.rx.tap.map { _ in .changeSign },
percentButton.rx.tap.map { _ in .percent },

divideButton.rx.tap.map { _ in .operation(.division) },
multiplyButton.rx.tap.map { _ in .operation(.multiplication) },
minusButton.rx.tap.map { _ in .operation(.subtraction) },
plusButton.rx.tap.map { _ in .operation(.addition) },

equalButton.rx.tap.map { _ in .equal },

dotButton.rx.tap.map { _ in .addDot },

zeroButton.rx.tap.map { _ in .addNumber("0") },
oneButton.rx.tap.map { _ in .addNumber("1") },
twoButton.rx.tap.map { _ in .addNumber("2") },
threeButton.rx.tap.map { _ in .addNumber("3") },
fourButton.rx.tap.map { _ in .addNumber("4") },
fiveButton.rx.tap.map { _ in .addNumber("5") },
sixButton.rx.tap.map { _ in .addNumber("6") },
sevenButton.rx.tap.map { _ in .addNumber("7") },
eightButton.rx.tap.map { _ in .addNumber("8") },
nineButton.rx.tap.map { _ in .addNumber("9") }
])

通过使用 map 方法将按钮点击事件转换为对应的命令。如:将 allClearButton 点击事件转换为清除命令,将 plusButton 点击事件转换为相加命令,将 oneButton 点击事件转换为添加数字1命令。最后使用 merge 操作符将这些命令合并。于是就得到了我们所需要的命令序列

几乎每个页面都是有状态的。我们通过命令序列来对状态进行修改,然后产生一个新的状态。例如,刚进页面后,点击了按钮 1 。那么初始状态为 0,在执行添加数字1命令后,状态就更新为 1。通过这种变换方式,就可以生成一个状态序列

let system = Observable.system(
CalculatorState.initial,
accumulator: CalculatorState.reduce,
scheduler: MainScheduler.instance,
feedback: { _ in commands }
)
.debug("calculator state")
.share(replay: 1)

命令序列触发,对页面状态进行更新,在用更新后的状态组成一个序列。这就是我们所需要的状态序列。接下来我们用这个状态序列来控制页面显示:

system.map { $0.screen }
.bind(to: resultLabel.rx.text)
.disposed(by: disposeBag)

system.map { $0.sign }
.bind(to: lastSignLabel.rx.text)
.disposed(by: disposeBag)

用 state.screen 来控制 resultLabel 的显示内容。用 state.sign 来控制 lastSignLabel 的显示内容。


Calculator

控制器主要负责数据绑定,而整个计算器的大脑在 Calculator.swift 文件内。

State:

这个页面主要有三种状态:

enum CalculatorState {
case oneOperand(screen: String)
case oneOperandAndOperator(operand: Double, operator: Operator)
case twoOperandsAndOperator(operand: Double, operator: Operator, screen: String)
}
  • oneOperand 一个操作数,例如:进入页面后,输入 1 时的状态
  • oneOperandAndOperator 一个操作数和一个运算符,例如:进入页面后,输入 1 + 时的状态
  • twoOperandsAndOperator 两个操作数和一个运算符,例如:进入页面后,输入 1 + 2 时的状态

Command:

这个计算器提供七种命令:

enum Operator {
case addition
case subtraction
case multiplication
case division
}

enum CalculatorCommand {
case clear
case changeSign
case percent
case operation(Operator)
case equal
case addNumber(Character)
case addDot
}
  • clear 清除,重置
  • changeSign 改变正负号
  • percent 百分比
  • operation 四则运算
  • equal 等于
  • addNumber 输入数字
  • addDot 输入 “.”

reduce:

当命令产生时,将它应用到当前状态上,然后生成新的状态:

extension CalculatorState {
static func reduce(state: CalculatorState, _ x: CalculatorCommand) -> CalculatorState {
switch x {
case .clear:
return CalculatorState.initial
case .addNumber(let c):
return state.mapScreen { $0 == "0" ? String(c) : $0 + String(c) }
case .addDot:
return state.mapScreen { $0.range(of: ".") == nil ? $0 + "." : $0 }
case .changeSign:
return state.mapScreen { "\(-(Double($0) ?? 0.0))" }
case .percent:
return state.mapScreen { "\((Double($0) ?? 0.0) / 100.0)" }
case .operation(let o):
switch state {
case let .oneOperand(screen):
return .oneOperandAndOperator(operand: screen.doubleValue, operator: o)
case let .oneOperandAndOperator(operand, _):
return .oneOperandAndOperator(operand: operand, operator: o)
case let .twoOperandsAndOperator(operand, oldOperator, screen):
return .twoOperandsAndOperator(operand: oldOperator.perform(operand, screen.doubleValue), operator: o, screen: "0")
}
case .equal:
switch state {
case let .twoOperandsAndOperator(operand, operat, screen):
let result = operat.perform(operand, screen.doubleValue)
return .oneOperand(screen: String(result))
default:
return state
}
}
}
}
  • clear 重置当前状态
  • addNumber, addDot, changeSign, percent 只需要更改屏显即可
  • operation 需要根据当前状态来确定如何变化状态。
    • 如果只有一个操作数,就添加操作符。
    • 如果有一个操作数和操作符,就替换操作符。
    • 如果有两个操作数和一个操作符,将他们的计算结果作为操作数保留,然后加入新的操作符,以及一个操作数 0.
  • equal 如果当前有两个操作数和一个操作符,将他们的计算结果作为操作数保留。否则什么都不做。

剩下的都是一些辅助代码,接下来我们再来看下全部代码:

ViewController:

class CalculatorViewController: ViewController {

@IBOutlet weak var lastSignLabel: UILabel!
@IBOutlet weak var resultLabel: UILabel!

@IBOutlet weak var allClearButton: UIButton!
@IBOutlet weak var changeSignButton: UIButton!
@IBOutlet weak var percentButton: UIButton!

@IBOutlet weak var divideButton: UIButton!
@IBOutlet weak var multiplyButton: UIButton!
@IBOutlet weak var minusButton: UIButton!
@IBOutlet weak var plusButton: UIButton!
@IBOutlet weak var equalButton: UIButton!

@IBOutlet weak var dotButton: UIButton!

@IBOutlet weak var zeroButton: UIButton!
@IBOutlet weak var oneButton: UIButton!
@IBOutlet weak var twoButton: UIButton!
@IBOutlet weak var threeButton: UIButton!
@IBOutlet weak var fourButton: UIButton!
@IBOutlet weak var fiveButton: UIButton!
@IBOutlet weak var sixButton: UIButton!
@IBOutlet weak var sevenButton: UIButton!
@IBOutlet weak var eightButton: UIButton!
@IBOutlet weak var nineButton: UIButton!

override func viewDidLoad() {
let commands: Observable<CalculatorCommand> = Observable.merge([
allClearButton.rx.tap.map { _ in .clear },

changeSignButton.rx.tap.map { _ in .changeSign },
percentButton.rx.tap.map { _ in .percent },

divideButton.rx.tap.map { _ in .operation(.division) },
multiplyButton.rx.tap.map { _ in .operation(.multiplication) },
minusButton.rx.tap.map { _ in .operation(.subtraction) },
plusButton.rx.tap.map { _ in .operation(.addition) },

equalButton.rx.tap.map { _ in .equal },

dotButton.rx.tap.map { _ in .addDot },

zeroButton.rx.tap.map { _ in .addNumber("0") },
oneButton.rx.tap.map { _ in .addNumber("1") },
twoButton.rx.tap.map { _ in .addNumber("2") },
threeButton.rx.tap.map { _ in .addNumber("3") },
fourButton.rx.tap.map { _ in .addNumber("4") },
fiveButton.rx.tap.map { _ in .addNumber("5") },
sixButton.rx.tap.map { _ in .addNumber("6") },
sevenButton.rx.tap.map { _ in .addNumber("7") },
eightButton.rx.tap.map { _ in .addNumber("8") },
nineButton.rx.tap.map { _ in .addNumber("9") }
])

let system = Observable.system(
CalculatorState.initial,
accumulator: CalculatorState.reduce,
scheduler: MainScheduler.instance,
feedback: { _ in commands }
)
.debug("calculator state")
.share(replay: 1)

system.map { $0.screen }
.bind(to: resultLabel.rx.text)
.disposed(by: disposeBag)

system.map { $0.sign }
.bind(to: lastSignLabel.rx.text)
.disposed(by: disposeBag)
}

func formatResult(_ result: String) -> String {
if result.hasSuffix(".0") {
return result.substring(to: result.index(result.endIndex, offsetBy: -2))
} else {
return result
}
}
}

Calculator:

enum Operator {
case addition
case subtraction
case multiplication
case division
}

enum CalculatorCommand {
case clear
case changeSign
case percent
case operation(Operator)
case equal
case addNumber(Character)
case addDot
}

enum CalculatorState {
case oneOperand(screen: String)
case oneOperandAndOperator(operand: Double, operator: Operator)
case twoOperandsAndOperator(operand: Double, operator: Operator, screen: String)
}

extension CalculatorState {
static let initial = CalculatorState.oneOperand(screen: "0")

func mapScreen(transform: (String) -> String) -> CalculatorState {
switch self {
case let .oneOperand(screen):
return .oneOperand(screen: transform(screen))
case let .oneOperandAndOperator(operand, operat):
return .twoOperandsAndOperator(operand: operand, operator: operat, screen: transform("0"))
case let .twoOperandsAndOperator(operand, operat, screen):
return .twoOperandsAndOperator(operand: operand, operator: operat, screen: transform(screen))
}
}

var screen: String {
switch self {
case let .oneOperand(screen):
return screen
case .oneOperandAndOperator:
return "0"
case let .twoOperandsAndOperator(_, _, screen):
return screen
}
}

var sign: String {
switch self {
case .oneOperand:
return ""
case let .oneOperandAndOperator(_, o):
return o.sign
case let .twoOperandsAndOperator(_, o, _):
return o.sign
}
}
}


extension CalculatorState {
static func reduce(state: CalculatorState, _ x: CalculatorCommand) -> CalculatorState {
switch x {
case .clear:
return CalculatorState.initial
case .addNumber(let c):
return state.mapScreen { $0 == "0" ? String(c) : $0 + String(c) }
case .addDot:
return state.mapScreen { $0.range(of: ".") == nil ? $0 + "." : $0 }
case .changeSign:
return state.mapScreen { "\(-(Double($0) ?? 0.0))" }
case .percent:
return state.mapScreen { "\((Double($0) ?? 0.0) / 100.0)" }
case .operation(let o):
switch state {
case let .oneOperand(screen):
return .oneOperandAndOperator(operand: screen.doubleValue, operator: o)
case let .oneOperandAndOperator(operand, _):
return .oneOperandAndOperator(operand: operand, operator: o)
case let .twoOperandsAndOperator(operand, oldOperator, screen):
return .twoOperandsAndOperator(operand: oldOperator.perform(operand, screen.doubleValue), operator: o, screen: "0")
}
case .equal:
switch state {
case let .twoOperandsAndOperator(operand, operat, screen):
let result = operat.perform(operand, screen.doubleValue)
return .oneOperand(screen: String(result))
default:
return state
}
}
}
}

extension Operator {
var sign: String {
switch self {
case .addition: return "+"
case .subtraction: return "-"
case .multiplication: return "×"
case .division: return "/"
}
}

var perform: (Double, Double) -> Double {
switch self {
case .addition: return (+)
case .subtraction: return (-)
case .multiplication: return (*)
case .division: return (/)
}
}
}

private extension String {
var doubleValue: Double {
guard let double = Double(self) else {
return Double.infinity
}
return double
}
}

参考

TableViewSectionedViewController - 多层级的列表页

演示如何使用 RxDataSources 来布局列表页,你可以在这里下载这个例子


简介

这是一个多层级列表页,它主要需要完成这些需求:

  • 每个 Section 显示对应的标题
  • 每个 Cell 显示对应的元素以及行号
  • 根据 Cell 的 indexPath 控制行高
  • 当 Cell 被选中时,显示一个弹框

整体结构

以上这些需求,只需要一页代码就能完成:

class SimpleTableViewExampleSectionedViewController
: ViewController
, UITableViewDelegate
{
@IBOutlet weak var tableView: UITableView!

let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Double>>(
configureCell: { (_, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "\(element) @ row \(indexPath.row)"
return cell
},
titleForHeaderInSection: { dataSource, sectionIndex in
return dataSource[sectionIndex].model
}
)

override func viewDidLoad() {
super.viewDidLoad()

let dataSource = self.dataSource

let items = Observable.just([
SectionModel(model: "First section", items: [
1.0,
2.0,
3.0
]),
SectionModel(model: "Second section", items: [
1.0,
2.0,
3.0
]),
SectionModel(model: "Third section", items: [
1.0,
2.0,
3.0
])
])


items
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)

tableView.rx
.itemSelected
.map { indexPath in
return (indexPath, dataSource[indexPath])
}
.subscribe(onNext: { pair in
DefaultWireframe.presentAlert("Tapped `\(pair.1)` @ \(pair.0)")
})
.disposed(by: disposeBag)

tableView.rx
.setDelegate(self)
.disposed(by: disposeBag)
}

// to prevent swipe to delete behavior
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
return .none
}

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 40
}
}

我们首先创建一个 dataSource: RxTableViewSectionedReloadDataSource<SectionModel<String, Double>>:

let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Double>>(
configureCell: { (_, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "\(element) @ row \(indexPath.row)"
return cell
},
titleForHeaderInSection: { dataSource, sectionIndex in
return dataSource[sectionIndex].model
}
)

通过使用这个辅助类型,我们就不用执行数据源代理方法,而只需要提供必要的配置函数就可以布局列表页了。

第一个函数 configureCell 是用来配置 Cell 的显示,而这里的参数 element 就是 SectionModel<String, Double> 中的 Double

第二个函数 titleForHeaderInSection 是用来配置 Section 的标题,而 dataSource[sectionIndex].model 就是 SectionModel<String, Double> 中的 String

然后为列表页订制一个多层级的数据源 items: Observable<[SectionModel<String, Double>]>,用这个数据源来绑定列表页。

这里 SectionModel<String, Double> 中的 String 是用来显示 Section 的标题。而 Double 是用来绑定对应的 Cell。假如我们的列表页是用来显示通讯录的,并且通讯录通过首字母来分组。那么应该把数据定义为 SectionModel<String, Person>,然后用首字母 String 来显示 Section 标题,用联系人 Person 来显示对应的 Cell

由于 SectionModel<Section, ItemType> 是一个范型,所以我们可以用它来定义任意类型的 Section 以及 Item

最后:

override func viewDidLoad() {
super.viewDidLoad()

...

tableView.rx
.setDelegate(self)
.disposed(by: disposeBag)
}

...

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 40
}

这个是用来控制行高的,tableView.rx.setDelegate(self)... 将自己设置成 tableView 的代理,通过 heightForHeaderInSection 方法提供行高。

参考

收起阅读 »

iOS RXSwift 6.1

iOS
更多示例RxExample 中包含许多具有代表性的示例。它们都是很好的学习材料。这里我们取出其中几个示例来展示如何应用 RxSwift :ImagePicker - 图片选择器TableViewSectionedView...
继续阅读 »

更多示例

RxExample 中包含许多具有代表性的示例。它们都是很好的学习材料。这里我们取出其中几个示例来展示如何应用 RxSwift :

有兴趣的同学还可以研究一下 RxExample 中其他的示例。

ImagePicker - 图片选择器

这是一个图片选择器的演示,你可以在这里下载这个例子

简介

这个 App 主要有这样几个交互:

  • 当用点击相机按钮时,让用户拍一张照片,然后显示出来。
  • 当用点击相册按钮时,让用户从相册中选出照片,然后显示出来。
  • 当用点击裁剪按钮时,让用户从相册中选出照片编辑,然后显示出来。

整体结构

...
override func viewDidLoad() {
super.viewDidLoad()

...

cameraButton.rx.tap
.flatMapLatest { [weak self] _ in
return UIImagePickerController.rx.createWithParent(self) { picker in
picker.sourceType = .camera
picker.allowsEditing = false
}
.flatMap { $0.rx.didFinishPickingMediaWithInfo }
.take(1)
}
.map { info in
return info[.originalImage] as? UIImage
}
.bind(to: imageView.rx.image)
.disposed(by: disposeBag)

...
}

我们忽略一些细节,看一下序列的转换过程:

cameraButton.rx.tap
.flatMapLatest { () -> Observable<[InfoKey: AnyObject]> ... } // 点击 -> 图片信息
.map { [InfoKey : AnyObject] -> UIImage? ... } // 图片信息 -> 图片
.bind(to: imageView.rx.image) // 数据绑定
.disposed(by: disposeBag)

最开始的按钮点击是一个 Void 序列,接着用 flatMapLatest 将它异步转化为图片信息序列 [String : AnyObject],然后用 map 同步的从图片信息中取出图片,从而得到了一个图片序列 UIImage?,最后将这个图片序列绑定到 imageView 上:

这是相机按钮点击后需要执行的操作。另外两个按钮(相册和裁剪)和它十分相似,只不过传入了不同的参数,通过不同的键取出图片:

...
override func viewDidLoad() {
super.viewDidLoad()

...

// 相机
cameraButton.rx.tap
.flatMapLatest { [weak self] _ in
return UIImagePickerController.rx.createWithParent(self) { picker in
picker.sourceType = .camera
picker.allowsEditing = false
}
.flatMap { $0.rx.didFinishPickingMediaWithInfo }
.take(1)
}
.map { info in
return info[UIImagePickerControllerOriginalImage] as? UIImage
}
.bind(to: imageView.rx.image)
.disposed(by: disposeBag)

// 相册
galleryButton.rx.tap
.flatMapLatest { [weak self] _ in
return UIImagePickerController.rx.createWithParent(self) { picker in
picker.sourceType = .photoLibrary
picker.allowsEditing = false
}
.flatMap {
$0.rx.didFinishPickingMediaWithInfo
}
.take(1)
}
.map { info in
return info[.originalImage] as? UIImage
}
.bind(to: imageView.rx.image)
.disposed(by: disposeBag)

// 裁剪
cropButton.rx.tap
.flatMapLatest { [weak self] _ in
return UIImagePickerController.rx.createWithParent(self) { picker in
picker.sourceType = .photoLibrary
picker.allowsEditing = true
}
.flatMap { $0.rx.didFinishPickingMediaWithInfo }
.take(1)
}
.map { info in
return info[.editedImage] as? UIImage
}
.bind(to: imageView.rx.image)
.disposed(by: disposeBag)
}
...

参考

收起阅读 »

iOS RXSwift 5.16

iOS
zip通过一个函数将多个 Observables 的元素组合起来,然后将每一个组合的结果发出来zip 操作符将多个(最多不超过8个) Observables 的元素通过一个函数组合起来,然后将这个组合的结果发出...
继续阅读 »

zip

通过一个函数将多个 Observables 的元素组合起来,然后将每一个组合的结果发出来

zip 操作符将多个(最多不超过8个) Observables 的元素通过一个函数组合起来,然后将这个组合的结果发出来。它会严格的按照序列的索引数进行组合。例如,返回的 Observable 的第一个元素,是由每一个源 Observables 的第一个元素组合出来的。它的第二个元素 ,是由每一个源 Observables 的第二个元素组合出来的。它的第三个元素 ,是由每一个源 Observables 的第三个元素组合出来的,以此类推。它的元素数量等于源 Observables 中元素数量最少的那个。


演示

let disposeBag = DisposeBag()
let first = PublishSubject<String>()
let second = PublishSubject<String>()

Observable.zip(first, second) { $0 + $1 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

first.onNext("1")
second.onNext("A")
first.onNext("2")
second.onNext("B")
second.onNext("C")
second.onNext("D")
first.onNext("3")
first.onNext("4")

输出结果:

1A
2B
3C
4D

withLatestFrom

将两个 Observables 最新的元素通过一个函数组合起来,当第一个 Observable 发出一个元素,就将组合后的元素发送出来

withLatestFrom 操作符将两个 Observables 中最新的元素通过一个函数组合起来,然后将这个组合的结果发出来。当第一个 Observable 发出一个元素时,就立即取出第二个 Observable 中最新的元素,通过一个组合函数将两个最新的元素合并后发送出去。


演示

当第一个 Observable 发出一个元素时,就立即取出第二个 Observable 中最新的元素,然后把第二个 Observable 中最新的元素发送出去。

let disposeBag = DisposeBag()
let firstSubject = PublishSubject<String>()
let secondSubject = PublishSubject<String>()

firstSubject
.withLatestFrom(secondSubject)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

firstSubject.onNext("🅰️")
firstSubject.onNext("🅱️")
secondSubject.onNext("1")
secondSubject.onNext("2")
firstSubject.onNext("🆎")

输出结果:

2

当第一个 Observable 发出一个元素时,就立即取出第二个 Observable 中最新的元素,将第一个 Observable 中最新的元素 first 和第二个 Observable 中最新的元素second组合,然后把组合结果 first+second发送出去。

let disposeBag = DisposeBag()
let firstSubject = PublishSubject<String>()
let secondSubject = PublishSubject<String>()

firstSubject
.withLatestFrom(secondSubject) {
(first, second) in
return first + second
}
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

firstSubject.onNext("🅰️")
firstSubject.onNext("🅱️")
secondSubject.onNext("1")
secondSubject.onNext("2")
firstSubject.onNext("🆎")

输出结果:

🆎2

window

将 Observable 分解为多个子 Observable,周期性的将子 Observable 发出来

window 操作符和 buffer 十分相似,buffer 周期性的将缓存的元素集合发送出来,而 window 周期性的将元素集合以 Observable 的形态发送出来。

buffer 要等到元素搜集完毕后,才会发出元素序列。而 window 可以实时发出元素序列。

using

创建一个可被清除的资源,它和 Observable 具有相同的寿命

通过使用 using 操作符创建 Observable 时,同时创建一个可被清除的资源,一旦 Observable 终止了,那么这个资源就会被清除掉了。

收起阅读 »

iOS RXSwift 5.15

iOS
timer创建一个 Observable 在一段延时后,产生唯一的一个元素timer 操作符将创建一个 Observable,它在经过设定的一段时间后,产生唯一的一个元素。这里存在其他版本的 timer&nbs...
继续阅读 »

timer

创建一个 Observable 在一段延时后,产生唯一的一个元素

timer 操作符将创建一个 Observable,它在经过设定的一段时间后,产生唯一的一个元素。

这里存在其他版本的 timer 操作符。


timer

创建一个 Observable 在一段延时后,每隔一段时间产生一个元素

public static func timer(
_ dueTime: RxTimeInterval, // 初始延时
period: RxTimeInterval?, // 时间间隔
scheduler: SchedulerType
)
-> Observable<E>

timeout

如果源 Observable 在规定时间内没有发出任何元素,就产生一个超时的 error 事件

如果 Observable 在一段时间内没有产生元素,timeout 操作符将使它发出一个 error 事件。


takeWhile

镜像一个 Observable 直到某个元素的判定为 false

takeWhile 操作符将镜像源 Observable 直到某个元素的判定为 false。此时,这个镜像的 Observable 将立即终止。


演示

let disposeBag = DisposeBag()

Observable.of(1, 2, 3, 4, 3, 2, 1)
.takeWhile { $0 < 4 }
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

1
2
3

takeUntil

忽略掉在第二个 Observable 产生事件后发出的那部分元素

takeUntil 操作符将镜像源 Observable,它同时观测第二个 Observable。一旦第二个 Observable 发出一个元素或者产生一个终止事件,那个镜像的 Observable 将立即终止。


演示

let disposeBag = DisposeBag()

let sourceSequence = PublishSubject<String>()
let referenceSequence = PublishSubject<String>()

sourceSequence
.takeUntil(referenceSequence)
.subscribe { print($0) }
.disposed(by: disposeBag)

sourceSequence.onNext("🐱")
sourceSequence.onNext("🐰")
sourceSequence.onNext("🐶")

referenceSequence.onNext("🔴")

sourceSequence.onNext("🐸")
sourceSequence.onNext("🐷")
sourceSequence.onNext("🐵")

输出结果:

next(🐱)
next(🐰)
next(🐶)
completed
收起阅读 »

iOS RXSwift 5.14

iOS
takeLast仅仅从 Observable 中发出尾部 n 个元素通过 takeLast 操作符你可以只发出尾部 n 个元素。并且忽略掉前面的元素。演示let disposeBag = Dispos...
继续阅读 »

takeLast

仅仅从 Observable 中发出尾部 n 个元素

通过 takeLast 操作符你可以只发出尾部 n 个元素。并且忽略掉前面的元素。


演示

let disposeBag = DisposeBag()

Observable.of("🐱", "🐰", "🐶", "🐸", "🐷", "🐵")
.takeLast(3)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

🐸
🐷
🐵

take

仅仅从 Observable 中发出头 n 个元素

通过 take 操作符你可以只发出头 n 个元素。并且忽略掉后面的元素,直接结束序列。


演示

let disposeBag = DisposeBag()

Observable.of("🐱", "🐰", "🐶", "🐸", "🐷", "🐵")
.take(3)
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

🐱
🐰
🐶


subscribeOn

指定 Observable 在那个 Scheduler 执行

ReactiveX 使用 Scheduler 来让 Observable 支持多线程。你可以使用 subscribeOn 操作符,来指示 Observable 在哪个 Scheduler 执行。

observeOn 操作符非常相似。它指示 Observable 在哪个 Scheduler 发出通知。

默认情况下,Observable 创建,应用操作符以及发出通知都会在 Subscribe 方法调用的 Scheduler 执行。subscribeOn 操作符将改变这种行为,它会指定一个不同的 Scheduler 来让 Observable 执行,observeOn 操作符将指定一个不同的 Scheduler 来让 Observable 通知观察者。

如上图所示,subscribeOn 操作符指定 Observable 在那个 Scheduler 开始执行,无论它处于链的那个位置。 另一方面 observeOn 将决定后面的方法在哪个 Scheduler 运行。因此,你可能会多次调用 observeOn 来决定某些操作符在哪个线程运行。


startWith

将一些元素插入到序列的头部

startWith 操作符会在 Observable 头部插入一些元素。

(如果你想在尾部加入一些元素可以用concat


演示

let disposeBag = DisposeBag()

Observable.of("🐶", "🐱", "🐭", "🐹")
.startWith("1")
.startWith("2")
.startWith("3", "🅰️", "🅱️")
.subscribe(onNext: { print($0) })
.disposed(by: disposeBag)

输出结果:

3
🅰️
🅱️
2
1
🐶
🐱
🐭
🐹
收起阅读 »