diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29c998597e..cf2ea07a7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,14 +6,14 @@ permissions: contents: read jobs: build: - runs-on: macos-26 + runs-on: xcode-27 env: - # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md - xcode: '/Applications/Xcode_26.2.app' - destination_ios: 'platform=iOS Simulator,OS=26.2,name=iPhone 17' + # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-arm64-Readme.md + xcode: '/Applications/Xcode_27.0.app' + destination_ios: 'platform=iOS Simulator,OS=27.0,name=iPhone 17' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: token: ${{ secrets.CAREEVOLUTIONBUILD_TOKEN }} submodules: 'recursive' diff --git a/Cartfile.resolved b/Cartfile.resolved index da07c63098..91dec9ef89 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1 +1 @@ -github "CareEvolution/MarkdownAttributedString" "1.0.4" +github "CareEvolution/MarkdownAttributedString" "1.0.5" diff --git a/Carthage/Checkouts/MarkdownAttributedString b/Carthage/Checkouts/MarkdownAttributedString index e9dd6fe902..6fc2f19792 160000 --- a/Carthage/Checkouts/MarkdownAttributedString +++ b/Carthage/Checkouts/MarkdownAttributedString @@ -1 +1 @@ -Subproject commit e9dd6fe9026acb85bcf89c12843342b8df2b8e0f +Subproject commit 6fc2f197928812d64938ac6e34135cc2c91108aa diff --git a/ORK1Kit/ORK1Kit.xcodeproj/xcshareddata/xcschemes/ORK1Kit.xcscheme b/ORK1Kit/ORK1Kit.xcodeproj/xcshareddata/xcschemes/ORK1Kit.xcscheme index 3231f6b58c..aae8d1558f 100644 --- a/ORK1Kit/ORK1Kit.xcodeproj/xcshareddata/xcschemes/ORK1Kit.xcscheme +++ b/ORK1Kit/ORK1Kit.xcodeproj/xcshareddata/xcschemes/ORK1Kit.xcscheme @@ -1,6 +1,6 @@ )task taskRunUUID:(NSUUID *)task self.taskRunUUID = taskRunUUID; - [self.childNavigationController.navigationBar setShadowImage:[UIImage new]]; self.hairline = [self findHairlineViewUnder:self.childNavigationController.navigationBar]; self.hairline.alpha = 0.0f; self.childNavigationController.toolbar.clipsToBounds = YES; + if (@available(iOS 26.0, *)) { + // ResearchKit views render poorly with translucent navigation bars on iOS 26 + self.childNavigationController.navigationBar.backgroundColor = [UIColor whiteColor]; + } + // Ensure taskRunUUID has non-nil valuetaskRunUUID (void)[self taskRunUUID]; self.restorationClass = [ORK1TaskViewController class]; @@ -1027,6 +1031,11 @@ - (void)showViewController:(ORK1StepViewController *)viewController goForward:(B // remove any progress strongSelf.pageViewController.navigationItem.titleView = nil; strongSelf.pageViewController.navigationItem.title = nil; + strongSelf.progressView.hidden = YES; + if (@available(iOS 26.0, *)) { + // Reset safe area insets that are adjusted when progress view is visible. + strongSelf.pageViewController.additionalSafeAreaInsets = UIEdgeInsetsZero; + } } else { ORK1OrderedTask *orderedTask = (ORK1OrderedTask *)strongSelf.task; if (orderedTask.progressIndicatorStyle == CEVRK1TaskProgressIndicatorStyleBar) { @@ -1037,16 +1046,17 @@ - (void)showViewController:(ORK1StepViewController *)viewController goForward:(B } else { // Linear calculatedProgress = (float)taskProgress.current / (float)taskProgress.total; } - [strongSelf.progressView setProgress:calculatedProgress withTheme:[CEVRK1Theme themeForElement:strongSelf.currentStepViewController]]; - strongSelf.pageViewController.navigationItem.titleView = strongSelf.progressView; - - // for UITesting, we will add a title that will not display, but should appear via accessibility - NSUInteger progressPercent = (NSUInteger)(calculatedProgress * 100); - strongSelf.pageViewController.navigationItem.title = [NSString stringWithFormat:@"ProgressBar:%@", @(progressPercent)]; - + [strongSelf.progressView setProgress:calculatedProgress withTheme:[CEVRK1Theme themeForElement:strongSelf.currentStepViewController] animated:animated]; + [strongSelf configureProgressView]; + strongSelf.progressView.hidden = NO; } else { strongSelf.pageViewController.navigationItem.titleView = nil; strongSelf.pageViewController.navigationItem.title = [NSString localizedStringWithFormat:ORK1LocalizedString(@"STEP_PROGRESS_FORMAT", nil) ,ORK1LocalizedStringFromNumber(@(taskProgress.current)), ORK1LocalizedStringFromNumber(@(taskProgress.total))]; + strongSelf.progressView.hidden = YES; + if (@available(iOS 26.0, *)) { + // Reset safe area insets that are adjusted when progress view is visible. + strongSelf.pageViewController.additionalSafeAreaInsets = UIEdgeInsetsZero; + } } } } @@ -1058,6 +1068,52 @@ - (void)showViewController:(ORK1StepViewController *)viewController goForward:(B }]; } +- (void)configureProgressView { + if (@available(iOS 26.0, *)) { + self.pageViewController.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever; + self.pageViewController.navigationItem.titleView = [UIView new]; + +#if DEBUG + // for UITesting, we will add a title that will not display (suppressed by the empty + // titleView above), but should appear via accessibility + NSUInteger progressPercent = (NSUInteger)(self.progressView.progress * 100); + self.pageViewController.navigationItem.title = [NSString stringWithFormat:@"ProgressBar:%@", @(progressPercent)]; +#else + self.pageViewController.navigationItem.title = nil; +#endif + + CEVRK1NavigationBarProgressView *progressView = self.progressView; + UINavigationBar *navigationBar = self.childNavigationController.navigationBar; + if (!progressView.superview) { + navigationBar.opaque = YES; + navigationBar.translucent = NO; + + progressView.translatesAutoresizingMaskIntoConstraints = NO; + [navigationBar addSubview:progressView]; + [NSLayoutConstraint activateConstraints:@[ + [progressView.topAnchor constraintEqualToAnchor:navigationBar.bottomAnchor], + [progressView.leadingAnchor constraintEqualToAnchor:navigationBar.safeAreaLayoutGuide.leadingAnchor constant:10], + [progressView.trailingAnchor constraintEqualToAnchor:navigationBar.safeAreaLayoutGuide.trailingAnchor constant:-10] + ]]; + } + + // Scoped to navigationBar rather than self.view: progressView's constraints are entirely + // self-contained within the nav bar subtree, and self.view's subtree also contains whatever + // step content view is currently presented. Forcing that to lay out early (before it's + // necessarily attached to a window, mid-transition) can trip pre-existing updateConstraints + // bugs in step views (e.g. ORK1ImageCaptureView) that assume they're only laid out once + // installed in a window. + [navigationBar layoutIfNeeded]; + self.pageViewController.additionalSafeAreaInsets = UIEdgeInsetsMake(progressView.bounds.size.height, 0, 0, 0); + } else { + self.pageViewController.navigationItem.titleView = self.progressView; + + // for UITesting, we will add a title that will not display, but should appear via accessibility + NSUInteger progressPercent = (NSUInteger)(self.progressView.progress * 100); + self.pageViewController.navigationItem.title = [NSString stringWithFormat:@"ProgressBar:%@", @(progressPercent)]; + } +} + - (BOOL)shouldPresentStep:(ORK1Step *)step { BOOL shouldPresent = (step != nil); diff --git a/ORK1Kit/ORK1Kit/Common/ORK1VideoCaptureStepViewController.m b/ORK1Kit/ORK1Kit/Common/ORK1VideoCaptureStepViewController.m index 86bf0855ab..4b93c2469a 100644 --- a/ORK1Kit/ORK1Kit/Common/ORK1VideoCaptureStepViewController.m +++ b/ORK1Kit/ORK1Kit/Common/ORK1VideoCaptureStepViewController.m @@ -83,10 +83,22 @@ - (void)setUpConstraints { NSDictionary *views = @{ @"videoCaptureView": _videoCaptureView }; _videoCaptureView.translatesAutoresizingMaskIntoConstraints = NO; - [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[videoCaptureView]|" - options:NSLayoutFormatDirectionLeadingToTrailing - metrics:nil - views:views]]; + // Top edge: on iOS 26+, pin to the safe area rather than the raw view edge, so this full-bleed + // view still reserves space for CEVRK1's navigation-bar progress view (added via + // additionalSafeAreaInsets in ORK1TaskViewController's configureProgressView) instead of + // rendering underneath it. Leading/trailing/bottom stay pinned to the raw edges (unchanged, + // still full-bleed there) — the nav bar is already opaque on iOS 26+ regardless of progress + // style (see ORK1TaskViewController commonInit), so this is a no-op except when the extra + // progress-bar inset is actually present. iOS <26 is left on the original raw-edge pin. + if (@available(iOS 26.0, *)) { + [constraints addObject:[_videoCaptureView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor]]; + } else { + [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[videoCaptureView]" + options:NSLayoutFormatDirectionLeadingToTrailing + metrics:nil + views:views]]; + } + [constraints addObject:[_videoCaptureView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor]]; [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[videoCaptureView]|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil diff --git a/ORK1Kit/ORK1Kit/Common/ORK1WebViewStepViewController.m b/ORK1Kit/ORK1Kit/Common/ORK1WebViewStepViewController.m index 1d996d3838..fffddfa556 100644 --- a/ORK1Kit/ORK1Kit/Common/ORK1WebViewStepViewController.m +++ b/ORK1Kit/ORK1Kit/Common/ORK1WebViewStepViewController.m @@ -154,9 +154,27 @@ - (ORK1StepResult *)result { - (void)viewDidLoad { [super viewDidLoad]; - _webView.frame = self.view.bounds; - _webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self.view addSubview:_webView]; + _webView.translatesAutoresizingMaskIntoConstraints = NO; + + // Top edge: on iOS 26+, pin to the safe area rather than the raw view edge, so this full-bleed + // view still reserves space for CEVRK1's navigation-bar progress view (added via + // additionalSafeAreaInsets in ORK1TaskViewController's configureProgressView) instead of + // rendering underneath it. Leading/trailing/bottom stay pinned to the raw edges (unchanged, + // still full-bleed there) — the nav bar is already opaque on iOS 26+ regardless of progress + // style (see ORK1TaskViewController commonInit), so this is a no-op except when the extra + // progress-bar inset is actually present. iOS <26 is left on the original raw-edge pin + // (previously achieved via frame = self.view.bounds; now via an equivalent top anchor). + NSLayoutYAxisAnchor *topAnchor = self.view.topAnchor; + if (@available(iOS 26.0, *)) { + topAnchor = self.view.safeAreaLayoutGuide.topAnchor; + } + [NSLayoutConstraint activateConstraints:@[ + [_webView.topAnchor constraintEqualToAnchor:topAnchor], + [_webView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], + [_webView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [_webView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + ]]; } - (void)viewDidDisappear:(BOOL)animated { diff --git a/ORK1Kit/ORK1Kit/Common/UIBarButtonItem+ORK1BarButtonItem.m b/ORK1Kit/ORK1Kit/Common/UIBarButtonItem+ORK1BarButtonItem.m index f1586fff0a..1143d81d62 100644 --- a/ORK1Kit/ORK1Kit/Common/UIBarButtonItem+ORK1BarButtonItem.m +++ b/ORK1Kit/ORK1Kit/Common/UIBarButtonItem+ORK1BarButtonItem.m @@ -37,19 +37,9 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE @implementation UIBarButtonItem (ORK1BarButtonItem) + (UIBarButtonItem *)ork_backBarButtonItemWithTarget:(id)target action:(SEL)selector { - NSString *regularImageName = @"arrowLeft"; - NSString *landscapeImageName = @"arrowLeftLandscape"; - - if ([UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft ) { - regularImageName = @"arrowRight"; - landscapeImageName = @"arrowRightLandscape"; - } - - UIImage *image = [UIImage imageNamed:regularImageName inBundle:ORK1Bundle() compatibleWithTraitCollection:nil]; - UIImage *landscapeImage = [UIImage imageNamed:landscapeImageName inBundle:ORK1Bundle() compatibleWithTraitCollection:nil]; + UIImage *image = [UIImage systemImageNamed:@"chevron.backward"]; UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:image - landscapeImagePhone:landscapeImage - style:UIBarButtonItemStyleDone + style:UIBarButtonItemStylePlain target:target action:selector]; item.accessibilityLabel = ORK1LocalizedString(@"AX_BUTTON_BACK", nil); diff --git a/ORK1Kit/ORK1Kit/Consent/ORK1ConsentReviewController.m b/ORK1Kit/ORK1Kit/Consent/ORK1ConsentReviewController.m index f493f05c90..026e5785ba 100644 --- a/ORK1Kit/ORK1Kit/Consent/ORK1ConsentReviewController.m +++ b/ORK1Kit/ORK1Kit/Consent/ORK1ConsentReviewController.m @@ -60,7 +60,7 @@ - (instancetype)initWithHTML:(NSString *)html delegate:(id *_accessibilityCustomActions; + UIBarButtonItem *_doneButton; } - (instancetype)init { @@ -84,18 +85,32 @@ - (void)addAccessoryViewWithDoneButton { UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; - UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] - initWithBarButtonSystemItem:UIBarButtonSystemItemDone - target:self action:@selector(keyboardAccessoryViewDoneButtonPressed)]; + UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithTitle:ORKLocalizedString(@"BUTTON_DONE", nil) style:UIBarButtonItemStylePlain target:self action:@selector(keyboardAccessoryViewDoneButtonPressed)]; + + if (@available(iOS 26.0, *)) { + doneButton.hidesSharedBackground = YES; + accessoryViewWithDoneButton.backgroundColor = ORKColor(ORKToolBarTintColorKey); + doneButton.tintColor = ORKViewTintColor(self); + } else { + [accessoryViewWithDoneButton setBarTintColor:ORKColor(ORKBackgroundColorKey)]; + } + accessoryViewWithDoneButton.items = @[flexibleSpace, doneButton]; - [accessoryViewWithDoneButton setBarTintColor:ORKColor(ORKBackgroundColorKey)]; self.inputAccessoryView = accessoryViewWithDoneButton; + _doneButton = doneButton; } - (void)keyboardAccessoryViewDoneButtonPressed { [self resignFirstResponder]; } +- (void)didMoveToWindow { + [super didMoveToWindow]; + if (@available(iOS 26.0, *)) { + _doneButton.tintColor = ORKViewTintColor(self); + } +} + - (void)layoutSubviews { [super layoutSubviews]; // Setting the frame directly causes a layout error on a form step (it looks like an iOS bug, as setting the frame should produce the same effect as setting the bounds and the center) diff --git a/ResearchKit/Common/ORKTaskViewController.m b/ResearchKit/Common/ORKTaskViewController.m index c136f79a0c..300d44af58 100644 --- a/ResearchKit/Common/ORKTaskViewController.m +++ b/ResearchKit/Common/ORKTaskViewController.m @@ -456,7 +456,11 @@ - (UIBarButtonItem *)rightBarItemWithText:(NSString *)text { } [progressLabelNavigationItem setText:text]; - return [[UIBarButtonItem alloc] initWithCustomView:progressLabelNavigationItem]; + UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:progressLabelNavigationItem]; + if (@available(iOS 26.0, *)) { + item.hidesSharedBackground = YES; + } + return item; } - (void)requestHealthStoreAccessWithReadTypes:(NSSet *)readTypes diff --git a/ResearchKit/Common/UIBarButtonItem+ORKBarButtonItem.m b/ResearchKit/Common/UIBarButtonItem+ORKBarButtonItem.m index df64f44029..24f31e2719 100644 --- a/ResearchKit/Common/UIBarButtonItem+ORKBarButtonItem.m +++ b/ResearchKit/Common/UIBarButtonItem+ORKBarButtonItem.m @@ -37,19 +37,9 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE @implementation UIBarButtonItem (ORKBarButtonItem) + (UIBarButtonItem *)ork_backBarButtonItemWithTarget:(id)target action:(SEL)selector { - NSString *regularImageName = @"arrowLeft"; - NSString *landscapeImageName = @"arrowLeftLandscape"; - - if ([UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft ) { - regularImageName = @"arrowRight"; - landscapeImageName = @"arrowRightLandscape"; - } - - UIImage *image = [UIImage imageNamed:regularImageName inBundle:ORKBundle() compatibleWithTraitCollection:nil]; - UIImage *landscapeImage = [UIImage imageNamed:landscapeImageName inBundle:ORKBundle() compatibleWithTraitCollection:nil]; + UIImage *image = [UIImage systemImageNamed:@"chevron.backward"]; UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:image - landscapeImagePhone:landscapeImage - style:UIBarButtonItemStyleDone + style:UIBarButtonItemStylePlain target:target action:selector]; item.accessibilityLabel = ORKLocalizedString(@"AX_BUTTON_BACK", nil); diff --git a/Testing/ORKTest/ORKTest.xcodeproj/xcshareddata/xcschemes/ORKTest.xcscheme b/Testing/ORKTest/ORKTest.xcodeproj/xcshareddata/xcschemes/ORKTest.xcscheme index 7b06edaa5e..d5d97f6740 100644 --- a/Testing/ORKTest/ORKTest.xcodeproj/xcshareddata/xcschemes/ORKTest.xcscheme +++ b/Testing/ORKTest/ORKTest.xcodeproj/xcshareddata/xcschemes/ORKTest.xcscheme @@ -1,6 +1,6 @@ @@ -88,7 +87,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "BA87DBAF20B80FFD004ACAFF" BuildableName = "ORKParkinsonStudy WatchKit App.app" - BlueprintName = "ORKParkinsonStudy WatchKit App" ReferencedContainer = "container:ORKParkinsonStudy.xcodeproj"> diff --git a/samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj/xcshareddata/xcschemes/ORKParkinsonStudy WatchKit App (Notification).xcscheme b/samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj/xcshareddata/xcschemes/ORKParkinsonStudy WatchKit App (Notification).xcscheme index 3b3dae5626..a79b54de50 100644 --- a/samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj/xcshareddata/xcschemes/ORKParkinsonStudy WatchKit App (Notification).xcscheme +++ b/samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj/xcshareddata/xcschemes/ORKParkinsonStudy WatchKit App (Notification).xcscheme @@ -71,7 +71,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "BA87DBAF20B80FFD004ACAFF" BuildableName = "ORKParkinsonStudy WatchKit App.app" - BlueprintName = "ORKParkinsonStudy WatchKit App" ReferencedContainer = "container:ORKParkinsonStudy.xcodeproj"> @@ -90,7 +89,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "BA87DBAF20B80FFD004ACAFF" BuildableName = "ORKParkinsonStudy WatchKit App.app" - BlueprintName = "ORKParkinsonStudy WatchKit App" ReferencedContainer = "container:ORKParkinsonStudy.xcodeproj"> diff --git a/samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj/xcshareddata/xcschemes/ORKParkinsonStudy WatchKit App.xcscheme b/samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj/xcshareddata/xcschemes/ORKParkinsonStudy WatchKit App.xcscheme index 7222a21df3..e4ce87b148 100644 --- a/samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj/xcshareddata/xcschemes/ORKParkinsonStudy WatchKit App.xcscheme +++ b/samples/ORKParkinsonStudy/ORKParkinsonStudy.xcodeproj/xcshareddata/xcschemes/ORKParkinsonStudy WatchKit App.xcscheme @@ -69,7 +69,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "BA87DBAF20B80FFD004ACAFF" BuildableName = "ORKParkinsonStudy WatchKit App.app" - BlueprintName = "ORKParkinsonStudy WatchKit App" ReferencedContainer = "container:ORKParkinsonStudy.xcodeproj"> @@ -86,7 +85,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "BA87DBAF20B80FFD004ACAFF" BuildableName = "ORKParkinsonStudy WatchKit App.app" - BlueprintName = "ORKParkinsonStudy WatchKit App" ReferencedContainer = "container:ORKParkinsonStudy.xcodeproj">