SwiftUI: How to get notified when a field in a singleton object get changed? - swift

class SharedData: ObservableObject {
static let shared = SharedData()
#Published var sharedState = SharedState()
}
struct SharedState {
var allMMS: [MMS] = []
var typeTrees: [TTMaker] = []
var sampleInputs: [String: String] = [:]
var selectedTypeTreeName: String?
var selectedMMSPathName: String?
var maps: [String: FunctionalMap] = [:]
var mapId: String?
var selectedMenuItem: String? = nil
}
struct ContentView: View {
#ObservedObject var sharedData = SharedData.shared
set a file / string on the view:
SharedData.shared.sharedState.typeTrees.append(ttMaker)
I would expect the List in the same would get updated:
List {
ForEach(SharedData.shared.sharedState.typeTrees, id: \.self) { tree in
Button(action: {
SharedData.shared.sharedState.selectedTypeTreeName = tree.newTree.filename
}) {
HStack {
Text(tree.newTree.filename)
Spacer()
if tree.newTree.filename == SharedData.shared.sharedState.selectedTypeTreeName {
Image(systemName: "checkmark")
}
}
}
}
}
Is it any similar oslution than context in React?

Related

Save objects in Realm only when Save button is pressed

I'm trying to create a really simple app in SwifUI + Realm but I cannot understand how can I make it work. Basically I have a list of objects. When I click on a row it will open a sheet to edit the underlying object, if i press "+" it will open same sheet to create a new object.
In the sheet object will be save only if "Save" button is pressed.
Here is my model
class PayableEntityType: Object {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name: String
#Persisted var iconName: String
#Persisted var mainColor: String
#Persisted var secondaryColor: String
#Persisted var _partitionValue: String = AppInfo.partitionValue
convenience init(name: String, mainColor: String, secondaryColor: String, iconName: String) {
self.init()
self.name = name
self.mainColor = mainColor
self.secondaryColor = secondaryColor
self.iconName = iconName
}
}
This is my list view
struct PayableEntityTypesListScreen: View {
#Environment(\.presentationMode) private var presentationMode
#ObservedResults(PayableEntityType.self) private var payableEntityTypes
#State var selectedPayableEntityType: PayableEntityType
#State var forEdit: PayableEntityType?
#State var openForSelect: Bool = false
#State private var showEdit: Bool = false
#State private var isNew: Bool = false
var body: some View {
NavigationView {
List {
ForEach(self.payableEntityTypes.sorted(byKeyPath: "name")) { payableEntityType in
Text(payableEntityType.name).onTapGesture {
self.rowTap(payableEntityType)
}
}
}.padding(.top, 1)
}.navigationTitle("Entity type")
.toolbar {
if !openForSelect {
self.newPayableEntityTypeButton()
}
}
}
private func rowTap(_ payableEntityType: PayableEntityType) {
self.selectedPayableEntityType = payableEntityType
if self.openForSelect {
self.presentationMode.wrappedValue.dismiss()
} else {
self.isNew = false
self.showEdit = true
}
}
#ToolbarContentBuilder
private func newPayableEntityTypeButton() -> some ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) {
AddButton(action: {
self.isNew = true
self.showEdit = true
}).sheet(isPresented: self.$showEdit) {
EditPayableEntityTypeScreen(payableEntityType: self.isNew ? PayableEntityType() : self.selectedPayableEntityType)
}
}
}
}
and this is the edit\create view
struct EditPayableEntityTypeScreen: View {
#Environment(\.realm) var realm
#Environment(\.presentationMode) private var presentationMode
#ObservedRealmObject var payableEntityType: PayableEntityType
#State private var errorMessage: String = ""
#State private var showingAlert: Bool = false
#State private var name: String = ""
#State private var iconName: String = ""
#State private var selectedMainColor: Color = .green
#State private var selectedSecondaryColor: Color = .black
private var isUpdating: Bool {
self.payableEntityType.realm != nil
}
init(payableEntityType: PayableEntityType) {
self.payableEntityType = payableEntityType
if self.isUpdating {
self._name = .init(initialValue: self.payableEntityType.name)
self._iconName = .init(initialValue: self.payableEntityType.iconName)
self._selectedMainColor = State<Color>.init(initialValue: Color(hex: self.payableEntityType.mainColor))
self._selectedSecondaryColor = State<Color>.init(initialValue: Color(hex: self.payableEntityType.secondaryColor))
}
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Info")) {
TextField("name", text: self.$name)
.disableAutocorrection(true)
}
Button(action: {
self.save()
}, label: {
Text("SAVE")
}).alert(isPresented: $showingAlert) {
Alert(title: Text("Warning"), message: Text(self.errorMessage), dismissButton: .default(Text("OK")))
}
}
}
}
private func save() {
if self.name.isEmpty {
self.errorMessage = "Missing name 😅"
self.showingAlert = true
return
}
self.errorMessage = ""
do {
if self.isUpdating {
try realm.write {
self.$payableEntityType.name.wrappedValue = self.name
self.$payableEntityType.iconName.wrappedValue = self.iconName
self.$payableEntityType.mainColor.wrappedValue = self.selectedMainColor.toHex()
self.$payableEntityType.secondaryColor.wrappedValue = self.selectedSecondaryColor.toHex()
}
} else {
self.payableEntityType.name = self.name
self.payableEntityType.iconName = self.iconName
self.payableEntityType.mainColor = self.selectedMainColor.toHex()
self.payableEntityType.secondaryColor = self.selectedSecondaryColor.toHex()
try realm.write {
realm.add(self.payableEntityType)
}
}
self.presentationMode.wrappedValue.dismiss()
} catch {
self.errorMessage = error.localizedDescription
self.showingAlert = true
}
}
}
This code works, but I think is not the right way to use Realm property wrappers.
Note: the PayableEntityTypesListScreen has a openForSelect variable because I want to reuse that view to only select one of list elements and the close it.

Cannot convert value of type 'Binding<[ContactEntity]>.Element' (aka 'Binding<ContactEntity>') to expected argument type 'ContactEntity'

Using Xcode 13.4.1 on macOS 12.5. I revised the working code to conform to MVVM. This was successful for the first Entity (all properties Optional) for all CRUD operations.
Using this code as a base, I tackled the second Entity (one Bool property NOT Optional), but it throws the compiler error inside the ForEach loop, against 'contact'. This code was error-free before the MVVM conversion. I've been at this for 4 days and am reaching out, but clearly my limited knowledge is inadequate.
ContactListView code below, supported by the ContactViewModel, which in turn relies on the CoreDataManager code.
import SwiftUI
import CoreData
//class FirstNameSort: ObservableObject {
// #Published var firstNameSort: Bool = false
//}
struct ContactsListView: View {
// MARK: - PROPERTIES
#Environment(\.managedObjectContext) var viewContext
#ObservedObject var contactVM = ContactViewModel()
#State private var totalContacts: Int = 0
#State private var search: String = ""
#State private var searchByChampions = false
#State var searchByFirstNames = false
#State private var totalChampions = 0
// MARK: - BODY
var body: some View {
NavigationView {
VStack {
// HStack {
// Toggle("Display Champions only", isOn: $searchByChampions)
// .toggleStyle(.switch)
// .foregroundColor(.blue)
// .padding()
// Toggle("Sort by First Names", isOn: $contactVM.sortFirstName)
// .toggleStyle(.switch)
// .foregroundColor(.blue)
// .padding()
//}
List {
HStack {
Text(searchByChampions ? "Total Champions" : "Total Contacts")
.foregroundColor(.gray)
Spacer()
Text("\(searchByChampions ? totalChampions : totalContacts)")
.bold()
}.foregroundColor(.green)
.padding()
ForEach($contactVM.listofContacts) { contact in
NavigationLink(destination:
ModifyContactView(contact: ***contact***)
.id(UUID()), label: {
ContactRowView(contact: ***contact***)
.id(UUID())
})
}
.onDelete(perform: contactVM.deleteContact)
}.navigationTitle("Contacts")
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: AddContactView(), label: {
Image(systemName: "plus.circle")
})
}
}
.onAppear {
countContacts()
countChampions()
}
.searchable(text: $search, prompt: Text("Contact Last Name?"))
// .onChange(of: search) { value in
// if !value.isEmpty {
// listofContacts.nsPredicate = NSPredicate(format: "contactLastName CONTAINS[dc] %#", value)
// } else {
// listofContacts.nsPredicate = nil
// }
// }
}
}.navigationViewStyle(.stack)
}
func countContacts() {
totalContacts = $contactVM.listofContacts.count
}
// func countChampions() {
// totalChampions = $contactVM.listOfChampions.count
// }
}
import CoreData
import SwiftUI
class ContactViewModel: ObservableObject {
#Environment(\.dismiss) var dismiss
#ObservedObject var dataVM = CoreDataManager()
#ObservedObject var qualifierVM = QualifierViewModel()
#Published var inputFirstName: String = ""
#Published var inputLastName: String = ""
#Published var inputCellNumber: String = ""
#Published var inputEmail: String = ""
#Published var inputChampion: Bool = false
#Published var inputComments: String = ""
#Published var inputCreated: Date = Date()
#Published var inputUpdated: Date = Date()
#Published var listOfFirstNames = []
#Published var listofContacts: [ContactEntity] = []
func fetchContacts() {
let request = NSFetchRequest<ContactEntity>(entityName: "ContactEntity")
do {
dataVM.listofContacts = try dataVM.container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addContact(
contactFirstName: String,
contactLastName: String,
contactCellNumber: String,
contactEmail: String,
contactChampion: Bool,
contactComments: String,
contactCreated: Date,
contactUpdated: Date) {
let newContact = ContactEntity(context: dataVM.container.viewContext)
newContact.contactFirstName = contactFirstName
newContact.contactLastName = contactLastName
newContact.contactCellNumber = contactCellNumber
newContact.contactEmail = contactEmail
newContact.contactChampion = contactChampion
newContact.contactComments = contactComments
newContact.contactUpdated = Date()
newContact.contactCreated = Date()
let uniqueClient = Set(dataVM.selectedClient)
for client in uniqueClient {
newContact.addToClients(client)
print("Client: \(client.clientName ?? "No client")")
}
saveContact()
dismiss()
}
func deleteContact(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = dataVM.listofContacts[index]
dataVM.container.viewContext.delete(entity)
saveContact()
}
func saveContact() {
do {
try dataVM.container.viewContext.save()
fetchContacts()
} catch let error {
print("Error saving. \(error)")
}
}
func sortLastName() -> [ Array<Any>] {
let listOfLastNames = dataVM.listofContacts.sorted {
$0.contactLastName ?? "" < $1.contactLastName ?? ""
}
return [listOfLastNames]
}
func sortFirstName() -> [ Array<Any>] {
let listOfFirstNames = dataVM.listofContacts.sorted {
$0.contactFirstName ?? "" < $1.contactFirstName ?? ""
}
return [listOfFirstNames]
}
}
import Foundation
import CoreData
class CoreDataManager: ObservableObject {
let container: NSPersistentContainer
#Published var listOfQualifiers: [QQEntity] = []
#Published var listofContacts: [ContactEntity] = []
#Published var listOfClients: [ClientEntity] = []
#Published var listOfOpportunities: [OpportunityEntity] = []
//#Published var selectedClient: [ClientEntity] = []
init() {
container = NSPersistentContainer(name: "B2BContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error loading Core Data. \(error)")
} else {
print("Successfully loaded Core Data...")
}
}
}
}

