How to track a CollectionView cell by time in Swift - ios

I've been working on a feature to detect when a user sees a post and when he doesn't. When the user does see the post I turn the cell's background into green, when it doesn't then it stays red. Now after doing that I notice that I turn on all the cells into green even tho the user only scroll-down the page, so I added a timer but I couldn't understand how to use it right so I thought myself maybe you guys have a suggestion to me cause I'm kinda stuck with it for like two days :(
Edit: Forgot to mention that a cell marks as seen if it passes the minimum length which is 2 seconds.
Here's my Code:
My VC(CollectionView):
import UIKit
class ViewController: UIViewController,UIScrollViewDelegate {
var impressionEventStalker: ImpressionStalker?
var impressionTracker: ImpressionTracker?
var indexPathsOfCellsTurnedGreen = [IndexPath]() // All the read "posts"
#IBOutlet weak var collectionView: UICollectionView!{
didSet{
collectionView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
impressionEventStalker = ImpressionStalker(minimumPercentageOfCell: 0.70, collectionView: collectionView, delegate: self)
}
}
func registerCollectionViewCells(){
let cellNib = UINib(nibName: CustomCollectionViewCell.nibName, bundle: nil)
collectionView.register(cellNib, forCellWithReuseIdentifier: CustomCollectionViewCell.reuseIdentifier)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
collectionView.delegate = self
collectionView.dataSource = self
registerCollectionViewCells()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
impressionEventStalker?.stalkCells()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
impressionEventStalker?.stalkCells()
}
}
// MARK: CollectionView Delegate + DataSource Methods
extension ViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let customCell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.reuseIdentifier, for: indexPath) as? CustomCollectionViewCell else {
fatalError()
}
customCell.textLabel.text = "\(indexPath.row)"
if indexPathsOfCellsTurnedGreen.contains(indexPath){
customCell.cellBackground.backgroundColor = .green
}else{
customCell.cellBackground.backgroundColor = .red
}
return customCell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 150, height: 225)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) // Setting up the padding
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
//Start The Clock:
if let trackableCell = cell as? TrackableView {
trackableCell.tracker = ImpressionTracker(delegate: trackableCell)
trackableCell.tracker?.start()
}
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
//Stop The Clock:
(cell as? TrackableView)?.tracker?.stop()
}
}
// MARK: - Delegate Method:
extension ViewController:ImpressionStalkerDelegate{
func sendEventForCell(atIndexPath indexPath: IndexPath) {
guard let customCell = collectionView.cellForItem(at: indexPath) as? CustomCollectionViewCell else {
return
}
customCell.cellBackground.backgroundColor = .green
indexPathsOfCellsTurnedGreen.append(indexPath) // We append all the visable Cells into an array
}
}
my ImpressionStalker:
import Foundation
import UIKit
protocol ImpressionStalkerDelegate:NSObjectProtocol {
func sendEventForCell(atIndexPath indexPath:IndexPath)
}
protocol ImpressionItem {
func getUniqueId()->String
}
class ImpressionStalker: NSObject {
//MARK: Variables & Constants
let minimumPercentageOfCell: CGFloat
weak var collectionView: UICollectionView?
static var alreadySentIdentifiers = [String]()
weak var delegate: ImpressionStalkerDelegate?
//MARK: Initializer
init(minimumPercentageOfCell: CGFloat, collectionView: UICollectionView, delegate:ImpressionStalkerDelegate ) {
self.minimumPercentageOfCell = minimumPercentageOfCell
self.collectionView = collectionView
self.delegate = delegate
}
// Checks which cell is visible:
func stalkCells() {
for cell in collectionView!.visibleCells {
if let visibleCell = cell as? UICollectionViewCell & ImpressionItem {
let visiblePercentOfCell = percentOfVisiblePart(ofCell: visibleCell, inCollectionView: collectionView!)
if visiblePercentOfCell >= minimumPercentageOfCell,!ImpressionStalker.alreadySentIdentifiers.contains(visibleCell.getUniqueId()){ // >0.70 and not seen yet then...
guard let indexPath = collectionView!.indexPath(for: visibleCell), let delegate = delegate else {
continue
}
delegate.sendEventForCell(atIndexPath: indexPath) // send the cell's index since its visible.
ImpressionStalker.alreadySentIdentifiers.append(visibleCell.getUniqueId()) // to avoid double events to show up.
}
}
}
}
// Func Which Calculate the % Of Visible of each Cell:
private func percentOfVisiblePart(ofCell cell:UICollectionViewCell, inCollectionView collectionView:UICollectionView) -> CGFloat{
guard let indexPathForCell = collectionView.indexPath(for: cell),
let layoutAttributes = collectionView.layoutAttributesForItem(at: indexPathForCell) else {
return CGFloat.leastNonzeroMagnitude
}
let cellFrameInSuper = collectionView.convert(layoutAttributes.frame, to: collectionView.superview)
let interSectionRect = cellFrameInSuper.intersection(collectionView.frame)
let percentOfIntersection: CGFloat = interSectionRect.height/cellFrameInSuper.height
return percentOfIntersection
}
}
ImpressionTracker:
import Foundation
import UIKit
protocol ViewTracker {
init(delegate: TrackableView)
func start()
func pause()
func stop()
}
final class ImpressionTracker: ViewTracker {
private weak var viewToTrack: TrackableView?
private var timer: CADisplayLink?
private var startedTimeStamp: CFTimeInterval = 0
private var endTimeStamp: CFTimeInterval = 0
init(delegate: TrackableView) {
viewToTrack = delegate
setupTimer()
}
func setupTimer() {
timer = (viewToTrack as? UIView)?.window?.screen.displayLink(withTarget: self, selector: #selector(update))
timer?.add(to: RunLoop.main, forMode: .default)
timer?.isPaused = true
}
func start() {
guard viewToTrack != nil else { return }
timer?.isPaused = false
startedTimeStamp = CACurrentMediaTime() // Current Time in seconds.
}
func pause() {
guard viewToTrack != nil else { return }
timer?.isPaused = true
endTimeStamp = CACurrentMediaTime()
print("Im paused!")
}
func stop() {
timer?.isPaused = true
timer?.invalidate()
}
#objc func update() {
guard let viewToTrack = viewToTrack else {
stop()
return
}
guard viewToTrack.precondition() else {
startedTimeStamp = 0
endTimeStamp = 0
return
}
endTimeStamp = CACurrentMediaTime()
trackIfThresholdCrossed()
}
private func trackIfThresholdCrossed() {
guard let viewToTrack = viewToTrack else { return }
let elapsedTime = endTimeStamp - startedTimeStamp
if elapsedTime >= viewToTrack.thresholdTimeInSeconds() {
viewToTrack.viewDidStayOnViewPortForARound()
startedTimeStamp = endTimeStamp
}
}
}
my customCell:
import UIKit
protocol TrackableView: NSObject {
var tracker: ViewTracker? { get set }
func thresholdTimeInSeconds() -> Double //Takes care of the screen's time, how much "second" counts.
func viewDidStayOnViewPortForARound() // Counter for how long the "Post" stays on screen.
func precondition() -> Bool // Checks if the View is full displayed so the counter can go on fire.
}
class CustomCollectionViewCell: UICollectionViewCell {
var tracker: ViewTracker?
static let nibName = "CustomCollectionViewCell"
static let reuseIdentifier = "customCell"
#IBOutlet weak var cellBackground: UIView!
#IBOutlet weak var textLabel: UILabel!
var numberOfTimesTracked : Int = 0 {
didSet {
self.textLabel.text = "\(numberOfTimesTracked)"
}
}
override func awakeFromNib() {
super.awakeFromNib()
cellBackground.backgroundColor = .red
layer.borderWidth = 0.5
layer.borderColor = UIColor.lightGray.cgColor
}
override func prepareForReuse() {
super.prepareForReuse()
print("Hello")
tracker?.stop()
tracker = nil
}
}
extension CustomCollectionViewCell: ImpressionItem{
func getUniqueId() -> String {
return self.textLabel.text!
}
}
extension CustomCollectionViewCell: TrackableView {
func thresholdTimeInSeconds() -> Double { // every 2 seconds counts as a view.
return 2
}
func viewDidStayOnViewPortForARound() {
numberOfTimesTracked += 1 // counts for how long the view stays on screen.
}
func precondition() -> Bool {
let screenRect = UIScreen.main.bounds
let viewRect = convert(bounds, to: nil)
let intersection = screenRect.intersection(viewRect)
return intersection.height == bounds.height && intersection.width == bounds.width
}
}

