I have a chat and when i scroll down to fetch older messages i want the collectionView to stay still and allow the user to manually scroll the older message who just loaded like in messenger. With my collectionView it automatically scroll to the top of the new items. I have added a method inside my scrollViewDidEndDragging but there is 2 problems:
It's glitchy meaning it scroll to the top of the new data then scroll back to the "previous position".
It's not exactly moving back to the previous position, the oldest message before loading the older data is not as the same place, it's almost in the center instead of staying on top. Here's my code:
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset.y
if contentOffset <= -40 {
self.collectionView.refreshControl?.beginRefreshing()
self.fetchMessages()
let beforeTableViewContentHeight = collectionView.contentSize.height
let beforeTableViewOffset = collectionView.contentOffset.y
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.collectionView.layer.layoutIfNeeded()
let insertCellHeight = beforeTableViewOffset + (self.collectionView.contentSize.height - beforeTableViewContentHeight)
let newOffSet = CGPoint(x: 0, y: insertCellHeight)
self.collectionView.contentOffset = newOffSet
}
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension RoomMessageViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: view.frame.width, height: 10)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return .init(top: 16, left: 0, bottom: 16, right: 0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 50)
let estimatedSizeCell = RoomMessageCell(frame: frame)
estimatedSizeCell.roomMessage = chatMessages[indexPath.section][indexPath.row]
estimatedSizeCell.layoutIfNeeded()
let targetSize = CGSize(width: view.frame.width, height: 1000)
let estimatedSize = estimatedSizeCell.systemLayoutSizeFitting(targetSize)
return CGSize(width: view.frame.width, height: estimatedSize.height)
}
}
[Updated code#1]
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.contentOffset.y <= -50 {
self.collectionView.reloadData()
self.fetchMessages()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) {
let previousContentSize = self.collectionView.contentSize
self.collectionView.collectionViewLayout.invalidateLayout()
self.collectionView.collectionViewLayout.prepare()
self.collectionView.layoutIfNeeded()
let newContentSize = self.collectionView.contentSize
print("previous Content Size \(previousContentSize) and new Content Size \(newContentSize)")
let contentOffset = newContentSize.height - previousContentSize.height
self.collectionView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
}
}
}
[Updated Code#2]
var lastDocumentSnapshot: DocumentSnapshot!
var isScrollBottom = false
var isFirstLoad = true
var isLoading = false
private var messages = [RoomMessage]()
private var chatMessages = [[RoomMessage]]()
override func viewDidAppear(_ animated: Bool) {
self.isScrollBottom = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !isScrollBottom {
DispatchQueue.main.async(execute: {
self.collectionView.scrollToBottom(animated: false)
self.isScrollBottom = true
self.isFirstLoad = false
})
}
}
func fetchMessages() {
var query: Query!
guard let room = room else{return}
guard let roomID = room.recentMessage.roomID else{return}
collectionView.refreshControl?.beginRefreshing()
if messages.isEmpty {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).limit(toLast: 15)
print("First 15 msg loaded")
} else {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).end(beforeDocument: lastDocumentSnapshot).limit(toLast: 15)
print("Next 15 msg loaded")
}
query.addSnapshotListener { (snapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
} else if snapshot!.isEmpty {
self.collectionView.refreshControl?.endRefreshing()
return
}
guard let lastSnap = snapshot?.documents.first else {return}
self.lastDocumentSnapshot = lastSnap
snapshot?.documentChanges.forEach({ (change) in
if change.type == .added {
let dictionary = change.document.data()
let timestamp = dictionary["timestamp"] as? Timestamp
var message = RoomMessage(dictionary: dictionary)
let date = timestamp?.dateValue()
let formatter1 = DateFormatter()
// formatter1.dateStyle = .medium
formatter1.timeStyle = .short
message.timestampHour = formatter1.string(from: date!)
message.timestampDate = date!
self.messages.append(message)
self.messages.sort(by: { $0.timeStamp.compare($1.timeStamp) == .orderedAscending })
}
self.attemptToAssembleGroupedMessages { (assembled) in
if assembled {
}
}
})
self.collectionView.refreshControl?.endRefreshing()
self.lastDocumentSnapshot = snapshot?.documents.first
}
}
// MARK: - Helpers
func configureUI() {
// collectionView.alwaysBounceVertical = true
self.hideKeyboardOnTap()
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.sectionHeadersPinToVisibleBounds = true
}
let refreshControl = UIRefreshControl()
collectionView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
}
fileprivate func attemptToAssembleGroupedMessages(completion: (Bool) -> ()){
chatMessages.removeAll()
let groupedMessages = Dictionary(grouping: messages) { (element) -> Date in
return element.timestampDate.reduceToMonthDayYear() }
// provide a sorting for the keys
let sortedKeys = groupedMessages.keys.sorted()
sortedKeys.forEach { (key) in
let values = groupedMessages[key]
chatMessages.append(values ?? [])
self.collectionView.reloadData()
}
completion(true)
}
#objc func handleRefresh() {
}
}
extension RoomMessageViewController {
//NEKLAS METHOD
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset.y
var lastContentSize = scrollView.contentSize
var currentOffset = scrollView.contentOffset
if contentOffset <= -50 {
self.isLoading = true
self.collectionView.reloadData()
self.fetchMessages()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) {
self.collectionView.layer.layoutIfNeeded()
let newContentSize = self.collectionView.contentSize
let delta = newContentSize.height - lastContentSize.height
lastContentSize = self.collectionView.contentSize
if delta > 0 {
currentOffset.y = currentOffset.y + delta
self.collectionView.setContentOffset(currentOffset, animated: false)
if self.isLoading { return }
self.isLoading = false
}
}
}
}
//ANOTHER METHOD (More precise but has some bug)
// override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// if scrollView.contentOffset.y <= -50 {
// self.collectionView.reloadData()
// self.fetchMessages()
// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) {
// let previousContentSize = self.collectionView.contentSize
// self.collectionView.collectionViewLayout.invalidateLayout()
// self.collectionView.collectionViewLayout.prepare()
// self.collectionView.layoutIfNeeded()
// let newContentSize = self.collectionView.contentSize
// print("previous Content Size \(previousContentSize) and new Content Size \(newContentSize)")
// let contentOffset = newContentSize.height - previousContentSize.height
// self.collectionView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
// }
// }
//}
[Updated Code #3]
var currentOffset: CGPoint = .zero
var lastContentSize: CGSize = .zero
var currentPage = 1
#objc func fetchMessages() {
var query: Query!
guard let room = room else{return}
guard let roomID = room.recentMessage.roomID else{return}
collectionView.refreshControl?.beginRefreshing()
if messages.isEmpty {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).limit(toLast: 15)
print("First 15 msg loaded")
} else {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).end(beforeDocument: lastDocumentSnapshot).limit(toLast: 15)
print("Next 15 msg loaded")
self.currentPage = self.currentPage + 1
print(self.currentPage)
}
query.addSnapshotListener { (snapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
} else if snapshot!.isEmpty {
self.collectionView.refreshControl?.endRefreshing()
return
}
guard let lastSnap = snapshot?.documents.first else {return}
self.lastDocumentSnapshot = lastSnap
snapshot?.documentChanges.forEach({ (change) in
if change.type == .added {
let dictionary = change.document.data()
let timestamp = dictionary["timestamp"] as? Timestamp
var message = RoomMessage(dictionary: dictionary)
let date = timestamp?.dateValue()
let formatter1 = DateFormatter()
formatter1.timeStyle = .short
message.timestampHour = formatter1.string(from: date!)
message.timestampDate = date!
self.messages.append(message)
self.messages.sort(by: { $0.timeStamp.compare($1.timeStamp) == .orderedAscending })
self.attemptToAssembleGroupedMessages { (assembled) in
if assembled {
}
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(10)) {
if self.currentPage == 1 {
self.collectionView.reloadData()
self.collectionView.scrollToBottom(animated: false)
} else {
self.collectionView.reloadData()
self.collectionView.layoutIfNeeded()
let newContentSize = self.collectionView.contentSize
let delta = newContentSize.height - self.lastContentSize.height
self.lastContentSize = self.collectionView.contentSize
if delta > 0 {
self.currentOffset.y = self.currentOffset.y + delta
self.collectionView.setContentOffset(self.currentOffset, animated: false)
}
}
self.collectionView.refreshControl?.endRefreshing()
self.lastDocumentSnapshot = snapshot?.documents.first
}
}
})
}
}
fileprivate func attemptToAssembleGroupedMessages(completion: (Bool) -> ()){
chatMessages.removeAll()
let groupedMessages = Dictionary(grouping: messages) { (element) -> Date in
return element.timestampDate.reduceToMonthDayYear() }
// provide a sorting for the keys
let sortedKeys = groupedMessages.keys.sorted()
sortedKeys.forEach { (key) in
let values = groupedMessages[key]
chatMessages.append(values ?? [])
// self.collectionView.reloadData()
}
completion(true)
}
}
extension RoomMessageViewController {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
// We capture any change of offSet/contentSize
self.lastContentSize = scrollView.contentSize
self.currentOffset = scrollView.contentOffset
}
Here are your updated codes:
var isFirstLoad = true
var isLoading = false
// here is place when you get your message after fetching
// THIS IS FIRST LOAD WHEN YOU OPEN SCREEN
// If pageNumber is 1 or isFirstLoad = true
// After fetching data, reloadData and scroll to lastIndex (newest message), must call this to get the final contentSize on firstLoad.
self.tableViewVideo.reloadData()
let lastIndexPath = IndexPath(row: self.listVideo.count - 1, section: 0)
self.tableViewVideo.scrollToRow(at: lastIndexPath, at: .bottom, animated: true)
self.isFirstLoad = false // reset it
Next, in your scrollViewDidScroll(scrollView: UIScrollView)
func scrollViewDidScroll(scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
self.lastContentSize = scrollView.contentSize
self.currentOffset = scrollView.contentOffset
if contentOffset <= -50 {
if self.isLoading { return } // Here is the FLAG can help you to avoid spamming scrolling that trigger history loading
self.isLoading = true
self.fetchMessages() // update your list then reloadData, end refreshing, set isLoading = false (reset FLAG)
}
}
After you have new data from history fetching, reloadData(), then now you can do your animation.
This is for history loading.
self.collectionView.reloadData()
self.collectionView.layoutIfNeeded()
let newContentSize = self.collectionView.contentSize
let delta = newContentSize.height - self.lastContentSize.height
self.lastContentSize = self.tableViewVideo.contentSize
if delta > 0 {
self.currentOffset.y = self.currentOffset.y + delta
self.collectionView.setContentOffset(self.currentOffset, animated: false)
}
self.collectionView.refreshControl?.endRefreshing()
Entire solution with real example:
import UIKit
struct VideoItem {
var thumbnailURL = ""
var videoURL = ""
var name = ""
}
class TaskListScreen: UIViewController {
#IBOutlet weak var tableViewVideo: UITableView!
#IBOutlet weak var labelHost: UILabel!
var listVideo: [VideoItem] = []
var currentOffset: CGPoint = .zero
var lastContentSize: CGSize = .zero
var isLoading = false
var currentPage = 1
override func viewDidLoad() {
super.viewDidLoad()
let refresh = UIRefreshControl()
refresh.tintColor = .red
refresh.addTarget(self, action: #selector(loadHistory), for: .valueChanged)
self.tableViewVideo.refreshControl = refresh
self.setupTableView()
self.initData()
self.showSkeletonLoadingView() // cover your message list
self.loadHistory() // currentPage = 1 mean latest messages
}
private func setupTableView() {
tableViewVideo.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: "CustomCell")
tableViewVideo.dataSource = self
tableViewVideo.delegate = self
}
#objc func loadHistory() {
let data: [VideoItem] = [
.init(thumbnailURL: "https://i.ytimg.com/vi/qk2y-TiLDZo/hqdefault.jpg", videoURL: "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8", name: "Big Buck Bunny"),
.init(thumbnailURL: "https://i.ytimg.com/vi/qk2y-TiLDZo/hqdefault.jpg", videoURL: "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8", name: "Big Buck Bunny"),
.init(thumbnailURL: "https://i.ytimg.com/vi/qk2y-TiLDZo/hqdefault.jpg", videoURL: "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8", name: "Big Buck Bunny"),
.init(thumbnailURL: "https://i.ytimg.com/vi/qk2y-TiLDZo/hqdefault.jpg", videoURL: "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8", name: "Big Buck Bunny"),
.init(thumbnailURL: "https://i.ytimg.com/vi/qk2y-TiLDZo/hqdefault.jpg", videoURL: "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8", name: "Big Buck Bunny"),
.init(thumbnailURL: "https://i.ytimg.com/vi/qk2y-TiLDZo/hqdefault.jpg", videoURL: "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8", name: "Big Buck Bunny"),
.init(thumbnailURL: "https://i.ytimg.com/vi/qk2y-TiLDZo/hqdefault.jpg", videoURL: "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8", name: "Big Buck Bunny"),
.init(thumbnailURL: "https://i.ytimg.com/vi/qk2y-TiLDZo/hqdefault.jpg", videoURL: "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8", name: "Big Buck Bunny"),
.init(thumbnailURL: "https://i.ytimg.com/vi/qk2y-TiLDZo/hqdefault.jpg", videoURL: "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8", name: "Big Buck Bunny"),
.init(thumbnailURL: "https://i.ytimg.com/vi/qk2y-TiLDZo/hqdefault.jpg", videoURL: "https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8", name: "Big Buck Bunny")]
// suppose that api takes 2 sec to finish
if self.currentPage == 1 { self.listVideo.removeAll() } // reset for first load
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) { [weak self] in
guard let _self = self else { return }
_self.listVideo.insert(contentsOf: data, at: 0) // update your data source here
if _self.currentPage == 1 {
_self.tableViewVideo.reloadData()
let lastIndexPath = IndexPath(row: _self.listVideo.count - 1, section: 0)
_self.tableViewVideo.scrollToRow(at: lastIndexPath, at: .bottom, animated: true)
_self.hideYourSkeletonLoadingView() // hide the cover that is covering your message list
} else {
_self.tableViewVideo.reloadData()
_self.tableViewVideo.layoutIfNeeded()
let newContentSize = _self.tableViewVideo.contentSize
let delta = newContentSize.height - _self.lastContentSize.height
_self.lastContentSize = _self.tableViewVideo.contentSize
if delta > 0 {
_self.currentOffset.y = _self.currentOffset.y + delta
_self.tableViewVideo.setContentOffset(_self.currentOffset, animated: false)
}
_self.tableViewVideo.refreshControl?.endRefreshing()
}
_self.currentPage += 1 // move to next page, for next load
}
}
}
extension TaskListScreen: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listVideo.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let item = listVideo[indexPath.item]
cell.labelTitle.text = "Book: \(indexPath.item + 1)" //item.name
return cell
}
}
extension TaskListScreen: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 120
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// We capture any change of offSet/contentSize
self.lastContentSize = scrollView.contentSize
self.currentOffset = scrollView.contentOffset
}
}
Related
Hello Respected Comrades,
I made a gallery app. initially I get a data of media from phone.
but when i add some new photos to device. i cant see them until :
re install app
fetching in view will appear (this is just a temperory fix)
I want to update my data dynamically with respect to phone data
code:
import UIKit
import Foundation
import Photos
import CoreData
enum MediaType{
case video, photo
}
View model
class GalleryPageViewModel : BaseViewModel{
var mediaArray = Spectator<MediaArray?>(value:
[])
var title = "Gallery"
var appDelegate:AppDelegate?
var context : NSManagedObjectContext?
let responder : AppResponder
var tempMedia = [MediaModel]()
init(with responder:AppResponder){
self.responder = responder
super.init()
}
}
extension GalleryPageViewModel{
func populatePhotos(){
mediaArray.value = []
tempMedia = []
PHPhotoLibrary.requestAuthorization{ [weak self] status in
if status == .authorized{
let assets = PHAsset.fetchAssets(with: nil)
assets.enumerateObjects{ (object,_,_) in
var image = UIImage()
image = self!.conversionPhotoToImage(asset: object)
var mediaType : MediaType = .photo
if object.mediaType == .video{
mediaType = .video
}
let media = MediaModel(img: image, asset: object, mediaType: mediaType)
self?.tempMedia.append(media)
}
}
else{
print("auth failed")
}
self?.setUpData()
}
}
func conversionPhotoToImage(asset : PHAsset) -> UIImage{
let manager = PHImageManager.default()
var media = UIImage()
let requestOptions=PHImageRequestOptions()
requestOptions.isSynchronous=true
requestOptions.deliveryMode = .highQualityFormat
manager.requestImage(for: asset, targetSize: CGSize(width: 1000, height: 1000), contentMode:.aspectFill, options: requestOptions) { image,_ in
media = image!
}
return media
}
func setUpData(){
self.mediaArray.value = tempMedia
}
}
extension GalleryPageViewModel{
func numberOfItemsInSection() -> Int{
return mediaArray.value?.count ?? 0
}
func didSelectAtRow(index:IndexPath){
self.responder.pushToDetailVc(data: mediaArray.value![index.row])
}
}
extension GalleryPageViewModel{
func setUpCoreData(){
appDelegate = UIApplication.shared.delegate as! AppDelegate
context = appDelegate?.persistentContainer.viewContext
}
func storeInCoreData(){
let entity = NSEntityDescription.entity(forEntityName: "MediaData", in: context!)
let newUser = NSManagedObject(entity: entity!, insertInto: context)
var i = 1
for data in mediaArray.value!{
newUser.setValue(data.img, forKey: "image")
newUser.setValue(data.duration, forKey: "duration")
newUser.setValue("data", forKey: "mediaType")
newUser.setValue(data.location, forKey: "location")
newUser.setValue(data.creationDate, forKey: "creationDate")
var id = String()
if data.mediaType == .photo{
id = "imgEn\(i)"
}
else{
id = "vidEn\(i)"
}
i+=1
newUser.setValue(id, forKey: "id")
}
}
}
View controller
import UIKit
import Photos
class GalleryPageViewController: BaseViewController {
var galleryCollectionView : UICollectionView?
var galleryCollectionViewFlowLayout : UICollectionViewFlowLayout? = nil
var viewModel: GalleryPageViewModel? {
didSet {
self.bindViewModel()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = viewModel?.title
view.backgroundColor = .systemBackground
setUpFlowLayout()
galleryCollectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: galleryCollectionViewFlowLayout!)
galleryCollectionView?.register(MediaCollectionViewCell.self, forCellWithReuseIdentifier: "cell")
setUpCollectionView()
viewConstraintsForGalleryPage()
viewModel?.populatePhotos()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
}
// MARK: - UI WORKS
extension GalleryPageViewController{
func setUpFlowLayout(){
galleryCollectionViewFlowLayout = UICollectionViewFlowLayout()
galleryCollectionViewFlowLayout?.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
galleryCollectionViewFlowLayout?.itemSize = CGSize(width: (view.frame.width/2) - 20, height: (self.view.frame.width/2)-10)
}
func setUpCollectionView(){
galleryCollectionView?.dataSource = self
galleryCollectionView?.delegate = self
}
func viewConstraintsForGalleryPage(){
// adding sub view to maintain heirarchy.
view.addSubview(galleryCollectionView!)
// constraints for views in this page
// collection view
galleryCollectionView?.translatesAutoresizingMaskIntoConstraints = false
// galleryCollectionView?.backgroundColor = .blue
galleryCollectionView?.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,constant: 10).isActive = true
galleryCollectionView?.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
galleryCollectionView?.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,constant: 10).isActive = true
galleryCollectionView?.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
}
}
//MARK: - COLLECTION VIEW DELEGATE AND DATA SOURCE
extension GalleryPageViewController : UICollectionViewDelegate{
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// vc.data = viewModel?.mediaArray.value?[indexPath.row]
viewModel?.didSelectAtRow(index: indexPath)
}
}
extension GalleryPageViewController : UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel?.numberOfItemsInSection() ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MediaCollectionViewCell
let cellData = viewModel?.mediaArray.value?[indexPath.row]
cell.imageView.image = cellData?.img
if cellData?.mediaType == .video{
cell.playBtn.isHidden = false
cell.durationLabel.isHidden = false
let manager = PHImageManager.default()
manager.requestAVAsset(forVideo: (cellData?.asset)!, options: nil){asset,_,_ in
let duration = asset?.duration
let durationTime = CMTimeGetSeconds(duration!)
let finalTime = durationTime/60
DispatchQueue.main.async {
cell.durationLabel.text = "\(Int(finalTime)) : \(Int(durationTime) % 60)"
}
}
}
else{
cell.playBtn.isHidden = true
cell.durationLabel.isHidden = true
}
return cell
}
}
//MARK: - BINDING VARIABLES
extension GalleryPageViewController{
private func bindViewModel(){
viewModel?.mediaArray.bind { [weak self] _ in
print("i am gonna reload")
print(self?.viewModel?.mediaArray.value?.isEmpty)
DispatchQueue.main.async {
self?.galleryCollectionView?.reloadData()
}
}
}
}
When I close all VC and go back to my root I see that some element views and cells, like HeaderViewNew, DisposalsShimmering, exist in a heap, these elements are declared like lazy var, why is this element not released from memory if I close VC that use it, it is normal, or I did some mistake?
I know that if I have a few examples of VC in memory this is a retail cycle, but with this element inside the heap hierarchy I am a little confused, and using info inside the Memory graph (main window) I can't solve this problem
Example of an object that has not been released:
import UIKit
protocol HeaderMenuOptionSelected: AnyObject {
func menuSelected(index: Int, title: String)
}
class HeadeViewMenu: UIView, UITableViewDelegate, UITableViewDataSource {
private var hiddenRows: Array<Int> = Array()
private var cellSize: CGFloat = 60
var selectedIndex: Int!
var tabBarHeight: CGFloat!
var tabBar: UIView!
let tabBarView = UIView()
var menuHeight: CGFloat = 0
var P_menuHeightConstraint: NSLayoutConstraint!
var menuArr: Array<HeaderMenuListOfOptions> = Array()
weak var delegate: HeaderMenuOptionSelected?
func clearMenuOptions() {
menuArr.removeAll()
P_menu.reloadData()
}
func setupListOfOptions(menuList: [HeaderMenuListOfOptions]) {
menuArr = menuList
P_menu.reloadData()
}
func setupMenuHeight(countOfElement: Int) {
menuHeight = cellSize * CGFloat(countOfElement)
}
// This trick need to use because if user open menu but user level not yet applied, after fetching setting from server need to update menu in menu open, but only ONE time
private var firstTimeOpen = true
func updateMenuHeight() {
guard firstTimeOpen != false else { return }
firstTimeOpen = false
if (P_menuHeightConstraint.constant != 0) {
menuHeight = cellSize * CGFloat(menuArr.count)
P_menuHeightConstraint.constant = menuHeight
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
} completion: { anim in
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
menuArr.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! HeaderMenuCell
// iOS way to set default text label
// var content = cell.defaultContentConfiguration()
// content.text = "test"
// cell.contentConfiguration = content
cell.P_label.text = menuArr[indexPath.row].title
cell.selectionStyle = .none
cell.P_label.isHidden = needHideElement(index: indexPath.row)
cell.P_img.isHidden = needHideElement(index: indexPath.row)
cell.P_countOfElements.isHidden = needHideElement(index: indexPath.row)
if selectedIndex == indexPath.row {
cell.P_img.image = UIImage(named: menuArr[indexPath.row].selectedImg)
cell.P_label.textColor = menuArr[indexPath.row].selectedColor
cell.initCount(count: menuArr[indexPath.row].count)
} else {
cell.P_img.image = UIImage(named: menuArr[indexPath.row].img)
cell.P_label.textColor = menuArr[indexPath.row].unselectedColor
cell.initCount(count: menuArr[indexPath.row].count)
}
return cell
}
func openPageByImitationGesture(index: Int) {
hideMenu()
delegate?.menuSelected(index: index, title: menuArr[index].title)
selectedIndex = index
P_menu.reloadData()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("index menu \(indexPath.row)")
hideMenu()
// FeedbackGenerator.shared.interactionFeedback()
// guard selectedIndex != indexPath.row else { return }
delegate?.menuSelected(index: indexPath.row, title: menuArr[indexPath.row].title)
// let cell = tableView.cellForRow(at: indexPath) as! HeaderMenuCell
selectedIndex = indexPath.row
tableView.reloadData()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return getRowHeight(index: indexPath.row)
}
func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) {
print("index unhi....\(indexPath.row)")
}
lazy var P_menu: UITableView = {
let view = UITableView()
view.translatesAutoresizingMaskIntoConstraints = false
view.delegate = self
view.dataSource = self
view.register(HeaderMenuCell.self, forCellReuseIdentifier: "cell")
view.isScrollEnabled = false
view.separatorColor = .clear
view.separatorStyle = .none
return view
}()
lazy var P_menuView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.2
view.layer.shadowOffset = CGSize(width: 0, height: 5)
view.layer.shadowRadius = 2
return view
}()
lazy var P_backgroundView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.translatesAutoresizingMaskIntoConstraints = false
view.alpha = 0
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(clickAtBackgroundView))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
view.addGestureRecognizer(tapGesture)
view.isUserInteractionEnabled = true
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
setupView()
}
deinit {
print("deinit called NewHeaderMenu")
}
func setupView() {
self.isHidden = true
self.addSubview(P_backgroundView)
self.addSubview(P_menuView)
self.P_menuView.addSubview(P_menu)
P_backgroundView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
P_backgroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
P_backgroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
P_backgroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
P_menu.topAnchor.constraint(equalTo: P_menuView.topAnchor).isActive = true
P_menu.leadingAnchor.constraint(equalTo: P_menuView.leadingAnchor).isActive = true
P_menu.trailingAnchor.constraint(equalTo: P_menuView.trailingAnchor).isActive = true
P_menu.bottomAnchor.constraint(equalTo: P_menuView.bottomAnchor).isActive = true
P_menuView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
P_menuView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
P_menuView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
P_menuHeightConstraint = P_menuView.heightAnchor.constraint(equalToConstant: 0)
P_menuHeightConstraint.isActive = true
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func openMenu() {
self.isHidden = false
UIView.animate(withDuration: 0.1) {
self.P_backgroundView.alpha = 0.2
self.hideTabBar()
} completion: { anim in
if (anim == true) {
self.P_menuHeightConstraint.constant = self.menuHeight
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
} completion: { anim in
}
}
}
}
func hideMenu() {
P_menuHeightConstraint.constant = 0
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
} completion: { anim in
if (anim == true) {
UIView.animate(withDuration: 0.1) {
self.P_backgroundView.alpha = 0
self.showTabBar()
} completion: { anim in
if (anim == true) {
self.isHidden = true
}
}
}
}
}
func needToHideRows(rows: [Int]) {
hiddenRows = rows
let countOfElements = (menuArr.count - rows.count)
print("Menu count of elements \(countOfElements)")
setupMenuHeight(countOfElement: countOfElements)
P_menu.reloadData()
}
func unhideAllElements(count: Int) {
hiddenRows = []
setupMenuHeight(countOfElement: count)
P_menu.reloadData()
}
func getRowHeight(index: Int) -> CGFloat {
var size: CGFloat = menuHeight / CGFloat(menuArr.count - hiddenRows.count)
hiddenRows.forEach { obj in
if (index == obj) {
size = 0
}
}
return size
}
func needHideElement(index: Int) -> Bool {
var needToHide = false
hiddenRows.forEach { obj in
if (index == obj) {
needToHide = true
}
}
return needToHide
}
func setupTabBar(view: UIView) {
guard tabBarHeight != nil else {
return
}
tabBar = view
tabBar.addSubview(tabBarView)
tabBarView.translatesAutoresizingMaskIntoConstraints = false
tabBarView.backgroundColor = .black
tabBarView.heightAnchor.constraint(equalToConstant: tabBarHeight).isActive = true
tabBarView.leadingAnchor.constraint(equalTo: tabBar.leadingAnchor).isActive = true
tabBarView.trailingAnchor.constraint(equalTo: tabBar.trailingAnchor).isActive = true
tabBarView.bottomAnchor.constraint(equalTo: tabBar.bottomAnchor).isActive = true
tabBarView.alpha = 0
}
func hideTabBar() {
UIView.animate(withDuration: 0.1) {
self.tabBarView.alpha = 0.2
}
}
func showTabBar() {
UIView.animate(withDuration: 0.1) {
self.tabBarView.alpha = 0
}
}
#objc func clickAtBackgroundView() {
hideMenu()
delegate?.menuSelected(index: selectedIndex, title: menuArr[selectedIndex].title)
}
func selectOptionNonProgrammatically(index: Int) {
// This function need for fixind problem when user open app and app had list of sevec filters for disposal tab
hideMenu()
// delegate?.menuSelected(index: index, title: title)
delegate?.menuSelected(index: index, title: menuArr[index].title)
}
func updateCounter(index: Int, count: Int?) {
guard count != nil && count != 0 else { return }
menuArr[index].count = count
P_menu.reloadData()
}
}
Inside VC that contained this element I do next:
1. Init
lazy var P_headerViewNewMenu: HeadeViewMenu = {
let view = HeadeViewMenu()
view.delegate = self
return view
}()
2. Deinit
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
P_headerViewNewMenu.delegate = nil
}
3. Init constraints
self.view.addSubview(P_headerViewNewMenu)
P_headerViewNewMenu.topAnchor.constraint(equalTo: P_headerViewNew.bottomAnchor).isActive = true
P_headerViewNewMenu.leadingAnchor.constraint(equalTo: P_headerViewNew.leadingAnchor).isActive = true
P_headerViewNewMenu.trailingAnchor.constraint(equalTo: P_headerViewNew.trailingAnchor).isActive = true
P_headerViewNewMenu.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
P_headerViewNewMenu.tabBarHeight = self.tabBarController?.tabBar.frame.height
P_headerViewNewMenu.setupTabBar(view: (self.tabBarController?.view)!)
I found the problem with be help of matt, years of experience of asking the right question ;) Problem was inside P_headerViewNewMenu.setupTabBar(view: (self.tabBarController?.view)!)
link to the object was not released.
Because VC was deallocated, and all objects continue to be in the heap, I haven't been understood that these are all objects that not called deinit, but VC was released from memory
I have this UITableView that has xib files as cells and it all works as intended, however the last cell only when I scroll down doesn't have a background color. It only happens when I scroll down and then it pops back to normal position. I'm essentially wondering how I can change the color of that cell just to my background color. Thank you.
//
// BlogViewController.swift
// testAPI
//
// Created by Dilan Piscatello on 4/2/20.
// Copyright © 2020 Dilan Piscatello. All rights reserved.
//
import Foundation
import UIKit
import Firebase
class BlogViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
var x = 0
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0{
return 1
}else{
return posts.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0{
let cell = Bundle.main.loadNibNamed("QuestionTableViewCell", owner: self, options: nil)?.first as! QuestionTableViewCell
print("this is how many times it ran")
cell.setPost(question: self.question)
return cell
}
else{
let cell = tableView.dequeueReusableCell(withIdentifier: "postcell", for: indexPath) as! PostTableViewCell
cell.setPost(post: posts[indexPath.row])
return cell
}
}
var question = "sdfsdfs"
var tableView:UITableView!
var lastUploadPostID:String?
var discussion:UILabel! = UILabel()
var posts = [Post]()
var fetchingMore = false
var endReached = false
let leadingScreensForBatching:CGFloat = 3.0
var cellHeights: [IndexPath:CGFloat] = [:]
var refreshControl:UIRefreshControl!
var postRef:DatabaseReference{
return Database.database().reference().child("posts")
}
var oldPostQuery:DatabaseQuery{
var queryRef:DatabaseQuery
let lastPost = self.posts.last
if lastPost == nil{
queryRef = postRef.queryOrdered(byChild: "timestamp")
}else{
let lastTimestamp = lastPost!.createdAt.timeIntervalSince1970*1000
queryRef = postRef.queryOrdered(byChild: "timestamp").queryEnding(atValue: lastTimestamp)
}
return queryRef
}
var newPostQuery:DatabaseQuery{
var queryRef:DatabaseQuery
let firstPost = self.posts.first
if firstPost == nil{
queryRef = postRef.queryOrdered(byChild: "timestamp")
}else{
let firstTimestamp = firstPost!.createdAt.timeIntervalSince1970*1000
queryRef = postRef.queryOrdered(byChild: "timestamp").queryStarting(atValue: firstTimestamp)
}
return queryRef
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func hi(){
let db = Firestore.firestore()
db.collection("blog").document("question").getDocument { (document,error) in
if error != nil{
print("cant get data")
}
if document != nil && document!.exists{
if let documentdata = document?.data() {
self.question = documentdata["question"] as! String
self.tableView.reloadData()
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
discussion.text = "Discussion"
//longTitleLabel.font = ................
discussion.font = UIFont(name: "HelveticaNeue-Bold", size: 31)
discussion.translatesAutoresizingMaskIntoConstraints = false
if let navigationBar = self.navigationController?.navigationBar {
navigationBar.addSubview(discussion)
//navigationBar.shadowImage = UIImage()
// navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
// navigationBar.isTranslucent = true
navigationBar.barTintColor = UIColor( red: 128/255, green: 117/255, blue: 255/255, alpha: 1)
discussion.leftAnchor.constraint(equalTo: navigationBar.leftAnchor, constant: 22).isActive = true
discussion.widthAnchor.constraint(equalToConstant: 300).isActive = true
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
self.tableView.separatorStyle = .none
view.addSubview(tableView)
hi()
print(question)
// Do any additional setup after loading the view.
let cellNib = UINib(nibName: "PostTableViewCell", bundle: nil)
tableView.register(cellNib, forCellReuseIdentifier: "postcell")
var layoutGuide:UILayoutGuide!
layoutGuide = view.safeAreaLayoutGuide
tableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10).isActive = true
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10).isActive = true
tableView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = true
tableView.delegate = self
tableView.dataSource = self
tableView.tableFooterView = UIView()
tableView.reloadData()
refreshControl = UIRefreshControl()
tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
beginBatchFetch()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// if let navigationController = navigationController as? ScrollingNavigationController {
// navigationController.followScrollView(tableView, delay: 0.0)
//}
listenForNewPosts()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopListeningForNewPosts()
}
#objc func handleRefresh(){
print()
newPostQuery.queryLimited(toFirst: 20).observeSingleEvent(of: .value, with: { (snapshot) in
var tempPosts = [Post]()
let firstPost = self.posts.first
for child in snapshot.children{
if let childSnapshot = child as? DataSnapshot,
let dict = childSnapshot.value as? [String:Any],
let post = Post.parse(childSnapshot.key, dict),
childSnapshot.key != firstPost?.id{
tempPosts.insert(post, at: 0)
}
}
self.posts.insert(contentsOf: tempPosts, at: 0)
self.tableView.reloadData()
self.refreshControl.endRefreshing()
//let newIndexPaths = (1..<tempPosts.count).map { i in
// return IndexPath(row: i, section: 1)
//}
//self.tableView.insertRows(at: newIndexPaths, with: .top)
// self.refreshControl.endRefreshing()
// self.tableView.scrollToRow(at: IndexPath(row:0,section: 0), at: .top, //animated:true)
//self.listenForNewPosts()
//return completion(tempPosts)
//self.posts = tempPosts
//self.tableView.reloadData()
})
}
func fetchPosts(completion: #escaping(_ posts:[Post])->()){
oldPostQuery.queryLimited(toLast: 20).observeSingleEvent(of: .value, with: { (snapshot) in
var tempPosts = [Post]()
let lastPost = self.posts.last
for child in snapshot.children{
if let childSnapshot = child as? DataSnapshot,
let dict = childSnapshot.value as? [String:Any],
let post = Post.parse(childSnapshot.key, dict),
childSnapshot.key != lastPost?.id{
tempPosts.insert(post, at: 0)
}
}
return completion(tempPosts)
//self.posts = tempPosts
//Zself.tableView.reloadData()
})
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
cell.selectionStyle = UITableViewCell.SelectionStyle.none
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 72.0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.size.height * leadingScreensForBatching {
if !fetchingMore && !endReached {
beginBatchFetch()
}
}
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
func beginBatchFetch(){
fetchingMore = true
fetchPosts{ newPosts in
self.posts.append(contentsOf: newPosts)
self.endReached = newPosts.count == 0
self.fetchingMore = false
UIView.performWithoutAnimation {
self.tableView.reloadData()
self.listenForNewPosts()
}
}
//fetch the post
}
var postListenerHandle:UInt?
func listenForNewPosts(){
guard !fetchingMore else{ return}
//to avoid the listeners twice (duplicate)
stopListeningForNewPosts()
postListenerHandle = newPostQuery.observe(.childAdded) { (snapshot) in
if snapshot.key != self.posts.first?.id {
if let data = snapshot.value as? [String:Any],
//let post = Post.parse(snapshot.key,data)
let _ = Post.parse(snapshot.key,data){
self.stopListeningForNewPosts()
if snapshot.key == self.lastUploadPostID{
self.handleRefresh()
self.lastUploadPostID = nil
}else{
}
}
}
}
}
func stopListeningForNewPosts(){
if let handle = postListenerHandle{
newPostQuery.removeObserver(withHandle: handle)
postListenerHandle = nil
}
}
// func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
//if let navigationController = navigationController as? //ScrollingNavigationController {
// navigationController.showNavbar(animated: true, scrollToTop: true)
//}
// return true
//}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let newPostNavBar = segue.destination as? UINavigationController,
let newPostVC = newPostNavBar.viewControllers[0] as? WritePostViewController{
newPostVC.delegate = self
}
}
}
extension BlogViewController: NewPostVCDelegate{
func didUploadPost(withID id: String) {
print("what up this is the id \(id)")
self.lastUploadPostID = id
}
}
In the storyboard add inside the tableView as last element a transparent view with height equal to 1
I try to make a like button and unlike but I it gives me error when I try to press the button in the simulator .if you know any other code for the like button to be much easier will be helpful ( or some websites , yt vids)
#IBOutlet weak var postTextLabel: UILabel!
#IBOutlet weak var subtitleLabel: UILabel!
#IBOutlet weak var profileImageView: UIImageView!
#IBOutlet weak var usernameLabel: UILabel!
#IBOutlet weak var likeLabel : UILabel!
#IBOutlet weak var likeBtn: UIButton!
#IBOutlet weak var unlikeBtn: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
profileImageView.layer.cornerRadius = profileImageView.bounds.height / 2
profileImageView.clipsToBounds = true
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
weak var post:Post?
func set(post:Post) {
self.post = post
var postID : String!
self.profileImageView.image = nil
ImageService.getImage(withURL: post.author.photoURL) { image , url in
guard let _post = self.post else {return}
if _post.author.photoURL.absoluteString == url.absoluteString {
self.profileImageView.image = image
}else {
print("not the right image")
}
}
usernameLabel.text = post.author.username
postTextLabel.text = post.text
subtitleLabel.text = post.createdAt.calenderTimeSinceNow()
}
var postID : String!
#IBAction func likePressed(_ sender: Any) {
self.postID = "post_0"
let ref = Database.database().reference()
let keyToPost = ref.child("posts").childByAutoId().key
ref.child("posts").child(self.postID).observeSingleEvent(of: .value) { (snapshot) in
if let post = snapshot.value as? [String : AnyObject] {
let updateLikes : [ String : Any] = [ "peopleWhoLike/\(keyToPost)" : Auth.auth().currentUser!.uid ]
ref.child("posts").child(self.postID).updateChildValues(updateLikes, withCompletionBlock : {(error ,reff) in
if error == nil {
ref.child("posts").child(self.postID).observeSingleEvent(of : .value, with: { (snap) in
if let properties = snap.value as? [ String : AnyObject] {
if let likes = properties["peopleWhoLike"] as? [String: AnyObject] {
let count = likes.count
self.likeLabel.text = "\(count) Likes"
}
}
})
}
})
}
}
}
#IBAction func unlikedPressed(_ sender:Any) {
let ref = Database.database().reference()
ref.child("posts").child(self.postID).observeSingleEvent(of: .value, with: { (snapshot) in
if let properties = snapshot.value as? [String : AnyObject] {
if let peopleWhoLike = properties["peopleWhoLike"] as? [String: AnyObject] {
for (id,person) in peopleWhoLike {
if person as? String == Auth.auth().currentUser!.uid {
ref.child("posts").child(self.postID).child("peopleWhoLike").child(id).removeValue(completionBlock: {(error , reff)in
if error == nil {
ref.child("posts").child(self.postID).observeSingleEvent(of: .value, with: {(snap) in
if let prop = snap.value as? [String : AnyObject] {
if let likes = prop["peopleWhoLike"] as? [String: AnyObject] {
let count = likes.count
self.likeLabel.text = "\(count) Likes"
ref.child("posts").child(self.postID).updateChildValues(["likes" : count])
} else {
self.likeLabel.text = " 0 Likes"
ref.child("posts").child(self.postID).updateChildValues(["likes" : 0])
}
}
})
}
})
self.likeBtn.isHidden = false
self.unlikeBtn.isHidden = true
self.unlikeBtn.isEnabled = true
break
}
}
}
}
})
ref.removeAllObservers()
}
}
class HomeController: UIViewController, UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return posts.count
case 1:
return fetchingMore ? 1 : 0
default:
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell
cell.set(post: posts[indexPath.row])
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "loadingCell", for: indexPath) as! LoadingCell
cell.spinner.startAnimating()
return cell
}
}
var tableView:UITableView!
var cellHeights: [IndexPath : CGFloat] = [:]
var posts = [Post]()
var fetchingMore = false
var endReached = false
let leadingScreensForBatching:CGFloat = 3.0
var refreshControl:UIRefreshControl!
var seeNewPostsButton:SeeNewPostsButton!
var seeNewPostsButtonTopAnchor:NSLayoutConstraint!
var lastUploadedPostID:String?
var postsRef:DatabaseReference {
return Database.database().reference().child("posts")
}
var oldPostsQuery:DatabaseQuery {
var queryRef:DatabaseQuery
let lastPost = posts.last
if lastPost != nil {
let lastTimestamp = lastPost!.createdAt.timeIntervalSince1970 * 1000
queryRef = postsRef.queryOrdered(byChild: "timestamp").queryEnding(atValue: lastTimestamp)
} else {
queryRef = postsRef.queryOrdered(byChild: "timestamp")
}
return queryRef
}
var newPostsQuery:DatabaseQuery {
var queryRef:DatabaseQuery
let firstPost = posts.first
if firstPost != nil {
let firstTimestamp = firstPost!.createdAt.timeIntervalSince1970 * 1000
queryRef = postsRef.queryOrdered(byChild: "timestamp").queryStarting(atValue: firstTimestamp)
} else {
queryRef = postsRef.queryOrdered(byChild: "timestamp")
}
return queryRef
}
#IBAction func handleLogoutButton(_ sender: Any) {
try! Auth.auth().signOut()
}
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView(frame: view.bounds, style: .plain)
let cellNib = UINib(nibName: "PostTableViewCell", bundle: nil)
tableView.register(cellNib, forCellReuseIdentifier: "postCell")
tableView.register(LoadingCell.self, forCellReuseIdentifier: "loadingCell")
tableView.backgroundColor = UIColor(white: 0.90,alpha:1.0)
view.addSubview(tableView)
var layoutGuide:UILayoutGuide!
if #available(iOS 11.0, *) {
layoutGuide = view.safeAreaLayoutGuide
} else {
// Fallback on earlier versions
layoutGuide = view.layoutMarginsGuide
}
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: layoutGuide.topAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = true
tableView.delegate = self
tableView.dataSource = self
tableView.reloadData()
refreshControl = UIRefreshControl()
if #available(iOS 10.0, *) {
tableView.refreshControl = refreshControl
} else {
// Fallback on earlier versions
tableView.addSubview(refreshControl)
}
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
seeNewPostsButton = SeeNewPostsButton()
view.addSubview(seeNewPostsButton)
seeNewPostsButton.translatesAutoresizingMaskIntoConstraints = false
seeNewPostsButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
seeNewPostsButtonTopAnchor = seeNewPostsButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor, constant: -44)
seeNewPostsButtonTopAnchor.isActive = true
seeNewPostsButton.heightAnchor.constraint(equalToConstant: 32.0).isActive = true
seeNewPostsButton.widthAnchor.constraint(equalToConstant: seeNewPostsButton.button.bounds.width).isActive = true
seeNewPostsButton.button.addTarget(self, action: #selector(handleRefresh), for: .touchUpInside)
//observePosts()
beginBatchFetch()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
listenForNewPosts()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopListeningForNewPosts()
}
func toggleSeeNewPostsButton(hidden:Bool) {
if hidden {
// hide it
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseOut, animations: {
self.seeNewPostsButtonTopAnchor.constant = -44.0
self.view.layoutIfNeeded()
}, completion: nil)
} else {
// show it
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: .curveEaseOut, animations: {
self.seeNewPostsButtonTopAnchor.constant = 12
self.view.layoutIfNeeded()
}, completion: nil)
}
}
#objc func handleRefresh() {
print("Refresh!")
toggleSeeNewPostsButton(hidden: true)
newPostsQuery.queryLimited(toFirst: 20).observeSingleEvent(of: .value, with: { snapshot in
var tempPosts = [Post]()
let firstPost = self.posts.first
for child in snapshot.children {
if let childSnapshot = child as? DataSnapshot,
let data = childSnapshot.value as? [String:Any],
let post = Post.parse(childSnapshot.key, data),
childSnapshot.key != firstPost?.id {
tempPosts.insert(post, at: 0)
}
}
self.posts.insert(contentsOf: tempPosts, at: 0)
let newIndexPaths = (0..<tempPosts.count).map { i in
return IndexPath(row: i, section: 0)
}
self.refreshControl.endRefreshing()
self.tableView.insertRows(at: newIndexPaths, with: .top)
self.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
self.listenForNewPosts()
})
}
func fetchPosts(completion:#escaping (_ posts:[Post])->()) {
oldPostsQuery.queryLimited(toLast: 20).observeSingleEvent(of: .value, with: { snapshot in
var tempPosts = [Post]()
let lastPost = self.posts.last
for child in snapshot.children {
if let childSnapshot = child as? DataSnapshot,
let data = childSnapshot.value as? [String:Any],
let post = Post.parse(childSnapshot.key, data),
childSnapshot.key != lastPost?.id {
tempPosts.insert(post, at: 0)
}
}
return completion(tempPosts)
})
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.size.height * leadingScreensForBatching {
if !fetchingMore && !endReached {
beginBatchFetch()
}
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeights[indexPath] = cell.frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 72.0
}
func beginBatchFetch() {
fetchingMore = true
self.tableView.reloadSections(IndexSet(integer: 1), with: .fade)
fetchPosts { newPosts in
self.posts.append(contentsOf: newPosts)
self.fetchingMore = false
self.endReached = newPosts.count == 0
UIView.performWithoutAnimation {
self.tableView.reloadData()
self.listenForNewPosts()
}
}
}
var postListenerHandle:UInt?
func listenForNewPosts() {
guard !fetchingMore else { return }
// Avoiding duplicate listeners
stopListeningForNewPosts()
postListenerHandle = newPostsQuery.observe(.childAdded, with: { snapshot in
if snapshot.key != self.posts.first?.id,
let data = snapshot.value as? [String:Any],
let post = Post.parse(snapshot.key, data) {
self.stopListeningForNewPosts()
if snapshot.key == self.lastUploadedPostID {
self.handleRefresh()
self.lastUploadedPostID = nil
} else {
self.toggleSeeNewPostsButton(hidden: false)
}
}
})
}
func stopListeningForNewPosts() {
if let handle = postListenerHandle {
newPostsQuery.removeObserver(withHandle: handle)
postListenerHandle = nil
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let newPostNavBar = segue.destination as? UINavigationController,
let newPostVC = newPostNavBar.viewControllers[0] as? NewPostViewController {
newPostVC.delegate = self
}
}
}
extension HomeController: NewPostVCDelegate {
func didUploadPost(withID id: String) {
self.lastUploadedPostID = id
}
/*
// 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.destination.
// Pass the selected object to the new view controller.
}
*/
}
I try to make a like button and unlike but I it gives me error when I try to press the button in the simulator .if you know any other code for the like button to be much easier will be helpful ( or some websites , yt vids)
On this line
ref.child("posts").child(self.postID)
the postID is undefined which is causing the crash. In the comments you state you assign a value to it, but the code in the question doesn't include how that's being done so there's a high likelyhood that value is not being assigned.
The fix would be to assign a value to that class var before that line, like this
#IBAction func likePressed(_ sender: Any) {
self.postID = "post_0" //or however you determine which post it is
// e.g. self.postID = getCurrentPostId()
let ref = Database.database().reference()
let keyToPost = ref.child("posts").childByAutoId().key
ref.child("posts").child(self.postID)...
You may also want to implement some basic error checking as well to ensure the postID is not nil before trying to call the Firebase function.
if let postID = getCurrentPostID() {
//perform the firebase function using postID
} else {
//display an error 'not post selected'
}
I'm changing my app to start to use toolbar buttons for the main pages instead of a menu. All of the menu links work and the toolbar buttons work on every page except one. When I click on the button the app freezes, and Xcode brings me to this line:
'weak var itemDetailPage = segue.destination as? ItemDetailViewController' with this error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
All the variables are nil.
Any help would be monumental!
Here is the code for the view controller it is specifically happening on:
import UIKit
import AVFoundation
import Alamofire
#objc protocol FavRefreshLikeCountsDelegate: class {
func updateLikeCounts(_ likeCount: Int)
}
#objc protocol FavRefreshReviewCountsDelegate: class {
func updateReviewCounts(_ reviewCount: Int)
}
class FavouriteItemsViewController: UICollectionViewController {
var populationItems = false
var selectedSubCategoryId:Int = 0
var selectedCityId:Int = 1
var selectedCityLat: String!
var selectedCityLng: String!
var items = [ItemModel]()
var currentPage = 0
var loginUserId:Int = 0
weak var selectedCell : AnnotatedPhotoCell!
#IBOutlet weak var menuButton: UIBarButtonItem!
var defaultValue: CGPoint!
override func viewDidLoad() {
if self.revealViewController() != nil {
menuButton.target = self.revealViewController()
menuButton.action = #selector(SWRevealViewController.revealToggle(_:))
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
collectionView!.register(AnnotatedPhotoCell.classForCoder(), forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: "AnnotatedPhotoCell")
if let layout = collectionView?.collectionViewLayout as? PinterestLayout {
layout.delegate = self
}
loadLoginUserId()
loadFavouriteItems()
defaultValue = collectionView?.frame.origin
animateCollectionView()
}
override func viewDidAppear(_ animated: Bool) {
updateNavigationStuff()
}
override var preferredStatusBarStyle : UIStatusBarStyle {
return UIStatusBarStyle.lightContent
}
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
loadFavouriteItems()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
weak var itemCell = sender as? AnnotatedPhotoCell
weak var itemDetailPage = segue.destination as? ItemDetailViewController
itemDetailPage!.selectedItemId = Int((itemCell!.item?.itemId)!)!
itemDetailPage!.selectedShopId = Int((itemCell!.item?.itemShopId)!)!
itemDetailPage!.favRefreshLikeCountsDelegate = self
itemDetailPage!.favRefreshReviewCountsDelegate = self
selectedCell = itemCell
updateBackButton()
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell : AnnotatedPhotoCell
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AnnotatedPhotoCell", for: indexPath) as! AnnotatedPhotoCell
cell.item = items[(indexPath as NSIndexPath).item]
let imageURL = configs.imageUrl + items[(indexPath as NSIndexPath).item].itemImage
cell.imageView?.loadImage(urlString: imageURL) { (status, url, image, msg) in
if(status == STATUS.success) {
print(url + " is loaded successfully.")
self.items[indexPath.item].itemImageBlob = image
}else {
print("Error in loading image" + msg)
}
}
cell.imageView.alpha = 0
cell.captionLabel.alpha = 0
UIView.animate(withDuration: 1, delay: 0, options: UIViewAnimationOptions.curveEaseIn, animations: {
cell.imageView.alpha = 1.0
cell.captionLabel.alpha = 1.0
}, completion: nil)
return cell
}
func updateBackButton() {
let backItem = UIBarButtonItem()
backItem.title = ""
navigationItem.backBarButtonItem = backItem
}
func updateNavigationStuff() {
self.navigationController?.navigationBar.topItem?.title = language.favouritePageTitle
self.navigationController?.navigationBar.titleTextAttributes = [ NSAttributedStringKey.font: UIFont(name: customFont.boldFontName, size: CGFloat(customFont.boldFontSize))!, NSAttributedStringKey.foregroundColor:UIColor.white]
self.navigationController!.navigationBar.barTintColor = Common.instance.colorWithHexString(configs.barColorCode)
}
func loadLoginUserId() {
let plistPath = Common.instance.getLoginUserInfoPlist()
let myDict = NSDictionary(contentsOfFile: plistPath)
if let dict = myDict {
loginUserId = Int(dict.object(forKey: "_login_user_id") as! String)!
print("Login User Id : " + String(loginUserId))
} else {
print("WARNING: Couldn't create dictionary from LoginUserInfo.plist! Default values will be used!")
}
}
func loadFavouriteItems() {
if self.currentPage == 0 {
_ = EZLoadingActivity.show("Loading...", disableUI: true)
}
Alamofire.request(APIRouters.GetFavouriteItems( loginUserId, configs.pageSize, self.currentPage)).responseCollection {
(response: DataResponse<[Item]>) in
if self.currentPage == 0 {
_ = EZLoadingActivity.hide()
}
if response.result.isSuccess {
if let items: [Item] = response.result.value {
if(items.count > 0) {
for item in items {
let oneItem = ItemModel(item: item)
self.items.append(oneItem)
self.currentPage+=1
}
}
}
self.collectionView!.reloadData()
} else {
print(response)
}
}
}
func animateCollectionView() {
moveOffScreen()
UIView.animate(withDuration: 1, delay: 0,
usingSpringWithDamping: 0.9,
initialSpringVelocity: 0.9, options: UIViewAnimationOptions.curveEaseOut, animations: {
self.collectionView?.frame.origin = self.defaultValue
}, completion: nil)
}
fileprivate func moveOffScreen() {
collectionView?.frame.origin = CGPoint(x: (collectionView?.frame.origin.x)!,
y: (collectionView?.frame.origin.y)! + UIScreen.main.bounds.size.height)
}
}
extension FavouriteItemsViewController : PinterestLayoutDelegate {
func collectionView(_ collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:IndexPath , withWidth width:CGFloat) -> CGFloat {
let item = items[(indexPath as NSIndexPath).item]
let boundingRect = CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
let size = CGSize(width: item.itemImageWidth, height: item.itemImageHeight)
var rect : CGRect
if item.itemImageBlob != nil {
rect = AVMakeRect(aspectRatio: item.itemImageBlob!.size, insideRect: boundingRect)
}else{
rect = AVMakeRect(aspectRatio: size, insideRect: boundingRect)
}
return rect.size.height
}
func collectionView(_ collectionView: UICollectionView, heightForAnnotationAtIndexPath indexPath: IndexPath, withWidth width: CGFloat) -> CGFloat {
let annotationPadding = CGFloat(4)
let annotationHeaderHeight = CGFloat(17)
let height = annotationPadding + annotationHeaderHeight + annotationPadding + 30
return height
}
}
extension FavouriteItemsViewController : FavRefreshLikeCountsDelegate, FavRefreshReviewCountsDelegate {
func updateLikeCounts(_ likeCount: Int) {
if selectedCell != nil {
selectedCell.lblLikeCount.text = "\(likeCount)"
}
}
func updateReviewCounts(_ reviewCount: Int) {
if selectedCell != nil {
selectedCell.lblReviewCount.text = "\(reviewCount)"
}
}
}
You should try be checking your ItemDetailViewController.
for example:
if segue.identifier == "ItemDetailVC" {
weak var itemCell = sender as? AnnotatedPhotoCell
weak var itemDetailPage = segue.destination as? ItemDetailViewController
...
}