状况

在上一章,咱们介绍了SwiftUI的首要特性,声明式语法。借助SwiftUI,咱们能够按期望在屏幕上显现的办法声明视图,余下交由体系来创立所需的代码。但声明式语法不只用于安排视图,还可在运用状况产生改动时更新视图。例如,咱们能够有下面图6-1中的界面,显现标题的Text视图,用户输入新标题的输入字段以及将旧标题替换成新标题的按钮。原标题的Text视图表明咱们界面的初始状况。用户在输入框中输入每个字符时状况都会产生更新(图6-1左图),点击按钮时,界面进入一个新状况,用户刺进的标题会替换原标题,文本的色彩也产生改动(图6-1右图)。

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

图6-1:用户界面

每次状况产生改动时,有必要更新视图来进行反应。在之前的体系中,这要求代码坚持数据及界面同步,但在声明式语法中咱们只需求声明每个状况的视图装备,体系会负责生成在屏幕上显现这些改动所需的代码。

界面或许经历的状况由存储在运用中的信息决定。例如,用户在输入结构中刺进的字符以及示例中运用的色彩都是存储在运用中的值。每逢这些值产生改动时,运用都会进入新状况,因而界面会产生更新进行反应。树立运用数据与界面之间的依赖需求许多的代码,但SwiftUI经过特点包装器让其坚持简略。

@State

在第3章中讨论过,特点包装器让咱们能够界说用赋给它们的值界说可执行任务的特点。SwiftUI完结了许多的特点包装器来存储值并向视图上报修正。设计用于存储单个视图状况的名为@State。这个特点包装器将值存储在类型为State的结构体中,并在值产生改动时告知体系,这样视图会主动更新来在屏幕中进行反映。

特点包装器@State是设计用于存储单个视图的状况的。因而,咱们应将这个类型的特点声明为视图结构体的一部分,并运用private,这样拜访就能够限定在所声明的结构体内了。

示例6-1:界说一个状况

struct ContentView: View {
    @State private var title: String = "Default Title"
    var body: some View {
        VStack {
            Text(title)
                .padding(10)
            Button(action: {
                title = "My New Title"
            }, label: {
                Text("Change Title")
            })
            Spacer()
        }.padding()
    }
}

示例6-1中的代码声明晰一个String类型名为title@State特点。该特点运用"Default Title"值进行初始化。在视图内容中,咱们在笔直堆叠中以Text视图显现这个值,并在其下放了一个Button视图来修正其值。稍后咱们会学习Button视图,但现在读者只需求知道Button视图显现一个标签并在用户点击按钮时操作一个操作。为展现标签,咱们运用带"Change Title"文本的Text视图来让用户知道按钮的作用,并界说好操作,咱们供给一个闭包修正title特点的值为"My New Title",这样在点击按钮时标题就会产生修正。

运用@State包装器创立的title特点在两个地方用到了,第一个是向用户显现当时值的Text视图,第二是Button视图中修正其值的操作。因而,每交点击按钮时,title特点的值会产生改动化,@State特点包装器告知体系运用的状况产生的改动,body特点的内容主动改写在屏幕上显现新值。

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

图6-2:初始状况(左)和点击按钮后的状况(右)

✍️跟我一同做:创立一个多平台项目。运用示例6-1中的代码更新ContentView视图。确保对画布启用了实时预览(图5-18,1号图)。点击Change Title按钮将字符串赋值给Text视图。会看到像图6-2右图中的作用。

整个进程是主动完结的。咱们不用对Text视图赋新值或是告知该视图新的值,这一切都由@State特点包装器处理。咱们能够包括多个存储界面状况的@State特点。例如,下例中咱们对视图增加了一个Bool类型的@State特点,在每次点击按钮时为title特点赋不同的文本。

示例6-2:界说多个状况

struct ContentView: View {
    @State private var title: String = "Default Title"
    @State private var isValid: Bool = true
    var body: some View {
        VStack {
            Text(title)
                .padding(10)
            Button(action: {
                isValid.toggle()
                title = isValid ? "Valid" : "Invalid"
            }, label: {
                Text("Change Validation")
            })
            Spacer()
        }.padding()
    }
}

