SwiftUI视图及修饰符

1,532 阅读6分钟

为什么 SwiftUI 使用结构体作为视图?

UIKit和APPKit使用 来表示视图,而不是结构体。SwiftUI 则不然:全面使用结构体来显示视图,其中有几个原因。

  1. 性能因素:结构比类更简单、更快。

在 UIKit 中,每个视图都源自一个名为UIView的类,该类具有许多属性和方法。所有这些属性和方法都会传递给其子类,无论它们是否需要它们,因为这就是继承的工作原理。 在 SwiftUI 中,我们所有的视图都是简单的结构体,并且几乎可以自由创建。

  1. 以一种干净的方式隔离状态

类可以自由更改其值,这可能会导致代码更加混乱——SwiftUI 如何知道值何时更改以更新 UI?

通过生成不会随时间变化的视图,SwiftUI 鼓励我们转向更实用的设计方法:我们的视图变得简单、惰性的东西,可以将数据转换为 UI,而不是可能失控的智能东西。

我们已经使用Color.red和LinearGradient作为视图——保存很少数据的简单类型。事实上,没有比用作Color.red视图更简单的了:它除了“用红色填充我的空间”之外不包含任何信息。

SwiftUI 主视图背后是什么?

内容视图背后有一个叫做 UIHostingController 的东西:它是 UIKit(Apple 最初的 iOS UI 框架)和 SwiftUI 之间的桥梁。

修饰符顺序

几乎每次我们对 SwiftUI 视图应用修饰符时,每个修饰符都会应用该修饰符创建一个新结构,而不是仅仅在视图上设置属性。

通过询问视图主体的类型来了解 SwiftUI 的底层。如:

Button("Hello, world!") {

    print(type(of: self.body))

}    
.background(.red)
.frame(width: 200, height: 200)  

Swift 的type(of:)函数打印特定值的确切类型,在本例中它将打印以下内容:ModifiedContent<ModifiedContent<Button<Text>, _BackgroundStyleModifier<Color>>, _FrameLayout>

你可以在这里看到两件事:

每次我们修改视图时,SwiftUI 都会通过使用泛型来应用该修饰符:ModifiedContent<OurThing, OurModifier>。 当我们应用多个修饰符时,它们只会叠加:ModifiedContent<ModifiedContent<....

正如您所看到的,我们以ModifiedContent类型堆叠结束 - 每个类型都需要一个要转换的视图以及要进行的实际更改,而不是直接修改视图。

  • 这意味着修饰符的顺序很重要。

  • 建议 使用三元条件运算符

SwiftUI 会监视@State属性的变化并重新调用我们的body属性,所以只要该属性发生变化,颜色就会立即更新。

Button("按钮") {

}
.foregroundStyle(useRedText ? .red : .blue)  

您通常可以使用常规if条件根据某种状态返回不同的视图,但这实际上为 SwiftUI 带来了更多工作。当我们翻转布尔条件时,它会摧毁一个来创造另一个,而不是仅仅重新着色它所拥有的东西。 有时使用if语句是不可避免的,但在可能的情况下更喜欢使用三元运算符。

为什么 SwiftUI 使用“some View”作为其视图类型?

SwiftUI 非常依赖于名为some View “不透明返回类型”的强大功能,您每次编写代码时都可以看到它的实际效果。这意味着“一个符合View协议的对象,但我们不想说是什么。”返回some View意味着即使我们不知道要返回什么视图类型,编译器也会知道。

some View就是说“这将是一个视图,例如Button或Text,但我不想说什么。”

首先,使用some View对于性能很重要:SwiftUI 需要能够查看我们显示的视图并了解它们如何变化,以便它可以正确更新用户界面。 返回View没有任何意义,因为 Swift 想知道视图里面有什么——它有一个必须填补的大洞。

环境修改器

许多修饰符可以应用于容器,这允许该修饰符作用于其中的子视图。 当然,如果子视图覆盖相同的修饰符,则子视图的版本优先。

VStack {

    Text("Gryffindor")
        .tint(.red)
    Text("Hufflepuff")
    Text("Ravenclaw")
    Text("Slytherin")
}
.tint(.green)   // 颜色

视图 作为属性

创建一个视图作为您自己视图的属性,然后在布局中使用该属性。 如:

struct ContentView: View {

    let motto1 = Text("Draco dormiens")
    let motto2 = Text("nunquam titillandus")

    var body: some View {

        VStack {
            motto1
              .foregroundStyle(.red)
            motto2
        }
    }
}  

但是,Swift 不允许我们创建一个 引用其他存储属性的 存储属性,因为这会在创建对象时引起问题。这意味着尝试创建TextField与本地属性的绑定将会导致问题。

自定义视图

创建

struct CapsuleText: View {

    var text: String
 
    var body: some View {

        Text(text)

            .font(.largeTitle)
            .padding()
            .foregroundStyle(.white)
            .background(.blue)
            .clipShape(.capsule)
    }
}  

使用

struct ContentView: View {

    var body: some View {

        VStack(spacing: 10) {

            CapsuleText(text: "First")

            CapsuleText(text: "Second")
        }
    }
}  

自定义修饰符

SwiftUI 为我们提供了一系列内置修饰符,例如font()、background()和clipShape()。但是,也可以创建执行特定操作的自定义修饰符。

要创建自定义修饰符,需创建一个符合ViewModifier协议的新结构体。这只有一个要求,即调用一种方法,该方法接受body要使用的任何内容,并且必须返回some View。

创建

struct Title: ViewModifier {

    func body(content: Content) -> some View {

        content
            .font(.largeTitle)
            .foregroundStyle(.white)
            .padding()
            .background(.blue)
            .clipShape(.rect(cornerRadius: 10))
    }
}  

使用

将视图与modifier()修饰符一起使用 - 是的,它是一个名为“modifier”的修饰符,但它允许我们将任何类型的修饰符应用于视图。

Text("Hello World")
    .modifier(Title()) 

通常,使用自定义修饰符时,建议创建扩展 以View使其更易于使用。

extension View {

    func titleStyle() -> some View {

        modifier(Title())

    }
}  

使用

Text("Hello World")
    .titleStyle() 

自定义修改器可以做的不仅仅是应用其他现有修改器 - 它们还可以根据需要创建新的视图结构。请记住,修饰符返回新对象而不是修改现有对象,因此我们可以创建一个将视图嵌入到堆栈中并添加另一个视图的对象:

水印

struct Watermark: ViewModifier {

    var text: String 

    func body(content: Content) -> some View {

        ZStack(alignment: .bottomTrailing) {

            content

            Text(text)
                .font(.caption)
                .foregroundStyle(.white)
                .padding(5)
                .background(.black)
        }
    }
} 

// 扩展
extension View {

    func watermarked(with text: String) -> some View {

        modifier(Watermark(text: text))

    }
}  

完成后,我们现在可以向任何视图添加水印

Color.blue
    .frame(width: 300, height: 200)
    .watermarked(with: "Hacking with Swift")  

自定义容器

有一个名为GridStack 的新结构体,它符合View协议并具有一定数量的行和列,并且网格内将有许多内容单元格,它们本身必须符合View协议。

struct GridStack<Content: View>: View {

    let rows: Int
    let columns: Int
    let content: (Int, Int) -> Content

    var body: some View {

        VStack {

            ForEach(0..<rows, id: \.self) { row in

                HStack {

                    ForEach(0..<columns, id: \.self) { column in

                        content(row, column)

                    }
                }
            }
        }
    }
}  

使用了 Swift 的一个更高级的功能,称为 泛型,在这种情况下意味着“你可以提供你喜欢的任何类型的内容,但无论它是什么,它都必须符合协议View。”

let content, 它定义了一个闭包,它必须能够接受两个整数并返回我们可以显示的某种内容。

使用

struct ContentView: View {

    var body: some View {

        GridStack(rows: 4, columns: 4) { row, col in

            HStack {
                Image(systemName: "\(row * 4 + col).circle")
                Text("R\(row) C\(col)")
            }  
        }
    }
}  

GridStack能够接受任何类型的细胞内容,只要它符合协议View 。

另外,为了获得更大的灵活性 ,可以使用@ViewBuilder

@ViewBuilder let content: (Int, Int) -> Content  

完成此操作后,现在将在单元闭包内自动创建一个隐式水平堆栈:

GridStack(rows: 4, columns: 4) { row, col in

    Image(systemName: "\(row * 4 + col).circle")

    Text("R\(row) C\(col)")

}