SwiftUI如何防止视图重新加载整个身体

Posted

技术标签:

【中文标题】SwiftUI如何防止视图重新加载整个身体【英文标题】:SwiftUI how to prevent view to reload whole body 【发布时间】:2020-06-14 08:25:08 【问题描述】:

基本上,我试图弄清楚我的 viewModel 何时更新,它会通知视图并刷新整个主体。如何避免这种情况。例如,如果我的视图 GoLiveView 已经呈现另一个视图 BroadcasterView,然后我的 goLiveViewModel 得到更新,GoLiveView 将被刷新,它会再次创建 BroadcasterView,因为 showBroadcasterView = true。正因为如此,它会导致很多问题。

struct GoLiveView: View 

@ObservedObject var goLiveViewModel = GoLiveViewModel()
@EnvironmentObject var sessionStore: SessionStore
@State private var showBroadcasterView = false
@State private var showLiveView = false

init() 
    goLiveViewModel.refresh()


var body: some View 
    NavigationView 
        List(goLiveViewModel.rooms)  room in // when goLiveViewModed get updated 
            NavigationLink(destination: LiveView(clientRole: .audience, room: room, showLiveView: $showLiveView))) 
                LiveCell(room: room)

            
        .background(Color.white)
        .navigationBarTitle("Live", displayMode: .inline)
        .navigationBarItems(leading:
            Button(action: 
                self.showBroadcasterView = true
        , label: 
            Image("ic_go_live").renderingMode(.original)
        )).frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color(red: 34/255, green: 34/255, blue: 34/255))

        .sheet(isPresented: $showBroadcasterView)  // here is problem, get called many times, hence reload whole body ,and create new instances of BroadcasterView(). Because showBroadcasterView = is still true.

                BroadcasterView(broadcasterViewModel: BroadcasterViewModel(showBroadcasterView: $showBroadcasterView))
                    .environmentObject(self.sessionStore)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.clear)
            

    

这是我的 GoliveViewModel

typealias RoomsFetchOuput = AnyPublisher<RoomsFetchState, Never>

enum RoomsFetchState: Equatable 

    static func == (lhs: RoomsFetchState, rhs: RoomsFetchState) -> Bool 
        switch (lhs, rhs) 
        case (.loading, .loading): return true
        case (.success(let lhsrooms), .success(let rhsrooms)):
            return lhsrooms == rhsrooms
        case (.noResults, .noResults): return true
        case (.failure, .failure): return true
        default: return false
        
    

    case loading
    case success([Room])
    case noResults
    case failure(Error)

class GoLiveViewModel: ObservableObject 

    private lazy var webServiceManager = WebServiceManager()
    @Published var rooms = [Room]()
    private lazy var timer = Timer()
    private var cancellables: [AnyCancellable] = []

    init() 
        timer = Timer.scheduledTimer(timeInterval: 4.0, target: self, selector: #selector(refresh) , userInfo: nil, repeats: true) //  call every 4 second refresh
    

    func fetch() -> RoomsFetchOuput 
        return webServiceManager.fetchAllRooms()
            .map ( result -> RoomsFetchState in
                switch result 
                case .success([]): return .noResults
                case let .success(rooms): return .success(rooms)
                case .failure(let error): return .failure(error)
                
            )
            .eraseToAnyPublisher()

        let isLoading: RoomsFetchOuput = .just(.loading)
        let initialState: RoomsFetchOuput = .just(.noResults)

        let idle: RoomsFetchOuput = Publishers.Merge(isLoading, initialState).eraseToAnyPublisher()

        return Publishers.Merge(idle, rooms).removeDuplicates().eraseToAnyPublisher()

    

    @objc func refresh() 
         cancellables.forEach  $0.cancel() 
          cancellables.removeAll()
        fetch()
            .sink  [weak self] state in
                guard let self = self else  return 
                switch state 
                case let .success(rooms):
                    self.rooms = rooms
                case .failure: print("failure")
                // show error alert to user
                case .noResults: print("no result")
                self.rooms = []
                // hide spinner
                case .loading:  print(".loading")
                    // show spinner
                
        
        .store(in: &cancellables)
    

【问题讨论】:

【参考方案1】:

SwfitUI 对此有一个模式。它需要使自定义视图符合Equatable 协议

struct CustomView: View, Equatable 

    static func == (lhs: CustomView, rhs: CustomView) -> Bool 
        // << return yes on view properties which identifies that the
        // view is equal and should not be refreshed (ie. `body` is not rebuilt)
    
...

并在构造处添加修饰符.equatable(),例如

var body: some View 
      CustomView().equatable()

是的,每次刷新超级视图时都会构造 CustomView 的新值(所以不要让 init 变重),但 body 只有在以下情况下才会被调用新构建的视图不等于以前构建的视图

最后,可以看出将 UI 层次结构分解为多个视图非常有用,它将允许优化刷新很多(但不仅是良好的设计、可维护性、可重用性等:^)。

【讨论】:

有一个错误导致每次用户进行选择时都会使用选择器重新渲染视图。这反过来又导致选择器在用户做出选择后在视觉上重置为其初始值。你的解决方案很棒,直到现在我才知道这个内置功能 谢谢。这个解决方案给了我在切换 UI 时随机挂断的答案。我还发现这篇文章解释得很详细-swiftui-lab.com/equatableview @Seitenwerk 你的问题到底是什么,你是如何解决的。我想我遇到了类似的问题。我目前正在使用 SwiftUIFlux,一旦它被更改(也是手动),它会自动更新选择器的日期,因此会导致选择器立即关闭。所以我不知何故需要打破这个更新周期,但还没有找到可行的解决方案。 如果您在View 中使用@EnvironmentObject,则此解决方案不起作用

以上是关于SwiftUI如何防止视图重新加载整个身体的主要内容,如果未能解决你的问题,请参考以下文章

如何防止键盘在 SwiftUI 中向上推视图? [复制]

如何在 Touch Down 实现时防止 SwiftUI 上的重新触发

如何防止 SwiftUI 像使用 @StateObject 一样重新初始化我的包装属性?

SwiftUI:防止 Image() 将视图矩形扩展到屏幕边界之外

如何阻止 SwiftUI 重新加载我通过 UIViewRepresentable 使用的 WKWebView 页面?

SwiftUI:如果 @ObservedObject 是 UIViewController 的子类,则不会重新加载视图内容。这是一个错误还是我错过了啥?