I have a list of machine name and type in tableView and when user click it, it push into detailVC.
At first the the user don't have an image, than user choose an image from gallery and return it as a PHAssets.
Than I convert the PHAssets into data and show it in collectionView, so I create an array of data and display it in collectionView.
Than I want to update MachineItem object to my data from PHAssets, since in MachineItem object have an array of Data.
So when user return from machine list in tableView, the object already update. But when I tap the list from tableView. The photo I save in an object is not showing, how can display it in my collectionView.
This is my Model
struct MachineItem: Codable {
var id = UUID().uuidString
var name: String
var type: String
var qrNumber = Int.random(in: 1..<10)
var maintenanceDate: String?
var images: [Data]?
}
This is my MachineStore class that perform all add, update, and remove object from MachineItem
class MachineStore {
var items: [MachineItem] = []
#discardableResult func add(_ machine: MachineItem, at index: Int) -> MachineItem {
let newMachine = MachineItem(id: machine.id, name: machine.name, type: machine.type, qrNumber: machine.qrNumber, maintenanceDate: machine.maintenanceDate, images: machine.images)
items.insert(newMachine, at: index)
return newMachine
}
func update(_ machine: MachineItem) {
if let index = items.firstIndex(where: { $0.id == machine.id }) {
items[index].name = machine.name
items[index].type = machine.type
items[index].qrNumber = machine.qrNumber
items[index].maintenanceDate = machine.maintenanceDate
items[index].images = machine.images
}
}
#discardableResult func remove(at index: Int) -> MachineItem {
return items.remove(at: index)
}
}
This is my MachineDataVC that have a list of MachineItem Object in TableView
class MachineDataVC: UIViewController {
var tableView = UITableView()
var store = MachineStore()
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = store.items[indexPath.row]
let detailVC = MachineDetailVC()
detailVC.item = item
detailVC.store = store
navigationController?.pushViewController(detailVC, animated: true)
tableView.deselectRow(at: indexPath, animated: true)
}
}
And this is the detailVC where I can't display the images data when back and fort from MachineDataVC to detailVC
class MachineDetailVC: UIViewController {
var item: MachineItem!
var store: MachineStore!
var images: [Data] = []
var photoCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
override func viewDidLoad() {
super.viewDidLoad()
// CollectionView setup
photoCollectionView.delegate = self
photoCollectionView.dataSource = self
photoCollectionView.register(MachineDetailCell.self, forCellWithReuseIdentifier: MachineDetailCell.cellID)
photoCollectionView.backgroundColor = .systemBackground
}
// This where PHAssets being retrieve
#objc func pickPhotos() {
let imagePickerVC = ImagePickerController()
imagePickerVC.settings.selection.max = 10
imagePickerVC.settings.theme.selectionStyle = .numbered
imagePickerVC.settings.fetch.assets.supportedMediaTypes = [.image]
imagePickerVC.settings.selection.unselectOnReachingMax = true
self.presentImagePicker(imagePickerVC) { (assets) in
} deselect: { (_) in
} cancel: { (_) in
} finish: { (assets) in
self.images = self.getImage(from: assets)
self.photoCollectionView.reloadData()
}
}
private func getImage(from assets: [PHAsset]) -> [Data] {
let images = assets.map { fetchImage(from: $0) }
print("Pick image:", images)
return images
}
private func fetchImage(from asset: PHAsset) -> Data {
let manager = PHImageManager.default()
let options = PHImageRequestOptions()
options.isSynchronous = true
var thumbnail = Data()
manager.requestImage(for: asset, targetSize: .init(width: 100, height: 100), contentMode: .aspectFill, options: options) { (result, info) in
if let selectedImage = result?.data {
thumbnail = selectedImage
}
}
return thumbnail
}
// This is how I setup collectionView
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MachineDetailCell.cellID, for: indexPath) as! MachineDetailCell
let image = images[indexPath.row]
cell.set(imageData: image)
return cell
}
// This is how I save the object from UIBarButtonItem
#objc func saveItem() {
guard let name = machineNameTF.text else { return }
guard let type = machineTypeTF.text else { return }
guard let date = machineMaintenanceDateTF.text else { return }
let machineItem = MachineItem(id: item.id, name: name, type: type, qrNumber: item.qrNumber, maintenanceDate: date, images: images)
store.update(machineItem)
}
}
What I've been trying so far is like this
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if !images.isEmpty {
return store.items.count
} else {
return images.count
}
}
I'm very glad if anyone can help me :)
I finally found my answer from my question, what I need to do is in my viewDidLoad I need to add in my MachineDetailVC
self.images = self.item.images ?? []
since when I save the data back into items in machineStore, I need to give the data into images array in MachineDetailVC
Related
I'm working on a project where the user can upload images from their library into a gallery. It's working fine, but I want the images to stay even after I refresh the app. I've been told that NSUserData will actually work better than CoreData, but I'm having a LOT of trouble implementing it into my code. I've never worked with UserData, so I am completely lost.
Any help would be appreciated!
View Controller:
// ViewController.swift
import UIKit
import PhotosUI
import Photos
import CoreData
import Foundation
// user images below
var imageArray = [UIImage]()
var imageIDs = [""]
var countImage = 0
class ViewController: UIViewController, PHPickerViewControllerDelegate {
let userDefaults = UserDefaults.standard
#IBOutlet var collectionView: UICollectionView!
// private let myFileManager = LocalFileManager.instance
// private let folderName = "closet_folder"
override func viewDidLoad() {
super.viewDidLoad()
// set up collection in closet
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addPhotos))
collectionView.register(ClosetCollectionViewCell.nib(), forCellWithReuseIdentifier: "ClosetCollectionViewCell")
collectionView.delegate = self
collectionView.dataSource = self
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSize(width: 115, height: 115)
collectionView.collectionViewLayout = layout
layout.minimumInteritemSpacing = 6
layout.minimumLineSpacing = 7
}
// access photo library
#objc private func addPhotos() {
var config = PHPickerConfiguration()
config.selectionLimit = 10
config.filter = .images
let vc = PHPickerViewController(configuration: config)
vc.delegate = self
present(vc, animated: true)
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true, completion: nil)
let group = DispatchGroup()
results.forEach { result in
group.enter()
result.itemProvider.loadObject(ofClass: UIImage.self) { reading, error in
defer {
group.leave()
}
guard let image = reading as? UIImage, error == nil else {
return
}
print(image)
imageArray.append(image)
// imageIDs.append("\(image)")
countImage += 1
print(countImage)
imageIDs.append(String(countImage))
// imageIDs.append("\(image)")
LocalFileManager.instance.saveImage(image: image, imageName: String(countImage), folderName: "closet_folder")
}
}
group.notify(queue: .main) {
self.collectionView.reloadData()
}
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
print("you tapped me!")
// when cell is tapped...
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// how many cells are shown? based on number of items the user uploaded
return imageArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// return cell for given item
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ClosetCollectionViewCell", for: indexPath) as! ClosetCollectionViewCell
// show every cell in imageArray
cell.imageView.image = LocalFileManager.instance.getImage(imageName: imageIDs[indexPath.row + 1], folderName: "closet_folder")
//imageArray[indexPath.row]
return cell
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
// margin of padding between cells
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 115, height: 115)
}
}
Here's the code with the saveImage and getImage functions:
It generates URLs for every image and the folder it's stored in.
//
// LocalFileManager.swift
import Foundation
import SwiftUI
class LocalFileManager {
static let instance = LocalFileManager()
private init() {}
func saveImage(image: UIImage, imageName: String, folderName: String) {
// create folder
createFolderIfNeeded(folderName: folderName)
// get path for image
guard
let data = image.pngData(),
let url = getURLforImage(imageName: imageName, folderName: folderName)
else { return }
// save image to path
do {
try data.write(to: url)
} catch let error {
print("image name: \(image) error saving image: \(error)")
}
}
func getImage(imageName: String, folderName: String) -> UIImage? {
guard let url = getURLforImage(imageName: imageName, folderName: folderName),
FileManager.default.fileExists(atPath: url.path)
else {
return nil
}
return UIImage(contentsOfFile: url.path)
}
// referenced functions below
// create folder to store images
private func createFolderIfNeeded(folderName: String) {
guard let url = getURLforFolder(folderName: folderName) else {
return
}
if !FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
}
catch let error {
print("folder name: \(folderName) error creating folder! \(error)")
}
}
}
// get URL for that folder
private func getURLforFolder(folderName: String) -> URL? {
guard let url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
else {
return nil
}
return url.appendingPathExtension(folderName)
}
// generate URL for images in folder
private func getURLforImage(imageName: String, folderName: String) -> URL? {
guard let folderURL = getURLforFolder(folderName: folderName) else {
return nil
}
return folderURL.appendingPathExtension(imageName)
}
}
How do you properly send data to a custom data NSObject class for a collection view? My variables are always returning as nil.
I have a splash screen in it's own view Controller. When all the data I want loaded has finished loading from firebase, I go to the main screen of the app via performSegue(withIdentifier:), here's the code for the code in question from the SplashScreenViewController:
func getDatabaseReference(){
let d = DispatchGroup()
d.enter()
let encodedURL = (postIDRefDic["post1"]! + "/postURL")
ref.child(encodedURL).observe(.value, with: { (snapshot) in
let newUrl = snapshot.value as! String
DemoSource.shared.url = newUrl
d.leave()
})
d.notify(queue: .main){
self.performSegue(withIdentifier: "showHome", sender: nil)
}
}
in the above code you can see that I'm seguing to my next view controller, HomeViewController, in the HomeViewController class I have a collection view which is helped by a custom class (the NSObject class) called DemoSource (also show above) that I'm using to assign the new data I just got in a variable within that class. This DemoSource class is a custom data class of type NSObject:
import UIKit
import Firebase
struct DataObj {
var image: UIImage?
var play_Url: URL?
var title = ""
var content = ""
}
class DemoSource: NSObject {
static let shared = DemoSource()
var demoData = [DataObj]()
var url = ""
override init() {
demoData += [
DataObj(image: #imageLiteral(resourceName: "ss-1") , play_Url: URL(string: url), title: "title ", content: "Description")
]
}
}
I use this class with my HomeViewController with the collection view:
import UIKit
import AVKit
import Firebase
import MMPlayerView
class HomeViewController: UIViewController {
var offsetObservation: NSKeyValueObservation?
lazy var mmPlayerLayer: MMPlayerLayer = {
let l = MMPlayerLayer()
l.cacheType = .memory(count: 5)
l.coverFitType = .fitToPlayerView
l.videoGravity = AVLayerVideoGravity.resizeAspect
l.replace(cover: CoverA.instantiateFromNib())
l.repeatWhenEnd = true
return l
}()
#IBOutlet weak var playerCollect: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
// remove previous download fails file
MMPlayerDownloader.cleanTmpFile()
self.navigationController?.mmPlayerTransition.push.pass(setting: { (_) in
})
offsetObservation = playerCollect.observe(\.contentOffset, options: [.new]) { [weak self] (_, value) in
guard let self = self, self.presentedViewController == nil else {return}
NSObject.cancelPreviousPerformRequests(withTarget: self)
self.perform(#selector(self.startLoading), with: nil, afterDelay: 0.2)
}
playerCollect.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 200, right:0)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.updateByContentOffset()
self?.startLoading()
}
mmPlayerLayer.getStatusBlock { [weak self] (status) in
switch status {
case .failed(let err):
let alert = UIAlertController(title: "err", message: err.description, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
case .ready:
print("Ready to Play")
case .playing:
print("Playing")
case .pause:
print("Pause")
case .end:
print("End")
default: break
}
}
mmPlayerLayer.getOrientationChange { (status) in
print("Player OrientationChange \(status)")
}
}
deinit {
offsetObservation?.invalidate()
offsetObservation = nil
print("ViewController deinit")
}
#IBAction func profileButtonTap(_ sender: Any) {
let uid = (Auth.auth().currentUser?.uid)!
let Splash = SpalshScreenViewController()
Splash.GetProfilePicture(uid: uid)
Splash.GetUsername(uid: uid)
Splash.GetName(uid: uid)
Splash.GetClipsNumber(uid: uid)
Splash.GetFollowersNumber(uid: uid)
Splash.GetFollowingsNumber(uid: uid)
performSegue(withIdentifier: "showProfile", sender: nil)
}
}
extension HomeViewController: MMPlayerFromProtocol {
func backReplaceSuperView(original: UIView?) -> UIView? {
guard let path = self.findCurrentPath(),
let cell = self.findCurrentCell(path: path) as? PlayerCell else {
return original
}
return cell.imgView
}
// add layer to temp view and pass to another controller
var passPlayer: MMPlayerLayer {
return self.mmPlayerLayer
}
func transitionWillStart() {
}
// show cell.image
func transitionCompleted() {
self.updateByContentOffset()
self.startLoading()
}
}
extension HomeViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let m = min(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height)
return CGSize(width: m, height: m*0.75)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
DispatchQueue.main.async { [unowned self] in
if self.presentedViewController != nil || self.mmPlayerLayer.isShrink == true {
//self.playerCollect.scrollToItem(at: indexPath, at: .centeredVertically, animated: true)
//self.updateDetail(at: indexPath)
} else {
self.presentDetail(at: indexPath)
}
}
}
fileprivate func updateByContentOffset() {
if mmPlayerLayer.isShrink {
return
}
if let path = findCurrentPath(),
self.presentedViewController == nil {
self.updateCell(at: path)
//Demo SubTitle
if path.row == 0, self.mmPlayerLayer.subtitleSetting.subtitleType == nil {
}
}
}
}
fileprivate func presentDetail(at indexPath: IndexPath) {
self.updateCell(at: indexPath)
mmPlayerLayer.resume()
if let vc = UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "DetailViewController") as? DetailViewController {
vc.data = DemoSource.shared.demoData[indexPath.row]
self.present(vc, animated: true, completion: nil)
}
}
fileprivate func updateCell(at indexPath: IndexPath) {
if let cell = playerCollect.cellForItem(at: indexPath) as? PlayerCell, let playURL = cell.data?.play_Url {
// this thumb use when transition start and your video dosent start
mmPlayerLayer.thumbImageView.image = cell.imgView.image
// set video where to play
mmPlayerLayer.playView = cell.imgView
mmPlayerLayer.set(url: playURL)
}
}
#objc fileprivate func startLoading() {
self.updateByContentOffset()
if self.presentedViewController != nil {
return
}
// start loading video
mmPlayerLayer.resume()
}
private func findCurrentPath() -> IndexPath? {
let p = CGPoint(x: playerCollect.contentOffset.x + playerCollect.frame.width/2, y: playerCollect.frame.height/2)
return playerCollect.indexPathForItem(at: p)
}
private func findCurrentCell(path: IndexPath) -> UICollectionViewCell? {
return playerCollect?.cellForItem(at: path)
}
}
extension HomeViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return DemoSource.shared.demoData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PlayerCell", for: indexPath) as? PlayerCell {
cell.data = DemoSource.shared.demoData[indexPath.row]
return cell
}
return UICollectionViewCell()
}
}
the first time I instantiate the Demosource class is:
extension HomeViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return DemoSource.shared.demoData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PlayerCell", for: indexPath) as? PlayerCell {
cell.data = DemoSource.shared.demoData[indexPath.row]
return cell
}
return UICollectionViewCell()
}
}
When I run my app it crashes as apparently, the url in the data class is nil, even though I set it with the url in my splashscreenviewcontroller? This DemoSource class get's instantiated before the variable is even populated it seems, when I did some debugging with breakpoints.
So my question is, after all this explaining... why isn't the url variable in the DemoSource class still nil and why is the class getting executed before when the view that uses this class is only called AFTER the data has been fetched from Firebase?
You have implemented DemoSource as a singleton, which means that it gets instantiated the first time that you reference DemoSource.shared. This is in getDatabaseReference. When it is instantiated url has its initial value (""), so that is what gets added to the the demoData array.
You don't need an initialiser.
You do need a function to add data to the demoData array.
class DemoSource: NSObject {
static let shared = DemoSource()
var demoData = [DataObj]()
add(urlString: String) {
demoData.append(DataObj(image: #imageLiteral(resourceName: "ss-1") , play_Url: URL(string: urlString), title: "title ", content: "Description"))
}
}
Then, in your getDatabaseReference -
func getDatabaseReference(){
let d = DispatchGroup()
d.enter()
let encodedURL = (postIDRefDic["post1"]! + "/postURL")
ref.child(encodedURL).observe(.value, with: { (snapshot) in
if let newUrl = snapshot.value as? String {
DemoSource.shared.add(urlString: newUrl)
}
d.leave()
})
d.notify(queue: .main){
self.performSegue(withIdentifier: "showHome", sender: nil)
}
}
Every time I tap on the like button (heart icon) the cell gets updated just fine but it gets duplicate too. I researched and tried to figure it out on my own but I couldn't see why it's being duplicated.
I investigated and I found that probably this might be a problem with the cellForRowAtIndexPath function but that function looks good to me.
Here's the relevant code:
HomeController.swift:
class HomeController: UICollectionViewController, UICollectionViewDelegateFlowLayout, HomePostCellDelegate {
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.register(HomePostCell.self, forCellWithReuseIdentifier: cellId)
let refreshControll = UIRefreshControl()
refreshControll.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
if #available(iOS 10.0, *) {
collectionView?.refreshControl = refreshControll
} else {
// Fallback on earlier versions
}
setupNavigationItems()
fetchAllPost()
}
#objc func handleUpdateFeed() {
handleRefresh()
}
#objc func handleRefresh() {
posts.removeAll()
fetchAllPost()
}
fileprivate func fetchAllPost() {
fetchPosts()
fetchFollowingUserIds()
}
fileprivate func fetchFollowingUserIds() {
guard let uid = FIRAuth.auth()?.currentUser?.uid else { return }
FIRDatabase.database().reference().child("following").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
guard let userIdsDictionary = snapshot.value as? [String: Any] else { return }
userIdsDictionary.forEach({ (key, value) in
FIRDatabase.fetchUserWithUid(uid: key, completion: { (user) in
self.fetchPostsWithUser(user: user)
})
})
}) { (err) in
print("failed to fetch following users ids:", err)
}
}
var posts = [Post]()
fileprivate func fetchPosts() {
guard let currentUserID = FIRAuth.auth()?.currentUser?.uid else { return }
FIRDatabase.fetchUserWithUid(uid: currentUserID) { (user) in
self.fetchPostsWithUser(user: user)
}
}
fileprivate func fetchPostsWithUser(user: User) {
let ref = FIRDatabase.database().reference().child("posts/\(user.uid)/")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
if #available(iOS 10.0, *) {
self.collectionView?.refreshControl?.endRefreshing()
} else {
// Fallback on earlier versions
}
guard let dictionaries = snapshot.value as? [String: Any] else { return }
dictionaries.forEach({ (key,value) in
guard let dictionary = value as? [String: Any] else { return }
var post = Post(user: user, dictionary: dictionary)
post.id = key
guard let uid = FIRAuth.auth()?.currentUser?.uid else { return }
FIRDatabase.database().reference().child("likes").child(key).child(uid).observe(.value, with: { (snapshot) in
if let value = snapshot.value as? Int, value == 1 {
post.hasLiked = true
} else {
post.hasLiked = false
}
self.posts.append(post)
self.posts.sort(by: { (p1, p2) -> Bool in
return p1.creationDate.compare(p2.creationDate) == .orderedDescending
})
self.collectionView?.reloadData()
print("fetch post with user reload data")
}, withCancel: { (err) in
print("Failed to fetch info for post")
})
})
}) { (error) in
print("Failed to fetch posts", error)
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let height: CGFloat = 310 // username + userProfileImageView
return CGSize(width: view.frame.width - 27.5, height: height)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! HomePostCell
cell.post = posts[indexPath.item]
cell.delegate = self
cell.photoImageView.image = nil
// Makes cell corners round
cell.layer.masksToBounds = true
cell.layer.cornerRadius = 17
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 5
}
func didLike(for cell: HomePostCell) {
guard let indexPath = collectionView?.indexPath(for: cell) else { return }
var post = self.posts[indexPath.item]
guard let postId = post.id else { return }
guard let uid = FIRAuth.auth()?.currentUser?.uid else { return }
let values = [uid : post.hasLiked == true ? 0 : 1]
FIRDatabase.database().reference().child("likes").child(postId).updateChildValues(values) { (err, _) in
if let err = err {
print("Failed to like post", err)
return
}
print("Successfully liked post!")
post.hasLiked = !post.hasLiked
self.posts[indexPath.item] = post
self.collectionView?.reloadItems(at: [indexPath])
}
}
}
Let me know if you have any questions, Thank you!
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.
I am trying to implement a pagination in a demo app. I am using a UICollectionView to display a lot of images from an API using SDWebImage. And the API supports pagination like this:
My problem is how to show this nextPage's images to my collectionview?
{
"meta":{
"code":200
},
"data":{ },
"pagination":{
"total":86,
"totalPages":3,
"page":1,
"nextPage":2,
"nextPageUrl":"http://.............?page=2"
}
}
And my aim is that to show this nextPageUrl's pic to the collectionview.
and here is my code :
class StoreViewController: UIViewController,UICollectionViewDataSource,UICollectionViewDelegate {
#IBOutlet var MyStoreCollectionView: UICollectionView!
var alldata: [PopulerMagazalarData]?
var indexPath: IndexPath?
var storeData : [PopulerMagazalarData] = []
let pagenumber = 1
override func viewDidLoad() {
super.viewDidLoad()
if let indexPath = self.indexPath, let storeData = self.alldata?[indexPath.row] {
let storeusername = storeData.username
GetDataFromUrl(from: "https://............./\(storeusername!)?page=\(pagenumber)")
}
}
And my data get fun from url ...
func GetDataFromUrl(from:String){
Alamofire.request(from, method: .get).validate().responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
self.storeData = [PopulerMagazalarData]()
//...Creating Data Obj.
let data = PopulerMagazalarData()
let username = json["data"]["store"]["user"]["username"].string
let userpic = json["data"]["store"]["user"]["profilePicture"].string
let productsCount = json["data"]["store"]["productsCount"].int
let description = json["data"]["store"]["description"].string
let followedby = json["data"]["store"]["user"]["counts"]["followedBy"].int
let count:Int? = json["data"]["products"].array?.count
if let ct = count {
for index in 0...ct-1{
let images = json["data"]["products"][index]["images"]["standart"]["url"].string
data.img1 = images
self.storeData.append(data)
}
}
//*****************
data.username = username
data.profilPic = userpic
data.producsCount = productsCount
data.desc = description
data.followedby = followedby
//******************
self.storeData.append(data)
// for refresh collecitonView
self.refresh_now()
case .failure(let error):
print(error)
}
}
}
//...CollectionView ReloadData func...
func refresh_now(){
DispatchQueue.main.async (
execute:
{
self.MyStoreCollectionView.reloadData()
}
)
}
and this is my collectionview funds :
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return 1
}
Please check my this answer first, add a footer view to your collectionview when footerview appears make a network call, append new array to previous array and reload your collectionview
Try this easy solution with help of CCBottomRefreshControl You need to just treat it like simple UIRefreshController.
let bottomRefreshController = UIRefreshControl()
bottomRefreshController.triggerVerticalOffset = 50
bottomRefreshController.addTarget(self, action: #selector(ViewController.refreshBottom), forControlEvents: .ValueChanged)
collectionView.bottomRefreshControl = bottomRefreshController
func refreshBottom() {
//api call for loading more data
loadMoreData()
}