The approach you probably want to use...
In you posted code, you've created an array of "read posts":
var indexPathsOfCellsTurnedGreen = [IndexPath]() // All the read "posts"
Assuming your real data will have multiple properties, such as:
struct TrackPost {
var message: String = ""
var postAuthor: String = ""
var postDate: Date = Date()
// ... other stuff
}
add another property to track whether or not it has been "seen":
struct TrackPost {
var message: String = ""
var postAuthor: String = ""
var postDate: Date = Date()
// ... other stuff
var hasBeenSeen: Bool = false
}
Move all of your "tracking" code out of the controller, and instead add a Timer to your cell class.
When the cell appears:
if hasBeenSeen for that cell's Data is false
start a 2-second timer
if the timer elapses, the cell has been visible for 2 seconds, so set hasBeenSeen to true (use a closure or protocol / delegate pattern to tell the controller to update the data source) and change the backgroundColor
if the cell is scrolled off-screen before the timer elapses, stop the timer and don't tell the controller anything
if hasBeenSeen is true to begin with, don't start the 2-second timer
Now, your cellForItemAt code will look something like this:
let p: TrackPost = myData[indexPath.row]
customCell.authorLabel.text = p.postAuthor
customCell.dateLabel.text = myDateFormat(p.postDate) // formatted as a string
customCell.textLabel.text = p.message
// setting hasBeenSeen in your cell should also set the backgroundColor
// and will be used so the cell knows whether or not to start the timer
customCell.hasBeenSeen = p.hasBeenSeen
// this will be called by the cell if the timer elapsed
customCell.wasSeenCallback = { [weak self] in
guard let self = self else { return }
self.myData[indexPath.item].hasBeenSeen = true
}

