MessagesExtension UICollectionView retaining too many cells containing MSStickerView

I have an iMessage Extension project that I'm building, which is pretty much done - except for memory allocation issues. Here's the rundown:

• I am using the default MainInterface.storyboard/MessagesViewController entry point, but in its viewDidLoad(), I am adding my own separate collection view controller as a subview. No more storyboard usage other than that.

• Inside my CollectionViewController.swift, there is 1 collectionView that remains the only one in the project.

• When a user changes sections, I change the collectionView objects and reloadData().

• Each cell contains an MSStickerView with an animating apng sticker.

• I am using "dequeueReusableCell" when creating these cells.

• There are on average about 12 stickers on the screen at all time.


All of this ^^^ is working fine and dandy. The issue that I'm having is memory allocation and the cells not being removed when scrolled offscreen. I know a strong reference would keep this from happening, but I can't find any instances of that. Also, SOMETIMES a cell will deinit and be removed. But that's very rare. Once I have scrolled through a whole section, the number of StickerCells that persist is always in the range of 27-[max on a page]...even when I go to a section that only has 6 cells.


I'm putting almost all of the code here for you guys to look at. Any help or advice you have would be greatly appreciated. I'm at a bit of a loss here...


Currently using this branch of PlanetSwift - a framework I use to streamline some collectionView creation. Makes things like cell layout/constraints much easier. 🙂

https://github.com/SmallPlanetSwift/PlanetSwift/tree/swift3


MyCollectionViewController.swift:

import UIKit
import Messages
import ImageIO
import PlanetSwift
import StoreKit
enum StickerCategories {
    case One
    case Two
    case Three
    case Four
    case Five
    case Six
}
class BigHeadsCollectionViewController: PlanetCollectionViewController {
    var productID = ""
    var productsRequest = SKProductsRequest()
    var products = [SKProduct]()
    var nonConsumablePurchaseMade = UserDefaults.standard.bool(forKey: "nonConsumablePurchaseMade")

    var currentSelectedNav: PlanetButton?
    var currentStickerCategory: StickerCategories = .One

