这是用户在 2025-1-7 18:21 为 https://developer.apple.com/tutorials/develop-in-swift/update-the-ui-with-state 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
Skip Navigation
  按钮和状态


用状态更新用户界面


探索如何通过创建一个掷虚拟骰子的应用程序来使用 @State 属性和按钮更新应用程序的用户界面。添加功能以增加或减少屏幕上骰子的数量,以便玩不同类型的游戏。

  第一部分

  创建自定义视图


自定义属性视图中,您学习了如何提取界面中重复的元素到一个自定义视图中。要制作一个包含多个骰子的应用程序,请创建一个自定义视图来表示单个骰子,然后在您的应用程序中使用它来制作更多的骰子。

  步骤 1


在 Xcode 中创建一个名为 DiceRoller 的 iOS 应用项目。

A screenshot showing the Welcome to Xcode window. Below the icon and welcome message are three buttons: Create New Project, Clone Git Repository, and Open Existing Project, with the first button highlighted.

  步骤 2


创建一个名为 DiceView 的 SwiftUI 视图文件。

A screenshot of the file save dialog, with the text, DiceView, in the Save As text field.

  步骤 3


将主体代码替换为骰子的图像。

DiceView.swift
struct DiceView: View {
    var body: some View {
        Image(systemName: "die.face.1")
    }
}

  步骤 4


向视图添加一个属性,以表示骰子上的点数。


您使用赋值运算符=为属性赋予默认值1

DiceView.swift


struct DiceView: View {
    var numberOfPips: Int = 1
    
    var body: some View {
        Image(systemName: "die.face.1")

  步骤 5


该属性被标记为关键字 var,这意味着您可以为其分配新值。此视图是动态的;当人们掷骰子时,您将更改该属性的值。


当您为结构的属性分配默认值时,在初始化器中并不是必需的。这就是为什么您不必在预览中更改DiceView实例以包含numberOfPips

A labeled diagram of the code, var numberOfPips colon Int equals 1. The text, var, is labeled Non-constant. The text, numberOfPips, is labeled Name. The text, Int, is labeled Type. The text, 1, is labeled Default value.

  步骤 6


使用字符串插值来显示骰子图像,使用您新属性的值。

DiceView.swift
    
    var body: some View {
        Image(systemName: "die.face.\(numberOfPips)")
    }
}

  步骤 7


使用修饰符来增加图像的大小。.resizable 修饰符告诉图像它可以拉伸以填充任何可用空间。您不希望骰子填充所有可用空间,因此通过设置其框架大小来限制图像。


您通常使用 .font 修饰符将 SF Symbols 的大小与其周围内容匹配。在这种情况下,您将图像作为纯图形内容使用,因此使用 .resizable.frame 是可以的。

DiceView.swift
    var body: some View {
        Image(systemName: "die.face.\(numberOfPips)")
            .resizable()
            .frame(width: 100, height: 100)
    }
}
DiceView.swift
//
//  DiceView.swift
//  DiceRoller
//
//
//


import SwiftUI


struct DiceView: View {
    var body: some View {
        Image(systemName: "die.face.1")
    }
}


#Preview {
    DiceView()
}
A screenshot showing the preview with a single small dice image in the center.
  第二节


添加一个掷骰子的按钮


使用一个 按钮 来更改骰子的图像。按钮使用一个 闭包 在人们点击时运行代码。

  步骤 1


将图像嵌入到一个 VStack 中,以便您可以在其下方添加按钮。

DiceView.swift
    
    var body: some View {
        VStack {
            Image(systemName: "die.face.\(numberOfPips)")
                .resizable()

  步骤 2


VStack内部,在Image及其修饰符下,开始输入Button。代码补全会为您提供几个建议;选择带有titleKeyaction的选项。


如果你看到 title 而不是 titleKey,请选择那个初始化器。

A screenshot showing a list of code completion suggestions for Button initializers. The second initializer, Button, title, action, is selected.

  步骤 3


将第一个占位符替换为字符串 "Roll"

DiceView.swift
                .resizable()
                .frame(width: 100, height: 100)
            
            Button("Roll", action: () -> Void)
        }
    }
