@@ -866,6 +866,106 @@ class NSAttributedString_SizingTests: XCTestCase {
866866 #endif
867867 }
868868
869+ // MARK: - Cache
870+
871+ func test_cache_cachedMatchesUncached_singleLine( ) throws {
872+ NSAttributedString . clearTextSizeCache ( )
873+
874+ let attributedString = try makeAttributedString ( font: Font . systemFont ( ofSize: 16 ) , lineBreakMode: . byWordWrapping)
875+
876+ let uncached = attributedString. computeBoundingRectSize ( numberOfLines: 1 , layoutWidth: 100 )
877+ let cachedMiss = attributedString. boundingRectSize ( numberOfLines: 1 , layoutWidth: 100 ) // computes + caches
878+ let cachedHit = attributedString. boundingRectSize ( numberOfLines: 1 , layoutWidth: 100 ) // served from cache
879+
880+ expect ( cachedMiss) == uncached
881+ expect ( cachedHit) == uncached
882+ }
883+
884+ func test_cache_cachedMatchesUncached_multiLine( ) throws {
885+ NSAttributedString . clearTextSizeCache ( )
886+
887+ let attributedString = try makeAttributedString ( font: Font . systemFont ( ofSize: 16 ) , lineBreakMode: . byWordWrapping)
888+
889+ let uncached = attributedString. computeBoundingRectSize ( numberOfLines: 0 , layoutWidth: 100 )
890+ let cachedMiss = attributedString. boundingRectSize ( numberOfLines: 0 , layoutWidth: 100 )
891+ let cachedHit = attributedString. boundingRectSize ( numberOfLines: 0 , layoutWidth: 100 )
892+
893+ expect ( cachedMiss) == uncached
894+ expect ( cachedHit) == uncached
895+ }
896+
897+ func test_cache_distinctFonts_notConfused( ) throws {
898+ NSAttributedString . clearTextSizeCache ( )
899+
900+ // same text, different font: the key includes the attributed string (which carries the font), so the two sizes
901+ // must not collapse to one cached entry.
902+ let small = try makeAttributedString ( font: Font . systemFont ( ofSize: 16 ) , lineBreakMode: . byWordWrapping)
903+ let large = try makeAttributedString ( font: Font . systemFont ( ofSize: 32 ) , lineBreakMode: . byWordWrapping)
904+
905+ let smallSize = small. boundingRectSize ( numberOfLines: 1 , layoutWidth: 100 )
906+ let largeSize = large. boundingRectSize ( numberOfLines: 1 , layoutWidth: 100 )
907+
908+ expect ( smallSize) != largeSize
909+ expect ( largeSize. height) > smallSize. height
910+ }
911+
912+ func test_cache_distinctWidths_notConfused( ) throws {
913+ NSAttributedString . clearTextSizeCache ( )
914+
915+ // same multi-line text at different widths wraps differently, so the cache (keyed by width) must keep them distinct.
916+ let attributedString = try makeAttributedString ( font: Font . systemFont ( ofSize: 16 ) , lineBreakMode: . byWordWrapping)
917+
918+ let narrow = attributedString. boundingRectSize ( numberOfLines: 0 , layoutWidth: 80 )
919+ let wide = attributedString. boundingRectSize ( numberOfLines: 0 , layoutWidth: 300 )
920+
921+ expect ( narrow) != wide
922+ expect ( narrow. height) > wide. height // narrower wraps to more lines -> taller
923+ }
924+
925+ func test_cache_clear_recomputesSameValue( ) throws {
926+ let attributedString = try makeAttributedString ( font: Font . systemFont ( ofSize: 16 ) , lineBreakMode: . byWordWrapping)
927+
928+ let before = attributedString. boundingRectSize ( numberOfLines: 2 , layoutWidth: 120 )
929+ NSAttributedString . clearTextSizeCache ( )
930+ let after = attributedString. boundingRectSize ( numberOfLines: 2 , layoutWidth: 120 )
931+
932+ expect ( after) == before
933+ }
934+
935+ func test_cache_mutableAttributedString_afterMutation_returnsNewSize( ) throws {
936+ NSAttributedString . clearTextSizeCache ( )
937+
938+ let attributes : [ NSAttributedString . Key : Any ] = [ . font: Font . systemFont ( ofSize: 16 ) ]
939+ let mutableString = NSMutableAttributedString ( string: " Hi " , attributes: attributes)
940+
941+ let sizeBeforeMutation = mutableString. boundingRectSize ( numberOfLines: 1 , layoutWidth: 1000 )
942+
943+ // mutate the same instance that was just used as a cache key, then ask again with that same instance.
944+ mutableString. append ( NSAttributedString ( string: " there, this is a much longer piece of text " , attributes: attributes) )
945+ let sizeAfterMutation = mutableString. boundingRectSize ( numberOfLines: 1 , layoutWidth: 1000 )
946+
947+ // the size must reflect the mutated (longer) content, not a stale value keyed by the pre-mutation string.
948+ expect ( sizeAfterMutation) != sizeBeforeMutation
949+ expect ( sizeAfterMutation. width) > sizeBeforeMutation. width
950+ expect ( sizeAfterMutation) == mutableString. computeBoundingRectSize ( numberOfLines: 1 , layoutWidth: 1000 )
951+ }
952+
953+ func test_cache_singleLine_sizeIndependentOfWidthAndLineBreakMode( ) throws {
954+ NSAttributedString . clearTextSizeCache ( )
955+
956+ // single-line sizing ignores layoutWidth and lineBreakMode (it measures the natural, unwrapped line)
957+ let attributedString = try makeAttributedString ( font: Font . systemFont ( ofSize: 16 ) , lineBreakMode: . byWordWrapping)
958+
959+ let narrow = attributedString. boundingRectSize ( numberOfLines: 1 , layoutWidth: 50 )
960+ let wide = attributedString. boundingRectSize ( numberOfLines: 1 , layoutWidth: 5000 )
961+ let charWrap = attributedString. boundingRectSize ( numberOfLines: 1 , layoutWidth: 123 , lineBreakMode: . byCharWrapping)
962+
963+ expect ( narrow) == wide
964+ expect ( narrow) == charWrap
965+ }
966+
967+ // MARK: - Helpers
968+
869969 private func makeAttributedString( font: Font , lineBreakMode: NSLineBreakMode ) throws -> NSAttributedString {
870970 NSAttributedString (
871971 string: Constants . string,
0 commit comments