Swiftui + Firestore + CombineLatest keep publishing data - swift

I'm trying to merges two published object from query of two different collections to make a single published array using combineLatest.
import Combine
import Firebase
class FirestoreViewModel: ObservableObject {
let userId: String
#Published var aList: [DocumentA]? = nil
#Published var bList: [DocumentB]? = nil
init(userId: String, listenForChanges: Bool = false) {
// Get the user
if (listenForChanges) {
self.loadA()
self.loadB()
}
}
var cList: AnyPublisher<[DocumentC]?, Never> {
return Publishers.CombineLatest(
$aList.map { (aList) -> [DocumentC]? in
guard let aList = aList else { return nil }
return aList.map {a in
DocumentC(from: a)
}
},
$bList.map { (bList) -> [DocumentC]? in
guard let bList = bList else { return nil }
return bList.map { n in
DocumentC(from: b)
}
})
.map { (aList, bList) -> [DocumentC]? in
if (aList == nil && bList == nil) { return nil }
var cList: [DocumentC] = []
if let aList = aList {
cList.append(contentsOf: aList)
}
if let bList = bList {
cList.append(contentsOf: bList)
}
return cList
}
.eraseToAnyPublisher()
}
private func loadA() {
// Start listening
Firestore.firestore().collection("colA").addSnapshotListener { (snapshot, error) in
if let error = error {
print("DEBUG: Unable to get user data: \(error.localizedDescription)")
return
}
// Invalid data return
guard let snapshot = snapshot else {
print("DEBUG: null data returned")
self.aList = nil
return
}
// Update the info
var aList: [DocumentA] = []
snapshot.documents.forEach { document in
let aData = document.data()
guard !aData.isEmpty else {
return
}
aList.append(DocumentA(from: aData)
}
self.aList = aList
}
}
private func loadB() {
// Start listening
Firestore.firestore().collection("colB").addSnapshotListener { (snapshot, error) in
if let error = error {
print("DEBUG: Unable to get user data: \(error.localizedDescription)")
return
}
// Invalid data return
guard let snapshot = snapshot else {
print("DEBUG: null data returned")
self.bList = nil
return
}
// Update the info
var bList: [DocumentB] = []
snapshot.documents.forEach { document in
let bData = document.data()
guard !bData.isEmpty else {
return
}
bList.append(DocumentB(from: bData))
}
self.userUnits = userUnits
}
}
}
When I debug, I can see that aList and bList are published once, however, the cList keeps been published eventually eating all the memory...
The cList is consumed via a onReceive statement in the view, which output each item.
Would anybody be able to tell me why the combineLatest keeps on publishing cList, though there are no fresh aList or bList published ?
Many thanks in advance.

In all likelihood, you have a situation where you update the view with values just received from cList, which causes the body to be recomputed, which causes another onReceive(vm.cList) {...}, which causes a new publisher to be returned by the computed property cList, which emits the values again and repeats the cycle.
Here's an simplified example of what I mean:
class ViewModel: ObservableObject {
#Published var aList: [Int] = [1,2]
var cList: AnyPublisher<Int, Never> {
$aList.map { $0 + $0 }.eraseToAnyPublisher()
}
}
struct ContentView: View {
#StateObject var vm = ViewModel()
#State var list: [Int] = []
var body: some View {
VStack {
ForEach(list, id: \.self) { v in
Text("\(v)")
}
}
.onReceive(vm.cList) { self.list = $0 }
}
}
To avoid that, cList shouldn't be a computed property. It could just be a lazy-ly assigned constant:
lazy var cList: AnyPublisher<Int, Never> =
$alist.map { $0 + $0 }
.eraseToAnyPublisher()

Related

StateObject does not change on Appear but changes value on Button Tap

On Clicking the Button present in BillContributorView.swift, the view updates but not on Appear. I have tried swapping StateObject with ObservedObject but not only does it not help but also, I read that we should instantiate our ObservableObject with StateObject.
BillContributorView.swift
#StateObject var billManager = BillManager()
var body: some View {
VStack {
HStack {
Text("Get: \(String(format: "%.2f", billManager.toGetAmount))")
.font(.footnote)
Button {
billManager.updateToGet(contributorID: memberID)
} label: { Text("To Get") }
}
.onAppear(perform: {
billManager.updateRoomID(name: roomID)
billManager.updateToGet(contributorID: memberID )
})
}
BillManager
class BillManager: ObservableObject {
#Published var roomID: String
#Published var toGetbills = [Bill]()
#Published var toGetAmount: Double = 0
init(name: String? = nil) {
if let name = name {
roomID = name
} else {
roomID = ""
}
}
func updateRoomID(name: String) {
roomID = name
}
func updateToGet(contributorID: String) {
let collectionName = "\(roomID)_BILLS"
toGetAmount = 0
db.collection(collectionName)
.whereField("payer", isEqualTo: Auth.auth().currentUser?.uid ?? "")
.whereField("contributor", isEqualTo: contributorID)
.addSnapshotListener { snapshot, error in
DispatchQueue.main.async {
guard let doc = snapshot?.documents else {
print("No Doc Found")
return
}
self.toGetbills = doc.compactMap { queryDocumentSnapshot -> Bill? in
let result = Result { try queryDocumentSnapshot.data(as: Bill.self) }
switch result {
case .success(let bill):
return bill
}
}
}
}
toGetAmount = 0
for bill in toGetbills {
toGetAmount += bill.itemPrice
}
}
Fixed the issue by updating the value inside the closure.
db.collection(collectionName)
.whereField("payer", isEqualTo: Auth.auth().currentUser?.uid ?? "")
.whereField("contributor", isEqualTo: contributorID)
.addSnapshotListener { snapshot, error in
self.toGetAmount = 0
guard let doc = snapshot?.documents else {
print("No Doc Found")
return
}
DispatchQueue.main.async {
self.toGetbills = doc.compactMap { queryDocumentSnapshot -> Bill? in
let result = Result { try queryDocumentSnapshot.data(as: Bill.self) }
switch result {
case .success(let bill):
self.toGetAmount += bill.itemPrice
return bill
case .failure( _):
print("Failure To Get Bill Request")
return nil
}
}
}
}

Filter Firebase Data SwiftUI

This code adds all the data in a single array. In HomeViev I use to Foreach and I added to data to list. But I have to split the data in two. status collection has two type "active" and "closed" but I don't know how can I filter
import SwiftUI
import Combine
import Firebase
let dbCollection = Firestore.firestore().collection("Signals")
class FirebaseSession : ObservableObject {
#Published var session: User? { didSet { self.didChange.send(self) }}
#Published var data = [Signal]()
var didChange = PassthroughSubject<FirebaseSession, Never>()
var handle: AuthStateDidChangeListenerHandle?
func listen () {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
print("Got user: \(user)")
self.session = User(uid: user.uid, email: user.email)
self.readData()
} else {
self.session = nil
}
}
}
func readData() {
dbCollection.addSnapshotListener { (documentSnapshot, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}else {
print("read data success")
}
documentSnapshot!.documentChanges.forEach { i in
// Read real time created data from server
if i.type == .added {
let id = i.document.documentID
let symbol = i.document.get("symbol") as? String ?? ""
let status = i.document.get("status") as? String ?? ""
self.data.append(Signal(id: id, symbol: symbol, status: status))
}
// Read real time modify data from server
if i.type == .modified {
self.data = self.data.map { (eachData) -> Signal in
var data = eachData
if data.id == i.document.documentID {
data.symbol = i.document.get("symbol") as! String
data.status = i.document.get("status") as? String ?? ""
return data
}else {
return eachData
}
}
}
// When data is removed...
if i.type == .removed {
let id = i.document.documentID
for i in 0..<self.data.count{
if self.data[i].id == id{
self.data.remove(at: i)
return
}
}
}
}
}
}
}
The question states
But I have to split the data in two
I assume that means two arrays; one for active and one for closed.
var activeData = [...
var closedData = [...
There are a couple of ways to do that
1)
Query Firestore for all status fields equal to active and load those documents into the active array and then another query for status fields equal closed and load those in the the closed array
2)
I would suggest a simpler approach
if i.type == .added {
let id = i.document.documentID
let symbol = i.document.get("symbol") as? String ?? ""
let status = i.document.get("status") as? String ?? ""
if status == "active" {
self.activeData.append(Signal(id: id, symbol: symbol, status: status))
} else {
self.closedData.append(Signal(id: id, symbol: symbol, status: status))
}
}
and do the same thing within .modified and .removed; identify the status so the code will know which array to remove it from.
EDIT:
Based on a comment
I don't know how to query this codes.
I am providing code to query for signals that are active. This code will return only active signals and as signals become active, inactive etc, this will modify a signalArray to stay in sync with the data.
let dbCollection = Firestore.firestore().collection("Signals")
let query = dbCollection.whereField("status", isEqualTo: "active").addSnapshotListener( { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let signalToAdd = Signal(withDoc: diff.document)
self.signalArray.append(signalToAdd)
}
if (diff.type == .modified) {
let docId = diff.document.documentID
if let indexOfSignalToModify = self.signalArray.firstIndex(where: { $0.signal_id == docId} ) {
let signalToModify = self.signalArray[indexOfSignalToModify]
signalToModify.updateProperties(withDoc: diff.document)
}
}
if (diff.type == .removed) {
let docId = diff.document.documentID
if let indexOfSignalToRemove = self.signalArray.firstIndex(where: { $0.signal_id == docId} ) {
self.signalArray.remove(at: indexOfSignalToRemove)
}
}
}
})
Note that my Signal Class has an initializer that accepts a QueryDocumentSnapshot to initialize it as well as a .updateProperties function to update its internal properties.

Custom Property Wrapper that Updates View Swift

Xcode 11.3, Swift 5.1.3
I am trying currently to create a custom property wrapper that allows me to link variables to a Firebase database. When doing this, to make it update the view, I at first tried to use the #ObservedObject #Bar var foo = []. But I get an error that multiple property wrappers are not supported. Next thing I tried to do, which would honestly be ideal, was try to make my custom property wrapper update the view itself upon being changed, just like #State and #ObservedObject. This both avoids needing to go down two layers to access the underlying values and avoid the use of nesting property wrappers. To do this, I checked the SwiftUI documentation and found out that they both implement the DynamicProperty protocol. I tried to use this too but failed because I need to be able to update the view (call update()) from within my Firebase database observers, which I cannot do since .update() is mutating.
Here is my current attempt at this:
import SwiftUI
import Firebase
import CodableFirebase
import Combine
#propertyWrapper
final class DatabaseBackedArray<Element>: ObservableObject where Element: Codable & Identifiable {
typealias ObserverHandle = UInt
typealias Action = RealtimeDatabase.Action
typealias Event = RealtimeDatabase.Event
private(set) var reference: DatabaseReference
private var currentValue: [Element]
private var childAddedObserverHandle: ObserverHandle?
private var childChangedObserverHandle: ObserverHandle?
private var childRemovedObserverHandle: ObserverHandle?
private var childAddedActions: [Action<[Element]>] = []
private var childChangedActions: [Action<[Element]>] = []
private var childRemovedActions: [Action<[Element]>] = []
init(wrappedValue: [Element], _ path: KeyPath<RealtimeDatabase, RealtimeDatabase>, events: Event = .all,
actions: [Action<[Element]>] = []) {
currentValue = wrappedValue
reference = RealtimeDatabase()[keyPath: path].reference
for action in actions {
if action.event.contains(.childAdded) {
childAddedActions.append(action)
}
if action.event.contains(.childChanged) {
childChangedActions.append(action)
}
if action.event.contains(.childRemoved) {
childRemovedActions.append(action)
}
}
if events.contains(.childAdded) {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childChanged) {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childRemoved) {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
private func setValue(to value: [Element]) {
guard let encodedValue = try? FirebaseEncoder().encode(currentValue) else {
fatalError("Could not encode value to Firebase.")
}
reference.setValue(encodedValue)
}
var wrappedValue: [Element] {
get {
return currentValue
}
set {
self.objectWillChange.send()
setValue(to: newValue)
}
}
var projectedValue: Binding<[Element]> {
return Binding(get: {
return self.wrappedValue
}) { newValue in
self.wrappedValue = newValue
}
}
var hasActiveObserver: Bool {
return childAddedObserverHandle != nil || childChangedObserverHandle != nil || childRemovedObserverHandle != nil
}
var hasChildAddedObserver: Bool {
return childAddedObserverHandle != nil
}
var hasChildChangedObserver: Bool {
return childChangedObserverHandle != nil
}
var hasChildRemovedObserver: Bool {
return childRemovedObserverHandle != nil
}
func connectObservers(for event: Event) {
if event.contains(.childAdded) && childAddedObserverHandle == nil {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childChanged) && childChangedObserverHandle == nil {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childRemoved) && childRemovedObserverHandle == nil {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
func removeObserver(for event: Event) {
if event.contains(.childAdded), let handle = childAddedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childAddedObserverHandle = nil
}
if event.contains(.childChanged), let handle = childChangedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childChangedObserverHandle = nil
}
if event.contains(.childRemoved), let handle = childRemovedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childRemovedObserverHandle = nil
}
}
func removeAction(_ action: Action<[Element]>) {
if action.event.contains(.childAdded) {
childAddedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childChanged) {
childChangedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childRemoved) {
childRemovedActions.removeAll { $0.id == action.id }
}
}
func removeAllActions(for event: Event) {
if event.contains(.childAdded) {
childAddedActions = []
}
if event.contains(.childChanged) {
childChangedActions = []
}
if event.contains(.childRemoved) {
childRemovedActions = []
}
}
}
struct School: Codable, Identifiable {
/// The unique id of the school.
var id: String
/// The name of the school.
var name: String
/// The city of the school.
var city: String
/// The province of the school.
var province: String
/// Email domains for student emails from the school.
var domains: [String]
}
#dynamicMemberLookup
struct RealtimeDatabase {
private var path: [String]
var reference: DatabaseReference {
var ref = Database.database().reference()
for component in path {
ref = ref.child(component)
}
return ref
}
init(previous: Self? = nil, child: String? = nil) {
if let previous = previous {
path = previous.path
} else {
path = []
}
if let child = child {
path.append(child)
}
}
static subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
static subscript(dynamicMember keyPath: KeyPath<Self, Self>) -> Self {
return Self()[keyPath: keyPath]
}
static let reference = Database.database().reference()
struct Event: OptionSet, Hashable {
let rawValue: UInt
static let childAdded = Event(rawValue: 1 << 0)
static let childChanged = Event(rawValue: 1 << 1)
static let childRemoved = Event(rawValue: 1 << 2)
static let all: Event = [.childAdded, .childChanged, .childRemoved]
static let constructive: Event = [.childAdded, .childChanged]
static let destructive: Event = .childRemoved
}
struct Action<Value>: Identifiable {
let id = UUID()
let event: Event
let action: (inout Value) -> Void
private init(on event: Event, perform action: #escaping (inout Value) -> Void) {
self.event = event
self.action = action
}
static func on<Value>(_ event: RealtimeDatabase.Event, perform action: #escaping (inout Value) -> Void) -> Action<Value> {
return Action<Value>(on: event, perform: action)
}
}
}
Usage example:
struct ContentView: View {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
var body: some View {
Text("School: ").bold() +
Text(schools.isEmpty ? "Loading..." : schools.first!.name)
}
}
When I try to use this though, the view never updates with the value from Firebase even though I am positive that the .childAdded observer is being called.
One of my attempts at fixing this was to store all of these variables in a singleton that itself conforms to ObservableObject. This solution is also ideal as it allows the variables being observed to be shared throughout my application, preventing multiples instances of the same date and allowing for a single source of truth. Unfortunately, this too did not update the view with the fetched value of currentValue.
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private init() {
//Send `objectWillChange` when `schools` property changes
_schools.objectWillChange.sink {
self.objectWillChange.send()
}
}
static let current = Session()
}
struct ContentView: View {
#ObservedObject
var session = Session.current
var body: some View {
Text("School: ").bold() +
Text(session.schools.isEmpty ? "Loading..." : session.schools.first!.name)
}
}
Is there any way to make a custom property wrapper that also updates a view in SwiftUI?
Making use of the DynamicProperty protocol we can easily trigger view updates by making use of SwiftUI's existing property wrappers. (DynamicProperty tells SwiftUI to look for these within our type)
#propertyWrapper
struct OurPropertyWrapper: DynamicProperty {
// A state object that we notify of updates
#StateObject private var updater = Updater()
var wrappedValue: T {
get {
// Your getter code here
}
nonmutating set {
// Tell SwiftUI we're going to change something
updater.notifyUpdate()
// Your setter code here
}
}
class Updater: ObservableObject {
func notifyUpdate() {
objectWillChange.send()
}
}
}
The solution to this is to make a minor tweak to the solution of the singleton. Credits to #user1046037 for pointing this out to me. The problem with the singleton fix mentioned in the original post, is that it does not retain the canceller for the sink in the initializer. Here is the correct code:
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private var cancellers = [AnyCancellable]()
private init() {
_schools.objectWillChange.sink {
self.objectWillChange.send()
}.assign(to: &cancellers)
}
static let current = Session()
}

