RxSwift Driver calling twice on first time - swift

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----->

Related

UnitTest does't work properly for MVP pattern Swift

I'm trying to write some UnitTests for the first time. My pattern is MVP and I'm trying to test my Presenter. I've created mock class: class TeamViewMock: TeamViewPresenterProtocol { }. It contains all the methods from my real Presenter. Inside the each method I'm trying to set the new value for the property, so when the method called - property should get a new value.
Only one property gets new value out of 4 and I've no clue why the other ones didn't get it.
You may see it in the following code
import XCTest
#testable import NHL
class TeamViewPresenterTest: XCTestCase {
var presenter: TeamViewPresenter!
var viewMock: TeamViewMock!
func setupPresenter() {
viewMock = TeamViewMock()
presenter = TeamViewPresenter(with: viewMock)
}
func testGetData() {
setupPresenter()
presenter.getData(completion: {_ in })
XCTAssertTrue(viewMock.isStart) // This one works and returns true
XCTAssertTrue(viewMock.isStop) // Return error
XCTAssertTrue(viewMock.isEndRefreshing) // Return error
XCTAssertTrue(viewMock.isReload) // Return error
}
}
class TeamViewMock: TeamViewPresenterProtocol {
var isStart = false
var isStop = false
var isEndRefreshing = false
var isReload = false
func startAnimating() {
self.isStart = true // Testing stops here and doesn't go any further...
}
func stopAnimating() {
self.isStop = true
}
func endRefreshing() {
self.isEndRefreshing = true
}
func reloadView(_ teams: NHLDTO) {
self.isReload = true
}
}
class TeamViewPresenter {
// MARK: - Public Properties
private weak var view: TeamViewPresenterProtocol?
public let dataFetcherService = DataFetcherService()
// MARK: - Initializers
init(with view: TeamViewPresenterProtocol) {
self.view = view
}
// MARK: - Public Methods
public func getData(completion: #escaping (AppError) -> Void) {
view?.startAnimating() // Testing stops here and doesn't go any further, but still returns true for the property isStart and error for the rest
dataFetcherService.fetchTeamData { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(error)
print(error)
case .success(let teams):
guard let teams = teams else { return }
self.view?.reloadView(teams)
self.view?.stopAnimating()
self.view?.endRefreshing()
}
}
}
}
protocol TeamViewPresenterProtocol: AnyObject {
func startAnimating()
func stopAnimating()
func reloadView(_ teams: NHLDTO)
func endRefreshing()
}

SwiftUI Combine for .onReceive notifications with AVPlayer

I'm working in SwiftUI and have an AudioPlayer type that is a subclass of AVPlayer; it publishes AVPlayer's timeControllerStatus(?) (.playing, .paused, and others?). Rather than subclassing AVPlayer, I would like to pass in an AVPlayer and have it notify me using .onReceive in some View. Here is the current, functional type I have:
import AVKit
import Combine
class AudioPlayer: AVPlayer, ObservableObject {
#Published var buffering: Bool = false
override init() {
super.init()
registerObservers()
}
private func registerObservers() {
self.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "timeControlStatus", let change = change, let newValue = change[NSKeyValueChangeKey.newKey] as? Int, let oldValue = change[NSKeyValueChangeKey.oldKey] as? Int {
let oldStatus = AVPlayer.TimeControlStatus(rawValue: oldValue)
let newStatus = AVPlayer.TimeControlStatus(rawValue: newValue)
if newStatus != oldStatus {
DispatchQueue.main.async {[weak self] in
if newStatus == .playing || newStatus == .paused {
self?.buffering = false
} else {
self?.buffering = true
}
}
}
}
}
}
And here is an example of a class like the one I would like (taken from Chris Mash's tutorial on SwiftUI & AVPlayer):
import Combine
import AVFoundation
class PlayerItemObserver {
let publisher = PassthroughSubject<Bool, Never>()
private var itemObservation: NSKeyValueObservation?
init(player: AVPlayer) {
// Observe the current item changing
itemObservation = player.observe(\.currentItem) { [weak self] player, change in
guard let self = self else { return }
// Publish whether the player has an item or not
self.publisher.send(player.currentItem != nil)
}
}
deinit {
if let observer = itemObservation {
observer.invalidate()
}
}
}
Your help is much appreciated.
As I understand, you need to observe timeControlStatus just like in article example. For this you can just replace observer:
import Combine
import AVFoundation
class PlayerItemObserver {
let controlStatusChanged = PassthroughSubject<AVPlayer.TimeControlStatus, Never>()
private var itemObservation: NSKeyValueObservation?
init(player: AVPlayer) {
itemObservation = player.observe(\.timeControlStatus) { [weak self] player, change in
guard let self = self else { return }
self.controlStatusChanged.send(player.timeControlStatus)
}
}
deinit {
if let observer = itemObservation {
observer.invalidate()
}
}
}
// MARK: init view
let player = AudioPlayer()
let playerObserver = PlayerItemObserver(player: player)
let contentView = SongListView(playerObserver: playerObserver)
// MARK: react on changing in view:
struct ContentView: View {
let playerObserver: PlayerItemObserver
var body: some View {
Text("Any view")
.onReceive(playerObserver.controlStatusChanged) { newStatus in
switch newStatus {
case .waitingToPlayAtSpecifiedRate:
print("waiting")
case .paused:
print("paused")
case .playing:
print("playing")
}
}
}
}
UPDATE you can achieve the same without "old school" observe, using #Published and AnyCancellable. The last one even don't need extra code in deinit. Here is this solution:
import Combine
import AVFoundation
class PlayerItemObserver {
#Published var currentStatus: AVPlayer.TimeControlStatus?
private var itemObservation: AnyCancellable?
init(player: AVPlayer) {
itemObservation = player.publisher(for: \.timeControlStatus).sink { newStatus in
self.currentStatus = newStatus
}
}
}
// MARK: you need to change view with new observation, but in general it will be the same
struct ContentView: View {
let playerObserver: PlayerItemObserver
var body: some View {
Text("Any view")
.onReceive(playerObserver.$currentStatus) { newStatus in
switch newStatus {
case nil:
print("nothing is here")
case .waitingToPlayAtSpecifiedRate:
print("waiting")
case .paused:
print("paused")
case .playing:
print("playing")
}
}
}
}
NSObject has a method that gives you a Publisher for any KVO-compliant property. It's not documented, but it was discussed in WWDC 2019 sessions. For example, Raleigh Ledet described it starting at 25m36s in Session 231: Introducing SwiftUI, and Michael LeHew used it at 11m47s in Session 721: Combine in Practice.
The method is declared like this:
public func publisher<Value>(
for keyPath: KeyPath<Self, Value>,
options:NSKeyValueObservingOptions = [.initial, .new]
) -> NSObject.KeyValueObservingPublisher<Self, Value>
So, for example, you can use it like this:
player.publisher(for: \.timeControlStatus, options: [.initial])
.sink { print("player status: \($0)") }
.store(in: &tickets)

Boolean returns nil and unable to access value from response on view controller

I have a usermodel that checks the backend if the email exists - then I drill back into a viewcontroller and set a boolean value that should trigger a function run. However the value is unchanged and I am trying to change this value from the usermodel but it is not accessible. I understand why it does not work.. but do not know how to resolve the issue.
static func sendEmailWithResetLink(email: String) {
let params : Parameters = [
PARAM_EMAIL : email
]
request(URL_RESET_PASSWORD as String, method: .post, parameters: params, headers: nil).responseJSON {
(response: DataResponse<Any>) in
hideProgress()
print("this is response \(response)")
switch(response.result)
{
case .success(_):
print("it did not fail")
let passwordResetVC = PasswordResetViewController()
passwordResetVC.hasFailed = false
break
case .failure(_):
print("it failed")
let passwordResetVC = PasswordResetViewController()
//here boolean is set that I am trying to access in viewcontroller
passwordResetVC.hasFailed = true
break
}
}
}
Here's what I would suggest. You probably have some of these in place already:
Create an PasswordResetViewController object has an #IBAction func resetButtonClicked triggered by a button or whatever, which kicks off the password reset process.
Create a UserManager class. This class is responsible for all profile management activies in your app. Among other things, it has the ability to reset user passwords. This UserManager would probably be a singleton, that' sprobably good enough for now.
Create a new UserManagerDelegate protocol. Add to it all capabilities that are required by the UserManager to inform them of whatever happened. For example: var passwordResetHasFailed: Bool { get set }.
Extend your PasswordResetViewController conform to this protocol.
Your VC gets a reference to the singleton UserManager object, stores it in an instance variable, and uses that to access the shared object from then on.
Make your PasswordResetViewController register itself as the delegate to the user manager, with userManager.delegate = self
The #IBAction func resetButtonClicked will just call userManager.resetPassword()
Your UserManager does whatever it needs to do to reset the user's password.
When it's done, it'll call self.delegate?.passwordResetHasFailed = true/false.
Since your PasswordResetViewController registered itself as the delegate of the UserManager, when the operation is done, its passwordResetHasFailed property will be changed, giving it a chance to respond (by updating some UI or whatever).
There are some limitations to this approach, but it's a decent way to get started. Some thing to note:
This lets you unit test your PasswordResetViewController. You can create a MockUserManager, and set tesPasswordResetViewController.userManager = MockUserManager(), allowing you to separate out the user manager, and test PasswordResetViewController in isolation.
You'll run into issues if you need multiple objects to subscribe to receive delegate call backs (since there can only be 1 delegate object). At that point, you can switch to using something like Promises, RxSwift or Combine. But that's a problem for a later time, and the migration would be easy.
Going off of #Alexander - Reinstate Monica and what I assume what the code to look like to approach your problem.
Using MVC:
In Models folder (data/ logic part)
public class User {
private var name: String!
private var userEmail: String!
public var hasFailed: Bool?
init() {
name = ""
userEmail = ""
hasFailed = nil
}
public func setName(name: String) { self.name = name }
public func getName() -> String { return name }
public func setEmail(email: String) { userEmail = email }
public func getEmail() ->String { return userEmail }
public static func sendEmailWithRestLing(email: String) {
// your other code
switch response.result {
case .success(_):
//your code
hasFailed = false
break
case .failuare(_):
// your code
hasFailed = true
break
}
}
}
User Manager class applying singleton design
final class UserManager {
private var user = User()
static let instance = UserManager()
private init(){}
public func userName(name: String) {
if (name.count > 3) {
user.setName(name: name)
}
else { print("user name is too short") }
}
public func userEmail(email: String) {
if (email.count > 3) {
user.setEmail(email: email)
}
else { print("user email is too short") }
}
public func getUserName() -> String {
let name = user.getName()
if (name.isEmpty) { return "user name is Empty" }
return name
}
public func getUserEmail() -> String {
let email = user.getEmail()
if (email.isEmpty) { return "user email is Empty" }
return email
}
public func doKatieTask(link: String) -> Int {
guard let myValue = user.hasFailed else {
return -1
}
if (myValue) { return 1}
return 0
}
}
So, Now in the Controllers folder and since we a one-to-one relation we will use delegate design pattern. If had had one-to-many with the view controller. Use observers.
class ViewController: UIViewController {
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var emailTextField: UITextField!
var _hasFail: Bool!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func doTask() {
UserManager.instance.userName(name: nameTextField.text!)
UserManager.instance.userEmail(email: emailTextField.text!)
switch UserManager.instance.doKatieTask(link: emailTextField.text!) {
case 0:
_hasFail = false
break
case 1:
_hasFail = true
break
default:
print("hasFailed is nil")
break
}
if let vc = storyboard?.instantiateViewController(identifier: "passwordVC") as? PasswordResetViewController {
vc.modalPresentationStyle = .fullScreen
vc.delegate = self
self.present(vc, animated: true, completion: nil)
}
}
}
extension ViewController: KatieDelegate {
var hasFailed: Bool {
get {
return _hasFail
}
set {
_hasFail = newValue
}
}
}
In PasswordReset UIViewController
protocol KatieDelegate {
var hasFailed: Bool { get set }
}
class PasswordResetViewController: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var emailLabel: UILabel!
var delegate: KatieDelegate?
override func viewDidLoad() {
super.viewDidLoad()
nameLabel.text = UserManger.instance.getUserName()
emailLabel.text = UserManger.instance.getUserEmail()
if let delegate = delegate {
print("The value for has failed is: .....\(delegate.hasFailed)!")
}
else { print("error with delegate") }
}
}

