How to test a combineLatest observable with RxTest? - ios

So I have this viewModel which has a validation observable that is simply the combination of 5 other signals into a boolean.
import RxSwift
class SchedulingFormViewModel: BaseViewModel {
let places = Variable<[String]>([])
var formIsValid: Observable<Bool>!
override init() {
super.init()
places.value = ["LUGAR 1", "LUGAR 2", "LUGAR 3"]
formIsValid = Observable.combineLatest(UserSession.currenctScheduling.dateSignal.asObservable(),
UserSession.currenctScheduling.carSignal.asObservable(),
UserSession.currenctScheduling.locationSignal.asObservable(),
UserSession.currenctScheduling.servicesSignal.asObservable())
{ (date: Date?, car: Car?, location: String?, services: [Service]?) in
print("DATE: \(date), CAR: \(car), LOCATION: \(location), SERVICES:\(services)")
guard let servicesArray = services else { return false }
let valid = date != nil && car != nil && location != nil && servicesArray.count > 0
return valid
}
}
}
And then I have my test class with a method that should just test whether the signal changes or not. I've tried a variety of approaches but none of them really gets the true value at the end. The observable only emits a true signal after the test has run.
My test class is setup like this
import XCTest
import RxSwift
import RxTest
#testable import Automobi
class SchedulingUnitTests: XCTestCase {
var viewModel: SchedulingFormViewModel!
var disposeBag: DisposeBag!
var scheduler: TestScheduler!
override func setUp() {
super.setUp()
viewModel = SchedulingFormViewModel()
UserSession.clearScheduling()
disposeBag = DisposeBag()
scheduler = TestScheduler(initialClock: 0)
}
}
I've tried the TestScheduler
func testFormValidation() {
UserSession.currenctScheduling.dateSignal.value = Date()
UserSession.currenctScheduling.carSignal.value = Car()
UserSession.currenctScheduling.servicesSignal.value = [Service(), Service()]
let location = scheduler.createHotObservable([
next(100, "TEST")
])
location.subscribe(onNext: { (text) in
print("LOCATION: \(text)")
UserSession.currenctScheduling.locationSignal.value = text
}).addDisposableTo(disposeBag)
let results = scheduler.createObserver(Bool.self)
scheduler.scheduleAt(0) {
self.viewModel.formIsValid.subscribe(results).addDisposableTo(self.disposeBag)
}
let expected = [
next(100, true)
]
scheduler.start()
XCTAssertEqual(results.events, expected)
}
Also
func testFormValidation() {
UserSession.currenctScheduling.dateSignal.value = Date()
UserSession.currenctScheduling.carSignal.value = Car()
UserSession.currenctScheduling.servicesSignal.value = [Service(), Service()]
let location = scheduler.createHotObservable([
next(100, "TEST"),
next(150, "TEST 2"),
next(200, "TEST 3")
])
location.subscribe(onNext: { (text) in
print("LOCATION: \(text)")
UserSession.currenctScheduling.locationSignal.value = text
}).addDisposableTo(disposeBag)
var results = [Bool]()
viewModel.formIsValid.subscribe(onNext: { (value) in
results.append(value)
}).addDisposableTo(disposeBag)
let expected = [true, true, true]
scheduler.start()
XCTAssertEqual(results, expected)
}
I've tried things like binding my formIsValid to a Variable and verifying its value at the end.
func testFormValidation() {
UserSession.currenctScheduling.dateSignal.value = Date()
UserSession.currenctScheduling.carSignal.value = Car()
UserSession.currenctScheduling.locationSignal.value = "TESTE"
UserSession.currenctScheduling.servicesSignal.value = [Service(), Service()]
sleep(5)
XCTAssertTrue(viewModel.formIsValid.value)
}
But I never get the expected result. I do get a true signal after all the tests fail and the code goes back to executing. Also when running the app the code executes as expecte I just need to catch it in the test. Any ideas?!

On your first try, you are probably getting a value on clock 0, so your expected value should be:
let expected = [
next(0, false)
next(100, true)
]

Related

Handling circular style events on observable sequence RxSwift

