Forcing code to wait for object instantiation before continuing - swift

I've written a class, UserLocation (below), which uses CoreLocation to get the user's current place (string) & latitude and longitude (also a string "lat,long" for passing to APIs).
My code below works, but when I initialize the class, the rest of my code doesn't wait for the init to finish before moving on, so I miss the opportunity to assign the location-related values that I'm trying to retrieve.
Is this a sound approach (or should I think about some other more "MVC" organization), and if it is, how can I get my code to wait for the initialization (with location find & reverse geocoding) to finish before moving on. Is there a way to put the code below the initialization into some sort of #escaping closure that's specified in the class's init? I'm new to swift so thanks for your kind advice.
In ViewController.swift's viewDidAppear():
let userLocation = UserLocation() // initializes properly but code below doesn't wait.
locationsArray[0].name = userLocation.place
locationsArray[0].coordinates = userLocation.coordinates
And my UserLocation.swift class:
import Foundation
import CoreLocation
class UserLocation {
var place = ""
var coordinates = ""
let locationManager = CLLocationManager()
var currentLocation: CLLocation!
init() {
returnResults()
}
func returnResults () {
getUserLocation { placemark in
if placemark != nil {
self.place = (placemark?.name)!
self.coordinates = "\((placemark?.location?.coordinate.latitude)!),\((placemark?.location?.coordinate.longitude)!)"
} else {
print("Error retrieving placemark")
}
}
}
func getUserLocation(completion: #escaping (CLPlacemark?) -> ()) {
var placemark: CLPlacemark?
locationManager.requestWhenInUseAuthorization()
if (CLLocationManager.authorizationStatus() == CLAuthorizationStatus.authorizedWhenInUse ||
CLLocationManager.authorizationStatus() == CLAuthorizationStatus.authorizedAlways) {
currentLocation = locationManager.location
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(currentLocation) { (placemarks, error) -> Void in
if error != nil {
print("Error getting location: \(error)")
placemark = nil
} else {
placemark = placemarks?.first
}
completion(placemark)
}
}
}
}
extension CLPlacemark {
var cityState: String {
var result = ""
switch (self.locality, self.administrativeArea, self.country) {
case (.some, .some, .some("United States")):
result = "\(locality!), \(administrativeArea!)"
case (.some, _ , .some):
result = "\(locality!), \(country!)"
default:
result = name ?? "Location Unknown"
}
return result
}
}

