I have the following code :
#property (weak, nonatomic) IBOutlet UIView *loadingView;
-(void) hideLoadingScreen{
self.loadingViewRect = self.loadingView.frame;
[self.loadingView setHidden:YES];
[self.loadingView setFrame:CGRectMake(0,0,0,0)];
}
- (void)viewDidAppear:(BOOL)animated{
[self.apiClient checkLoggedInWithCallback:^(int status){
if(status == ONLINE_LOGGED_IN){
[self.dbAPI isPullSynchronized:^(BOOL isPullSynced){
if(isPullSynced){
self.data = [self.dbAPI getTodayVisits];
[[self tableView] reloadData];
[self hideLoadingScreen];
} else {
[self.apiClient getTodayVisits:^(NSArray* visits){
[self.dbAPI insertTodayVisits:visits withCallback:^(int status){
self.data = [self.dbAPI getTodayVisits];
[[self tableView] reloadData];
}];
}];
}
}];
} else if(status == ONLINE_NOT_LOGGED_IN || status == ONLINE_INVALID_TOKEN || status == OFFLINE_NOT_LOGGED_IN) {
//Redirect to login
} else {
//Get from Local DB
self.data = [self.dbAPI getTodayVisits];
[[self tableView] reloadData];
}
}];
}
The hideLoadingScreen method won't execute (I mean it gets executed, but the UI doesn't update).
I tried all things to make it work (including dispatching [self hideLoadingScreen] to the main thread via GCD and performSelectorOnMainThread / making a __block BOOL variable isLoading and sleeping on the main thread till that variable was changed etc.). I also called the hideLoadingView method on the viewDidAppear method and it works, but I want it to hide when the callback is executed. Unfortunately, I couldn't find a solution by searching on stackoverflow, neither on google (I must say I tried all the solutions I found).
L.E. I logged self.loadingView as Rob Napier suggested
New code:
-(void) hideLoadingScreen{
self.loadingViewRect = self.loadingView.frame;
NSLog(#"hideLoadingScreen before: %#",self.loadingView);
[self.loadingView setHidden:YES];
[self.loadingView setFrame:CGRectMake(0,0,0,0)];
NSLog(#"hideLoadingScreen after: %#",self.loadingView);
}
- (void)viewDidAppear:(BOOL)animated{
NSLog(#"%#",self.loadingView);
[self.apiClient checkLoggedInWithCallback:^(int status){
if(status == ONLINE_LOGGED_IN){
[self.dbAPI isPullSynchronized:^(BOOL isPullSynced){
if(isPullSynced){
dispatch_async(dispatch_get_main_queue(), ^{
self.data = [self.dbAPI getTodayVisits];
[[self tableView] reloadData];
NSLog(#"async before: %#",self.loadingView);
[self hideLoadingScreen];
NSLog(#"async after: %#",self.loadingView);
});
} else {
[self.apiClient getTodayVisits:^(NSArray* visits){
[self.dbAPI insertTodayVisits:visits withCallback:^(int status){
self.data = [self.dbAPI getTodayVisits];
[[self tableView] reloadData];
}];
}];
}
}];
} else if(status == ONLINE_NOT_LOGGED_IN || status == ONLINE_INVALID_TOKEN || status == OFFLINE_NOT_LOGGED_IN) {
//Redirect to login
} else {
//Get from Local DB
self.data = [self.dbAPI getTodayVisits];
[[self tableView] reloadData];
}
}];
}
Logs:
2016-01-08 16:22:25.973 sunwaves.reporting[4566:282042] async before: <UIView: 0x7fa681e745d0; frame = (0 0; 1024 650); autoresize = RM+H+BM; layer = <CALayer: 0x7fa681e291c0>>
2016-01-08 16:22:25.973 sunwaves.reporting[4566:282042] hideLoadingScreen before: <UIView: 0x7fa681e745d0; frame = (0 0; 1024 650); autoresize = RM+H+BM; layer = <CALayer: 0x7fa681e291c0>>
2016-01-08 16:22:25.974 sunwaves.reporting[4566:282042] hideLoadingScreen after: <UIView: 0x7fa681e745d0; frame = (0 0; 0 0); hidden = YES; autoresize = RM+H+BM; layer = <CALayer: 0x7fa681e291c0>>
2016-01-08 16:22:25.974 sunwaves.reporting[4566:282042] async after: <UIView: 0x7fa681e745d0; frame = (0 0; 0 0); hidden = YES; autoresize = RM+H+BM; layer = <CALayer: 0x7fa681e291c0>>
The majority of UIKit calls must be made on the main queue. You should use dispatch_async(dispatch_get_main_queue(),... to do this.
This includes your calls to reloadData at a minimum.
You assignments to self.data are also likely not thread-safe (unless you've done something special in the setter). So those need to be on the main queue.
And of course your calls to hideLoadingScreen.
I assume that most of these blocks execute off the main queue, so that means putting in dispatch_async in several places.
- (void)viewDidAppear:(BOOL)animated{
[self.apiClient checkLoggedInWithCallback:^(int status){
if(status == ONLINE_LOGGED_IN){
[self.dbAPI isPullSynchronized:^(BOOL isPullSynced){
if(isPullSynced){
dispatch_async(dispatch_get_main_queue(), ^{
self.data = [self.dbAPI getTodayVisits];
[[self tableView] reloadData];
[self hideLoadingScreen];
});
} else {
[self.apiClient getTodayVisits:^(NSArray* visits){
[self.dbAPI insertTodayVisits:visits withCallback:^(int status){
dispatch_async(dispatch_get_main_queue(), ^{
self.data = [self.dbAPI getTodayVisits];
[[self tableView] reloadData];
});
}];
}];
}
}];
} else if(status == ONLINE_NOT_LOGGED_IN || status == ONLINE_INVALID_TOKEN || status == OFFLINE_NOT_LOGGED_IN) {
//Redirect to login
} else {
//Get from Local DB
dispatch_async(dispatch_get_main_queue(), ^{
self.data = [self.dbAPI getTodayVisits];
[[self tableView] reloadData];
});
}
}];
}
Related
This is my Sign In button :
- (IBAction)signInButtonAction:(id)sender
{
if ([self.phoneNumberTextField.text length] == 0 || [self.passwordTextField.text length] == 0) {
[self initializeAlertControllerForOneButtonWithTitle:#"Alert!" withMessage:kEmptyTextFieldAlertMSG withYesButtonTitle:#"Ok" withYesButtonAction:nil];
} else {
if ([self alreadyRegisteredPhoneNumber] == YES) {
if ([self.password isEqualToString:self.passwordTextField.text]) {
[self goToHomeViewController];
} else {
[self initializeAlertControllerForOneButtonWithTitle:#"Wrong Password!" withMessage:kWrongPasswordMSG withYesButtonTitle:#"Ok" withYesButtonAction:nil];
}
} else {
[self initializeAlertControllerForOneButtonWithTitle:#"Alert!" withMessage:kSignInPhoneNumberDoesnotExistMSG withYesButtonTitle:#"Ok" withYesButtonAction:nil];
}
}
}
And this is my alreadyRegisteredPhoneNumber method, where I am just checking if the given phone number is registered or not.
- (BOOL)alreadyRegisteredPhoneNumber
{
[self.activityIndicator startAnimating];
__block BOOL isThisPhoneNumberRegistered = NO;
BlockWeakSelf weakSelf = self;
SignInViewController *strongSelf = weakSelf;
dispatch_sync(dispatch_queue_create("mySyncQueue", NULL), ^{
NSString *PhoneNumberWithIsoCountryCode = [NSString stringWithFormat:#"%#%#", strongSelf.countryCode, strongSelf.phoneNumberTextField.text];
strongSelf.userModelClass = [[RetrieveDataClass class] retrieveUserInfoForPhoneNumber:PhoneNumberWithIsoCountryCode];
if ([strongSelf.userModelClass.phone_number length] != 0) {
strongSelf.phoneNumber = strongSelf.userModelClass.phone_number;
strongSelf.password = strongSelf.userModelClass.password;
isThisPhoneNumberRegistered = YES;
[strongSelf.activityIndicator stopAnimating];
}
});
return isThisPhoneNumberRegistered;
}
With this method the problem is that the activityIndicator is not appearing when I press Sign In button. Otherwise it works synchronize way, which is perfect.
The reason activityIndicator is not appearing is that I have to update UI related change in dispatch_async(dispatch_get_main_queue() (Or not??). But If I rearrange my code like this :
- (BOOL)alreadyRegisteredPhoneNumber
{
[self.activityIndicator startAnimating];
__block BOOL isThisPhoneNumberRegistered = NO;
BlockWeakSelf weakSelf = self;
SignInViewController *strongSelf = weakSelf;
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_sync(dispatch_queue_create("mySyncQueue", NULL), ^{
NSString *PhoneNumberWithIsoCountryCode = [NSString stringWithFormat:#"%#%#", strongSelf.countryCode, strongSelf.phoneNumberTextField.text];
strongSelf.userModelClass = [[RetrieveDataClass class] retrieveUserInfoForPhoneNumber:PhoneNumberWithIsoCountryCode];
if ([strongSelf.userModelClass.phone_number length] != 0) {
strongSelf.phoneNumber = strongSelf.userModelClass.phone_number;
strongSelf.password = strongSelf.userModelClass.password;
isThisPhoneNumberRegistered = YES;
[strongSelf.activityIndicator stopAnimating];
}
});
});
return isThisPhoneNumberRegistered;
}
The activityIndicator is showing perfectly, but it always return isThisPhoneNumberRegistered = NO, because it works asynchronize way, where my update isThisPhoneNumberRegistered = YES return lately.
So If I want, this scenario where:
User press Sign In button
Then the activityIndicator shows up
My server call (strongSelf.userModelClass = [[RetrieveDataClass class] retrieveUserInfoForPhoneNumber:PhoneNumberWithIsoCountryCode];) retrieve data and set isThisPhoneNumberRegistered = YES or isThisPhoneNumberRegistered = NO according to it
Then return the value to If else condition
if ([self alreadyRegisteredPhoneNumber] == YES) {
if ([self.password isEqualToString:self.passwordTextField.text]) {
[self goToHomeViewController];
} else {
[self initializeAlertControllerForOneButtonWithTitle:#"Wrong Password!" withMessage:kWrongPasswordMSG withYesButtonTitle:#"Ok" withYesButtonAction:nil];
}
How can I do it?
A lot of Thanks in advance.
In general, you want to use Asynch processes so you can "do other stuff" such as updating the UI to let the user know something is going on.
In order to do that, you need to separate your logic. Try to think of it in these terms:
signInButtonAction {
if no phone or password entered
show alert
else
showAnimatedSpinner()
checkForRegisteredPhone()
}
checkForRegisteredPhone {
// start your async check phone number process
// on return from the async call,
if !registered
hide spinner
show kSignInPhoneNumberDoesnotExistMSG message
else
checkForMatchingPassword()
}
checkForMatchingPassword {
if !passwordsMatch
hide spinner
show "passwords don't match" message
else
hide spinner
goToHomeViewController
}
In other words, don't write your code so it gets "locked into" a process, preventing anything else from happening.
I'm trying to run some intensive processes serially, with multiple serial queues. The code is working, however my UI update doesn't occur, even though the method is called.
Here is the code that runs several processes serially.
- (void)importProcess {
dispatch_queue_t serialQueue = dispatch_queue_create("com.cyt.importprocessqueue", DISPATCH_QUEUE_SERIAL);
NSLog(#"checking image sizes");
__block NSMutableArray *assets;
dispatch_sync(serialQueue, ^() {
assets = [self checkImageSizes];
});
dispatch_sync(serialQueue, ^() {
[self appendLogToTextView:[NSString stringWithFormat:#"%i screenshot(s) ignored due to invalid size.",(int)(self.assets.count-assets.count)]];
if(assets.count==0) {
[self showNoRunesFoundAlert];
}
else {
[self appendLogToTextView:#"Preparing to process screenshots..."];
self.assets = [NSArray arrayWithArray:assets];
}
});
NSLog(#"processing uploads");
dispatch_sync(serialQueue, ^() {
[self processUploads];
});
NSLog(#"recognizing images");
dispatch_sync(serialQueue, ^() {
[self recognizeImages];
});
NSLog(#"recognizing images");
dispatch_sync(serialQueue, ^() {
[self processRuneText];
});
//dispatch_sync(dispatch_get_main_queue(), ^ {
//});
}
Within checkImageSizes, I have another serial queue:
- (NSMutableArray *)checkImageSizes {
dispatch_queue_t serialQueue = dispatch_queue_create("com.cyt.checkimagesizequeue", DISPATCH_QUEUE_SERIAL);
NSMutableArray *assets = [NSMutableArray new];
for(int i=0;i<self.assets.count;i++) {
dispatch_sync(serialQueue, ^{
PHAsset *asset = (PHAsset *)self.assets[i];
if(asset.pixelWidth==self.screenSize.width && asset.pixelHeight==self.screenSize.height) {
[assets addObject:asset];
NSString *logText = [NSString stringWithFormat:#"Screenshot %i/%i size is OK.",i+1,(int)self.assets.count];
[self performSelectorOnMainThread:#selector(appendLogToTextView:) withObject:logText waitUntilDone:YES];
}
else {
[self appendLogToTextView:[NSString stringWithFormat:#"ERROR: Screenshot %i/%i has invalid size. Skipping...",i+1,(int)self.assets.count]];
}
});
}
return assets;
}
The appendLogToTextView method is supposed to update the UI. Here is that code:
- (void)appendLogToTextView:(NSString *)newText {
dispatch_block_t block = ^ {
self.logText = [NSString stringWithFormat:#"%#\n%#", self.logText, newText];
NSString *textViewText = [self.logText substringFromIndex:1];
[UIView setAnimationsEnabled:NO];
if(IOS9) {
[self.textView scrollRangeToVisible:NSMakeRange(0,[self.textView.text length])];
self.textView.scrollEnabled = NO;
self.textView.text = textViewText;
self.textView.scrollEnabled = YES;
}
else {
self.textView.text = textViewText;
NSRange range = NSMakeRange(self.textView.text.length - 1, 1);
[self.textView scrollRangeToVisible:range];
}
[UIView setAnimationsEnabled:YES];
};
if ([NSThread isMainThread]) {
block();
}
else {
dispatch_sync(dispatch_get_main_queue(), block);
}
}
As you can see, I have tried calling appendLogToTextView both directly and using performSelectorOnMainThread. However, I'm not getting any updates to my text view, even though I confirm that the method is being called properly.
Interestingly, the UI updating works properly when I only use a single serial queue and use iteration counts to call the next method, as shown in the code below (_serialQueue is defined in viewDidLoad). However, I do not believe that implementation is good practice, as I'm wrapping serial queues within serial queues.
- (void)checkImageSizes {
NSMutableArray *assets = [NSMutableArray new];
for(int i=0;i<self.assets.count;i++) {
dispatch_async(_serialQueue, ^{
PHAsset *asset = (PHAsset *)self.assets[i];
if(asset.pixelWidth==self.screenSize.width && asset.pixelHeight==self.screenSize.height) {
[assets addObject:asset];
[self appendLogToTextView:[NSString stringWithFormat:#"Screenshot %i/%i size is OK.",i+1,(int)self.assets.count]];
}
else {
[self appendLogToTextView:[NSString stringWithFormat:#"ERROR: Screenshot %i/%i has invalid size. Skipping...",i+1,(int)self.assets.count]];
}
//request images
if(i==self.assets.count-1) {
[self appendLogToTextView:[NSString stringWithFormat:#"%i screenshot(s) ignored due to invalid size.",(int)(self.assets.count-assets.count)]];
if(assets.count==0) {
[self showNoRunesFoundAlert];
}
else {
[self appendLogToTextView:#"Preparing to process screenshots..."];
self.assets = [NSArray arrayWithArray:assets];
[self processUploads];
}
}
});
}
}
What am I not understanding about serial queues that is causing the UI updates in this version of the code to work, but my attempt at a "cleaner" implementation to fail?
In the end, I just ended up using dispatch groups and completion blocks in order to solve this problem.
I need to send a large amount of HTTP request in a for loop, and once I finish a task, I need to update progress on main thread.
float __block progress = 0.0f;
float __block tempValue = 0.0f;
dispatch_queue_t operationQueue = dispatch_queue_create("Operation Data", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int uploadCount = 0; uploadCount < mutableArray.count; uploadCount++) {
dispatch_barrier_async(operationQueue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
DDLogInfo(#"add Task.");
[MPRestAPI copyMediaToAlbum:mutableArray[uploadCount] targetAlbum:albumName success:^(BOOL bSuccess){
if (bSuccess) {
dispatch_async(dispatch_get_main_queue(), ^{
progress = (float)(uploadCount+1)/(float)mutableArray.count;
if(uploadCount+1 == mutableArray.count){
progressHUD.progress = 1.0;
[progressHUD hide:YES];
[self dismissViewControllerAnimated:NO completion:nil];
}else if(progress > tempValue) {
progressHUD.progress = progress;
tempValue = progress + 0.01;
}
});
}
}failure:^(NSError* error){
if(uploadCount == mutableArray.count -1){
[progressHUD hide:YES];
[self dismissViewControllerAnimated:NO completion:nil];
}
}];
});
dispatch_semaphore_signal(semaphore);
}
Then there comes low memory, and it crashed. and mutableArray.count can be 5000,so I wonder what should I do to fix this problem?
I have a modal view in which a countdown starts when the user had performed some action. At the end of the countdown, the modal view would close. The following is the procedure I have written towards that goal but unfortunately, it is causing some thread problems. Is there any way to rewrite this in a way that does not have potential thread issues?
- (void)countDown {
static int i = 3;
if (i == 3) {
i--;
UIImage *three = [UIImage imageNamed:#"Three"];
countDownFlag = [[UIImageView alloc] initWithImage:three];
countDownFlag.frame = CGRectMake(0, 370, countDownFlag.frame.size.width, countDownFlag.frame.size.height);
countDownFlag.center = CGPointMake(width / 2, countDownFlag.center.y);
[self.view addSubview:countDownFlag];
[self performSelector:#selector(countDown) withObject:nil afterDelay:0.5];
} else if (i == 2) {
i--;
UIImage *two = [UIImage imageNamed:#"Two"];
[countDownFlag setImage:two];
[self performSelector:#selector(countDown) withObject:nil afterDelay:0.5];
} else if (i == 1) {
i--;
UIImage *one = [UIImage imageNamed:#"One"];
[countDownFlag setImage:one];
[self performSelector:#selector(countDown) withObject:nil afterDelay:0.5];
} else {
[self dismissViewControllerAnimated:YES completion:nil];
}
}
Edit: the picture shows what XCode tells me about the problem
More edit:
My code has changed to reflect vadian's answer. Here is the update
- (void)startCountDown {
NSLog(#"starting count down");
counter = 3;
[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(countDown:) userInfo:nil repeats:YES];
}
- (void)countDown:(NSTimer *)timer {
if (counter == 3) {
countDownFlag = [[UIImageView alloc] init];
countDownFlag.frame = CGRectMake(0, 370, countDownFlag.frame.size.width, countDownFlag.frame.size.height);
countDownFlag.center = CGPointMake(width / 2, countDownFlag.center.y);
dispatch_async(dispatch_get_main_queue(), ^{
[self.view addSubview:countDownFlag];
});
} else if (counter == 0) {
[timer invalidate];
[self dismissViewControllerAnimated:YES completion:nil];
return;
}
NSArray *imageNameArray = #[#"", #"One", #"Two", #"Three"];
UIImage *image = [UIImage imageNamed:imageNameArray[counter]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.countDownFlag setImage:image];
});
counter--;
}
I new guess that the problem probably lies in the code that calls the countdown. Here is the code that does this.
- (void)changeLanguage:(BOOL) isMyanmar {
if (hasRun == YES) {
return;
}
hasRun = YES;
NSUserDefaults * userdefaults = [NSUserDefaults standardUserDefaults];
[userdefaults setBool:isMyanmar forKey:#"myanmar"];
[userdefaults synchronize];
[self updateCurrentLanguageText];
if ([self isMyanmar]) {
[self changeLanguageFlag:#"MyanmarFlagBig"];
} else {
[self changeLanguageFlag:#"UnitedKingdomFlagBig"];
}
//>>>>>>>>>>>>>>>>>>>> the problem is probably here
[self startCountDown];
}
Any code that runs after changing language code fails. The problem is probably there.
Edit: the thread problem is gone but the countdown isn't happening anymore.
After I have moved the code in changeLanguage into the methods that call it, the problem is mysteriously gone. (Repetitive but it works.)
- (void)changeLanguageToEnglish {
[theLock lock];
if (hasRun == YES) {
[theLock unlock];
return;
}
hasRun = YES;
[userdefaults setBool:false forKey:#"myanmar"];
[userdefaults synchronize];
[self updateCurrentLanguageText];
if ([self isMyanmar]) {
[self changeLanguageFlag:#"MyanmarFlagBig"];
} else {
[self changeLanguageFlag:#"UnitedKingdomFlagBig"];
}
[self startCountDown];
[theLock unlock];
}
- (void)changeLanguageToMyanmar {
[theLock lock];
if (hasRun == YES) {
[theLock unlock];
return;
}
hasRun = YES;
[userdefaults setBool:true forKey:#"myanmar"];
[userdefaults synchronize];
[self updateCurrentLanguageText];
if ([self isMyanmar]) {
[self changeLanguageFlag:#"MyanmarFlagBig"];
} else {
[self changeLanguageFlag:#"UnitedKingdomFlagBig"];
}
[self startCountDown];
[theLock unlock];
}
But the problem is that the countdown isn't happening anymore.
You could use an NSTimer to perform the countdown and a static variable for the counter.
The method startCountDown starts the timer.
The method countDown:(NSTimer *)timer is called every 0.5 seconds. The passed NSTimer argument is exactly the timer which has been started.
it performs the action according to its value and decrements the counter. If the counter is 0, the timer is invalidated and the controller dismissed.
static UInt8 counter = 0;
- (void)startCountDown {
countDownFlag = [[UIImageView alloc] init];
countDownFlag.frame = CGRectMake(0, 370, countDownFlag.frame.size.width, countDownFlag.frame.size.height);
countDownFlag.center = CGPointMake(width / 2, countDownFlag.center.y);
[self.view addSubview:countDownFlag];
counter = 3;
[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(countDown:) userInfo:nil repeats:YES];
}
- (void)countDown:(NSTimer *)timer {
if (counter == 0) {
[timer invalidate];
[self dismissViewControllerAnimated:YES completion:nil];
return;
}
NSArray *imageNameArray = #[#"", #"One", #"Two", #"Three"];
UIImage *image = [UIImage imageNamed:imageNameArray[counter]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.countDownFlag setImage:image];
});
counter--;
}
Place static int i = 3; outside your method countDown, the way you did it it declares new variable every time you call that method.
So I have successfully turned a button into an off and on switch that changes the label.
I was also able to have it start a timed processed set off when that is to occur, and it have the ability to shut off the timed process.
Anyways I need to way to shut down the timed process I was wondering if there was a way to stop it without using the disposable. With a second takeUntil signal.
Edit I think what I was trying to do was slightly misleading let me show my current solution that works.
-(RACSignal*) startTimer {
return [[RACSignal interval:1.0
onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]];
}
-(void) viewWillAppear:(BOOL)animated {}
-(void) viewDidLoad {
self.tableView.delegate = self;
self.tableView.dataSource = self;
RACSignal* pressedStart = [self.start rac_signalForControlEvents:UIControlEventTouchUpInside];
#weakify(self);
RACSignal* textChangeSignal = [pressedStart map:^id(id value) {
#strongify(self);
return [self.start.titleLabel.text isEqualToString:#"Start"] ? #"Stop" : #"Start";
}];
// Changes the title
[textChangeSignal subscribeNext:^(NSString* text) {
#strongify(self);
[self.start setTitle:text forState:UIControlStateNormal];
}];
RACSignal* switchSignal = [[textChangeSignal map:^id(NSString* string) {
return [string isEqualToString:#"Stop"] ? #0 : #1;
}] filter:^BOOL(id value) {
NSLog(#"Switch %#",value);
return [value boolValue];
}];
[[self rac_signalForSelector:#selector(viewWillAppear:)]
subscribeNext:^(id x) {
}];
static NSInteger t = 0;
// Remake's it self once it is on finished.
self.disposable = [[[textChangeSignal filter:^BOOL(NSString* text) {
return [text isEqualToString:#"Stop"] ? [#1 boolValue] : [#0 boolValue];
}]
flattenMap:^RACStream *(id value) {
NSLog(#"Made new Sheduler");
#strongify(self);
return [[self startTimer] takeUntil:switchSignal];
}] subscribeNext:^(id x) {
NSLog(#"%#",x);
#strongify(self);
t = t + 1;
NSLog(#"%zd",t);
[self updateTable];
}];
[[self rac_signalForSelector:#selector(viewWillDisappear:)] subscribeNext:^(id x) {
NSLog(#"viewWillAppear Dispose");
[self.disposable dispose];
}];
}
-(BOOL) isGroupedExcercisesLeft {
BOOL isGroupedLeft = NO;
for (int i =0;i < [self.excercises count]; i++) {
Excercise* ex = [self.excercises objectAtIndex:i];
if(ex.complete == NO && ex.grouped == YES) {
isGroupedLeft = YES;
break;
}
}
return isGroupedLeft;
}
-(void) updateTable {
// Find the
NSInteger nextRow;
if (([self.excercises count] > 0 || self.excercises !=nil) && [self isGroupedExcercisesLeft]) {
for (int i =0;i < [self.excercises count]; i++) {
Excercise* ex = [self.excercises objectAtIndex:i];
if(ex.complete == NO && ex.grouped == YES) {
nextRow = i;
break;
}
}
NSIndexPath* path = [NSIndexPath indexPathForItem:nextRow inSection:0];
NSArray* indexPath = #[path];
// update //
Excercise* ex = [self.excercises objectAtIndex:nextRow];
[self.tableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionTop animated:YES];
if (ex.seconds <= 0) {
RLMRealm* db = [RLMRealm defaultRealm];
[db beginWriteTransaction];
ex.complete = YES;
[db commitWriteTransaction];
}
else {
// Update Seconds
RLMRealm* db = [RLMRealm defaultRealm];
[db beginWriteTransaction];
ex.seconds = ex.seconds - 1000;
NSLog(#"Seconds: %zd",ex.seconds);
[db commitWriteTransaction];
// Update table
[self.tableView reloadRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationNone];
}
} else {
NSLog(#"Done");
SIAlertView *alertView = [[SIAlertView alloc] initWithTitle:#"Deskercise" andMessage:#"Excercises Complete"];
[alertView addButtonWithTitle:#"Ok"
type:SIAlertViewButtonTypeDefault
handler:^(SIAlertView *alert) {
}];
alertView.transitionStyle = SIAlertViewTransitionStyleBounce;
[alertView show];
NSLog(#"Dispose");
[self.disposable dispose];
}
}
The issue with using takeUntil:self.completeSignal is that when you change completeSignal to another value, it isn't passed to any function that was already waiting for the variable that completeSignal was previously holding.
- (RACSignal*) startTimer {
#weakify(self)
return [[[RACSignal interval:1.0
onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]]
takeUntil:[[self.start rac_signalForControlEvents:UIControlEventTouchUpInside]
merge:[[RACObserve(self, completeSignal) skip:1] flattenMap:
^RACStream *(RACSignal * signal) {
#strongify(self)
return self.completeSignal;
}]]
];
}
The signal is now observing and flattening completeSignal, which will give the desired effect. Signals that complete without sending next events are ignored by takeUntil:, so use self.completedSignal = [RACSignal return:nil], which sends a single next event and then completes.
However, this code is anything but ideal, let's look at a better solution.
#property (nonatomic, readwrite) RACSubject * completeSignal;
- (RACSignal*) startTimer {
return [[[RACSignal interval:1.0
onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]]
takeUntil:[[self.start rac_signalForControlEvents:UIControlEventTouchUpInside]
merge:self.completeSignal]
];
}
- (void) viewDidLoad {
[super viewDidLoad];
self.completeSignal = [RACSubject subject];
self.tableView.delegate = self;
self.tableView.dataSource = self;
RACSignal * pressedStart = [self.start rac_signalForControlEvents:UIControlEventTouchUpInside];
#weakify(self);
RACSignal* textChangeSignal = [[pressedStart startWith:nil] scanWithStart:#"Stop" reduce:^id(id running, id next) {
return #{#"Start":#"Stop", #"Stop":#"Start"}[running];
}];
[self.start
rac_liftSelector:#selector(setTitle:forState:)
withSignals:textChangeSignal, [RACSignal return:#(UIControlStateNormal)], nil];
[[[pressedStart flattenMap:^RACStream *(id value) { //Using take:1 so that it doesn't get into a feedback loop
#strongify(self);
return [self startTimer];
}] scanWithStart:#0 reduce:^id(NSNumber * running, NSNumber * next) {
return #(running.unsignedIntegerValue + 1);
}] subscribeNext:^(id x) {
#strongify(self);
[self updateTable];
NSLog(#"%#", x);
}];
}
- (void) updateTable {
//If you uncomment these then it'll cause a feedback loop for the signal that calls updateTable
//[self.start sendActionsForControlEvents:UIControlEventTouchUpInside];
//[self.completeSignal sendNext:nil];
if ([self.excercises count] > 0 || self.excercises !=nil) {
} else {
}
}
Let's run through the list of changes:
completeSignal is now a RACSubject (a manually controlled RACSignal).
For purity and to get rid of the #weakify directive, textChangeSignal now uses the handy scanWithStart:reduce: method, which lets you access an accumulator (this works well for methods that work with an incrementing or decrementing number).
start's text is now being changed by the rac_liftSelector function, which takes RACSignals and unwraps them when all have fired.
Your flattenMap: to replace pressedStart with [self startTimer] now uses scanWithStart:reduce, which is a much more functional way to keep count.
I'm not sure if you were testing by having updateTable contain completion signals but it definitely causes a logic issue with your flattenMap: of pressedButton, the resulting feedback loop eventually crashes the program when the stack overflows.