Is there a way to getting the data out of a document when reading it back from Firestore. Besides Using "ForEach" or "List"
If we look at typical example:
struct User: Identifiable {
var id: String = UUID().uuidString
var name: String
var surname: String
}
class userViewModel: ObservableObject {
#Published var users = [User]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("users").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.users = documents.map { (queryDocumentSnapshot) -> User in
let data = queryDocumentSnapshot.data()
let name = data["name"] as? String ?? ""
let surname = data["surname"] as? String ?? ""
return User(name: name, surname: surname)
}
}
}
}
#ObservedObject private var viewModel = userViewModel()
var body: some View {
NavigationView {
List(viewModel.users) { user in
VStack(alignment: .leading) {
Text(user.name).font(.title)
Text(user.surname).font(.subheadline)
}
}.navigationBarTitle("Users")
.onAppear() {
self.viewModel.fetchData()
}
}
}
}
My main issue, is in my SwiftUI View, I do not want to create a List. But I want the data out of viewModel.users. Grabbing some information I just passed in to be used in my custom screen.
I'm very new to SwiftUI, So I feel I'm just doing something dumb, but the only way I've been able to get the information out is to iterate over it. But it doesn't leave me with the views I would need.
As discussed in the comments, you can send parameters to your new View via the NavigationLink. Here's a simple example:
struct ContentView : View {
#ObservedObject private var viewModel = userViewModel()
var body: some View {
NavigationView {
List(viewModel.users) { user in
NavigationLink(destination: UserView(user: user)) {
VStack(alignment: .leading) {
Text(user.name).font(.title)
Text(user.surname).font(.subheadline)
}
}
}.navigationBarTitle("Users")
.onAppear() {
self.viewModel.fetchData()
}
}
}
}
struct UserView : View {
var user : User
var body: some View {
VStack {
Text(user.name)
Text(user.surname)
}
}
}
I want to write a ToDoList in swiftUI with core data. Everything works so far but I want to have a checkbox next to each item it Signify whether it is completed or not.
I have added a property isChecked:boolean in core data but I don't know how to properly read it from the database. How to use a Toggle() in my case?
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: ToDoListItem.getAllToDoListItems())
var items: FetchedResults<ToDoListItem>
#State var text: String = ""
var body: some View {
NavigationView {
List {
Section (header: Text("NewItem")){
HStack {
TextField("Enter new Item.",text: $text)
Button(action: {
if !text.isEmpty{
let newItem = ToDoListItem(context: context)
newItem.name = text
newItem.createdAt = Date()
// current date as created
newItem.isChecked = false
do {
try context.save()
} catch {
print(error)
}
// to clear the textField from the previous entry
text = ""
}
}, label: {
Text("Save")
})
}// end of vstack
}
Section {
ForEach(items){ toDoListItem in
VStack(alignment: .leading){
// to have a checkbox
Button {
toDoListItem.isChecked.toggle()
} label: {
Label(toDoListItem.name!, systemImage: toDoListItem.isChecked ? "checkbox.square" : "square")
}
if let name = toDoListItem.name {
// Toggle(isOn: toDoListItem.isChecked)
Text(name)
.font(.headline)
}
//Text(toDoListItem.name!)
//.font(.headline)
if let createdAt = toDoListItem.createdAt {
//Text("\(toDoListItem.createdAt!)")
Text("\(createdAt)")
}
}
}.onDelete(perform: { indexSet in
guard let index = indexSet.first else {
return
}
let itemToDelete = items[index]
context.delete(itemToDelete)
do {
try context.save()
}
catch {
print(error)
}
})
}
}
.navigationTitle("To Do List")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ToDoListItem.swift
class ToDoListItem: NSManagedObject,Identifiable {
#NSManaged var name:String?
#NSManaged var createdAt:Date?
#NSManaged var isChecked:Bool
// mapped to the entry properties in database
}
extension ToDoListItem {
static func getAllToDoListItems() -> NSFetchRequest<ToDoListItem>{
let request:NSFetchRequest<ToDoListItem> = ToDoListItem.fetchRequest() as!
NSFetchRequest<ToDoListItem>
// cast as todolist item
let sort = NSSortDescriptor(key: "createdAt", ascending: true)
// above order of sorting
request.sortDescriptors = [sort]
return request
}
}
Should isChecked be an optional as well?
I try to implement a Search Bar with Algolia, and I use the MVVM pattern.
Here's my View Model:
class AlgoliaViewModel: ObservableObject {
#Published var idList = [String]()
func searchUser(text: String){
let client = SearchClient(appID: "XXX", apiKey: "XXX")
let index = client.index(withName: "Users")
let query = Query(text)
index.search(query: query) { result in
if case .success(let response) = result {
print("Response: \(response)")
do {
let hits: Array = response.hits
var idList = [String]()
for x in hits {
idList.append(x.objectID.rawValue)
}
DispatchQueue.main.async {
self.idList = idList
print(self.idList)
}
}
catch {
print("JSONSerialization error:", error)
}
}
}
}
}
Here is my View :
struct NewChatView : View {
#State private var searchText = ""
#ObservedObject var viewModel = AlgoliaViewModel()
var body : some View{
VStack(alignment: .leading){
Text("Select To Chat").font(.title).foregroundColor(Color.black.opacity(0.5))
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 12){
HStack {
TextField("Start typing",
text: $searchText,
onCommit: { self.viewModel.searchUser(text: self.searchText) })
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.viewModel.searchUser(text: self.searchText)
}) {
Image(systemName: "magnifyingglass")
}
} .padding()
List {
ForEach(viewModel.idList, id: \.self){ i in
Text(i)
}
}
}
}
}.padding()
}
}
I often use this pattern with Firebase and everything works fine, but here with Algolia the List remains empty in the NewChatView.
The print(self.idList) statement inside the View-Model shows the right idList, but it does not update the List inside the NewChatView.
You first need to create your own custom Identifiable and Hashable model to display the searchValue in a List or ForEach.
Something like this:
struct MySearchModel: Identifiable, Hashable {
let id = UUID().uuidString
let searchValue: String
}
Then use it in your AlgoliaViewModel. Set a default value of an empty array.
You can also map the hits received and convert it to your new model. No need for the extra for loop.
class AlgoliaViewModel: ObservableObject {
#Published var idList: [MySearchModel] = []
func searchUser(text: String) {
let client = SearchClient(appID: "XXX", apiKey: "XXX")
let index = client.index(withName: "Users")
let query = Query(text)
index.search(query: query) { result in
if case .success(let response) = result {
print("Response: \(response)")
do {
let hits: Array = response.hits
DispatchQueue.main.async {
self.idList = hits.map({ MySearchModel(searchValue: $0.objectID.rawValue) })
print(self.idList)
}
}
catch {
print("JSONSerialization error:", error)
}
}
}
}
}
For the NewChatView, you can remove the ScrollView as it conflicts with the elements inside your current VStack and would hide the List with the results as well. The following changes should display all your results.
struct NewChatView : View {
#State private var searchText = ""
#ObservedObject var viewModel = AlgoliaViewModel()
var body: some View{
VStack(alignment: .leading) {
Text("Select To Chat")
.font(.title)
.foregroundColor(Color.black.opacity(0.5))
VStack {
HStack {
TextField("Start typing",
text: $searchText,
onCommit: { self.viewModel.searchUser(text: self.searchText)
})
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.viewModel.searchUser(text: self.searchText)
}) {
Image(systemName: "magnifyingglass")
}
} .padding()
List {
ForEach(viewModel.idList) { i in
Text(i.searchValue)
.foregroundColor(Color.black)
}
}
}
}.padding()
}
}
There is a ListView. I make a transaction in Cloud Firestore by changing the field of an element when I click on it in the list. Data in the database changes as it should, but after this action all the elements in the list disappear (although there is .onAppear {fetchData}). An important point: this is a child view, there is no such problem in the parent view.
I also added a button at the bottom of the list to execute fetchData (), when I click on it, the data returns to the list
What could be the problem? Thanks
import SwiftUI
struct SecondView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
VStack {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Button("Update data"){
let updBook = book
self.viewModel.myTransaction(book: updBook)
}
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.navigationBarTitle("Books")
.onAppear() {
self.viewModel.fetchData()
}
Button("update list"){
self.viewModel.fetchData()
}
}
}
}
ViewModel:
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
}
}
func deleteBook(book: Book){
if let bookID = book.id{
db.collection("books").document(bookID).delete()
}
}
func updateBook(book: Book) {
if let bookID = book.id{
do {
try db.collection("books").document(bookID).setData(from: book) }
catch {
print(error)
}
}
}
func addBook(book: Book) {
do {
let _ = try db.collection("books").addDocument(from: book)
}
catch {
print(error)
}
}
func myTransaction(book: Book){
let bookID = book.id
let targetReference = db.collection("books").document(bookID!)
db.runTransaction({ (transaction, errorPointer) -> Any? in
let targetDocument: DocumentSnapshot
do {
try targetDocument = transaction.getDocument(targetReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let oldValue = targetDocument.data()?["pages"] as? Int else {
let error = NSError(
domain: "AppErrorDomain",
code: -1,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(targetDocument)"
]
)
errorPointer?.pointee = error
return nil
}
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
transaction.updateData(["pages": oldValue + 1], forDocument: targetReference)
return nil
}) { (object, error) in
if let error = error {
print("Transaction failed: \(error)")
} else {
print("Transaction successfully committed!")
}
}
}
}
Parent view:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Button("Update"){
let delBook = book
self.viewModel.myTransaction(book: delBook)
}
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.navigationBarTitle("Books")
.onAppear() {
self.viewModel.fetchData()
}
NavigationLink(destination: SecondView()){
Text("Second View")
}
}
}
}
}
A possible solution might be that your Views and its ViewModels interfere with each other. It looks like you create two instances of the same BookViewModel:
struct ContentView: View {
#ObservedObject var viewModel = BooksViewModel()
struct SecondView: View {
#ObservedObject var viewModel = BooksViewModel()
Try creating one BooksViewModel and pass it between views (you can use an #EnvironmentObject).
I have been able to save data in a Realm database, but have been unable to show the results in a SwiftUI List.
I know I have the data and have no problem printing the results in the console.
Is there a way to convert Realm Result into a format that can be displayed on a SwiftUI List?
import SwiftUI
import RealmSwift
import Combine
class Dog: Object {
#objc dynamic var name = ""
#objc dynamic var age = 0
override static func primaryKey() -> String? {
return "name"
}
}
class SaveDog {
func saveDog(name: String, age: String) {
let dog = Dog()
dog.age = Int(age)!
dog.name = name
// Get the default Realm
let realm = try! Realm()
print(Realm.Configuration.defaultConfiguration.fileURL!)
// Persist your data easily
try! realm.write {
realm.add(dog)
}
print(dog)
}
}
class RealmResults: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
func getRealmResults() -> String{
let realm = try! Realm()
var results = realm.objects(Dog.self) { didSet
{didChange.send(())}}
print(results)
return results.first!.name
}
}
struct dogRow: View {
var dog = Dog()
var body: some View {
HStack {
Text(dog.name)
Text("\(dog.age)")
}
}
}
struct ContentView : View {
#State var dogName: String = ""
#State var dogAge: String = ""
let saveDog = SaveDog()
#ObjectBinding var savedResults = RealmResults()
let realm = try! Realm()
let dogs = Dog()
var body: some View {
VStack {
Text("Hello World")
TextField($dogName)
TextField($dogAge)
Button(action: {
self.saveDog.saveDog(name: self.dogName,
age:self.dogAge)
// self.savedResults.getRealmResults()
}) {
Text("Save")
}
//insert list here to show realm data
List(0 ..< 5) {
item in
Text(self.savedResults.getRealmResults())
} //Displays the same thing 5 times
}
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Some of the code probably may not make sense because I was attempting several approaches to see if anything would work.
This line, for example, will display the result in the List View.
return results.first!.name
If I just return results, nothing displays in the List Text View.
As I have commented below I will attempt the ForEach approach when I have time. That looks promising.
The data that you pass in List or a ForEach must conform to the Identifiable protocol.
Either you adopt it in your Realm models or you use .identified(by:) method.
Even with that, the View won't reload if the data changes.
You could wrap Results and make it a BindableObject, so the view can detect the changes and reload itself:
class BindableResults<Element>: ObservableObject where Element: RealmSwift.RealmCollectionValue {
var results: Results<Element>
private var token: NotificationToken!
init(results: Results<Element>) {
self.results = results
lateInit()
}
func lateInit() {
token = results.observe { [weak self] _ in
self?.objectWillChange.send()
}
}
deinit {
token.invalidate()
}
}
And use it like:
struct ContentView : View {
#ObservedObject var dogs = BindableResults(results: try! Realm().objects(Dog.self))
var body: some View {
List(dogs.results.identified(by: \.name)) { dog in
DogRow(dog: dog)
}
}
}
This is the most straight forward way of doing it:
struct ContentView: View {
#State private var dog: Results<Dog> = try! Realm(configuration: Realm.Configuration(schemaVersion: 1)).objects(Dog.self)
var body: some View {
ForEach(dog, id: \.name) { i in
Text(String((i.name)!))
}
}
}
...That's it, and it works!
I have created a generic solution to display and add/delete for any Results<T>. By default, Results<T> is "live". SwiftUI sends changes to View when the #Published property WILL update. When a RealmCollectionChange<Results<T>> notification is received, Results<T> has already updated; Therefore, a fatalError will occur on deletion due to index out of range. Instead, I use a "live" Results<T> for tracking changes and a "frozen" Results<T> for use with the View. A full working example, including how to use a generic View with RealmViewModel<T> (shown below), can be found here: SwiftUI+Realm. The enum Status is used to display a ProgressView, "No records found", etc., when applicable, as shown in the project. Also, note that the "frozen" Results<T> is used when needing a count or single object. When deleting, the IndexSet by onDelete is going to return a position from the "frozen" Results<T> so it checks that the object still existing in the "live" Results<T>.
class RealmViewModel<T: RealmSwift.Object>: ObservableObject, Verbose where T: Identifiable {
typealias Element = T
enum Status {
// Display ProgressView
case fetching
// Display "No records found."
case empty
// Display results
case results
// Display error
case error(Swift.Error)
enum _Error: String, Swift.Error {
case fetchNotCalled = "System Error."
}
}
init() {
fetch()
}
deinit {
token?.invalidate()
}
#Published private(set) var status: Status = .error(Status._Error.fetchNotCalled)
// Frozen results: Used for View
#Published private(set) var results: Results<Element>?
// Live results: Used for NotificationToken
private var __results: Results<Element>?
private var token: NotificationToken?
private func notification(_ change: RealmCollectionChange<Results<Element>>) {
switch change {
case .error(let error):
verbose(error)
self.__results = nil
self.results = nil
self.token = nil
self.status = .error(error)
case .initial(let results):
verbose("count:", results.count)
//self.results = results.freeze()
//self.status = results.count == 0 ? .empty : .results
case .update(let results, let deletes, let inserts, let updates):
verbose("results:", results.count, "deletes:", deletes, "inserts:", inserts, "updates:", updates)
self.results = results.freeze()
self.status = results.count == 0 ? .empty : .results
}
}
var count: Int { results?.count ?? 0 }
subscript(_ i: Int) -> Element? { results?[i] }
func fetch() {
status = .fetching
//Realm.asyncOpen(callback: asyncOpen(_:_:))
do {
let realm = try Realm()
let results = realm.objects(Element.self).sorted(byKeyPath: "id")
self.__results = results
self.results = results.freeze()
self.token = self.__results?.observe(notification)
status = results.count == 0 ? .empty : .results
} catch {
verbose(error)
self.__results = nil
self.results = nil
self.token = nil
status = .error(error)
}
}
func insert(_ data: Element) throws {
let realm = try Realm()
try realm.write({
realm.add(data)
})
}
func delete(at offsets: IndexSet) throws {
let realm = try Realm()
try realm.write({
offsets.forEach { (i) in
guard let id = results?[i].id else { return }
guard let data = __results?.first(where: { $0.id == id }) else { return }
realm.delete(data)
}
})
}
}
Here is another option using the new Realm frozen() collections. While this is early days the UI is updated automatically when 'assets' get added to the database. In this example they are being added from an NSOperation thread, which should be a background thread.
In this example the sidebar lists different property groups based on the distinct values in the database - note that you may wish to implement this in a more robust manner - but as a quick POC this works fine. See image below.
struct CategoryBrowserView: View {
#ObservedObject var assets: RealmSwift.List<Asset> = FileController.shared.assets
#ObservedObject var model = ModelController.shared
#State private var searchTerm: String = ""
#State var isEventsShowing: Bool = false
#State var isProjectsShowing: Bool = false
#State var isLocationsShowing: Bool = false
var projects: Results<Asset> {
return assets.sorted(byKeyPath: "project").distinct(by: ["project"])
}
var events: Results<Asset> {
return assets.sorted(byKeyPath: "event").distinct(by: ["event"])
}
var locations: Results<Asset> {
return assets.sorted(byKeyPath: "location").distinct(by: ["location"])
}
#State var status: Bool = false
var body: some View {
VStack(alignment: .leading) {
ScrollView {
VStack(alignment: .leading) {
// Projects
DisclosureGroup(isExpanded: $isProjectsShowing) {
VStack(alignment:.trailing, spacing: 4) {
ForEach(filteredProjectsCollection().freeze()) { asset in
HStack {
Text(asset.project)
Spacer()
Image(systemName: self.model.selectedProjects.contains(asset.project) ? "checkmark.square" : "square")
.resizable()
.frame(width: 17, height: 17)
.onTapGesture { self.model.addProject(project: asset.project) }
}
}
}.frame(maxWidth:.infinity)
.padding(.leading, 20)
} label: {
HStack(alignment:.center) {
Image(systemName: "person.2")
Text("Projects").font(.system(.title3))
Spacer()
}.padding([.top, .bottom], 8).foregroundColor(.secondary)
}
// Events
DisclosureGroup(isExpanded: $isEventsShowing) {
VStack(alignment:.trailing, spacing: 4) {
ForEach(filteredEventsCollection().freeze()) { asset in
HStack {
Text(asset.event)
Spacer()
Image(systemName: self.model.selectedEvents.contains(asset.event) ? "checkmark.square" : "square")
.resizable()
.frame(width: 17, height: 17)
.onTapGesture { self.model.addEvent(event: asset.event) }
}
}
}.frame(maxWidth:.infinity)
.padding(.leading, 20)
} label: {
HStack(alignment:.center) {
Image(systemName: "calendar")
Text("Events").font(.system(.title3))
Spacer()
}.padding([.top, .bottom], 8).foregroundColor(.secondary)
}
// Locations
DisclosureGroup(isExpanded: $isLocationsShowing) {
VStack(alignment:.trailing, spacing: 4) {
ForEach(filteredLocationCollection().freeze()) { asset in
HStack {
Text(asset.location)
Spacer()
Image(systemName: self.model.selectedLocations.contains(asset.location) ? "checkmark.square" : "square")
.resizable()
.frame(width: 17, height: 17)
.onTapGesture { self.model.addLocation(location: asset.location) }
}
}
}.frame(maxWidth:.infinity)
.padding(.leading, 20)
} label: {
HStack(alignment:.center) {
Image(systemName: "flag")
Text("Locations").font(.system(.title3))
Spacer()
}.padding([.top, .bottom], 8).foregroundColor(.secondary)
}
}.padding(.all, 10)
.background(Color(NSColor.controlBackgroundColor))
}
SearchBar(text: self.$searchTerm)
.frame(height: 30, alignment: .leading)
}
}
func filteredProjectsCollection() -> AnyRealmCollection<Asset> {
if self.searchTerm.isEmpty {
return AnyRealmCollection(self.projects)
} else {
return AnyRealmCollection(self.projects.filter("project CONTAINS[c] %# || event CONTAINS[c] %# || location CONTAINS[c] %# || tags CONTAINS[c] %#", searchTerm, searchTerm, searchTerm, searchTerm))
}
}
func filteredEventsCollection() -> AnyRealmCollection<Asset> {
if self.searchTerm.isEmpty {
return AnyRealmCollection(self.events)
} else {
return AnyRealmCollection(self.events.filter("project CONTAINS[c] %# || event CONTAINS[c] %# || location CONTAINS[c] %# || tags CONTAINS[c] %#", searchTerm, searchTerm, searchTerm, searchTerm))
}
}
func filteredLocationCollection() -> AnyRealmCollection<Asset> {
if self.searchTerm.isEmpty {
return AnyRealmCollection(self.locations)
} else {
return AnyRealmCollection(self.locations.filter("project CONTAINS[c] %# || event CONTAINS[c] %# || location CONTAINS[c] %# || tags CONTAINS[c] %#", searchTerm, searchTerm, searchTerm, searchTerm))
}
}
func filteredCollection() -> AnyRealmCollection<Asset> {
if self.searchTerm.isEmpty {
return AnyRealmCollection(self.assets)
} else {
return AnyRealmCollection(self.assets.filter("project CONTAINS[c] %# || event CONTAINS[c] %# || location CONTAINS[c] %# || tags CONTAINS[c] %#", searchTerm, searchTerm, searchTerm, searchTerm))
}
}
func delete(at offsets: IndexSet) {
if let realm = assets.realm {
try! realm.write {
realm.delete(assets[offsets.first!])
}
} else {
assets.remove(at: offsets.first!)
}
}
}
struct CategoryBrowserView_Previews: PreviewProvider {
static var previews: some View {
CategoryBrowserView()
}
}
struct CheckboxToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
return HStack {
configuration.label
Spacer()
Image(systemName: configuration.isOn ? "checkmark.square" : "square")
.resizable()
.frame(width: 22, height: 22)
.onTapGesture { configuration.isOn.toggle() }
}
}
}