Skip to content

Instantly share code, notes, and snippets.

@1mash0
Last active January 8, 2024 12:32
Show Gist options
  • Save 1mash0/628e7328c9573d697ae3d3d3b2827c0c to your computer and use it in GitHub Desktop.
Save 1mash0/628e7328c9573d697ae3d3d3b2827c0c to your computer and use it in GitHub Desktop.
ColorタイルとHero Animation
import SwiftUI
struct ContentView: View {
@State private var colors: [Color] = [
.black,
.blue,
.brown,
.cyan,
.gray,
.green,
.indigo,
.mint,
.orange,
.pink,
.purple,
.red,
.teal,
.yellow
]
@State private var selectedColor: Color?
@State private var isShowing = false
@State private var isAnimation = false
@Namespace private var effect
var body: some View {
ZStack {
if let selectedColor, isShowing {
SelectedImage(
isShowing: $isShowing,
isAnimation: $isAnimation,
color: selectedColor,
imageEffect: effect
)
} else {
VStack {
Text("Colors")
.font(.headline)
colorsView
.opacity(isShowing ? 0.0 : 1.0)
Spacer()
}
}
}
}
private var colorsView: some View {
LazyVGrid(
columns: Array(repeating: .init(.flexible()), count: 3),
alignment: .center,
spacing: 20
) {
ForEach(colors, id: \.self) { color in
color
.clipShape(RoundedRectangle(cornerRadius: 15))
.overlay {
Text(color.description.capitalized)
.font(.headline)
.foregroundStyle(.white)
}
.matchedGeometryEffect(id: color, in: effect)
.frame(width: 80, height: 80)
.onTapGesture {
if !isAnimation {
withAnimation(.spring(response: 0.3, dampingFraction: 1.0)) {
selectedColor = color
isShowing.toggle()
isAnimation = true
} completion: {
isAnimation = false
}
}
}
}
}
.padding()
}
}
struct SelectedImage: View {
@State private var initOffset: CGFloat = .zero
@State private var offset: CGFloat = .zero
@State private var headerHeight: CGFloat = 200
@State private var scale: CGFloat = 1.0
@State private var blurRadius: CGFloat = .zero
@State private var isDetailShowing: Bool = false
let defaultHeaderHeight: CGFloat = 200
let iconHeight: CGFloat = 200
@Binding var isShowing: Bool
@Binding var isAnimation: Bool
let color: Color
let imageEffect: Namespace.ID
@Namespace private var detailEffect
var body: some View {
ZStack(alignment: .top) {
GeometryReader { proxy in
ScrollView {
ZStack {
LazyVStack(spacing: 0) {
ForEach(0..<50, id: \.self) { i in
Text("item \(i)")
.frame(maxWidth: .infinity, minHeight: 44)
.background(.white)
}
}
.padding(.top, defaultHeaderHeight)
GeometryReader { gr in
color
.clipShape(.rect(cornerRadius: 15))
.frame(height: headerHeight)
.overlay {
VStack {
Text(color.description.capitalized)
.font(.title)
.foregroundStyle(.white)
}
}
.matchedGeometryEffect(id: color, in: imageEffect)
.blur(radius: blurRadius)
.onAppear {
initOffset = gr.frame(in: .global).origin.y
}
.offset(y: gr.frame(in: .global).origin.y < initOffset
? abs(gr.frame(in: .global).origin.y)
: initOffset - gr.frame(in: .global).origin.y)
.onChange(of: gr.frame(in: .global).origin.y, initial: true) { _, newValue in
offset = gr.frame(in: .global).minY
blurRadius = self.calcBlurRadius(maxRadius: 3.0, yOffset: offset)
headerHeight = self.calcHeaderHeight(
minHeight: defaultHeaderHeight / 2,
maxHeight: defaultHeaderHeight,
yOffset: offset
)
}
.onTapGesture {
withAnimation(.spring(response: 0.3, dampingFraction: 1.0)) {
isDetailShowing.toggle()
}
}
}
}
}
.opacity(isDetailShowing ? 0.0 : 1.0)
.ignoresSafeArea(edges: .vertical)
.background(.white)
if isDetailShowing {
color
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay {
VStack {
Text(color.description.capitalized)
.font(.largeTitle)
.foregroundStyle(.white)
}
}
.ignoresSafeArea()
}
HStack {
Button {
if isDetailShowing {
withAnimation(.spring(response: 0.3, dampingFraction: 1.0)) {
isDetailShowing.toggle()
}
} else {
withAnimation(.spring(response: 0.3, dampingFraction: 1.0)) {
isShowing.toggle()
isAnimation = true
} completion: {
isAnimation = false
}
}
} label: {
Label("", systemImage: "chevron.backward")
.bold()
.foregroundStyle(.white)
}
Spacer()
}
.padding(.horizontal)
}
}
}
func calcHeaderHeight(minHeight: CGFloat, maxHeight: CGFloat, yOffset: CGFloat) -> CGFloat {
if maxHeight + yOffset < minHeight {
return minHeight
}
return maxHeight + yOffset
}
func calcBlurRadius(maxRadius: CGFloat, yOffset: CGFloat) -> CGFloat {
if yOffset - initOffset < 0 {
return 0.0
}
return yOffset - initOffset < maxRadius ? yOffset - initOffset : maxRadius
}
func calcScale(minScale: CGFloat, maxScale: CGFloat, headerHeight: CGFloat) -> CGFloat {
if headerHeight == defaultHeaderHeight {
return maxScale
}
return headerHeight / defaultHeaderHeight < minScale ? minScale : headerHeight / defaultHeaderHeight
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment