大师学SwiftUI第6章 - 声明式用户界面 Part 2

发布时间:2024年01月05日

控制视图

控件是交互工具,用户通过交互修改界面状态、选取选项或插入、修改或删除信息。我们实现过其中的一部分,如前例中的??Button???视图以及??TextField??视图。要定义一个有用的接口,需要学习有关视图的更多知识以及其它由SwiftUI所提供的控制视图。

按钮视图

我们已经学到,??Button??视图创建一个简单的控件,在点击时执行操作。以下是该结构体部分初始化方法。

  • Button(String, action: Closure):此初始化方法创建一个??Button???视图。第一个参数是定义按钮标签的字符串,??action??参数是在点击按钮时执行的代码的闭包。
  • Button(action: Closure, label: Closure):此初始化方法创建一个??Button???视图。??action???参数是在点击按钮时执行的代码的闭包,??label??参数是返回用于创建标签的视图的闭包。
  • Button(String, role: ButtonRole?, action: Closure):此初始化方法创建一个??Button???视图。第一个参数是定义按钮标签的字符串。??role???参数一个结构体,包含描述按钮目的的类型属性。有两个属性:??cancel???和??destructive???。??action??参数是在点击按钮时执行的代码的闭包。

我们已经实现过第二个初始化方法创建按钮,但如果仅需对标签使用字符中,可以简化代码使用第一个初始方法加后置的用于操作的闭包。

示例6-10:实现??Button??视图

struct ContentView: View {
    @State private var colorActive: Bool = false
    
    var body: some View {
        VStack(spacing: 10) {
            Text("Default Title")
                .padding()
                .background(colorActive ? Color.green : Color.clear)
            Button("Change Color") {
                colorActive.toggle()
            }
            Spacer()
        }.padding()
    }
}

上例在??VStack???中包仿一个??Text???视图和一个??Button???视图。??Text???视图展示固定的文本,背景色用??colorActive???属性定义。如果属性值是??true???,我们将??green???色赋值给背景,否则颜色为??clear???(透明)。在按下按钮时,会切换这一属性的值,再次运算??body??属性的值,文本的背景修改为下一个颜色。

图6-4:按钮视图

图6-4:按钮视图

??跟我一起做:创建一个多平台项目。使用示例6-10的代码更新??ContentView??视图。点击Change Color按钮。会看到文本背景色的变化(参见图6-4右图)。

如果希望将视图与控件执行的操作进行分离,可以将相关语句移到函数中。例如,可以上在??ContentView???结构体中添加一个函数,用于切换??colorActive??属性的值,然后在按钮的操作中调用这个函数。应用的功能的相同,但代码更有条理。

示例6-11:使用函数来组织代码

struct ContentView: View {
    @State private var colorActive: Bool = false
    
    var body: some View {
        VStack(spacing: 10) {
            Text("Default Title")
                .padding()
                .background(colorActive ? Color.green : Color.clear)
            Button("Change Color") {
                changeColor()
            }
            Spacer()
        }.padding()
    }
    
    func changeColor() {
        colorActive.toggle()
    }
}

如果按钮唯一的操作就是调用方法,可以简化视图的定义为声明??action??参数并指定所要执行操作的方法名。如下所示。

示例6-12:引用方法

Button("Change Color", action: changeColor)

声明方法名称带括号会马上执行方法,但仅声明名称会提供一个方法的引用供系统稍后执行。

??跟我一起做:使用示例6-11中的代码更新??ContentView??视图。应用功能和之前相同。使用示例6-12中的??Button???视图更新??Button??视图。点击按钮确定所执行的操作。

上例中,我们使用了三元运算符来根据??colorActive???属性的值选取??background()???修饰符的值。这是推荐的做法,这样SwiftUI可以识别视图并有效管理状态的转换,但我们也可以使用??if else??语句来响应修改。例如,有时会用按钮这类控件在界面中显示或隐藏视图。

示例6-13:在界面中添加及删除视图

struct ContentView: View {
    @State private var showInfo = false
    
    var body: some View {
        VStack(spacing: 10) {
            Button("Show Information") {
                showInfo.toggle()
            }.padding()
            if showInfo {
                Text("This is the information")
            }
            Spacer()
        }
    }
}

