CoreData adding entity - swiftui

Here is how I add new entity.
func addCountry(name: String, code: String, flagImageUri: String?, wikiDataId: String) {
let newCountry = CountryEntity(context: container.viewContext)
newCountry.name = name
newCountry.code = code
newCountry.flagImageUri = flagImageUri
newCountry.wikiDataId = wikiDataId
save()
}
Here is my data:
However when I use the add function in my view, I got this error:
CoreData: error: +[CountryEntity entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
And this is my button:
Button(action: {
country.isFaved = !country.isFaved
coreDataModel.addCountry(name: country.name, code: country.code, flagImageUri: country.flagImageUri, wikiDataId: country.wikiDataId)
}) {
Image(systemName: "star.fill")
.foregroundColor(country.isFaved ? .black : .white)
.scaledToFit()
}
This is the whole class. I'm fetching, saving ,adding and deleting all data here. I did everything like the video I watched in youtube.
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "CountryCoreData")
#Published var savedCountries: [CountryEntity] = []
init() {
container.loadPersistentStores(completionHandler: { _, error in
if let error = error {
print("CoreData failed to load: \(error.localizedDescription)")
} else {
print("Successfully loaded")
}
})
}
func fetchCountries() -> [CountryEntity]? {
let request = NSFetchRequest<CountryEntity>(entityName: "CountryEntity")
do {
let fetchedCountries = try container.viewContext.fetch(request)
return fetchedCountries
} catch {
print("Something went wrong while data fetching \(error)")
return nil
}
}
func delete(code: String) {
guard let fetchedCountries = fetchCountries() else { return }
for country in fetchedCountries {
if country.code!.contains(code) {
container.viewContext.delete(country)
save()
}
}
}
func addCountry(name: String, code: String, flagImageUri: String?, wikiDataId: String) {
let newCountry = CountryEntity(context: container.viewContext)
print("OSMAN")
newCountry.name = name
newCountry.code = code
newCountry.flagImageUri = flagImageUri
newCountry.wikiDataId = wikiDataId
save()
}
func save() {
do {
try container.viewContext.save()
fetchCountries()
} catch {
print("Error while saving the data: \(error)")
}
}
}
How can I solve this problem?

Related

Swift UI App crash during the run time with main app

