I have just download Multi-party example and install in 3 different devices with a same session token. (Media Mode: Routed)
Also, I have created 1 publisher and 2 subscribers generate token and added in code.
I am getting a stream/video of a publisher in all subscriber devices screen.
But in publisher, I am not getting a stream/video and audio of subscribers ?
class ViewController: UIViewController {
#IBOutlet var endCallButton: UIButton!
#IBOutlet var swapCameraButton: UIButton!
#IBOutlet var muteMicButton: UIButton!
#IBOutlet var userName: UILabel!
#IBOutlet var collectionView: UICollectionView!
var subscribers: [IndexPath: OTSubscriber] = [:]
lazy var session: OTSession = {
return OTSession(
apiKey: kApiKey,
sessionId: kSessionId,
delegate: self
)!
}()
lazy var publisher: OTPublisher = {
let settings = OTPublisherSettings()
settings.name = UIDevice.current.name
return OTPublisher(delegate: self, settings: settings)!
}()
var error: OTError?
override func viewDidLoad() {
super.viewDidLoad()
self.session.connect(withToken: kToken, error: &error)
self.userName.text = UIDevice.current.name
}
override func viewDidAppear(_ animated: Bool) {
guard let layout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
layout.itemSize = CGSize(width: self.collectionView.bounds.size.width / 2,
height: self.collectionView.bounds.size.height / 2)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func swapCameraAction(_ sender: AnyObject) {
let previousPosition = self.publisher.cameraPosition
self.publisher.cameraPosition = previousPosition == .front ? .back : .front
}
#IBAction func muteMicAction(_ sender: AnyObject) {
self.publisher.publishAudio = !publisher.publishAudio
let buttonImage: UIImage = {
if !self.publisher.publishAudio {
return #imageLiteral(resourceName: "mic_muted-24")
} else {
return #imageLiteral(resourceName: "mic-24")
}
}()
self.muteMicButton.setImage(buttonImage, for: .normal)
}
#IBAction func endCallAction(_ sender: AnyObject) {
self.session.disconnect(&error)
}
}
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func reloadCollectionView() {
print("self.subscribers.count: \(self.subscribers.count)")
self.collectionView.isHidden = self.subscribers.count == 0
self.collectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return self.subscribers.count
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "subscriberCell",
for: indexPath
) as! SubscriberCollectionCell
cell.subscriber = self.subscribers[indexPath]
return cell
}
}
// MARK: - OpenTok Methods
extension ViewController {
func doPublish() {
self.swapCameraButton.isEnabled = true
self.muteMicButton.isEnabled = true
self.endCallButton.isEnabled = true
if let pubView = self.publisher.view {
let publisherDimensions = CGSize(width: view.bounds.size.width / 4,
height: view.bounds.size.height / 6)
pubView.frame = CGRect(origin: CGPoint(x:collectionView.bounds.size.width - publisherDimensions.width,
y:collectionView.bounds.size.height - publisherDimensions.height + collectionView.frame.origin.y),
size: publisherDimensions)
view.addSubview(pubView)
}
self.session.publish(self.publisher, error: &error)
}
func doSubscribe(to stream: OTStream) {
if let subscriber = OTSubscriber(stream: stream, delegate: self) {
let indexPath = IndexPath(item: self.subscribers.count, section: 0)
self.subscribers[indexPath] = subscriber
self.session.subscribe(subscriber, error: &error)
self.reloadCollectionView()
}
}
func findSubscriber(byStreamId id: String) -> (IndexPath, OTSubscriber)? {
for (_, entry) in self.subscribers.enumerated() {
if let stream = entry.value.stream, stream.streamId == id {
return (entry.key, entry.value)
}
}
return nil
}
func findSubscriberCell(byStreamId id: String) -> SubscriberCollectionCell? {
for cell in collectionView.visibleCells {
if let subscriberCell = cell as? SubscriberCollectionCell,
let subscriberOfCell = subscriberCell.subscriber,
(subscriberOfCell.stream?.streamId ?? "") == id {
return subscriberCell
}
}
return nil
}
}
// MARK: - OTSession delegate callbacks
extension ViewController: OTSessionDelegate {
func sessionDidConnect(_ session: OTSession) {
print("Session connected")
doPublish()
}
func sessionDidDisconnect(_ session: OTSession) {
print("Session disconnected")
self.subscribers.removeAll()
reloadCollectionView()
}
func session(_ session: OTSession, streamCreated stream: OTStream) {
print("Session streamCreated: \(stream.streamId)")
if self.subscribers.count == 4 {
print("Sorry this sample only supports up to 4 self.subscribers :)")
return
}
self.doSubscribe(to: stream)
}
func session(_ session: OTSession, streamDestroyed stream: OTStream) {
print("Session streamDestroyed: \(stream.streamId)")
guard let (index, subscriber) = self.findSubscriber(byStreamId: stream.streamId) else {
return
}
subscriber.view?.removeFromSuperview()
self.subscribers.removeValue(forKey: index)
self.reloadCollectionView()
}
func session(_ session: OTSession, didFailWithError error: OTError) {
print("session Failed to connect: \(error.localizedDescription)")
}
}
// MARK: - OTPublisher delegate callbacks
extension ViewController: OTPublisherDelegate {
func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) {
print("Publisher streamCreated")
//TESTING
self.reloadCollectionView()
}
func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) {
print("Publisher streamDestroyed")
}
func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) {
print("Publisher failed: \(error.localizedDescription)")
}
}
// MARK: - OTSubscriber delegate callbacks
extension ViewController: OTSubscriberDelegate {
func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) {
print("Subscriber connected")
self.reloadCollectionView()
}
func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) {
print("Subscriber failed: \(error.localizedDescription)")
}
func subscriberVideoDataReceived(_ subscriber: OTSubscriber) {
print("Subscriber subscriberVideoDataReceived")
}
}
Even I tried to console log subscribers count in publisher,
I am getting 0.
What I am missing, I need subscriber videos and count ??
Related
I am trying to get groupchat offline messages from the offline database. But when I am trying to get the messages, then getting the duplicate messages multiple times. I don't know how to resolve this issue. Please help me to resolve this issue.
I attached the code of my XMPP implementation....
This is my XMPPController class where I can setup the client with the server.
class XMPPController: NSObject {
var xmppStream: XMPPStream
let hostName: String
let userJID: XMPPJID
let hostPort: UInt16
let password: String
var xmppStorage: XMPPMessageArchivingCoreDataStorage?
var messageArchiving: XMPPMessageArchiving?
init(hostName: String, userJIDString: String, hostPort: UInt16 = 5222, password: String) throws {
guard let userJID = XMPPJID(string: userJIDString) else {
throw XMPPControllerError.wrongUserJID
}
self.hostName = hostName
self.userJID = userJID
self.hostPort = hostPort
self.password = password
// Stream Configuration
self.xmppStream = XMPPStream()
self.xmppStream.hostName = hostName
self.xmppStream.hostPort = hostPort
self.xmppStream.startTLSPolicy = XMPPStreamStartTLSPolicy.allowed
self.xmppStream.myJID = userJID
super.init()
self.xmppStream.addDelegate(self, delegateQueue: DispatchQueue.main)
xmppStorage = XMPPMessageArchivingCoreDataStorage()
messageArchiving = XMPPMessageArchiving(messageArchivingStorage: xmppStorage)
messageArchiving?.clientSideMessageArchivingOnly = false
messageArchiving?.activate(xmppStream)
messageArchiving?.addDelegate(self, delegateQueue: DispatchQueue.main)
}
func connect() {
if !self.xmppStream.isDisconnected {
return
}
try! self.xmppStream.connect(withTimeout: XMPPStreamTimeoutNone)
}
}
extension XMPPController: XMPPStreamDelegate {
func xmppStreamDidConnect(_ stream: XMPPStream) {
print("Stream: Connected")
try! stream.authenticate(withPassword: self.password)
}
func xmppStreamDidAuthenticate(_ sender: XMPPStream) {
self.xmppStream.send(XMPPPresence())
print("Stream: Authenticated")
}
func xmppStream(_ sender: XMPPStream, didReceive message: XMPPMessage) {
print("Message XMPPController Called")
}
}
This is my ViewController class implementation. In this, we can create and join room and send messages in group.
class GroupMessagesViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var inputTextField: UITextField!
#IBOutlet weak var photosButton: UIPhotosButton!
var xmppController: XMPPController!
var messages = [Message]()
var xmppRoom: XMPPRoom!
var isXMPPRoomDidReceiveMsgCalled = false
override func viewDidLoad() {
super.viewDidLoad()
// Group Chat
self.createOrJoinRoom()
}
deinit {
xmppRoom.deactivate()
xmppRoom.removeDelegate(self)
xmppRoom = nil
}
func retrieveGroupMessagesFromOfflineDb() {
let fetchRequest = NSFetchRequest<XMPPMessageArchiving_Message_CoreDataObject>(entityName: "XMPPMessageArchiving_Message_CoreDataObject")
fetchRequest.returnsDistinctResults = true
// let predicate = NSPredicate(format: "bareJidStr == %#", "groupdemo#conference.chat.webolutionit.co.uk")
// fetchRequest.predicate = predicate
let predicate = NSPredicate(format: "messageStr CONTAINS[cd] %# AND bareJidStr == %#", "groupchat", "groupdemo#conference.\(XMPPKeys.host)")
fetchRequest.predicate = predicate
let context = xmppController.xmppStorage?.mainThreadManagedObjectContext
do {
let messageHistoryArray = try context?.fetch(fetchRequest)
if messageHistoryArray?.count != 0 {
self.messages.removeAll()
messageHistoryArray?.forEach({ (msg) in
var message: Message!
if msg.body.contains(".jpg") {
message = Message(message: msg.body, from: msg.message.fromStr, to: msg.message.toStr, type: msg.message.type, contentType: .photo)
}
else {
message = Message(message: msg.body, from: msg.message.fromStr, to: msg.message.toStr, type: msg.message.type, contentType: .text)
}
self.messages.append(message)
})
isXMPPRoomDidReceiveMsgCalled = true
self.tableView.reloadData()
tableView.scrollToBottom()
}
}
catch {
isXMPPRoomDidReceiveMsgCalled = true
print(error.localizedDescription)
}
}
func createOrJoinRoom() {
let roomStorage = XMPPRoomMemoryStorage()
let roomId = XMPPJID(string: "groupdemo#conference.\(XMPPKeys.host)")
xmppRoom = XMPPRoom(roomStorage: roomStorage!, jid: roomId!, dispatchQueue: DispatchQueue.main)
let userId = XMPPJID(string: "testUser#chat.\(XMPPKeys.receiverJID)")
xmppRoom.inviteUser(userId!, withMessage: "Join Please")
xmppRoom.activate(xmppController.xmppStream)
xmppRoom.addDelegate(self, delegateQueue: DispatchQueue.main)
xmppRoom.join(usingNickname: XMPPKeys.senderJID, history: nil, password: nil)
}
// MARK:- IBACTIONS
#IBAction func backBtnPressed(_ sender: UIButton) {
_ = self.navigationController?.popViewController(animated: true)
}
#IBAction func sendBtnPressed(_ sender: UIButton) {
if let text = inputTextField.text {
if text != "" {
let message = XMLElement.element(withName: "message") as! XMLElement
message.addAttribute(withName: "from", stringValue: XMPPKeys.senderJID)
message.addAttribute(withName: "to", stringValue: "groupdemo#conference.\(XMPPKeys.host)")
message.addAttribute(withName: "type", stringValue: "groupchat")
message.addAttribute(withName: "id", stringValue: xmppController.xmppStream.generateUUID)
let uuid = UIDevice.current.identifierForVendor?.uuidString
message.addAttribute(withName: "uuid", stringValue: uuid.leoSafe())
let body = XMLElement.element(withName: "body") as! XMLElement
body.stringValue = text
message.addChild(body)
xmppController.xmppStream.send(message)
}
}
}
}
//MARK: UITableView Delegate & DataSource
extension GroupMessagesViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(messages.count)
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let message = messages[indexPath.row]
let currentUser = message.to.leoSafe().components(separatedBy: "/")[0]
if message.contentType == .text {
let cell = tableView.dequeueReusableCell(withIdentifier: XMPPKeys.senderJID.contains(currentUser) ? "MessageTableViewCell" : "UserMessageTableViewCell") as! MessageTableViewCell
cell.set(message)
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: currentUser == XMPPKeys.senderJID ? "MessageAttachmentTableViewCell" : "UserMessageAttachmentTableViewCell") as! MessageAttachmentTableViewCell
cell.delegate = self
cell.set(message)
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard tableView.isDragging else { return }
cell.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
UIView.animate(withDuration: 0.3, animations: {
cell.transform = CGAffineTransform.identity
})
}
}
//MARK: UItextField Delegate
extension GroupMessagesViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return textField.resignFirstResponder()
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return true
}
}
//MARK: MessageTableViewCellDelegate Delegate
extension GroupMessagesViewController: MessageTableViewCellDelegate {
func messageTableViewCellUpdate() {
tableView.beginUpdates()
tableView.endUpdates()
}
}
extension GroupMessagesViewController: XMPPRoomDelegate {
func xmppRoomDidJoin(_ sender: XMPPRoom) {
print("Room Joined")
sender.fetchConfigurationForm()
let a = xmppRoom.fetchMembersList()
self.retrieveGroupMessagesFromOfflineDb()
}
func xmppRoomDidCreate(_ sender: XMPPRoom) {
print("Room Created")
}
func xmppRoomDidLeave(_ sender: XMPPRoom) {
print("Room Leave")
}
func xmppRoomDidDestroy(_ sender: XMPPRoom) {
print("Room Destroyed")
}
func xmppRoom(_ sender: XMPPRoom, didReceive message: XMPPMessage, fromOccupant occupantJID: XMPPJID) {
print("Group Message Retrieved")
if isXMPPRoomDidReceiveMsgCalled {
if message.isGroupChatMessageWithBody {
var msg: Message!
if (message.body?.contains(".jpg"))! {
msg = Message(message: message.body, from: message.fromStr, to: message.toStr, type: message.type!, contentType: .photo)
}
else {
msg = Message(message: message.body, from: message.fromStr, to: message.toStr, type: message.type!, contentType: .text)
}
self.messages.append(msg)
self.tableView.reloadData()
self.tableView.scrollToBottom()
inputTextField.text = ""
}
}
}
func xmppRoom(_ sender: XMPPRoom, didFetchModeratorsList items: [Any]) {
print(items)
}
func xmppRoom(_ sender: XMPPRoom, didFetchMembersList items: [Any]) {
print(items)
}
}
// XMPPMUCDelegate METHODS
// ACCEPTING GROUP INVITATION
extension GroupMessagesViewController: XMPPMUCDelegate {
func xmppMUC(_ sender: XMPPMUC, roomJID: XMPPJID, didReceiveInvitation message: XMPPMessage) {
let x = message.element(forName: "x", xmlnsPrefix: XMPPMUCUserNamespace)
let invite = x?.elements(forName: "invite")
if !(invite?.isEmpty)! {
let conferenceRoomId = message.attribute(forName: "from")?.stringValue
self.joinMultiUserChat(roomName: conferenceRoomId.leoSafe())
}
}
func joinMultiUserChat(roomName: String) {
let roomJID = XMPPJID(string: roomName)
let xmppStorage = XMPPRoomMemoryStorage()
xmppRoom = XMPPRoom(roomStorage: xmppStorage!, jid: roomJID!, dispatchQueue: DispatchQueue.main)
xmppRoom.activate(xmppController.xmppStream)
xmppRoom.addDelegate(self, delegateQueue: DispatchQueue.main)
xmppRoom.join(usingNickname: xmppController!.xmppStream.myJID!.full, history: nil)
}
}
I have the websocket server that server push the data every two seconds based on my subscription. I need to update the row based on the in the tableview.currently I am using Starscream module for websocket implementation. how to update specific rows value to every two seconds
import UIKit
import Starscream
struct Stocks: Codable {
let changepercent: String
let changeprice: String
let close: String
let currentprice: String
let high: String
let id: Int
let low: String
let name: String
let `open`: String
let type: String
let instid: String
let exchange: String
}
class ViewController: UIViewController, WebSocketDelegate,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var stocktableView: UITableView!
var arrContacts = [Stocks]()
var socket: WebSocket!
override func viewDidLoad() {
super.viewDidLoad()
var request = URLRequest(url: URL(string: "ws://192.168.18.97:8080/sss/landingstream")!)
//var request = URLRequest(url: URL(string: "ws://192.168.18.97:8080/Setrega/landingstream")!)
request.timeoutInterval = 5
socket = WebSocket(request: request)
socket.delegate = self
socket.connect()
}
// MARK: Websocket Delegate Methods.
func websocketDidConnect(socket: WebSocketClient) {
print("websocket is connected")
}
func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
if let e = error as? WSError {
print("websocket is disconnected: \(e.message)")
} else if let e = error {
print("websocket is disconnected: \(e.localizedDescription)")
} else {
print("websocket disconnected")
}
}
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
print("Received text: \(text)")
let decoder = JSONDecoder()
do {
let iexEvent: Stocks = try decoder.decode(Stocks.self, from: text.data(using: .utf8)!)
DispatchQueue.main.async {
self.stocktableView.reloadData()
}
} catch {
print(error)
}
}
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
print("Received data: \(data.count)")
}
// MARK: Write Text Action
#IBAction func writeText(_ sender: UIBarButtonItem) {
socket.write(string: "{\"requestType\": \"INSTRUMENT_PRICE\",\"topic\": \"SBIN\"}")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.arrContacts.count;
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StocksCell", for: indexPath)
cell.textLabel?.text = arrContacts[indexPath.row].changeprice
return cell
}
// MARK: Disconnect Action
#IBAction func disconnect(_ sender: UIBarButtonItem) {
if socket.isConnected {
sender.title = "Connect"
socket.disconnect()
} else {
sender.title = "Disconnect"
socket.connect()
}
}
}
Instead of this:
DispatchQueue.main.async {
self.stocktableView.reloadData()
}
Try to find the rows that were changed with this function:
final func indexesOfStocks(stocks:[Stocks]) -> [Int] {
return stocks.reduce([]) { (currentResult, currentStocks) in
if let currentStockIndex = self.arrContacts.index(where: { currentStocks.id == $0.id }) {
return currentResult + [currentStockIndex]
}
return currentResult
}
}
Update property arrContacts:
final func updateArrContacts(indexesOfStocksValue:[Int], iexEvents:[Stocks]) {
for i in stride(from: 0, to: indexesOfStocksValue.count, by: 1) {
self.arrContacts[indexesOfStocksValue[i]] = iexEvents[i]
}
}
And reload rows for updates items only:
final func updateRows(stocksIndexes:[Int]) {
DispatchQueue.main.async {
self.stocktableView.performBatchUpdates({
let indexesToUpdate = stocksIndexes.reduce([], { (currentResult, currentStocksIndex) -> [IndexPath] in
if currentStocksIndex < self.stocktableView.numberOfRows(inSection: 0) {
return currentResult + [IndexPath.init(row: currentStocksIndex, section: 0)]
}
return currentResult
})
self.stocktableView.reloadRows(at: indexesToUpdate, with: UITableViewRowAnimation.automatic)
}) { (_) in
}
}
}
Now you can update rows with that code:
let indexesOfStocksValue = self.indexesOfStocks(stocks: iexEvents) // iexEvents is an array of Stocks
self.updateArrContacts(indexesOfStocksValue: indexesOfStocksValue, iexEvents: iexEvents)
self.updateRows(stocksIndexes: indexesOfStocksValue)
This solution is based on idea that after websocketDidReceiveMessage: only existing items in arrContacts should be updated. No new items will be added and no items will be removed.
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
}
I am using library SJSegmentedViewController for my project, github link to pod
Problem:
I have main view controller (FilterVC) on which I have a button "APPLY", on its action I want to access an array stored in another viewcontroller (FilterSkillVC), I am doing this using delegation, but still what I get is an empty instance
UPDATED
MY FilterVC code
import UIKit
import SJSegmentedScrollView
protocol FilterVCDelegate {
func btnApply()
}
class FilterVC: UIViewController {
var selectedSegment: SJSegmentTab?
var segmentedVC : SJSegmentedViewController?
var vcDelegate : FilterVCDelegate?
#IBOutlet weak var containerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
segmentViewInitialization()
}
#IBAction func btnApply(_ sender: Any) {
vcDelegate?.btnApply()
}
}
extension FilterVC {
var titles: [String] {
return [SegmentTitles.skillSet.rawValue,
SegmentTitles.cuisines.rawValue,
SegmentTitles.others.rawValue ]
}
var tabs: [String] {
return [StoryboardId.skillSet.rawValue,
StoryboardId.skillSet.rawValue,
StoryboardId.others.rawValue ]
}
func segmentViewInitialization() {
segmentedVC = CSSegment.setupTabs(storyboard: self.storyboard, tabs: tabs, titles: titles) as? SJSegmentedViewController
segmentedVC?.delegate = self
segmentedVC?.selectedSegmentViewHeight = 2.0
segmentedVC?.segmentTitleColor = .white
segmentedVC?.selectedSegmentViewColor = AppColor.secondary.value
segmentedVC?.segmentBackgroundColor = AppColor.primary.value
segmentedVC?.segmentViewHeight = 64.0
segmentedVC?.segmentShadow = SJShadow.light()
segmentedVC?.segmentTitleFont = AppFont.avenirMedium.size(14.0)
containerView.addSubview((segmentedVC?.view)!)
segmentedVC?.view.frame = containerView.bounds
}
}
extension FilterVC: SJSegmentedViewControllerDelegate {
func didMoveToPage(_ controller: UIViewController, segment: SJSegmentTab?, index: Int) {
if index != tabs.count-1 {
let filterVC = controller as! FilterSkillVC
filterVC.updateCurrentHeader(currentTab:SegmentTitles(rawValue: titles[index])!)
}
if selectedSegment != nil {
selectedSegment?.titleColor(.white)
}
if (segmentedVC?.segments.count)! > 0 {
selectedSegment = segmentedVC?.segments[index]
selectedSegment?.titleColor(AppColor.secondary.value)
}
}
}
My Skill VC code
import UIKit
class FilterSkillVC: UIViewController {
#IBOutlet var tblView: UITableView!
var instance = FilterVC()
lazy var arraySkills = [JSTags]()
lazy var arrayCuisines = [JSTags]()
var arrayID = [String]()
var currentHeader: SegmentTitles = .skillSet
override func viewDidLoad() {
super.viewDidLoad()
apiSkillCall()
apiCuisineCall()
registerCell(cellId: .filterListCell, forTableView: tblView)
tblView.tableFooterView = UIView()
// let instance = FilterVC()
instance.vcDelegate = self
}
func updateCurrentHeader(currentTab : SegmentTitles){
currentHeader = currentTab
tblView.reloadData()
}
//MARK: ----- Custom Methods
func countForHeader() -> NSInteger {
switch currentHeader {
case .skillSet:
return arraySkills.count
case .cuisines:
return arrayCuisines.count
default:
return 0
}
}
func titleForHeader(_ index: NSInteger) -> (name: String?, obj: AnyObject?) {
switch currentHeader {
case .skillSet:
return (name: arraySkills[index].name, obj: arraySkills[index])
case .cuisines:
return (name: arrayCuisines[index].name, obj: arrayCuisines[index])
default:
return (name: nil, obj: nil)
}
}
//MARK: ----- Handle Response Methods
func handleSkillsResponse(response: Response) {
switch response{
case .success(let response):
if let skills = response as? [JSTags] {
self.arraySkills = skills
}
case .failure(let str):
Alerts.shared.show(alert: .oops, message: /str, type: .error)
case .dataNotExist(let str):
Alerts.shared.show(alert: .oops, message: str, type: .info)
}
tblView.reloadData()
}
func handleCuisineResponse(response: Response) {
switch response{
case .success(let response):
if let cuisines = response as? [JSTags] {
self.arrayCuisines = cuisines
tblView.reloadData()
}
case .failure(let str):
Alerts.shared.show(alert: .oops, message: /str, type: .error)
case .dataNotExist(let str):
Alerts.shared.show(alert: .oops, message: str, type: .info)
}
}
//MARK: API Methods
func apiSkillCall() {
APIManager.shared.request(with: ProfileEndPoint.fetchSkills()) { (response) in
self.handleSkillsResponse(response: response)
}
}
func apiCuisineCall() {
APIManager.shared.request(with: ProfileEndPoint.fetchCuisines()) { (response) in
self.handleCuisineResponse(response: response)
}
}
}
extension FilterSkillVC : UITableViewDelegate, UITableViewDataSource, FilterListCellDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return countForHeader()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.filterListCell.rawValue) as! FilterListCell
let filter = titleForHeader(indexPath.row)
cell.lblFilterLabel.text = filter.name
//Mark: Cell delegate
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 70
}
//Mark: FilterCellDelegate Method
func buttonTapped(cell: FilterListCell) {
if let indexPath = self.tblView.indexPath(for: cell) {
print("Button tapped on row \(indexPath.row)")
if currentHeader == .skillSet {
arraySkills[indexPath.row].isSelected = !arraySkills[indexPath.row].isSelected
}
else {
arrayCuisines[indexPath.row].isSelected = !arrayCuisines[indexPath.row].isSelected
}
}
}
}
extension FilterSkillVC : FilterVCDelegate {
func btnApply() {
for object in arraySkills {
if object.isSelected {
arrayID.append((object.id) ?? "")
}
}
for object in arrayCuisines {
if object.isSelected {
arrayID.append((object.id) ?? "")
}
}
}
}
You are losing the reference to the instance as soon as the viewDidLoad method is completed.
Make instance a global Variable.
Like so :
import UIKit
class FilterSkillVC: UIViewController {
#IBOutlet var tblView: UITableView!
var instance = FilterVC() //New line added here.
lazy var arraySkills = [JSTags]()
lazy var arrayCuisines = [JSTags]()
var arrayID = [String]()
var currentHeader: SegmentTitles = .skillSet
override func viewDidLoad() {
super.viewDidLoad()
apiSkillCall()
apiCuisineCall()
registerCell(cellId: .filterListCell, forTableView: tblView)
tblView.tableFooterView = UIView()
//let instance = FilterVC() //Commented this.
instance.vcDelegate = self
}
More updates :
In the didMoveToPage method, you are getting a reference to a FilterVC (from a storyboard ??), now this instance of FilterVC is different from the instance of filterVC we created.
Please add this change and try :
func didMoveToPage(_ controller: UIViewController, segment: SJSegmentTab?, index: Int) {
if index != tabs.count-1 {
let filterVC = controller as! FilterSkillVC
filterVC.updateCurrentHeader(currentTab:SegmentTitles(rawValue: titles[index])!)
self.vcDelegate = filterVC // <== Updated this line.
}
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.