How to write unit tests for Firestore - swift

I am a beginner and I need to test my FirestoreService class below. I tried to make a mock but I do not go in the snapshot. I just need to test the case where it fails.
class FirebaseService {
let decoder = JSONDecoder()
var movies = [Movies]()
enum Result {
case success([Movies])
case failure(Error)
}
func getMovies(completion: #escaping (Result) -> Void) {
let movieReference = Firestore.firestore().collection("movies").order(by: "id")
movieReference.addSnapshotListener { (snapshot, _) in
guard let snapshot = snapshot else {return}
do {
self.movies = try snapshot.decoded()
completion(.success(self.movies))
} catch {
completion(.failure(error))
}
}
}
This is my FirebaseServiceTests class
XCTestclass FirebaseServiceTests: XCTestCase {
func testGetMovies() {
let firebaseSercice = FirebaseService()
let expectation = XCTestExpectation(description: "Wait for queue change ")
firebaseSercice.getMovies { result in
XCTAssertNotNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 0.5)
}
func testGetMoviesWithMock() {
let firebaseSercice = MockDatabaseReference()
let expectation = XCTestExpectation(description: "Wait for queue change ")
firebaseSercice.getMovies { result in
XCTAssertNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 0.5)
}
}
private class MockDatabaseReference: FirebaseService {
override func getMovies(completion: #escaping (Result) -> Void) {
let movieTestReference = Firestore.firestore().collection("mov").order(by: "id")
movieTestReference.addSnapshotListener { (snapshot, _) in
guard let snapshot = snapshot else {return}
do {
self.movies = try snapshot.decoded()
completion(.success(self.movies))
} catch {
completion(.failure(error))
}
}
}
}
Could you explain me how to do ? Thank you.

Related

Show error response in alert RxSwift using Driver

How to get an error response with driver so I can show it in alert. When I see the trait driver is can't error out, so should I use subject or behaviourRelay to get error response when I subscribe. Actually I like how to use driver but I don't know how to passed error response using driver.
this is my network service
func getMovies(page: Int) -> Observable<[MovieItem]> {
return Observable.create { observer -> Disposable in
self.service.request(endpoint: .discover(page: page)) { data, response, error in
if let _ = error {
observer.onError(MDBError.unableToComplete)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
observer.onError(MDBError.invalidResponse)
return
}
guard let data = data else {
observer.onError(MDBError.invalidData)
return
}
if let decode = self.decode(jsonData: MovieResults.self, from: data) {
observer.onNext(decode.results)
}
observer.onCompleted()
}
return Disposables.create()
}
}
This is my viewModel
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
class PopularViewModel: ViewModelType {
struct Input {
let viewDidLoad: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let movies: Driver<[MovieItem]>
}
private let service: NetworkDataFetcher
init(service: NetworkDataFetcher = NetworkDataFetcher(service: NetworkService())) {
self.service = service
}
func transform(input: Input) -> Output {
let loading = ActivityIndicator()
let movies = input.viewDidLoad
.flatMap { _ in
self.service.getMovies(page: 1)
.trackActivity(loading)
.asDriver(onErrorJustReturn: [])
}
let errorResponse = movies
return Output(loading: loading.asDriver(),movies: movies)
}
}
this is how I bind the viewModel in viewController
let input = PopularViewModel.Input(viewDidLoad: rx.viewDidLoad.asDriver())
let output = viewModel.transform(input: input)
output.movies.drive { [weak self] movies in
guard let self = self else { return }
self.populars = movies
self.updateData(on: movies)
}.disposed(by: disposeBag)
output.loading
.drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
.disposed(by: disposeBag)
You do this the same way you handled the ActivityIndicator...
The ErrorRouter type below can be found here.
This is such a common pattern that I have created an API class that takes care of this automatically.
class PopularViewModel: ViewModelType {
struct Input {
let viewDidLoad: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let movies: Driver<[MovieItem]>
let displayAlertMessage: Driver<String>
}
private let service: NetworkDataFetcher
init(service: NetworkDataFetcher = NetworkDataFetcher(service: NetworkService())) {
self.service = service
}
func transform(input: Input) -> Output {
let loading = ActivityIndicator()
let errorRouter = ErrorRouter()
let movies = input.viewDidLoad
.flatMap { [service] in
service.getMovies(page: 1)
.trackActivity(loading)
.rerouteError(errorRouter)
.asDriver(onErrorRecover: { _ in fatalError() })
}
let displayAlertMessage = errorRouter.error
.map { $0.localizedDescription }
.asDriver(onErrorRecover: { _ in fatalError() })
return Output(
loading: loading.isActive.asDriver(onErrorRecover: { _ in fatalError() }),
movies: movies,
displayAlertMessage: displayAlertMessage
)
}
}

How to run two snapshot listeners functions same time on start and after it separately

I have two snapshot listeners and I need to run them in same completion block to get data to same array on first time when application starts. After application is started and listeners are listening I need to run functions separately. I cannot use completion block because if data changes on fetchOwnGames function it also calls another fetchFriendsGames function.
func fetchData(completion: #escaping () -> Void) {
if games.count == 0 {
self.fetchOwnGames {
self.fetchFriendsGames {
completion()
}
}
}
}
Also I cannot use dispatchGroup because if function completion called dispatchGroup.leave() function is getting error
func fetchData(completion: #escaping () -> Void) {
if games.count == 0 {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
fetchOwnGames {
dispatchGroup.leave()
}
dispatchGroup.enter()
fetchFriendsGames {
dispatchGroup.leave()
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion()
}
}
}
How I can call functions separately but data comes same time.
Here is my fetchOwnGames and fetchFriendGames functions
func fetchOwnGames(completion: #escaping () -> Void) {
guard let ownUid = UserService.shared.currentUser?.id else { return }
ownListener = Constants.FirebaseCollection.gamesCollection
.order(by: "startTime")
.whereField("ownerUid", isEqualTo: ownUid)
.limit(toLast: 5)
.addSnapshotListener { [self] querySnapshot, error in
guard let querySnapshot = querySnapshot, error == nil else {
print("DEBUG: error", error?.localizedDescription as Any)
return
}
querySnapshot.documentChanges.forEach { (change) in
switch change.type {
case .added:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games.append(data)
self.games = games.sorted(by: { $0.endTime.compare($1.endTime) == .orderedDescending})
self.ownGames.append(data)
SettingsManager.shared.gamesCount = ownGames.count
case .modified:
guard let data = try? change.document.data(as: Game.self) else { return }
if let index = self.games.firstIndex(where: { $0.courseId == data.courseId }) {
self.games[index] = data
}
case .removed:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games = self.games.filter { $0 != data }
self.games = games.sorted(by: { $0.endTime.compare($1.endTime) == .orderedDescending})
SettingsManager.shared.gamesCount = games.count
}
}
print("DEBUG2: owngames count", ownGames.count)
completion()
}
}
func fetchFriendsGames(completion: #escaping () -> Void) {
userService.fetchFriends(friendCompletion: { [self] friends in
let friendsUidArray = friends.map { $0.id }
if friendsUidArray.count == 0 {
completion()
} else {
for uid in friendsUidArray {
guard let uid = uid else { return }
friendListener = Constants.FirebaseCollection.gamesCollection
.order(by: "startTime", descending: true)
.whereField("ownerUid", isEqualTo: uid)
.limit(to: 5)
.addSnapshotListener({ querySnapshot, error in
guard let querySnapshot = querySnapshot, error == nil else {
print("DEBUG: error", error?.localizedDescription as Any)
return
}
querySnapshot.documentChanges.forEach { change in
switch change.type {
case .added:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games.append(data)
self.friendGames.append(data)
self.games = games.sorted(by: { $0.startTime.compare($1.startTime) == .orderedDescending})
case .modified:
guard let data = try? change.document.data(as: Game.self) else { return }
if let index = self.games.firstIndex(where: { $0.courseId == data.courseId }) {
self.games[index] = data
}
case .removed:
guard let data = try? change.document.data(as: Game.self) else { return }
self.games = self.games.filter { $0 != data }
self.games = games.sorted(by: { $0.endTime.compare($1.endTime) == .orderedDescending})
}
}
print("DEBUG3: friendgames count", friendGames.count)
completion()
})
}
}
})
}
Got it work with adding boolean checker.
private var appStarted = false
func fetchData(completion: #escaping () -> Void) {
if games.count == 0 {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
fetchOwnGames {
if !self.appStarted {
dispatchGroup.leave()
}
}
dispatchGroup.enter()
fetchFriendsGames {
if !self.appStarted {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.appStarted = true
completion()
}
}
}

Update data stored in Firebase using MVVM nested models

I built an MVVM architecture to support my app which is supposed to control the pipeline between the frontend and my firebase database. Initially, I successfully implemented the entire work by coding totally in the frontend, but there are lots of bugs when I encapsulated them into a function.
For example, the next sheet will be presented when the currently presented sheet gets dismissed. Sometimes I needed to wait for a long time until the app is unfrozen. Even worse, the app crashed down when I clicked the button.
I heard that nested models don't work yet if SwiftUI is in use (reference). However, I just cannot come up with a better solution if my classes are untested.
// This is Model
import Foundation
import SwiftUI
struct userModel {
var uid = UUID()
var name = ""
var bio = ""
var interest = ""
var level = 1
var xp = 0
var email = ""
var image: Data = Data(count: 0)
init() {
}
init(_ name:String, _ xp: Int) {
self.name = name
self.xp = xp
self.level = self.xp2Level(xp: xp)
}
func xp2Level(xp:Int) -> Int {
if xp < 9500 {
return xp / 500 + 1
}
else if xp < 29500 {
return (xp - 9500) / 1000 + 1
}
else {
return (xp - 29500) / 2000 + 1
}
}
}
// This is ViewModel
import Foundation
import SwiftUI
import Firebase
class userViewModel: ObservableObject {
#Published var user: userModel = userModel()
#Published var isLoading = false
#AppStorage("status") var status = false
private var ref = Firestore.firestore()
private let store = Storage.storage().reference()
var picker = false
func updateXP(completion: #escaping () -> Int) -> Int {
guard let uid = Auth.auth().currentUser?.uid else {
return 0
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let xp = doc.get("xp") as? Int {
self.user.xp = xp
}
}
}
return completion()
}
func updateLevel(completion: #escaping () -> Int) -> Int {
guard let uid = Auth.auth().currentUser?.uid else {
return 1
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let level = doc.get("level") as? Int {
self.user.level = level
}
}
}
return completion()
}
func updateName (completion: #escaping () -> String) -> String {
guard let uid = Auth.auth().currentUser?.uid else {
return ""
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let name = doc.get("username") as? String {
self.user.name = name
}
}
}
return completion()
}
func updateBio (completion: #escaping () -> String) -> String {
guard let uid = Auth.auth().currentUser?.uid else {
return ""
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let bio = doc.get("bio") as? String {
self.user.bio = bio
}
}
}
return completion()
}
func updateInterest (completion: #escaping () -> String) -> String {
guard let uid = Auth.auth().currentUser?.uid else {
return ""
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let interest = doc.get("interest") as? String {
self.user.interest = interest
}
}
}
return completion()
}
func updatePhoto (completion: #escaping () -> Data) -> Data {
guard let uid = Auth.auth().currentUser?.uid else {
return Data(count: 0)
}
// catch the information of the current user
let db = ref.collection("Users")
db.addSnapshotListener { [self] (querySnapshot, error) in
guard (querySnapshot?.documents) != nil else {
print("Document is empty")
return
}
let docRef = db.document(uid)
docRef.getDocument { (snapshot, error) in
if snapshot != nil {
let imageRef = store.child("profile_Photos").child(uid)
imageRef.getData(maxSize: 1000 * 64 * 64, completion: { (data, error) in
if let error = error {
print("Encountered error: \(error) when getting image")
self.user.image = Data(count: 0)
} else if let data = data,
!data.isEmpty{
// self.currentUser.image = Image(uiImage: UIImage(data: data)!).resizable()
self.user.image = data
} else {
// self.currentUser.image = Image(systemName: "person").resizable()
self.user.image = Data(count: 0)
}
})
} else if let error = error {
print(error)
}
}
}
return completion()
}
public func getXP() -> Int{
updateXP {
return (self.user.xp) as Int
}
}
public func getLevel() -> Int {
updateLevel(completion: {
return (self.user.level) as Int
})
}
public func getName() -> String {
updateName(completion: {
return (self.user.name) as String
})
}
public func getBio() -> String {
updateBio(completion: {
return (self.user.bio) as String
})
}
public func getInterest() -> String {
updateInterest(completion: {
return (self.user.interest) as String
})
}
public func getPhoto() -> Data {
updatePhoto(completion: {
return (self.user.image) as Data
})
}
func updatePersonalInfo() {
//sending user data to Firebase
let uid = Auth.auth().currentUser?.uid
isLoading = true
self.uploadImage(imageData: self.getPhoto(), path: "profile_Photos") { (url) in
self.ref.collection("Users").document(uid ?? "").setData([
"uid": uid ?? "",
"imageurl": url,
"username": self.user.name,
"bio": self.user.bio,
"interest" : self.user.interest
], merge: true) { (err) in
if err != nil{
self.isLoading = false
return
}
self.isLoading = false
// success means settings status as true...
self.status = true
}
}
}
func increaseXPnLV() {
//sending user data to Firebase
let uid = Auth.auth().currentUser!.uid
let docRef = ref.collection("Users").document(uid)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
docRef.updateData(["xp": FieldValue.increment(Int64(50))])
// update level
let xp = document.data()!["xp"] as! Int
docRef.updateData(["level": self.user.xp2Level(xp: xp)])
} else {
print("Document does not exist")
}
}
}
func uploadImage(imageData: Data, path: String, completion: #escaping (String) -> ()){
let storage = Storage.storage().reference()
let uid = Auth.auth().currentUser?.uid
storage.child(path).child(uid ?? "").putData(imageData, metadata: nil) { (_, err) in
print("imageData: \(imageData)")
if err != nil{
completion("")
return
}
// Downloading Url And Sending Back...
storage.child(path).child(uid ?? "").downloadURL { (url, err) in
if err != nil{
completion("")
return
}
completion("\(url!)")
}
}
}
}
// This is View
import SwiftUI
import CoreData
import Firebase
import FirebaseFirestore
struct Goals: View {
let current_user_id = Auth.auth().currentUser?.uid
#State private var showingAlert = false
var ref = Firestore.firestore()
#StateObject var currentUser: userViewModel
#StateObject var homeData = HomeViewModel()
#State var txt = ""
#State var edge = UIApplication.shared.windows.first?.safeAreaInsets
#FetchRequest(entity: Goal.entity(), sortDescriptors: [NSSortDescriptor(key: "date",
ascending: true)], animation: .spring()) var results : FetchedResults<Goal>
// let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var greeting : String = "Hello"
#Environment(\.managedObjectContext) var context
var body: some View {
ForEach(results){goal in
Button(action: {
context.delete(goal)
try! context.save()
if current_user_id != nil {
currentUser.updateXPnLV()
self.showingAlert = true
}
}, label: Text("Something")
)
.alert(isPresented: $showingAlert) {
() -> Alert in
Alert(title: Text("Congratulations!"), message: Text("You completed a goal today, XP+50!"), dismissButton: .default(Text("OK")))
}
}
}
}
EDIT
Another error I saw is AttributeGraph precondition failure: attribute failed to set an initial value: 805912, ForEachChild<Array<userInfoModel>, ObjectIdentifier, HStack<VStack<HStack<TupleView<(Text, Divider, Text)>>>>>.
AppStorage is for use in a View while it may appear to be working all the SwiftUI wrappers with the exception of #Published inside an ObservableObject seem to be unreliable outside of a struct that is a View.
https://developer.apple.com/documentation/swiftui/appstorage
As a standard practice all your class and struct should be capitalized so change class CurrentUserViewModel andclass UserInfoModel
Also, change #StateObject var currentUser: currentUserViewModel to #StateObject var currentUser: CurrentUserViewModel = CurrentUserViewModel()
The initialization is the most important part that is missing.
Everything in the ForEach is just floating it isn't within a variable, function or inside the body. Where is your body?
This is probably what the error is talking about. wrap all of that code in a body
var body: some View {
//All the code for the ForEach
}
Your Button seems to be missing a title or label
Remove this line
() -> Alert in
I am sure there are other little things. I suggest you start from scratch in this View and you start putting in code line by line.
Here is a starting point. The Firebase part needs quite a but of work but you should be able to get started by focusing on the code that I commented out and removing the code to mimic a response from Firebase.
All of this is in the FirebaseManager class
Once this is working the rest will work.
The code as is works so you can see it in action with the fake responses
///Keep all the Firebase Code HERE
class FirebaseManager{
//private var ref = Firestore.firestore()
//private let store = Storage.storage().reference()
func retrieveFromDB(collectionName: String, variableName: String, completion: #escaping (Result<Any, Error>) -> Void) {
print(#function)
//This is sample code, likely has errors because I dont have Firebase setup but you can see the logic so you can touch up
// guard let uid = Auth.auth().currentUser?.uid else {
// completion(.failure(FirebaseError.notLoggedIn))
// return
// }
// catch the information of the current user
// let db = ref.collection(collectionName)
// db.addSnapshotListener { [self] (querySnapshot, error) in
//
// if let error = error{
// completion(.failure(error))
// return
// }
// guard (querySnapshot?.documents) != nil else {
// print("Document is empty")
// completion(.failure(FirebaseError.emptyDocument))
// return
// }
// let docRef = db.(document(uid)
//
// docRef.getDocument { (snapshot, error) in
// if let error = error{
// completion(.failure(error))
// return
// }
//
// completion(.success(snapshot.get(variableName)))
// }
// }
//For sample purposes I will mimic response remove this in your actual code
DispatchQueue.main.async {
if variableName == "xp" || variableName == "level"{
completion(.success(Int.random(in: 0...200)))
}else{
let strings = ["apple", "orange", "banana", "kiwi", "startfruit"]
completion(.success(strings.randomElement()!))
}
}
}
///For Int variables
func retrieveFromUsers(variableName: String, completion: #escaping (Result<Int, Error>) -> Void) {
print(#function)
retrieveFromDB(collectionName: "Users", variableName: variableName, completion: {result in
switch result {
case .success(let value):
let xp = value as? Int
if xp != nil{
completion(.success(xp!))
}else{
completion(.failure(FirebaseError.wrongType))
}
return
case .failure(let error):
print(error)
completion(.failure(error))
}
})
}
///For String variables
func retrieveUserProperty(variableName: String, completion: #escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveFromDB(collectionName: "Users", variableName: variableName, completion: {result in
switch result {
case .success(let value):
let username = value as? String
if username != nil{
completion(.success(username!))
}else{
completion(.failure(FirebaseError.wrongType))
}
return
case .failure(let error):
print(error)
completion(.failure(error))
}
})
}
func retrieveXP(completion: #escaping (Result<Int, Error>) -> Void) {
print(#function)
retrieveFromUsers(variableName: "xp", completion: completion)
}
func retrieveLevel(completion: #escaping (Result<Int, Error>) -> Void) {
print(#function)
retrieveFromUsers(variableName: "level", completion: completion)
}
func retrieveName (completion: #escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveUserProperty(variableName: "username", completion: completion)
}
func retrieveBio (completion: #escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveUserProperty(variableName: "bio", completion: completion)
}
func retrieveInterest (completion: #escaping (Result<String, Error>) -> Void) {
print(#function)
retrieveUserProperty(variableName: "interest", completion: completion)
}
//Database code to retrieve Image needs to be added
func updateDB(collectionName: String, variableName: String, incrementBy: Int, completion: #escaping (Result<Int, Error>) -> Void) {
print(#function)
//sending user data to Firebase
// let uid = Auth.auth().currentUser!.uid
// let docRef = ref.collection(collectionName).document(uid)
// docRef.getDocument { (document, error) in
// if let document = document, document.exists {
// docRef.updateData([variableName: FieldValue.increment(incrementBy)])
//let newValue = document.data()![variableName] as! Int
// completion(.success(newValue))
// } else {
// completion(.failure(FirebaseError.documentDoesntExist))
// }
// }
//For sample purposes I will mimic response remove this in your actual code
DispatchQueue.main.async {
completion(.success(Int.random(in: 0...200) + incrementBy))
}
}
func updateDB(collectionName: String, variableName: String, value: String, completion: #escaping (Result<String, Error>) -> Void) {
print(#function)
//sending user data to Firebase
// let uid = Auth.auth().currentUser!.uid
// let docRef = ref.collection(collectionName).document(uid)
// docRef.getDocument { (document, error) in
// if let document = document, document.exists {
// docRef.updateData([variableName: value])
//let newValue = document.data()![variableName] as! Int
// completion(.success(newValue))
// } else {
// completion(.failure(FirebaseError.documentDoesntExist))
// }
// }
//For sample purposes I will mimic response remove this in your actual code
DispatchQueue.main.async {
let strings = ["apple", "orange", "banana", "kiwi", "startfruit"]
completion(.success(strings.randomElement()!))
}
}
func updateDB(collectionName: String, variableName: String, value: Int, completion: #escaping (Result<Int, Error>) -> Void) {
print(#function)
//sending user data to Firebase
// let uid = Auth.auth().currentUser!.uid
// let docRef = ref.collection(collectionName).document(uid)
// docRef.getDocument { (document, error) in
// if let document = document, document.exists {
// docRef.updateData([variableName: value])
//let newValue = document.data()![variableName] as! Int
// completion(.success(newValue))
// } else {
// completion(.failure(FirebaseError.documentDoesntExist))
// }
// }
//For sample purposes I will mimic response
DispatchQueue.main.async {
completion(.success(Int.random(in: 0...200)))
}
}
func updateUsers(variableName: String, value: String, completion: #escaping (Result<String, Error>) -> Void) {
print(#function)
updateDB(collectionName: "Users", variableName: variableName, value: value, completion: completion)
}
func updateUsers(variableName: String, value: Int, completion: #escaping (Result<Int, Error>) -> Void) {
print(#function)
updateDB(collectionName: "Users", variableName: variableName, value: value, completion: completion)
}
func updateUsers(variableName: String, incrementBy: Int, completion: #escaping (Result<Int, Error>) -> Void) {
print(#function)
updateDB(collectionName: "Users", variableName: variableName, incrementBy: incrementBy, completion: completion)
}
//Code to update Image will need to be added
}
Here is the rest
import SwiftUI
import CoreData
//Capitalized no other changes here
struct UserModel {
var uid = UUID()
var name = ""
var bio = ""
var interest = ""
var level = 1
var xp = 0
var email = ""
var image: Data = Data(count: 0)
init() {
print(#function)
}
init(_ name:String, _ xp: Int) {
print(#function)
self.name = name
self.xp = xp
self.level = self.xp2Level(xp: xp)
}
func xp2Level(xp:Int) -> Int {
print(#function)
if xp < 9500 {
return xp / 500 + 1
}
else if xp < 29500 {
return (xp - 9500) / 1000 + 1
}
else {
return (xp - 29500) / 2000 + 1
}
}
}
//This is to standardize what comes from your Firebase Code. Try to condense code that is duplicated
enum FirebaseError: Error {
case notLoggedIn
case emptyDocument
case wrongType
case documentDoesntExist
}
Capitalize
class UserViewModel: ObservableObject {
let alertVM = AlertViewModel.shared
#Published var user: UserModel = UserModel()
#Published var isLoading = false
//AppStorage wont work here
var status: Bool{
get{
UserDefaults.standard.bool(forKey: "status")
}
set{
UserDefaults.standard.set(newValue, forKey: "status")
}
}
//Separate all Firebase Code
let firebaseManager = FirebaseManager()
init() {
populateAllVariables()
}
func increaseXPnLV() {
print(#function)
//sending xp to Firebase
firebaseManager.updateUsers(variableName: "xp", incrementBy: 50, completion: {result in
switch result {
case .success(let newXp):
self.user.xp = newXp
//sending level to Firebase
self.firebaseManager.updateUsers(variableName: "level", value: self.user.xp2Level(xp: newXp), completion: {result in
switch result {
case .success(let newLevel):
print("newLevel = \(newLevel)")
self.user.level = newLevel
self.alertVM.scheduleAlert(title: "Congratulations!", message: "You completed a goal today, XP+50!")
return
case .failure(let error as NSError):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
return
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
func populateAllVariables() {
print(#function)
getXP()
getLevel()
getName()
getBio()
getInterest()
}
public func getXP() {
print(#function)
firebaseManager.retrieveXP(completion: {result in
switch result {
case .success(let xp):
self.user.xp = xp
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
public func getLevel() {
print(#function)
firebaseManager.retrieveLevel(completion: {result in
switch result {
case .success(let level):
self.user.level = level
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
public func getName() {
print(#function)
firebaseManager.retrieveName(completion: {result in
switch result {
case .success(let name):
self.user.name = name
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
public func getBio() {
print(#function)
firebaseManager.retrieveBio(completion: {result in
switch result {
case .success(let bio):
self.user.bio = bio
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
public func getInterest() {
print(#function)
firebaseManager.retrieveInterest(completion: {result in
switch result {
case .success(let interest):
self.user.interest = interest
case .failure(let error):
//Show alert here
self.alertVM.scheduleAlert(error: error)
print(error)
}
})
}
///This will need work
// public func getPhoto() -> Data {
// updatePhoto(completion: {
// return (self.user.image) as Data
// })
// }
//It is best to separate work from the View
func deleteGoal(moc: NSManagedObjectContext, goal: Goal) -> Bool{
print(#function)
var result = false
moc.performAndWait {
moc.delete(goal)
do{
try moc.save()
result = true
}catch{
self.alertVM.scheduleAlert(error: error)
result = false
}
}
return result
}
}
//This is to centralize alerts. When you are using the web there will be errors and therefore alerts that the user should be aware of
struct CustomAlert: Identifiable {
let id: UUID = UUID()
let title: String
let message: String
let dismissButtonTitle: String
}
//Again to centralize the alerts. I like putting this on the uppermost View so you can send alerts from anywhere
class AlertViewModel: ObservableObject {
//Singleton keeps everything connected
static let shared: AlertViewModel = AlertViewModel()
#Published var currentAlert: CustomAlert?
private init() {
//Required because you need to share the instance
}
//Use this for a custom message
func scheduleAlert(title: String = "ERROR", message: String, dismissButtonTitle: String = "OK") {
currentAlert = CustomAlert(title: title, message: message, dismissButtonTitle: dismissButtonTitle)
}
//Use this if you have a fully formed Error
func scheduleAlert(error: Error) {
let error = error as NSError
currentAlert = CustomAlert(title: "ERROR", message: (error.localizedFailureReason ?? "") + error.localizedDescription + (error.localizedRecoverySuggestion ?? ""), dismissButtonTitle: "OK")
}
}
struct Goals: View {
//The View should never be aware of where your data is stored. all Firebase code should be removed
#StateObject var currentUser: UserViewModel = UserViewModel()
//This observes the alerts that are sent from anywhere
#StateObject var alertVM: AlertViewModel = AlertViewModel.shared
//No code provided
//#StateObject var homeData = HomeViewModel()
#FetchRequest(entity: Goal.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Goal.date, ascending: true)], animation: .spring()) var results : FetchedResults<Goal>
#Environment(\.managedObjectContext) var context
var body: some View {
VStack{
Text("Name = " + currentUser.user.name.description)
Text("Level = " + currentUser.user.level.description)
Text("XP = " + currentUser.user.xp.description)
ForEach(results){goal in
Button(action: {
//There should be some kind of check here to make sure the goal got deleted
if currentUser.deleteGoal(moc: context, goal: goal){
//No code provided
//if current_user_id != nil {
currentUser.increaseXPnLV()
}
//}
}, label: {
//Missing Brackets
Text("Goal \(goal.name?.description ?? "") Completed")
})
//This gets presented from everywhere
.alert(item: $alertVM.currentAlert, content: {current in
Alert(title: Text(current.title), message: Text(current.message), dismissButton: .cancel(Text(current.dismissButtonTitle)))
})
}
}
}
}

Migrating to Combine causes Snapshotlistener to fail

I am migrating the codebase to Combine and the snapshotlistener isn't working i.e. when the document is updated in the back-end, it doesn't get reflected in the app.
This is where I call the service from the Controller:
#objc func loadPlans() {
guard let userEmail = userEmail else { return }
getPlansToken = PlanService.sharedInstance.queryPlans(userEmail: userEmail)
.receive(on: DispatchQueue.main)
.sink { (completion) in
switch completion {
case .failure(let error):
self.showAlert(alertTitle: "Error", message: error.localizedDescription)
case .finished:
print("Publisher stopped observing")
}
} receiveValue: { (mealplans) in
self.mealplanArray.removeAll()
self.mealplanArray = mealplans.map({return PlanViewModel(mealplan: $0)})
self.planTableView.reloadData()
self.refreshControl.endRefreshing()
}
}
And this is the service:
var listener : ListenerRegistration!
let db = Firestore.firestore()
static let sharedInstance = PlanService()
func queryPlans(userEmail: String) -> Future<[Mealplan], Error> {
var mealplans = [Mealplan]()
return Future { promise in
self.listener = self.db.collection("Meal_Plans")
.whereField("userId", isEqualTo: userEmail)
.order(by: "timeOfCreation", descending: true)
.addSnapshotListener({ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No mealplans")
return
}
mealplans = documents.compactMap { queryDocumentSnapshot -> Mealplan? in
return try? queryDocumentSnapshot.data(as: Mealplan.self)
}
promise(.success(self.updateTimeZone(source: mealplans)))
if let error = error {
promise(.failure(error))
}
})
}
}

RxSwift to juggle local database and remote network?

Currently I have services that manages retrieving data from the local storage, but also checks the remote network for any modified data. It is using a completion handler with Result pattern and protocol type, but would like to convert this to an observable approach.
Here is the current logic:
struct AuthorWorker: AuthorWorkerType, Loggable {
private let store: AuthorStore
private let remote: AuthorRemote
init(store: AuthorStore, remote: AuthorRemote) {
self.store = store
self.remote = remote
}
}
extension AuthorWorker {
func fetch(id: Int, completion: #escaping (Result<AuthorType, DataError>) -> Void) {
store.fetch(id: id) {
// Immediately return local response
completion($0)
guard case .success(let cacheElement) = $0 else { return }
// Sync remote updates to cache if applicable
remote.fetch(id: id) {
// Validate if any updates occurred and return
guard case .success(let element) = $0,
element.modifiedAt > cacheElement.modifiedAt else {
return
}
// Update local storage with updated data
self.store.createOrUpdate(element) {
guard case .success = $0 else { return }
// Callback handler again if updated
completion($0)
}
}
}
}
}
I'm always instantly returning the local data to the UI so the user doesn't wait. In the background, it is checking the remote network for modified data and updates the UI again only if necessary. I use it like this:
authorWorker.fetch(1) { [weak self] in
guard case .success(let value) = $0 else {
// alert error
}
self?.myLabel.text = value.name
}
How can this be converted to RxSwift or an observable concept? This is what I got started, but I don't see the code on the walls like Neo yet when it comes to Rx, so I need help seeing the light.
extension AuthorWorker {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable<AuthorType>.create { observer in
store.fetch(id: id) {
// Immediately return local response
observer.on(.next($0))
guard case .success(let cacheElement) = $0 else {
observer.on(.completed)
return
}
// Sync remote updates to cache if applicable
remote.fetch(id: id) {
// Validate if any updates occurred and return
guard case .success(let element) = $0,
element.modifiedAt > cacheElement.modifiedAt else {
observer.on(.completed)
return
}
// Update local storage with updated data
self.store.createOrUpdate(element) {
guard case .success = $0 else {
observer.on(.completed)
return
}
// Callback handler again if updated
observer.on(.next($0))
observer.on(.completed)
}
}
}
}
}
}
Then I would use it like this?
authorWorker.fetch(1).subscribe { [weak self] in
guard let element = $0.element else {
// Handle error how?
return
}
self?.myLabel.text = element.name
}
Is this the right approach or is there a more recommended way to do this? Is it also worth converting the underlying remote and local stores to observable as well, or does it make sense not to convert all things to observable all the time?
New Answer
Based on the comment, I see that you want something much more elaborate than my first answer, so here you go.
func worker<T: Equatable>(store: Observable<T>, remote: Observable<T>) -> (value: Observable<T>, store: Observable<T>) {
let sharedStore = store.share(replay: 1)
let sharedRemote = remote.share(replay: 1)
let value = Observable.merge(sharedStore, sharedRemote)
.distinctUntilChanged()
.takeUntil(sharedRemote.materialize().filter { $0.isStopEvent })
let store = Observable.zip(sharedStore, sharedRemote)
.filter { $0.0 != $0.1 }
.map { $0.1 }
return (value: value, store: store)
}
Here is the code above being used in your AuthorWorker class:
extension AuthorWorker {
func fetch(id: Int) -> Observable<AuthorType> {
let (_value, _store) = worker(store: store.fetch(id: id), remote: remote.fetch(id: id))
_ = _store
.subscribe(onNext: store.createOrUpdate)
return _value
}
}
And here is a test suite proving it works properly:
class Tests: XCTestCase {
var scheduler: TestScheduler!
var emission: TestableObserver<String>!
var storage: TestableObserver<String>!
var disposeBag: DisposeBag!
override func setUp() {
super.setUp()
scheduler = TestScheduler(initialClock: 0)
emission = scheduler.createObserver(String.self)
storage = scheduler.createObserver(String.self)
disposeBag = DisposeBag()
}
func testHappyPath() {
let storeProducer = scheduler.createColdObservable([.next(10, "store"), .completed(11)])
let remoteProducer = scheduler.createColdObservable([.next(20, "remote"), .completed(21)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "store"), .next(20, "remote"), .completed(21)])
XCTAssertEqual(storage.events, [.next(20, "remote"), .completed(21)])
}
func testSameValue() {
let storeProducer = scheduler.createColdObservable([.next(10, "store"), .completed(11)])
let remoteProducer = scheduler.createColdObservable([.next(20, "store"), .completed(21)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "store"), .completed(21)])
XCTAssertEqual(storage.events, [.completed(21)])
}
func testRemoteFirst() {
let storeProducer = scheduler.createColdObservable([.next(20, "store"), .completed(21)])
let remoteProducer = scheduler.createColdObservable([.next(10, "remote"), .completed(11)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "remote"), .completed(11)])
XCTAssertEqual(storage.events, [.next(20, "remote"), .completed(21)])
}
func testRemoteFirstSameValue() {
let storeProducer = scheduler.createColdObservable([.next(20, "store"), .completed(21)])
let remoteProducer = scheduler.createColdObservable([.next(10, "store"), .completed(11)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "store"), .completed(11)])
XCTAssertEqual(storage.events, [.completed(21)])
}
}
Previous Answer
I'd be inclined to aim for a usage like this:
let result = authorWorker.fetch(id: 1)
.share()
result
.map { $0.description }
.catchErrorJustReturn("")
.bind(to: myLabel.rx.text)
.disposed(by: disposeBag)
result
.subscribe(onError: { error in
// handle error here
})
.disposed(by: disposeBag)
The above can be accomplished if you have something like the below for example:
extension AuthorWorker {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable.merge(store.fetch(id: id), remote.fetch(id: id))
.distinctUntilChanged()
}
}
extension AuthorStore {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable.create { observer in
self.fetch(id: id, completion: { result in
switch result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
}
})
return Disposables.create()
}
}
}
extension AuthorRemote {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable.create { observer in
self.fetch(id: id, completion: { result in
switch result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
}
})
return Disposables.create()
}
}
}