This is not necessarily a Swift issue. Your problem is caused by the fact that returnResults executes the variables setup in an async manner because it calls an async function - getUserLocation, which is async as reverseGeocodeLocation is async (this is how CoreLocation works - you don't get the location synchronously, but in a callback).
You don't want to wait for returnResults to execute the callback, as this would mean blocking the main thread while CoreLocation initializes and tries to determine the location. Instead you should follow the async pattern by using a completion block that returnResults can use to signal the completion of the location retrieval.
An example for the above would be:
class UserLocation {
var place = ""
var coordinates = ""
let locationManager = CLLocationManager()
var currentLocation: CLLocation!
init() {
// don't call anymore from here, let the clients ask for the locations
}
// This was renamed from returnResults to a more meaningful name
// Using the Bool in the completion to signal the success/failure
// of the location retrieval
func updateLocations(withCompletion completion: #escaping (Bool) -> Void) {
getUserLocation { placemark in
if placemark != nil {
self.place = (placemark?.name)!
self.coordinates = "\((placemark?.location?.coordinate.latitude)!),\((placemark?.location?.coordinate.longitude)!)"
completion(true)
} else {
print("Error retrieving placemark")
completion(false)
}
}
}
...
You can then modify the called code to something like this:
let userLocation = UserLocation()
userLocation.updateLocations { success in
guard success else { return }
locationsArray[0].name = userLocation.place
locationsArray[0].coordinates = userLocation.coordinates
}
You don't block the main thread, and you execute the appropriate code when the location is available.

Related

How do I get the #Published variable to update properly in Swift? [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 25 days ago.
Background
I'm trying to build a class that will easily convert string like addresses into a CLLocationCoordinate2D for later use that will be saved to a database.
I have a class that is similar to below:
final class PublishMapData: ObservableObject {
#Published var userAddressLat: Double = 0.0
#Published var userAddressLong: Double = 0.0
func saveMapData(address: String){
let address = "One Apple Park Way, Cupertino, CA 95014" //simulating the call from a save button for instance
convertAddress(address: address)
print(String(userAddressLat)) //this prints 0.0
print(String(userAddressLong)) //this print 0.0
//{...extra code here...}
//this is where I would be storing the coordinates into something like Firestore for later use
}
func convertAddress(address: String) {
getCoordinate(addressString: address) { (location, error) in
if error != nil {
return
}
DispatchQueue.main.async {
self.userAddressLat = location.latitude
print(self.userAddressLat) //this prints the correct value
self.userAddressLong = location.longitude
print(self.userAddressLong) //this prints the correct value
}
}
}
private func getCoordinate(addressString : String, completionHandler: #escaping(CLLocationCoordinate2D, NSError?) -> Void ) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(addressString) { (placemarks, error) in
if error == nil {
if let placemark = placemarks?[0] {
let location = placemark.location!
completionHandler(location.coordinate, nil)
return
}
}
completionHandler(kCLLocationCoordinate2DInvalid, error as NSError?)
}
}
}
For some reason, I'm not getting the lat, long values from the convertAddress function to properly get stored within the #Published variables. What am I doing wrong?
I'm still learning Swift. Thanks in advance for any assistance.
According to https://developer.apple.com/documentation/corelocation/clgeocoder/1423509-geocodeaddressstring
geocodeAddressString(_:completionHandler:)
is an asynchronous function, which means its completion handler will get executed at a later point in time and the called function returns immediately.
Thus when you call
convertAddress(address: address)
it returns immediately scheduling the dispatchQueue closure to be called later.
print(String(userAddressLat)) //this prints 0.0
print(String(userAddressLong))
are executed next which prints 0.0
DispatchQueue.main.async {
self.userAddressLat = location.latitude
print(self.userAddressLat) //this prints the correct value
self.userAddressLong = location.longitude
print(self.userAddressLong) //this prints the correct value
}
are executed later.
#lastbreath Thank you for highlighting the asynchronous nature of geocodeAddressString(_:completionHandler:). Because of that I found that I could use an asynchronous geocodeAddressString call in lieu of my previous approach.
This was noted in the link you provided:
func geocodeAddressString(_ addressString: String) async throws -> [CLPlacemark]
This is the fixed code...much more simplistic to achieve sending the values to #Published variable.
final class PublishMapData: ObservableObject {
#Published var userAddressLat: Double = 0.0
#Published var userAddressLong: Double = 0.0
func saveMapData(address: String){
Task {
do {
let address = "One Apple Park Way, Cupertino, CA 95014" //simulating the call from a save button for instance
try await getCoordinate(addressString: address)
print(String(userAddressLat)) //this now prints correct value
print(String(userAddressLong)) //this now prints correct value
//{...extra code here...}
//this is where I would be storing the coordinates into something like Firestore for later use
}
catch {
}
}
}
func getCoordinate(addressString : String) async throws {
let geocoder = CLGeocoder()
let placemark = try await geocoder.geocodeAddressString(addressString)
await MainActor.run(body: {
userAddressLat = placemark[0].location!.coordinate.latitude
userAddressLong = placemark[0].location!.coordinate.longitude
})
}
}
Thank you for your assistance in getting the answer.

Is there a battery level did change notification equivalent for kIOPSCurrentCapacityKey on macOS?

I am building a Swift app that monitors the battery percentage, as well as the charging state, of a Mac laptop's battery. On iOS, there is a batteryLevelDidChange notification that is sent when the device's battery percentage changes, as well as a batteryStateDidChange notification that is sent when the device is plugged in, unplugged, and fully charged.
What is the macOS equivalent of those two notifications in Swift, or more specifically, for kIOPSCurrentCapacityKey and kIOPSIsChargingKey? I read through the notification documentation and didn't see any notifications for either. Here is the code I have for fetching the current battery charge level and charging status:
import Cocoa
import IOKit.ps
class MainViewController: NSViewController {
enum BatteryError: Error { case error }
func getMacBatteryPercent() {
do {
guard let snapshot = IOPSCopyPowerSourcesInfo()?.takeRetainedValue()
else { throw BatteryError.error }
guard let sources: NSArray = IOPSCopyPowerSourcesList(snapshot)?.takeRetainedValue()
else { throw BatteryError.error }
for powerSource in sources {
guard let info: NSDictionary = IOPSGetPowerSourceDescription(snapshot, ps as CFTypeRef)?.takeUnretainedValue()
else { throw BatteryError.error }
if let name = info[kIOPSNameKey] as? String,
let state = info[kIOPSIsChargingKey] as? Bool,
let capacity = info[kIOPSCurrentCapacityKey] as? Int,
let max = info[kIOPSMaxCapacityKey] as? Int {
print("\(name): \(capacity) of \(max), \(state)")
}
}
} catch {
print("Unable to get mac battery percent.")
}
}
override func viewDidLoad() {
super.viewDidLoad()
getMacBatteryPercent()
}
}
(I'm replying to this almost 3-year-old question as it is the third result that comes up on the Google search "swift iokit notification".)
The functions you're looking for are IOPSNotificationCreateRunLoopSource and IOPSCreateLimitedPowerNotification.
Simplest usage of IOPSNotificationCreateRunLoopSource:
import IOKit
let loop = IOPSNotificationCreateRunLoopSource({ _ in
// Perform usual battery status fetching
}, nil).takeRetainedValue() as CFRunLoopSource
CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, .defaultMode)
Note that the second parameter context is passed as the only parameter in the callback function, which can be used to pass the instance as a pointer to the closure since C functions do not capture context. (See the link below for actual implementation.)
Here is my code that converts the C-style API into a more Swift-friendly one using the observer pattern: (don't know how much performance benefit it will has for removing run loops)
import Cocoa
import IOKit
// Swift doesn't support nested protocol(?!)
protocol BatteryInfoObserverProtocol: AnyObject {
func batteryInfo(didChange info: BatteryInfo)
}
class BatteryInfo {
typealias ObserverProtocol = BatteryInfoObserverProtocol
struct Observation {
weak var observer: ObserverProtocol?
}
static let shared = BatteryInfo()
private init() {}
private var notificationSource: CFRunLoopSource?
var observers = [ObjectIdentifier: Observation]()
private func startNotificationSource() {
if notificationSource != nil {
stopNotificationSource()
}
notificationSource = IOPSNotificationCreateRunLoopSource({ _ in
BatteryInfo.shared.observers.forEach { (_, value) in
value.observer?.batteryInfo(didChange: BatteryInfo.shared)
}
}, nil).takeRetainedValue() as CFRunLoopSource
CFRunLoopAddSource(CFRunLoopGetCurrent(), notificationSource, .defaultMode)
}
private func stopNotificationSource() {
guard let loop = notificationSource else { return }
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), loop, .defaultMode)
}
func addObserver(_ observer: ObserverProtocol) {
if observers.count == 0 {
startNotificationSource()
}
observers[ObjectIdentifier(observer)] = Observation(observer: observer)
}
func removeObserver(_ observer: ObserverProtocol) {
observers.removeValue(forKey: ObjectIdentifier(observer))
if observers.count == 0 {
stopNotificationSource()
}
}
// Functions for retrieving different properties in the battery description...
}
Usage:
class MyBatteryObserver: BatteryInfo.ObserverProtocol {
init() {
BatteryInfo.shared.addObserver(self)
}
deinit {
BatteryInfo.shared.removeObserver(self)
}
func batteryInfo(didChange info: BatteryInfo) {
print("Changed")
}
}
Credits to this post and Koen.'s answer.
I'd Use this link to get the percentage (looks cleaner)
Fetch the battery status of my MacBook with Swift
And to find changes in the state, use a timer to re-declare your battery state every 5 seconds and then set it as a new variable var OldBattery:Int re-declare it once again and set it as NewBattery, then, write this code:
if (OldBattery =! NewBattery) {
print("battery changed!")
// write the function you want to happen here
}

