How can I replace addObserver with publisher in receiving notifications from NotificationCenter - swift

Currently I am using addObserver method to receive my wished notification in my ObservableObject, from the other hand we can use publisher method to receive wished notification in a View, I like to use publisher method inside my ObservableObject instead of addObserver method, how could I do that? How can I receive/notified the published value from publisher in my class?
import SwiftUI
struct ContentView: View {
#StateObject var model: Model = Model()
var body: some View {
Text("Hello, world!")
.padding()
.onReceive(orientationDidChangedPublisher) { _ in
print("orientationDidChanged! from View")
}
}
}
var orientationDidChangedPublisher = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
class Model: ObservableObject {
init() { orientationDidChangeNotification() }
private func orientationDidChangeNotification() { NotificationCenter.default.addObserver(self, selector: #selector(readScreenInfo), name: UIDevice.orientationDidChangeNotification, object: nil) }
#objc func readScreenInfo() { print("orientationDidChanged! from ObservableObject") }
}

You can use the same publisher(for:) inside your class:
import Combine
class Model: ObservableObject {
var cancellable : AnyCancellable?
init() {
cancellable = NotificationCenter.default
.publisher(for: UIDevice.orientationDidChangeNotification)
.sink(receiveValue: { (notification) in
//do something with that notification
print("orientationDidChanged! from ObservableObject")
})
}
}

There are two ways. First using the Combine framework (don't forget to import Combine), and the other using the usual way.
// Without Combine:
class MyClass1 {
let notification = NotificationCenter.default
.addObserver(forName: UIDevice.orientationDidChangeNotification,
object: nil, queue: .main) { notification in
// Do what you want
let orientationName = UIDevice.current.orientation.isPortrait ? "Portrait" : "Landscape"
print("DID change rotation to " + orientationName)
}
deinit {
NotificationCenter.default.removeObserver(notification)
}
}
// Using Combine
class MyClass2 {
let notification = NotificationCenter.default
.publisher(for: UIDevice.orientationDidChangeNotification)
.sink { notification in
// Do What you want
let orientationName = UIDevice.current.orientation.isPortrait ? "Portrait" : "Landscape"
print("DID change rotation to " + orientationName)
}
deinit {
notification.cancel()
}
}
In deinit you should remove all the observers, like i've shown above.
EDIT:
more about deinit:
deinit is the opposite side of init. It is called whenever your instance of the class is about to be removed from the memory.
If you are using the Combine way, it is fine to don't use deinit because as soon as the instance of the class is removed form the memory, the notification which is of type AnyCancelleable is removed from the memory as well, and that results in the notification being cancelled automatically.
But that automatic cancellation doesnt happen when you are using the normal way, and if you dont remove the observer you added, you'll have multiple observers listening to the notification. For example if you delete the deinit in the MyClass1, you'll see that the "DID change rotation to " is typed more than once (3 times for me) when you are using the class in a SwiftUI view, because the class was initialized more thatn once before the SwiftUI view is stable.

Related

Why is my Publisher subscription immediately cancelled when called from a SwiftUI view initializer?

I have created a gravityPublisher (to receive updates in the current gravity vector) and subscribe to it by calling the following function:
private func enableGravityDetection(_ gravityEnabled: Bool) {
if gravityEnabled {
if cancellable == nil {
cancellable = gravityProvider
.gravityPublisher
.receive(on: RunLoop.main)
.print()
.assign(to: \.gravity, on: self)
}
} else {
cancellable?.cancel()
}
}
The gravityPublisher itself is just a CurrentValueSubject
private var gravitySubject =
CurrentValueSubject<CMAcceleration, Never>(CMAcceleration())
erased to AnyPublisher<CMAcceleration, Never>. The cancellable is stored as a state property as follows:
#State private var cancellable: Cancellable?
I need to call this function from within a SwiftUI view. When I call it from an onAppear block everything works as expected and I continuously receive gravity vector updates:
struct CustomView: View {
var body: some View {
MyView()
.onAppear {
enableGravityDetection(true)
}
}
}
However, when I call the same (unmodified) function from the view's initializer, I don't receive any gravity updates as the subscription is immediately canceled:
struct CustomView: View {
init() {
enableGravityDetection(true)
}
var body: some View {
MyView()
}
}
The print() statement in the stream above prints the following output in this case:
receive subscription: (ReceiveOn)
request unlimited
receive cancel // ← Why?
Why is that and how can I fix this?
I need to call this function in the initializer as I sometimes need to stop receiving gravity updates and need to decide this every time the view is recreated. (onAppear is only called once.)

Using a class inside a struct is giving an error: "partial application of 'mutating' method is not allowed"

I am creating a class inside a struct to create a timer that sends information between an Apple Watch and the paired phone. When trying to run the timer with a button the error:
Partial application of the 'mutating' method is not allowed
The way I'm creating the class is the following:
import SwiftUI
struct ContentView: View {
//Timer to send information to phone
var timerLogic: TimerLogic!
var body: some View {
Button(action: startTimer, //"partial application of 'mutating' method is not allowed"
label: {
Image(systemName: "location")
})
}
// Class with the timer logic
class TimerLogic {
var structRef: ContentView!
var timer: Timer!
init(_ structRef: ContentView) {
self.structRef = structRef
self.timer = Timer.scheduledTimer(
timeInterval: 3.0,
target: self,
selector: #selector(timerTicked),
userInfo: nil,
repeats: true)
}
func stopTimer() {
self.timer?.invalidate()
self.structRef = nil
}
// Function to run with each timer tick
#objc private func timerTicked() {
self.structRef.timerTicked()
}
}
mutating func startTimer() {
self.timerLogic = TimerLogic(self)
}
// Function to run with each timer tick, this can be any action
func timerTicked() {
let data = ["latitude": "\(location.coordinate.latitude)", "longitud": "\(location.coordinate.longitude)"]
connectivity.sendMessage(data)
}
}
The closest solution that might solve the error is this one or is there another one?
SwiftUI Views should not have mutating properties/functions. Instead, they should use property wrappers like #State and #StateObject for state.
Besides the mutating function, you're fighting the principals of SwiftUI a bit. For example, you should never try to keep a reference to a View and call a function on it. Views are transient in SwiftUI and should not be expected to exist again if you need to call back to them. Also, SwiftUI tends to go hand-in-hand with Combine, which would be a good fit for implementing in your Timer code.
This might be a reasonable refactor:
import SwiftUI
import Combine
// Class with the timer logic
class TimerLogic : ObservableObject {
#Published var timerEvent : Timer.TimerPublisher.Output?
private var cancellable: AnyCancellable?
func startTimer() {
cancellable = Timer.publish(every: 3.0, on: RunLoop.main, in: .default)
.autoconnect()
.sink { event in
self.timerEvent = event
}
}
func stopTimer() {
cancellable?.cancel()
}
}
struct ContentView: View {
//Timer to send information to phone
#StateObject var timerLogic = TimerLogic()
var body: some View {
Button(action: timerLogic.startTimer) {
Image(systemName: "location")
}
.onChange(of: timerLogic.timerEvent) { _ in
timerTicked()
}
}
// Function to run with each timer tick, this can be any action
func timerTicked() {
print("Timer ticked...")
//...
}
}

How to set addObserver in SwiftUI?

How do I add NotificationCenter.default.addObserve in SwiftUI?
When I tried adding observer I get below error
Argument of '#selector' refers to instance method 'VPNDidChangeStatus'
that is not exposed to Objective-C
But when I add #objc in front of func I get below error
#objc can only be used with members of classes, #objc protocols, and
concrete extensions of classes
Here is my code
let NC = NotificationCenter.default
var body: some View {
VStack() {
}.onAppear {
self.NC.addObserver(self, selector: #selector(self.VPNDidChangeStatus),
name: .NEVPNStatusDidChange, object: nil)
}
}
#objc func VPNDidChangeStatus(_ notification: Notification) {
// print("VPNDidChangeStatus", VPNManager.shared.status)
}
The accepted answer may work but is not really how you're supposed to do this. In SwiftUI you don't need to add an observer in that way.
You add a publisher and it still can listen to NSNotification events triggered from non-SwiftUI parts of the app and without needing combine.
Here as an example, a list will update when it appears and when it receives a notification, from a completed network request on another view / controller or something similar etc.
If you need to then trigger an #objc func for some reason, you will need to create a Coordinator with UIViewControllerRepresentable
struct YourSwiftUIView: View {
let pub = NotificationCenter.default
.publisher(for: NSNotification.Name("YourNameHere"))
var body: some View {
List() {
ForEach(userData.viewModels) { viewModel in
SomeRow(viewModel: viewModel)
}
}
.onAppear(perform: loadData)
.onReceive(pub) { (output) in
self.loadData()
}
}
func loadData() {
// do stuff
}
}
I have one approach for NotificationCenter usage in SwiftUI.
For more information Apple Documentation
Notification extension
extension NSNotification {
static let ImageClick = Notification.Name.init("ImageClick")
}
ContentView
struct ContentView: View {
var body: some View {
VStack {
DetailView()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.ImageClick))
{ obj in
// Change key as per your "userInfo"
if let userInfo = obj.userInfo, let info = userInfo["info"] {
print(info)
}
}
}
}
DetailView
struct DetailView: View {
var body: some View {
Image(systemName: "wifi")
.frame(width: 30,height: 30, alignment: .center)
.foregroundColor(.black)
.onTapGesture {
NotificationCenter.default.post(name: NSNotification.ImageClick,
object: nil, userInfo: ["info": "Test"])
}
}
}
I use this extension so it's a bit nicer on the call site:
/// Extension
extension View {
func onReceive(
_ name: Notification.Name,
center: NotificationCenter = .default,
object: AnyObject? = nil,
perform action: #escaping (Notification) -> Void
) -> some View {
onReceive(
center.publisher(for: name, object: object),
perform: action
)
}
}
/// Usage
struct MyView: View {
var body: some View {
Color.orange
.onReceive(.myNotification) { _ in
print(#function)
}
}
}
extension Notification.Name {
static let myNotification = Notification.Name("myNotification")
}
It is not SwiftUI-native approach, which is declarative & reactive. Instead you should use NSNotificationCenter.publisher(for:object:) from Combine.
See more details in Apple Documentation
This worked for me
let NC = NotificationCenter.default
self.NC.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: nil,
using: self.VPNDidChangeStatus)
func VPNDidChangeStatus(_ notification: Notification) {
}
exchange this
self.NC.addObserver(self, selector: #selector(self.VPNDidChangeStatus),
name: .NEVPNStatusDidChange, object: nil)
to
self.NC.addObserver(self, selector: #selector(VPNDidChangeStatus(_:)),
name: .NEVPNStatusDidChange, object: nil)