isValid特点存储表明当时校验状况的布尔值,这样能够在屏幕上显现相应的文本。赋值给title特点的字符串经过三元表达式来进行选取。运用三元运算符来设置视图是一种引荐的实践,由于它让体系能够获取视图能呼应的所有或许的状况,并在状况间产生平滑的过渡。如isValid的值为true,将单词“Valid”赋值给title特点,不然赋值”Invalid”。每次点击按钮时,isValid特点的值都会发改动,屏幕上会显现不同的文本(拜见示例3-55了解更多有关toggle()办法的信息)。

留意:示例6-2中有两种状况,同时产生改动,但体系会接纳这一状况,保障界面仅在需求时产生更新。

@State特点创立本身和视图之间的依赖,因而在每次值产生改动时视图更新。说法是视图与特点产生了绑定。到目前为止咱们运用的都是单向绑定。特点产生修正时视图更新。但也存在视图中值被用户修正,有必要要在没有代码介入的状况下将值存回特点的状况。为此,SwiftUI支持咱们界说双向绑定。双向绑定声明的办法是在特点名前增加$符号。

需求双向绑定的视图通常是操控视图,比方创立用户可打开和关闭的开关,或可刺进文本的输入框。下例完结了一个TextField视图来演示这一功能。TextField视图创立一个输入框。其初始化办法需求的值是字符串及占位符文本,咱们用绑定特点来存储用户刺进的值。(稍后咱们会学习TextField视图及其它操控视图。)

示例6-3:界说双向绑定

struct ContentView: View {
    @State private var title: String = "Default Title"
    @State private var titleInput: String = ""
    var body: some View {
        VStack {
            Text(title)
                .padding(10)
            TextField("Insert Title", text: $titleInput)
                .textFieldStyle(.roundedBorder)
            Button(action: {
                title = titleInput
                titleInput = ""
            }, label: {
                Text("Change Title")
            })
            Spacer()
        }.padding()
    }
}

本例中,咱们向视图增加了一个存储用户刺进文本的@State特点,然后在标题和按钮之间界说了一个TextField视图。TextField视图运用占位文本”Insert Title”进行初始化,将新的titleInput特点喂给视图的绑定特点($titleInput)。这在TextField视图和特点之间创立了一个永久的衔接,每逢用户在输入框中输入或删去字符时,新值都会赋值给该特点。

Button视图的操作中,咱们做了两个修正。首先将titleInput特点的值赋值给title特点。这样就会用户刺进的文本更新视图标题。然后将空字符会串赋值给titleInput特点,来铲除输入框以便用户再次输入。

✍️跟我一同做:运用示例6-3中的代码更新ContentView视图。点击输入框,输入文本。按下Change Title按钮。标题就会修正为该段文本。

@Binding

@State特点归于声明它的结构体,应仅在结构体内部的代码拜访(因而咱们将其声明为private),但在第5章中咱们学到,在视图急剧增长时,主张将它们别离整合到独立的结构体中。这样收拾视图的问题是其它结构体就无法再引用这些@State特点了,也就无法再读取或修正它们的值了。在其它视图中界说新的@State特点也不是解决方案,由于这创立的是新状况。咱们需求的是树立一个视图中@State特点与其它视图中代码的双向绑定。为此,SwiftUI内置了Binding结构体和@Binding特点包装器。

以下示例和前例相同,但这次咱们将TextTextField视图放到一个单独的HeaderView中。这个视图中包括两个@Binding特点,用于拜访ContentView视图中的@State特点,这样处理就是同样的状况了。

示例6-4:运用@Binding特点

struct ContentView: View {
    @State private var title: String = "Default Title"
    @State private var titleInput: String = ""
    var body: some View {
        VStack {
            HeaderView(title: title, titleInput: $titleInput)
            Button(action: {
                title = titleInput
                titleInput = ""
            }, label: {
                Text("Change Title")
            })
            Spacer()
        }.padding()
    }
}
struct HeaderView: View {
    var title: String
    @Binding var titleInput: String
    var body: some View {
        VStack {
            Text(title)
                .padding(10)
            TextField("Insert Title", text: $titleInput)
                .textFieldStyle(.roundedBorder)
        }
    }
}