I am trying to call model form #main App where the model has the dependency on a repository with init function. The repository has the URLSession and Baseurl properties . I have passed the required property on both approach ..
Here is approach I have tried based on Xcode suggestions ..
#main
struct HomwWorkWithSwiftUIApp: App {
#StateObject var model = FruitsModel(fruitRepository: FruitsRepository.self as! FruitsRepository)
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
}
}
As a result as was crashed at run time with error Thread 1: signal SIGABRT
The second approach is passing the require parameters like this ..
#main
struct HomwWorkWithSwiftUIApp: App {
#StateObject var model = FruitsModel(fruitRepository: RealFruitsRepository(session: URLSession, baseURL: EndPoint.baseUrl))
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
}
}
It giving error ..Cannot convert value of type 'URLSession.Type' to expected argument type 'URLSession'
Here is attempt for URLSession instance.
#main
struct HomwWorkWithSwiftUIApp: App {
init() {
}
var url : URLSession
init(url: URLSession) {
self.url = url
}
#StateObject var model = FruitsModel(fruitRepository: RealFruitsRepository(session: url, baseURL: EndPoint.baseUrl))
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
}
}
Here is the screenshot ..
Here is the repository code ..
import Foundation
protocol FruitsRepository: WebRepository {
func loadFruits() async throws -> [Fruits]
}
struct RealFruitsRepository: FruitsRepository {
let session: URLSession
let baseURL: String
init(session: URLSession, baseURL: String) {
self.session = session
self.baseURL = baseURL
}
func loadFruits() async throws -> [Fruits] {
guard let request = try? API.allFruits.urlRequest(baseURL: baseURL) else {
throw APIError.invalidURL
}
guard let data = try? await call(request: request) else {
throw APIError.unexpectedResponse
}
guard let fruits = getDecodedFruitesResopnse(from: data) else {
throw APIError.unexpectedResponse
}
return fruits
}
private func getDecodedFruitesResopnse(from data: Data)-> [Fruits]? {
guard let fruites = try? JSONDecoder().decode([Fruits].self, from: data) else {
return nil
}
return fruites
}
}
extension RealFruitsRepository {
enum API {
case allFruits
case fruitDetails(Fruits)
}
}
extension RealFruitsRepository.API: APICall {
var path: String {
switch self {
case .allFruits:
return "/all"
case let .fruitDetails(fruit):
let encodedName = fruit.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
return "/name/\(encodedName ?? fruit.name)"
}
}
var method: String {
switch self {
case .allFruits, .fruitDetails:
return "GET"
}
}
var headers: [String: String]? {
return ["Accept": "application/json"]
}
func body() throws -> Data? {
return nil
}
}
Here is the model class ..
import Foundation
import Combine
protocol FruitsModelInput {
func getFruits() async
}
protocol FruitsModelOutput {
var state: FruitViewStates { get }
var fruitRecordsCount: Int { get }
func getFruit(index: Int)-> Fruits
func getFruitsDetails(for row:Int)-> FruitsDetails
}
struct FruitsDetails {
let genus, name: String
}
final class FruitsModel: ObservableObject {
private var fruitsRepository: FruitsRepository
var fruits: [Fruits] = []
#Published var state: FruitViewStates = .none
private var cancellables:Set<AnyCancellable> = Set()
init(fruitRepository: FruitsRepository) {
self.fruitsRepository = fruitRepository
}
}
extension FruitsModel: FruitsModelOutput {
func getFruitsDetails(for row: Int) -> FruitsDetails {
if row >= 0 {
let fruit = fruits[row]
return FruitsDetails(genus: fruit.genus, name: fruit.name)
}
return FruitsDetails(genus: "", name: "")
}
var fruitRecordsCount: Int {
return fruits.count
}
func getFruit(index: Int) -> Fruits {
if fruits.count > 0 {
return (fruits[index])
} else {
return Fruits(genus: "", name: "", id: 0, family: "", order: "", nutritions: Nutritions(carbohydrates: 0.0, protein: 0.0, fat: 0.0, calories: 0, sugar: 0.0))
}
}
}
extension FruitsModel: FruitsModelInput {
func getFruits() async {
state = .showActivityIndicator
do {
fruits = try await fruitsRepository.loadFruits()
self.state = .showFruitList
} catch let error {
fruits = []
print(error)
state = .showError((error as! APIError).localizedDescription)
}
}
}

Call async func in another function and update UI

Here is my async function class:
class MoviesViewModel: ObservableObject {
#Published var topRated: [Movie] = []
#Published var popular: [Movie] = []
#Published var upcoming: [Movie] = []
func getUpcomingMovies() {
if let movies = getMovies(path: "upcoming") {
DispatchQueue.main.async {
self.upcoming = movies
}
}
}
func getPopularMovies() {
if let movies = getMovies(path: "popular") {
DispatchQueue.main.async {
self.popular = movies
}
}
}
func getTopRatedMovies() {
DispatchQueue.main.async {
if let movies = self.getMovies(path: "top_rated") {
self.topRated = movies
}
}
}
func getMovies(path: String) -> [Movie]? {
var movies: [Movie]?
let urlString = "https://api.themoviedb.org/3/movie/\(path)?api_key=\(apiKey)&language=en-US&page=1"
guard let url = URL(string: urlString) else { return [] }
let session = URLSession.shared
let dataTask = session.dataTask(with: url, completionHandler: { data, _, error in
if error != nil {
print(error)
}
do {
if let safeData = data {
let decodedData = try JSONDecoder().decode(NowPlaying.self, from: safeData)
DispatchQueue.main.async {
movies = decodedData.results
}
}
}
catch {
print(error)
}
})
dataTask.resume()
return movies
}
}
When I printed the movies in getMovies function, I can get movies from api without problem. However, UI does not update itself. I used DispatchQueue.main.async function but it did not solve my problem. What can I do in this situation?
dataTask works asynchronously. Your code returns nil even before the asynchronous task is going to start. You have to use a completion handler as described in Returning data from async call in Swift function.
I highly recommend to use async/await in this case. You get rid of a lot of boilerplate code and you don't need to care about dispatching threads.
#MainActor
class MoviesViewModel: ObservableObject {
#Published var topRated: [Movie] = []
#Published var popular: [Movie] = []
#Published var upcoming: [Movie] = []
func getUpcomingMovies() async throws {
self.upcoming = try await getMovies(path: "upcoming")
}
func getPopularMovies() async throws {
self.popular = try await getMovies(path: "popular")
}
func getTopRatedMovies() async throws {
self.topRated = try await getMovies(path: "top_rated")
}
func getMovies(path: String) async throws -> [Movie] {
let urlString = "https://api.themoviedb.org/3/movie/\(path)?api_key=\(apiKey)&language=en-US&page=1"
guard let url = URL(string: urlString) else { throw URLError(.badURL) }
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(NowPlaying.self, from: data).results
}
}

