├── README.md ├── TypingAnimationSwiftUI.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── TypingAnimationSwiftUI.xcscheme └── xcuserdata │ └── songyeepark.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── TypingAnimationSwiftUI ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── LoopingTextExampleView.swift ├── OnboardingExampleView.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── StaticTextExampleView.swift ├── TypingAnimationSwiftUI.entitlements ├── TypingAnimationSwiftUIApp.swift └── TypingTextUI ├── AnyViewModifier.swift ├── Constants.swift ├── EmptyModifier.swift ├── MediumTypingTextStyle.swift ├── TypingTextSettings.swift ├── TypingTextStyle.swift └── TypingTextView.swift /README.md: -------------------------------------------------------------------------------- 1 | # TypingAnimationSwiftUI 2 | Add a typewriter style of animation to a SwiftUI Text, with customizable settings (typing speed, backspacing, haptic feedback). 3 | 4 | ## Previews 5 | https://github.com/sndwvv/TypingAnimationSwiftUI/assets/108448821/1281e058-eb13-4744-901a-f5078490f1a2 6 | 7 | https://github.com/sndwvv/TypingAnimationSwiftUI/assets/108448821/82088ac6-cd0b-45f5-a9ae-809fdab4e2f3 8 | 9 | ## Implementation 10 | 11 | Simply initialize the TypingTextView with a text string: 12 | ```swift 13 | TypingTextView(text: "Hello, World!") 14 | ``` 15 | 16 | Adjust settings as needed. 17 | Make use of the completion block to do something after the animation is complete. 18 | 19 | ```swift 20 | let fullText = "Hello, World!" 21 | let settings = TypingTextSettings( 22 | typingSpeed: 0.04, 23 | backspaceSpeed: 0.01, 24 | delaySeconds: 1.0, 25 | isBackspaceEnabled: false, 26 | isHapticsEnabled: false 27 | ) 28 | ``` 29 | 30 | ```swift 31 | var body: some View { 32 | VStack { 33 | TypingTextView( 34 | text: fullText, 35 | settings: settings 36 | ) { 37 | // animation complete 38 | } 39 | .font(.system(size: 12, weight: .heavy)) 40 | .foregroundColor(.primary) 41 | } 42 | } 43 | ``` 44 | 45 | | TypingTextSetting | Description | 46 | | ------------- | ------------- | 47 | | typingSpeed | Adjusts typing speed | 48 | | backspaceSpeed | Adjust backspacing / reverse typing speed | 49 | | delaySeconds | Adds a delay after backspacing is complete | 50 | | style | TypingTextStyle: add styling using one of the pre-made ViewModifiers | 51 | | isBackspaceEnabled | If true, text is fully typed then backspaced / removed | 52 | | isHapticsEnabled | Adds a haptic feedback every time a character is added or removed | 53 | 54 | Pass an array of texts and have it loop continuously (refer to LoopingTextExample.swift) 55 | 56 | ```swift 57 | struct LoopingTextExampleView: View { 58 | 59 | let texts = Constants.quotes 60 | let settings = TypingTextSettings( 61 | typingSpeed: 0.04, 62 | backspaceSpeed: 0.01, 63 | delaySeconds: 1.0, 64 | style: .medium, 65 | isBackspaceEnabled: true, 66 | isHapticsEnabled: true 67 | ) 68 | 69 | @State private var currentTypingIndex = 0 70 | 71 | var body: some View { 72 | VStack { 73 | TypingTextView( 74 | text: texts[currentTypingIndex % texts.count], 75 | settings: settings) { 76 | // Go to next text when the animation finishes 77 | currentTypingIndex += 1 78 | } 79 | .id(currentTypingIndex) 80 | .frame(maxWidth: .infinity, maxHeight: .infinity) 81 | .padding(.horizontal) 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ## Styling 88 | 89 | Add modifiers to the end as usual: 90 | ```swift 91 | TypingTextView(text: "Hello World") 92 | .foregroundColor(.brown) 93 | .font(.system(size: 20, weight: .bold)) 94 | ``` 95 | 96 | Or create your own modifier and add to the settings as the TypingTextStyle: 97 | ```swift 98 | enum TypingTextStyle { 99 | case plain 100 | case medium 101 | // add more 102 | 103 | func modifier() -> some ViewModifier { 104 | switch self { 105 | case .medium: 106 | return AnyViewModifier(MediumTypingTextStyle()) 107 | default: 108 | return AnyViewModifier(EmptyModifier()) 109 | } 110 | } 111 | } 112 | ``` 113 | ## Upcoming 114 | * macOS compatibitlity 115 | 116 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1EA941E72A3845030034E938 /* TypingAnimationSwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA941E62A3845030034E938 /* TypingAnimationSwiftUIApp.swift */; }; 11 | 1EA941E92A3845030034E938 /* LoopingTextExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA941E82A3845030034E938 /* LoopingTextExampleView.swift */; }; 12 | 1EA941EB2A3845030034E938 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EA941EA2A3845030034E938 /* Assets.xcassets */; }; 13 | 1EA941EF2A3845030034E938 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EA941EE2A3845030034E938 /* Preview Assets.xcassets */; }; 14 | 1EA941F62A3845340034E938 /* TypingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA941F52A3845340034E938 /* TypingTextView.swift */; }; 15 | 1EA941FA2A3847B50034E938 /* AnyViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA941F92A3847B50034E938 /* AnyViewModifier.swift */; }; 16 | 1EA941FE2A3847F20034E938 /* EmptyModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA941FD2A3847F20034E938 /* EmptyModifier.swift */; }; 17 | 1EA942002A3848620034E938 /* MediumTypingTextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA941FF2A3848620034E938 /* MediumTypingTextStyle.swift */; }; 18 | 1EA942022A3848870034E938 /* TypingTextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA942012A3848870034E938 /* TypingTextStyle.swift */; }; 19 | 1EA942042A3848C20034E938 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA942032A3848C20034E938 /* Constants.swift */; }; 20 | 1EA942062A3851E20034E938 /* StaticTextExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA942052A3851E20034E938 /* StaticTextExampleView.swift */; }; 21 | 1EA942082A385BDF0034E938 /* TypingTextSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA942072A385BDF0034E938 /* TypingTextSettings.swift */; }; 22 | 1EA9420A2A3AE8360034E938 /* OnboardingExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA942092A3AE8360034E938 /* OnboardingExampleView.swift */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 1EA941E32A3845030034E938 /* TypingAnimationSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TypingAnimationSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 1EA941E62A3845030034E938 /* TypingAnimationSwiftUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingAnimationSwiftUIApp.swift; sourceTree = ""; }; 28 | 1EA941E82A3845030034E938 /* LoopingTextExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopingTextExampleView.swift; sourceTree = ""; }; 29 | 1EA941EA2A3845030034E938 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | 1EA941EC2A3845030034E938 /* TypingAnimationSwiftUI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TypingAnimationSwiftUI.entitlements; sourceTree = ""; }; 31 | 1EA941EE2A3845030034E938 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 32 | 1EA941F52A3845340034E938 /* TypingTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingTextView.swift; sourceTree = ""; }; 33 | 1EA941F92A3847B50034E938 /* AnyViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyViewModifier.swift; sourceTree = ""; }; 34 | 1EA941FD2A3847F20034E938 /* EmptyModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyModifier.swift; sourceTree = ""; }; 35 | 1EA941FF2A3848620034E938 /* MediumTypingTextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediumTypingTextStyle.swift; sourceTree = ""; }; 36 | 1EA942012A3848870034E938 /* TypingTextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingTextStyle.swift; sourceTree = ""; }; 37 | 1EA942032A3848C20034E938 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 38 | 1EA942052A3851E20034E938 /* StaticTextExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTextExampleView.swift; sourceTree = ""; }; 39 | 1EA942072A385BDF0034E938 /* TypingTextSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingTextSettings.swift; sourceTree = ""; }; 40 | 1EA942092A3AE8360034E938 /* OnboardingExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingExampleView.swift; sourceTree = ""; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | 1EA941E02A3845030034E938 /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | 1EA941DA2A3845030034E938 = { 55 | isa = PBXGroup; 56 | children = ( 57 | 1EA941E52A3845030034E938 /* TypingAnimationSwiftUI */, 58 | 1EA941E42A3845030034E938 /* Products */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | 1EA941E42A3845030034E938 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 1EA941E32A3845030034E938 /* TypingAnimationSwiftUI.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | 1EA941E52A3845030034E938 /* TypingAnimationSwiftUI */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 1EA941E62A3845030034E938 /* TypingAnimationSwiftUIApp.swift */, 74 | 1EA942052A3851E20034E938 /* StaticTextExampleView.swift */, 75 | 1EA941E82A3845030034E938 /* LoopingTextExampleView.swift */, 76 | 1EA942092A3AE8360034E938 /* OnboardingExampleView.swift */, 77 | 1EA941F82A3847A60034E938 /* TypingTextUI */, 78 | 1EA941EA2A3845030034E938 /* Assets.xcassets */, 79 | 1EA941EC2A3845030034E938 /* TypingAnimationSwiftUI.entitlements */, 80 | 1EA941ED2A3845030034E938 /* Preview Content */, 81 | ); 82 | path = TypingAnimationSwiftUI; 83 | sourceTree = ""; 84 | }; 85 | 1EA941ED2A3845030034E938 /* Preview Content */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 1EA941EE2A3845030034E938 /* Preview Assets.xcassets */, 89 | ); 90 | path = "Preview Content"; 91 | sourceTree = ""; 92 | }; 93 | 1EA941F82A3847A60034E938 /* TypingTextUI */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 1EA941F52A3845340034E938 /* TypingTextView.swift */, 97 | 1EA942012A3848870034E938 /* TypingTextStyle.swift */, 98 | 1EA942072A385BDF0034E938 /* TypingTextSettings.swift */, 99 | 1EA941F92A3847B50034E938 /* AnyViewModifier.swift */, 100 | 1EA941FD2A3847F20034E938 /* EmptyModifier.swift */, 101 | 1EA941FF2A3848620034E938 /* MediumTypingTextStyle.swift */, 102 | 1EA942032A3848C20034E938 /* Constants.swift */, 103 | ); 104 | path = TypingTextUI; 105 | sourceTree = ""; 106 | }; 107 | /* End PBXGroup section */ 108 | 109 | /* Begin PBXNativeTarget section */ 110 | 1EA941E22A3845030034E938 /* TypingAnimationSwiftUI */ = { 111 | isa = PBXNativeTarget; 112 | buildConfigurationList = 1EA941F22A3845030034E938 /* Build configuration list for PBXNativeTarget "TypingAnimationSwiftUI" */; 113 | buildPhases = ( 114 | 1EA941DF2A3845030034E938 /* Sources */, 115 | 1EA941E02A3845030034E938 /* Frameworks */, 116 | 1EA941E12A3845030034E938 /* Resources */, 117 | ); 118 | buildRules = ( 119 | ); 120 | dependencies = ( 121 | ); 122 | name = TypingAnimationSwiftUI; 123 | productName = TypingAnimationSwiftUI; 124 | productReference = 1EA941E32A3845030034E938 /* TypingAnimationSwiftUI.app */; 125 | productType = "com.apple.product-type.application"; 126 | }; 127 | /* End PBXNativeTarget section */ 128 | 129 | /* Begin PBXProject section */ 130 | 1EA941DB2A3845030034E938 /* Project object */ = { 131 | isa = PBXProject; 132 | attributes = { 133 | BuildIndependentTargetsInParallel = 1; 134 | LastSwiftUpdateCheck = 1430; 135 | LastUpgradeCheck = 1430; 136 | TargetAttributes = { 137 | 1EA941E22A3845030034E938 = { 138 | CreatedOnToolsVersion = 14.3; 139 | }; 140 | }; 141 | }; 142 | buildConfigurationList = 1EA941DE2A3845030034E938 /* Build configuration list for PBXProject "TypingAnimationSwiftUI" */; 143 | compatibilityVersion = "Xcode 14.0"; 144 | developmentRegion = en; 145 | hasScannedForEncodings = 0; 146 | knownRegions = ( 147 | en, 148 | Base, 149 | ); 150 | mainGroup = 1EA941DA2A3845030034E938; 151 | productRefGroup = 1EA941E42A3845030034E938 /* Products */; 152 | projectDirPath = ""; 153 | projectRoot = ""; 154 | targets = ( 155 | 1EA941E22A3845030034E938 /* TypingAnimationSwiftUI */, 156 | ); 157 | }; 158 | /* End PBXProject section */ 159 | 160 | /* Begin PBXResourcesBuildPhase section */ 161 | 1EA941E12A3845030034E938 /* Resources */ = { 162 | isa = PBXResourcesBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | 1EA941EF2A3845030034E938 /* Preview Assets.xcassets in Resources */, 166 | 1EA941EB2A3845030034E938 /* Assets.xcassets in Resources */, 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXResourcesBuildPhase section */ 171 | 172 | /* Begin PBXSourcesBuildPhase section */ 173 | 1EA941DF2A3845030034E938 /* Sources */ = { 174 | isa = PBXSourcesBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | 1EA941E92A3845030034E938 /* LoopingTextExampleView.swift in Sources */, 178 | 1EA942002A3848620034E938 /* MediumTypingTextStyle.swift in Sources */, 179 | 1EA9420A2A3AE8360034E938 /* OnboardingExampleView.swift in Sources */, 180 | 1EA941F62A3845340034E938 /* TypingTextView.swift in Sources */, 181 | 1EA942022A3848870034E938 /* TypingTextStyle.swift in Sources */, 182 | 1EA941FE2A3847F20034E938 /* EmptyModifier.swift in Sources */, 183 | 1EA941FA2A3847B50034E938 /* AnyViewModifier.swift in Sources */, 184 | 1EA942082A385BDF0034E938 /* TypingTextSettings.swift in Sources */, 185 | 1EA941E72A3845030034E938 /* TypingAnimationSwiftUIApp.swift in Sources */, 186 | 1EA942042A3848C20034E938 /* Constants.swift in Sources */, 187 | 1EA942062A3851E20034E938 /* StaticTextExampleView.swift in Sources */, 188 | ); 189 | runOnlyForDeploymentPostprocessing = 0; 190 | }; 191 | /* End PBXSourcesBuildPhase section */ 192 | 193 | /* Begin XCBuildConfiguration section */ 194 | 1EA941F02A3845030034E938 /* Debug */ = { 195 | isa = XCBuildConfiguration; 196 | buildSettings = { 197 | ALWAYS_SEARCH_USER_PATHS = NO; 198 | CLANG_ANALYZER_NONNULL = YES; 199 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 200 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 201 | CLANG_ENABLE_MODULES = YES; 202 | CLANG_ENABLE_OBJC_ARC = YES; 203 | CLANG_ENABLE_OBJC_WEAK = YES; 204 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 205 | CLANG_WARN_BOOL_CONVERSION = YES; 206 | CLANG_WARN_COMMA = YES; 207 | CLANG_WARN_CONSTANT_CONVERSION = YES; 208 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 209 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 210 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 217 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 219 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 220 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 221 | CLANG_WARN_STRICT_PROTOTYPES = YES; 222 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 223 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 224 | CLANG_WARN_UNREACHABLE_CODE = YES; 225 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 226 | COPY_PHASE_STRIP = NO; 227 | DEBUG_INFORMATION_FORMAT = dwarf; 228 | ENABLE_STRICT_OBJC_MSGSEND = YES; 229 | ENABLE_TESTABILITY = YES; 230 | GCC_C_LANGUAGE_STANDARD = gnu11; 231 | GCC_DYNAMIC_NO_PIC = NO; 232 | GCC_NO_COMMON_BLOCKS = YES; 233 | GCC_OPTIMIZATION_LEVEL = 0; 234 | GCC_PREPROCESSOR_DEFINITIONS = ( 235 | "DEBUG=1", 236 | "$(inherited)", 237 | ); 238 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 239 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 240 | GCC_WARN_UNDECLARED_SELECTOR = YES; 241 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 242 | GCC_WARN_UNUSED_FUNCTION = YES; 243 | GCC_WARN_UNUSED_VARIABLE = YES; 244 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 245 | MTL_FAST_MATH = YES; 246 | ONLY_ACTIVE_ARCH = YES; 247 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 248 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 249 | }; 250 | name = Debug; 251 | }; 252 | 1EA941F12A3845030034E938 /* Release */ = { 253 | isa = XCBuildConfiguration; 254 | buildSettings = { 255 | ALWAYS_SEARCH_USER_PATHS = NO; 256 | CLANG_ANALYZER_NONNULL = YES; 257 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 258 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 259 | CLANG_ENABLE_MODULES = YES; 260 | CLANG_ENABLE_OBJC_ARC = YES; 261 | CLANG_ENABLE_OBJC_WEAK = YES; 262 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 263 | CLANG_WARN_BOOL_CONVERSION = YES; 264 | CLANG_WARN_COMMA = YES; 265 | CLANG_WARN_CONSTANT_CONVERSION = YES; 266 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 267 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 268 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 269 | CLANG_WARN_EMPTY_BODY = YES; 270 | CLANG_WARN_ENUM_CONVERSION = YES; 271 | CLANG_WARN_INFINITE_RECURSION = YES; 272 | CLANG_WARN_INT_CONVERSION = YES; 273 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 274 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 275 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 276 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 277 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 278 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 279 | CLANG_WARN_STRICT_PROTOTYPES = YES; 280 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 281 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 282 | CLANG_WARN_UNREACHABLE_CODE = YES; 283 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 284 | COPY_PHASE_STRIP = NO; 285 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 286 | ENABLE_NS_ASSERTIONS = NO; 287 | ENABLE_STRICT_OBJC_MSGSEND = YES; 288 | GCC_C_LANGUAGE_STANDARD = gnu11; 289 | GCC_NO_COMMON_BLOCKS = YES; 290 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 291 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 292 | GCC_WARN_UNDECLARED_SELECTOR = YES; 293 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 294 | GCC_WARN_UNUSED_FUNCTION = YES; 295 | GCC_WARN_UNUSED_VARIABLE = YES; 296 | MTL_ENABLE_DEBUG_INFO = NO; 297 | MTL_FAST_MATH = YES; 298 | SWIFT_COMPILATION_MODE = wholemodule; 299 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 300 | }; 301 | name = Release; 302 | }; 303 | 1EA941F32A3845030034E938 /* Debug */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 307 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 308 | CODE_SIGN_ENTITLEMENTS = TypingAnimationSwiftUI/TypingAnimationSwiftUI.entitlements; 309 | CODE_SIGN_STYLE = Automatic; 310 | CURRENT_PROJECT_VERSION = 1; 311 | DEVELOPMENT_ASSET_PATHS = "\"TypingAnimationSwiftUI/Preview Content\""; 312 | DEVELOPMENT_TEAM = 93MY63J6YG; 313 | ENABLE_HARDENED_RUNTIME = YES; 314 | ENABLE_PREVIEWS = YES; 315 | GENERATE_INFOPLIST_FILE = YES; 316 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 317 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 318 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 319 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 320 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 321 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 322 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 323 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 324 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 325 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 326 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 327 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 328 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 329 | MACOSX_DEPLOYMENT_TARGET = 13.2; 330 | MARKETING_VERSION = 1.0; 331 | PRODUCT_BUNDLE_IDENTIFIER = co.songpark.TypingAnimationSwiftUI; 332 | PRODUCT_NAME = "$(TARGET_NAME)"; 333 | SDKROOT = auto; 334 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 335 | SUPPORTS_MACCATALYST = NO; 336 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 337 | SWIFT_EMIT_LOC_STRINGS = YES; 338 | SWIFT_VERSION = 5.0; 339 | TARGETED_DEVICE_FAMILY = "1,2"; 340 | }; 341 | name = Debug; 342 | }; 343 | 1EA941F42A3845030034E938 /* Release */ = { 344 | isa = XCBuildConfiguration; 345 | buildSettings = { 346 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 347 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 348 | CODE_SIGN_ENTITLEMENTS = TypingAnimationSwiftUI/TypingAnimationSwiftUI.entitlements; 349 | CODE_SIGN_STYLE = Automatic; 350 | CURRENT_PROJECT_VERSION = 1; 351 | DEVELOPMENT_ASSET_PATHS = "\"TypingAnimationSwiftUI/Preview Content\""; 352 | DEVELOPMENT_TEAM = 93MY63J6YG; 353 | ENABLE_HARDENED_RUNTIME = YES; 354 | ENABLE_PREVIEWS = YES; 355 | GENERATE_INFOPLIST_FILE = YES; 356 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 357 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 358 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 359 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 360 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 361 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 362 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 363 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 364 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 365 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 366 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 367 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 368 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 369 | MACOSX_DEPLOYMENT_TARGET = 13.2; 370 | MARKETING_VERSION = 1.0; 371 | PRODUCT_BUNDLE_IDENTIFIER = co.songpark.TypingAnimationSwiftUI; 372 | PRODUCT_NAME = "$(TARGET_NAME)"; 373 | SDKROOT = auto; 374 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 375 | SUPPORTS_MACCATALYST = NO; 376 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 377 | SWIFT_EMIT_LOC_STRINGS = YES; 378 | SWIFT_VERSION = 5.0; 379 | TARGETED_DEVICE_FAMILY = "1,2"; 380 | }; 381 | name = Release; 382 | }; 383 | /* End XCBuildConfiguration section */ 384 | 385 | /* Begin XCConfigurationList section */ 386 | 1EA941DE2A3845030034E938 /* Build configuration list for PBXProject "TypingAnimationSwiftUI" */ = { 387 | isa = XCConfigurationList; 388 | buildConfigurations = ( 389 | 1EA941F02A3845030034E938 /* Debug */, 390 | 1EA941F12A3845030034E938 /* Release */, 391 | ); 392 | defaultConfigurationIsVisible = 0; 393 | defaultConfigurationName = Release; 394 | }; 395 | 1EA941F22A3845030034E938 /* Build configuration list for PBXNativeTarget "TypingAnimationSwiftUI" */ = { 396 | isa = XCConfigurationList; 397 | buildConfigurations = ( 398 | 1EA941F32A3845030034E938 /* Debug */, 399 | 1EA941F42A3845030034E938 /* Release */, 400 | ); 401 | defaultConfigurationIsVisible = 0; 402 | defaultConfigurationName = Release; 403 | }; 404 | /* End XCConfigurationList section */ 405 | }; 406 | rootObject = 1EA941DB2A3845030034E938 /* Project object */; 407 | } 408 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI.xcodeproj/xcshareddata/xcschemes/TypingAnimationSwiftUI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI.xcodeproj/xcuserdata/songyeepark.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | TypingAnimationSwiftUI.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 1EA941E22A3845030034E938 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/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 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/LoopingTextExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoopingTextExampleView.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LoopingTextExampleView: View { 11 | 12 | let texts = Constants.quotes 13 | let settings = TypingTextSettings( 14 | typingSpeed: 0.04, 15 | backspaceSpeed: 0.01, 16 | delaySeconds: 1.0, 17 | style: .medium, 18 | isBackspaceEnabled: true, 19 | isHapticsEnabled: true 20 | ) 21 | 22 | @State private var currentTypingIndex = 0 23 | 24 | var body: some View { 25 | VStack { 26 | TypingTextView( 27 | text: texts[currentTypingIndex % texts.count], 28 | settings: settings) { 29 | 30 | // Completion Block 31 | // Go to next text when the animation finishes 32 | currentTypingIndex += 1 33 | } 34 | .id(currentTypingIndex) 35 | .frame(maxWidth: .infinity, maxHeight: .infinity) 36 | .padding(.horizontal) 37 | } 38 | } 39 | 40 | } 41 | 42 | struct LoopingTextExampleView_Previews: PreviewProvider { 43 | static var previews: some View { 44 | LoopingTextExampleView() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/OnboardingExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingExampleView.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/15. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OnboardingExampleView: View { 11 | 12 | @State var name: String = "" 13 | @State var showNamePromptText: Bool = false 14 | @State var showNameTextField: Bool = false 15 | @State var becomeFirstResponder: Bool = false 16 | 17 | var body: some View { 18 | ZStack { 19 | VStack(spacing: 4) { 20 | marcusGreetingView 21 | 22 | if showNamePromptText { 23 | namePromptTextView 24 | } 25 | 26 | if showNameTextField { 27 | nameTextField 28 | } 29 | } 30 | } 31 | } 32 | 33 | var marcusGreetingView: some View { 34 | let greetingText = "Greetings, I am Marcus." 35 | let settings = TypingTextSettings( 36 | typingSpeed: 0.05, 37 | style: .medium, 38 | isHapticsEnabled: true 39 | ) 40 | return TypingTextView( 41 | text: greetingText, 42 | settings: settings 43 | ) { 44 | withAnimation(.easeInOut) { 45 | showNamePromptText = true 46 | } 47 | } 48 | } 49 | 50 | var namePromptTextView: some View { 51 | let promptText = "What is your name?" 52 | let settings = TypingTextSettings( 53 | typingSpeed: 0.05, 54 | style: .medium, 55 | isHapticsEnabled: true 56 | ) 57 | return TypingTextView( 58 | text: promptText, 59 | settings: settings 60 | ) { 61 | withAnimation(.easeInOut) { 62 | showNameTextField = true 63 | } 64 | } 65 | } 66 | 67 | var nameTextField: some View { 68 | return OnboardingTextField( 69 | text: $name, 70 | becomeFirstResponder: $becomeFirstResponder 71 | ) 72 | .multilineTextAlignment(.center) 73 | .frame(height: 50) 74 | .padding(.horizontal, 50) 75 | .onAppear { 76 | withAnimation(.easeInOut) { 77 | becomeFirstResponder = true 78 | } 79 | } 80 | } 81 | } 82 | 83 | struct OnboardingExampleView_Previews: PreviewProvider { 84 | static var previews: some View { 85 | OnboardingExampleView() 86 | } 87 | } 88 | 89 | // MARK: OnboardingTextField 90 | 91 | struct OnboardingTextField: UIViewRepresentable { 92 | typealias UIViewType = UITextField 93 | 94 | @Binding var text: String 95 | @Binding var becomeFirstResponder: Bool 96 | 97 | func makeUIView(context: Context) -> UITextField { 98 | let textField = UITextField() 99 | textField.textAlignment = .center 100 | return textField 101 | } 102 | 103 | func updateUIView(_ textField: UITextField, context: Context) { 104 | if self.becomeFirstResponder { 105 | DispatchQueue.main.async { 106 | textField.becomeFirstResponder() 107 | self.becomeFirstResponder = false 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/StaticTextExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaticTextExampleView.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StaticTextExampleView: View { 11 | 12 | let fullText = "Hello, World!" 13 | let settings = TypingTextSettings( 14 | typingSpeed: 0.04, 15 | backspaceSpeed: 0.01, 16 | delaySeconds: 1.0, 17 | isBackspaceEnabled: false, 18 | isHapticsEnabled: false 19 | ) 20 | 21 | var body: some View { 22 | VStack { 23 | TypingTextView( 24 | text: fullText, 25 | settings: settings 26 | ) { 27 | // animation complete 28 | } 29 | .font(.system(size: 12, weight: .heavy)) 30 | .foregroundColor(.primary) 31 | } 32 | } 33 | 34 | } 35 | 36 | struct StaticTextExampleView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | StaticTextExampleView() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/TypingAnimationSwiftUI.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/TypingAnimationSwiftUIApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypingAnimationSwiftUIApp.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct TypingAnimationSwiftUIApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | LoopingTextExampleView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/TypingTextUI/AnyViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyViewModifier.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AnyViewModifier: ViewModifier { 11 | private let modifier: (AnyView) -> AnyView 12 | 13 | init(_ modifier: Modifier) { 14 | self.modifier = { view in AnyView(view.modifier(modifier)) } 15 | } 16 | 17 | func body(content: Content) -> some View { 18 | modifier(AnyView(content)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/TypingTextUI/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/13. 6 | // 7 | 8 | struct Constants { 9 | 10 | static let quotes = [ 11 | "Endure, persevere, conquer: the essence of a stoic soul.", 12 | "Embrace adversity, for it molds the character of resilience.", 13 | "In every obstacle lies an opportunity for growth and wisdom.", 14 | "Find solace in the present moment; it holds infinite potential.", 15 | "Master your mind, and you shall conquer the world within.", 16 | "Let go of what you cannot control, and find inner freedom.", 17 | "Embrace discomfort to unlock the path to true strength.", 18 | "Accept the impermanence of life, and cherish each fleeting moment.", 19 | "Cultivate virtues, and become a beacon of light in this world.", 20 | "Choose your thoughts wisely, for they shape your reality." 21 | ] 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/TypingTextUI/EmptyModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyModifier.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EmptyModifier: ViewModifier { 11 | func body(content: Content) -> some View { 12 | content 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/TypingTextUI/MediumTypingTextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginTypingTextStyle.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MediumTypingTextStyle: ViewModifier { 11 | func body(content: Content) -> some View { 12 | content 13 | .font(.system(size: 20, weight: .bold)) 14 | .foregroundColor(.accentColor) 15 | .multilineTextAlignment(.center) 16 | .lineLimit(nil) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/TypingTextUI/TypingTextSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypingTextSettings.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/13. 6 | // 7 | 8 | struct TypingTextSettings { 9 | 10 | let typingSpeed: Double 11 | let backspaceSpeed: Double 12 | let delaySeconds: Double 13 | let style: TypingTextStyle 14 | let isBackspaceEnabled: Bool 15 | let isHapticsEnabled: Bool 16 | 17 | init(typingSpeed: Double = 0.05, 18 | backspaceSpeed: Double = 0.02, 19 | delaySeconds: Double = 3.0, 20 | style: TypingTextStyle = .plain, 21 | isBackspaceEnabled: Bool = false, 22 | isHapticsEnabled: Bool = false) 23 | { 24 | self.typingSpeed = typingSpeed 25 | self.backspaceSpeed = backspaceSpeed 26 | self.delaySeconds = delaySeconds 27 | self.style = style 28 | self.isBackspaceEnabled = isBackspaceEnabled 29 | self.isHapticsEnabled = isHapticsEnabled 30 | } 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/TypingTextUI/TypingTextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypingTextStyle.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum TypingTextStyle { 11 | case plain 12 | case medium 13 | // add more 14 | 15 | func modifier() -> some ViewModifier { 16 | switch self { 17 | case .medium: 18 | return AnyViewModifier(MediumTypingTextStyle()) 19 | default: 20 | return AnyViewModifier(EmptyModifier()) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TypingAnimationSwiftUI/TypingTextUI/TypingTextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypingTextView.swift 3 | // TypingAnimationSwiftUI 4 | // 5 | // Created by Songyee Park on 2023/06/13. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | struct TypingTextView: View { 12 | 13 | let text: String 14 | let settings: TypingTextSettings 15 | let completion: (() -> Void)? 16 | 17 | @State private var displayText = "" 18 | @State private var timer: AnyCancellable? 19 | @State private var isTyping = true 20 | @State private var isDelaying = false 21 | 22 | init(text: String, 23 | settings: TypingTextSettings = TypingTextSettings(), 24 | completion: (() -> Void)? = nil) 25 | { 26 | self.text = text 27 | self.settings = settings 28 | self.completion = completion 29 | } 30 | 31 | var body: some View { 32 | Text(displayText) 33 | .modifier(settings.style.modifier()) 34 | .onAppear(perform: startTyping) 35 | .onDisappear(perform: cancelTyping) 36 | } 37 | 38 | private func startTyping() { 39 | isTyping = true 40 | isDelaying = false 41 | startTimer(with: settings.typingSpeed, action: appendNextCharacter) 42 | } 43 | 44 | private func cancelTyping() { 45 | timer?.cancel() 46 | } 47 | 48 | private func startBackspacing() { 49 | if settings.isBackspaceEnabled { 50 | isTyping = false 51 | isDelaying = false 52 | startTimer(with: settings.backspaceSpeed, action: removeLastCharacter) 53 | } else { 54 | cancelTyping() 55 | completion?() 56 | } 57 | } 58 | 59 | private func startDelayBeforeBackspacing() { 60 | if settings.isBackspaceEnabled { 61 | isDelaying = true 62 | DispatchQueue.main.asyncAfter(deadline: .now() + settings.delaySeconds, execute: startBackspacing) 63 | } else { 64 | cancelTyping() 65 | completion?() 66 | } 67 | } 68 | 69 | private func startTimer(with interval: Double, action: @escaping () -> Void) { 70 | timer = Timer.publish(every: interval, on: .main, in: .common) 71 | .autoconnect() 72 | .sink { _ in 73 | action() 74 | } 75 | } 76 | 77 | private func appendNextCharacter() { 78 | if displayText.count < text.count { 79 | appendCharacterAtCurrentIndex() 80 | if settings.isHapticsEnabled { 81 | generateHapticFeedbackIfEnabled(with: .light) 82 | } 83 | } else { 84 | cancelTyping() 85 | startDelayBeforeBackspacing() 86 | } 87 | } 88 | 89 | private func appendCharacterAtCurrentIndex() { 90 | let index = text.index(text.startIndex, offsetBy: displayText.count) 91 | displayText.append(text[index]) 92 | } 93 | 94 | private func removeLastCharacter() { 95 | if displayText.count > 0 { 96 | displayText.removeLast() 97 | generateHapticFeedbackIfEnabled(with: .soft) 98 | } else { 99 | cancelTyping() 100 | completion?() 101 | } 102 | } 103 | 104 | private func generateHapticFeedbackIfEnabled(with style: UIImpactFeedbackGenerator.FeedbackStyle) { 105 | if settings.isHapticsEnabled { 106 | UIImpactFeedbackGenerator(style: style).impactOccurred() 107 | } 108 | } 109 | } 110 | 111 | 112 | struct TypingText_Previews: PreviewProvider { 113 | static var previews: some View { 114 | TypingTextView(text: "Hello World") 115 | .foregroundColor(.brown) 116 | .font(.system(size: 20, weight: .bold)) 117 | } 118 | } 119 | --------------------------------------------------------------------------------