├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── IntroKit
│ ├── IntroKit.swift
│ ├── ViewModels
│ └── HapticManager.swift
│ └── Views
│ └── Onboarding
│ └── PurposeView.swift
└── Tests
└── IntroKitTests
└── IntroKitTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2023] [Andreas Ink]
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: 5.8
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: "IntroKit",
8 | platforms: [.iOS(.v16)],
9 | products: [
10 | // Products define the executables and libraries a package produces, and make them visible to other packages.
11 | .library(
12 | name: "IntroKit",
13 | targets: ["IntroKit"]),
14 | ],
15 | dependencies: [
16 | // Dependencies declare other packages that this package depends on.
17 | // .package(url: /* package url */, from: "1.0.0"),
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
21 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
22 | .target(
23 | name: "IntroKit",
24 | dependencies: []),
25 | .testTarget(
26 | name: "IntroKitTests",
27 | dependencies: ["IntroKit"]),
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IntroKit
2 |
3 | [](https://developer.apple.com/documentation/swiftui)
4 | [](https://developer.apple.com/ios/)
5 |
6 | IntroKit is a powerful and highly customizable SwiftUI framework designed to enhance the onboarding experience of your iOS applications. Inspired by the interactive onboarding experience of ChatGPT iOS, IntroKit enables developers to effortlessly deliver clear and engaging onboarding to their users, showcasing the key features and benefits of using their app.
7 |
8 | 
9 |
10 |
11 | ## Features
12 |
13 | - Dynamic typing animation to focus on benefits of your app
14 | - Customizable components
15 | - Adaptive for onboarding and plain states
16 | - Core Haptics integration
17 | - Utilizes SwiftUI's newest and most powerful features
18 |
19 | ## Installation
20 |
21 | You can add IntroKit to an Xcode project by adding it as a package dependency.
22 |
23 | 1. From the **File** menu, select **Swift Packages › Add Package Dependency…**
24 | 2. Enter the following URL: https://github.com/AndreasInk/IntroKit.git
25 | 3. Click **Next**. Specify the version details, then click **Next** again to add the package to your project.
26 |
27 | **Or** you can add IntroKit in your SPM package.swift file...
28 | ```swift
29 | .package(url: "https://github.com/AndreasInk/IntroKit.git", .upToNextMajor(from: "1.0.0"))
30 | ```
31 |
32 | ## Usage
33 |
34 | ```swift
35 | import SwiftUI
36 | import IntroKit
37 |
38 | struct ContentView: View {
39 | @StateObject var introViewModel = IntroViewModel()
40 | var body: some View {
41 | PurposeView(icon: "figure.walk", title: "I walk to...",
42 | introText: ["Live healthier", "Think clearer", "Dream deeper", "Feel happier"],
43 | cta: "Next")
44 | .environmentObject(introViewModel)
45 | }
46 | }
47 | ```
48 |
49 | In this example, `PurposeView` is used to generate an onboarding screen with the provided `introText` and call-to-action button text `cta`.
50 |
51 | ## Contributing
52 |
53 | Contributions to IntroKit are welcome and greatly appreciated! Please feel free to create a pull request or open an issue on this GitHub repository.
54 |
55 | ## TODO
56 |
57 | - [x] Clean up code
58 | - [x] Further document code
59 |
60 |
61 | ## License
62 |
63 | IntroKit is available under the MIT license. See the [LICENSE](https://github.com/AndreasInk/IntroKit/blob/main/LICENSE) file for more info.
64 |
65 | ---
66 | Built with ❤️ using SwiftUI.
67 |
68 |
--------------------------------------------------------------------------------
/Sources/IntroKit/IntroKit.swift:
--------------------------------------------------------------------------------
1 | public struct IntroKit {
2 | public private(set) var text = "Hello, World!"
3 |
4 | public init() {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/IntroKit/ViewModels/HapticManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HapticManager.swift
3 | //
4 | //
5 | // Created by Andreas Ink on 5/21/23.
6 | //
7 |
8 | import SwiftUI
9 | import CoreHaptics
10 |
11 | // https://www.hackingwithswift.com/books/ios-swiftui/making-vibrations-with-uinotificationfeedbackgenerator-and-core-haptics
12 | /// Handles haptics with dynamic intensity support
13 | public class HapticManager: ObservableObject {
14 | public static let shared = HapticManager()
15 | @Published private var engine: CHHapticEngine?
16 |
17 | public func prepareHaptics() {
18 | guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
19 |
20 | do {
21 | engine = try CHHapticEngine()
22 | try engine?.start()
23 | } catch {
24 | print("Error creating the engine: \(error.localizedDescription)")
25 | }
26 | }
27 |
28 | /// Generate a haptic feedback based on given flexibility and intensity
29 | public func generateHaptic(flexibility: CHHapticEvent.ParameterID, intensity: Float) {
30 | guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
31 | var events = [CHHapticEvent]()
32 |
33 | let intensityParam = CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity)
34 | let sharpnessParam = CHHapticEventParameter(parameterID: flexibility, value: intensity)
35 | let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensityParam, sharpnessParam], relativeTime: 0)
36 | events.append(event)
37 |
38 | do {
39 | let pattern = try CHHapticPattern(events: events, parameters: [])
40 | let player = try engine?.makePlayer(with: pattern)
41 | try player?.start(atTime: 0)
42 | } catch {
43 | print("Failed to play haptic pattern: \(error.localizedDescription)")
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/IntroKit/Views/Onboarding/PurposeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PurposeView.swift
3 | //
4 | //
5 | // Created by Andreas Ink on 5/21/23.
6 | //
7 |
8 | import SwiftUI
9 | import CoreHaptics
10 |
11 | /// A reusable and extensible view with animated text input and typing effect
12 | public struct PurposeView: View {
13 | @State private var letters: [Letter] = []
14 | @State private var sentence: String = ""
15 | @State private var randomValue: Double = 0
16 | @StateObject private var engine = HapticManager.shared
17 | @FocusState private var textFieldSelected: Bool
18 |
19 | let iconView: IconView
20 | let titleView: TitleView
21 | let ctaView: CTAView
22 | let introText: [String]
23 | let placeholder: String
24 | let onSubmit: (String) -> Void
25 |
26 | var showsTextField: Bool = true
27 | var usesHaptics: Bool = true
28 |
29 | public init(
30 | iconView: IconView,
31 | titleView: TitleView,
32 | ctaView: CTAView,
33 | introText: [String],
34 | placeholder: String = "",
35 | showsTextField: Bool = true,
36 | usesHaptics: Bool = true,
37 | onSubmit: @escaping (String) -> Void
38 | ) {
39 | self.iconView = iconView
40 | self.titleView = titleView
41 | self.ctaView = ctaView
42 | self.introText = introText
43 | self.placeholder = placeholder
44 | self.showsTextField = showsTextField
45 | self.usesHaptics = usesHaptics
46 | self.onSubmit = onSubmit
47 | }
48 |
49 | public var body: some View {
50 | VStack(alignment: .leading) {
51 | iconView.padding(.bottom)
52 | titleView.padding(.vertical)
53 |
54 | ZStack {
55 | if showsTextField {
56 | TextField(placeholder, text: $sentence)
57 | .font(.largeTitle.bold())
58 | .focused($textFieldSelected)
59 | .opacity(0.01) // Hide textfield for animation effect
60 | .onChange(of: sentence) { _ in
61 | Task { await handleTypingEffect() }
62 | }
63 | }
64 |
65 | HStack(spacing: 1) {
66 | ForEach(letters + [Letter(" ")]) { letter in
67 | Text(String(letter.char))
68 | .font(.largeTitle.bold())
69 | .foregroundStyle(
70 | LinearGradient(
71 | colors: [.accentColor, .accentColor.opacity(0.5)],
72 | startPoint: .leading,
73 | endPoint: .bottomTrailing
74 | )
75 | )
76 | }
77 | Spacer()
78 | }
79 | }
80 |
81 | if !sentence.isEmpty {
82 | ctaView
83 | .simultaneousGesture(TapGesture().onEnded({ _ in
84 | onSubmit(sentence)
85 | sentence = ""
86 | letters = []
87 | generateRandomHaptic()
88 | }))
89 | .buttonStyle(.borderedProminent)
90 | .transition(.opacity)
91 | }
92 | }
93 | .padding()
94 | .task {
95 | engine.prepareHaptics()
96 | await displayIntroText()
97 | }
98 | }
99 |
100 | private func handleTypingEffect() async {
101 | letters = sentence.map { Letter(String($0)) }
102 | generateRandomHaptic()
103 | }
104 |
105 | private func displayIntroText() async {
106 | for text in introText {
107 | await showTextAnimation(text)
108 | }
109 | letters = []
110 | textFieldSelected = true
111 | }
112 |
113 | private func showTextAnimation(_ text: String) async {
114 | letters = []
115 | let characters = text.map { Letter(String($0)) }
116 | for character in characters {
117 | try? await Task.sleep(for: .seconds(0.1))
118 | withAnimation {
119 | letters.append(character)
120 | }
121 | generateRandomHaptic()
122 | }
123 |
124 | try? await Task.sleep(for: .seconds(0.5))
125 | }
126 |
127 | private func generateRandomHaptic() {
128 | let oldValue = randomValue
129 | randomValue = Double.random(in: 0...100)
130 | let amount = abs(oldValue - randomValue) / 100.0
131 | if usesHaptics {
132 | engine.generateHaptic(flexibility: .hapticSharpness, intensity: Float(amount))
133 | }
134 | }
135 | }
136 |
137 | public struct Letter: Identifiable {
138 | public let id = UUID()
139 | public let char: String
140 |
141 | public init(_ char: String) {
142 | self.char = char
143 | }
144 | }
145 |
146 | // MARK: - Preview
147 | #Preview {
148 | PurposeView(
149 | iconView: Image(systemName: "figure.walk").foregroundStyle(Color.accentColor).font(.largeTitle),
150 | titleView: Text("I walk to...")
151 | .font(.headline)
152 | .foregroundStyle(.blue),
153 | ctaView: Button("Next") { },
154 | introText: ["Live healthier", "Think clearer", "Dream deeper", "Feel happier"],
155 | placeholder: "Type here...",
156 | onSubmit: { print($0) }
157 | )
158 | }
159 |
--------------------------------------------------------------------------------
/Tests/IntroKitTests/IntroKitTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import IntroKit
3 |
4 | final class IntroKitTests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | XCTAssertEqual(IntroKit().text, "Hello, World!")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------