/** * Copyright (c) 2015-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "NSTextStorage+FontScaling.h" typedef NS_OPTIONS(NSInteger, RCTTextSizeComparisonOptions) { RCTTextSizeComparisonSmaller = 1 << 0, RCTTextSizeComparisonLarger = 1 << 1, RCTTextSizeComparisonWithinRange = 1 << 2, }; @implementation NSTextStorage (FontScaling) - (void)scaleFontSizeToFitSize:(CGSize)size minimumFontSize:(CGFloat)minimumFontSize maximumFontSize:(CGFloat)maximumFontSize { CGFloat bottomRatio = 1.0/128.0; CGFloat topRatio = 128.0; CGFloat ratio = 1.0; NSAttributedString *originalAttributedString = [self copy]; CGFloat lastRatioWhichFits = 0.02; while (true) { [self scaleFontSizeWithRatio:ratio minimumFontSize:minimumFontSize maximumFontSize:maximumFontSize]; RCTTextSizeComparisonOptions comparsion = [self compareToSize:size thresholdRatio:0.01]; if ( (comparsion & RCTTextSizeComparisonWithinRange) && (comparsion & RCTTextSizeComparisonSmaller) ) { return; } else if (comparsion & RCTTextSizeComparisonSmaller) { bottomRatio = ratio; lastRatioWhichFits = ratio; } else { topRatio = ratio; } ratio = (topRatio + bottomRatio) / 2.0; CGFloat kRatioThreshold = 0.005; if ( ABS(topRatio - bottomRatio) < kRatioThreshold || ABS(topRatio - ratio) < kRatioThreshold || ABS(bottomRatio - ratio) < kRatioThreshold ) { [self replaceCharactersInRange:(NSRange){0, self.length} withAttributedString:originalAttributedString]; [self scaleFontSizeWithRatio:lastRatioWhichFits minimumFontSize:minimumFontSize maximumFontSize:maximumFontSize]; return; } [self replaceCharactersInRange:(NSRange){0, self.length} withAttributedString:originalAttributedString]; } } - (RCTTextSizeComparisonOptions)compareToSize:(CGSize)size thresholdRatio:(CGFloat)thresholdRatio { NSLayoutManager *layoutManager = self.layoutManagers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject; [layoutManager ensureLayoutForTextContainer:textContainer]; // Does it fit the text container? NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; NSRange truncatedGlyphRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:glyphRange.length - 1]; if (truncatedGlyphRange.location != NSNotFound) { return RCTTextSizeComparisonLarger; } CGSize measuredSize = [layoutManager usedRectForTextContainer:textContainer].size; // Does it fit the size? BOOL fitsSize = size.width >= measuredSize.width && size.height >= measuredSize.height; CGSize thresholdSize = (CGSize){ size.width * thresholdRatio, size.height * thresholdRatio, }; RCTTextSizeComparisonOptions result = 0; result |= (fitsSize) ? RCTTextSizeComparisonSmaller : RCTTextSizeComparisonLarger; if (ABS(measuredSize.width - size.width) < thresholdSize.width) { result = result | RCTTextSizeComparisonWithinRange; } return result; } - (void)scaleFontSizeWithRatio:(CGFloat)ratio minimumFontSize:(CGFloat)minimumFontSize maximumFontSize:(CGFloat)maximumFontSize { [self beginEditing]; [self enumerateAttribute:NSFontAttributeName inRange:(NSRange){0, self.length} options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock: ^(UIFont *_Nullable font, NSRange range, BOOL *_Nonnull stop) { if (!font) { return; } CGFloat fontSize = MAX(MIN(font.pointSize * ratio, maximumFontSize), minimumFontSize); [self addAttribute:NSFontAttributeName value:[font fontWithSize:fontSize] range:range]; } ]; [self endEditing]; } @end