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