diff --git a/README.md b/README.md index c0487bf..e2f782a 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ ToDo - [ ] Show statistics - [x] How often gets the feed updated (min, max, avg) - [ ] Automatically choose best interval? - - [ ] Show time of next update + - [x] Show time of next update - [ ] Feeds with authentication diff --git a/baRSS/FeedDownload.h b/baRSS/FeedDownload.h index 7b810d7..b6d91ff 100644 --- a/baRSS/FeedDownload.h +++ b/baRSS/FeedDownload.h @@ -26,6 +26,7 @@ @class Feed; @interface FeedDownload : NSObject +@property (class, readonly) NSDate *dateScheduled; @property (class, readonly) BOOL allowNetworkConnection; @property (class, readonly) BOOL isUpdating; @property (class, setter=setPaused:) BOOL isPaused; diff --git a/baRSS/FeedDownload.m b/baRSS/FeedDownload.m index 864e851..93f2adf 100644 --- a/baRSS/FeedDownload.m +++ b/baRSS/FeedDownload.m @@ -29,6 +29,7 @@ #import +static NSTimer *_timer; static SCNetworkReachabilityRef _reachability = NULL; static BOOL _isReachable = NO; static BOOL _isUpdating = NO; @@ -41,6 +42,9 @@ static BOOL _nextUpdateIsForced = NO; #pragma mark - User Interaction - +/// @return Date when background update will fire. If updates are paused, date is @c distantFuture. ++ (NSDate *)dateScheduled { return _timer.fireDate; } + /// @return @c YES if current network state is reachable and updates are not paused by user. + (BOOL)allowNetworkConnection { return (_isReachable && !_updatePaused); } @@ -80,9 +84,8 @@ static BOOL _nextUpdateIsForced = NO; + (void)scheduleUpdateForUpcomingFeeds { if (![self allowNetworkConnection]) // timer will restart once connection exists return; - NSDate *nextTime = [StoreCoordinator nextScheduledUpdate]; - if (!nextTime) return; // no timer means no feeds to update - if ([nextTime timeIntervalSinceNow] < 1) { // mostly, if app was closed for a long time + NSDate *nextTime = [StoreCoordinator nextScheduledUpdate]; // if nextTime = nil, then no feeds to update + if (nextTime && [nextTime timeIntervalSinceNow] < 1) { // mostly, if app was closed for a long time nextTime = [NSDate dateWithTimeIntervalSinceNow:1]; } [self scheduleTimer:nextTime]; @@ -104,16 +107,16 @@ static BOOL _nextUpdateIsForced = NO; @param nextTime If @c nil timer will be disabled with a @c .fireDate very far in the future. */ + (void)scheduleTimer:(NSDate*)nextTime { - static NSTimer *timer; - if (!timer) { - timer = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:[self class] selector:@selector(updateTimerCallback) userInfo:nil repeats:YES]; - [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; - } + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _timer = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:[self class] selector:@selector(updateTimerCallback) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; + }); if (!nextTime) - nextTime = [NSDate dateWithTimeIntervalSinceNow:NSTimeIntervalSince1970]; + nextTime = [NSDate distantFuture]; NSTimeInterval tolerance = [nextTime timeIntervalSinceNow] * 0.15; - timer.tolerance = (tolerance < 1 ? 1 : tolerance); // at least 1 sec - timer.fireDate = nextTime; + _timer.tolerance = (tolerance < 1 ? 1 : tolerance); // at least 1 sec + _timer.fireDate = nextTime; } /** diff --git a/baRSS/Preferences/Feeds Tab/SettingsFeeds.m b/baRSS/Preferences/Feeds Tab/SettingsFeeds.m index 5b4837d..4a89ef9 100644 --- a/baRSS/Preferences/Feeds Tab/SettingsFeeds.m +++ b/baRSS/Preferences/Feeds Tab/SettingsFeeds.m @@ -37,6 +37,9 @@ @property (strong) NSArray *currentlyDraggedNodes; @property (strong) NSUndoManager *undoManager; + +@property (strong) NSTimer *timerStatusInfo; +@property (strong) NSDateComponentsFormatter *intervalFormatter; @end @implementation SettingsFeeds @@ -47,7 +50,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; - (void)viewDidLoad { [super viewDidLoad]; - [self activateSpinner:([FeedDownload isUpdating] ? -1 : 0)]; // start spinner if update is in progress when preferences open [self.outlineView registerForDraggedTypes:[NSArray arrayWithObject:dragNodeType]]; [self.dataStore setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:YES]]]; @@ -69,6 +71,64 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; } +#pragma mark - Activity Spinner & Status Info + + +/// Initialize status info timer +- (void)viewWillAppear { + self.intervalFormatter = [[NSDateComponentsFormatter alloc] init]; + self.intervalFormatter.unitsStyle = NSDateComponentsFormatterUnitsStyleShort; // e.g., '30 min' + self.intervalFormatter.maximumUnitCount = 1; + self.timerStatusInfo = [NSTimer timerWithTimeInterval:NSTimeIntervalSince1970 target:self selector:@selector(keepTimerRunning) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:self.timerStatusInfo forMode:NSRunLoopCommonModes]; + // start spinner if update is in progress when preferences open + [self activateSpinner:([FeedDownload isUpdating] ? -1 : 0)]; +} + +/// Timer cleanup +- (void)viewWillDisappear { + // in viewWillDisappear otherwise dealloc will not be called + [self.timerStatusInfo invalidate]; + self.timerStatusInfo = nil; + self.intervalFormatter = nil; +} + +/// Callback method to update status info. Will be called more often when interval is getting shorter. +- (void)keepTimerRunning { + NSDate *date = [FeedDownload dateScheduled]; + if (date) { + double nextFire = fabs(date.timeIntervalSinceNow); + if (nextFire > 60) { // update 1/min + nextFire = fmod(nextFire, 60); // next update will align with minute + } else { + nextFire = 1; // update 1/sec + } + NSString *str = [self.intervalFormatter stringFromTimeInterval: date.timeIntervalSinceNow]; + self.spinnerLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Next update in %@", nil), str]; + [self.timerStatusInfo setFireDate:[NSDate dateWithTimeIntervalSinceNow: nextFire]]; + } +} + +/// Start ( @c c @c > @c 0 ) or stop ( @c c @c = @c 0 ) activity spinner. Also, sets status info. +- (void)activateSpinner:(NSInteger)c { + if (c == 0) { + [self.spinner stopAnimation:nil]; + self.spinnerLabel.stringValue = @""; + [self.timerStatusInfo fire]; + } else { + [self.timerStatusInfo setFireDate:[NSDate distantFuture]]; + [self.spinner startAnimation:nil]; + if (c == 1) { // exactly one feed + self.spinnerLabel.stringValue = NSLocalizedString(@"Updating 1 feed …", nil); + } else if (c < 0) { // unknown number of feeds + self.spinnerLabel.stringValue = NSLocalizedString(@"Updating feeds …", nil); + } else { + self.spinnerLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Updating %lu feeds …", nil), c]; + } + } +} + + #pragma mark - Notification callback methods @@ -89,25 +149,6 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; [self activateSpinner:[notify.object integerValue]]; } -/// Start or stop activity spinner (will run on main thread). If @c c @c == @c 0 stop spinner. -- (void)activateSpinner:(NSInteger)c { - dispatch_async(dispatch_get_main_queue(), ^{ - if (c == 0) { - [self.spinner stopAnimation:nil]; - self.spinnerLabel.stringValue = @""; - } else { - [self.spinner startAnimation:nil]; - if (c < 0) { // unknown number of feeds - self.spinnerLabel.stringValue = NSLocalizedString(@"Updating feeds …", nil); - } else if (c == 1) { - self.spinnerLabel.stringValue = NSLocalizedString(@"Updating 1 feed …", nil); - } else { - self.spinnerLabel.stringValue = [NSString stringWithFormat:NSLocalizedString(@"Updating %lu feeds …", nil), c]; - } - } - }); -} - #pragma mark - Persist state @@ -133,6 +174,8 @@ static NSString *dragNodeType = @"baRSS-feed-drag"; [self.undoManager endUndoGrouping]; if (!flag && self.dataStore.managedObjectContext.hasChanges) { [StoreCoordinator saveContext:self.dataStore.managedObjectContext andParent:YES]; + [FeedDownload scheduleUpdateForUpcomingFeeds]; + [self.timerStatusInfo fire]; return YES; } [self.undoManager disableUndoRegistration]; diff --git a/baRSS/Preferences/Feeds Tab/SettingsFeeds.xib b/baRSS/Preferences/Feeds Tab/SettingsFeeds.xib index 02c0285..bea7501 100644 --- a/baRSS/Preferences/Feeds Tab/SettingsFeeds.xib +++ b/baRSS/Preferences/Feeds Tab/SettingsFeeds.xib @@ -221,19 +221,19 @@ CA - - - - - + - + + + + +