    override func viewDidLoad() {
        mainBundlePath = "bundle:/
        loadView()
  
        collectionView = stickersArea?.collectionView
        collectionView.dataSource = self
        collectionView.delegate = self
  
        nav_section1?.button.isSelected = true
        currentSelectedNav = nav_section1?.button
  
        nav_section1?.button.addTarget(self, action: #selector(switchCategories), for: .touchUpInside)
        nav_section2?.button.addTarget(self, action: #selector(switchCategories), for: .touchUpInside)
        nav_section3?.button.addTarget(self, action: #selector(switchCategories), for: .touchUpInside)
        nav_section4?.button.addTarget(self, action: #selector(switchCategories), for: .touchUpInside)
        nav_section5?.button.addTarget(self, action: #selector(switchCategories), for: .touchUpInside)
        nav_section6?.button.addTarget(self, action: #selector(switchCategories), for: .touchUpInside)
  
        nav_section1?.button.tag = 0
        nav_section2?.button.tag = 1
        nav_section3?.button.tag = 2
        nav_section4?.button.tag = 3
        nav_section5?.button.tag = 4
        nav_section6?.button.tag = 5
  
        super.viewDidLoad()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        topNav?.view.frame.origin.y = self.topLayoutGuide.length
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        objects = objectsForDestination(currentStickerCategory)
        collectionView.reloadData()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        print("didReceiveMemoryWarning")
    }

    func switchCategories(_ sender: AnyObject) {
        var targetCategory: StickerCategories
        switch sender.tag {
            case 0:
                targetCategory = .One
            case 1:
                targetCategory = .Two
            case 2:
                targetCategory = .Three
            case 3:
                targetCategory = .Four
            case 4:
                targetCategory = .Five
            default:
                targetCategory = .Six
        }
  
        if targetCategory != currentStickerCategory {
            currentSelectedNav?.isSelected = false
            if let senderButton = sender as? PlanetButton {
                currentSelectedNav = senderButton
                currentSelectedNav?.isSelected = true
            }
      
            currentStickerCategory = targetCategory
            objects = objectsForDestination(currentStickerCategory)
            collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
            collectionView.reloadData()
        }
    }

    override func configureCollectionView() {
        super.configureCollectionView()
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        layout.minimumInteritemSpacing = 0
        layout.minimumLineSpacing = 0
        collectionView.isScrollEnabled = true
        collectionView.setCollectionViewLayout(layout, animated: false)
        collectionView.backgroundColor = UIColor.white
    }

    override open var cellMapping: [String: PlanetCollectionViewCell.Type] {
        return [
            StickerCell.reuseIdentifier: StickerCell.self,
            RestoreButtonCell.reuseIdentifier: RestoreButtonCell.self
        ]
    }

    override func reuseIdentifier(_ indexPath: IndexPath) -> String {
        if let cell = (cellObject(indexPath) as? BigHeadsCellTemplate) {
            return cell.reuseId
        }
        return RestoreButtonCell.reuseIdentifier
    }

    override func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: 0, bottom: 40, right: 0)
    }

    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if let stickerCell = cell as? StickerCell, let sticker = stickerCell.stickerView?.sticker {
            if stickerCanAnimate(sticker: sticker) {
                stickerCell.stickerView?.startAnimating()
            }
        }
    }

    func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if let stickerCell = cell as? StickerCell, let sticker = stickerCell.stickerView?.sticker {
            if stickerCanAnimate(sticker: sticker) {
                stickerCell.stickerView?.stopAnimating()
            }
        }
    }

    private func stickerCanAnimate(sticker: MSSticker) -> Bool {
        guard let stickerImageSource = CGImageSourceCreateWithURL(sticker.imageFileURL as CFURL, nil) else { return false }
        let stickerImageFrameCount = CGImageSourceGetCount(stickerImageSource)
        return stickerImageFrameCount > 1
    }

    fileprivate var stickersArea: CollectionView? {
        return mainXmlView?.elementForId("stickersArea")?.asCollectionView
    }
    fileprivate var topNav: View? {
        return mainXmlView?.elementForId("sectionsBar")?.asView
    }
    fileprivate var nav_section1: Button? {
        return mainXmlView?.elementForId("sectionButtonOne")?.asButton
    }
    fileprivate var nav_section2: Button? {
        return mainXmlView?.elementForId("sectionButtonTwo")?.asButton
    }
    fileprivate var nav_section3: Button? {
        return mainXmlView?.elementForId("sectionButtonThree")?.asButton
    }
    fileprivate var nav_section4: Button? {
        return mainXmlView?.elementForId("sectionButtonFour")?.asButton
    }
    fileprivate var nav_section5: Button? {
        return mainXmlView?.elementForId("sectionButtonFive")?.asButton
    }
    fileprivate var nav_section6: Button? {
        return mainXmlView?.elementForId("sectionButtonSix")?.asButton
    }
}
enum StickerType: String {
    case Free
    case Paid
}
struct BigHeadsCellTemplate: PlanetCollectionViewTemplate {
    let type: StickerType
    let fileName: String
    let localizedName: String
    var reuseId: String { return StickerCell.reuseIdentifier }

    var cellSize: Float {
        var screenWidth = UIScreen.main.bounds.width
        let screenHeight = UIScreen.main.bounds.height
        if screenWidth > screenHeight { screenWidth = screenHeight } /
        return Float(floor(screenWidth / 3))
    }

    var size: TemplateSize {
        return (width: .Fixed(points: cellSize), height: .Fixed(points: cellSize))
    }
    let stickerPadding: Float = 20.0


func decorate(_ cell: UICollectionViewCell) {
        if let cell = cell as? StickerCell {
            cell.stickerView?.removeFromSuperview()
            cell.stickerView = nil
          
            cell.isLocked = type == .Paid
            cell.buyStickersButton?.button.isHidden = !cell.isLocked
            cell.lockedIcon?.imageView.isHidden = !cell.isLocked
            if let sticker = configureSticker(usingImageName: fileName, localizedName: localizedName) {
                let stickerFrame = CGRect(x: 0, y: 0, width: CGFloat(cellSize - stickerPadding), height: CGFloat(cellSize - stickerPadding))
                cell.stickerView = MSStickerView(frame: stickerFrame, sticker: sticker)
              
                guard let stickerHolder = cell.stickerHolder?.view, let stickerView = cell.stickerView else { return }
                stickerHolder.addSubview(stickerView)
                stickerHolder.alpha = type == .Paid ? 0.4 : 1
            }
        }
    }


    func configureSticker(usingImageName imageName:String, localizedName: String) -> MSSticker? {
        guard let imagePath = Bundle.main.path(forResource: "Assets/pngs/"+imageName, ofType: ".png") else {
            return nil
        }
        let path =  URL(fileURLWithPath: imagePath)
        do {
            let description = NSLocalizedString(localizedName, comment: "")
            let sticker = try MSSticker(contentsOfFileURL: path , localizedDescription: description)
            return sticker
        }
        catch {
            fatalError("Failed to create sticker: \(error)")
        }
    }
}


Inside the StickerCell.swift:

import UIKit
import Messages
import PlanetSwift
class StickerCell: UICollectionViewCell, PlanetCollectionViewCell {
    static let reuseIdentifier = "FreeCell"
    public let bundlePath = "bundle: // Assets/cells/stickerCell.xml"

    public var xmlView: View?
    var stickerView: MSStickerView?
    var stickerHolder: View? { return element(forKey: "stickerHolder")?.asView }
    var buyStickersButton: Button? { return element(forKey: "buyStickersButton")?.asButton }
    var lockedIcon: ImageView? { return element(forKey: "lockedIcon")?.asImageView }
    var isLocked: Bool = false

    func element(forKey key: String) -> GaxbElement? {
        return xmlView?.objectForId(key)
    }

    deinit {
        print("*** deinit: ", stickerView?.sticker?.imageFileURL)
    }
}
MessagesExtension UICollectionView retaining too many cells containing MSStickerView
 
 
Q