Displaying multiple UICollectionViewCells via enum? - swift

I have an iOS app, written in swift. It's a social platform where the users can post 9 different types of posts (text, image, video, link, audio, poll, chat, etc). I set those using an enum
enum PostType: String {
case image = "image"
case gif = "gif"
case video = "video"
case text = "text"
case link = "link"
case audio = "audio"
case poll = "poll"
case chat = "chat"
case quote = "quote"
}
I'm utilising FirebaseDatabase to store the data. In the DashboardViewController I query the database and get the posts in an array along with the corresponding users, ready to be displayed.
func loadPosts() {
activityIndicator.startAnimating()
Api.Feed.observeFeedPosts(withUserId: Api.Users.CURRENT_USER!.uid) {
post in
guard let userId = post.userUid else { return }
self.fetchUser(uid: userId, completed: {
self.posts.insert(post, at: 0)
self.activityIndicator.stopAnimating()
self.collectionView.reloadData()
})
}
Api.Feed.observeFeedRemoved(withUserId: Api.Users.CURRENT_USER!.uid) { (post) in
self.posts = self.posts.filter { $0.id != post.id } // removed all array elements matching the key
self.users = self.users.filter { $0.id != post.userUid }
self.collectionView.reloadData()
}
}
func fetchUser(uid: String, completed: #escaping () -> Void ) {
Api.Users.observeUsersShort(withId: uid) {
user in
self.users.insert(user, at: 0)
completed()
}
}
Whenever the user creates a new post, it stores PostType.text.rawValue (for example, it gives "text" String) on the database to differentiate between them (either video, photo, text, etc). Now, I have to use the PostType enum to figure out what the post type is and display the corresponding UICollectionViewCell. Now, if its a single cell, it's easy. I can do this and it works:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellName.postTextCVC, for: indexPath) as! PostTextCVC
let user = users[indexPath.row]
let post = posts[indexPath.row]
cell.delegatePostTextCVC = self
cell.user = user
cell.dashboardVC = self
cell.post = post
return cell
}
The problem is, how to use the enum to display the appropriate cell?

Keep a variable PostType variable in your Post class. In cellForItemAt check post type of the post and dequeue respective cell.
Something like this.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let post = posts[indexPath.row]
let type: PostType = post.type // Get the post type eg. text, image etc.
switch type {
case .text:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellName.postTextCVC, for: indexPath) as! PostTextCVC
let user = users[indexPath.row]
cell.delegatePostTextCVC = self
cell.user = user
cell.dashboardVC = self
cell.post = post
return cell
case .image:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellName.postImageCVC, for: indexPath) as! PostImageCVC
return cell
case .video:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellName.postTextCVC, for: indexPath) as! PostVideoCVC
return cell
}
}
If you are using separate nib files for each collection view cell, make sure you register all possible nibs with collection view like this.
collectionView.register(UINib(nibName: "PostTextCVC", bundle: nil), forCellWithReuseIdentifier: CellName.postTextCVC)

Related

swift different number of cell in each section

I'm trying to pull data from firebase and show it in a collectionView with sections. How can add a different number of cells per section as each firebase data node will not have the same number of posts.
var users = [User]()
var posts = [Post]()
Updated collection view methods:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! FollowingCell
let user = self.users[indexPath.section]
let post = self.dataSource[user]?[indexPath.row]
cell.post = post
return cell
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) as! FollowingHeader
let user = self.users[indexPath.section]
header.user = user
return header
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let user = self.users[section]
return self.dataSource[user]?.count ?? 0
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return self.dataSource.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 120, height: 80)
}
How I'm adding the posts and user. As you can see I'm adding the post to the dataSource but for some reason, only 1 post per use is added when one of the users has two posts.
func fetchFollowedEvents() {
self.collectionView.refreshControl?.endRefreshing()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
let ref = Database.database().reference().child("followed-posts").child(currentUid)
ref.observeSingleEvent(of: .value) { (snapshot) in
guard let dictionary = snapshot.value as? [String: Any] else { return }
dictionary.forEach { (key, value) in
Database.database().reference().child("posts").child(key).observeSingleEvent(of: .value) { (postSnapshot) in
guard let postDictionary = postSnapshot.value as? [String: Any] else { return }
guard let uid = postDictionary["uid"] as? String else { return }
Database.fetchUserWithUID(uid: uid) { (user) in
let post = Post(postId: key, user: user, dictionary: postDictionary)
post.id = key
self.users.append(user)
self.dataSource[user] = [post]
self.collectionView.reloadData()
}
}
}
}
}
try creating data source like this
var users : [User] = []()
var dataSource : [User:[Post]] = [:]() //I Prefer this one
(or)
var dataSource :[Int: [Post]] = [:]()
a dictionary with Users as Key and Array of post as Value
So now for number of sections you can use
self.userser.count()
(or)
self.dataSource.count()
So for number of rows in sections, you can do as so
let user = self.users[section]
return self.dataSource[user].count() ?? 0
(or)
return self.dataSource[section].count() ?? 0
in cell for row at index you can get the data as
let user = self.users[indexPath.section]
let data = self.dataSource[user][indexPath.row]
(or)
let data = self.dataSource[indexPath.section][indexPath.row]
Hope this would help you.
Happy Codding!

