0

Using iOS14.4, Swift5.3.2, XCode12.2,

I try to dismiss a SwiftUI's GridView (see below code).

The dismiss function is done by the \.presentationMode property of the @Environment as explained here.

Everything works until the moment where I introduced a @Binding property that mutates the parent-View at the very moment of the dismissal. (see dataStr = titles[idx] in code excerpt below).

I read that dismissal by \.presentationMode only works if the parent-View is not updated during the time the child-View is shown.

But I absolutely need to cause a mutation on the parent-View when the user taps on an element of the GridView at play here.

How can I re-write so that parent-View is updated AND dismissal of Child-View still work ?

struct GridView: View {
    
    @Environment(\.presentationMode) private var presentationMode
    
    @Binding var dataStr: String
    
    @State private var titles = [String]()
    
    let layout = [
        GridItem(.flexible()),
        GridItem(.flexible())
    ]
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: layout, spacing: 10) {
                ForEach(titles.indices, id: \.self) { idx in
                    VStack {
                        Text(titles[idx])
                        Image(titles[idx])
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: (UIScreen.main.bounds.width / 2) - 40)
                    }
                        .onTapGesture {

                            // WITHOUT THIS LINE OF CODE - EVERYTHING WORKS. WHY???????????????
                            dataStr = titles[idx]

                            self.presentationMode.wrappedValue.dismiss()
                        }
                }
            }
            .padding()
        }
    }
}

As @jnpdx asked, here you can see the parent-View. Please find the GridView(dataStr: self.$dataStr) inside the .sheet() of the ToolBarItem()....

import SwiftUI

struct MainView: View {
    
    @EnvironmentObject var mediaViewModel: MediaViewModel
    @EnvironmentObject var commService: CommunicationService
    
    @State private var dataStr = ""
    @State private var connectionsLabel = ""
    @State private var commumincationRole: THRole = .noMode
    @State private var showingInfo = false
    @State private var showingGrid = false
    
    init() {
        UINavigationBar.appearance().tintColor = UIColor(named: "title")        
    }
    
    var body: some View {
        NavigationView {
            if mediaViewModel.mediaList.isEmpty {
                LoadingAnimationView()                    
                    .navigationBarHidden(true)
                    .ignoresSafeArea()
            } else {
                if dataStr.isEmpty {
                    
                    MainButtonView(dataStr: $dataStr,
                        commumincationRole: $commumincationRole,
                          connectionsLabel: $connectionsLabel
                    )
                        .navigationBarHidden(false)
                        .navigationTitle("Trihow Pocket")
                        .navigationBarColor(backgroundColor: UIColor(named: "btnInactive"), titleColor: UIColor(named: "title"))
                        .toolbar {
                            ToolbarItem(placement: .navigationBarLeading) {
                                Button(action: {
                                    showingInfo.toggle()
                                }) {
                                    Image(systemName: "ellipsis")
                                }
                                .sheet(isPresented: $showingInfo) {
                                    InfoView()
                                }
                            }
                            ToolbarItem(placement: .navigationBarTrailing) {
                                Button(action: {
                                    showingGrid.toggle()
                                }) {
                                    Image(systemName: "square.grid.3x3")
                                }
                                .sheet(isPresented: $showingGrid) {

                                    // GRIDVIEW CALLING THE CHILD-VIEW IS HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                                    GridView(dataStr: self.$dataStr)
                                }
                            }
                    }
                } else {
                    let str = self.dataStr
                    #if os(iOS)
                    PageViewiOS(dataStr: self.$dataStr, commumincationRole: $commumincationRole)
                        .navigationBarHidden(true)
                        .onAppear() {
                            if commumincationRole == .moderatorMode {
                                commService.send(thCmd: THCmd(key: .tagID, sender: "", content: str))
                            }
                        }
                        .ignoresSafeArea()
                    #elseif os(macOS)
                    PageViewMacOS()
                        .ignoresSafeArea()
                    #endif
                }
            }
        }
        .onTHComm_PeerAction(service: commService) { (peers) in
            
            let idsOrNames = peers.map { (peer) -> String in
                if let id = peer.id {
                    return "\(id)"
                } else if let name = peer.name {
                    return "\(name)"
                } else {
                    return ""
                }
            }
            connectionsLabel = "Connected devices: \n\(idsOrNames.lineFeedString)"
        }
        .onTHComm_ReceiveCmd(service: commService) { (thCmd) in
            if (commumincationRole == .moderatorMode) || (commumincationRole == .discoveryMode) {
                switch thCmd.key {
                case .tagID:
                    dataStr = thCmd.content
                case .closeID:
                    dataStr = ""
                default:
                    break
                }
            }
        }
        .onTHComm_LastMessageLog(service: commService) { (log) in
            print(log)
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}

struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
            .environmentObject(MediaViewModel())
            .environmentObject(MultipeerConnectivityService())
    }
}
iKK
  • 6,394
  • 10
  • 58
  • 131
  • It may be helpful to see how you're presenting this view originally (from the parent) – jnpdx Mar 15 '21 at 17:43
  • @jnpdx, I added the parent-View (...and yes, it is rather complicated)... – iKK Mar 15 '21 at 17:51
  • 1
    Hm. Not seeing anything immediately, but it's hard without testable code. What if you wrap the dismiss call in `DispatchQueue.main.async {}`? – jnpdx Mar 15 '21 at 17:54
  • Yes, the `async` did not work - but with an `asyncAfter` it worked !!! i.e. I wrapped the `dataStr = thumNames[idx]` inside `DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {...}` and now it dismisses. Remains the question what if the parent-update can't be done at the very end like in my example? It seems that SwiftUI has a problem dismissing when updates happen on the parent-View. Other people described it [here](https://stackoverflow.com/questions/56517400/swiftui-dismiss-modal) – iKK Mar 15 '21 at 18:12
  • What error do you get? Have you tried using a simple Text() view instead of PageViewiOS? I just did. It worked. – mahan Mar 16 '21 at 08:37

1 Answers1

0

With the help of @jnpdx, I found a workaround.

Wrap the binding-property (i.e. dataStr in my example) into a delayed block that executes after something like 50ms:

.onTapGesture {
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
        dataStr = thumNames[idx]
    }
    self.presentationMode.wrappedValue.dismiss()
}

Of course, this workaround only works in my case, because I do no longer need to keep the Child-View open. There might be other situations where the Parent-View needs to be updated prior to closing the Child-View (i.e. here the update of dataStr can be done right at the closing moment of the Child-View).

I am still wondering how to deal with dismiss-problems for any case where the Child-View makes the Parent-View update prior to closing. These are situations where SwiftUI's dismiss function no longer work from then on. Any mutation of the Parent-View cause the Child-View to separate somehow and dismisss no longer works.

Any idea what to do in that case ?

iKK
  • 6,394
  • 10
  • 58
  • 131