Typecasting causing struct values to change (Swift)

After downcasting an array of structs, my Variables View window shows that all of the values in my struct have shifted "down" (will explain in a second). But when I print(structName), the values are fine. However, when I run an equality check on the struct, it once again behaves as though my values have shifted.
For example, I am trying to downcast Model A to ModelProtocol. var m = Model A and has the values {id: "1234", name: "Cal"}. When I downcast, m now has the values { id:"\0\0", name:"1234" }.
Actual Example Below:
Models that I want to downcast:
struct PrivateSchoolModel: Decodable, SchoolProtocol {
var id: String
var name: String
var city: String
var state: String
}
struct PublicSchoolModel: Decodable, SchoolProtocol {
var id: String
var name: String
var city: String
var state: String
var latitude: String
var longitude: String
}
Protocol I want to downcast to:
protocol SchoolProtocol {
var id: String { get set }
var name: String { get set }
var city: String { get set }
var state: String { get set }
var longitude: Float { get set }
var latitude: Float { get set }
}
extension SchoolProtocol {
var longitude: Float {
get { return -1.0 }
set {}
}
var latitude: Float {
get { return -1.0 }
set {}
}
}
Downcasting:
guard let downcastedArr = privateSchoolArray as? [SchoolProtocol] else { return [] }
Result (item at index 0) or originalArr:
id = "1234"
name = "Leo High School"
city = "Bellview"
state = "WA"
Result (item at index 0) of downcastedArr:
id = "\0\0"
name = "1234"
city = "Leo High School"
state = "Bellview"
But if I print(downcastArr[0]), it will show:
id = "1234"
name = "Leo High School"
city = "Bellview"
state = "WA"
But if I try originalArray[0].id == downcastArr[0].id, it returns false
My Code with the problem:
class SchoolJSONHandler {
private enum JSONFile: String {
case publicSchool = "publicSchool"
case privateSchool = "privateSchool"
}
private lazy var privateSchoolArray = getPrivateSchools()
private lazy var publicSchoolArray = getPublicSchools()
func getSchoolArray(sorted: Bool, filtered: Bool, by stateAbbreviation: String?) -> [SchoolProtocol] {
var schools = combineArrays()
if sorted {
schools.sort(by: { $0.name < $1.name })
}
if filtered {
guard let abbr = stateAbbreviation else { return [] }
schools = schools.filter {
return $0.state == abbr
}
}
return schools
}
private func combineArrays() -> [SchoolProtocol] {
// EVERYTHING IS FINE IN NEXT LINE
let a = privateSchoolArray
// PROBLEM OCCURS IN NEXT 2 LINES WHEN DOWNCASTING
let b = privateSchoolArray as [SchoolProtocol]
let c = publicSchoolArray as [SchoolProtocol]
return b + c
}
private func getPublicSchools() -> [PublicSchoolModel] {
guard let jsonData = getJSONData(from: .publicSchool) else { return [] }
guard let schools = decode(jsonData: jsonData, using: [PublicSchoolModel].self) else { return [] }
return schools
}
private func getPrivateSchools() -> [PrivateSchoolModel] {
guard let jsonData = getJSONData(from: .privateSchool) else { return [] }
guard let schools = decode(jsonData: jsonData, using: [PrivateSchoolModel].self) else { return [] }
return schools
}
private func getJSONData(from resource: JSONFile) -> Data? {
let url = Bundle.main.url(forResource: resource.rawValue, withExtension: "json")!
do {
let jsonData = try Data(contentsOf: url)
return jsonData
}
catch {
print(error)
}
return nil
}
private func decode<M: Decodable>(jsonData: Data, using modelType: M.Type) -> M? {
do {
//here dataResponse received from a network request
let decoder = JSONDecoder()
let model = try decoder.decode(modelType, from:
jsonData) //Decode JSON Response Data
return model
} catch let parsingError {
print("Error", parsingError)
}
return nil
}
}
And then it is just called in another class by schoolJSONHandler.getSchoolArray(sorted: true, filtered: true, by: "WA")

