Is it bad idea to create static stream in reactive programming? - ios

Now I'm working on iOS using RxSwift framework. In my app I have to user user location, but I don't need it to be updated in real time. It's enough if location updated every time user opens app or does some defined action. Therefore, how about implementing singleton class where the last result is cached. Each update by action changes cached result and accepts it to the stream. Stream's default value is cached value. Then, views where user location is needed would subscribe on this stream.
Example implementation using Cache and RxSwift
import Foundation
import Cache
import CoreLocation
import RxSwift
import RxCocoa
class UserLocationManager: NSObject {
private enum Keys: String {
case diskConfig = "Disk Config"
case lastLocation = "User Last Location"
}
// MARK: - Variables
private func cache<T: Codable>(model: T.Type) -> Cache.Storage<T> {
let storage = try! Cache.Storage(diskConfig: DiskConfig(name: Keys.diskConfig.rawValue), memoryConfig: MemoryConfig(expiry: .never), transformer: TransformerFactory.forCodable(ofType: model))
return storage
}
private let locationManager = CLLocationManager()
private var lastPosition: MapLocation? {
get {
do {
return try cache(model: MapLocation.self).object(forKey: Keys.lastLocation.rawValue)
}
catch { return nil }
}
set {
do {
guard let location = newValue else { return }
try cache(model: MapLocation.self).setObject(location, forKey: Keys.lastLocation.rawValue)
}
catch { }
}
}
private let disposeBag = DisposeBag()
static let shared = UserLocationManager()
var locationStream = BehaviorRelay<CLLocationCoordinate2D?>(value: nil)
// MARK: - Methods
func updateLocation() {
if CLLocationManager.locationServicesEnabled() {
locationManager.requestLocation()
}
}
func subscribe() {
locationStream.accept(lastPosition?.clCoordinate2D)
locationStream.subscribe(onNext: { [weak self] location in
guard let `self` = self else { return }
guard let location = location else { return }
self.lastPosition = MapLocation(latitude: location.latitude, longitude: location.longitude)
}).disposed(by: disposeBag)
locationManager.delegate = self
}
// MARK: - Lifecycle
override init() {
super.init()
defer {
self.subscribe()
}
}
}
// MARK: - CLLocationManagerDelegate
extension UserLocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
UserLocationManager.shared.locationStream.accept(location.coordinate)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
}
}

There's no problem conceptually of having a global stream that can be subscribed to. However, your specific implementation is worrisome to me.
One of the cool things about Observable streams is that they are lazy, no work is done unless needed, but you are writing extra code to bypass that feature and I don't think it's necessary. Also, storing there current location when the app goes into the background and just assuming that is a valid location when the app comes back to the foreground (possibly weeks later) sounds inappropriate to me.
The RxCocoa package already has an Rx wrapper for CLLocationManager. It seems to me it would be far simpler to just use it. If you only need one location update then use .take(1). I'd be inclined to add a filter on the accuracy of the location before the take(1).

Related

Cannot get #EnvironmentObject in Controller