Swift completion handler in class and function

I have a Class with a function that connect to a firestoreDB and get some data:
import UIKit
import CoreLocation
import Firebase
private let _singletonInstance = GetBottlesFromDB()
class GetBottlesFromDB: NSObject {
class var sharedInstance: GetBottlesFromDB { return _singletonInstance }
var Pins = [LayoutBottlesFromDB]()
// MARK: - init
override init() {
super.init()
populatePinList(completion: { pin in self.Pins } )
//print("GET ALL PINS: \(Pins)")
}
func populatePinList(completion: #escaping ([LayoutBottlesFromDB]) -> ()) {
Pins = []
AppDelegate.ADglobalVar.db.collection("Bottles").whereField("pickupuser", isEqualTo: NSNull()).getDocuments { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
print("start getting documents:")
for document in querySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
//print("\(document.documentID)")
let bottleID:String = document.documentID
let bottlekind:Int = document.data()["bottle"] as! Int
var bottletitel:String
var bottlesub:String
var bottleurl:String = (document.data()["pic"] as? String)!
let pin = LayoutBottlesFromDB(document.data()["lat"] as! CLLocationDegrees, document.data()["long"] as! CLLocationDegrees, ID: bottleID, title: bottletitel, subtitle: bottlesub, type: bottlekind, url:bottleurl)
//print("GET DAATA from DB: \(pin)")
self.Pins.append(pin)
} //for
completion(self.Pins)
} //else
} //querysnap
}//function
}//class
in my ViewController I call this function.
for pin in GetBottlesFromDB.sharedInstance.Pins{
print("Add Pin : \(pin)")
}
My pProblem is that the function will called but the print is empty.
The function doesn't wait for a completion. What did I do wrong?
You are calling directly GetBottlesFromDB.sharedInstance.Pins and this will not wait for completion of populatePinList method so that's why you are getting blank So You need to wait for completion or you can check if data is not available in pins variable the you need to call completion method like this way:
GetBottlesFromDB.sharedInstance.populatePinList { (pins) in
for pin in pins{
print("Add Pin : \(pin)")
}
}
Nothing in your code waits for the execution of the asynchronous method, so that's no surprise. Also, it would be a terrible design because it would block your app. In addition, your singleton implementaion is overly verbose and doesn't guarantee that it stays a singleton, so I'd recommend to change it to
class GetBottlesFromDB {
private(set) var Pins = [LayoutBottlesFromDB]()
static let shared = GetBottlesFromDB()
private init() {}
// populatePinList as before
}
and in your view controller, e.g. in viewDidLoad do:
override func viewDidLoad() {
GetBottlesFromDB.shared.populatePinList { pins in
pins.forEach { print("Add Pin: \(pin)") }
}
}