本例中的按钮切换??@State???属性??showInfo???的值。在按钮下方,可查看到该属性的当前值。若其值为??true???,显示 ??Text???视图,否则什么也不显示。因此,在按下按钮时,??showInfo???的值发生改变,??body???属性的内容会重新绘制,??Text???视图根据??showInfo??的当前值出现或消失。

图6-5:动态界面

图6-5:动态界面

??if else??语句可用于选择是否执行按钮的操作,但SwiftUI提供了如下修饰符来在要做禁用操作时禁用按钮。

  • disabled(Bool):这一修饰符决定该控件是否响应用户的交互。

下例使用了该修饰符在点击后禁用按钮,因此用户只能执行一次操作。

示例6-14:禁用按钮

struct ContentView: View {
    @State private var color = Color.clear
    @State private var buttonDisabled = false
    
    var body: some View {
        VStack(spacing: 10) {
            Text("Default Title")
                .padding()
                .background(color)
            Button("Change Color") {
                color = Color.green
                buttonDisabled = true
            }
            .disabled(buttonDisabled)
            Spacer()
        }.padding()
    }
}

这个视图包含两个??@State???属性,一个用于追踪颜色,另一个表示按钮是否处于禁用状态。在点击按钮时,操作中将??true???赋值给??buttonDisabled??属性,按钮就停止运作了,这样用户只能点一次。

图6-6:按钮禁用

图6-6:按钮禁用

和之前一样,??Button???视图的初始化方法可以包含一个??label???参数来定义所需视图的标签。这个参数非常灵活,可以包含像??Text???视图和??Image???视图的视图。按钮中的图片以原始渲染模式显示 ,也就是说以原始颜色显示,但还有一种模式可以创建带图片的蒙版,以应用的着重色或赋值给控件的前景色显示。为选取渲染模式,??Image??视图包含如下修饰符。

  • renderingMode(TemplateRenderingMode):这个修饰符对??Image???视图定义了渲染械。参数是包含??original???和??template??值的枚举。

下例定义了一个带图片和文本的按钮。将??renderingMode()???修饰符应用于??Image??视图来以模板显示图片。

示例6-15:定义带图按钮的标签

struct ContentView: View {
    @State private var expanded: Bool = false
    
    var body: some View {
        VStack(spacing: 10) {
            Text("Default Title")
                .frame(minWidth: 0, maxWidth: expanded ? .infinity : 150, maxHeight: 50)
                .background(Color.yellow)
            Button(action: {
                expanded.toggle()
            }, label: {
                VStack {
                    Image(expanded ? .contract : .expand)
                        .renderingMode(.template)
                    Text(expanded ? "Contract" : "Expand")
                }
            })
            Spacer()
        }.padding()
    }
}

示例6-15中的视图包含一个??@State???属性??expanded???,用于控制??Text???视图的宽度。如该属性的值为??true???,我们使用??infinity???值让宽度为最宽,否则,宽度为150点。每当用户点击按钮时,??expanded???属性的值通过??toggle()???方法进行切换,??Text??视图的宽度随之发生变化。

图6-7:带模板图片的按钮

图6-7:带模板图片的按钮

??跟我一起做:下载expand.png和contract.png并添加至资源目录。使用示例6-15中的代码更新??ContentView??视图,点击Expand按钮。此时会看到图6-7中的界面。删除??renderingMode()??修饰符。我们应当会看到原色图。

可以通过如下修饰符对按钮赋标准样式。

  • buttonStyle(ButtonStyle):此修饰符定义了按钮的样式。参数是遵循??ButtonStyle??协议的一个结构体。
  • controlSize(ControlSize):此修饰符定义了按钮的样式。参数是一个枚举,值有??large???、??mini???、??regular???和??small??。

SwiftUI框架自带有??PrimitiveButtonStyle???协议提供标准样式。为此,该协议定义了类型属性??automatic???、??bordered???、??borderedProminent???、??borderless???和??plain???。这些样式满足不同目的。例如,??bordered???样式创建一个灰色背景的按钮,表示二级操作,??borderedProminent??样式创建一个应用着重色的按钮,表示主操作,比如用于保存或提交数据。例如以下视图包含两个按钮,一个取消处理,另一个将信息发送给服务端。