I would like to know the best possible way to handle the following situation, I have tried an approach as it will be described but I have encountered an issue of events calling each other repeatedly in a circular way hence it causes stackoverflow 😂
I have 4 observables as follows: -
let agreeToPrivacyPolicyObservable = BehaviorRelay<Bool>(value: false)
let agreeToTermsObservable = BehaviorRelay<Bool>(value: false)
let agreeToMarketingEmailObservable = BehaviorRelay<Bool>(value: false)
let agreeToAllOptionsObservable = BehaviorRelay<Bool>(value: false)
Goal:
Sync agree to all button with individual options. ie if agree to all is true/checked then force other options to be checked as well and vice-versa. Additionally if the previous state of all items were checked and either of them emit unchecked then remove a checkmark on Agree to all button.
The image below visualizes my goal above.
What I have tried:
Observable.combineLatest(
agreeToPrivacyPolicyObservable,
agreeToTermsObservable,
agreeToMarketingEmailObservable,
agreeToAllOptionsObservable
, resultSelector:{(termsChecked,privacyChecked,marketingChecked,agreeToAllChecked) in
switch (termsChecked,privacyChecked,marketingChecked,agreeToAllChecked) {
case (true, true, true,true):
//All boxes are checked nothing to change.
break
case (false,false,false,false):
//All boxes are unchecked nothing to change.
break
case (true,true,true,false):
// I omitted the `triggeredByAgreeToAll` flag implementation details for clarity
if triggeredByAgreeToAll {
updateIndividualObservables(checked: false)
}else {
agreeToAllOptionsObservable.accept(true)
}
case (false,false,false,true):
if triggeredByAgreeToAll {
updateIndividualObservables(checked: true)
}else {
agreeToAllOptionsObservable.accept(false)
}
default:
if triggeredByAgreeToAll && agreeToAllChecked {
updateIndividualObservables(checked: true)
}else if triggeredByAgreeToAll && agreeToAllChecked == false {
updateIndividualObservables(checked: false)
} else if (termsChecked == false || privacyChecked == false || marketingChecked == false ) {
agreeToAllOptionsObservable.accept(false)
}
}
}
})
.observeOn(MainScheduler.instance)
.subscribe()
.disposed(by: rx.disposeBag)
// Helper function
func updateIndividualObservables(checked: Bool) {
agreeToPrivacyPolicyObservable.accept(checked)
agreeToTermsObservable.accept(checked)
agreeToMarketingEmailObservable.accept(checked)
}
Explanation:
My attempt gives me Reentracy anomaly was detected error , which according to my observations is caused by events being triggered repeatedly. This seems to occurs in the default switch case (on my solution above). I think this solution is not good as I have to check which event triggered the function execution.
Is there any better approach or is it possible to refactor this solution into something easily manageable? Btw Feel free to ignore my implementation and suggest a different better approach if any. Thanks!
UPDATES (WORKING SOLUTION)
I successfully implemented a working solution by using #Rugmangathan idea (Found on the accepted answer). So I leave my solution here to help anyone in the future facing the same issue.
Here is the working solution: -
import Foundation
import RxSwift
import RxRelay
/// This does all the magic of selecting checkboxes.
/// It is shared across any view which uses the license Agreement component.
class LicenseAgreemmentState {
static let shared = LicenseAgreemmentState()
let terms = BehaviorRelay<Bool>(value: false)
let privacy = BehaviorRelay<Bool>(value: false)
let marketing = BehaviorRelay<Bool>(value: false)
let acceptAll = BehaviorRelay<Bool>(value: false)
private let disposeBag = DisposeBag()
func update(termsChecked: Bool? = nil, privacyChecked: Bool? = nil, marketingChecked: Bool? = nil, acceptAllChecked: Bool? = nil) {
if let acceptAllChecked = acceptAllChecked {
// User toggled acceptAll button so change everything to it's value.
acceptAll.accept(acceptAllChecked)
updateIndividualObservables(termsChecked: acceptAllChecked, privacyChecked: acceptAllChecked, marketingChecked: acceptAllChecked)
} else {
// If either of the individual item is missing change acceptAll to false
if termsChecked == nil || privacyChecked == nil || marketingChecked == nil {
acceptAll.accept(false)
}
updateIndividualObservables(termsChecked: termsChecked, privacyChecked: privacyChecked, marketingChecked: marketingChecked)
}
// Deal with the case user triggered select All from individual items and vice-versa.
Observable.combineLatest(terms, privacy, marketing,resultSelector: {(termsChecked,privacyChecked, marketingChecked) in
switch (termsChecked,privacyChecked, marketingChecked) {
case (true, true, true):
self.acceptAll.accept(true)
case (false,false,false):
self.acceptAll.accept(false)
default:
break
}
})
.observeOn(MainScheduler.instance)
.subscribe()
.disposed(by: disposeBag)
}
// MARK: - Helpers
private func updateIndividualObservables(termsChecked: Bool?,privacyChecked: Bool?, marketingChecked:Bool?) {
if let termsChecked = termsChecked {
terms.accept(termsChecked)
}
if let privacyChecked = privacyChecked {
privacy.accept(privacyChecked)
}
if let marketingChecked = marketingChecked {
marketing.accept(marketingChecked)
}
}
}
Your helper function updateIndividualObservables(:) triggers an event every time you update which in turn triggers the combineLatest you implemented above.
I would suggest you to keep a State object instead
struct TermsAndConditionState {
var terms: Bool
var privacy: Bool
var marketing: Bool
var acceptAll: Bool
}
In updateIndividualObservables method change this state and implement this state change with your respective checkboxes
func render(state: TermsAndConditionState) {
if state.acceptAll {
// TODO: update all checkboxes
} else {
// TODO: update individual checkboxes
}
}
This is a simple state machine. State machines are implemented in Rx using the scan(_:accumulator:) or scan(into:accumulator:) operator like so:
struct Input {
let agreeToPrivacyPolicy: Observable<Void>
let agreeToTerms: Observable<Void>
let agreeToMarketingEmail: Observable<Void>
let agreeToAllOptions: Observable<Void>
}
struct Output {
let agreeToPrivacyPolicy: Observable<Bool>
let agreeToTerms: Observable<Bool>
let agreeToMarketingEmail: Observable<Bool>
let agreeToAllOptions: Observable<Bool>
}
func viewModel(input: Input) -> Output {
enum Action {
case togglePrivacyPolicy
case toggleTerms
case toggleMarketingEmail
case toggleAllOptions
}
let action = Observable.merge(
input.agreeToPrivacyPolicy.map { Action.togglePrivacyPolicy },
input.agreeToTerms.map { Action.toggleTerms },
input.agreeToMarketingEmail.map { Action.toggleMarketingEmail },
input.agreeToAllOptions.map { Action.toggleAllOptions }
)
let state = action.scan(into: State()) { (current, action) in
switch action {
case .togglePrivacyPolicy:
current.privacyPolicy = !current.privacyPolicy
case .toggleTerms:
current.terms = !current.terms
case .toggleMarketingEmail:
current.marketingEmail = !current.marketingEmail
case .toggleAllOptions:
if !current.allOptions {
current.privacyPolicy = true
current.terms = true
current.marketingEmail = true
}
}
current.allOptions = current.privacyPolicy && current.terms && current.marketingEmail
}
return Output(
agreeToPrivacyPolicy: state.map { $0.privacyPolicy },
agreeToTerms: state.map { $0.terms },
agreeToMarketingEmail: state.map { $0.marketingEmail },
agreeToAllOptions: state.map { $0.allOptions }
)
}
struct State {
var privacyPolicy: Bool = false
var terms: Bool = false
var marketingEmail: Bool = false
var allOptions: Bool = false
}

