How to pass potential error between methods - swift

I'm trying to wrap Core Bluetooth Peripheral methods for use in React Native. It's a counterpart for already finished android code, so the API is set.
When I'm calling CBPeripheralManager.addService, I need to fulfill or reject a promise, handed from the javascript side.
The problem is, Core Bluetooth doesn't offer a callback for the method, it seems to expect private func peripheralManager(_ peripheral: CBPeripheralManager, didAddService service: CBService, error: Error?)
I'm new to iOS and Swift so this behavior seems strange to me. Any ideas how can I wrap the function so I can handle the error reporting properly?
Thanks
class BLE: NSObject, CBPeripheralManagerDelegate {
var advertising: Bool = false
var servicesMap = Dictionary<String, CBMutableService>()
var manager: CBPeripheralManager!
override init() {
super.init()
manager = CBPeripheralManager(delegate: self, queue: nil, options: nil)
}
func addService(promise, serviceUUID) {
let serviceUUID = CBUUID(string: uuid)
let service = CBMutableService(type: serviceUUID, primary: true)
manager.add(service)
}
private func peripheralManager(_ peripheral: CBPeripheralManager, didAddService service: CBService, error: Error?) {
if let error = error {
// this should reject the addService promise
return
}
// this should fulfill the promise
}
}

It's unclear what the type of promise is, but you'll need to store it somewhere, and then fulfill it later. For example, you might add a property:
var pendingServices: [CBUUID: Promise] = [:]
(I don't know what you're promise type really is here)
Then you'd store it in addService:
assert(pendingServices[serviceUUID] == nil)
pendingServices[serviceUUID] = promise
And later in (the correct; see my comment) delegate method, you'd deal with it:
if let promise = pendingServices.removeValue(forKey: service.uuid) {
promise.fulfill() // Or whatever you do with it
}

Related

I'm trying to work with the CoreBluetooth module, but part of my code doesn't run

I'm attempting to use my Mac as a bluetooth peripheral using the CoreBluetooth module. I've looked at the docs and various examples of it, but my code still doesn't work. I put print statements throughout it, and for some reason the only print statements which show up in my output are "1" and "1.5". This is in an XCode 11.6 playground running on Mac OS Catalina 10.15.6, if that helps.
import AppKit
import PlaygroundSupport
import CoreBluetooth
print("1")
let myCBUUID = CBUUID(string:"2FC62EDD-EFED-457A-A88E-6E9BC1B8D7AF")
let properties: CBCharacteristicProperties = [.notify, .read, .write]
let permissions: CBAttributePermissions = [.readable, .writeable]
let characteristic = CBMutableCharacteristic(type:myCBUUID,properties: properties, value:nil, permissions: permissions)
let myCBService = CBMutableService(type:myCBUUID,primary: true)
myCBService.characteristics = [characteristic]
print("1.5")
class Peripheral: CBPeripheralManager, CBPeripheralManagerDelegate
{
var peripheralManager : CBPeripheralManager!
convenience init(delegate: CBPeripheralManagerDelegate?,
queue: DispatchQueue?){
print("1.75")
self.init()
peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true])
peripheralManager.add(myCBService)
}
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager)
{
print("2")
print("state: \(peripheral.state)")
}
func Advertise(){
print("hello its me")
peripheralManager.startAdvertising([CBAdvertisementDataLocalNameKey : "My Peripheral", CBAdvertisementDataLocalNameKey : myCBUUID])
}
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: NSError?)
{
if let error = error
{
print("Failed due to error: \(error)")
return
}
print("Success")
}
}
let myPeripheral = Peripheral.init()
myPeripheral.Advertise()
(this is my first time posting, sorry if i messed anything up and feel free to make suggestions in the comments if i left something out)

CLGeocoder() returns nil unexpectedly