What about a much simpler approach like:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for subview in collectionView!.visibleCells {
if /* check visible percentage */ {
if !(subview as! TrackableCollectionViewCell).timerRunning {
(subview as! TrackableCollectionViewCell).startTimer()
}
} else {
if (subview as! TrackableCollectionViewCell).timerRunning {
(subview as! TrackableCollectionViewCell).stopTimer()
}
}
}
}
With a Cell-Class extended by:
class TrackableCollectionViewCell {
static let minimumVisibleTime: TimeInterval = 2.0
var timerRunning: Bool = true
private var timer: Timer = Timer()
func startTimer() {
if timerRunning {
return
}
timerRunning = true
timer = Timer.scheduledTimer(withTimeInterval: minimumVisibleTime, repeats: false) { (_) in
// mark cell as seen
}
}
func stopTimer() {
timerRunning = false
timer.invalidate()
}
}

Related

Why are 2 network calls using the same search string required before the UICollectionView decides there is data that needs to be refreshed?

I have a strange problem in a UIViewController that the user uses to search for a GIF.
There are essentially 2 issues:
The user has to enter the same search term twice before the UICollectionView triggers the cellForRowAt data source method after a call is made to reloadData().
After you enter the search term the first time, heightChanged() is called, but the self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height comes back as 0 even though I've confirmed that data is being received back from the server. The second time you enter the search term, the height is a non-zero value and the collection view shows the cells.
Here is an example of how I have to get the data to show up:
Launch app, go this UIViewController
Enter a search term (ie. "baseball")
Nothing shows up (even though reloadData() was called and new data is in the view model.)
Delete a character from the search term (ie. "basebal")
Type in the missing character (ie. "baseball")
The UICollectionView refreshes via a call to reloadData() and then calls cellForRowAt:.
Here is the entire View Controller:
import UIKit
protocol POGIFSelectViewControllerDelegate: AnyObject {
func collectionViewHeightDidChange(_ height: CGFloat)
func didSelectGIF(_ selectedGIFURL: POGIFURLs)
}
class POGIFSelectViewController: UIViewController {
//MARK: - Constants
private enum Constants {
static let POGIFCollectionViewCellIdentifier: String = "POGIFCollectionViewCell"
static let verticalPadding: CGFloat = 16
static let searchBarHeight: CGFloat = 40
static let searchLabelHeight: CGFloat = 24
static let activityIndicatorTopSpacing: CGFloat = 10
static let gifLoadDuration: Double = 0.2
static let gifStandardFPS: Double = 1/30
static let gifMaxDuration: Double = 5.0
}
//MARK: - Localized Strings
let localizedSearchGIFs = PALocalizedStringFromTable("RECOGNITION_IMAGE_SELECTION_GIF_BODY_TITLE", table: "Recognition-Osiris", comment: "Search GIFs") as String
//MARK: - Properties
var viewModel: POGIFSearchViewModel?
var activityIndicator = MDCActivityIndicator()
var gifLayout = PAGiphyCellLayout()
var selectedGIF: POGIFURLs?
//MARK: - IBOutlet
#IBOutlet weak var GIFCollectionView: UICollectionView!
#IBOutlet weak var searchGIFLabel: UILabel! {
didSet {
self.searchGIFLabel.text = self.localizedSearchGIFs
}
}
#IBOutlet weak var searchField: POSearchField! {
didSet {
self.searchField.delegate = self
}
}
#IBOutlet weak var activityIndicatorContainer: UIView!
//MARK: - Delegate
weak var delegate: POGIFSelectViewControllerDelegate?
//MARK: - View Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
self.setupActivityIndicator(activityIndicator: self.activityIndicator, activityIndicatorContainer: self.activityIndicatorContainer)
self.viewModel = POGIFSearchViewModel(data: PAData.sharedInstance())
if let viewModel = self.viewModel {
viewModel.viewDelegate = self
viewModel.viewDidBeginLoading()
}
self.gifLayout.delegate = self
self.gifLayout.isAXPGifLayout = true;
self.GIFCollectionView.collectionViewLayout = self.gifLayout
self.GIFCollectionView.backgroundColor = .orange
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// This Patch is to fix a bug where GIF contentSize was not calculated correctly on first load.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) {
self.viewModel?.viewDidBeginLoading()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.heightChanged()
}
//MARK: - Helper Methods
func heightChanged() {
guard let delegate = self.delegate else { return }
let height = self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height + Constants.verticalPadding * 3 + Constants.searchLabelHeight + Constants.searchBarHeight + activityIndicatorContainer.frame.size.height + Constants.activityIndicatorTopSpacing
print("**** Items in Collection View -> self.viewModel?.gifModel.items.count: \(self.viewModel?.gifModel.items.count)")
print("**** self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height: \(self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height); height: \(height)")
delegate.collectionViewHeightDidChange(height)
}
func reloadCollectionView() {
self.GIFCollectionView.collectionViewLayout.invalidateLayout()
self.GIFCollectionView.reloadData()
self.GIFCollectionView.layoutIfNeeded()
self.heightChanged()
}
func imageAtIndexPath(_ indexPath: IndexPath) -> UIImage? {
guard let previewURL = self.viewModel?.gifModel.items[indexPath.row].previewGIFURL else { return nil }
var loadedImage: UIImage? = nil
let imageManager = SDWebImageManager.shared()
imageManager.loadImage(with: previewURL, options: .lowPriority, progress: nil) { (image: UIImage?, data: Data?, error: Error?, cacheType: SDImageCacheType, finished: Bool, imageURL: URL?) in
loadedImage = image
}
return loadedImage
}
func scrollViewDidScrollToBottom() {
guard let viewModel = self.viewModel else { return }
if viewModel.viewDidSearchMoreGIFs() {
self.activityIndicator.startAnimating()
} else {
self.activityIndicator.stopAnimating()
}
}
}
extension POGIFSelectViewController: POSearchFieldDelegate {
func searchFieldTextChanged(text: String?) {
guard let viewModel = self.viewModel else { return }
viewModel.viewDidSearchGIFs(withSearchTerm: text)
}
}
extension POGIFSelectViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print("**** CELL FOR ROW AT -> self.viewModel?.gifModel.items.count: \(self.viewModel?.gifModel.items.count)")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.POGIFCollectionViewCellIdentifier, for: indexPath) as! POGIFCollectionViewCell
guard let previewURL = self.viewModel?.gifModel.items[indexPath.row].previewGIFURL else {
return cell
}
var cellState: POGIFCollectionViewCell.CellState = .dimmedState
if self.selectedGIF == nil {
cellState = .defaultState
} else if (self.selectedGIF?.previewGIFURL?.absoluteString == previewURL.absoluteString) {
cellState = .selectedState
}
cell.setupUI(withState: cellState, URL: previewURL) { [weak self] () in
UIView.animate(withDuration: Constants.gifLoadDuration) {
guard let weakSelf = self else { return }
weakSelf.GIFCollectionView.collectionViewLayout.invalidateLayout()
}
}
if cell.GIFPreviewImageView.animationDuration > Constants.gifMaxDuration {
cell.GIFPreviewImageView.animationDuration = Constants.gifMaxDuration
}
cell.backgroundColor = .green
return cell
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let viewModel = self.viewModel else { return 0 }
return viewModel.gifModel.items.count
}
}
extension POGIFSelectViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let selectedGIF = self.viewModel?.gifModel.items[indexPath.row],
let delegate = self.delegate else {
return
}
self.selectedGIF = selectedGIF
delegate.didSelectGIF(selectedGIF)
self.reloadCollectionView()
}
}
extension POGIFSelectViewController: POGIFSearchViewModelToViewProtocol {
func didFetchGIFsWithSuccess() {
self.activityIndicator.stopAnimating()
print("**** didFetchGIFsWithSuccess() -> about to reload collection view")
self.reloadCollectionView()
}
func didFetchGIFsWithError(_ error: Error!, request: PARequest!) {
self.activityIndicator.stopAnimating()
}
}
extension POGIFSelectViewController: PAGiphyLayoutCellDelegate {
func heightForCell(givenWidth cellWidth: CGFloat, at indexPath: IndexPath!) -> CGFloat {
guard let image = self.imageAtIndexPath(indexPath) else {
return 0
}
if (image.size.height < 1 || image.size.width < 1 || self.activityIndicator.isAnimating) {
return cellWidth
}
let scaleFactor = image.size.height / image.size.width
let imageViewToHighlightedViewSpacing: CGFloat = 4 // this number comes from 2 * highlightedViewBorderWidth from POGIFCollectionViewCell
return cellWidth * scaleFactor + imageViewToHighlightedViewSpacing
}
func heightForHeaderView() -> CGFloat {
return 0
}
}
You'll see that the heightChanged() method calls a delegate method. That method is in another UIViewController:
func collectionViewHeightDidChange(_ height: CGFloat) {
self.collectionViewHeightConstraint.constant = height
}
So, I can't figure out why I need to either delete a character from the search term and re-add it in order for the data to refresh even though the very first call populated the view model with new data.
It's bizarre. Please help.

