├── .gitignore ├── AestheticTextDemo ├── AestheticTextDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── AestheticTextDemo │ ├── AestheticTextDemoApp.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ └── ContentView.swift └── Example.png ├── LICENSE ├── Package.swift ├── README.md └── Sources └── AestheticText └── AestheticText.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm 7 | .netrc 8 | -------------------------------------------------------------------------------- /AestheticTextDemo/AestheticTextDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 27AB03982D887DFB008B7456 /* AestheticText in Frameworks */ = {isa = PBXBuildFile; productRef = 27AB03972D887DFB008B7456 /* AestheticText */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXFileReference section */ 14 | 27AB03882D887DE0008B7456 /* AestheticTextDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AestheticTextDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 15 | /* End PBXFileReference section */ 16 | 17 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 18 | 27AB038A2D887DE0008B7456 /* AestheticTextDemo */ = { 19 | isa = PBXFileSystemSynchronizedRootGroup; 20 | path = AestheticTextDemo; 21 | sourceTree = ""; 22 | }; 23 | /* End PBXFileSystemSynchronizedRootGroup section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | 27AB03852D887DE0008B7456 /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | 27AB03982D887DFB008B7456 /* AestheticText in Frameworks */, 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | 27AB037F2D887DE0008B7456 = { 38 | isa = PBXGroup; 39 | children = ( 40 | 27AB038A2D887DE0008B7456 /* AestheticTextDemo */, 41 | 27AB03892D887DE0008B7456 /* Products */, 42 | ); 43 | sourceTree = ""; 44 | }; 45 | 27AB03892D887DE0008B7456 /* Products */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | 27AB03882D887DE0008B7456 /* AestheticTextDemo.app */, 49 | ); 50 | name = Products; 51 | sourceTree = ""; 52 | }; 53 | /* End PBXGroup section */ 54 | 55 | /* Begin PBXNativeTarget section */ 56 | 27AB03872D887DE0008B7456 /* AestheticTextDemo */ = { 57 | isa = PBXNativeTarget; 58 | buildConfigurationList = 27AB03932D887DE1008B7456 /* Build configuration list for PBXNativeTarget "AestheticTextDemo" */; 59 | buildPhases = ( 60 | 27AB03842D887DE0008B7456 /* Sources */, 61 | 27AB03852D887DE0008B7456 /* Frameworks */, 62 | 27AB03862D887DE0008B7456 /* Resources */, 63 | ); 64 | buildRules = ( 65 | ); 66 | dependencies = ( 67 | ); 68 | fileSystemSynchronizedGroups = ( 69 | 27AB038A2D887DE0008B7456 /* AestheticTextDemo */, 70 | ); 71 | name = AestheticTextDemo; 72 | packageProductDependencies = ( 73 | 27AB03972D887DFB008B7456 /* AestheticText */, 74 | ); 75 | productName = AestheticTextDemo; 76 | productReference = 27AB03882D887DE0008B7456 /* AestheticTextDemo.app */; 77 | productType = "com.apple.product-type.application"; 78 | }; 79 | /* End PBXNativeTarget section */ 80 | 81 | /* Begin PBXProject section */ 82 | 27AB03802D887DE0008B7456 /* Project object */ = { 83 | isa = PBXProject; 84 | attributes = { 85 | BuildIndependentTargetsInParallel = 1; 86 | LastSwiftUpdateCheck = 1630; 87 | LastUpgradeCheck = 1630; 88 | TargetAttributes = { 89 | 27AB03872D887DE0008B7456 = { 90 | CreatedOnToolsVersion = 16.3; 91 | }; 92 | }; 93 | }; 94 | buildConfigurationList = 27AB03832D887DE0008B7456 /* Build configuration list for PBXProject "AestheticTextDemo" */; 95 | developmentRegion = en; 96 | hasScannedForEncodings = 0; 97 | knownRegions = ( 98 | en, 99 | Base, 100 | ); 101 | mainGroup = 27AB037F2D887DE0008B7456; 102 | minimizedProjectReferenceProxies = 1; 103 | packageReferences = ( 104 | 27AB03962D887DFB008B7456 /* XCLocalSwiftPackageReference "../../AestheticText" */, 105 | ); 106 | preferredProjectObjectVersion = 77; 107 | productRefGroup = 27AB03892D887DE0008B7456 /* Products */; 108 | projectDirPath = ""; 109 | projectRoot = ""; 110 | targets = ( 111 | 27AB03872D887DE0008B7456 /* AestheticTextDemo */, 112 | ); 113 | }; 114 | /* End PBXProject section */ 115 | 116 | /* Begin PBXResourcesBuildPhase section */ 117 | 27AB03862D887DE0008B7456 /* Resources */ = { 118 | isa = PBXResourcesBuildPhase; 119 | buildActionMask = 2147483647; 120 | files = ( 121 | ); 122 | runOnlyForDeploymentPostprocessing = 0; 123 | }; 124 | /* End PBXResourcesBuildPhase section */ 125 | 126 | /* Begin PBXSourcesBuildPhase section */ 127 | 27AB03842D887DE0008B7456 /* Sources */ = { 128 | isa = PBXSourcesBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXSourcesBuildPhase section */ 135 | 136 | /* Begin XCBuildConfiguration section */ 137 | 27AB03912D887DE1008B7456 /* Debug */ = { 138 | isa = XCBuildConfiguration; 139 | buildSettings = { 140 | ALWAYS_SEARCH_USER_PATHS = NO; 141 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 142 | CLANG_ANALYZER_NONNULL = YES; 143 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 144 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 145 | CLANG_ENABLE_MODULES = YES; 146 | CLANG_ENABLE_OBJC_ARC = YES; 147 | CLANG_ENABLE_OBJC_WEAK = YES; 148 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 149 | CLANG_WARN_BOOL_CONVERSION = YES; 150 | CLANG_WARN_COMMA = YES; 151 | CLANG_WARN_CONSTANT_CONVERSION = YES; 152 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 153 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 154 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 155 | CLANG_WARN_EMPTY_BODY = YES; 156 | CLANG_WARN_ENUM_CONVERSION = YES; 157 | CLANG_WARN_INFINITE_RECURSION = YES; 158 | CLANG_WARN_INT_CONVERSION = YES; 159 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 160 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 161 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 162 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 163 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 164 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 165 | CLANG_WARN_STRICT_PROTOTYPES = YES; 166 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 167 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 168 | CLANG_WARN_UNREACHABLE_CODE = YES; 169 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 170 | COPY_PHASE_STRIP = NO; 171 | DEBUG_INFORMATION_FORMAT = dwarf; 172 | ENABLE_STRICT_OBJC_MSGSEND = YES; 173 | ENABLE_TESTABILITY = YES; 174 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 175 | GCC_C_LANGUAGE_STANDARD = gnu17; 176 | GCC_DYNAMIC_NO_PIC = NO; 177 | GCC_NO_COMMON_BLOCKS = YES; 178 | GCC_OPTIMIZATION_LEVEL = 0; 179 | GCC_PREPROCESSOR_DEFINITIONS = ( 180 | "DEBUG=1", 181 | "$(inherited)", 182 | ); 183 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 184 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 185 | GCC_WARN_UNDECLARED_SELECTOR = YES; 186 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 187 | GCC_WARN_UNUSED_FUNCTION = YES; 188 | GCC_WARN_UNUSED_VARIABLE = YES; 189 | IPHONEOS_DEPLOYMENT_TARGET = 18.4; 190 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 191 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 192 | MTL_FAST_MATH = YES; 193 | ONLY_ACTIVE_ARCH = YES; 194 | SDKROOT = iphoneos; 195 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 196 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 197 | }; 198 | name = Debug; 199 | }; 200 | 27AB03922D887DE1008B7456 /* Release */ = { 201 | isa = XCBuildConfiguration; 202 | buildSettings = { 203 | ALWAYS_SEARCH_USER_PATHS = NO; 204 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 205 | CLANG_ANALYZER_NONNULL = YES; 206 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 207 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 208 | CLANG_ENABLE_MODULES = YES; 209 | CLANG_ENABLE_OBJC_ARC = YES; 210 | CLANG_ENABLE_OBJC_WEAK = YES; 211 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 212 | CLANG_WARN_BOOL_CONVERSION = YES; 213 | CLANG_WARN_COMMA = YES; 214 | CLANG_WARN_CONSTANT_CONVERSION = YES; 215 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 216 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 217 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 218 | CLANG_WARN_EMPTY_BODY = YES; 219 | CLANG_WARN_ENUM_CONVERSION = YES; 220 | CLANG_WARN_INFINITE_RECURSION = YES; 221 | CLANG_WARN_INT_CONVERSION = YES; 222 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 223 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 224 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 225 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 226 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 227 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 228 | CLANG_WARN_STRICT_PROTOTYPES = YES; 229 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 230 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 231 | CLANG_WARN_UNREACHABLE_CODE = YES; 232 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 233 | COPY_PHASE_STRIP = NO; 234 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 235 | ENABLE_NS_ASSERTIONS = NO; 236 | ENABLE_STRICT_OBJC_MSGSEND = YES; 237 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 238 | GCC_C_LANGUAGE_STANDARD = gnu17; 239 | GCC_NO_COMMON_BLOCKS = YES; 240 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 241 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 242 | GCC_WARN_UNDECLARED_SELECTOR = YES; 243 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 244 | GCC_WARN_UNUSED_FUNCTION = YES; 245 | GCC_WARN_UNUSED_VARIABLE = YES; 246 | IPHONEOS_DEPLOYMENT_TARGET = 18.4; 247 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 248 | MTL_ENABLE_DEBUG_INFO = NO; 249 | MTL_FAST_MATH = YES; 250 | SDKROOT = iphoneos; 251 | SWIFT_COMPILATION_MODE = wholemodule; 252 | VALIDATE_PRODUCT = YES; 253 | }; 254 | name = Release; 255 | }; 256 | 27AB03942D887DE1008B7456 /* Debug */ = { 257 | isa = XCBuildConfiguration; 258 | buildSettings = { 259 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 260 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 261 | CODE_SIGN_STYLE = Automatic; 262 | CURRENT_PROJECT_VERSION = 1; 263 | ENABLE_PREVIEWS = YES; 264 | GENERATE_INFOPLIST_FILE = YES; 265 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 266 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 267 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 268 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 269 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 270 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 271 | LD_RUNPATH_SEARCH_PATHS = ( 272 | "$(inherited)", 273 | "@executable_path/Frameworks", 274 | ); 275 | MARKETING_VERSION = 1.0; 276 | PRODUCT_BUNDLE_IDENTIFIER = com.kylebashour.AestheticTextDemo; 277 | PRODUCT_NAME = "$(TARGET_NAME)"; 278 | SWIFT_EMIT_LOC_STRINGS = YES; 279 | SWIFT_VERSION = 5.0; 280 | TARGETED_DEVICE_FAMILY = "1,2"; 281 | }; 282 | name = Debug; 283 | }; 284 | 27AB03952D887DE1008B7456 /* Release */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 288 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 289 | CODE_SIGN_STYLE = Automatic; 290 | CURRENT_PROJECT_VERSION = 1; 291 | ENABLE_PREVIEWS = YES; 292 | GENERATE_INFOPLIST_FILE = YES; 293 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 294 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 295 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 296 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 297 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 298 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 299 | LD_RUNPATH_SEARCH_PATHS = ( 300 | "$(inherited)", 301 | "@executable_path/Frameworks", 302 | ); 303 | MARKETING_VERSION = 1.0; 304 | PRODUCT_BUNDLE_IDENTIFIER = com.kylebashour.AestheticTextDemo; 305 | PRODUCT_NAME = "$(TARGET_NAME)"; 306 | SWIFT_EMIT_LOC_STRINGS = YES; 307 | SWIFT_VERSION = 5.0; 308 | TARGETED_DEVICE_FAMILY = "1,2"; 309 | }; 310 | name = Release; 311 | }; 312 | /* End XCBuildConfiguration section */ 313 | 314 | /* Begin XCConfigurationList section */ 315 | 27AB03832D887DE0008B7456 /* Build configuration list for PBXProject "AestheticTextDemo" */ = { 316 | isa = XCConfigurationList; 317 | buildConfigurations = ( 318 | 27AB03912D887DE1008B7456 /* Debug */, 319 | 27AB03922D887DE1008B7456 /* Release */, 320 | ); 321 | defaultConfigurationIsVisible = 0; 322 | defaultConfigurationName = Release; 323 | }; 324 | 27AB03932D887DE1008B7456 /* Build configuration list for PBXNativeTarget "AestheticTextDemo" */ = { 325 | isa = XCConfigurationList; 326 | buildConfigurations = ( 327 | 27AB03942D887DE1008B7456 /* Debug */, 328 | 27AB03952D887DE1008B7456 /* Release */, 329 | ); 330 | defaultConfigurationIsVisible = 0; 331 | defaultConfigurationName = Release; 332 | }; 333 | /* End XCConfigurationList section */ 334 | 335 | /* Begin XCLocalSwiftPackageReference section */ 336 | 27AB03962D887DFB008B7456 /* XCLocalSwiftPackageReference "../../AestheticText" */ = { 337 | isa = XCLocalSwiftPackageReference; 338 | relativePath = ../../AestheticText; 339 | }; 340 | /* End XCLocalSwiftPackageReference section */ 341 | 342 | /* Begin XCSwiftPackageProductDependency section */ 343 | 27AB03972D887DFB008B7456 /* AestheticText */ = { 344 | isa = XCSwiftPackageProductDependency; 345 | productName = AestheticText; 346 | }; 347 | /* End XCSwiftPackageProductDependency section */ 348 | }; 349 | rootObject = 27AB03802D887DE0008B7456 /* Project object */; 350 | } 351 | -------------------------------------------------------------------------------- /AestheticTextDemo/AestheticTextDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AestheticTextDemo/AestheticTextDemo/AestheticTextDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AestheticTextDemoApp.swift 3 | // AestheticTextDemo 4 | // 5 | // Created by Kyle Bashour on 3/17/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct AestheticTextDemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AestheticTextDemo/AestheticTextDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AestheticTextDemo/AestheticTextDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /AestheticTextDemo/AestheticTextDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AestheticTextDemo/AestheticTextDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // AestheticTextDemo 4 | // 5 | // Created by Kyle Bashour on 3/17/25. 6 | // 7 | 8 | import SwiftUI 9 | import AestheticText 10 | 11 | struct ContentView: View { 12 | var body: some View { 13 | NavigationStack { 14 | ScrollView { 15 | VStack(spacing: 40) { 16 | Text("This text will be compressed so that lines are as close to equal-length as possible.") 17 | .multilineTextAlignment(.center) 18 | .aestheticText() 19 | .caption(".aestheticText()", alignment: .center) 20 | 21 | Text("It works great for empty-state views, with centered text.") 22 | .multilineTextAlignment(.center) 23 | .caption("Default", alignment: .center) 24 | 25 | Text("It works great for empty-state views, with centered text.") 26 | .multilineTextAlignment(.center) 27 | .aestheticText() 28 | .caption(".aestheticText()", alignment: .center) 29 | 30 | VStack(alignment: .leading, spacing: 40) { 31 | Text("It can sometimes create slightly awkward results for non-centered text.") 32 | .caption("Default", alignment: .leading) 33 | 34 | Text("It can sometimes create slightly awkward results for non-centered text.") 35 | .aestheticText() 36 | .caption(".aestheticText()", alignment: .leading) 37 | } 38 | } 39 | .padding() 40 | } 41 | .navigationTitle("Aesthetic Text") 42 | } 43 | } 44 | } 45 | 46 | extension View { 47 | func caption(_ text: String, alignment: HorizontalAlignment) -> some View { 48 | VStack(alignment: alignment, spacing: 10) { 49 | self 50 | 51 | Text(text) 52 | .font(.caption) 53 | .monospaced() 54 | .bold() 55 | .foregroundStyle(.secondary) 56 | } 57 | } 58 | } 59 | 60 | #Preview { 61 | ContentView() 62 | } 63 | -------------------------------------------------------------------------------- /AestheticTextDemo/Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kylebshr/AestheticText/eec68fe70399c95a27e8e54fc325db2dadd3f7f8/AestheticTextDemo/Example.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Kyle Bashour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "AestheticText", 8 | platforms: [ 9 | .iOS(.v16), 10 | .macOS(.v13), 11 | ], 12 | products: [ 13 | .library( 14 | name: "AestheticText", 15 | targets: ["AestheticText"] 16 | ), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "AestheticText", 21 | path: "Sources" 22 | ), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AestheticText 2 | 3 | A SwiftUI modifier that will compress `Text` horizontally as much as possible without wrapping to another line - that is, make each line as close in width as possible. For ✨aesthetics✨ 4 | 5 | ### Installation and Usage 6 | 7 | - File > Add Package Dependencies... 8 | - Add `https://github.com/kylebshr/AestheticText.git` 9 | - Select “Up to Next Major” with “1.0.0” 10 | 11 | ```swift 12 | import AestheticText 13 | 14 | Text(someAwesomeText) 15 | .aestheticText() 16 | ``` 17 | 18 | ### Implementation 19 | 20 | This is implemented using a single-view `Layout` that measures its content using a binary search to find the smallest possible width (with a tuned level of precison) that does not affect the height. 21 | 22 | ### Drawbacks 23 | 24 | Since this implementation has no knowledge of the actual content of the Text being rendered, this comes with some drawbacks. For example, it may cause the second line of two lines of text to become longer than the first, since it results in a narrower width, which may actually look worse and be undesirable. For this reason, it works best with centered text, where this behavior doesn’t look off. This screenshot from the demo project illustrates the issue: 25 | 26 | Demo app screenshot 27 | -------------------------------------------------------------------------------- /Sources/AestheticText/AestheticText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AestheticText.swift 3 | // AestheticText 4 | // 5 | // Created by Kyle Bashour on 3/15/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | /// Wraps text "aesthetically" by compressing the views width to be as small as possible without 12 | /// affecting the height - in practice, making the length of lines of text as equal as possible. 13 | /// 14 | /// The method works best with centered text, as it may cause the second of two lines of text to 15 | /// to be longer than the first, which can look off with left or right-aligned text. 16 | public func aestheticText() -> some View { 17 | modifier(AestheticTextModifier()) 18 | } 19 | } 20 | 21 | private struct AestheticTextModifier: ViewModifier { 22 | func body(content: Content) -> some View { 23 | AestheticTextLayout() { 24 | content 25 | } 26 | } 27 | } 28 | 29 | private struct AestheticTextLayout: Layout { 30 | struct CacheKey: Hashable { 31 | let width: CGFloat? 32 | let height: CGFloat? 33 | } 34 | 35 | typealias Cache = [CacheKey: CGSize] 36 | 37 | func makeCache(subviews: Subviews) -> Cache { 38 | [:] 39 | } 40 | 41 | func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize { 42 | assert(subviews.count == 1) 43 | 44 | guard let subview = subviews.first else { 45 | return .zero 46 | } 47 | 48 | let key = CacheKey(width: proposal.width, height: proposal.height) 49 | if let size = cache[key] { 50 | return size 51 | } 52 | 53 | let sizeThatFits = subview.sizeThatFits(proposal) 54 | let size = smallestSize(for: subview, proposal: proposal, sizeThatFits: sizeThatFits) 55 | cache[key] = size 56 | 57 | return size 58 | } 59 | 60 | func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) { 61 | assert(subviews.count == 1) 62 | 63 | guard let subview = subviews.first else { 64 | return 65 | } 66 | 67 | let key = CacheKey(width: proposal.width, height: proposal.height) 68 | if let size = cache[key] { 69 | return subview.place(at: bounds.origin, proposal: ProposedViewSize(size)) 70 | } 71 | 72 | let sizeThatFits = subview.sizeThatFits(proposal) 73 | let size = smallestSize(for: subview, proposal: proposal, sizeThatFits: sizeThatFits) 74 | cache[key] = size 75 | 76 | subview.place(at: bounds.origin, proposal: ProposedViewSize(size)) 77 | } 78 | 79 | /// Performs a binary search to find the smallest width, up to a tuned precision, 80 | /// that does not affect the height (e.g., cause the text to wrap to another line). 81 | private func smallestSize(for subview: LayoutSubview, proposal: ProposedViewSize, sizeThatFits: CGSize) -> CGSize { 82 | var maxWidth = sizeThatFits.width 83 | // It will never make sense to wrap to less than half of the current width. 84 | var minWidth = maxWidth * 0.5 85 | 86 | // Get within 10% of the smallest possible size based on the range of possible sizes. 87 | let precision = max(1, (maxWidth - minWidth) * 0.1) 88 | let naturualHeight = sizeThatFits.height 89 | 90 | while maxWidth - minWidth > precision { 91 | let midWidth = (minWidth + maxWidth) / 2 92 | 93 | var testProposal = proposal 94 | testProposal.width = midWidth 95 | let testSize = subview.sizeThatFits(testProposal) 96 | 97 | if testSize.height == naturualHeight { 98 | // Shrink further 99 | maxWidth = midWidth 100 | } else { 101 | // Too narrow, increase width 102 | minWidth = midWidth 103 | } 104 | } 105 | 106 | return CGSize(width: maxWidth, height: naturualHeight) 107 | } 108 | } 109 | --------------------------------------------------------------------------------