swiftrx-swiftiglistkit

IGListKitSections doesn't get deallocated


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


Solution

  • 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