UICollection View "Reload Data" After Scrolling Crash

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)
}

reload selected collection cell in independent time interval

I am trying to make countdown timer app with collectionView.
Features & Functions:
Each cell has own label and timer function.
CountDown timer will run if user touches a cell.
Time string have to updated as timer run.
I successfully build a timer in each cell but I'm stuck updating timeLabel (reload selected cell).
Please check the codes below and give me some hint.
class ListViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
var recipeList: TimeRecipeList
var timer = Timer()
required init?(coder aDecoder: NSCoder) {
recipeList = TimeRecipeList()
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
let width = (view.frame.size.width - 10) / 2
let layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.itemSize = CGSize(width: width, height: width)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "AddItemSegue" {
if let addItemVC = segue.destination as? AddRecipeViewController {
addItemVC.delegate = self
}
}
}
}
extension ListViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return recipeList.item.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RecipeListCell", for: indexPath)
let item = recipeList.item[indexPath.row]
configureText(for: cell, with: item)
return cell
}
func configureText(for cell: UICollectionViewCell, with item: TimeRecipe) {
if let label = cell.viewWithTag(1) as? UILabel {
label.text = item.name
}
if let label = cell.viewWithTag(2) as? UILabel {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
let timeString = formatter.string(from: TimeInterval(item.time))
label.text = timeString
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RecipeListCell", for: indexPath)
let item = recipeList.item[indexPath.row]
let cellTimer = TimerControl()
cellTimer.second = item.time
cellTimer.timerRun()
configureText(for: cell, with: item)
}
class TimerControl {
var timer = Timer()
var second: Int = 0
func timerRun() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(countDown), userInfo: nil, repeats: true)
}
#objc func countDown() {
//let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RecipeListCell", for: indexPath)
if second <= 1 {
timer.invalidate()
} else {
second -= 1
//collectionView.reloadItems(at: [indexPath])
//MARK: Reload selected cell
}
}
}
}
extension ListViewController: AddRecipeViewControllerDelegate {
func addRecipeViewControllerDidCancel(_ controller: AddRecipeViewController) {
dismiss(animated: true)
}
func addRecipeViewControllerDisSave(_ controller: AddRecipeViewController, didFinishAdding item: TimeRecipe) {
dismiss(animated: true)
let rowIndex = recipeList.item.count
recipeList.item.append(item)
let indexPath = IndexPath(row: rowIndex, section: 0)
let indexPaths = [indexPath]
collectionView.insertItems(at: indexPaths)
}
}
#objc func countDown(indexPath: IndexPath) {
if second <= 1 {
timer.invalidate()
} else {
second -= 1
//MARK: Reload selected cell
yourarray[indexPath.row] = newvalues
collectionView.reloadItems(at: [indexPath])
}
}

UICollectionView Drag and Drop cell between collectionView

Hi I am using KDDragAndDropCollectionView for drag and drop feature between two three different collectionView. Everything is working fine, but I am not able to restrict the movement for particular case. I have three collectionView. The user can drop from A to B and B to C. But He cannot drah and drop from A to C or C to B or B to A.
Here is my code.
import SlideMenuControllerSwift
class MainViewController: NavigationBarViewController,KDDragAndDropCollectionViewDataSource {
#IBOutlet weak var inProgressView: UIView!
#IBOutlet weak var doneview: UIView!
#IBOutlet weak var toDoView: UIView!
#IBOutlet weak var doneCollectionView: UICollectionView!
#IBOutlet weak var inProgressCollectionView: UICollectionView!
#IBOutlet weak var toDoCollectionView: UICollectionView!
var drop: UIDropDown!
var toDoDataArray = [String]()
var inProgressDataArray = [String]()
var doneDataArray = [String]()
var dragAndDropManager : KDDragAndDropManager?
//MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
toDoDataArray.append("A")
toDoDataArray.append("B")
toDoDataArray.append("C")
inProgressDataArray.append("D")
doneDataArray.append("B")
doneDataArray.append("C")
// Do any additional setup after loading the view.
self.setUp()
self.setupDropDown()
self.dragAndDropManager = KDDragAndDropManager(canvas: self.view, collectionViews: [toDoCollectionView, inProgressCollectionView,doneCollectionView])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
}
//MARK: - Private Method
func setupDropDown() {
drop = UIDropDown(frame: CGRect(x: 24, y: 80, width: 200, height: 40))
//drop.center = CGPoint(x: self.view.frame.midX, y: self.view.frame.midY)
drop.placeholder = "Select Month"
drop.options = ["Weekly", "Monthly", "Bi-Annual", "Annual"]
drop.didSelect { (option, index) in
self.drop.placeholder = option
print("You just select: \(option) at index: \(index)")
}
self.view.addSubview(drop)
}
func setUp()
{
//Setup Navigation Bar
self.menuIconImage = #imageLiteral(resourceName: "hamIco")
self.setNavigationBarButtonItem()
self.setNavigationBarItem()
}
/*
// 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.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
//MARK: - Navigation Bar Button Action Method
//MARK: - Button Action Methods
/**
Button Action method. Gets called when the left navigation item
Parameters: sender - the button on which the event occurred
**/
#IBAction func leftBtnAction(sender: UIButton) {
self.toggleLeft()
}
// MARK: - UITableView DataSource abd Delegate
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("Step 1 called")
if collectionView.tag == 0 {
return toDoDataArray.count
}
else if collectionView.tag == 1 {
return inProgressDataArray.count
}
return doneDataArray.count
}
// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print("Step 2 called")
if collectionView.tag == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TO_DO_COLLECTION_CELL_ID, for: indexPath) as! ToDoCollectionViewCell
cell.isHidden = false
if let kdCollectionView = collectionView as? KDDragAndDropCollectionView {
if let draggingPathOfCellBeingDragged = kdCollectionView.draggingPathOfCellBeingDragged {
if draggingPathOfCellBeingDragged.item == indexPath.item {
cell.isHidden = true
}
}
}
return cell
}
else if collectionView.tag == 1 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: IN_PROGRESS_COLLECTION_CELL_ID, for: indexPath) as! InProgressCollectionViewCell
cell.isHidden = false
if let kdCollectionView = collectionView as? KDDragAndDropCollectionView {
if let draggingPathOfCellBeingDragged = kdCollectionView.draggingPathOfCellBeingDragged {
if draggingPathOfCellBeingDragged.item == indexPath.item {
cell.isHidden = true
}
}
}
return cell
}
else{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DONE_COLLECTION_Cell_ID, for: indexPath) as! DoneCollectionViewCell
cell.isHidden = false
if let kdCollectionView = collectionView as? KDDragAndDropCollectionView {
if let draggingPathOfCellBeingDragged = kdCollectionView.draggingPathOfCellBeingDragged {
if draggingPathOfCellBeingDragged.item == indexPath.item {
cell.isHidden = true
}
}
}
return cell
}
}
// MARK : KDDragAndDropCollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, dataItemForIndexPath indexPath: IndexPath) -> AnyObject {
print("Step 3 called")
if collectionView.tag == 0 {
return toDoDataArray[indexPath.item] as AnyObject
}
else if collectionView.tag == 1 {
return inProgressDataArray[indexPath.item] as AnyObject
}
return doneDataArray[indexPath.item] as AnyObject
//return data[collectionView.tag][indexPath.item]
}
func collectionView(_ collectionView: UICollectionView, insertDataItem dataItem : AnyObject, atIndexPath indexPath: IndexPath) -> Void {
print("Step 4 called")
if collectionView.tag == 0 {
if let di = dataItem as? String {
toDoDataArray.insert(di, at: indexPath.item)
//data[collectionView.tag].insert(di, at: indexPath.item)
}
}
else if collectionView.tag == 1 {
if let di = dataItem as? String {
inProgressDataArray.insert(di, at: indexPath.item)
//data[collectionView.tag].insert(di, at: indexPath.item)
}
}
else{
if let di = dataItem as? String {
doneDataArray.insert(di, at: indexPath.item)
}
}
}
func collectionView(_ collectionView: UICollectionView, deleteDataItemAtIndexPath indexPath : IndexPath) -> Void {
print("Step 5 called")
if collectionView.tag == 0 {
toDoDataArray.remove(at: indexPath.item)
}
else if collectionView.tag == 1 {
inProgressDataArray.remove(at: indexPath.item)
}
else{
doneDataArray.remove(at: indexPath.item)
}
}
func collectionView(_ collectionView: UICollectionView, moveDataItemFromIndexPath from: IndexPath, toIndexPath to : IndexPath) -> Void {
print("Step 6 called")
if collectionView.tag == 0 {
let fromDataItem: String = toDoDataArray[from.item]
toDoDataArray.remove(at: from.item)
toDoDataArray.insert(fromDataItem, at: to.item)
}
else if collectionView.tag == 1 {
let fromDataItem: String = inProgressDataArray[from.item]
inProgressDataArray.remove(at: from.item)
inProgressDataArray.insert(fromDataItem, at: to.item)
}
else{
let fromDataItem: String = doneDataArray[from.item]
doneDataArray.remove(at: from.item)
doneDataArray.insert(fromDataItem, at: to.item)
}
}
func collectionView(_ collectionView: UICollectionView, indexPathForDataItem dataItem: AnyObject) -> IndexPath? {
print("Step 7 called")
if collectionView.tag == 0 {
if let candidate : String = dataItem as? String {
for item : String in toDoDataArray {
if candidate == item {
let position = toDoDataArray.index(of: item)! // ! if we are inside the condition we are guaranteed a position
let indexPath = IndexPath(item: position, section: 0)
return indexPath
}
}
}
}
else if collectionView.tag == 1 {
if let candidate : String = dataItem as? String {
for item : String in inProgressDataArray {
if candidate == item {
let position = inProgressDataArray.index(of: item)! // ! if we are inside the condition we are guaranteed a position
let indexPath = IndexPath(item: position, section: 0)
return indexPath
}
}
}
}
else{
if let candidate : String = dataItem as? String {
for item : String in doneDataArray {
if candidate == item {
let position = doneDataArray.index(of: item)! // ! if we are inside the condition we are guaranteed a position
let indexPath = IndexPath(item: position, section: 0)
return indexPath
}
}
}
}
return nil
}
}
//MARK: - Extension written for Slide the Menu
extension MainViewController : SlideMenuControllerDelegate {
func leftWillOpen() {
print("SlideMenuControllerDelegate: leftWillOpen")
}
func leftDidOpen() {
print("SlideMenuControllerDelegate: leftDidOpen")
}
func leftWillClose() {
print("SlideMenuControllerDelegate: leftWillClose")
}
func leftDidClose() {
print("SlideMenuControllerDelegate: leftDidClose")
}
func rightWillOpen() {
print("SlideMenuControllerDelegate: rightWillOpen")
}
func rightDidOpen() {
print("SlideMenuControllerDelegate: rightDidOpen")
}
func rightWillClose() {
print("SlideMenuControllerDelegate: rightWillClose")
}
func rightDidClose() {
print("SlideMenuControllerDelegate: rightDidClose")
}
}

Using periodicObserver on AVPlayer to track playback progress

Edit: I think problem is somehow connected with cell reuse ability, but still can't figure out how to fix
I'm building an application that has a tableView with a posts with an URL to a music track. When post becomes fully visible, my player starts playback of posts track. During playback I should display progress of playback on a waveform view in a post.
The problem is that observation correctly works only with 3 first cells, for the next cells waveformPlot does not get updated and seeking stops working.
I have no idea what is causing this problem. Any help is appreciated.
Here is a layout:
In my viewController's viewDidLoad I init AVPlayer observer with addPeriodicTimeObserver and assign its return type to a variable playerObserver.
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
timeLinePostsArray = User.sharedUser.requestTimelineData()
playerObserver = StreamMusicPlayer.shared.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2),
queue: DispatchQueue.main,
using: { (progress) in
if StreamMusicPlayer.shared.isPlaying {
self.cellToObserve?.updatePlot(with: progress)
}
})
}
Below is the code I use to detect fully visible cell. When fully visible cell is detected, I assign it to variable cellToObserve for observer and begin playback of cell's music track.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for item in tableView.indexPathsForVisibleRows!{
if tableView.bounds.contains(tableView.rectForRow(at: item)){
let fullyVisibleCell = tableView.cellForRow(at: item) as! HomeControllerTableViewCell
if StreamMusicPlayer.currentItemURL != fullyVisibleCell.containedMusicTrack?.trackURL.absoluteString {
//Play music track and observe the playback progress
self.cellToObserve = fullyVisibleCell
self.numberOfCellToObserve = tableView.indexPath(for: fullyVisibleCell)?.row
fullyVisibleCell.playButton.isEnabled = true
fullyVisibleCell.plot.isUserInteractionEnabled = true
StreamMusicPlayer.playItem(musicTrack: fullyVisibleCell.containedMusicTrack!)
}
} else {
let _cell = tableView.cellForRow(at: item) as! HomeControllerTableViewCell
_cell.plot.isUserInteractionEnabled = false
_cell.playButton.isEnabled = false
}
}
}
In the viewController's deinit method I attempt to remove player's observer.
func attemptRemoveObservation() {
if self.playerObserver != nil {
StreamMusicPlayer.shared.removeTimeObserver(self.playerObserver)
self.playerObserver = nil
}
}
deinit {
self.attemptRemoveObservation()
}
This is my player's implementation:
class StreamMusicPlayer: AVPlayer {
private override init(){
super.init()
}
static var currentItemURL: String?
static var shared = AVPlayer()
static func playItem(musicTrack: MusicTrack) {
let item = AVPlayerItem(url: musicTrack.trackURL)
StreamMusicPlayer.shared.replaceCurrentItem(with: item)
StreamMusicPlayer.currentItemURL = musicTrack.trackURL.absoluteString
StreamMusicPlayer.shared.play()
}
}
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}
If needed, here is full viewController's source code:
//
// HomeViewController.swift
// CheckMyTrack
//
// Created by Alexey Savchenko on 09.03.17.
// Copyright © 2017 Alexey Savchenko. All rights reserved.
//
import UIKit
import CoreMedia
import Foundation
class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
//MARK: Vars
var playerObserver: Any!
var timeLinePostsArray: [TimeLinePost] = []
var cellToObserve: HomeControllerTableViewCell?
var numberOfCellToObserve: Int?
//MARK: Outlets
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
timeLinePostsArray = User.sharedUser.requestTimelineData()
playerObserver = StreamMusicPlayer.shared.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2),
queue: DispatchQueue.main,
using: { (progress) in
if StreamMusicPlayer.shared.isPlaying {
self.cellToObserve?.updatePlot(with: progress)
}
})
}
func attemptRemoveObservation(){
if self.playerObserver != nil{
StreamMusicPlayer.shared.removeTimeObserver(self.playerObserver)
self.playerObserver = nil
}
}
//MARK: TableView delegate methods
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return timeLinePostsArray.count
}
//TODO: Implement correct change of observing cell plot
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for item in tableView.indexPathsForVisibleRows!{
if tableView.bounds.contains(tableView.rectForRow(at: item)){
let fullyVisibleCell = tableView.cellForRow(at: item) as! HomeControllerTableViewCell
if StreamMusicPlayer.currentItemURL != fullyVisibleCell.containedMusicTrack?.trackURL.absoluteString {
//Play music track and observe the playback progress
self.cellToObserve = fullyVisibleCell
self.numberOfCellToObserve = tableView.indexPath(for: fullyVisibleCell)?.row
fullyVisibleCell.playButton.isEnabled = true
fullyVisibleCell.plot.isUserInteractionEnabled = true
StreamMusicPlayer.playItem(musicTrack: fullyVisibleCell.containedMusicTrack!)
}
} else {
let _cell = tableView.cellForRow(at: item) as! HomeControllerTableViewCell
_cell.plot.isUserInteractionEnabled = false
_cell.playButton.isEnabled = false
}
}
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// if let _cell = cell as? HomeControllerTableViewCell{
// if self.playerToken != nil{
// StreamMusicPlayer.shared.removeTimeObserver(self.playerToken)
// self.playerToken = nil
// }
// }
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "HomeViewControllerCell", for: indexPath) as? HomeControllerTableViewCell
let track = timeLinePostsArray[indexPath.row].musicTrack
cell?.containedMusicTrack = track
cell?.authorNameLabel.text = timeLinePostsArray[indexPath.row].authorName
cell?.authorPicture.image = timeLinePostsArray[indexPath.row].authorPicture
cell?.trackName.text = track.trackName
cell?.plot.populateWithData(from: track.trackWaveformData)
cell?.plot.normalColor = UIColor.gray
cell?.plot.progressColor = UIColor.orange
cell?.playAction = { (cell) in
if StreamMusicPlayer.shared.isPlaying {
if StreamMusicPlayer.currentItemURL == cell.containedMusicTrack?.trackURL.absoluteString {
cell.playButton.setImage(UIImage(named: "play"), for: .normal)
StreamMusicPlayer.shared.pause()
}
} else {
StreamMusicPlayer.shared.play()
cell.playButton.setImage(UIImage(named: "pause"), for: .normal)
}
}
cell?.likeAction = { (cell) in
}
return cell!
}
deinit {
self.attemptRemoveObservation()
}
}
I have found the origin of problem.
It is necessary to implement prepareForReuse method for a custom cell class I use and perform a clean-up of a cell's waveformPlot.
WaveformPlot's waveforms property requires to be explicitly emptied.
My implementation is below:
override func prepareForReuse() {
plot.clearPlot()
plot.waveforms = []
}
func clearPlot(){
for item in self.subviews{
item.removeFromSuperview()
}
guard let layers = self.layer.sublayers else { return }
for _item in layers{
_item.removeFromSuperlayer()
}
}

Resources