Как использовать средство выбора для отношений CoreData в SwiftUI

#core-data #swiftui #picker

Вопрос:

Всем доброго дня,

Я пытаюсь понять, как отношения CoreData могут работать с элементами пользовательского интерфейса, такими как сборщики.

На данный момент у меня есть приложение для просмотра 3 (на основе стандартного кода Xcode), которое отображает список родительских объектов, у которых есть дети, у которых есть дети. Я хочу, чтобы средство выбора выбирало, на какого внука должна ссылаться дочерняя сущность.

На данный момент у меня есть два забавных побочных эффекта:

  1. Когда я запускаю приложение в качестве предварительного просмотра (так что есть предварительно заполненные данные… этот пример кода сломается без данных на месте),
  • выбранный внук в окне выбора-это внук первого ребенка, независимо от того, в какого ребенка вы попали в первом представлении.
  • Когда я возвращаюсь и выбираю другого ребенка, теперь выбранный захватывает правильный начальный выбор из дочерней сущности
  1. Когда я выбираю дочерний элемент и «сохраняю» его, значение в сводке дочерних элементов не меняется до тех пор, пока я не нажму на другого дочернего элемента, и в этот момент значение изменится до перехода к модальному представлению.

Мне явно чего-то не хватает в моем понимании последовательности событий при представлении модалов в 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-это правильный путь сюда.

Таким образом, решение проблем, с которыми я столкнулся, было:

  1. Используйте @ObservedObject вместо @State для передачи объектов CoreData
  2. Убедитесь, что у средства выбора определен тег. В документации, которую я прочитал, подразумевалось, что это генерируется автоматически, если вы используете «.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")
            }
        }
        
    }
}