Swift教程

SwiftUI - 配置 View 的几种方法

本文主要是介绍SwiftUI - 配置 View 的几种方法,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

本文翻译自 Configuring SwiftUI views

初始化、modifiers 和继承

总的来说,有三种方式来配置 SwiftUI 的 view —— 初始化 View 时传入参数、使用 modifiers、或者通过 view 所在的 environment。

比如以下的例子中我们配置了一个 Text 来作为 TitleViewbody。这个例子展示了两种配置方式,一种是初始化时传入文本,另一种是按顺序使用 modifiers 来修改文本的字体和颜色:

struct TitleView: View {
    var title: String

    var body: some View {
        Text(title)
            .font(.headline)
            .italic()
            .foregroundColor(.blue)
    }
}
复制代码

如上述例子中所示,我们把一系列 modifiers 串联起来配置界面,而不是通过修改一个个参数值来实现。这是 SwiftUI 的声明式编程风格和 UIKit 或 AppKit 这种命令式框架很不一样的地方。

以上例子中我们通过直接调用 Text 的方法来对它进行配置。除此之外还有一些间接的方式,SwiftUI 会自动把许多 modifiers 或 properties 传递到 view hierarchy 的下层,进而影响下层 View 的展示。

当我们想要让相同类型的配置或风格对多个同级 View 生效时,这种继承的方式会非常有用。就像下面的例子,我们想要让 TextList 都使用 monospaced 这种字体,只需要把 font() 这个 modifier 应用到它们的父级 VStack 上就好了:

struct ListView: View {
    var title: String
    var items: [Item]
    @Binding var selectedItem: Item?

    var body: some View {
        VStack {
            Text(title).bold()
            List(items, selection: $selectedItem) { item in
                Text(item.title)
            }
        }.font(.system(.body, design: .monospaced))
    }
}
复制代码

这种继承的方式让我们可以把共享的风格或者配置应用到 view hierarchy 的每个 view 上。这不仅让代码变得简洁,也为这些共享的配置或风格(字体、颜色等)建立了单一的数据源。

以下是另外一个例子,这里我们想要改变整个 navigation stack 的 accentColor。仅需将它设置到根上的 NavigationView 上,颜色就会对所有 child view 生效,包括被系统所管理的导航栏。

struct ContactListView: View {
    @ObservedObject var contacts: ContactList

    var body: some View {
        NavigationView {
            List(contacts) { contact in
                ...
            }
            .navigationBarItems(
                trailing: Button(
                    action: { ... },
                    label: {
                        // This image will be colored purple
                        Image(systemName: "person.badge.plus")
                    }
                )
            )
        }.accentColor(.purple)
    }
}
复制代码

然而,有时候我们想把相同的样式应用到一组 view 上,但不希望改变这些 view 和它们的 parent view 的关系。比如,我们想要创建一个 view 来展示地址,它由几个 Text view 组成:

struct AddressView: View {
    var address: Address

    var body: some View {
        VStack(alignment: .leading) {
            Text(address.recipient)
                .font(.headline)
                .padding(3)
                .background(Color.secondary)
            Text(address.street)
                .font(.headline)
                .padding(3)
                .background(Color.secondary)
            HStack {
                Text(address.postCode)
                Text(address.city)
            }
            Text(address.country)
        }
    }
}
复制代码

以上例子中前两个 Text 的样式是一样的,如果我们出于节省代码的角度考虑把这些样式提到了上层的 VStack,那么另外两个 child view 就会收到影响。

对这种情况,SwiftUI 提供了 Group 类型,它可以让我们把若干 view 当作一个组来对待,但这个组不会影响这些 view 在整个 hierarchy 内的布局、绘制、位置等。使用 Group 的话,就可以把前两个 Text 放到组里,利用组为这两个 Text 设置相同的 modifiers:

struct AddressView: View {
    var address: Address

    var body: some View {
        VStack(alignment: .leading) {
            Group {
                Text(address.recipient)
                Text(address.street)
            }
            .font(.headline)
            .padding(3)
            .background(Color.secondary)
            ...
        }
    }
}
复制代码

Group 的特殊之处在于它会将 modifiers 直接应用到它的 children 上,而不是它自己身上。如果你使用 VStack 来做这件事的话,padding(), background() 这些 modifiers 就会影响到 VStack 自身,而不仅仅是 Text

Views 和 extensions 的比较

到目前为止,我们主要通过 modifiers 来处理样式,但是 UI 配置的主要部分还是在于如何构造视图本身。

假如我们正在编写一个登录页面。为了让我们的页面看起来更好看点儿,我们给每个文本框前面都加了一个图标,实现如下:

struct SignUpForm: View {
    ...
    @State private var username = ""
    @State private var email = ""

    var body: some View {
        Form {
            Text("Sign up").font(.headline)
            HStack {
                Image(systemName: "person.circle.fill")
                TextField("Username", text: $username)
            }
            HStack {
                Image(systemName: "envelope.circle.fill")
                TextField("Email", text: $email)
            }
            Button(
                action: { ... },
                label: { Text("Continue") }
            )
        }
    }
}
复制代码

以上代码中,我们两次使用了 HStack + Image + TextField 这种形式的布局。考虑到这种布局可能经常使用,我们希望把它封装为一个独立的组件以便在其他地方重用。

遇到这种情况,我们一开始的想法或许是创建一个新类型的 View。它需要 iconName, title 两个属性来作战室,还需要一个 @Binding 引用来把用户输入的内容传给调用方。就像下面这样:

struct IconPrefixedTextField: View {
    var iconName: String
    var title: String
    @Binding var text: String

    var body: some View {
        HStack {
            Image(systemName: iconName)
            TextField(title, text: $text)
        }
    }
}
复制代码

有了以上组件后,我们就可以重构先前的 SignUpForm,去掉重复的布局代码了:

struct SignUpForm: View {
    ...

    var body: some View {
        Form {
            ...
            IconPrefixedTextField(
                iconName: "person.circle.fill",
                title: "Username",
                text: $username
            )
            IconPrefixedTextField(
                iconName: "envelope.circle.fill",
                title: "Email",
                text: $email
            )
            ...
        }
    }
}
复制代码

虽然上面的代码重用了我们创建的 IconPrefixedTextField 组件,但它是否让我们的原始代码得到了简化却值得怀疑。比起原来直接布局的方式,上述代码看起来似乎更复杂了。

让我们从 SwiftUI 自身的 API 中汲取一些灵感,看看如果把上述配置代码改成 View extension 来实现的话,会是什么样子:

extension View {
    func prefixedWithIcon(named name: String) -> some View {
        HStack {
            Image(systemName: name)
            self
        }
    }
}
复制代码

有了以上的 extension,任何视图只需要调用 prefixedWithIcon() 方法,就能在前面加上一个图标。因此 SignUpForm 可以简化为:

struct SignUpForm: View {
    ...

    var body: some View {
        Form {
            ...
            TextField("Username", text: $username)
                .prefixedWithIcon(named: "person.circle.fill")
            TextField("Email", text: $email)
                .prefixedWithIcon(named: "envelope.circle.fill")
            ...
        }
    }
}
复制代码

TextField 仍然是 TextField,只是通过 extension 对它做了一些扩展。不需要像之前的例子那样,需要进入到 IconPrefixedTextField 的实现中,才能知道它的作用。

在开发过程中,要在 创建新类型的 View 或者 使用 extension 这两种手段之间做选择有时候会很困难,实际上也没有明确的对错之分。但如果你发现你创建的 View 类型只是为了把一些属性传递给其他 view,那么你可能得问问自己,这段代码是否用 extension 会更好点儿。

自定义 modifiers

除了编写 View extension,我们还可以编写自己的 view modifiers,编写它需要实现 ViewModifiers 协议。自定义的 modifiers 可以拥有自己的属性、状态、和生命周期,为 SwiftUI 扩展出各种新功能。

比如说,我们希望给我们之前的登录页面增加输入校验能力,用户输入一段合法的字符后,文本框的边框变为绿色。尽管可以在 SignUpForm 中实现该功能,但我们可以把这个功能构建为可以重用的 view modifiers:

struct Validation<Value>: ViewModifier {
    var value: Value
    var validator: (Value) -> Bool

    func body(content: Content) -> some View {
        // 这里我们使用 Group 来做类型擦除,让我们的方法返回单一类型
        // 因为调用 border() 会导致返回一个不用的类型
        Group {
            if validator(value) {
                content.border(Color.green)
            } else {
                content
            }
        }
    }
}
复制代码

从以上例子可以看出 ViewModifier 很像一个 view,它也有一个会返回 some Viewbody。不同之处在于 ViewModifier 是针对已经存在的 view 进行操作的(通过 content 参数传入),而不是完全独立的。这样做的好处是我们可以把校验逻辑添加到任意 view 中,就像使用 extension 那样:

TextField("Username", text: $username)
    .modifier(Validation(value: username) { name in
        name.count > 4
    })
    .prefixedWithIcon(named: "person.circle.fill")
复制代码

就像在 "创建新类型 View" 和 "使用 extension" 这两者之间做选择一样,何时使用 ViewModifier 来配置界面也没有明确的答案。

然而,ViewModifierView 这两种方案有个优势是它们可以有自己的状态和属性,extension 则没有。这点可以作为选择的考量之一。

总结

就像它的前辈一样,SwiftUI 提供了多种方式来让我们组织 UI 代码。尽管我们的许多自定义组件可能会通过创建一个新的 View 类型的方式来实现,但是 extension 或 ViewModifier 可以使我们以更加轻量级的方式在代码中共享样式和配置,并且这种样式和配置不止能应用到一种 View 上,许多种 View 都能适用。

这篇关于SwiftUI - 配置 View 的几种方法的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!