原文为 @twostrawsSwiftUI By Example,就是简单记录一些主要内容。

Introduction

A brief explanation of the basics of SwiftUI

What is SwiftUI?

  1. 声明式对比命令式,能够更好地处理不同状态的UI
  2. 跨平台:支持 iOS, macOS, tvOS, watchOS

SwiftUI vs Interface Builder and storyboards

  1. IB难以阅读和修改
  2. IB难以看出修改了内容
  3. IB和Swift交互不够友好,充满了Objective-C设计
  4. SwiftUI是个仅支持Swift 的库,这样才可以充分利用Swift的特性

Anyway, we’ll get onto exactly how SwiftUI works soon. For now, the least you need to know is that SwiftUI fixes many problems people had with the old Swift + Interface Builder approach:

  • We no longer have to argue about programmatic or storyboard-based design, because SwiftUI gives us both at the same time.
  • We no longer have to worry about creating source control problems when committing user interface work, because code is much easier to read and manage than storyboard XML.
  • We no longer need to worry so much about stringly typed APIs – there are still some, but significantly fewer.
  • We no longer need to worry about calling functions that don’t exist, because our user interface gets checked by the Swift compiler.

So, I hope you’ll agree there are lots of benefits to be had from moving to SwiftUI!

Frequently asked questions about SwiftUI

Does SwiftUI replace UIKit?

No. Many parts of SwiftUI directly build on top of existing UIKit components, such as UITableView. Of course, many other parts don’t – they are new controls rendered by SwiftUI and not UIKit.

But the point isn’t to what extent UIKit is involved. Instead, the point is that we don’t care. SwiftUI more or less completely masks UIKit’s behavior, so if you write your app for SwiftUI and Apple replaces UIKit with a singing elephant in two years you don’t have to care – as long as Apple makes the elephant compatible with the same methods and properties that UIKit exposed to SwiftUI, your code doesn’t change.

Is SwiftUI fast?

SwiftUI is screamingly fast – in all my tests so far it seems to outpace UIKit. Having spoken to the team who made it I’m starting to get an idea why: first, they aggressively flatten their layer hierarchy so the system has to do less drawing, but second many operations bypass Core Animation entirely and go straight to Metal for extra speed.

So, yes: SwiftUI is incredibly fast, and all without us having to do any extra work.

How to follow this quick start guide

最好顺序阅读该教程

Migrating from UIKit to SwiftUI

如果你用过UIKit,不难发现SwiftUI就是把UI前缀去掉既是对应的组件。

Here’s a list to get you started, with UIKit class names followed by SwiftUI names:

  • UITableView: List
  • UICollectionView: No SwiftUI equivalent
  • UILabel: Text
  • UITextField: TextField
  • UITextField with isSecureTextEntry set to true: SecureField
  • UITextView: No SwiftUI equivalent
  • UISwitch: Toggle
  • UISlider: Slider
  • UIButton: Button
  • UINavigationController: NavigationView
  • UIAlertController with style .alert: Alert
  • UIAlertController with style .actionSheet: ActionSheet
  • UIStackView with horizontal axis: HStack
  • UIStackView with vertical axis: VStack
  • UIImageView: Image
  • UISegmentedControl: SegmentedControl
  • UIStepper: Stepper
  • UIDatePicker: DatePicker
  • NSAttributedString: Incompatible with SwiftUI; use Text instead.

Text and images

Getting started with basic controls

What’s in the basic template?

SceneDelegate.swift is responsible for managing the way your app is shown.

1
2
3
4
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()

Open ContentView.swift and let’s look at some actual SwiftUI code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import SwiftUI

struct ContentView : View {
var body: some View {
Text("Hello World")
}
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif

First, notice how ContentView is a struct.

Second, ContentView conforms to the View protocol.

第三,body的返回类型是some viewsome关键字是Swift 5.1中的新关键字,是名为opaque return types的功能的一部分,在这种情况下,就是字面意思:”这将返回某种View,但SwiftUI不需要知道(或关心)什么。”

Finally, below ContentView is a similar-but-different struct called ContentView_Previews.

How to create static labels with a Text view

1
2
Text("Hello World")
.lineLimit(3)
1
2
Text("This is an extremely long string that will never fit even the widest of Phones")
.truncationMode(.middle)

How to style text views with fonts, colors, line spacing, and more

1
2
3
4
5
6
7
Text("This is an extremely long string that will never fit even the widest of Phones")
.truncationMode(.middle)
.font(Font.body)
.foregroundColor(Color.green)
.background(Color.gray)
.lineLimit(nil)
.lineSpacing(30)

How to format text inside text views

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ContentView: View {
static let taskDateFormat: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()

var dueDate = Date()

var body: some View {
Text("Task due date: \(dueDate, formatter: Self.taskDateFormat)")
}
}

How to draw images using Image views

1
2
3
var body: some View {
Image("example-image")
}
1
2
3
Image(systemName: "cloud.heavyrain.fill")
.foregroundColor(.red)
.font(.largeTitle)

How to adjust the way an image is fitted to its space

1
2
3
Image("example-image")
.resizable()
.aspectRatio(contentMode: .fill)

How to render a gradient

1
2
3
4
Text("Hello World")
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [.white, .black]), startPoint: .top, endPoint: .bottom), cornerRadius: 0)

支持更多颜色的渐变

1
2
3
4
Text("Hello World")
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [.white, .red, .black]), startPoint: .leading, endPoint: .trailing), cornerRadius: 0)

How to display solid shapes

1
2
3
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)

How to use images and other views as a backgrounds

1
2
3
4
5
6
Text("Hacking with Swift")
.font(.largeTitle)
.background(
Image("example-image")
.resizable()
.frame(width: 100, height: 100))

View layout

Position views in a grid structure and more

How to create stacks using VStack and HStack

1
2
3
4
VStack {
Text("SwiftUI")
Text("rocks")
}

How to customize stack layouts with alignment and spacing

1
2
3
4
VStack(alignment: .leading, spacing: 20) {
Text("SwiftUI")
Text("rocks")
}

How to control spacing around individual views using padding

1
2
Text("SwiftUI")
.padding(.bottom, 100)

How to layer views on top of each other using ZStack

1
2
3
4
5
ZStack {
Rectangle()
.fill(Color.red)
Text("Hacking with Swift")
}

How to return different view types

第一种方案

1
2
3
4
5
6
7
8
9
var body: some View {
Group {
if Bool.random() {
Image("example-image")
} else {
Text("Better luck next time")
}
}
}

第二种方案

If you haven’t heard of this concept, it effectively forces Swift to forget about what specific type is inside the AnyView, allowing them to look like they are the same thing. This has a performance cost, though, so don’t use it often.

1
2
3
4
5
6
7
var body: AnyView {
if Bool.random() {
return AnyView(Image("example-image"))
} else {
return AnyView(Text("Better luck next time"))
}
}

How to create views in a loop using ForEach

1
2
3
4
5
6
7
VStack(alignment: .leading) {
ForEach((1...10).reversed()) {
Text("\($0)…")
}

Text("Ready or not, here I come!")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
struct ContentView : View {
let colors: [Color] = [.red, .green, .blue]

var body: some View {
VStack {
ForEach(colors.identified(by: \.self)) { color in
Text(color.description.capitalized)
.padding()
.background(color)
}
}
}
}

How to create different layouts using size classes

1
2
3
4
5
6
7
8
9
10
11
struct ContentView : View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?

var body: some View {
if horizontalSizeClass == .compact {
return Text("Compact")
} else {
return Text("Regular")
}
}
}

How to place content outside the safe area

1
2
3
4
Text("Hello World")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)

Reading input

Respond to interaction and control your program state

Working with state

SwiftUI solves this problem by removing state from our control. When we add properties to our views they are effectively inert – they have values, sure, but changing them doesn’t do anything. But if we added the special @State attribute before them, SwiftUI will automatically watch for changes and update any parts of our views that use that state.

How to create a toggle switch

相反,我们应该定义一个@State布尔属性,用于存储切换的当前值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView : View {
@State var showGreeting = true

var body: some View {
VStack {
Toggle(isOn: $showGreeting) {
Text("Show welcome message")
}.padding()

if showGreeting {
Text("Hello World!")
}
}
}
}

How to create a tappable button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct ContentView : View {
@State var showDetails = false

var body: some View {
VStack {
Button(action: {
self.showDetails.toggle()
}) {
Text("Show details")
}

if showDetails {
Text("You should follow me on Twitter: @twostraws")
.font(.largeTitle)
.lineLimit(nil)
}
}
}
}

How to read text from a TextField

1
2
3
4
5
6
7
8
9
10
struct ContentView : View {
@State var name: String = "Tim"

var body: some View {
VStack {
TextField($name)
Text("Hello, \(name)!")
}
}
}

How to add a border to a TextField

1
2
TextField($yourBindingHere)
.textFieldStyle(.roundedBorder)

How to create secure text fields using SecureField

1
2
3
4
5
6
7
8
9
10
struct ContentView : View {
@State var password: String = ""

var body: some View {
VStack {
SecureField($password)
Text("You entered: \(password)")
}
}
}

How to create a Slider and read values from it

1
2
3
4
5
6
7
8
9
10
struct ContentView : View {
@State var celsius: Double = 0

var body: some View {
VStack {
Slider(value: $celsius, from: -100, through: 100, by: 0.1)
Text("\(celsius) Celsius is \(celsius * 9 / 5 + 32) Fahrenheit")
}
}
}

How to create a date picker and read values from it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView : View {
var colors = ["Red", "Green", "Blue", "Tartan"]
@State var selectedColor = 0

var body: some View {
VStack {
Picker(selection: $selectedColor, label: Text("Please choose a color")) {
ForEach(0 ..< colors.count) {
Text(self.colors[$0]).tag($0)
}
}
Text("You selected: \(colors[selectedColor])")
}
}
}

How to create a segmented control and read values from it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ContentView : View {
@State var favoriteColor = 0
var colors = ["Red", "Green", "Blue"]

var body: some View {
VStack {
SegmentedControl(selection: $favoriteColor) {
ForEach(0..<colors.count) { index in
Text(self.colors[index]).tag(index)
}
}

Text("Value: \(colors[favoriteColor])")
}
}
}

How to read tap and double-tap gestures

1
2
3
4
Image("example-image")
.tapAction(count: 2) {
print("Double tapped!")
}

How to add a gesture recognizer to a view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView : View {
@State var scale: Length = 1.0

var body: some View {
Image("example-image")
.scaleEffect(scale)

.gesture(
TapGesture()
.onEnded { _ in
self.scale += 0.1
}
)
}
}

Lists

Create scrolling tables of data

Working with lists

SwiftUI’s List view is similar to UITableView in that it can show static or dynamic table view cells based on your needs.

How to create a list of static items

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct RestaurantRow: View {
var name: String

var body: some View {
Text("Restaurant: \(name)")
}
}

struct ContentView: View {
var body: some View {
List {
RestaurantRow(name: "Joe's Original")
RestaurantRow(name: "The Real Joe's Original")
RestaurantRow(name: "Original Joe's")
}
}
}

How to create a list of dynamic items

In order to handle dynamic items, you must first tell SwiftUI how it can identify which item is which. This is done using the Identifiable protocol, which has only one requirement: some sort of id value that SwiftUI can use to see which item is which.

ForEach一样,可以使用符合Identifiable 协议的Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Restaurant: Identifiable {
var id = UUID()
var name: String
}

struct RestaurantRow: View {
var restaurant: Restaurant

var body: some View {
Text("Come and eat at \(restaurant.name)")
}
}

struct ContentView: View {
var body: some View {
let first = Restaurant(name: "Joe's Original")
let second = Restaurant(name: "The Real Joe's Original")
let third = Restaurant(name: "Original Joe's")
let restaurants = [first, second, third]

return List(restaurants) { restaurant in
RestaurantRow(restaurant: restaurant)
}
// return List(restaurants, rowContent: RestaurantRow.init)
}
}

How to add sections to a list

1
2
3
4
5
Section(header: Text("Other tasks"), footer: Text("End")) {
TaskRow()
TaskRow()
TaskRow()
}

How to make a grouped list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ExampleRow: View {
var body: some View {
Text("Example Row")
}
}

struct ContentView : View {
var body: some View {
List {
Section(header: Text("Examples")) {
ExampleRow()
ExampleRow()
ExampleRow()
}
}.listStyle(.grouped)
}
}

Working with implicit stacking

What happens if you create a dynamic list and put more than one thing in each row? SwiftUI’s solution is simple, flexible, and gives us great behavior by default: it creates an implicit HStack to hold your items, so they automatically get laid out horizontally.

List 会隐式的创建一个HStack 封装所有元素到Row中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ExampleRow: View {
var body: some View {
Text("Example Row")
}
}

struct ContentView : View {
var body: some View {
List {
Section(header: Text("Examples")) {
ExampleRow()
ExampleRow()
ExampleRow()
}
}.listStyle(.grouped)
}
}

Containers

Place your views inside a navigation controller

Working with containers

SwiftUI is designed to be composed right out of the box, which means you can place one view inside another as much as you need.

常见的容器有: NavigationView, TabbedView, Group

How to embed a view in a navigation view

1
2
3
4
NavigationView {
Text("SwiftUI")
.navigationBarTitle(Text("Welcome"))
}

How to add bar items to a navigation view

1
2
3
4
5
6
7
8
9
10
11
12
var body: some View {
NavigationView {
Text("SwiftUI")
.navigationBarTitle(Text("Welcome"))
.navigationBarItems(trailing:
Button(action: {
print("Help tapped!")
}) {
Text("Help")
})
}
}

How to group views together

Stack 不能超过10个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var body: some View {
VStack {
Group {
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
}

Group {
Text("Line")
Text("Line")
Text("Line")
Text("Line")
Text("Line")
}
}
}

Alerts and action sheets

Show modal notifications when something happens

Working with presentations

SwiftUI’s declarative approach to programming means that we don’t create and present alert and action sheets in the same way as we did in UIKit. Instead, we define the conditions in which they should be shown, tell it what they should look like, then leave it to figure the rest out for itself.

How to show an alert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView : View {
@State var showingAlert = false

var body: some View {
Button(action: {
self.showingAlert = true
}) {
Text("Show Alert")
}
.presentation($showingAlert) {
Alert(title: Text("Important message"), message: Text("Wear sunscreen"), dismissButton: .default(Text("Got it!")))
}
}
}

How to add actions to alert buttons

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ContentView : View {
@State var showingAlert = false

var body: some View {
Button(action: {
self.showingAlert = true
}) {
Text("Show Alert")
}
.presentation($showingAlert) {
Alert(title: Text("Are you sure you want to delete this?"), message: Text("There is no undo"), primaryButton: .destructive(Text("Delete")) {
print("Deleting...")
}, secondaryButton: .cancel())
}
}
}

How to show an action sheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct ContentView : View {
@State var showingSheet = false

var sheet: ActionSheet {
ActionSheet(title: Text("Action"), message: Text("Quote mark"), buttons: [.default(Text("Woo"), onTrigger: {
self.showingSheet = false
})])
}

var body: some View {
Button(action: {
self.showingSheet = true
}) {
Text("Woo")
}
.presentation(showingSheet ? sheet : nil)
}
}

Presenting views

