From 48263ae7dbc5b59cec364231c97574e2dff792c9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 26 May 2023 23:18:55 +0200 Subject: [PATCH] Add tap on search to focus search field on iOS --- Model/NavigationModel.swift | 13 ++++++++- Model/RecentsModel.swift | 4 ++- Model/Search/SearchModel.swift | 28 +++++++++++++++++++ Shared/Search/FocusableSearchTextField.swift | 29 ++++++++++++++++++++ Shared/Search/SearchView.swift | 12 ++++++-- Yattee.xcodeproj/project.pbxproj | 14 ++++++++++ 6 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 Shared/Search/FocusableSearchTextField.swift diff --git a/Model/NavigationModel.swift b/Model/NavigationModel.swift index 55bde054..22850eeb 100644 --- a/Model/NavigationModel.swift +++ b/Model/NavigationModel.swift @@ -63,7 +63,9 @@ final class NavigationModel: ObservableObject { } } - @Published var tabSelection: TabSelection! + @Published var tabSelection: TabSelection! { didSet { + if oldValue == tabSelection { multipleTapHandler() } + }} @Published var presentingAddToPlaylist = false @Published var videoToAddToPlaylist: Video! @@ -295,6 +297,15 @@ final class NavigationModel: ObservableObject { channelPresentedInSheet = channel presentingChannelSheet = true } + + func multipleTapHandler() { + switch tabSelection { + case .search: + self.search.focused = true + default: + print("not implemented") + } + } } typealias TabSelection = NavigationModel.TabSelection diff --git a/Model/RecentsModel.swift b/Model/RecentsModel.swift index 6aee9c4f..4ce95369 100644 --- a/Model/RecentsModel.swift +++ b/Model/RecentsModel.swift @@ -39,7 +39,9 @@ final class RecentsModel: ObservableObject { func addQuery(_ query: String) { if !query.isEmpty { - NavigationModel.shared.tabSelection = .search + if NavigationModel.shared.tabSelection != .search { + NavigationModel.shared.tabSelection = .search + } add(.init(from: query)) } } diff --git a/Model/Search/SearchModel.swift b/Model/Search/SearchModel.swift index 314daad4..9ab8e28c 100644 --- a/Model/Search/SearchModel.swift +++ b/Model/Search/SearchModel.swift @@ -16,9 +16,23 @@ final class SearchModel: ObservableObject { @Published var querySuggestions = [String]() private var suggestionsDebouncer = Debouncer(.milliseconds(200)) + @Published var focused = false + var accounts: AccountsModel { .shared } private var resource: Resource! + init() { + #if os(iOS) + addKeyboardDidHideNotificationObserver() + #endif + } + + deinit { + #if os(iOS) + removeKeyboardDidHideNotificationObserver() + #endif + } + var isLoading: Bool { resource?.isLoading ?? false } @@ -136,4 +150,18 @@ final class SearchModel: ObservableObject { } } } + + #if os(iOS) + private func addKeyboardDidHideNotificationObserver() { + NotificationCenter.default.addObserver(self, selector: #selector(onKeyboardDidHide), name: UIResponder.keyboardDidHideNotification, object: nil) + } + + @objc func onKeyboardDidHide() { + focused = false + } + + private func removeKeyboardDidHideNotificationObserver() { + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardDidHideNotification, object: nil) + } + #endif } diff --git a/Shared/Search/FocusableSearchTextField.swift b/Shared/Search/FocusableSearchTextField.swift new file mode 100644 index 00000000..ed52a297 --- /dev/null +++ b/Shared/Search/FocusableSearchTextField.swift @@ -0,0 +1,29 @@ +import Introspect +import Repeat +import SwiftUI + +@available(iOS 15.0, macOS 12, *) +struct FocusableSearchTextField: View { + @ObservedObject private var state = SearchModel.shared + + #if os(iOS) + @State private var textField: UITextField? + #elseif os(macOS) + @State private var textField: NSTextField? + #endif + + var body: some View { + SearchTextField() + #if os(iOS) + .introspectTextField { field in + textField = field + } + .onChange(of: state.focused) { newValue in + if newValue, let textField, !textField.isFirstResponder { + textField.becomeFirstResponder() + textField.selectedTextRange = textField.textRange(from: textField.beginningOfDocument, to: textField.endOfDocument) + } + } + #endif + } +} diff --git a/Shared/Search/SearchView.swift b/Shared/Search/SearchView.swift index 87d89cdf..b8c3614d 100644 --- a/Shared/Search/SearchView.swift +++ b/Shared/Search/SearchView.swift @@ -95,7 +95,11 @@ struct SearchView: View { filtersMenu } - SearchTextField() + if #available(macOS 12, *) { + FocusableSearchTextField() + } else { + SearchTextField() + } } #endif } @@ -175,7 +179,11 @@ struct SearchView: View { searchMenu } ToolbarItem(placement: .principal) { - SearchTextField() + if #available(iOS 15, *) { + FocusableSearchTextField() + } else { + SearchTextField() + } } } .navigationBarTitleDisplayMode(.inline) diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index f3e5f511..54902e58 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -686,6 +686,9 @@ 379DC3D128BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; }; 379DC3D228BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; }; 379DC3D328BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; }; + 379E7C332A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C322A20FE3900AF8118 /* FocusableSearchTextField.swift */; }; + 379E7C342A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E7C322A20FE3900AF8118 /* FocusableSearchTextField.swift */; }; + 379E7C362A2105B900AF8118 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 379E7C352A2105B900AF8118 /* Introspect */; }; 379EF9E029AA585F009FE6C6 /* HideShortsButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */; }; 379EF9E129AA585F009FE6C6 /* HideShortsButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */; }; 379EF9E229AA585F009FE6C6 /* HideShortsButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */; }; @@ -1386,6 +1389,7 @@ 379ACB502A1F8DB000E01914 /* HomeSettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSettingsButton.swift; sourceTree = ""; }; 379B0252287A1CDF001015B5 /* OrientationTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrientationTracker.swift; sourceTree = ""; }; 379DC3D028BA4EB400B09677 /* Seek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Seek.swift; sourceTree = ""; }; + 379E7C322A20FE3900AF8118 /* FocusableSearchTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableSearchTextField.swift; sourceTree = ""; }; 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */ = {isa = PBXFileReference; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = HideShortsButtons.swift; sourceTree = ""; }; 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualitySettings.swift; sourceTree = ""; }; 37A2B345294723850050933E /* CacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheModel.swift; sourceTree = ""; }; @@ -1626,6 +1630,7 @@ 37F7AB5228A94EB900FB46B5 /* IOKit.framework in Frameworks */, 370F4FDF27CC16CB001B35DC /* libxcb-shape.0.0.0.dylib in Frameworks */, 370F4FE127CC16CB001B35DC /* libuchardet.0.0.7.dylib in Frameworks */, + 379E7C362A2105B900AF8118 /* Introspect in Frameworks */, 370F4FDB27CC16CB001B35DC /* libswscale.6.4.100.dylib in Frameworks */, 370F4FDC27CC16CB001B35DC /* libavutil.57.17.100.dylib in Frameworks */, 370F4FE327CC16CB001B35DC /* libbrotlicommon.1.dylib in Frameworks */, @@ -2187,6 +2192,7 @@ 3782B95527557A2400990149 /* Search */ = { isa = PBXGroup; children = ( + 379E7C322A20FE3900AF8118 /* FocusableSearchTextField.swift */, 3782B94E27553A6700990149 /* SearchSuggestions.swift */, 374710042755291C00CE0F87 /* SearchTextField.swift */, 37AAF27F26737550007FC770 /* SearchView.swift */, @@ -2686,6 +2692,7 @@ 374D11E62943C56300CB4350 /* Cache */, 371AC0B1294D1C230085989E /* CachedAsyncImage */, 379325D629A265AE00181CF1 /* Logging */, + 379E7C352A2105B900AF8118 /* Introspect */, ); productName = "Yattee (macOS)"; productReference = 37D4B0CF2671614900C925CA /* Yattee.app */; @@ -3122,6 +3129,7 @@ 374924DA2921050B0017D862 /* LocationsSettings.swift in Sources */, 378FFBC428660172009E3FBE /* URLParser.swift in Sources */, 3784B23D2728B85300B09468 /* ShareButton.swift in Sources */, + 379E7C332A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */, 37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 3743CA52270F284F00E4D32B /* View+Borders.swift in Sources */, 3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, @@ -3440,6 +3448,7 @@ 377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */, 374924DB2921050B0017D862 /* LocationsSettings.swift in Sources */, 371AC0A0294D13AA0085989E /* UnwatchedFeedCountModel.swift in Sources */, + 379E7C342A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */, 37F5C7E12A1E2AF300927B73 /* ListView.swift in Sources */, 37192D5828B179D60012EEDD /* ChaptersView.swift in Sources */, 3784CDE327772EE40055BBF2 /* Watch.swift in Sources */, @@ -5166,6 +5175,11 @@ package = 3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */; productName = ActiveLabel; }; + 379E7C352A2105B900AF8118 /* Introspect */ = { + isa = XCSwiftPackageProductDependency; + package = 37BD07C52698B27B003EBB87 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; + productName = Introspect; + }; 37BADCA42699FB72009BE4FB /* Alamofire */ = { isa = XCSwiftPackageProductDependency; package = 37BADCA32699FB72009BE4FB /* XCRemoteSwiftPackageReference "Alamofire" */;