I have a search function in my app which works perfectly except for the fact that it seems to be one reload behind. For example if I type in 'S' nothing will show but if I then type an 'a' after all the results for 'S' will show up. I tried adding collectionView reload at the end of the fetch function but if I do that nothing shows up period.
#objc func textFieldDidChange(_ textField: UITextField) {
guard let text = textField.text else { return }
if text.count == 0 {
self.posts.removeAll()
self.filteredPosts.removeAll()
} else {
fetchSearchedPosts(searchTerm: text)
}
self.collectionView.reloadData()
}
func fetchSearchedPosts(searchTerm: String) {
self.collectionView.refreshControl?.endRefreshing()
let ref = Database.database().reference().child("posts").queryOrdered(byChild: "title").queryStarting(atValue: searchTerm).queryEnding(atValue: "\(searchTerm)\u{f8ff}")
ref.observeSingleEvent(of: .value) { (snapshot) in
if !snapshot.exists() { return }
guard let dictionaries = snapshot.value as? [String: Any] else { return }
self.posts.removeAll()
dictionaries.forEach({ (key, value) in
guard let postDictionary = value as? [String: Any] else { return }
guard let uid = postDictionary["uid"] as? String else { return }
Database.fetchUserWithUID(uid: uid, completion: { (user) in
let post = Post(postId: key, user: user, dictionary: postDictionary)
let nowTimeStamp = Date().timeIntervalSince1970
let dateTime = post.endTimeDate
let timeStamp = dateTime.timeIntervalSince1970
if nowTimeStamp < timeStamp {
post.id = key
self.posts.append(post)
} else {
return
}
})
self.posts.sort(by: { (post1, post2) -> Bool in
return post1.title.compare(post2.title) == .orderedAscending
})
})
self.collectionView.reloadData()
}
}
You need a DispatchGroup as you have nested asynchronous calls
func fetchSearchedPosts(searchTerm: String) {
self.collectionView.refreshControl?.endRefreshing()
let ref = Database.database().reference().child("posts").queryOrdered(byChild: "title").queryStarting(atValue: searchTerm).queryEnding(atValue: "\(searchTerm)\u{f8ff}")
ref.observeSingleEvent(of: .value) { (snapshot) in
if !snapshot.exists() { return }
guard let dictionaries = snapshot.value as? [String: Any] else { return }
self.posts.removeAll()
let g = DispatchGroup() ///// 1
dictionaries.forEach({ (key, value) in
guard let postDictionary = value as? [String: Any] else { return }
guard let uid = postDictionary["uid"] as? String else { return }
g.enter() ///// 2
Database.fetchUserWithUID(uid: uid, completion: { (user) in
let post = Post(postId: key, user: user, dictionary: postDictionary)
let nowTimeStamp = Date().timeIntervalSince1970
let dateTime = post.endTimeDate
let timeStamp = dateTime.timeIntervalSince1970
if nowTimeStamp < timeStamp {
post.id = key
self.posts.append(post)
} else {
g.leave() ///// 3.a
return
}
g.leave() ///// 3.b
})
})
g.notify(queue:.main) { ///// 4
self.posts.sort(by: { (post1, post2) -> Bool in
return post1.title.compare(post2.title) == .orderedAscending
})
self.collectionView.reloadData()
}
}
}
Related
Whenever paging in tableview, the view model is running fetchDataRx. It works well, but I don't think it's right to sort the entire data every time you paginate and call the addSnapShotListener. If my code is not correct, how can I correct it?
// MARK: ViewController.swift
timeLineTableView.rx.didScroll
.withLatestFrom(viewModel.activated)
.subscribe(onNext: { [weak self] isActivated in
if !isActivated {
guard let self = self else { return }
let position = self.timeLineTableView.contentOffset.y
if position > self.timeLineTableView.contentSize.height - 100 - self.timeLineTableView.frame.size.height {
self.viewModel.fetchPosts.onNext(())
}
}
})
.disposed(by: disposeBag)
//MARK: ViewModel.swift
let fetchPosts: AnyObserver<Void>
let fetching = PublishSubject<Void>()
fetchPosts = fetching.asObserver()
fetching
.do(onNext: { _ in activating.onNext(true) })
.withLatestFrom(posts)
.map { $0.count }
.flatMap{ (count) -> Observable<[post]> in
fireBaseService.fetchDataRx(startIdx: count) }
.map { $0.map { ViewPost(post: $0) } }
.do(onNext: { _ in activating.onNext(false) })
.do(onError: { err in error.onNext(err) })
.subscribe(onNext: { newPosts in
let oldData = posts.value
posts.accept(oldData + newPosts)
})
.disposed(by: disposeBag)
//MARK: FirebaseService.swift
protocol FirebaseServiceProtocol {
func fetchDataRx(startIdx: Int) -> Observable<[post]>
func fetchData(startIdx: Int, completion: #escaping (Result<[post], Error>) -> Void)
}
class FireBaseService: FirebaseServiceProtocol {
func fetchDataRx(startIdx: Int) -> Observable<[post]> {
return Observable.create { (observer) -> Disposable in
self.fetchData(startIdx: startIdx) { result in
switch result {
case .success(let data):
observer.onNext(data)
case .failure(let error):
observer.onError(error)
}
observer.onCompleted()
}
return Disposables.create()
}
}
func fetchData(startIdx: Int, completion: #escaping (Result<[post], Error>) -> Void) {
let db = Firestore.firestore()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
if startIdx == 0 {
DispatchQueue.global().async {
let first = db.collection("lolCourt")
.order(by: "date")
.limit(to: 8)
var nextPosts = [post]()
first.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
guard let url = document.data()["url"] as? String else {
continue
}
guard let champion1 = document.data()["champion1"] as? String else {
continue
}
guard let champion1Votes = document.data()["champion1Votes"] as? Double else {
continue
}
guard let champion2 = document.data()["champion2"] as? String else {
continue
}
guard let champion2Votes = document.data()["champion2Votes"] as? Double else {
continue
}
guard let text = document.data()["text"] as? String else {
continue
}
guard let date = document.data()["date"] as? Double else {
continue
}
nextPosts.append(post(url: url,
champion1: champion1,
champion1Votes: champion1Votes,
champion2: champion2,
champion2Votes: champion2Votes,
text: text,
date: formatter.string(from: Date(timeIntervalSince1970: date))))
}
}
completion(.success(nextPosts))
}
}
}
else {
DispatchQueue.global().async {
let first = db.collection("lolCourt")
.order(by: "date")
.limit(to: startIdx)
first.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving : \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
return
}
let next = db.collection("lolCourt")
.order(by: "date")
.start(afterDocument: lastSnapshot)
.limit(to: 8)
var nextPosts = [post]()
next.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
guard let url = document.data()["url"] as? String else {
continue
}
guard let champion1 = document.data()["champion1"] as? String else {
continue
}
guard let champion1Votes = document.data()["champion1Votes"] as? Double else {
continue
}
guard let champion2 = document.data()["champion2"] as? String else {
continue
}
guard let champion2Votes = document.data()["champion2Votes"] as? Double else {
continue
}
guard let text = document.data()["text"] as? String else {
continue
}
guard let date = document.data()["date"] as? Double else {
continue
}
nextPosts.append(post(url: url,
champion1: champion1,
champion1Votes: champion1Votes,
champion2: champion2,
champion2Votes: champion2Votes,
text: text,
date: formatter.string(from: Date(timeIntervalSince1970: date))))
}
}
completion(.success(nextPosts))
}
}
}
}
}
}
I think you are doing it wrong... I would expect to see something more like this:
class FireBaseService {
func getPage<T>(query: Query? = nil, build: #escaping ([String: Any]) -> T) -> Observable<([T], nextPage: Query?)> {
Observable.create { observer in
let db = Firestore.firestore()
let page = query ?? db.collection("lolCourt")
.order(by: "date")
.limit(to: 8)
let listener = page
.addSnapshotListener { snapshot, error in
guard let snapshot = snapshot else { observer.onError(error ?? RxError.unknown); return }
let items = snapshot.documents.map { build($0.data()) }
if let lastSnapshot = snapshot.documents.last {
let next = page
.start(afterDocument: lastSnapshot)
observer.onSuccess((items, nextPage: next))
}
else {
observer.onSuccess((items, nextPage: nil))
}
}
return Disposables.create { listener.remove() }
}
}
}
Use the above in your favorite state machine system. Here is an example using my CLE library.
// in view controller
let fireBaseService = FireBaseService()
let activityIndicator = ActivityIndicator()
let errorRouter = ErrorRouter()
func getPage(nextPage: Query?) -> Observable<([Post?], nextPage: Query?)> {
fireBaseService.getPage(query: nextPage, build: Post.init(dict:))
.rerouteError(errorRouter)
.trackActivity(activityIndicator)
}
let posts = cycle(
inputs: [
getPage(nextPage: nil).map(ViewModel.Input.response),
timeLineTableView.rx.reachedBottom(offset: 20).map(to: ViewModel.Input.next)
],
initialState: ([Post?](), nextPage: Query?.none),
environment: getPage(nextPage:),
reduce: ViewModel.reduce(state:input:getPage:)
)
.map { $0.0.compactMap { $0 } }
and the view model:
enum ViewModel {
enum Input {
case response([Post?], nextPage: Query?)
case next
}
static func reduce(state: inout ([Post?], nextPage: Query?), input: Input, getPage: #escaping (Query) -> Observable<([Post?], nextPage: Query?)>) -> Observable<Input> {
switch input {
case let .response(posts, nextPage):
state.0 += posts
state.nextPage = nextPage
case .next:
guard let nextPage = state.nextPage else { break }
return getPage(nextPage)
.map(Input.response)
}
return .empty()
}
}
I'm trying to filter my firebase data within the table view to group items that have the same category (child value) after each other. Currently, it's not returning the values based on the child value. I think it's being done in random order.
Here is my code:
How I'm attempting to filter the data:
self.items.append(item)
self.items.sort(by: { (item1, item2) -> Bool in
return item1.category.compare(item2.category) == .orderedSame
})
self.tableView.reloadData()
Model:
struct Item {
var id: String?
var user: User
var fromId: String?
let item: String
let category: String
let creationDate: Date
init(user: User, dictionary: [String: Any]) {
self.user = user
self.item = dictionary["item"] as? String ?? ""
self.fromId = dictionary["fromId"] as? String ?? ""
self.category = dictionary["category"] as? String ?? ""
let secondsFrom1970 = dictionary["creationDate"] as? Double ?? 0
self.creationDate = Date(timeIntervalSince1970: secondsFrom1970)
}
}
Fetch function:
fileprivate func fetchListItems() {
self.tableView.refreshControl?.endRefreshing()
guard let uid = Auth.auth().currentUser?.uid else { return }
guard let listId = list?.id else { return }
let ref = Database.database().reference().child("lists").child(listId).child("list-items")
ref.observe(.childAdded) { (snapshot) in
let itemId = snapshot.key
let itemRef = Database.database().reference().child("items").child(itemId)
itemRef.observeSingleEvent(of: .value, with: { (snapshot) in
let itemId = snapshot.key
guard let dictionary = snapshot.value as? [String: Any] else { return }
guard let uid = dictionary["fromId"] as? String else { return }
Database.fetchUserWithUID(uid: uid, completion: { (user) in
var item = Item(user: user, dictionary: dictionary)
item.id = snapshot.key
self.items.append(item)
self.items.sort(by: { (item1, item2) -> Bool in
return item1.category.compare(item2.category) == .orderedSame
})
self.tableView.reloadData()
ref.keepSynced(true)
})
})
}
}
Database:
When the function to create or delete a list is called inside of my app the remaining lists are duplicated and displayed multiple times within the collectionView until the app is reloaded. I only called the fetchLists function twice, in the viewDidLoad and in the pull to refresh function. On pull to refresh the lists return to normal.
Fetch list:
fileprivate func fetchLists() {
self.collectionView?.refreshControl?.endRefreshing()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
let ref = Database.database().reference().child("list-feed").child(currentUid)
ref.observe(.value) { (listFeedSnapshot) in
guard var allObjects = listFeedSnapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.forEach({ (allObjectsSnapshot) in
let listId = allObjectsSnapshot.key
let listRef = Database.database().reference().child("lists").child(listId)
listRef.observeSingleEvent(of: .value, with: { (snapshot) in
guard let dict = snapshot.value as? [String: Any] else { return }
guard let uid = dict["uid"] as? String else { return }
Database.fetchUserWithUID(uid: uid, completion: { (user) in
guard let dictionary = snapshot.value as? [String: Any] else { return }
var list = List(user: user, dictionary: dictionary)
let listId = snapshot.key
list.id = snapshot.key
self.list = list
self.lists.append(list)
self.lists.sort(by: { (list1, list2) -> Bool in
return list1.creationDate.compare(list2.creationDate) == .orderedDescending
})
self.collectionView?.reloadData()
ref.keepSynced(true)
listRef.keepSynced(true)
})
})
})
}
}
Create list:
let values = ["uid": uid, "title": listNameText, "creationDate": Date().timeIntervalSince1970] as [String : Any]
ref.updateChildValues(values, withCompletionBlock: { (error, ref) in
if let error = error {
self.navigationItem.rightBarButtonItem?.isEnabled = true
print("failed to save user info into db:", error.localizedDescription)
return
}
let memberValues = [uid : 1]
ref.child("list-members").updateChildValues(memberValues)
self.handleUpdateFeeds(with: ref.key!)
self.handleListFeeds(with: ref.key!)
print("successfully created list in db")
Update feeds:
func handleUpdateFeeds(with listId: String) {
guard let uid = Auth.auth().currentUser?.uid else { return }
let values = [listId: 1]
Database.database().reference().child("list-feed").child(uid).updateChildValues(values)
}
func handleListFeeds(with listId: String) {
guard let uid = Auth.auth().currentUser?.uid else { return }
let values = [listId: 1]
Database.database().reference().child("user-lists").child(uid).updateChildValues(values)
}
Firebase database:
{
"list-feed" : {
"otxFDz0FNbVPpLN27DYBQVP4e403" : {
"-LjeAoHJTrYK7xjwcpJ9" : 1,
"-LjeApq-Mb_d_lAz-ylL" : 1
}
},
"lists" : {
"-LjeAoHJTrYK7xjwcpJ9" : {
"creationDate" : 1.5630020966384912E9,
"title" : "Test 1",
"uid" : "otxFDz0FNbVPpLN27DYBQVP4e403"
},
"-LjeApq-Mb_d_lAz-ylL" : {
"creationDate" : 1.563002101329072E9,
"list-members" : {
"otxFDz0FNbVPpLN27DYBQVP4e403" : 1
},
"title" : "Test 2",
"uid" : "otxFDz0FNbVPpLN27DYBQVP4e403"
}
}
}
Since you're calling ref.observe(, you're attaching a permanent observer to the data. This means that if you call fetchLists a second, you're attaching a second observer and you'll get the same data twice.
If you only want the data to be read once per call to fetchLists, you should use observeSingleEventOfType:
fileprivate func fetchLists() {
self.collectionView?.refreshControl?.endRefreshing()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
let ref = Database.database().reference().child("list-feed").child(currentUid)
ref.observeSingleEvent(of: .value) { (listFeedSnapshot) in
Also see the documentation on reading data once.
My app has an option to save posts for users to watch later. The code is:
func savedPost(for cell: FirstView) {
guard let indexPath = collectionView.indexPath(for: cell) else { return }
var post = self.posts[indexPath.item]
guard let currentuserId = Auth.auth().currentUser?.uid else { return }
let targetUid = post.user.uid
guard let postId = post.id else { return }
let ref = Database.database().reference().child("save_post").child(currentuserId).child(postId)
if post.hasSaved {
ref.removeValue { (err, _) in
if let _ = err {
showErr(info: NSLocalizedString("failtoUnsave", comment: ""), subInfo: tryLater)
return
}
post.hasSaved = false
self.posts[indexPath.item] = post
self.collectionView.reloadItems(at: [indexPath])
}
} else {
let values = ["userId": targetUid]
ref.updateChildValues(values) { (err, ref) in
if let _ = err {
showErr(info: NSLocalizedString("failtoSave", comment: ""), subInfo: tryLater)
}
post.hasSaved = true
self.posts[indexPath.item] = post
self.collectionView.reloadItems(at: [indexPath])
}
}
}
With this code my firebase database in "save_post" has -> currentUseruId -> postid -> postUserId.
On ProfileController users can view saved Posts from "savedPost" Tab. The code is:
var savedPosts = [Post]()
fileprivate func fetchSaved() {
var userIds = [String]()
var postIds = [String]()
guard let uid = self.user?.uid else { return }
let getIDsRef = Database.database().reference().child("save_post").child(uid)
let query = getIDsRef.queryOrderedByKey()
query.observeSingleEvent(of: .value) { (snapshot) in
let dictionary = snapshot.value as? [String: Any]
dictionary?.forEach({ (key,value) in
guard let dic = value as? [String: String] else { return }
postIds.append(key)
userIds.append(dic["userId"] ?? "")
})
var i = 0
while i < userIds.count {
self.fetchPostsWithUserIDPostID(userID: userIds[i], postID: postIds[i])
i += 1
}
}
}
fileprivate func fetchPostsWithUserIDPostID(userID: String, postID: String) {
let getPostRef = Database.database().reference().child("video_list")
getPostRef.child(userID).child(postID).observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: Any] else { return }
let ref = Database.database().reference().child("users").child(userID)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let dict = snapshot.value as? [String: Any] else { return }
let user = User(uid: userID, dictionary: dict)
var post = Post(user: user, dictionary: dictionary)
post.id = postID
guard let currentUserUID = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("likes").child(postID).child(currentUserUID).observeSingleEvent(of: .value, with: { (snapshot) in
if let value = snapshot.value as? Int, value == 1 {
post.hasLiked = true
} else {
post.hasLiked = false
}
post.hasSaved = true
self.savedPosts.append(post)
self.savedPosts.sort(by: { (p1, p2) -> Bool in
return p1.creationDate.compare(p2.creationDate) == .orderedDescending
})
self.collectionView.reloadData()
})
})
})
}
However, when I click "savedPost" tab, there is no post shown. I don't know where my mistake is. I have all the necessary code under all override func collectionView(....). I believe the error should come from the code listed above. I am sincerely looking for help to resolve this issue. Thanks.
There could be a number of things going on here. It would be good to throw some print statements in there to make sure that 1) the data you're getting back from the database looks like what you expect, and 2) that you're properly parsing it into Post objects. Do you have your cells defined properly for your CollectionView? Also, I don't see where you are defining the data source for the CollectionView.
I want to delete a post, if the post has equal/more then 5 dislikes. I implemented the counter part and this works already. But the post will not be deleted even if I have 6 dislikes like on this post below:
Here you can see my database.
This is the code, I want to delete the posts after 5 dislikes:
// Dislike Button
func addTapGestureToDislikeImageView() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDidTapDislike))
dislikeImageView.addGestureRecognizer(tapGesture)
dislikeImageView.isUserInteractionEnabled = true
}
#objc func handleDidTapDislike() {
guard let postId = post?.id else { return }
PostApi.shared.incrementDislikes(postId: postId, onSuccess: { (post) in
self.updateDislike(post: post)
self.post?.dislikes = post.dislikes
self.post?.isDisliked = post.isDisliked
self.post?.dislikeCount = post.dislikeCount
}, onError: { (errorMessage) in
ProgressHUD.showError(errorMessage)
})
}
func updateDislike(post: PostModel) {
if post.isDisliked == false || post.dislikes == nil {
dislikeImageView.image = UIImage(named: "icons8-gefaellt-nicht-50")
} else {
dislikeImageView.image = UIImage(named: "icons8-gefaellt-nicht-filled-50")
}
guard let count = post.dislikeCount else { return }
if count >= 5 {
deletePost()
}
}
func deletePost() {
// Remove the post from the DB
let ref = Database.database().reference()
ref.child("posts").child((post?.id)!).removeValue { error,ref in
if error != nil {
print(error!.localizedDescription)
}
}
}
Here I increment Dislikes:
func incrementDislikes(postId id: String, onSuccess: #escaping (PostModel) -> Void, onError: #escaping (_ errorMessage: String?) -> Void) {
let postRef = REF_POSTS.child(id)
postRef.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
if var post = currentData.value as? [String : AnyObject], let uid = UserApi.shared.CURRENT_USER_ID {
var dislikes: Dictionary<String, Bool>
dislikes = post["dislikes"] as? [String : Bool] ?? [:]
var dislikeCount = post["dislikeCount"] as? Int ?? 0
if let _ = dislikes[uid] {
// Unstar the post and remove self from stars
dislikeCount -= 1
dislikes.removeValue(forKey: uid)
} else {
// Star the post and add self to stars
dislikeCount += 1
dislikes[uid] = true
}
post["dislikeCount"] = dislikeCount as AnyObject?
post["dislikes"] = dislikes as AnyObject?
// Set value and report transaction success
currentData.value = post
return TransactionResult.success(withValue: currentData)
}
return TransactionResult.success(withValue: currentData)
}) { (error, committed, snapshot) in
if let error = error {
onError(error.localizedDescription)
}
guard let dic = snapshot?.value as? [String: Any] else { return }
guard let postId = snapshot?.key else { return }
let updatePost = PostModel(dictionary: dic, key: postId)
onSuccess(updatePost)
}
}
Thanks for your help!
I think you need to share more information, or any error you see, or debug it and test where the action stops. But try this one maybe it will work.
#objc func handleDidTapDislike() {
guard let postId = post?.id else { return }
PostApi.shared.incrementDislikes(postId: postId, onSuccess: { (post) in
self.updateDislike(post: post)
self.post?.dislikes = post.dislikes
self.post?.isDisliked = post.isDisliked
self.post?.dislikeCount = post.dislikeCount
if post.dislikeCount > 4 {
deletePost()
}
}, onError: { (errorMessage) in
ProgressHUD.showError(errorMessage)
})}
and then edit updateDislike() function
func updateDislike(post: PostModel) {
if post.isDisliked == false || post.dislikes == nil {
dislikeImageView.image = UIImage(named: "icons8-gefaellt-nicht-50")
} else {
dislikeImageView.image = UIImage(named: "icons8-gefaellt-nicht-filled-50")
}
}