How to set addObserver in SwiftUI? - swift

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)

Related

SwiftUI: Why is onAppear executing twice? [duplicate]

Trying to load an image after the view loads, the model object driving the view (see MovieDetail below) has a urlString. Because a SwiftUI View element has no life cycle methods (and there's not a view controller driving things) what is the best way to handle this?
The main issue I'm having is no matter which way I try to solve the problem (Binding an object or using a State variable), my View doesn't have the urlString until after it loads...
// movie object
struct Movie: Decodable, Identifiable {
let id: String
let title: String
let year: String
let type: String
var posterUrl: String
private enum CodingKeys: String, CodingKey {
case id = "imdbID"
case title = "Title"
case year = "Year"
case type = "Type"
case posterUrl = "Poster"
}
}
// root content list view that navigates to the detail view
struct ContentView : View {
var movies: [Movie]
var body: some View {
NavigationView {
List(movies) { movie in
NavigationButton(destination: MovieDetail(movie: movie)) {
MovieRow(movie: movie)
}
}
.navigationBarTitle(Text("Star Wars Movies"))
}
}
}
// detail view that needs to make the asynchronous call
struct MovieDetail : View {
let movie: Movie
#State var imageObject = BoundImageObject()
var body: some View {
HStack(alignment: .top) {
VStack {
Image(uiImage: imageObject.image)
.scaledToFit()
Text(movie.title)
.font(.subheadline)
}
}
}
}
We can achieve this using view modifier.
Create ViewModifier:
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
Create View extension:
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Use like this:
struct SomeView: View {
var body: some View {
VStack {
Text("HELLO!")
}.onLoad {
print("onLoad")
}
}
}
I hope this is helpful. I found a blogpost that talks about doing stuff onAppear for a navigation view.
Idea would be that you bake your service into a BindableObject and subscribe to those updates in your view.
struct SearchView : View {
#State private var query: String = "Swift"
#EnvironmentObject var repoStore: ReposStore
var body: some View {
NavigationView {
List {
TextField($query, placeholder: Text("type something..."), onCommit: fetch)
ForEach(repoStore.repos) { repo in
RepoRow(repo: repo)
}
}.navigationBarTitle(Text("Search"))
}.onAppear(perform: fetch)
}
private func fetch() {
repoStore.fetch(matching: query)
}
}
import SwiftUI
import Combine
class ReposStore: BindableObject {
var repos: [Repo] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ReposStore, Never>()
let service: GithubService
init(service: GithubService) {
self.service = service
}
func fetch(matching query: String) {
service.search(matching: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let repos): self?.repos = repos
case .failure: self?.repos = []
}
}
}
}
}
Credit to: Majid Jabrayilov
Fully updated for Xcode 11.2, Swift 5.0
I think the viewDidLoad() just equal to implement in the body closure.
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.
As an example, this creates two views that use onAppear() and onDisappear() to print messages, with a navigation link to move between the two:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Hello World")
}
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
ref: https://www.hackingwithswift.com/quick-start/swiftui/how-to-respond-to-view-lifecycle-events-onappear-and-ondisappear
I'm using init() instead. I think onApear() is not an alternative to viewDidLoad(). Because onApear is called when your view is being appeared. Since your view can be appear multiple times it conflicts with viewDidLoad which is called once.
Imagine having a TabView. By swiping through pages onApear() is being called multiple times. However viewDidLoad() is called just 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 can I replace addObserver with publisher in receiving notifications from NotificationCenter

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.

How to remove Observer in swiftUI?

I have a view in which I add the observer in onAppear and remove that observer in onDisappear method. But the observer does not get removed. I read the documentation but did not find any solution. Looking for help. thanks
struct MainListView: View {
let NC = NotificationCenter.default
var body: some View {
VStack(alignment: .center, spacing: 1) {
.......
}
.onDisappear{
self.NC.removeObserver(self, name: Notification.Name(rawValue: "redrawCategories"), object: self)
}
.onAppear {
self.NC.addObserver(forName: Notification.Name(rawValue: "redrawCategories"), object: nil, queue: nil) { (notification) in
.......
}
}
}
}
Here is SwiftUI approach to observe notifications
struct MainListView: View {
let redrawCategoriesPublisher = NotificationCenter.default.publisher(for:
Notification.Name(rawValue: "redrawCategories"))
var body: some View {
VStack(alignment: .center, spacing: 1) {
Text("Demo")
}
.onReceive(redrawCategoriesPublisher) { notification in
// do here what is needed
}
}
}

SwiftUI using NSSharingServicePicker in MacOS

I am trying to use a Share function inside my MacOS app in SwiftUI. I am having a URL to a file, which I want to share. It can be images/ documents and much more.
I found NSSharingServicePicker for MacOS and would like to use it. However, I am struggeling to use it in SwiftUI.
Following the documentation, I am creating it like this:
let shareItems = [...]
let sharingPicker : NSSharingServicePicker = NSSharingServicePicker.init(items: shareItems as [Any])
sharingPicker.show(relativeTo: NSZeroRect, of:shareView, preferredEdge: .minY)
My problem is in that show() method. I need to set a NSRect, where I can use NSZeroRect.. but I am struggeling with of: parameter. It requires a NSView. How can I convert my current view as NSView and use it that way. Or can I use my Button as NSView(). I am struggling with that approach.
Another option would be to use a NSViewRepresentable. But should I just create a NSView and use it for that method.
Here is minimal working demo example
struct SharingsPicker: NSViewRepresentable {
#Binding var isPresented: Bool
var sharingItems: [Any] = []
func makeNSView(context: Context) -> NSView {
let view = NSView()
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
if isPresented {
let picker = NSSharingServicePicker(items: sharingItems)
picker.delegate = context.coordinator
// !! MUST BE CALLED IN ASYNC, otherwise blocks update
DispatchQueue.main.async {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
class Coordinator: NSObject, NSSharingServicePickerDelegate {
let owner: SharingsPicker
init(owner: SharingsPicker) {
self.owner = owner
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) {
// do here whatever more needed here with selected service
sharingServicePicker.delegate = nil // << cleanup
self.owner.isPresented = false // << dismiss
}
}
}
Demo of usage:
struct TestSharingService: View {
#State private var showPicker = false
var body: some View {
Button("Share") {
self.showPicker = true
}
.background(SharingsPicker(isPresented: $showPicker, sharingItems: ["Message"]))
}
}
Another option without using NSViewRepresentable is:
extension NSSharingService {
static func submenu(text: String) -> some View {
return Menu(
content: {
ForEach(items, id: \.title) { item in
Button(action: { item.perform(withItems: [text]) }) {
Image(nsImage: item.image)
Text(item.title)
}
}
},
label: {
Image(systemName: "square.and.arrow.up")
}
)
}
}
You lose things like the "more" menu item or recent recipients. But in my opinion it's more than enough, simple and pure SwiftUI.