You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

我的疑问

  1. 为什么CustomNavigationBarModifier的init方法会被多次调用?这和SwiftUI处理视图修改的机制有关吗?
  2. 怎么避免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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 13:45:55