Swift UI detail remove

孤人 提交于 2020-01-24 14:03:30

问题


I have a SwiftUI list which presents a detail view/pushes to the navigation when a cell is tapped:

import SwiftUI

struct DevicesInRangeList: View {
    @ObservedObject var central = Central()

    var body: some View {
        NavigationView {
            List(central.peripheralsInRange) { peripheral in
                NavigationLink(destination: DeviceView(peripheral: peripheral).onAppear {
                    self.central.connect(peripheral: peripheral)
                }.onDisappear {
                    self.central.disconnect(peripheral: peripheral)
                }) {
                    DeviceRow(deviceID: peripheral.deviceID, name: peripheral.name)
                }
            }.onAppear {
                self.central.scanning = true
            }.onDisappear {
                self.central.scanning = false
            }.navigationBarTitle("Devices in range")
        }
    }
}

If I tap a row, the detail is displayed. If the peripheral disconnects it is removed from the peripheralsInRange array and the row is removed – but the detail is still displayed. How can the detail be removed when the associated row is removed?


Edit: After Asperi's answer I have the following, which still doesn't work:

struct DevicesInRangeList: View {
    @ObservedObject var central = Central()

    @State private var localPeripherals: [Peripheral] = []

    @State private var activeDetails = false
    var body: some View {
        NavigationView {
            List(localPeripherals, id: \.self) { peripheral in
                NavigationLink(destination:
                    DeviceView(peripheral: peripheral)
                        .onReceive(self.central.$peripheralsInRange) { peripherals in
                            if !peripherals.contains(peripheral) {
                                self.activeDetails = false
                            }
                        }
                        .onAppear {
                            self.central.connect(peripheral: peripheral)
                        }
                        .onDisappear {
                            self.central.disconnect(peripheral: peripheral)
                        }
                , isActive: self.$activeDetails) {
                    DeviceRow(deviceID: peripheral.deviceID, name: peripheral.name)
                }
            }.onReceive(central.$peripheralsInRange) { peripherals in
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    self.localPeripherals = peripherals
                }
            }.onAppear {
                self.central.scanning = true
                self.localPeripherals = self.central.peripheralsInRange
            }.onDisappear {
                self.central.scanning = false
            }.navigationBarTitle("Devices in range")
        }
    }
}

回答1:


The best way is check existence od data before displaying it. I adopted apple's master / demo to show how to do it. In this template application they use @State var as source of records, but the idea is the same. Check existence of "record" IN DETAIL VIEW.

import SwiftUI

private let dateFormatter: DateFormatter = {
    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .medium
    dateFormatter.timeStyle = .medium
    return dateFormatter
}()

struct ContentView: View {
    @State private var dates = [Date]()

    var body: some View {
        NavigationView {
            MasterView(dates: $dates)
                .navigationBarTitle(Text("Master"))
                .navigationBarItems(
                    leading: EditButton(),
                    trailing: Button(
                        action: {
                            withAnimation { self.dates.insert(Date(), at: 0) }
                        }
                    ) {
                        Image(systemName: "plus")
                    }
                )
            DetailView(dates: $dates).navigationBarTitle(Text("Detail"))
        }.navigationViewStyle(DoubleColumnNavigationViewStyle())
    }
}

struct MasterView: View {
    @Binding var dates: [Date]

    var body: some View {
        List {
            ForEach(dates, id: \.self) { date in
                NavigationLink(
                    destination: DetailView(dates: self._dates, selectedDate: date).navigationBarTitle(Text("Detail"))
                ) {
                    Text("\(date, formatter: dateFormatter)")
                }
            }.onDelete { indices in
                indices.forEach { self.dates.remove(at: $0) }
            }
        }
    }
}

struct DetailView: View {
    @Binding var dates: [Date]
    var selectedDate: Date?

    var body: some View {
            if let selectedDate = selectedDate, dates.contains(selectedDate) {
                return Text("\(selectedDate, formatter: dateFormatter)")
            } else {
                return Text("Detail view content goes here")
            }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}




回答2:


Well... will be a bit long, but it worth... I've reproduced the defect behaviour on simplified model... and here is the reason of issue

2020-01-22 19:53:41.008064+0200 Test[5539:983123] [TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. Table view: <_TtC7SwiftUIP33_BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView: 0x7fd095042600; baseClass = UITableView; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = ; layer = ; contentOffset: {0, -116}; contentSize: {375, 400.5}; adjustedContentInset: {116, 0, 0, 0}; dataSource: <_TtGC7SwiftUIP13$7fff2c6b223419ListCoreCoordinatorGVS_20SystemListDataSourceOs5Never_GOS_19SelectionManagerBoxS2___: 0x7fd093f62b60>>

This exception breaks navigation stack so details view is not closed either by itself or forcefully by isActive state.

So here is initial code that reproduces the issue (once started just navigate any row and wait for 20 secs)

// view model holding some sequence of data to be shown in List
class TestedModel: ObservableObject {
    @Published var originalRange = [1, 2, 3, 4, 5, 6, 7, 8, 9]
}

// simple detail view 
struct DetachedDetailView: View {
    let item: Int
    var body: some View {
        Text("Details of item \(item)")
    }
}

// Issue demo view
struct TestNavigationLinkDestruction_Issue: View {
    @ObservedObject var model = TestedModel()

    var body: some View {
        NavigationView {
            List(model.originalRange, id: \.self) { item in
                NavigationLink("Item \(item)", destination:
                    DetachedDetailView(item: item))
            }
        }
        .onAppear {
            // >> by this simulated async update of List while in Details
            DispatchQueue.main.asyncAfter(deadline: .now() + 20) {
                self.model.originalRange = [10, 20, 30, 40, 50, 60, 70, 80, 90]
            }
        }
    }
}

And here is a solution... the idea is separate in time update of List content and moment of making decision is it needed to close details

struct TestNavigationLinkDestruction_Fixed: View {
    @ObservedObject var model = TestedModel()

    @State private var selected: Int? = nil
    @State private var localStorage: [Int] = []


    var body: some View {
        NavigationView {
            // List locally stored items
            List(localStorage, id: \.self) { item in
                NavigationLink("Item \(item)", destination:
                    DetachedDetailView(item: item)
                        .onReceive(self.model.$originalRange) { items  in
                            if !items.contains(item) {
                                self.selected = nil // !!! unwind at once
                            }
                        }
                , tag:item, selection: self.$selected)
            }
            .onReceive(self.model.$originalRange) { items  in
                DispatchQueue.main.async {
                    self.localStorage = items // !!! postpone local data update
                }
            }
        }
        .onAppear {
            self.localStorage = self.model.originalRange // ! initial load from model

            // >>> simulate async data update
            DispatchQueue.main.asyncAfter(deadline: .now() + 20) {
                self.model.originalRange = [10, 20, 30, 40, 50, 60, 70, 80, 90]
            }
        }
    }
}

So.. all you need is to adopt above to your code, I'm sure it's feasible.



来源:https://stackoverflow.com/questions/59859178/swift-ui-detail-remove

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!