SwiftUI Combine for .onReceive notifications with AVPlayer - swift

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)

Related

GlobalActor directive doesn't guarantee a function will be called on that actor

Assuming I have defined a global actor:
#globalActor actor MyActor {
static let shared = MyActor()
}
And I have a class, in which a couple of methods need to act under this:
class MyClass {
#MyActor func doSomething(undoManager: UndoManager) {
// Do something here
undoManager?.registerUndo(withTarget: self) {
$0.reverseSomething(undoManager: UndoManager)
}
}
#MyActor func reverseSomething(undoManager: UndoManager) {
// Do the reverse of something here
print(\(Thread.isMainThread) /// Prints true when called from undo stack
undoManager?.registerUndo(withTarget: self) {
$0.doSomething(undoManager: UndoManager)
}
}
}
Assume the code gets called from a SwiftUI view:
struct MyView: View {
#Environment(\.undoManager) private var undoManager: UndoManager?
let myObject: MyClass
var body: some View {
Button("Do something") { myObject.doSomething(undoManager: undoManager) }
}
}
Note that when the action is undone the 'reversing' func it is called on the MainThread. Is the correct way to prevent this to wrap the undo action in a task? As in:
#MyActor func reverseSomething(undoManager: UndoManager) {
// Do the reverse of something here
print(\(Thread.isMainThread) /// Prints true
undoManager?.registerUndo(withTarget: self) {
Task { $0.doSomething(undoManager: UndoManager) }
}
}
I am surprised that the compiler does not generate a warning about calling a global actor 'MyActor'-isolated instance method in a synchronous nonisolated context (i.e. the closure). It would appear that the compiler is confused by the closure syntax within an actor isolated method.
Anyway, you can wrap it in a Task and it should run that on the appropriate actor:
#MyActor func doSomething(undoManager: UndoManager) {
// Do something here
undoManager.registerUndo(withTarget: self) { target in
Task { #MyActor in
target.reverseSomething(undoManager: undoManager)
}
}
}
That having been said, I have found erratic UndoManager behavior when using it from a background thread (i.e., not on the main actor).
So, especially because undo/redo is behavior generally initiated from the UI (on the main thread), I would keep it on the main thread, and only run the desired work on another actor. E.g.:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
#State var input: String = ""
var body: some View {
VStack {
TextField(text: $input) {
Text("enter value")
}
Button("Add record") {
viewModel.addAndPrepareUndo(for: input)
input = ""
}.disabled(input.isEmpty)
Button("Undo") {
viewModel.undo()
}.disabled(!viewModel.canUndo)
Button("Redo") {
viewModel.redo()
}.disabled(!viewModel.canRedo)
}
.padding()
}
}
#globalActor actor MyGlobalActor {
static let shared = MyGlobalActor()
}
#MainActor
class ViewModel: ObservableObject {
#MyGlobalActor
var values: [String] = []
#Published var canUndo = false
#Published var canRedo = false
private var undoManager = UndoManager()
func undo() {
undoManager.undo()
updateUndoStatus()
}
func redo() {
undoManager.redo()
updateUndoStatus()
}
func updateUndoStatus() {
canUndo = undoManager.canUndo
canRedo = undoManager.canRedo
}
func addAndPrepareUndo(for newValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.removeAndPrepareRedo(for: newValue)
}
updateUndoStatus()
Task { #MyGlobalActor in
values.append(newValue)
print(#function, values)
}
}
func removeAndPrepareRedo(for revertValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.addAndPrepareUndo(for: revertValue)
}
updateUndoStatus()
Task { #MyGlobalActor in
values.removeLast()
print(#function, values)
}
}
}
Now, this is a somewhat contrived example (for something this simple, we wouldn't have a simply array on a global actor), but hopefully it illustrates the idea.
Or, you can use a non-global actor:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
#State var input: String = ""
var body: some View {
VStack {
TextField(text: $input) {
Text("enter value")
}
Button("Add record") {
viewModel.addAndPrepareUndo(for: input)
input = ""
}.disabled(input.isEmpty)
Button("Undo") {
viewModel.undo()
}.disabled(!viewModel.canUndo)
Button("Redo") {
viewModel.redo()
}.disabled(!viewModel.canRedo)
}
.padding()
}
}
#MainActor
class ViewModel: ObservableObject {
var model = Model()
#Published var canUndo = false
#Published var canRedo = false
private var undoManager = UndoManager()
func undo() {
undoManager.undo()
updateUndoStatus()
}
func redo() {
undoManager.redo()
updateUndoStatus()
}
func updateUndoStatus() {
canUndo = undoManager.canUndo
canRedo = undoManager.canRedo
}
func addAndPrepareUndo(for newValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.removeAndPrepareRedo(for: newValue)
}
updateUndoStatus()
Task {
await model.append(newValue)
await print(#function, model.values())
}
}
func removeAndPrepareRedo(for revertValue: String) {
undoManager.registerUndo(withTarget: self) { [weak self] target in
guard let self else { return }
self.addAndPrepareUndo(for: revertValue)
}
updateUndoStatus()
Task {
await model.removeLast()
await print(#function, model.values())
}
}
}
actor Model {
private var strings: [String] = []
func append(_ string: String) {
strings.append(string)
}
func removeLast() {
strings.removeLast()
}
func values() -> [String] {
strings
}
}

SwiftUI Class won't DeInit

I've got a class class MapSearch that I instantiate when I need to auto-complete address results. It works perfectly but it never deinitializes and I can't figure out why.
Easily test by creating the files below. Use the back button after navigating to the test page and watch the console messages. You will see that the view model initializes and deinitializes as it should, but you'll only see MapSearch initialize.
HomeView.swift
import SwiftUI
struct HomeView: View {
var body: some View {
NavigationView {
NavigationLink(destination: TestView(viewModel: TestViewModel()) {
Text("TestView")
}
}
}
}
TestView.swift
import SwiftUI
struct TestView: View {
#StateObject var viewModel: ViewModel
var body: some View {
Text("Hello World")
}
}
TestViewModel.swift
import Foundation
extension TestView {
#MainActor
class ViewModel: ObservableObject {
#Published var mapSearch: MapSearch()
init() {
print("Test View Model Initialized")
}
deinit {
print("Test View Model Deinitialized")
}
}
}
MapSearch.swift
import Combine
import CoreLocation
import Foundation
import MapKit
/// Uses MapKit and CoreLocation to auto-complete an address
class MapSearch: NSObject, ObservableObject {
#Published var countryName: String = "United States"
#Published var locationResults: [MKLocalSearchCompletion] = []
#Published var searchTerm = ""
private var cancellables: Set<AnyCancellable> = []
private var searchCompleter = MKLocalSearchCompleter()
private var currentPromise: ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
$searchTerm
.debounce(for: .seconds(0.2), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap({ (currentSearchTerm) in
self.searchTermToResults(searchTerm: currentSearchTerm, countryName: self.countryName)
})
.sink(receiveCompletion: { (_) in
}, receiveValue: { (results) in
// Show country specific results
self.locationResults = results.filter { $0.subtitle.contains(self.countryName) }
})
.store(in: &cancellables)
print("MapSearch Initialized")
}
deinit {
print("MapSearch Deinitialized")
}
func searchTermToResults(searchTerm: String, countryName: String) -> Future<[MKLocalSearchCompletion], Error> {
Future { promise in
self.searchCompleter.queryFragment = searchTerm
self.currentPromise = promise
}
}
}
extension MapSearch: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
currentPromise?(.success(completer.results))
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
// deal with the error here, it will finish the Combine publisher stream
currentPromise?(.failure(error))
}
}
The MapSearch class needed to be adjusted to add [weak self] in the combine calls. Now it deinits properly.
Here's the code for reference:
import Combine
import CoreLocation
import Foundation
import MapKit
/// Uses MapKit and CoreLocation to auto-complete an address
class MapSearch: NSObject, ObservableObject {
#Published var countryName: String = "United States"
#Published var locationResults: [MKLocalSearchCompletion] = []
#Published var searchTerm = ""
private var cancellables: Set<AnyCancellable> = []
private var searchCompleter = MKLocalSearchCompleter()
private var currentPromise: ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
$searchTerm
.debounce(for: .seconds(0.2), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap({ [weak self] (currentSearchTerm) in
(self?.searchTermToResults(searchTerm: currentSearchTerm, countryName: self?.countryName ?? "")) ??
Future { [weak self] promise in
self?.searchCompleter.queryFragment = self?.searchTerm ?? ""
self?.currentPromise = promise
}
})
.sink(receiveCompletion: { (_) in
}, receiveValue: { [weak self] (results) in
// Show country specific results
self?.locationResults = results.filter { $0.subtitle.contains(self?.countryName ?? "") }
})
.store(in: &cancellables)
print("MapSearch Initialized")
}
deinit {
print("MapSearch Deinitialized")
}
func searchTermToResults(searchTerm: String, countryName: String) -> Future<[MKLocalSearchCompletion], Error> {
Future { [weak self] promise in
self?.searchCompleter.queryFragment = searchTerm
self?.currentPromise = promise
}
}
}
extension MapSearch: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
currentPromise?(.success(completer.results))
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
// deal with the error here, it will finish the Combine publisher stream
currentPromise?(.failure(error))
}
}

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()
}

