SwiftUI: How to only run code when the user stops typing in a TextField? - swift

so I'm trying to make a search bar that doesn't run the code that displays the results until the user stops typing for 2 seconds (AKA it should reset a sort of timer when the user enters a new character). I tried using .onChange() and an AsyncAfter DispatchQueue and it's not working (I think I understand why the current implementation isn't working, but I'm not sure I'm even attack this problem the right way)...
struct SearchBarView: View {
#State var text: String = ""
#State var justUpdatedSuggestions: Bool = false
var body: some View {
ZStack {
TextField("Search", text: self.$text).onChange(of: self.text, perform: { newText in
appState.justUpdatedSuggestions = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
appState.justUpdatedSuggestions = false
})
if justUpdatedSuggestions == false {
//update suggestions
}
})
}
}
}

The possible approach is to use debounce from Combine framework. To use that it is better to create separated view model with published property for search text.
Here is a demo. Prepared & tested with Xcode 12.4 / iOS 14.4.
import Combine
class SearchBarViewModel: ObservableObject {
#Published var text: String = ""
}
struct SearchBarView: View {
#StateObject private var vm = SearchBarViewModel()
var body: some View {
ZStack {
TextField("Search", text: $vm.text)
.onReceive(
vm.$text
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
) {
guard !$0.isEmpty else { return }
print(">> searching for: \($0)")
}
}
}
}

There are usually two most common techniques used when dealing with delaying search query calls: throttling or debouncing.
To implement these concepts in SwiftUI, you can use Combine frameworks throttle/debounce methods.
An example of that would look something like this:
import SwiftUI
import Combine
final class ViewModel: ObservableObject {
private var disposeBag = Set<AnyCancellable>()
#Published var text: String = ""
init() {
self.debounceTextChanges()
}
private func debounceTextChanges() {
$text
// 2 second debounce
.debounce(for: 2, scheduler: RunLoop.main)
// Called after 2 seconds when text stops updating (stoped typing)
.sink {
print("new text value: \($0)")
}
.store(in: &disposeBag)
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
TextField("Search", text: $viewModel.text)
}
}
You can read more about Combine and throttle/debounce in official documentation: throttle, debounce

Related

How can I make a State wrapper outside of View in SwiftUI?

