Here I have created a sample app that uses diffable data source for a collection view with a custom collection view layout. The specific layout I am using is from this tutorial.
Here is the relevant part of the code if you don't want to clone the repo and try it for yourself.
import UIKit
let cellIdentifier = "testRecordCell"
struct Record:Hashable {
let identifier = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
static func == (lhs: Record, rhs: Record) -> Bool {
return lhs.identifier == rhs.identifier
}
var timeStamp: Date
init(daysBack: Int){
self.timeStamp = Calendar.current.date(byAdding: .day, value: -1*daysBack, to: Date())!
}
}
class Cell:UICollectionViewCell {
}
class Section: Hashable {
var id = UUID()
// 2
var records:[Record]
init(records:[Record]) {
self.records = records
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Section, rhs: Section) -> Bool {
lhs.id == rhs.id
}
}
extension Section {
static var allSections: [Section] = [
Section(records: [
Record(daysBack: 5),Record(daysBack: 6)
]),
Section(records: [
Record(daysBack: 3)
])
]
}
class ViewController: UICollectionViewController {
private lazy var dataSource = makeDataSource()
private var sections = Section.allSections
fileprivate typealias DataSource = UICollectionViewDiffableDataSource<Section,Record>
fileprivate typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot<Section,Record>
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView?.register(Cell.self, forCellWithReuseIdentifier: cellIdentifier)
applySnapshot()
if let layout = collectionView.collectionViewLayout as? PinterestLayout {
layout.delegate = self
}
}
}
extension ViewController {
fileprivate func makeDataSource() -> DataSource {
let dataSource = DataSource(
collectionView: self.collectionView,
cellProvider: { (collectionView, indexPath, testRecord) ->
UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath)
cell.backgroundColor = .black
return cell
})
return dataSource
}
func applySnapshot(animatingDifferences: Bool = true) {
// 2
var snapshot = DataSourceSnapshot()
snapshot.appendSections(sections)
sections.forEach { section in
snapshot.appendItems(section.records, toSection: section)
}
//This part errors out: "request for number of items in section 0 when there are only 0 sections in the collection view"
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
}
extension ViewController: PinterestLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat {
return CGFloat(10)
}
}
Somehow, the layout is not registering that the collection view does have items and sections in it. When you run it normally, it errors out when you are trying to apply the snapshot: "request for number of items in section 0 when there are only 0 sections in the collection view"
Then, in the prepare() function of the Pinterest layout, when I set a breakpoint and inspect collectionView.numberOfSections() it returns 0. So somehow the snapshot is not communicating with the collection view. Notice that I never use the collectionView's delegate method numberOfSections because I am using the diffable data source...
My impression is that diffable data source is usually used with compositional layout though I have not seen anywhere that this is a requirement.
So is there any way to do this?
The problem is this line:
func applySnapshot(animatingDifferences: Bool = true) {
Change true to false and you won't crash any more.
Hi it's too late for answer but i tried same way and same problem
My case problem is approach numberOfItems in collectionView
82 lines in PinterestLayout below
"for item in 0..<collectionView.numberOfItems(inSection: 0) {"
so.. i inject actual numberOfItems in snapshot from view to custom layout
myLayout.updateNumberOfItems(currentSnapshot.numberOfItems)
dataSource.apply(currentSnapshot)
and just use the numberOfItems instead of collectionView.numberOfItems.
and it's work for me
Please let me know if i'm wrong thx ")
Related
I have made a collection view with cells arranged in rows in columns using a great tutorial. I have added a button to a toolbar in the main view controller that calls collectionView.reloadData() as I want a user to be able to edit values which will in turn update the datasource and then reload the collection view to show the updates.
Running this on a simulator it works, but if any scrolling takes place it causes this crash *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array'. If no scrolling has taken place then calling collectionView.reloadData() works. I can't find where this empty array is which is causing the crash. I have tried printing all the arrays that are used in the code in the console but none appear to be empty. Have tried commenting out various lines of code to try and narrow down where the problem is, it seems to be something in the override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? block. I have also tried reseting the collection view frame co-ordinates to 0 before reload data is called but that also didn't work. Have been stuck going round in circles for a few days which no luck. Any suggestions as to where I am going wrong would be hugely appreciated! My code so far is below (please excuse the long winded explanation and code);
View Controller
import UIKit
class ViewController: UIViewController {
// variable to contain cell indexpaths sent from collectionViewFlowLayout.
var cellIndexPaths = [IndexPath] ()
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var collectionViewFlowLayout: UICollectionViewFlowLayout! {
didSet {
collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
#IBAction func editButtonPressed(_ sender: UIButton) {
collectionView.reloadData()
}
#IBAction func doneButtonPressed(_ sender: UIButton) {
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
}
}
extension ViewController:UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! collectionViewCell
cell.backgroundColor = .green
cell.textLabel.text = "\(indexPath) - AAAABBBBCCCC"
return cell
}
}
extension ViewController:UICollectionViewDelegate, IndexPathDelegate {
func getIndexPaths(indexPathArray: Array<IndexPath>) {
cellIndexPaths = indexPathArray.uniqueValues
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
var cellArray = [UICollectionViewCell] ()
print(cellIndexPaths)
for indexPathItem in cellIndexPaths {
if let cell = collectionView.cellForItem(at: indexPathItem) {
if indexPathItem.section == indexPath.section {
cellArray.append(cell)
}
}
for cells in cellArray {
cells.backgroundColor = .red
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { (timer) in
cells.backgroundColor = .green
}
}
}
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cell = collectionView.cellForItem(at: indexPath)
if let safeCell = cell {
let cellSize = CGSize(width: safeCell.frame.width, height: safeCell.frame.height)
return cellSize
} else {
return CGSize (width: 300, height: 100)
}
}
}
extension Array where Element: Hashable {
var uniqueValues: [Element] {
var allowed = Set(self)
return compactMap { allowed.remove($0) }
}
}
Flow Layout
import UIKit
protocol IndexPathDelegate {
func getIndexPaths(indexPathArray: Array<IndexPath>)
}
class collectionViewFlowLayout: UICollectionViewFlowLayout {
override var collectionViewContentSize: CGSize {
return CGSize(width: 10000000, height: 100000)
}
override func prepare() {
setupAttributes()
indexItemDelegate()
}
// MARK: - ATTRIBUTES FOR ALL CELLS
private var allCellAttributes: [[UICollectionViewLayoutAttributes]] = []
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for rowAttrs in allCellAttributes {
for itemAttrs in rowAttrs where rect.intersects(itemAttrs.frame) {
layoutAttributes.append(itemAttrs)
}
}
return layoutAttributes
}
// MARK: - SETUP ATTRIBUTES
var cellIndexPaths = [IndexPath] ()
private func setupAttributes() {
allCellAttributes = []
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
for row in 0..<rowsCount {
var rowAttrs: [UICollectionViewLayoutAttributes] = []
xOffset = 0
for col in 0..<columnsCount(in: row) {
let itemSize = size(forRow: row, column: col)
let indexPath = IndexPath(row: row, column: col)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height).integral
rowAttrs.append(attributes)
xOffset += itemSize.width
cellIndexPaths.append(indexPath)
}
yOffset += rowAttrs.last?.frame.height ?? 0.0
allCellAttributes.append(rowAttrs)
}
}
// MARK: - CONVERT SECTIONS TO ROWS, ITEMS TO COLUMNS
private var rowsCount: Int {
return collectionView!.numberOfSections
}
private func columnsCount(in row: Int) -> Int {
return collectionView!.numberOfItems(inSection: row)
}
// MARK: - GET CELL SIZE
private func size(forRow row: Int, column: Int) -> CGSize {
guard let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout,
let size = delegate.collectionView?(collectionView!, layout: self, sizeForItemAt: IndexPath(row: row, column: column)) else {
assertionFailure("Implement collectionView(_,layout:,sizeForItemAt: in UICollectionViewDelegateFlowLayout")
return .zero
}
return size
}
private func indexItemDelegate () {
let delegate = collectionView?.delegate as? IndexPathDelegate
delegate?.getIndexPaths(indexPathArray: cellIndexPaths)
}
}
// MARK: - INDEX PATH EXTENSION
//creates index path with rows and columns instead of sections and items
private extension IndexPath {
init(row: Int, column: Int) {
self = IndexPath(item: column, section: row)
}
}
Collection Cell
import UIKit
class collectionViewCell: UICollectionViewCell {
#IBOutlet weak var textLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
I have managed to get around the issue by using the UICollectionView.reloadSections(sections: IndexSet) method. This doesn't cause any crashes. I loop through all sections and add each section to an IndexSet variable then use that in the reload sections method like this;
var indexSet = IndexSet()
let rowCount = collectionView.numberOfSections
for row in 0..<rowCount {
indexSet = [row]
collectionView.reloadSections(indexSet)
}
I am trying to use UITableView with UIViewRepresantable, it kinda works, but not really. Some rows are placed in the wrong place, either overlapping other rows, either extra padding is added. Also it doesn't seem to adapt the row height to the content of the row even if I set automaticDimension.
Here is the demo:
Here is the code:
import SwiftUI
struct ContentView: View {
#State var rows : [String] = []
var listData = ListData()
var body: some View {
VStack {
Button(action: {
self.getDataFromTheServer()
}) {
Text("Get 100 entries from the server")
}
UIList(rows: $rows)
}
}
func getDataFromTheServer() {
for _ in 1...100 {
self.rows.append(self.listData.data)
self.listData.data.append("a")
}
}
}
class ListData {
var data: String = ""
}
class HostingCell: UITableViewCell {
var host: UIHostingController<AnyView>?
}
struct UIList: UIViewRepresentable {
#Binding var rows: [String]
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.dataSource = context.coordinator
tableView.delegate = context.coordinator
tableView.register(HostingCell.self, forCellReuseIdentifier: "Cell")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = UITableView.automaticDimension
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
DispatchQueue.main.async {
uiView.reloadData()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(rows: $rows)
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
#Binding var rows: [String]
init(rows: Binding<[String]>) {
self._rows = rows
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.rows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let tableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HostingCell
let view = Text(rows[indexPath.row])
.background(Color.blue)
.lineLimit(nil)
// create & setup hosting controller only once
if tableViewCell.host == nil {
let hostingController = UIHostingController(rootView: AnyView(view))
tableViewCell.host = hostingController
let tableCellViewContent = hostingController.view!
tableCellViewContent.translatesAutoresizingMaskIntoConstraints = false
tableViewCell.contentView.addSubview(tableCellViewContent)
tableCellViewContent.topAnchor.constraint(equalTo: tableViewCell.contentView.topAnchor).isActive = true
tableCellViewContent.leftAnchor.constraint(equalTo: tableViewCell.contentView.leftAnchor).isActive = true
tableCellViewContent.bottomAnchor.constraint(equalTo: tableViewCell.contentView.bottomAnchor).isActive = true
tableCellViewContent.rightAnchor.constraint(equalTo: tableViewCell.contentView.rightAnchor).isActive = true
} else {
// reused cell, so just set other SwiftUI root view
tableViewCell.host?.rootView = AnyView(view)
}
tableViewCell.setNeedsLayout()
return tableViewCell
}
}
}
From Apple:
Thank you for your feedback, it is noted. Engineering has determined that there are currently no plans to address this issue.
We recommend using List, and filing a separate enhancement for any needs beyond what List can offer.
UIHostingControllers don’t size themselves very well, which I suspect is what’s wrecking your autolayout. Try adding this after you set the controller’s rootView:
hostingController.preferredContentSize = hostingController.sizeThatFits( )
That said, you may want to avoid the UIHostingController entirely… if you’re going to the trouble of using UITableView, why not make the cell in UIKit too? Or if you need SwiftUI for the rows, why not use List instead of hosting a UITableView?
This is my first time working with Google AdMob native ads, I believe I followed the implementation instruction.. All that is left is actually displaying the ads within the collection view, and this is where I am stuck. I do not know how to correctly display the Ads in between users uploaded post. Basically I need help adding a xib to a collection view. task: Ads should be populating while scrolling through posts..
I am using
Collection View
Xib
Google AdMob Native advanced
I also do not receive any errors or crashes, and the console prints so I am obviously doing something wrong.. Received native ad:
The console also - print("Ads not dispalying ") and print("Not what I want")
Heres my code
import UIKit
import Firebase
class FollowingFeedViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UISearchBarDelegate, FeedCellDelegate, PeopleToFollowDelegate, GADUnifiedNativeAdLoaderDelegate {
// MARK: - Google ADMob
/// The ad unit ID from the AdMob UI.
let adUnitID = "ca-app-pub-3940256099942544/3986624511"
/// The number of native ads to load (between 1 and 5 for this example).
let numAdsToLoad = 5
/// The native ads.
var nativeAds = [GADUnifiedNativeAd]()
/// The ad loader that loads the native ads.
var adLoader: GADAdLoader!
func adLoaderDidFinishLoading(_ adLoader: GADAdLoader) {
addNativeAds()
}
func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADUnifiedNativeAd) {
print("Received native ad: \(nativeAd)")
// Add the native ad to the list of native ads.
nativeAds.append(nativeAd)
}
func adLoader(_ adLoader: GADAdLoader, didFailToReceiveAdWithError error: GADRequestError) {
print("\(adLoader) failed with error: \(error.localizedDescription)")
}
/// Add native ads to the list.
func addNativeAds() {
if nativeAds.count <= 0 {
print("Ads not dispalying ")
return
}
let adInterval = (posts.count / nativeAds.count) + 1
var index = 0
for nativeAd in nativeAds {
if index < collectionObject.count {
collectionObject.insert(nativeAd, at: index)
index += adInterval
} else {
print("Not what I want")
break
}
}
}
// MARK: - Properties
var posts = [Post]()
var collectionObject = [AnyObject]()
var viewSinglePost = false
var post: Post?
var currentKey: String?
var userProfileController: ProfileViewController?
var header: FeedReusableView?
#IBOutlet weak var collectionView: UICollectionView!
// MARK: - UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if posts.count > 4 {
if indexPath.item == posts.count - 1 {
fetchPosts()
}
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PostsCell", for: indexPath) as? FollowingCell {
cell.delegate = self
cell.post = posts[indexPath.item] as Post
handleUsernameLabelTapped(forCell: cell)
handleMentionTapped(forCell: cell)
handleHashtagTapped(forCell: cell)
return cell
} else {
let nativeAd = collectionObject[indexPath.row] as! GADUnifiedNativeAd
nativeAd.rootViewController = self
let nativeAdCell = collectionView.dequeueReusableCell(withReuseIdentifier: "UnifiedNativeAdCell", for: indexPath)
// Get the ad view from the Cell. The view hierarchy for this cell is defined in
let adView : GADUnifiedNativeAdView = nativeAdCell.contentView.subviews
.first as! GADUnifiedNativeAdView
// Associate the ad view with the ad object.
// This is required to make the ad clickable.
adView.nativeAd = nativeAd
adView.mediaView?.mediaContent = nativeAd.mediaContent
// Populate the ad view with the ad assets.
(adView.headlineView as! UILabel).text = nativeAd.headline
(adView.advertiserView as! UILabel).text = nativeAd.advertiser
(adView.bodyView as! UILabel).text = nativeAd.body
adView.bodyView?.isHidden = nativeAd.body == nil
(adView.iconView as? UIImageView)?.image = nativeAd.icon?.image
adView.iconView?.isHidden = nativeAd.icon == nil
// In order for the SDK to process touch events properly, user interaction
// should be disabled.
adView.callToActionView?.isUserInteractionEnabled = false
return nativeAdCell
}
}
// MARK: - ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
// // Google Admob
let options = GADMultipleAdsAdLoaderOptions()
options.numberOfAds = numAdsToLoad
// Prepare the ad loader and start loading ads.
adLoader = GADAdLoader(adUnitID: adUnitID,
rootViewController: self,
adTypes: [.unifiedNative],
options: [options])
collectionView.dataSource = self
collectionView.delegate = self
adLoader.delegate = self
adLoader.load(GADRequest())
self.collectionView.register(UINib(nibName: "NativeAdCell", bundle: nil), forCellWithReuseIdentifier: "UnifiedNativeAdCell")
addNativeAds()
}
#objc func handleRefresh() {
posts.removeAll(keepingCapacity: false)
self.currentKey = nil
fetchPosts()
collectionView?.reloadData()
header?.profilesCollectionView.reloadData()
}
}
Fetch Post
func fetchPosts() {
guard let currentUid = Auth.auth().currentUser?.uid else { return }
if currentKey == nil {
USER_FEED_REF.child(currentUid).queryLimited(toLast: 5).observeSingleEvent(of: .value, with: { (snapshot) in
self.collectionView?.refreshControl?.endRefreshing()
guard let first = snapshot.children.allObjects.first as? DataSnapshot else { return }
guard let allObjects = snapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.forEach({ (snapshot) in
let postId = snapshot.key
self.fetchPost(withPostId: postId)
})
self.currentKey = first.key
})
} else {
USER_FEED_REF.child(currentUid).queryOrderedByKey().queryEnding(atValue: self.currentKey).queryLimited(toLast: 6).observeSingleEvent(of: .value, with: { (snapshot) in
guard let first = snapshot.children.allObjects.first as? DataSnapshot else { return }
guard let allObjects = snapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.forEach({ (snapshot) in
let postId = snapshot.key
if postId != self.currentKey {
self.fetchPost(withPostId: postId)
}
})
self.currentKey = first.key
})
}
}
func fetchPost(withPostId postId: String) {
Database.fetchPost(with: postId) { (post) in
self.posts.append(post)
self.posts.sort(by: { (post1, post2) -> Bool in
return post1.creationDate > post2.creationDate
})
self.collectionView?.reloadData()
}
}
}
I think this will work, you have two data source as I can see from your code
var posts = [Post]()
var collectionObject = [AnyObject]()
and you want to create cell for all of them but the only data source you are going to show is posts based on your code
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts.count
}
this can be solved by changing your code to something like this
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if (section == 0) {
return posts.count
} else {
return collectionObject.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if (indexPath.section == 0) {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PostsCell", for: indexPath) as FollowingCell
cell.delegate = self
cell.post = posts[indexPath.item] as Post
handleUsernameLabelTapped(forCell: cell)
handleMentionTapped(forCell: cell)
handleHashtagTapped(forCell: cell)
return cell
} else {
let nativeAd = collectionObject[indexPath.row] as! GADUnifiedNativeAd
nativeAd.rootViewController = self
let nativeAdCell = collectionView.dequeueReusableCell(withReuseIdentifier: "UnifiedNativeAdCell", for: indexPath)
// Get the ad view from the Cell. The view hierarchy for this cell is defined in
let adView : GADUnifiedNativeAdView = nativeAdCell.contentView.subviews
.first as! GADUnifiedNativeAdView
// Associate the ad view with the ad object.
// This is required to make the ad clickable.
adView.nativeAd = nativeAd
adView.mediaView?.mediaContent = nativeAd.mediaContent
// Populate the ad view with the ad assets.
(adView.headlineView as! UILabel).text = nativeAd.headline
(adView.advertiserView as! UILabel).text = nativeAd.advertiser
(adView.bodyView as! UILabel).text = nativeAd.body
adView.bodyView?.isHidden = nativeAd.body == nil
(adView.iconView as? UIImageView)?.image = nativeAd.icon?.image
adView.iconView?.isHidden = nativeAd.icon == nil
// In order for the SDK to process touch events properly, user interaction
// should be disabled.
adView.callToActionView?.isUserInteractionEnabled = false
return nativeAdCell
}
}
This code first add posts and then start adding your adds.
Check this and let me know if it's worked or not. If this show the cell then you can mix two data source together and populate your collection view cells with condition like if you create new object that math this condition with your current object posts & collectionObject
if indexPath.item % 4 == 0 {
show adds
} else {
show posts
}
Hope this will help
Your FollowingFeedViewController is not a subclass from UICollectionViewController, right? Because of that, you should set the delegate and dataSource properties of your collectionView instance.
Probably something like this:
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
}
It looks like at least some of your code is missing. I can't see a fetchPosts method.
I note that handleRefresh calls fetchPosts just before reloadData. Does fetchPosts load the data synchronously or asynchronously? If it loads the data asynchronously, reloadData will be called before the data is ready.
Similarly for the ads. I can't see any reloadData (or equivalent) calls to the collection view once the ads are ready. If you post your full code it'll be easier to diagnose the problem.
I have two Lists each with simple items. I can rearrange items within a list, with the code shown below. I want to be able to drag an item from one list and drop it into the other list. Not sure what I need to enable to make that happen. As is, I can drag something from one list all over the screen, but I can't drop it anywhere except within its own list; if I release the drag anywhere else, it just flies back to its original location.
Clearly, I need to add something(s) to enable the desired behavior; any ideas on what's required (not necessarily using .onMove -- that's within the same list) would be most appreciated.
struct ContentView: View {
#State private var users1 = ["AAA", "BBB", "CCC"]
#State private var users2 = ["DDD", "EEE", "FFF"]
#State private var isEditable = true
var body: some View {
HStack {
Spacer()
List {
ForEach(users1, id: \.self) { user in
Text(user)
.background(Color(.yellow))
}
.onMove(perform: move1)
}
.environment(\.editMode, isEditable ? .constant(.active) : .constant(.inactive))
Spacer()
List {
ForEach(users2, id: \.self) { user in
Text(user)
.background(Color(.orange))
}
.onMove(perform: move2)
}
.environment(\.editMode, isEditable ? .constant(.active) : .constant(.inactive))
Spacer()
}
}
func move1(from source: IndexSet, to destination: Int) {
users1.move(fromOffsets: source, toOffset: destination)
}
func move2(from source: IndexSet, to destination: Int) {
users2.move(fromOffsets: source, toOffset: destination)
}
}
I don't think this is possible with SwiftUI yet. This piece of code shows how you use two UITableViews. I hope you can improve this to achieve what you are looking for.
First create this class which does all the magic. You need to import UIKit and MobileCoreServices.
class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITableViewDragDelegate, UITableViewDropDelegate {
var leftTableView = UITableView()
var rightTableView = UITableView()
var removeIndex = IndexPath()
var leftItems: [String] = [
"Hello",
"What",
"is",
"happening"
]
var rightItems: [String] = [
"I",
"don't",
"know"
]
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let string = tableView == leftTableView ? leftItems[indexPath.row] : rightItems[indexPath.row]
self.removeIndex = indexPath
guard let data = string.data(using: .utf8) else { return [] }
let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypePlainText as String)
return [UIDragItem(itemProvider: itemProvider)]
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
let section = tableView.numberOfSections - 1
let row = tableView.numberOfRows(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
coordinator.session.loadObjects(ofClass: NSString.self) { items in
guard let strings = items as? [String] else {
return
}
var indexPaths = [IndexPath]()
for (index, string) in strings.enumerated() {
let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
if tableView == self.leftTableView {
self.leftItems.insert(string, at: indexPath.row)
} else {
self.rightItems.insert(string, at: indexPath.row)
}
indexPaths.append(indexPath)
}
if tableView == self.leftTableView {
self.rightItems.remove(at: self.removeIndex.row)
self.rightTableView.deleteRows(at: [self.removeIndex], with: .automatic)
} else {
self.leftItems.remove(at: self.removeIndex.row)
self.leftTableView.deleteRows(at: [self.removeIndex], with: .automatic)
}
tableView.insertRows(at: indexPaths, with: .automatic)
}
}
override func viewDidLoad() {
super.viewDidLoad()
leftTableView.dataSource = self
rightTableView.dataSource = self
leftTableView.frame = CGRect(x: 0, y: 40, width: 150, height: 400)
rightTableView.frame = CGRect(x: 150, y: 40, width: 150, height: 400)
leftTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
rightTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
view.addSubview(leftTableView)
view.addSubview(rightTableView)
leftTableView.dragDelegate = self
leftTableView.dropDelegate = self
rightTableView.dragDelegate = self
rightTableView.dropDelegate = self
leftTableView.dragInteractionEnabled = true
rightTableView.dragInteractionEnabled = true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if tableView == leftTableView {
return leftItems.count
} else {
return rightItems.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if tableView == leftTableView {
cell.textLabel?.text = leftItems[indexPath.row]
} else {
cell.textLabel?.text = rightItems[indexPath.row]
}
return cell
}
}
After that you wrap this:
struct TableView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> TableViewController {
let v = TableViewController()
return v
}
func updateUIViewController(_ viewController: TableViewController, context: Context) {
}
}
and finally use it:
struct ContentView: View {
var body: some View {
VStack {
TableView()
}
}
}
I hope this helps.
here is my simplified code in my view controller
class WishListVC: UIViewController {
#IBOutlet weak var wishListCollectionView: UICollectionView!
private var products = [Product]()
private var selectedProduct : Product?
override func viewDidLoad() {
super.viewDidLoad()
}
//MARK: - cell Delegate
extension WishListVC : ListProductCellDelegate {
func addToCartButtonDidTapped(at selectedIndexPath: IndexPath, collectionView: UICollectionView) {
guard let userOrder = userOrder else {return}
let selectedProduct = products[selectedIndexPath.item]
Order.addProductToOrderRealmDatabase(userOrder: userOrder, selectedProduct: selectedProduct)
wishListCollectionView.reloadData()
updateBadgeOnCartTabBar()
}
func stepperButtonDidTapped(at selectedIndexPath: IndexPath, stepperValue: Int, collectionView: UICollectionView) {
guard let userOrder = userOrder else {return}
let selectedProduct = products[selectedIndexPath.item]
if stepperValue > 0 {
Product.changeProductQuantityInRealmDatabase(selectedProduct: selectedProduct, quantity: stepperValue)
} else {
Order.removeProductFromOrderRealmDatabase(userOrder: userOrder, selectedProduct: selectedProduct)
Product.changeProductQuantityInRealmDatabase(selectedProduct: selectedProduct, quantity: 0)
}
wishListCollectionView.reloadData()
updateBadgeOnCartTabBar()
}
}
//MARK: - Collection View Data Source
extension WishListVC : UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return products.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: WishListStoryboardData.CollectionViewIdentifiers.productSliderCell.rawValue, for: indexPath) as? ListProductCell else { return UICollectionViewCell()}
cell.productData = products[indexPath.item]
cell.delegate = self
cell.collectionView = wishListCollectionView
return cell
}
}
and here is the code for my collection view cell:
protocol ListProductCellDelegate {
func addToCartButtonDidTapped(at selectedIndexPath: IndexPath, collectionView : UICollectionView)
func stepperButtonDidTapped( at selectedIndexPath: IndexPath, stepperValue: Int, collectionView : UICollectionView)
}
class ListProductCell: UICollectionViewCell {
#IBOutlet weak var productImageViewAspectRatio: NSLayoutConstraint!
#IBOutlet weak var addToCartButton: UIButton!
#IBOutlet weak var stepper: GMStepper!
var collectionView : UICollectionView?
var delegate: ListProductCellDelegate?
var productData : Product? {
didSet {
updateUI()
}
}
#IBAction func addToCartButtonDidPressed(_ sender: UIButton) {
guard let collectionView = collectionView else {return}
guard let selectedIndexPath = collectionView.indexPathForView(view: sender) else {return}
self.delegate?.addToCartButtonDidTapped(at: selectedIndexPath, collectionView: collectionView)
}
#IBAction func stepperDidTapped(_ sender: GMStepper) {
guard let collectionView = self.collectionView else {return}
guard let selectedIndexPath = collectionView.indexPathForView(view: sender) else {return}
self.delegate?.stepperButtonDidTapped(at: selectedIndexPath, stepperValue: Int(sender.value), collectionView: collectionView)
}
private func updateUI() {
guard let product = productData else {return}
stepper.value = Double(product.quantity)
setLikeButton(product: product)
setCartAndStepperButton()
}
private func setCartAndStepperButton() {
guard let selectedProduct = productData else {return}
func showStepperButton(status: Bool) {
// to decide whether to show stepper or add to cart button.
stepper.isHidden = !status
stepper.isEnabled = status
addToCartButton.isHidden = status
addToCartButton.isEnabled = !status
}
if selectedProduct.quantity == 0 {
showStepperButton(status: false)
} else {
showStepperButton(status: true)
}
}
}
I don't understand why after I tap the stepper for the first time after the 'Add To Cart' disappear, the collection view will disappear.
I don't have collectionView.isHidden in my entire code, but I don't know why my collection view disappear like the file .gif below
http://g.recordit.co/NAEc36MbrM.gif
but if the stepper is already show with some stepper value more than 1, then it will make my collection view dissapear like the gif below
http://recordit.co/SLdqf1ztFZ.gif
the minimum stepper value is set to be 1.
If I change the collection view reload data wishListCollectionView.reloadData() in the stepperButtonDidTapped method above to be just reload data in certain cell only using wishListCollectionView.reloadItems(at: [selectedIndexPath]) the problem will be solved, but the stepper value seems it will be updated little slower, and it looks laggy.
I don't know how to trace the last line that will be executed so it makes my collection view disappears.
and if I reload the data in the main thread using:
DispatchQueue.main.async {
self.wishListCollectionView.reloadData()
}
it won't make the collection view disappear, but If I edit the cell index 4 it will affect the cell index 1 like gif here: http://g.recordit.co/6802BJDdtx.gif
I change the number in the fifth cell but it will automatically change the second cell.
Note to:
Use Xcode Ui Debugging tool and check if your collectionView is hidden or empty
Realm is realTime database ,any changes you make in database will be applied
on your arrays too(like products array)
The reason of stepper problem is becouse of :
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: WishListStoryboardData.CollectionViewIdentifiers.productSliderCell.rawValue, for: indexPath) as? ListProductCell else { return UICollectionViewCell()}
and your using stepperButtonDidTapped delegete inside of cell.
declure your cells once and store them inside array,like below:
var cellArray:[ListProductCell]=[]
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if (cellArray.count > indexPath.row){
return cellArray[indexPath.row]
}else{
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: WishListStoryboardData.CollectionViewIdentifiers.productSliderCell.rawValue, for: indexPath) as? ListProductCell else { return UICollectionViewCell()}
cell.productData = products[indexPath.item]
cell.delegate = self
cell.collectionView = wishListCollectionView
cellArray.append(cell)
return cell
}}
My case load get data success but not see data when reload.
This problem case born set Height in func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize is less than height cell on storyboard.