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
Related
I have purposely left an empty image in my assets catalog so that I can get my collectionView to somehow skip that image if it is nil, but so far it will render an empty image in that cell. It is better than crashing my app but how can I get it to skip that image?
here is my cellForItemAt indexPath code. Any ideas would be much appreciated.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: K.collectionViewCell, for: indexPath) as! CollectionViewCell
DispatchQueue.main.async {
let image = RoomModel.roomModel.rooms[indexPath.item]
if let image = image {
cell.imageView.image = image
}
}
return cell
}
Model Solution:
import UIKit
var data = [long list of UIImage(named: ...)]
class RoomModel {
var rooms = data.compactMap { ($0) }
var roomNo = 0
func setRoomNo(sender: Int) {
roomNo = sender
}
func getRoom() -> UIImage {
let image = rooms[roomNo]
return image
}
}
itemForRow Solution:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: K.collectionViewCell, for: indexPath) as! CollectionViewCell
DispatchQueue.main.async {
cell.imageView.image = self.roomModel.rooms[indexPath.item]
}
return cell
}
You would need to modify the data model to not include it if the image is nil and can be done by a simple if check before adding it in the model and then your cell would not render it as it is omitted from the data model.
I have a filter operation which is done on a bottom sheet and this value is then converted to a dictionary, I have a collectionView embeded in a UIView to display filter parameters.
When an item is selected and a filter button is pressed, I want to save the selected value using UserDefault so I can persist the searched Item if the user decides to go to the filter page again until the user press rest then I clear everything.
Currently my filter works as expected but my issue now is the persistence. When I preselect the cells, and I try to deselect that field, I get a crash.
Here's what I do
fileprivate let data1 = UserDefaultsConfig.contributionTypeFilter["selectedPaymentIndex"] as? Data
var arrSelectedIndex = [IndexPath]() // This is selected cell Index array
var arrSelectedData = [String]()
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell?.contentView.backgroundColor = UIColor.white.withAlphaComponent(0.8)
let strData = PaymentMethodFilter.allCases[indexPath.item].rawValue.capitalized
arrSelectedIndex.remove(at: indexPath.row)
arrSelectedData.remove(at: indexPath.row)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let strData = PaymentMethodFilter.allCases[indexPath.item].rawValue.capitalized
arrSelectedIndex.append(indexPath)
arrSelectedData.append(strData)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeue(dequeueableCell: PaymentMethodFilterCollectionCell.self, forIndexPath: indexPath)
cell.feedSubviews(with: PaymentMethodFilter.allCases[indexPath.row])
guard let selectedData = data1 else {
return cell
}
arrSelectedData = UserDefaultsConfig.contributionTypeFilter["payment_method_names"] as? [String] ?? []
arrSelectedIndex = NSKeyedUnarchiver.unarchiveObject(with: selectedData) as? [IndexPath] ?? []
arrSelectedIndex.forEach {
collectionView.selectItem(at: $0, animated: true, scrollPosition: .right)
}
return cell
}
Fatal error: Index out of range: file
/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/Array.swift,
line 1221 2020-08-16 15:36:26.359431+0100 Riby[40882:1056506] Fatal
error: Index out of range: file
/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/Array.swift,
line 1221
I want to save the selected value
To record what is selected, do not make an array of index paths, because index paths can change (and in your code, that's just what happens). Instead, make an array of unique identifiers for the data represented by the selected cells. That way, you can always find them again. What doesn't change is the data, so that's the way to identify things.
Basically, your underlying data model should consist of uniquely identifiable entries. (If you use a diffable data source, that rule will be enforced for you, which is just one many reasons why diffable data sources are great.) You can always convert between actual data objects and current cells in the collection view, in both directions.
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)
I have go this uicollectionviewcell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! PostCell
if let CurrentPost = posts[indexPath.row] as? Post{
//determine which constraint to call
if(CurrentPost.PostText != nil){
if(CurrentPost.PostImage != nil){
cell.postImage.image = CurrentPost.PostImage
cell.cellConstraintsWithImageWithText()
}else{
cell.postImage.image = nil
cell.cellConstraintsWithoutImageWithText()
}
}else{
cell.postImage.image = CurrentPost.PostImage
cell.cellConstraintsWithImageWithoutText()
}
}
return cell
}
My goal is to determine which function to goal based on absence or presence of image and text.Now the problem is all this functions get called because some cells do have images cellConstraintsWithImageWithText is being called,others don't have them so cellConstraintsWithoutImageWithText is being called.How can i call a single function for a single cell rather than all cells?
This happens because cells are being reused. The easiest way to handle this is to store index paths of cells with text in your view controller. When cell is dequeued just check if index path is present in stored array and layout accordingly.
In your ViewController
var cellsWithText: [IndexPath] = []
In cellForItemAt indexPath
...
cell.postImage.image = nil
cell.cellConstraintsWithoutImageWithText()
if !cellsWithText.contains(indexPath) {
cellsWithText.append(indexPath)
}
...
Now at the beginning at cellForItemAt indexPath
if let CurrentPost = posts[indexPath.row] as? Post {
if cellsWithText.contains(indexPath) {
// layout for text
} else {
// layout for image
}
I also noticed that your'e using posts[indexPath.row] but your using the collectionView, that doesn't have rows and has item instead. That also might be the issue.
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.