I know that State wrappers are for View and they designed for this goal, but I wanted to try build and test some code if it is possible, my goal is just for learning purpose,
I have 2 big issues with my code!
Xcode is unable to find T.
How can I initialize my state?
import SwiftUI
var state: State<T> where T: StringProtocol = State(get: { state }, set: { newValue in state = newValue })
struct ContentView: View {
var body: some View {
Text(state)
}
}
Update: I could do samething for Binding here, Now I want do it for State as well with up code
import SwiftUI
var state2: String = String() { didSet { print(state2) } }
var binding: Binding = Binding.init(get: { state2 }, set: { newValue in state2 = newValue })
struct ContentView: View {
var body: some View {
TextField("Enter your text", text: binding)
}
}
If I could find the answer of my issue then, i can define my State and Binding both outside of View, 50% of this work done and it need another 50% for State Wrapper.
New Update:
import SwiftUI
var state: State<String> = State.init(initialValue: "Hello") { didSet { print(state.wrappedValue) } }
var binding: Binding = Binding.init(get: { state.wrappedValue }, set: { newValue in state = State(wrappedValue: newValue) })
struct ContentView: View {
var body: some View {
Text(state) // <<: Here is the issue!
TextField("Enter your text", text: binding)
}
}
Even if you create a State wrapper outside a view, how will the view know when to refresh its body?
Without a way to notify the view, your code will do the same as:
struct ContentView: View {
var body: some View {
Text("Hello")
}
}
What you can do next depends on what you want to achieve.
If all you need is a way to replicate the State behaviour outside the view, I recommend you take a closer look at the Combine framework.
An interesting example is CurrentValueSubject:
var state = CurrentValueSubject<String, Never>("state1")
It stores the current value and also acts as a Publisher.
What will happen if we use it in a view that doesn't observe anything?
struct ContentView: View {
var body: some View {
Text(state.value)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state.value = "state2"
}
}
}
}
The answer is: nothing. The view is drawn once and, even if the state changes, the view won't be re-drawn.
You need a way to notify the view about the changes. In theory you could do something like:
var state = CurrentValueSubject<String, Never>("state1")
struct ContentView: View {
#State var internalState = ""
var body: some View {
Text(internalState)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state.value = "state2"
}
}
.onReceive(state) {
internalState = $0
}
}
}
But this is neither elegant nor clean. In these cases we should probably use #State:
struct ContentView: View {
#State var state = "state1"
var body: some View {
Text(state)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
state = "state2"
}
}
}
}
To sum up, if you need a view to be refreshed, just use the native SwiftUI property wrappers (like #State). And if you need to declare state values outside the view, use ObservableObject + #Published.
Otherwise there is a huge Combine framework which does exactly what you want. I recommend you take a look at these links:
Combine: Getting Started
Using Combine

How to detect if keyboard is present in swiftui

I want to know if the keyboard is present when the button is pressed. How would I do this? I have tried but I don't have any luck. Thanks.
Using this protocol, KeyboardReadable, you can conform to any View and get keyboard updates from it.
KeyboardReadable protocol:
import Combine
import UIKit
/// Publisher to read keyboard changes.
protocol KeyboardReadable {
var keyboardPublisher: AnyPublisher<Bool, Never> { get }
}
extension KeyboardReadable {
var keyboardPublisher: AnyPublisher<Bool, Never> {
Publishers.Merge(
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.map { _ in true },
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in false }
)
.eraseToAnyPublisher()
}
}
It works by using Combine and creating a publisher so we can receive the keyboard notifications.
With an example view of how it can be applied:
struct ContentView: View, KeyboardReadable {
#State private var text: String = ""
#State private var isKeyboardVisible = false
var body: some View {
TextField("Text", text: $text)
.onReceive(keyboardPublisher) { newIsKeyboardVisible in
print("Is keyboard visible? ", newIsKeyboardVisible)
isKeyboardVisible = newIsKeyboardVisible
}
}
}
You can now read from the isKeyboardVisible variable to know if the keyboard is visible.
When the TextField is active with the keyboard showing, the following prints:
Is keyboard visible? true
When the keyboard is then hidden upon hitting return, the following prints instead:
Is keyboard visible? false
You can use keyboardWillShowNotification/keyboardWillHideNotification to update as soon as they keyboard starts to appear or disappear, and the keyboardDidShowNotification/keyboardDidHideNotification variants to update after the keyboard has appeared or disappeared. I prefer the will variant because the updates are instant for when the keyboard shows.
iOS 15:
You can use the focused(_:) view modifier and #FocusState property wrapper to know whether a text field is editing, and also change the editing state.
#State private var text: String = ""
#FocusState private var isTextFieldFocused: Bool
var body: some View {
VStack {
TextField("hello", text: $text)
.focused($isTextFieldFocused)
if isTextFieldFocused {
Button("Keyboard is up!") {
isTextFieldFocused = false
}
}
}
}
My little improvement #George's answer.
Implement publisher right inside the View protocol
extension View {
var keyboardPublisher: AnyPublisher<Bool, Never> {
Publishers
.Merge(
NotificationCenter
.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.map { _ in true },
NotificationCenter
.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in false })
.debounce(for: .seconds(0.1), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
}
I also added debounce operator in order to prevent true - false toggle when you have multiple TextFields and user moves between them.
Use in any View
struct SwiftUIView: View {
#State var isKeyboardPresented = false
#State var firstTextField = ""
#State var secondTextField = ""
var body: some View {
VStack {
TextField("First textField", text: $firstTextField)
TextField("Second textField", text: $secondTextField)
}
.onReceive(keyboardPublisher) { value in
isKeyboardPresented = value
}
}
}

ObservableObject text not updating even with objectWillChange - multiple classes

I am currently writing a utility, and as part of this, a string is received (through WebSockets and the Starscream library) and the string value is then displayed in the SwiftUI view (named ReadingsView).
The structure of the code is as follows - there are two classes, the WSManager class which manages the WebSocket connections, and the GetReadings class which has the ObservableObject property, which manages and stores the readings.
When the string is received using the didReceive method in the WSManager class, it is decoded by the decodeText method in the WSManager class, which then calls the parseReceivedStrings method in the GetReadings class.
class WSManager : WebSocketDelegate {
func didReceive(event: WebSocketEvent, client: WebSocket) {
case .text(let string):
// Decode the text
DispatchQueue.main.async {
self.decodeText(recvText: string)
print("Received text: \(string)")
}
recvString = string
}
func decodeText(recvText: String) {
// If the message is allowed, then pass it to getReadings
print("Decoding")
if recvText.hasPrefix("A=") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .allreadings)
print("All readings received")
} else if recvText.hasPrefix("T = ") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .temperature)
} else if recvText.hasPrefix("P = ") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .pressure)
} else if recvText.hasPrefix("H = ") {
getReadings.parseReceivedStrings(recvText: recvText, readingType: .humidity)
} else {
print("Unrecognised string.")
}
}
}
enum ReadingType {
case allreadings
case temperature
case pressure
case humidity
}
class GetReadings: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
#Published var temp: Float = 0.0 {
willSet {
print("Temp new = " + String(temp))
objectWillChange.send()
}
}
#Published var pressure: Float = 0.0 {
willSet {
print("Pressure new = " + String(pressure))
objectWillChange.send()
}
}
#Published var humidity: Float = 0.0 {
willSet {
print("Humidity new = " + String(humidity))
objectWillChange.send()
}
}
func getAll() {
//print(readings.count)
//print(readings.count)
wsManager.socket.write(string: "get_all")
}
func parseReceivedStrings (recvText: String, readingType: ReadingType) {
if readingType == .allreadings {
// Drop first two characters
let tempText = recvText.dropFirst(2)
// Split the string into components
let recvTextArray = tempText.components(separatedBy: ",")
// Deal with the temperature
temp = (recvTextArray[0] as NSString).floatValue
// Pressure
pressure = (recvTextArray[1] as NSString).floatValue
// Humidity
humidity = (recvTextArray[2] as NSString).floatValue
}
}
}
When the values are parsed, I would expect the values in the ReadingsView to update instantly, as I have marked the variables as #Published, as well as using the objectWillChange property to manually push the changes. The print statements within the willSet parameters reflect the new values, but the text does not update. In the ReadingsView code, I have compensated for this by manually calling the parseReceivedString method when the refresh button is pressed (this is used as part of the WebSocket protocol to send the request), but this causes the readings to be one step behind where they should be. Ideally, I would want the readings to update instantly once they have been parsed in the method described in the previous paragraph.
struct ReadingsView: View {
#ObservedObject var getReadings: GetReadings
var body: some View {
VStack {
Text(String(self.getReadings.temp))
Text(String(self.getReadings.pressure))
Text(String(self.getReadings.humidity))
Button(action: {
print("Button clicked")
self.getReadings.getAll()
self.getReadings.parseReceivedStrings(recvText: wsManager.recvString, readingType: .allreadings)
}) {
Image(systemName: "arrow.clockwise.circle.fill")
.font(.system(size: 30))
}
.padding()
}
}
}
I am wondering whether I have used the right declarations or whether what I am trying to do is incompatible with using multiple classes - this is my first time using SwiftUI so I may be missing a few nuances. Thank you for your help in advance.
Edited - added code
ContentView
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection){
ReadingsView(getReadings: GetReadings())
.tabItem {
VStack {
Image(systemName: "thermometer")
Text("Readings")
}
} .tag(0)
SetupView()
.tabItem {
VStack {
Image(systemName: "slider.horizontal.3")
Text("Setup")
}
}
.tag(1)
}
}
}
If you are using ObservableObject you don't need to write objectWillChange.send() in willSet of your Published properties.
Which means you can as well remove:
let objectWillChange = ObservableObjectPublisher()
which is provided by default in ObservableObject classes.
Also make sure that if you're updating your #Published properties you do it in the main queue (DispatchQueue.main). Asynchronous requests are usually performed in background queues and you may try to update your properties in the background which will not work.
You don't need to wrap all your code in DispatchQueue.main - just the part which updates the #Published property:
DispatchQueue.main.async {
self.humidity = ...
}
And make sure you create only one GetReadings instance and share it across your views. For that you can use an #EnvironmentObject.
In the SceneDelegate where you create your ContentView:
// create GetReadings only once here
let getReadings = GetReadings()
// pass it to WSManager
// ...
// pass it to your views
let contentView = ContentView().environmentObject(getReadings)
Then in your ReadingsView you can access it like this:
#EnvironmentObject var getReadings: GetReadings
Note that you don't need to create it in the TabView anymore:
TabView(selection: $selection) {
ReadingsView()
...
}

