├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md ├── Sources └── CustomSearchbarButton │ ├── CustomSearchbarButton.swift │ └── Preview.swift └── Tests └── CustomSearchbarButtonTests └── CustomSearchbarButtonTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CustomSearchbarButton", 8 | platforms: [ 9 | .iOS(.v15) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "CustomSearchbarButton", 15 | targets: ["CustomSearchbarButton"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", .upToNextMajor(from: "0.3.1")) 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "CustomSearchbarButton", 27 | dependencies: [ 28 | .product(name: "Introspect", package: "SwiftUI-Introspect") 29 | ]), 30 | .testTarget( 31 | name: "CustomSearchbarButtonTests", 32 | dependencies: ["CustomSearchbarButton"]), 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CustomSearchbarButton 2 | 3 | This package enables you to effortlessly incorporate UIKit SearchBar buttons into your SwiftUI SearchBar, enhancing its functionality. 4 | 5 | ## Installation 6 | 7 | Requirement: iOS15+. CustomSearchbarButton can be installed through [Swift Package Manager](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app). 8 | 9 | ```swift 10 | .package(url: "https://github.com/underthestars-zhy/CustomSearchbarButton", .upToNextMajor(from: "1.1.0")) 11 | ``` 12 | 13 | ## Get Started 14 | 15 | ### Default Button 16 | 17 | By using CustomSearchbarButton, you only need to add a simple modification to your view where you add `searchable` to add a custom SearchBar button. 18 | 19 | ```swift 20 | .searchbarButton(image: UIImage(systemName: "...")!, type: .bookmark, visibility: .auto) { 21 | // some action 22 | } 23 | ``` 24 | 25 | #### Button Type 26 | 27 | CustomSearchBarButton supports four different types of buttons: 28 | 29 | * Search (Cannot set visibility) 30 | * Clear 31 | * Bookbmark 32 | * Result List 33 | 34 | #### Visibilty 35 | 36 | By adjusting the visibility setting, you can control the button's visibility based on specific conditions. 37 | 38 | * Auto 39 | * Visible 40 | * Hidden 41 | 42 | ### Menu Button 43 | 44 | CustomSearchbarButton enables you to add a menu button to the SearchBar, which will appear in the same position as the bookmark button. 45 | 46 | 47 | ```swift 48 | .searchbarMenuButton(image: UIImage(systemName: "star")!, menu: createMenu(), mode: .always) 49 | ``` 50 | 51 | #### Mode 52 | 53 | CustomSearchBarButton supports four different types of visibility rules: 54 | 55 | * Always 56 | * Never 57 | * Unless Editing 58 | * While Editing 59 | -------------------------------------------------------------------------------- /Sources/CustomSearchbarButton/CustomSearchbarButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Introspect 3 | import Combine 4 | 5 | struct SearchbarButtonModifier: ViewModifier { 6 | let visibility: Visibility 7 | let type: UISearchBar.Icon 8 | let image: UIImage 9 | let action: () -> () 10 | 11 | @State var vcDelegate: VCDelegate? = nil 12 | @State var sbDelegate: Delegate? = nil 13 | 14 | func body(content: Content) -> some View { 15 | content 16 | .introspectSearchController { sc in 17 | let vcDelegate = VCDelegate(visibility: visibility, type: type) 18 | sc.delegate = vcDelegate 19 | self.vcDelegate = vcDelegate 20 | 21 | let delegate = Delegate { 22 | if type == .clear { 23 | action() 24 | } 25 | } searchButton: { 26 | if type == .search { 27 | action() 28 | } 29 | } bookmarkButton: { 30 | if type == .bookmark { 31 | action() 32 | } 33 | } resultListButton: { 34 | if type == .resultsList { 35 | action() 36 | } 37 | } 38 | 39 | sc.searchBar.delegate = delegate 40 | self.sbDelegate = delegate 41 | 42 | sc.searchBar.setImage(image, for: type, state: .normal) 43 | sc.searchBar.setImage(image, for: type, state: [.highlighted, .selected]) 44 | 45 | switch visibility { 46 | case .visible: 47 | setVisibility(sc: sc, value: true) 48 | case .hidden: 49 | setVisibility(sc: sc, value: false) 50 | case .auto: 51 | if sc.isActive { 52 | setVisibility(sc: sc, value: true) 53 | } else { 54 | setVisibility(sc: sc, value: false) 55 | } 56 | } 57 | } 58 | } 59 | 60 | func setVisibility(sc: UISearchController, value: Bool) { 61 | switch type { 62 | case .search: 63 | return 64 | case .clear: 65 | sc.searchBar.showsCancelButton = value 66 | case .bookmark: 67 | sc.searchBar.showsBookmarkButton = value 68 | case .resultsList: 69 | sc .searchBar.showsSearchResultsButton = value 70 | @unknown default: 71 | fatalError() 72 | } 73 | } 74 | } 75 | 76 | struct SearchbarMenuButtonModifier: ViewModifier { 77 | let mode: UITextField.ViewMode 78 | let menu: UIMenu 79 | let image: UIImage 80 | 81 | func body(content: Content) -> some View { 82 | content 83 | .introspectSearchController { sc in 84 | sc.searchBar.searchTextField.rightViewMode = mode 85 | let bookmarkButton = UIButton(type: .custom) 86 | bookmarkButton.setImage(image, for: .normal) 87 | bookmarkButton.menu = menu 88 | bookmarkButton.showsMenuAsPrimaryAction = true 89 | sc.searchBar.searchTextField.rightView = bookmarkButton 90 | } 91 | } 92 | } 93 | 94 | class VCDelegate: NSObject, UISearchControllerDelegate { 95 | let visibility: Visibility 96 | let type: UISearchBar.Icon 97 | 98 | func willPresentSearchController(_ searchController: UISearchController) { 99 | switch visibility { 100 | case .auto, .visible: 101 | setVisibility(sc: searchController, value: true) 102 | case .hidden: 103 | setVisibility(sc: searchController, value: false) 104 | } 105 | } 106 | 107 | func didPresentSearchController(_ searchController: UISearchController) { 108 | switch visibility { 109 | case .auto, .visible: 110 | searchController.searchBar.showsBookmarkButton = true 111 | case .hidden: 112 | setVisibility(sc: searchController, value: true) 113 | } 114 | } 115 | 116 | func didDismissSearchController(_ searchController: UISearchController) { 117 | switch visibility { 118 | case .auto, .hidden: 119 | setVisibility(sc: searchController, value: false) 120 | case .visible: 121 | setVisibility(sc: searchController, value: true) 122 | } 123 | } 124 | 125 | init(visibility: Visibility, type: UISearchBar.Icon) { 126 | self.visibility = visibility 127 | self.type = type 128 | } 129 | 130 | func setVisibility(sc: UISearchController, value: Bool) { 131 | switch type { 132 | case .search: 133 | return 134 | case .clear: 135 | sc.searchBar.showsCancelButton = value 136 | case .bookmark: 137 | sc.searchBar.showsBookmarkButton = value 138 | case .resultsList: 139 | sc .searchBar.showsSearchResultsButton = value 140 | @unknown default: 141 | fatalError() 142 | } 143 | } 144 | } 145 | 146 | class Delegate: NSObject, UISearchBarDelegate { 147 | let cancelButton: () -> () 148 | let searchButton: () -> () 149 | let bookmarkButton: () -> () 150 | let resultListButton: () -> () 151 | 152 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 153 | cancelButton() 154 | } 155 | 156 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 157 | searchButton() 158 | } 159 | 160 | func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) { 161 | bookmarkButton() 162 | } 163 | 164 | func searchBarResultsListButtonClicked(_ searchBar: UISearchBar) { 165 | resultListButton() 166 | } 167 | 168 | init(cancelButton: @escaping () -> Void, searchButton: @escaping () -> Void, bookmarkButton: @escaping () -> Void, resultListButton: @escaping () -> Void) { 169 | self.cancelButton = cancelButton 170 | self.searchButton = searchButton 171 | self.bookmarkButton = bookmarkButton 172 | self.resultListButton = resultListButton 173 | } 174 | } 175 | 176 | typealias Visibility = SearchbarButtonVisibility 177 | 178 | public enum SearchbarButtonVisibility { 179 | case visible 180 | case hidden 181 | case auto 182 | } 183 | 184 | public extension View { 185 | func searchbarButton(image: UIImage, type: UISearchBar.Icon, visibility: SearchbarButtonVisibility = .auto, action: @escaping () -> ()) -> some View { 186 | self 187 | .modifier(SearchbarButtonModifier(visibility: visibility, type: type, image: image, action: action)) 188 | } 189 | } 190 | 191 | 192 | public extension View { 193 | func searchbarMenuButton(image: UIImage, menu: UIMenu, mode: UITextField.ViewMode) -> some View { 194 | self 195 | .modifier(SearchbarMenuButtonModifier(mode: mode, menu: menu, image: image)) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Sources/CustomSearchbarButton/Preview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preview.swift 3 | // 4 | // 5 | // Created by 朱浩宇 on 2023/5/29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SwiftUIView: View { 11 | @State var text = "" 12 | @State var change = false 13 | 14 | var body: some View { 15 | if #available(iOS 16.0, *) { 16 | NavigationStack { 17 | List(1..<10, id: \.self) { id in 18 | Text("\(change ? "\(id)" : "HI")") 19 | } 20 | .searchable(text: $text) 21 | .navigationTitle("Search") 22 | .searchbarMenuButton(image: UIImage(systemName: "star")!, menu: createMenu(), mode: .always) 23 | } 24 | 25 | } else { 26 | // Fallback on earlier versions 27 | } 28 | } 29 | 30 | func createMenu() -> UIMenu { 31 | let usersItem = UIAction(title: "Users", image: UIImage(systemName: "person.fill")) { (action) in 32 | 33 | print("Users action was tapped") 34 | } 35 | 36 | let addUserItem = UIAction(title: "Add User", image: UIImage(systemName: "person.badge.plus")) { (action) in 37 | 38 | print("Add User action was tapped") 39 | } 40 | 41 | let removeUserItem = UIAction(title: "Remove User", image: UIImage(systemName: "person.fill.xmark.rtl")) { (action) in 42 | print("Remove User action was tapped") 43 | } 44 | 45 | let menu = UIMenu(title: "My Menu", options: .displayInline, children: [usersItem , addUserItem , removeUserItem]) 46 | 47 | return menu 48 | } 49 | } 50 | 51 | struct SwiftUIView_Previews: PreviewProvider { 52 | static var previews: some View { 53 | SwiftUIView() 54 | } 55 | } 56 | 57 | struct SwiftUIView2_Previews: PreviewProvider { 58 | static var previews: some View { 59 | SwiftUIView() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/CustomSearchbarButtonTests/CustomSearchbarButtonTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CustomSearchbarButton 3 | 4 | final class CustomSearchbarButtonTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(CustomSearchbarButton().text, "Hello, World!") 10 | } 11 | } 12 | --------------------------------------------------------------------------------