diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e61e00f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4eea5a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Colin O'Brien + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..9c095ec --- /dev/null +++ b/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 5.7 +import PackageDescription + +let package = Package( + name: "TypewriterCarouselView", + platforms: [ + .iOS(.v16) + ], + products: [ + .library( + name: "TypewriterCarouselView", + targets: ["TypewriterCarouselView"] + ) + ], + dependencies: [], + targets: [ + .target( + name: "TypewriterCarouselView", + dependencies: [] + ) + ] +) diff --git a/Sources/TypewriterCarouselView/TypewriterCarouselView.swift b/Sources/TypewriterCarouselView/TypewriterCarouselView.swift new file mode 100644 index 0000000..a27b7c9 --- /dev/null +++ b/Sources/TypewriterCarouselView/TypewriterCarouselView.swift @@ -0,0 +1,40 @@ +// +// TypewriterView.swift +// +// +// Created by Colin O'Brien on 4/04/24. +// + +import SwiftUI + +struct TypewriterCarouselView: View { + + @State var text: [String] + @State private var currentText: String? + @State private var cursor: Int = -1 + + var typingDelay: Duration = .milliseconds(50) + var onWritingFinishedDelay: Duration = .seconds(5) + var onDeletingFinishedDelay: Duration = .seconds(1) + var mode: TypewriterView.Mode = .writeAndDelete + + var body: some View { + TypewriterView( + text: currentText, + typingDelay: typingDelay, + onTypingFinished: { + cursor += 1 + currentText = text[cursor % text.count] + }, + onWritingFinishedDelay: onWritingFinishedDelay, + onDeletingFinishedDelay: onDeletingFinishedDelay, + mode: mode + ) + .font(.title) + .padding() + } +} + +#Preview { + TypewriterCarouselView(text: ["Hello", "World!"]) +} diff --git a/Sources/TypewriterCarouselView/TypewriterView.swift b/Sources/TypewriterCarouselView/TypewriterView.swift new file mode 100644 index 0000000..288a9b4 --- /dev/null +++ b/Sources/TypewriterCarouselView/TypewriterView.swift @@ -0,0 +1,94 @@ +// +// TypewriterView.swift +// +// +// Created by Colin O'Brien on 4/04/24. +// + +import SwiftUI + +struct TypewriterView: View { + + enum Mode { + case write, writeAndDelete + } + + var text: String? + var typingDelay: Duration = .milliseconds(50) + var onTypingFinished: (() -> Void) = {} + var onWritingFinishedDelay: Duration = .seconds(5) + var onDeletingFinishedDelay: Duration = .seconds(1) + var mode: Mode = .writeAndDelete + + @State private var animatedText: AttributedString = "" + @State private var typingTask: Task? + + var body: some View { + Text(animatedText) + .onChange(of: text, perform: { _ in animateText() }) + .onAppear { animateText() } + } + + private func animateText() { + typingTask?.cancel() + + guard let text = text else { + onTypingFinished() + return + } + + typingTask = Task { + let defaultAttributes = AttributeContainer() + animatedText = AttributedString( + text, + attributes: defaultAttributes.foregroundColor(.clear) + ) + + var index = animatedText.startIndex + while index < animatedText.endIndex { + try Task.checkCancellation() + + // Update the style + animatedText[animatedText.startIndex...index] + .setAttributes(defaultAttributes) + + // Wait + try await Task.sleep(for: typingDelay) + + // Advance the index, character by character + index = animatedText.index(afterCharacter: index) + } + + // Wait + try await Task.sleep(for: onWritingFinishedDelay) + + switch mode { + case .write: onTypingFinished() + + case .writeAndDelete: + index = animatedText.endIndex + + while index > animatedText.startIndex { + try Task.checkCancellation() + + animatedText[index..