@Binding特点总是会从@State特点中接纳值,因而不用为其赋默认值,但树立的衔接是双向的,因而要记住在@State特点的前面增加$符号来与@Binding特点进行衔接(HeaderView(title: title, titleInput: $titleInput))。因@Binding特点和@State特点之间是双向绑定,用户输入的值存储在同一个地方,每逢体系识别到按钮点击改动时,HeaderView结构体的body特点会再次进行处理,新的值就会显现到屏幕上了。

✍️跟我一同做:运用示例6-4中的代码更新ContentView视图。记住保留底部的#Preview宏以便在画布中进行预览。在输入框中刺进值,点击Change Title按钮。作用和之前相同。

绑定结构体

前面讨论过,特点包装器以结构体进行界说,因而包括自己的特点。SwiftUI允许经过在特点名前加下划线(如_title)来拜访特点的底层结构体。拜访到结构体后,就能够处理其特点了。界说@State特点包装器的结构体为State。这是一个泛型结构体,能够处理任意类型的值。以下是该结构体界说用于存储状况值的特点。

  • wrappedValue:此特点回来由@State特点办理的值。
  • projectedValue:此特点回来Binding类型的结构体,创立与视图间的双向绑定。

wrappedValue特点存储赋给@State特点的值,就像是上例中赋值给title特点的”Default Title”字符串。projectedValue特点存储Binding类型的结构体,创立将值存回到特点的双向绑定。如果直接读取@State特点(如title),回来的值存储在wrappedValue特点中,如果在特点名前加上$符号(如$title),咱们拜访的是存储在projectedValue特点中的Binding结构体。这是SwiftUI引荐的运用@State特点的办法,但在理论上咱们也能够直接拜访这些特点,如下例所示。

示例6-5:拜访State结构体的特点

struct ContentView: View {
    @State private var title: String = "Default Title"
    @State private var titleInput: String = ""
    var body: some View {
        VStack {
            Text(_title.wrappedValue)
                .padding(10)
            TextField("Inserted Title", text: _titleInput.projectedValue)
                .textFieldStyle(.roundedBorder)
            Button(action: {
                _title.wrappedValue = _titleInput.wrappedValue
                _titleInput.wrappedValue = ""
            }, label: {
                Text("Change Title")
            })
            Spacer()
        }.padding()
    }
}

它和前面的示例作用相同,但没有运用SwiftUI的快捷办法,而是直接读取了State结构体中的wrappedValueprojectedValue特点。当时不用这么做,但有时可用于克服SwiftUI本身的缺点。比方,SwiftUI不允许咱们在赋值给body的闭包外处理@State特点,但咱们能够用一个State结构体来替换另一个。为此,能够运用以下由State结构体所供给的初始化办法。

  • State(initialValue: Value):该初始化办法运用initialValue所供给的值创立一个State特点。
  • State(wrappedValue: Value):该初始化办法运用wrappedValue所供给的包装值创立一个State特点。

例如,咱们期望对前例的输入框赋一个初始值,能够对ContentView结构体增加一个初始化办法,并用它对该特点赋一个新的State结构体。

示例6-6:初始化@State特点

    init() {
        _titleInput = State(initialValue: "Hello World")
    }

✍️跟我一同做:运用示例6-5中的代码更新ContentView视图。在ContentView结构体中增加示例6-6的初始化办法(放在@State特点下面)。这时会看到输入框初始化为了”Hello World”。

留意:这样拜访绑定特点的内容仅在没有其它挑选时才引荐运用。只需有或许,就应运用SwiftUI所供给的特点包装器或在第5章(示例5-58)中介绍过的onAppear()修饰符中装置始化@State特点,或者是在可观测对象是中存储状况,这个稍后会学到。

咱们能够按拜访@State特点同样的办法拜访@Binding特点。如果只像示例6-5那样读取该特点,回来值就是其间存储的值,如果在名称前面加$,回来值是特点包装器用于树立与视图双向绑定的Binding结构体。但如果在@Binding特点的名称前增加下划线(如_title),回来值就不是State结构体而不是Binding结构体。这是由于@Binding特点包装器在类型为Binding的结构体中界说。当然,结构体中还包括拜访这些值的特点。

  • wrappedValue:该特点回来由@Binding特点办理的值。
  • projectedValue:该特点回来创立与视图间双向绑定的类型为Binding的结构体。