No Preview

  步骤 4


第二个参数,action: () -> Void,需要一个闭包,当人们点击按钮时执行代码。按 Tab 选择占位符,然后按 Return。


Xcode 完全移除了 action 参数,并用一对大括号中的闭包替代。大括号内有一个 code 占位符,您将在其中编写按钮的代码。

DiceView.swift
                .frame(width: 100, height: 100)
            
            Button("Roll") {
                code
            }
        }
    }
No Preview

  步骤 5


将最后一个占位符替换为代码,以选择骰子的随机点数。


Int.random 从括号内的范围中选择一个随机整数;代码 1...6 创建一个从 1 到 6 的整数范围。

DiceView.swift
            
            Button("Roll") {
                numberOfPips = Int.random(in: 1...6)
            }
        }
No Preview


您的代码中有一个错误,您将在下一部分通过将 numberOfPips 更改为 @State 属性来修复它。

A screenshot showing a list of code completion suggestions for Button initializers. The second initializer, Button, title, action, is selected.
  第 3 节


使用状态更新视图


所有应用都有数据,或称为状态,这些数据会随着时间而变化。当应用的状态发生变化时,它可能需要更新其界面。然而,SwiftUI 默认并不会监控应用中的每个属性。


在这个应用中,当一个人点击滚动按钮时,你需要更新图像。为了告诉 SwiftUI 监视 numberOfPips 并在其变化时更新 UI,请使用关键字 @State 标记该属性。

  步骤 1


numberOfPips设置为@State属性。然后点击“Roll”按钮几次,检查图像是否变化。


视图状态由视图拥有。您始终将状态属性标记为 private,以便其他视图无法干扰它们的值。

DiceView.swift


struct DiceView: View {
    @State private var numberOfPips: Int = 1
    
    var body: some View {

  步骤 2


为按钮添加边框,以使其与图像区分开。

DiceView.swift
                numberOfPips = Int.random(in: 1...6)
            }
            .buttonStyle(.bordered)
        }
    }

  步骤 3


要使旧骰子图像过渡到新图像时淡出,请使用withAnimation来动画化变化。


添加 withAnimation 指示 SwiftUI 动画任何在其代码中发生的状态变化。它使用尾随闭包,类似于 Button 的工作方式。

DiceView.swift
            
            Button("Roll") {
                withAnimation {
                    numberOfPips = Int.random(in: 1...6)
                }
DiceView.swift
//
//  DiceView.swift
//  DiceRoller
//
//
//


import SwiftUI


struct DiceView: View {
    @State private var numberOfPips: Int = 1
    
    var body: some View {
        VStack {
            Image(systemName: "die.face.\(numberOfPips)")
                .resizable()
                .frame(width: 100, height: 100)
            
            Button("Roll") {
                numberOfPips = Int.random(in: 1...6)
            }
            .buttonStyle(.bordered)
        }
    }
}


#Preview {
    DiceView()
}
A screenshot showing the preview with a single large dice image in the center, with a bordered button labeled Roll below it.
  第 4 节


创建一个动态骰子显示


通过创建另一个属性来添加选择骰子数量的功能。用@State标记该属性可以确保在骰子数量变化时界面更新。

  步骤 1


ContentView中,用一个标题替换VStack的内容。


您可以像在视图中使用修饰符一样,在Font类型上使用修饰符。

ContentView.swift
    var body: some View {
        VStack {
            Text("Dice Roller")
                .font(.largeTitle.lowercaseSmallCaps())
        }
        .padding()

  步骤 2


添加一个HStack,其中包含三个DiceView实例。尝试掷每个骰子。

ContentView.swift
            Text("Dice Roller")
                .font(.largeTitle.lowercaseSmallCaps())
            
            HStack {
                DiceView()
                DiceView()
                DiceView()
            }
        }
        .padding()

  步骤 3


要能够显示任意数量的骰子,请使用 ForEach 视图。通过使用从 1 到 3 的范围重复 DiceView 三次。手动输入代码;ForEach 为许多不同的用途提供代码补全选项。


