From e27ed0286686ad4f29ac75efe0d8a2b4fc7df2c0 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Sun, 12 Jul 2026 00:34:31 +0900 Subject: [PATCH 1/3] Guard iOS 26-only layout symbols and harden the corner-radius lookup UIViewLayoutRegion is now compiled out below the iOS 26 SDK, matching the #ifdef discipline used in TOCropToolbar, so the library builds in Xcode 16 again. The private corner-radius KVC read falls back to a sensible constant instead of raising if the key ever disappears. --- Objective-C/TOCropViewController/TOCropViewController.m | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Objective-C/TOCropViewController/TOCropViewController.m b/Objective-C/TOCropViewController/TOCropViewController.m index c00e47d4..78ffacd9 100644 --- a/Objective-C/TOCropViewController/TOCropViewController.m +++ b/Objective-C/TOCropViewController/TOCropViewController.m @@ -275,6 +275,7 @@ - (CGRect)frameForToolbarWithVerticalLayout:(BOOL)verticalLayout { UIEdgeInsets insets = self.statusBarSafeInsets; // fix: On iOS 26, overlay with iPadOS windowingControl area. +#if defined(__IPHONE_26_0) if (@available(iOS 26.0, *)) { if (!verticalLayout) { #if __IPHONE_OS_VERSION_MAX_ALLOWED < 180000 @@ -286,6 +287,7 @@ - (CGRect)frameForToolbarWithVerticalLayout:(BOOL)verticalLayout { #endif } } +#endif CGRect frame = CGRectZero; if (!verticalLayout) { // In landscape laying out toolbar to the left @@ -328,13 +330,18 @@ - (CGRect)frameForToolbarWithVerticalLayout:(BOOL)verticalLayout { } #if !TARGET_OS_VISION + // Fall back to a radius matching current-generation devices in case + // the private key is ever renamed or removed + CGFloat cornerRadius = 44.0f; const char *components[] = {"Radius", "Corner", "display", "_"}; NSString *selectorName = @""; for (NSInteger i = 3; i >= 0; i--) { selectorName = [selectorName stringByAppendingString:[NSString stringWithCString:components[i] encoding:NSUTF8StringEncoding]]; } - const CGFloat cornerRadius = [[UIScreen.mainScreen valueForKey:selectorName] floatValue]; + @try { + cornerRadius = [[UIScreen.mainScreen valueForKey:selectorName] floatValue]; + } @catch (NSException *exception) { } #else const CGFloat cornerRadius = 64.0f; #endif From 32186a858f8564055d3ae1017baf3dc6f7a0a0c4 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Mon, 27 Jul 2026 02:24:26 +0900 Subject: [PATCH 2/3] Address review findings in the iOS 26 toolbar work - Setting showOnlyIcons to false on iOS 26 hid the icon buttons without any text ones to replace them, leaving no way to commit or cancel the crop. The property is now ignored wherever the text buttons don't exist, and the documentation says so on the toolbar and the controller - Hiding every action button left an empty glass capsule stranded at the frame it had when the last one was visible - The private display-corner-radius lookup runs once via dispatch_once and logs when it falls back, rather than being re-read on every layout pass --- .../TOCropViewController.h | 2 +- .../TOCropViewController.m | 30 +++++++++++-------- .../Views/TOCropToolbar.h | 9 ++++-- .../Views/TOCropToolbar.m | 14 +++++++++ 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/Objective-C/TOCropViewController/TOCropViewController.h b/Objective-C/TOCropViewController/TOCropViewController.h index 77aaf485..fc1f485f 100644 --- a/Objective-C/TOCropViewController/TOCropViewController.h +++ b/Objective-C/TOCropViewController/TOCropViewController.h @@ -176,7 +176,7 @@ /** If true, button icons are visible in portairt instead button text. - Default is NO. + Default is NO. Has no effect on iOS 26 and up, where the toolbar is always icon-only. */ @property (nonatomic, assign) BOOL showOnlyIcons; diff --git a/Objective-C/TOCropViewController/TOCropViewController.m b/Objective-C/TOCropViewController/TOCropViewController.m index 78ffacd9..1b4aa545 100644 --- a/Objective-C/TOCropViewController/TOCropViewController.m +++ b/Objective-C/TOCropViewController/TOCropViewController.m @@ -330,18 +330,24 @@ - (CGRect)frameForToolbarWithVerticalLayout:(BOOL)verticalLayout { } #if !TARGET_OS_VISION - // Fall back to a radius matching current-generation devices in case - // the private key is ever renamed or removed - CGFloat cornerRadius = 44.0f; - const char *components[] = {"Radius", "Corner", "display", "_"}; - NSString *selectorName = @""; - for (NSInteger i = 3; i >= 0; i--) { - selectorName = [selectorName stringByAppendingString:[NSString stringWithCString:components[i] - encoding:NSUTF8StringEncoding]]; - } - @try { - cornerRadius = [[UIScreen.mainScreen valueForKey:selectorName] floatValue]; - } @catch (NSException *exception) { } + // Look the value up only once since it can't change for the life of the + // process, and fall back to a radius matching current-generation devices + // in case the private key is ever renamed or removed + static CGFloat cornerRadius = 44.0f; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + const char *components[] = {"Radius", "Corner", "display", "_"}; + NSString *selectorName = @""; + for (NSInteger i = 3; i >= 0; i--) { + selectorName = [selectorName stringByAppendingString:[NSString stringWithCString:components[i] + encoding:NSUTF8StringEncoding]]; + } + @try { + cornerRadius = [[UIScreen.mainScreen valueForKey:selectorName] floatValue]; + } @catch (NSException *exception) { + NSLog(@"TOCropViewController: Unable to read the display corner radius. Falling back to a default value."); + } + }); #else const CGFloat cornerRadius = 64.0f; #endif diff --git a/Objective-C/TOCropViewController/Views/TOCropToolbar.h b/Objective-C/TOCropViewController/Views/TOCropToolbar.h index f06be1e0..4231974c 100644 --- a/Objective-C/TOCropViewController/Views/TOCropToolbar.h +++ b/Objective-C/TOCropViewController/Views/TOCropToolbar.h @@ -39,21 +39,24 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) UIEdgeInsets backgroundViewOutsets; /* The 'Done' buttons to commit the crop. The text button is displayed - in portrait mode and the icon one, in landscape. */ + in portrait mode and the icon one, in landscape. + The text button is nil on iOS 26 and up, where the toolbar is always icon-only. */ @property (nonatomic, strong, readonly) UIButton *doneTextButton; @property (nonatomic, strong, readonly) UIButton *doneIconButton; @property (nonatomic, copy) NSString *doneTextButtonTitle; @property (null_resettable, nonatomic, copy) UIColor *doneButtonColor; /* The 'Cancel' buttons to cancel the crop. The text button is displayed - in portrait mode and the icon one, in landscape. */ + in portrait mode and the icon one, in landscape. + The text button is nil on iOS 26 and up, where the toolbar is always icon-only. */ @property (nonatomic, strong, readonly) UIButton *cancelTextButton; @property (nonatomic, strong, readonly) UIButton *cancelIconButton; @property (nonatomic, readonly) UIView *visibleCancelButton; @property (nonatomic, copy) NSString *cancelTextButtonTitle; @property (nullable, nonatomic, copy) UIColor *cancelButtonColor; -/* Show the tick and cross buttons instead of 'Done' and 'Cancel'. */ +/* Show the tick and cross buttons instead of 'Done' and 'Cancel'. + Always YES, and not settable, on iOS 26 and up. */ @property (nonatomic, assign) BOOL showOnlyIcons API_DEPRECATED("iOS 26 uses icons only", ios(7.0, 18.0)); /* The cropper control buttons */ diff --git a/Objective-C/TOCropViewController/Views/TOCropToolbar.m b/Objective-C/TOCropViewController/Views/TOCropToolbar.m index 3f4d5e02..65b18e30 100644 --- a/Objective-C/TOCropViewController/Views/TOCropToolbar.m +++ b/Objective-C/TOCropViewController/Views/TOCropToolbar.m @@ -337,6 +337,12 @@ - (void)layoutSubviews { // The convenience method for calculating button's frame inside of the container rect - (void)layoutToolbarButtons:(NSArray *)buttons withSameButtonSize:(CGSize)size inContainerRect:(CGRect)containerRect horizontally:(BOOL)horizontally { + // With no buttons to hold, collapse the glass container instead of leaving an + // empty capsule stranded at the frame it had when the last button was visible + if (@available(iOS 26.0, *)) { + _glassView.hidden = (buttons.count == 0); + } + if (!buttons.count) { return; } @@ -495,6 +501,14 @@ - (CGRect)doneButtonFrame { } - (void)setShowOnlyIcons:(BOOL)showOnlyIcons { + // The text variants of the Done and Cancel buttons aren't created at all on iOS 26 + // and up, where the toolbar is permanently icon-only. Honouring a NO in that case + // would hide the icon buttons with nothing to replace them, leaving no way to + // commit or cancel the crop at all. + if (_doneTextButton == nil || _cancelTextButton == nil) { + return; + } + if (_showOnlyIcons == showOnlyIcons) return; From abb76a7d33f6f9b79878a310141d17767735f18b Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Tue, 28 Jul 2026 00:39:58 +0900 Subject: [PATCH 3/3] Note the iOS 26 toolbar fixes in the changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43049801..f6ab4c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ x.y.z Release Notes (yyyy-MM-dd) - Touch cancellation from incoming calls or system alerts left the crop view stuck in its editing appearance. - Swift: replacing the `delegate` left the previous delegate receiving callbacks, and clearing it discarded closures the host had assigned directly. - Swift: the `cropViewControllerDidTapDone` bridge added in 3.1.2 captured the delegate strongly, the same retain cycle as the other callbacks. +- iOS 26-only layout symbols are compiled out on older SDKs, restoring Xcode 16 buildability. +- The private display-corner-radius lookup on iOS 26 now fails gracefully if the key ever disappears. +- Setting `showOnlyIcons` to false on iOS 26 hid the icon buttons without creating any text ones, leaving no way to commit or cancel the crop. The property is now ignored where the toolbar is icon-only. +- On iOS 26, hiding every toolbar action button left an empty glass capsule stranded on screen. 3.1.2 Release Notes (2026-04-07)