How to hide SearchBar on scroll in SwiftUI? - ios

I'm trying to hide Search bar in my app like Apple did in their messages app:
I've already implemented UISearchBar in SwiftUI:
struct SearchBar: UIViewRepresentable {
#Binding var text: String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.searchBarStyle = .minimal
searchBar.placeholder = "Поиск по названию, дедлайну или описанию"
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context _: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
How can I implement hide and hiding animation in SwiftUI?

SwiftUI 3.0 (iOS 15.0+)
Apple made it possible in very native way.
You just need to use .searchable() modifier with view you want to make searchable and ensure that you have NavigationView as parent of your views.
Example
struct CountriesView: View {
let countries = ["United States", "India", "Ukraine", "Russia", "Sweden"]
#State var search: String = ""
var body: some View {
NavigationView {
List(countries.filter({
// we filter our country list by checking whether country name
// does contain search string or not
$0.lowercased().contains(search.lowercased())
})) { country in
Text(country)
}.searchable(text: $search)
}
}
}
By the way, you can use computed properties to filter your array in more convenient way.
Then, the computed property for the filter from the example will be:
var searchResults: [String] {
return countries.filter({$0.lowercased().contains(search.lowercased())})
}
So, List will look like this:
List(searchResults) {
// ...
}
p.s. you can learn more about computed variables & properties in this swift.org article

Related

How to get selected range from UIViewRepresentable delegate method of UITextView in SwiftUI

I have a code and I need to get the selected range of TextView from (func textViewDidChangeSelection) to the ContentView. How do I do such a thing in SwiftUI?
this is a basic idea of my code.
struct ContentView: View {
#State private var text = ""
var body: some View {
UITextViewRepresentable(text: $text)
}
}
struct UITextViewRepresentable: UIViewRepresentable {
let textView = UITextView()
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
// SwiftUI -> UIKit
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
class Coordinator: NSObject, UITextViewDelegate {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func textViewDidChange(_ textView: UITextView) {
// UIKit -> SwiftUI
_text.wrappedValue = textView.text
}
func textViewDidChangeSelection(_ textView: UITextView) {
// Fires off every time the user changes the selection.
print(textView.selectedRange)
}
}
}
Just add an extra binding for it, of type NSRange, as that is the type of textView.selectedRange.
That means adding one to the UIViewRepresentable:
struct UITextViewRepresentable: UIViewRepresentable {
let textView = UITextView()
#Binding var text: String
#Binding var range: NSRange // <---
and one to the coordinator:
func makeCoordinator() -> Coordinator {
Coordinator(text: $text, range: $range) // <---
}
class Coordinator: NSObject, UITextViewDelegate {
#Binding var text: String
#Binding var range: NSRange // <---
init(text: Binding<String>, range: Binding<NSRange>) {
_text = text
_range = range
}
// ...
func textViewDidChangeSelection(_ textView: UITextView) {
range = textView.selectedRange
}
Now in ContentView, you can get it as a #State:
#State private var text = ""
#State private var range = NSRange()
var body: some View {
UITextViewRepresentable(text: $text, range: $range)
}

How to change UISearchBar background shape on iOS 16

It seems like there's a bug in iOS 16 where a UISearchBar in a UISplitViewController's primary position displays with a square background. This doesn't happen if the search bar is in other positions, or if the split view is collapsed (eg. on iPhone).
I've reported it (FB10847490) but any ideas how I could work around this in the meantime? It seems like .background/.backgroundColor and searchTextField.background/backgroundColor both affect other subviews and not the view that is causing the square appearance.
Sample app:
struct ContentView: View {
var body: some View {
HostingController()
}
}
struct HostingController: UIViewControllerRepresentable {
#State private var text = ""
func makeUIViewController(context: Context) -> some UIViewController {
let controller = UISplitViewController(style: .doubleColumn)
let contentView = UIHostingController(rootView: VStack {
SearchBar(text: $text)
})
let primaryController = UINavigationController(rootViewController: contentView)
controller.setViewController(primaryController, for: .primary)
controller.setViewController(UIHostingController(rootView: SearchBar(text: $text)), for: .secondary)
controller.preferredSplitBehavior = .overlay
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
struct SearchBar: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UISearchBar {
let searchBar = UISearchBar()
searchBar.placeholder = "Search..."
searchBar.returnKeyType = .done
searchBar.enablesReturnKeyAutomatically = false
searchBar.searchBarStyle = .minimal
searchBar.text = text
searchBar.searchBarStyle = .minimal
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: Context) {
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPad")
}
}

How do I make my own focusable view in SwiftUI using FocusState?

So I want to implement a custom control as a UIViewRepresentable which correctly handles focus using an #FocusState binding.
So I want to be able to manage the focus like so:
struct MyControl: UIViewRepresentable { ... }
struct Container: View {
#FocusState var hasFocus: Bool = false
var body: some View {
VStack {
MyControl()
.focused($hasFocus)
Button("Focus my control") {
hasFocus = true
}
}
}
}
What do I have to implement in MyControl to have it respond to the focus state properly? Is there a protocol or something which must be implemented?
Disclaimer: the solution is not suitable for full custom controls. For these cases, you can try to pass the FocusState as binding: let isFieldInFocus: FocusState<Int?>.Binding
In my case, I have wrapped UITextView. In order to set the focus, I only used .focused($isFieldInFocus).
Some of the information can be obtained through the property wrapper(#Environment(\.)), but this trick does not work with focus.
struct ContentView: View {
#FocusState var isFieldInFocus: Bool
#State var text = "Test message"
#State var isDisabled = false
var body: some View {
VStack {
Text("Focus for UITextView")
.font(.headline)
AppTextView(text: $text)
.focused($isFieldInFocus)
.disabled(isDisabled)
.frame(height: 200)
HStack {
Button("Focus") {
isFieldInFocus = true
}
.buttonStyle(.borderedProminent)
Button("Enable/Disable") {
isDisabled.toggle()
}
.buttonStyle(.borderedProminent)
}
}
.padding()
.background(.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AppTextView: UIViewRepresentable {
#Binding var text: String
#Environment(\.isEnabled) var isEnabled: Bool
#Environment(\.isFocused) var isFocused: Bool // doesn't work
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.font = UIFont.preferredFont(forTextStyle: .body)
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
print("isEnabled", isEnabled, "isFocused", isFocused)
}
class Coordinator : NSObject, UITextViewDelegate {
private var parent: AppTextView
init(_ textView: AppTextView) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}

SwiftUI - #Published property is not updating view from coordinator

I'm using a wrapper around UISearchBar and I'm seeing a different behavior when passing a #Published property into this SearchBar wrapper versus a TextField.
Both are updating the #ObservedObject var query = Query() class #Published var input property as expected but only the TextField is then updating the SearchSheet view. I would like for the view to be updated when input has been changed in SearchBar similarly to how it is updated from TextField.
Edit: I've updated my question to include the ContentView where it looks like this issue is specific to when the sheet is called from a Button in a NavigationBarItem.
struct ContentView: View {
#State var showingSearch = false
var body: some View {
NavigationView {
VStack {
Text("Hello World")
}
.navigationBarItems(trailing:
Button(action: {
self.showingSearch.toggle()
}) {
Image(systemName: "magnifyingglass")
}
.sheet(isPresented: $showingSearch) {
SearchSheet(isPresented: self.$showingSearch)
}
)
}
}
}
class Query: ObservableObject {
#Published var input = "" {
didSet {
// Called as expected in both cases but only TextField updates the SearchSheet view.
}
}
}
struct SearchSheet: View {
#ObservedObject var query = Query()
var body: some View {
VStack {
// Does not update the SearchSheet view. I would like it understand why and how to update it.
SearchBar(text: $query.input, placeholder: "Search")
// Does update the SearchSheet view.
TextField("Search", text: $query.input)
Text("\(query.input)")
}
}
}
import SwiftUI
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.searchBarStyle = .minimal
searchBar.autocapitalizationType = .none
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
struct SearchBar_Previews: PreviewProvider {
#State private static var text = ""
static var previews: some View {
SearchBar(text: $text, placeholder: "Search")
}
}
Moving the display of the sheet out from navigationBarItems resolves the issue. At the time I believe this is a bug.
.navigationBarItems(trailing:
Button(action: {
self.showingSearch.toggle()
}) {
Image(systemName: "magnifyingglass")
}
)
.sheet(isPresented: $showingSearch) {
SearchSheet(isPresented: self.$showingSearch)
}

Re-focusing a first responder in SwiftUI

I know how to create a UIViewRepresentable in SwiftUI in order to enable first responder functionality for a TextField:
struct FirstResponderTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
func makeUIView(context: UIViewRepresentableContext<FirstResponderTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<FirstResponderTextField>) {
uiView.placeholder = placeholder
uiView.text = text
if (!uiView.isFirstResponder) {
uiView.becomeFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var firstResponderTextField: FirstResponderTextField
init(_ firstResponderTextField: FirstResponderTextField) {
self.firstResponderTextField = firstResponderTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
firstResponderTextField.text = textField.text ?? ""
}
}
}
My problem is attempting to "re-focus" this custom text field. So while this text field DOES get focused when my ContentView is initialized, I want to know how I can re-focus this text field programmatically, AFTER it has lost focus.
Here is my ContentView:
struct ContentView: View {
#State var textField1: String = ""
#State var textField2: String = ""
var body: some View {
Form {
Section {
FirstResponderTextField(placeholder: "Text Field 1", text: $textField1)
TextField("Text Field 2", text: $textField2)
}
Section {
Button(action: {
// ???
}, label: {
Text("Re-Focus Text Field 1")
})
}
}
}
}
Here is what I've tried. I thought maybe I could create a #State variable which can control the FirstResponderTextField, so I went ahead and changed my structs as follows:
struct ContentView: View {
#State var textField1: String = ""
#State var textField2: String = ""
#State var isFocused: Bool = true
var body: some View {
Form {
Section {
FirstResponderTextField(placeholder: "Text Field 1", text: $textField1, isFocused: $isFocused)
TextField("Text Field 2", text: $textField2)
}
Section {
Button(action: {
self.isFocused = true
}, label: {
Text("Re-Focus Text Field 1")
})
}
}
}
}
struct FirstResponderTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
#Binding var isFocused: Bool
func makeUIView(context: UIViewRepresentableContext<FirstResponderTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<FirstResponderTextField>) {
uiView.placeholder = placeholder
uiView.text = text
if (isFocused) {
uiView.becomeFirstResponder()
isFocused = false
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var firstResponderTextField: FirstResponderTextField
init(_ firstResponderTextField: FirstResponderTextField) {
self.firstResponderTextField = firstResponderTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
firstResponderTextField.text = textField.text ?? ""
}
}
}
It does not appear to be working. I mean, it works when I first click the button, but stops working afterwards.
Also, I am now getting this warning:
Modifying state during view update, this will cause undefined behaviour.
Is it possible to create a UIViewRepresentable that can be re-focused whenever I want?
You should not be setting the isFocused to false in your FirstResponderTextField.
Instead, in FirstResponderTextField, observe the value changes in the isFocused binding, and set your control to being first responder or not accordingly.
I created a solution for you with an ObserableObject corresponding to the TextFieldState in this gist on github
For future reference, this is what I changed:
instead of #State var isFocused: Bool = true in ContentView, use #ObservedObject var textFieldState = TextFieldState()
have a property #ObservedObject var state: TextFieldState in FirstResponderTextField
simply do this in FirstResponderTextField.updateUIView:
if state.isFirstResponder {
uiView.becomeFirstResponder()
}

Resources