Initializers with different stored properties

I have the following view where I pass a binding to an item that I need to be selected.
struct SelectionListView<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
private let data: Data
#Binding private var isPresented: Bool
#Binding private var selectedElement: Data.Element
init(
data: Data,
selectedElement: Binding<Data.Element>,
isPresented: Binding<Bool>
) {
self.data = data
_selectedElement = selectedElement
_isPresented = isPresented
}
var body: some View {
VStack {
ForEach(data) { element in
Button(element.name) {
selectedElement = element
isPresented.toggle()
}
.foregroundColor(
selectedElement.id == item.id
? .black
: .white
)
}
}
}
}
I would need a slightly different initializer of this view where I can only pass the element ID, instead of the whole element. I'm having trouble achieving this solution. To make it even more clear, it would be great if I could have a second initializer such that:
init(
data: Data,
selectedId: Binding<Data.Element.ID>,
isPresented: Binding<Bool>
)
Here is a working version. I decided to store the element or id in their own enum cases. I made the view separate just so it is a little easier to understand what I did.
Working code:
struct SelectionListView<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
enum Selected {
case element(Binding<Data.Element>)
case id(Binding<Data.Element.ID>)
}
#Binding private var isPresented: Bool
private let data: Data
private let selected: Selected
init(
data: Data,
selectedElement: Binding<Data.Element>,
isPresented: Binding<Bool>
) {
self.data = data
selected = .element(selectedElement)
_isPresented = isPresented
}
init(
data: Data,
selectedId: Binding<Data.Element.ID>,
isPresented: Binding<Bool>
) {
self.data = data
selected = .id(selectedId)
_isPresented = isPresented
}
var body: some View {
SelectionListItem(data: data) { dataElement in
switch selected {
case .element(let element):
element.wrappedValue = dataElement
print("Selected element:", element.wrappedValue)
case .id(let id):
id.wrappedValue = dataElement.id
print("Selected element ID:", id.wrappedValue)
}
isPresented.toggle()
}
}
}
struct SelectionListItem<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
let data: Data
let action: (Data.Element) -> Void
var body: some View {
VStack {
ForEach(data) { element in
Button(element.name) {
action(element)
}
.foregroundColor(
.red // Temporary because I don't know what `item.id` is
// selectedElement.id == item.id
// ? .black
// : .white
)
}
}
}
}
Other code for minimal working example:
struct ContentView: View {
#State private var selection: StrItem
#State private var selectionId: StrItem.ID
#State private var isPresented = true
private let data: [StrItem]
init() {
data = [StrItem("Hello"), StrItem("world!")]
_selection = State(initialValue: data.first!)
_selectionId = State(initialValue: data.first!.id)
}
var body: some View {
// Comment these to try each initializer
//SelectionListView(data: data, selectedElement: $selection, isPresented: $isPresented)
SelectionListView(data: data, selectedId: $selectionId, isPresented: $isPresented)
}
}
protocol Named {
var name: String { get }
}
struct StrItem: Identifiable, Named {
let id = UUID()
let str: String
var name: String { id.uuidString }
init(_ str: String) {
self.str = str
}
}
I'm not really sure what you are trying to achieve. Something feels off :) But anyway, here's a variant of your code that would do what you want:
struct SelectionListView<Data>: View where Data: RandomAccessCollection, Data.Element: Identifiable, Data.Element: Named {
private let data: Data
#Binding private var isPresented: Bool
#Binding private var selectedElement: Data.Element
#Binding private var selectedId: Data.Element.ID
init(
data: Data,
selectedElement: Binding<Data.Element>,
isPresented: Binding<Bool>
) {
self.data = data
_selectedElement = selectedElement
_selectedId = .constant(selectedElement.wrappedValue.id)
_isPresented = isPresented
}
init(
data: Data,
selectedId: Binding<Data.Element.ID>,
isPresented: Binding<Bool>
) {
self.data = data
_selectedElement = .constant(data.first(where: { $0.id == selectedId.wrappedValue })!)
_selectedId = selectedId
_isPresented = isPresented
}
var body: some View {
VStack {
ForEach(data) { element in
Button(element.name) {
selectedElement = element
selectedId = element.id
isPresented.toggle()
}
.foregroundColor(
selectedElement.id == element.id
? .black
: .gray
)
}
}
}
}

