Onreceive String.Publisher Lead to Infinite Loop

onReceive String.publisher lead to infinite loop

Hopy, you need one source of true. If you don't like to use your model, the equivalent code with State / Binding pair could looks like

struct ContentView: View {
@State var name: String = ""
@State var flag = false
var body: some View {
let subject = CurrentValueSubject<String, Never>(name)
return VStack {
TextField("Input Name", text: $name).textFieldStyle(RoundedBorderTextFieldStyle()).padding()
.onReceive(subject) { name in
print("change to \(name)")
self.flag.toggle() // toggle every char typing
}
}
}
}

In your example I disable (see the commented line) the default "request" in model

import SwiftUI
import Combine

class Model: ObservableObject{
var someBool = false {
willSet {
print("will change to", newValue)
print("ask SwiftUI to update from model")
//self.objectWillChange.send()
}
didSet {
print(oldValue, "changed")
}
}
}

struct ContentView: View {
@State var name = ""
@StateObject var model = Model()

var body: some View {
VStack {
TextField("Input Name", text: $name).textFieldStyle(RoundedBorderTextFieldStyle())

.onReceive(name.publisher.reduce("", {t,c in
t + String(c)
})) {text in
print("change to \(text)")
self.model.someBool.toggle()
}
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

while typing it prints

true changed
change to Qw
will change to true
ask SwiftUI to update from model
false changed
change to Qwe
will change to false
ask SwiftUI to update from model
true changed
change to Qwer
will change to true
ask SwiftUI to update from model
false changed
change to Qwert
will change to false
ask SwiftUI to update from model
true changed
...

Now uncomment the line in your model

class Model: ObservableObject{
var someBool = false {
willSet {
print("will change to", newValue)
print("ask SwiftUI to update from model")
self.objectWillChange.send()
}
didSet {
print(oldValue, "changed")
}
}
}

and run it again ...
it will print in infinite loop

...
change to
will change to true
ask SwiftUI to update from model
false changed
change to
will change to false
ask SwiftUI to update from model
true changed
change to
will change to true
ask SwiftUI to update from model
false changed
change to
will change to false
ask SwiftUI to update from model
true changed
change to
...

your model changes, SwiftUI is reevaluating its body and due this the model changes again ... in a loop.

The minimal loop example

import SwiftUI
import Combine

class Model: ObservableObject {
@Published var flag = false
}
struct ContentView: View {
@StateObject var model = Model()
var body: some View {
Color.yellow
.onReceive(model.$flag) {_ in
print(".")
self.model.flag.toggle()
}
}
}

Get value change update on Binding in SwiftUI

Use this instead:

.onChange(of: pinValue) { output in
print(output)
}

How can I get data from ObservedObject with onReceive in SwiftUI?

First in your view you need to request the HeadingProvider to start updating heading. You need to listen to objectWillChange notification, the closure has one argument which is the new value that is being set on ObservableObject.

I have changed your Compass a bit:

struct Compass: View {

@StateObject var headingProvider = HeadingProvider()
@State private var angle: CGFloat = 0

var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.modifier(RotationEffect(angle: angle))
.onReceive(self.headingProvider.objectWillChange) { newHeading in
withAnimation(.easeInOut(duration: 1.0)) {
self.angle = newHeading
}
}

Text(String("\(angle)"))
.font(.system(size: 20))
.fontWeight(.light)
.padding(.top, 15)
} .onAppear(perform: {
self.headingProvider.updateHeading()
})
}
}

I have written an example HeadingProvider:

public class HeadingProvider: NSObject, ObservableObject {

public let objectWillChange = PassthroughSubject<CGFloat,Never>()

public private(set) var heading: CGFloat = 0 {
willSet {
objectWillChange.send(newValue)
}
}

private let locationManager: CLLocationManager

public override init(){
self.locationManager = CLLocationManager()
super.init()
self.locationManager.delegate = self
}

public func updateHeading() {
locationManager.startUpdatingHeading()
}
}

extension HeadingProvider: CLLocationManagerDelegate {

public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
DispatchQueue.main.async {
self.heading = CGFloat(newHeading.trueHeading)
}
}
}

Remember you need to handle asking for permission to read user's location and you need to call stopUpdatingHeading() at some point.

Swift Combine: Using timer publisher in an observable object

This is a bit different to your original but nothing important is changed I hope.

import Combine
import SwiftUI

class TimerViewModel: ObservableObject {
private var assignCancellable: AnyCancellable? = nil

@Published var tick: String = "0:0:0"

init() {
assignCancellable = Timer.publish(every: 1.0, on: .main, in: .default)
.autoconnect()
.map { String(describing: $0) }
.assign(to: \TimerViewModel.tick, on: self)
}
}

struct ContentView: View {
@State private var currentTime: String = "Initial"
@ObservedObject var viewModel = TimerViewModel()

var body: some View {
VStack {
Text(currentTime)
Text(viewModel.tick) // why doesn't this work?
}
.onReceive(Timer.publish(every: 0.9, on: .main, in: .default).autoconnect(),
perform: {
self.currentTime = String(describing: $0)
}
)
}
}

I made viewModel an ObservedObject just to simplify the code.

The Timer.publish method along with autoconnect make Timer easier to use. I have found that using the same publisher with multiple subscribers causes problems as the first cancel kills the publisher.

I removed the deinit() as the cancel seems to be implicit for subscribers.

There was an interference between updates from onReceive and viewModel but changing the onReceive to 0.9 fixed that.

Finally I have discovered that the print() method in Combine is very useful for watching pipelines.

SwiftUI with NotificationCenter publishers

@State is not ready yet in init, so it cannot be used for such purposes. The approach can be as follows:

var cancellables = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)
.sink(receiveValue: { _ in
print(">> in init")
})
.store(in: &cancellables)
}

in such defined cancellables you can store all subscribers created in init, but you will not be able to use it later in code, but this approach is good for once defined notification handlers.

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

If the @State value will be used in a View, you don't need extra variable name

  struct BuilderPicker: View {
// let name: String = ""
let options: Array<String> = ["1", "2","3","4","5"]
@State var selectedOption = 0
var body: some View {
HStack {
Text(options[selectedOption])
.font(.body)
.padding(.leading, 10)
Picker(selection: $selectedOption, label: Text(options[selectedOption])) {
ForEach(0 ..< options.count) {
Text(self.options[$0]).tag($0)
}
}.pickerStyle(SegmentedPickerStyle())
.padding(.trailing, 25)}
// }.onTapGesture {
// self.selectedOption = self.selectedOption == 0 ? 1 : 0
// }
.padding(.init(top: 10, leading: 10, bottom: 10, trailing: 0))
.border(Color.secondary, width: 3)
.padding(.init(top: 0, leading: 15, bottom: 0, trailing: 15))
.font(.body)
}

}

If you need separated operation on the @State, the simplest way is adding one line : onReceive() to the view.

  HStack {
Text("")
.font(.body)
.padding(.leading, 10)
Picker(selection: $selectedOption, label: Text("")) {
ForEach(0 ..< options.count) {
Text(self.options[$0]).tag($0)
}
}.pickerStyle(SegmentedPickerStyle())
.padding(.trailing, 25)}
// }.onTapGesture {
// self.selectedOption = self.selectedOption == 0 ? 1 : 0
// }
.padding(.init(top: 10, leading: 10, bottom: 10, trailing: 0))
.border(Color.secondary, width: 3)
.padding(.init(top: 0, leading: 15, bottom: 0, trailing: 15))
.font(.body)
.onReceive([self.selectedOption].publisher.first()) { (value) in
print(value)
}


Related Topics



Leave a reply



Submit