I have a list of locations (about 30 elements):
var locations: [CLLocation] = [
CLLocation(latitude: 45.471172, longitude: 9.163317),
...
]
My purpose is to get street names from that list, so I decided to use CLGeocoder().
I call a function inside a viewDidLoad(), and every location is processed by lookUpCurrentLocation().
override func viewDidLoad() {
super.viewDidLoad()
for location in locations {
lookUpCurrentLocation(location: location, completionHandler: { streetName in
print(streetName)
})
}
}
func lookUpCurrentLocation(location: CLLocation, completionHandler: #escaping (String?) -> Void) {
CLGeocoder().reverseGeocodeLocation(location, completionHandler: { (placemarks, error) in
let placemark = placemarks?[0]
completionHandler(placemarks?[0].name)
})
}
My problem:
when the app starts, it prints a list of nil or only first two nil and the others street names.
terminal image 1
terminal image 2
I aspect to see the whole list processed without any nil.
Any hints?
As Leo said, you don’t want to run the requests concurrently. As the documentation says:
After initiating a reverse-geocoding request, do not attempt to initiate another reverse- or forward-geocoding request. Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail. When the maximum rate is exceeded, the geocoder passes an error object with the value CLError.Code.network to your completion handler.
There are a few approaches to make these asynchronous requests run sequentially:
The simple solution is to make the method recursive, invoking the next call in the completion handler of the prior one:
func retrievePlacemarks(at index: Int = 0) {
guard index < locations.count else { return }
lookUpCurrentLocation(location: locations[index]) { name in
print(name ?? "no name found")
DispatchQueue.main.async {
self.retrievePlacemarks(at: index + 1)
}
}
}
And then, just call
retrievePlacemarks()
FWIW, I might use first rather than [0] when doing the geocoding:
func lookUpCurrentLocation(location: CLLocation, completionHandler: #escaping (String?) -> Void) {
CLGeocoder().reverseGeocodeLocation(location) { placemarks, _ in
completionHandler(placemarks?.first?.name)
}
}
I don’t think it’s possible for reverseGeocodeLocation to return a non-nil, zero-length array (in which case your rendition would crash with an invalid subscript error), but the above does the exact same thing as yours, but also eliminates that potential error.
An elegant way to make asynchronous tasks run sequentially is to wrap them in an asynchronous Operation subclass (such as a general-purpose AsynchronousOperation seen in the latter part of this answer).
Then you can define a reverse geocode operation:
class ReverseGeocodeOperation: AsynchronousOperation {
private static let geocoder = CLGeocoder()
let location: CLLocation
private var geocodeCompletionBlock: ((String?) -> Void)?
init(location: CLLocation, geocodeCompletionBlock: #escaping (String?) -> Void) {
self.location = location
self.geocodeCompletionBlock = geocodeCompletionBlock
}
override func main() {
ReverseGeocodeOperation.geocoder.reverseGeocodeLocation(location) { placemarks, _ in
self.geocodeCompletionBlock?(placemarks?.first?.name)
self.geocodeCompletionBlock = nil
self.finish()
}
}
}
Then you can create a serial operation queue and add your reverse geocode operations to that queue:
private let geocoderQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = Bundle.main.bundleIdentifier! + ".geocoder"
queue.maxConcurrentOperationCount = 1
return queue
}()
func retrievePlacemarks() {
for location in locations {
geocoderQueue.addOperation(ReverseGeocodeOperation(location: location) { string in
print(string ?? "no name found")
})
}
}
If targeting iOS 13 and later, you can use Combine, e.g. define a publisher for reverse geocoding:
extension CLGeocoder {
func reverseGeocodeLocationPublisher(_ location: CLLocation, preferredLocale locale: Locale? = nil) -> AnyPublisher<CLPlacemark, Error> {
Future<CLPlacemark, Error> { promise in
self.reverseGeocodeLocation(location, preferredLocale: locale) { placemarks, error in
guard let placemark = placemarks?.first else {
return promise(.failure(error ?? CLError(.geocodeFoundNoResult)))
}
return promise(.success(placemark))
}
}.eraseToAnyPublisher()
}
}
And then you can use a publisher sequence, where you specify maxPublishers of .max(1) to make sure it doesn’t perform them concurrently:
private var placemarkStream: AnyCancellable?
func retrievePlacemarks() {
placemarkStream = Publishers.Sequence(sequence: locations).flatMap(maxPublishers: .max(1)) { location in
self.geocoder.reverseGeocodeLocationPublisher(location)
}.sink { completion in
print("done")
} receiveValue: { placemark in
print("placemark:", placemark)
}
}
There are admittedly other approaches to make asynchronous tasks run sequentially (often involving calling wait using semaphores or dispatch groups), but I don’t think that those patterns are advisable, so I’ve excluded them from my list of alternatives, above.
Here's an implementation using Combine, with a persistent cache. Need more intelligent cache expiry logic, etc, but it is a starting point. Patches welcome.
https://gist.github.com/lhoward/dd6b64fb8f5782c933359e0d54bcb7d3