I'm trying to use #EnvironmentObject to control some aspects of my app. The issue I'm having is that one of my controllers can't access the environment object. I get the fatal error "No #ObservableObject of type Environment found".
I've searched other questions, and every solution I could find consisted of sending .environmentObject(myEnvironment) to the view in question. The problem is this is not a view, and I don't seem to have that option.
Also, in my SceneDelegate I send the environmentObject to the first view, so that is not the problem.
Here is my code.
First, I created a model to declare all my environment variables
Environment
struct Environment {
var showMenu: Bool
var searchText: String
var location : Location
init() {
self.showMenu = false
self.searchText = ""
self.location = Location()
}
}
Next I have a controller which purpose is to handle any actions related to the environment, right now it has none
EnvironmentController
import Foundation
class EnvironmentController : ObservableObject {
#Published var environment = Environment()
}
Now, in the SceneDelegate I call the NextDeparturesView, which in turn calls, the MapView.
MapView
import SwiftUI
import MapKit
//MARK: Map View
struct MapView : UIViewRepresentable {
#EnvironmentObject var environmentController: EnvironmentController
var locationController = LocationController()
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
let coordinate = CLLocationCoordinate2D(
latitude: environmentController.environment.location.latitude,
longitude: environmentController.environment.location.longitude)
let span = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.showsUserLocation = true
uiView.setRegion(region, animated: true)
}
}
You'll notice that in the MapView I call the LocationController, which is where the fatal error occurs
LocationController
import SwiftUI
import MapKit
import CoreLocation
final class LocationController: NSObject, CLLocationManagerDelegate, ObservableObject {
//MARK: Vars
#EnvironmentObject var environmentController: EnvironmentController
#ObservedObject var userSettingsController = UserSettingsController()
//var declaration - Irrelevant code to the question
//MARK: Location Manager
var locationManager = CLLocationManager()
//MARK: Init
override init() {
//more irrelevant code
super.init()
//Ask for location access
self.updateLocation()
}
//MARK: Functions
func updateLocation() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
if locationManager.responds(to: #selector(CLLocationManager.requestAlwaysAuthorization)){
locationManager.requestAlwaysAuthorization()
}
else {
locationManager.startUpdatingLocation()
}
}
//MARK: CLLocationManagerDelegate methods
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Error updating location :%#", error)
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .notDetermined:
self.setDefaultLocation()
break
case .restricted:
self.setDefaultLocation()
break
case .denied:
self.setDefaultLocation()
break
default:
locationManager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let currentLocation = manager.location?.coordinate
self.environmentController.environment.location.latitude = Double(currentLocation!.latitude)
self.environmentController.environment.location.longitude = Double(currentLocation!.longitude)
manager.stopUpdatingLocation()
}
//MARK: Other Functions
func recenter() {
locationManager.startUpdatingLocation()
}
func setDefaultLocation() {
if self.$userSettingsController.userCity.wrappedValue == "" {
self.environmentController.environment.location.latitude = 0.0
self.environmentController.environment.location.longitude = 0.0
} else {
self.environmentController.environment.location.latitude = self.citiesDictionary[self.userSettingsController.userCity]!.latitude
self.environmentController.environment.location.longitude = self.citiesDictionary[self.userSettingsController.userCity]!.longitude
}
}
}
So, this is where the fatal error occurs. For instance, my app usually calls setDefaultLocation() first, and the app is crashing there. Any idea what I am doing wrong, or how to solve it?
Thank you in advance.
EDIT
After much help from #pawello2222 I've solved my problem, however with some changes to the overall structure of my application.
I will accept his answer as the correct one, but I'll provide a list of things that I did, so anyone seeing this in the future might get nudged in the right direction.
I was wrongly assuming that View and UIViewRepresentable could both access the #EnvironmentObject. Only View can.
In my Environment struct, instead of a Location var, I now have a LocationController, so the same instance is used throughout the application. In my LocationController I now have a #Published var location: Location, so every View has access to the same location.
In structs of the type View I create the #EnvironmentObject var environmentController: EnvironmentController and use the LocationController associated with it. In other class types, I simply have an init method which receives a LocationController, which is sent through the environmentController, for instance, when I call MapView I do: MapView(locController: environmentController.environment.locationController) thus insuring that it is the same controller used throughout the application and the same Location that is being changed. It is important that to use #ObservedObject var locationController: LocationController in classes such as MapView, otherwise changes won't be detected.
Hope this helps.
Don't use #EnvironmentObject in your Controller/ViewModel (in fact anywhere outside a View). If you want to observe changes to Environment in your Controller you can do this:
class Environment: ObservableObject {
#Published var showMenu: Bool = false
#Published var searchText: String = ""
#Published var location : Location = Location()
}
class Controller: ObservableObject {
#Published var showMenu: Bool
private var environment: Environment
private var cancellables = Set<AnyCancellable>()
init(environment: Environment) {
_showMenu = .init(initialValue: environment.showMenu)
environment.$showMenu
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.showMenu = value
})
.store(in: &cancellables)
}
}
You can also use other forms of Dependency Injection to inject the Environment (or even use a singleton).
Generally there are different ways to show your Environment variables (eg. showMenu) in the View (and refresh it):
1) The Environment is injected into your View (NOT to ViewModel) as an #EnvironmentObject - for cases when you need to access the Environment from the View only.
2) The ViewModel subscribes to the Environment (as presented above) and publishes its own variables to the View. No need to use an #EnvironmentObject in your View then.
3) The Environment is injected into your View as an #EnvironmentObject and then is passed to the ViewModel.

How can I wait until the LocationService Callback is executed?

I'm trying to execute a Location Service once. This LocationService is called from another object class, that will add the location info into the parameters. All of this will be one object.
The problem is that when I init the object, everything is populated less the location data, which will be populated a few ms later.
I need to wait until the callback is executed, to successfully generate the full object before using it
So considering that I have the next "LocationService" class
public class LocationService: NSObject, CLLocationManagerDelegate{
let manager = CLLocationManager()
var locationCallback: ((CLLocation?) -> Void)!
var locationServicesEnabled = false
var didFailWithError: Error?
public func run(callback: #escaping (CLLocation?) -> Void) {
locationCallback = callback
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
manager.requestWhenInUseAuthorization()
locationServicesEnabled = CLLocationManager.locationServicesEnabled()
if locationServicesEnabled {
manager.startUpdatingLocation()
}else {
locationCallback(nil)
}
}
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locationCallback(locations.last!)
manager.stopUpdatingLocation()
}
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
didFailWithError = error
locationCallback(nil)
manager.stopUpdatingLocation()
}
deinit {
manager.stopUpdatingLocation()
}
}
And it is called from the object class like this:
class ObjectX: NSObject{
//Class variables
#objc override init() {
let getLocation = LocationService()
getLocation.run {
if let location = $0 {
//get location parameters
}}
Finally the ObjectX class is initiated and used from other place
let getLocation = ObjectX()
//After initiate it I use the object for other purposes, but here the object is not complete, the location parameters have not been populated yet
How can I wait in the class that is calling it until the callback is executed? Should I use getLocation.performSelector()? How?
Maybe this is not the best way to resolve this but it worked for me.
Basically, instead of creating the ObjectX and setting the in the process, the location service is gonna be called the before, then in the callback, the ObjectX is gonna be initialised, then we can set the location parameters for the ObjectX with the location object the we received in the object.
We remove the location set from the initialiser
class ObjectX: NSObject{
//Class variables
#objc override init() {
//Setting the rest of the parameters that are not location
}}
Then class that was initialising the object, we init and run the LocationService, then in callback we create the ObjectX and we set the location parameters
let ls = LocationService()
ls.run { location in
let objectX = ObjectX()
objectX.location = location
//We can use the object here
}

iOS RxSwift how to connect Core bluetooth to Rx sequences?

