diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift index d2325e355044..4b81a644cc99 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift @@ -83,7 +83,7 @@ class AddLocationsDataSource: indentationLevel: 1 )) } - updateDataSnapshot(with: [locationsList]) + reloadDataSnapshot(with: [locationsList]) } private func isLocationInCustomList(node: LocationNode) -> Bool { @@ -110,20 +110,12 @@ extension AddLocationsDataSource: UITableViewDelegate { extension AddLocationsDataSource: LocationCellDelegate { func toggleExpanding(cell: LocationCell) { - let items = toggledItems(for: cell).first!.map { item in - var item = item - if containsChild(parent: customListLocationNode, child: item.node) { - item.isSelected = true - } - return item - } - - updateDataSnapshot(with: [items], reloadExisting: true, completion: { + toggledItems(for: cell) { if let indexPath = self.tableView.indexPath(for: cell), let item = self.itemIdentifier(for: indexPath) { self.scroll(to: item, animated: true) } - }) + } } func toggleSelecting(cell: LocationCell) { @@ -142,7 +134,7 @@ extension AddLocationsDataSource: LocationCellDelegate { } else { customListLocationNode.remove(selectedLocation: item.node, with: locationList) } - updateDataSnapshot(with: [locationList], completion: { + reloadDataSnapshot(with: [locationList], completion: { let locations = self.customListLocationNode.children.reduce([]) { partialResult, locationNode in partialResult + locationNode.locations } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/AllLocationDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/AllLocationDataSource.swift index bc15ae3976c8..ac3c9a3b63ef 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/AllLocationDataSource.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/AllLocationDataSource.swift @@ -22,6 +22,7 @@ class AllLocationDataSource: LocationDataSourceProtocol { /// and city names. func reload(_ relays: LocationRelays) { let rootNode = RootLocationNode() + let expandedRelays = nodes.flatMap { [$0] + $0.flattened }.filter { $0.showsChildren }.map { $0.code } for relay in relays.relays { guard case @@ -32,7 +33,13 @@ class AllLocationDataSource: LocationDataSourceProtocol { let relayLocation = RelayLocation.hostname(countryCode, cityCode, relay.hostname) for ancestorOrSelf in relayLocation.ancestors + [relayLocation] { - addLocation(ancestorOrSelf, rootNode: rootNode, serverLocation: serverLocation, relay: relay) + addLocation( + ancestorOrSelf, + rootNode: rootNode, + serverLocation: serverLocation, + relay: relay, + showsChildren: expandedRelays.contains(ancestorOrSelf.stringRepresentation) + ) } } @@ -56,7 +63,8 @@ class AllLocationDataSource: LocationDataSourceProtocol { _ location: RelayLocation, rootNode: LocationNode, serverLocation: REST.ServerLocation, - relay: REST.ServerRelay + relay: REST.ServerRelay, + showsChildren: Bool ) { switch location { case let .country(countryCode): @@ -64,7 +72,8 @@ class AllLocationDataSource: LocationDataSourceProtocol { name: serverLocation.country, code: LocationNode.combineNodeCodes([countryCode]), locations: [location], - isActive: true // Defaults to true, updated when children are populated. + isActive: true, // Defaults to true, updated when children are populated. + showsChildren: showsChildren ) if !rootNode.children.contains(countryNode) { @@ -77,7 +86,8 @@ class AllLocationDataSource: LocationDataSourceProtocol { name: serverLocation.city, code: LocationNode.combineNodeCodes([countryCode, cityCode]), locations: [location], - isActive: true // Defaults to true, updated when children are populated. + isActive: true, // Defaults to true, updated when children are populated. + showsChildren: showsChildren ) if let countryNode = rootNode.countryFor(code: countryCode), @@ -92,7 +102,8 @@ class AllLocationDataSource: LocationDataSourceProtocol { name: relay.hostname, code: LocationNode.combineNodeCodes([hostCode]), locations: [location], - isActive: relay.active + isActive: relay.active, + showsChildren: showsChildren ) if let countryNode = rootNode.countryFor(code: countryCode), diff --git a/ios/MullvadVPN/View controllers/SelectLocation/CustomListsDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/CustomListsDataSource.swift index 28f40e924af3..2176b471bba4 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/CustomListsDataSource.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/CustomListsDataSource.swift @@ -26,9 +26,11 @@ class CustomListsDataSource: LocationDataSourceProtocol { /// Constructs a collection of node trees by copying each matching counterpart /// from the complete list of nodes created in ``AllLocationDataSource``. func reload(allLocationNodes: [LocationNode]) { + let expandedRelays = nodes.flatMap { [$0] + $0.flattened }.filter { $0.showsChildren }.map { $0.code } nodes = repository.fetchAll().map { list in let customListWrapper = CustomListLocationNodeBuilder(customList: list, allLocations: allLocationNodes) let listNode = customListWrapper.customListLocationNode + listNode.showsChildren = expandedRelays.contains(listNode.code) listNode.forEachDescendant { node in // Each item in a section in a diffable data source needs to be unique. @@ -36,6 +38,7 @@ class CustomListsDataSource: LocationDataSourceProtocol { // equality, each node code needs to be prefixed with the code of its // parent custom list to uphold this. node.code = LocationNode.combineNodeCodes([listNode.code, node.code]) + node.showsChildren = expandedRelays.contains(node.code) } return listNode diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift index f26a0d2da492..ec7f5b75e298 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift @@ -34,13 +34,14 @@ extension [LocationCellViewModel] { mutating func addSubNodes(from item: LocationCellViewModel, at indexPath: IndexPath) { let section = LocationSection.allCases[indexPath.section] let row = indexPath.row + 1 + item.node.showsChildren = true let locations = item.node.children.map { LocationCellViewModel( section: section, node: $0, indentationLevel: item.indentationLevel + 1, - isSelected: false + isSelected: item.isSelected ) } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift index 60cd693a3c48..ccfb0a994f78 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift @@ -24,6 +24,7 @@ final class LocationDataSource: private var excludedLocation: LocationCellViewModel? let tableView: UITableView let sections: [LocationSection] + let updateQueue = DispatchQueue(label: "LocationDataSource.UpdateQueue") var didSelectRelayLocations: ((UserSelectedRelays) -> Void)? var didTapEditCustomLists: (() -> Void)? @@ -55,96 +56,108 @@ final class LocationDataSource: } func setRelays(_ relaysWithLocation: LocationRelays, selectedRelays: RelaySelection) { - let allLocationsDataSource = - dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource - - let customListsDataSource = - dataSources.first(where: { $0 is CustomListsDataSource }) as? CustomListsDataSource - - allLocationsDataSource?.reload(relaysWithLocation) - customListsDataSource?.reload(allLocationNodes: allLocationsDataSource?.nodes ?? []) - - setSelectedRelays(selectedRelays) - filterRelays(by: currentSearchString) + updateQueue.async { [weak self] in + guard let self = self, + let allLocationsDataSource = dataSources + .first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource, + let customListsDataSource = dataSources + .first(where: { $0 is CustomListsDataSource }) as? CustomListsDataSource else { return } + allLocationsDataSource.reload(relaysWithLocation) + customListsDataSource.reload(allLocationNodes: allLocationsDataSource.nodes) + setSelectedRelays(selectedRelays) + filterRelays(by: currentSearchString) + } } func filterRelays(by searchString: String) { - currentSearchString = searchString + updateQueue.async { [weak self] in + guard let self else { return } + currentSearchString = searchString + + let list = sections.enumerated().map { index, section in + self.dataSources[index] + .search(by: searchString) + .flatMap { node in + let rootNode = RootLocationNode(children: [node]) + return self.recursivelyCreateCellViewModelTree( + for: rootNode, + in: section, + indentationLevel: 0 + ) + } + } - let list = sections.enumerated().map { index, section in - dataSources[index] - .search(by: searchString) - .flatMap { node in - let rootNode = RootLocationNode(children: [node]) - return recursivelyCreateCellViewModelTree(for: rootNode, in: section, indentationLevel: 0) + DispatchQueue.main.async { + self.reloadDataSnapshot(with: list) { + if searchString.isEmpty { + self.updateSelection(completion: { + self.scrollToSelectedRelay() + }) + } else { + self.scrollToTop(animated: false) + } } - } - - updateDataSnapshot(with: list, reloadExisting: !searchString.isEmpty) { - self.tableView.reloadData() - - if searchString.isEmpty { - self.updateSelection(self.selectedLocation, animated: false, completion: { - self.scrollToSelectedRelay() - }) - } else { - self.scrollToTop(animated: false) } } } /// Refreshes the custom list section and keeps all modifications intact (selection and expanded states). func refreshCustomLists(selectedRelays: RelaySelection) { - guard let allLocationsDataSource = - dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource, - let customListsDataSource = - dataSources.first(where: { $0 is CustomListsDataSource }) as? CustomListsDataSource - else { - return - } - - // Take a "snapshot" of the currently expanded nodes. - let expandedNodes = customListsDataSource.nodes - .flatMap { [$0] + $0.flattened } - .filter { $0.showsChildren } - - // Reload data source with (possibly) updated custom lists. - customListsDataSource.reload(allLocationNodes: allLocationsDataSource.nodes) + updateQueue.async { [weak self] in + guard let self, + let allLocationsDataSource = + dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource, + let customListsDataSource = + dataSources.first(where: { $0 is CustomListsDataSource }) as? CustomListsDataSource + else { + return + } - // Reapply current selection. - setSelectedRelays(selectedRelays) + // Reload data source with (possibly) updated custom lists. + customListsDataSource.reload(allLocationNodes: allLocationsDataSource.nodes) - // Reapply current search filter. - let searchResultNodes = dataSources[0].search(by: currentSearchString) + // Reapply current selection. + setSelectedRelays(selectedRelays) - // Reapply expanded status and override nodes being hidden by search filter. - RootLocationNode(children: searchResultNodes).forEachDescendant { node in - node.showsChildren = expandedNodes.contains(node) - node.isHiddenFromSearch = false - } + // Reapply current search filter. + let list = customListsDataSource + .search(by: currentSearchString) + .flatMap { node in + let rootNode = RootLocationNode(children: [node]) + return self.recursivelyCreateCellViewModelTree( + for: rootNode, + in: .customLists, + indentationLevel: 0 + ) + } - // Construct node tree. - let list = searchResultNodes.flatMap { node in - let rootNode = RootLocationNode(children: [node]) - return recursivelyCreateCellViewModelTree(for: rootNode, in: .customLists, indentationLevel: 0) + DispatchQueue.main.async { + self.reloadDataSnapshot(with: [ + list, + self.snapshot().itemIdentifiers(inSection: .allLocations), + ]) + } } - - updateDataSnapshot(with: [ - list, - snapshot().itemIdentifiers(inSection: .allLocations), - ], reloadExisting: true) } func setSelectedRelays(_ selectedRelays: RelaySelection) { - selectedLocation = mapSelection(from: selectedRelays.selected) - - excludedLocation = mapSelection(from: selectedRelays.excluded) - excludedLocation?.excludedRelayTitle = selectedRelays.excludedTitle - - tableView.reloadData() + updateQueue.async { [weak self] in + guard let self else { return } + selectedLocation = mapSelection(from: selectedRelays.selected) + excludedLocation = mapSelection(from: selectedRelays.excluded) + excludedLocation?.excludedRelayTitle = selectedRelays.excludedTitle + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.updateSelection(completion: { + self.scrollToSelectedRelay() + }) + } + } } - func scrollToSelectedRelay() { + // MARK: - Private functions + + private func scrollToSelectedRelay() { indexPathForSelectedRelay().flatMap { tableView.scrollToRow(at: $0, at: .middle, animated: false) } @@ -185,14 +198,15 @@ final class LocationDataSource: return nil } - private func updateSelection(_ item: LocationCellViewModel?, animated: Bool, completion: (() -> Void)? = nil) { - selectedLocation = item + private func updateSelection(completion: (() -> Void)? = nil) { guard let selectedLocation else { return } - let rootNode = selectedLocation.node.root + var snapshot = snapshot() // Exit early if no changes to the node tree should be made. guard selectedLocation.node != rootNode else { + // Apply the updated snapshot + applySnapshotUsingReloadData(snapshot, completion: completion) completion?() return } @@ -215,22 +229,12 @@ final class LocationDataSource: indentationLevel: 1 ) - // Insert the new node tree below the selected item. - var snapshotItems = snapshot().itemIdentifiers(inSection: selectedLocation.section) - snapshotItems.insert(contentsOf: nodesToAdd, at: indexPath.row + 1) - - let list = sections.enumerated().map { index, section in - index == indexPath.section - ? snapshotItems - : snapshot().itemIdentifiers(inSection: section) - } + let existingItems = snapshot.itemIdentifiers(inSection: selectedLocation.section) + snapshot.deleteItems(nodesToAdd) + snapshot.insertItems(nodesToAdd, afterItem: existingItems[indexPath.row]) - updateDataSnapshot( - with: list, - reloadExisting: true, - animated: animated, - completion: completion - ) + // Apply the updated snapshot + applySnapshotUsingReloadData(snapshot, completion: completion) } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -310,25 +314,21 @@ extension LocationDataSource: UITableViewDelegate { } func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if let item = itemIdentifier(for: indexPath), item == selectedLocation { - cell.setSelected(true, animated: false) + if let item = itemIdentifier(for: indexPath) { + cell.setSelected(item == selectedLocation, animated: false) } } func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { if let indexPath = indexPathForSelectedRelay() { - if let cell = tableView.cellForRow(at: indexPath) { - cell.setSelected(false, animated: false) - } + tableView.deselectRow(at: indexPath, animated: false) } - return indexPath } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let item = itemIdentifier(for: indexPath) else { return } selectedLocation = item - var customListSelection: UserSelectedRelays.CustomListSelection? if let topmostNode = item.node.root as? CustomListLocationNode { customListSelection = UserSelectedRelays.CustomListSelection( @@ -341,7 +341,6 @@ extension LocationDataSource: UITableViewDelegate { locations: item.node.locations, customListSelection: customListSelection ) - didSelectRelayLocations?(relayLocations) } @@ -354,12 +353,9 @@ extension LocationDataSource: LocationCellDelegate { func toggleExpanding(cell: LocationCell) { guard let indexPath = tableView.indexPath(for: cell), let item = itemIdentifier(for: indexPath) else { return } - - let items = toggledItems(for: cell) - - updateDataSnapshot(with: items, reloadExisting: true, completion: { + toggledItems(for: cell) { self.scroll(to: item, animated: true) - }) + } } func toggleSelecting(cell: LocationCell) { diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSourceProtocol.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSourceProtocol.swift index 79f78ebc9943..edb1de9196a1 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSourceProtocol.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSourceProtocol.swift @@ -18,13 +18,16 @@ protocol LocationDataSourceProtocol { extension LocationDataSourceProtocol { func search(by text: String) -> [LocationNode] { guard !text.isEmpty else { - resetNodes() return nodes } var filteredNodes: [LocationNode] = [] - searchableNodes.forEach { countryNode in + searchableNodes.forEach { node in + // Use a copy of the node to preserve the expanded state, + // allowing us to restore the previous view state after a search. + let countryNode = node.copy() + countryNode.showsChildren = false if countryNode.name.fuzzyMatch(text) { @@ -62,16 +65,4 @@ extension LocationDataSourceProtocol { return filteredNodes } - - private func resetNodes() { - nodes.forEach { node in - node.showsChildren = false - node.isHiddenFromSearch = false - - node.forEachDescendant { descendantNode in - descendantNode.showsChildren = false - descendantNode.isHiddenFromSearch = false - } - } - } } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationDiffableDataSourceProtocol.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationDiffableDataSourceProtocol.swift index 0dce2859e0ea..47e6181f1cf7 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationDiffableDataSourceProtocol.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationDiffableDataSourceProtocol.swift @@ -39,51 +39,102 @@ extension LocationDiffableDataSourceProtocol { } } - func toggledItems(for cell: LocationCell) -> [[LocationCellViewModel]] { + func toggledItems(for cell: LocationCell, completion: (() -> Void)? = nil) { guard let indexPath = tableView.indexPath(for: cell), - let item = itemIdentifier(for: indexPath) else { return [[]] } - + let item = itemIdentifier(for: indexPath) else { return } + let snapshot = snapshot() let section = sections[indexPath.section] let isExpanded = item.node.showsChildren - var locationList = snapshot().itemIdentifiers(inSection: section) + var locationList = snapshot.itemIdentifiers(inSection: section) item.node.showsChildren = !isExpanded if !isExpanded { locationList.addSubNodes(from: item, at: indexPath) + addItem(locationList, toSection: section, index: indexPath.row, completion: completion) } else { locationList.removeSubNodes(from: item.node) + updateSnapshotRetainingOnly(locationList, toSection: section, completion: completion) + } + } + + private func addItem( + _ items: [LocationCellViewModel], + toSection section: LocationSection, + index: Int, + completion: (() -> Void)? = nil + ) { + var snapshot = snapshot() + let existingItems = snapshot.itemIdentifiers(inSection: section) + + // Filter itemsToAdd to only include items not already in the section + let uniqueItems = items.filter { item in + existingItems.firstIndex(where: { $0 == item }) == nil } - return sections.enumerated().map { index, section in - index == indexPath.section - ? locationList - : snapshot().itemIdentifiers(inSection: section) + // Insert unique items at the specified index + if index < existingItems.count { + snapshot.insertItems(uniqueItems, afterItem: existingItems[index]) + } else { + // If the index is beyond bounds, append to the end + snapshot.appendItems(uniqueItems, toSection: section) } + applyAndReconfigureSnapshot(snapshot, in: section, completion: completion) + } + + private func updateSnapshotRetainingOnly( + _ itemsToKeep: [LocationCellViewModel], + toSection section: LocationSection, + completion: (() -> Void)? = nil + ) { + var snapshot = snapshot() + + // Ensure the section exists in the snapshot + guard snapshot.sectionIdentifiers.contains(section) else { return } + + // Get the current items in the section + let currentItems = snapshot.itemIdentifiers(inSection: section) + + // Determine the items that should be deleted + let itemsToDelete = currentItems.filter { !itemsToKeep.contains($0) } + snapshot.deleteItems(itemsToDelete) + + // Apply the updated snapshot + applyAndReconfigureSnapshot(snapshot, in: section, completion: completion) } - func updateDataSnapshot( + private func applyAndReconfigureSnapshot( + _ snapshot: NSDiffableDataSourceSnapshot, + in section: LocationSection, + completion: (() -> Void)? = nil + ) { + self.apply(snapshot, animatingDifferences: true) { + // After adding, reconfigure specified items to update their content + var updatedSnapshot = self.snapshot() + + // Ensure the items exist in the snapshot before attempting to reconfigure + let existingItems = updatedSnapshot.itemIdentifiers(inSection: section) + + // Reconfigure the specified items + updatedSnapshot.reconfigureItems(existingItems) + + // Apply the reconfigured snapshot without animations to avoid any flickering + self.apply(updatedSnapshot, animatingDifferences: false) + } + } + + func reloadDataSnapshot( with list: [[LocationCellViewModel]], - reloadExisting: Bool = false, animated: Bool = false, completion: (() -> Void)? = nil ) { var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections(sections) for (index, section) in sections.enumerated() { let items = list[index] - snapshot.appendItems(items, toSection: section) - - if reloadExisting { - snapshot.reconfigureItems(items) - } - } - - DispatchQueue.main.async { - self.apply(snapshot, animatingDifferences: animated, completion: completion) } + self.apply(snapshot, animatingDifferences: animated, completion: completion) } func recursivelyCreateCellViewModelTree( diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift index 16529c234003..05fc73540083 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift @@ -72,11 +72,6 @@ final class LocationViewController: UIViewController { } } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - dataSource?.scrollToSelectedRelay() - } - override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) tableView.flashScrollIndicators() diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift index 4c0ab086d2cb..74298b386c01 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift @@ -47,7 +47,7 @@ final class LocationViewControllerWrapper: UIViewController { private var entryLocationViewController: LocationViewController? private let exitLocationViewController: LocationViewController private let segmentedControl = UISegmentedControl() - private let locationViewContainer = UIStackView() + private let locationViewContainer = UIView() private var selectedEntry: UserSelectedRelays? private var selectedExit: UserSelectedRelays? @@ -111,6 +111,10 @@ final class LocationViewControllerWrapper: UIViewController { setUpNavigation() setUpSegmentedControl() addSubviews() + entryLocationViewController.flatMap { + add($0) + } + add(exitLocationViewController) swapViewController() } @@ -226,17 +230,21 @@ final class LocationViewControllerWrapper: UIViewController { } } + private func add(_ locationViewController: LocationViewController) { + addChild(locationViewController) + locationViewController.didMove(toParent: self) + locationViewContainer.addConstrainedSubviews([locationViewController.view]) { + locationViewController.view.pinEdgesToSuperview() + } + } + @objc private func segmentedControlDidChange(sender: UISegmentedControl) { multihopContext = .allCases[segmentedControl.selectedSegmentIndex] swapViewController() } - func swapViewController() { - locationViewContainer.arrangedSubviews.forEach { view in - view.removeFromSuperview() - } - + private func swapViewController() { var selectedRelays: RelaySelection var oldViewController: LocationViewController? var newViewController: LocationViewController? @@ -263,16 +271,12 @@ final class LocationViewControllerWrapper: UIViewController { exitLocationViewController ) } - - oldViewController?.removeFromParent() - - if let newViewController { - newViewController.setSelectedRelays(selectedRelays) - - addChild(newViewController) - newViewController.didMove(toParent: self) - - locationViewContainer.addArrangedSubview(newViewController.view) + newViewController?.setSelectedRelays(selectedRelays) + oldViewController?.view.isUserInteractionEnabled = false + newViewController?.view.isUserInteractionEnabled = true + UIView.animate(withDuration: 0.0) { + oldViewController?.view.alpha = 0 + newViewController?.view.alpha = 1 } } }