示例6-16:按钮样式

struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            HStack {
                Button("Cancel") {
                    print("Cancel Action")
                }.buttonStyle(.bordered)
                Spacer()
                Button("Send") {
                    print("Send Information")
                }.buttonStyle(.borderedProminent)
            }
            Spacer()
        }.padding()
    }
}

突出按钮应仅用于表示主操作。本例中,Cancel按钮加了边框,告诉用户这是一个二级操作,重要级为次级,但Send按钮为突出的,表示在点击该按钮时执行重要操作。

图6-8:标准样式按钮

图6-8:标准样式按钮

按钮用于取消处理(如上例)或删除某一项时,我们可以通过??Button???的初始化方法为其赋一个特定的角色。这样系统可以根据角色在应用运行的设备上对按钮添加样式。例如,在移动设备上,??destructive??角色的按钮以红色显示。

示例6-17:赋予角色

Button("Delete", role: .destructive) {
                    print("Delete Action")
                }.buttonStyle(.bordered)

图6-9:销毁按钮

图6-9:销毁按钮

??跟我一起做:使用示例6-16的代码更新??ContentView??视图。会看到如图6-8中所示的按钮。将Cancel按钮替换为示例6-17中的??Button??视图。会看到如图6-9中所示的删除按钮。

这些样式对SF图标进行了美化。SF图标替换普通图片的优势是它们会按对按钮添加的字体大小进行缩放。这配合对按钮自身进行缩放的??controlSize()??修饰符,使得我们可以创建不同大小的按钮。

示例6-18:缩放按钮

struct ContentView: View {
    var body: some View {
        VStack(spacing: 10) {
            Button(action: {
                print("Send information")
            }, label: {
                HStack {
                    Image(systemName: "mail")
                        .imageScale(.large)
                    Text("Send")
                }
            })
            .buttonStyle(.borderedProminent)
            .font(.largeTitle)
            .controlSize(.large)
            Spacer()
        }.padding()
    }
}

本例中,我们应用了??imageScale()???修饰符来缩放SF图标,??font()???修饰符对按钮添加了大字体,??controlSize()??修饰符对按钮进行缩放。结果如下。

图6-10:自定义大小的按钮

图6-10:自定义大小的按钮

如果希望定义一个样式与系统自带的进行区分,则需要创建自己的??ButtonStyle??结构体。该协议只要求实现如下方法。

  • makeBody(configuration: Configuration):该方法定义并返回一个替换按钮体的视图。??configuration???参数为包含按钮信息的??Configuration??类型的值。

该方法接收一个??Configuration???类型的值,是??ButtonStyleConfiguration??的类型别名,包含返回按钮相关信息的属性。以下是其中的属性。

  • isPressed:该属性返回表示按钮是否按下的布尔值。
  • label:该属性返回定义按钮当前标签的一个或多个视图。

以下示例定义在点击时会放大的示例。样式包含一个内边距和绿色边框。要应用这些样式,必须创建一个符合??ButtonStyle???协议的结构体,即实现??makeBody()??方法并通过该方法返回希望赋值给按钮体的视图。

组成按钮体的视图由??Configuration???结构体的??label??属性提供,因此我们可以读取并修改这一属性值来应用新的样式,如下所示。

示例6-19:为按钮添加自定义样式

struct MyStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        let pressed = configuration.isPressed
        return configuration.label
            .padding()
            .border(Color.green, width: 5)
            .scaleEffect(pressed ? 1.2 : 1.0)
    }
}

struct ContentView: View {
    @State private var color = Color.gray
    
    var body: some View {
        VStack(spacing: 10) {
            Text("Default title")
                .padding().foregroundColor(color)
            Button("Change Color") {
                color = Color.green
            }.buttonStyle(MyStyle())
            Spacer()
        }.padding()
    }
}

