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

View not updating in modal hierarchy #5

Open
jeanbaptistebeau opened this issue Oct 21, 2022 · 0 comments
Open

View not updating in modal hierarchy #5

jeanbaptistebeau opened this issue Oct 21, 2022 · 0 comments

Comments

@jeanbaptistebeau
Copy link

I'm trying to create a modal popup system working similarly to .fullScreenCover, looking something like this:

My requirements are:

  • Have custom transition and presentation style (therefore I can't use .fullScreenCover)
  • Be able to present modal from child components

Here's a functional code snippet that satisfies those two conditions, you can run it:

struct Screen: View {
    @StateObject private var model = Model()

    var body: some View {
        Navigation {
            VStack {
                Text("model.number: \(model.number)").opacity(0.5)
                ChildComponent(number: $model.number)
                Spacer()
            }
            .padding(.vertical, 30)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.purple.opacity(0.4))
        }
    }
}

struct ChildComponent: View {
    @EnvironmentObject var navigator: Navigator
    @Binding var number: Int
        
    @State private var isFullScreenPresented = false
    
    var body: some View {
        VStack(spacing: 20) {
            Text("\(number)").bold()
            Button("Change (custom)", action: presentCustom).foregroundColor(.black)
            Button("Change (full screen)", action: presentFullScreen).foregroundColor(.black)
        }
        .padding(30)
        .background(Color.black.opacity(0.1))
        .modalBottom(id: "childModal") {
            NumberModalView(number: $number)
        }
        .fullScreenCover(isPresented: $isFullScreenPresented) {
            NumberModalView(number: $number).environment(\.dismissModal, { isFullScreenPresented = false })
        }
    }
    
    func presentCustom() {
        navigator.presentModalBottom(id: "childModal")
    }
    
    func presentFullScreen() {
        isFullScreenPresented = true
    }
}

struct ModalView<Content:View>: View {
    @Environment(\.dismissModal) var dismissCallback

    @ViewBuilder var content: () -> Content
    
    var body: some View {
        VStack(spacing: 30) {
            Button("Dismiss", action: { dismissCallback() }).foregroundColor(.black)
            content()
        }
        .padding(30)
        .frame(maxWidth: .infinity)
        .background(Color.purple.opacity(0.8))
        .frame(maxHeight: .infinity, alignment: .bottom)
    }
}

struct NumberModalView: View {
    @Binding var number: Int
    
    var body: some View {
        ModalView {
            HStack(spacing: 20) {
                Button(action: { number -= 1 }) { Image(systemName: "minus.circle").resizable().foregroundColor(.black).frame(width: 30, height: 30) }
                Text("\(number)").bold()
                Button(action: { number += 1 }) { Image(systemName: "plus.circle").resizable().foregroundColor(.black).frame(width: 30, height: 30) }
            }
        }
    }
}


// MARK: - Navigation

struct Navigation<Content:View>: View {
    @ViewBuilder var content: () -> Content

    @StateObject private var navigator = Navigator()
    @State private var modalPresentations: [String:ModalData] = [:]
    
    var body: some View {
        ZStack {
            content()
            
            if let modalID = navigator.currentModalBottom, let modal = modalPresentations[modalID] {
                modal.content().environment(\.dismissModal, navigator.dismissModalBottom)
            }
        }
        .environmentObject(navigator)
        .onPreferenceChange(ModalPresentationKey.self) { modalPresentations in
            self.modalPresentations = modalPresentations
        }
    }
}


// MARK: - Model

class Model: ObservableObject {
    @Published var number: Int = 0
}

struct ModalData: Hashable {
    var id: String
    var content: () -> AnyView

    static func == (lhs: ModalData, rhs: ModalData) -> Bool { lhs.id == rhs.id }
    func hash(into hasher: inout Hasher) { hasher.combine(id) }
}

class Navigator: ObservableObject {
    @Published var currentModalBottom: String?
    
    func presentModalBottom(id: String) {
        currentModalBottom = id
    }
    
    func dismissModalBottom() {
        currentModalBottom = nil
    }
}


// MARK: - Dismiss (Environment key)

private struct ModalDismissKey: EnvironmentKey {
    static let defaultValue: () -> Void = {}
}

extension EnvironmentValues {
    var dismissModal: () -> Void {
        get { self[ModalDismissKey.self] }
        set { self[ModalDismissKey.self] = newValue }
    }
}


// MARK: - Present (Preference key)

struct ModalPresentationKey: PreferenceKey {
    static var defaultValue: [String:ModalData] = [:]
    static func reduce(value: inout [String:ModalData], nextValue: () -> [String:ModalData]) {
        for (k,v) in nextValue() { value[k] = v }
    }
}

extension View {
    
    func modalBottom<V:View>(id: String, @ViewBuilder content: @escaping () -> V) -> some View {
        preference(key: ModalPresentationKey.self, value: [
            id: ModalData(id: id, content: { AnyView(content()) })
        ])
    }
}



// MARK: - Preview

struct ParentView_Previews: PreviewProvider {
    static var previews: some View {
        ZStack {
            Screen()
        }
    }
}

Now the problem: while the parent view value gets updated, the modal view value is not updated. If you try with the default full screen, you'll see that it works normally.

I'm guessing it's a problem with data flow and the fact that the modal is not a child of the component.

Since I've already spent weeks on this problem, here are some surprising things I found:

  • If you replace the @StateObject model with a simple @State var of type Int in Screen, it works (?!). In my case, I have a complex model which I can't replace with simple state variables.
  • If you add a dependency to the navigator in NumberModalView, by adding @Environment(\.dismissModal) var dismissCallback, it works. This seems crazy to me, I don't see what role the navigator is playing in the modal data flow.

How to make the modal view react to model changes while keeping my requirements above?

Source:
https://stackoverflow.com/questions/74149682/view-not-updating-in-modal-hierarchy

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

No branches or pull requests

1 participant