How to call a method once two variables have been set

I am using iOS Swift, and I am trying to understand how to execute a method once the value of two variables have been set up (non-null value) once the requests have finished.
After reading some documentation, I have found out some concepts which are interesting. The first one would be didSet, which works as an observer.
I could call the method using this method by simply using didSet if I would require just one variable
didSet
var myVar: String 0 {
didSet {
print("Hello World.")
}
}
Nevertheless, I also need to wait for the second one myVar2, so it would not work.
I have also found DispatchQueue, which I could use to wait a second before calling the method (the requests that I am using are pretty fast)
DispatchQueue
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
print("Hello world")
})
but I consider that this solution is not efficient.
Is there anyway to combine these two variables or requests in order to call a method once they have finishing setting the value?
Update
I have tried to replicate David s answer, which I believe is correct but I get the following error on each \.
Type of expression is ambiguous without more context
I copy here my current code
var propertiesSet: [KeyPath<SearchViewController, Car>:Bool] = [\SearchViewController.firstCar:false, \SearchViewController.secondCar:false] {
didSet {
if propertiesSet.allSatisfy({ $0.value }) {
// Conditions passed, execute your custom logic
print("All Set")
} else {
print("Not yet")
}
}
}
var firstCar: Car? {
didSet {
propertiesSet[\SearchViewController.firstCar] = true
}
}
var secondCar: Car? {
didSet {
propertiesSet[\SearchViewController.secondCar] = true
}
}
The variables are set individually, each one on its own request.
You could make your properties optional and check they both have values set before calling your function.
var varA: String? = nil {
didSet {
if varA != nil && varB != nil {
myFunc()
}
}
}
var varB: String? = nil {
didSet {
if varA != nil && varB != nil {
myFunc()
}
}
}
Or you can call your function on each didSet and use a guard condition at the start of your function to check that both of your properties have values, or bail out:
var varA: String? = nil {
didSet {
myFunc()
}
}
var varB: String? = nil {
didSet {
myFunc()
}
}
func myFunc() {
guard varA != nil && varB != nil else { return }
// your code
}
First, you should think very carefully about what your semantics are here. When you say "set," do you mean "assigned a value" or do you mean "assigned a non-nil value?" (I assume you mean the latter in this case.) You should ask yourself, what should happen if your method has already fired, and then another value is set? What if one of the properties has a value is set, then nil is set, then another value set? Should that fire the method 1, 2, or 3 times?
Whenever possible you should work to make these kinds of issues impossible by requiring that the values be set together, in an init rather than mutable properties, for example.
But obviously there are cases where this is necessary (UI is the most common).
If you're targeting iOS 13+, you should explore Combine for these kinds of problems. As one approach:
class Model: ObservableObject {
#Published var first: String?
#Published var second: String?
#Published var ready = false
private var observers: Set<AnyCancellable> = []
init() {
$first.combineLatest($second)
.map { $0 != nil && $1 != nil }
.assign(to: \.ready, on: self)
.store(in: &observers)
}
}
let model = Model()
var observers: Set<AnyCancellable> = []
model.$ready
.sink { if $0 { print("GO!") } }
.store(in: &observers)
model.first = "ready"
model.second = "set"
// prints "GO!"
Another approach is to separate the incidental state that includes optionals, from the actual object you're constructing, which does not.
// Possible parameters for Thing
struct Parameters {
var first: String?
var second: String?
}
// The thing you're actually constructing that requires all the parameters
struct Thing {
let first: String
let second: String
init?(parameters: Parameters) {
guard let first = parameters.first,
let second = parameters.second
else { return nil }
self.first = first
self.second = second
}
}
class TheUIElement {
// Any time the parameters change, try to make a Thing
var parameters: Parameters = Parameters() {
didSet {
thing = Thing(parameters: parameters)
}
}
// If you can make a Thing, then Go!
var thing: Thing? {
didSet {
if thing != nil { print("GO!") }
}
}
}
let element = TheUIElement()
element.parameters.first = "x"
element.parameters.second = "y"
// Prints "GO!"
You need to add a didSet to all variables that need to be set for your condition to pass. Also create a Dictionary containing KeyPaths to your variables that need to be set and a Bool representing whether they have been set already.
Then you can create a didSet on your Dictionary containing the "set-state" of your required variables and when all of their values are true meaning that all of them have been set, execute your code.
This solution scales well to any number of properties due to the use of a Dictionary rather than manually writing conditions like if aSet && bSet && cSet, which can get out of hand very easily.
class AllSet {
var propertiesSet: [KeyPath<AllSet, String>:Bool] = [\.myVar:false, \.myVar2:false] {
didSet {
if propertiesSet.allSatisfy({ $0.value }) {
// Conditions passed, execute your custom logic
print("All Set")
} else {
print("Not yet")
}
}
}
var myVar: String {
didSet {
propertiesSet[\.myVar] = true
}
}
var myVar2: String {
didSet {
propertiesSet[\.myVar2] = true
}
}
init(myVar: String, myVar2: String) {
self.myVar = myVar
self.myVar2 = myVar2
}
}
let all = AllSet(myVar: "1", myVar2: "2")
all.myVar = "2" // prints "Not yet"
all.myVar2 = "1" // prints "All set"

