SwiftUI By Example 笔记

原文为 @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")
}
}
}
}