Kingfisher caches the data in RAM - ios

I do like a news feed, I had the following problem, if for example the user uploads more than 300 news, then the application will already occupy more than 300 megabytes of memory. Once during the test, I did get didReceiveMemoryWarning and it helped only the full cleansing of the dataSource. I also use Kingfisher to cache images. What is the best way for this situation? Cache the first data and if the user will return to the top (to the newest data), then load them from the cache or if some better way? Thanks.
Update: this is news JSON model.
["peopleProperties": ["numberOfPeopleDescription": "Nobody here", "numberOfPeople": 0, "availableSeats": 0], "isPrivateStatus": false, "additionalInfo": ["note": ""], "ownerID": "", "ticketsInfo": ["tickets": []], "isTest": false, "isNewPendingRequest": false, "dateProperties": ["isEditable": true, "iso8601": "", "day": "", "endTimeStamp": 0.0, "isFlexDate": true, "isFlexTime": true, "timeStamp": 0.0], "boolProperties": ["isPartnerGeneratedCard": false, "isAutoGeneratedCard": true, "isUserCreatedCard": false, "isAdminCreatedCard": false], "location": ["formattedAddress": "692 N Robertson Blvd (at Santa Monica Blvd),West Hollywood, CA 90069,United States", "fullLocationName": "692 N Robertson Blvd", "coordinate": ["longitude": -118.38528500025966, "latitude": 34.083373986214625]], "id": "", "photoURLsProperties": ["placePhotoURLs": ["example"], "placeLogoURLs": []], "services": ["serviceURL": "", "serviceID": "41cf5080f964a520a61e1fe3", "index": 1], "version": 1, "title": "The Abbey Food & Bar", "ownerName": "", "phones": [:]]
UPDATE 1. Sometimes it comes to app crash. My test controller
import UIKit
import SVProgressHUD
class CardTestTableViewController: UITableViewController {
// MARK: - Managers
fileprivate let firCardDatabaseManager = FIRCardDatabaseManager()
fileprivate let apiManager = ableCardsAPIManager()
// MARK: - API Manager's properties
fileprivate var firstCardsCount = 0
fileprivate var isSecondTypeRequestLaunch = false
/// Main cards array
fileprivate var cardsModels = [CardModel]()
fileprivate var firCardsModels = [CardModel]()
fileprivate var backendCardsModels = [CardModel]()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
definesPresentationContext = true
requestAllData()
// table view
registerCells()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(false, animated: false)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
debugPrint("didReceiveMemoryWarning")
cardsModels.removeAll()
tableView.reloadData()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if cardsModels.count > 0 {
debugPrint("cardsModels.first!.toJSON()", cardsModels.first!.toJSON(), "cardsModels.first!.toJSON()")
}
return cardsModels.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = cardTestTableViewCell(tableView, indexPath: indexPath)
let lastElement = cardsModels.count - 15
if indexPath.row == lastElement {
secondRequest(indexPath.row)
}
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 298
}
}
extension CardTestTableViewController {
fileprivate func registerCells() {
let nib = UINib(nibName: CardTestTableViewCell.defaultReuseIdentifier, bundle: Bundle.main)
tableView.register(nib, forCellReuseIdentifier: CardTestTableViewCell.defaultReuseIdentifier)
}
}
// MARK: - Cells
extension CardTestTableViewController {
fileprivate func cardTestTableViewCell(_ tableView: UITableView, indexPath: IndexPath) -> CardTestTableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CardTestTableViewCell.defaultReuseIdentifier, for: indexPath) as! CardTestTableViewCell
let card = cardsModels[indexPath.row]
cell.setupData(card)
return cell
}
}
// MARK: - Requests
extension CardTestTableViewController {
#objc fileprivate func requestAllData() {
let requestGroup = DispatchGroup()
if let topViewController = UIApplication.topViewController() {
if topViewController.isKind(of: CardViewController.self) {
SVProgressHUD.show()
}
}
firCardsModels.removeAll()
backendCardsModels.removeAll()
requestGroup.enter()
firCardDatabaseManager.getCardModelsByUserLocation(success: { [weak self] (userCardsModels) in
debugPrint("Finish +++ fir", userCardsModels.count)
self?.firCardsModels = userCardsModels
requestGroup.leave()
}) { (error) in
// TODO: - Think about it: Do not show an error, because we have cards with FourSquare
debugPrint("FIRCardDatabaseManager error", error.localizedDescription)
requestGroup.leave()
}
requestGroup.enter()
apiManager.requestCards(10, secondRequestLimit: 50, isFirstRequest: true, success: { [weak self] (cards) in
self?.backendCardsModels = cards
requestGroup.leave()
}) { (error) in
requestGroup.leave()
}
requestGroup.notify(queue: .main) { [weak self] in
guard let _self = self else { return }
_self.cardsModels.removeAll()
_self.cardsModels.append(contentsOf: _self.firCardsModels)
_self.cardsModels.append(contentsOf: _self.backendCardsModels)
self?.tableView.reloadData()
// for api manager
self?.firstCardsCount = _self.cardsModels.count
SVProgressHUD.dismiss()
}
}
fileprivate func secondRequest(_ index: Int) {
// the second request
debugPrint("swipe index", index, "firstCardsCount", firstCardsCount)
// This is for how much to the end of the deck, we ask for more cards.
let muchMoreIndex = 15
let checkNumber = firstCardsCount-1 - index - muchMoreIndex
debugPrint("checkNumber", checkNumber)
if checkNumber == 0 || checkNumber < 0 {
guard !isSecondTypeRequestLaunch else { return }
isSecondTypeRequestLaunch = true
apiManager.requestCards(0, secondRequestLimit: 50, isFirstRequest: false, success: { [weak self] (backendCards) in
DispatchQueue.main.async {
guard let _self = self else { return }
_self.cardsModels.append(contentsOf: backendCards)
_self.firstCardsCount = _self.cardsModels.count
_self.isSecondTypeRequestLaunch = false
_self.tableView.reloadData()
}
}, fail: { [weak self] (error) in
self?.isSecondTypeRequestLaunch = false
})
}
}
}
import UIKit
import Kingfisher
class CardTestTableViewCell: UITableViewCell {
#IBOutlet private weak var titleLabel: UILabel!
#IBOutlet private weak var cardImageView: UIImageView!
#IBOutlet private weak var profileImageView: UIImageView!
override func prepareForReuse() {
cardImageView.image = nil
profileImageView.image = nil
}
func setupData(_ card: CardModel) {
downloadImages(card)
setupLabelsData(card)
}
private func downloadImages(_ card: CardModel) {
if let placeAvatarURLString = card.photoURLsProperties.placePhotoURLs.first {
if let placeAvatarURL = URL(string: placeAvatarURLString) {
cardImageView.kf.indicatorType = .activity
cardImageView.kf.setImage(with: placeAvatarURL)
} else {
cardImageView.image = UIImage(named: "CardDefaultImage")
}
} else if let eventLogoURLPath = card.photoURLsProperties.placeLogoURLs.first {
if let url = URL(string: eventLogoURLPath) {
cardImageView.kf.indicatorType = .activity
cardImageView.kf.setImage(with: url)
} else {
cardImageView.image = UIImage(named: "CardDefaultImage")
}
} else {
cardImageView.image = UIImage(named: "CardDefaultImage")
}
guard card.boolProperties.isAutoGeneratedCard != true && card.boolProperties.isAdminCreatedCard != true else {
profileImageView.image = #imageLiteral(resourceName: "ProfileDefaultIcon")
return
}
let firImageDatabaseManager = FIRImageDatabaseManager()
firImageDatabaseManager.downloadCardUserProfileImageBy(card.ownerID) { [weak self] (url, error) in
DispatchQueue.main.async {
guard error == nil else {
self?.profileImageView.image = #imageLiteral(resourceName: "ProfileDefaultIcon")
return
}
guard let _url = url else {
self?.profileImageView.image = #imageLiteral(resourceName: "ProfileDefaultIcon")
return
}
self?.profileImageView.kf.indicatorType = .activity
self?.profileImageView.kf.setImage(with: _url)
}
}
}
private func setupLabelsData(_ card: CardModel) {
titleLabel.text = card.title
}
}
Update 2. When I commented out the code that is associated with the Kingfisher framework, then there is no memory leak and application crash.

I solved my problem, in fact Kingfisher initially stores all images in RAM, it has the property that if the application received a memory warning, then it should free up memory, but in my case this was not. So I set the limits for Kingfisher that you can only use 1 megabyte of RAM memory.
I placed this function in AppDelegate and call in function didFinishLaunchingWithOptions
fileprivate func setupKingfisherSettings() {
ImageCache.default.maxMemoryCost = 1
}

Related

Problem with saving data using Core Data in swift

I'm trying to save data to the core data and then display it on another view controller. I have a table view with custom cell, which have a button. I've created a selector, so when we tap on the button in each of the cell, it should save all the data from the cell. Here is my parent view controller:
import UIKit
import SafariServices
import CoreData
class ViewController: UIViewController, UISearchBarDelegate {
#IBOutlet weak var pecodeTableView: UITableView!
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var savedNews = [SavedNews]()
var newsTitle: String?
var newsAuthor: String?
var urlString: String?
var newsDate: String?
var isSaved: Bool = false
private var articles = [Article]()
private var viewModels = [NewsTableViewCellViewModel]()
private let searchVC = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
pecodeTableView.delegate = self
pecodeTableView.dataSource = self
pecodeTableView.register(UINib(nibName: S.CustomCell.customNewsCell, bundle: nil), forCellReuseIdentifier: S.CustomCell.customCellIdentifier)
fetchAllNews()
createSearchBar()
loadNews()
saveNews()
countNewsToCategory()
}
#IBAction func goToFavouritesNews(_ sender: UIButton) {
performSegue(withIdentifier: S.Segues.goToFav, sender: self)
}
private func fetchAllNews() {
APICaller.shared.getAllStories { [weak self] result in
switch result {
case .success(let articles):
self?.articles = articles
self?.viewModels = articles.compactMap({
NewsTableViewCellViewModel(author: $0.author ?? "Unknown", title: $0.title, subtitle: $0.description ?? "No description", imageURL: URL(string: $0.urlToImage ?? "")
)
})
DispatchQueue.main.async {
self?.pecodeTableView.reloadData()
}
case .failure(let error):
print(error)
}
}
}
private func createSearchBar() {
navigationItem.searchController = searchVC
searchVC.searchBar.delegate = self
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 120
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModels.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: S.CustomCell.customCellIdentifier, for: indexPath) as! CustomNewsCell
cell.configure(with: viewModels[indexPath.row])
cell.saveNewsBtn.tag = indexPath.row
cell.saveNewsBtn.addTarget(self, action: #selector(didTapCellButton(sender:)), for: .touchUpInside)
return cell
}
#objc func didTapCellButton(sender: UIButton) {
guard viewModels.indices.contains(sender.tag) else { return }
print("Done")// check element exist in tableview datasource
if !isSaved {
saveNews()
print("success")
}
//Configure selected button or update model
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let article = articles[indexPath.row]
guard let url = URL(string: article.url ?? "") else {
return
}
let vc = SFSafariViewController(url: url)
present(vc, animated: true)
}
//Search
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let text = searchBar.text, !text.isEmpty else {
return
}
APICaller.shared.Search(with: text) { [weak self] result in
switch result {
case .success(let articles):
self?.articles = articles
self?.viewModels = articles.compactMap({
NewsTableViewCellViewModel(author: $0.author ?? "Unknown", title: $0.title, subtitle: $0.description ?? "No description", imageURL: URL(string: $0.urlToImage ?? "")
)
})
DispatchQueue.main.async {
self?.pecodeTableView.reloadData()
self?.searchVC.dismiss(animated: true, completion: nil)
}
case .failure(let error):
print(error)
}
}
}
}
extension ViewController {
func loadNews() {
let request: NSFetchRequest<SavedNews> = SavedNews.fetchRequest()
do {
let savedNews = try context.fetch(request)
//Handle saved news
if savedNews.count > 0 {
isSaved = true
}
} catch {
print("Error fetching data from context \(error)")
}
}
func saveNews() {
//Initialize the context
let news = SavedNews(context: self.context)
//Putting data
news.title = newsTitle
news.author = newsAuthor
news.publishedAt = newsDate
news.url = urlString
do {
try context.save()
} catch {
print("Error when saving data \(error)")
}
}
func countNewsToCategory() {
//Initialize the context
let request: NSFetchRequest<SavedNews> = SavedNews.fetchRequest()
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
])
request.predicate = predicate
do {
savedNews = try context.fetch(request)
} catch {
print("Error fetching data from category \(error)")
}
}
}
I don't know where is the problem, I've created a correct data model, but data could not be saved. Here is my model:
import Foundation
struct APIResponse: Codable {
let articles: [Article]
}
struct Article: Codable {
let author: String?
let source: Source
let title: String
let description: String?
let url: String?
let urlToImage: String?
let publishedAt: String
}
struct Source: Codable {
let name: String
}
And also my model in Core Data:
My second view controller, to which I want display the data:
import UIKit
import CoreData
class FavouriteNewsViewController: UIViewController {
#IBOutlet weak var favTableView: UITableView!
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var savedNews = [SavedNews]()
override func viewDidLoad() {
super.viewDidLoad()
favTableView.delegate = self
favTableView.delegate = self
loadSavedNews()
favTableView.register(UINib(nibName: S.FavouriteCell.favouriteCell, bundle: nil), forCellReuseIdentifier: S.FavouriteCell.favouriteCellIdentifier)
// Do any additional setup after loading the view.
}
}
extension FavouriteNewsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return savedNews.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = favTableView.dequeueReusableCell(withIdentifier: S.FavouriteCell.favouriteCellIdentifier, for: indexPath) as! FavouritesCell
print(savedNews)
let article = savedNews[indexPath.row]
if let articleTitle = article.title {
cell.favTitle.text = articleTitle
}
if let articleAuthor = article.author {
cell.favAuthor.text = articleAuthor
}
if let articleDesc = article.desc {
cell.favDesc.text = article.desc
}
return cell
}
}
extension FavouriteNewsViewController {
func loadSavedNews() {
let request: NSFetchRequest<SavedNews> = SavedNews.fetchRequest()
do {
savedNews = try context.fetch(request)
} catch {
print("Error fetching data from context \(error)")
}
}
func deleteNews(at indexPath: IndexPath) {
// Delete From NSObject
context.delete(savedNews[indexPath.row])
// Delete From current News list
savedNews.remove(at: indexPath.row)
// Save deletion
do {
try context.save()
} catch {
print("Error when saving data \(error)")
}
}
}
you did not assign your properties that you are trying to save
newsTitle,newsAuthor,newsDate,urlString
seems these properties have nil value . make sure these properties have valid value before save .

Tableview using realm won't reload when deleted in swift

I have a simple tableView with saved data. I created a delete button that lets me multi-delete from realm. That part works, it is when the tableview is suppose to reload that it seems to not work. I have seen a lot of answers that say you should reload it on the main thread, or view or whatever, using dispatchQueue.main.async
using just normal tableView.reloadData() didn't reload the tableview but when I use the dispatchQueue version it does delete a value but usually the last value in the tableView.
For example my tableView has the strings Uno and Un in that descending order. If I chose to delete Uno when I press the delete button the tableview does reload leaving only one value but that value is Uno, but realm Database tells me I deleted Uno and when I go back to that view it shows Un. It just isn't reloading correctly.
I have tried to place the reloadData in the dispatch at many different locations, but it still doesn't reload correctly. I am curious what I am doing wrong.
this is the viewController with the tableview where I delete the data in the tableView:
import UIKit
import Realm
import RealmSwift
class OtherViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var otherTableView: UITableView!
var realm: Realm!
var realmedData = ""
var realmList: Results<Realmed> {
get {
return realm.objects(Realmed.self)
}
}
let deleteBtn = UIBarButtonItem()
var testingBool = false
var realmArr = [String]()
var idValue = [Int]()
var idArr = [Int]()
var spanArrValue: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
otherTableView.reloadData()
realm = try! Realm()
self.otherTableView.delegate = self
self.otherTableView.dataSource = self
self.otherTableView.reloadData()
deleteBtnInfo(btn: deleteBtn)
self.navigationItem.rightBarButtonItem = deleteBtn
}
func deleteBtnInfo(btn: UIBarButtonItem) {
btn.title = "Delete"
btn.style = .plain
btn.target = self
btn.action = #selector(didTapDeleteBtn(sender:))
testingBool = false
}
#objc func didTapDeleteBtn(sender: AnyObject) {
testingBool = !testingBool
if testingBool == true {
deleteBtn.title = "Remove"
otherTableView.allowsMultipleSelection = true
otherTableView.allowsMultipleSelectionDuringEditing = true
} else if testingBool == false {
deleteBtn.title = "Delete"
didPressRemove()
DispatchQueue.main.async {
self.otherTableView.reloadData()
}
otherTableView.allowsMultipleSelection = false
otherTableView.allowsMultipleSelectionDuringEditing = false
}
}
func didPressRemove() {
if idValue.count == 0 {
print("Please Select what to Delete")
} else {
deleteRealm(idInt: idValue)
}
}
func deleteRealm(idInt: [Int]) {
do {
try realm.write {
for deleteIndex in idInt {
let deleteValue = realm.objects(RealmTwo.self).filter("id == %#", deleteIndex as Any)
print(deleteIndex)
realm.delete(deleteValue)
}
}
} catch {
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var counted = realm.objects(RealmTwo.self).filter("realmLbl == %#", realmedData)
return counted.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "otherCell", for: indexPath) as! OtherTableViewCell
var celledItem = realm.objects(Realmed.self)
for item in celledItem {
for items in item.realmTwo {
self.idArr.append(items.id)
self.realmArr.append(items.spanish)
}
}
cell.otherLbl.text = "\(realmArr[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if testingBool == false {
print(realmArr[indexPath.row])
} else {
self.idValue.append(idArr[indexPath.row])
print(spanArrValue)
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if testingBool == true {
if let index = idValue.index(of: idArr[indexPath.row]) {
idValue.remove(at: index)
print(spanArrValue)
}
}
}
}
this is the realm class for the data that I am trying to delete.
import Foundation
import UIKit
import Realm
import RealmSwift
class RealmTwo: Object {
#objc dynamic var id = Int()
#objc dynamic var realmLbl = String()
#objc dynamic var spanish = String()
#objc dynamic var french = String()
let realmed = LinkingObjects(fromType: Realmed.self, property: "realmTwo")
convenience init(id: Int, realmLbl: String, spanish: String, french: String) {
self.init()
self.id = id
self.realmLbl = realmLbl
self.spanish = spanish
self.french = french
}
}
As I said above, I placed reloadData() in different places and these are where I placed them, just in case you want to know:
func didPressRemove() {
if idValue.count == 0 {
print("Please Select what to Delete")
} else {
deleteRealm(idInt: idValue)
DispatchQueue.main.async {
self.otherTableView.reloadData()
}
}
}
func deleteRealm(idInt: [Int]) {
do {
try realm.write {
for deleteIndex in idInt {
let deleteValue = realm.objects(RealmTwo.self).filter("id == %#", deleteIndex as Any)
print(deleteIndex)
realm.delete(deleteValue)
DispatchQueue.main.async {
self.otherTableView.reloadData()
}
}
}
} catch {
}
}
I am just not sure where the reloadData is suppose to go, or if that is the real problem. Thank you for the help, and ask if there is anything else I can do.
There are a couple of issues but the main issue is that you're deleting the object from realm but that object is still hanging around in your dataSource tableView array, realmArr.
There are a whole bunch of solutions but the simplest is to add an observer to the realm results and when an item is added, changed or removed, have that update your dataSource array and then reload the tableview. One option also here is to use those results as the dataSource instead of a separate array. Realm Results objects behave very similar to an array and are great a a dataSource.
Conceptually the realm code is similar to
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
tableView.reloadData() //this is when the realm data is intially loaded.
case .update(_, let deletions, let insertions, let modifications):
//handle add, edit and modify per event.
// with an add, add the provided object to your dataSource
// same thing for remove and modify
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
//reload the tableView now the dataSource has been updated
}
There are several options of handling those events and they are all covered in the Realm documentation. See Realm Notifications for further details about setting up the notifications.
A second option is to manually keep things in sync; e.g. when deleting the item from Realm, also delete the item from your dataSource array
This is how I managed to solve this problem.
import UIKit
import Realm
import RealmSwift
class OtherViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var notificationToken: NotificationToken? = nil
#IBOutlet weak var otherTableView: UITableView!
var realm: Realm!
var realmedData = ""
var realmList: Results<RealmTwo> {
get {
return realm.objects(RealmTwo.self).filter("%# == realmLbl", realmedData)
}
}
var realmingList: Results<RealmTwo> {
get {
return realm.objects(RealmTwo.self)
}
}
let deleteBtn = UIBarButtonItem()
var testingBool = false
var realmArr = [String]()
var idValue = [Int]()
var idArr = [Int]()
var spanArrValue: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
otherTableView.allowsMultipleSelectionDuringEditing = true
realm = try! Realm()
notificationToken = realmList.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.otherTableView else {return}
switch changes {
case .initial:
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
fatalError("\(error)")
}
}
self.otherTableView.delegate = self
self.otherTableView.dataSource = self
self.otherTableView.reloadData()
deleteBtnInfo(btn: deleteBtn)
self.navigationItem.rightBarButtonItem = deleteBtn
}
func deleteBtnInfo(btn: UIBarButtonItem) {
btn.title = "Delete"
btn.style = .plain
btn.target = self
btn.action = #selector(didTapDeleteBtn(sender:))
testingBool = false
}
#objc func didTapDeleteBtn(sender: AnyObject) {
testingBool = !testingBool
if testingBool == true {
deleteBtn.title = "Remove"
} else if testingBool == false {
deleteBtn.title = "Delete"
}
}
func didPressRemove() {
if testingBool == false {
print("Select what to Delete")
} else {
deleteRealm(idInt: idValue)
otherTableView.isEditing = false
}
}
#IBAction func pressEdit(_ sender: Any) {
testingBool = !testingBool
if testingBool == true {
otherTableView.isEditing = true
} else if testingBool == false {
otherTableView.isEditing = false
}
}
#IBAction func pressDelete(_ sender: Any) {
deleteRealm(idInt: idValue)
}
func deleteRealm(idInt: [Int]) {
do {
try realm.write {
for deleteIndex in idInt {
let deletingValue = realmList.filter("id == %#", deleteIndex as Any)
print("DeleteValue: \(deletingValue)")
realm.delete(deletingValue)
}
}
} catch {
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return realmList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "otherCell", for: indexPath) as! OtherTableViewCell
cell.otherLbl.text = realmList.filter("%# == realmLbl", realmedData)[indexPath.row].spanish
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if otherTableView.isEditing == false {
} else {
let idArr = realmList.filter("%# == realmLbl", realmedData)[indexPath.row].id
self.idValue.append(idArr)
print("ID: \(idValue)")
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if otherTableView.isEditing == true {
let idArr = realmList.filter("%# == realmLbl", realmedData)[indexPath.row].id
if let index = idValue.index(of: idArr) {
idValue.remove(at: index)
print("ID: \(idValue)")
}
}
}
deinit {
notificationToken?.invalidate()
}
}
Thank you