How to check if core data is empty

How do I check if core data is empty using Swift. I tried this method:
var people = [NSManagedObject]()
if people == nil {
}
but this results in this error:
“binary operator '==' cannot be applied to operands of type [NSManagedObject] and nil”
Swift 3 solution:
var isEmpty: Bool {
do {
let request = NSFetchRequest(entityName: YOUR_ENTITY)
let count = try context.count(for: request)
return count == 0
} catch {
return true
}
}
Based on Dejan Skledar's answer I got rid of some compiler warnings and adopted it to Swift 2.0.
func entityIsEmpty(entity: String) -> Bool
{
let context = NSManagedObjectContext()
let request = NSFetchRequest(entityName: entity)
var results : NSArray?
do {
results = try context.executeFetchRequest(request) as! [NSManagedObject]
return results.count == 0
} catch let error as NSError {
// failure
print("Error: \(error.debugDescription)")
return true
}
}
However, I am not sure if the if let res=results clause along with its else clause ist required or not.
To check if the Core Database is empty you have to make a NSFetchRequest on the entity you want to check, and check if the results of the request are empty.
You can check it with this function:
func entityIsEmpty(entity: String) -> Bool
{
var appDel:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var context = NSManagedObjectContext()
var request = NSFetchRequest(entityName: entity)
var error = NSErrorPointer()
var results:NSArray? = self.context.executeFetchRequest(request, error: error)
if let res = results
{
if res.count == 0
{
return true
}
else
{
return false
}
}
else
{
println("Error: \(error.debugDescription)")
return true
}
}
Or simplier and shorter solution: (using .countForFetchRequest)
func entityIsEmpty(entity: String) -> Bool
{
var appDel:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var context = NSManagedObjectContext()
var request = NSFetchRequest(entityName: entity)
var error = NSErrorPointer()
var results:NSArray? = self.context.executeFetchRequest(request, error: error)
var count = context.countForFetchRequest(request, error: error)
if error != nil
{
println("Error: \(error.debugDescription)")
return true
}
else
{
if count == 0
{
return true
}
else
{
return false
}
}
}
SwiftUI Solution
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \MyObject.created, ascending: true)],
animation: .default)
private var myobjects: FetchedResults<MyObject>
var body: some View {
if self.myobjects.isEmpty{
Text("There are no objects in the current database.")
}
else{
// whatever
}
}
}