State特点相同,咱们能够拜访及处理Binding结构体中存储的值。比方,下例中又完结了一个单独的视图,和示例6-4相同办理标题和输入框。在初始化了HeaderView之后,咱们经过wrappedValue特点获取到Binding结构体中存储的值,对字符串的字符数计数,然后将结果与标题一同显现出来。

示例6-7:拜访@Binding特点的值

struct ContentView: View {
    @State private var title: String = "Default Title"
    @State private var titleInput: String = ""
    var body: some View {
        VStack {
            HeaderView(title: $title, titleInput: $titleInput)
            Button(action: {
                title = titleInput
                titleInput = ""
            }, label: {
                Text("Change Title")
            })
            Spacer()
        }.padding()
    }
}
struct HeaderView: View {
    @Binding var title: String
    @Binding var titleInput: String
    let counter: Int
    init(title: Binding<String>, titleInput: Binding<String>) {
        _title = title
        _titleInput = titleInput
        let sentence = _title.wrappedValue
        counter = sentence.count
    }
    var body: some View {
        VStack {
            Text("(title) ((counter))")
                .padding(10)
            TextField("Inserted Title", text: $titleInput)
                .textFieldStyle(.roundedBorder)
        }
    }
}

示例6-7HeaderView中,咱们界说了一个特点counter,并运用wrappedValue特点回来的字符串的字符数进行初始化。由于@Binding特点没有初始值,有必要运用ContentView视图接纳到的值进行初始化(_title = title)。留意HeaderView结构体接纳到值是可办理String类型值的Binding结构体,因而参数的类型有必要用Binding<String>来声明。

初始化完值之后,咱们能够在视图中进行显现。标题现在显现 为用户刺进的文本以及字符串的字符数。

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

图6-3:@Binding特点的值界说的标题

✍️跟我一同做:运用示例6-7中的代码更新ContentView.swift文件。刺进标题。会看到如图6-3所示的标题及其右侧的字符数。

HeaderView视图的@Binding特点与ContentView视图的@State特点相衔接,因而可接纳到该特点的值,但有时这种结构体实例是单独创立的,因而需求一个绑定值。要界说这种值,能够自己创立一个Binding结构体。结构体中包括如下初始化办法和类型办法。

  • Binding(get: Closure, set: Closure):这一初始化办法创立一个Binding结构体。get参数是一个会回来当时值的闭包,set参数是一个接纳存储或处理新值的闭包。
  • constant(Value):这个类型办法创立一个带不可变值的Binding结构体。该参数是咱们期望赋值给该结构体的值。

许多场景下需求用到Binding值。例如,咱们期望创立一个HeaderView的预览,有必要为titletitleInput特点供给值。以下示例描绘了怎么创立一个新的Binding结构体供给这些值,以及怎么界说这一视图的预览。

示例6-8:创立一个Binding结构体

#Preview("Header") {
    let constantTitle = Binding<String>(
        get: { return "My Preview Title" },
        set: { value in
            print(value)
        })
    let constantInput = Binding<String>(
        get: { return "" },
        set: { value in
            print(value)
        })
    return HeaderView(title: constantTitle, titleInput: constantInput)
}

Binding结构体包括一个getter和一个setter。getter回来当时值,setter接纳赋值给结构体的值。本例中,回来的是字符串,由于没对结构体赋新值,只是在操控台中打印出了这个值。实例赋值给常量constantTitleconstantInput,然后发送给HeaderView结构体,因而在画布该视图有值能够显现。

本例中的Binding结构体没有多大用途,只是供给了Binding结构体所需求的值。在这种场景,能够运用constant()办法来简化代码。这一类型办法运用不可变值创立并回来一个Binding结构体,所以咱们不用自己创立结构体。

示例6-9:经过不可变值创立一个Binding结构体

#Preview("Header") {
    HeaderView(title: .constant("My Preview Title"), titleInput: .constant(""))
}

✍️跟我一同做:在ContentView.swift文件中增加示例6-9中的结构体。此刻会在画布顶部多出现一个按钮,显现HeaderView视图的预览。

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

代码请见:GitHub库房