dynamically load sections of UIController depending on data

I am trying to implement a UICollectionView for a "Add Friends" page in my application and I need the following functionality:
When the view first appears, display and friend requests that a user may have.
User inputs username into search bar, and then (if found) that username shows up in a cell ABOVE the cells for friend requests.
I have been trying to implement this by using sections in a UICollevtionViewController but it has been a headache...
import UIKit
import Firebase
class AddUserController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchBarDelegate {
var searchedUser: User?
var friendRequests = [User]()
let cellId = "cellId"
let headerId = "headerId"
var sectionCategories = ["Add Friends", "Friend Requests"]
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.delegate = self
collectionView?.dataSource = self
collectionView?.register(SearchedUserCell.self, forCellWithReuseIdentifier: cellId)
collectionView?.register(SectionHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: headerId)
getFriendRequests()
}
fileprivate func fetchSearchedUsers(username: String) {
let ref = Database.database().reference().child("usernames")
ref.child(username).observeSingleEvent(of: .value, with: { (snapshot) in
// populate user info
guard let uidOfSearchedUser = snapshot.value as? String else {return}
Database.database().reference().child("users").child(uidOfSearchedUser).observeSingleEvent(of: .value, with: { (snapshot) in
guard let userDictionary = snapshot.value as? [String: Any] else {return}
self.searchedUser = User(uid: uidOfSearchedUser, dictionary: userDictionary)
self.collectionView?.reloadData()
}, withCancel: { (err) in
print("error retrieving user", err)
})
}) { (err) in
print("unable to find user: ", err)
}
}
func getFriendRequests() {
guard let uid = Auth.auth().currentUser?.uid else {return}
Database.database().reference().child("users").child(uid).child("requests").observe(.value) { (snapshot) in
guard let requestsDictionary = snapshot.value as? [String: Any] else {return}
for (key, value) in requestsDictionary {
print(key, value)
Database.database().reference().child("users").child(key).observeSingleEvent(of: .value, with: { (snapshot) in
guard let userDictionary = snapshot.value as? [String: Any] else {return}
let user = User(uid: key, dictionary: userDictionary)
print("friend request from: ", user.username)
self.friendRequests.append(user)
})
}
}
self.collectionView?.reloadData()
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 {
return 4
} else if section == 1 {
return 1
}
return 0
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let sendFriendRequestcell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! SearchedUserCell
let dealWithFriendRequestCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! SearchedUserCell
if indexPath.section == 0 {
//Cell for searched user
sendFriendRequestcell.user = searchedUser
return sendFriendRequestcell
} else if indexPath.section == 1 {
//Populate all the friend requests that exist
if friendRequests.count != 0 {
dealWithFriendRequestCell.user = friendRequests[indexPath.item]
}
return dealWithFriendRequestCell
}
return sendFriendRequestcell
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
//Change this to return only 2 only if friend requests exist.
return 2
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) as! SectionHeaderView
headerView.sectionName = sectionCategories[indexPath.section]
return headerView
default:
assert(false, "Unexpected element kind")
}
}
}
Im using firebase to store/fetch both my searched users and any friend requests. I can see users in the friend requests.
The issues I'm having are:
Dynamically showing sections (the "Add Friends" section and cells shouldn't be displayed unless user searches for a user.
This particular user (screenshot) has a friend request, but the cell isn't populated to show that. I can see that I am retrieving user correctly.
Is a collection view like this even the best way for me to handle this? Seems a bit complicated.
I say your problem is state. Here is an example how state can be modeled with an enum with associated values. Read more on enums here.
struct User {}
struct UserRequest {}
class Controller: NSObject {
var collectionView: NSCollectionView!
enum Sections: Int {
case searched = 0
case requests = 1
}
enum State {
case none
case requests(requests: [UserRequest])
case searched(user: User)
case searchedAndRequests(user: User, requests: [UserRequest])
}
var state: State = .none {
didSet {
collectionView.reloadData()
}
}
}
extension Controller: NSCollectionViewDataSource {
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
switch state {
case .requests(let requests):
return requests.count
case .searched(_):
return 1
case .searchedAndRequests(_, let requests):
switch Section(rawValue: section)! {
case .searched:
return 1
case .requests:
return requests.count
default:
fatalError("section not allowed.")
}
default:
fatalError("invalid state.")
}
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
// Create cell(s) here or later.
switch state {
case .searched(let user):
// Populate user cell and return it.
break
case .requests(let requests):
let request = requests[indexPath.item]
// Populate cell with request and return it.
case .searchedAndRequests(let user, let requests):
switch Section(rawValue: indexPath.section)! {
case .searched:
// Populate user cell and return it.
break
case .requests:
let request = requests[indexPath.item]
// Populate cell with request and return it.
default:
fatalError("section not allowed.")
}
default:
fatalError("invalid state")
}
}
func numberOfSections(in collectionView: NSCollectionView) -> Int {
switch state {
case .none:
return 0
case .requests(_), .searched(_):
return 1
case .searchedAndRequests(_, _):
return 2
}
}
}