示例6-19中,我们定义了一个结构体??MyStyle???并实现了所要求的??makeBody()???方法。该方法通过类型属性获取到了按钮的当前配置,进而修改并返回标签。首先,我们读取??isPressed???属性的值来了解按钮是否被按下,然后对??label???属性应用新的样式。这个属性返回创建按钮当前标签的一个视图拷贝,然后通过修改其值我们也就修改了标签。本例中,我们应用了一个内边距、一个边框,然后根据??isPressed???属性的值赋了一个缩放比例。如果该值为??true???,也就是按钮被按下了,我们将比例设为1.2进行放大,但在值为??false??时,比例又回到了1。

在这一视图中,我们创建了该结构体的实例并通过??buttonStyle()???修饰符将其值赋值给??Button??视图。如果如下所示。

图6-11:带自定义样式的按钮

图6-11:带自定义样式的按钮

??跟我一起做:使用示例6-19中的代码更新??ContentView.swift??文件。点击按钮。会看到按钮如图6-11右图那样放大了。SwiftUI自动对按钮添加了动画。我们会在第11章中学习自定义动画以及如何创建。

文本框视图

??TextField??又是一个我们之前介绍过的控件。该视图创建一个输入框,用户可进行交互并插入值(单行文本)。以下是结构体中所包含的一个初始化方法。

  • TextField(String, text: Binding, axis: Axis):此初始化方法创建一个输入框。第一个参数定义该字段的占位符,??text???参数是用于存储由用户所插入值的绑定属性,??axis???参数定义在文本超出视图边界时沿哪条轴进行滚动。这是一个枚举,值有??horizontal???和??vertical??。

框架为??TextField??视图定义了几个修饰符。以下是最常用的一些。

  • textFieldStyle(TextFieldStyle):此修饰符定义文本框的样式。参数是一个符合??TextFieldStyle???协议的结构体。框架自带了几个提供标准样式的结构体。这些结构体定义了类型属性??automatic???、??plain???、??roundedBorder???和??squareBorder??。
  • autocorrectionDisabled(Bool):此修饰符启用或禁用系统的自动修正特性。默认,该值为??true??(禁用状态)。
  • textInputAutocapitalization(TextInputAutocapitalization?):此修饰符定义用于格式化文本的大写样式。该参数是一个结构体,包含类型属性??characters???、??never???、??, sentences (默认值)???和??words??。
  • keyboardType(UIKeyboardType):此修饰符定义待定输入框后系统打开的键盘类型。其参数是一个枚举,值有??default???、??asciiCapable???、??numbersAndPunctuation???、??URL???、??numberPad???、??phonePad???、??namePhonePad???、??emailAddress???、??decimalPad???、??twitter???、??webSearch???、??asciiCapableNumberPad???和??alphabet??。

我们已经学习如何包含一个简单的??TextField??视图来获取用户的输入,但只使用了少数几个修饰符。下例展示了如何对视图添加样式让单词变成大写。

示例6-20:配置文本框

struct ContentView: View {
    @State private var title: String = "Default Title"
    @State private var titleInput: String = ""
    
    var body: some View {
        VStack(spacing: 15) {
            Text(title)
                .lineLimit(1)
                .padding()
                .background(Color.yellow)
            TextField("Insert Title", text: $titleInput)
                .textFieldStyle(.roundedBorder)
                .textInputAutocapitalization(.words)
            Button("Save") {
                title = titleInput
                titleInput = ""
            }
            Spacer()
        }.padding()
    }
}

示例6-20中对??TextField???视图应用的样式为??roundedBorder??。它为输入框添加一个边框,让视图占据的区域变得可见,如下所示。

图6-12:带圆角边框的文本框

图6-12:带圆角边框的文本框

??跟我一起做:使用示例6-20中的代码更新??ContentView??视图。在输入框中插入文本并按下Save按钮。会看到如图6-12所示的效果。

除了按钮,用户通过期望能够通过点击键盘上的Done按钮保存数据。为此框架提供了如下的修饰符。

  • onSubmit(of: SubmitTriggers, Closure):在发生触发条件(比如在键盘上按下Done/Return按钮时)时该修饰符执行一个操作。??of???参数是指定修饰符所响应的触发条件类型的结构体。结构体中包含??search???和??text??(默认)属性。第二个参数是希望执行的闭包。
  • submitLabel(SubmitLabel):该修饰符指定虚拟键盘中Done按钮所使用的标签。参数结构体包含的类型属性有??continue???、??done???、??go???、??join???、??next???、??return???、??route???、??search???和??send??。
  • submitScope(Bool):该修饰符指定在发生触发条件时是否提交视图。