Swift: too many static functions?

I have a class that represents Calendar items (model) retrieved from the event store. I haven't implemented any delegation yet for the AppDelegate or ViewControllers.
All my methods in this class are static functions - the main reason is so that I can "see" them from the AppDelegate or the VC. I have a suspicion that:
1) I need to make this a singleton - whose only function is to retrieve calendar items from the eventStore and post to the UI
2) learn how to code better - perhaps creating an instance of the class in the AppDelegate and the VC
This is still very fuzzy to me - not sure if posting code would help, but the class has a bunch of "static func .... doSomething() { ...}" and is called by the AppDelegate and VC as "ClassName.doSomething()..."
I'm prepared to refactor the Class code, thinking that a singleton would work - or perhaps things are just fine as they are...
EDITED: Adding code:
import Foundation
import EventKit
class Calendars: NSObject {
enum calendarAuthState {
case restricted
case authorized
case denied
case notDetermined
}
struct Calendar {
var id: String
var color: NSColor
var title: String
var isUserActive: Bool
var events: [EventItem]
}
struct EventItem {
var originalStartDate: Date
var date: String
var title: String
var isAllDayEvent: Bool
}
static var calendarState: calendarAuthState = .notDetermined
static var eventStore = EKEventStore()
static var currentCalendars = [Calendar]()
//MARK: Check Calendar Authorization Status
static func calendarAuthorizationStatus() {
let status = EKEventStore.authorizationStatus(for: .event)
switch (status) {
case EKAuthorizationStatus.notDetermined:
// This happens on first-run
calendarState = .notDetermined
case EKAuthorizationStatus.authorized:
calendarState = .authorized
case EKAuthorizationStatus.restricted:
self.requestAccessToCalendar()
calendarState = .restricted
case EKAuthorizationStatus.denied:
self.requestAccessToCalendar()
calendarState = .denied
}
}
static func requestAccessToCalendar() {
self.eventStore.requestAccess(to: EKEntityType.event, completion: {
(accessGranted: Bool, error: Error?) in
if accessGranted == true {
DispatchQueue.main.async(execute: {
self.calendarState = .authorized
})
} else {
DispatchQueue.main.async(execute: {
self.calendarState = .denied
})
}
})
}
//MARK: Do the two below
static func createMenuFromCalendars() {
guard calendarState == .authorized else {
return
}
let calendars = self.returnCalendars()
guard calendars.count >= 0 else {
return
}
self.addCalendarsToMenuItems(from: calendars)
}
//MARK: First, return the calendar titles from the Store
static func returnCalendars() -> [Calendar] {
guard self.calendarState == .authorized else {
return[]
}
let calendars = self.eventStore.calendars(for: .event)
for calendar in calendars {
self.currentCalendars.append(Calendar(id: calendar.calendarIdentifier, color: calendar.color, title: calendar.title, isUserActive: false, events: []))
}
return self.currentCalendars
}
//MARK: Next, send those to the Menu for MenuItem creation
static func addCalendarsToMenuItems(from calendars:[Calendar]) {
let appDelegate = NSApplication.shared.delegate as! AppDelegate
let appMainMenu = NSApp.mainMenu
if let calendarMenu = appMainMenu?.item(withTitle: "Calendars") {
let calendarSubMenu = calendarMenu.submenu
for calendar in calendars {
let menuItem = calendarSubMenu?.addItem(withTitle: calendar.title, action: #selector(appDelegate.actionFromSelectedCalendar) , keyEquivalent: "")
menuItem?.isEnabled = true
menuItem?.state = .off
menuItem?.target = appDelegate.self
menuItem?.toolTip = calendar.id
}
}
}
class func retrieveCalendarEvents() {
guard self.calendarState == .authorized || !(self.currentCalendars.isEmpty) else {
return
}
let startDate = Date()
let endDate = Date(timeIntervalSinceNow: 4*24*3600)
var activeCalendars = findUserActiveCalendars(in: currentCalendars)
//need to flush the events at this stage or they'll pile
guard !((activeCalendars?.isEmpty)!) else {
return
}
var eventCalendar = [EKCalendar]()
for dayBookCalendar in activeCalendars! {
// much of the risk here is unwrapping optionals unsafely!!!!! - refactor this and other please
eventCalendar.append(self.eventStore.calendar(withIdentifier: dayBookCalendar.id)!)
let eventPredicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: eventCalendar)
let returnedEvents = eventStore.events(matching: eventPredicate)
let calendarIndex = findCalendarIndex(by: dayBookCalendar.id, in: currentCalendars)
for event in returnedEvents {
let eventItems = eventItem(from: event)
currentCalendars[calendarIndex!].events.append(eventItems)
}
}
}
//MARK: Helper methods and stuff
static func changeUserCalendarState(with id:String, state:Bool) {
guard !(currentCalendars.isEmpty) else {
return
}
let calendarIndex = findCalendarIndex(by: id, in:self.currentCalendars)
if let calendarIndex = calendarIndex {
currentCalendars[calendarIndex].isUserActive = !state
retrieveCalendarEvents()
}
}
static func findCalendarIndex(by id:String, in calendarArray: [Calendar]) -> Int? {
return calendarArray.index(where: {$0.id == id})
}
static func findUserActiveCalendars(in calendarArray: [Calendar]) -> [Calendar]? {
return calendarArray.filter({$0.isUserActive == true})
}
// static func flushEventsFromCalendar(in calendarArray: inout [Calendar]) {
// calendarArray.map({$0.events.removeAll()})
// }
static func eventItem(from events:EKEvent) -> EventItem {
return EventItem(originalStartDate: events.startDate, date:eventTime(from: events.startDate), title: events.title!, isAllDayEvent: events.isAllDay)
}
static func parseCalendarEvents(from events:[EKEvent]) -> [EventItem] { //can this be variadic?
var calendarEvents = [EventItem]()
for event in events {
calendarEvents.append(eventItem(from: event))
}
return calendarEvents
}
static func eventTime(from date:Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.timeStyle = .short
dateFormatter.locale = Locale.current
let stringTime = dateFormatter.string(from: date)
return stringTime
}
}
''
I think you're making an elementary mistake about object-oriented programming. In your Calendars class you seem to have encapsulated all the code for accessing the user's calendar. Then you seem to have reasoned: "Well, this code needs to be callable from anywhere. Therefore all my class's members need to be global (static / class)."
That's a mistake. There is nothing wrong with doing such encapsulation; indeed it's a good thing. But then the way to use your encapsulation is with a helper instance. For example, let's say you're in a view controller (which is most likely after all). Then it can have a property:
let calendarHelper = Calendars()
Now all (or nearly all) your members can (and should) become instance members. Remember, instances of the same type each get to maintain state separately from one another; that is part of their encapsulation. You're going to want that ability.
If your underlying reason for thinking you need static/class members is that you only want one EKEventStore instance for the life of the app, then push the globalness / staticness down to that one object (e.g. by a "shared" EKEventStore and methods for accessing it) and let everything else be a normal instance member.
From what you've said, suspicion 1) is correct - you need to use a singleton:
class CalendarService {
private var eventStore = EKEventStore()
//Static shared instance, this is your singleton
static var sharedInstance = CalendarService()
//Your public methods for adding events can go here
public func doSomething() {
//...
}
//As can your private methods for producing, deleting and editing calendar events + checking permissions
}
Usage:
CalendarService.sharedInstance.doSomething()
I can't really say much more without specific examples of your existing code.

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).