Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to reload data without using onAppear in SwiftUI in watchOS #468

Open
onmyway133 opened this issue Oct 17, 2019 · 3 comments
Open

How to reload data without using onAppear in SwiftUI in watchOS #468

onmyway133 opened this issue Oct 17, 2019 · 3 comments

Comments

@onmyway133
Copy link
Owner

onmyway133 commented Oct 17, 2019

From onAppeear

Adds an action to perform when the view appears.

In theory, this should be triggered every time this view appears. But in practice, it is only called when it is pushed on navigation stack, not when we return to it.

So if user goes to a bookmark in a bookmark list, unbookmark an item and go back to the bookmark list, onAppear is not called again and the list is not updated.

import SwiftUI

struct BookmarksView: View {
    let service: Service
    @State var items: [AnyItem]
    @EnvironmentObject var storeContainer: StoreContainer

    var body: some View {
        List(items)  { item in
            makeItemRow(item: item)
                .padding([.top, .bottom], 4)
        }
        .onAppear(perform: {
            self.items = storeContainer.bookmarks(service: service).map({ AnyItem(item: $0) })
        })
    }
}

So instead of relying on UI state, we should rely on data state, by listening to onReceive and update our local @State

struct BookmarksView: View {
    let service: Service
    @State var items: [AnyItem]
    @EnvironmentObject var storeContainer: StoreContainer

    var body: some View {
        List(items)  { item in
            makeItemRow(item: item)
                .padding([.top, .bottom], 4)
        }
        .onAppear(perform: {
            self.reload()
        })
        .onReceive(storeContainer.objectWillChange, perform: { _ in
            self.reload()
        })
    }

    private func reload() {
        self.items = storeContainer.bookmarks(service: service).map({ AnyItem(item: $0) })
    }
}

Inside our ObservableObject, we need to trigger changes notification

final class StoreContainer: ObservableObject {
    let objectWillChange = PassthroughSubject<(), Never>()

    func bookmark(item: ItemProtocol) {
        defer {
            objectWillChange.send(())
        }        
    }

    func unbookmark(item: ItemProtocol) {
        defer {
            objectWillChange.send(())
        }
    }
}
@micahnap
Copy link

Curious if this is a retain cycle given self is being captured in the life cycle closures, and then retained in body?

var body: some View {
      ....
        .onAppear(perform: {
            self.reload()
        })
        .onReceive(storeContainer.objectWillChange, perform: { _ in
            self.reload()
        })
    }

@onmyway133
Copy link
Owner Author

@micahnap It's a struct, no no weak strong

@muizidn
Copy link

muizidn commented Oct 3, 2020

I am using SwiftUI on Xcode 11.2
My current solution is to employ navigation using UIKit framework rather than relying to NavigationView and SwiftUI native mechanism.

Here is the little code.

protocol PREVRouteDelegate: class {
    func show()
    func push()
}

final class PREVHostingVC: UIHostingController<AnyView>, PREVRouteDelegate {
    class ROUTE: ObservableObject {
        var delegate: PREVRouteDelegate? = nil
        init() {}
    }
    init() {
        let route = ROUTE()
        super.init(rootView: AnyView(PREV().environmentObject(route)))
        route.delegate = self
    }
    
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        print("UIKIT.DIDLOAD")
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        print("UIKIT.WILLAPPEAR")
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        print("UIKIT.DIDAPPEAR")
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        print("UIKIT.WILLDISAPPEAR")
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        print("UIKIT.DIDDISAPPEAR")
    }
    
    func show() {
        let vc = NEXTHostingVC()
        vc.modalPresentationStyle = .overFullScreen
        present(vc, animated: true, completion: nil)
    }
    
    func push() {
        let vc = NEXTHostingVC()
        navigationController?
            .pushViewController(vc, animated: true)
    }
    
}


struct PREV: View {
    @State private var isPresented = false
    @State private var zstack = false
    @State private var error = String?.none
    
    @EnvironmentObject var route: PREVHostingVC.ROUTE
    
    var body: some View {
        VStack {
            Button("Present SWIFTUI!") {
                self.isPresented.toggle()
            }
            Button("Show UIKIT!") {
                self.route.delegate?.show()
            }
            Button("Push UIKIt!") {
                self.route.delegate?.push()
            }
            NavigationView {
                NavigationLink(
                    "Push SwiftUI",
                    destination: NEXT(
                        text: "Push SwiftUI..."))
            }
            Button("ERROR!") {
                self.error = "Just an error!"
            }
            Button("ZSTACK!") {
                self.zstack.toggle()
            }
            ZStack(alignment: .bottom) {
                EmptyView()
                if zstack {
                    Text("Zstack shown")
                }
            }
        }
       //  .onError(with: $error)
        .onAppear(perform: {
          print("ON APPEAR")
        })
        .sheet(isPresented: $isPresented) {
            NEXT(text: "SHEET...")
        }
    }
}


protocol NEXTRouteDelegate: class {
    func dismiss()
    func pop()
}

final class NEXTHostingVC: UIHostingController<AnyView>, NEXTRouteDelegate, PanModalPresentable {
    var panScrollable: UIScrollView? { nil }
    
    class ROUTE: ObservableObject {
        var delegate: NEXTRouteDelegate? = nil
        init() {}
    }
    init() {
        let route = ROUTE()
        super.init(rootView: AnyView(NEXT(text:"NEXT HOSTING VC").environmentObject(route)))
        route.delegate = self
    }
    
    @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func dismiss() {
        self.dismiss(animated: true, completion: nil)
    }
    
    func pop() {
        navigationController?.popViewController(animated: true)
    }
}


struct NEXT: View {
    @Environment(\.presentationMode) var presentationMode
    @EnvironmentObject var route: NEXTHostingVC.ROUTE
    
    let text: String

    var body: some View {
        VStack {
            Text(text)
            
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Dismiss presentationMode")
            }
            
            Button(action: {
                self.route.delegate?.dismiss()
            }) {
                Text("Dismiss NEXTHOSTINGVC")
            }
            
            Button(action: {
                self.route.delegate?.pop()
            }) {
                Text("Pop NEXTHOSTINGVC")
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.red)
        .edgesIgnoringSafeArea(.all)
    }
}

Which I found this pattern to be super useful, especially for iOS 13 SwiftUI's which lack a lot of UIKit capabilities. (for me, this navigation system).
Also, the capability to use modalPresentationStyle = .fullScreen / .overFullScreen which only available for SwiftUI in iOS 14.

For reloading using onAppear I found out that in UIKit's counterpart, viewWillAppear will only be invoked when NEXT is presented using .fullScreen.

window.rootViewController = UINavigationController(
    rootViewController: PREVHostingVC()
 )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants