import UIKit
import Common
final class TokensViewController: UIViewController {
var presenter: TokensPresenter!
var addButton: UIBarButtonItem? {
navigationItem.rightBarButtonItem
}
private(set) var gridView: GridView!
private(set) var gridLayout: UICollectionViewFlowLayout!
private(set) var dataSource: UICollectionViewDiffableDataSource<GridSection, GridCell>!
let headerHeight: CGFloat = 50
let emptySearchScreenView = GridViewEmptySearchScreen()
let emptyListScreenView = GridViewEmptyListScreen()
private var configuredWidth: CGFloat = 0
var searchBarAdded = false
let searchController = CommonSearchController()
override func loadView() {
gridLayout = UICollectionViewFlowLayout()
gridView = GridView(frame: .zero, collectionViewLayout: gridLayout)
self.view = gridView
gridView.configure()
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupEmptyScreensLayout()
setupEmptyScreensEvents()
setupDelegates()
setupDataSource()
setupDragAndDrop()
setupNotificationsListeners()
}
// MARK: - App events
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
configureLayout()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
presenter.viewWillAppear()
startSafeAreaKeyboardAdjustment()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopSafeAreaKeyboardAdjustment()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
private extension TokensViewController {
func setupView() {
extendedLayoutIncludesOpaqueBars = true
view.backgroundColor = Theme.Colors.Fill.background
title = T.Commons.tokens
accessibilityTraits = .header
}
func setupDelegates() {
searchController.searchBarDelegate = self
gridView.delegate = self
}
func setupDataSource() {
dataSource = UICollectionViewDiffableDataSource(
collectionView: gridView,
cellProvider: { collectionView, indexPath, item in
if item.cellType == .serviceTOTP {
if collectionView.isEditing {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewEditItemCell.reuseIdentifier,
for: indexPath
) as? GridViewEditItemCell
cell?.update(
name: item.name,
additionalInfo: item.additionalInfo,
serviceTypeName: item.serviceTypeName,
iconType: item.iconType,
category: item.category,
canBeDragged: item.canBeDragged
)
return cell
} else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewItemCell.reuseIdentifier,
for: indexPath
) as? GridViewItemCell
cell?.update(
name: item.name,
secret: item.secret,
serviceTypeName: item.serviceTypeName,
additionalInfo: item.additionalInfo,
iconType: item.iconType,
category: item.category,
useNextToken: item.useNextToken
)
return cell
}
} else if item.cellType == .serviceHOTP {
if collectionView.isEditing {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewEditItemCell.reuseIdentifier,
for: indexPath
) as? GridViewEditItemCell
cell?.update(
name: item.name,
additionalInfo: item.additionalInfo,
serviceTypeName: item.serviceTypeName,
iconType: item.iconType,
category: item.category,
canBeDragged: item.canBeDragged
)
return cell
} else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewCounterItemCell.reuseIdentifier,
for: indexPath
) as? GridViewCounterItemCell
cell?.update(
name: item.name,
secret: item.secret,
serviceTypeName: item.serviceTypeName,
additionalInfo: item.additionalInfo,
iconType: item.iconType,
category: item.category
)
return cell
}
}
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridEmptyCollectionViewCell.reuseIdentifier,
for: indexPath
) as? GridEmptyCollectionViewCell
return cell
})
dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath
-> UICollectionReusableView? in
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: GridSectionHeader.reuseIdentifier,
for: indexPath
) as? GridSectionHeader
header?.setIsEditing(collectionView.isEditing)
header?.dataSource = self
if let data = self?.dataSource.snapshot().sectionIdentifiers[indexPath.section] {
header?.setConfiguration(data)
}
return header
}
}
func setupDragAndDrop() {
gridView.dragDelegate = self
gridView.dropDelegate = self
gridView.dragInteractionEnabled = presenter.enableDragAndDropOnStart
}
func setupEmptyScreensLayout() {
view.addSubview(emptySearchScreenView, with: [
emptySearchScreenView.leadingAnchor.constraint(equalTo: gridView.frameLayoutGuide.leadingAnchor),
emptySearchScreenView.trailingAnchor.constraint(equalTo: gridView.frameLayoutGuide.trailingAnchor),
emptySearchScreenView.topAnchor.constraint(equalTo: gridView.frameLayoutGuide.topAnchor),
emptySearchScreenView.bottomAnchor.constraint(equalTo: gridView.frameLayoutGuide.bottomAnchor)
])
emptySearchScreenView.isHidden = true
emptySearchScreenView.alpha = 0
view.addSubview(emptyListScreenView, with: [
emptyListScreenView.leadingAnchor.constraint(equalTo: gridView.frameLayoutGuide.leadingAnchor),
emptyListScreenView.trailingAnchor.constraint(equalTo: gridView.frameLayoutGuide.trailingAnchor),
emptyListScreenView.topAnchor.constraint(equalTo: gridView.safeTopAnchor),
emptyListScreenView.bottomAnchor.constraint(equalTo: gridView.safeBottomAnchor)
])
emptyListScreenView.isHidden = true
emptyListScreenView.alpha = 0
}
func configureLayout() {
guard let screenWidth = UIApplication.keyWindow?.bounds.size.width,
configuredWidth != screenWidth else { return }
configuredWidth = screenWidth
let cellHeight = Theme.Metrics.servicesCellHeight
let minimumCellWidth: CGFloat = Theme.Metrics.pageWidth
let itemsInRow = Int(screenWidth / minimumCellWidth)
let margin: CGFloat = 0
let marginsWidth = margin * CGFloat(itemsInRow - 1)
let screenWidthWithoutMargins = screenWidth - marginsWidth
let elementWidth = floor(screenWidthWithoutMargins / CGFloat(itemsInRow))
gridLayout.itemSize = CGSize(width: elementWidth, height: cellHeight)
gridLayout.minimumInteritemSpacing = margin
gridLayout.headerReferenceSize = CGSize(width: 100, height: headerHeight)
gridLayout.minimumLineSpacing = 0
}
func setupEmptyScreensEvents() {
emptyListScreenView.pairNewService = { [weak self] in self?.presenter.handleShowCamera() }
emptyListScreenView.import2FAS = { [weak self] in
AnalyticsLog(.onboardingBackupFile)
self?.presenter.handleImport2FAS()
}
emptyListScreenView.importGA = { [weak self] in
AnalyticsLog(.onboardingGA)
self?.presenter.handleImportGA()
}
emptyListScreenView.help = { [weak self] in self?.presenter.handleShowHelp() }
}
func setupNotificationsListeners() {
let center = NotificationCenter.default
center.addObserver(
self, selector: #selector(notificationServicesWereUpdated), name: .servicesWereUpdated, object: nil
)
center.addObserver(
self, selector: #selector(notificationSectionsWereUpdated), name: .sectionsWereUpdated, object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeInactive),
name: UIApplication.willResignActiveNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeInactive),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
}
}
import UIKit
import Common
final class TokensViewController: UIViewController {
var presenter: TokensPresenter!
var addButton: UIBarButtonItem? {
navigationItem.rightBarButtonItem
}
private(set) var gridView: GridView!
private(set) var gridLayout: UICollectionViewFlowLayout!
private(set) var dataSource: UICollectionViewDiffableDataSource<GridSection, GridCell>!
let headerHeight: CGFloat = 50
let emptySearchScreenView = GridViewEmptySearchScreen()
let emptyListScreenView = GridViewEmptyListScreen()
private var configuredWidth: CGFloat = 0
var searchBarAdded = false
let searchController = CommonSearchController()
override func loadView() {
gridLayout = UICollectionViewFlowLayout()
gridView = GridView(frame: .zero, collectionViewLayout: gridLayout)
self.view = gridView
gridView.configure()
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupEmptyScreensLayout()
setupEmptyScreensEvents()
setupDelegates()
setupDataSource()
setupDragAndDrop()
setupNotificationsListeners()
}
// MARK: - App events
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
configureLayout()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
presenter.viewWillAppear()
startSafeAreaKeyboardAdjustment()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopSafeAreaKeyboardAdjustment()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
private extension TokensViewController {
func setupView() {
extendedLayoutIncludesOpaqueBars = true
view.backgroundColor = Theme.Colors.Fill.background
title = T.Commons.tokens
accessibilityTraits = .header
}
func setupDelegates() {
searchController.searchBarDelegate = self
gridView.delegate = self
}
func setupDataSource() {
dataSource = UICollectionViewDiffableDataSource(
collectionView: gridView,
cellProvider: { collectionView, indexPath, item in
if item.cellType == .serviceTOTP {
if collectionView.isEditing {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewEditItemCell.reuseIdentifier,
for: indexPath
) as? GridViewEditItemCell
cell?.update(
name: item.name,
additionalInfo: item.additionalInfo,
serviceTypeName: item.serviceTypeName,
iconType: item.iconType,
category: item.category,
canBeDragged: item.canBeDragged
)
return cell
} else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewItemCell.reuseIdentifier,
for: indexPath
) as? GridViewItemCell
cell?.update(
name: item.name,
secret: item.secret,
serviceTypeName: item.serviceTypeName,
additionalInfo: item.additionalInfo,
iconType: item.iconType,
category: item.category,
useNextToken: item.useNextToken
)
return cell
}
} else if item.cellType == .serviceHOTP {
if collectionView.isEditing {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewEditItemCell.reuseIdentifier,
for: indexPath
) as? GridViewEditItemCell
cell?.update(
name: item.name,
additionalInfo: item.additionalInfo,
serviceTypeName: item.serviceTypeName,
iconType: item.iconType,
category: item.category,
canBeDragged: item.canBeDragged
)
return cell
} else {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridViewCounterItemCell.reuseIdentifier,
for: indexPath
) as? GridViewCounterItemCell
cell?.update(
name: item.name,
secret: item.secret,
serviceTypeName: item.serviceTypeName,
additionalInfo: item.additionalInfo,
iconType: item.iconType,
category: item.category
)
return cell
}
}
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: GridEmptyCollectionViewCell.reuseIdentifier,
for: indexPath
) as? GridEmptyCollectionViewCell
return cell
})
dataSource.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath
-> UICollectionReusableView? in
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: GridSectionHeader.reuseIdentifier,
for: indexPath
) as? GridSectionHeader
header?.setIsEditing(collectionView.isEditing)
header?.dataSource = self
if let data = self?.dataSource.snapshot().sectionIdentifiers[indexPath.section] {
header?.setConfiguration(data)
}
return header
}
}
func setupDragAndDrop() {
gridView.dragDelegate = self
gridView.dropDelegate = self
gridView.dragInteractionEnabled = presenter.enableDragAndDropOnStart
}
func setupEmptyScreensLayout() {
view.addSubview(emptySearchScreenView, with: [
emptySearchScreenView.leadingAnchor.constraint(equalTo: gridView.frameLayoutGuide.leadingAnchor),
emptySearchScreenView.trailingAnchor.constraint(equalTo: gridView.frameLayoutGuide.trailingAnchor),
emptySearchScreenView.topAnchor.constraint(equalTo: gridView.frameLayoutGuide.topAnchor),
emptySearchScreenView.bottomAnchor.constraint(equalTo: gridView.frameLayoutGuide.bottomAnchor)
])
emptySearchScreenView.isHidden = true
emptySearchScreenView.alpha = 0
view.addSubview(emptyListScreenView, with: [
emptyListScreenView.leadingAnchor.constraint(equalTo: gridView.frameLayoutGuide.leadingAnchor),
emptyListScreenView.trailingAnchor.constraint(equalTo: gridView.frameLayoutGuide.trailingAnchor),
emptyListScreenView.topAnchor.constraint(equalTo: gridView.safeTopAnchor),
emptyListScreenView.bottomAnchor.constraint(equalTo: gridView.safeBottomAnchor)
])
emptyListScreenView.isHidden = true
emptyListScreenView.alpha = 0
}
func configureLayout() {
guard let screenWidth = UIApplication.keyWindow?.bounds.size.width,
configuredWidth != screenWidth else { return }
configuredWidth = screenWidth
let cellHeight = Theme.Metrics.servicesCellHeight
let minimumCellWidth: CGFloat = Theme.Metrics.pageWidth
let itemsInRow = Int(screenWidth / minimumCellWidth)
let margin: CGFloat = 0
let marginsWidth = margin * CGFloat(itemsInRow - 1)
let screenWidthWithoutMargins = screenWidth - marginsWidth
let elementWidth = floor(screenWidthWithoutMargins / CGFloat(itemsInRow))
gridLayout.itemSize = CGSize(width: elementWidth, height: cellHeight)
gridLayout.minimumInteritemSpacing = margin
gridLayout.headerReferenceSize = CGSize(width: 100, height: headerHeight)
gridLayout.minimumLineSpacing = 0
}
func setupEmptyScreensEvents() {
emptyListScreenView.pairNewService = { [weak self] in self?.presenter.handleShowCamera() }
emptyListScreenView.import2FAS = { [weak self] in
AnalyticsLog(.onboardingBackupFile)
self?.presenter.handleImport2FAS()
}
emptyListScreenView.importGA = { [weak self] in
AnalyticsLog(.onboardingGA)
self?.presenter.handleImportGA()
}
emptyListScreenView.help = { [weak self] in self?.presenter.handleShowHelp() }
}
func setupNotificationsListeners() {
let center = NotificationCenter.default
center.addObserver(
self, selector: #selector(notificationServicesWereUpdated), name: .servicesWereUpdated, object: nil
)
center.addObserver(
self, selector: #selector(notificationSectionsWereUpdated), name: .sectionsWereUpdated, object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeInactive),
name: UIApplication.willResignActiveNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(notificationAppDidBecomeInactive),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
}
}
years of careful development and continuous improvements
average rating score in Google Play and AppStore
million downloads worldwide















The project is developed with the support of an awesome group of contributors that regularly improve the 2FAS app codebase, content of our application and build the 2FAS community.
We are giving the world the 2FAS apps completely free. If you believe in keeping the Internet safe, open and secure for everyone, we would greatly value your contribution!
We are working 100% remotely, but if you're interested in cooperation, let's have an online call or meet for coffee in Las Vegas!
Two Factor Authentication Service, Inc.
1887 Whitney Mesa Dr #2130
Henderson, Nevada 89014