赋值给??onSubmit()???修饰符的闭包在聚焦于视图(例如用户编辑输入框)时执行。如果应用于??TextField???视图,可省略??of??参数,如下例如下。

示例6-21:响应Done按钮

struct ContentView: View {
    @State private var title: String = "Default Title"
    @State private var titleInput: String = ""
    
    var body: some View {
        VStack(spacing: 15) {
            Text(title)
                .lineLimit(1)
                .padding()
                .background(Color.yellow)
            TextField("Insert Title", text: $titleInput)
                .textFieldStyle(.roundedBorder)
                .submitLabel(.continue)
                .onSubmit {
                    assignTitle()
                }
            HStack {
                Spacer()
                Button("Save") {
                    assignTitle()
                }
            }
            Spacer()
        }.padding()
    }
    func assignTitle() {
        title = titleInput
        titleInput = ""
    }
}

示例6-21中的代码实现了??submitLabel()??修饰符来修改Done按钮的标题为Continue,然后向结构体添加一个名为??assignTitle()???的方法,执行和之前同样的操作。该方法在两处有调用,赋值给??onSubmit()???修饰符的闭包和??Button??视图操作,因此在按下界面的按钮或点击键盘上的Done/Return按钮时执行该操作。不管用户决定执行什么操作,插入文本框的值总是存储于??title??属性中。

??跟我一起做:使用示例6-21中的代码更新??ContentView??结构体,并在iPhone模拟器上运行应用。点击输入框,插入文本并在键盘上点击Continue按钮。(若要在模拟器上启用虚拟键盘,打开I/O菜单,点击Keyword,选择Toggle Software Keyboard选项。)文本会像此前一样赋值给标题。

在视图可接收输入或处理用户选定的反馈时,我们就说视图聚焦了。SwiftUI包含了一些处理这种状态的工具。可以在视图获得焦点时处理某一任务、知道视图是否获得焦点或是从视图移除焦点。为此有两个属性包装器:??@FocusState???和??@FocusedBinding???。??@FocusState???存储表明焦点当前存储在哪里的值,??@FocusedBinding??用于将状态传递给其它视图。为管理状态,框架内置了如下 修饰符。

  • focused(Binding, equals: Hashable):此修饰符将视图当前状态存储于绑定属性中。第一个参数是对??@FocusState???属性的引用,??equals??参数是用于标识视图的可哈希值。
  • focusable(Bool):此标识符表示是否可将焦点放在视图上。

为追踪视图的状态,我们需要一个可哈希数据类型的??@FocusState???属性,提供用于标识视图的值。下例中,属性通过枚举值进行创建。定义了两个值??name???和??surname??,用于追踪两个输入框的聚焦状态,并在用户输入时修改背景色。

示例6-22:响应焦点中的变化

enum FocusName: Hashable {
    case name
    case surname
}

struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme
    @FocusState var focusName: FocusName?
    @State private var title: String = "Default Name"
    @State private var nameInput: String = ""
    @State private var surnameInput: String = ""
    
    var body: some View {
        let color: Color = colorScheme == .dark ? .black : .white
        VStack(spacing: 10) {
            Text(title)
                .lineLimit(1)
                .padding()
                .background(Color.yellow)
            TextField("Insert Name", text: $nameInput)
                .textFieldStyle(.roundedBorder)
                .padding(4)
                .background(focusName == .name ? Color(white: 0.9) : color)
                .focused($focusName, equals: .name)
            TextField("Insert Surname", text: $surnameInput)
                .textFieldStyle(.roundedBorder)
                .padding(4)
                .background(focusName == .surname ? Color(white: 0.9) : color)
                .focused($focusName, equals: .surname)
            HStack {
                Spacer()
                Button("Save") {
                    title = nameInput + " " + surnameInput
                }
            }
            Spacer()
        }.padding()
    }
}