Crash when deleting data with relation in RealmSwift

I am creating an application using RealmSwift.
The following implementation crashed when deleting related data.
After removing only "UnderlayerItem", it succeeded.
Crash when deleting UnderlayerItem and deleting Item.
The error is:
Thread 1: Exception: "The RLMArray has been invalidated or the object
containing it has been deleted."
How do I delete without crashing?
struct ListView: View {
#ObservedObject private var fetcher = Fetcher()
#State private var title = ""
var body: some View {
NavigationView {
VStack {
TextField("add", text: $title) {
let item = Item()
item.title = self.title
let realm = try! Realm()
try! realm.write {
realm.add(item)
}
self.title = ""
}
ForEach(self.fetcher.items) { (item: Item) in
NavigationLink(destination: DetailView(item: item, id: item.id)) {
Text(item.title)
}
}
}
}
}
}
struct DetailView: View {
var item: Item
var id: String
#State private var title = ""
#ObservedObject private var fetcher = Fetcher()
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
TextField("add", text: $title) {
let realm = try! Realm()
if let item = realm.objects(Item.self).filter("id == '\(self.id)'").first {
try! realm.write() {
let underlayerItem = UnderlayerItem()
underlayerItem.title = self.title
item.underlayerItems.append(underlayerItem)
}
}
self.title = ""
}
ForEach(self.item.underlayerItems) { (underlayerItems: UnderlayerItem) in
Text(underlayerItems.title)
}
Button(action: {
self.presentationMode.wrappedValue.dismiss()
self.fetcher.delete(id: self.id)
}) {
Text("Delete")
}
}
}
}
class Fetcher: ObservableObject {
var realm = try! Realm()
var objectWillChange: ObservableObjectPublisher = .init()
private(set) var items: Results<Item>
private var notificationTokens: [NotificationToken] = []
init() {
items = realm.objects(Item.self)
notificationTokens.append(items.observe { _ in
self.objectWillChange.send()
})
}
func delete(id: String) {
guard let item = realm.objects(Item.self).filter("id == '\(id)'").first else { return }
try! realm.write() {
for underlayerItem in item.underlayerItems {
realm.delete(realm.objects(UnderlayerItem.self).filter("id == '\(underlayerItem.id)'").first!)
}
}
try! realm.write() {
realm.delete(item)
}
}
}
class Item: Object, Identifiable {
#objc dynamic var id = NSUUID().uuidString
#objc dynamic var title = ""
#objc dynamic var createdAt = NSDate()
let underlayerItems: List<UnderlayerItem> = List<UnderlayerItem>()
override static func primaryKey() -> String? {
return "id"
}
}
class UnderlayerItem: Object, Identifiable {
#objc dynamic var id = NSUUID().uuidString
#objc dynamic var title = ""
#objc dynamic var createdAt = NSDate()
override static func primaryKey() -> String? {
return "id"
}
}
You don't need to iterate over the objects in the list to delete them. Just do this
try! realm.write() {
realm.delete(item.underlayerItems)
}
I believe it's crashing because you're attempting to access an item that was deleted
self.item.underlayerItems

