Instantly share code, notes, and snippets.
Created
February 15, 2025 01:25
-
Star
1
(1)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save Koshimizu-Takehito/8a624af1e8b0f40be917dc00340e1d56 to your computer and use it in GitHub Desktop.
ColorNavigation2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct ContentView: View { | |
@State var isExpanded = [true] + Array(repeating: false, count: PageItem.samples.count) | |
var body: some View { | |
ZStack { | |
ForEach(1..<isExpanded.count, id: \.self) { offset in | |
let item = PageItem.samples[(offset-1) % PageItem.samples.count] | |
PageView( | |
title: item.title, | |
message: item.message, | |
color: .color(offset: offset), | |
isExpanded: $isExpanded[offset], | |
isExpandedPrevious: isExpanded[offset-1] | |
) | |
} | |
} | |
.ignoresSafeArea() | |
} | |
} | |
struct PageView: View { | |
var title: String | |
var message: String | |
var color: Color | |
@Binding var isExpanded: Bool | |
var isExpandedPrevious: Bool | |
var body: some View { | |
circle | |
.overlay { contents } | |
.mask { circle } | |
.onTapGesture { | |
isExpanded.toggle() | |
} | |
} | |
var circle: some View { | |
CircleView( | |
color: color, | |
isExpanded: isExpanded, | |
isExpandedPrevious: isExpandedPrevious | |
) | |
} | |
var contents: some View { | |
GeometryReader { geometry in | |
VStack(alignment: .leading, spacing: 16) { | |
Text(title) | |
.font(.largeTitle) | |
.fontWeight(.bold) | |
TypingText(fullText: message, animatableData: isExpanded ? 1 : 0) | |
.font(.custom("Courier", size: 16)) | |
.lineSpacing(12) | |
.animation(typing()?.delay(1.2), value: isExpanded) | |
} | |
.multilineTextAlignment(.leading) | |
.allowsTightening(true) | |
.fixedSize(horizontal: false, vertical: true) | |
.foregroundStyle(isExpanded ? .black : .clear) | |
.opacity(isExpanded ? 1 : 0) | |
.scaleEffect(isExpanded ? 1 : 0) | |
.offset(x: isExpanded ? 0 : 0.8 * geometry.size.width) | |
.animation(.customSpring, value: isExpanded) | |
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) | |
} | |
.padding(.horizontal) | |
.allowsHitTesting(false) | |
} | |
func typing() -> Animation? { | |
if isExpanded { | |
Animation.linear(duration: Double(message.count) / 30.0) | |
} else { | |
Animation?.none | |
} | |
} | |
} | |
struct TypingText: View, Animatable { | |
var fullText: String | |
var animatableData: Double | |
var body: some View { | |
Text(displayText) | |
} | |
private var displayText: AttributedString { | |
let text = fullText | |
let count = Int(Double(text.count) * min(max(animatableData, 0), 1)) | |
let startIndex = text.index(text.startIndex, offsetBy: 0) | |
let endIndex = text.index(text.startIndex, offsetBy: count) | |
let visibles = AttributedString(text[startIndex..<endIndex]) | |
var invisibles = AttributedString(text[endIndex..<text.endIndex]) | |
invisibles.foregroundColor = .clear | |
return visibles + invisibles | |
} | |
} | |
struct CircleView: View { | |
var color: Color | |
var isExpanded: Bool | |
var isExpandedPrevious: Bool | |
var body: some View { | |
GeometryReader { geometry in | |
let width = geometry.size.width | |
let height = geometry.size.height | |
let diagonal = sqrt(width * width + height * height) | |
let diameter = isExpanded ? diagonal : (min(width, height) / 2) | |
ZStack { | |
Circle() | |
.foregroundStyle(color) | |
Label("Next", systemImage: "arrow.right") | |
.font(.title2) | |
.fontWeight(.semibold) | |
.offset(x: isExpanded ? 0 : -0.1 * diameter/2) | |
.opacity(isExpanded ? 0 : 1) | |
.scaleEffect(isExpanded ? 0 : 1) | |
} | |
.frame(width: diameter, height: diameter) | |
.offset( | |
x: isExpanded ? -(diagonal - width)/2 : 0.2 * diameter, | |
y: isExpanded ? -(diagonal - height)/2 : 0.1 * diameter | |
) | |
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) | |
.scaleEffect(isExpandedPrevious ? 1 : 0, anchor: .bottomTrailing) | |
.offset(x: isExpandedPrevious ? 0 : diameter) | |
.animation(.customSpring, value: isExpandedPrevious) | |
} | |
.animation(.customSpring, value: isExpanded) | |
} | |
} | |
extension Animation { | |
static var customSpring: Self { | |
.interpolatingSpring(duration: 0.9, bounce: 0.3) | |
} | |
} | |
extension Color { | |
static func color(offset: Int) -> Self { | |
let hue = Double((123 * offset) % 360) / 360.0 | |
return Color(hue: hue, saturation: 0.4, brightness: 0.96) | |
} | |
} | |
struct PageItem { | |
var title: String | |
var message: String | |
static let samples: [Self] = [ | |
Self.init( | |
title: "SwiftUI\nFramework", | |
message: "SwiftUI is a declarative UI framework that simplifies cross-platform development with state-driven rendering, real-time previews, and data binding." | |
), | |
Self.init( | |
title: "UIKit\nFramework", | |
message: "UIKit is Apple’s core UI framework for iOS, offering components for layouts, animations, and interactions, with Auto Layout and extensive support." | |
), | |
Self.init( | |
title: "Metal\nFramework", | |
message: "Metal is a low-level API for high-performance graphics and computing, optimizing rendering for 3D graphics, gaming, and machine learning." | |
), | |
Self.init( | |
title: "Swift\nConcurrency", | |
message: "Swift Concurrency enables efficient asynchronous programming with async/await, structured concurrency, and actors for thread safety." | |
) | |
] | |
} | |
#Preview { | |
ContentView() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment