学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

SwiftUI Concepts Tutorials 笔记

Scene

A scene contains the view hierarchy of your app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import SwiftUI


@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
TabView {
ContentView()
.tabItem {
Label("Journal", systemImage: "book")
}
SettingsView()
.tabItem {
Label("Settings", systemImage: "gear")
}
}
}
}
}

In this sample, body returns the primary scene WindowGroup, which describes the view hierarchy of the sample’s main window.

WindowGroup: It provides platform-specific behaviors for your app, such as supporting multiple windows in macOS and iPadOS.

The computed body property can return one or more primary and secondary scenes.

View Layout

Text and symbols

Text

A Text view displays read-only text.

1
2
3
4
5
Text("Hamlet")
.font(.largeTitle)
Text("by William Shakespeare")
.font(.caption)
.italic()

Symbols

Symbols, such as the iconography that SF Symbols provides

You can customize their colors and sizes using standard view modifiers provided in SwiftUI.

Even though you specify a system or custom symbol in an Image, treat SF Symbols more like text.

1
2
3
4
5
6
7
HStack {
Image(systemName: "folder.badge.plus")
Image(systemName: "heart.circle.fill")
Image(systemName: "alarm")
}
.symbolRenderingMode(.multicolor)
.font(.largeTitle)

Labels

To use both text and a symbol to represent a single element in your app, use a Label.

1
2
3
Label("Favorite Books", systemImage: "books.vertical")
.labelStyle(.titleAndIcon)
.font(.largeTitle)

Controls

use the controlSize(_:) modifier to make a control smaller or larger, or you can use the progressViewStyle(_:) modifier to choose a linear or circular appearance for a progress bar.

There are general-purpose controls like Menu and Link, and specialized views like EditButton and ColorPicker. Use these views to provide familiar UI elements rather than creating custom controls that you’ll need to maintain.

Images and shapes

Images

Display photos and other rich graphics in an Image. By default, an Image displays at the asset’s original size. You can add modifiers like resizable(capInsets:resizingMode:) and scaledToFit() or scaledToFill() to scale it to the available space.

1
2
3
Image("Yellow_Daisy")
.resizable()
.scaledToFit()

accessing an image asset from a server, use an AsyncImage to handle the download while keeping your app responsive.
Fitting images into available space

Shapes

SwiftUI provides several common shapes, and modifiers to change their size, color, or other aspects of their appearance.

1
2
3
4
5
6
7
8
9
HStack {
Rectangle()
.foregroundColor(.blue)
Circle()
.foregroundColor(.orange)
RoundedRectangle(cornerRadius: 15, style: .continuous)
.foregroundColor(.green)
}
.aspectRatio(3.0, contentMode: .fit)

The HStack provides some default spacing between each shape and, to give each shape a square space to fill, the aspectRatio(_:contentMode:) modifier **makes the HStack three times as wide **as it is tall.

For an example of the rich possibilities of composing shapes, see Drawing paths and shapes.

Scaling views to complement text

When you need to provide a numeric value that adapts to the environment’s effective font size, use the ScaledMetric property wrapper.

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


// Source file for Section 2
struct KeywordBubble: View {
let keyword: String
let symbol: String
@ScaledMetric(relativeTo: .title) var paddingWidth = 14.5
var body: some View {
Label(keyword, systemImage: symbol)
.font(.title)
.foregroundColor(.white)
.padding(paddingWidth)
.background {
Capsule()
.fill(.purple.opacity(0.75))
}
}
}

using ScaledMetric to scale dimensions in proportion to text, see Applying custom fonts to text.

Layering content

Define an overlay

When you arrange content on the z-axis, you can use a ZStack or an overlay or background modifier, like overlay(alignment:content:) or background(_:in:fillStyle:),respectively. A ZStack sizes each view based on the available space, without consideration for the other views in the stack. To specify that the size of some content depends on the size of other content, define this secondary content inside one of the overlay or background modifiers.

ZStack可以定义stack里每个view的大小

如果一个view的大小依赖于另一个,使用overlay或background

背景修饰符将其内容放在它所修改的视图的后面,而不是前面

hide a View

the data determining whether to hide a view might be a Binding, or an Environment value.

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

struct IfElseTrain: View {
var longerTrain: Bool

var body: some View {
VStack {
HStack {
Image(systemName: "train.side.rear.car")
if longerTrain {
Image(systemName: "train.side.middle.car")
}
Image(systemName: "train.side.front.car")
}
Divider()
}
}
}

Use an opacity modifier when you don’t want other content to shift around as the view appears or disappears.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import SwiftUI
struct OpacityTrain: View {
var longerTrain: Bool

var body: some View {
VStack {
HStack {
Image(systemName: "train.side.rear.car")
Image(systemName: "train.side.middle.car")
.opacity(longerTrain ? 1 : 0)
Image(systemName: "train.side.front.car")
}
Divider()
}
}
}

Organzing and aligning content with stacks

nested stacks

  • firstTextBaseline
    A guide that marks the top-most text baseline in a view.
1
2
3
4
5
6
7
8
9
struct VerticalAlignmentFirstTextBaseline: View {
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 0) {
Color.red.frame(height: 1)
Text("First Text Baseline").font(.title).border(.gray)
Color.red.frame(height: 1)
}
}
}

Add padding around subviews

You can add padding to the outer edges of a view to put some space between that view and any neighboring views, or to the edge of a window or scene.

padding(_:_:) without any parameters puts space around all four edges.

Add a view to create space

This Spacer() between views pushes the content views as far apart as possible.

State and data flow

State and binding

Indicate data dependencies in a view using state, and share those dependencies with other views using bindings.

Separate properties and imperative code from the view

当视图需要管理多个状态数据时,在单独的特定于视图的结构中(a separate view-specific structure)管理这些数据可能会很有帮助。这种方法通过将属性和命令式代码(imperative code)移出视图,使视图的声明性接口代码(declarative interface code)更具可读性。它还有助于使单元测试状态更改更容易实现。

Bind the view to its state data

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
import SwiftUI

struct RecipeEditor: View {
@Binding var config: RecipeEditorConfig

var body: some View {
NavigationStack {
RecipeEditorForm(config: $config)
...
}
}


struct RecipeEditorForm: View {
@Binding var config: RecipeEditorConfig

var body: some View {
Form {
TextField("Recipe name", text: $config.recipe.title)
Section {
TextField(text: $config.recipe.servings, prompt: Text("4-6")) {
Text("Servings")
}

}
Section("Ingredients") {
TextEditor(text: $config.recipe.ingredients)
.modifier(MacSpecificModifiers())
}
Section("Directions") {
TextEditor(text: $config.recipe.directions)
.modifier(MacSpecificModifiers())
}
}
}
}
  1. Binding属性包装器为视图所需的数据提供了双向的读写绑定。
  2. RecipeEditor将绑定变量config传递给RecipeEditorForm。它将变量作为绑定传递,通过在变量名config前加上$符号来表示。由于RecipeEditorFormconfig作为绑定接收,因此表单可以向config读写数据。

Create a state variable in another view

1
2
3
4
5
6
7
8
9
10
11
import SwiftUI


struct ContentListView: View {
@Binding var selection: Recipe.ID?
let selectedSidebarItem: SidebarItem
@EnvironmentObject private var recipeBox: RecipeBox
@State private var recipeEditorConfig = RecipeEditorConfig()

...
}

recipeEditorConfig声明包含State属性包装器的属性,它告诉SwiftUI创建和管理recipeEditorConfig的实例。每次视图状态发生变化,也就是说,recipeEditorConfig包含的数据发生变化时,SwiftUI将重新初始化视图,将recipeEditorConfig实例重新连接到视图,并重新构建在计算属性body中定义的视图,该属性反映了数据的当前状态。有关更多信息,请参见模型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var body: some View {
RecipeListView(selection: $selection, selectedSidebarItem: selectedSidebarItem)
.navigationTitle(selectedSidebarItem.title)
.toolbar {
ToolbarItem {
Button {
recipeEditorConfig.presentAddRecipe(sidebarItem: selectedSidebarItem)
} label: {
Image(systemName: "plus")
}
.sheet(isPresented: $recipeEditorConfig.isPresented,
onDismiss: didDismissEditor) {
RecipeEditor(config: $recipeEditorConfig)
}
}
}
}

修饰符表(ispresent:onDismiss:content:)接收一个由美元符号($)前缀表示的绑定。此绑定允许工作表读取和写入属性。例如,当一个人通过向下滑动来取消工作表时,工作表将设置recipeEditorConfig.ispresent to false。这个改变导致SwiftUI重新初始化并重建视图。

Creating a custom input control that binds to a value

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
import SwiftUI


struct StarRating: View {
@Binding var rating: Int
private let maxRating = 5


var body: some View {
HStack {
ForEach(1..<maxRating + 1, id: \.self) { value in
Image(systemName: "star")
.symbolVariant(value <= rating ? .fill : .none)
.foregroundColor(.accentColor)
.onTapGesture {
if value != rating {
rating = value
} else {
rating = 0
}
}
}
}
}
}
  1. By defining rating as a binding variable, StarRating can read and write the value even though another view is responsible for creating the value.
  2. The id parameter is of type ID, which is Hashable. The ForEach structure uses this parameter to identify the data, that is, the integer values 1 through 5. The parameter value is the identity key path \.self, which specifies an Int instance for each integer. Because Int is hashable, using this key path satisfies the requirements of the ForEach initializer method init(_:id:content:). And because the data is an increasing range of integers that will never have duplicate values, it’s okay to use each integer value as its identifier.

Defining the source of truth using a custom binding

Limit its use to use cases where using a state variable or object isn’t possible.

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
import SwiftUI


struct DetailView: View {
@Binding var recipeId: Recipe.ID?
@EnvironmentObject private var recipeBox: RecipeBox
@State private var showDeleteConfirmation = false

private var recipe: Binding<Recipe> {
Binding {
if let id = recipeId {
return recipeBox.recipe(with: id) ?? Recipe.emptyRecipe()
} else {
return Recipe.emptyRecipe()
}
} set: { updatedRecipe in
recipeBox.update(updatedRecipe)
}
}

var body: some View {
ZStack {
if recipeBox.contains(recipeId) {
RecipeDetailView(recipe: recipe)
.navigationTitle(recipe.wrappedValue.title)
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
RecipeDetailToolbar(
recipe: recipe,
showDeleteConfirmation: $showDeleteConfirmation,
deleteRecipe: deleteRecipe)
}
} else {
RecipeNotSelectedView()
}
}
}
...
}
  1. The computed recipe property doesn’t return a Recipe. Instead, it returns a custom Binding of type Recipe. This allows the view to share the recipe as a source of truth with other views.

  2. A Binding provides read and write access to a value.uses the init(get:set:) initializer method to create a binding.

  3. Because the computed property recipe returns a Binding, it isn’t necessary to include the dollar sign ($) prefix that’s required when passing a state variable as a binding.

    RecipeDetailView(recipe: recipe)

  4. A wrappedValue is the underlying value referenced by the binding. recipe.wrappedValue.title gets the wrappedValue of the recipe binding

    The navigationTitle(_:) modifier accepts a string value not a binding to a string value

Reference

SwiftUI Concepts Tutorials