Move your user from one view to another

How to push a new view using NavigationButton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct DetailView: View {
var body: some View {
Text("Detail")
}
}

struct ContentView : View {
var body: some View {
NavigationView {
NavigationButton(destination: DetailView()) {
Text("Click")
}.navigationBarTitle(Text("Navigation"))
}
}
}

How to push a new view when a list row is tapped

SwiftUI doesn’t have a direct equivalent of the didSelectRowAt method of UITableView, but it doesn’t need one because we can combine NavigationButtonwith a list row and get the behavior for free.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct Restaurant: Identifiable {
var id = UUID()
var name: String
}

struct RestaurantRow: View {
var restaurant: Restaurant

var body: some View {
Text(restaurant.name)
}
}

struct RestaurantView: View {
var restaurant: Restaurant

var body: some View {
Text("Come and eat at \(restaurant.name)")
.font(.largeTitle)
}
}

struct ContentView: View {
var body: some View {
let first = Restaurant(name: "Joe's Original")
let restaurants = [first]

return NavigationView {
List(restaurants) { restaurant in
NavigationButton(destination: RestaurantView(restaurant: restaurant)) {
RestaurantRow(restaurant: restaurant)
}
}.navigationBarTitle(Text("Select a restaurant"))
}
}
}

How to present a new view using PresentationButton

1
2
3
4
5
6
7
8
9
10
struct DetailView: View {
var body: some View {
Text("Detail")
}
}
struct ContentView : View {
var body: some View {
PresentationButton(Text("Click to show"), destination: DetailView())
}
}

Transforming views

Clip, size, scale, spin, and more

How to give a view a custom frame

1
2
3
4
5
6
7
8
9
10
11
12
13
Button(action: {
print("Button tapped")
}) {
Text("Welcome")
.frame(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 200)
.font(.largeTitle)
}

Text("Please log in")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.font(.largeTitle)
.foregroundColor(.white)
.background(Color.red)

How to adjust the position of a view

1
2
3
4
5
6
7
VStack {
Text("Home")
Text("Options")
.offset(y: 15)
.padding(.bottom, 15)
Text("Help")
}

How to color the padding around a view

Padding 的先后顺序影响结果,因为代码是顺序执行的。

1
2
3
4
5
6
7
8
9
Text("Hacking with Swift")
.background(Color.black)
.foregroundColor(.white)
.padding()

Text("Hacking with Swift")
.padding()
.background(Color.black)
.foregroundColor(.white)

How to stack modifiers to create more advanced effects

1
2
3
4
5
6
7
8
9
Text("Forecast: Sun")
.font(.largeTitle)
.foregroundColor(.white)
.padding()
.background(Color.red)
.padding()
.background(Color.orange)
.padding()
.background(Color.yellow)

How to draw a border around a view

1
2
3
Text("Hacking with Swift")
.padding()
.border(Color.red, width: 4, cornerRadius: 16)

How to draw a shadow around a view

1
2
3
4
Text("Hacking with Swift")
.padding()
.border(Color.red, width: 4)
.shadow(color: .red, radius: 5, x: 20, y: 20)

How to clip a view so only part is visible

1
2
3
4
5
6
7
8
9
Button(action: {
print("Button tapped")
}) {
Image(systemName: "bolt.fill")
.foregroundColor(.white)
.padding()
.background(Color.green)
.clipShape(Circle())
}

How to rotate a view

1
2
3
4
5
6
7
8
9
10
11
struct ContentView: View {
@State var rotation: Double = 0

var body: some View {
VStack {
Slider(value: $rotation, from: 0.0, through: 360.0, by: 1.0)
Text("Up we go")
.rotationEffect(.degrees(rotation))
}
}
}

How to rotate a view in 3D

SwiftUI’s rotation3DEffect() modifier lets us rotate views in 3D space to create beautiful effects in almost no code.

1
2
3
4
Text("EPISODE LLVM")
.font(.largeTitle)
.foregroundColor(.yellow)
.rotation3DEffect(.degrees(45), axis: (x: 1, y: 0, z: 0))

How to scale a view up or down

1
2
Text("Up we go")
.scaleEffect(5)

How to round the corners of a view

1
2
3
4
Text("Round Me")
.padding()
.background(Color.red)
.cornerRadius(25)

How to adjust the opacity of a view

1
2
3
4
Text("Now you see me")
.padding()
.background(Color.red)
.opacity(0.3)

How to adjust the accent color of a view

iOS uses tint colors to give apps a coordinated theme, and the same functionality is available in SwiftUI under the name accent colors.

How to mask one view with another

1
2
3
4
5
Image("stripes")
.resizable()
.frame(width: 300, height: 300)
.mask(Text("SWIFT!")
.font(Font.system(size: 72).weight(.black)))

How to blur a view

1
2
Text("Welcome to my SwiftUI app")
.blur(radius: 2)

How to blend views together

1
2
3
4
5
ZStack {
Image("paul-hudson")
Image("example-image")
.blendMode(.multiply)
}

How to adjust views by tinting, and desaturating, and more

1
2
Image("paul-hudson")
.contrast(0.5)

Animation

Bring your views to life with movement

How to create a basic animation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct ContentView: View {
@State var angle: Double = 0
@State var borderThickness: Length = 1

var body: some View {
Button(action: {
self.angle += 45
self.borderThickness += 1
}) {
Text("Tap here")
.padding()
.border(Color.red, width: borderThickness)
.rotationEffect(.degrees(angle))
.animation(.basic())
}
}
}

How to create a spring animation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView: View {
@State var angle: Double = 0

var body: some View {
Button(action: {
self.angle += 45
}) {
Text("Tap here")
.padding()
.rotationEffect(.degrees(angle))
.animation(.spring())
}
}
}

How to create an explicit animation

Explicit animations are often helpful because they cause every affected view to animation, not just those that have implicit animations attached. For example, if view A has to make room for view B as part of the animation, but only view B has an animation attached, then view A will jump to its new position without animating unless you use explicit animations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView: View {
@State var opacity: Double = 1

var body: some View {
Button(action: {
withAnimation {
self.opacity -= 0.2
}
}) {
Text("Tap here")
.padding()
.opacity(opacity)
}
}
}

How to add and remove views with a transition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct ContentView: View {
@State var showDetails = false

var body: some View {
VStack {
Button(action: {
withAnimation {
self.showDetails.toggle()
}
}) {
Text("Tap to show details")
}

if showDetails {
Text("Details go here.")
}
}
}
}

By default, SwiftUI uses a fade animation to insert or remove views, but you can change that if you want by attaching a transition() modifier to a view.

1
2
Text("Details go here.")
.transition(.move(edge: .bottom))

How to combine transitions

1
Text("Details go here.").transition(AnyTransition.opacity.combined(with: .slide))

或者使用拓展来封装常用的过渡效果

1
2
3
4
5
6
7
extension AnyTransition {
static var moveAndScale: AnyTransition {
AnyTransition.move(edge: .bottom).combined(with: .scale())
}
}
// Usage
Text("Details go here.").transition(.moveAndScale)

How to create asymmetric transitions

SwiftUI lets us specify one transition when adding a view and another when removing it, all done using the asymmetric() transition type.

1
Text("Details go here.").transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .bottom)))

Composing views

Make your UI structure easier to understand

How to create and compose custom views

都是基于数据驱动UI,所以每一级封装都是传Model:User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
struct User {
var name: String
var jobTitle: String
var emailAddress: String
var profilePicture: String
}

struct ProfilePicture: View {
var imageName: String

var body: some View {
Image(imageName)
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
}
}

struct EmailAddress: View {
var address: String

var body: some View {
HStack {
Image(systemName: "envelope")
Text(address)
}
}
}

struct UserDetails: View {
var user: User

var body: some View {
VStack(alignment: .leading) {
Text(user.name)
.font(.largeTitle)
.foregroundColor(.primary)
Text(user.jobTitle)
.foregroundColor(.secondary)
EmailAddress(address: user.emailAddress)
}
}
}

struct UserView: View {
var user: User

var body: some View {
HStack {
ProfilePicture(imageName: user.profilePicture)
UserDetails(user: user)
}
}
}

struct ContentView: View {
let user = User(name: "Paul Hudson", jobTitle: "Editor, Hacking with Swift", emailAddress: "paul@hackingwithswift.com", profilePicture: "paul-hudson")

var body: some View {
UserView(user: user)
}
}

How to combine text views together

这样子拼接文本很方便

1
2
3
4
5
6
7
8
var body: some View {
Text("SwiftUI ")
.font(.largeTitle)
+ Text("is ")
.font(.headline)
+ Text("awesome")
.font(.footnote)
}

但是有个修改器不适合拼接如:foregroundColor

这时候就需要换成能够使用+的修改器,如color

1
2
3
4
5
6
Text("SwiftUI ")
.color(.red)
+ Text("is ")
.color(.orange)
+ Text("awesome")
.color(.blue)

How to store views as properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView : View {
let title = Text("Paul Hudson")
.font(.largeTitle)
let subtitle = Text("Author")
.foregroundColor(.secondary)

var body: some View {
VStack {
title
.color(.red)
subtitle
}
}
}

How to create custom modifiers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.red)
.foregroundColor(Color.white)
.font(.largeTitle)
}
}

struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI")
.modifier(PrimaryLabel())
}
}

Tooling

Build better apps with help from Xcode

How to preview your layout at different Dynamic Type sizes

1
2
3
4
5
6
7
8
9
10
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environment(\.colorScheme, .dark)
}
}
}
#endif

How to preview your layout in light and dark mode

If you want to see both light and dark mode side by side, place multiple previews in a group, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environment(\.colorScheme, .light)

ContentView()
.environment(\.colorScheme, .dark)
}
}
}
#endif

How to preview your layout in different devices

1
2
ContentView()
.previewDevice(PreviewDevice(rawValue: "iPhone SE"))

How to preview your layout in a navigation view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ContentView : View {
var body: some View {
Text("Hello World")
.navigationBarTitle(Text("Welcome"))
}
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
}
}
}
#endif

How to use Instruments to profile your SwiftUI code and identify slow layouts

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import Combine
import SwiftUI

class FrequentUpdater: BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var timer: Timer?

init() {
timer = Timer.scheduledTimer(
withTimeInterval: 0.01,
repeats: true
) { _ in
self.didChange.send(())
}
}
}

struct ContentView : View {
@ObjectBinding var updater = FrequentUpdater()
@State var tapCount = 0

var body: some View {
VStack {
Text("\(UUID().uuidString)")

Button(action: {
self.tapCount += 1
}) {
Text("Tap count: \(tapCount)")
}
}
}
}

检测我们的代码

默认情况下,SwiftUI工具告诉我们各种各样的事情:

1.在此期间创建了多少视图以及创建它们需要多长时间(“View Body”)
2.视图的属性是什么以及它们如何随时间变化(“View Properties”)
3.发生了多少次Core Animation提交(“Core Animation Commits”)
4.每个函数调用的确切时间(“Time Profiler”)

监视body调用

如果您选择View Body轨道 - 这是instrument列表中的第一行 - 您应该能够看到乐器将结果分解为SwiftUI和您的项目,前者是原始类型,如文本视图和按钮,以及后者包含您的自定义视图类型。在我们的例子中,这意味着“ContentView”应该出现在自定义视图中,因为这是我们视图的名称。

现在,您在这里看不到的是您的代码与SwiftUI视图的完美一对一映射,因为SwiftUI积极地折叠其视图层次结构以尽可能少地完成工作。所以,不要指望在代码中看到任何VStack创建 - 这个应用程序实际上是免费的。

在这个屏幕上,重要的数字是计数和平均持续时间 - 每件事创建的次数,以及花费的时间。因为这是一个压力测试你应该看到非常高的数字,但我们的布局是微不足道的,所以平均持续时间可能是几十微秒。

img

跟踪状态(state)变化

接下来,选择“View Properties”轨道,这是仪器列表中的第二行。这将显示所有视图的所有属性,包括其当前值和所有先前值。

我们的示例应用程序有一个按钮,通过在数字中添加一个来更改其标签,并且在此工具中可见 - 请查看视图类型ContentView和属性类型State

可悲的是,Instruments还没有(还能)向我们展示那里的确切属性名称,如果你跟踪了几个整数状态,这可能会更加令人困惑。然而,它确实有一个不同的技巧:在记录窗口的顶部是一个标记当前视图位置的箭头,如果你拖动它,你会看到应用程序状态随时间的变化 - 每次你点击按钮,你会看到状态整数上升一个,你可以前进和后退来看它发生。

这可以释放巨大的能力,因为它可以让我们直接看到状态变化导致慢速重绘或其他工作 - 这几乎就像是在时间机器中,您可以在运行期间的每个点检查应用程序的确切状态。

img

识别慢速绘图

虽然SwiftUI能够直接调用Metal以提高性能,但大多数情况下它更喜欢使用Core Animation进行渲染。这意味着我们会自动从Instruments获取内置的Core Animation分析工具,包括检测昂贵提交(expensive commits)的能力。

当多个更改放在一个组中时,Core Animation的效果最佳,称为transaction。我们在一个事务中有效地堆叠了一系列工作,然后要求CA继续渲染工作 - 称为提交事务。

因此,当Instruments向我们展示昂贵的Core Animation提交时,它真正向我们展示的是SwiftUI因为更新而被迫重绘屏幕上的像素的次数。理论上,这应该只在我们的应用程序的实际状态导致不同的视图层次结构时发生,因为SwiftUI应该能够将我们的body属性的新输出与先前的输出进行比较。

img

寻找缓慢的函数调用

Time Profiler,它向我们展示了在代码的每个部分花费了多少时间。这与乐器中的常规时间分析器完全相同,但如果您之前没有尝试过,那么您至少需要知道:

  1. 右侧的扩展详细信息窗口默认显示最重的堆栈跟踪,这是运行时间最长的代码段。明亮的代码(白色或黑色,取决于您的macOS配色方案)是您编写的代码;昏暗代码(灰色)是系统库代码。

  2. 在左侧,您可以看到创建的所有线程,以及公开指示器,让您深入了解它们调用的函数以及这些函数调用的函数等。大多数工作将在“start”内部进行。

  3. 为避免混乱,您可能需要单击底部的“调用树”按钮,然后选择“隐藏系统库”。这只会显示您编写的代码,但是如果您的问题是您使用的系统库很糟糕,这可能没有帮助。

  4. 要直接了解具体细节,您还可以单击“调用树”并选择“反转调用树”以翻转事物,以便叶子功能(树末端的功能)显示在顶部,现在可以向下钻取公开指示器(向上钻取?)到调用它们的函数。

最后一些提示

在您收取配置自己的代码之前,有一些事情需要注意:

  1. 在检查应用程序性能的一小部分时,您应该单击并拖动相关范围,以便仅查看该应用程序部分的统计信息。这使您可以专注于特定操作的性能,例如响应按下按钮。
  2. 即使你在仪器中看到纯色条,它们只是从远处看起来那样 - 你可以通过按住Cmd并按 - 和+来查看更多细节
  3. 要获得最准确的数字,请始终在真实设备上进行配置。
  4. 如果要通过分析代码进行更改,请始终一次进行一次更改。如果你进行两次更改,可能会使你的性能提高20%而另一种会降低10%,但是将它们合在一起意味着你可能会认为它们整体性能提高了10%。
  5. Instruments在release模式下运行您的代码,从而实现Swift的所有优化。这也会影响您添加到代码中的任何调试标志,因此请小心。

What now?

How to continue learning SwiftUI after the basics

SwiftUI tips and tricks

SwiftUI拥有强大的标题功能,但也有许多较小的提示和技巧可以帮助您编写更好的应用程序。

@State 设为私有

1
@State private var score = 0

具有常量绑定的原型

1
2
TextField(.constant("Hello"))
.textFieldStyle(.roundedBorder)

使用语义颜色

1
Color.red

依靠自适应填充

1
2
Text("Row 1")
.padding(10)

合并文本视图

1
2
3
4
5
6
7
8
9
10
11
12
struct ContentView : View {
var body: some View {
Text("Colored ")
.color(.red)
+
Text("SwifUI ")
.color(.green)
+
Text("Text")
.color(.blue)
}
}

如何使print()工作

右键单击预览画布(preview canvas)中的播放按钮,然后选择“调试预览(Debug Preview)”。

依靠隐式HStack

1
2
3
4
5
6
7
8
9
10
struct ContentView : View {
let imageNames = ["paul-hudson", "swiftui"]

var body: some View {
List(imageNames.identified(by: \.self)) { image in
Image(image).resizable().frame(width: 40)
Text(image)
}
}
}

分割大视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct ContentView : View {
let users = ["Paul Hudson", "Taylor Swift"]

var body: some View {
NavigationView {
List(users.identified(by: \.self)) { user in
NavigationButton(destination: Text("Detail View")) {
Image("example-image").resizable().frame(width: 50, height: 50)

VStack(alignment: .leading) {
Text("Johnny Appleseed").font(.headline)
Text("Occupation: Programmer")
}
}
}.navigationBarTitle(Text("Users"))
}
}
}

更好的预览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
Group {
ContentView()
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
ContentView()
.environment(\.colorScheme, .dark)
NavigationView {
ContentView()
}
}
}
}
#endif

创建自定义修改器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.black)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
}
}

struct ContentView : View {
var body: some View {
Text("Hello World")
.modifier(PrimaryLabel())
}
}

动画变化很容易

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ContentView : View {
@State var showingWelcome = false

var body: some View {
VStack {
Toggle(isOn: $showingWelcome.animation(.spring())) {
Text("Toggle label")
}

if showingWelcome {
Text("Hello World")
}
}
}
}

微星 MSI Z390 GAMING PRO CARBON AC 华硕 Vega64 黑苹果

机器配置

主板:微星 MSI MPG Z390 GAMING PRO CARBON AC

CPU:Intel i9 9900K

内存:Avexir DDR4 2400

显卡:华硕 ASUS AREZ-STRIX-RXVEGA64-O8G-GAMING

电源:EVGA SuperNOVA 750 G+

散热:MASTERLIQUID ML240R RGB

固态:三星 970 EVO 500G

板载声卡:ALC S1220A

板载有线网卡:Intel I219V7

显示器:戴尔 P2715Q 4K显示器

机箱:追风者 PK-515ETG

安装过程

重要提示:已降 BIOS 至 Version 7B17v10,不在卡 no nvram variable。

  1. 【黑果小兵】macOS Mojave 10.14.3 18D42 正式版 with Clover 4859原版镜像
  2. 执行:300系列主板请于drivers64UEFI目录中移除AptioMemoryFix-64.efi添加OsxAptioFix2Drv-free2000.efi该驱动位于**/EFI/CLOVER/drivers-off目录下。顺利进入macOS**安装界面
  3. 系统后序安装和复制 EFI 到硬盘教程:联想小新Air 13安装黑苹果兼macOS Mojave安装教程
  4. 进入系统设置页面前:卡在 AppleUSBHostResources panic。解决方案是boot 添加 safe mode
  5. 合并 WindowsClover 引导
  6. 完善驱动
  7. 结束

附图

![Hackintosh-i9 2019-03-15](https://gewill.org/assets/Hackintosh-i9 2019-03-15.png)总结

  1. 小兵教程很好用。
  2. 就是一直卡在 BIOS 部分,所以无论做什么努力的都没有效果。所以购买主板一定要看 tonymacx86.com 的 Buyer’s Guide 和网友的成功贴。我只之前微星 Z170A M3 安装黑苹果成功,就想当然的买了微星的这块主板。活生生的因为 BIOS 问题,至少1个月的安装时间浪费了。
  3. 买主板别买带无解的无线网卡,明明可以省200块,去买同系列的微星 MPG Z390 GAMING PRO CARBON。然后去买免驱的无线蓝牙网卡:Broadcom 94360CD。

EFI

EFI已上传 https://github.com/gewill/Hackintosh-Installation-with-MSI-Z390-and-i9-9900K

处理方法

由于环信将 i386 x86_64 armv7 arm64 几个平台都合并到了一起,所以使用动态库上传appstore时需要将i386 x86_64两个平台删除后,才能正常提交审核。

所以我们使用 fastlane sh 在打包gym 时前后分别将 x86 的移除remove86framework和添加add86framework 。这样就可以继续使用 fastlane 一键打包。

如果出错,使用 pod 删除并重新安装完整版的环信 SDK。

Fastfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
desc "Deploy a new version to the App Store"
lane :release do
remove86framework
gym
add86framework
deliver(force: true)
# frameit
end

desc "Remove x86 framework"
lane :remove86framework do
sh(%(
cd ../Pods/HyphenateLite/iOS_Pods_IMSDK*
pwd

if [ ! -d "./bak" ]; then
mkdir ./bak
fi
if [ -d "./bak/HyphenateLite.framework" ]; then
rm -rf ./bak/HyphenateLite.framework
fi
cp -r HyphenateLite.framework ./bak
lipo HyphenateLite.framework/HyphenateLite -thin armv7 -output HyphenateLite_armv7
lipo HyphenateLite.framework/HyphenateLite -thin arm64 -output HyphenateLite_arm64
lipo -create HyphenateLite_armv7 HyphenateLite_arm64 -output HyphenateLite
mv HyphenateLite HyphenateLite.framework/

))
end

desc "Add x86 framework back"
lane :add86framework do
sh(%(
cd ../Pods/HyphenateLite/iOS_Pods_IMSDK*
pwd
if [ -d "./HyphenateLite.framework" ]; then
rm -rf ./HyphenateLite.framework
fi
cp -r ./bak/HyphenateLite.framework HyphenateLite.framework
))
end

参考文档

  1. 环信:http://docs.easemob.com/im/300iosclientintegration/20iossdkimport#%E9%9B%86%E6%88%90%E5%8A%A8%E6%80%81%E5%BA%93%E4%B8%8A%E4%BC%A0appstore
  2. fastlane sh 文档:https://docs.fastlane.tools/actions/sh/

一旦你开始编写完整的应用程序与 RxSwift 和 RxCocoa, 你也需要照顾更多的中间主题比简单地观察事件和处理他们与 Rx。

在一个完整的生产质量应用程序中, 您需要构建一个错误处理策略, 执行更高级的多线程处理, 创建一个坚实的测试套件等等。

在这部分, 你将通过学习四个挑战性的章节, 这将解除您的 Rx 从一个菜鸟级的状态到一个实战经验的战士。

第十四章:错误处理实践

生活将是美好的, 如果我们生活在一个完美的世界, 但不幸的事情往往不像预期的那样去。即使是最好的 RxSwift 开发人员也不能避免遇到错误, 因此他们需要知道如何优雅和高效地处理它们。在本章中, 您将学习如何处理错误, 如何通过重试来管理错误恢复, 或者只是向整个宇宙投降, 让错误继续。

管理错误

常见错误类型:网络连接失败、无效的输入和API错误或者HTTP错误。

在 RxSwift 中, 错误处理是框架的一部分, 可以通过两种方式进行处理:  

  • Catch: 从错误中恢复并使用默认值。 
  • Retry: 重试有限 (或无限制) 次数。

本章项目的起始版本没有任何实际的错误处理。所有错误都是用一个返回虚拟版本的 catchErrorJustReturn 捕获的。这听起来像是一个方便的解决方案, 但是在 RxSwift 中有更好的处理方法。在任何一流的应用程序中, 都应该有一个一致的、信息错误处理方法。

抛出错误

一个好的起点是处理 RxCocoa 错误,它包装底层 Apple 框架返回的系统错误。

1
public func data(request: URLRequest) -> Observable<Data> {...}
1
2
3
4
5
if 200 ..< 300 ~= response.statusCode { 
return data
} else {
throw RxCocoaURLError.httpRequestFailed(response: response, data: data)
}

用 catch 处理错误

在解释如何抛出错误之后, 是时候看看如何处理错误了。最基本的方法是使用 catch。catch 的工作方式很像在Swift 中的 do-try-catch 流程。执行可观察的操作, 如果出现错误, 则返回包装错误的事件。

在 RxSwift 中有两个主要的运算符来捕获错误。

1
func catchError(_ handler:) -> RxSwift.Observable<Self.E>
1
func catchErrorJustReturn(_ element:) -> RxSwift.Observable<Self.E>

一个常见的陷阱

链式调用时候,一个错误就会导致整个订阅以错误结束。有的时候我们想知道错误的仅有一步细节,如 HTTP 404 错误,属于 API 返回错误的具体信息的约定,就需要移除 .catchErrorJustReturn(ApiController.Weather.empty) 来避免出现不期望的错误事件发射。

捕捉错误

我们先定义一个属性来缓存天气数据。

1
var cache = [String: Weather]()

再把输入框的序列改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let textSearch = searchInput.flatMap { text in
return ApiController.shared.currentWeather(city: text ?? "Error")
.do(onNext: { data in
if let text = text {
self.cache[text] = data
}
}, onError: { [weak self] e in
guard let strongSelf = self else { return }
DispatchQueue.main.async {
strongSelf.showError(error: e)
}
})
.retryWhen(retryHandler)
.catchError { error in
if let text = text, let cachedData = self.cache[text] {
return Observable.just(cachedData)
} else {
return Observable.just(ApiController.Weather.empty)
}
}
}

错误的重试机制

关于重试上面已经设计到了 .retryWhen(retryHandler),具体处理实现如下:

1
2
3
4
5
6
7
8
9
10
11
let retryHandler: (Observable<Error>) -> Observable<Int> = { e in
return e.enumerated().flatMap { (attempt, error) -> Observable<Int> in
if attempt >= maxAttempts - 1 {
return Observable.error(error)
} else if let casted = error as? ApiController.ApiError, casted == .invalidKey {
return ApiController.shared.apiKey.filter {$0 != ""}.map { _ in return 1 }
}
print("== retrying after \(attempt + 1) seconds ==")
return Observable<Int>.timer(Double(attempt + 1), scheduler: MainScheduler.instance).take(1)
}
}

或者简化版:.retry(3)

自定义错误

创建自定义错误遵循一般的 Swift 原则, 所以没有一个好的 Swift 程序员不会知道, 但它仍然是好的, 看看如何处理错误和创建定制的操作符。

创建自定义错误

先定义错误枚举,然后在结果过滤中抛出定义的错误即可。

1
2
3
4
5
enum ApiError: Error {
case cityNotFound
case serverFailure
case invalidKey
}
1
2
3
4
5
6
7
8
9
10
11
return session.rx.response(request: request).map() { response, data in
if 200 ..< 300 ~= response.statusCode {
return try JSON(data: data)
} else if response.statusCode == 401 {
throw ApiError.invalidKey
} else if 400 ..< 500 ~= response.statusCode {
throw ApiError.cityNotFound
} else {
throw ApiError.serverFailure
}
}

订阅者可以根据实际场景把错误展示给用户,这部分和 Swift 中自定义错误一致,就不展开讲了。

错误处理进阶

比如处理API 中 401 类型,需要提示用户授权失败,需要重新登录或授权。本例中需要是指API Key 失效,需要用户填写新的有效的API Key。

1
2
3
else if response.statusCode == 401 {
throw ApiError.invalidKey
}

Materialize 和 dematerialize

materialize 和 dematerialize 通常一起使用, 并且有能力完全打破原始可观察的合同。当没有其他选择来处理特定的情况时, 请小心地使用它们, 并且只有在必要的时候。

常见的应用是:进行日志记录

1
2
3
4
5
observableToLog.materialize()
.do(onNext: { (event) in
myAdvancedLogEvent(event)
})
.dematerialize()

第十五章:介绍调度程序

到目前为止,您已经设法使用调度程序,同时避免任何有关它们如何处理线程或并发的解释。 在前面的章节中,您使用了隐式使用某种并发/线程级别的方法,例如bufferdelaySubscriptioninterval运算符。

您可能感觉调度程序有一些神奇的东西,但在您了解调度程序之前,您还需要了解那些observeOn函数的全部内容。

本章将介绍调度程序背后的美观,在这里您将了解为什么Rx抽象如此强大以及为什么使用异步编程远比使用锁或队列更省心。

调度程序到底是什么?

在你着手学习时调度程序, 了解他们是什么和他们不是什么是很重要的。总而言之, 调度程序是一个过程发生的上下文。此上下文可以是线程、调度队列或类似的实体, 甚至是在 OperationQueueScheduler 内部使用的 NSOperation。

下图是个好的例子:

schedulerExample

在此图中,具有缓存运算符的概念。 observable向服务器发出请求并检索一些数据。此数据由名为cache的自定义运算符处理,该运算符将数据存储在某处。 在此之后,数据被传递给不同调度程序中的所有订阅者,很可能是位于主线程之上的MainScheduler,使得UI的更新成为可能。

揭开调度程序

调度程序虽然工作方式和GCD类似,却不是等价的。

要记住的重要一点是, 调度程序不是线程, 并且它们没有与线程的一对一关系。始终检查计划程序执行操作的上下文, 而不是线程。在本章的后面部分, 您将遇到一些好的例子来帮助您理解这一点。

设置项目

编写两个函数打印当前调度程序所在线程。

切换调度程序

Rx 中最重要的事情之一是随时切换调度程序的能力, 没有任何限制, 除了内部进程生成事件所强加的约束之外。

fruit 是在主线程上生成的, 但是将它移动到后台线程是很好的。要在后台线程中创建 fruit , 必须使用 subscribeOn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let fruit = Observable<String>.create { observer in
observer.onNext("[apple]")
sleep(2)
observer.onNext("[pineapple]")
sleep(2)
observer.onNext("[strawberry]")
return Disposables.create()
}

fruit
.subscribeOn(globalScheduler)
.dump()
.observeOn(MainScheduler.instance)
.dumpingSubscription()
.disposed(by: bag)

观察是 Rx 的三基本概念之一。它涉及实体生成事件, 以及这些事件的观察者。在这种情况下, 在对应 subscribeOn 的情况下, 操作符 observeOn 改变了观察发生的调度程序。

这是一个非常常见的模式。您使用后台进程从服务器检索数据并处理接收的数据, 只切换到 MainScheduler 处理 final 事件并在用户界面中显示数据。

陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let globalScheduler = ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global())
let bag = DisposeBag()
let animal = BehaviorSubject(value: "[dog]")


let animalsThread = Thread() {
sleep(3)
animal.onNext("[cat]")
sleep(3)
animal.onNext("[tiger]")
sleep(3)
animal.onNext("[fox]")
sleep(3)
animal.onNext("[leopard]")
}

animalsThread.name = "Animals Thread"
animalsThread.start()


animal.subscribeOn(MainScheduler.instance)
.dump()
.observeOn(globalScheduler)
.dumpingSubscription()
.disposed(by:bag)


RunLoop.main.run(until: Date(timeIntervalSinceNow: 13))

输出结果:

1
2
3
4
5
6
7
8
9
10
00s | [D] [dog] received on Main Thread
00s | [S] [dog] received on Anonymous Thread
03s | [D] [cat] received on Animals Thread
03s | [S] [cat] received on Anonymous Thread
06s | [D] [tiger] received on Animals Thread
06s | [S] [tiger] received on Anonymous Thread
09s | [D] [fox] received on Animals Thread
09s | [S] [fox] received on Anonymous Thread
12s | [D] [leopard] received on Animals Thread
12s | [S] [leopard] received on Anonymous Thread

结果出人意料,并没有如愿发生在主线程上。这是一个常见的和危险的陷阱, 它来自于在默认情况下认为 Rx 是异步的或多线程的,但这不是事实。

Rx 和一般抽象是自由线程的;在处理数据时, 没有发生魔术般的线程切换。如果不指定其他的线程, 则始终在原始线程上执行计算。

任何线程切换都是在程序员使用运算符 subscribeOnobserveOn 的显式请求之后发生的。

认为 Rx 做一些线程处理默认会陷入一个常见的陷阱。上面发生的事情是对 Subject 的误用。原始计算在指定的线程上发生, 并且这些事件使用 Thread() { ... } 在该线程中被推入。由于 Subject 的性质, Rx 没有能力切换原始的计算调度程序, 并移动到另一个线程, 因为没有直接控制的 Subject 被推出。

但是下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let globalScheduler = ConcurrentDispatchQueueScheduler(queue: DispatchQueue.global())
let bag = DisposeBag()
let animal = BehaviorSubject(value: "[dog]")

let fruit = Observable<String>.create { observer in
observer.onNext("[apple]")
sleep(2)
observer.onNext("[pineapple]")
sleep(2)
observer.onNext("[strawberry]")
return Disposables.create()
}

fruit.subscribeOn(globalScheduler)
.dump()
.observeOn(MainScheduler.instance)
.dumpingSubscription()
.disposed(by:bag)

RunLoop.main.run(until: Date(timeIntervalSinceNow: 13))

输出:

1
2
3
4
5
6
00s | [D] [apple] received on Anonymous Thread
00s | [S] [apple] received on Main Thread
02s | [D] [pineapple] received on Anonymous Thread
02s | [S] [pineapple] received on Main Thread
04s | [D] [strawberry] received on Anonymous Thread
04s | [S] [strawberry] received on Main Thread

为什么这适用于fruit 线程呢? 这是因为使用Observable.create可以让Rx控制Thread块内部发生的事情,这样你就可以更加精确地定制线程处理。
这种意想不到的结果通常被称为“冷和热”可观测问题。
在上面的例子中,你正在处理热观察。 observable在订阅期间没有任何副作用,但它确实有自己的上下文,其中生成事件并且RxSwift无法控制它(即,它运行自己的Thread)。
相反,冷观察不会在任何观察者订阅之前产生任何元素。 这实际上意味着它没有自己的上下文,直到订阅时,它创建一些上下文并开始生成元素。

热 vs 冷

上面的部分谈到了冷热观测量的话题。冷热观测量的话题是相当固执己见的, 产生了很多争论, 所以让我们简单在这里看看。这个概念可以归结为一个非常简单的问题:

HotVSCold

一些副作用的例子是:

  • 向服务器发送请求
  • 编辑本地数据库
  • 写入文件系统
  • 发射火箭

最佳实践和内置调度程序

调度程序是一个非平凡的主题, 因此它们会为最常见的用例提供一些最佳实践。在本节中, 您将获得串行和并发调度程序的快速介绍, 了解它们如何处理数据并查看哪种类型对特定上下文更有效。

串行与并发调度程序

虑到调度程序只是一个上下文, 它可以是任何东西 (调度队列、线程、自定义上下文), 并且所有转换序列的运算符都需要保留隐式保证, 因此需要确保您使用的是正确的计划程序。

  • 如果您使用的是串行调度程序, Rx 将按顺序进行计算。对于串行调度队列, 调度程序还可以在底层执行自己的优化。

  • 在并发计划程序中, Rx 将尝试同时运行代码, 但 observeOnsubscribeOn 将保留执行任务所需的顺序, 并确保订阅代码在正确的计划程序上结束。

    MainScheduler

MainScheduler 位于主线程的顶端。此计划程序用于处理用户界面上的更改并执行其他高优先级任务。作为在 iOS、tvOS 或 macOS 上开发应用程序的一般做法, 不应使用此计划程序执行长时间运行的任务, 因此应避免诸如服务器请求或其他繁重任务之类的事情。

MainScheduler 还用于在使用单位和更多特定的(如:Driver)执行所有计算。如前一章所述, Driver 确保始终在 MainScheduler 中执行计算, 以使您能够将数据直接绑定到应用程序的用户界面。

SerialDispatchQueueScheduler

SerialDispatchQueueScheduler 负责在串行 DispatchQueue 。这个调度程序在使用 observeOn 时,有很多优化的优势。

你可以使用此计划程序来处理以串行方式更好地安排的后台作业。例如, 如果应用程序与服务器的单个路径 (如Firebase 或 GraphQL 应用程序) 进行对话, 则可能需要避免调度多个同时请求, 这会给接收端造成太大的压力。这个调度程序是你绝对想要的, 像串行任务队列一样先进。

ConcurrentDispatchQueueScheduler

ConcurrentDispatchQueueScheduler 与 SerialDispatchQueueScheduler类似,负责在DispatchQueue上抽象工作。 主要区别在于,调度程序使用并行队列而不是串行队列。

此类调度程序在使用 observeOn 时没有进行优化, 因此在决定使用哪种调度程序时, 请记住说明这一点。

对于需要同时结束的多个长时间运行的任务,并发的调度程序可能是一个不错的选择。 将多个可观察对象与阻塞运算符组合在一起,以便在准备好时将所有结果组合在一起,可以防止串行调度程序以最佳状态执行。 相反,并发调度程序可以执行多个并发任务并优化结果的收集。

OperationQueueScheduler

OperationQueueScheduler类似于ConcurrentDispatchQueueScheduler,但不是通过DispatchQueue抽象工作,它在NSOperationQueue上执行工作。 有时你需要对正在运行的并发作业进行更多控制,而对于并发DispatchQueue则无法做到这一点。

如果需要调整最大并发作业数,则这是作业的调度程序。 您可以定义maxConcurrentOperationCount来限制并发操作的数量以满足您的应用程序的需要。

TestScheduler

TestScheduler是一种特殊的野兽。 它仅用于测试,因此尽量不要在生产代码中使用此调度程序。 这种特殊的调度程序简化了操作员测试; 它是RxTest库的一部分。 您将在关于测试的专用章节中了解如何使用此调度程序,但是让我们快速浏览一下,因为您正在进行调度程序的全程浏览。

第十六章:使用 RxTest 进行测试

本章将向您介绍 RxTest, 以及以后的 RxBlocking, 通过编写针对多个 RxSwift 操作的测试, 还可以编写针对生产 RxSwift 代码的测试。

本章例子是:一个将Hex颜色值转换为RGB值的应用。架构是MVVM。

使用 RxTest 测试操作符

RxTest 是 RxSwift 外一个独立的库。它是托管在 RxSwift 仓库, 但需要一个单独的 pod 安装和导入。RxTest 为测试 RxSwift 代码提供了许多有用的补充, 如 TestScheduler, 它是一个虚拟时间计划程序, 它使你可以对测试的时间线性操作进行粒状控制, 并且包括 next(_:_:), completed(_:_:)error(_:_:) 在测试中 specified 时间启用将这些事件添加到观测量中。它还增加了热和冷的观测量, 你可以想到的热和冷三明治。当然, 不是真的三明治。

热和冷的序列是什么?

RxSwift 很长的时间来简化和简化你的 Rx 代码, 并且有热和冷的序列的区别, 当它涉及到观测量, 在 RxSwift 可以被认为是可观测的特征而不是具体的类型。

热序列:

  • 使用资源,无论是否有订阅者。
  • 生成元素,无论是否有订阅者。
  • 主要用于有状态类型,如变量。

冷序列:

  • 仅在订阅时消耗资源。
  • 仅在有订阅者时才生成元素。
  • 主要用于网络等异步操作。

下面是 amb 的测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func testAmb() {

let observer = scheduler.createObserver(String.self)

let observableA = scheduler.createHotObservable([
next(100, "a"),
next(110, "b"),
next(300, "c")
])

let observableB = scheduler.createHotObservable([
next(90, "1"),
next(200, "2"),
next(300, "3")
])

let ambObservable = observableA.amb(observableB)

scheduler.scheduleAt(0) {
self.subscription = ambObservable.subscribe(observer)
}

scheduler.start()

let results = observer.events.map {
$0.value.element!
}

XCTAssertEqual(results, ["1", "2", "3"])
}

上面是同步测试,如果想测试异步错误,最简单地是使用 RxBlocking

使用 RxBlocking

RxBlocking 是另一个库, 寄存在 RxSwift 仓库。它的主要目的是通过它的 toBlocking(timeout:) 方法将 observable 转换为 BlockingObservable

1
2
3
4
5
func testToArray() {
let scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
let toArrayObservable = Observable.of(1, 2).subscribeOn(scheduler)
XCTAssertEqual(try! toArrayObservable.toBlocking().toArray(), [1, 2])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func testToArrayMaterialized() {
let scheduler = ConcurrentDispatchQueueScheduler(qos: .default)

let toArrayObservable = Observable.of(1, 2).subscribeOn(scheduler)

let result = toArrayObservable
.toBlocking()
.materialize()

switch result {
case .completed(elements: let elements):
XCTAssertEqual(elements, [1, 2])
case .failed(_, error: let error):
XCTFail(error.localizedDescription)
}
}

测试 RxSwift 生产代码

下面例子是测试MVVM中VM层:

使用 toBlocking 很方便编写异步测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import XCTest
import RxSwift
import RxCocoa
import RxTest
@testable import Testing

class TestingViewModel: XCTestCase {

var viewModel: ViewModel!
var scheduler: ConcurrentDispatchQueueScheduler!

override func setUp() {
super.setUp()

viewModel = ViewModel()
scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
}

func testColorIsRedWhenHexStringIsFF0000_async() {

let disposeBag = DisposeBag()

let expect = expectation(description: #function)

let expectedColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)

var result: UIColor!

viewModel.color.asObservable()
.skip(1)
.subscribe(onNext: {
result = $0
expect.fulfill()
})
.disposed(by: disposeBag)

viewModel.hexString.value = "#ff0000"

waitForExpectations(timeout: 1.0) { error in
guard error == nil else {
XCTFail(error!.localizedDescription)
return
}

XCTAssertEqual(expectedColor, result)
}
}

func testColorIsRedWhenHexStringIsFF0000() {

let colorObservable = viewModel.color.asObservable().subscribeOn(scheduler)

viewModel.hexString.value = "#ff0000"

do {
guard let result = try colorObservable.toBlocking(timeout: 1.0).first() else { return }

XCTAssertEqual(result, .red)
} catch {
print(error)
}
}


func testRgbIs010WhenHexStringIs00FF00() {

let rgbObservable = viewModel.rgb.asObservable().subscribeOn(scheduler)

viewModel.hexString.value = "#00ff00"

let result = try! rgbObservable.toBlocking().first()!

XCTAssertEqual(0 * 255, result.0)
XCTAssertEqual(1 * 255, result.1)
XCTAssertEqual(0 * 255, result.2)
}

func testColorNameIsRayWenderlichGreenWhenHexStringIs006636() {

let colorNameObservable = viewModel.colorName.asObservable().subscribeOn(scheduler)

viewModel.hexString.value = "#006636"

XCTAssertEqual(try! colorNameObservable.toBlocking().first()!, "rayWenderlichGreen")
}
}

第十七章:创建自定义响应式拓展

在学习了RxSwift和RxCocoa,以及如何编写测试,这里我们将学习如何给Apple和第三方库编写RxSwift的拓展。本章例子是给NSURLSession编写拓展,来实现网络请求、缓存。但却是一个教学实例,如果生产项目可以有很多网络库可以直接使用,如RxAlamofire、RxMoya等。

如何创建拓展

创建一个Cocoa或库的拓展不是一个简单的任务。你会发现这个过程可能很棘手,你的解决方案在继续之前可能需要一些前期思考。

这里我们将学习如何拓展URLSession,加上Rx的命名空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
extension Reactive where Base: URLSession {

func response(request: URLRequest) -> Observable<(HTTPURLResponse, Data)> {
return Observable.create { observer in
// content goes here

let task = self.base.dataTask(with: request) { (data, response, error) in
guard let response = response, let data = data else {
observer.on(.error(error ?? RxURLSessionError.unknown))
return
}

guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxURLSessionError.invalidResponse(response: response)))
return
}
observer.onNext((httpResponse, data))
observer.on(.completed)
}
task.resume()

return Disposables.create(with: task.cancel)
}
}

func data(request: URLRequest) -> Observable<Data> {
if let url = request.url?.absoluteString, let data = internalCache[url] {
return Observable.just(data)
}

return response(request: request).cache().map { (response, data) -> Data in
if 200 ..< 300 ~= response.statusCode {
return data
} else {
throw RxURLSessionError.requestFailed(response: response, data: data)
}
}
}

func string(request: URLRequest) -> Observable<String> {
return data(request: request).map { d in
return String(data: d, encoding: .utf8) ?? ""
}
}

func json(request: URLRequest) -> Observable<JSON> {
return data(request: request).map { d in
return try JSON(data: d)
}
}

func image(request: URLRequest) -> Observable<UIImage> {
return data(request: request).map { d in
return UIImage(data: d) ?? UIImage()
}
}

}

如何创建自定义运算符

这里我们演示如何使用运算符进行缓存网络请求结果(HTTPURLResponse、Data)的序列,简单起见使用字典。

1
2
3
4
5
6
7
8
9
10
11
fileprivate var internalCache = [String: Data]()

extension ObservableType where E == (HTTPURLResponse, Data) {
func cache() -> Observable<E> {
return self.do(onNext: { (response, data) in
if let url = response.url?.absoluteString, 200 ..< 300 ~= response.statusCode {
internalCache[url] = data
}
})
}
}

如何使用封装的拓展

在cell中更具给定url下载gif并显示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
func downloadAndDisplay(gif stringUrl: String) {
guard let url = URL(string: stringUrl) else { return }
let request = URLRequest(url: url)
activityIndicator.startAnimating()

let s = URLSession.shared.rx.data(request: request)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { imageData in
self.gifImageView.animate(withGIFData: imageData)
self.activityIndicator.stopAnimating()
})
disposable.setDisposable(s)
}

测试封装的拓展

这里我们使用RxNimble来辅助我们编写测试。RxNimble 使测试更容易编写, 并有助于代码更简明。

RxBlocking版本:

1
2
let result = try! observable.toBlocking().first()
expect(result) == 42

RxNimble版本:

1
expect(observable).first == 42

完整测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import XCTest
import RxSwift
import RxBlocking
import Nimble
import RxNimble
import OHHTTPStubs
import SwiftyJSON

@testable import iGif

class iGifTests: XCTestCase {

let obj = ["array":["foo","bar"], "foo":"bar"] as [String : Any]
let request = URLRequest(url: URL(string: "http://raywenderlich.com")!)
let errorRequest = URLRequest(url: URL(string: "http://rw.com")!)

override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
stub(condition: isHost("raywenderlich.com")) { _ in
return OHHTTPStubsResponse(jsonObject: self.obj, statusCode: 200, headers: nil)
}
stub(condition: isHost("rw.com")) { _ in
return OHHTTPStubsResponse(error: RxURLSessionError.unknown)
}
}

override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
OHHTTPStubs.removeAllStubs()
}

func testData() {
let observable = URLSession.shared.rx.data(request: self.request)
expect(observable.toBlocking().firstOrNil()).toNot(beNil())
}

func testString() {
let observable = URLSession.shared.rx.string(request: self.request)
let string = "{\"array\":[\"foo\",\"bar\"],\"foo\":\"bar\"}"
expect(observable.toBlocking().firstOrNil()) == string
}

func testJSON() {
let observable = URLSession.shared.rx.json(request: self.request)
let string = "{\"array\":[\"foo\",\"bar\"],\"foo\":\"bar\"}"
let json = try? JSON(data: string.data(using: .utf8)!)
expect(observable.toBlocking().firstOrNil()) == json
}

func testError() {
var erroredCorrectly = false
let observable = URLSession.shared.rx.json(request: self.errorRequest)
do {
let _ = try observable.toBlocking().first()
assertionFailure()
} catch (RxURLSessionError.unknown) {
erroredCorrectly = true
} catch {
assertionFailure()
}
expect(erroredCorrectly) == true
}
}

extension BlockingObservable {
func firstOrNil() -> E? {
do {
return try first()
} catch {
return nil
}
}
}

常用封装

RxSwift社区非常活跃,并且已经有很多扩展和封装。 一些基于Apple组件,而另一些则基于许多iOS和macOS项目中广泛使用的第三方库。
你可以在 http://community.rxswift.org 找到最新的封装列表。

下面几个常用的封装:

  • RxDataSources
  • RxAlamofire
  • RxBluetoothKit

小结

关于何时需要抽象,没有真正明确的规则,但建议如果框架满足以下一个或多个条件,则应用此策略:

  • 使用带有完成和失败信息的回调
  • 使用大量委托异步返回信息
  • 需要与应用程序的其他RxSwift部分进行互操作

由于Rx是一个多平台框架,因此它不会对您的Rx驱动的应用程序运行的设备做出任何假设。 RxSwift严格遵循RxPython,RxRuby,RxJS和所有其他平台所遵循的通用API设计,因此它不包含任何特定功能或与UIKit或Cocoa的集成,以帮助您开发iOS或macOS。

RxCocoa是一个独立的库(尽管它与RxSwift捆绑在一起),它允许您使用许多预构建的功能来更好地与UIKit和Cocoa集成。

RxCocoa将为您提供开箱即用的类来进行反应式网络,对用户交互作出反应,将数据模型绑定到UI控件等等。

第十二章:开始学习 RxCocoa

更新天气信息界面,根据API获取的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ApiController.shared.currentWeather(city: "RxSwift")
.observeOn(MainScheduler.instance)
.subscribe(onNext: { data in
self.tempLabel.text = "\(data.temperature)° C"
self.iconLabel.text = data.icon
self.humidityLabel.text = "\(data.humidity)%"
self.cityNameLabel.text = data.cityName
})
.disposed(by:bag)

let search = searchCityName.rx.controlEvent(.editingDidEndOnExit).asObservable()
.map { self.searchCityName.text }
.filter { ($0 ?? "").characters.count > 0 }
.flatMapLatest { text in
return ApiController.shared.currentWeather(city: text ?? "Error")
.catchErrorJustReturn(ApiController.Weather.empty)
}
.asDriver(onErrorJustReturn: ApiController.Weather.empty)

search.map { "\($0.temperature)° C" }
.drive(tempLabel.rx.text)
.disposed(by:bag)

search.map { $0.icon }
.drive(iconLabel.rx.text)
.disposed(by:bag)

search.map { "\($0.humidity)%" }
.drive(humidityLabel.rx.text)
.disposed(by:bag)

search.map { $0.cityName }
.drive(cityNameLabel.rx.text)
.disposed(by:bag)

第十三章:RxCocoa 中级

添加搜索框支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
override func viewDidLoad() {
super.viewDidLoad()
style()

let searchInput = searchCityName.rx.controlEvent(.editingDidEndOnExit).asObservable()
.map { self.searchCityName.text }
.filter { ($0 ?? "").count > 0 }

let textSearch = searchInput.flatMap { text in
return ApiController.shared.currentWeather(city: text ?? "Error")
.catchErrorJustReturn(ApiController.Weather.dummy)
}

let mapInput = mapView.rx.regionDidChangeAnimated
.skip(1)
.map { _ in self.mapView.centerCoordinate }

let mapSearch = mapInput.flatMap { coordinate in
return ApiController.shared.currentWeather(lat: coordinate.latitude, lon: coordinate.longitude)
.catchErrorJustReturn(ApiController.Weather.dummy)
}

let currentLocation = locationManager.rx.didUpdateLocations
.map { locations in
return locations[0]
}
.filter { location in
return location.horizontalAccuracy < kCLLocationAccuracyHundredMeters
}

let geoInput = geoLocationButton.rx.tap.asObservable()
.do(onNext: {
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
})

let geoLocation = geoInput.flatMap {
return currentLocation.take(1)
}

let geoSearch = geoLocation.flatMap { location in
return ApiController.shared.currentWeather(lat: location.coordinate.latitude, lon: location.coordinate.longitude)
.catchErrorJustReturn(ApiController.Weather.dummy)
}

let search = Observable.from([geoSearch, textSearch, mapSearch])
.merge()
.asDriver(onErrorJustReturn: ApiController.Weather.dummy)

let running = Observable.from([searchInput.map { _ in true },
geoInput.map { _ in true },
mapInput.map { _ in true},
search.map { _ in false }.asObservable()])
.merge()
.startWith(true)
.asDriver(onErrorJustReturn: false)

running
.skip(1)
.drive(activityIndicator.rx.isAnimating)
.disposed(by: bag)

running
.drive(tempLabel.rx.isHidden)
.disposed(by: bag)

running
.drive(iconLabel.rx.isHidden)
.disposed(by: bag)

running
.drive(humidityLabel.rx.isHidden)
.disposed(by: bag)

running
.drive(cityNameLabel.rx.isHidden)
.disposed(by: bag)

search.map { "\($0.temperature)° C" }
.drive(tempLabel.rx.text)
.disposed(by: bag)

search.map { $0.icon }
.drive(iconLabel.rx.text)
.disposed(by: bag)

search.map { "\($0.humidity)%" }
.drive(humidityLabel.rx.text)
.disposed(by: bag)

search.map { $0.cityName }
.drive(cityNameLabel.rx.text)
.disposed(by: bag)

locationManager.rx.didUpdateLocations
.subscribe(onNext: { locations in
print(locations)
})
.disposed(by: bag)

mapButton.rx.tap
.subscribe(onNext: {
self.mapView.isHidden = !self.mapView.isHidden
})
.disposed(by: bag)

mapView.rx.setDelegate(self)
.disposed(by: bag)

search.map { [$0.overlay()] }
.drive(mapView.rx.overlays)
.disposed(by: bag)
}

第五章:Filtering Operator

Ignoring

忽略所有event,转化为一个 Comletable。

1
func ignoreElements() -> Completable

仅发射指定index的event,其余都忽略。

1
func elementAt(_ index: Int) -> Observable<String>

或者直接使用 .filter ,是 RxSwift 对 Swift 标准库中 .filter 对应版本。具体用法很简单,和 Swift 版一样。

Skipping

.skip(n) 即可跳过前 n 个 event。

.skipWhile 跳过符合条件的 event,但是遇到不符合条件的 event 后,不再跳过该 event 以及后面的event。

1
2
3
4
5
6
7
8
9
Observable.of(2, 2, 3, 4, 5)
.skipWhile { integer in
print("\(integer) skipWhile")
return integer % 2 == 0
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

Console 输出:

1
2
3
4
5
6
2 skipWhile
2 skipWhile
3 skipWhile
3
4
5

.skipUntil(trigger) 持续跳过直到另一个Observable生成元素。

Taking

Taking 和 Skipping 是相反的。

.take(n) 只发射前 n 个 event 。

.takeWhile { } 放射符合条件的event,直到遇到不符合条件的元素,不再发射。

1
2
3
4
5
6
7
8
9
10
11
12
let disposeBag = DisposeBag()

Observable.of(2, 2, 4, 4, 6, 6)
.enumerated()
.takeWhile { index, integer in
integer % 2 == 0 && index < 3
}
.map { $0.element }
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

Console 输出:

1
2
3
2
2
4

.takeUntil(trigger) 类似的,持续发射,直到另一个Observable生成元素,后面不再发射。

Distinct

.distinctUntilChanged() 只发射不同于前一个的元素。

1
2
3
4
5
6
Observable.of("A", "A", "B", "B", "A")
.distinctUntilChanged()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

Console 输出:

1
2
3
A
B
A

distinctUntilChanged(_:) 对阻止重复发射不符合 Equatable 类型的元素时很有用。因为你可以控制前后两个元素是否相等。

第六章:Filtering 实践

上一章介绍了RxSwift中函数式编程的观念。操作符操作一个Observable,输出一个心得Observable。得益于此,我么可以链式调用操作符。

改进 Combinestagram 项目

就是把上一章的 filtering 操作符应用在项目中。

基于时间的操作符

基于时间的操作符使用 Scheduler

take(_:scheduler:) 是一个过滤操作符,和 take(1)takeWhile(...) 类似。

只会发送指定时间段内的元素。一旦时间点过了,就会发送 .complete 事件。

1
2
3
4
5
6
7
8
9
10
11
private func errorMessage() {
alert(title: "No access to Camera Roll",
text: "You can grant access to Combinestagram from the Settings app")
.asObservable()
.take(5.0, scheduler: MainScheduler.instance)
.subscribe(onCompleted: { [weak self] in
self?.dismiss(animated: true, completion: nil)
_ = self?.navigationController?.popViewController(animated: true)
})
.disposed(by: bag)
}

以下是一些合适使用 throttle 的场景:

  • 搜索输入框的订阅,然后发送当前文本内容的API请求。通过 throttle , 你可以让用户快速输入,同时仅在用户完成输入时才你的向服务器发送请求。
  • 当用户点击按钮来展示一个 modal view controller 时,你可以阻止双击以及展示两次 modal view controller。通过 throttle 点击事件,可以现实只接收最后一次点击事件,不论用户双击或三击。
  • 如果你只关心用户拖动手势时停留的地方。你可以使用 throttle 当前触摸位置,只考虑那些停止改变的位置的元素。
1
2
3
4
5
6
7
8
images.asObservable()
.throttle(0.5, scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] photos in
guard let preview = self?.imagePreview else { return }
preview.image = UIImage.collage(images: photos,
size: preview.frame.size)
})
.disposed(by: bag)

第七章:Transforming Operator

当你决定学习 RxSwift,或许会觉得它是深奥的库。也许让你想起来当初学习 iOS 或者 Swift 的时候。但学习到第七章你应该意识到 RxSwift 并非魔法。它是精心构建的API,能让极大地提升效率和简化代码。学到这里你应该会感觉不错。

本章你将学习运算符中最重要的一类:transforming 运算符。你将一直使用 transforming,准备Observable数据以供订阅者使用。需要强调的是,RxSwift 中的 transforming 运算符与 Swift 标准库之间有相似之处 ,如 map (_:)flatMap (_:)

Transforming 元素

将可观察到的单个元素转换为所有这些元素的数组的一种简便方法是使用 toArraytoArray

1
2
3
4
5
6
7
let disposeBag = DisposeBag()
Observable.of("A", "B", "C")
.toArray()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

map 可将每个元素进行 transforming 。map

1
2
3
4
5
6
7
8
9
10
11
12
let disposeBag = DisposeBag()
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut

Observable<NSNumber>.of(123, 4, 56)
.map {
formatter.string(from: $0) ?? ""
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
1
2
3
4
5
6
7
8
9
10
let disposeBag = DisposeBag()
Observable.of(1, 2, 3, 4, 5, 6)
.enumerated()
.map { index, integer in
index > 2 ? integer * 2 : integer
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

Transforming 内部 Observable

RxSwift 包含 flatMap 系列中的几个运算符, 允许你深入Observable,处理其Observable类型的属性。flatMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Student {
var score: BehaviorSubject<Int>
}

let disposeBag = DisposeBag()

let ryan = Student(score: BehaviorSubject(value: 80))
let charlotte = Student(score: BehaviorSubject(value: 90))

let student = PublishSubject<Student>()

student
.flatMap {
$0.score
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

student.onNext(ryan)
ryan.score.onNext(85)

student.onNext(charlotte)
ryan.score.onNext(95)

charlotte.score.onNext(100)

What makes

flatMapLatest different is that it will automatically switch to the latest observable and

unsubscribe from the the previous one.

flatMapLatestflatMap 略有不同之处是它会自动切换到最后一个 Observable,注销订阅之前的 Observable。

flatMapLatest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let disposeBag = DisposeBag()

let ryan = Student(score: BehaviorSubject(value: 80))
let charlotte = Student(score: BehaviorSubject(value: 90))

let student = PublishSubject<Student>()

student
.flatMapLatest {
$0.score
}
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)

student.onNext(ryan)

ryan.score.onNext(85)

student.onNext(charlotte)

ryan.score.onNext(95)

charlotte.score.onNext(100)

// Only one thing to point out here that’s different from the previous example of flatMap:
// Changing ryan’s score here will have no effect. It will not be printed out. This is because flatMapLatest has already switched to the latest observable, for charlotte.

观察事件

使用具体化运算符 materialize, 可以将 Observable 发射的每个事件包装成 Observable。比如下面的例子中 $0.score.materialize()BehaviorSubject<Int> 封装为 Observable<Event<Int>> 。这样的话,我的就可以自己处理 .error 和 .complete 事件,而不终结该 Observable 。

对应的我们再使用 .dematerialize() 转回 Observable<Int> ,即可正常订阅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
enum MyError: Error {
case anError
}

let disposeBag = DisposeBag()

let ryan = Student(score: BehaviorSubject(value: 80))
let charlotte = Student(score: BehaviorSubject(value: 100))

let student: BehaviorSubject<Student> = BehaviorSubject(value: ryan)

let studentScore: Observable<Event<Int>> = student
.flatMapLatest {
$0.score.materialize()
}

studentScore
.filter {
guard $0.error == nil else {
print($0.error!)
return false
}

return true
}
.dematerialize()
.subscribe(onNext: {
print("subscribe: \($0)")
})
.disposed(by: disposeBag)

ryan.score.onNext(85)
ryan.score.onError(MyError.anError)
ryan.score.onNext(90)
student.onNext(charlotte)

Console 输出结果:

1
2
3
4
subscribe: 80
subscribe: 85
anError
subscribe: 100

第八章:Transforming 实践

在上一章中学习了 RxSwift 的主要功能,包括:map 和 flatMap 。

开始 GitFeed 项目

本章中要处理的项目将显示 GitHub 仓库的动态, 如所有最新的 like, fork 和 comment.。

该项目将有两个不同的故事情节:

  • 主要的情节是要接触到 GitHub 的 JSON API, 接收 JSON 响应, 并最终将其转换为对象集合.  
  • 次要将提取的对象保存到磁盘并在 “新” 活动列表之前的表中显示它们。从服务器获取事件。

网络获取数据

我们将会将 URLSession 添加个响应式拓展。

CocoaAPIToRxAPI

具体就是把网络API的请求和响应分别封装起来:

其中 share(replay:, scope:) ,可以避免 URLSession.rx.response(request:) 因过早 .complete 造成订阅时重新发送网络请求。原理就是封装多个 Observable 的元素为一个新的 Observable,并指定缓冲区最大元素数量,生命周期。定义如下:

1
func share(replay: Int = default, scope: SubjectLifetimeScope = default)

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
func fetchEvents(repo: String) {
// 1. 使用 map 构造一个请求
let response = Observable.from([repo])
.map { urlString -> URL in
return URL(string: "https://api.github.com/repos/\(urlString)/events")!
}
.map { [weak self] url -> URLRequest in
var request = URLRequest(url: url)
if let modifiedHeader = self?.lastModified.value {
request.addValue(modifiedHeader as String,
forHTTPHeaderField: "Last-Modified")
}
return request
}
.flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in
return URLSession.shared.rx.response(request: request)
}
.share(replay: 1, scope: .whileConnected)

// 2. 转换响应
response
.filter { response, _ in
// 检查右边的值是否在左边的 range 内
return 200..<300 ~= response.statusCode
}
.map { _, data -> [[String: Any]] in
guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
let result = jsonObject as? [[String: Any]] else {
return []
}
return result
}
.filter { objects in
return objects.count > 0
}
.map { objects in
return objects.flatMap(Event.init)
}
.subscribe(onNext: { [weak self] newEvents in
self?.processEvents(newEvents)
})
.disposed(by: bag)

response
.filter { response, _ in
return 200..<400 ~= response.statusCode
}
.flatMap { response, _ -> Observable<NSString> in
guard let value = response.allHeaderFields["Last-Modified"] as? NSString else {
return Observable.empty()
}
return Observable.just(value)
}
.subscribe(onNext: { [weak self] modifiedHeader in
guard let strongSelf = self else { return }
strongSelf.lastModified.value = modifiedHeader
try? modifiedHeader.write(to: strongSelf.modifiedFileURL, atomically: true,
encoding: String.Encoding.utf8.rawValue)
})
.disposed(by: bag)
}

第九章:合并Operator

在前面的章节中, 你学习了如何创建、filter 和转换可观察序列。RxSwift filtering 和转换运算符的行为与 Swift 的标准集合运算符非常相似。你看到了 RxSwift 与 flatMap 的真正力量, 它让你用很少的代码执行很多任务. 

这一章将向你展示几种不同的组合序列的方法, 以及如何在每个序列中组合数据。你将使用的一些操作员与 Swift 集合操作员非常相似。它们有助于结合异步序列中的元素, 就像使用 Swift 数组一样。

前缀和串联

startWith(_:) 运算符赋值给定初始值的可观察序列。此值必须与可观察元素的类型相同。

1
2
let numbers = Observable.of(2, 3, 4)
let observable = numbers.startWith(1) observable.subscribe(onNext: { value in print(value) })

类似地 .concat(_:) 可以合并两个序列。

1
2
3
let first = Observable.of(1, 2, 3) let second = Observable.of(4, 5, 6)
let observable = Observable.concat([first, second])
observable.subscribe(onNext: { value in print(value) })

Merge

Merge 也是合并,不过会按照顺序发射事件。

merge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let left = PublishSubject<String>()
let right = PublishSubject<String>()

let source = Observable.of(left.asObservable(), right.asObservable())

let observable = source.merge()
let disposable = observable.subscribe(onNext: { value in
print(value)
})

var leftValues = ["Berlin", "Munich", "Frankfurt"]
var rightValues = ["Madrid", "Barcelona", "Valencia"]

repeat {
if arc4random_uniform(2) == 0 {
if !leftValues.isEmpty {
left.onNext("Left: " + leftValues.removeFirst())
}
} else if !rightValues.isEmpty {
right.onNext("Right: " + rightValues.removeFirst())
}
} while !leftValues.isEmpty || !rightValues.isEmpty

disposable.dispose()

Console 输出:

1
2
3
4
5
6
Right: Madrid
Right: Barcelona
Left: Berlin
Left: Munich
Left: Frankfurt
Right: Valencia

Combining 元素

RxSwift中一组基本的运算符是 combineLatest 。他们组合来自几个序列的值:

Combining elements

Every time one of the inner (combined) sequences emits a value, it calls a closure you provide. You receive the last value from each of the inner sequences. This has many concrete applications, such as observing several text fields at once and combining their value, watching the status of multiple sources, and so on.

每当内部(组合)序列中的一个发出一个值时,它会调用您提供的闭包。 你会收到每个内部序列的最后一个值。 这里有很多具体的应用场景,例如一次观察几个文本字段并结合它们的价值,观察多个来源的状态等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let left = PublishSubject<String>()
let right = PublishSubject<String>()

let observable = Observable.combineLatest(left, right, resultSelector: {
lastLeft, lastRight in
"\(lastLeft) \(lastRight)"
})
let disposable = observable.subscribe(onNext: { value in
print(value)
})

print("> Sending a value to Left")
left.onNext("Hello,")
print("> Sending a value to Right")
right.onNext("world")
print("> Sending another value to Right")
right.onNext("RxSwift")
print("> Sending another value to Left")
left.onNext("Have a good day,")

disposable.dispose()

Console 输出:

1
2
3
4
5
6
7
> Sending a value to Left
> Sending a value to Right
Hello, world
> Sending another value to Right
Hello, RxSwift
> Sending another value to Left
Have a good day, RxSwift

zip 总是按照顺序一对一对的组合。类似把两个数组按照相同的下标进行组合。

zip

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Weather {
case cloudy
case sunny
}
let left: Observable<Weather> = Observable.of(.sunny, .cloudy, .cloudy, .sunny)
let right = Observable.of("Lisbon", "Copenhagen", "London", "Madrid", "Vienna")

let observable = Observable.zip(left, right) { weather, city in
return "It's \(weather) in \(city)"
}
observable.subscribe(onNext: { value in
print(value)
})

Console 输出:

1
2
3
4
It's sunny in Lisbon
It's cloudy in Copenhagen
It's cloudy in London
It's sunny in Madrid

Triggers

应用程序有不同的需求,必须管理多个输入源。 你经常需要立即接受来自多个观察对象的输入。 有些会简单地在代码中触发行为,而其他的将提供数据。 RxSwift 已经覆盖了强大的运算符这会让你的生活更轻松。 或者,至少你的编码生活!

首先看看 withLatestFrom(_:)。 经常被初学者忽略,它在处理用户界面等方面是一个有用的配套工具。trigger

1
2
3
4
5
6
7
8
9
10
11
12
13
let button = PublishSubject<Void>()
let textField = PublishSubject<String>()

let observable = button.withLatestFrom(textField)
_ = observable.subscribe(onNext: { value in
print(value)
})

textField.onNext("Par")
textField.onNext("Pari")
textField.onNext("Paris")
button.onNext(())
button.onNext(())

Console 输出:

1
2
Paris
Paris

sample(_:) 和 几乎只有一个变化:每当触发器observable发射一个值时,sample(_:) 从其它的可观察值发出最新值,但只有在自上一次元素后才到达。 如果没有新的数据到达,sample(_:) 将不会发射任何东西。sample

Switches

Switches

amb(_:) 运算符订阅左侧和右侧的可观察序列。 它等待其中任何一个发出一个元素,然后退订另一个序列。 之后,它只传递第一个活动可观察元素。 它可以从模棱两可中选出一个:首先,你不知道你感兴趣的是哪一个序列,等其中先发射一个序列再做决定。

这个运算符经常被忽视。 它有一些精选的实际应用场景,如连接到冗余服务器,并坚持使用第一个响应的服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let left = PublishSubject<String>()
let right = PublishSubject<String>()

let observable = left.amb(right)
let disposable = observable.subscribe(onNext: { value in
print(value)
})

left.onNext("Lisbon")
right.onNext("Copenhagen")
left.onNext("London")
left.onNext("Madrid")
right.onNext("Vienna")

disposable.dispose()

更流行的选项是 switchLatest() 运算符:

可以自由切换偏爱那个子序列发射的元素。switchLatest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let one = PublishSubject<String>()
let two = PublishSubject<String>()
let three = PublishSubject<String>()

let source = PublishSubject<Observable<String>>()

let observable = source.switchLatest()
let disposable = observable.subscribe(onNext: { value in
print(value)
})

source.onNext(one)
one.onNext("Some text from sequence one")
two.onNext("Some text from sequence two")

source.onNext(two)
two.onNext("More text from sequence two")
one.onNext("and also from sequence one")

source.onNext(three)
two.onNext("Why don't you seem me?")
one.onNext("I'm alone, help me")
three.onNext("Hey it's three. I win.")

source.onNext(one)
one.onNext("Nope. It's me, one!")

disposable.dispose()

Console 输出:

1
2
3
4
5
Some text from sequence one
More text from sequence two
Hey it's three. I win.
Nope. It's me, one!

形成可观察序列的心智模型可能很困难。 别担心, 你会习惯它。 练习是顺序理解序列的关键。 随着您的体验增长,请随时审查这些示例! 你将在下一章中更好地了解如何使用它。

Combining 一个序列中元素

所有的厨师都知道你减少得越多,酱汁就越美味。 虽然不是针对厨师,但 RxSwift 拥有将酱汁减少到最有味道的组分的工具。

reduce(_:_:) 和 Swift 标准库中类似。reduce

1
2
3
4
5
6
7
8
let source = Observable.of(1, 3, 5, 7, 9)
let observable = source.reduce(0, accumulator: { summary, newValue in
return summary + newValue
})

observable.subscribe(onNext: { value in
print(value)
})

Console 输出:

1
2
25

scan(_:accumulator:)reduce(_:_:) 只有一个最终结果不同:每次发射事件,都会计算结果并发射。scan

1
2
3
4
5
6
let source = Observable.of(1, 3, 5, 7, 9)

let observable = source.scan(0, accumulator: +)
observable.subscribe(onNext: { value in
print(value)
})

Console 输出:

1
2
3
4
5
6
1
4
9
16
25

第十章:合并Operator实践

在前一章中,你学习了将操作符合并,并通过对一些相当令人头脑灵活的概念进行越来越详细的练习。 一些运算符可能会让你对这些反应性概念的真实应用感到疑惑。
在“……实践”一章中,你将有机会尝试一些最强大的运算符。 你将学会解决类似于你在自己的应用程序中遇到的问题。

准备网络后端服务

这里使用NASA的EONET服务。

我们称之为EONET服务。 它抽象访问由EONET服务器公开的数据,将它们作为服务提供给你的应用程序。 你会看到,结合Rx,这种模式将找到许多应用程序。 它可以让你在应用程序内清晰地分离数据生产和消耗。 你可以轻松替换或模拟生产环境,而不会对用户方面产生任何影响。

先封装一个通用网络请求,然后把 categories 封装为单例,方便所有订阅者订阅。

Categories view controller

categories view controller 显示 categories 列表。

Adding the event download service

Getting events for categories

Events view controller

Wiring the days selector

Splitting event downloads

第十一章:基于时间的Operator

时间就是一切。响应式编程背后的核心思想是基于时间异步数据流的模型。在这方面, RxSwift 提供了一系列操作, 使你能够处理时间和序列在一段时间内的响应和转换方式。正如你将在本章中看到的, 管理序列的时间维度是简单而直接的。

📒RxSwift I:开始学习 RxSwift

Rx_Logo_M.png

第一章:RxSwift 介绍

RxSwift是一个组合异步和事件驱动编程的库,通过使用可观察序列和功能样式运算符来,从而允许通过调度程序进行参数化执行。

RxSwift 在本质上简化了开发异步程序,允许代码对新数据作出反应,并以顺序和孤立的方式处理它。

介绍异步编程

51526546882_.pic.jpg

使用 Cocoa 和 UIKit 异步的API的问题在于:复杂的异步代码变得非常难写,部分原因是苹果SDK提供的API种类繁多。如NotificationCenter、Delegate、GCD、闭包、Combine。

异步编程词汇表:

  1. 状态(State),具体地说:共享可变状态
  2. 命令式编程(Imperative programming)
  3. 副作用(Side effects)
  4. 声明式编程(Declarative code):声明式代码让您可以定义行为片段。只要有相关事件发生,RxSwift 就会运行这些行为,并提供一个不可变的、孤立的数据片段来处理。这样,您可以使用异步代码,但做出与简单 for 循环相同的假设:您正在使用不可变数据并且可以以顺序、确定的方式执行代码。
  5. 响应式系统(Reactive systems)

响应式系统(Reactive systems)是一个相当抽象的术语,它涵盖了Web或iOS应用程序,它们显示了大多数或全部以下特性:

  • 响应式设计(Responsive):始终保持UI更新,代表了最新的应用程序状态。
  • 能复原的(Resilient):每个行为都是独立定义的,并提供灵活的错误恢复。
  • 灵活的(Elastic): 该代码处理不同的工作负载,通常实现诸如懒惰驱动数据收集、事件节流和资源共享等特性。
  • 消息驱动(Message driven):组件使用基于消息的通信来提高可重用性和隔离性,解耦类的生命周期和实现。

RxSwift 基础

61526549641_.pic_hd.jpg

这个标志是一只电鳗。(Rx 项目曾经被称为Volta)

RxSwift 是微软开源的 ReactiveX 的Swift语言的实现。

RxSwift 在传统的Cocoa编程和纯函数编程之间找到了最佳位置。它允许您对事件作出反应, 方法是使用不可变的代码定义以确定性的、可组合的方式处理异步输入部分。

Rx代码的三个组成部分是 Observable, OperatorScheduler

Observable<Element>类提供了Rx代码的基础:异步产生一系列事件的能力,它可以“携带”数据的不可变快照。简单来说,它允许类在一段时间内订阅其他类发出的值。

ObservableType 协议 (Observable需要遵循的) 非常简单。Observable可能发出 (并且Observer能接受) 仅三类型事件:

- next 下一个事件: “携带” 最新 (或 “下一个 “) 数据值的事件。这是Observer “接收” 值的方式。

- completed 已完成的事件: 此事件以成功终止事件序列。这意味着Observable完成其生命周期成功, 不会发出任何其他事件。

- error 错误事件: Observable终止带有错误, 不会发出其他事件.

两种不同的可观测序列: 有限和无限的。

由于它们是高度解耦和可组合的, 所以这些方法通常称为Operator。比如filter。

Operator也是高度可组合的,它们总是把数据作为输入并输出它们的结果,所以你可以用许多不同的方式轻松地将它们连接起来,实现比单个Operator自己能做的更多的事情。

SchedulerGCDOperationQueue的Rx等价物。

RxSwift将充当你的订阅(在左边)和Scheduler(在右边)之间的调度器,将工件发送到正确的上下文,并无缝地允许它们与彼此的输出一起工作。

71526551674_.pic_hd.jpg

要读取此关系图, 请在不同的计划程序中按预定的顺序 (1、2、3、…) 来执行彩色作品。例如:

·蓝色网络订阅以在基于自定义 NSOperation 的计划程序上运行的一段代码 (1) 开始。

·数据输出块作为下一个块 (2) 的输入, 它运行在一个不同的调度程序上, 它位于并发后台 GCD 队列中。

·最后, 在主线程调度程序上计划最后一块蓝色代码 (3), 以便用新数据更新 UI。

App architecture 应用的架构

值得一提的是,RxSwift并没有以任何方式改变应用程序的架构;它主要处理事件、异步数据序列和通用通信协议。

通过在苹果开发文档中实现MVC体系结构,可以创建具有Rx的应用程序。如果你喜欢的话,你也可以选择实现MVP架构或MVVM。RxSwift也可以帮你实现自己的单向数据架构。

微软的MVVM架构是专门针对在平台上创建的事件驱动软件开发的,该平台提供数据绑定。RxSwift和MVVM很好地结合在一起,在这本书的末尾,你会看到这个模式以及如何用RxSwift来实现它。

MVVM和RxSwift结合在一起的原因是,ViewModel允许您公开Observable属性,这些属性可以直接绑定到View Controller 代码中的UIKit控件。这使得绑定模型数据到UI非常简单地展示和编码:

81526552550_.pic.jpg

本书中的所有其他示例都使用MVC架构来保持示例代码简单易懂。

RxCocoa

RxSwift是通用Rx API的实现。因此,它不涉及任何Cocoa或UIKit类。

RxCocoa是RxSwift的配套库,所有的类都有助于UIKit和Cocoa的开发。除了具有一些高级类之外,RxCocoa还为许多UI组件添加了响应式扩展,以便您可以订阅不同的UI事件。

例如,使用RxCocoa订阅UISwitch的状态变化是非常容易的,例如:

1
2
3
4
toggleSwitch.rx.isOn
.subcribe(onNext: {enabled in
print(enabled ? "it's ON" : "it's OFF")
})

RxCocoa adds the rx.isOn property (among others) to the UISwitch class so you can subscribe to generally useful event sequences.

RxCocoa将rx.isOn属性(其中之一)添加到UISwitch类,这样您就可以订阅通常有用的Observable序列。

101526552877_.pic.jpg

安装

官方git:https://github.com/ReactiveX/RxSwift

使用 CocoaPodsCarthageSwift Package Manager 均很方便集成RxSwift。

RxSwift 和 Combine

RxSwift 和 Combine(以及 Swift 中的其他相应式编程框架)共享许多通用语言和非常相似的概念。

RxSwift是一个较旧的,完善的框架,具有一些自己的原始概念,运算符名称和类型多样性,这主要是由于其多平台跨语言标准,该标准也适用于Linux,这对于Server-Side Swift非常有用。它也是开源的,所以如果你愿意,你可以直接为其核心做出贡献,并确切地看到它的特定部分是如何工作的。它与所有支持 Swift 的 Apple 平台版本兼容,一直支持 iOS 8。

Combine 是 Apple 新的、闪亮的框架,涵盖了类似的概念,但专门针对 Swift 和 Apple 自己的平台量身定制。它与 Swift 标准库共享许多通用语言,因此即使新手也觉得 API 非常熟悉。它仅支持从iOS 13,macOS 10.15等开始的较新的Apple平台。不幸的是,截至今天,它还没有开源,并且不支持Linux。

幸运的是,由于 RxSwift 和 Combine 非常相似,因此您的 RxSwift 知识可以轻松转移到 Combine,反之亦然。RxCombine(https://github.com/CombineCommunity/RxCombine)等项目允许您根据需要混合搭配 RxSwift Observables 和 Combine Publishers。

RxSwift 6.5.0 也迎来了 Swift Concurrency 的支持。提供了互操作性:

  1. await 调用Observablevalues
  2. 封装 async Task 为 Observable

社区

RxSwift社区非常友好,思想开放,并且热衷于讨论模式,常用技巧或互相帮助。

更多的Rx库和实验,像雨后春笋一样的涌现,可以在这里找到:https://github.com/RxSwiftCommunity

可能最好的方式来满足许多对RxSwift感兴趣的人,这是Slack的频道:http://rxswift-slack.herokuapp.com

Slack频道拥有约5000名成员! 日常的主题包括:互相帮助,讨论RxSwift或其同伴库的潜在新功能,以及共享RxSwift博客文章和会议讲座。

第二章:Observables

Observable 是什么

Observable、observable sequence 和 sequence 在 Rx 都是一个意思。或者在其他Rx实现中称之为stream。

最好称之为 Observable,不过翻译过来还是序列顺口些。

Observable 的生命周期

  • observable发出包含元素的next事件。 它可以继续这样做,直到它:
  • …发出error事件并终止,或
  • …发出completed事件并终止。
  • 一旦observable被终止,它不能再发出事件。

将这种概念化的最佳方法之一是使用弹珠图(Marble Diagrams 基于时间轴上绘制的值)。

141526623515_.pic.jpg

151526623523_.pic.jpg

创建 Observable

类型擦除的ObservableType,也就是 Swift 中的泛型。

它代表了一种推式风格队列。

let observable: Observable = Observable.just(one)

订阅 Observable

订阅observable sequence的事件处理程序。

func subscribe(_ on: @escaping (RxSwift.Event<Self.E>) -> Swift.Void) -> Disposable

1
2
3
4
5
6
7
8
9
let one = 1
let two = 2
let three = 3

let observable = Observable.of(one, two, three)

observable.subscribe(onNext: { element in
print(element)
})

在指定的范围内生成一个整数的observable sequence,使用指定的scheduler生成和发送Observer消息。

1
static func range(start: Self.E, count: Self.E, scheduler: ImmediateSchedulerType = default) -> RxSwift.Observable<Self.E>
1
2
3
4
5
6
7
8
let observable = Observable<Int>.range(start: 1, count: 10)
observable
.subscribe(onNext: { i in
let n = Double(i)
let fibonacci = Int(((pow(1.61803, n) - pow(0.61803, n)) /
2.23606).rounded())
print(fibonacci)
})

Disposing 和 terminating

请记住, 在收到订阅之前, Observable的内容不会执行任何事情。它是触发一个Observable的开始发出事件的订阅, 直到它发出. error 或.completed完成事件并终止。您可以通过取消对它的订阅来手动终止Observable。

subscription.dispose()

单独管理每个订阅将是单调乏味的, 因此 RxSwift 引入 DisposeBag 类型。DisposeBag 持有 disposable协议的对象,通常是使用.disposed(by:) 方法, 并将调用dispose(), 当DisposeBag即将deallocated。

1
2
3
4
5
6
let observable = Observable.of("A", "B", "C")

let subscription = observable.subscribe { event in
print(event)
}
subscription.dispose()

create操作符接受一个名为subscribe的参数。 它的工作是提供对可观察对象进行调用订阅的实现。 换句话说,它定义了将发送给订阅者的所有事件。

1
static func create(_ subscribe: @escaping (AnyObserver<String>) -> Disposable) -> Observable<String>

创建 observable 工厂类

可以创建一个可观察的工厂,向每个订阅者发布一个新的Observable,而不是创建一个等待订阅者的Observable。

1
static func deferred(_ observableFactory: @escaping () throws -> Observable<Int>) -> Observable<Int>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let disposeBag = DisposeBag()

var flip = false

let factory: Observable<Int> = Observable.deferred {
flip = !flip

if flip {
return Observable.of(1, 2, 3)
} else {
return Observable.of(4, 5, 6)
}
}

for _ in 0...3 {
factory.subscribe(onNext: {
print($0, terminator: "")
})
.disposed(by: disposeBag)

print()
}
1
2
3
4
123 
456
123
456

使用Traits

Traits是具有比常规Observable更窄的行为集合的一种Observable。它们的使用是可选的;您可以在任何可能使用trait的地方使用常规Observable。他们的目的是提供一种给你的API或者代码的读者更清楚地表达意图的方式。

RxSwift中有三种Traits: Single, Maybe 和 Completable.

Singles将发出.success(value)或.error事件。 .success(value)实际上是.next和.completed事件的组合。 这对于一次性进程非常有用,它可以成功并产生一个值或失败,例如下载数据或从磁盘加载数据。

下面的例子是读取 Copyright.txt 的文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
let disposeBag = DisposeBag()

enum FileReadError: Error {
case fileNotFound, unreadable, encodingFailed
}

func loadText(from filename: String) -> Single<String> {
return Single.create { single in
let disposable = Disposables.create()

guard let path = Bundle.main.path(forResource: filename, ofType: "txt") else {
single(.error(FileReadError.fileNotFound))
return disposable
}

guard let data = FileManager.default.contents(atPath: path) else {
single(.error(FileReadError.unreadable))
return disposable
}

guard let contents = String(data: data, encoding: .utf8) else {
single(.error(FileReadError.encodingFailed))
return disposable
}

single(.success(contents))

return disposable
}
}

loadText(from: "Copyright")
.subscribe {
switch $0 {
case .success(let string):
print(string)
case .error(let error):
print(error)
}
}
.disposed(by: disposeBag)

副作用

do Operator允许插入副作用,处理程序执行过程中并不会以任何方式更改发出的事件的操作。

打印debug 信息

debug Operator, 它将打印observable的每个事件的信息。

第三章:Subjects

Subject 是什么

既可以作为Observable,也可以作为Observer,这就是所谓的 Subjects。

也是最方便互操作的,BehaviorRelay比较常用。

RxSwift 中有四个subject类型:

  1. PublishSubject: 开始为空, 只向订阅者发出新元素.
  2. ReplaySubject: 用缓冲区大小, 并将保持元素的缓冲区大小, 并将其重播到新订阅者.
  3. BehaviorSubject: 从初始值开始, 将其重播或将最新的元素给新订阅者.
  4. AsyncSubject:仅发出序列中的最后一个next事件,并且仅当subject收到completed事件时才发出。这是一个很少使用的主题。

PublishSubject

1
2
3
/// Represents an object that is both an observable sequence as well as an observer.
/// Each notification is broadcasted to all subscribed observers.
public final class PublishSubject<Element>

当你只是想让订阅者只接受在订阅的时候以后发生的新的事件,直到他们unsubscribe,或者subject已经terminated以.completed或.error事件的方式,PublishSubject就可以派上用场。

RxSwiftPublishSubject

第一个订阅者在将 1 添加到subject后进行订阅,因此它不会收到该事件。不过,它确实得到了2和3。由于第二个订阅者在添加2之前不会加入,因此它只能获得3。

1
2
3
4
5
6
extension ObservableType {
/// 添加带有`id`的观察者并打印每个发出的事件。
func addObserver(_ id: String) -> Disposable {
subscribe { print("观察者:", id, "事件:", $0) }
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 从订阅开始向所有观察者广播新事件。
example("PublishSubject") {
let disposeBag = DisposeBag()
let subject = PublishSubject<String>()

subject.addObserver("1").disposed(by: disposeBag)
subject.onNext("🐶")
subject.onNext("🐱")

subject.addObserver("2").disposed(by: disposeBag)
subject.onNext("🅰️")
subject.onNext("🅱️")

subject.onCompleted()
}
1
2
3
4
5
6
7
8
9
--- PublishSubject example ---
观察者: 1 事件: next(🐶)
观察者: 1 事件: next(🐱)
观察者: 1 事件: next(🅰️)
观察者: 2 事件: next(🅰️)
观察者: 1 事件: next(🅱️)
观察者: 2 事件: next(🅱️)
观察者: 1 事件: completed
观察者: 2 事件: completed

ReplaySubject

1
2
3
4
/// Represents an object that is both an observable sequence as well as an observer.
///
/// Each notification is broadcasted to all subscribed and future observers, subject to buffer trimming policies.
public class ReplaySubject<Element>

ReplaySubject将临时缓存, 或缓冲区, 它们发出的最新元素, 由您选择的 specified 大小决定。然后, 他们会将该缓冲区重播到新订阅者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 向所有观察者广播新事件,并向新观察者广播之前指定的bufferSize大小的事件数。
example("ReplaySubject") {
let disposeBag = DisposeBag()
let subject = ReplaySubject<String>.create(bufferSize: 1)

subject.addObserver("1").disposed(by: disposeBag)
subject.onNext("🐶")
subject.onNext("🐱")

subject.addObserver("2").disposed(by: disposeBag)
subject.onNext("🅰️")
subject.onNext("🅱️")

subject.onCompleted()
}
1
2
3
4
5
6
7
8
9
10
--- ReplaySubject example ---
观察者: 1 事件: next(🐶)
观察者: 1 事件: next(🐱)
观察者: 2 事件: next(🐱) // 相比PublishSubject多了订阅前一个事件
观察者: 1 事件: next(🅰️)
观察者: 2 事件: next(🅰️)
观察者: 1 事件: next(🅱️)
观察者: 2 事件: next(🅱️)
观察者: 1 事件: completed
观察者: 2 事件: completed

BehaviorSubject

1
2
3
/// Represents a value that changes over time.
/// Observers can subscribe to the subject to receive the last (or initial) value and all subsequent notifications.
public final class BehaviorSubject<Element>

当您希望使用最新数据预先填充View时, BehaviorSubject非常有用。例如, 可以将用户详情页中的控件绑定到BehaviorSubject, 以便在应用程序获取新数据时, 可以使用最新值来预先填充显示。

RxSwiftBehaviorSubject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
example("BehaviorSubject") {
let disposeBag = DisposeBag()
let subject = BehaviorSubject(value: "🔴")

subject.addObserver("1").disposed(by: disposeBag)
subject.onNext("🐶")
subject.onNext("🐱")

subject.addObserver("2").disposed(by: disposeBag)
subject.onNext("🅰️")
subject.onNext("🅱️")

subject.onCompleted()
}
1
2
3
4
5
6
7
8
9
10
11
--- BehaviorSubject example ---
观察者: 1 事件: next(🔴) // 相比ReplaySubject多了订阅前一个事件
观察者: 1 事件: next(🐶)
观察者: 1 事件: next(🐱)
观察者: 2 事件: next(🐱)
观察者: 1 事件: next(🅰️)
观察者: 2 事件: next(🅰️)
观察者: 1 事件: next(🅱️)
观察者: 2 事件: next(🅱️)
观察者: 1 事件: completed
观察者: 2 事件: completed

Relay

Relay在保持其replay行为的同时包装了subject。与其他subject不同,可以使用 accept(_:) 添加值,而非 onNext(_:)。这是因为Relay只能接受值,即不能向它们添加错误或已完成的事件。

PublishRelay将包装PublishSubject ,而BehaviorRelay将包装BehaviorSubject。中继与包装主体的区别在于,它们可以保证永远不会终止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import RxRelay

example("PublishRelay") {
let disposeBag = DisposeBag()
let subject = PublishRelay<String>()

subject.addObserver("1").disposed(by: disposeBag)
subject.accept("🐶")
subject.accept("🐱")

subject.addObserver("2").disposed(by: disposeBag)
subject.accept("🅰️")
subject.accept("🅱️")
}
1
2
3
4
5
6
7
--- PublishRelay example ---
观察者: 1 事件: next(🐶) //next事件和PublishSubject一样,少了completed事件
观察者: 1 事件: next(🐱)
观察者: 1 事件: next(🅰️)
观察者: 2 事件: next(🅰️)
观察者: 1 事件: next(🅱️)
观察者: 2 事件: next(🅱️)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import RxRelay

example("BehaviorRelay") {
let disposeBag = DisposeBag()
let subject = BehaviorRelay<String>(value: "🔴")

subject.addObserver("1").disposed(by: disposeBag)
subject.accept("🐶")
subject.accept("🐱")

subject.addObserver("2").disposed(by: disposeBag)
subject.accept("🅰️")
subject.accept("🅱️")
}
1
2
3
4
5
6
7
8
9
--- BehaviorRelay example ---
观察者: 1 事件: next(🔴) //next事件和BehaviorSubject,少了completed事件
观察者: 1 事件: next(🐶)
观察者: 1 事件: next(🐱)
观察者: 2 事件: next(🐱)
观察者: 1 事件: next(🅰️)
观察者: 2 事件: next(🅰️)
观察者: 1 事件: next(🅱️)
观察者: 2 事件: next(🅱️)

第四章:Observables 和 Subjects 实践

本章重点是在一个完整的应用开发中使用RxSwift,一步一步的学会如何把概念应用到实际项目中。

在 view controller 中使用 subject/relay

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
override func viewDidLoad() {
super.viewDidLoad()

images
.subscribe(onNext: { [weak imagePreview] photos in
guard let preview = imagePreview else { return }
preview.image = photos.collage(size: preview.frame.size)
})
.disposed(by: bag)

images
.subscribe(onNext: { [weak self] photos in
self?.updateUI(photos: photos)
})
.disposed(by: bag)
}

使用 subject 在 view controller 之间传值

view controller 之间传值可以通过 delegate,但是用 subject 更好。

Talkingtootherviewcontrollersviadelegate

Talkingtootherviewcontrollersviasubjects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@IBAction func actionAdd() {
let photosViewController = storyboard!.instantiateViewController(
withIdentifier: "PhotosViewController") as! PhotosViewController

navigationController!.pushViewController(photosViewController, animated: true)

photosViewController.selectedPhotos
.subscribe(
onNext: { [weak self] newImage in
guard let images = self?.images else { return }
images.accept(images.value + [newImage])
},
onDisposed: {
print("completed photo selection")
}
)
.disposed(by: bag)
}

封装已有API为Observable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import Foundation
import Photos
import RxSwift
import UIKit

class PhotoWriter {
enum Errors: Error {
case couldNotSavePhoto
}

static func save(_ image: UIImage) -> Observable<String> {
return Observable.create { observer in
var savedAssetId: String?
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
savedAssetId = request.placeholderForCreatedAsset?.localIdentifier
}, completionHandler: { success, error in
DispatchQueue.main.async {
if success, let id = savedAssetId {
observer.onNext(id)
observer.onCompleted()
} else {
observer.onError(error ?? Errors.couldNotSavePhoto)
}
}
})

return Disposables.create()
}
}
}

RxSwift traits 实践

Single

Single 只有.success.error 两种事件 。适合作为封装网络接口的返回值,要么成功,要么失败。single

1
2
3
4
5
6
7
8
9
PhotoWriter.save(image)
.asSingle()
.subscribe(onSuccess: { [weak self] id in
self?.showMessage("Saved with id: \(id)")
self?.actionClear()
}, onError: { [weak self] error in
self?.showMessage("Error", description: error.localizedDescription)
})
.disposed(by: bag)

Maybe

Maybe 有三种事件:.success.completed.error

和 Single 一样,你既可以通过 Maybe.create({ ... }) 直接创建,也可以使用 .asMaybe()

maybe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PhotoWriter.save(image)
.asMaybe()
.subscribe(
onSuccess: { [weak self] id in
self?.showMessage("Saved with id: \(id)")
self?.actionClear()
},
onError: { [weak self] error in
self?.showMessage("Error", description: error.localizedDescription)
},
onCompleted: { [weak self] in
self?.showMessage("Saved")
}
)
.disposed(by: bag)

Completable

Completable 只有.completed.error 两种事件。适合只关心是否完成,而不需要传递value的情况。使用方法:Completable.create({ ... })

completable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class PhotoWriter {
enum Errors: Error {
case couldNotSavePhoto
}

static func save(_ image: UIImage) -> Completable {
return Completable.create { completable in
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: image)
}, completionHandler: { success, error in
DispatchQueue.main.async {
if success {
completable(.completed)
} else {
completable(.error(error ?? Errors.couldNotSavePhoto))
}
}
})
return Disposables.create()
}
}
}

