【问题标题】:How can I hide and unhide views in SwiftUI with proper onAppear and onDisappear and keep state?如何使用正确的 onAppear 和 onDisappear 在 SwiftUI 中隐藏和取消隐藏视图并保持状态?
【发布时间】:2020-10-22 21:49:07
【问题描述】:

我正在尝试在 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)

【问题讨论】:

    标签: swiftui


    【解决方案1】:

    为了解决这个问题,我们需要以某种方式维护状态,并且能够在不创建不同视图树的情况下执行 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))
    

    【讨论】:

    猜你喜欢
    • 2013-02-17
    • 2021-08-25
    • 1970-01-01
    • 2017-01-05
    • 2019-10-22
    • 2019-11-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多