ForEach not working in SwiftUI after conforming to Identifiable protocol

I have an array of Message which conforms to the Identifiable protocol but I keep getting this error: Generic parameter 'ID' could not be inferred. Even with the id: \.self won't work.
What's going on here?
struct Message: Identifiable {
var id = UUID()
var text: String
var createdAt: Date = Date()
var senderId: String
init(dictionary: [String: Any]) {
self.text = dictionary["text"] as? String ?? ""
self.senderId = dictionary["senderId"] as? String ?? ""
}
}
#State var messages: [Message] = []
ForEach(messages) { message in
// Generic parameter 'ID' could not be inferred
}
You need Text(message.id.uuidString)
import SwiftUI
struct Message: Identifiable {
var id = UUID()
var text: String
var createdAt: Date = Date()
var senderId: String
init(dictionary: [String: Any]) {
self.text = dictionary["text"] as? String ?? ""
self.senderId = dictionary["senderId"] as? String ?? ""
}
}
struct ContentView: View {
#State var messages: [Message] = [Message.init(dictionary: ["text":"t1","senderId":"1"]),Message.init(dictionary: ["text":"t2","senderId":"2"])]
var body: some View {
VStack {
ForEach(messages) { message in
Text(message.id.uuidString)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Edit:
import SwiftUI
struct Message: Identifiable {
var id = UUID()
var text: String
var createdAt: Date = Date()
var senderId: String
init(dictionary: [String: Any]) {
self.text = dictionary["text"] as? String ?? ""
self.senderId = dictionary["senderId"] as? String ?? ""
}
}
struct ContentView: View {
#State var messages: [Message] = []
var body: some View {
VStack {
ForEach(messages) { message in
Text(message.id.uuidString)
}
}.onAppear() {
self.messages = [Message.init(dictionary: ["text":"t1","senderId":"1"]),Message.init(dictionary: ["text":"t2","senderId":"2"])]
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}