Я пытаюсь написать @Query
с пользовательским предикатом в подробном представлении SwiftUI. Когда я щелкаю элементы в списке, приложение зависает, и использование памяти постоянно увеличивается линейно.
Если я поставлю предикат на @Query
в главном представлении, приложение будет работать нормально.
Все это основано на шаблонном приложении SwiftUI+SwiftData с минимальными изменениями в подробном представлении:
Подробный вид (где возникает проблема):
struct DetailView: View {
var item: Item
// Issue occurs when a Query with a filter is set. Without the filter it works fine
// contents of Predicate doesn't matter so long as it's valid
@Query(filter: #Predicate<Item> { item in
true
}) private var items: [Item]
var body: some View {
VStack {
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
Text("Count: \(items.count)")
}
}
}
Посмотреть список:
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
// If this Query is modified, no issue
@Query private var items: [Item]
var body: some View {
NavigationSplitView {
List {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
}
} detail: {
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(timestamp: Date())
modelContext.insert(newItem)
}
}
}
Модель данных:
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
Время работы приложения:
@main
struct TestSwiftDataApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Item.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
навигационные ссылки отображаются до того, как они будут нажаты
Я думаю, что, возможно, решение состоит в том, чтобы просто вывести запрос из подробного представления и передать количество элементов в качестве аргумента конструктора.
@User45i6h45ih3455 User45i6h45ih3455 это надуманный пример для простоты.
Это не документированный способ использования NavigationSplitView
. Можете ли вы использовать NavigationSplitView
, как описано в документации ? То есть ваш List
должен иметь selection:
, а разделенное представление должно автоматически отображать подробное представление в конструкторе представлений detail:
на основе выбора. Не следует использовать пункт назначения NavigationLink
.
Кроме того, я не могу воспроизвести это на iOS 17.4. Все работает как положено - приложение не зависает.
Мне удалось воспроизвести это на macOS, и я мог видеть в консоли с активным ведением журнала отладки Core Data, что одна и та же выборка выполнялась снова и снова, когда я нажимал на элемент на боковой панели. Я также могу убедиться, что изменение реализации NavigationSplitView, предложенное @Sweeper, устраняет эту проблему.
Я забыл сказать, что при воспроизведении проблемы мне удалось «исправить» ее так же, как это сделал ОП, удалив предикат.
Еще одно исследование: при использовании NavigationLink я вижу, что в консоли выполняется одна выборка, но с вариантом выбора (без NavigationLink) я вижу две выборки. Когда я добавляю let _ = Self._printChanges()
в тело каждого представления, я вижу, что разница заключается в том, что при выборе элемента на боковой панели изменение свойства выбора также запускает выполнение выборки, отличной от той, что в DetailView. Для меня это похоже на ошибку, но, безусловно, на данный момент нужно знать, если запрос возвращает много объектов.
@Sweeper Итак, в проекте, который вдохновил меня на это, я фактически использовал NavigationStack вместо NavigationSplitView. И для этого проекта, и для примера кода здесь кажется, что переход на использование navigationDestination
решил проблемы? Если вы хотите написать ответ, предполагая, что я приму его
И да, когда я использую Self._printChanges(), он просто говорит, что @self менялся снова и снова.
Обратите внимание: хотя это правда, что это исправлено, я продолжаю сталкиваться с этой проблемой снова с помощью дополнительных навигационных ссылок... расстраивает.
Это не документированный способ использования NavigationSplitView
. Вам следует использовать переключатель List
, чтобы контролировать отображение подробного представления.
@Query private var items: [Item]
@State private var selectedItem: PersistentIdentifier?
var body: some View {
NavigationSplitView {
List(selection: $selectedItem) {
ForEach(items) { item in
NavigationLink(value: item.id) {
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
}
} detail: {
if let selectedItem, let item: Item = modelContext.registeredModel(for: selectedItem) {
DetailView(item: item)
} else {
Text("Select an item")
}
}
Если вместо этого вы используете NavigationStack
, использование navigationDestination(for:)
на основе значения решает проблему.
NavigationStack {
List {
ForEach(items) { item in
NavigationLink(value: item) {
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
}
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
}
Решение NavigationStack устранило мою проблему. Есть ли причина, по которой вы решили получить его на основе идентификатора вместо прямой передачи элемента?
@Ezekiel В версии NavigationSplitView
ForEach(item)
использует свойство id
для тегов строк списка, и выбор List
основан на этом, так что это более естественно. Для NavigationStack
действительно незачем это делать - просто копипастить удобнее :)
Я не уверен, есть ли недостаток производительности, но вы всегда можете просто добавить свою собственную вычисляемую переменную, которая принимает все нефильтрованные результаты, а затем самостоятельно отфильтровать ее вручную.