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
Understanding Optional Global Variables in Swift
Applying Impulses in Spritekit
Swift Casting Generic to Optional with a Nil Value Causes Fatalerror
Apple Mach-O Linker Error (Static, Not Ld)
Comma Automatically Being Added to Textfield in Swift
Custom Markers Disappear on Zoomin The Map and Appear on Zoomout The Map with Clustering
How to Set a Known Position and Orientation as a Starting Point of Arkit
Error Combining Nscalendarunit with or (Pipe) in Swift 2.0
How to Create Rounded Image with Border and Shadow as Mkannotationview in Swift
Filter, Closure, Functional Syntax Version of for Loop with Multiple Conditions
How Do Uniqueness Constraints as (Comma,Separated,Attributes) Work with Swift in Coredata
Swift 4: How to Asynchronously Use Urlsessiondatatask But Have The Requests Be in a Timed Queue
How to Get Unsaferawpointer on The Swift Object
How to Provide Default Implementation of an Objective-C Protocol in a Swift Protocol Extension
Print Not Working in Swift 3 Extensions
Actions Assigned to Nsmenuitem Dont Seem to Work