ForEach 视图是动态的;它根据输入计算其子视图,这些输入可能会随着应用程序的状态而变化。您可以使用 1...3 创建一个范围,就像您在 Int.random(in: 1...6) 中所做的那样。ForEach 视图为范围内的每个值创建一个 DiceView

ContentView.swift
            
            HStack {
                ForEach(1...3, id: \.description) { _ in
                    DiceView()
                }
            }
        }

  步骤 4


1...3 范围是静态的。为了使其适应任意数量的骰子,您将再次使用视图状态。添加一个状态属性以表示骰子的数量。

ContentView.swift


struct ContentView: View {
    @State private var numberOfDice: Int = 1
    
    var body: some View {
        VStack {

  步骤 5


使用新属性使范围动态。

ContentView.swift
            
            HStack {
                ForEach(1...numberOfDice, id: \.description) { _ in
                    DiceView()
                }

  步骤 6


ForEach视图和HStack下方,添加两个按钮以增加和减少骰子的数量。


查看每个按钮闭包内的代码。在 Swift 中,您可以使用 +=–= 来增加或减少属性的当前值。在这种情况下,您是在增加或减少 1。

ContentView.swift
                }
            }
            
            HStack {
                Button("Remove Dice") {
                    numberOfDice -= 1
                }
                
                Button("Add Dice") {
                    numberOfDice += 1
                }
            }
            .padding()
        }
        .padding()

  步骤 7


将骰子的数量减少到一个,然后点击移除骰子按钮。预览崩溃,因为范围 1...0 是无效的。

A screenshot showing the preview, which has a large red X icon and the message, Preview Crashed.

  步骤 8


为了防止人们在只有一个 DiceView 时点击“移除骰子”按钮,您可以 禁用 该按钮以防止崩溃,并给人们一个按钮无响应的视觉提示。当 numberOfDice 的值为 1 时,使用 .disabled 修饰符来禁用“移除骰子”按钮。


您使用 == 运算符来检查两个数字是否相等。此比较的结果是一个 Bool,它有两个值之一:truefalse。当 numberOfDice == 1true 时,修饰符会禁用按钮。

ContentView.swift
                    numberOfDice -= 1
                }
                .disabled(numberOfDice == 1)
                
                Button("Add Dice") {

  步骤 9


骰子的图像固定为 100x100 点,因此屏幕上只能显示三个骰子图像。使用 .disabled 修饰符与添加骰子按钮一起,防止人们拥有超过三个骰子。

ContentView.swift
                    numberOfDice += 1
                }
                .disabled(numberOfDice == 3)
            }
            .padding()

  步骤 10


使用withAnimation动画化骰子数量的变化。

ContentView.swift
            HStack {
                Button("Remove Dice") {
                    withAnimation {
                        numberOfDice -= 1
                    }
ContentView.swift
//
//  ContentView.swift
//  DiceRoller
//
//
//


import SwiftUI


struct ContentView: View {
    var body: some View {
        VStack {
            Text("Dice Roller")
                .font(.largeTitle.lowercaseSmallCaps())
        }
        .padding()
    }
}


#Preview {
    ContentView()
}
A screenshot showing the text, Dice Roller, centered horizontally and vertically.
  第 5 节


调整界面以支持更多骰子


使用灵活的宽度和高度,使骰子图像能够根据屏幕上的骰子数量动态调整大小。

  步骤 1


将骰子限制增加到五个,然后点击添加骰子,直到有五个骰子。


一个HStack视图总是会给其子视图提供所请求的大小,即使它需要超出屏幕的边界。

ContentView.swift
                    }
                }
                .disabled(numberOfDice == 5)
            }
            .padding()

  步骤 2


ContentView预览固定,然后切换到DiceView。使用灵活的框架以允许骰子图像缩小,并点击添加骰子,直到再次有五个骰子。