How to infer a generic paramater with an async/await function

I have an async/await function to make sure that the data gets passed along first;
func downloadFirebaseData() async -> String {
let group = DispatchGroup()
group.enter() // stop the thread/enter the function
let db = Firestore.firestore()
withUnsafeThrowingContinuation { continuation in
db.collection("annotations")
.getDocuments { (querySnapshot, error) in
defer {
group.leave() // << end on any return
}
if let Lng = i.document.get("lng") as? String {
DispatchQueue.main.async {
annotationLng.append(Lng) //edit the array
print("downloadLngServerData ()\(annotationLng)")
}
}
if let Lat = i.document.get("lat") as? String {
DispatchQueue.main.async {
annotationLat.append(Lat) //edit the array
print("downloadLatServerData ()\(annotationLat)")
}
}
}
}
group.wait() // clear up the thread now, exit the function
}
And its called under my view with;
.task {
try await downloadFirebaseData() //error 1
}
#State annotationLat: [String] = []
#State annotationLng: [String] = []
Inside of firebase database:
annotationLat = ["42.828392","29.18273","97.27352"]
annotationLng = ["42.828392","29.18273","97.27352"]
I have 2 errors;
Invalid conversion from throwing function of type '#Sendable () async throws -> Void' to non-throwing function type '#Sendable () async -> Void'
This was under the .task
My second error:
Generic parameter 'T' could not be inferred
This was under withUnsafeThrowingContinuation
The first error I somewhat get, but even after I modified from my original code, the error still persisted.
For the second error, I know that I might have to define that this is a string somewhere, because I don't think that the app knows that I'm trying to work with a string.
This assumes that the Firestore path for the documents is
annotations/{id}
and that each document has variables lat and lng of type String
import Foundation
import FirebaseFirestoreSwift
import FirebaseFirestore
import CoreLocation
//struct to keep the latitude and longitude together, they should not be in separate arrays
struct Annotation: Codable, Identifiable{
#DocumentID var id: String?
var lat: String?
var lng: String?
}
extension Annotation{
//Safely unwrap the Strings into doubles and then create the coordinate
var coordinate: CLLocationCoordinate2D? {
guard let latStr = lat, let lngStr = lng, let latitude = Double(latStr), let longitude = Double(lngStr) else{
print("Unable to get valid latitude and longitude")
return nil
}
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
return coordinate
}
}
struct CustomFirestoreService{
let store: Firestore = .firestore()
init(){}
func getAnnotations() async throws -> [Annotation]{
let ANNOTATIONS_PATH = "annotations"
return try await retrieve(path: ANNOTATIONS_PATH)
}
///retrieves all the documents in the collection at the path
private func retrieve<FC : Codable>(path: String) async throws -> [FC]{
//Firebase provided async await.
let querySnapshot = try await store.collection(path).getDocuments()
return querySnapshot.documents.compactMap { document in
do{
return try document.data(as: FC.self)
}catch{
print(error)
return nil
}
}
}
}
Then in your View
import SwiftUI
struct AnnotationsView: View {
let service: CustomFirestoreService = CustomFirestoreService()
#State private var annotations: [Annotation] = []
var body: some View {
if annotations.isEmpty{
Text("Hello, World!")
.task {
do{
annotations = try await service.getAnnotations()
//Do any other work here, this line won't run unless the annotations are populated.
}catch{
print(error)
}
}
}else{
List(annotations){ annotation in
if let coord = annotation.coordinate{
VStack{
Text("Latitude = \(coord.latitude)")
Text("Longitude = \(coord.longitude)")
}
}else{
Text("Invalid Coordinate Value. Check firestore values for document \(annotation.id ?? "no id")")
}
}
}
}
}
struct AnnotationsView_Previews: PreviewProvider {
static var previews: some View {
AnnotationsView()
}
}
This makes some assumptions but if you paste it into your project you should get some working code.
You don't need this for your code but this is what a conversion from the "old" closures to the new async await would look like.
public func retrieve<FC : Codable>(path: String) async throws -> [FC]{
typealias MyContinuation = CheckedContinuation<[FC], Error>
return try await withCheckedThrowingContinuation { (continuation: MyContinuation) in
store.collection(path)
.getDocuments() { (querySnapshot, err) in
if let err = err {
//This throws an error
continuation.resume(throwing: err)
} else {
let array = querySnapshot?.documents.compactMap { document in
try? document.data(as: FC.self)
} ?? []
//This returns an array
continuation.resume(returning: array)
}
}
}
}
If you aren't calling continuation there is no point in returning a continuation of any kind.