PhotoWriter.save(image)
.subscribe(
onCompleted: { [weak self] in
self?.showMessage("Saved")
},
onError: { [weak self] error in
self?.showMessage("Error", description: error.localizedDescription)
}
)
.disposed(by: bag)

RealmsSchema

系列文章目录:

  1. Realm 笔记 (一)
  2. Realm 笔记 (二)

第七章:Realm配置

打开一个Realm:let realm = try! Realm()

Realm没有使用单例,而是每次直接尝试新建一个Realm实例,有以下几个运行时优化:

  1. 调用Realm()返回同一个共享实例,而不受创建该Realm实例的线程限制。而Object和Realm实例被限制在创建的线程中,所以不能跨线程分享。
  2. Realm也提供了多个安全措施,如:使用不同的密钥或文件不存在均会报错。

书中均使用 try! ,但实际生产环境你可以进行错误处理。

deleteRealmIfMigrationNeeded 新版直接删除Realm文件,不做迁移处理。

存在文件在Documents文件夹有几个好处:自动备份到用户的iCloud storage,方便用户使用iTunes备份和访问。当然苹果推荐存储文件在Library文件夹。

App Bundle 文件夹是只读的,所以该目录中的Realm是能作为只读数据库。

加密在Realm极为简单,只要在配置中添加一个64位的密钥即可。