DiceView.swift
            Image(systemName: "die.face.\(numberOfPips)")
                .resizable()
                .frame(maxWidth: 100, maxHeight: 100)
            
            Button("Roll") {

  步骤 3


现在骰子图像具有灵活的宽度和高度,HStack 可以将它们缩小以适应屏幕。但是当有四个或五个骰子时,由于HStack 上下没有任何限制其高度的内容,它们会在垂直方向上被拉伸。为防止这种情况,将骰子图像的纵横比设置为 1,然后点击添加骰子,直到再次有五个骰子。


1:1 或正方形的宽高比具有相等的宽度和高度。.fit 内容模式意味着如果图像的宽高比与可用空间不同,它将缩小到较小的轴,并在另一侧留出空白。

DiceView.swift
                .resizable()
                .frame(maxWidth: 100, maxHeight: 100)
                .aspectRatio(1, contentMode: .fit)
            
            Button("Roll") {
ContentView.swift
//
//  ContentView.swift
//  DiceRoller
//
//
//


import SwiftUI


struct ContentView: View {
    @State private var numberOfDice: Int = 1
    
    var body: some View {
        VStack {
            Text("Dice Roller")
                .font(.largeTitle.lowercaseSmallCaps())
            
            HStack {
                ForEach(1...numberOfDice, id: \.description) { _ in
                    DiceView()
                }
            }
            
            HStack {
                Button("Remove Dice") {
                    withAnimation {
                        numberOfDice -= 1
                    }
                }
                .disabled(numberOfDice == 1)
                
                Button("Add Dice") {
                    withAnimation {
                        numberOfDice += 1
                    }
                }
                .disabled(numberOfDice == 5)
            }
            .padding()
        }
        .padding()
    }
}


#Preview {
    ContentView()
}
A screenshot showing a vertical stack. At the top is the text, Dice Roller. Below is a horizontal stack of five DiceViews. The leftmost and rightmost views extend beyond the edges of the screen. Below the dice is a horizontal stack of two buttons: Remove Dice and Add Dice. The Add Dice button is disabled.
  第六节


在按钮标签中使用图像


自定义添加和移除骰子按钮以显示图像而不是文本。

  步骤 1


ContentView中,为添加骰子按钮添加另一个参数以添加图像。


按钮显示的视图称为其标签。在许多情况下,您会使用图像和文本的组合作为按钮标签。

ContentView.swift
                .disabled(numberOfDice == 1)
                
                Button("Add Dice", systemImage: "plus.circle.fill") {
                    withAnimation {
                        numberOfDice += 1

  步骤 2


对于这个按钮,一张图片足以告诉人们它的功能。但按钮应该始终有一个文本标签——无论是否可见——以便那些依赖于 VoiceOver 等功能的人可以使用。使用 .labelStyle 修饰符隐藏按钮文本。


尽管修饰符影响两个按钮,但“移除骰子”按钮仍然有文本标签,因为它没有图标可显示。

ContentView.swift
            }
            .padding()
            .labelStyle(.iconOnly)
        }
        .padding()

  步骤 3


使用标题字体增大按钮大小。

ContentView.swift
            .padding()
            .labelStyle(.iconOnly)
            .font(.title)
        }
        .padding()

  步骤 4


在“移除骰子”按钮上添加一张图片。

ContentView.swift
            
            HStack {
                Button("Remove Dice", systemImage: "minus.circle.fill") {
                    withAnimation {
                        numberOfDice -= 1
ContentView.swift
//
//  ContentView.swift
//  DiceRoller
//
//
//


import SwiftUI


struct ContentView: View {
    @State private var numberOfDice: Int = 1
    
    var body: some View {
        VStack {
            Text("Dice Roller")
                .font(.largeTitle.lowercaseSmallCaps())
            
            HStack {
                ForEach(1...numberOfDice, id: \.description) { _ in
                    DiceView()
                }
            }
            
            HStack {
                Button("Remove Dice") {
                    withAnimation {
                        numberOfDice -= 1
                    }
                }
                .disabled(numberOfDice == 1)
                
                Button("Add Dice", systemImage: "plus.circle.fill") {
                    withAnimation {
                        numberOfDice += 1
                    }
                }
                .disabled(numberOfDice == 5)
            }
            .padding()
        }
        .padding()
    }
}


#Preview {
    ContentView()
}