How to do network request without clicking at button - ios

I have a textfield and I need that when I write smth. in textfield it sends request to server without clicking at any button. For example if my sending text is the same as server's text it print that text. I am using SwiftUI and Combine. Below is my code.
#State var cancellable: AnyCancellable?
.onReceive(Just($homePageVM.orderNumber)) { res in
if !homePageVM.orderNumber.isEmpty {
cancellable = homePageVM.orderNumber
.publisher
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
homePageVM.getVoucherInfo { result in
if homePageVM.orderNumber == homePageVM.getVoucherInfoModel?.orderNumber {
print(result)
}
}
})
}
}
I am using onreceive because I need to send the data every time i change the text. But in the result i receive an unended cycle. How can i send data only one time every time I change my text? Please help me.

It is very strange to use onReceive with Just and sink, there is no point in using Combine at all. The onChange modifier would replace all of this. However, the best way to do what you need simply with multiple View structs and the task modifier, e.g.
struct View1: View {
...
var body: some View {
View2(orderNumber: model.orderNumber)
}
}
struct View2: View {
let orderNumber: Int
#State var result: String?
// body called when any of the properties change
var body: some View {
Text(result ?? "No result")
.task(id: orderNumber) { newOrderNumber in
result = await fetchVoucherInfo(orderNumber: newOrderNumber)
} // The `task` is started if the orderNumber has changed.
}
}

Related

SwiftUI View Reading a Property with an Async Action

I am building an app where I need to change the SwiftUI View based on a bool property. The problem is that boolean property value depends on the result of an async operation.
Label("\(tweet.likeCount)", systemImage: isTweetLiked ? "heart.fill": "heart")
private var isTweetLiked: Bool {
Task {
return await model.isTweetLikedByCurrentUser(tweet: tweet)
}
}
Cannot convert return expression of type 'Task<Bool, Never>' to return type 'Bool'
How can I accomplish this?
I can only provide limited insight from the code reference you have given.
Here, you would have to wait for async operation to complete then you can change the state of the property to which properties of Label might be binded.
I tested this code locally, It works.
struct ContentView: View {
#State var likeCount: Int = 0
#State var isTweetLiked: Bool = false
var body: some View {
Label("\(likeCount)", systemImage: isTweetLiked ? "heart.fill": "heart").onAppear{
Task {
await updateUsers()
}
}
}
func updateUsers() async {
do {
let isLiked = await model.isTweetLikedByCurrentUser(tweet: tweet)
DispatchQueue.main.async {
likeCount = isLiked ? likeCount + 1 : isLiked
isTweetLiked = isLiked
}
} catch {
print("Oops!")
}
}
}
Certainly, I tested with a sample API, but I have changed the code to resemble your code for ease of understanding.
Also, I am calling the updateUsers from onAppear. Which might not be what you want, you can call it from a place which suits your custom requirements.

"Binding<String> action tried to update multiple times per frame" on TextField with CurrentValueSubject binding

