I'm passing an object of a coredata model(ItemModel) using enums like this
.sheet(item: $activeSheet, onDismiss: { self.activeSheet = nil }) { item in
switch item {
case .newDoc:
NewDocumentView(ref: nil).environment(\.managedObjectContext, context)
case .detailFolder(let data):
DetailedFolderView(item: data, dirName: data.wrappedName, updater: viewUpdater).environment(\.managedObjectContext, context)
case .detailDoc(let file):
DetailDocumentView(item: file, fileName: file.wrappedItemName)
.environment(\.managedObjectContext, context)
}
}
but while i try to update the ItemModel object in the DetailDocumentView using the managed object context after saving and dismissing the view the parent view isnt updated, the view updates only after restarting the app
Can anyone suggest a way to update the parent view after updating the object of the coredata model(ItemModel) being passed in DetailDocumentView. Any suggestions are welcome :)
Been stuck on this issue for hours, tried using observable objects, tried using a publisher when context is updated, but noting has worked so far :(
Heres a reference of the code used when tapped on a file to present DetailDocumentView
/// creates a grid to display data
/// - Parameters:
/// - geometry: setup dimension for the grid view
/// - folders: files required
/// - Returns: returns a grid
private func gridView(_ geometry: GeometryProxy, files: FetchedResults<ItemModel>) -> some View {
QGrid(files,
columns: 2,
columnsInLandscape: 0,
vSpacing: 20,
hSpacing: 35,
vPadding: 10,
hPadding: 10,
isScrollable: false,
showScrollIndicators: false) { file in
FileItemView(item: file)
.environment(\.managedObjectContext, context)
.onTapGesture {
self.activeSheet = .detailDoc(file: file)
}
}
}
The enum case I used to present multiple sheet is below
/// for presenting sheets
enum HomeViewActiveSheet: Any {
case newDoc
case detailFolder(folder: FolderModel)
case detailDoc(file: ItemModel)
}
// so that it can be used in the .sheet init
extension HomeViewActiveSheet: Identifiable {
var id: ObjectIdentifier {
ObjectIdentifier(Self.self)
}
}
And I declare it in the parent class as this:
#State private var activeSheet: HomeViewActiveSheet? = .none
Related
I'd like to have a "Clear all" button into this "Classroom" view but I really don't know how to proceed.
First, here're my settings below:
// ClassroomView.swift
import SwiftUI
struct ClassroomView: View {
#Environment(\.managedObjectContext) private var viewContext
var classroom: FetchedResults<Classroom>.Element
var body: some View {
ScrollView {
VStack {
// Some Code Here
Button("classroom-button-clear", action: { /* What to write here? */ })
// Some Code Here
ForEach(classroom.studentArray) { student in
StudentView(student: student)
Divider()
}
// Some Code Here
}
}
}
}
struct StudentView: View {
var student: FetchedResults<Student>.Element
var body: some View {
HStack {
Text(student.date!)
Spacer()
Text(student.name!)
}
}
}
extension Classroom {
public var studentArray: [Student] {
let set = student as? Set<Student> ?? []
return set.sorted {
$0.date! > $1.date!
}
}
}
// ClassroomModel.xcdatamodeld
// Entity 1: Classroom (id, date, title, etc...)
// Relation: student - Student - classroom (To Many)
// Entity 2: Student (id, date, name, etc...)
// Relation: classroom - Classroom - student (To One)
If it was a simple delete button, it could be something like this inside the ForEach(classroom.studentArray):
// StudentView.swift
Button("student-button-clear", action: {
CoreDataManager.shared.deleteStudent(student: student, context: viewContext)
})
// CoreDataManager.swift
func deleteStudent(student: student, context: NSManagedObjectContext) {
context.delete(student)
save(context: context)
}
But I don't know how to make it works outside the ForEach and also make sure every elements are deleted and not just one!
Maybe, I should place context.delete(student) inside a ForEach too but this doesn't feel right.
How do you guys do that? Thanks.
You can write a new function inside the CoreDataManager file that receives a Classroom and, iterating through all of its Students, deletes each one of them.
Like this:
func deleteStudentsForClassroom(_ classroom: Classroom, context: NSManagedObjectContext) {
classroom.studentArray.forEach {
context.delete($0)
}
save(context: context)
}
You can then call this function to erase all students of the classroom from CoreData.
Using SwiftUI, CoreData and #FetchRequest wrapper, I simply do not get any view updates on cahnegs on related entities. I set up the following simple and working example:
The CoreData model looks as follows:
GroupEntity -hasMany[items]-> ItemEntity
ItemEntity -hasOne[group]-> GroupEntity
I added two extensions to the CoreData autogenerated Classes to get around the nasty optionals here. The button at the bottom creates a group with three items in it. They're named equally but are truely different due to their unique id. The view simply lists a group and all its related items and their according value below. Tapping an item increases it's value by one. Tapping the "change GroupName" button at the bottom of each group section changes the group's name. There is no additional code than the following:
import SwiftUI
extension ItemEntity {
var wName: String { name ?? "nameless item"}
}
extension GroupEntity {
var wName: String { name ?? "nameless group" }
var aItems: [ItemEntity] {
let set = items as? Set<ItemEntity> ?? []
return set.sorted {
$0.wName < $1.wName
}
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: GroupEntity.entity(), sortDescriptors: []) var groups: FetchedResults<GroupEntity>
var body: some View {
List {
ForEach(groups, id: \.self) { group in
Section(header: Text("\(group.wName) >> \(group.aItems.reduce(0, {$0 + $1.value}))")) {
ForEach(group.aItems, id: \.id) { (item: ItemEntity) in
Text("\(item.wName) >> \(item.value)")
.onTapGesture {
item.value = item.value + 1
print("item value: \(item.value)")
// -> value changed & stored, but view IS NOT UPDATED
try? moc.save()
}
}
Button(action: {
group.name = group.wName + " [changed]"
// -> value changed,stored and view IS UPDATED
try? moc.save()
}) {
Text("change \(group.wName)")
}
}
}
Button(action: addGroupItems) {
Text("Add Group with Items")
}
}
}
func addGroupItems() {
let group = GroupEntity(context: moc)
group.id = UUID()
group.name = "G1"
let item1 = ItemEntity(context: moc)
item1.id = UUID()
item1.name = "I1"
item1.value = 1
group.addToItems(item1)
let item2 = ItemEntity(context: moc)
item2.id = UUID()
item2.name = "I2"
item2.value = 2
group.addToItems(item2)
let item3 = ItemEntity(context: moc)
item3.id = UUID()
item3.name = "I3"
item3.value = 3
group.addToItems(item3)
// save
try? moc.save()
}
}
Whenever the Group's name is changed, the View automatically refreshes and makes the change visible. But when tapping an item, it's value increases (as the print statement proves) but the view is not updated. Please note the additional requirement, that the group's section header also includes a computed sum of all it's item's values. This schould also be updated when an item changes. That's why simply exporting the Item into an separated itemRowView(item: item) and declaring item as #ObservedObject var item: ItemEntity in there is not the solution here.
I assume, that the #FetchRequest wrapper somehow ignores all changes to related items. But I cannot belief that it's that useless for related data because that's CoreDatas main power?
Thank you all for any helpful comment and idea!
Just by idea - try the following
.onTapGesture {
item.value = item.value + 1
print("item value: \(item.value)")
// -> value changed & stored, but view IS NOT UPDATED
try? moc.save()
group.objectWillChange.send() // << here !!
}
Okay so I found a solution that worked for me. Adding a publisher watching for changes in the managedObjectContext and updating whenever a change isd etected does the trick.
struct ContentView: View {
private var mocDidChanged = NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange)
/* ... */
var body: some View {
List { /* .. */ }.onReceive(self.mocDidChanged) { _ in
self.refresh()
}
}
func refresh() {
/* update related state variables here */
}
}
I am trying to pass data to a new view in my app, so that my script can plug the data into the link and fetch the respective article information. My view is being called with ArticleView(articleID: 0) though this changes with the ID of the article. The beginning of my view begins as such...
struct ArticleView: View {
var articleID: Int
#Envionment(.\imageCache) var cache: ImageCache
#ObservedObject private var data = Result2()
let defaults = UserDefaults.standard
...
init() {
self.data.results.append(Story.init(id: 0, title: "test", image: "", story: "", published: "", author: ""))
self.loadArticle(CDNLink: "http://\(self.defaults.object(forKey: "domain") as! String)/cdn?funct=fetchArticle&articleID=\(self.articleID)")
}
What I am trying to understand is how do I get the article id from my dashboard to being stored in var articleID so my loadArticle function can then run the script and pull the content in? I can provide the information for Result2() along with the data structures for Response2 and Story upon request. Thanks!
You have 2 options:
To ask for a parameter from initializer:
struct ArticleView: View {
var articleID: Int
...
init(articleID: Int) {
self.articleID = articleID
// Do whatever you need
}
var body: some View {
SomeView()
}
}
Omit initializer and put all you need in .onAppear clause
struct ArticleView: View {
var articleID: Int
...
var body: some View {
SomeView()
.onApppear {
// Do whatever you need
}
}
}
In second case the compiler will create an initializer automatically with all values needed (in your case - articleID)
I have an array of objects ("things") of some third party library that I want to display in a SwiftUI View.
These "Thing" objects are Identifiable and Hashable by an id, but when reloading a new set of Things, their content may have changed (let's say a "status" or a "text" of that Thing, although it is the same Thing again). So the id keeps the same, but the content of a Thing can change.
Problem is, that SwiftUI doesn't update the UI when I get a new array of Things. I assume this is because the Things are "identified" as the same Things again by their id.
I can not change Thing, because it is from a third party library.
Now I simply wrapped Thing into another class and suddenly it works! But I want to understand why this works, and if it is defined behaviour and not just coincidence or "luck".
Can anybody explain what happens here behind the scene? Especially what is the main difference between DirectThingView and WrappedThingView that causes SwiftUI to update the UI for the latter and not for the former?
Or are the any suggestions how to solve this problem in a better way?
Here is sample code that shows everything:
It displays the Things in two columns; first column uses the DirectThingView and second column uses the WrappedThingView. If you hit the "Reload" button, the things array gets filled with changed Things, but only the UI of the right column correctly updates the values; the left column always stays at its initial state.
//
// TestView.swift
//
// Created by Manfred Schwind on 10.07.20.
// Copyright © 2020 mani.de. All rights reserved.
//
import SwiftUI
// The main model contains an array of "Things",
// every Thing has an id and contains a text.
// For testing purposes, every other time a Thing gets instantiated, its text contains either "A" or "B".
// Problem here: the "current" text of a Thing with the same id can change, when Things are reloaded.
class TestViewModel: ObservableObject {
#Published var things = [Thing(id: 1), Thing(id: 2), Thing(id: 3)]
}
struct TestView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
VStack (spacing: 30) {
HStack (spacing: 40) {
// We try to display the current Thing array in the UI
// The views in the first column directly store the Thing:
// Problem here: the UI does not update for changed Things ...
VStack {
Text("Direct")
ForEach(self.viewModel.things, id: \.self) { thing in
DirectThingView(viewModel: thing)
}
}
// The views in the second column store the Thin wrapped into another class:
// In this case, the problem magically went away!
VStack {
Text("Wrapped")
ForEach(self.viewModel.things, id: \.self) { thing in
WrappedThingView(viewModel: thing)
}
}
}
Button(action: {
// change the Thing array in the TestViewModel, this causes the UI to update:
self.viewModel.things = [Thing(id: 1), Thing(id: 2), Thing(id: 3)]
}) {
Text("Reload")
}
}
}
}
struct DirectThingView: View {
// first approach just stores the passed Thing directly internally:
private let viewModel: Thing
init(viewModel: Thing) {
self.viewModel = viewModel
}
var body: some View {
Text(self.viewModel.text)
}
}
struct WrappedThingView: View {
// second approach stores the passed Thing wrapped into another Object internally:
private let viewModel: WrappedThing
init(viewModel: Thing) {
// take the Thing like in the first approach, but internally store it wrapped:
self.viewModel = WrappedThing(childModel: viewModel)
}
var body: some View {
Text(self.viewModel.childModel.text)
}
// If type of WrappedThing is changed from class to struct, then the problem returns!
private class WrappedThing {
let childModel: Thing
init(childModel: Thing) {
self.childModel = childModel
}
}
}
// Thing has do be Identifiable and Hashable for ForEach to work properly:
class Thing: Identifiable, Hashable {
// Identifiable:
let id: Int
// The text contains either "A" or "B", in alternating order on every new Thing instantiation
var text: String
init(id: Int) {
self.id = id
struct Holder {
static var flip: Bool = false
}
self.text = Holder.flip ? "B" : "A"
Holder.flip = !Holder.flip
}
// Hashable:
public func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
// Equatable (part of Hashable):
public static func == (lhs: Thing, rhs: Thing) -> Bool {
return lhs.id == rhs.id
}
}
#if DEBUG
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
#endif
Thank you very much in advance!
I found the answer for myself. The problem is the implementation of Equatable of Thing here. As implemented above, an old and a new version of the same Thing (but with different content) are considered equal. But SwiftUI distincts between "identity" and "equality" and this has to be properly implemented.
In the code above, Identifiable and Hashable are OK, but Equatable has to be changed to be more precise. So e.g. this fixes the problem:
// Thing has do be Identifiable and Hashable for ForEach to work properly:
class Thing: Identifiable, Hashable {
// Identifiable:
let id: Int
// The text contains either "A" or "B", in alternating order on every new Thing instantiation
var text: String
init(id: Int) {
self.id = id
struct Holder {
static var flip: Bool = false
}
self.text = Holder.flip ? "B" : "A"
Holder.flip = !Holder.flip
}
// Hashable:
public func hash(into hasher: inout Hasher) {
// we are lazy and just use the id here, "collisions" are then separated by func ==
hasher.combine(self.id)
}
// Equatable (part of Hashable):
public static func == (lhs: Thing, rhs: Thing) -> Bool {
// We are lazy again (in reality Thing has many properties) and we consider
// two Things to be the equal ONLY when they point to the same address.
// So we get the "same but different" semantic that we want, when we are
// getting a new version of the Thing.
// (Same in the sense of identity, different in the sense of equality)
return lhs === rhs
}
}
I'm trying to remove the logic from the view, while keeping the benefits of SwiftUI. Idea 1 works but it makes use of an extra variable than I would want to. Idea 2 gives error: Property wrappers are not yet supported on local properties. The view should return "bar". What is the best way of making this work? Many thanks.
import Combine
import Foundation
import SwiftUI
// Model
enum Model: String, RawRepresentable {
case foo = "foo"
case bar = "bar"
}
// State
var data1: String = Model.foo.rawValue
class State: ObservableObject {
#Published internal var data2: String = data1
}
// Logic
func logic() {
// Idea 1: OK
//data1 = Model.bar.rawValue
//print(State().data2)
// Idea 2: Error Property wrappers are not yet supported on local properties
#EnvironmentObject private var state: State
state.data2 = Model.bar.rawValue
print(state.data2)
}
// View
struct bar: View {
#EnvironmentObject private var state: State
internal var body: some View {
logic()
return Text(verbatim: self.state.data2)
}
}
If you want a function to have access to a view's state, pass the state:
func logic(state: State) {
state.data2 = Model.bar.rawValue
print(state.data2)
}
But what you've done here is an infinite loop. Modifying a view's state causes the view to be re-rendered. So every time the view is rendered, it modifies its state and forces it to be rendered again. That will never resolve. What you may mean here is to change the state when the view first appears, in which case you'd call logic this way:
struct Bar: View {
#EnvironmentObject private var state: State
internal var body: some View {
Text(verbatim: state.data2)
.onAppear{ logic(state: self.state) }
}
}