Combine #Published property send values also when not updated - swift

I was trying to create a dynamic Form using SwiftUI and Combine, that loads options of an input (in the example, number) based on another input (in the example, myString).
The problem is that the Combine stack get executed continuously, making lots of network requests (in the example, simulated by the delay), even if the value is never changed.
I think that the expected behavior is that $myString publishes values only when it changes.
class MyModel: ObservableObject {
// My first choice on the form
#Published var myString: String = "Jhon"
// My choice that depends on myString
#Published var number: Int?
var updatedImagesPublisher: AnyPublisher<Int, Never> {
return $myString
.removeDuplicates()
.print()
.flatMap { newImageType in
return Future<Int, Never> { promise in
print("Executing...")
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let newNumber = Int.random(in: 1...200)
return promise(.success(newNumber))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
struct ContentView: View {
#ObservedObject var model: MyModel = MyModel()
var body: some View {
Text("\(model.number ?? -100)")
.onReceive(model.updatedImagesPublisher) { newNumber in
self.model.number = newNumber
}
}
}

The problem is the updatedImagesPublisher is a computed property. It means that you create a new instance every time you access it. What happens in your code. The Text object subscribes to updatedImagesPublisher, when it receives a new value, it updates the number property of the Model. number is #Published property, it means that objectWillChange method will be called every time you change it and the body will be recreated. New Text will subscribe to new updatedImagesPublisher (because it is computed property) and receive the value again. To avoid such behaviour just use lazy property instead of computed property.
lazy var updatedImagesPublisher: AnyPublisher<Int, Never> = {
return $myString
.removeDuplicates()
.print()
.flatMap { newImageType in
return Future<Int, Never> { promise in
print("Executing...")
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let newNumber = Int.random(in: 1...200)
return promise(.success(newNumber))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}()

I assume it is because you create new publisher for every view update, try the following instead. (Tested with Xcode 11.4)
class MyModel: ObservableObject {
// My first choice on the form
#Published var myString: String = "Jhon"
// My choice that depends on myString
#Published var number: Int?
lazy var updatedImagesPublisher: AnyPublisher<Int, Never> = {
return $myString
.removeDuplicates()
.print()
.flatMap { newImageType in
return Future<Int, Never> { promise in
print("Executing...")
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let newNumber = Int.random(in: 1...200)
return promise(.success(newNumber))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}()
}

Related

Publishing and Consuming a transcript from SFSpeechRecognizer

I'm using Apple's example of an Observable wrapper around SFSpeechRecognizer as follows:
class SpeechRecognizer: ObservableObject {
#Published var transcript: String
func transcribe() {}
}
The goal is to use a ViewModel to both consume the transcript as it is generated, as well as passing on the value to a SwiftUI View for visual debugging:
class ViewModel : ObservableObject {
#Published var SpeechText: String = ""
#ObservedObject var speech: SpeechRecognizer = SpeechRecognizer()
public init() {
speech.transcribe()
speech.transcript.publisher
.map { $0 as! String? ?? "" }
.sink(receiveCompletion: {
print ($0) },
receiveValue: {
self.SpeechText = $0
self.doStuff(transcript: $0)
})
}
private void doStuffWithText(transcript: String) {
//Process the output as commands in the application
}
}
I can confirm that if I observe transcript directly in a SwiftUI view, that the data is flowing through. My problem is receiving the values as they change, and then assigning that data to my own published variable.
How do I make this work?
Subscription should be stored otherwise it is canceled immediately, also you need to make subscription before actual usage (and some other memory related modifications made). So I assume you wanted something like:
class ViewModel : ObservableObject {
#Published var SpeechText: String = ""
var speech: SpeechRecognizer = SpeechRecognizer() // << here !!
private var subscription: AnyCancellable? = nil // << here !!
public init() {
self.subscription = speech.transcript.publisher // << here !!
.map { $0 as! String? ?? "" }
.sink(receiveCompletion: {
print ($0) },
receiveValue: { [weak self] value in
self?.SpeechText = value
self?.doStuffWithText(transcript: value)
})
self.speech.transcribe() // << here !!
}
private func doStuffWithText(transcript: String) {
//Process the output as commands in the application
}
}
Tested with Xcode 13.2

Easier way of dealing with CurrentValueSubject

I have a Complex class which I pass around as an EnvironmentObject through my SwiftUI views. Complex contains several CurrentValueSubjects. I don't want to add the Published attribute to the publishers on class Complex, since Complex is used a lot around the views and that will force the views to reload on every published value.
Instead, I want a mechanism which can subscribe to specific publisher which Complex holds. That way, Views can choose on which publisher the view should re-render itself.
The code below works, but I was wondering if there was an easier solution, it feels like a lot of work just to listen to the updates CurrentValueSubject gives me:
import SwiftUI
import Combine
struct ContentView: View {
let complex = Complex()
var body: some View {
PublisherView(boolPublisher: .init(publisher: complex.boolPublisher))
.environmentObject(complex)
}
}
struct PublisherView: View {
#EnvironmentObject var complex: Complex
#ObservedObject var boolPublisher: BoolPublisher
var body: some View {
Text("\(String(describing: boolPublisher.publisher))")
}
}
class Complex: ObservableObject {
let boolPublisher: CurrentValueSubject<Bool, Never> = .init(true)
// A lot more...
init() {
startToggling()
}
func startToggling() {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [unowned self] in
let newValue = !boolPublisher.value
print("toggling to \(newValue)")
boolPublisher.send(newValue)
startToggling()
}
}
}
class BoolPublisher: ObservableObject {
private var cancellableBag: AnyCancellable? = nil
#Published var publisher: Bool
init(publisher: CurrentValueSubject<Bool, Never>) {
self.publisher = publisher.value
cancellableBag = publisher
.receive(on: DispatchQueue.main)
.sink { [weak self] value in
self?.publisher = value
}
}
}

How do I bind information received via an API to different views?

I am working on a project of my first application. I have a REST API that provides information that should be used to update the state of various views. But I have not yet managed to find a solution that satisfies me in all respects. First of all, I am not sure if I am choosing the right approach at all. I know that it would be very inefficient to fetch information from the server every time a view is updated and that the use of a cache is therefore essential. Since the topic is already very complex for me, I'll leave out everything that has to do with CACHE for now.
Enough talk, I'll just walk you through my code.
Here is the core of my API. The enum endpoint specifies the desired request. It can be assumed that each request has a different return value type. For example, in the cases of my stripped down API version: String, Object2(class), Object3(class).
struct GetInformationAPI {
static var shared = GetInformationAPI()
enum Endpoint {
static let baseUrl = URL(string: "https://localhost:8080/getInformation/")!
case getUsername
case getObject2
case getObject3
var request: URLRequest {
var request = URLRequest(url: Endpoint.baseUrl)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(ViewNavigatorModel.shared.token.value, forHTTPHeaderField: "Authorization")
switch self {
case .getUsername:
request.url = Endpoint.baseUrl.appendingPathComponent("username")
return request
case .getObject2:
request.url = Endpoint.baseUrl.appendingPathComponent("Object2")
return request
case .getObject3:
request.url = Endpoint.baseUrl.appendingPathComponent("Object3")
return request
}
}
}
let sharedJSONDecoder = JSONDecoder()
private let apiQueue = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)
}
To get an answer to the requested query, I would normally use a method like the following for each case:
func getObject2() -> AnyPublisher<Object2, Never> {
URLSession.shared.dataTaskPublisher(for: Endpoint.getObject2.request)
.receive(on: apiQueue)
.map{ $0.0 }
.decode(type: Object2.self, decoder: sharedJSONDecoder)
.catch{_ in Empty() }
.eraseToAnyPublisher()
}
Then I would call it in the specific view on appearance and bind it to a #Propertywrapper.
However, I am looking for a solution for any number of requests, each with any response value return type. So I tried this:
func getInformation_2<Input: ObservableObjectConvertable, Output: ObservableObject>(for using: Endpoint, input: Input) -> AnyPublisher<Output, Never> {
URLSession.shared.dataTaskPublisher(for: using.request)
.receive(on: apiQueue)
.map{ $0.0 }
.decode(type: Input.self, decoder: sharedJSONDecoder)
.catch{_ in Empty() }
.map{$0.toObservableObject() }
.eraseToAnyPublisher()
}
As you can see, I made the function generic, with the input being convertible to ObservableObject. I thought this would make it easier to link to the views.
Then when called in the different views, Enpoint needs to be specified, the return type, as well as an example of the Input needs to be set. ( I didn't know how else to declare the Input type).
But from there I'm not really getting anywhere, I found a solution where I catch the ObservableObject that is spit out by this Publisher and in turn it is packed into another ObservableObject. I don't think that's quite the point.
Here is my further, hopefully not too confusing code.
protocol ObservableObjectConvertable: Codable {
func toObservableObject<T: ObservableObject>() -> T
}
struct Object2: ObservableObjectConvertable {
func toObservableObject<T>() -> T where T : ObservableObject {
return Object2OO(theValue: value) as! T
}
var value: Int
}
class Object2OO: ObservableObject {
static var uninitialized = Object2OO()
var uninitialized: Bool
#Published var value: Int
init(theValue: Int) { value = theValue; uninitialized = false}
init() { uninitialized = true; value = -1}
}
struct View2: View {
#StateObject var view2Model = View2Model()
var body: some View {
VStack{
if(view2Model.object2oo.uninitialized) {
Text("...")
} else {
View2Inside(object2oo: view2Model.object2oo)
}
}.onAppear(perform: getInformation)
}
func getInformation() {
view2Model.getInformation()
}
}
struct View2Inside: View {
#ObservedObject var object2oo: Object2OO
var body: some View {
Text("object 2 has the value: \(object2oo.value)")
}
}
class View2Model: ObservableObject {
#Published var object2oo = Object2OO()
var subscriptions = Set<AnyCancellable>()
func getInformation() {
let publisher: AnyPublisher<Object2OO, Never> = GetInformationAPI.shared.getInformation_2(for: .getObject2, input: Object2(value: 1) )
publisher
.sink(
receiveValue: { print("value received: \($0.value)")
self.object2oo = $0
})
.store(in: &subscriptions)
}
}
Maybe someone can shed some light for me in all this darkness... :)

Unable to use a defined state variable in the init()

I am trying to implement a search bar in my app, as now I want to use the keyword typed in the search bar to make an API call to fetch backend data, here is my code:
struct SearchView: View {
#State private var searchText : String=""
#ObservedObject var results:getSearchList
init(){
results = SearchList(idStr: self.searchText)
}
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
}.navigationBarTitle(Text("Search"))
}
}
}
I implement SearchBar view followed the this tutorial https://www.appcoda.com/swiftui-search-bar/ exactly,
and getSearchList is a class which has an var called idStr,
struct searchResEntry: Codable, Identifiable{
var id:Int
var comment:String
}
class SearchList: ObservableObject {
// 1.
#Published var todos = [searchResEntry]()
var idStr: String
init(idStr: String) {
self.idStr = idStr
let url = URL(string: "https://..." + idStr)!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
// 3.
let decodedData = try JSONDecoder().decode([searchResEntry].self, from: todoData)
DispatchQueue.main.async {
self.todos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
the problem I am struggling now is that I want to use the variable searchText to initialize the getSearchList , getSearchList has an var called idStr, this idStr is to used to store the typed keyword, my code always get an error: 'self' used before all stored properties are initialized , I have no idea how to deal with this.
Here is your code, edited by me:
struct SearchView: View {
#StateObject var results = SearchList()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $results.searchText)
}.navigationBarTitle(Text("Search"))
}
}
}
struct SearchResEntry: Codable, Identifiable {
var id:Int
var backdrop_path:String
}
class SearchList: ObservableObject {
#Published var todos = [SearchResEntry]()
#Published var searchText: String = ""
var cancellable: AnyCancellable?
init() {
cancellable = $searchText.debounce(
for: .seconds(0.2),
scheduler: RunLoop.main
).sink { _ in
self.performSearch()
}
}
func performSearch() {
if let pathParam = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
let url = URL(string: "https://hw9node-310902.uc.r.appspot.com/mutisearch/\(pathParam)") {
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
let decodedData = try JSONDecoder().decode([SearchResEntry].self, from: todoData)
DispatchQueue.main.async {
self.todos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
} else {
print("Invalid URL")
}
}
}
Explanation
You are free to reverse the optional changes i made, but here are my explanations:
Use capital letter at the beginning of a Type's name. e.g write struct SearchResEntry, don't write struct searchResEntry. This is convention. Nothing big will happen if you don't follow conventions, but if anyone other than you (or maybe even you in 6 months) look at that code, chances are they go dizzy.
Dont start a Type's name with verbs like get! Again, this is just a convention. If anyone sees a getSomething() or even GetSomething() they'll think thats a function, not a Type.
Let the searchText be a published property in your model that performs the search. Don't perform search on init, instead use a function so you can initilize once and perform search any time you want (do results.performSearch() in your View). Also you can still turn your searchText into a binding to pass to your search bar (look at how i did it).
EDIT answer to your comment
I could right-away think of 3 different answers to your comment. This is the best of them, but also the most complicated one. Hopefully i chose the right option:
As you can see in the class SearchList i've added 2 things. First one is a cancellable to store an AnyCancellable, and second is the thing in init() { ... }. In init, we are doing something which results in an AnyCancellable and then we are storing that in the variable that i added.
What am i doing In init?
first $searchText gives us a Publisher. Basically, the publisher is called whenever the searchText value changes. Then you see .debounce(for: .seconds(0.2), on: RunLoop.main) which means only let the latest input go through and reach the next thing (the next thing is .sink { } as you can see), only if the user has stopped writing for 0.2 seconds. This is very helpful to avoid a load of requests to the server which can eventually make servers give you a 429 Too Many Requests error if many people are using your app (You can remove the whole .debounce thing if you don't like it). And the last thing is .sink { } which when any value reaches that point, it'll call the performSearch func for you and new results will be acquired from the server.
Alternative way
(again talking about your comment)
This is the simpler way. Do as follows:
remove init() { ... } completely if you've added it
remove var cancellable completely if you've added it
in your SearchView, do:
.onChange(of: results.searchText) { _ in
results.performSearch()
}
pretty self-explanatory; it'll perform the search anytime the searchText value is changed.

Using Combine to parse phone number String

I'm trying to wrap my mind around how Combine works. I believe I'm doing something wrong when I use the .assign operator to mutate the #Published property I'm operating on. I've read the documentation on Publishers, Subscribers, and Operators. But I'm a bit loose on where exactly to create the Publisher if I don't want it to be a function call.
import SwiftUI
import Combine
struct PhoneNumberField: View {
let title: String
#ObservedObject var viewModel = ViewModel()
var body: some View {
TextField(title,text: $viewModel.text)
}
class ViewModel: ObservableObject {
#Published var text: String = ""
private var disposables = Set<AnyCancellable>()
init() {
$text.map { value -> String in
self.formattedNumber(number: value)
}
//something wrong here
.assign(to: \.text, on: self)
.store(in: &disposables)
}
func formattedNumber(number: String) -> String {
let cleanPhoneNumber = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let mask = "+X (XXX) XXX-XXXX"
var result = ""
var index = cleanPhoneNumber.startIndex
for ch in mask where index < cleanPhoneNumber.endIndex {
if ch == "X" {
result.append(cleanPhoneNumber[index])
index = cleanPhoneNumber.index(after: index)
} else {
result.append(ch)
}
}
return result
}
}
}
struct PhoneNumberParser_Previews: PreviewProvider {
static var previews: some View {
PhoneNumberField(title: "Phone Number")
}
}
Use .receive(on:):
$text.map { self.formattedNumber(number: $0) }
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.text = value
})
.store(in: &disposables)
This will allow you to listen to changes of the text variable and update it in the main queue. Using main queue is necessary if you want to update #Published variables read by some View.
And to avoid having a retain cycle (self -> disposables -> assign -> self) use sink with a weak self.