I've been trying to find a good way to connect a TextField with a viewmodel, so that the inputs to the TextField don't immediately update the view.
Here is a working example:
struct TextFieldSamples: View {
#StateObject var vm = TFViewModel()
var body: some View {
print(Self._printChanges())
return VStack {
TextField("placeholder",
text: Binding(
get: { vm.outputText },
set: { vm.subject.value = $0 }
))
Text(vm.outputText) // this will only update when the textfield holds >5 characters
}
}
}
class TFViewModel: ObservableObject {
#Published var outputText: String = ""
let subject = CurrentValueSubject<String, Never>("")
var subscriptions = Set<AnyCancellable>()
init() {
subject
.filter({ $0.count > 5 })
.sink { self.outputText = $0 }
.store(in: &subscriptions)
}
}
However, I came across the propertyWrapped CurrentValueSubject below on the swiftbysundell.com blog
#propertyWrapper
struct Input<Value> {
var wrappedValue: Value {
get { subject.value }
set { subject.send(newValue) }
}
var projectedValue: AnyPublisher<Value, Never> {
subject.eraseToAnyPublisher()
}
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue: Value) {
subject = CurrentValueSubject(wrappedValue)
}
}
Trying to use it, will result in the error Binding<String> action tried to update multiple times per frame.
(Not only that, the input behavior of the TextField gets messed up. This shows especially with Japanese input where the character transformations don't are not working most of the time.)
Here's a example showing this behavior:
struct TextFieldSamples: View {
#StateObject var vm = TFViewModel()
var body: some View {
print(Self._printChanges())
return VStack {
TextField("placeholder", text: $vm.inputText)
Text(vm.outputText)
}
}
}
class TFViewModel: ObservableObject {
#Input var inputText: String = ""
#Published var outputText: String = ""
var subscriptions = Set<AnyCancellable>()
init() {
$inputText
.filter({ $0.count > 5 })
.sink { self.outputText = $0 }
.store(in: &subscriptions)
}
}
I don't quite understand why the PropertyWrapper version is not working as expected.
The issue appears to a combination of the binding to the inputText field of vm and the binding to the vm itself as a #StateObject.
When text is entered, It sends the value to the inputText binding. This allows the system to flag that the TextField needs to be re-evaluated.
But inputText is a struct property of the vm object. Because vm is a #StateObject when inputText changes, the vm object broadcasts that the TextFieldSamples view needs to be re-evaluated. As part of that it also tries to set the value of textInput (to the value it already holds) which causes the "binding fired multiple times in the same frame" message.
You can eliminate the extra time the Binding fires by making vm a #State property instead of #StateObject. Now the change to inputText will fire once, and vm will not broadcast the object change message that tries to fire it the second time.
However, when you do that the system won't know TextFieldSamples needs to be reevaluated so it will never see the change to outputText.

Is there a way to call a function when a SwiftUI Picker selection changes an EnvironmentObject?

I have a SwiftUI Form I'm working with, which updates values of an EnvironmentObject. I need to call a function when the value of a Picker changes, but every way I've found is either really messy, or causes other problems. What I'm looking for is a similar way in which the Slider works, but there doesn't seem to be one. Basic code without any solution is here:
class ValueHolder : ObservableObject {
#Published var sliderValue : Float = 0.5
static let segmentedControlValues : [String] = ["one", "two", "three", "four", "five"]
#Published var segmentedControlValue : Int = 3
}
struct ContentView: View {
#EnvironmentObject var valueHolder : ValueHolder
func sliderFunction() {
print(self.valueHolder.sliderValue)
}
func segmentedControlFunction() {
print(ValueHolder.segmentedControlValues[self.valueHolder.segmentedControlValue])
}
var body: some View {
Form {
Text("\(self.valueHolder.sliderValue)")
Slider(value: self.$valueHolder.sliderValue, onEditingChanged: {_ in self.sliderFunction()
})
Text("\(ValueHolder.segmentedControlValues[self.valueHolder.segmentedControlValue])")
Picker("", selection: self.$valueHolder.segmentedControlValue) {
ForEach(0..<ValueHolder.segmentedControlValues.count) {
Text("\(ValueHolder.segmentedControlValues[$0])")
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
After reviewing this similar (but different) question here: Is there a way to call a function when a SwiftUI Picker selection changes? I have tried using onReceive() as below, but it is also called when the Slider value changes, resulting in unwanted behavior.
.onReceive([self.valueHolder].publisher.first(), perform: {_ in
self.segmentedControlFunction()
})
I have tried changing the parameter for onReceive to filter it by only that value. The value passed is correct, but the segmentedControlFunction still gets called when the slider moves, not just when the picker changes.
.onReceive([self.valueHolder.segmentedControlValue].publisher.first(), perform: {_ in
self.segmentedControlFunction()
})
How can I get the segmentedControlFunction to be called in a similar way to the sliderFunction?
There is much simpler approach, which looks more appropriate for me.
The generic schema as follows
Picker("Label", selection: Binding( // proxy binding
get: { self.viewModel.value }, // get value from storage
set: {
self.viewModel.value = $0 // set value to storage
self.anySideEffectFunction() // didSet function
}) {
// picker content
}
As I was proofing the original question and tinkering with my code I accidentally stumbled across what I think is probably the best solution. So, in case it will help anyone else, here it is:
struct ContentView: View {
#EnvironmentObject var valueHolder : ValueHolder
#State private var sliderValueDidChange : Bool = false
func sliderFunction() {
if self.sliderValueDidChange {
print("Slider value: \(self.valueHolder.sliderValue)\n")
}
self.sliderValueDidChange.toggle()
}
var segmentedControlValueDidChange : Bool {
return self._segmentedControlValue != self.valueHolder.segmentedControlValue
}
#State private var _segmentedControlValue : Int = 0
func segmentedControlFunction() {
self._segmentedControlValue = self.valueHolder.segmentedControlValue
print("SegmentedControl value: \(ValueHolder.segmentedControlValues[self.valueHolder.segmentedControlValue])\n")
}
var body: some View {
Form {
Text("\(self.valueHolder.sliderValue)")
Slider(value: self.$valueHolder.sliderValue, onEditingChanged: {_ in self.sliderFunction()
})
Text("\(ValueHolder.segmentedControlValues[self.valueHolder.segmentedControlValue])")
Picker("", selection: self.$valueHolder.segmentedControlValue) {
ForEach(0..<ValueHolder.segmentedControlValues.count) {
Text("\(ValueHolder.segmentedControlValues[$0])")
}
}.pickerStyle(SegmentedPickerStyle())
.onReceive([self._segmentedControlValue].publisher.first(), perform: {_ in
if self.segmentedControlValueDidChange {
self.segmentedControlFunction()
}
})
}
}
}
Note that the onReceive() is for the a new #State property, which pairs with a Bool evaluating if it's the same as the #EnvironmentObject counterpart. The #State value is then updated on the function call. Also note that in the solution I've changed how the sliderFunction works so that it is only called once, when the #EnvironmentObject value actually changes. It's a bit hacky, but the Slider and Picker both work in the same way when updating values and calling their respective functions.

Is there a way to load a function with a Navigation Link in Swift?

I'd like to know if there is way to call a function along with a NavigationLink in Swift. I have a detail view for a list of posts but in order to get all of the information for that detailed post view I need to call a fetcher function in order to load a bunch of extra information which I cannot make with the initial call as it would largely increase the time to make the initial request for posts. Something like the following, keep in mind this most definitely isn't how it would look just what I envision as how it would work.
List(self.posts) { result in
NavigationLink(call: PostFetchingFunction(PostID: result.ID) -> destination: DetailedPostView(post: PostFetchingFunction.result)) {
Text("Go to detailed post view")
}
}
As I said, this, most definitely isn't correct Swift code, but just a code visualization of what I'd like to do might be helpful.
You could achieve this using a provider pattern conforming to ObservableObject
1. ContentView
struct ContentView: View {
var body: some View {
NavigationView {
List(0...100, id: \.self) { (index) in
NavigationLink("Show \(index)",
destination: NextView(provider: ItemProvider(id: index)))
}
.navigationBarTitle("List")
}
}
}
Destination is NextView
NextView requires something of type ItemProvider for it's initialization (we'll see this later)
2. NextView
struct NextView: View {
#ObservedObject var provider: ItemProvider
var body: some View {
Text(provider.title)
.navigationBarTitle("Item", displayMode: .inline)
.onAppear {
self.provider.load()
}
}
}
ItemProvider is an #ObservedObject which makes it a listener for changes in order to update the view
.onAppear is where we run a funtion, in this case self.provider.load() to get the provider to begin fetching
3. ItemProvider
class ItemProvider: ObservableObject {
private var id: Int
#Published var title: String = ""
init(id: Int) {
self.id = id
}
func load() {
title = "Loading \(id)"
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
guard let _weakSelf = self else { return }
_weakSelf.title = "Loaded \(_weakSelf.id)"
}
}
}
ItemProvider has to conform to ObservableObject in order to emit changes
Any variables within that are marked #Published will emit a change signal
ItemProvider has a load function that actually does the fetching and if it updates any #Published variables, the connected Views will be notified and will update automatically

How to use Combine on a SwiftUI View

This question relates to this one: How to observe a TextField value with SwiftUI and Combine?
But what I am asking is a bit more general.
Here is my code:
struct MyPropertyStruct {
var text: String
}
class TestModel : ObservableObject {
#Published var myproperty = MyPropertyStruct(text: "initialText")
func saveTextToFile(text: String) {
print("this function saves text to file")
}
}
struct ContentView: View {
#ObservedObject var testModel = TestModel()
var body: some View {
TextField("", text: $testModel.myproperty.text)
}
}
Scenario: As the user types into the textfield, the saveTextToFile function should be called. Since this is saving to a file, it should be slowed-down/throttled.
So My question is:
Where is the proper place to put the combine operations in the code below.
What Combine code do I put to accomplish: (A) The string must not contain spaces. (B) The string must be 5 characters long. (C) The String must be debounced/slown down
I wanted to use the response here to be a general pattern of: How should we handle combine stuff in a SwiftUI app (not UIKit app).
You should do what you want in your ViewModel. Your view model is the TestModel class (which I suggest you rename it in TestViewModel). It's where you are supposed to put the logic between the model and the view. The ViewModel should prepare the model to be ready for the visualization. And that is the right place to put your combine logic (if it's related to the view, of course).
Now we can use your specific example to actually make an example. To be honest there are a couple of slight different solutions depending on what you really want to achieve. But for now I'll try to be as generic as possible and then you can tell me if the solution is fine or it needs some refinements:
struct MyPropertyStruct {
var text: String
}
class TestViewModel : ObservableObject {
#Published var myproperty = MyPropertyStruct(text: "initialText")
private var canc: AnyCancellable!
init() {
canc = $myproperty.debounce(for: 0.5, scheduler: DispatchQueue.main).sink { [unowned self] newText in
let strToSave = self.cleanText(text: newText.text)
if strToSave != newText.text {
//a cleaning has actually happened, so we must change our text to reflect the cleaning
self.myproperty.text = strToSave
}
self.saveTextToFile(text: strToSave)
}
}
deinit {
canc.cancel()
}
private func cleanText(text: String) -> String {
//remove all the spaces
let resultStr = String(text.unicodeScalars.filter {
$0 != " "
})
//take up to 5 characters
return String(resultStr.prefix(5))
}
private func saveTextToFile(text: String) {
print("text saved")
}
}
struct ContentView: View {
#ObservedObject var testModel = TestViewModel()
var body: some View {
TextField("", text: $testModel.myproperty.text)
}
}
You should attach your own subscriber to the TextField publisher and use the debounce publisher to delay the cleaning of the string and the calling to the saving method. According to the documentation:
debounce(for:scheduler:options:)
Use this operator when you want to wait for a pause in the delivery of
events from the upstream publisher. For example, call debounce on the
publisher from a text field to only receive elements when the user
pauses or stops typing. When they start typing again, the debounce
holds event delivery until the next pause.
When the user stops typing the debounce publisher waits for the specified time (in my example here above 0.5 secs) and then it calls its subscriber with the new value.
The solution above delays both the saving of the string and the TextField update. This means that users will see the original string (the one with spaces and maybe longer than 5 characters) for a while, before the update happens. And that's why, at the beginning of this answer, I said that there were a couple of different solutions depending on the needs. If, indeed, we want to delay just the saving of the string, but we want the users to be forbidden to input space characters or string longer that 5 characters, we can use two subscribers (I'll post just the code that changes, i.e. the TestViewModel class):
class TestViewModel : ObservableObject {
#Published var myproperty = MyPropertyStruct(text: "initialText")
private var saveCanc: AnyCancellable!
private var updateCanc: AnyCancellable!
init() {
saveCanc = $myproperty.debounce(for: 0.5, scheduler: DispatchQueue.main)
.map { [unowned self] in self.cleanText(text: $0.text) }
.sink { [unowned self] newText in
self.saveTextToFile(text: self.cleanText(text: newText))
}
updateCanc = $myproperty.sink { [unowned self] newText in
let strToSave = self.cleanText(text: newText.text)
if strToSave != newText.text {
//a cleaning has actually happened, so we must change our text to reflect the cleaning
DispatchQueue.main.async {
self.myproperty.text = strToSave
}
}
}
}
deinit {
saveCanc.cancel()
updateCanc.cancel()
}
private func cleanText(text: String) -> String {
//remove all the spaces
let resultStr = String(text.unicodeScalars.filter {
$0 != " "
})
//take up to 5 characters
return String(resultStr.prefix(5))
}
private func saveTextToFile(text: String) {
print("text saved: \(text)")
}
}

Resources