RxSwift Driver calling twice on first time

I have a CoreLocation manager that should handle all CLLocationManager by offering observable properties through RxSwift (and its Extensions and DelegateProxies). LocationRepository looks like this:
class LocationRepository {
static let sharedInstance = LocationRepository()
var locationManager: CLLocationManager = CLLocationManager()
private (set) var supportsRequiredLocationServices: Driver<Bool>
private (set) var location: Driver<CLLocationCoordinate2D>
private (set) var authorized: Driver<Bool>
private init() {
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
supportsRequiredLocationServices = Observable.deferred {
let support = CLLocationManager.locationServicesEnabled() && CLLocationManager.significantLocationChangeMonitoringAvailable() && CLLocationManager.isMonitoringAvailable(for:CLCircularRegion.self)
return Observable.just(support)
}
.asDriver(onErrorJustReturn: false)
authorized = Observable.deferred { [weak locationManager] in
let status = CLLocationManager.authorizationStatus()
guard let locationManager = locationManager else {
return Observable.just(status)
}
return locationManager.rx.didChangeAuthorizationStatus.startWith(status)
}
.asDriver(onErrorJustReturn: CLAuthorizationStatus.notDetermined)
.map {
switch $0 {
case .authorizedAlways:
return true
default:
return false
}
}
location = locationManager.rx.didUpdateLocations.asDriver(onErrorJustReturn: []).flatMap {
return $0.last.map(Driver.just) ?? Driver.empty()
}
.map { $0.coordinate }
}
func requestLocationPermission() {
locationManager.requestAlwaysAuthorization()
}
}
My presenter then listens to changes on the repository properties. LocatorPresenter looks like this:
class LocatorPresenter: LocatorPresenterProtocol {
weak var view: LocatorViewProtocol?
var repository: LocationRepository?
let disposeBag = DisposeBag()
func handleLocationAccessPermission() {
guard repository != nil, view != nil else {
return
}
repository?.authorized.drive(onNext: {[weak self] (authorized) in
if !authorized {
print("not authorized")
if let sourceView = self?.view! as? UIViewController, let authorizationView = R.storyboard.locator.locationAccessRequestView() {
sourceView.navigationController?.present(authorizationView, animated: true)
}
} else {
print("authorized")
}
}).addDisposableTo(disposeBag)
}
}
It does work, but I'm getting the Driver calling twice for the first time I try to get the authorization status, so the access request view gets presented twice. What am I missing here?
Regards!
From startWith documentation:
StartWith
emit a specified sequence of items before beginning to emit the items from the source Observable
I have not tried it, but probably if you remove startWith(status) you won't receive the status twice.
It seems you are receiving the next sequence from the observable:
---------------------------------unauthorized----authorized----->
So with the line:
startWith(status) // status is unauthorized
you finally get this one:
-------unauthorized---------unauthorized----authorized----->