How would I write a test to make sure the UIbutton "Show all Providers" turns up when there's more than 12 or more items in the table view?

So I'm completely new to testing and I just needed some help figuring out for example how I would write a test for each of the three cases in the enum of the View Model (none, dontSeeProvider, showAllProviders).
enum ProvidersButtonType {
case none, dontSeeProvider, showAllProviders
}
I haven't been able to figure out how to write a test for cases "showAllProviders" and "dontSeeProviders".
This is the View Model:
import RxSwift
import RxCocoa
struct TopProvidersPickerItem {
let provider: MVPD
let logoImage: Observable<UIImage>
init(provider: MVPD, imageLoader: DecodableProviding) {
self.init(provider: provider, logoImage: imageLoader.image(fromURL: provider.logoUrl))
}
init(provider: MVPD, logoImage: Observable<UIImage>) {
self.provider = provider
self.logoImage = logoImage.catchErrorJustReturn(UIImage())
}
}
enum ProvidersButtonType {
case none, dontSeeProvider, showAllProviders
}
struct TopProvidersPickerViewModel {
var caption: String {
return "Get access to more full episodes by signing in with your TV Provider"
}
let buttonType = Variable<ProvidersButtonType>(.none)
let items: Observable<[TopProvidersPickerItem]>
let selectedItem: PublishSubject<TopProvidersPickerItem> = PublishSubject()
let showAllProvidersTrigger: PublishSubject<Void> = PublishSubject()
let mvpdPicked: Observable<MVPD>
init(topProviders: Observable<[MVPD]>, imageLoader: DecodableProviding) {
let items = topProviders.map({ mvpds in
return mvpds.map { mvpd in
TopProvidersPickerItem(provider: mvpd, imageLoader: imageLoader)
}
})
self.init(items: items)
}
init(items: Observable<[TopProvidersPickerItem]>) {
self.items = items
mvpdPicked = selectedItem.map { $0.provider }
let buttonType = items.map { (array) -> ProvidersButtonType in
if array.count > 12 {
return .showAllProviders
} else {
return .dontSeeProvider
}
}
buttonType.bind(to: self.buttonType)
}
}
This is the View Controller:
import UIKit
import RxCocoa
import RxSwift
public class ProviderCollectionViewCell: UICollectionViewCell {
#IBOutlet public private(set) weak var imageView: UIImageView!
}
public class TopProvidersPickerViewController: UIViewController,
ViewModelHolder {
var viewModel: TopProvidersPickerViewModel! = nil
private let bag = DisposeBag()
#IBOutlet public private(set) weak var collectionView: UICollectionView!
#IBOutlet public private(set) weak var captionLabel: UILabel!
#IBOutlet weak var viewAllProvidersButton: UIButton!
override public func viewDidLoad() {
super.viewDidLoad()
captionLabel.text = viewModel.caption
setupRx()
}
private func setupRx() {
viewModel.buttonType.asObservable().subscribe(onNext: { [button = self.viewAllProvidersButton] type in
button?.isHidden = false
switch type {
case .none:
button?.isHidden = true
case .dontSeeProvider:
button?.setTitle("Don't see provider", for: .normal)
case .showAllProviders:
button?.setTitle("Show all providers", for: .normal)
}
})
.disposed(by: bag)
viewModel.items
.bind(to: collectionView
.rx
.items(cellIdentifier: "ProviderCell", cellType: ProviderCollectionViewCell.self)) { [ unowned self ] _, item, cell in
item.logoImage.bind(to: cell.imageView.rx.image).addDisposableTo(self.bag)
}
.addDisposableTo(bag)
collectionView
.rx
.modelSelected(TopProvidersPickerItem.self)
.bind(to: self.viewModel.selectedItem)
.addDisposableTo(bag)
viewAllProvidersButton
.rx
.tap
.bind(to: self.viewModel.showAllProvidersTrigger)
.addDisposableTo(bag)
}
}
I wrote a test for the "none" case, but haven't been able to figure out the other two cases:
import FBSnapshotTestCase
import OHHTTPStubs
import RxSwift
#testable import AuthSuite
class TopProvidersPickerViewControllerTests: FBSnapshotTestCase,
ProvidersViewControllerTests {
override func setUp() {
super.setUp()
recordMode = true
}
func testDoesNotShowButtonWhenLoadingProviders() {
let viewModel = TopProvidersPickerViewModel(items: .never())
let controller = TopProvidersPickerViewController.instantiateViewController(with: viewModel)
presentViewController(controller)
FBSnapshotVerifyView(controller.view)
}
I've never used FB Snapshot Tester. I'm going to have to look into that.
Here's how I would do it:
I wouldn't expose the enum to the ViewController. setupRx() would contain this instead:
private func setupRx() {
viewModel.buttonTitle
.bind(to: viewAllProvidersButton.rx.title(for: .normal))
.disposed(by: bag)
viewModel.buttonHidden
.bind(to: viewAllProvidersButton.rx.isHidden)
.disposed(by: bag)
// everything else
}
Then to test the title of the button, for example, I would use these tests:
import XCTest
import RxSwift
#testable import RxPlayground
class TopProvidersPickerViewModelTests: XCTestCase {
func testButtonTitleEmptyItems() {
let topProviders = Observable<[MVPD]>.just([])
let decodableProviding = MockDecodableProviding()
let viewModel = TopProvidersPickerViewModel(topProviders: topProviders, imageLoader: decodableProviding)
var title: String = ""
_ = viewModel.buttonTitle.subscribe(onNext: { title = $0 })
XCTAssertEqual(title, "Don't see provider")
}
func testButtonTitle12Items() {
let topProviders = Observable<[MVPD]>.just(Array(repeating: MVPD(), count: 12))
let decodableProviding = MockDecodableProviding()
let viewModel = TopProvidersPickerViewModel(topProviders: topProviders, imageLoader: decodableProviding)
var title: String = ""
_ = viewModel.buttonTitle.subscribe(onNext: { title = $0 })
XCTAssertEqual(title, "Don't see provider")
}
func testButtonTitle13Items() {
let topProviders = Observable<[MVPD]>.just(Array(repeating: MVPD(), count: 13))
let decodableProviding = MockDecodableProviding()
let viewModel = TopProvidersPickerViewModel(topProviders: topProviders, imageLoader: decodableProviding)
var title: String = ""
_ = viewModel.buttonTitle.subscribe(onNext: { title = $0 })
XCTAssertEqual(title, "Show all providers")
}
}
class MockDecodableProviding: DecodableProviding {
// nothing needed for these tests.
}

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