SwiftUI and RxSwift Observer Closure Behavior

I'm building an iOS app, using RxSwift and SwiftUI. I'm completely new to these frameworks so I was following a few tutorials, but I'm having a hard time figuring how to setup a Observer coupled with SwiftUI whereas I'd like to keep updating my UI as long as my BehaviorRelay list of events is updated, here's what I've got in my UI:
import SwiftUI
import RxSwift
struct EventsTableView: View {
private let observer: EventsTableObserver = EventsTableObserver()
init() {
observer.setObserver()
EventViewModel.getAllEvents()
}
var body: some View {
List{
ForEach(observer.events_view,id: \.id) { event in
HStack {
Text(event.title)
}
}
}
}
}
class EventsTableObserver {
private let disposeBag = DisposeBag()
var events_view = [Event]()
func setObserver(){
EventGroup.shared.events.asObservable()
.subscribe(onNext: {
[unowned self] events in
self.events_view = events
})
.disposed(by: disposeBag)
}
}
The problem is that apparently after my closure ends, self.events_view is not keeping the stored events values as I'd like to, even though the events are being updated. Can someone give me a direction here?

How do you call a method on a UIView from outside the UIViewRepresentable in SwiftUI?

I want to be able to pass a reference to a method on the UIViewRespresentable (or perhaps it’s Coordinator) to a parent View. The only way I can think to do this is by creating a field on the parent View struct with a class that I then pass to the child, which acts as a delegate for this behaviour. But it seems pretty verbose.
The use case here is to be a able to call a method from a standard SwiftUI Button that will zoom the the current location in a MKMapView that’s buried in a UIViewRepresentable elsewhere in the tree. I don’t want the current location to be a Binding as I want this action to be a one off and not reflected constantly in the UI.
TL;DR is there a standard way of having a parent get a reference to a child in SwiftUI, at least for UIViewRepresentables? (I understand this is probably not desirable in most cases and largely runs against the SwiftUI pattern).
I struggled with that myself, here's what worked using Combine and PassthroughSubject:
struct OuterView: View {
private var didChange = PassthroughSubject<String, Never>()
var body: some View {
VStack {
// send the PassthroughSubject over
Wrapper(didChange: didChange)
Button(action: {
self.didChange.send("customString")
})
}
}
}
// This is representable struct that acts as the bridge between UIKit <> SwiftUI
struct Wrapper: UIViewRepresentable {
var didChange: PassthroughSubject<String, Never>
#State var cancellable: AnyCancellable? = nil
func makeUIView(context: Context) → SomeView {
let someView = SomeView()
// ... perform some initializations here
// doing it in `main` thread is required to avoid the state being modified during
// a view update
DispatchQueue.main.async {
// very important to capture it as a variable, otherwise it'll be short lived.
self.cancellable = didChange.sink { (value) in
print("Received: \(value)")
// here you can do a switch case to know which method to call
// on your UIKit class, example:
if (value == "customString") {
// call your function!
someView.customFunction()
}
}
}
return someView
}
}
// This is your usual UIKit View
class SomeView: UIView {
func customFunction() {
// ...
}
}
I'm sure there are better ways, including using Combine and a PassthroughSubject. (But I never got that to work.) That said, if you're willing to "run against the SwiftUI pattern", why not just send a Notification? (That's what I do.)
In my model:
extension Notification.Name {
static let executeUIKitFunction = Notification.Name("ExecuteUIKitFunction")
}
final class Model : ObservableObject {
#Published var executeFuntionInUIKit = false {
willSet {
NotificationCenter.default.post(name: .executeUIKitFunction, object: nil, userInfo: nil)
}
}
}
And in my UIKit representable:
NotificationCenter.default.addObserver(self, selector: #selector(myUIKitFunction), name: .executeUIKitFunction, object: nil)
Place that in your init or viewDidLoad, depending on what kind of representable.
Again, this is not "pure" SwiftUI or Combine, but someone better than me can probably give you that - and you sound willing to get something that works. And trust me, this works.
EDIT: Of note, you need to do nothing extra in your representable - this simply works between your model and your UIKit view or view controller.
I was coming here to find a better answer, then the one I came up myself with, but maybe this does actually help someone?
It's pretty verbose though nevertheless and doesn't quite feel like the most idiomatic solution, so probably not exactly what the question author was looking for. But it does avoid polluting the global namespace and allows synchronous (and repeated) execution and returning values, unlike the NotificationCenter-based solution posted before.
An alternative considered was using a #StateObject instead, but I need to support iOS 13 currently where this is not available yet.
Excursion: Why would I want that? I need to handle a touch event, but I'm competing with another gesture defined in the SwiftUI world, which would take precedence over my UITapGestureRecognizer. (I hope this helps by giving some context for the brief sample code below.)
So what I came up with, was the following:
Add an optional closure as state (on FooView),
Pass it as a binding into the view representable (BarViewRepresentable),
Fill this from makeUIView,
So that this can call a method on BazUIView.
Note: It causes an undesired / unnecessary subsequent update of BarViewRepresentable, because setting the binding changes the state of the view representable though, but this is not really a problem in my case.
struct FooView: View {
#State private var closure: ((CGPoint) -> ())?
var body: some View {
BarViewRepresentable(closure: $closure)
.dragGesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onEnded { value in
self.closure?(value.location)
})
)
}
}
class BarViewRepresentable: UIViewRepresentable {
#Binding var closure: ((CGPoint) -> ())?
func makeUIView(context: UIViewRepresentableContext<BarViewRepresentable>) -> BazUIView {
let view = BazUIView(frame: .zero)
updateUIView(view: view, context: context)
return view
}
func updateUIView(view: BazUIView, context: UIViewRepresentableContext<BarViewRepresentable>) {
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else { return }
strongSelf.closure = { [weak view] point in
guard let strongView = view? else {
return
}
strongView.handleTap(at: point)
}
}
}
}
class BazUIView: UIView { /*...*/ }
This is how I accomplished it succesfully. I create the UIView as a constant property in the SwiftUI View. Then I pass that reference into the UIViewRepresentable initializer which I use inside the makeUI method. Then I can call any method (maybe in an extension to the UIView) from the SwiftUI View (for instance, when tapping a button). In code is something like:
SwiftUI View
struct MySwiftUIView: View {
let myUIView = MKMapView(...) // Whatever initializer you use
var body: some View {
VStack {
MyUIView(myUIView: myUIView)
Button(action: { myUIView.buttonTapped() }) {
Text("Call buttonTapped")
}
}
}
}
UIView
struct MyUIView: UIViewRepresentable {
let myUIView: MKMapView
func makeUIView(context: UIViewRepresentableContext<MyUIView>) -> MKMapView {
// Configure myUIView
return myUIView
}
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MyUIView>) {
}
}
extension MKMapView {
func buttonTapped() {
print("The button was tapped!")
}
}