I need to add searchbar and nil value to picker. I want to create a custom picker and use it everywhere to avoid code repetition. I was able to create a picker using this and this code, but I could not find how to capture the value I chose in another view.
This code helps me search inside the picker.
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.autocapitalizationType = .none
searchBar.searchBarStyle = .minimal
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
}
This is the custom picker I created.
struct CustomPicker<Item>: View where Item: Hashable {
let items: [Item]
let title: String
let text: KeyPath<Item, String>
let needOptional: Bool
let needSearchBar: Bool
#State var item: Item? = nil
#State var searchText: String = ""
var filteredItems: [Item] {
items.filter {
searchText.isEmpty ? true : $0[keyPath: text].lowercased().localizedStandardContains(searchText.lowercased())
}
}
var body: some View {
Picker(selection: $item, label: Text(title)) {
if needSearchBar {
SearchBar(text: $searchText, placeholder: "Search")
.padding(.horizontal, -12)
}
if needOptional {
Text("[none]").tag(nil as Item?)
.foregroundColor(.red)
}
ForEach(filteredItems, id: \.self) { item in
Text(item[keyPath: text])
.tag(item as Item?)
}
}
.onAppear{
searchText = ""
}
}
}
Usage
Country Model Example
struct Country: Codable, Identifiable, Hashable {
let id = UUID()
let name: String
enum CodingKeys: String, CodingKey {
case id, name
}
}
in ContentView
Form {
CustomPicker(items: countryArray, title: "Country", text: \Country.name, needOptional: true, needSearchBar: true)
}
How do I catch the selected Item(optional) in ContentView?
Use #Binding to bind your item.
struct CustomPicker<Item>: View where Item: Hashable {
let items: [Item]
let title: String
let text: KeyPath<Item, String>
let needOptional: Bool
let needSearchBar: Bool
#Binding var item: Item? // <--Here
#State var searchText: String = ""
//-----Other code---------//
And use #State here in content view for getting selected value.
struct ContentView: View {
#State private var selectedItem: Country? //<-- Here
private let countryArray: [Country] = [.init(name: "A"), .init(name: "B")]
var body: some View {
Form {
CustomPicker(items: countryArray, title: "Country", text: \Country.name, needOptional: true, needSearchBar: true, item: $selectedItem) // <-- Here
if let selectedItem = selectedItem {
Text(selectedItem.name)
}
}
}
}
Related
I have a Picker that updates a #Binding value, that is linked to the original #State value. The problem is that it gets updated only the first time I change it, and then it always remains like that. Not only in the sheet, but also in the ContentView, which is the original view in which the #State variable is declared. Here's a video showing the problem, and here's the code:
ContentView:
struct ContentView: View {
#State var cityForTheView = lisbon
#State var showCitySelection = false
var body: some View {
VStack {
//View
}
.sheet(isPresented: $showCitySelection) {
MapView(cityFromCV: $cityForTheView)
}
}
}
MapView:
struct MapView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var cityFromCV : CityModel
#State var availableCities = [String]()
#State var selectedCity = "Lisbon"
var body: some View {
NavigationView {
ZStack {
VStack {
Form {
HStack {
Text("Select a city:")
Spacer()
Picker("", selection: $selectedCity) {
ForEach(availableCities, id: \.self) {
Text($0)
}
}
.accentColor(.purple)
}
.pickerStyle(MenuPickerStyle())
Section {
Text("Current city: \(cityFromCV.cityName)")
}
}
}
}
}
.interactiveDismissDisabled()
.onAppear {
availableCities = []
for ct in cities {
availableCities.append(ct.cityName)
}
}
.onChange(of: selectedCity) { newv in
self.cityFromCV = cities.first(where: { $0.cityName == newv })!
}
}
}
CityModel:
class CityModel : Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var cityName : String
var country : String
var imageName : String
init(cityName: String, country: String, imageName : String) {
self.cityName = cityName
self.country = country
self.imageName = imageName
}
static func == (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
static func < (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
}
var cities = [
lisbon,
CityModel(cityName: "Madrid", country: "Spain", imageName: "Madrid"),
CityModel(cityName: "Barcelona", country: "Spain", imageName: "Barcelona"),
CityModel(cityName: "Paris", country: "France", imageName: "Paris")
]
var lisbon = CityModel(cityName: "Lisbon", country: "Portugal", imageName: "Lisbon")
What am I doing wrong?
The problem are:
at line 35: you use selectedCity to store the selected data from your picker.
Picker("", selection: $selectedCity) {
then at your line 46: you use cityFromCV.cityName to display the data which will always show the wrong data because you store your selected data in the variable selectedCity.
Section {
Text("Current city: \(cityFromCV.cityName)")
}
Solution: Just change from cityFromCV.cityName to selectedCity.
I have a textField and next to it a button that should clear the text. My problems is though that the TextField View only carries a binder. Hence the actual text in the textfield isn't deleted since Bindings doesn't reload views.
I tried to narrow it down as much as possible in an example:
struct ContentView: View {
#State var presentedView = false
#State var text: String = ""
var body: some View {
Button("Hello, world!") { presentedView = true }
.padding()
.sheet(isPresented: $presentedView, content: {
SomeView(viewModel: .init(textBinder: $text), text: $text)
})
}
}
struct SomeView: View {
let viewModel: ViewModel
#Binding var text: String
var body: some View {
HStack {
TextFieldTyped(text: $text)
Spacer()
Button("erase") {
$text.wrappedValue = ""
}
}
}
struct ViewModel {
let textBinder: Binding<String>
}
}
struct TextFieldTyped: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.becomeFirstResponder()
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldTyped
init(_ textField: TextFieldTyped) {
self.parent = textField
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if (textField.text?.count ?? 0) == 1, string.isEmpty {
parent.text = string
} else if string.isEmpty, !(textField.text ?? "").isEmpty {
parent.text = String(textField.text?.dropLast() ?? "")
} else {
parent.text = (textField.text ?? "").isEmpty ? string : (textField.text ?? "") + string
}
return true
}
}
}
try the following code without the useless ViewModel, and an update of text in updateUIView:
struct ContentView: View {
#State var presentedView = false
#State var text: String = ""
var body: some View {
Button("Hello, world!") { presentedView = true }
.padding()
.sheet(isPresented: $presentedView, content: {
SomeView(text: $text)
})
}
}
struct SomeView: View {
#Binding var text: String
var body: some View {
HStack {
TextFieldTyped(text: $text)
Spacer()
Button("erase") {
text = "" // <--- here
}
}
}
}
struct TextFieldTyped: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.becomeFirstResponder()
uiView.text = text // <--- here
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldTyped
init(_ textField: TextFieldTyped) {
self.parent = textField
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if (textField.text?.count ?? 0) == 1, string.isEmpty {
parent.text = string
} else if string.isEmpty, !(textField.text ?? "").isEmpty {
parent.text = String(textField.text?.dropLast() ?? "")
} else {
parent.text = (textField.text ?? "").isEmpty ? string : (textField.text ?? "") + string
}
return true
}
}
}
The other answer is perfectly fine but it is also good idea to pull related vars out into their own struct, as recommended in Data Essentials in SwiftUI (WWDC20) at 4:00. Here is how I think it would be achieved in your case:
struct SomeViewConfig {
var presented = false
var text: String = ""
mutating func present() {
presented = true
text = ""
}
}
struct ContentView2: View {
#State var someViewConfig = SomeViewConfig()
var body: some View {
Button("Hello, world!") { someViewConfig.present() }
.padding()
.sheet(isPresented: $someViewConfig.presented) {
SomeView(config: $someViewConfig)
}
}
}
struct SomeView: View {
#Binding var config: SomeViewConfig
var body: some View {
HStack {
TextFieldTyped(text: $config.text)
Spacer()
Button("erase") {
config.text = ""
}
}
}
}
I have a SearchBar that I've added a dismiss property to. dismiss is used by the cancel button, but also might be used by the parent view when displaying a sheet. How do I define the SearchBar in the parent view to be able to reference the dismiss property?
The relevant parts of the SearchBar look like this:
struct SearchBar: View {
#Binding var text: String
#Binding var isSearching: Bool
let prompt: String
var body: some View {
...
}
var dismiss: Void {
// dismiss the keyboard
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
isSearching = false
text = ""
}
}
I envisioned the parent SearchView to look something like this:
struct SearchView: View {
#State private var isShowingDataView = false
#State private var searchText = ""
#State private var isSearching = false
let prompt = "Search"
#State private var searchBar: View
var body: some View {
VStack(alignment: .leading) {
searchBar = SearchBar(text: $searchText, isSearching: $isSearching, prompt: prompt)
...
Button(action: {
showData(data: data)
}) {
HStack {
...
}
}
}
}
func showData(data: Data) {
dataShowing = data
if isSearching {
searchBar.dismiss
}
isShowingDataView = true
}
}
With the above I get the error:
"Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements"
on the searchBar definition line, and
"Type '()' cannot conform to 'View'"
on the VStack line.
dismiss should be a method on your Searchview, and you should pass a closure to SearchBar for it to call when its cancel button is tapped.
struct SearchBar: View {
#Binding var text: String
let prompt: String
let isSearching: Bool
let cancel: () -> Void
var body: some View {
HStack {
TextField(prompt, text: $text)
Button("Cancel", action: cancel)
}
.disabled(!isSearching)
}
}
struct SearchView: View {
#State private var isShowingDataView = false
#State private var searchText = ""
#State private var isSearching = false
let prompt = "Search"
var body: some View {
VStack(alignment: .leading) {
SearchBar(
text: $searchText,
prompt: prompt,
isSearching: $isSearching,
cancel: self.cancelSearch
)
Button(action: {
showData(data: data)
}) {
HStack {
...
}
}
}
}
private func showData(data: Data) {
dataShowing = data
cancelSearch()
isShowingDataView = true
}
private func cancelSearch() {
guard isSearching else { return }
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
isSearching = false
text = ""
}
}
Context
I have created a UIViewRepresentable to wrap a UITextField so that:
it can be set it to become the first responder when the view loads.
the next textfield can be set to become the first responder when enter is pressed
Problem
When used inside a NavigationView, unless the keyboard is dismissed from previous views, the view doesn't observe the value in their ObservedObject.
Question
Why is this happening? What can I do to fix this behaviour?
Screenshots
Keyboard from root view not dismissed:
Keyboard from root view dismissed:
Code
Here is the said UIViewRepresentable
struct SimplifiedFocusableTextField: UIViewRepresentable {
#Binding var text: String
private var isResponder: Binding<Bool>?
private var placeholder: String
private var tag: Int
public init(
_ placeholder: String = "",
text: Binding<String>,
isResponder: Binding<Bool>? = nil,
tag: Int = 0
) {
self._text = text
self.placeholder = placeholder
self.isResponder = isResponder
self.tag = tag
}
func makeUIView(context: UIViewRepresentableContext<SimplifiedFocusableTextField>) -> UITextField {
// create textfield
let textField = UITextField()
// set delegate
textField.delegate = context.coordinator
// configure textfield
textField.placeholder = placeholder
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.tag = self.tag
// return
return textField
}
func makeCoordinator() -> SimplifiedFocusableTextField.Coordinator {
return Coordinator(text: $text, isResponder: self.isResponder)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<SimplifiedFocusableTextField>) {
// update text
uiView.text = text
// set first responder ONCE
if self.isResponder?.wrappedValue == true && !uiView.isFirstResponder && !context.coordinator.didBecomeFirstResponder{
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
private var isResponder: Binding<Bool>?
var didBecomeFirstResponder = false
init(text: Binding<String>, isResponder: Binding<Bool>?) {
_text = text
self.isResponder = isResponder
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = false
}
}
}
}
And to reproduce, here is the contentView:
struct ContentView: View {
var body: some View {
return NavigationView { FieldView(tag: 0) }
}
}
and here's the view with the field and its view model
struct FieldView: View {
#ObservedObject private var viewModel = FieldViewModel()
#State private var focus = false
var tag: Int
var body: some View {
return VStack {
// listen to viewModel's value
Text(viewModel.value)
// text field
SimplifiedFocusableTextField("placeholder", text: self.$viewModel.value, isResponder: $focus, tag: self.tag)
// push to stack
NavigationLink(destination: FieldView(tag: self.tag + 1)) {
Text("Continue")
}
// dummy for tapping to dismiss keyboard
Color.green
}
.onAppear {
self.focus = true
}.dismissKeyboardOnTap()
}
}
public extension View {
func dismissKeyboardOnTap() -> some View {
modifier(DismissKeyboardOnTap())
}
}
public struct DismissKeyboardOnTap: ViewModifier {
public func body(content: Content) -> some View {
return content.gesture(tapGesture)
}
private var tapGesture: some Gesture {
TapGesture().onEnded(endEditing)
}
private func endEditing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
.compactMap({$0})
.first?.windows
.filter {$0.isKeyWindow}
.first?.endEditing(true)
}
}
class FieldViewModel: ObservableObject {
var subscriptions = Set<AnyCancellable>()
// diplays
#Published var value = ""
}
It looks like SwiftUI rendering engine again over-optimized...
Here is fixed part - just make destination unique forcefully using .id. Tested with Xcode 11.4 / iOS 13.4
NavigationLink(destination: FieldView(tag: self.tag + 1).id(UUID())) {
Text("Continue")
}
I am setting the leading and trailing navigationBarItems like this
.navigationBarItems(leading: SearchBar(text: $searchText), trailing: rightTopBarItems())
The trailing items show up fine but SearchBar does not appear. Is it possible to add a UIViewRepresentable as a navigationBarItem? I cannot find anything in the documentation.
SearchBar code:
import SwiftUI
struct SearchBar: UIViewRepresentable {
#Binding var text: 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.autocapitalizationType = .none
searchBar.searchBarStyle = UISearchBar.Style.minimal
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
I had the same problem.
So, I gave up adding UIViewRepresentable and decided to useTextField directly.
I hope this serves your purpose.
struct SearchTextField: View {
#State var input: String = ""
#Binding var searchText: String
var body: some View {
HStack {
Image(systemName: "magnifyingglass")
TextField("Search word", text: $input, onCommit: {
self.searchText = self.input
})
}
}
}
final class SearchViewModel: ObservableObject {
#Published var searchText = ""
}
struct ContentView: View {
#ObservedObject var viewModel = SearchViewModel()
var body: some View {
NavigationView {
Text("Hello")
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading:
HStack {
SearchTextField(searchText: $viewModel.searchText)
}
)
}
}
}