Updated Reverse Geolocation

There are a number of examples showing how to do reverse geolocation, but nothing recent on implementation in SwiftUI. My current code uses the iPhone GPS to generate coordinates that are used with maps to show the location. I would also like to display the street address since a map without text indicating the location isn't very helpful.
My Questions:
Do I have all the relevant code to implement reverse geolocation?
I have seen examples using storyboards and print statements to display the location, but how do I return the location to a Swiftui view with an #escaping closure?
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
#Published var currentAddress: String = ""
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.distanceFilter = 10 // distance before update (meters)
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.startUpdatingLocation()
}
func startLocationServices() {
if locationManager.authorizationStatus == .authorizedAlways || locationManager.authorizationStatus == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
} else {
locationManager.requestWhenInUseAuthorization()
}
}
func getLocationCoordinates() -> (Double, Double) {
let coordinate = self.locationManager.location != nil ? self.locationManager.location!.coordinate : CLLocationCoordinate2D()
print("location = \(coordinate.latitude), \(coordinate.longitude)")
return (Double(coordinate.latitude), Double(coordinate.longitude))
}
// Using closure
func getAddress(handler: #escaping (String) -> Void)
{
self.currentAddress = ""
let coordinate = self.locationManager.location != nil ? self.locationManager.location!.coordinate : CLLocationCoordinate2D()
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
// Place details
var placeMark: CLPlacemark?
placeMark = placemarks?[0]
guard let placemark = placemarks?.first else { return }
if let streetNumber = placemark.subThoroughfare,
let street = placemark.subThoroughfare,
let city = placemark.locality,
let state = placemark.administrativeArea {
DispatchQueue.main.async {
self.currentAddress = "\(streetNumber) \(street) \(city) \(state)"
}
} else if let city = placemark.locality, let state = placemark.administrativeArea {
DispatchQueue.main.async {
self.currentAddress = "\(city) \(state)"
}
} else {
DispatchQueue.main.async {
self.currentAddress = "Address Unknown"
}
}
}
)
print( self.currentAddress)
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if locationManager.authorizationStatus == .authorizedAlways || locationManager.authorizationStatus == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
}
}
// Get Placemark
func getPlace(for location: CLLocation,
completion: #escaping (CLPlacemark?) -> Void) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemarks, error in
guard error == nil else {
print("*** Error in \(#function): \(error!.localizedDescription)")
completion(nil)
return
}
guard let placemark = placemarks?[0] else {
print("*** Error in \(#function): placemark is nil")
completion(nil)
return
}
completion(placemark)
}
}
}
If I add the follow code say in ContentView:
#State private var entryLat: Double = 0.0
#State private var entryLong: Double = 0.0
let result = lm.getLocationCoordinates()
entryLat = result.0
entryLong = result.1
How would I call getPlace?
To use the following code you need to setup the appropriate entitlements and authorizations.
Here is a working example of using geolocation in swiftui, from code I got from
a number of sources on the net years ago.
This should give you a base to do reverse geolocation in swiftui:
import Foundation
import CoreLocation
import SwiftUI
import Combine
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
let locationProvider = LocationProvider()
#State var currentAddress = ""
var body: some View {
Text(currentAddress)
.onAppear {
getAddress()
}
}
func getAddress() {
// for testing Tokyo
let location = CLLocation(latitude: 35.684602, longitude: 139.751992)
locationProvider.getPlace(for: location) { plsmark in
guard let placemark = plsmark else { return }
if let streetNumber = placemark.subThoroughfare,
let street = placemark.subThoroughfare,
let city = placemark.locality,
let state = placemark.administrativeArea {
self.currentAddress = "\(streetNumber) \(street) \(city) \(state)"
} else if let city = placemark.locality, let state = placemark.administrativeArea {
self.currentAddress = "\(city) \(state)"
} else {
self.currentAddress = "Address Unknown"
}
}
}
}
/**
A Combine-based CoreLocation provider.
On every update of the device location from a wrapped `CLLocationManager`,
it provides the latest location as a published `CLLocation` object and
via a `PassthroughSubject<CLLocation, Never>` called `locationWillChange`.
*/
public class LocationProvider: NSObject, ObservableObject {
private let lm = CLLocationManager()
/// Is emitted when the `location` property changes.
public let locationWillChange = PassthroughSubject<CLLocation, Never>()
/**
The latest location provided by the `CLLocationManager`.
Updates of its value trigger both the `objectWillChange` and the `locationWillChange` PassthroughSubjects.
*/
#Published public private(set) var location: CLLocation? {
willSet {
locationWillChange.send(newValue ?? CLLocation())
}
}
/// The authorization status for CoreLocation.
#Published public var authorizationStatus: CLAuthorizationStatus?
/// A function that is executed when the `CLAuthorizationStatus` changes to `Denied`.
public var onAuthorizationStatusDenied : ()->Void = {presentLocationSettingsAlert()}
/// The LocationProvider intializer.
///
/// Creates a CLLocationManager delegate and sets the CLLocationManager properties.
public override init() {
super.init()
self.lm.delegate = self
self.lm.desiredAccuracy = kCLLocationAccuracyBest
self.lm.activityType = .fitness
self.lm.distanceFilter = 10
self.lm.allowsBackgroundLocationUpdates = true
self.lm.pausesLocationUpdatesAutomatically = false
self.lm.showsBackgroundLocationIndicator = true
}
/**
Request location access from user.
In case, the access has already been denied, execute the `onAuthorizationDenied` closure.
The default behavior is to present an alert that suggests going to the settings page.
*/
public func requestAuthorization() -> Void {
if self.authorizationStatus == CLAuthorizationStatus.denied {
onAuthorizationStatusDenied()
}
else {
self.lm.requestWhenInUseAuthorization()
}
}
/// Start the Location Provider.
public func start() throws -> Void {
self.requestAuthorization()
if let status = self.authorizationStatus {
guard status == .authorizedWhenInUse || status == .authorizedAlways else {
throw LocationProviderError.noAuthorization
}
}
else {
/// no authorization set by delegate yet
#if DEBUG
print(#function, "No location authorization status set by delegate yet. Try to start updates anyhow.")
#endif
/// In principle, this should throw an error.
/// However, this would prevent start() from running directly after the LocationProvider is initialized.
/// This is because the delegate method `didChangeAuthorization`,
/// setting `authorizationStatus` runs only after a brief delay after initialization.
//throw LocationProviderError.noAuthorization
}
self.lm.startUpdatingLocation()
}
/// Stop the Location Provider.
public func stop() -> Void {
self.lm.stopUpdatingLocation()
}
// todo deal with errors
public func getPlace(for location: CLLocation, completion: #escaping (CLPlacemark?) -> Void) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemarks, error in
guard error == nil else {
print("=====> Error \(error!.localizedDescription)")
completion(nil)
return
}
guard let placemark = placemarks?.first else {
print("=====> Error placemark is nil")
completion(nil)
return
}
completion(placemark)
}
}
}
/// Present an alert that suggests to go to the app settings screen.
public func presentLocationSettingsAlert(alertText : String? = nil) -> Void {
let alertController = UIAlertController (title: "Enable Location Access", message: alertText ?? "The location access for this app is set to 'never'. Enable location access in the application settings. Go to Settings now?", preferredStyle: .alert)
let settingsAction = UIAlertAction(title: "Settings", style: .default) { (_) -> Void in
guard let settingsUrl = URL(string:UIApplication.openSettingsURLString) else {
return
}
UIApplication.shared.open(settingsUrl)
}
alertController.addAction(settingsAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
alertController.addAction(cancelAction)
UIApplication.shared.windows[0].rootViewController?.present(alertController, animated: true, completion: nil)
}
/// Error which is thrown for lacking localization authorization.
public enum LocationProviderError: Error {
case noAuthorization
}
extension LocationProvider: CLLocationManagerDelegate {
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.authorizationStatus = status
#if DEBUG
print(#function, status.name)
#endif
//print()
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
self.location = location
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
if let clErr = error as? CLError {
switch clErr {
case CLError.denied : do {
print(#function, "Location access denied by user.")
self.stop()
self.requestAuthorization()
}
case CLError.locationUnknown : print(#function, "Location manager is unable to retrieve a location.")
default: print(#function, "Location manager failed with unknown CoreLocation error.")
}
}
else {
print(#function, "Location manager failed with unknown error", error.localizedDescription)
}
}
}
extension CLAuthorizationStatus {
/// String representation of the CLAuthorizationStatus
var name: String {
switch self {
case .notDetermined: return "notDetermined"
case .authorizedWhenInUse: return "authorizedWhenInUse"
case .authorizedAlways: return "authorizedAlways"
case .restricted: return "restricted"
case .denied: return "denied"
default: return "unknown"
}
}
}

SwiftUI ObservableObject and Published issue

here is something that keeps me awake for three days already: I'm writing a little app that connects via BlueTooth to an Arduino. To get visual feedback about the connection state and the transmitted data, I use a view that allows me to connect/disconnect as well as shows me the state and data:
VStack {
Text("Glove Training App")
.font(.title)
HStack {
Button(action: { MyBluetoothManager.shared.scan() }) {
Text("Connect")
.padding(30)
}
Text(" | ")
Button(action: { MyBluetoothManager.shared.disconnect()}) {
Text("Disconnect")
.padding(30)
}
}
Text(manager.stateChange)
.font(.subheadline)
.padding(.bottom, 30)
Text(peripheral.transmittedString)
.font(.subheadline)
.padding(.bottom, 30)
}
}
In a separate file I have all the BT management:
class MyBluetoothManager: NSObject, ObservableObject {
#Published var stateChange: String = "Initializing..." {
willSet { objectWillChange.send() }
}
static let shared = MyBluetoothManager()
let central = CBCentralManager(delegate: MyCentralManagerDelegate.shared,
queue: nil, options: [
CBCentralManagerOptionRestoreIdentifierKey: restoreIdKey,
])
(...)
func setConnected(peripheral: CBPeripheral) {
(...)
state = .connected(peripheral)
self.stateChange = "Connected"
print("Connected")
}
}
class MyPeripheralDelegate: NSObject, ObservableObject, CBPeripheralDelegate {
let objectWillChange = ObservableObjectPublisher()
var transmittedString: String = "No data" {
willSet { objectWillChange.send()
}
}
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
(...)
let rxData = characteristic.value
if let str = NSString(data: rxData!, encoding: String.Encoding.utf8.rawValue) as String? {
print(str)
self.transmittedString = str
let measurement = str.components(separatedBy: "|")
(...)
} else {
print("not a valid UTF-8 sequence")
}
}
}
The values are initially set correctly, but then never updated. In the terminal I can see the printed values and the app works otherwise as expected. I'm on the latest version of XCode.
I looked at several tutorials, and this seems to be tricky. Any help would be highly appreciated.
Cheers,
Christian
EDIT: Here is the full BluetoothManager class (not my code mostly but works fine):
class MyBluetoothManager: NSObject, ObservableObject {
#Published var stateChange: String = "Initializing..." {
willSet { objectWillChange.send() }
}
static let shared = MyBluetoothManager()
let central = CBCentralManager(delegate: MyCentralManagerDelegate.shared,
queue: nil, options: [
CBCentralManagerOptionRestoreIdentifierKey: restoreIdKey,
])
var state = State.poweredOff
enum State {
case poweredOff
case restoringConnectingPeripheral(CBPeripheral)
case restoringConnectedPeripheral(CBPeripheral)
case disconnected
case scanning(Countdown)
case connecting(CBPeripheral, Countdown)
case discoveringServices(CBPeripheral, Countdown)
case discoveringCharacteristics(CBPeripheral, Countdown)
case connected(CBPeripheral)
case outOfRange(CBPeripheral)
var peripheral: CBPeripheral? {
switch self {
case .poweredOff: return nil
case .restoringConnectingPeripheral(let p): return p
case .restoringConnectedPeripheral(let p): return p
case .disconnected: return nil
case .scanning: return nil
case .connecting(let p, _): return p
case .discoveringServices(let p, _): return p
case .discoveringCharacteristics(let p, _): return p
case .connected(let p): return p
case .outOfRange(let p): return p
}
}
}
func scan() {
guard central.state == .poweredOn else {
self.stateChange = "Cannot scan, BT is not powered on"
print("Cannot scan, BT is not powered on")
return
}
central.scanForPeripherals(withServices: [myDesiredServiceId], options: nil)
state = .scanning(Countdown(seconds: 10, closure: {
self.central.stopScan()
self.state = .disconnected
self.stateChange = "Scan timed out"
print("Scan timed out")
}))
}
func disconnect(forget: Bool = false) {
if let peripheral = state.peripheral {
central.cancelPeripheralConnection(peripheral)
}
if forget {
UserDefaults.standard.removeObject(forKey: peripheralIdDefaultsKey)
UserDefaults.standard.synchronize()
}
self.stateChange = "Disconnected"
state = .disconnected
}
func connect(peripheral: CBPeripheral) {
central.connect(peripheral, options: nil)
state = .connecting(peripheral, Countdown(seconds: 10, closure: {
self.central.cancelPeripheralConnection(peripheral)
self.state = .disconnected
self.stateChange = "Connect timed out"
print("Connect timed out")
}))
}
func discoverServices(peripheral: CBPeripheral) {
peripheral.delegate = MyPeripheralDelegate.shared
peripheral.discoverServices([myDesiredServiceId])
state = .discoveringServices(peripheral, Countdown(seconds: 10, closure: {
self.disconnect()
self.stateChange = "Could not discover services"
print("Could not discover services")
}))
}
func discoverCharacteristics(peripheral: CBPeripheral) {
guard let myDesiredService = peripheral.myDesiredService else {
self.disconnect()
return
}
peripheral.delegate = MyPeripheralDelegate.shared
peripheral.discoverCharacteristics([myDesiredCharacteristicId],
for: myDesiredService)
state = .discoveringCharacteristics(peripheral, Countdown(seconds: 10,
closure: {
self.disconnect()
self.stateChange = "Could not discover characteristics"
print("Could not discover characteristics")
}))
}
func setConnected(peripheral: CBPeripheral) {
guard let myDesiredCharacteristic = peripheral.myDesiredCharacteristic
else {
self.stateChange = "Missing characteristic"
print("Missing characteristic")
disconnect()
return
}
UserDefaults.standard.set(peripheral.identifier.uuidString,
forKey: peripheralIdDefaultsKey)
UserDefaults.standard.synchronize()
peripheral.delegate = MyPeripheralDelegate.shared
peripheral.setNotifyValue(true, for: myDesiredCharacteristic)
state = .connected(peripheral)
self.stateChange = "Connected"
print("Connected")
}
}
Button(action: { MyBluetoothManager.shared.scan() }) {
Text("Connect")
.padding(30)
}
Text(" | ")
Button(action: { MyBluetoothManager.shared.disconnect()}) {
Text("Disconnect")
.padding(30)
}
}
Text(manager.stateChange) << why don't you use MyBluetoothManager.shared here ? is there a second instance? this might be the error...but unfortunately you just showed us a small piece of code...