问题
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