diff --git a/CareKit.xcodeproj/project.pbxproj b/CareKit.xcodeproj/project.pbxproj index 105a11bd1..77cca8023 100644 --- a/CareKit.xcodeproj/project.pbxproj +++ b/CareKit.xcodeproj/project.pbxproj @@ -175,6 +175,10 @@ FC9047E821E5A1890012C685 /* CustomSectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = FC9047E621E5A1890012C685 /* CustomSectionView.m */; }; FCE629F121EBE40600D541A9 /* CustomSegmentedControlSection.h in Headers */ = {isa = PBXBuildFile; fileRef = FCE629EF21EBE40600D541A9 /* CustomSegmentedControlSection.h */; }; FCE629F221EBE40600D541A9 /* CustomSegmentedControlSection.m in Sources */ = {isa = PBXBuildFile; fileRef = FCE629F021EBE40600D541A9 /* CustomSegmentedControlSection.m */; }; + FCE629FC21ED82B100D541A9 /* OCKCustomCareCardViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = FCE629FA21ED82B100D541A9 /* OCKCustomCareCardViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FCE629FD21ED82B100D541A9 /* OCKCustomCareCardViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FCE629FB21ED82B100D541A9 /* OCKCustomCareCardViewController.m */; }; + FCE62A0021ED8DF200D541A9 /* CustomHeaderView.h in Headers */ = {isa = PBXBuildFile; fileRef = FCE629FE21ED8DF200D541A9 /* CustomHeaderView.h */; }; + FCE62A0121ED8DF200D541A9 /* CustomHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = FCE629FF21ED8DF200D541A9 /* CustomHeaderView.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -390,6 +394,10 @@ FC9047E621E5A1890012C685 /* CustomSectionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSectionView.m; sourceTree = ""; }; FCE629EF21EBE40600D541A9 /* CustomSegmentedControlSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomSegmentedControlSection.h; sourceTree = ""; }; FCE629F021EBE40600D541A9 /* CustomSegmentedControlSection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSegmentedControlSection.m; sourceTree = ""; }; + FCE629FA21ED82B100D541A9 /* OCKCustomCareCardViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCKCustomCareCardViewController.h; sourceTree = ""; }; + FCE629FB21ED82B100D541A9 /* OCKCustomCareCardViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCKCustomCareCardViewController.m; sourceTree = ""; }; + FCE629FE21ED8DF200D541A9 /* CustomHeaderView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomHeaderView.h; sourceTree = ""; }; + FCE629FF21ED8DF200D541A9 /* CustomHeaderView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomHeaderView.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -592,6 +600,8 @@ children = ( 24EA9FA31C9A3D420036028E /* OCKCareCardViewController.h */, 24EA9FA41C9A3D420036028E /* OCKCareCardViewController.m */, + FCE629FA21ED82B100D541A9 /* OCKCustomCareCardViewController.h */, + FCE629FB21ED82B100D541A9 /* OCKCustomCareCardViewController.m */, 24EA9FD31C9A3DDE0036028E /* Table View Cell */, 24EA9FD11C9A3DC60036028E /* Detail View */, ); @@ -898,6 +908,8 @@ children = ( FC9047E521E5A1890012C685 /* CustomSectionView.h */, FC9047E621E5A1890012C685 /* CustomSectionView.m */, + FCE629FE21ED8DF200D541A9 /* CustomHeaderView.h */, + FCE629FF21ED8DF200D541A9 /* CustomHeaderView.m */, FCE629EF21EBE40600D541A9 /* CustomSegmentedControlSection.h */, FCE629F021EBE40600D541A9 /* CustomSegmentedControlSection.m */, ); @@ -916,6 +928,7 @@ BA660FF91EB1FF1100B7B1A7 /* OCKPatientHeaderView.h in Headers */, 248929821C90E70100EBBE1F /* OCKContactInfoTableViewCell.h in Headers */, BAD3286A1DB028AF0021A773 /* OCKInsightsRingTableViewCell.h in Headers */, + FCE62A0021ED8DF200D541A9 /* CustomHeaderView.h in Headers */, 24F8E4761C934FEE003B77BD /* OCKDefines_Private.h in Headers */, 241707CF1C9A3AB7005D7123 /* OCKGroupedBarChartView.h in Headers */, AEB37F691DF22BEF002370A6 /* OCKColor.h in Headers */, @@ -979,6 +992,7 @@ 8677EE141C9775EB00588CD6 /* OCKCarePlanEventResult.h in Headers */, FCE629F121EBE40600D541A9 /* CustomSegmentedControlSection.h in Headers */, 2489297B1C90E70100EBBE1F /* OCKConnectTableViewHeader.h in Headers */, + FCE629FC21ED82B100D541A9 /* OCKCustomCareCardViewController.h in Headers */, BA3793D81EB66BC50004A540 /* OCKPatientWidget.h in Headers */, BA3793E81EB66D7D0004A540 /* OCKDefaultPatientWidgetView.h in Headers */, 241707D11C9A3AB7005D7123 /* OCKInsightItem.h in Headers */, @@ -1167,6 +1181,8 @@ BA38457F1D9CA698007990DA /* OCKGlyph.m in Sources */, FCE629F221EBE40600D541A9 /* CustomSegmentedControlSection.m in Sources */, 24FD43741E56843E004439EA /* OCKConnectHeaderView.m in Sources */, + FCE629FD21ED82B100D541A9 /* OCKCustomCareCardViewController.m in Sources */, + FCE62A0121ED8DF200D541A9 /* CustomHeaderView.m in Sources */, 8677EE191C9775EB00588CD6 /* OCKCarePlanStore.xcdatamodeld in Sources */, 241707D01C9A3AB7005D7123 /* OCKGroupedBarChartView.m in Sources */, BA8A8CA11D9DAC680078C5EA /* OCKWeekView.m in Sources */, diff --git a/CareKit/CareCard/OCKCustomCareCardViewController.h b/CareKit/CareCard/OCKCustomCareCardViewController.h new file mode 100644 index 000000000..698785a54 --- /dev/null +++ b/CareKit/CareCard/OCKCustomCareCardViewController.h @@ -0,0 +1,207 @@ +// +// OCKCustomCareCardViewController.h +// CareKit +// +// Created by Damian Dara on 15/1/19. +// Copyright © 2019 carekit.org. All rights reserved. +// + +#import + + +NS_ASSUME_NONNULL_BEGIN + +@class OCKCarePlanStore, OCKCustomCareCardViewController; + +/** + An object that adopts the `OCKCareCardViewControllerDelegate` protocol can use it modify or update the events before they are displayed. + */ +@protocol OCKCustomCareCardViewControllerDelegate + +@optional + +/** + Asks the delegate if care card view controller should automatically mark the state of an intervention activity when + the user selects and deselects the intervention circle button. If this method is not implemented, care card view controller + handles all event completion by default. + + If returned NO, the `careCardViewController:didSelectButtonWithInterventionEvent` method can be implemeted to provide + custom logic for completion. + + @param viewController The view controller providing the callback. + @param interventionActivity The intervention activity that the user selected. + */ +- (BOOL)careCardViewController:(OCKCustomCareCardViewController *)viewController shouldHandleEventCompletionForActivity:(OCKCarePlanActivity *)interventionActivity; + +/** + Tells the delegate when the user tapped an intervention event. + + If the user must perform some activity in order to complete the intervention event, + then this method can be implemented to show a custom view controller. + + If the completion status of the event is dependent on the presented activity, the developer can implement + the `careCardViewController:shouldHandleEventCompletionForActivity` to control the completion status of the event. + + @param viewController The view controller providing the callback. + @param interventionEvent The intervention event that the user selected. + */ +- (void)careCardViewController:(OCKCustomCareCardViewController *)viewController didSelectButtonWithInterventionEvent:(OCKCarePlanEvent *)interventionEvent; + +/** + Tells the delegate when the user selected an intervention activity. + + This can be implemented to show a custom detail view controller. + If not implemented, a default detail view controller will be presented. + + @param viewController The view controller providing the callback. + @param interventionActivity The intervention activity that the user selected. + */ +- (void)careCardViewController:(OCKCustomCareCardViewController *)viewController didSelectRowWithInterventionActivity:(OCKCarePlanActivity *)interventionActivity; + +/** + Tells the delegate when a new set of events is fetched from the care plan store. + + This is invoked when the date changes or when the care plan store's `carePlanStoreActivityListDidChange` delegate method is called. + This provides a good opportunity to update the store such as fetching data from HealthKit. + + @param viewController The view controller providing the callback. + @param events An array containing the fetched set of intervention events grouped by activity. + @param dateComponents The date components for which the events will be displayed. + */ +- (void)careCardViewController:(OCKCustomCareCardViewController *)viewController willDisplayEvents:(NSArray*>*)events dateComponents:(NSDateComponents *)dateComponents; + +/** + Asks the delegate if the care card view controller should enable pull-to-refresh behavior on the activities list. If not implemented, + pull-to-refresh will not be enabled. + + If returned YES, the `careCardViewController:didActivatePullToRefreshControl:` method should be implemented to provide custom + refreshing behavior when triggered by the user. + + @param viewController The view controller providing the callback. + */ +- (BOOL)shouldEnablePullToRefreshInCareCardViewController:(OCKCustomCareCardViewController *)viewController; + +/** + Tells the delegate the user has triggered pull to refresh on the activities list. + + Provides the opportunity to refresh data in the local store by, for example, fetching from a cloud data store. + This method should always be implmented in cases where `shouldEnablePullToRefreshInCareCardViewController:` might return YES. + + @param viewController The view controller providing the callback. + @param refreshControl The refresh control which has been triggered, where `isRefreshing` should always be YES. + It is the developers responsibility to call `endRefreshing` as appropriate, on the main thread. + */ +- (void)careCardViewController:(OCKCustomCareCardViewController *)viewController didActivatePullToRefreshControl:(UIRefreshControl *)refreshControl; + +@end + + +/** + The `OCKCareCardViewController` class is a view controller that displays the activities and events + from an `OCKCarePlanStore` that are of intervention type (see `OCKCarePlanActivityTypeIntervention`). + + It includes a master view and a detail view. Therefore, it must be embedded inside a `UINavigationController`. + */ +OCK_CLASS_AVAILABLE +@interface OCKCustomCareCardViewController : UIViewController + +- (instancetype)init NS_UNAVAILABLE; + +/** + Returns an initialized care card view controller using the specified store. + + @param store A care plan store. + + @return An initialized care card view controller. + */ +- (instancetype)initWithCarePlanStore:(OCKCarePlanStore *)store; + +/** + The care plan store that provides the content for the care card. + + The care card displays activites and events that are of intervention type (see `OCKCarePlanActivityTypeIntervention`). + */ +@property (nonatomic, readonly) OCKCarePlanStore *store; + +/** + The delegate can be used to modify or update the internvention events before they are displayed. + + See the `OCKCareCardViewControllerDelegate` protocol. + */ +@property (nonatomic, weak, nullable) id delegate; + +/** + The last intervention activity selected by the user. + + This value is nil if no intervention activity has been selected yet. + */ +@property (nonatomic, readonly, nullable) OCKCarePlanActivity *lastSelectedInterventionActivity; + +/** + The last intervention event selected by the user. + + This value is nil if no intervention event has been selected yet. + */ +@property (nonatomic, readonly, nullable) OCKCarePlanEvent *lastSelectedInterventionEvent; + +/** + A reference to the `UITableView` contained in the view controller + */ +@property (nonatomic, readonly, nonnull) UITableView *tableView; + +/** + A reference to the `UIButton` container in the view controller + */ +@property (nonatomic, nonnull) UIButton *actionButton; + +/** + A message that will be displayed in the table view's background view if there are + no intervention activities to display. + + If the value is not specified, nothing will be shown when the table is empty. + */ +@property (nonatomic, nullable) NSString *noActivitiesText; + +/** + Header's title + */ +@property (nonatomic, nullable) NSString *headerTitleText; + +/** + Date label's text value + */ +@property (nonatomic, nullable) NSString *dateTitleText; + +/** + Action button title located at the bottom of the page + */ +@property (nonatomic, nullable) NSString *actionButtonTitle; + +/** + Action button background color + */ +@property (nonatomic, nullable) UIColor *buttonColor; + +/** + The property that allows activities to be grouped. + + If true, the activities will be grouped by groupIdentifier into sections, + otherwise the activities will all be in one section and groupIdentifier is ignored. + + The default is false. + */ +@property (nonatomic) BOOL isGrouped; + +/** + The property that allows activities to be sorted. + + If true, the activities will be sorted alphabetically by title and by groupIdentifier if isGrouped is true, + otherwise the activities will be sorted in the order they are added in the care plan store. + + The default is true. + */ +@property (nonatomic) BOOL isSorted; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CareKit/CareCard/OCKCustomCareCardViewController.m b/CareKit/CareCard/OCKCustomCareCardViewController.m new file mode 100644 index 000000000..9066023d6 --- /dev/null +++ b/CareKit/CareCard/OCKCustomCareCardViewController.m @@ -0,0 +1,629 @@ +// +// OCKCustomCareCardViewController.m +// CareKit +// +// Created by Damian Dara on 15/1/19. +// Copyright © 2019 carekit.org. All rights reserved. +// + +#import "OCKCustomCareCardViewController.h" +#import "OCKWeekView.h" +#import "OCKCareCardDetailViewController.h" +#import "OCKWeekViewController.h" +#import "NSDateComponents+CarePlanInternal.h" +#import "OCKHeaderView.h" +#import "OCKLabel.h" +#import "OCKCareCardTableViewCell.h" +#import "OCKWeekLabelsView.h" +#import "OCKCarePlanStore_Internal.h" +#import "OCKHelpers.h" +#import "OCKDefines_Private.h" +#import "OCKGlyph_Internal.h" +#import "CustomHeaderView.h" + + +#define RedColor() OCKColorFromRGB(0xEF445B); + + +@interface OCKCustomCareCardViewController() + +@property (nonatomic) NSDateComponents *selectedDate; + +@end + +@implementation OCKCustomCareCardViewController { + NSMutableArray *> *_events; + NSCalendar *_calendar; + NSMutableArray *_constraints; + NSMutableArray *_sectionTitles; + NSMutableArray *> *> *_tableViewData; + NSString *_otherString; + NSString *_optionalString; + BOOL _isGrouped; + BOOL _isSorted; + UIRefreshControl *_refreshControl; + OCKLabel *_noActivitiesLabel; + CustomHeaderView *_headerView; +} + +- (instancetype)init { + OCKThrowMethodUnavailableException(); + return nil; +} + +- (instancetype)initWithCarePlanStore:(OCKCarePlanStore *)store { + self = [super init]; + if (self) { + _store = store; + _calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]; + _isGrouped = NO; + _isSorted = YES; + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + _otherString = OCKLocalizedString(@"ACTIVITY_TYPE_OTHER_SECTION_HEADER", nil); + _optionalString = OCKLocalizedString(@"ACTIVITY_TYPE_OPTIONAL_SECTION_HEADER", nil); + + self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; + + self.store.careCardUIDelegate = self; + + _headerView = [CustomHeaderView new]; + _headerView.title = self.headerTitleText; + _headerView.date = self.dateTitleText; + _headerView.tintColor = self.buttonColor; + [self.view addSubview:_headerView]; + + _actionButton = [UIButton buttonWithType: UIButtonTypeRoundedRect]; + [_actionButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + [_actionButton setTitle:self.actionButtonTitle forState:UIControlStateNormal]; + [_actionButton setBackgroundColor: self.buttonColor]; + [self.view addSubview:_actionButton]; + + _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + _tableView.dataSource = self; + _tableView.delegate = self; + [self.view addSubview:_tableView]; + + [self prepareView]; + + self.selectedDate = [NSDateComponents ock_componentsWithDate:[NSDate date] calendar:_calendar]; + + _tableView.estimatedRowHeight = 90.0; + _tableView.rowHeight = UITableViewAutomaticDimension; + _tableView.tableFooterView = [UIView new]; + _tableView.estimatedSectionHeaderHeight = 0; + _tableView.estimatedSectionFooterHeight = 0; + + _refreshControl = [[UIRefreshControl alloc] init]; + _refreshControl.tintColor = [UIColor grayColor]; + [_refreshControl addTarget:self action:@selector(didActivatePullToRefreshControl:) forControlEvents:UIControlEventValueChanged]; + _tableView.refreshControl = _refreshControl; + [self updatePullToRefreshControl]; + + _noActivitiesLabel = [OCKLabel new]; + _noActivitiesLabel.hidden = YES; + _noActivitiesLabel.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + _noActivitiesLabel.textStyle = UIFontTextStyleTitle2; + _noActivitiesLabel.textColor = [UIColor lightGrayColor]; + _noActivitiesLabel.text = self.noActivitiesText; + _noActivitiesLabel.textAlignment = NSTextAlignmentCenter; + _noActivitiesLabel.numberOfLines = 0; + _tableView.backgroundView = _noActivitiesLabel; + + self.navigationController.navigationBar.translucent = NO; + [self.navigationController.navigationBar setBarTintColor:[UIColor colorWithRed:245.0/255.0 green:244.0/255.0 blue:246.0/255.0 alpha:1.0]]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; +} + +- (void)showToday:(id)sender { + self.selectedDate = [NSDateComponents ock_componentsWithDate:[NSDate date] calendar:_calendar]; + if (_tableViewData.count > 0) { + [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:NSNotFound inSection:0] atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; + } +} + +- (void)didActivatePullToRefreshControl:(UIRefreshControl *)sender +{ + if (nil == _delegate || + ![_delegate respondsToSelector:@selector(careCardViewController:didActivatePullToRefreshControl:)]) { + + return; + } + + [_delegate careCardViewController:self didActivatePullToRefreshControl:sender]; +} + +- (void)prepareView { + + _tableView.showsVerticalScrollIndicator = NO; + + [self setUpConstraints]; +} + +- (void)setUpConstraints { + [NSLayoutConstraint deactivateConstraints:_constraints]; + + _constraints = [NSMutableArray new]; + + _tableView.translatesAutoresizingMaskIntoConstraints = NO; + _headerView.translatesAutoresizingMaskIntoConstraints = NO; + _actionButton.translatesAutoresizingMaskIntoConstraints = NO; + + [_constraints addObjectsFromArray:@[ + [NSLayoutConstraint constraintWithItem:_headerView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeTop + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:_headerView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeLeading + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:_headerView + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:_headerView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:132], + + [NSLayoutConstraint constraintWithItem:_tableView + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:_headerView + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:_tableView + attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:_actionButton + attribute: NSLayoutAttributeTop + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:_tableView + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeLeading + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:_tableView + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:0.0], + // Button + [_actionButton.bottomAnchor constraintEqualToAnchor:self.view.layoutMarginsGuide.bottomAnchor], + + [NSLayoutConstraint constraintWithItem:_actionButton + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:44.0], + + [NSLayoutConstraint constraintWithItem:_actionButton + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeLeading + multiplier:1.0 + constant:0.0], + + [NSLayoutConstraint constraintWithItem:_actionButton + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeTrailing + multiplier:1.0 + constant:0.0] + + ]]; + + [NSLayoutConstraint activateConstraints:_constraints]; +} + +- (void)setSelectedDate:(NSDateComponents *)selectedDate { + NSDateComponents *today = [self today]; + _selectedDate = [selectedDate isLaterThan:today] ? today : selectedDate; + + [self fetchEvents]; +} + +- (void)setDelegate:(id)delegate +{ + _delegate = delegate; + + if ([NSOperationQueue currentQueue] != [NSOperationQueue mainQueue]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self updatePullToRefreshControl]; + }); + } else { + [self updatePullToRefreshControl]; + } +} + +- (void)setNoActivitiesText:(NSString *)noActivitiesText { + _noActivitiesText = noActivitiesText; + _noActivitiesLabel.text = noActivitiesText; +} + +- (void)setHeaderTitleText:(NSString *)headerTitleText { + _headerTitleText = headerTitleText; + _headerView.title = headerTitleText; +} + +- (void)setButtonColor:(UIColor *)buttonColor { + _buttonColor = buttonColor; + _headerView.tintColor = buttonColor; + [_actionButton setBackgroundColor:buttonColor]; +} + +- (void)setActionButtonTitle:(NSString *)actionButtonTitle { + _actionButtonTitle = actionButtonTitle; + [_actionButton setTitle:actionButtonTitle forState:UIControlStateNormal]; +} + +- (void)setDateTitleText:(NSString *)dateTitleText { + _dateTitleText = dateTitleText; + _headerView.date = dateTitleText; +} + +#pragma mark - Helpers + +- (void)fetchEvents { + [self.store eventsOnDate:self.selectedDate + type:OCKCarePlanActivityTypeIntervention + completion:^(NSArray *> *eventsGroupedByActivity, NSError *error) { + NSAssert(!error, error.localizedDescription); + dispatch_async(dispatch_get_main_queue(), ^{ + _events = [NSMutableArray new]; + for (NSArray *events in eventsGroupedByActivity) { + [_events addObject:[events mutableCopy]]; + } + + if (self.delegate && + [self.delegate respondsToSelector:@selector(careCardViewController:willDisplayEvents:dateComponents:)]) { + [self.delegate careCardViewController:self willDisplayEvents:[_events copy] dateComponents:_selectedDate]; + } + + _noActivitiesLabel.hidden = (_events.count > 0); + [self createGroupedEventDictionaryForEvents:_events]; + + [_tableView reloadData]; + }); + }]; +} + +- (NSDateComponents *)dateFromSelectedIndex:(NSInteger)index { + NSDateComponents *newComponents = [NSDateComponents new]; + newComponents.year = self.selectedDate.year; + newComponents.month = self.selectedDate.month; + newComponents.weekOfMonth = self.selectedDate.weekOfMonth; + newComponents.weekday = index + 1; + + NSDate *newDate = [_calendar dateFromComponents:newComponents]; + return [NSDateComponents ock_componentsWithDate:newDate calendar:_calendar]; +} + +- (NSDateComponents *)today { + return [NSDateComponents ock_componentsWithDate:[NSDate date] calendar:_calendar]; +} + +- (UIViewController *)detailViewControllerForActivity:(OCKCarePlanActivity *)activity { + OCKCareCardDetailViewController *detailViewController = [[OCKCareCardDetailViewController alloc] initWithIntervention:activity]; + return detailViewController; +} + +- (OCKCarePlanActivity *)activityForIndexPath:(NSIndexPath *)indexPath { + return _tableViewData[indexPath.section][indexPath.row].firstObject.activity; +} + +- (BOOL)delegateCustomizesRowSelection { + return self.delegate && [self.delegate respondsToSelector:@selector(careCardViewController:didSelectRowWithInterventionActivity:)]; +} + +- (void)updatePullToRefreshControl +{ + if (nil != _delegate && + [_delegate respondsToSelector:@selector(shouldEnablePullToRefreshInCareCardViewController:)] && + [_delegate shouldEnablePullToRefreshInCareCardViewController:self]) { + + _tableView.refreshControl = _refreshControl; + } else { + [_tableView.refreshControl endRefreshing]; + _tableView.refreshControl = nil; + } +} + +- (UIImage *)createCustomImageName:(NSString*)customImageName { + UIImage *customImageToReturn; + if (customImageName != nil) { + NSBundle *bundle = [NSBundle mainBundle]; + customImageToReturn = [UIImage imageNamed: customImageName inBundle:bundle compatibleWithTraitCollection:nil]; + } else { + OCKGlyphType defaultGlyph = OCKGlyphTypeHeart; + customImageToReturn = [[OCKGlyph glyphImageForType:defaultGlyph] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + } + + return customImageToReturn; +} + +- (void)createGroupedEventDictionaryForEvents:(NSArray *> *)events { + NSMutableDictionary *groupedEvents = [NSMutableDictionary new]; + NSMutableArray *groupArray = [NSMutableArray new]; + + for (NSArray *activityEvents in events) { + OCKCarePlanEvent *firstEvent = activityEvents.firstObject; + NSString *groupIdentifier = firstEvent.activity.groupIdentifier ? firstEvent.activity.groupIdentifier : _otherString; + + if (firstEvent.activity.optional) { + groupIdentifier = _optionalString; + } + + if (!_isGrouped) { + // Force only one grouping + groupIdentifier = _otherString; + } + + if (groupedEvents[groupIdentifier]) { + NSMutableArray *objects = [groupedEvents[groupIdentifier] mutableCopy]; + [objects addObject:activityEvents]; + groupedEvents[groupIdentifier] = objects; + } else { + NSMutableArray *objects = [[NSMutableArray alloc] initWithArray:activityEvents]; + groupedEvents[groupIdentifier] = @[objects]; + [groupArray addObject:groupIdentifier]; + } + } + + if (_isGrouped && _isSorted) { + + NSMutableArray *sortedKeys = [[groupedEvents.allKeys sortedArrayUsingSelector:@selector(compare:)] mutableCopy]; + if ([sortedKeys containsObject:_otherString]) { + [sortedKeys removeObject:_otherString]; + [sortedKeys addObject:_otherString]; + } + + if ([sortedKeys containsObject:_optionalString]) { + [sortedKeys removeObject:_optionalString]; + [sortedKeys addObject:_optionalString]; + } + + _sectionTitles = [sortedKeys copy]; + + } else { + + _sectionTitles = [groupArray mutableCopy]; + + } + + NSMutableArray *array = [NSMutableArray new]; + for (NSString *key in _sectionTitles) { + NSMutableArray *groupArray = [NSMutableArray new]; + NSArray *groupedEventsArray = groupedEvents[key]; + + if (_isSorted) { + + NSMutableDictionary *activitiesDictionary = [NSMutableDictionary new]; + for (NSArray *events in groupedEventsArray) { + NSString *activityTitle = events.firstObject.activity.title; + activitiesDictionary[activityTitle] = events; + } + + NSArray *sortedActivitiesKeys = [activitiesDictionary.allKeys sortedArrayUsingSelector:@selector(compare:)]; + for (NSString *activityKey in sortedActivitiesKeys) { + [groupArray addObject:activitiesDictionary[activityKey]]; + } + + [array addObject:groupArray]; + + } else { + + [array addObject:[groupedEventsArray mutableCopy]]; + + } + } + + _tableViewData = [array mutableCopy]; +} + + +#pragma mark - OCKWeekViewDelegate + +- (void)weekViewSelectionDidChange:(UIView *)weekView { + OCKWeekView *currentWeekView = (OCKWeekView *)weekView; + NSDateComponents *selectedDate = [self dateFromSelectedIndex:currentWeekView.selectedIndex]; + self.selectedDate = selectedDate; +} + +- (BOOL)weekViewCanSelectDayAtIndex:(NSUInteger)index { + NSDateComponents *today = [self today]; + NSDateComponents *selectedDate = [self dateFromSelectedIndex:index]; + return ![selectedDate isLaterThan:today]; +} + +#pragma mark - OCKCareCardCellDelegate + +- (void)careCardTableViewCell:(OCKCareCardTableViewCell *)cell didUpdateFrequencyofInterventionEvent:(OCKCarePlanEvent *)event { + _lastSelectedInterventionEvent = event; + _lastSelectedInterventionActivity = event.activity; + + if (self.delegate && + [self.delegate respondsToSelector:@selector(careCardViewController:didSelectButtonWithInterventionEvent:)]) { + [self.delegate careCardViewController:self didSelectButtonWithInterventionEvent:event]; + } + + BOOL shouldHandleEventCompletion = YES; + if (self.delegate && + [self.delegate respondsToSelector:@selector(careCardViewController:shouldHandleEventCompletionForActivity:)]) { + shouldHandleEventCompletion = [self.delegate careCardViewController:self shouldHandleEventCompletionForActivity:event.activity]; + } + + if (shouldHandleEventCompletion) { + OCKCarePlanEventState state = (event.state == OCKCarePlanEventStateCompleted) ? OCKCarePlanEventStateNotCompleted : OCKCarePlanEventStateCompleted; + + [self.store updateEvent:event + withResult:nil + state:state + completion:^(BOOL success, OCKCarePlanEvent * _Nonnull event, NSError * _Nonnull error) { + NSAssert(success, error.localizedDescription); + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray *events = [cell.interventionEvents mutableCopy]; + [events replaceObjectAtIndex:event.occurrenceIndexOfDay withObject:event]; + cell.interventionEvents = events; + }); + }]; + } +} + +- (void)careCardTableViewCell:(OCKCareCardTableViewCell *)cell didSelectInterventionActivity:(OCKCarePlanActivity *)activity { + NSIndexPath *indexPath = [_tableView indexPathForCell:cell]; + OCKCarePlanActivity *selectedActivity = [self activityForIndexPath:indexPath]; + _lastSelectedInterventionActivity = selectedActivity; + + if ([self delegateCustomizesRowSelection]) { + [self.delegate careCardViewController:self didSelectRowWithInterventionActivity:selectedActivity]; + } else { + [self.navigationController pushViewController:[self detailViewControllerForActivity:selectedActivity] animated:YES]; + } +} + + +#pragma mark - OCKCarePlanStoreDelegate + +- (void)carePlanStore:(OCKCarePlanStore *)store didReceiveUpdateOfEvent:(OCKCarePlanEvent *)event { + for (int i = 0; i < _tableViewData.count; i++) { + NSMutableArray *> *groupedEvents = _tableViewData[i]; + + for (int j = 0; j < groupedEvents.count; j++) { + NSMutableArray *events = groupedEvents[j]; + + if ([events.firstObject.activity.identifier isEqualToString:event.activity.identifier]) { + if (events[event.occurrenceIndexOfDay].numberOfDaysSinceStart == event.numberOfDaysSinceStart) { + [events replaceObjectAtIndex:event.occurrenceIndexOfDay withObject:event]; + _tableViewData[i][j] = events; + + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:j inSection:i]; + OCKCareCardTableViewCell *cell = [_tableView cellForRowAtIndexPath:indexPath]; + cell.interventionEvents = events; + } + break; + } + + } + + } + + if ([event.date isInSameWeekAsDate: self.selectedDate]) { + } +} + +- (void)carePlanStoreActivityListDidChange:(OCKCarePlanStore *)store { + [self fetchEvents]; +} + + +#pragma mark - UIPageViewControllerDelegate + +- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed { + if (completed) { + + NSDateComponents *components = [NSDateComponents new]; + + NSDate *newDate = [_calendar dateByAddingComponents:components toDate:[_calendar dateFromComponents:self.selectedDate] options:0]; + + self.selectedDate = [NSDateComponents ock_componentsWithDate:newDate calendar:_calendar]; + } +} + +#pragma mark - UITableViewDelegate + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + NSString *sectionTitle = _sectionTitles[section]; + if ([sectionTitle isEqualToString:_otherString] && (_sectionTitles.count == 1 || (_sectionTitles.count == 2 && [_sectionTitles containsObject:_optionalString]))) { + sectionTitle = @""; + } + return sectionTitle; +} + +- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath { + return NO; +} + +#pragma mark - UITableViewDataSource + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return _tableViewData.count; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return _tableViewData[section].count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + static NSString *CellIdentifier = @"CareCardCell"; + OCKCareCardTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + if (!cell) { + cell = [[OCKCareCardTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault + reuseIdentifier:CellIdentifier]; + } + cell.interventionEvents = _tableViewData[indexPath.section][indexPath.row]; + cell.delegate = self; + return cell; +} + + +#pragma mark - UIViewControllerPreviewingDelegate + +- (UIViewController *)previewingContext:(id )previewingContext viewControllerForLocation:(CGPoint)location { + NSIndexPath *indexPath = [_tableView indexPathForRowAtPoint:location]; + CGRect headerFrame = [_tableView headerViewForSection:0].frame; + + if (indexPath && + !CGRectContainsPoint(headerFrame, location) && + ![self delegateCustomizesRowSelection]) { + CGRect cellFrame = [_tableView cellForRowAtIndexPath:indexPath].frame; + previewingContext.sourceRect = cellFrame; + return [self detailViewControllerForActivity:[self activityForIndexPath:indexPath]]; + } + + return nil; +} + +- (void)previewingContext:(id )previewingContext commitViewController:(UIViewController *)viewControllerToCommit { + [self.navigationController pushViewController:viewControllerToCommit animated:YES]; +} + +@end + + diff --git a/CareKit/CareKit.h b/CareKit/CareKit.h index bd25ad341..3c50c597b 100644 --- a/CareKit/CareKit.h +++ b/CareKit/CareKit.h @@ -50,6 +50,7 @@ // Care Card #import +#import #import // Care Content diff --git a/CareKit/Common/CustomHeaderView.h b/CareKit/Common/CustomHeaderView.h new file mode 100644 index 000000000..8481def86 --- /dev/null +++ b/CareKit/Common/CustomHeaderView.h @@ -0,0 +1,22 @@ +// +// CustomHeaderView.h +// CareKit +// +// Created by Damian Dara on 15/1/19. +// Copyright © 2019 carekit.org. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface CustomHeaderView : UIView + +@property (copy, nonatomic) NSString *title; +@property (copy, nonatomic) NSString *date; +@property (copy, nonatomic) UIColor *tintColor; + +@end + + +NS_ASSUME_NONNULL_END diff --git a/CareKit/Common/CustomHeaderView.m b/CareKit/Common/CustomHeaderView.m new file mode 100644 index 000000000..5759edec2 --- /dev/null +++ b/CareKit/Common/CustomHeaderView.m @@ -0,0 +1,95 @@ +// +// CustomHeaderView.m +// CareKit +// +// Created by Damian Dara on 15/1/19. +// Copyright © 2019 carekit.org. All rights reserved. +// + +#import "OCKLabel.h" +#import "CustomHeaderView.h" + +@implementation CustomHeaderView { + OCKLabel *_titleLabel; + OCKLabel *_dateLabel; + UIView *_separatorView; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (self) { + [self initViews]; + [self initConstraints]; + } + return self; +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + [self initViews]; + [self initConstraints]; + } + return self; +} + +- (void)setTitle:(NSString *)title { + _title = title; + _titleLabel.text = title; +} + +- (void)setDate:(NSString *)date { + _date = date; + _dateLabel.text = date; +} + +- (void)setTintColor:(UIColor *)tintColor { + _tintColor = tintColor; + _dateLabel.textColor = tintColor; +} + +- (void)initViews { + self.backgroundColor = [UIColor whiteColor]; + + _titleLabel = [OCKLabel new]; + _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; + _titleLabel.font = [UIFont systemFontOfSize:26.0 weight:UIFontWeightBold]; + [self addSubview:_titleLabel]; + + _dateLabel = [OCKLabel new]; + _dateLabel.translatesAutoresizingMaskIntoConstraints = NO; + _dateLabel.textColor = self.tintColor; + _dateLabel.font = [UIFont systemFontOfSize:12.0 weight:UIFontWeightRegular]; + _dateLabel.textAlignment = NSTextAlignmentRight; + [self addSubview:_dateLabel]; + + _separatorView = [UIView new]; + _separatorView.translatesAutoresizingMaskIntoConstraints = NO; + _separatorView.backgroundColor = [UIColor lightGrayColor]; + [self addSubview:_separatorView]; +} + +- (void)initConstraints { + NSMutableArray *constraints = [NSMutableArray arrayWithArray:@[ + // Title label + [NSLayoutConstraint constraintWithItem:_titleLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1.0 constant:20.0], + [NSLayoutConstraint constraintWithItem:_titleLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:50], + [NSLayoutConstraint constraintWithItem:_titleLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-8], + [NSLayoutConstraint constraintWithItem:_titleLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:0], + + // Date label + [NSLayoutConstraint constraintWithItem:_dateLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:_titleLabel attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:7], + [NSLayoutConstraint constraintWithItem:_dateLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:-20], + [NSLayoutConstraint constraintWithItem:_dateLabel attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:_titleLabel attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0], + [NSLayoutConstraint constraintWithItem:_dateLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:0], + + // Separator view + [NSLayoutConstraint constraintWithItem:_separatorView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0], + [NSLayoutConstraint constraintWithItem:_separatorView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0.0], + [NSLayoutConstraint constraintWithItem:_separatorView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0.0], + [NSLayoutConstraint constraintWithItem:_separatorView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:0.5] + ]]; + [NSLayoutConstraint activateConstraints:constraints]; +} + +@end