swift5项目 swift solution_Custom View

概览

在 SwiftUI 官方教程中,Apple 时常提出“化整为零”的界面布局思想。简单来说,Apple 推荐 SwiftUI 视图的构建方式是:用若干自定义小视图来构成上层的功能视图。

这是为什么呢?

在本篇博文中,我们将用一个通俗易懂的示例来讨论这样做的重要原因。

无需等待,Let’s Go!


1. 为何要“化整为零”?

1.1 遵循 DRY 原则

首先,从软件工程的角度来说,用自定义视图来代替原始布局代码有利于封装和代码重用。

比如 SwiftUI 中需要在各个功能视图顶部弹出 HUD 小视图:

swift5项目 swift solution_Custom View_02

如果在每个功能视图中都写一遍 HUD 视图代码,之后一旦需要修改 HUD 实现,在所有父视图中都需做出改动,这种“牵一发而动全身”的行为显然不符合日后的代码维护。

所以,这时把 HUD 的实现放在定制视图中无疑是一个明智的选择:

struct HUD<Content: View>: View {
  @ViewBuilder let content: Content

  var body: some View {
    content
      .padding(.horizontal, 12)
      .padding(16)
      .background(
        Capsule()
          .foregroundColor(Color.white)
          .shadow(color: Color(.black).opacity(0.16), radius: 12, x: 0, y: 5)
      )
  }
}

现在,在所有功能视图中我们都可以嵌入统一的 HUD 子视图,避免了代码重复:

swift5项目 swift solution_状态刷新_03

1.2 自定义视图有利于 SwiftUI 优化界面刷新

SwiftUI 非常聪明,当视图对应的状态发生变化时,它会及时的刷新状态对应的内容。

不过在有些情况下, SwiftUI 仍然需要我们来为它提供“更优化”的刷新建议。

具体来说:如果一个状态对应的视图界面被刷新,其中所有的视图都会被刷新,尽管其中某些视图实际上无需刷新(绕口令?)。

这时,若我们用自定义子视图分割显示逻辑代码,且被更改的状态没有影响到该子视图,则 SwiftUI 就不会刷新这些子视图,从而有效的提高了视图渲染性能。

有些童鞋可能担心 SwiftUI 中将父视图划分为大量自定义子视图会带来显示性能上的灾难。

其实这种担心是多虑了。

首先,SwiftUI 中视图的实现非常轻量级,而且 Apple 对 SwiftUI 中整个视图继承体系的渲染在底层做了很好的优化,嵌入大量子视图一般不会显著影响显示性能。其次,如果确实有性能问题,我们随时可以通过性能检测工具来确定性能瓶颈点,再选择优化也不迟。

过早优化是“万恶之源”,切记切记!😉

2. 一个“栗子”

也许大家对上面 “自定义视图有利于 SwiftUI 优化界面刷新” 这一概念仍不是太理解。

没关系,下面我们就用一个简单的例子让小伙伴们直观感受界面刷新优化前后的差别。

extension ShapeStyle where Self == Color {
    static var random: Color {
        Color(
            red: .random(in: 0...1),
            green: .random(in: 0...1),
            blue: .random(in: 0...1)
        )
    }
}

struct HyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.title)
            .padding()
            .foregroundColor(.white)
            .background(Color.gray)
            .clipShape(RoundedRectangle(cornerRadius: 10))
    }
}

struct ContentView: View {
    @State var s_one = 0
    @State var s_two = 0
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 50.0) {
                Text("one: \(s_one)")
                
                HStack {
                    Text("two: \(s_two)")
                }
                .padding()
                .background(Color.random)
                                
                HStack(spacing: 50) {
                    Button("add one"){
                        s_one += 1
                    }.buttonStyle(HyButtonStyle())
                    
                    Button("add two"){
                        s_two += 1
                    }.buttonStyle(HyButtonStyle())
                }
            }
            .font(.title2)
            .padding()
            .background(Color.random)
            .navigationTitle("视图“冗余”刷新演示")
        }
    }
}

顺面说一下,上述代码利用了

SwiftUI 如何快速识别视图(View)界面的刷新是由哪个状态的改变导致的? 博文中的调试技术。

简单来说,当视图被刷新时其背景色也会发生变化,我们可以用肉眼轻易辨别出每个视图是否被刷新了。


上面是一段非常简单的代码,按道理来说,状态 s_one 只会影响父视图中的 Text("one: ") 文本,而 s_two 只会影响 Text("two: ") 文本。但实际上它们会互相影响:

swift5项目 swift solution_swift5项目_04

如上图所示:改变 s_one 会刷新 Text("two: "),而改变 s_two 同样也会强制不相干 Text("one: ") 视图的刷新(观察它们背景色的变化)。

下面,我们将 Text("two: ") 包装到自定义的子视图 HySubView 中去:

struct HySubView: View {
    @Binding var val: Int
    
    var body: some View {
        HStack {
            Text("s_two: \(val)")
        }
        .padding()
        .background(Color.random)
    }
}

将 ContentView 中原内容替换为 HySubView:

VStack(spacing: 50.0) {
	Text("one: \(s_one)")
	
	HySubView(val: $s_two)

	// 其它代码从略...
}

现在,我们再来看看更改 s_one 和 s_two 状态对它们的影响:

swift5项目 swift solution_状态刷新_05

可以看到,现在 s_one 和 s_two 状态的变化只会影响其对应的视图,而不会影响到无关的视图了。于是乎我们利用自定义子视图避免了无必要的“冗余”刷新操作。

这只是一个非常简单的例子,在包含大量状态的复杂界面中,利用自定义子视图无疑会带来可观的界面渲染性能提升!棒棒哒!💯🚀

总结

在本篇博文里,我们讨论了 SwiftUI 中利用子视图代替父视图界面布局内容的重要优势,并为大家举了一个非常通俗易懂的例子。

那么,感谢观赏,再会!😎