Accessing an array in another view controller

I have an array of NSObjects that I need to read in another viewcontroller. However I'm unsure what level I should be setting the data for it.
This screen shot below best explains what I'm trying to do. Each HomeController has a title, members list, description and inset collectionview (yellow bar). I need the collection views number of cells to equal the number of members.
I tried creating a reference to HomeController inside the inset collectionview by using lazy var but that got the the error:
fatal error: Index out of range
lazy var homeController: HomeController = {
let hc = HomeController()
hc.liveCell = self
return hc
}()
Again this is done from within the inset collectionview
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath :
IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: profileImageCellId, for: indexPath) as! profileImageCell
let room = homeController.rooms[indexPath.row]
print(room.members?.count)
return cell
}
Any suggestions?
EDIT
Data is added to the array using this function
var rooms = [Room]()
func fetchAllRooms(){
Database.database().reference().child("rooms").observe(.childAdded, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let room = Room()
room.rid = snapshot.key
room.setValuesForKeys(dictionary)
self.rooms.append(room)
print(snapshot)
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
})
}
print("end of room snap")
}, withCancel: nil)
}
Here is the cell for item at index path at the HomeController level
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell = UICollectionViewCell()
let section = indexPath.section
let liveCell = collectionView.dequeueReusableCell(withReuseIdentifier: LiveCellId, for: indexPath) as! LiveCell
let cell = liveCell
let room = rooms[indexPath.row]
liveCell.liveStreamNameLabel.text = room.groupChatName
liveCell.descriptionLabel.text = room.groupChatDescription
return cell
}
You need to check the count of your array in order to prevent the crash Index out of range
if homeController.rooms.count > indexPath.row {
let room = homeController.rooms[indexPath.row]
print(room.members?.count)
}
Can you Debug and share below two things then we can look further on this
Check whats the index path you are getting
Check if your array have data

Why does the enum can only read the top case?

I have a navigation tab bar and each tab contains a picture of array, see picture below. My question is. Why does the enum only see the top case "mens", it doesn't switch to women, arts and saved. I tried to delete the case mens, then it can only see the case "women", whatever is on top, thats only thing cellForItemAtIndexPath can see. Everything is already connected I tried to get them one by one to see if they're working and they're all working. But it can only see what's on top. I dont understand what's missing in here,
class StreamDetailController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate{
private enum Tabs: Int {
case mens
case women
case arts
case saved
}
var menImage = [Men]()
var artsImage = [Arts]()
var womenImage = [Women]()
var savedImage = [SavedPhotos]()
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let tab = Tabs(rawValue: section) else {
assertionFailure()
return section
}
switch tab {
case .mens:
return menImage.count
case .women:
return womenImage.count
case .arts:
return artsImage.count
case .saved:
return savedImage.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let tab = Tabs(rawValue: indexPath.section) else {
assertionFailure()
return UICollectionViewCell()
}
switch tab {
case .mens:
let cell = extraCells.dequeueReusableCell(withReuseIdentifier: "Cells", for: indexPath) as! ExtraViewCell
let men = menImage[indexPath.item]
cell.boxImage.image = men.image
return cell
case .women:
let cell = extraCells.dequeueReusableCell(withReuseIdentifier: "Cells", for: indexPath) as! ExtraViewCell
let women = womenImage[indexPath.item]
cell.boxImage.image = women.image
return cell
case .arts:
let cell = extraCells.dequeueReusableCell(withReuseIdentifier: "Cells", for: indexPath) as! ExtraViewCell
let art = artsImage[indexPath.item]
cell.boxImage.image = art.image
return cell
case .saved:
let cell = extraCells.dequeueReusableCell(withReuseIdentifier: "Cells", for: indexPath) as! ExtraViewCell
let save = savedImage[indexPath.item]
cell.boxImage.image = save.image
return cell
}
}
Setting
cases
Tab Bar
The problem is you are using section and not row to identify the row. Most likely you only have one section and multiple rows. That's why the section is always zero and points to the first element of your enum. You should change your code to the following:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let tab = Tabs(rawValue: indexPath.row) else {
assertionFailure()
return UICollectionViewCell()
}
switch tab {
case .mens:
let cell = extraCells.dequeueReusableCell(withReuseIdentifier: "Cells", for: indexPath) as! ExtraViewCell
let men = menImage[indexPath.item]
cell.boxImage.image = men.image
return cell
case .women:
let cell = extraCells.dequeueReusableCell(withReuseIdentifier: "Cells", for: indexPath) as! ExtraViewCell
let women = womenImage[indexPath.item]
cell.boxImage.image = women.image
return cell
case .arts:
let cell = extraCells.dequeueReusableCell(withReuseIdentifier: "Cells", for: indexPath) as! ExtraViewCell
let art = artsImage[indexPath.item]
cell.boxImage.image = art.image
return cell
case .saved:
let cell = extraCells.dequeueReusableCell(withReuseIdentifier: "Cells", for: indexPath) as! ExtraViewCell
let save = savedImage[indexPath.item]
cell.boxImage.image = save.image
return cell
}
}
I have replaced indexPath.section with indexPath.row

