Related
I have a chat in which there are 4 views.
Settings view
Recent Messages view, where you can see the last message you have received/sent
Contacts view where you just see the contacts you have
Chat log view
When I send message in chat log view, the content in Recent Messages is updated, because the last message is changed, when updating my chat log view exits to main view
Here is my Views:
the main view with tab bar from where 3 other views is accessed
Messages view: where the last messages is shown
Chat Log view
the chat log view exits when I do handleSend function in ChatLogViewModel
struct MainView: View {
#StateObject var vm = MainMessagesViewModel() //view model variable
#State private var isPresented = false //bool variable to show/hide login view
var body: some View {
NavigationView {
TabView(selection: .constant(0)) {
ContactsView()
.tabItem {
Label("მეგობრები", systemImage: "person.2")
}
.tag(0)
MessagesView().environmentObject(vm)
.tabItem {
Label("ჩატები", systemImage: "message")
}
.tag(1)
SettingsView().environmentObject(vm)
.tabItem {
Label("დაყენებები", systemImage: "gear")
}
.tag(2)
}
.fullScreenCover(isPresented: $isPresented, onDismiss: nil){ //iOS 16 style to call a view with #published variable (#state needed)
LogInView(didCompleteLogin: {
self.vm.isCurrentlyLogOut = false
self.vm.fetchCurrentUser()
})
}
.onReceive(vm.$isCurrentlyLogOut) { isCurrentlyLogOut in
self.isPresented = isCurrentlyLogOut
self.vm.fetchCurrentUser()
}
.onChange(of: isPresented) { isCurrentlyLogOut in
self.vm.isCurrentlyLogOut = isCurrentlyLogOut
self.vm.fetchCurrentUser()
}
}
}
}
class MainMessagesViewModel: ObservableObject {
#Published var user : User? //current user (optional because is nil on start)
#Published var isCurrentlyLogOut = false
#Published var recentMessages = [RecentMessage]() //array containing recent messages user has sent
private var firestoreListener : ListenerRegistration?
init() {
DispatchQueue.main.async {
self.isCurrentlyLogOut = FirebaseManager.shared.auth.currentUser?.uid == nil //figure out if there is no user logged in
}
fetchCurrentUser() //fetch a logged in user
fetchRecentMessages()
}
func fetchCurrentUser() { //fetching the user variable inside vm
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else { return } //if there is a uid available create uid variable
FirebaseManager.shared.firestore.collection("users").document(uid).getDocument { snapshot, err in //retrieve from users/uid/ user's data, which is stored in snapshot
if let err = err { //error handler
print("Failed to fetch current user: ", err)
return
}
guard let data = snapshot?.data() else { return } //create a data variable if there is a data in snapshot
self.user = .init(data: data) //vm's user is initialized with struct's init()
}
}
private func fetchRecentMessages() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {return}
FirebaseManager.shared.firestore
.collection("recent_messages")
.document(uid)
.collection("messages")
.addSnapshotListener { querySnapshot, err in
if let err = err {
print(err)
return
}
querySnapshot?.documentChanges.forEach({ change in
let docId = change.document.documentID
let data = change.document.data()
if let index = self.recentMessages.firstIndex(where: {rm in
return rm.documentId == docId
}) {
self.recentMessages.remove(at: index)
}
self.recentMessages.insert(.init(documentId: docId, data: data), at: 0)
})
}
}
}
//MARK: - CHAT LOG VIEW
struct ChatLogView: View {
#ObservedObject var vm: ChatLogViewModel //view model variable
let interlocutor: User? //variable for interlocutor (provided from main view)
init(interlocutor: User?) { //initialize interlocutor
self.interlocutor = interlocutor
self.vm = .init(interlocutor: interlocutor) //initialize view model using interlocutor
}
var body: some View {
VStack{
messagesView
}
.navigationTitle(interlocutor?.email ?? "")
}
//MARK: - MESSAGES VIEW
private var messagesView: some View {
ScrollView {
ScrollViewReader { scrollViewProxy in
VStack {
ForEach(vm.chatMessages) { message in
VStack {
if message.fromId == FirebaseManager.shared.auth.currentUser?.uid{
HStack {
Spacer()
HStack {
Text(message.text)
.foregroundColor(.white)
}
.padding(.vertical, 5)
.padding(.horizontal, 15)
.background(Color.blue)
.cornerRadius(25)
}
.padding(.horizontal)
} else {
HStack {
HStack {
Text(message.text)
.foregroundColor(.black)
}
.padding(.vertical, 5)
.padding(.horizontal, 15)
.background(Color(.init(white: 0.94, alpha: 1)))
.cornerRadius(25)
Spacer()
}
.padding(.horizontal)
}
}
}
HStack {
Spacer()
}
.id("empty")
}
.onReceive(vm.$count) { _ in //when count of messages changes
withAnimation(.easeOut(duration: 0.5)) { //perform animation to scroll to the bottom of view
scrollViewProxy.scrollTo("empty", anchor: .bottom)
}
}
}
}
.safeAreaInset(edge: .bottom) { //insert a view to the bottom
chatBottomBar
}
}
//MARK: - CHAT BOTTOM BAR VIEW
private var chatBottomBar: some View {
HStack {
Image(systemName: "paperclip")
.padding(.leading, 5)
TextField("Message", text: $vm.chatText)
.padding(.vertical, 5)
.padding(.horizontal, 15)
.background(Color.white)
.cornerRadius(25)
Button {
vm.handleSend()
} label: {
Image(systemName: "arrow.up.circle.fill")
.font(.title)
}
}
.padding(.horizontal, 5)
.padding(.vertical, 7)
.background(Color(.init(white: 0.95, alpha: 0.99)))
}
}
class ChatLogViewModel: ObservableObject {
let interlocutor : User? //user whom you send a message
#Published var chatText = "" //var for a text of message
#Published var chatMessages = [ChatMessage]() //array of messages
#Published var count = 0 //var to track when a message is added to scroll down
init (interlocutor : User?) {
self.interlocutor = interlocutor //init a interlocutor using a interlocutor from contacts view
fetchMessages() //fetch messages from firestore to the app
}
func fetchMessages() {
guard let fromId = FirebaseManager.shared.auth.currentUser?.uid else {return} //create a fromID using current user's if available
guard let toId = interlocutor?.uid else {return} //create a toID using interlocutor's id
FirebaseManager.shared.firestore.collection("messages") //retrieve message data from path /messages/fromId/toId/ (ordered by time)
.document(fromId)
.collection(toId)
.order(by: "timestamp")
.addSnapshotListener { querySnapshot, err in //continiously listen to selected folder, the data will be in querySnapshot
if let err = err { //error handler
print("Failed to listen messages: ", err)
}
querySnapshot?.documentChanges.forEach({ change in //if there is a change of documents in querySnapshot
if change.type == .added {
let data = change.document.data() //create data variable
let docId = change.document.documentID //create docId variable
let chatMessage = ChatMessage(documentId: docId, data: data) //create chatMessage variable (initilize it with stucts's init using docId and data)
self.chatMessages.append(chatMessage) //add a message to messages array
}
})
DispatchQueue.main.async { //if message is added increase messages count (for scroll to bottom)
self.count += 1
}
}
}
func handleSend() { //function to send a message
guard let fromId = FirebaseManager.shared.auth.currentUser?.uid else {return} //if there is current user's id store it in fromId
guard let toId = interlocutor?.uid else {return} //if there is interculor store their uid in toId
let document = FirebaseManager.shared.firestore.collection("messages") //create a message document |FOR SENDER| in message/fromId/toId/
.document(fromId)
.collection(toId)
.document()
let messageData = [ //create a message data variable
"fromId" : fromId,
"toId" : toId,
"text" : chatText,
"timestamp" : Timestamp()
] as [String : Any]
document.setData(messageData) { err in //for the document created set data using created messageData
if let err = err { //error handler
print("Failed to send a message: ", err)
return
}
self.persistRecentMessage()
self.chatText = "" //when the handleSend() is called clean chatText variable as so textFiled
self.count += 1 //increase message count (for scroll down)
}
let recepientMessageDocument = FirebaseManager.shared.firestore.collection("messages") //create a message document |FOR RECEPINT| in messages/toId/fromId/
.document(toId)
.collection(fromId)
.document()
recepientMessageDocument.setData(messageData) { err in //for the document created set data using created messageData
if let err = err { //don't have to create another message data because it is the same
print("Failed to send a message: ", err)
return
}
}
}
private func persistRecentMessage() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {return} //create uid if uid of current user is available
guard let toId = interlocutor?.uid else {return}
let document = FirebaseManager.shared.firestore.collection("recent_messages") //create document to store the message to /recent_messages/uid/messages/toId
.document(uid)
.collection("messages")
.document(toId)
let data = [ //recent message data variable
"timestamp" : Timestamp(),
"text" : self.chatText,
"formId" : uid,
"toId" : toId,
"profilePicURL": interlocutor?.profilePictureUrl ?? "",
"email" : interlocutor?.email ?? ""
] as [String : Any]
document.setData(data) { err in //set recent message to created document
if let err = err {
print("Faild to store recent message: ", err)
return
}
}
let recepientData = [ //recent message data variable
"timestamp" : Timestamp(),
"text" : self.chatText,
"formId" : uid,
"toId" : toId,
"profilePicURL": "",
"email" : ""
] as [String : Any]
let recepientRecentMessageDocument = FirebaseManager.shared.firestore
.collection("recent_messages") //create a recent message document |FOR RECEPINT| in /recent_messages/toId/messages/uid
.document(toId)
.collection("messages")
.document(uid)
recepientRecentMessageDocument.setData(data) { err in //for the document created set data using created messageData
if let err = err { //dont have to create another message data because it is the same
print("Failed to send a message: ", err)
return
}
}
}
}
struct MessagesView: View {
#EnvironmentObject var vm : MainMessagesViewModel
var body: some View{
ScrollView {
ForEach (vm.recentMessages) { recentMessage in
VStack {
NavigationLink {
Text("")
} label: {
HStack {
WebImage(url: URL(string:recentMessage.profilePictureUrl))
.resizable()
.scaledToFill()
.frame(width: 64, height: 64)
.clipShape(Circle())
.padding(.horizontal)
VStack (alignment: .leading) {
Text(recentMessage.email)
Text(recentMessage.text)
}
Spacer()
Text("date")
}
.padding(.horizontal)
Divider()
}
}
}
}
.navigationBarTitle(Text("მეგობრები"), displayMode: .inline)
}
}
I need to chat log view do not exit when updating contents in messages view.
When I commented out self.persistRecentMessage() from func handleSend() function in ChatLogViewModel, disabling in this way updating of contents in Messages View, ChatLogView stopped exiting
After importing Firebase to my Project which i want to Create for iOS (Swift)
I tried to create a user, with ProfileImage and Username.
When i tap "Create Account" i get an error message it says "Error signing up".
This is the Error Message in the Terminal
2018-07-20 23:15:50.764062+0200 Collect App[3341:120860] [discovery] errors encountered while discovering extensions: Error Domain=PlugInKit Code=13 "query cancelled" UserInfo={NSLocalizedDescription=query cancelled}
I will paste my code here.
//
// SignUpViewController.swift
import Foundation
import UIKit
import Firebase
import FirebaseAuth
import FirebaseDatabase
import FirebaseStorage
class SignUpViewController: UIViewController {
#IBOutlet weak var userNameTextField: UITextField!
#IBOutlet weak var userEmailTextField: UITextField!
#IBOutlet weak var userPasswordTextField: UITextField!
#IBOutlet weak var confirmuserPasswordTextField: UITextField!
#IBOutlet weak var profileImageView: UIImageView!
#IBOutlet weak var dismissButton: UIButton!
#IBAction func tapToChangeProfileButton(_ sender: Any) {
}
#IBAction func handleDismissButton(_ sender: Any) {
self.dismiss(animated: false, completion: nil)
}
var imagePicker:UIImagePickerController!
private var authUser : User? {
return Auth.auth().currentUser
}
public func sendVerificationMail() {
if self.authUser != nil && !self.authUser!.isEmailVerified {
self.authUser!.sendEmailVerification(completion: { (error) in
// Notify the user that the mail has sent or couldn't because of an error.
})
}
else {
// Either the user is not available, or the user is already verified.
}
}
override func viewDidLoad() {
super.viewDidLoad()
let imageTap = UITapGestureRecognizer(target: self, action: #selector(openImagePicker))
profileImageView.isUserInteractionEnabled = true
profileImageView.addGestureRecognizer(imageTap)
profileImageView.layer.cornerRadius = profileImageView.bounds.height / 2
profileImageView.clipsToBounds = true
//tapToChangeProfileButton.addTarget(self, action: #selector(openImagePicker), for: .touchUpInside)
imagePicker = UIImagePickerController()
imagePicker.allowsEditing = true
imagePicker.sourceType = .photoLibrary
imagePicker.delegate = self
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#objc func openImagePicker(_ sender:Any) {
// Open Image Picker
self.present(imagePicker, animated: true, completion: nil)
}
#IBAction func CreateAccount(_ sender: Any) {
guard let username = userNameTextField.text else { return }
guard let email = userEmailTextField.text else { return }
guard let pass = userPasswordTextField.text else { return }
guard let image = profileImageView.image else { return }
Auth.auth().createUser(withEmail: email, password: pass) { user, error in
if error == nil && user != nil {
print("User created!")
// 1. Upload the profile image to Firebase Storage
self.uploadProfileImage(image) { url in
if url != nil {
let changeRequest = Auth.auth().currentUser?.createProfileChangeRequest()
changeRequest?.displayName = username
changeRequest?.photoURL = url
changeRequest?.commitChanges { error in
if error == nil {
print("User display name changed!")
self.saveProfile(username: username, profileImageURL: url!) { success in
if success {
self.dismiss(animated: true, completion: nil)
} else {
self.resetForm()
}
}
} else {
print("Error: \(error!.localizedDescription)")
self.resetForm()
}
}
} else {
self.resetForm()
}
}
} else {
self.resetForm()
}
}
}
func resetForm() {
let alert = UIAlertController(title: "Error signing up", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
func uploadProfileImage(_ image:UIImage, completion: #escaping ((_ url:URL?)->())) {
guard let uid = Auth.auth().currentUser?.uid else { return }
let storageRef = Storage.storage().reference().child("user/\(uid)")
// Create a reference to the file you want to download
let starsRef = storageRef.child("images/stars.jpg")
// Fetch the download URL
starsRef.downloadURL { url, error in
if let error = error {
// Handle any errors
} else {
// Get the download URL for 'images/stars.jpg'
}
}
}
func saveProfile(username:String, profileImageURL:URL, completion: #escaping ((_ success:Bool)->())) {
guard let uid = Auth.auth().currentUser?.uid else { return }
let databaseRef = Database.database().reference().child("users/profile/\(uid)")
let userObject = [
"username": username,
"photoURL": profileImageURL.absoluteString
] as [String:Any]
databaseRef.setValue(userObject) { error, ref in
completion(error == nil)
}
}
#IBAction func AlreadyaccountButtonTapped(_ sender: Any) {
print("Already account Button Tapped")
let LoginViewController = self.storyboard?.instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController
self.present(LoginViewController, animated: true)
}
//EmailVerificationViewController
#IBAction func ResendEmailVerificationTapped(_ sender: Any) {
self.sendVerificationMail()
}
#IBAction func LoginButtonTapped(_ sender: Any) {
print("Login Button Tapped")
let LoginViewController = self.storyboard?.instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController
self.present(LoginViewController, animated: true)
}
func displayMessage(userMessage:String) -> Void {
DispatchQueue.main.async
{
let alertController = UIAlertController(title: "Alert", message: userMessage, preferredStyle: .alert)
let OKAction = UIAlertAction(title: "OK", style: .default) { (action:UIAlertAction!) in
// Code in this block will trigger when OK button tapped.
print("Ok button tapped")
DispatchQueue.main.async
{
self.dismiss(animated: true, completion: nil)
}
}
alertController.addAction(OKAction)
self.present(alertController, animated: true, completion:nil)
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
}
}
extension SignUpViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let pickedImage = info[UIImagePickerControllerEditedImage] as? UIImage {
self.profileImageView.image = pickedImage
}
picker.dismiss(animated: true, completion: nil)
}
}
Thanks for your time !
Now the New Code with another i error that i mentioned in the Comment
#IBAction func CreateAccount(_ sender: Any) {
guard let username = userNameTextField.text else { return }
guard let image = profileImageView.image else { return }
Auth.auth().createUser(withEmail: self.userEmailTextField.text!, password: self.userPasswordTextField.text!) { user, error in
if error == nil && user != nil {
print("User created!")
// 1. Upload the profile image to Firebase Storage
self.uploadProfileImage(image, uid: user.uid) { url in
if url != nil {
let changeRequest = Auth.auth().currentUser?.createProfileChangeRequest()
changeRequest?.displayName = username
changeRequest?.photoURL = url
changeRequest?.commitChanges { error in
if error == nil {
print("User display name changed!")
self.saveProfile(username: username, profileImageURL: url!) { success in
if success {
self.dismiss(animated: true, completion: nil)
} else {
self.resetForm()
}
}
} else {
print("Error: \(error!.localizedDescription)")
self.resetForm()
}
}
} else {
self.resetForm()
}
}
} else {
self.resetForm()
}
}
}
func resetForm() {
let alert = UIAlertController(title: "Error signing up", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
func uploadProfileImage(_ image:UIImage, uid:String, completion: #escaping ((_ url:URL?)->())) {
let storageRef = Storage.storage().reference().child("user/\(uid)")
// Create a reference to the file you want to download
let starsRef = storageRef.child("images/stars.jpg")
// Fetch the download URL
starsRef.downloadURL { url, error in
if let error = error {
// Handle any errors
} else {
// Get the download URL for 'images/stars.jpg'
}
}
}
You are getting that error because your url is nil.
It is nil because you are creating the user, upon successful creation self.uploadProfileImage(image) is called in order to get the image url of the current user but the user is not logged in.
A solution would be:
Sign in the user and upon success then call uploadProfileImage.
I got some error when I send photo message.
I'm a beginner with swift language. I want to create undergraduate project about chat app. I need some help to fix this problem. Everything had worked smoothly till now but I am facing a small bug that is when I receive an image then it don't appears on the left side of the screen both incoming and outgoing images appear on the right side of the screen.
import UIKit
import JSQMessagesViewController
import MobileCoreServices
import AVKit
import FirebaseDatabase
import FirebaseStorage
import FirebaseAuth
import SDWebImage
class ChatViewController: JSQMessagesViewController {
var messages = [JSQMessage]()
var avatarDict = [String: JSQMessagesAvatarImage]()
var messageRef = FIRDatabase.database().reference().child("messages")
override func viewDidLoad() {
super.viewDidLoad()
if let currentUser = FIRAuth.auth()?.currentUser
{
self.senderId = currentUser.uid
if currentUser.isAnonymous == true
{
self.senderDisplayName = "anonymous"
} else
{
self.senderDisplayName = "\(currentUser.displayName!)"
}
}
observeMessages()
}
func observeUsers(_ id: String)
{
FIRDatabase.database().reference().child("users").child(id).observe(.value, with: {
snapshot in
if let dict = snapshot.value as? [String: AnyObject]
{
let avatarUrl = dict["profileUrl"] as! String
self.setupAvatar(avatarUrl, messageId: id)
}
})
}
func setupAvatar(_ url: String, messageId: String)
{
if url != "" {
let fileUrl = URL(string: url)
let data = try? Data(contentsOf: fileUrl!)
let image = UIImage(data: data!)
let userImg = JSQMessagesAvatarImageFactory.avatarImage(with: image, diameter: 30)
self.avatarDict[messageId] = userImg
self.collectionView.reloadData()
} else {
avatarDict[messageId] = JSQMessagesAvatarImageFactory.avatarImage(with: UIImage(named: "profileImage"), diameter: 30)
collectionView.reloadData()
}
}
func observeMessages() {
messageRef.observe(.childAdded, with: { snapshot in
// print(snapshot.value)
if let dict = snapshot.value as? [String: AnyObject] {
let mediaType = dict["MediaType"] as! String
let senderId = dict["senderId"] as! String
let senderName = dict["senderName"] as! String
self.observeUsers(senderId)
switch mediaType {
case "TEXT":
let text = dict["text"] as! String
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, text: text))
case "PHOTO":
let photo = JSQPhotoMediaItem(image: nil)
let fileUrl = dict["fileUrl"] as! String
let downloader = SDWebImageDownloader.shared()
downloader.downloadImage(with: URL(string: fileUrl)!, options: [], progress: nil, completed: { (image, data, error, finished) in
DispatchQueue.main.async(execute: {
photo?.image = image
self.collectionView.reloadData()
})
})
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: photo))
if self.senderId == senderId {
photo?.appliesMediaViewMaskAsOutgoing = true
} else {
photo?.appliesMediaViewMaskAsOutgoing = false
}
case "VIDEO":
let fileUrl = dict["fileUrl"] as! String
let video = URL(string: fileUrl)!
let videoItem = JSQVideoMediaItem(fileURL: video, isReadyToPlay: true)
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: videoItem))
if self.senderId == senderId {
videoItem?.appliesMediaViewMaskAsOutgoing = true
} else {
videoItem?.appliesMediaViewMaskAsOutgoing = false
}
default:
print("unknown data type")
}
self.collectionView.reloadData()
}
})
}
override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
let newMessage = messageRef.childByAutoId()
let messageData = ["text": text, "senderId": senderId, "senderName": senderDisplayName, "MediaType": "TEXT"]
newMessage.setValue(messageData)
self.finishSendingMessage()
}
override func didPressAccessoryButton(_ sender: UIButton!) {
print("didPressAccessoryButton")
let sheet = UIAlertController(title: "Media Messages", message: "Please select a media", preferredStyle: UIAlertControllerStyle.actionSheet)
let cancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel) { (alert:UIAlertAction) in
}
let photoLibrary = UIAlertAction(title: "Photo Library", style: UIAlertActionStyle.default) { (alert: UIAlertAction) in
self.getMediaFrom(kUTTypeImage)
}
let videoLibrary = UIAlertAction(title: "Video Library", style: UIAlertActionStyle.default) { (alert: UIAlertAction) in
self.getMediaFrom(kUTTypeMovie)
}
sheet.addAction(photoLibrary)
sheet.addAction(videoLibrary)
sheet.addAction(cancel)
self.present(sheet, animated: true, completion: nil)
}
func getMediaFrom(_ type: CFString) {
print(type)
let mediaPicker = UIImagePickerController()
mediaPicker.delegate = self
mediaPicker.mediaTypes = [type as String]
self.present(mediaPicker, animated: true, completion: nil)
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
return messages[indexPath.item]
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item]
let bubbleFactory = JSQMessagesBubbleImageFactory()
if message.senderId == self.senderId {
return bubbleFactory!.outgoingMessagesBubbleImage(with: .black)
} else {
return bubbleFactory!.incomingMessagesBubbleImage(with: .blue)
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
let message = messages[indexPath.item]
return avatarDict[message.senderId]
//return JSQMessagesAvatarImageFactory.avatarImageWithImage(UIImage(named: "profileImage"), diameter: 30)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("number of item:\(messages.count)")
return messages.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
return cell
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
print("didTapMessageBubbleAtIndexPath: \(indexPath.item)")
let message = messages[indexPath.item]
if message.isMediaMessage {
if let mediaItem = message.media as? JSQVideoMediaItem {
let player = AVPlayer(url: mediaItem.fileURL)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true, completion: nil)
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func logoutDidTapped(_ sender: AnyObject) {
do {
try FIRAuth.auth()?.signOut()
} catch let error {
print(error)
}
// Create a main storyboard instance
let storyboard = UIStoryboard(name: "Main", bundle: nil)
// From main storyboard instantiate a View controller
let LogInVC = storyboard.instantiateViewController(withIdentifier: "LogInVC") as! LogInViewController
// Get the app delegate
let appDelegate = UIApplication.shared.delegate as! AppDelegate
// Set LogIn View Controller as root view controller
appDelegate.window?.rootViewController = LogInVC
}
func sendMedia(_ picture: UIImage?, video: URL?) {
print(picture)
print(FIRStorage.storage().reference())
if let picture = picture {
let filePath = "\(FIRAuth.auth()!.currentUser)/\(Date.timeIntervalSinceReferenceDate)"
print(filePath)
let data = UIImageJPEGRepresentation(picture, 0.1)
let metadata = FIRStorageMetadata()
metadata.contentType = "image/jpg"
FIRStorage.storage().reference().child(filePath).put(data!, metadata: metadata) { (metadata, error)
in
if error != nil {
print(error?.localizedDescription)
return
}
let fileUrl = metadata!.downloadURLs![0].absoluteString
let newMessage = self.messageRef.childByAutoId()
let messageData = ["fileUrl": fileUrl, "senderId": self.senderId, "senderName": self.senderDisplayName, "MediaType": "PHOTO"]
newMessage.setValue(messageData)
}
} else if let video = video {
let filePath = "\(FIRAuth.auth()!.currentUser)/\(Date.timeIntervalSinceReferenceDate)"
print(filePath)
let data = try? Data(contentsOf: video)
let metadata = FIRStorageMetadata()
metadata.contentType = "video/mp4"
FIRStorage.storage().reference().child(filePath).put(data!, metadata: metadata) { (metadata, error)
in
if error != nil {
print(error?.localizedDescription)
return
}
let fileUrl = metadata!.downloadURLs![0].absoluteString
let newMessage = self.messageRef.childByAutoId()
let messageData = ["fileUrl": fileUrl, "senderId": self.senderId, "senderName": self.senderDisplayName, "MediaType": "VIDEO"]
newMessage.setValue(messageData)
}
}
}
}
extension ChatViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
print("did finish picking")
// get the image
print(info)
if let picture = info[UIImagePickerControllerOriginalImage] as? UIImage {
sendMedia(picture, video: nil)
}
else if let video = info[UIImagePickerControllerMediaURL] as? URL {
sendMedia(nil, video: video)
}
self.dismiss(animated: true, completion: nil)
collectionView.reloadData()
}
}
You just need to understand what is going on in your code , as i am looking into your code you are reloading collectionview before the JSQPhotoMediaItem or JSQVideoMediaItem objects are completely configured and added these MediaItem into your message array, so make sure you have completely configured your JSQPhotoMediaItem and JSQVideoMediaItem objects and only after that you add these object to your message array.
you can add this into your switch case condition
case "PHOTO":
let photo = JSQPhotoMediaItem(image: nil)
let fileUrl = dict["fileUrl"] as! String
let downloader = SDWebImageDownloader.shared()
downloader.downloadImage(with: URL(string: fileUrl)!, options: [], progress: nil, completed: { (image, data, error, finished) in
DispatchQueue.main.async(execute: {
photo?.image = image // you have image in your media object
if self.senderId == senderId {
photo?.appliesMediaViewMaskAsOutgoing = true
} else {
photo?.appliesMediaViewMaskAsOutgoing = false
}
// you just configured media object by using appliesMediaViewMaskAsOutgoing
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: photo))
// successfully added object into your message array now you should reload collectionview
self.collectionView.reloadData()
})
})
//same goes for your video condition
But still i would recommend you to follow each step one by one.
Follow these steps for adding A JSQPhotoMediaItem object
make a JSQPhotoMediaItem Object like
let photoItem = JSQPhotoMediaItem(image: UIImage(named: <your Image Object>))
make sure you have downloaded your image from server and successfully have your image object ready to add in the JSQPhotoMediaItem Object (as you are downloading it from firebase) first get the image then add it to your JSQPhotoMediaItem
now you have to tell your message type( outgoing or incoming ) so add appliesMediaViewMaskAsOutgoing property to according to you need so this would be in your code
if self.senderId == senderId {
photoItem?.appliesMediaViewMaskAsOutgoing = true
} else {
photoItem?.appliesMediaViewMaskAsOutgoing = false
}
now this is the final step for appending this object to your array
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: photoItem))
now reload collectionView as you have successfully configured your media item ( JSQPhotoMediaItem )
self.collectionView.reloadData()
same goes for the JSQVideoMediaItem objects.
I'm using Alamofire to execute a number of asynchronous requests concurrently, and SwiftyJSON to handle the response.
I need help making sure that appending to moviesByCategory occurs in order.
For example, the "top_rated" data response should be the first element appended to moviesByCategory, not "upcoming".
var moviesByCategory = [[JSON]]()
override func viewDidLoad() {
super.viewDidLoad()
let apiEndPoints = ["top_rated", "popular", "now_playing", "upcoming"]
let dispatchGroup = DispatchGroup()
for endPoint in apiEndPoints {
let endPointURL = URL(string: "https://api.themoviedb.org/3/movie/\(endPoint)?api_key=\(apiKey)&language=en-US&page=1")!
dispatchGroup.enter()
getMoviesFromEndPoint(url: endPointURL)
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.tableView.reloadData()
}
}
func getMoviesFromEndPoint(url: URL, group: dispatchGroup) {
Alamofire.request(url).responseData { response in
if let data = response.result.value {
let json = JSON(data: data)
self.moviesByCategory.append(json["results"].arrayValue)
}
}
}
The purpose for DispatchGroup is to reload the UITableView once all requests have completed.
Any help with this would be tremendously appreciated. Please do point out where I am wrong.
Add a completion handler parameter to getMoviesFromEndPoint:
func getMoviesFromEndPoint(url: URL, completion: () -> Void) { ... }
and leave the group within after the network call completed:
getMoviesFromEndPoint(url: endPointURL) {
dispatchGroup.leave()
}
Complete code:
override func viewDidLoad() {
super.viewDidLoad()
let apiEndPoints = ["top_rated", "popular", "now_playing", "upcoming"]
let dispatchGroup = DispatchGroup()
for endPoint in apiEndPoints {
let endPointURL = URL(string: "https://api.themoviedb.org/3/movie/\(endPoint)?api_key=\(apiKey)&language=en-US&page=1")!
dispatchGroup.enter()
getMoviesFromEndPoint(url: endPointURL) {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.tableView.reloadData()
}
}
func getMoviesFromEndPoint(url: URL, completion: () -> Void) {
Alamofire.request(url).responseData { response in
if let data = response.result.value {
let json = JSON(data: data)
self.moviesByCategory.append(json["results"].arrayValue)
}
completion()
}
}
I'm struggling with this Swift code already for some time and do not find the problem. The code
below should provide the File Directory as DataSource for a NSOutlineView. The GUI is quite simple
just a window with a NSOutlineView and a Object for the OutlineViewController instance.
When I start the application it shows the root entry, when I expand the root entry it shows for a short period the sub items. Then the application crashes with an Error in file "main.swift" at line "NSApplicationMain(C_ARGC, C_ARGV) --> "EXC_BAD_ACCESS(code=EXC_I386_GPFLT)" ?
If added some println() to proof the directory structure - this seems to be fine.
The swift code:
import Cocoa
import Foundation
class FileSystemItem {
let propertyKeys = [NSURLLocalizedNameKey, NSURLEffectiveIconKey, NSURLIsPackageKey, NSURLIsDirectoryKey,NSURLTypeIdentifierKey]
let fileURL: NSURL
var name: String! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLNameKey], error: nil)
return resourceValues[NSURLNameKey] as? NSString
}
var localizedName: String! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLLocalizedNameKey], error: nil)
return resourceValues[NSURLLocalizedNameKey] as? NSString
}
var icon: NSImage! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLEffectiveIconKey], error: nil)
return resourceValues[NSURLEffectiveIconKey] as? NSImage
}
var dateOfCreation: NSDate! {
let resourceValues = self.fileURL.resourceValuesForKeys([NSURLCreationDateKey], error: nil)
return resourceValues[NSURLCreationDateKey] as? NSDate
}
var dateOfLastModification: NSDate! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLContentModificationDateKey], error: nil)
return resourceValues[NSURLContentModificationDateKey] as? NSDate
}
var typeIdentifier: String! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLTypeIdentifierKey], error: nil)
return resourceValues[NSURLTypeIdentifierKey] as? NSString
}
var isDirectory: String! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLIsDirectoryKey], error: nil)
return resourceValues[NSURLIsDirectoryKey] as? NSString
}
var children: [FileSystemItem] {
var childs: [FileSystemItem] = []
var isDirectory: ObjCBool = ObjCBool(1)
let fileManager = NSFileManager.defaultManager()
var checkValidation = NSFileManager.defaultManager()
if (checkValidation.fileExistsAtPath(fileURL.relativePath)) {
if let itemURLs = fileManager.contentsOfDirectoryAtURL(fileURL, includingPropertiesForKeys:propertyKeys, options:.SkipsHiddenFiles, error:nil) {
for fsItemURL in itemURLs as [NSURL] {
if (fileManager.fileExistsAtPath(fsItemURL.relativePath, isDirectory: &isDirectory))
{
if(isDirectory == true) {
let checkItem = FileSystemItem(fileURL: fsItemURL)
childs.append(checkItem)
}
}
}
}
}
return childs
}
init (fileURL: NSURL) {
self.fileURL = fileURL
}
func hasChildren() -> Bool {
return self.children.count > 0
}
}
class OutlineViewController : NSObject, NSOutlineViewDataSource {
let rootFolder : String = "/"
let rootfsItem : FileSystemItem
let fsItemURL : NSURL
let propertyKeys = [NSURLLocalizedNameKey, NSURLEffectiveIconKey, NSURLIsPackageKey, NSURLIsDirectoryKey,NSURLTypeIdentifierKey]
init() {
self.fsItemURL = NSURL.fileURLWithPath(rootFolder)
self.rootfsItem = FileSystemItem(fileURL: fsItemURL)
for fsItem in rootfsItem.children as [FileSystemItem] {
for fsSubItem in fsItem.children as [FileSystemItem] {
println("\(fsItem.name) - \(fsSubItem.name)")
}
}
}
func outlineView(outlineView: NSOutlineView!, numberOfChildrenOfItem item: AnyObject!) -> Int {
if let theItem: AnyObject = item {
let tmpfsItem: FileSystemItem = item as FileSystemItem
return tmpfsItem.children.count
}
return 1
}
func outlineView(outlineView: NSOutlineView!, isItemExpandable item: AnyObject!) -> Bool {
if let theItem: AnyObject = item {
let tmpfsItem: FileSystemItem = item as FileSystemItem
return tmpfsItem.hasChildren()
}
return false
}
func outlineView(outlineView: NSOutlineView!, child index: Int, ofItem item: AnyObject!) -> AnyObject! {
if let theItem: AnyObject = item {
let tmpfsItem: FileSystemItem = item as FileSystemItem
return tmpfsItem.children[index]
}
return rootfsItem
}
func outlineView(outlineView: NSOutlineView!, objectValueForTableColumn tableColumn: NSTableColumn!, byItem item: AnyObject!) -> AnyObject! {
if let theItem: AnyObject = item {
let tmpfsItem: FileSystemItem = item as FileSystemItem
return tmpfsItem.localizedName
}
return "-empty-"
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet var window: NSWindow
func applicationDidFinishLaunching(aNotification: NSNotification?) {
// Insert code here to initialize your application
}
func applicationWillTerminate(aNotification: NSNotification?) {
// Insert code here to tear down your application
}
}
Any hints ?
I had a similar problem with EXC_BAD_ACCESS on an NSOutlineView - with an NSOutlineViewDataSource. The same behaviour of as soon as the node was expanded, the data was displayed then the crash occurred. Some profiling in instruments showed that somewhere a Zombie object was created, and then the Outline view tried to access it.
I think this is a bug - but I managed to get around it by changing all Swift 'Strings' to 'NSStrings'. This may have to be done for all Swift types if you are using them.
In order to ensure everything was an NSString, I had to declare constants within the class such as:
var empty_string : NSString = ""
Because anytime I fed it a Swift string all hell broke loose. Oh well hopefully this will be fixed in the future!
So, just to clarify what is going on. NSOutlineView does not retain objects that it is given for its "model"; it was always expected that the client would retain them. For ARC code, this doesn't work well, because if you return a new instance to the NSOutlineView methods the object will not be retained by anything and will quickly be freed. Then subsequent outlineView delegate methods the touch these objects will lead to crashes. The solution to that is to retain the objects yourself in your own array.
Note that the objects returned from objectValueForTableColumn are retained by the NSControl's objectValue.
Back to Swift: As Thomas noted the objects have to be objc objects since they are bridged to an objc class. A Swift string is implicitly bridged to a temporary NSString. This leads to a crash because of the above issue, since nothing retains the NSString instance. That is why maintaining an array of NSStrings "solves" this problem.
The solution would be for NSOutlineView to have an option to retain the items given to it. Please consider logging a bug request for it to do this through bugreporter.apple.com
Thanks,
corbin (I work on NSOutlineView)
It seems that
outlineView(outlineView: NSOutlineView!, objectValueForTableColumn tableColumn: NSTableColumn!, byItem item: AnyObject!) -> AnyObject!
needs to return an object that conforms to obj-c protocol. So you can return
#objc class MyClass {
...
}
(or NSString and the like). But not native Swift stuff like String or Array etc.
I believe one of the problems going on here is the fact that the "children" array is getting replaced every time the children property is accessed.
I think this causes some weak references inside the NSOutlineView to break when it queries the DataSource for information.
If you cache the "children" and access the cache to compute "numberOfChildren" and "getChildForIndex" you should see an improvement.
In Swift 3.0 I used the following code, which compiles and runs without problems. It is far away from being complete but a step in the right direction, since I am trying to translate TreeTest into Swift.
import Cocoa
import Foundation
class FileSystemItem: NSObject {
let propertyKeys: [URLResourceKey] = [.localizedNameKey, .effectiveIconKey, .isDirectoryKey, .typeIdentifierKey]
var fileURL: URL
var name: String! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.nameKey])
return resourceValues.name
}
var localizedName: String! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.localizedNameKey])
return resourceValues.localizedName
}
var icon: NSImage! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.effectiveIconKey])
return resourceValues.effectiveIcon as? NSImage
}
var dateOfCreation: Date! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.creationDateKey])
return resourceValues.creationDate
}
var dateOfLastModification: Date! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.contentModificationDateKey])
return resourceValues.contentAccessDate
}
var typeIdentifier: String! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.typeIdentifierKey])
return resourceValues.typeIdentifier
}
var isDirectory: Bool! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.isDirectoryKey])
return resourceValues.isDirectory
}
init(url: Foundation.URL) {
self.fileURL = url
}
var children: [FileSystemItem] {
var childs: [FileSystemItem] = []
let fileManager = FileManager.default
// show no hidden Files (if you want this, comment out next line)
// let options = FileManager.DirectoryEnumerationOptions.skipsHiddenFiles
var directoryURL = ObjCBool(false)
let validURL = fileManager.fileExists(atPath: fileURL.relativePath, isDirectory: &directoryURL)
if (validURL && directoryURL.boolValue) {
// contents of directory
do {
let childURLs = try
fileManager.contentsOfDirectory(at: fileURL, includingPropertiesForKeys: propertyKeys, options: [])
for childURL in childURLs {
let child = FileSystemItem(url: childURL)
childs.append(child)
}
}
catch {
print("Unexpected error occured: \(error).")
}
}
return childs
}
func hasChildren() -> Bool {
return self.children.count > 0
}
}
class OutLineViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource {
#IBOutlet weak var outlineView: NSOutlineView!
#IBOutlet weak var pathController: NSPathControl!
var fileSystemItemURL: URL!
let propertyKeys: [URLResourceKey] = [.localizedNameKey, .effectiveIconKey, .isDirectoryKey, .typeIdentifierKey]
var rootfileSystemItem: FileSystemItem!
var rootURL: URL!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let userDirectoryURL = URL(fileURLWithPath: NSHomeDirectory())
// directory "Pictures" is set as root
let rootURL = userDirectoryURL.appendingPathComponent("Pictures", isDirectory: true)
self.pathController.url = rootURL
self.rootfileSystemItem = FileSystemItem(url: rootURL)
for fileSystemItem in rootfileSystemItem.children as [FileSystemItem] {
for subItem in fileSystemItem.children as [FileSystemItem] {
print("\(fileSystemItem.name) - \(subItem.name)")
}
}
//FileSystemItem.rootItemWithPath(self.pathControl.URL.path)
//self.searchForFilesInDirectory(picturesPath)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func pathControllerAction(_ sender: NSPathControl) {
print("controller clicked")
}
// MARK: - outline data source methods
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.children.count
}
return 1
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.hasChildren()
}
return false
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.children[index]
}
return rootfileSystemItem
}
func outlineView(_ outlineView: NSOutlineView, objectValueFor tableColumn: NSTableColumn?, byItem item: Any?) -> Any? {
if let fileSystemItem = item as? FileSystemItem {
switch tableColumn?.identifier {
case "tree"?:
return fileSystemItem.localizedName
case "coordinate"?:
return " empty "
default:
break
}
}
return " -empty- "
}
// MARK: - outline view delegate methods
func outlineView(_ outlineView: NSOutlineView, shouldEdit tableColumn: NSTableColumn?, item: Any) -> Bool {
return false
}
}
With a new edit the outline view now shows all files and directories. You can influence the appearance in the children section in class FileSystemItem.