Image not behaving accurately in data search results on TableView

I have data coming from Firebase and when the data is loaded I either want the an image to be hidden or shown based on some logic in my custom cell. It works perfectly fine when the data isn't being filtered but the second I type in the search bar or change the scope bar to a different index the image doesn't behave right.
For example: Index 0 should not have the image and index 1 should. Which is how it displays when it first loads. However, when I search I know the previous index 1 (now index 0) should still have it's image but it doesn't. BUT if I click to go to the detail controller it brings me to the right page. It's like it loads all the accurate info but does the logic on the original index 0. I would love some help as I have been searching for an answer FOREVER. Thank you in advance!
tableViewCell:
class SearchTalentCell: UITableViewCell {
#IBOutlet weak var userProfileImage: UIImageView!
#IBOutlet weak var talentUserName: UILabel!
#IBOutlet weak var selectedImg: UIImageView!
#IBOutlet weak var inviteSentImg: UIImageView!
var prospectRef: FIRDatabaseReference!
//#IBOutlet weak var radioButton: UIButton!
var currentTalent: UserType!
//var delegate: SearchCellDelegate?
func setTalent(talent: UserType) {
currentTalent = talent
currentTalent.userKey = talent.userKey
}
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: #selector(selectTapped))
tap.numberOfTapsRequired = 1
selectedImg.addGestureRecognizer(tap)
selectedImg.isUserInteractionEnabled = true
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
/*#IBAction func radioButtonTapped(_ sender: Any) {
delegate?.didTapRadioButton(userKey: currntTalent.userKey, searchSelected: currntTalent.searchSelected!.rawValue, radioButton: radioButton)
}*/
func configureCell(user: UserType, img: UIImage? = nil) {
prospectRef = Cast.REF_PRE_PRODUCTION_CASTING_POSITION.child(ProjectDetailVC.currentProject).child(FIRDataCast.prospect.rawValue).child(CastingDetailVC.positionName).child(user.userKey)
//setTalent(talent: user)
self.talentUserName.text = "\(user.firstName) \(user.lastName)"
//self.inviteSentImg.image = UIImage(named: "inviteSent")
//user.adjustSearchSelected(talent: user, radioButton: radioButton)
prospectRef.observeSingleEvent(of: .value, with: { (snapshot) in
if let _ = snapshot.value as? NSNull {
self.inviteSentImg.isHidden = true
print("**Image hidden")
} else {
self.inviteSentImg.image = UIImage(named: "inviteSent")
print("**Image shown")
}
})
//Image Caching
if img != nil {
self.userProfileImage.image = img
} else {
if let imageURL = user.profileImage {
let ref = FIRStorage.storage().reference(forURL: imageURL)
ref.data(withMaxSize: 2 * 1024 * 1024, completion: { (data, error) in
if error != nil {
print("ZACK: Unable to download image from Firebase Storage")
} else {
print("ZACK: Image downloaded from Firebase Storage")
if let imgData = data {
if let img = UIImage(data: imgData) {
self.userProfileImage.image = img
SearchTalentVC.userProfileImageCache.setObject(img, forKey: imageURL as NSString)
}
}
}
})
}
}
}
Viewcontroller:
class SearchTalentVC: UITableViewController/*, SearchCellDelegate*/ {
var searchingRole = [Cast]()
var unfilteredTalent = [UserType]()
var filteredTalent = [UserType]()
var selectedTalent = [UserType]()
var matchingTalentUserKeys = [String]()
var isFiltered = false
var prospectRef: FIRDatabaseReference!
static var userProfileImageCache: NSCache<NSString, UIImage> = NSCache()
let searchController = UISearchController(searchResultsController: nil)
//#IBOutlet weak var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search Talent"
searchController.searchBar.barStyle = .black
navigationItem.searchController = searchController
definesPresentationContext = true
searchController.searchBar.scopeButtonTitles = ["All", "Role Specific"]
searchController.searchBar.tintColor = UIColor.white
searchController.searchBar.delegate = self
searchController.searchResultsUpdater = self
getTalentProfiles()
}
func searchBarIsEmpty() -> Bool {
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredTalent = unfilteredTalent.filter({ (talent : UserType) -> Bool in
let doesTalentMatch = (scope == "All") || doesUserKeyMatch(talent: talent.userKey)
if searchBarIsEmpty() {
return doesTalentMatch
} else {
let fullName = "\(talent.firstName) \(talent.lastName)"
return doesTalentMatch && fullName.lowercased().contains(searchText.lowercased())
}
})
tableView.reloadData()
}
func doesUserKeyMatch(talent: String) -> Bool {
self.filterRoleFeature()
return matchingTalentUserKeys.contains(talent)
}
func isSearching() -> Bool {
let searchBarScopeIsFiltering = searchController.searchBar.selectedScopeButtonIndex != 0
return searchController.isActive && (!searchBarIsEmpty() || searchBarScopeIsFiltering)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
if isSearching() {
return filteredTalent.count
} else {
return unfilteredTalent.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "userSearchCell"
if let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? SearchTalentCell {
var talent: UserType
if isSearching() {
print("we are searching")
talent = self.filteredTalent[indexPath.row]
print("indexPath: \(indexPath.row)")
} else {
print("we are not searching")
talent = self.unfilteredTalent[indexPath.row]
}
if let imageURL = talent.profileImage {
if let img = SearchTalentVC.userProfileImageCache.object(forKey: imageURL as NSString) {
cell.configureCell(user: talent, img: img)
} else {
cell.configureCell(user: talent)
//cell.delegate = self
}
return cell
} else {
cell.configureCell(user: talent)
//cell.delegate = self
return SearchTalentCell()
}
} else {
return SearchTalentCell()
}
}
extension SearchTalentVC: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
let searchBar = searchController.searchBar
let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
filterContentForSearchText(searchController.searchBar.text!, scope: scope)
self.tableView.reloadData()
}
}
extension SearchTalentVC: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])
}
}
If you are hidding image in If part then you should put logic of showing image also in else block. See if this solve your problem.
if let _ = snapshot.value as? NSNull {
self.inviteSentImg.isHidden = true
print("**Image hidden")
} else {
self.inviteSentImg.isHidden = false
self.inviteSentImg.image = UIImage(named: "inviteSent")
print("**Image shown")
}

