Ron Baker
Ron Baker

Reputation: 381

Memory Leak in ViewController

So im having somewhat of an issue with my ios app. Im facing a memory leak when I enter my view controller that controls the display of my comments. I am using IGListKit so anyone that is familar with that would be a great help to this question but help is needed none the less. This is my newCommentsViewController that handles pulling the comments from firebase and sends them to the datasource.

    import UIKit
    import IGListKit
    import Firebase


class NewCommentsViewController: UIViewController, UITextFieldDelegate,CommentsSectionDelegate,CommentInputAccessoryViewDelegate {
    //array of comments which will be loaded by a service function
    var comments = [CommentGrabbed]()
    var messagesRef: DatabaseReference?
    var bottomConstraint: NSLayoutConstraint?
    public let addHeader = "addHeader" as ListDiffable
    public var eventKey = ""
    //This creates a lazily-initialized variable for the IGListAdapter. The initializer requires three parameters:
    //1 updater is an object conforming to IGListUpdatingDelegate, which handles row and section updates. IGListAdapterUpdater is a default implementation that is suitable for your usage.
    //2 viewController is a UIViewController that houses the adapter. This view controller is later used for navigating to other view controllers.
    //3 workingRangeSize is the size of the working range, which allows you to prepare content for sections just outside of the visible frame.

    lazy var adapter: ListAdapter = {
        return ListAdapter(updater: ListAdapterUpdater(), viewController: self)
    }()


    // 1 IGListKit uses IGListCollectionView, which is a subclass of UICollectionView, which patches some functionality and prevents others.
    let collectionView: UICollectionView = {
        // 2 This starts with a zero-sized rect since the view isn’t created yet. It uses the UICollectionViewFlowLayout just as the ClassicFeedViewController did.
        let view = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())
        // 3 The background color is set to white
        view.backgroundColor = UIColor.white
        return view
    }()

    //will fetch the comments from the database and append them to an array
    fileprivate func fetchComments(){
        comments.removeAll()
        messagesRef = Database.database().reference().child("Comments").child(eventKey)
       // print(eventKey)
        // print(comments.count)
        let query = messagesRef?.queryOrderedByKey()
        query?.observe(.value, with: { (snapshot) in
            guard let allObjects = snapshot.children.allObjects as? [DataSnapshot] else {
                return
            }
           // print(snapshot)

            allObjects.forEach({ (snapshot) in
                guard let commentDictionary = snapshot.value as? [String: Any] else{
                    return
                }
                guard let uid = commentDictionary["uid"] as? String else{
                    return
                }
                UserService.show(forUID: uid, completion: { (user) in
                    if let user = user {
                        let commentFetched = CommentGrabbed(user: user, dictionary: commentDictionary)
                        commentFetched.commentID = snapshot.key
                        let filteredArr = self.comments.filter { (comment) -> Bool in
                            return comment.commentID == commentFetched.commentID
                        }
                        if filteredArr.count == 0 {
                            self.comments.append(commentFetched)

                        }
                        self.adapter.performUpdates(animated: true)
                    }else{
                        print("user is null")

                    }
                    self.comments.sort(by: { (comment1, comment2) -> Bool in
                        return comment1.creationDate.compare(comment2.creationDate) == .orderedAscending
                    })
                    self.comments.forEach({ (comments) in
                    })
                })

            })

        }, withCancel: { (error) in
            print("Failed to observe comments")
        })

        //first lets fetch comments for current event
    }

    //allows you to gain access to the input accessory view that each view controller has for inputting text
    lazy var containerView: CommentInputAccessoryView = {
        let frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 50)
        let commentInputAccessoryView = CommentInputAccessoryView(frame:frame)
        commentInputAccessoryView.delegate = self
        return commentInputAccessoryView
    }()


    @objc func handleSubmit(for comment: String?){
        guard let comment = comment, comment.count > 0 else{
            return
        }

        let userText = Comments(content: comment, uid: User.current.uid, profilePic: User.current.profilePic!,eventKey: eventKey)
        sendMessage(userText)
        // will clear the comment text field
        self.containerView.clearCommentTextField()
    }


    @objc func handleKeyboardNotification(notification: NSNotification){

        if let userinfo = notification.userInfo {

            if let keyboardFrame = (userinfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue{

                self.bottomConstraint?.constant = -(keyboardFrame.height)

                let isKeyboardShowing = notification.name == NSNotification.Name.UIKeyboardWillShow

                self.bottomConstraint?.constant = isKeyboardShowing ? -(keyboardFrame.height) : 0
                if isKeyboardShowing{
                    let contentInset = UIEdgeInsetsMake(0, 0, (keyboardFrame.height), 0)
                    collectionView.contentInset = UIEdgeInsetsMake(0, 0, (keyboardFrame.height), 0)
                    collectionView.scrollIndicatorInsets = contentInset
                }else {
                    let contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
                    collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
                    collectionView.scrollIndicatorInsets = contentInset
                }


                UIView.animate(withDuration: 0, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
                    self.view.layoutIfNeeded()
                }, completion: { (completion) in
                    if self.comments.count > 0  && isKeyboardShowing {
                        let item = self.collectionView.numberOfItems(inSection: self.collectionView.numberOfSections - 1)-1
                        let lastItemIndex = IndexPath(item: item, section: self.collectionView.numberOfSections - 1)
                        self.collectionView.scrollToItem(at: lastItemIndex, at: UICollectionViewScrollPosition.top, animated: true)


                    }
                })
            }
        }
    }

    override var inputAccessoryView: UIView? {
        get {
            return containerView
        }
    }

    override var canBecomeFirstResponder: Bool {
        return true
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.frame = CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height-40)
        view.addSubview(collectionView)
        collectionView.alwaysBounceVertical = true
        adapter.collectionView = collectionView
        adapter.dataSource = self
        NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
        collectionView.register(CommentCell.self, forCellWithReuseIdentifier: "CommentCell")
//        collectionView.register(CommentHeader.self, forCellWithReuseIdentifier: "HeaderCell")
        collectionView.keyboardDismissMode = .onDrag
        navigationItem.title = "Comments"
        self.navigationItem.hidesBackButton = true
        let backButton = UIBarButtonItem(image: UIImage(named: "icons8-Back-64"), style: .plain, target: self, action: #selector(GoBack))
        self.navigationItem.leftBarButtonItem = backButton
    }

    @objc func GoBack(){
        print("BACK TAPPED")
        self.dismiss(animated: true, completion: nil)
    }

    //look here
    func CommentSectionUpdared(sectionController: CommentsSectionController){
        print("like")
        self.fetchComments()
        self.adapter.performUpdates(animated: true)
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        fetchComments()
        tabBarController?.tabBar.isHidden = true
        //submitButton.isUserInteractionEnabled = true

    }
    //viewDidLayoutSubviews() is overridden, setting the collectionView frame to match the view bounds.
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        //    collectionView.frame = view.bounds
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

extension NewCommentsViewController: ListAdapterDataSource {
    // 1 objects(for:) returns an array of data objects that should show up in the collection view. loader.entries is provided here as it contains the journal entries.
    func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
        let items:[ListDiffable] = comments
        //print("comments = \(comments)")
        return items
    }




    // 2 For each data object, listAdapter(_:sectionControllerFor:) must return a new instance of a section controller. For now you’re returning a plain IGListSectionController to appease the compiler — in a moment, you’ll modify this to return a custom journal section controller.
    func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
        //the comment section controller will be placed here but we don't have it yet so this will be a placeholder
//        if let object = object as? ListDiffable, object === addHeader {
//            return CommentsHeaderSectionController()
//        }
        let sectionController = CommentsSectionController()
        sectionController.delegate = self

        return sectionController
    }

    // 3 emptyView(for:) returns a view that should be displayed when the list is empty. NASA is in a bit of a time crunch, so they didn’t budget for this feature.
    func emptyView(for listAdapter: ListAdapter) -> UIView? {
        let view = UIView()
        view.backgroundColor = UIColor.white
        return view
    }
}

extension NewCommentsViewController {
    func sendMessage(_ message: Comments) {
        ChatService.sendMessage(message, eventKey: eventKey)

    }
}

I did some debugging using both instruments and the debugger graph tool and it seems to be pointing me to my commentsSectionController

import UIKit
import IGListKit
import Foundation
import Firebase

protocol CommentsSectionDelegate: class {
    func CommentSectionUpdared(sectionController: CommentsSectionController)
}
class CommentsSectionController: ListSectionController,CommentCellDelegate {
    weak var delegate: CommentsSectionDelegate? = nil
    var comment: CommentGrabbed?
    let userProfileController = ProfileeViewController(collectionViewLayout: UICollectionViewFlowLayout())
    var eventKey: String?
    var dummyCell: CommentCell?
    override init() {
        super.init()
        // supplementaryViewSource = self
        //sets the spacing between items in a specfic section controller
        inset = UIEdgeInsets(top: 5, left: 0, bottom: 0, right: 0)
    }
    // MARK: IGListSectionController Overrides
    override func numberOfItems() -> Int {
        return 1
    }

specifically this function here

    override func sizeForItem(at index: Int) -> CGSize {
        let frame = CGRect(x: 0, y: 0, width: collectionContext!.containerSize.width, height: 50)
        dummyCell = CommentCell(frame: frame)
        dummyCell?.comment = comment
        dummyCell?.layoutIfNeeded()
        let targetSize =  CGSize(width: collectionContext!.containerSize.width, height: 55)
        let estimatedSize = dummyCell?.systemLayoutSizeFitting(targetSize)
        let height = max(40+8+8, (estimatedSize?.height)!)
        return  CGSize(width: collectionContext!.containerSize.width, height: height)

    }