第八章:多个Realm和共享Realm

创建一个RealmProvider以方便操作Realm,尤其是涉及多个Realm或加密等情况。

img

如需转换JSON、CSV或纯文本为Realm,并内置到App Bundle。此时可以创建一个Target和来Tools.swift,自动生成Realm文件。把生成的Realm文件拖入Xcode即可。

img

第九章:依赖注入和测试

在本章中,您将学习两个重要主题:如何使用依赖注入来改进 Flash Cards 应用的架构,以及如何编写由Realm支持的同步和异步测试。

本章不会深入研究诸如测试驱动开发等主题,而是专注测试使用 Realm Object的类的技巧,并且依赖于特定于Realm的功能,例如更改通知。

测试 cards 场景(Scene)

  • CardsViewController:设置所需的手势识别器并根据用户的输入更新UI。本章不会介绍UI测试。
  • CardsModel:从Realm中抽象查询对象的简单封装。为它编写测试将意味着在测试中重复相同的代码并比较输出,而不提供实际值。除此之外,你真正要在这里测试的将是Realm的底层实现,它已经经过了充分测试。
  • CardsViewModel:实现此场景的业务逻辑。它格式化Model的输出,以显示在屏幕上,并提供与模型交互的方法。这将是一些单元测试的完美候选!

img

这里只是简单测试 ViewModel 初始化和更新状态。

img

测试 sets 场景

在显示列表的场景中,您的视图模型依赖于Realm通知来动态更新数据。你必须:

  • 模拟Realm框架以测试依赖通知的 ViewModel 和其他高级Realm功能。
  • 编写依赖Realm通知的测试

为了使测试有效,您需要稍微更改代码以提供一个方法将测试的 Realm providers 注入到Model和ViewModel中。

img

img

异步测试需要使用XCTestExpectation,这个期望类将帮助你等待某些条件在您的测试中得到满足,然后再转到您测试断言的部分。

在使用XCTWaiter类来持有测试的执行,直到满足期望为止。 XCTWaiter是Apple的XCTest框架中的一个便捷类,它暂时停止当前代码的执行,而不会阻塞当前线程。 XCTWaiter定期检查期望的状态,因此当异步代码将执行计数增加到3时,XCTWaiter将负责恢复执行测试。

步骤如下:

  • 初始化XCTestExpectation和expectedFulfillmentCount
  • 在异步回调中fulfill,
  • 最后添加定期检查结果的帮助类XCTWaiter。

img

测试 Word of Today 场景

img

小结:

在本章中,你将亲身体验用Realm编写同步和异步测试的简易性。本章中重点是:

  • 测试自己的逻辑,而不是Realm,因为它已经在全球数百万用户中得到了充分测试和使用。
  • “哑”模型和视图控制器可以更轻松地测试视图模型,您通常可以在其中放置MVVM应用程序的逻辑。
  • 编写需要这些依赖项的特定测试版本的测试时,使用中央provider结构来抽取某些依赖项的检索很有用。

通常,测试基于Realm的代码可能不会改变您构建自己的测试套件的方式。借助本章的经验,您应该能够为您的Realm项目编写可靠的测试,无论你心意哪一种架构。

注意:对于单元测试,模拟Realm本身是一个偏爱的问题。如果你想模拟Realm并从测试套件中删除Realm依赖关系,你可以模拟关键的方法,比如对象(^)、过滤器(^)等等。风险是你必须增加和保持的代码量,以保持与Realm自己行为的一致,尤其是已经有了基于内存的Realm这个特别棒的方案。

笔者觉得模拟Realm的唯一好处是,Realm依赖增加了CI服务器需要安装,构建和测试应用程序的时间。

第十章:高效率的多线程

Realm线程:多线程访问同一个Realm总是可以读写到最新的数据。

线程间传递Object,有以下两个替代方法:

  • 传递Object的主键
  • 使用ThreadSafeReference:let ref = ThreadSafeReference(to: myRealmObject)let myRealmobject = realm.resolve(ref)

主线程:访问带主键合适数量的数据时,无需担心性能问题。如在View Controller lifecycle 和 UIKit delegate 中直接读取Object,和更新UI。因为总是在同一个Realm文件的快照中工作,所以速度最快。

img

后台线程:(不推荐)由于是异步并发执行,会因为Realm 快照之间同步问题,产生性能和体积大的问题。

专用线程:(推荐)可以解决主线程影响UI,后台线程性能问题。使用Thread,自己维护一个线程的生命周期,所有读写都在该线程操作。还可以缓存任务批量提交读写事务,进一步提高性能。

img

img

简版(无缓冲机制)

img

未完待续…

img

系列文章目录:

  1. Realm 笔记 (一)
  2. Realm 笔记 (二)

Realm Database 基于C++编写的核心引擎,支持多平台多语言的移动端数据库。因其面向对象存取模型,高效的性能,开源的特性,不失为移动端数据库好的选择。之前项目也有使用过,基本上看官方文档和Demo,即可解决大部分问题。最近买了 Advanced Swift Spring Bundle,包含 Realm: Building Modern Swift Apps with Realm Database, 故再系统学习一遍,是为此笔记。

img


第一章:介绍Realm

  1. Realm API是更现代且符合最佳实践的代码,比处理C语言的API和SQLite容易的多。亦即无需使用SQL语言,而是苹果的NSPredicate。
  2. Realm 数据库的设计哲学的基础之一是现代的应用开发使用的对象。模型就是对象,Realm提供基类Object,继承自NSObject。属性也支持Swift中原始和基本类型,对集合类也进行了封装,纯面向对象编程。

对象:是指面向对象编程中的对象,Object:文中专指Realm中模型的基类Object

img

img

3.如果你偏爱 struct,只需要添加 toStruct()fromStruct(_) 方法到 object,即可快速读取struct的数据。

img


第三章:Object基础和数据类型

数据类型:

  1. 对象类型属性:@objc dynamic var 修饰,String、Date、Data,支持可选
  2. 原始类型属性:包含Bool、Int、Float、Double。let allowsPublication = RealmOptional<Bool>() 需要使用Realm封装的可选类型
  3. 自定义类型:比如封装CLLocation、封装枚举值

img

属性速查表

img

Object支持还以下几个属性:

  1. 计算属性
  2. 主键:在移动应用使用自增主键不是一个好主意,尤其是使用Realm。
  3. 索引:谨慎的使用索引,仅在反复查询的属性上使用。
  4. 忽略属性

@objcMembers 修饰 Object,是非常适合使用的一个场景。


第四章:模式和关系

  1. 对一
  2. 对多(Object):使用List,和Swift中Array类似
  3. 对多(Value):
  4. 反向关系:LinkingObjects

第五章:读写

查询结果(Results)

Results 是一个惰性抓取持久化数据的API。

过滤结果:使用NSPredicate

  1. 子查询谓词(Sub-query predicates)

img

  1. 谓词速查表

img

img

排序结果:单属性排序 .sorted(byKeyPath: “firstName”) 和多个属性排序

img

写入数据时:因为存储Object包括修改属性,都会修改硬盘文件,必须进行写入事务。


第六章:通知和响应式应用

更改的通知

Realm 的核心特征之一就是数据永不过时的理念。

通知三个级别:

  1. Object
  2. Collection:list、results、linking objects
  3. Realm

通知的细节:

  1. 线程:通知回调在和订阅通知相同线程被调用。
  2. Run loop:Realm使用 run loop 发送更改通知。因此你只能在有 run loop的线程订阅通知。
  3. 通知的间隔尺度(granularity):Realm在每次成功写入事务后推送通知给观察者。因为推送使用的是订阅线程的run loop(可能有时候忙于其它事情),可能会在Realm发生了其它更改才送达。这种情况下,Realm会聚集所有更改一起推送通知。
  4. 仅限持久化的 Object
  5. 通知令牌(Notification tokens):手动invalidate()或者在内存中释放(View Controller 被释放)

响应式应用

响应式系统拥有以下几个关键特性:对发生的变化做出反应,使用基于消息的工作流程,具有扩展能力等等。Realm 都提供了完整的支持。

该书对IB的讲解还是比较全面详细,建议大家读读,也算是差缺补漏。以下是我的一些笔记。

  1. xib 固化为 nib,storyboard 固化为 storyboardc。
  2. IB文件冲突,Open source 的方式查看和编辑。
  3. 同一个sb文件中的不同的VC都应该设置一个不同的StoryboardID与之对应
  4. App 启动过程,手动新建 main.swift 即可编辑修改。
  5. IB文件的 Target Membership的作用:当工程的某个Target被编译,只有IB文件中该Target被勾选,才会被序列化为对应的nib或storyboardc文件,并存放在该Target对应的Bundle中。
  6. Custom Class -> Module 标签是针对Swift设计的,代表命名空间。
  7. Document -> Label 给每个控件起一个简短的名字。
  8. IB使用Auto Layout,根据实时反馈机制,发现问题解决问题。
0%