MPMusicPlayerController.shuffleMode cannot be set

Hello I have a question about the MPMusicPlayerController in Swift. I am currently working on a Music App were I want to shuffle music by songs. So when the App Starts it basically sets the Playback Queue and then the Shuffle Mode. I can successfully set the queue (and play the songs) but I get an error when I set the Shuffle Mode:
musicPlayer.musicPlayer.shuffleMode = .songs
ERROR:
2018-07-03 15:01:36.450977+0200 Hitbeat[29053:8378883] [SDKPlayback] -[MPMusicPlayerController setShuffleMode:2] completed error: Error Domain=MPCPlayerRequestErrorDomain Code=1 "No commands provided." UserInfo={NSDebugDescription=No commands provided.}
What does that mean?
I have the idea that it may be because the queue is not set completely when setting the shuffleMode but I am not sure and it would not make any sense that one would have to set a song queue first in order to set the mode in which order songs to play. Maybe something else is the problem?
Also everything takes place on the Main Thread. (MPMusicPlayerController always has to be called in the Main Thread)
Thanks a lot I hope you guys can help me.
here are some code snippets:
MusicPlayerManager.swift
import os.log
import MediaPlayer
import NotificationCenter
class MusicPlayerManager {
let musicPlayer: MPMusicPlayerController
lazy var musicPickerAndAdder = MusicPickerAndAdder()
init() {
// Instantiate a new music player
musicPlayer = MPMusicPlayerApplicationController.applicationQueuePlayer
// Add a playback queue containing all songs on the device
switch MPMediaLibrary.authorizationStatus() {
case .authorized:
let catalogSongStoreID: String = ""
let catalogQueueDescriptor = MPMusicPlayerStoreQueueDescriptor(storeIDs: [catalogSongStoreID])
musicPlayer.setQueue(with: catalogQueueDescriptor)
default:
break
}
Timer.scheduledTimer(withTimeInterval: 15, repeats: false) {_ in
print("shuffle mode setter")
self.musicPlayer.shuffleMode = MPMusicShuffleMode.songs
}
}
func updateOnlineMusicQueue() {
var musicPickerIds = [String]()
DispatchQueue.global(qos: .userInitiated).sync {
musicPickerIds = musicPickerAndAdder.ids
}
if !musicPickerIds.isEmpty{
musicPlayer.setQueue(with: musicPickerIds)
}else {
updateOfflineMusicQueue()
}
musicPlayer.pause()
}
func play() {
if musicPlayer.playbackState == .playing {
musicPlayer.pause()
musicPlayer.skipToBeginning()
}
if !musicPlayer.isPreparedToPlay {
musicPlayer.prepareToPlay { (error) in
if error == nil {
self.musicPlayer.play()
self.startSongMasterTimer()
}
}
}else {
musicPlayer.play()
startSongMasterTimer()
}
}
func pauseAndSkip() {
// if self.musicPlayer.shuffleMode.rawValue != 2 { // does not work here would work on pause and skip
// self.musicPlayer.shuffleMode = MPMusicShuffleMode.songs
// }
//print("shuffler \(self.musicPlayer.shuffleMode.rawValue)")
//print("At \(musicPlayer.currentPlaybackTime) of \((musicPlayer.nowPlayingItem?.playbackDuration!)")
musicPlayer.pause()
//if musicPlayer.nowPlayingItem != nil {
musicPlayer.skipToNextItem()
//}
musicPlayer.prepareToPlay { (error) in
if error == nil {
self.musicPlayer.pause()
}
}
}
func currentSongInfo() -> SongInfo {
let songTitle = musicPlayer.nowPlayingItem?.title?.replacingOccurrences(of: "-", with: " ") ?? "" // To guarantee there is only one - between Song and Artist
let artistName = musicPlayer.nowPlayingItem?.artist?.replacingOccurrences(of: "-", with: " ") ?? ""
let songInfo = SongInfo(title: songTitle, artist: artistName)
return songInfo
}
func addSongToLibrary() {
//print("Id of Item to Add: \(musicPlayer.nowPlayingItem?.playbackStoreID)")
if musicPlayer.nowPlayingItem != nil {
musicPickerAndAdder.addResourceToUserMusicLibrary(resourceId: (musicPlayer.nowPlayingItem?.playbackStoreID)!)
}
//ToDo add to myHitbeat Playlist
}
}
class SongInfo {
let title: String
let artist: String
init(title:String,artist:String) {
self.title = title
self.artist = artist
}
}
MusicPickerAndAdder.swift
import Foundation
class MusicPickerAndAdder {
lazy var authorizationManager: AuthorizationManager = {
return AuthorizationManager(appleMusicManager: self.appleMusicManager)
}()
var appleMusicManager = AppleMusicManager()
private var idsArraySize = 100
static var idCategoriesStakes = ["Chart_Ids" : 0.10,
"Recently_Played_Ids" : 0.10,
"Experiment_Ids" : 0.30,
"Recommendations_Ids" : 0.50,] // Addition of all Values must be 1 (100%)
private var chartIds: [String] {
var chartsIds = [String]()
let chartsIdsGroup = DispatchGroup()
chartsIdsGroup.enter()
let limit = Int(Double(idsArraySize) * MusicPickerAndAdder.idCategoriesStakes["Recently_Played_Ids"]!)
appleMusicManager.performAppleMusicGetChartSongs(regionCode: Locale.current.regionCode?.lowercased() ?? "us", limit: limit) { (storeIds, error) in
if error != nil {
print("There was an Error getting Charts")
chartsIdsGroup.leave()
return
}
chartsIds = storeIds
chartsIdsGroup.leave()
}
chartsIdsGroup.wait()
print("Charts sucessfully fetched")
return chartsIds
}
private var recentlyPlayedIds: [String] {
var recentIds = [String]()
let recentIdsGroup = DispatchGroup()
recentIdsGroup.enter()
let limit = Int(Double(idsArraySize) * MusicPickerAndAdder.idCategoriesStakes["Recently_Played_Ids"]!)
appleMusicManager.performAppleMusicGetRecentlyPlayed(userToken: authorizationManager.userToken, limit: limit) {
(storeIds, error) in
if error != nil {
print("There was an Error getting Recently Played")
recentIdsGroup.leave()
return
}
recentIds = storeIds
recentIdsGroup.leave()
}
recentIdsGroup.wait()
print("Recently Played sucessfully fetched: \(recentIds)")
return recentIds
}
private var experimentIds: [String] {
return ["pl.u-XkD04oZIY0Kxrl"]
}
private var recommendationsIds: [String] {
return [String]()
}
// Never request Ids in Main (UI) Thread
var ids: [String] {
var ids = [String]()
ids += recentlyPlayedIds
ids += chartIds
ids += experimentIds
ids += recommendationsIds
print("Store Ids for Songs \(ids)")
return ids.shuffled() // shuffles list of items
}
init() {
requestAppleMusicAuthorization()
}
//MARK: Private Methods
private func requestAppleMusicAuthorization() {
UserDefaults.standard.register(defaults: ["tutorial": true])
if !UserDefaults.standard.bool(forKey: "tutorial") {
authorizationManager.requestCloudServiceAuthorization()
authorizationManager.requestMediaLibraryAuthorization()
}
}
}
extension MusicPickerAndAdder { // to Add Songs
func addResourceToUserMusicLibrary(resourceId: String) {
appleMusicManager.performAddResourceToLibrary(resourceId: resourceId, userToken: authorizationManager.userToken)
}
}
extension MutableCollection {
/// Shuffles the contents of this collection.
mutating func shuffle() {
let c = count
guard c > 1 else { return }
for (firstUnshuffled, unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) {
// Change `Int` in the next line to `IndexDistance` in < Swift 4.1
let d: Int = numericCast(arc4random_uniform(numericCast(unshuffledCount)))
let i = index(firstUnshuffled, offsetBy: d)
swapAt(firstUnshuffled, i)
}
}
}
extension Sequence {
/// Returns an array with the contents of this sequence, shuffled.
func shuffled() -> [Element] {
var result = Array(self)
result.shuffle()
return result
}
}
PS: MusicPickerAndAdder may look a little messy though I don't think the problem lies there! What it basically does is fetching some data from the Apple Music API which works fine, and adding Songs to the User Library which works too.
Okay after trying everything out possible I came up with two solutions that work for me. Weirdly I found out that a freeze of the interface only occurs when no song has played so far. If a song is currently playing or even if a song has played and was paused afterwards there is no ui freeze. So I came up with this function:
private func setShuffleMode() { // does work though startup and restarting takes longer
musicPlayer.play()
Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) {_ in
print("shuffle mode setter")
self.musicPlayer.pause()
//self.musicPlayer.pause()// may stop interface freezing if occuring
self.musicPlayer.shuffleMode = MPMusicShuffleMode.songs // freeze of ui only occurs when no song played before
}
}
I tried out several time intervals it still failed sometimes if it was a second it never failed on 1.5 seconds so I left it there
The problem though was that starting the App as well as restarting it was taking a little bit longer. So I came up with a second solution
private func setShuffleMode2 () { // still in test if shuffle mode gets set fast or even ever set
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) {timer in
if self.musicPlayer.playbackState == .playing && self.musicPlayer.currentPlaybackTime > 3{
self.musicPlayer.shuffleMode = .songs
print("shuffle mode setter")
timer.invalidate()
}
}
}
Here I have a repeating timer which is always checking if an item is playing and if it is playing for a certain time already, if it is it sets the shuffle mode and stops repeating. I have tested the second function and it worked great though there is the trade of that there is always the possibility that it is not getting called for some time. How long that will be depends on the time interval and currentPlaybackTime > someTime value.

