diff --git a/Sources/SupportDocs/SupportDocsView+Search.swift b/Sources/SupportDocs/SupportDocsView+Search.swift index ed8ae24..be99c85 100755 --- a/Sources/SupportDocs/SupportDocsView+Search.swift +++ b/Sources/SupportDocs/SupportDocsView+Search.swift @@ -7,46 +7,6 @@ import SwiftUI - -/** - Search bar at the top of the NavigationView. - - Source: [http://blog.eppz.eu/swiftui-search-bar-in-the-navigation-bar/]( http://blog.eppz.eu/swiftui-search-bar-in-the-navigation-bar/). - - MIT License - */ -internal class SearchBarConfigurator: NSObject, ObservableObject { - - /// The text inside the search bar. - @Published var searchText: String = "" - - /// Instance of the search controller. - let searchController: UISearchController = UISearchController(searchResultsController: nil) - - override init() { - super.init() - - /// Prevent a gray overlay over the list. - self.searchController.obscuresBackgroundDuringPresentation = false - - /// Set the delegate. - self.searchController.searchResultsUpdater = self - } -} - -/** - Delegate method of `UISearchController` - */ -extension SearchBarConfigurator: UISearchResultsUpdating { - func updateSearchResults(for searchController: UISearchController) { - - /// Publish search bar text changes. - if let searchBarText = searchController.searchBar.text { - self.searchText = searchBarText - } - } -} - /** View Modifier for applying the search bar. */ diff --git a/Sources/SupportDocs/SupportDocsView.swift b/Sources/SupportDocs/SupportDocsView.swift index 02b2a19..5511975 100755 --- a/Sources/SupportDocs/SupportDocsView.swift +++ b/Sources/SupportDocs/SupportDocsView.swift @@ -37,19 +37,6 @@ public struct SupportDocsView: View { This is only for SwiftUI -- You don't need to do this in UIKit. As long as you set `options.navigationBar.dismissButtonTitle = "Dismiss"`, SupportDocs will dismiss itself. */ public init(dataSourceURL: URL, options: SupportOptions = SupportOptions(), isPresented: Binding? = nil) { - - /** - The custom `NavigationConfigurator` modifier only works for iOS 14 and above, so for lower versions set `UINavigationBar.appearance()` instead. - */ - if #available(iOS 14.0, *) { } else { - let navBarAppearance = UINavigationBarAppearance() - navBarAppearance.titleTextAttributes = [.foregroundColor: options.navigationBar.titleColor] - navBarAppearance.largeTitleTextAttributes = [.foregroundColor: options.navigationBar.titleColor] - navBarAppearance.backgroundColor = options.navigationBar.backgroundColor - UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance - UINavigationBar.appearance().standardAppearance = navBarAppearance - UINavigationBar.appearance().tintColor = options.navigationBar.buttonTintColor - } self.dataSourceURL = dataSourceURL self.options = options self.isPresented = isPresented @@ -111,26 +98,47 @@ public struct SupportDocsView: View { */ @State internal var sections: [SupportSection] = [SupportSection]() + /** + Reference of the search bar and its delegate. + */ + @ObservedObject var searchBarConfigurator = SearchBarConfigurator() + public var body: some View { NavigationView { - Group { + ZStack { if isDownloadingJSON { /// Show the loading spinner if JSON is downloading. ActivityIndicator(isAnimating: $isDownloadingJSON, style: options.other.activityIndicatorStyle) } else { + List { /// First, display the titles of your documents. - ForEach(sections) { section in + ForEach( + + /// Filter the sections. Display only those that contain documents where their titles contain the search bar's text. + sections.filter { section in + return searchBarConfigurator.searchText.isEmpty || section.supportItems.contains(where: { + $0.title.localizedStandardContains(searchBarConfigurator.searchText) + }) + } + ) { section in Section(header: Text(section.name)) { - ForEach(section.supportItems) { item in + ForEach( + + /// Filter the documents in each section. + section.supportItems.filter { item in + return searchBarConfigurator.searchText.isEmpty || item.title.localizedStandardContains(searchBarConfigurator.searchText) + } + ) { item in SupportItemRow( title: item.title, titleColor: section.color, url: URL(string: item.url) ?? options.other.error404, progressBarOptions: options.progressBar ) + .animation(nil) } } .displayTextAsConfigured() /// Prevent default all-caps behavior if possible (iOS 14 and above). @@ -138,16 +146,18 @@ public struct SupportDocsView: View { /// Then, display the footer. Customize this inside `options.other.footer`. options.other.footer - .listRowInsets(EdgeInsets()) - .frame(maxWidth: .infinity, minHeight: 60) - .background(Color(UIColor.systemGroupedBackground)) + .listRowInsets(EdgeInsets()) + .frame(maxWidth: .infinity, minHeight: 60) + .background(Color(UIColor.systemGroupedBackground)) + } .listStyle(for: options.listStyle) /// Set the `listStyle` of your selection. + .transition(.opacity) /// Fade the List in once the JSON loads. + } } - .transition(.opacity) /// Fade it in once the JSON loads. .navigationBarTitle(Text(options.navigationBar.title), displayMode: .large) /// Set your title. - .configureNavigationBarIfAvailable(navigationOptions: options.navigationBar) + .configureBar(for: options, searchBarConfigurator: searchBarConfigurator) /** If you have a dismiss button, display it. diff --git a/Sources/SupportDocs/SupportOptions/SupportOptions+SearchBar.swift b/Sources/SupportDocs/SupportOptions/SupportOptions+SearchBar.swift new file mode 100755 index 0000000..34841ee --- /dev/null +++ b/Sources/SupportDocs/SupportOptions/SupportOptions+SearchBar.swift @@ -0,0 +1,121 @@ +// +// SupportOptions+SearchBar.swift +// SupportDocsSwiftUI +// +// Created by Zheng on 11/28/20. +// + +import UIKit +import SwiftUI +import Combine + +public extension SupportOptions { + + /** + Customize the appearance of the Search Bar. + */ + struct SearchBar { + + /** + Customize the appearance of the Search Bar. + + - parameter placeholder: The placeholder shown in the search bar before the user has entered text. + - parameter placeholderColor: Color of the placeholder and search icon. + - parameter textColor: Color of the search text. + - parameter tintColor: Color of the cursor and "Cancel" button. + - parameter backgroundColor: Background color of the search text field. + - parameter clearButtonMode: A mode that controls when the standard Clear button appears in the text field. + */ + public init( + placeholder: String = "Search", + placeholderColor: UIColor = UIColor.secondaryLabel.withAlphaComponent(0.75), + textColor: UIColor = UIColor.label, + tintColor: UIColor = UIColor.blue, + backgroundColor: UIColor = UIColor.white.withAlphaComponent(0.3), + clearButtonMode: UITextField.ViewMode = .whileEditing + ) { + self.placeholder = placeholder + self.placeholderColor = placeholderColor + self.textColor = textColor + self.tintColor = tintColor + self.backgroundColor = backgroundColor + self.clearButtonMode = clearButtonMode + } + + + /** + The placeholder shown in the search bar before the user has entered text. + */ + public var placeholder: String = "Search" + + /** + Color of the placeholder and search icon. + */ + public var placeholderColor: UIColor = UIColor.secondaryLabel.withAlphaComponent(0.75) + + /** + Color of the search text. + */ + public var textColor: UIColor = UIColor.label + + /** + Color of the cursor and "Cancel" button. + */ + public var tintColor: UIColor = UIColor.blue + + /** + Background color of the search text field. + */ + public var backgroundColor: UIColor = UIColor.white.withAlphaComponent(0.3) + + /** + A mode that controls when the standard Clear button appears in the text field. + */ + public var clearButtonMode: UITextField.ViewMode = .whileEditing + } +} + +/** + Search bar at the top of the NavigationView. + + Source: [http://blog.eppz.eu/swiftui-search-bar-in-the-navigation-bar/]( http://blog.eppz.eu/swiftui-search-bar-in-the-navigation-bar/). + + MIT License + */ +class SearchBarConfigurator: NSObject, ObservableObject { + + let objectWillChange = PassthroughSubject() + + /// The text inside the search bar. + @Published var searchText: String = "" { + willSet { + self.objectWillChange.send() + } + } + + /// Instance of the search controller. + let searchController: UISearchController = UISearchController(searchResultsController: nil) + + override init() { + super.init() + + /// Prevent a gray overlay over the list. + self.searchController.obscuresBackgroundDuringPresentation = false + + /// Set the delegate. + self.searchController.searchResultsUpdater = self + } +} + +/** + Listen for changes in the search bar's text. + */ +extension SearchBarConfigurator: UISearchResultsUpdating { + func updateSearchResults(for searchController: UISearchController) { + + /// Publish search bar text changes. + if let searchBarText = searchController.searchBar.text { + self.searchText = searchBarText + } + } +} diff --git a/Sources/SupportDocs/SupportOptions/SupportOptions.swift b/Sources/SupportDocs/SupportOptions/SupportOptions.swift index e90c611..cadf355 100755 --- a/Sources/SupportDocs/SupportOptions/SupportOptions.swift +++ b/Sources/SupportDocs/SupportOptions/SupportOptions.swift @@ -13,6 +13,7 @@ import SwiftUI # Parameters - `categories`: Allows you to group documents with the same `tag` into the same section of the list. Each category may contain more than one `tag`. - `navigationBar`: Customize the Navigation Bar's `title`, `titleColor`, `dismissButtonTitle`, `buttonTintColor`, and `backgroundColor`. + - `searchBar`: Customize the `placeholder`, `placeholderColor`, `textColor`, `tintColor`, and `backgroundColor` of the Search Bar. - `progressBar`: Customize the `foregroundColor` and `backgroundColor` of the progress bar. - `listStyle`: The style of the `List`. Defaults to `.insetGroupedListStyle`. - `navigationViewStyle`: The style of the `NavigationView`. Defaults to `.defaultNavigationViewStyle`. @@ -22,6 +23,8 @@ public struct SupportOptions { /** Allows you to group documents with the same `tag` into the same section of the list. Each category may contain more than one `tag`. + + Leave as `nil` to display all documents regardless of their `tag`s. */ public var categories: [Category]? = nil @@ -32,6 +35,13 @@ public struct SupportOptions { */ public var navigationBar: NavigationBar = NavigationBar(dismissButtonView: nil) + /** + Customize the appearance of the Search Bar. + + Set to `nil` to not show a search bar. + */ + public var searchBar: SearchBar? = SearchBar() + /** Customize the `foregroundColor` and `backgroundColor` of the progress bar. */ @@ -57,6 +67,7 @@ public struct SupportOptions { - parameter categories: Allows you to group documents with the same `tag` into the same section of the list. Each category may contain more than one `tag`. - parameter navigationBar: Customize the Navigation Bar's `title`, `titleColor`, `dismissButtonTitle`, `buttonTintColor`, and `backgroundColor`. + - parameter searchBar: Customize the `placeholder`, `placeholderColor`, `textColor`, `tintColor`, `backgroundColor`, and `clearButtonMode` of the Search Bar. - parameter progressBar: Customize the `foregroundColor` and `backgroundColor` of the progress bar. - parameter listStyle: The style of the `List`. Defaults to `.insetGroupedListStyle`. - parameter navigationViewStyle: The style of the `NavigationView`. Defaults to `.defaultNavigationViewStyle`. @@ -65,6 +76,7 @@ public struct SupportOptions { public init( categories: [Category]? = nil, navigationBar: NavigationBar = NavigationBar(dismissButtonView: nil), + searchBar: SearchBar? = SearchBar(), progressBar: ProgressBar = ProgressBar(), listStyle: CustomListStyle = CustomListStyle.insetGroupedListStyle, navigationViewStyle: CustomNavigationViewStyle = CustomNavigationViewStyle.defaultNavigationViewStyle, @@ -72,6 +84,7 @@ public struct SupportOptions { ) { self.categories = categories self.navigationBar = navigationBar + self.searchBar = searchBar self.progressBar = progressBar self.listStyle = listStyle self.navigationViewStyle = navigationViewStyle diff --git a/Sources/SupportDocs/Utilities.swift b/Sources/SupportDocs/Utilities.swift index 250be9a..bf4c9d9 100755 --- a/Sources/SupportDocs/Utilities.swift +++ b/Sources/SupportDocs/Utilities.swift @@ -7,54 +7,76 @@ import SwiftUI +// MARK: - Navigation and Search Bar Configuration +/// Partially from https://github.com/Geri-Borbas/iOS.Blog.SwiftUI_Search_Bar_in_Navigation_Bar, MIT License + /** - Configure the navigation bar's look, for iOS 14 and above. - - Source: [https://stackoverflow.com/a/58427754/14351818](https://stackoverflow.com/a/58427754/14351818). + ViewModifier that applies SupportOptions' `NavigationBar` and `SearchBar` configurations. */ -internal struct NavigationConfigurator: UIViewControllerRepresentable { - var configure: (UINavigationController) -> Void = { _ in } - - func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewController { - UIViewController() - } - func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext) { - if let nc = uiViewController.navigationController { - self.configure(nc) - } +struct BarModifier: ViewModifier { + + let options: SupportOptions + let searchBarConfigurator: SearchBarConfigurator + + func body(content: Content) -> some View { + content + .overlay( /// Workaround to apply the `ViewControllerResolver` + ViewControllerResolver { viewController in + + /** + Only add a search bar if it's not set to `nil`. + */ + if let searchBarOptions = options.searchBar { + viewController.navigationItem.searchController = self.searchBarConfigurator.searchController + + let searchBar = searchBarConfigurator.searchController.searchBar + + let icon = UIImage(systemName: "magnifyingglass")?.withTintColor(searchBarOptions.placeholderColor, renderingMode: .alwaysOriginal) + searchBar.setImage(icon, for: UISearchBar.Icon.search, state: .normal) + searchBar.tintColor = searchBarOptions.tintColor + + searchBar.searchTextField.attributedPlaceholder = NSAttributedString(string: searchBarOptions.placeholder, attributes: [.foregroundColor: searchBarOptions.placeholderColor]) + searchBar.searchTextField.textColor = searchBarOptions.textColor + searchBar.searchTextField.backgroundColor = searchBarOptions.backgroundColor + + searchBar.searchTextField.clearButtonMode = searchBarOptions.clearButtonMode + } + + /** + Now set the Navigation Bar's configuration + */ + let navBarAppearance = UINavigationBarAppearance() + navBarAppearance.configureWithOpaqueBackground() + navBarAppearance.titleTextAttributes = [.foregroundColor: options.navigationBar.titleColor] + navBarAppearance.largeTitleTextAttributes = [.foregroundColor: options.navigationBar.titleColor] + + if let backgroundColor = options.navigationBar.backgroundColor { + navBarAppearance.backgroundColor = backgroundColor + viewController.navigationController?.navigationBar.scrollEdgeAppearance = navBarAppearance + } + + viewController.navigationController?.navigationBar.standardAppearance = navBarAppearance + + viewController.navigationController?.navigationBar.barTintColor = options.navigationBar.backgroundColor + viewController.navigationController?.navigationBar.tintColor = options.navigationBar.buttonTintColor + + } + .frame(width: 0, height: 0) + ) } } /** - Apply the `NavigationConfigurator`. + For easier usage of the bar modifier. */ -internal extension View { - @ViewBuilder - func configureNavigationBarIfAvailable(navigationOptions: SupportOptions.NavigationBar) -> some View { - if #available(iOS 14, *) { - self.background( - NavigationConfigurator { nc in /// Set the properties of `options.navigationBar`. - let navBarAppearance = UINavigationBarAppearance() - navBarAppearance.configureWithOpaqueBackground() - navBarAppearance.titleTextAttributes = [.foregroundColor: navigationOptions.titleColor] - navBarAppearance.largeTitleTextAttributes = [.foregroundColor: navigationOptions.titleColor] - - if let backgroundColor = navigationOptions.backgroundColor { - navBarAppearance.backgroundColor = backgroundColor - nc.navigationBar.scrollEdgeAppearance = navBarAppearance - } - nc.navigationBar.standardAppearance = navBarAppearance - - nc.navigationBar.barTintColor = navigationOptions.backgroundColor - nc.navigationBar.tintColor = navigationOptions.buttonTintColor - } - ) - } else { - self - } +extension View { + func configureBar(for options: SupportOptions, searchBarConfigurator: SearchBarConfigurator) -> some View { + return self.modifier(BarModifier(options: options, searchBarConfigurator: searchBarConfigurator)) } } +// MARK: - Other Utilities + /** Hide or show a View (support for iOS 13).