I'm trying to create an observable sequence to indicate the status of Bluetooth on device. I'm using ReplaySubject<CBManagerState>, but am curious if there is something better, as I hear bad things about using onNext()
What is the appropriate way to connect callback delegates to the RxSwift observable domain?
class BluetoothStatusMonitor: NSObject, CBPeripheralManagerDelegate {
let bluetoothStatusSequence = ReplaySubject<CBManagerState>.create(bufferSize: 1)
var bluetoothPeripheralManager: CBPeripheralManager?
func checkBluetoothStatus()
{
//silently check permissions, without alert
let options = [CBCentralManagerOptionShowPowerAlertKey:0]
bluetoothPeripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: options)
}
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
bluetoothStatusSequence.onNext(peripheral.state)
}
}
This is exactly the kind of things that Subjects are good for. They exist primarily to convert non-Rx code into Rx code. That said, RxCocoa has the DelegateProxy type that is designed to handle a lot of the work necessary to do delegates right. It's still hard to figure out exactly how to implement one, but once you get the hang of it they are quite useful...
I have to admit that most of the code is black magic to me, but it does work. I try to explain as much as I can in comments below.
import RxSwift
import RxCocoa
import CoreBluetooth
// The HasDelegate protocol is an associated type for the DelegateProxyType
extension CBPeripheralManager: HasDelegate {
public typealias Delegate = CBPeripheralManagerDelegate
}
class CBPeripheralManagerDelegateProxy
: DelegateProxy<CBPeripheralManager, CBPeripheralManagerDelegate>
, DelegateProxyType
, CBPeripheralManagerDelegate {
init(parentObject: CBPeripheralManager) {
super.init(parentObject: parentObject, delegateProxy: CBPeripheralManagerDelegateProxy.self)
}
deinit {
_didUpdateState.onCompleted()
}
static func registerKnownImplementations() {
register { CBPeripheralManagerDelegateProxy(parentObject: $0) }
}
// a couple of static functions for getting and setting a delegate on the object.
static func currentDelegate(for object: CBPeripheralManager) -> CBPeripheralManagerDelegate? {
return object.delegate
}
static func setCurrentDelegate(_ delegate: CBPeripheralManagerDelegate?, to object: CBPeripheralManager) {
object.delegate = delegate
}
// required delegate functions must be implemented in the class. This is where Subjects come in.
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
_didUpdateState.onNext(peripheral.state)
}
fileprivate let _didUpdateState = PublishSubject<CBManagerState>()
}
extension Reactive where Base: CBPeripheralManager {
var delegate: CBPeripheralManagerDelegateProxy {
return CBPeripheralManagerDelegateProxy.proxy(for: base)
}
var state: Observable<CBManagerState> {
return delegate._didUpdateState
}
var didUpdateState: Observable<Void> {
return delegate._didUpdateState.map { _ in }
}
// optional methods are setup using the `methodInvoked` function on the delegate
var willRestoreState: Observable<[String: Any]> {
return delegate.methodInvoked(#selector(CBPeripheralManagerDelegate.peripheralManager(_:willRestoreState:)))
.map { $0[1] as! [String: Any] }
}
var didStartAdvertising: Observable<Error?> {
return delegate.methodInvoked(#selector(CBPeripheralManagerDelegate.peripheralManagerDidStartAdvertising(_:error:)))
.map { $0[1] as? Error }
}
// I didn't implement all of the optionals. Use the above as a template to implement the rest.
}
As far as I can tell, the methodInvoked function performs some meta-programming magic on the object to install the method at runtime. This is done because many of the iOS classes that have delegates actually behave differently depending on whether or not the method was defined on the delegate (regardless of what the method does,) so we don't want to simply give the proxy every method in the protocol.
Of course, once you have the above in place. You can do all the standard RX stuff with your peripheral manager:
bluetoothManager.rx.state
.subscribe(onNext: { state in print("current state:", state) })
.disposed(by: disposeBag)
bluetoothManager.rx.didStartAdvertising
.subscribe(onNext: { error in
if let error = error {
print("there was an error:", error)
}
}
.disposed(by: disposeBag)

Custom CLLocationManager didUpdateLocations Function

I read the Radar.io (https://radar.io/documentation/sdk#ios
) iOS SDK documentation and I'm curious how I create a custom
locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) function.
class ViewController: UIViewController, RadarDelegate {
override func viewDidLoad() {
super.viewDidLoad()
Radar.setDelegate(self)
}
}
And
Radar.trackOnce(completionHandler: { (status: RadarStatus, location: CLLocation?, events: [RadarEvent]?, user: RadarUser?) in
// do something with status, location, events, user
})
source: https://radar.io/documentation/sdk#ios
How i get location update and control CLLocationManager in a custom class like this?
protocol MyRadarCloneDelegate {
}
class MyRadarClone {
static func setDelegate(_ delegate:) {
}
static func startTracking() {
}
// some other function for control CLLocationManager
}
If I understand your question correctly you want to access your location from Radar.trackOnce into your custom class.
You can do it this way.
Update your startTracking method this way:
func startTracking(location: CLLocation) {
//do somtrhing with your location
print(location)
}
And into your Radar.trackOnce method add below code:
guard let userLocation = location else {return}
let myRadar = MyRadarClone()
myRadar.startTracking(location: userLocation)
Hope this will help.
Note: Code is not tested.
Don’t create any custom protocols or functions for this
Create a NotificationCenter
Post from locationDidUpdate
Observe where you want
I think this is a best idea
I'm assuming the objective is to create a custom lifecycle method that acts like viewDidLayoutSubviews but is instead locationManagerDidGetLocation, or something, that is called only once in any view controller you put it in. If instead you simply want to extend the location manager's delegate to other view controllers, the strategy is the same (with a small adjustment at the end):
class SomeViewController: UIViewController {
func locationManagerDidGetLocation() {
doSomethingThatRequiresUserLocation()
}
}
CLLocationManagerDelegate has a method for new location data (and one for new location value). Simply post a notification from it:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
NotificationCenter.default.post(name: .locationManagerDidUpdateLocations, object: nil)
}
You can also share the value of the location across your app in a static structure variable or in the app's delegate:
struct CurrentLocation {
static var coordinate: CLLocationCoordinate2D?
}
And update it through the delegate:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// update shared property
let lastLocation = locations.last!
CurrentLocation.coordinate = lastLocation.coordinate
// post app-wide notification
NotificationCenter.default.post(name: .locationManagerDidUpdateLocations, object: nil)
}
To access that value just call CurrentLocation.coordinate anywhere in your app.
Then simply add a notification observer to an object, like a UIViewController, listening for that post:
class SomeViewController: UIViewController {
func addNotificationObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(locationManagerDidUpdateLocationsHandler), name: .locationManagerDidUpdateLocations, object: nil)
}
}
And when the first post arrives, remove the observer, so it only gets called once, and call your custom method:
#objc func locationManagerDidUpdateLocationsHandler() {
NotificationCenter.default.removeObserver(self, name: .locationManagerDidUpdateLocations, object: nil)
locationManagerDidGetLocation()
}
And now it works like a regular lifecycle method that is fired only once when the user's location is available:
class SomeViewController: UIViewController {
func locationManagerDidGetLocation() {
doSomethingThatRequiresUserLocation()
}
}
You can, of course, not remove the observer and have it function like an extended delegate of the location manager. And don't forget to remove all observers in deinit if these view controllers don't persist the life of the app.

