IGListKitSections doesn't get deallocated

196 Views Asked by At

I have a problem with IGListKit sections deallocating. Trying to debug the issue with Xcode memory graph.

My setup is AuthController -> AuthViewModel -> AuthSocialSectionController -> AuthSocialViewModel and some other sections.

AuthController gets presented from several parts of the app if user is not logged in. When I tap close, AuthViewModel and AuthController gets deallocated, but it's underlying sections does not. Memory graph shows nothing leaked in this case, but deinit methods doesn't get called.

But when I'm trying to authorize with social account (successfully) and then look at the memory graph, it shows that sections, that doesn't get deallocated like this:

Memory graph Memory graph Memory graph

In this case AuthViewModel doesn't get deallocated either, but after some time it does, but it can happen or not.

I checked every closure and delegate for weak reference, but still no luck.

My code, that I think makes most sense:

class AuthViewController: UIViewController {
 fileprivate let collectionView: UICollectionView = UICollectionView(frame: .zero,
                                                                     collectionViewLayout: UICollectionViewFlowLayout())
 lazy var adapter: ListAdapter
  = ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)

 fileprivate lazy var previewProxy: SJListPreviewProxy = {
  SJListPreviewProxy(adapter: adapter)
 }()

 fileprivate let viewModel: AuthViewModel

 fileprivate let disposeBag = DisposeBag()

 init(with viewModel: AuthViewModel) {
  self.viewModel = viewModel

  super.init(nibName: nil, bundle: nil)

  hidesBottomBarWhenPushed = true
  setupObservers()
 }

 private func setupObservers() {
  NotificationCenter.default.rx.notification(.SJAProfileDidAutoLogin)
   .subscribe(
    onNext: { [weak self] _ in
     self?.viewModel.didSuccessConfirmationEmail()
     self?.viewModel.recoverPasswordSuccess()
   })
   .disposed(by: disposeBag)
 }

 required init?(coder _: NSCoder) {
  fatalError("init(coder:) has not been implemented")
 }

 // MARK: - View Controller Lifecycle

 override func viewDidLoad() {
  super.viewDidLoad()
  setup()
 }

 // MARK: - Private

 @objc private func close() {
  dismiss(animated: true, completion: nil)
 }

 /// Метод настройки экрана
 private func setup() {
  if isForceTouchEnabled() {
   registerForPreviewing(with: previewProxy, sourceView: collectionView)
  }

  view.backgroundColor = AppColor.instance.gray
  title = viewModel.screenName
  let item = UIBarButtonItem(image: #imageLiteral(resourceName: "close.pdf"), style: .plain, target: self, action: #selector(AuthViewController.close))
  item.accessibilityIdentifier = "auth_close_btn"
  asViewController.navigationItem.leftBarButtonItem = item
  navigationItem.titleView = UIImageView(image: #imageLiteral(resourceName: "logo_superjob.pdf"))

  collectionViewSetup()
 }

 // Настройка collectionView
 private func collectionViewSetup() {
  collectionView.keyboardDismissMode = .onDrag
  collectionView.backgroundColor = AppColor.instance.gray
  view.addSubview(collectionView)
  adapter.collectionView = collectionView
  adapter.dataSource = self
  collectionView.snp.remakeConstraints { make in
   make.edges.equalToSuperview()
  }
 }
}

// MARK: - DataSource CollectionView

extension AuthViewController: ListAdapterDataSource {

 func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
  return viewModel.sections(for: listAdapter)
 }

 func listAdapter(_: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
  return viewModel.createListSectionController(for: object)
 }

 func emptyView(for _: ListAdapter) -> UIView? {
  return nil
 }
}

// MARK: - AuthViewModelDelegate

extension AuthViewController: AuthViewModelDelegate {
 func hideAuth(authSuccessBlock: AuthSuccessAction?) {
  dismiss(animated: true, completion: {
   authSuccessBlock?()
  })
 }

 func reload(animated: Bool, completion: ((Bool) -> Void)? = nil) {
  adapter.performUpdates(animated: animated, completion: completion)
 }

 func showErrorPopover(with item: CommonAlertPopoverController.Item,
                       and anchors: (sourceView: UIView, sourceRect: CGRect)) {
  let popover = CommonAlertPopoverController(with: item,
                                             preferredContentWidth: view.size.width - 32.0,
                                             sourceView: anchors.sourceView,
                                             sourceRect: anchors.sourceRect,
                                             arrowDirection: .up)
  present(popover, animated: true, completion: nil)
 }
}

class AuthViewModel {

 fileprivate let assembler: AuthSectionsAssembler

 fileprivate let router: AuthRouter

 fileprivate let profileFacade: SJAProfileFacade

 fileprivate let api3ProfileFacade: API3ProfileFacade

 fileprivate let analytics: AnalyticsProtocol

 fileprivate var sections: [Section] = []

 weak var authDelegate: AuthDelegate?
 weak var vmDelegate: AuthViewModelDelegate?
  
  var authSuccessBlock: AuthSuccessAction?
  
  private lazy var socialSection: AuthSocialSectionViewModel = { [unowned self] in
  self.assembler.socialSection(delegate: self)
 }()

  init(assembler: AuthSectionsAssembler,
      router: AuthRouter,
      profileFacade: SJAProfileFacade,
      api3ProfileFacade: API3ProfileFacade,
      analytics: AnalyticsProtocol,
      delegate: AuthDelegate? = nil,
      purpose: Purpose) {
  self.purpose = purpose
  authDelegate = delegate
  self.assembler = assembler
  self.router = router
  self.profileFacade = profileFacade
  self.api3ProfileFacade = api3ProfileFacade
  self.analytics = analytics
  sections = displaySections()
 }
  
  private func authDisplaySections() -> [Section] {
  let sections: [Section?] = [vacancySection,
                              authHeaderSection,
                              socialSection,
                              authLoginPasswordSection,
                              signInButtonSection,
                              switchToSignUpButtonSection,
                              recoverPasswordSection]
  return sections.compactMap { $0 }
 }
}

class AuthSocialSectionController: SJListSectionController, SJUpdateCellsLayoutProtocol {
 fileprivate let viewModel: AuthSocialSectionViewModel

 init(viewModel: AuthSocialSectionViewModel) {
  self.viewModel = viewModel
  super.init()
  minimumInteritemSpacing = 4
  viewModel.vmDelegate = self
 }

 override func cellType(at _: Int) -> UICollectionViewCell.Type {
  return AuthSocialCell.self
 }

 override func cellInitializationType(at _: Int) -> SJListSectionCellInitializationType {
  return .code
 }

 override func configureCell(_ cell: UICollectionViewCell, at index: Int) {
  guard let itemCell = cell as? AuthSocialCell else {
   return
  }
  let item = viewModel.item(at: index)
  itemCell.imageView.image = item.image
 }

 override func separationStyle(at _: Int) -> SJCollectionViewCellSeparatorStyle {
  return .none
 }
}

extension AuthSocialSectionController {
 override func numberOfItems() -> Int {
  return viewModel.numberOfItems
 }
  
 override func didSelectItem(at index: Int) {
  viewModel.didSelectItem(at: index)
 }

}

// MARK: - AuthSocialSectionViewModelDelegate

extension AuthSocialSectionController: AuthSocialSectionViewModelDelegate {
 func sourceViewController() -> UIViewController {
  return viewController ?? UIViewController()
 }
}

protocol AuthSocialSectionDelegate: class {

 func successfullyAuthorized(type: SJASocialAuthorizationType)

 func showError(with error: Error)
}

protocol AuthSocialSectionViewModelDelegate: SJListSectionControllerOperationsProtocol, ViewControllerProtocol {
 func sourceViewController() -> UIViewController
}

class AuthSocialSectionViewModel: NSObject {
 struct Item {
  let image: UIImage
  let type: SJASocialAuthorizationType
 }

 weak var delegate: AuthSocialSectionDelegate?
 weak var vmDelegate: AuthSocialSectionViewModelDelegate?

 fileprivate var items: [Item]

 fileprivate let api3ProfileFacade: API3ProfileFacade
 fileprivate let analyticsFacade: SJAAnalyticsFacade
 fileprivate var socialButtonsDisposeBag = DisposeBag()

 init(api3ProfileFacade: API3ProfileFacade,
      analyticsFacade: SJAAnalyticsFacade) {
  self.api3ProfileFacade = api3ProfileFacade
  self.analyticsFacade = analyticsFacade
  items = [
   Item(image: #imageLiteral(resourceName: "ok_icon.pdf"), type: .OK),
   Item(image: #imageLiteral(resourceName: "vk_icon.pdf"), type: .VK),
   Item(image: #imageLiteral(resourceName: "facebook_icon.pdf"), type: .facebook),
   Item(image: #imageLiteral(resourceName: "mail_icon.pdf"), type: .mail),
   Item(image: #imageLiteral(resourceName: "google_icon.pdf"), type: .google),
   Item(image: #imageLiteral(resourceName: "yandex_icon.pdf"), type: .yandex)
  ]

  if analyticsFacade.isHHAuthAvailable() {
   items.append(Item(image: #imageLiteral(resourceName: "hh_icon"), type: .HH))
  }
 }

 // MARK: - actions

 func didSelectItem(at index: Int) {
  guard let vc = vmDelegate?.sourceViewController() else {
   return
  }

  let itemType: SJASocialAuthorizationType = items[index].type

  socialButtonsDisposeBag = DisposeBag()
  
  api3ProfileFacade.authorize(with: itemType, sourceViewController: vc)
   .subscribe(
    onNext: { [weak self] _ in
     self?.delegate?.successfullyAuthorized(type: itemType)
    },
    onError: { [weak self] error in
     if case let .detailed(errorModel)? = error as? ApplicantError {
      self?.vmDelegate?.asViewController.showError(with: errorModel.errors.first?.detail ?? "")
     } else {
      self?.vmDelegate?.asViewController.showError(with: "Неизвестная ошибка")
     }
   })
   .disposed(by: socialButtonsDisposeBag)
 }
}

// MARK: - DataSource

extension AuthSocialSectionViewModel {
 var numberOfItems: Int {
  return items.count
 }

 func item(at index: Int) -> Item {
  return items[index]
 }
}

// MARK: - ListDiffable

extension AuthSocialSectionViewModel: ListDiffable {
 func diffIdentifier() -> NSObjectProtocol {
  return ObjectIdentifier(self).hashValue as NSObjectProtocol
 }

 func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
  return object is AuthSocialSectionViewModel
 }
}

Where assembler is responsible for creating everyting, for example AuthSocialSection:

func socialSection(delegate: AuthSocialSectionDelegate?) -> AuthSocialSectionViewModel {
  let vm = AuthSocialSectionViewModel(api3ProfileFacade: api3ProfileFacade,
                                      analyticsFacade: analyticsFacade)
  vm.delegate = delegate
  return vm
 }

How can I properly debug this issue? Any advice or help is really appreciated

2

There are 2 best solutions below

0
Max Kraev On BEST ANSWER

Found an issue in AuthSocialSectionController. Somehow passing viewController from IGList context through delegates caused memory issues. When I commented out the viewModel.vmDelegate = self the issue was gone.

That explains why the AuthViewModel was deallocating properly when I hit close button without attempting to authorize. Only when I hit authorize, that viewController property was called.

Thanks for help @vpoltave

1
vpoltave On

This lines from your AuthViewController can this cause leaks?

// adapter has viewController: self
lazy var adapter: ListAdapter
        = ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)

fileprivate lazy var previewProxy: SJListPreviewProxy = {
    // capture self.adapter ?
    SJListPreviewProxy(adapter: adapter)
}()

I'm not sure, but at least you can try :)


UPDATE

I was wondering about this lazy closures and self inside, it won't create retain cycle because lazy initialization are @nonescaping.