SwiftUI自定义ViewModifier多次初始化的原因及优化咨询
SwiftUI自定义ViewModifier多次初始化问题
问题背景
我在SwiftUI中实现了遵循ViewModifier协议的自定义导航栏修饰符CustomNavigationBarModifier,用来统一应用内导航栏样式,但发现它的init方法被意外多次调用,导致不必要的重复操作。
相关实现代码
import SwiftUI enum Decoration: String { case Human case Arc } enum DestinationType { case view(() -> AnyView) case action(() -> Void) } struct CustomNavigationBarModifier: ViewModifier { @Environment(\.dismiss) private var dismiss var destination: DestinationType? var onButtonClick: (() -> Void)? init( destination: DestinationType? = nil, onButtonClick: (() -> Void)? = nil ) { self.destination = destination self.onButtonClick = onButtonClick print("Decoration / 28") } func body(content: Content) -> some View { content .background(Color.clear.ignoresSafeArea(.all)) .navigationBarBackButtonHidden(true) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button(action: { dismiss() }) { Image("Back_Arrow") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 35, height: 35) .foregroundStyle(Color("252A3F_FCFCFC")) } } } .onAppear{ print("Decoration / 58") } } } extension View { func applyCustomNavigationBar( destination: DestinationType? = nil, onButtonClick: (() -> Void)? = nil ) -> some View { self.modifier(CustomNavigationBarModifier(destination: destination, onButtonClick: onButtonClick)) } }
打印结果
Decoration / 28 Decoration / 58 Decoration / 28 Decoration / 28
我的疑问
- 为什么
CustomNavigationBarModifier的init方法会被多次调用?这和SwiftUI处理视图修改的机制有关吗? - 怎么避免
init方法多次调用以优化性能?有没有办法控制ViewModifier的初始化时机?
使用该修饰符的视图代码
import SwiftUI struct SkillsDashboard: View { @State private var cardSize: CGSize = .zero @State private var showSheet: Bool = false let skills = ["Swift", "Objective-C", "Kotlin", "Java", "Python", "JavaScript", "Ruby", "C++"] let columns = [ GridItem(.flexible(), spacing: 8), GridItem(.flexible(), spacing: 8) ] var body: some View { ZStack { ScrollView { Prediction( Button(action: { showSheet.toggle() }, label: { Text("添加技能") .multilineTextStyle(config: (.center, 20, .heavy, "252A3F_0F2534", 1)) }) ) .padding(.horizontal, 10) LazyVGrid(columns: columns, spacing: 8) { ForEach(skills, id: \.self) { _ in skillCard() } } } } .padding(.all, 8) .background(Color("EFEFF4_0F2534")) .sheet(isPresented: $showSheet) { VStack(spacing: 0) { HStack{} .frame(height: 16) .padding(0) LazyVGrids() HStack(alignment: .center, spacing: 0) { Image("AngularBorder") .resizable() .aspectRatio(contentMode: .fill) .foregroundColor(Color("FF5252_FCFCFC")) .frame(width: 34, height: 92) Spacer() Image("AngularBorder") .resizable() .aspectRatio(contentMode: .fill) .foregroundColor(Color("FF5252_FCFCFC")) .frame(width: 34, height: 92) .scaleEffect(x: -1, y: 1) } .overlay { Button { } label: { RoundedRectangle(cornerRadius: 100) .strokeBorder(Color("FF5252_FCFCFC"), lineWidth: 3, antialiased: true) .frame(height: 60) .background(Color("FFE30A_DBD5C9")) } .padding(.horizontal, 8) } .padding(.horizontal, 8) .padding(.top, -3) FormSheet() .frame(maxWidth: .infinity, alignment: .top) Spacer() } .frame(maxHeight: .infinity, alignment: .top) .padding(0) .presentationCornerRadius(25) .background(Color("FFE30A_DBD5C9")) } .applyCustomNavigationBar() } }
问题解答
1. 多次初始化的原因
这完全和SwiftUI的视图处理机制有关。SwiftUI是声明式框架,它会根据状态变化频繁重建视图树,ViewModifier作为视图的一部分,会在以下场景被重新初始化:
- 父视图的状态更新(比如
SkillsDashboard里的showSheet切换时,整个视图树会重新计算) - 修饰符依赖的环境变量(比如你用到的
@Environment(\.dismiss))发生变化 - SwiftUI在布局、渲染阶段会多次调用视图构建方法,连带触发修饰符的初始化
SwiftUI从设计上就不保证View或ViewModifier的实例只创建一次,它的核心是确保视图始终和当前状态保持一致,所以多次初始化是正常现象。
2. 优化方案
方案一:把耗时操作移出init
如果你的init里有重操作(比如网络请求、大量计算),绝对不要放在init里,而是移到onAppear并加判断确保只执行一次,或者用State管理初始化状态:
struct CustomNavigationBarModifier: ViewModifier { @Environment(\.dismiss) private var dismiss var destination: DestinationType? var onButtonClick: (() -> Void)? @State private var hasInitialized = false init( destination: DestinationType? = nil, onButtonClick: (() -> Void)? = nil ) { self.destination = destination self.onButtonClick = onButtonClick print("Decoration / 28") } func body(content: Content) -> some View { content .background(Color.clear.ignoresSafeArea(.all)) .navigationBarBackButtonHidden(true) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button(action: { dismiss() }) { Image("Back_Arrow") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 35, height: 35) .foregroundStyle(Color("252A3F_FCFCFC")) } } } .onAppear { guard !hasInitialized else { return } hasInitialized = true // 这里放原本要在init里执行的一次性操作 print("只执行一次的初始化逻辑") } } }
方案二:让修饰符遵循Equatable(参数可比较时)
如果CustomNavigationBarModifier的参数(destination、onButtonClick)可以判断相等,让它遵循Equatable协议,SwiftUI会自动跳过不必要的重建:
// 先让DestinationType遵循Equatable enum DestinationType: Equatable { case view(() -> AnyView) case action(() -> Void) // 闭包默认不可Equatable,这里简化判断(如果闭包内容不影响视图) static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (.view, .view), (.action, .action): return true default: return false } } } struct CustomNavigationBarModifier: ViewModifier, Equatable { @Environment(\.dismiss) private var dismiss var destination: DestinationType? var onButtonClick: (() -> Void)? static func == (lhs: Self, rhs: Self) -> Bool { lhs.destination == rhs.destination // 闭包比较复杂,通常可以忽略,除非你有特殊需求 } // 其他代码不变 }
注意:闭包类型本身不支持Equatable,如果修饰符依赖闭包,这个方案局限性较大,优先用方案一。
方案三:桥接UIKit控制生命周期(严格控制时)
如果必须确保只初始化一次,可以用UIKit的导航栏实现,通过UIViewControllerRepresentable桥接,利用UIKit的生命周期来控制:
struct CustomNavigationBar: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> some UIViewController { let vc = UIViewController() // 这里的代码只会执行一次 print("导航栏初始化") // 配置导航栏样式、按钮等 return vc } func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { // 状态更新时的操作(如果需要) } } // 使用示例 struct SkillsDashboard: View { var body: some View { ZStack { // ... 视图内容 } .toolbar { ToolbarItem(placement: .navigationBarLeading) { CustomNavigationBar() } } } }
内容的提问来源于stack exchange,提问作者Ehz uiq
相关产品推荐
相关产品推荐