Empty Collection View Swift

I followed 1 tutorial and i was able to fill a collectionView with some data(imageview and text):
let appleProducts = ["A", "B", "C", "D"]
let imageArray = [UIImage(named: "pug1"), UIImage(named: "pug2"), UIImage(named: "pug3"), UIImage(named: "pug4")]
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return appleProducts.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath as IndexPath) as! CollectionViewCell
cell.imageView?.image = self.imageArray[indexPath.row]
cell.title?.text = self.appleProducts[indexPath.row]
return cell
}
Now passing from the demo project to mine, I want to fill this CollectionView with data(Picture and text) that I get from FirebaseDatabse so I created this method:
struct item {
let pictureId: String!
let picture: String!
}
var items = [item]()
func getLatestAddedItems(){
self.items.removeAll()
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("Items").observe(.value, with: {
snapshot in
//self.items.insert(item(picture: picture), at: 0)
for childSnap in snapshot.children.allObjects {
let snap = childSnap as! FIRDataSnapshot
//print(snap.key)
let picture = (snap.value as? NSDictionary)?["bookImage"] as? String ?? ""
//print(picture)
self.items.append(item(pictureId:snap.key, picture:picture))
}
print(self.items.count)
})
}
And I create this button to call GetLatestAddedItems Method:
#IBAction func selectAction(_ sender: AnyObject) {
getLatestAddedItems()
}
And this one to check results:
#IBAction func gettableAction(_ sender: AnyObject) {
print(self.items[0].picture)
print(self.items[1].picture)
print(self.items[2].picture)
print(self.items.count) }
OutPut results:
picture 1 link
picture 2 link
picture 3 link
3
Everythings look fine and correct, now after making required changes in ContentView methods:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath as IndexPath) as! CollectionViewCell
//cell.imageView?.image = self.imageArray[indexPath.row]
let url = NSURL(string: items[indexPath.row].picture)
let data = NSData(contentsOf: url! as URL)
cell.imageView?.image = UIImage(data: data! as Data)
cell.title?.text = self.items[indexPath.row].pictureId
return cell
}
Now I'm getting an empty ContentView, the first time with button it works because I call the getLatestAddedItems() that will get and add data to the Items table, I try to call it in both ViewWillAppear or Viewdidload but nothings changes.
This is what I think the return items.count is returning 0 so nothings will appear any suggestions ?
Move your collectionView's protocol delegation initialisation to one of the ViewController lifecycle scope such as viewDidLoad() or viewWillAppear(_animated : Bool) if you are using a custom viewController(i.e embed a collectionView inside a viewController)
And reload your collectionView every time your user receives a value from its database.
override func viewDidLoad(){
super.viewDidLoad()
self.collectionView.dataSource = self
self.collectionView.delegate = self
}
func getLatestAddedItems(){
self.items.removeAll()
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("Items").observe(.childAdded, with: {
snapshot in
for childSnap in snapshot.children.allObjects {
let snap = childSnap as! FIRDataSnapshot
let picture = (snap.value as? NSDictionary)?["bookImage"] as? String ?? ""
self.items.append(item(pictureId:snap.key, picture:picture))
print(self.items.count)
self.collectionView.reloadData()
}
})
}
PS:- All the calls to your firebase server are asynchronous, which takes some time to retrieve the data from your FBDB, so put print(self.items.count) should be inside the completionBlock of the firebase observing call otherwise if it is put outside it will be called even before your data has been retrieved from FBDB.