updating a view with changing variables

I have this program with SwiftUI. The program is for calculating the bedtime using machine learning based on 3 user inputs. I have a Text("") showing users their updated bedtime.
I want the program to update the bedtime automatically and display it on my Text(""). I tried many methods and none seems to work. What I tried so far
onAppear - only updates once bedtime when the program first runs
onTapGesture - only updates the bedtime when tapping on the picker (scrolling the picker doesn't work), and it somehow hinders updating the stepper (clicking +/- doesn't change the hours)
using didSet with class conforming to observableObject, #Pulished vars in the class and #ObservedObject in the view struct. Didn't work as well but I tried it only when the class has default values
using didSet in the struct - it didn't update bedtime
Does anyone know if there's an easier way to have the bedtime updated however the user scrolls the picker and whenever a variable changes?
UI looks for detail
struct ContentView: View {
static var defaultWakeUpTime : Date {
var defaultTime = DateComponents()
defaultTime.hour = 7
defaultTime.minute = 0
return Calendar.current.date(from: defaultTime) ?? Date()
}
#State private var wakeUp = defaultWakeUpTime
#State private var sleepAmount = 8.0
#State private var coffeeAmount = 0 {
didSet {
calculateSleepTime()
}
}
#State private var showTime : String = " "
func calculateSleepTime() {**CONTENT**}
var body: some View {
NavigationView {
VStack {
Spacer(minLength: 20)
Text("Your optimum sleep time is \(showTime)")
Spacer(minLength: 10)
Section {
Text("When do you want to wake up?")
.font(.headline)
DatePicker("Please choose a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
.labelsHidden()
.datePickerStyle(WheelDatePickerStyle())
}
Spacer()
Form {
Text("How many hours would you like to sleep?")
.font(.headline)
Stepper(value: $sleepAmount, in: 4...12, step: 0.25) {
Text("\(sleepAmount, specifier: "%g" ) hours")
}
}
Spacer()
Section {
Text("How many cups of coffee do you drink?")
.font(.headline)
Picker("Coffee Selector", selection: $coffeeAmount) {
ForEach (1..<21) {
Text("\($0) " + "Cup")
}
}
.labelsHidden()
}
}
.navigationBarTitle(Text("BetterSleep"))
.onAppear(perform: calculateSleepTime)
}
}
}
I would use a viewModel and use subscriptions to track values and calculate sleep time.
Change your ContentView at the top to this
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
Now precede any variables with viewModel.
Create a new .swift file I just called it ViewModel but you don't have to.
import Combine
final class ViewModel: ObservableObject {
#Published private(set) var bedTime: String = ""
#Published var wakeUp: Date = Date()
#Published var sleepAmount: Double = 8.0
#Published var coffeeAmount = 0
private var cancellables = Set<AnyCancellable>()
init() {
$wakeUp
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.calculateSleepTime()
}.store(in: &cancellables)
$sleepAmount
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.calculateSleepTime()
}.store(in: &cancellables)
$coffeeAmount
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.calculateSleepTime()
}.store(in: &cancellables)
}
private func calculateSleepTime() {
// Your Logic
self.bedTime =
}
}
Now anytime one of the values changes the suggested bedtime will update. Remember to add one to the coffeeAmount as it starts at 0.