??@FocusState???属性的初始值是??nil???,表示未聚焦于任何视图。在用户点击文本框时,焦点移至该视图,标识视图的值会被赋值给该属性。通过将该值与枚举中的值进行比较,我们就知道是哪个??TextField???视图于聚焦状态,相应地修改背景色。注意??roundedBorder??样式对文本框添加了一个边框和白色背景,所以本例中只有边距的背景可见。

图6-13:聚焦

图6-13:聚焦

在移动设备中,在可处理输入的视图(如??TextField???视图)获取到焦点时会打开虚拟键盘。只要焦点还在该视图上键盘就保持打开状态。也就是说要关闭键盘,我们必须移除该视图的焦点。在SwiftUI中通过对??@FocusState???属性的赋值??nil??来实现,如下所示。

示例6-23:关闭键盘

Button("Save") {
                    title = nameInput + " " + surnameInput
                    focusName = nil
                }

示例6-22中的??Button??视图换成了示例6-23中的??Button??视图。现在,每当点击Save按钮时,会对值进行处理并关闭键盘。

??跟我一起做:使用示例6-22中的代码更新??ContentView.swift??文件并在iPhone模拟器上运行应用。点击输入框。背景会像图6-13那样变成灰色。使用示例6-23中的视图替换原??Button??视图。再次运行应用。在两个文本框中插入值并点击Save按钮。此时标题会被赋上新值,虚拟键盘关闭。

上例中,我们没有检测用户是否插入了值,但通常应用必须防止用户保存无效值或空值。有几种控制方式。一种是在存储之前就检测值。我们允许用户输入任意值,但仅保存应用所接受的值。

示例6-24:在存储前检测值

Button("Save") {
                    let tempName = nameInput.trimmingCharacters(in: .whitespaces)
                    let tempSurname = surnameInput.trimmingCharacters(in: .whitespaces)
                    
                    if !tempName.isEmpty && !tempSurname.isEmpty {
                        title = nameInput + " " + surnameInput
                        focusName = nil
                    }
                }

本例中,我们首先对??nameInput???和??surnameInput??进行修剪去除其首尾的空格(参数第4章字符串一节),然后在将它们赋值给??title??属性之前检测结果值是否为空。Save按钮仍保持为激活状态,但仅在用户对两个字段都插入值时才执行保存。

??跟我一起做:使用示例6-24中的代码更新??ContentView???视图中的??Button??视图。此时必须同时对名和姓两个字段插入值才能修改标题。

另一种方式是在用户插入的为非应用预期值时通过??disabled()??修饰符禁用按钮。

示例6-25:禁用按钮

Button("Save") {
                    let tempName = nameInput.trimmingCharacters(in: .whitespaces)
                    let tempSurname = surnameInput.trimmingCharacters(in: .whitespaces)
                    
                    if !tempName.isEmpty && !tempSurname.isEmpty {
                        title = nameInput + " " + surnameInput
                        focusName = nil
                    }
                }
            }.disabled(nameInput.isEmpty || surnameInput.isEmpty)

本例中,我们使用了前面介绍的??disabled()??修饰符来在用户在两个字段中输入文本前禁用按钮。如果其中一个或两个字段为空,按钮就无法使用。

??跟我一起做:使用示例6-25中的代码更新??Button??视图。只有同时插入名和姓时才能按下Save按钮。

除了可检测属性是否包含有效值,我们还能限定用户在字段中输入的内容。例如,我们可以只接受数字或指定数量的字符。这时,我们需要在每次视图状态发生改变时检测用户插入的值是否有效。框架为此内置了如下的修饰符。

  • onChange(of: State, initial: Bool, Closure):该修饰符在状态发生改变时执行闭包。??of???参数是存储待检测值的属性,??initial??参数为指定在视图出现时是否还执行检测的布尔值,最后一个参数是在系统报出值发生改变时执行的闭包。闭包可接收两个值,一个表示属性的老值,另一个表示新值。

该修饰符只能检测一个状态,因此我们应对所有希望进行控制的视图应用该修饰符。例如,我们可以在示例中对那两个??TextField??视图使用它来限定允许用户输入的字符数。如果超出,会移除掉多余的字符将结果赋回属性,如下所示。

