fix(ios): dismiss workspace search when leaving its page

This commit is contained in:
Kaede Akino committed 2026-10-06 23:18:36 +08:00
1 parent 69b9b78316
commit 2502b76cd6
2 files changed
+103 -6

No files matched your search

+14 -4
View File
@@ -50,6 +50,7 @@ private final class MobileToolbarSearchController: UIViewController, UISearchCon
private var presentation: Binding<Bool>?
private var onSubmit: () -> Void = {}
private var isEnabled = false
private var isVisible = false
private var isDismissing = false
private weak var owner: UIViewController?
private var previousDefinesPresentationContext = false
@@ -79,11 +80,20 @@ private final class MobileToolbarSearchController: UIViewController, UISearchCon
DispatchQueue.main.async { [weak self] in self?.synchronize() }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
isVisible = true
synchronize()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Tab and navigation stacks retain their children. Release the native
// presentation before another page takes over the shared toolbar.
isVisible = false
removeSearch()
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if parent != nil { synchronize() }
@@ -99,7 +109,7 @@ private final class MobileToolbarSearchController: UIViewController, UISearchCon
}
private func synchronize() {
guard isEnabled else {
guard isEnabled, isVisible else {
removeSearch()
return
}
@@ -161,7 +171,7 @@ private final class MobileToolbarSearchController: UIViewController, UISearchCon
}
func willPresentSearchController(_ searchController: UISearchController) {
guard isEnabled, !isDismissing else {
guard isEnabled, isVisible, !isDismissing else {
searchController.isActive = false
return
}
+89 -2
View File
@@ -9,6 +9,60 @@ import UIKit
@Suite(.serialized, .requiresKeyWindow)
@MainActor
struct MobileTerminalSearchIntegrationTests {
@Test(arguments: [ConnectionHistoryEntry.Kind.ssh, .sftp])
func leavingWorkspaceSearchRestoresVaultNavigationAcrossResizing(kind: ConnectionHistoryEntry.Kind) async throws {
let fixture = try SearchFixture(kind: kind, fullRoot: true)
defer { fixture.close() }
let scene = try #require(fixture.scene)
try await fixture.settle()
let tabs = try #require(fixture.tabController)
let workspaceSearch = try #require(fixture.searchController)
for _ in 0..<2 {
workspaceSearch.isActive = true
try await fixture.settle()
workspaceSearch.searchBar.becomeFirstResponder()
try await fixture.enter("deploy")
#expect(workspaceSearch.isActive)
scene.selectedDestination = .vault
try await fixture.settle()
#expect(!workspaceSearch.isActive)
#expect(!fixture.workspace.find.isPresented)
#expect(!workspaceSearch.searchBar.searchTextField.isFirstResponder)
// Retained pages still receive model updates while another tab is visible.
if kind == .sftp {
fixture.workspace.sftpChannel.query = "deploy updated"
} else {
fixture.workspace.find.updateQuery("deploy updated")
}
try await fixture.settle()
#expect(!workspaceSearch.isActive)
let vaultSearch = try #require(fixture.searchController)
#expect(vaultSearch !== workspaceSearch)
vaultSearch.isActive = true
try await fixture.settle()
// Vault uses SwiftUI's search delegate, so drive UIKit's dismissal
// rather than calling the workspace adapter's cancel callback.
vaultSearch.isActive = false
try await fixture.settle()
#expect(!vaultSearch.isActive)
for (width, height): (CGFloat, CGFloat) in [(900, 600), (600, 900)] {
fixture.resize(width: width, height: height)
try await fixture.settle()
#expect(!tabs.isTabBarHidden)
#expect(fixture.searchController === vaultSearch)
#expect(!vaultSearch.isActive)
}
scene.selectedDestination = .workspaces
try await fixture.settle()
#expect(fixture.searchController === workspaceSearch)
#expect(!workspaceSearch.isActive)
#expect(workspaceSearch.searchBar.text == "deploy updated")
}
}
enum Dismissal: CaseIterable {
case native
case terminalThenNative
@@ -171,17 +225,28 @@ private final class SearchFixture {
let window: UIWindow
let previous: UIWindow?
let host: UIViewController
let scene: MobileSceneModel?
init(kind: ConnectionHistoryEntry.Kind, fullWorkspace: Bool = false) throws {
init(kind: ConnectionHistoryEntry.Kind, fullWorkspace: Bool = false, fullRoot: Bool = false) throws {
app = try MobileAppModel(inMemory: true)
workspace = MobileWorkspace(
descriptor: .init(hostID: UUID(), title: "Search", kind: kind), hosts: app.library)
let scene = try #require(UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first)
previous = scene.keyWindow
window = UIWindow(windowScene: scene)
if fullWorkspace {
if fullRoot {
let model = MobileSceneModel(app: app, sessionID: "search-navigation-test")
model.workspaceSession.acceptRecovery(
.init(workspaces: [workspace], descriptors: [workspace.descriptor], order: [workspace.id]))
model.selectedDestination = .workspaces
model.workspaceID = workspace.id
self.scene = model
host = UIHostingController(rootView: MobileRootView(app: app, scene: model))
} else if fullWorkspace {
self.scene = nil
host = UIHostingController(rootView: WorkspaceSearchFixtureContent(app: app, workspace: workspace))
} else {
self.scene = nil
host = UIHostingController(rootView: SearchFixtureContent(workspace: workspace))
}
window.rootViewController = host
@@ -190,6 +255,14 @@ private final class SearchFixture {
var searchController: UISearchController? { searchController(in: host) }
var tabController: UITabBarController? {
func find(in controller: UIViewController) -> UITabBarController? {
if let tabs = controller as? UITabBarController { return tabs }
return controller.children.lazy.compactMap { find(in: $0) }.first
}
return find(in: host)
}
func enter(_ text: String) async throws {
let field = try #require(searchController?.searchBar.searchTextField)
field.text = text
@@ -205,6 +278,14 @@ private final class SearchFixture {
func settle() async throws { try await Task.sleep(for: .milliseconds(700)) }
func resize(width: CGFloat, height: CGFloat) {
let frame = CGRect(x: 0, y: 0, width: width, height: height)
window.frame = frame
host.view.frame = frame
host.view.setNeedsLayout()
host.view.layoutIfNeeded()
}
func close() {
window.endEditing(true)
window.isHidden = true
@@ -214,6 +295,12 @@ private final class SearchFixture {
}
private func searchController(in controller: UIViewController) -> UISearchController? {
if let tabs = controller as? UITabBarController, let selected = tabs.selectedViewController {
return searchController(in: selected)
}
if let navigation = controller as? UINavigationController, let top = navigation.topViewController {
return searchController(in: top)
}
if let search = controller.navigationItem.searchController { return search }
for child in controller.children {
if let search = searchController(in: child) { return search }