├── .github └── workflows │ └── build.yml ├── .gitignore ├── Example ├── ExpandableTextExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── ExpandableTextExample │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── ExpandableTextExampleApp.swift │ └── Preview Content │ │ └── Preview Assets.xcassets │ │ └── Contents.json ├── en.lproj │ └── Localizable.strings └── it.lproj │ └── Localizable.strings ├── LICENSE ├── Package.swift ├── README.md └── Sources ├── ExpandableText ├── ExpandableText+Modifiers.swift └── ExpandableText.swift └── Utilities ├── OverlayAdapter.swift ├── TruncationTextMask.swift └── ViewSizeReader.swift /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift build 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: | 21 | xcodebuild \ 22 | -scheme ExpandableText \ 23 | -sdk iphonesimulator \ 24 | -destination 'platform=iOS Simulator,name=iPhone 14' 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .netrc 8 | .swiftpm 9 | -------------------------------------------------------------------------------- /Example/ExpandableTextExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 816E11FA29AA525C0006F0B1 /* ExpandableTextExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 816E11F929AA525C0006F0B1 /* ExpandableTextExampleApp.swift */; }; 11 | 816E11FC29AA525C0006F0B1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 816E11FB29AA525C0006F0B1 /* ContentView.swift */; }; 12 | 816E11FE29AA525D0006F0B1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 816E11FD29AA525D0006F0B1 /* Assets.xcassets */; }; 13 | 816E120129AA525D0006F0B1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 816E120029AA525D0006F0B1 /* Preview Assets.xcassets */; }; 14 | 8196CFD32A9B76AC00646D12 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8196CFD52A9B76AC00646D12 /* Localizable.strings */; }; 15 | 8196CFDB2A9B79AA00646D12 /* ExpandableText in Frameworks */ = {isa = PBXBuildFile; productRef = 8196CFDA2A9B79AA00646D12 /* ExpandableText */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 816E11F629AA525C0006F0B1 /* ExpandableTextExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExpandableTextExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 816E11F929AA525C0006F0B1 /* ExpandableTextExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableTextExampleApp.swift; sourceTree = ""; }; 21 | 816E11FB29AA525C0006F0B1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 22 | 816E11FD29AA525D0006F0B1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23 | 816E120029AA525D0006F0B1 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 24 | 8196CFD42A9B76AC00646D12 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 25 | 8196CFD62A9B76EF00646D12 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 26 | 8196CFD92A9B799300646D12 /* ExpandableText */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ExpandableText; path = ..; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 816E11F329AA525C0006F0B1 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | 8196CFDB2A9B79AA00646D12 /* ExpandableText in Frameworks */, 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 81189B9D29AA554F003AEFC0 /* Frameworks */ = { 42 | isa = PBXGroup; 43 | children = ( 44 | ); 45 | name = Frameworks; 46 | sourceTree = ""; 47 | }; 48 | 816E11ED29AA525B0006F0B1 = { 49 | isa = PBXGroup; 50 | children = ( 51 | 8196CFD72A9B797800646D12 /* Packages */, 52 | 8196CFD52A9B76AC00646D12 /* Localizable.strings */, 53 | 816E11F829AA525C0006F0B1 /* ExpandableTextExample */, 54 | 816E11F729AA525C0006F0B1 /* Products */, 55 | 81189B9D29AA554F003AEFC0 /* Frameworks */, 56 | ); 57 | sourceTree = ""; 58 | }; 59 | 816E11F729AA525C0006F0B1 /* Products */ = { 60 | isa = PBXGroup; 61 | children = ( 62 | 816E11F629AA525C0006F0B1 /* ExpandableTextExample.app */, 63 | ); 64 | name = Products; 65 | sourceTree = ""; 66 | }; 67 | 816E11F829AA525C0006F0B1 /* ExpandableTextExample */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | 816E11F929AA525C0006F0B1 /* ExpandableTextExampleApp.swift */, 71 | 816E11FB29AA525C0006F0B1 /* ContentView.swift */, 72 | 816E11FD29AA525D0006F0B1 /* Assets.xcassets */, 73 | 816E11FF29AA525D0006F0B1 /* Preview Content */, 74 | ); 75 | path = ExpandableTextExample; 76 | sourceTree = ""; 77 | }; 78 | 816E11FF29AA525D0006F0B1 /* Preview Content */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 816E120029AA525D0006F0B1 /* Preview Assets.xcassets */, 82 | ); 83 | path = "Preview Content"; 84 | sourceTree = ""; 85 | }; 86 | 8196CFD72A9B797800646D12 /* Packages */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 8196CFD92A9B799300646D12 /* ExpandableText */, 90 | ); 91 | name = Packages; 92 | sourceTree = ""; 93 | }; 94 | /* End PBXGroup section */ 95 | 96 | /* Begin PBXNativeTarget section */ 97 | 816E11F529AA525C0006F0B1 /* ExpandableTextExample */ = { 98 | isa = PBXNativeTarget; 99 | buildConfigurationList = 816E120429AA525D0006F0B1 /* Build configuration list for PBXNativeTarget "ExpandableTextExample" */; 100 | buildPhases = ( 101 | 816E11F229AA525C0006F0B1 /* Sources */, 102 | 816E11F329AA525C0006F0B1 /* Frameworks */, 103 | 816E11F429AA525C0006F0B1 /* Resources */, 104 | ); 105 | buildRules = ( 106 | ); 107 | dependencies = ( 108 | ); 109 | name = ExpandableTextExample; 110 | packageProductDependencies = ( 111 | 8196CFDA2A9B79AA00646D12 /* ExpandableText */, 112 | ); 113 | productName = ExpandableTextExample; 114 | productReference = 816E11F629AA525C0006F0B1 /* ExpandableTextExample.app */; 115 | productType = "com.apple.product-type.application"; 116 | }; 117 | /* End PBXNativeTarget section */ 118 | 119 | /* Begin PBXProject section */ 120 | 816E11EE29AA525B0006F0B1 /* Project object */ = { 121 | isa = PBXProject; 122 | attributes = { 123 | BuildIndependentTargetsInParallel = 1; 124 | LastSwiftUpdateCheck = 1420; 125 | LastUpgradeCheck = 1420; 126 | TargetAttributes = { 127 | 816E11F529AA525C0006F0B1 = { 128 | CreatedOnToolsVersion = 14.2; 129 | }; 130 | }; 131 | }; 132 | buildConfigurationList = 816E11F129AA525B0006F0B1 /* Build configuration list for PBXProject "ExpandableTextExample" */; 133 | compatibilityVersion = "Xcode 14.0"; 134 | developmentRegion = en; 135 | hasScannedForEncodings = 0; 136 | knownRegions = ( 137 | en, 138 | Base, 139 | it, 140 | ); 141 | mainGroup = 816E11ED29AA525B0006F0B1; 142 | packageReferences = ( 143 | ); 144 | productRefGroup = 816E11F729AA525C0006F0B1 /* Products */; 145 | projectDirPath = ""; 146 | projectRoot = ""; 147 | targets = ( 148 | 816E11F529AA525C0006F0B1 /* ExpandableTextExample */, 149 | ); 150 | }; 151 | /* End PBXProject section */ 152 | 153 | /* Begin PBXResourcesBuildPhase section */ 154 | 816E11F429AA525C0006F0B1 /* Resources */ = { 155 | isa = PBXResourcesBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | 816E120129AA525D0006F0B1 /* Preview Assets.xcassets in Resources */, 159 | 8196CFD32A9B76AC00646D12 /* Localizable.strings in Resources */, 160 | 816E11FE29AA525D0006F0B1 /* Assets.xcassets in Resources */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXResourcesBuildPhase section */ 165 | 166 | /* Begin PBXSourcesBuildPhase section */ 167 | 816E11F229AA525C0006F0B1 /* Sources */ = { 168 | isa = PBXSourcesBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | 816E11FC29AA525C0006F0B1 /* ContentView.swift in Sources */, 172 | 816E11FA29AA525C0006F0B1 /* ExpandableTextExampleApp.swift in Sources */, 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | /* End PBXSourcesBuildPhase section */ 177 | 178 | /* Begin PBXVariantGroup section */ 179 | 8196CFD52A9B76AC00646D12 /* Localizable.strings */ = { 180 | isa = PBXVariantGroup; 181 | children = ( 182 | 8196CFD42A9B76AC00646D12 /* it */, 183 | 8196CFD62A9B76EF00646D12 /* en */, 184 | ); 185 | name = Localizable.strings; 186 | sourceTree = ""; 187 | }; 188 | /* End PBXVariantGroup section */ 189 | 190 | /* Begin XCBuildConfiguration section */ 191 | 816E120229AA525D0006F0B1 /* Debug */ = { 192 | isa = XCBuildConfiguration; 193 | buildSettings = { 194 | ALWAYS_SEARCH_USER_PATHS = NO; 195 | CLANG_ANALYZER_NONNULL = YES; 196 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 197 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 198 | CLANG_ENABLE_MODULES = YES; 199 | CLANG_ENABLE_OBJC_ARC = YES; 200 | CLANG_ENABLE_OBJC_WEAK = YES; 201 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 202 | CLANG_WARN_BOOL_CONVERSION = YES; 203 | CLANG_WARN_COMMA = YES; 204 | CLANG_WARN_CONSTANT_CONVERSION = YES; 205 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 206 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 207 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 208 | CLANG_WARN_EMPTY_BODY = YES; 209 | CLANG_WARN_ENUM_CONVERSION = YES; 210 | CLANG_WARN_INFINITE_RECURSION = YES; 211 | CLANG_WARN_INT_CONVERSION = YES; 212 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 213 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 214 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 215 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 216 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 217 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 218 | CLANG_WARN_STRICT_PROTOTYPES = YES; 219 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 220 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 221 | CLANG_WARN_UNREACHABLE_CODE = YES; 222 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 223 | COPY_PHASE_STRIP = NO; 224 | DEBUG_INFORMATION_FORMAT = dwarf; 225 | ENABLE_STRICT_OBJC_MSGSEND = YES; 226 | ENABLE_TESTABILITY = YES; 227 | GCC_C_LANGUAGE_STANDARD = gnu11; 228 | GCC_DYNAMIC_NO_PIC = NO; 229 | GCC_NO_COMMON_BLOCKS = YES; 230 | GCC_OPTIMIZATION_LEVEL = 0; 231 | GCC_PREPROCESSOR_DEFINITIONS = ( 232 | "DEBUG=1", 233 | "$(inherited)", 234 | ); 235 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 236 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 237 | GCC_WARN_UNDECLARED_SELECTOR = YES; 238 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 239 | GCC_WARN_UNUSED_FUNCTION = YES; 240 | GCC_WARN_UNUSED_VARIABLE = YES; 241 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 242 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 243 | MTL_FAST_MATH = YES; 244 | ONLY_ACTIVE_ARCH = YES; 245 | SDKROOT = iphoneos; 246 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 247 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 248 | }; 249 | name = Debug; 250 | }; 251 | 816E120329AA525D0006F0B1 /* Release */ = { 252 | isa = XCBuildConfiguration; 253 | buildSettings = { 254 | ALWAYS_SEARCH_USER_PATHS = NO; 255 | CLANG_ANALYZER_NONNULL = YES; 256 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 257 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_ARC = YES; 260 | CLANG_ENABLE_OBJC_WEAK = YES; 261 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 262 | CLANG_WARN_BOOL_CONVERSION = YES; 263 | CLANG_WARN_COMMA = YES; 264 | CLANG_WARN_CONSTANT_CONVERSION = YES; 265 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 266 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 267 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 268 | CLANG_WARN_EMPTY_BODY = YES; 269 | CLANG_WARN_ENUM_CONVERSION = YES; 270 | CLANG_WARN_INFINITE_RECURSION = YES; 271 | CLANG_WARN_INT_CONVERSION = YES; 272 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 273 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 274 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 275 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 276 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 277 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 278 | CLANG_WARN_STRICT_PROTOTYPES = YES; 279 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 280 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 281 | CLANG_WARN_UNREACHABLE_CODE = YES; 282 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 283 | COPY_PHASE_STRIP = NO; 284 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 285 | ENABLE_NS_ASSERTIONS = NO; 286 | ENABLE_STRICT_OBJC_MSGSEND = YES; 287 | GCC_C_LANGUAGE_STANDARD = gnu11; 288 | GCC_NO_COMMON_BLOCKS = YES; 289 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 290 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 291 | GCC_WARN_UNDECLARED_SELECTOR = YES; 292 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 293 | GCC_WARN_UNUSED_FUNCTION = YES; 294 | GCC_WARN_UNUSED_VARIABLE = YES; 295 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 296 | MTL_ENABLE_DEBUG_INFO = NO; 297 | MTL_FAST_MATH = YES; 298 | SDKROOT = iphoneos; 299 | SWIFT_COMPILATION_MODE = wholemodule; 300 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 301 | VALIDATE_PRODUCT = YES; 302 | }; 303 | name = Release; 304 | }; 305 | 816E120529AA525D0006F0B1 /* Debug */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 309 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 310 | CODE_SIGN_STYLE = Automatic; 311 | CURRENT_PROJECT_VERSION = 1; 312 | DEVELOPMENT_ASSET_PATHS = "\"ExpandableTextExample/Preview Content\""; 313 | DEVELOPMENT_TEAM = 8PT88A387A; 314 | ENABLE_PREVIEWS = YES; 315 | GENERATE_INFOPLIST_FILE = YES; 316 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 317 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 318 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 319 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 320 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 321 | LD_RUNPATH_SEARCH_PATHS = ( 322 | "$(inherited)", 323 | "@executable_path/Frameworks", 324 | ); 325 | MARKETING_VERSION = 1.0; 326 | PRODUCT_BUNDLE_IDENTIFIER = it.ned.ExpandableTextExample; 327 | PRODUCT_NAME = "$(TARGET_NAME)"; 328 | SWIFT_EMIT_LOC_STRINGS = YES; 329 | SWIFT_VERSION = 5.0; 330 | TARGETED_DEVICE_FAMILY = "1,2"; 331 | }; 332 | name = Debug; 333 | }; 334 | 816E120629AA525D0006F0B1 /* Release */ = { 335 | isa = XCBuildConfiguration; 336 | buildSettings = { 337 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 338 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 339 | CODE_SIGN_STYLE = Automatic; 340 | CURRENT_PROJECT_VERSION = 1; 341 | DEVELOPMENT_ASSET_PATHS = "\"ExpandableTextExample/Preview Content\""; 342 | DEVELOPMENT_TEAM = 8PT88A387A; 343 | ENABLE_PREVIEWS = YES; 344 | GENERATE_INFOPLIST_FILE = YES; 345 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 346 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 347 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 348 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 349 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 350 | LD_RUNPATH_SEARCH_PATHS = ( 351 | "$(inherited)", 352 | "@executable_path/Frameworks", 353 | ); 354 | MARKETING_VERSION = 1.0; 355 | PRODUCT_BUNDLE_IDENTIFIER = it.ned.ExpandableTextExample; 356 | PRODUCT_NAME = "$(TARGET_NAME)"; 357 | SWIFT_EMIT_LOC_STRINGS = YES; 358 | SWIFT_VERSION = 5.0; 359 | TARGETED_DEVICE_FAMILY = "1,2"; 360 | }; 361 | name = Release; 362 | }; 363 | /* End XCBuildConfiguration section */ 364 | 365 | /* Begin XCConfigurationList section */ 366 | 816E11F129AA525B0006F0B1 /* Build configuration list for PBXProject "ExpandableTextExample" */ = { 367 | isa = XCConfigurationList; 368 | buildConfigurations = ( 369 | 816E120229AA525D0006F0B1 /* Debug */, 370 | 816E120329AA525D0006F0B1 /* Release */, 371 | ); 372 | defaultConfigurationIsVisible = 0; 373 | defaultConfigurationName = Release; 374 | }; 375 | 816E120429AA525D0006F0B1 /* Build configuration list for PBXNativeTarget "ExpandableTextExample" */ = { 376 | isa = XCConfigurationList; 377 | buildConfigurations = ( 378 | 816E120529AA525D0006F0B1 /* Debug */, 379 | 816E120629AA525D0006F0B1 /* Release */, 380 | ); 381 | defaultConfigurationIsVisible = 0; 382 | defaultConfigurationName = Release; 383 | }; 384 | /* End XCConfigurationList section */ 385 | 386 | /* Begin XCSwiftPackageProductDependency section */ 387 | 8196CFDA2A9B79AA00646D12 /* ExpandableText */ = { 388 | isa = XCSwiftPackageProductDependency; 389 | productName = ExpandableText; 390 | }; 391 | /* End XCSwiftPackageProductDependency section */ 392 | }; 393 | rootObject = 816E11EE29AA525B0006F0B1 /* Project object */; 394 | } 395 | -------------------------------------------------------------------------------- /Example/ExpandableTextExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/ExpandableTextExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/ExpandableTextExample/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 | -------------------------------------------------------------------------------- /Example/ExpandableTextExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/ExpandableTextExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ExpandableTextExample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // ExpandableTextExample 4 | // 5 | // Created by ned on 25/02/23. 6 | // 7 | 8 | import SwiftUI 9 | import ExpandableText 10 | 11 | struct ContentView: View { 12 | 13 | let loremIpsum = #"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."# 14 | 15 | var body: some View { 16 | ScrollView { 17 | VStack(alignment: .leading, spacing: 20) { 18 | ExpandableText(loremIpsum) 19 | .border(.red) 20 | 21 | ExpandableText(loremIpsum) 22 | .border(.red) 23 | .environment(\.layoutDirection, .rightToLeft) 24 | 25 | ExpandableText(loremIpsum) 26 | .font(.system(.headline, design: .rounded)) 27 | .foregroundColor(.secondary) 28 | .lineLimit(4) 29 | .moreButtonText("read more") 30 | .moreButtonFont(.system(.headline, design: .rounded).bold()) 31 | .moreButtonColor(.red) 32 | .expandAnimation(.easeInOut(duration: 2)) 33 | .trimMultipleNewlinesWhenTruncated(false) 34 | .enableCollapse(true) 35 | .border(.red) 36 | 37 | ExpandableText("**Markdown** is _supported_") 38 | .border(.red) 39 | 40 | ExpandableText(String(localized: "EXAMPLE_LOCALIZED_TEXT_KEY")) 41 | .border(.red) 42 | 43 | Spacer() 44 | } 45 | .padding() 46 | } 47 | } 48 | } 49 | 50 | struct ContentView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | ContentView() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/ExpandableTextExample/ExpandableTextExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpandableTextExampleApp.swift 3 | // ExpandableTextExample 4 | // 5 | // Created by ned on 25/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ExpandableTextExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/ExpandableTextExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | ExpandableTextExample 4 | 5 | Created by ned on 27/08/23. 6 | 7 | */ 8 | 9 | "EXAMPLE_LOCALIZED_TEXT_KEY" = "This is an example of a localized text. This is an example of a localized text. This is an example of a localized text. This is an example of a localized text. This is an example of a localized text. This is an example of a localized text. This is an example of a localized text. This is an example of a localized text. This is an example of a localized text."; 10 | -------------------------------------------------------------------------------- /Example/it.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | ExpandableTextExample 4 | 5 | Created by ned on 27/08/23. 6 | 7 | */ 8 | 9 | "EXAMPLE_LOCALIZED_TEXT_KEY" = "Questo è un esempio di un testo tradotto. Questo è un esempio di un testo tradotto. Questo è un esempio di un testo tradotto. Questo è un esempio di un testo tradotto. Questo è un esempio di un testo tradotto. Questo è un esempio di un testo tradotto. Questo è un esempio di un testo tradotto. Questo è un esempio di un testo tradotto. Questo è un esempio di un testo tradotto."; 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 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: "ExpandableText", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "ExpandableText", 15 | targets: ["ExpandableText"] 16 | ), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "ExpandableText", 27 | dependencies: [], 28 | path: "Sources" 29 | ), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExpandableText 2 | [![build](https://github.com/n3d1117/ExpandableText/actions/workflows/build.yml/badge.svg)](https://github.com/n3d1117/ExpandableText/actions/workflows/build.yml) 3 | [![swift-version](https://img.shields.io/badge/swift-5.7-orange.svg)](https://github.com/apple/swift/) 4 | [![ios-version](https://img.shields.io/badge/ios-13.0+-brightgreen.svg)](https://github.com/apple/ios/) 5 | [![xcode-version](https://img.shields.io/badge/xcode-14.2-blue)](https://developer.apple.com/xcode/) 6 | [![license](https://img.shields.io/badge/license-The%20Unlicense-yellow.svg)](LICENSE) 7 | 8 | An expandable text view that displays a truncated version of its contents with a "more" button that expands the view to show the full contents. 9 | 10 | iOS 13+ compatible, fully customizable, written in SwiftUI. 11 | 12 | ## Installation 13 | Available via the [Swift Package Manager](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app). Requires iOS 13+. 14 | 15 | ``` 16 | https://github.com/n3d1117/ExpandableText 17 | ``` 18 | 19 | ## Features 20 | - Customizable line limit 21 | - Customizable font, color, and `more` button appearance with SwiftUI-like modifiers 22 | - Automatically hide `more` button if the whole text fits within the view 23 | - Support right-to-left languages 24 | - Support re-collapsing text by tapping on expanded text body (by [@JThramer](https://github.com/JThramer)) 25 | - Support custom expand animation 26 | - Automatically trim multiple new lines when truncated (can be disabled) 27 | 28 | ## Usage 29 | 30 | ### Basic usage 31 | 32 | 33 | 51 | 55 | 56 |
34 | 35 | ```swift 36 | import ExpandableText 37 | 38 | let loremIpsum = """ 39 | Lorem ipsum dolor sit amet, consectetur adipiscing 40 | elit, sed do eiusmod tempor incididunt ut labore et 41 | dolore magna aliqua. Ut enim ad minim veniam, quis 42 | nostrud exercitation ullamco laboris nisi ut aliquip 43 | ex ea commodo consequat. Duis aute irure dolor in 44 | reprehenderit in voluptate velit esse cillum dolore 45 | eu fugiat nulla pariatur. 46 | """ 47 | 48 | ExpandableText(loremIpsum) 49 | ``` 50 | 52 | 53 | ![Basic usage demo](https://user-images.githubusercontent.com/11541888/221367314-5e59b284-41a9-43d2-9ac2-4d51ee3bc46b.png) 54 |
57 | 58 | ### Customization options 59 | 60 | 61 | 76 | 80 | 81 |
62 | 63 | ```swift 64 | ExpandableText(loremIpsum) 65 | .font(.headline) 66 | .foregroundColor(.secondary) 67 | .lineLimit(4) 68 | .moreButtonText("read more") 69 | .moreButtonFont(.headline.bold()) 70 | .moreButtonColor(.red) 71 | .enableCollapse(true) 72 | .expandAnimation(.easeInOut(duration: 2)) 73 | .trimMultipleNewlinesWhenTruncated(false) 74 | ``` 75 | 77 | 78 | ![Customization demo](https://user-images.githubusercontent.com/11541888/221367312-3062bd32-5eae-45d4-bf3a-0474985cb712.png) 79 |
82 | 83 | ## Credits 84 | - [NuPlay/ExpandableText](https://github.com/NuPlay/ExpandableText) for inspiration and some portions of code 85 | 86 | ## License 87 | Available under The Unlicense license. See [LICENSE](LICENSE) file for further information. 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Sources/ExpandableText/ExpandableText+Modifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpandableText+Modifiers.swift 3 | // 4 | // 5 | // Created by ned on 25/02/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public extension ExpandableText { 12 | 13 | /** 14 | Sets the font for the text in the `ExpandableText` instance. 15 | - Parameter font: The font to use for the text. Defaults to `body` 16 | - Returns: A new `ExpandableText` instance with the specified font applied. 17 | */ 18 | func font(_ font: Font) -> Self { 19 | var copy = self 20 | copy.font = font 21 | return copy 22 | } 23 | 24 | /** 25 | Sets the foreground color for the text in the `ExpandableText` instance. 26 | - Parameter color: The foreground color to use for the text. Defaults to `primary` 27 | - Returns: A new `ExpandableText` instance with the specified foreground color applied. 28 | */ 29 | func foregroundColor(_ color: Color) -> Self { 30 | var copy = self 31 | copy.color = color 32 | return copy 33 | } 34 | 35 | /** 36 | Sets the maximum number of lines to use for rendering the text in the `ExpandableText` instance. 37 | - Parameter limit: The maximum number of lines to use for rendering the text. Defaults to `3` 38 | - Returns: A new `ExpandableText` instance with the specified line limit applied. 39 | */ 40 | func lineLimit(_ limit: Int) -> Self { 41 | var copy = self 42 | copy.lineLimit = limit 43 | return copy 44 | } 45 | 46 | /** 47 | Sets the text to use for the "show more" button in the `ExpandableText` instance. 48 | - Parameter moreText: The text to use for the "show more" button. Defaults to `more` 49 | - Returns: A new `ExpandableText` instance with the specified "show more" button text applied. 50 | */ 51 | func moreButtonText(_ moreText: String) -> Self { 52 | var copy = self 53 | copy.moreButtonText = moreText 54 | return copy 55 | } 56 | 57 | /** 58 | Sets the font to use for the "show more" button in the `ExpandableText` instance. 59 | - Parameter font: The font to use for the "show more" button. Defaults to the same font as the text 60 | - Returns: A new `ExpandableText` instance with the specified "show more" button font applied. 61 | */ 62 | func moreButtonFont(_ font: Font) -> Self { 63 | var copy = self 64 | copy.moreButtonFont = font 65 | return copy 66 | } 67 | 68 | /** 69 | Sets the color to use for the "show more" button in the `ExpandableText` instance. 70 | - Parameter color: The color to use for the "show more" button. Defaults to `accentColor` 71 | - Returns: A new `ExpandableText` instance with the specified "show more" button color applied. 72 | */ 73 | func moreButtonColor(_ color: Color) -> Self { 74 | var copy = self 75 | copy.moreButtonColor = color 76 | return copy 77 | } 78 | 79 | /** 80 | Sets the animation to use when expanding the `ExpandableText` instance. 81 | - Parameter animation: The animation to use for the expansion. Defaults to `default` 82 | - Returns: A new `ExpandableText` instance with the specified expansion animation applied. 83 | */ 84 | func expandAnimation(_ animation: Animation) -> Self { 85 | var copy = self 86 | copy.expandAnimation = animation 87 | return copy 88 | } 89 | 90 | /** 91 | Enables collapsing behavior by tapping on the text body when the state is expanded. 92 | - Parameter value: Whether or not to enable collapse functionality. 93 | - Returns: A new `ExpandableText` instance with the specified collapse ability applied. 94 | */ 95 | func enableCollapse(_ value: Bool) -> Self { 96 | var copy = self 97 | copy.collapseEnabled = value 98 | return copy 99 | } 100 | 101 | /** 102 | Sets whether multiple consecutive newline characters should be trimmed when truncating the text in the `ExpandableText` instance. 103 | - Parameter value: A boolean value indicating whether to trim multiple consecutive newline characters. Defaults to `true` 104 | - Returns: A new `ExpandableText` instance with the specified trimming behavior applied. 105 | */ 106 | func trimMultipleNewlinesWhenTruncated(_ value: Bool) -> Self { 107 | var copy = self 108 | copy.trimMultipleNewlinesWhenTruncated = value 109 | return copy 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/ExpandableText/ExpandableText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpandableText.swift 3 | // ExpandableText 4 | // 5 | // Created by ned on 23/02/23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | /** 12 | An expandable text view that displays a truncated version of its contents with a "show more" button that expands the view to show the full contents. 13 | 14 | To create a new ExpandableText view, use the init method and provide the initial text string as a parameter. The text string will be automatically trimmed of any leading or trailing whitespace and newline characters. 15 | 16 | Example usage with default parameters: 17 | ```swift 18 | ExpandableText("Lorem ipsum dolor sit amet, consectetur adipiscing elit...") 19 | .font(.body) 20 | .foregroundColor(.primary) 21 | .lineLimit(3) 22 | .moreButtonText("more") 23 | .moreButtonColor(.accentColor) 24 | .expandAnimation(.default) 25 | .trimMultipleNewlinesWhenTruncated(true) 26 | ``` 27 | */ 28 | public struct ExpandableText: View { 29 | 30 | @State private var isExpanded: Bool = false 31 | @State private var isTruncated: Bool = false 32 | 33 | @State private var intrinsicSize: CGSize = .zero 34 | @State private var truncatedSize: CGSize = .zero 35 | @State private var moreTextSize: CGSize = .zero 36 | 37 | private let text: String 38 | internal var font: Font = .body 39 | internal var color: Color = .primary 40 | internal var lineLimit: Int = 3 41 | internal var moreButtonText: String = "more" 42 | internal var moreButtonFont: Font? 43 | internal var moreButtonColor: Color = .accentColor 44 | internal var expandAnimation: Animation = .default 45 | internal var collapseEnabled: Bool = false 46 | internal var trimMultipleNewlinesWhenTruncated: Bool = true 47 | 48 | /** 49 | Initializes a new `ExpandableText` instance with the specified text string, trimmed of any leading or trailing whitespace and newline characters. 50 | - Parameter text: The initial text string to display in the `ExpandableText` view. 51 | - Returns: A new `ExpandableText` instance with the specified text string and trimming applied. 52 | */ 53 | public init(_ text: String) { 54 | self.text = text.trimmingCharacters(in: .whitespacesAndNewlines) 55 | } 56 | 57 | public var body: some View { 58 | content 59 | .lineLimit(isExpanded ? nil : lineLimit) 60 | .applyingTruncationMask(size: moreTextSize, enabled: shouldShowMoreButton) 61 | .readSize { size in 62 | truncatedSize = size 63 | isTruncated = truncatedSize != intrinsicSize 64 | } 65 | .background( 66 | content 67 | .lineLimit(nil) 68 | .fixedSize(horizontal: false, vertical: true) 69 | .hidden() 70 | .readSize { size in 71 | intrinsicSize = size 72 | isTruncated = truncatedSize != intrinsicSize 73 | } 74 | ) 75 | .background( 76 | Text(moreButtonText) 77 | .font(moreButtonFont ?? font) 78 | .hidden() 79 | .readSize { moreTextSize = $0 } 80 | ) 81 | .contentShape(Rectangle()) 82 | .onTapGesture { 83 | if (isExpanded && collapseEnabled) || 84 | shouldShowMoreButton { 85 | withAnimation(expandAnimation) { isExpanded.toggle() } 86 | } 87 | } 88 | .modifier(OverlayAdapter(alignment: .trailingLastTextBaseline, view: { 89 | if shouldShowMoreButton { 90 | Button { 91 | withAnimation(expandAnimation) { isExpanded.toggle() } 92 | } label: { 93 | Text(moreButtonText) 94 | .font(moreButtonFont ?? font) 95 | .foregroundColor(moreButtonColor) 96 | } 97 | } 98 | })) 99 | } 100 | 101 | private var content: some View { 102 | Text(.init( 103 | trimMultipleNewlinesWhenTruncated 104 | ? (shouldShowMoreButton ? textTrimmingDoubleNewlines : text) 105 | : text 106 | )) 107 | .font(font) 108 | .foregroundColor(color) 109 | .frame(maxWidth: .infinity, alignment: .leading) 110 | } 111 | 112 | private var shouldShowMoreButton: Bool { 113 | !isExpanded && isTruncated 114 | } 115 | 116 | private var textTrimmingDoubleNewlines: String { 117 | text.replacingOccurrences(of: #"\n\s*\n"#, with: "\n", options: .regularExpression) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/Utilities/OverlayAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayAdapter.swift 3 | // 4 | // 5 | // Created by ned on 25/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | internal struct OverlayAdapter: ViewModifier { 11 | let alignment: Alignment 12 | let view: () -> V 13 | 14 | init(alignment: Alignment, @ViewBuilder view: @escaping () -> V) { 15 | self.alignment = alignment 16 | self.view = view 17 | } 18 | 19 | func body(content: Content) -> some View { 20 | if #available(iOS 15.0, *) { 21 | content.overlay(alignment: alignment, content: view) 22 | } else { 23 | content.overlay(view(), alignment: alignment) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Utilities/TruncationTextMask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TruncationTextMask.swift 3 | // 4 | // 5 | // Created by ned on 25/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | private struct TruncationTextMask: ViewModifier { 11 | 12 | let size: CGSize 13 | let enabled: Bool 14 | 15 | @Environment(\.layoutDirection) private var layoutDirection 16 | 17 | func body(content: Content) -> some View { 18 | if enabled { 19 | content 20 | .mask( 21 | VStack(spacing: 0) { 22 | Rectangle() 23 | HStack(spacing: 0) { 24 | Rectangle() 25 | HStack(spacing: 0) { 26 | LinearGradient( 27 | gradient: Gradient(stops: [ 28 | Gradient.Stop(color: .black, location: 0), 29 | Gradient.Stop(color: .clear, location: 0.9) 30 | ]), 31 | startPoint: layoutDirection == .rightToLeft ? .trailing : .leading, 32 | endPoint: layoutDirection == .rightToLeft ? .leading : .trailing 33 | ) 34 | .frame(width: size.width, height: size.height) 35 | 36 | Rectangle() 37 | .foregroundColor(.clear) 38 | .frame(width: size.width) 39 | } 40 | }.frame(height: size.height) 41 | } 42 | ) 43 | } else { 44 | content 45 | .fixedSize(horizontal: false, vertical: true) 46 | } 47 | } 48 | } 49 | 50 | internal extension View { 51 | func applyingTruncationMask(size: CGSize, enabled: Bool) -> some View { 52 | modifier(TruncationTextMask(size: size, enabled: enabled)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Utilities/ViewSizeReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewSizeReader.swift 3 | // 4 | // 5 | // Created by ned on 25/02/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // https://www.fivestars.blog/articles/swiftui-share-layout-information/ 11 | private struct SizePreferenceKey: PreferenceKey { 12 | static var defaultValue: CGSize = .zero 13 | static func reduce(value: inout CGSize, nextValue: () -> CGSize) {} 14 | } 15 | 16 | internal extension View { 17 | func readSize(onChange: @escaping (CGSize) -> Void) -> some View { 18 | background( 19 | GeometryReader { geometryProxy in 20 | Color.clear 21 | .preference(key: SizePreferenceKey.self, value: geometryProxy.size) 22 | } 23 | ) 24 | .onPreferenceChange(SizePreferenceKey.self, perform: onChange) 25 | } 26 | } 27 | --------------------------------------------------------------------------------