Unit testing DI with CoreData won't work

I'm trying to write a test to make sure that my view model's model property when set calls my fetchPlan method from the model and then sets my 'plan' property in my view model. It seems to be setting the property but the values are missing...
Here's my view model
final class PlanProgressViewModel: PlanProgressViewModelView {
// MARK: - Properties
fileprivate var plan: PlanData?
// MARK: - PlanProgressViewModelView
weak var viewDelegate: PlanProgressViewModelViewDelegate?
var model: PlanModel? {
didSet {
model?.fetchCurrentPlan(completionHandler: { (plan) in
guard let plan = plan else {return}
self.plan = plan
})
}
}
// Testing this fails...
var planName: String! {
guard let plan = plan else {return "No plan"}
return plan.name
}
var planProgressionString: String! {
return "\(Int(round(self.progress * 100)))%"
}
var progress: Double! {
guard let plan = plan, let workouts = plan.workouts, let completedWorkouts = plan.completedWorkouts else {return 0}
return Double(Int(completedWorkouts) / workouts.count)
}
}
Here's my test suite, i'm using a mock to return hardcoded data from the model.
var sut: PlanProgressViewModel!
var model: MockPlanModel!
var moc: NSManagedObjectContext!
override func setUp() {
super.setUp()
moc = setupInMemoryMOC()
let mockModel = MockPlanModel(moc: moc)
model = mockModel
let viewModel = PlanProgressViewModel()
viewModel.model = model
sut = viewModel
}
override func tearDown() {
moc = nil
model = nil
sut = nil
super.tearDown()
}
// This passes
func testModelFetchesCurrentPlanOnce() {
XCTAssertEqual(model.fetchPlanWasCalled, 1)
}
// This is failing
func testPlanName() {
XCTAssertEqual(sut.planName, "Test plan")
}
Here's my method for setting up a in memory persistent store...
public func setupInMemoryMOC() -> NSManagedObjectContext {
let mom = NSManagedObjectModel.mergedModel(from: [Bundle.main])
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom!)
do {
try psc.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: nil, options: nil)
} catch {
fatalError()
}
let moc = NSManagedObjectContext.init(concurrencyType: .mainQueueConcurrencyType)
moc.persistentStoreCoordinator = psc
return moc }
Here's my mock for the model which returns hardcoded data...
public class MockPlanModel: MWPlanModel {
var fetchPlanWasCalled = 0
override public func fetchCurrentPlan(completionHandler: #escaping (_ plan: PlanData?) -> ()) {
fetchPlanWasCalled += 1
let moc = setupInMemoryMOC()
let plan = createTestPlan(moc: moc)
completionHandler(plan)
}}
Here's my helper method for creating the model object, (PlanData is a protocol that my 'Plan' NSManaged object inherits).
public func createTestPlan(moc: NSManagedObjectContext) -> PlanData {
let plan: Plan = Plan(context: moc)
plan.name = "Test plan"
plan.completedWorkouts = 5
plan.currentPlan = true
for _ in 0..<5 {
plan.mutableOrderedSetValue(forKeyPath: #keyPath(Plan.workouts)).add(createTestCompletedWorkout(moc: moc))
}
return plan }
The plan name should be "Test plan" as that is what i set the hardcoded value to be but it fails and returns an empty string instead...
Really stuck on this, i'm fairly new to testing so i appreciate any help with this. Thanks
#MartinR comment good point out :
The non-optional on the left side "Test plan" gets automatically promoted to an optional. because sut.planName is an optional. more check this Swift comparing Strings optionals vs non-optional
You need to unwrap sut.planName
if let planName = sut.planName{
XCTAssertEqual(planName, "Test plan")
}
Or try this way :
XCTAssertEqual((sut.planName ?? ""), "Test plan")
Note: If you face still same issue then update your Xcode
// Using Apple XCTest (Xcode 7.3.1), this produces the output:
// "XCTAssertEqual failed: ("Optional(1)") is not equal to ("Optional(2)") - "
XCTAssertEqual(1, 2)
More details : Here Bug reported

Unit testing with Realm in iOS

I followed the guide on Realm's website, but my test are still producing varying results, depending on the random order they are done.
Either they succeed when run individually, or fail because the data was already set. For example:
XCTAssert(observer.events[0].value.element == "", "Initail hub name is expected to be equal to be an empty string but was \(String(describing: observer.events[0].value.element))")
XCTAssert(observer.events[1].value.element == testHubName, "Initail hub name is expected to be equal to be the name we set but was \(String(describing: observer.events[1].value.element))")
In this example, event[0] is already set to testHubName.
Or it gets a #throw RLMException(#"Object has been deleted or invalidated.");
So I guess there is some kind race going around, but this is in my setup method:
func setupRealm() {
testRealm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "DORSessionViewModelTestsRealm"))
try! self.testRealm.write {
self.testRealm.deleteAll()
}
}
So as far as I understand I should have a fresh Realm for every test, so where is the race going on?
Edit:
Adding a bigger part of the class
class DORSessionViewModelTests: XCTestCase {
var disposeBag: DisposeBag = DisposeBag()
var scheduler = TestScheduler(initialClock: 0)
var testRealm: Realm!
override func setUp() {
super.setUp()
setupRealm()
setupRx()
}
override func tearDown() {
super.tearDown()
}
func testHubNameLabelTextUpdate() {
let expectation = self.expectation(description: "expect hub name text to update")
let testHubName = "testHubName"
let viewModel = getSessionViewModel()
let observer = scheduler.createObserver(String.self)
scheduler.start()
let hubNameDriver = viewModel.hubNameLabelText()
hubNameDriver.asObservable().subscribe(observer).disposed(by: disposeBag)
hubNameDriver.asObservable().skip(1).subscribeNext { _ in expectation.fulfill()}.disposed(by: disposeBag)
try! self.testRealm.write {
viewModel.dependencies..hub.name = testHubName
}
waitForExpectations(timeout: 5, handler: { error in
XCTAssert(observer.events[0].value.element == "", "Initail hub name is expected to be equal to be an empty string but was \(String(describing: observer.events[0].value.element))")
XCTAssert(observer.events[1].value.element == testHubName, "Initail hub name is expected to be equal to be the name we set but was \(String(describing: observer.events[1].value.element))")
})
}
func setupRx() {
disposeBag = DisposeBag()
scheduler = TestScheduler(initialClock: 0)
}

Resources