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) 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 c00e47d4..1b4aa545 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,24 @@ - (CGRect)frameForToolbarWithVerticalLayout:(BOOL)verticalLayout { } #if !TARGET_OS_VISION - 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]; + // 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;