如何使用适当的onAppear和onDisappear并保持状态在SwiftUI中隐藏和取消隐藏视图?

时间:2020-10-22 13:49:07

标签: swiftui

我正在尝试在SwiftUI中隐藏和取消隐藏视图,但这似乎很困难。隐藏和取消隐藏视图时,我无法保持状态的连续性。

struct CounterView: View {
    var title: String
    var delay = 0.5
    
    @State var ticker: Int = 0
    @State var visible = IsVisible()
    
    class IsVisible {
        var bool: Bool = false
    }
    
    var body: some View {
        Text("\(title) \(ticker)")
            .onAppear {
                print("\(title) onAppear")
                if !self.visible.bool {
                    self.visible.bool = true
                    startCounter()
                }
            }
            .onDisappear {
                print("\(title) onDisappear")
                self.visible.bool = false
            }
    }
    
    func startCounter() {
        self.ticker += 1
        print(self.ticker)
        
        if self.visible.bool {
            DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
                self.startCounter()
            }
        }
    }
}


struct ContentView: View {
    @State var hide = false
    
    var body: some View {
        VStack {
            VStack {
                Button {
                    self.hide.toggle()
                } label: {
                    Text(hide ? "Show" : "Hide")
                }
                myView()
            }
        }
    }
    
    @ViewBuilder
    func myView() -> some View {
        let view = CounterView(title: "Test", delay: 0.1)
        if hide {
            view.hidden()
        } else {
            view
        }
    }
}

复制步骤

1. Open app
2. Tap hide
3. Tap show

预期结果

Counter continues from last number When the view is hidden it gets no accidental onAppear callbacks

实际结果

Counter restarts from 0
When hiding the view a new view is created and it gets an onAppear callback

想要的解决方案

理想情况下,我要么会做

let view = CounterView(title: "Test", delay: 0.1)
if hide {
    view.hidden()
} else {
    view
}

甚至更好

CounterView(title: "Test", delay: 0.1).hide(true)

1 个答案:

答案 0 :(得分:0)

为了解决此问题,我们需要以某种方式维护状态,并能够执行诸如view.hide(true)之类的操作而无需创建其他视图树。通过将视图包装在UIKit视图中,我们可以实现以下目标:

Original view structure:
- MyView

New view structure:
- HideableView (SwiftUI)
  -> HideableView.Container (UIView)
     -> UIHostingController (UIView)
        -> MyView (SwiftUI)

然后我们可以完全控制视图的生命周期,因为我们可以通过这种方式在其超级视图(HideableView.Container)中添加和删除UIHostingController。

extension View {
    func hide(_ hide: Bool) -> some View {
        HideableView(isHidden: .constant(hide), view: self)
    }
    
    func hide(_ isHidden: Binding<Bool>) -> some View {
        HideableView(isHidden: isHidden, view: self)
    }
}

struct HideableView<Content: View>: UIViewRepresentable {
    
    @Binding var isHidden: Bool
    var view: Content
    
    func makeUIView(context: Context) -> ViewContainer<Content> {
        return ViewContainer(isContentHidden: isHidden, child: view)
    }
    
    func updateUIView(_ container: ViewContainer<Content>, context: Context) {
        container.child.rootView = view
        container.isContentHidden = isHidden
    }
    
    class ViewContainer<Content: View>: UIView {
        var child: UIHostingController<Content>
        var didShow = false
        var isContentHidden: Bool {
            didSet {
                addOrRemove()
            }
        }
        
        init(isContentHidden: Bool, child: Content) {
            self.child = UIHostingController(rootView: child)
            self.isContentHidden = isContentHidden
            super.init(frame: .zero)
            addOrRemove()
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            child.view.frame = bounds
        }
        
        func addOrRemove() {
            if isContentHidden && child.view.superview != nil {
                child.view.removeFromSuperview()
            }
            if !isContentHidden && child.view.superview == nil {
                if !didShow {
                    DispatchQueue.main.async {
                        if !self.isContentHidden {
                            self.addSubview(self.child.view)
                            self.didShow = true
                        }
                    }
                } else {
                    addSubview(child.view)
                }
            }
        }
        
    }
}

那么我们可以简单地做到:

CounterView(title: "Test", delay: 0.1).hide(true)

或更明确地说:

HideableView(isHidden: .constant(true), view: CounterView(title: "Test", delay: 0.1))