    override var minimumLineSpacing: CGFloat {
        get {
            return 0.0
        }
        set {
            self.minimumLineSpacing = 0.0
        }
    }

    override func cellForItem(at index: Int) -> UICollectionViewCell {
        guard let cell = collectionContext?.dequeueReusableCell(of: CommentCell.self, for: self, at: index) as? CommentCell else {
            fatalError()
        }
        //  print(comment)
        cell.comment = comment
        cell.delegate = self
        return cell
    }
    override func didUpdate(to object: Any) {
        comment = object as? CommentGrabbed
    }
    override func didSelectItem(at index: Int){
    }

    func optionsButtonTapped(cell: CommentCell){
        print("like")

        let comment = self.comment
        _ = comment?.uid

        // 3
        let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)

        // 4
        if comment?.uid != User.current.uid {
            let flagAction = UIAlertAction(title: "Report as Inappropriate", style: .default) { _ in
                ChatService.flag(comment!)

                let okAlert = UIAlertController(title: nil, message: "The post has been flagged.", preferredStyle: .alert)
                okAlert.addAction(UIAlertAction(title: "Ok", style: .default))
                self.viewController?.present(okAlert, animated: true, completion: nil)
            }
            let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
            let replyAction = UIAlertAction(title: "Reply to Comment", style: .default, handler: { (_) in
                //do something here later to facilitate reply comment functionality
                print("Attempting to reply to user \(comment?.user.username) comment")

            })
            alertController.addAction(replyAction)
            alertController.addAction(cancelAction)
            alertController.addAction(flagAction)
        }else{
            let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
            let deleteAction = UIAlertAction(title: "Delete Comment", style: .default, handler: { _ in
                ChatService.deleteComment(comment!, (comment?.eventKey)!)
                let okAlert = UIAlertController(title: nil, message: "Comment Has Been Deleted", preferredStyle: .alert)
                okAlert.addAction(UIAlertAction(title: "Ok", style: .default))
                self.viewController?.present(okAlert, animated: true, completion: nil)
                self.onItemDeleted()

            })
            alertController.addAction(cancelAction)
            alertController.addAction(deleteAction)

        }
        self.viewController?.present(alertController, animated: true, completion: nil)

    }
    func onItemDeleted() {
        delegate?.CommentSectionUpdared(sectionController: self)
    }
    func handleProfileTransition(tapGesture: UITapGestureRecognizer){
        userProfileController.user = comment?.user
        if Auth.auth().currentUser?.uid != comment?.uid{
                    self.viewController?.present(userProfileController, animated: true, completion: nil)
        }else{
            //do nothing

        }


    }

    deinit {
        print("CommentSectionController class removed from memory")
    }


}

Here is a screenshot is what I saw in the debugger graph tool even when i leave the screen and check the debugger tool those blocks are still there.

enter image description here

So my question is does anyone see anything I don't see with the function. I really want to fix this memory leak. In addition to that this memory leak doesn't seem to be evident when I use my phone but when I use my simulator it is a huge memory leak....Any insight is greatly appreciated

Upvotes: 1

Views: 3515

Answers (4)

totocaster
totocaster

Reputation: 6363

Couple of things:

  1. DatabaseReference's query observer that is owned by your ViewController is capturing self (your view controller). This makes a circle of ownerships which never let each other to deinit. This is called retain cycle.

Add [unowned self] or [weak self] to your completion block like so:

query?.observe(.value, with: { [weak self] (snapshot) in
   // your code...
}

I used weak here because query is optional.

  1. Remove notification observers when you no longer need them by calling NotificationCenter.default.removeObserver(self).

Shameless plug of my library: Consider Typist for managing keyboard in UIKit, it avoids any interaction with NotificationCenter and is super easy to setup and use.

Upvotes: 1

pacification
pacification

Reputation: 6018

I believe you should rewrite this closure with [unowned self] (or weak):

UserService.show(forUID: uid, completion: { [unowned self] user in }

And as @Dhiru mentioned, you should remove all notification observers.

Upvotes: 0

SPatel
SPatel

Reputation: 4946

You should use weak or unowned reference in closure, Like below

query?.observe(.value, with: { [unowned self] (snapshot) in

}

Upvotes: 0

Dhiru
Dhiru

Reputation: 3060

Remove your notification Observer in viewWillDisappear method

override func viewWillDisappear(animated: Bool) {
    NSNotificationCenter.defaultCenter().removeObserver(self)
}

Upvotes: 0

Related Questions