Fetching Tweets with Swift IOS

I'm practicing on a sample application that has a social feed page. I'm trying to display each tweet with the corresponding media. I was able to get the text and media but not as one tweet and the further I could get is displaying the media link. Any help on how to get the tweet with the media displayed would be appreciated. To make it clearer the user should be able to view the text and any picture/video from the application without the need to open any links.
import UIKit
class ViewController: UIViewController,
UITableViewDelegate,UITableViewDataSource {
//importing objects
#IBOutlet weak var mytextfield: UITextField!
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var myimageView: UIImageView!
#IBOutlet weak var myTableview: UITableView!
#IBOutlet weak var myScroll: UIScrollView!
var tweets:[String] = []
//Activity Indicator
var activityInd = UIActivityIndicatorView()
func startA()
{
UIApplication.shared.beginIgnoringInteractionEvents()
activityInd.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
activityInd.center = view.center
activityInd.startAnimating()
view.addSubview(activityInd)
}
//setting table view
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tweets.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
cell.mytextview.text = tweets[indexPath.row]
return cell
}
#IBAction func mysearchbutton(_ sender: UIButton) {
if mytextfield.text != ""
{
startA()
let user = mytextfield.text?.replacingOccurrences(of: " ", with: "")
getStuff(user: user!)
}
}
//Create a function that gets all the stuff
func getStuff(user:String)
{
let url = URL(string: "https://twitter.com/" + user)
let task = URLSession.shared.dataTask(with: url!) { (data,response, error) in
if error != nil
{
DispatchQueue.main.async
{
if let errorMessage = error?.localizedDescription
{
self.myLabel.text = errorMessage
}else{
self.myLabel.text = "There has been an error try again"
}
}
}else{
let webContent:String = String(data: data!,encoding: String.Encoding.utf8)!
if webContent.contains("<title>") && webContent.contains("data-resolved-url-large=\"")
{
//get user name
var array:[String] = webContent.components(separatedBy: "<title>")
array = array[1].components(separatedBy: " |")
let name = array[0]
array.removeAll()
//getprofile pic
array = webContent.components(separatedBy: "data-resolved-url-large=\"")
array = array[1].components(separatedBy: "\"")
let profilePic = array[0]
print(profilePic)
//get tweets
array = webContent.components(separatedBy: "data-aria-label-part=\"0\">")
//get tweets media
// array = webContent.components(separatedBy: "data-pre-embedded=\"true\" dir=\"ltr\" >")
array.remove(at: 0)
for i in 0...array.count-1
{
let newTweet = array[i].components(separatedBy: "<")
array[i] = newTweet[0]
}
self.tweets = array
DispatchQueue.main.async {
self.myLabel.text = name
self.updateImage(url: profilePic)
self.myTableview.reloadData()
self.activityInd.stopAnimating()
UIApplication.shared.endIgnoringInteractionEvents()
}
}else{
DispatchQueue.main.async {
self.myLabel.text = "User not found"
self.activityInd.stopAnimating()
UIApplication.shared.endIgnoringInteractionEvents()
}
}
}
}
task.resume()
}
//Function that gets profile pic data
func updateImage(url:String)
{
let url = URL(string: url)
let task = URLSession.shared.dataTask(with: url!){ (data, response, error) in
DispatchQueue.main.async
{
self.myimageView.image = UIImage(data: data!)
}
}
task.resume()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
myScroll.contentSize.height = 1000
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
#SYou can use TwitterKit SDK in iOS for your App. Twitter SDK is is fully capable to fulfil your needs. Whatever feed functionality you want you just need to configure it in twitter kit.
When showing Tweets you can implement these features for your feed :
The style (dark or light)
Colors (text, links, background)
Action Buttons
The delegate (TWTRTweetViewDelegate) to be notified of user interaction with the Tweet
To Show tweets you can do this :
For showing tweets you have two options :
You can load any public tweets (Attention : For Showing Public Tweets You need Public Tweet IDs)
Swift 4
For e.g
//
// PublicTweets.swift
// TwitterFeedDemo
//
// Created by User on 21/12/17.
// Copyright © 2017 Test Pvt. Ltd. All rights reserved.
//
import UIKit
import TwitterKit
class PublicTweets : UITableViewController {
// setup a 'container' for Tweets
var tweets: [TWTRTweet] = [] {
didSet {
tableView.reloadData()
}
}
var prototypeCell: TWTRTweetTableViewCell?
let tweetTableCellReuseIdentifier = "TweetCell"
var isLoadingTweets = false
override func viewDidLoad() {
super.viewDidLoad()
if let user = Twitter.sharedInstance().sessionStore.session()?.userID {
Twitter.sharedInstance().sessionStore.logOutUserID(user)
}
self.tableView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
// Create a single prototype cell for height calculations.
self.prototypeCell = TWTRTweetTableViewCell(style: .default, reuseIdentifier: tweetTableCellReuseIdentifier)
// Register the identifier for TWTRTweetTableViewCell.
self.tableView.register(TWTRTweetTableViewCell.self, forCellReuseIdentifier: tweetTableCellReuseIdentifier)
// Setup table data
loadTweets()
}
func loadTweets() {
// Do not trigger another request if one is already in progress.
if self.isLoadingTweets {
return
}
self.isLoadingTweets = true
// set tweetIds to find
let tweetIDs = ["944116014828138496","943585637881352192","943840936135741440"];
// Find the tweets with the tweetIDs
let client = TWTRAPIClient()
client.loadTweets(withIDs: tweetIDs) { (twttrs, error) -> Void in
// If there are tweets do something magical
if ((twttrs) != nil) {
// Loop through tweets and do something
for i in twttrs! {
// Append the Tweet to the Tweets to display in the table view.
self.tweets.append(i as TWTRTweet)
}
} else {
print(error as Any)
}
}
}
}
// MARK
// MARK: UITableViewDataSource UITableViewDelegate
extension PublicTweets {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of Tweets.
return tweets.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Retrieve the Tweet cell.
let cell = tableView.dequeueReusableCell(withIdentifier: tweetTableCellReuseIdentifier, for: indexPath) as! TWTRTweetTableViewCell
// Assign the delegate to control events on Tweets.
cell.tweetView.delegate = self
cell.tweetView.showActionButtons = true
// Retrieve the Tweet model from loaded Tweets.
let tweet = tweets[indexPath.row]
// Configure the cell with the Tweet.
cell.configure(with: tweet)
// Return the Tweet cell.
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let tweet = self.tweets[indexPath.row]
self.prototypeCell?.configure(with: tweet)
return TWTRTweetTableViewCell.height(for: tweet, style: TWTRTweetViewStyle.compact, width: self.view.bounds.width , showingActions:true)
}
}
extension PublicTweets : TWTRTweetViewDelegate {
//Handle Following Events As Per Your Needs
func tweetView(_ tweetView: TWTRTweetView, didTap url: URL) {
}
func tweetView(_ tweetView: TWTRTweetView, didTapVideoWith videoURL: URL) {
}
func tweetView(_ tweetView: TWTRTweetView, didTap image: UIImage, with imageURL: URL) {
}
func tweetView(_ tweetView: TWTRTweetView, didTap tweet: TWTRTweet) {
}
func tweetView(_ tweetView: TWTRTweetView, didTapProfileImageFor user: TWTRUser) {
}
func tweetView(_ tweetView: TWTRTweetView, didChange newState: TWTRVideoPlaybackState) {
}
}
You can also show other users tweets by just having their ScreenName or Twitter UserID
For e.g.
//
// SelfTweets.swift
// TwitterFeedDemo
//
// Created by User on 21/12/17.
// Copyright © 2017 Test Pvt. Ltd. All rights reserved.
//
import Foundation
import UIKit
import TwitterKit
class SelfTweets: TWTRTimelineViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let user = Twitter.sharedInstance().sessionStore.session()?.userID {
let client = TWTRAPIClient()
self.dataSource = TWTRUserTimelineDataSource.init(screenName:"li_ios", userID: user, apiClient: client, maxTweetsPerRequest: 10, includeReplies: true, includeRetweets: false)
}
}
}

Firebase chat app crashes when clicking on table cell

I downloaded a demo chat application that runs perfectly but when I implement it into my own app it crashes and the code is exactly the same. The app gives you the ability to create a chat room and my app works up to this point but when you click on the chat room name that now appears on the table I get the following error:
Assertion failure in -[Irish_League_Grounds.ChatViewController viewWillAppear:], /Users/ryanball/Desktop/Irish League
Grounds/Pods/JSQMessagesViewController/JSQMessagesViewController/Controllers/JSQMessagesViewController.m:277
2017-05-17 17:32:55.815 Irish League Grounds[20456:681491]
Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'Invalid parameter not
satisfying: self.senderDisplayName != nil'
Here is my code for the two view controllers I'm segueing between:
import UIKit
import Firebase
enum Section: Int {
case createNewChannelSection = 0
case currentChannelsSection
}
class ChannelListViewController: UITableViewController {
// MARK: Properties
var senderDisplayName: String?
var newChannelTextField: UITextField?
private var channelRefHandle: FIRDatabaseHandle?
private var channels: [Channel] = []
private lazy var channelRef: FIRDatabaseReference = FIRDatabase.database().reference().child("channels")
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
title = "RW RIC"
observeChannels()
}
deinit {
if let refHandle = channelRefHandle {
channelRef.removeObserver(withHandle: refHandle)
}
}
// MARK :Actions
#IBAction func createChannel(_ sender: AnyObject) {
if let name = newChannelTextField?.text {
let newChannelRef = channelRef.childByAutoId()
let channelItem = [
"name": name
]
newChannelRef.setValue(channelItem)
}
}
// MARK: Firebase related methods
private func observeChannels() {
// We can use the observe method to listen for new
// channels being written to the Firebase DB
channelRefHandle = channelRef.observe(.childAdded, with: { (snapshot) -> Void in
let channelData = snapshot.value as! Dictionary<String, AnyObject>
let id = snapshot.key
if let name = channelData["name"] as! String!, name.characters.count > 0 {
self.channels.append(Channel(id: id, name: name))
self.tableView.reloadData()
} else {
print("Error! Could not decode channel data")
}
})
}
// MARK: Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if let channel = sender as? Channel {
let chatVc = segue.destination as! ChatViewController
chatVc.senderDisplayName = senderDisplayName
chatVc.channel = channel
chatVc.channelRef = channelRef.child(channel.id)
}
}
// MARK: UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let currentSection: Section = Section(rawValue: section) {
switch currentSection {
case .createNewChannelSection:
return 1
case .currentChannelsSection:
return channels.count
}
} else {
return 0
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let reuseIdentifier = (indexPath as NSIndexPath).section == Section.createNewChannelSection.rawValue ? "NewChannel" : "ExistingChannel"
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
if (indexPath as NSIndexPath).section == Section.createNewChannelSection.rawValue {
if let createNewChannelCell = cell as? CreateChannelCell {
newChannelTextField = createNewChannelCell.newChannelNameField
}
} else if (indexPath as NSIndexPath).section == Section.currentChannelsSection.rawValue {
cell.textLabel?.text = channels[(indexPath as NSIndexPath).row].name
}
return cell
}
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if (indexPath as NSIndexPath).section == Section.currentChannelsSection.rawValue {
let channel = channels[(indexPath as NSIndexPath).row]
self.performSegue(withIdentifier: "ShowChannel", sender: channel)
}
}
}
Here is the second view controller:
import UIKit
import Photos
import Firebase
import JSQMessagesViewController
final class ChatViewController: JSQMessagesViewController {
// MARK: Properties
private let imageURLNotSetKey = "NOTSET"
var channelRef: FIRDatabaseReference?
private lazy var messageRef: FIRDatabaseReference = self.channelRef!.child("messages")
fileprivate lazy var storageRef: FIRStorageReference = FIRStorage.storage().reference(forURL: "gs://chatchat-871d0.appspot.com")
private lazy var userIsTypingRef: FIRDatabaseReference = self.channelRef!.child("typingIndicator").child(self.senderId)
private lazy var usersTypingQuery: FIRDatabaseQuery = self.channelRef!.child("typingIndicator").queryOrderedByValue().queryEqual(toValue: true)
private var newMessageRefHandle: FIRDatabaseHandle?
private var updatedMessageRefHandle: FIRDatabaseHandle?
private var messages: [JSQMessage] = []
private var photoMessageMap = [String: JSQPhotoMediaItem]()
private var localTyping = false
var channel: Channel? {
didSet {
title = channel?.name
}
}
var isTyping: Bool {
get {
return localTyping
}
set {
localTyping = newValue
userIsTypingRef.setValue(newValue)
}
}
lazy var outgoingBubbleImageView: JSQMessagesBubbleImage = self.setupOutgoingBubble()
lazy var incomingBubbleImageView: JSQMessagesBubbleImage = self.setupIncomingBubble()
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.senderId = FIRAuth.auth()?.currentUser?.uid
observeMessages()
// No avatars
collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSize.zero
collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
observeTyping()
}
deinit {
if let refHandle = newMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
if let refHandle = updatedMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
}
// MARK: Collection view data source (and related) methods
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
return messages[indexPath.item]
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item] // 1
if message.senderId == senderId { // 2
return outgoingBubbleImageView
} else { // 3
return incomingBubbleImageView
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
let message = messages[indexPath.item]
if message.senderId == senderId { // 1
cell.textView?.textColor = UIColor.white // 2
} else {
cell.textView?.textColor = UIColor.black // 3
}
return cell
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
return nil
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForMessageBubbleTopLabelAt indexPath: IndexPath!) -> CGFloat {
return 15
}
override func collectionView(_ collectionView: JSQMessagesCollectionView?, attributedTextForMessageBubbleTopLabelAt indexPath: IndexPath!) -> NSAttributedString? {
let message = messages[indexPath.item]
switch message.senderId {
case senderId:
return nil
default:
guard let senderDisplayName = message.senderDisplayName else {
assertionFailure()
return nil
}
return NSAttributedString(string: senderDisplayName)
}
}
// MARK: Firebase related methods
private func observeMessages() {
messageRef = channelRef!.child("messages")
let messageQuery = messageRef.queryLimited(toLast:25)
// We can use the observe method to listen for new
// messages being written to the Firebase DB
newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) -> Void in
let messageData = snapshot.value as! Dictionary<String, String>
if let id = messageData["senderId"] as String!, let name = messageData["senderName"] as String!, let text = messageData["text"] as String!, text.characters.count > 0 {
self.addMessage(withId: id, name: name, text: text)
self.finishReceivingMessage()
} else if let id = messageData["senderId"] as String!, let photoURL = messageData["photoURL"] as String! {
if let mediaItem = JSQPhotoMediaItem(maskAsOutgoing: id == self.senderId) {
self.addPhotoMessage(withId: id, key: snapshot.key, mediaItem: mediaItem)
if photoURL.hasPrefix("gs://") {
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: nil)
}
}
} else {
print("Error! Could not decode message data")
}
})
// We can also use the observer method to listen for
// changes to existing messages.
// We use this to be notified when a photo has been stored
// to the Firebase Storage, so we can update the message data
updatedMessageRefHandle = messageRef.observe(.childChanged, with: { (snapshot) in
let key = snapshot.key
let messageData = snapshot.value as! Dictionary<String, String>
if let photoURL = messageData["photoURL"] as String! {
// The photo has been updated.
if let mediaItem = self.photoMessageMap[key] {
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: key)
}
}
})
}
private func fetchImageDataAtURL(_ photoURL: String, forMediaItem mediaItem: JSQPhotoMediaItem, clearsPhotoMessageMapOnSuccessForKey key: String?) {
let storageRef = FIRStorage.storage().reference(forURL: photoURL)
storageRef.data(withMaxSize: INT64_MAX){ (data, error) in
if let error = error {
print("Error downloading image data: \(error)")
return
}
storageRef.metadata(completion: { (metadata, metadataErr) in
if let error = metadataErr {
print("Error downloading metadata: \(error)")
return
}
if (metadata?.contentType == "image/gif") {
mediaItem.image = UIImage.gifWithData(data!)
} else {
mediaItem.image = UIImage.init(data: data!)
}
self.collectionView.reloadData()
guard key != nil else {
return
}
self.photoMessageMap.removeValue(forKey: key!)
})
}
}
private func observeTyping() {
let typingIndicatorRef = channelRef!.child("typingIndicator")
userIsTypingRef = typingIndicatorRef.child(senderId)
userIsTypingRef.onDisconnectRemoveValue()
usersTypingQuery = typingIndicatorRef.queryOrderedByValue().queryEqual(toValue: true)
usersTypingQuery.observe(.value) { (data: FIRDataSnapshot) in
// You're the only typing, don't show the indicator
if data.childrenCount == 1 && self.isTyping {
return
}
// Are there others typing?
self.showTypingIndicator = data.childrenCount > 0
self.scrollToBottom(animated: true)
}
}
override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
// 1
let itemRef = messageRef.childByAutoId()
// 2
let messageItem = [
"senderId": senderId!,
"senderName": senderDisplayName!,
"text": text!,
]
// 3
itemRef.setValue(messageItem)
// 4
JSQSystemSoundPlayer.jsq_playMessageSentSound()
// 5
finishSendingMessage()
isTyping = false
}
func sendPhotoMessage() -> String? {
let itemRef = messageRef.childByAutoId()
let messageItem = [
"photoURL": imageURLNotSetKey,
"senderId": senderId!,
]
itemRef.setValue(messageItem)
JSQSystemSoundPlayer.jsq_playMessageSentSound()
finishSendingMessage()
return itemRef.key
}
func setImageURL(_ url: String, forPhotoMessageWithKey key: String) {
let itemRef = messageRef.child(key)
itemRef.updateChildValues(["photoURL": url])
}
// MARK: UI and User Interaction
private func setupOutgoingBubble() -> JSQMessagesBubbleImage {
let bubbleImageFactory = JSQMessagesBubbleImageFactory()
return bubbleImageFactory!.outgoingMessagesBubbleImage(with: UIColor.jsq_messageBubbleBlue())
}
private func setupIncomingBubble() -> JSQMessagesBubbleImage {
let bubbleImageFactory = JSQMessagesBubbleImageFactory()
return bubbleImageFactory!.incomingMessagesBubbleImage(with: UIColor.jsq_messageBubbleLightGray())
}
override func didPressAccessoryButton(_ sender: UIButton) {
let picker = UIImagePickerController()
picker.delegate = self
if (UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera)) {
picker.sourceType = UIImagePickerControllerSourceType.camera
} else {
picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
}
present(picker, animated: true, completion:nil)
}
private func addMessage(withId id: String, name: String, text: String) {
if let message = JSQMessage(senderId: id, displayName: name, text: text) {
messages.append(message)
}
}
private func addPhotoMessage(withId id: String, key: String, mediaItem: JSQPhotoMediaItem) {
if let message = JSQMessage(senderId: id, displayName: "", media: mediaItem) {
messages.append(message)
if (mediaItem.image == nil) {
photoMessageMap[key] = mediaItem
}
collectionView.reloadData()
}
}
// MARK: UITextViewDelegate methods
override func textViewDidChange(_ textView: UITextView) {
super.textViewDidChange(textView)
// If the text is not empty, the user is typing
isTyping = textView.text != ""
}
}
// MARK: Image Picker Delegate
extension ChatViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [String : Any]) {
picker.dismiss(animated: true, completion:nil)
// 1
if let photoReferenceUrl = info[UIImagePickerControllerReferenceURL] as? URL {
// Handle picking a Photo from the Photo Library
// 2
let assets = PHAsset.fetchAssets(withALAssetURLs: [photoReferenceUrl], options: nil)
let asset = assets.firstObject
// 3
if let key = sendPhotoMessage() {
// 4
asset?.requestContentEditingInput(with: nil, completionHandler: { (contentEditingInput, info) in
let imageFileURL = contentEditingInput?.fullSizeImageURL
// 5
let path = "\(FIRAuth.auth()?.currentUser?.uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000))/\(photoReferenceUrl.lastPathComponent)"
// 6
self.storageRef.child(path).putFile(imageFileURL!, metadata: nil) { (metadata, error) in
if let error = error {
print("Error uploading photo: \(error.localizedDescription)")
return
}
// 7
self.setImageURL(self.storageRef.child((metadata?.path)!).description, forPhotoMessageWithKey: key)
}
})
}
} else {
// Handle picking a Photo from the Camera - TODO
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion:nil)
}
}
The problem lies with this section of your code:
guard let senderDisplayName = message.senderDisplayName else {
assertionFailure()
return nil
}
assertionFailure() must link to another function that runs assert(false) or some equivalent to that. assert(_) is meant to verify that a particular parameter or comparison returns true, or if it does not, it will crash the app. The app will not crash if it is a production build (like those on the App Store) because asserts are meant for debugging purposes.
Basically, the guard statement is necessary to verify that message.senderDisplayName is unwrappable to some value (not nil). If message.senderDisplayName is nil, then there is no point in running the code below the guard and the contents of the guard should be run instead. assertionFailure() will crash the app during testing and during production it will be ignored. When it is ignored, nil will be returned for the function and it will continue on as nothing happened.

Resources