Getting user's current location but getting back "Optional" California.. etc

I'm trying to get the user's current location using Swift. Here is what I am currently using:
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager();
//Info about user
#IBOutlet weak var userTF: UITextField!
#IBOutlet weak var BarbCustTF: UITextField!
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.requestWhenInUseAuthorization();
self.locationManager.startUpdatingLocation();
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// GPS STUFF
// UPDATE LOCATION
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
CLGeocoder().reverseGeocodeLocation(manager.location!) { (placemarks, ErrorType) -> Void in
if(ErrorType != nil)
{
print("Error: " + ErrorType!.localizedDescription);
return;
}
if(placemarks?.count > 0)
{
let pm = placemarks![0] ;
self.displayLocationInfo(pm);
}
}
}
// STOP UPDATING LOCATION
func displayLocationInfo(placemark: CLPlacemark)
{
self.locationManager.stopUpdatingLocation();
print(placemark.locality);
print(placemark.postalCode);
print(placemark.administrativeArea);
print(placemark.country);
}
// PRINT OUT ANY ERROR WITH LOCATION MANAGER
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
print("Error: " + error.localizedDescription);
}
Everything seems to work fine, but the output I'm getting is like really weird and says Optional in front of it, and is definitely (unfortunately) not my current location.
This is the output I'm getting when I print it to the console
Optional("Cupertino")
Optional("95014")
Optional("CA")
Optional("United States")
Things I've tried:
1) In my info.plist I have : NSLocationWhenInUseUsageDescription
2) I've also heard weird stuff happens and I tried going to Debug>>Location>> and changing it to in city and all sorts of things (didn't help)
I think that the problem is something in my function LocationManager that has to do with like "wrapping" or something ? I'm not sure, this is my first day messing with iOS programming with Swift and I don't really know what wrapping is but I think that may be what's going on from what I've seen on the internet... Basically I dont understand why i'm printing out some default apple locations (California.. blah blah) I don't live in Cali (unfortunately).
Instead of this
print(placemark.locality);
do this
if let locality = placemark.locality {
print(locality)
}
The if let pattern here is a way of only printing locality if it's not nil. This is the way to do it in this case.
If you were sure that locality was never nil, you could do
print(placemark.locality!)
but if locality happened to be nil, your app would crash on that line.

Resources