import Foundation import Display import AsyncDisplayKit private func loadCountryNamesAndCodes() -> [(String, String, Int)] { guard let filePath = frameworkBundle.path(forResource: "PhoneCountries", ofType: "txt") else { return [] } guard let stringData = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { return [] } guard let data = String(data: stringData, encoding: .utf8) else { return [] } let delimiter = ";" let endOfLine = "\n" var result: [(String, String, Int)] = [] var currentLocation = data.startIndex while true { guard let codeRange = data.range(of: delimiter, options: [], range: currentLocation ..< data.endIndex) else { break } let countryCode = data.substring(with: currentLocation ..< codeRange.lowerBound) guard let idRange = data.range(of: delimiter, options: [], range: codeRange.upperBound ..< data.endIndex) else { break } let countryId = data.substring(with: codeRange.upperBound ..< idRange.lowerBound) let maybeNameRange = data.range(of: endOfLine, options: [], range: idRange.upperBound ..< data.endIndex) let nameRangeIndex = maybeNameRange?.lowerBound ?? data.endIndex var countryName = data.substring(with: idRange.upperBound ..< nameRangeIndex) if countryName.hasSuffix("\r") { countryName = countryName.substring(to: countryName.index(before: countryName.endIndex)) } if let countryCodeInt = Int(countryCode) { result.append((countryName, countryId, countryCodeInt)) } if let maybeNameRange = maybeNameRange { currentLocation = maybeNameRange.upperBound } else { break } } return result } private let countryNamesAndCodes: [(String, String, Int)] = loadCountryNamesAndCodes() private final class InnerCoutrySearchResultsController: UIViewController, UITableViewDelegate, UITableViewDataSource { private let displayCodes: Bool private let theme: AuthorizationTheme private let tableView: UITableView var searchResults: [(String, String, Int)] = [] { didSet { self.tableView.reloadData() } } var itemSelected: (((String, String, Int)) -> Void)? init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool) { self.displayCodes = displayCodes self.theme = theme self.tableView = UITableView(frame: CGRect(), style: .plain) super.init(nibName: nil, bundle: nil) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .white self.view.addSubview(self.tableView) self.tableView.frame = self.view.bounds self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.tableView.dataSource = self self.tableView.delegate = self self.tableView.backgroundColor = self.theme.backgroundColor self.tableView.separatorColor = self.theme.separatorColor self.tableView.backgroundView = UIView() } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.searchResults.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: UITableViewCell if let currentCell = tableView.dequeueReusableCell(withIdentifier: "CountryCell") { cell = currentCell } else { cell = UITableViewCell() let label = UILabel() label.font = Font.medium(17.0) cell.accessoryView = label } cell.textLabel?.text = self.searchResults[indexPath.row].0 if self.displayCodes, let label = cell.accessoryView as? UILabel { label.text = "+\(self.searchResults[indexPath.row].2)" label.sizeToFit() label.textColor = self.theme.primaryColor } cell.textLabel?.textColor = self.theme.primaryColor cell.backgroundColor = self.theme.backgroundColor cell.selectedBackgroundView?.backgroundColor = self.theme.itemHighlightedBackgroundColor return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { self.itemSelected?(self.searchResults[indexPath.row]) } } private final class InnerCountrySelectionController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchResultsUpdating, UISearchBarDelegate { private let strings: PresentationStrings private let theme: AuthorizationTheme private let displayCodes: Bool private let tableView: UITableView private let sections: [(String, [(String, String, Int)])] private let sectionTitles: [String] private var searchController: UISearchController! private var searchResultsController: InnerCoutrySearchResultsController! var dismiss: (() -> Void)? var itemSelected: (((String, String, Int)) -> Void)? init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool) { self.strings = strings self.theme = theme self.displayCodes = displayCodes self.tableView = UITableView(frame: CGRect(), style: .plain) var sections: [(String, [(String, String, Int)])] = [] for (name, id, code) in countryNamesAndCodes.sorted(by: { lhs, rhs in return lhs.0 < rhs.0 }) { let title = name.substring(to: name.index(after: name.startIndex)).uppercased() if sections.isEmpty || sections[sections.count - 1].0 != title { sections.append((title, [])) } sections[sections.count - 1].1.append((name, id, code)) } self.sections = sections var sectionTitles = sections.map { $0.0 } sectionTitles.insert(UITableViewIndexSearch, at: 0) self.sectionTitles = sectionTitles super.init(nibName: nil, bundle: nil) self.title = strings.Login_SelectCountry_Title self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) self.definesPresentationContext = true } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .white self.searchResultsController = InnerCoutrySearchResultsController(strings: self.strings, theme: self.theme, displayCodes: self.displayCodes) self.searchResultsController.itemSelected = { [weak self] item in self?.itemSelected?(item) } self.searchController = UISearchController(searchResultsController: self.searchResultsController) self.searchController.searchResultsUpdater = self self.searchController.dimsBackgroundDuringPresentation = false self.searchController.searchBar.delegate = self self.searchController.searchBar.keyboardAppearance = self.theme.keyboardAppearance self.searchController.hidesNavigationBarDuringPresentation = true self.view.addSubview(self.tableView) self.tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.tableView.tableHeaderView = self.searchController.searchBar self.tableView.dataSource = self self.tableView.delegate = self self.tableView.sectionIndexColor = self.theme.accentColor self.tableView.backgroundColor = self.theme.backgroundColor self.tableView.separatorColor = self.theme.separatorColor self.tableView.backgroundView = UIView() self.tableView.frame = self.view.bounds self.view.addSubview(self.tableView) self.searchController.searchBar.barTintColor = self.theme.searchBarBackgroundColor self.searchController.searchBar.tintColor = self.theme.accentColor self.searchController.searchBar.backgroundColor = self.theme.searchBarBackgroundColor self.searchController.searchBar.setTextColor(theme.searchBarTextColor) let searchImage = generateImage(CGSize(width: 8.0, height: 28.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(self.theme.searchBarFillColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))) context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - size.width), size: CGSize(width: size.width, height: size.width))) context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.width / 2.0), size: CGSize(width: size.width, height: size.height - size.width))) }) self.searchController.searchBar.setSearchFieldBackgroundImage(searchImage, for: []) self.searchController.searchBar.backgroundImage = UIImage() } override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() if #available(iOSApplicationExtension 11.0, *) { var frame = self.searchController.view.frame frame.origin.y = 12.0 self.searchController.view.frame = frame } } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() } func numberOfSections(in tableView: UITableView) -> Int { return self.sections.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.sections[section].1.count } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return self.sections[section].0 } func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { (view as? UITableViewHeaderFooterView)?.backgroundView?.backgroundColor = self.theme.backgroundColor (view as? UITableViewHeaderFooterView)?.textLabel?.textColor = self.theme.primaryColor } func sectionIndexTitles(for tableView: UITableView) -> [String]? { return self.sectionTitles } func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { if index == 0 { return 0 } else { return max(0, index - 1) } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: UITableViewCell if let currentCell = tableView.dequeueReusableCell(withIdentifier: "CountryCell") { cell = currentCell } else { cell = UITableViewCell() let label = UILabel() label.font = Font.medium(17.0) cell.accessoryView = label } cell.textLabel?.text = self.sections[indexPath.section].1[indexPath.row].0 if self.displayCodes, let label = cell.accessoryView as? UILabel { label.text = "+\(self.sections[indexPath.section].1[indexPath.row].2)" label.sizeToFit() label.textColor = self.theme.primaryColor } cell.textLabel?.textColor = self.theme.primaryColor cell.backgroundColor = self.theme.backgroundColor cell.selectedBackgroundView?.backgroundColor = self.theme.itemHighlightedBackgroundColor return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { self.itemSelected?(self.sections[indexPath.section].1[indexPath.row]) } func updateSearchResults(for searchController: UISearchController) { guard let normalizedQuery = searchController.searchBar.text?.lowercased() else { self.searchResultsController.searchResults = [] return } var results: [(String, String, Int)] = [] for (_, items) in self.sections { for item in items { if item.0.lowercased().hasPrefix(normalizedQuery) { results.append(item) } } } self.searchResultsController.searchResults = results } @objc func cancelPressed() { self.dismiss?() } } final class AuthorizationSequenceCountrySelectionController: ViewController { static func lookupCountryNameById(_ id: String) -> String? { for (name, itemId, _) in countryNamesAndCodes { if id == itemId { return name } } return nil } private let theme: AuthorizationTheme private var controllerNode: AuthorizationSequenceCountrySelectionControllerNode { return self.displayNode as! AuthorizationSequenceCountrySelectionControllerNode } private let innerNavigationController: UINavigationController private let innerController: InnerCountrySelectionController var completeWithCountryCode: ((Int, String) -> Void)? init(strings: PresentationStrings, theme: AuthorizationTheme, displayCodes: Bool = true) { self.theme = theme self.innerController = InnerCountrySelectionController(strings: strings, theme: theme, displayCodes: displayCodes) self.innerNavigationController = UINavigationController(rootViewController: self.innerController) self.innerController.navigation_setNavigationController(self.innerNavigationController) self.innerNavigationController.navigationBar.barTintColor = theme.navigationBarBackgroundColor self.innerNavigationController.navigationBar.tintColor = theme.accentColor self.innerNavigationController.navigationBar.shadowImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(theme.navigationBarSeparatorColor.cgColor) context.fill(CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: UIScreenPixel))) }) self.innerNavigationController.navigationBar.isTranslucent = false self.innerNavigationController.navigationBar.titleTextAttributes = [NSAttributedStringKey.font: Font.semibold(17.0), NSAttributedStringKey.foregroundColor: theme.navigationBarTextColor] super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = theme.statusBarStyle self.innerController.dismiss = { [weak self] in self?.cancelPressed() } } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func loadDisplayNode() { self.displayNode = AuthorizationSequenceCountrySelectionControllerNode() self.displayNodeDidLoad() self.innerNavigationController.willMove(toParentViewController: self) self.addChildViewController(self.innerNavigationController) self.displayNode.view.addSubview(self.innerNavigationController.view) self.innerNavigationController.didMove(toParentViewController: self) self.innerController.itemSelected = { [weak self] args in let (_, countryId, code) = args self?.completeWithCountryCode?(code, countryId) self?.controllerNode.animateOut() } self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: true, completion: nil) } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.innerNavigationController.viewWillAppear(false) self.innerNavigationController.viewDidAppear(false) self.controllerNode.animateIn() } override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) transition.animateView { self.innerNavigationController.view.frame = CGRect(origin: CGPoint(), size: layout.size) //self.innerController.view.frame = CGRect(origin: CGPoint(), size: layout.size) } } private func cancelPressed() { self.controllerNode.animateOut() } }