How to Add Max length for a character for Swift UI

Hi i am creating a to do application and i am facing a problem when a user entering some characters to a UIText field i remember there was a way in SWIFT 5 to put a max length but i can't find one in SWIFT UI can someone send me a link or guide me step by step HOW CAN I ADD A MAX LENTGH TO A SWIFT UI PROJECT TO THE TEXT FIELD! THANKS
I tried to find it Everywhere but i can't
struct NewTaskView: View {
var taskStore: TaskStore
#Environment(\.presentationMode) var presentationMode
#State var text = ""
#State var priority: Task.Priority = .Низкий
var body: some View {
Form {
TextField("Название задания", text: $text)
VStack {
Text("Приоритет")
.multilineTextAlignment(.center)
Picker("Priority", selection: $priority.caseIndex) {
ForEach(Task.Priority.allCases.indices) { priorityIndex in
Text(
Task.Priority.allCases[priorityIndex].rawValue
.capitalized
)
.tag(priorityIndex)
}
}
.pickerStyle( SegmentedPickerStyle() )
}
I want to put max length to a text field where is written TextField("Название задания", text: $text)
It seems like this can be achieved with Combine, by creating a wrapper around the text and opening a 2 way subscription, with the text subscribing to the TextField and the TextField subscribing to the ObservableObject. I'd say the way it works its quite logical from a Reactive point of view but would have liked to find a cleaner solution that didn't require another object to be created.
import SwiftUI
import Combine
class TextBindingManager: ObservableObject {
#Published var text = "" {
didSet {
if text.count > characterLimit && oldValue.count <= characterLimit {
text = oldValue
}
}
}
let characterLimit = 5
}
struct ContentView: View {
#ObservedObject var textBindingManager = TextBindingManager()
var body: some View {
TextField("Placeholder", text: $textBindingManager.text)
}
}
I read this article. please check here
This is my whole code. I don't use EnvironmentObject.
struct ContentView: View {
#ObservedObject private var restrictInput = RestrictInput(5)
var body: some View {
Form {
TextField("input text", text: $restrictInput.text)
}
}
class RestrictInput: ObservableObject {
#Published var text = ""
private var canc: AnyCancellable!
init (_ maxLength: Int) {
canc = $text
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.map { String($0.prefix(maxLength)) }
.assign(to: \.text, on: self)
}
deinit {
canc.cancel()
}
}