SwiftUI Tutorial: Creating a Cyberpunk-Inspired Scrollable Text Reader in SwiftUI
Hello, fellow coders! Today, we're going to dive into the neon-lit world of SwiftUI to create a cyberpunk-inspired scrollable text reader. This tutorial will walk you through the process of building a custom scroll view with a sleek progress indicator and a unique "back to top" feature. Let's get started!
The Concept
Our app will feature a dark-themed scrollable view with green accents, reminiscent of classic terminal interfaces. As users scroll through the content, they'll see a progress bar that transforms into a circular progress wheel when idle. When they reach the bottom, an "arrow up" button appears to quickly return to the top.
Key Components
ContentView
: The main view that handles scrolling and progress tracking.ProgressWheel
: A circular progress indicator.TextView
: The view that contains our cyberpunk-themed text content.
Step 1: Setting Up the ContentView
Let's start by creating our ContentView
struct:
struct ContentView<Content: View>: View {
@State private var scrollOffset: CGFloat = 0
@State private var contentHeight: CGFloat = 0
@State private var showArrow: Bool = false
@State private var isAtBottom: Bool = false
@State private var isScrolling: Bool = false
@State private var showWheel: Bool = false
@State private var isTouching: Bool = false
var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
// ... body implementation ...
}
This view uses several @State
properties to track scrolling behavior and view states.
Step 2: Implementing the ScrollView
Inside the body
of ContentView
, we'll set up our custom ScrollView
:
var body: some View {
ScrollViewReader { scrollProxy in
GeometryReader { fullView in
ZStack(alignment: .bottom) {
ScrollView {
// ... ScrollView content ...
}
.scrollIndicators(.hidden)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
self.isTouching = true
self.showWheel = false
}
.onEnded { _ in
self.isTouching = false
startWheelTimer()
}
)
// ... preference changes and other modifiers ...
progressView(fullView: fullView, scrollProxy: scrollProxy)
}
}
}
.preferredColorScheme(.dark)
}
This setup allows us to track scroll position and content height using preference keys.
Step 3: Creating the Progress Indicator
We'll implement a progressView
function to display either a progress bar or a progress wheel:
func progressView(fullView: GeometryProxy, scrollProxy: ScrollViewProxy) -> some View {
let progress = min(max(0, -scrollOffset / (contentHeight - fullView.size.height)), 1)
let progressPercentage = Int(progress * 100)
return ZStack {
// ... Progress bar implementation ...
if showWheel {
ProgressWheel(progress: progress)
// ... styling and positioning ...
}
}
// ... animations ...
}
This function calculates the scroll progress and displays either a bar or wheel based on the current state.
Step 4: Implementing the ProgressWheel
Create a separate ProgressWheel
struct for the circular progress indicator:
struct ProgressWheel: View {
var progress: CGFloat
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 4)
.opacity(0.3)
.foregroundColor(.green)
Circle()
.trim(from: 0.0, to: progress)
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
.foregroundColor(.green)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear, value: progress)
Text("\(Int(progress * 100))%")
.font(.system(size: 14, weight: .bold, design: .monospaced))
.foregroundColor(.green)
}
}
}
This wheel will display the current progress percentage in a circular format.
Step 5: Creating the TextView
Finally, let's implement the TextView
that will showcase our cyberpunk-themed content:
struct TextView: View {
var body: some View {
ContentView {
VStack {
Text("CodeWithWilliamJiamin")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.green)
.padding(.bottom, 10)
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(Color(red: 0.1, green: 0.1, blue: 0.1))
.stroke(Color.green, lineWidth: 2)
.shadow(color: Color.green.opacity(0.5), radius: 10)
ScrollView {
Text(introText)
.font(.custom("Courier", size: 16))
.foregroundColor(.green)
.padding()
}
}
.padding()
}
.background(Color(red: 0.05, green: 0.05, blue: 0.05))
}
}
// ... introText implementation ...
}
This view wraps our content in the ContentView
and applies cyberpunk-inspired styling.
Full Code
import SwiftUI
struct ContentView<Content: View>: View {
@State private var scrollOffset: CGFloat = 0
@State private var contentHeight: CGFloat = 0
@State private var showArrow: Bool = false
@State private var isAtBottom: Bool = false
@State private var isScrolling: Bool = false
@State private var showWheel: Bool = false
@State private var isTouching: Bool = false
var content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
ScrollViewReader { scrollProxy in
GeometryReader { fullView in
ZStack(alignment: .bottom) {
ScrollView {
GeometryReader { scrollViewGeo in
Color.clear.preference(key: ScrollOffsetKey.self, value: scrollViewGeo.frame(in: .global).minY)
}
.frame(height: 0)
.id(0)
VStack {
content
}
.padding(.horizontal)
.frame(maxWidth: .infinity, alignment: .leading)
.background(GeometryReader { contentGeo in
Color.clear.preference(key: ContentHeightPreferenceKey.self, value: contentGeo.size.height)
})
}
.scrollIndicators(.hidden)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
self.isTouching = true
self.showWheel = false
}
.onEnded { _ in
self.isTouching = false
startWheelTimer()
}
)
.onPreferenceChange(ScrollOffsetKey.self) { value in
self.scrollOffset = value - fullView.safeAreaInsets.top
self.isAtBottom = -scrollOffset >= (contentHeight - fullView.size.height)
self.isScrolling = true
self.showWheel = false
startWheelTimer()
}
.onPreferenceChange(ContentHeightPreferenceKey.self) {
self.contentHeight = $0
}
progressView(fullView: fullView, scrollProxy: scrollProxy)
}
}
}
.preferredColorScheme(.dark)
}
private func startWheelTimer() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if !self.isScrolling && !self.isTouching {
withAnimation(.easeInOut(duration: 0.5)) {
self.showWheel = true
}
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.isScrolling = false
}
}
func progressView(fullView: GeometryProxy, scrollProxy: ScrollViewProxy) -> some View {
let progress = min(max(0, -scrollOffset / (contentHeight - fullView.size.height)), 1)
let progressPercentage = Int(progress * 100)
return ZStack {
if !showWheel {
HStack(spacing: 10) {
if isAtBottom {
Button(action: {
withAnimation(.easeInOut(duration: 1.5)) {
scrollProxy.scrollTo(0, anchor: .top)
}
}) {
Image(systemName: "arrow.up")
.font(.system(size: 24, weight: .bold))
.foregroundColor(.green)
.frame(width: 55, height: 55)
.background(
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(Color.black)
RoundedRectangle(cornerRadius: 10)
.stroke(Color.green, lineWidth: 2)
}
)
.shadow(color: Color.green.opacity(0.8), radius: 5, x: 0, y: 0)
}
.transition(.asymmetric(insertion: .scale.combined(with: .slide), removal: .scale.combined(with: .opacity)))
}
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.fill(Color.black)
.frame(width: 190, height: 55)
RoundedRectangle(cornerRadius: 10)
.stroke(Color.green, lineWidth: 2)
.frame(width: 190, height: 55)
.shadow(color: Color.green.opacity(0.8), radius: 5, x: 0, y: 0)
HStack(spacing: 10) {
Text("\(progressPercentage)%")
.font(.system(size: 18, weight: .bold, design: .monospaced))
.foregroundColor(.green)
.frame(width: 60, alignment: .leading)
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 5)
.fill(Color.green.opacity(0.3))
.frame(width: geo.size.width, height: 8)
RoundedRectangle(cornerRadius: 5)
.fill(Color.green)
.frame(width: geo.size.width * CGFloat(progress), height: 8)
.shadow(color: Color.green.opacity(0.8), radius: 5, x: 0, y: 0)
}
}
.frame(height: 8)
}
.padding(.horizontal, 20)
}
}
.frame(width: isAtBottom ? 260 : 190, alignment: .trailing)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.bottom, 20)
.transition(.opacity)
}
if showWheel {
ProgressWheel(progress: progress)
.frame(width: 60, height: 60)
.background(
Circle()
.fill(Color.black)
.shadow(color: Color.green.opacity(0.8), radius: 10, x: 0, y: 0)
)
.transition(.scale.combined(with: .opacity))
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing, 20)
.padding(.bottom, 20)
}
}
.animation(.spring(response: 0.5, dampingFraction: 0.7), value: progressPercentage)
.animation(.spring(response: 0.5, dampingFraction: 0.7), value: isAtBottom)
.animation(.easeInOut(duration: 0.5), value: showWheel)
}
}
struct ProgressWheel: View {
var progress: CGFloat
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 4)
.opacity(0.3)
.foregroundColor(.green)
Circle()
.trim(from: 0.0, to: progress)
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
.foregroundColor(.green)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear, value: progress)
Text("\(Int(progress * 100))%")
.font(.system(size: 14, weight: .bold, design: .monospaced))
.foregroundColor(.green)
}
}
}
struct ScrollOffsetKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct ContentHeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
import SwiftUI
struct TextView: View {
var body: some View {
ContentView {
VStack {
Text("CodeWithWilliamJiamin")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.green)
.padding(.bottom, 10)
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(Color(red: 0.1, green: 0.1, blue: 0.1))
.stroke(Color.green, lineWidth: 2)
.shadow(color: Color.green.opacity(0.5), radius: 10)
ScrollView {
Text(introText)
.font(.custom("Courier", size: 16))
.foregroundColor(.green)
.padding()
}
}
.padding()
}
.background(Color(red: 0.05, green: 0.05, blue: 0.05))
}
}
var introText: String {
"""
Welcome to the digital realm of William!
[SYSTEM INIT...]
[LOADING PROFILE...]
[PROFILE LOADED SUCCESSFULLY]
Greetings, fellow coders and tech enthusiasts! I'm William, your guide through the neon-lit corridors of code and creativity. As a passionate coder and dedicated YouTuber, I'm here to illuminate the path of programming for aspiring developers across the globe.
[CHANNEL INFO]
Name: CodeWithWilliam
Mission: Decrypting the complexities of coding, one video at a time
Status: Online and thriving
Are you ready to embark on an epic coding adventure? My YouTube channel, CodeWithWilliam, is your portal to a world where lines of code transform into powerful applications and innovative solutions. From beginner-friendly tutorials to advanced coding techniques, we explore it all!
[CONTENT OVERVIEW]
- Python Mastery
- Web Development Wizardry
- Mobile App Alchemy
- Data Science Decryption
- Machine Learning Marvels
- And much more!
But wait, there's more! By joining our Patreon community, you gain access to an exclusive digital vault filled with:
[PATREON PERKS]
- Complete source code for all projects
- Early access to upcoming videos
- Personalized coding advice
- Exclusive behind-the-scenes content
- Vote on future video topics
- Join our private Discord server
The best part? All the code you download through our Patreon can be used commercially! That's right - build, innovate, and monetize with confidence.
[CALL TO ACTION]
Don't let this opportunity slip through your fingers! Join our cyberpunk coding revolution now:
1. Subscribe to CodeWithWilliam on YouTube
2. Become a Patreon supporter for exclusive benefits
3. Download, learn, and create without limits
Remember, in this digital age, code is the new currency, and knowledge is power. Together, we'll hack the matrix of possibilities and sculpt the future of technology.
Are you ready to level up your coding skills? The neon-lit path to programming mastery awaits. Join me, William, and let's code our way to a brighter, more innovative future!
[END TRANSMISSION]
#CodeWithWilliam #CodingRevolution #PatreonPerks #LearnToCode #FutureOfTech
"""
}
}
#Preview {
TextView()
}
//
// CyberTextReaderApp.swift
// CyberTextReader
//
// Created by WilliamJiamin on 2024/8/4.
//
import SwiftUI
@main
struct CyberTextReaderApp: App {
var body: some Scene {
WindowGroup {
TextView()
}
}
}
Conclusion
And there you have it! We've created a cyberpunk-inspired scrollable text reader with a custom progress indicator and smooth animations. This project demonstrates the power of SwiftUI in creating complex, interactive user interfaces with relatively little code.
Remember, the key to mastering SwiftUI is practice and experimentation. Try modifying this project to add your own unique features or styling. Happy coding, and may your terminal always glow with the green light of success!