Swift Pusher example doing nothing

I am using the starter code Pusher provides, when I put it into a basic swift project and I send it a message nothing happens. I have the cocoa pod package installed as well.
The statement "data received" should print.
There are also no errors.
Are you initialising the Pusher object as a var? You need to maintain a strong reference to the object otherwise it can become deallocated.
As an example it should be something like:
class ViewController: UIViewController {
var pusher: Pusher!
override func viewDidLoad() {
super.viewDidLoad()
let options = PusherClientOptions(
host: .cluster("mycluster")
)
pusher = Pusher(
key: "app_key",
options: options
)
// subscribe to channel and bind to event
let channel = pusher.subscribe("my-channel")
let _ = channel.bind(eventName: "my-event", callback: { (data: Any?) -> Void in
if let data = data as? [String : AnyObject] {
if let message = data["message"] as? String {
print(message)
}
}
})
pusher.connect()
}
}

Difference between a Callback and Competition Handler in Swift

In the Combine framework, I have found following text
The Combine framework provides a declarative approach for how your app
processes events. Rather than potentially implementing multiple
delegate callbacks or completion handler
Can somebody tell me what is the difference between completion handler and callback in Swift?
A delegate callback is when you have a delegate that you know in advance implements a method (e.g. because it adopts a protocol), and you call that method by name.
A completion handler is when someone hands you a function and you just call it blindly by reference.
to be clear actually you can achieve the same functionality with both ways however the there are completely different approach for designing your app
let me clarify with simple example the difference between both with the same function is making network call
delegate protocol
// enum to define the request type
enum RequestTypes {
case UserRegister
case UserLogin
}
protocol ServiceDelegate {
func didCompleteRequest(responseModel: AnyObject, tag: RequestTypes)
}
// you can also add default impl to the methods here
extension ServiceDelegate {
func didCompleteRequest(responseModel: AnyObject, tag: RequestTypes){}
}
class BaseService<ResponseModel: Codable> {
var session: URLSession!
var delegate: ServiceDelegate?
// MARK: Rebuilt Methods
func FireRequest(){
// Request Preparation
let serviceUrl = URL(string: /* your url */)!
var request = URLRequest(url: serviceUrl)
request.httpMethod = "GET"
// Firing the request
session = URLSession.init(configuration: URLSessionConfiguration.default)
session.dataTask(with: request) { (data, response, error) in
if let data = data {
do {
guard let object = try? JSONDecoder().decode(ResponseModel.self , from: data) else {/* handle error or call delegate error method here */ return }
delegate?.didCompleteRequest(responseModel: object, tag: .UserLogin)
}
}
}.resume()
}
}
class ViewController: UIViewController, ServiceDelegate {
override func viewDidLoad() {
super.viewDidLoad()
fetchNewData()
}
func fetchNewData(){
let service = BaseService<YourModel>()
service.delegate = self
service.FireRequest()
}
func didCompleteRequest(responseModel: AnyObject, tag: RequestTypes) {
if tag == /* the tag you are waiting */ .UserLogin {
// YourModel is available here
}
}
}
completion handler
class BaseService<ResponseModel: Codable> {
var session: URLSession!
// MARK: Rebuilt Methods
func FireRequest(completion: ((ResponseModel?) -> Void)?){
// Request Preparation
let serviceUrl = URL(string: /* your url */)!
var request = URLRequest(url: serviceUrl)
request.httpMethod = "GET"
// Firing the request
session = URLSession.init(configuration: URLSessionConfiguration.default)
session.dataTask(with: request) { (data, response, error) in
if let data = data {
do {
guard let object = try? JSONDecoder().decode(ResponseModel.self , from: data) else {/* handle error or call delegate error method here */ return }
DispatchQueue.main.async {
completion?(object)
}
}
}
}.resume()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fetchNewData()
}
func fetchNewData(){
let service = BaseService<YourModel>()
service.FireRequest(completion: { [weak self] (response) in
// yourModel Available here once the request completed
})
}
}
A delegate callback is one to one communication between various ViewControllers and classes. It basically lets you know that a particular change has been done in particular view or any where else and now you can make change after this action.
While completion handler is a block executed after completing a particular process or task.
Callback is a way to sending data back to some other function on some particular occasion. there are 2 ways to implement callbacks in swift.
Using Protocols / Delegate
Using Completion Handler
Using Protocols / Delegate Example:
Declare Protocol
protocol MyDelegate {
public method(param: String);
}
Your ViewController should extend the delegate
class YourViewController: MyDelegate {
// Your Other methods
func method(param: String) {
// Do your stuff
}
}
Now in your other classes you can send callback to ViewController through delegate object like
delegate.method(param: "your_param");
Using Completion Handler Example:
public func method(param: String, completionHandler: #escaping (_ param: String) -> Void)
{
...
// now you can send data back to the caller function using completionHandler on some particular occasion
completionHandler("param");
}
We can call this function like
method(param: String, completionHandler: { (result, alreadyUserId) in
// here you will receive callback
});
Callbacks and Completion Handlers are synonymous when referring to asynchronous methods.
I’ve found the main difference being in how its used in defining what’s returned to the caller where a callback is used when referring to a method where the scope is returned to the previous calling method and a completion handler refers to a method when it returns some Result type to the caller.

Why aren't my Characteristic appearing in BLE advertisement Swift

I want to advertise some characteristics from an iPhone acting as a peripheral, but although the service seems to contain the characteristics they don't show up when you look at it with a BLE scanner, the service works fine and displays the localNameKey.
thanks
import UIKit
import CoreBluetooth
class ViewController: UIViewController, CBPeripheralManagerDelegate{
var peripheralManager: CBPeripheralManager!
let myCustomServiceUUID: CBUUID = CBUUID(string: "B5893BC9-63AB-42A5-BB33-EEAE686BED1D")
let myCustomCharacteristic: CBUUID = CBUUID(string: "9BA41369-C5B7-456B-B4E3-BB0A8DFF3A95")
let myCustomCharacteristic2: CBUUID = CBUUID(string: "9BA41369-C5B7-456B-B4E3-BB0A8DFF3A85")
var myService: CBMutableService!
var myCharacteristics: CBMutableCharacteristic!
var myCharacteristics2: CBMutableCharacteristic!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
myService = CBMutableService(type: myCustomServiceUUID, primary: true)
myCharacteristics = CBMutableCharacteristic(type: myCustomCharacteristic, properties: CBCharacteristicProperties.Broadcast, value: nil, permissions: CBAttributePermissions.Readable)
myCharacteristics2 = CBMutableCharacteristic(type: myCustomCharacteristic2, properties: CBCharacteristicProperties.Broadcast, value: nil, permissions: CBAttributePermissions.Readable)
myService.characteristics = [myCharacteristics, myCharacteristics2]
peripheralManager.addService(myService)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func peripheralManagerDidUpdateState(peripheral: CBPeripheralManager) {
if peripheral.state == CBPeripheralManagerState.PoweredOn {
let dataTobeAdvetised :[String: AnyObject!] = [CBAdvertisementDataServiceUUIDsKey: [myService.UUID], CBAdvertisementDataLocalNameKey: "MY Device"]
self.peripheralManager.startAdvertising(dataTobeAdvetised)
print(myService)
print("It should be working!........")
} else if peripheral.state == CBPeripheralManagerState.PoweredOff {
self.peripheralManager.stopAdvertising()
}
}
}
You aren't permitted to use CBCharacteristic.broadcast property value.
From the documentation -
CBCharacteristicPropertyBroadcast
The characteristic’s value can be broadcast using a characteristic
configuration descriptor.
This property is not allowed for local characteristics published via
the addService: method of the CBPeripheralManager class. This means
that you cannot use this property when you initialize a new
CBMutableCharacteristic object via the
initWithType:properties:value:permissions: method of the
CBMutableCharacteristic class.
You should use some combination of CBCharacteristicPropertyRead, CBCharacteristicPropertyWriteWithoutResponse and CBCharacteristicPropertyWrite depending on your requirements.
Old question, but hopefully this will help somebody else. I was struggling with the same issue, but for me there was no didAddService callback. Turns out GATT initialisation should be after the peripheral state has changed to PoweredOn.
func peripheralManagerDidUpdateState(peripheral: CBPeripheralManager) {
if peripheral.state == CBPeripheralManagerState.PoweredOn {
//Do initialisation and then start advertising.
}
}