I can't implement NFCNDEFReaderSessionDelegate in SwiftUI
So I create a class to implement it
struct ContentView: View {
#State var out="n/a"
var body: some View {
VStack{
Text(out)
Button(
action:{
self.scan()
},
label:{Text("Scan")}
)
}
}
func scan(){
var nfc=Nfc()
nfc.start()
out = ???
}
}
Nfc.swift
class Nfc: NSObject, NFCNDEFReaderSessionDelegate{
func start(){
let session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: true)
session.begin()
print("start")
}
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
print("didInvalidateWithError")
}
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
print("scan")
}
func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
print("readerSessionDidBecomeActive")
}
}
I can successfully scan the NFC tag, but it did not trigger any callback, only show me this error
[CoreNFC] 00000002 81bfda80 -[NFCNDEFReaderSession _callbackDidBecomeActive]:228 Delegate does not implement -readerSessionDidBecomeActive: method
But I actually implemented this method.
If I add this method, I cannot scan the tag.
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
print("didDetect")
}
You should create a view that will conform to UIViewControllerRepresentable and the view has a Coordinator that handles all the delegate functionalities.
The issue is not related to the SwiftUI implementation, just add the readerSessionDidBecomeActive callback to your Nfc class:
public func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) {
print("readerSessionDidBecomeActive")
}
Related
I am using CoreBluetooth in SwiftUI and want to set the CBPeripheral use it throughout the app. Using a wrapper class as an EnvironmentObject seems like the right way to go, however I am not sure how to set it from within the ViewModel.
class BluetoothViewModel: NSObject, ObservableObject {
private var centralManager: CBCentralManager?
#Published var peripherals: [CBPeripheral] = []
override init() {
super.init()
self.centralManager = CBCentralManager(delegate: self, queue: .main)
self.centralManager?.delegate = self
}
func connectToDevice(btPeripheral: CBPeripheral) -> Void {
print("trying to connect to \(btPeripheral.name ?? "unknown")")
centralManager?.connect(btPeripheral, options: nil)
}
func disconnectDevice(btPeripheral: CBPeripheral) -> Void {
print("trying to disconnect to \(btPeripheral.name ?? "unknown")")
centralManager?.cancelPeripheralConnection(btPeripheral)
// WOULD LIKE TO SET THE #EnvrionmentObject TO nil HERE
}
}
...and after the connection:
extension BluetoothViewModel: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
print("powered on")
self.centralManager?.scanForPeripherals(withServices: nil)
} else {
//need to put an alert here to check bluetooth
print("powered off")
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if !peripherals.contains(peripheral) {
self.peripherals.append(peripheral)
}
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("connected to \(peripheral.name ?? "unknown")")
connectedPeripheral.peripheral = peripheral
// WOULD LIKE TO SET THE #EnvrionmentObject here
}
}
This way in the view I show the Bluetooth device
#EnvironmentObject var connectedPeripheral: ConnectedPeripheral
#ObservedObject private var bluetoothViewModel = BluetoothViewModel()
var body: some View {
if (connectedPeripheral.peripheral == nil) {
BluetoothDevicesView(). //this is where I list the devices and allow for selection
} else {
Text("We have one")
Button("Disconnect") {
print("pressed")
// DISCONNECT DEVICE HERE
bluetoothViewModel(connectedPeripheral.peripheral)
}
.buttonStyle(.borderedProminent)
}
}
The wrapper class would be basic
import SwiftUI
import CoreBluetooth
#MainActor class ConnectedPeripheral: ObservableObject {
#Published var peripheral: CBPeripheral? = nil
}
I didn't think having the ViewModel as an EnvironmentVariable would be the right way to go and I need to use the CBPerpheral in other views as well. I also would probably just turn the ViewModel into some sort of helper class.
I was also thinking of potentially setting the #EnvironmentVariable by having a #Published object in the BluetoothViewModel, but would not know how to watch for it to change. I'm open to suggestions.
I'm currently making use UITabBarController in SwiftUI. Here is the implementation:
struct MyTabView: View {
private var viewControllers: [UIHostingController<AnyView>]
public init( _ views: [AnyView]) {
self.viewControllers = views.map { UIHostingController(rootView:$0) }
}
public var body: some View {
return TabBarController(controllers: viewControllers)
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UITabBarController {
let tabBarController = UITabBarController()
tabBarController.viewControllers = controllers
return tabBarController
}
func updateUIViewController(_ tabBarController: UITabBarController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITabBarControllerDelegate {
var parent: TabBarController
init(_ tabBarController: TabBarController) {
self.parent = tabBarController
}
}
}
Inside of my SwiftUI I have the following:
struct ContentView: View {
var body: some View {
MyTabView([
AnyView(Text("Moo Moo")),
AnyView(MyPage())
])
}
}
struct MyPage:View {
var body:some View {
NavigationView {
VStack {
ForEach((1...10).reversed(), id: \.self) { value -> AnyView in
print("For Each Value Called")
return AnyView(MyView(text: String(value)))
}
}
}
}
}
struct MyView:View {
let text:String
var body:some View {
Text(text).onAppear {
print("On Appear Called - Making Service Call for \(text)")
}
}
}
I have the following questions:
When running this code the On Appear Called - Making Service Call for \(text), is called twice. What would cause this? My expectation is that it is only run once. Should this be occurring?
Is this a SwiftUI bug lurking around or is this expected behaviour?
Yes, your expectation would be correct. However, it looks like a bug.
The problem appear when having content inside NavigationView. If you use .onAppear() on the NavigationView, you will see it called only once. If you use onAppear() on the VStack, it's already twice.
This has reported in this thread aswell
From my view, this behavior is wrong. Maybe report to Apple or ask why
maybe I found a solution:
add on every very first NavigationLink the modifier .isDetailLink(false)
for me it stops the double onAppear calls
I am using Big Sur and SwiftUI with the SwiftUI lifecycle. I want to implement an alert, where the user gets asked, if the application can be quit or not. How is this possible with SwiftUI? It should look like this:
It's possible by using this code (this code opens the Alert only in the key window):
import SwiftUI
import AppKit
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
#Published var willTerminate = false
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
// check, if at least one window is open:
if NSApplication.shared.windows.count == 0 {
// if no one is open, close it
return .terminateNow
}
// if one or more are open, set the willTerminate variable, so that the alert can be shown
self.willTerminate = true
// return a .terminateLater (to which we need to reply later!)
return .terminateLater
}
/// This method tells the application, that it should not close
func `continue`() {
NSApplication.shared.reply(toApplicationShouldTerminate: false)
}
/// This method closes the application
func close() {
NSApplication.shared.reply(toApplicationShouldTerminate: true)
}
}
#main
struct WindowShouldCloseApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate: AppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
And this is the ContentView.swift:
import SwiftUI
struct ContentView: View {
#EnvironmentObject private var appDelegate: AppDelegate
#State private var window: NSWindow?
var body: some View {
Text("Hello, world!")
.padding()
.background(WindowAccessor(window: self.$window)) // access the window
.alert(isPresented: Binding<Bool>(get: { self.appDelegate.willTerminate && self.window?.isKeyWindow ?? false }, set: { self.appDelegate.willTerminate = $0 }), content: {
// show an alert, if the application should be closed
Alert(title: Text("Really close?"),
message: Text("Do you really want to close the application?"),
primaryButton: .default(Text("Continue"), action: { self.appDelegate.continue() }),
secondaryButton: .destructive(Text("Close"), action: { self.appDelegate.close() }))
})
}
}
// thanks to Asperi: https://stackoverflow.com/questions/63432700/how-to-access-nswindow-from-main-app-using-only-swiftui/63439982#63439982
struct WindowAccessor: NSViewRepresentable {
#Binding var window: NSWindow?
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
I need to call loadData in my ContentView when the app becomes active. ExtensionDelegate is a class which handle app events such as applicationDidBecomeActive. But I don't understand how to get ContentView inside ExtensionDelegate.
This is my ContentView:
struct ContentView: View {
let network = Network()
#State private var currentIndex: Int = 0
#State private var sources: [Source] = []
var body: some View {
ZStack {
// Some view depends on 'sources'
}
.onAppear(perform: loadData)
}
func loadData() {
network.getSources { response in
switch response {
case .result(let result):
self.sources = result.results
case .error(let error):
print(error)
}
}
}
}
And ExtensionDelegate:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
}
func applicationDidBecomeActive() {
// Here I need to call 'loadData' of my ContentView
}
func applicationWillResignActive() {
}
...
The simplest solution as I see would be to use notification
in ContentView
let needsReloadNotification = NotificationCenter.default.publisher(for: .needsNetworkReload)
var body: some View {
ZStack {
// Some view depends on 'sources'
}
.onAppear(perform: loadData)
.onReceive(needsReloadNotification) { _ in self.loadData()}
}
and in ExtensionDelegate
func applicationDidBecomeActive() {
NotificationCenter.default.post(name: .needsNetworkReload, object: nil)
}
and somewhere in shared
extension Notification.Name {
static let needsNetworkReload = Notification.Name("NeedsReload")
}
I did a exmaple long time ago how to send a simple message from an iPhone to a Apple Watch using Swift:
import UIKit
import WatchConnectivity
class ViewController: UIViewController, WCSessionDelegate {
// MARK: Outlets
#IBOutlet weak var textField: UITextField!
// MARK: Variables
var wcSession : WCSession! = nil
// MARK: Overrides
override func viewDidLoad() {
super.viewDidLoad()
wcSession = WCSession.default
wcSession.delegate = self
wcSession.activate()
}
// MARK: Button Actions
#IBAction func sendText(_ sender: Any) {
let txt = textField.text!
let message = ["message":txt]
wcSession.sendMessage(message, replyHandler: nil) { (error) in
print(error.localizedDescription)
}
}
// MARK: WCSession Methods
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
// Code
}
func sessionDidBecomeInactive(_ session: WCSession) {
// Code
}
func sessionDidDeactivate(_ session: WCSession) {
// Code
}
}
Now I'm trying to do the same using SwiftUI but no success so far.
Can anyone help with this problem?
I just need to know how to use the WCSession Class and the WCSessionDelegate with SwiftUI.
Thanks
I just had the same question as you and I figured it out:
First you need to implement a class that conforms to WCSessionDelegate. I like to use a separate class for that:
import WatchConnectivity
class ConnectivityProvider: NSObject, WCSessionDelegate {
private let session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
}
func send(message: [String:Any]) -> Void {
session.sendMessage(message, replyHandler: nil) { (error) in
print(error.localizedDescription)
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
// code
}
func sessionDidBecomeInactive(_ session: WCSession) {
// code
}
func sessionDidDeactivate(_ session: WCSession) {
// code
}
}
Now you need a ViewModel that takes your ConnectivityProvider as an argument. The ViewModel will be responsible for the connection of your View and the ConnectivityProvider. It also holds the value for the Textfield that later gets defined inside your View.
import SwiftUI
final class ViewModel: ObservableObject {
private(set) var connectivityProvider: ConnectivityProvider
var textFieldValue: String = ""
init(connectivityProvider: ConnectivityProvider) {
self.connectivityProvider = connectivityProvider
}
func sendMessage() -> Void {
let txt = textFieldValue
let message = ["message":txt]
connectivityProvider.send(message: message)
}
}
Now you can build a simple View that consists of a Textfield and a Button. Your View will be dependent on your ViewModel that you just defined.
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
TextField("Message Content", text: $viewModel.textFieldValue)
Button(action: {
self.viewModel.sendMessage()
}) {
Text("Send Message")
}
}
}
}
Last but not least you need to combine your ConnectivityProvider, ViewModel and View inside of your SceneDelegate:
let viewModel = ViewModel(connectivityProvider: ConnectivityProvider())
let contentView = ContentView(viewModel: viewModel)
...
window.rootViewController = UIHostingController(rootView: contentView)
==================================
Update: How to activate the Session?
First add a new function to your ConnectivityProvider that activates the session:
class ConnectivityProvider: NSObject, WCSessionDelegate {
...
func connect() {
guard WCSession.isSupported() else {
print("WCSession is not supported")
return
}
session.activate()
}
...
}
Now you can call the connect function whenever you need your WCSession to be connected. You should be able to connect it everywhere, like in your SceneDelegate, inside your ViewModel, or even directly inside of the init of your ConnectivityProvider:
ConnectivityProvider init:
class ConnectivityProvider: NSObject, WCSessionDelegate {
private let session: WCSession
init(session: WCSession = .default) {
self.session = session
super.init()
self.session.delegate = self
self.connect()
}
...
}
ViewModel:
import SwiftUI
final class ViewModel: ObservableObject {
private(set) var connectivityProvider: ConnectivityProvider
var textFieldValue: String = ""
init(connectivityProvider: ConnectivityProvider) {
self.connectivityProvider = connectivityProvider
self.connectivityProvider.connect()
}
func sendMessage() -> Void {
let txt = textFieldValue
let message = ["message":txt]
connectivityProvider.send(message: message)
}
}
SceneDelegate:
let connectivityProvider = ConnectivityProvider()
connectivityProvider.connect()
let viewModel = ViewModel(connectivityProvider: connectivityProvider)
let contentView = ContentView(viewModel: viewModel)
...
window.rootViewController = UIHostingController(rootView: contentView)