#core-data #swiftui #picker
Вопрос:
Всем доброго дня,
Я пытаюсь понять, как отношения CoreData могут работать с элементами пользовательского интерфейса, такими как сборщики.
На данный момент у меня есть приложение для просмотра 3 (на основе стандартного кода Xcode), которое отображает список родительских объектов, у которых есть дети, у которых есть дети. Я хочу, чтобы средство выбора выбирало, на какого внука должна ссылаться дочерняя сущность.
На данный момент у меня есть два забавных побочных эффекта:
- Когда я запускаю приложение в качестве предварительного просмотра (так что есть предварительно заполненные данные… этот пример кода сломается без данных на месте),
- выбранный внук в окне выбора-это внук первого ребенка, независимо от того, в какого ребенка вы попали в первом представлении.
- Когда я возвращаюсь и выбираю другого ребенка, теперь выбранный захватывает правильный начальный выбор из дочерней сущности
- Когда я выбираю дочерний элемент и «сохраняю» его, значение в сводке дочерних элементов не меняется до тех пор, пока я не нажму на другого дочернего элемента, и в этот момент значение изменится до перехода к модальному представлению.
Мне явно чего-то не хватает в моем понимании последовательности событий при представлении модалов в SwiftUI… может ли что-нибудь пролить свет на то, что я сделал не так?
Вот видео, чтобы сделать это более понятным: https://github.com/andrewjdavison/Test31/blob/main/Test31 — first click issue.mov?raw=true
Репозиторий Git с образцом находится https://github.com/andrewjdavison/Test31.git, но вкратце:
Модель данных:
Посмотреть Источник:
import SwiftUI
import CoreData
struct LicenceView : View {
@Environment(.managedObjectContext) private var viewContext
@Binding var licence: Licence
@Binding var showModal: Bool
@State var selectedElement: Element
@FetchRequest private var elements: FetchedResults<Element>
init(currentLicence: Binding<Licence>, showModal: Binding<Bool>, context: NSManagedObjectContext) {
self._licence = currentLicence
self._showModal = showModal
let fetchRequest: NSFetchRequest<Element> = Element.fetchRequest()
fetchRequest.sortDescriptors = []
self._elements = FetchRequest(fetchRequest: fetchRequest)
_selectedElement = State(initialValue: currentLicence.wrappedValue.licenced!)
}
func save() {
licence.licenced = selectedElement
try! viewContext.save()
showModal = false
}
var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $selectedElement, label: Text("Element")) {
ForEach(elements, id: .self) { element in
Text("(element.desc!)")
}
}
Text("Selected: (selectedElement.desc!)")
Button(action: {save()}) {
Text("Save")
}
}
}
}
struct RegisterView : View {
@Environment(.managedObjectContext) private var viewContext
@State var showModal: Bool = false
var currentRegister: Register
@State var currentLicence: Licence
init(currentRegister: Register) {
currentLicence = Array(currentRegister.licencedUsers! as! Set<Licence>)[0]
self.currentRegister = currentRegister
}
var body: some View {
VStack {
List {
ForEach (Array(currentRegister.licencedUsers! as! Set<Licence>), id: .self) { licence in
Button(action: {currentLicence = licence; showModal = true}) {
HStack {
Text("(licence.leasee!) : ")
Text("(licence.licenced!.desc!)")
}
}
}
}
}
.sheet(isPresented: $showModal) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}
}
}
struct ContentView: View {
@Environment(.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: Register.id, ascending: true)],
animation: .default)
private var registers: FetchedResults<Register>
var body: some View {
NavigationView {
List {
ForEach(registers) { register in
NavigationLink(destination: RegisterView(currentRegister: register)) {
Text("Register id (register.id!)")
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
[1]: https://i.stack.imgur.com/AfaNb.png
Комментарии:
1. Вероятно, потому, что вы не наблюдаете изменений в объектах
ObservableObjects
/ CoreData. Не используйте@Binding
или@State
ObservableObjects
используйте@ObservedObject
вместо этого объекты / CoreData. Это также позволит вам избавиться от макияжа@FetchRequest
вLicenceView
иselectedElement
потому, что вы можете использовать$licence.licenced
его вместо этого. Как правило, SwiftUI плохо работает с пользовательскимиinit
настройками, если вы обнаружите, что выполняете всю эту работу на заказinit
, вам нужно пересмотреть свой подход. SwiftUI перезагружаетсяView
, когда захочет, и ваши переменные будут сброшены.2. Кроме того, в
RegisterView
вас, вероятно, лучше использоватьsheet(item:)
и@State var currentLicence: Licence?
. Зная, что вы не увидите никаких изменений с этой переменной, потому что она не наблюдает, ее единственная цель-вызватьsheet
. Если вы хотите увидеть изменения, которые вы должны использовать@ObservedObject
в подвиде3. У вашего сборщика отсутствует метка. Выбор не знает, что есть изменения
4. Скорее всего, вещь типа попробуйте изменить свой тег на
.tag(element as Element?)
и, конечноlicence.licenced
, должна бытьElement
5.Вы также должны изучить, что это
@ObservedObject
не должно быть инициализировано в том жеView
месте, в котором оно находится. Это должно исходить из предыдущегоView
. У вас будет утечка и некоторая нестабильность, если вы сделаете это наinit
Ответ №1:
Я действительно не понимал этого
• selected grandchild in the picker is the grandchild of the first child, irrespective of which child you're dropped into in the first view.
• When I drop back and pick another child, now the picked grabs the correct initial selection from the child entity
Не могли бы вы прикрепить видео, представляющее проблему?
Но я могу дать вам решение проблемы предварительного просмотра и второй проблемы.
Предварительный просмотр
Если вы используете предварительный просмотр с основными данными, вам необходимо использовать viewContext
файл, созданный с помощью MockData, и передать его в свое представление. Здесь я предоставляю общий код, который может быть изменен для каждого из ваших представлений:
В вашей Persistance
структуре (менеджер CoreData) объявите предварительный просмотр переменной с вашими элементами предварительного просмотра:
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
// Here you create your Mock Data
let newItem = Item(context: viewContext)
newItem.yourProperty = yourValue
do {
try viewContext.save()
} catch {
// error handling
}
return result
}()
Убедитесь, что у inMemory: Bool
него есть инициализация, так как он отвечает за разделение реального viewContext и previewContext:
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error (error), (error.userInfo)")
}
})
}
Создайте макет элемента из вашего viewContext и передайте его для предварительного просмотра:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
let context = PersistenceController.preview.container.viewContext
let request: NSFetchRequest<Item> = Item.fetchRequest()
let fetchedItems = try! context.fetch(request)
YourView(item: fetchedItems)
}
}
Если вы используете @FetchRequest
и @FetchedResults
это облегчает задачу, так как они будут создавать и извлекать объекты для вас. Просто выполните предварительный просмотр, как это:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
YourView().environment(.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
Вот структура сохранения, созданная Xcode в момент инициализации проекта:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let item = Item(context: viewContext)
item.property = yourProperty
do {
try viewContext.save()
} catch {
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error (error), (error.userInfo)")
}
})
}
}
Вторая проблема
Основные объекты данных создаются с помощью классов, поэтому их тип является ссылочным. Когда вы изменяете свойство класса, оно не уведомляет структуру представления о необходимости перерисовки с новым значением. (исключение составляют классы, которые создаются для уведомления об изменениях.)
Вам нужно явно указать вашей RegisterView
структуре перерисовать себя после того, как вы откажетесь от своей LicenceView
. Вы можете сделать это, создав еще одну переменную в своем RegisterView
— @State var id = UUID()
. Затем прикрепите .id(id)
модификатор в конце вашего VStack
VStack {
//your code
}.id(id)
Наконец, создайте функцию viewDismissed
, которая изменит id
свойство в вашей структуре:
func viewDismissed() {
id = UUID()
}
Теперь прикрепите эту функцию к своему листу с дополнительным параметром onDismiss
.sheet(isPresented: $showModal, onDismiss: viewDismissed) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}
Комментарии:
1. Спасибо — второй отлично справился с обновлением элементов. В ближайшее время я добавлю видео по другой проблеме.
2. Видеосвязь есть github.com/andrewjdavison/Test31/blob/main/…
3. Использование трюка с идентификатором неэффективно. Объекты CoreData-это наблюдаемые объекты, которые Apple предоставила для наблюдения за изменениями. Вам следует посмотреть Демистификацию SwiftUI с WWDC21, вместо целевого обновления вы просто перезагружаете все это
4. Спасибо, Лорем… согласен. Работаем над тем, чтобы все было сведено к наблюдаемым объектам для CoreData…
Ответ №2:
ОК. Огромное спасибо Лорему за то, что он привел меня к ответу. Спасибо также за Рома, но оказывается, что его решение, хотя и помогло решить одну из моих ключевых проблем, действительно приводит к неэффективности — и не решило вторую.
Если другие сталкиваются с той же проблемой, я оставлю репозиторий Github открытым, но суть всего этого заключалась в том, что @State не следует использовать, когда вы делитесь объектами CoreData. @ObservedObject-это правильный путь сюда.
Таким образом, решение проблем, с которыми я столкнулся, было:
- Используйте @ObservedObject вместо @State для передачи объектов CoreData
- Убедитесь, что у средства выбора определен тег. В документации, которую я прочитал, подразумевалось, что это генерируется автоматически, если вы используете «.self» в качестве идентификатора для объектов в каждом, но, похоже, это не всегда надежно. поэтому добавление «.tag(элемент как элемент?)» в мой выбор помогло здесь. Примечание: Это должен быть необязательный тип, потому что CoreData делает необязательными все типы атрибутов.
Эти двое в одиночку решили проблемы.
Пересмотренная структура «LicenceView» находится здесь, но все решение заключается в репо.
Ваше здоровье!
struct LicenceView : View {
@Environment(.managedObjectContext) private var viewContext
@ObservedObject var licence: Licence
@Binding var showModal: Bool
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: Element.desc, ascending: true)],
animation: .default)
private var elements: FetchedResults<Element>
func save() {
try! viewContext.save()
showModal = false
}
var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $licence.licenced, label: Text("Element")) {
ForEach(elements, id: .self) { element in
Text("(element.desc!)")
.tag(element as Element?)
}
}
Text("Selected: (licence.licenced!.desc!)")
Button(action: {save()}) {
Text("Save")
}
}
}
}