示例6-26:控制用户的输入

Text(title)
                .lineLimit(1)
                .padding()
                .background(Color.yellow)
            TextField("Insert Name", text: $nameInput)
                .textFieldStyle(.roundedBorder)
                .padding(4)
                .background(focusName == .name ? Color(white: 0.9) : color)
                .focused($focusName, equals: .name)
                .onChange(of: nameInput, initial: false) { old, value in
                    if value.count > 10 {
                        nameInput = String(value.prefix(10))
                    }
                }
            TextField("Insert Surname", text: $surnameInput)
                .textFieldStyle(.roundedBorder)
                .padding(4)
                .background(focusName == .surname ? Color(white: 0.9) : color)
                .focused($focusName, equals: .surname)
                .onChange(of: surnameInput, initial: false) { old, value in
                    if value.count > 15 {
                        surnameInput = String(value.prefix(15))
                    }
                }

示例6-26的代码中,我们检测存储文本框状态的属性的变化。在用户输入或删除字符时,相应的属性值发生改变,执行赋值给??onChange()???修饰符的闭包。闭包接收属性的值。使用该值,我们检测用户插入的文本是否有效并进行相应的响应。在示例中,我们计算字符串中的字符数,如果值超出上限,我们使用??prefix()??方法从文本的开头进行截取,并将结果赋回给属性,这会更新视图并删除文本框中多余的字符。结果 是在字符数超出上限时,用户就无法输入更多的字符了。

??跟我一起做:使用示例6-26中的代码更新项目中的??TextField??视图。在iPhone模拟器中运行应用。插入名和姓。在名超过10个字符、姓超过15个字符时都无法再添加更多的字符。

当然,我们可以指定字符数外的其它条件。下例创建了一个仅接收整数数字的小应用。

示例6-27:仅接收整数数字

struct ContentView: View {
    @State private var title: String = "Default Name"
    @State private var numberInput = ""
    
    var body: some View {
        VStack(spacing: 10) {
            Text(title)
                .padding()
                .background(Color.yellow)
            TextField("Insert Number", text: $numberInput)
                .textFieldStyle(.roundedBorder)
                .padding(4)
                .keyboardType(.numbersAndPunctuation)
                .onChange(of: numberInput, initial: false) { old, value in
                    if !value.isEmpty && Int(value) == nil {
                        numberInput = old
                    }
                }
            HStack {
                Spacer()
                Button("Save") {
                    title = numberInput
                    numberInput = ""
                }
            }
            Spacer()
        }.padding()
    }
}

和之前一样,视图中包含一个带有??onChange()???修饰符的??TextField???。不同之处于在于如何对输入有进行有效性检测。本例中,我们需要确保文本框不为空,然后查看是否可以将其转化为整数,这表示用户只输入了数字。如果不能,就将闭包接收到的旧值赋值给??numberInput??属性,文本框回复到之前的状态。

注意我们还实现了??keyboardType()??修饰符来显示适配我们预期用户输入内容(本例为数字)的键盘。

??跟我一起做:使用示例6-27中的代码更新??ContentView.swift??文件。在iPhone模拟器上运行应用。此时只能输入数字。

默认,??TextField???视图只显示一行文本,但我们可以使用??lineLimit()???修饰符来允许视图进行扩展来包含更多的文本。(此前展开??Text??视图实现的同一个修饰符)。除了应用修饰符来设置我们所需的行数,我们还要告诉视图在纵轴上滚动内容,如下所示。

示例6-28:定义多行文本框

struct ContentView: View {
    @State private var text: String = ""
    
    var body: some View {
        TextField("Insert Text", text: $text, axis: .vertical)
            .textFieldStyle(.roundedBorder)
            .padding(20)
            .lineLimit(5)
    }
}

本例中,??TextView???视图会进行扩展,直至到5行的高度时,然后会在垂直方向上滚动来允许用户持续输入。如若要对视图设置最小和最大尺寸,可以使用区间来声明修饰符,如??lineLimit(3...5)??。

图6-14:多行文本框

图6-14:多行文本框

其它相关内容请见??虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记??

文章来源:https://blog.csdn.net/ardor123/article/details/135403658
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。