What is the cause of the zombies in the following code

I have the following class for collecting device motion data:
class MotionManager: NSObject {
static let shared = MotionManager()
private override init() {}
// MARK: - Class Variables
private let motionManager = CMMotionManager()
fileprivate lazy var locationManager: CLLocationManager = {
var locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.activityType = .fitness
locationManager.distanceFilter = 10.0
return locationManager
}()
private let queue: OperationQueue = {
let queue = OperationQueue()
queue.name = "MotionQueue"
queue.qualityOfService = .utility
return queue
}()
fileprivate var motionDataRecord = MotionDataRecord()
private var attitudeReferenceFrame: CMAttitudeReferenceFrame = .xTrueNorthZVertical
var interval: TimeInterval = 0.01
var startTime: TimeInterval?
// MARK: - Class Functions
func start() {
startTime = Date().timeIntervalSince1970
startDeviceMotion()
startAccelerometer()
startGyroscope()
startMagnetometer()
startCoreLocation()
}
func startCoreLocation() {
switch CLLocationManager.authorizationStatus() {
case .authorizedAlways:
locationManager.startUpdatingLocation()
locationManager.startUpdatingHeading()
case .notDetermined:
locationManager.requestAlwaysAuthorization()
case .authorizedWhenInUse, .restricted, .denied:
break
}
}
func startAccelerometer() {
if motionManager.isAccelerometerAvailable {
motionManager.accelerometerUpdateInterval = interval
motionManager.startAccelerometerUpdates(to: queue) { (data, error) in
if error != nil {
log.error("Accelerometer Error: \(error!)")
}
guard let data = data else { return }
self.motionDataRecord.accelerometer = data
}
} else {
log.error("The accelerometer is not available")
}
}
func startGyroscope() {
if motionManager.isGyroAvailable {
motionManager.gyroUpdateInterval = interval
motionManager.startGyroUpdates(to: queue) { (data, error) in
if error != nil {
log.error("Gyroscope Error: \(error!)")
}
guard let data = data else { return }
self.motionDataRecord.gyro = data
}
} else {
log.error("The gyroscope is not available")
}
}
func startMagnetometer() {
if motionManager.isMagnetometerAvailable {
motionManager.magnetometerUpdateInterval = interval
motionManager.startMagnetometerUpdates(to: queue) { (data, error) in
if error != nil {
log.error("Magnetometer Error: \(error!)")
}
guard let data = data else { return }
self.motionDataRecord.magnetometer = data
}
} else {
log.error("The magnetometer is not available")
}
}
func startDeviceMotion() {
if motionManager.isDeviceMotionAvailable {
motionManager.deviceMotionUpdateInterval = interval
motionManager.startDeviceMotionUpdates(using: attitudeReferenceFrame, to: queue) { (data, error) in
if error != nil {
log.error("Device Motion Error: \(error!)")
}
guard let data = data else { return }
self.motionDataRecord.deviceMotion = data
self.motionDataRecord.timestamp = Date().timeIntervalSince1970
self.handleMotionUpdate()
}
} else {
log.error("Device motion is not available")
}
}
func stop() {
locationManager.stopUpdatingLocation()
locationManager.stopUpdatingHeading()
motionManager.stopAccelerometerUpdates()
motionManager.stopGyroUpdates()
motionManager.stopMagnetometerUpdates()
motionManager.stopDeviceMotionUpdates()
}
func handleMotionUpdate() {
print(motionDataRecord)
}
}
// MARK: - Location Manager Delegate
extension MotionManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedAlways || status == .authorizedWhenInUse {
locationManager.startUpdatingLocation()
} else {
locationManager.stopUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
motionDataRecord.location = location
}
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
motionDataRecord.heading = newHeading
}
}
However I'm getting EXC_BAD_ACCESS after it runs for a while. I ran the zombie instrument and it appears that handleMotionUpdate() is the caller at fault. And MotionDataRecord or some of it's properties are what are being deallocated somehow...
MotionDataRecord is a struct:
struct MotionDataRecord {
var timestamp: TimeInterval = 0
var location: CLLocation?
var heading: CLHeading?
var motionAttitudeReferenceFrame: CMAttitudeReferenceFrame = .xTrueNorthZVertical
var deviceMotion: CMDeviceMotion?
var altimeter: CMAltitudeData?
var accelerometer: CMAccelerometerData?
var gyro: CMGyroData?
var magnetometer: CMMagnetometerData?
}
Any ideas what's going on here?
Edit:
Have added a stripped down version of the project to github here
Edit:
Screenshot of zombies instrument:
Okay, I'm going to try to do a little thought-experiment to suggest what might be happening here.
Keep in mind first the following points:
Your MotionDataRecord is a struct consisting almost entirely of reference type instance properties. This forces the struct to participate in reference counting.
You are wildly accessing the properties of this struct on different threads. Your locationManager:didUpdateLocations: sets motionDataRecord.location on the main thread, while e.g. your motionManager.startDeviceMotionUpdates sets motionDataRecord.deviceMotion on a background thread (queue).
Every time you set a struct property, you mutate the struct. But there is actually no such thing as struct mutation in Swift: a struct is a value type. What really happens is that the entire struct is copied and replaced (initializeBufferWithCopyOfBuffer in the zombie log).
Okay, so on multiple simultaneous threads you are coming in and replacing your struct-full-of-references. Each time you do that, one struct copy goes out of existence and another comes into existence. It's a struct-full-of-references, so this involves reference counting.
So suppose the process looks like this:
Make the new struct.
Set the new struct's reference properties to the old struct's reference properties (except for the one we are changing) by copying the references. There is some retain-and-release here but it all balances out.
Set the new struct's reference property that we are replacing. This does a retain on the new value and releases the old value.
Swap the new struct into place.
But none of that is atomic. Thus, those steps can run out of order, interleaved between one another, because (remember) you've got more than one thread accessing the struct at the same time. So imagine that, on another thread, we access the struct between steps and 3 and 4. In particular, between steps 3 and 4 on one thread, we perform steps 1 and 2 on the other thread. At that moment, the old struct is still in place, with its reference to the property that we are replacing pointing to garbage (because it was released and deallocated in step 3 on the first thread). We attempt to do our copy on the garbage property. Crash.
So, in a nutshell, I would suggest (1) make MotionDataRecord a class instead of a struct, and (2) get your threading straightened out (at the very least, get onto the main thread in the CMMotionManager callbacks before you touch the MotionDataRecord).