-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
[Style] 바텀시트 배치
- Loading branch information
Showing
14 changed files
with
353 additions
and
295 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
182 changes: 182 additions & 0 deletions
182
Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/BottomSheetList.swift
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
// | ||
// BottomSheetList.swift | ||
// SpoonMe | ||
// | ||
// Created by 이지훈 on 1/12/25. | ||
// | ||
|
||
import SwiftUI | ||
|
||
struct BottomSheetListItem: View { | ||
let title: String | ||
let subTitle: String | ||
let cellTitle: String | ||
let hasChip: Bool | ||
|
||
var body: some View { | ||
HStack(spacing: 12) { | ||
VStack(alignment: .leading, spacing: 4) { | ||
HStack { | ||
Text(title) | ||
.font(.system(size: 16, weight: .medium)) | ||
.lineLimit(1) | ||
if hasChip { | ||
Text("chip") | ||
.font(.system(size: 12)) | ||
.padding(.horizontal, 8) | ||
.padding(.vertical, 4) | ||
.background(Color.red.opacity(0.1)) | ||
.foregroundColor(Color.red) | ||
.cornerRadius(12) | ||
} | ||
} | ||
|
||
Text(subTitle) | ||
.font(.caption1m) | ||
.foregroundColor(.gray) | ||
.lineLimit(1) | ||
|
||
Text(cellTitle) | ||
.font(.body1b) | ||
.foregroundColor(.gray) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
.padding(.vertical, 8) | ||
.padding(.horizontal, 12) | ||
.background( | ||
RoundedRectangle(cornerRadius: 4) | ||
.stroke(Color.gray.opacity(0.3), lineWidth: 1) | ||
) | ||
} | ||
.layoutPriority(1) | ||
|
||
//Todo: 실제 이미지로 교체 | ||
RoundedRectangle(cornerRadius: 8) | ||
.fill(Color.gray.opacity(0.1)) | ||
.frame(width: 98.adjusted, height: 98.adjusted) | ||
.layoutPriority(0) | ||
} | ||
.padding(.horizontal, 16) | ||
.frame(height: 120.adjusted) | ||
} | ||
} | ||
|
||
struct BottomSheetListView: View { | ||
@State private var currentStyle: BottomSheetStyle = .minimal | ||
@State private var offset: CGFloat = 0 | ||
@GestureState private var isDragging: Bool = false | ||
|
||
// 각 상태별 높이값을 저장 | ||
private var snapPoints: [CGFloat] { | ||
[ | ||
BottomSheetStyle.minimal.height, | ||
BottomSheetStyle.half.height, | ||
BottomSheetStyle.full.height | ||
] | ||
} | ||
|
||
private func getClosestSnapPoint(to offset: CGFloat) -> BottomSheetStyle { | ||
let screenHeight = UIScreen.main.bounds.height | ||
let currentHeight = screenHeight - offset | ||
|
||
// 현재 높이와 각 스냅 포인트와의 거리를 계산 | ||
let distances = [ | ||
(abs(currentHeight - BottomSheetStyle.minimal.height), BottomSheetStyle.minimal), | ||
(abs(currentHeight - BottomSheetStyle.half.height), BottomSheetStyle.half), | ||
(abs(currentHeight - BottomSheetStyle.full.height), BottomSheetStyle.full) | ||
] | ||
|
||
// 가장 가까운 스냅 포인트 반환 | ||
return distances.min(by: { $0.0 < $1.0 })?.1 ?? .minimal | ||
} | ||
|
||
var body: some View { | ||
GeometryReader { _ in | ||
VStack(spacing: 0) { | ||
// 핸들바 영역 | ||
VStack(spacing: 8) { | ||
RoundedRectangle(cornerRadius: 3) | ||
.fill(Color.gray.opacity(0.5)) | ||
.frame(width: 36, height: 5) | ||
.padding(.top, 10) | ||
|
||
Text("타이틀") | ||
.font(.system(size: 18, weight: .semibold)) | ||
.padding(.bottom, 8) | ||
} | ||
.frame(height: 60) | ||
.background(Color.white) | ||
|
||
// 컨텐츠 영역 | ||
ScrollView { | ||
LazyVStack(spacing: 0) { | ||
ForEach(0..<5) { _ in | ||
BottomSheetListItem( | ||
title: "상호명", | ||
subTitle: "주소", | ||
cellTitle: "제목 셀", | ||
hasChip: true | ||
) | ||
Divider() | ||
} | ||
} | ||
} | ||
.disabled(currentStyle == .minimal) | ||
} | ||
.frame(maxHeight: .infinity) | ||
.background(Color.white) | ||
.cornerRadius(10, corners: [.topLeft, .topRight]) | ||
.offset(y: UIScreen.main.bounds.height - currentStyle.height + offset) | ||
.gesture( | ||
DragGesture() | ||
.updating($isDragging) { _, state, _ in | ||
state = true | ||
} | ||
.onChanged { value in | ||
let translation = value.translation.height | ||
|
||
if currentStyle == .full && translation < 0 { | ||
offset = 0 | ||
} else { | ||
offset = translation | ||
} | ||
} | ||
.onEnded { value in | ||
let translation = value.translation.height | ||
let velocity = value.predictedEndTranslation.height - translation | ||
|
||
// 속도가 빠른 경우 (빠른 스와이프) | ||
if abs(velocity) > 500 { | ||
if velocity > 0 { // 아래로 빠른 스와이프 | ||
switch currentStyle { | ||
case .full: currentStyle = .half | ||
case .half: currentStyle = .minimal | ||
case .minimal: break | ||
} | ||
} else { // 위로 빠른 스와이프 | ||
switch currentStyle { | ||
case .full: break | ||
case .half: currentStyle = .full | ||
case .minimal: currentStyle = .half | ||
} | ||
} | ||
} else { | ||
// 일반적인 드래그의 경우 | ||
let screenHeight = UIScreen.main.bounds.height | ||
let currentOffset = screenHeight - currentStyle.height + translation | ||
currentStyle = getClosestSnapPoint(to: currentOffset) | ||
} | ||
|
||
// 오프셋 초기화 | ||
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { | ||
offset = 0 | ||
} | ||
} | ||
) | ||
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: currentStyle) | ||
} | ||
} | ||
} | ||
|
||
#Preview { | ||
Home() | ||
} |
28 changes: 28 additions & 0 deletions
28
Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/BottomSheetStyle.swift
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// | ||
// BottomSheetStyle.swift | ||
// SpoonMe | ||
// | ||
// Created by 이지훈 on 1/12/25. | ||
// | ||
|
||
import SwiftUI | ||
|
||
enum BottomSheetStyle { | ||
case full | ||
case half | ||
case minimal | ||
|
||
var height: CGFloat { | ||
let screenHeight = UIScreen.main.bounds.height | ||
// let bottomSafeArea = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0 | ||
|
||
switch self { | ||
case .full: | ||
return screenHeight * 0.876 | ||
case .half: | ||
return screenHeight * 0.5 | ||
case .minimal: | ||
return screenHeight * 0.25 | ||
} | ||
} | ||
} |
99 changes: 99 additions & 0 deletions
99
Spoony-iOS/Spoony-iOS/Resource/Components/BottomSheet/CustomBottomSheet.swift
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// | ||
// CustomBottomSheet.swift | ||
// SpoonMe | ||
// | ||
// Created by 이지훈 on 1/12/25. | ||
// | ||
|
||
import SwiftUI | ||
|
||
struct RoundedCorner: Shape { | ||
var radius: CGFloat = .infinity | ||
var corners: UIRectCorner = .allCorners | ||
|
||
func path(in rect: CGRect) -> Path { | ||
let path = UIBezierPath(roundedRect: rect, | ||
byRoundingCorners: corners, | ||
cornerRadii: CGSize(width: radius, height: radius)) | ||
return Path(path.cgPath) | ||
} | ||
} | ||
|
||
struct CustomBottomSheet<Content: View>: View { | ||
let style: BottomSheetStyle | ||
@Binding var isPresented: Bool | ||
let content: Content | ||
|
||
@GestureState private var translation: CGFloat = 0 | ||
@State private var offsetY: CGFloat = 0 | ||
|
||
private let headerHeight: CGFloat = 60 | ||
|
||
init(style: BottomSheetStyle, isPresented: Binding<Bool>, @ViewBuilder content: () -> Content) { | ||
self.style = style | ||
self._isPresented = isPresented | ||
self.content = content() | ||
} | ||
|
||
var body: some View { | ||
GeometryReader { geometry in | ||
ZStack(alignment: .top) { | ||
if isPresented { | ||
Color.black | ||
.opacity(0.3) | ||
.ignoresSafeArea() | ||
.onTapGesture { | ||
isPresented = false | ||
} | ||
} | ||
|
||
VStack(spacing: 0) { | ||
VStack(spacing: 8) { | ||
RoundedRectangle(cornerRadius: 3) | ||
.fill(Color.gray300) | ||
.frame(width: 36.adjusted, height: 5.adjusted) | ||
.padding(.top, 10) | ||
|
||
Text("타이틀") | ||
.font(.body2b) | ||
.padding(.bottom, 8) | ||
} | ||
.frame(height: headerHeight) | ||
.background(Color.white) | ||
|
||
// 스크롤 가능한 컨텐츠 표기용 | ||
content | ||
.frame(maxWidth: .infinity, maxHeight: .infinity) | ||
.background(Color.white) | ||
} | ||
.frame(height: style.height - offsetY) | ||
.background(Color.white) | ||
.cornerRadius(10) | ||
.offset(y: max(geometry.size.height - style.height + offsetY + translation, 0)) | ||
.animation(.interactiveSpring(), value: isPresented) | ||
.gesture( | ||
DragGesture() | ||
.updating($translation) { value, state, _ in | ||
state = value.translation.height | ||
} | ||
.onEnded { value in | ||
let snapDistance = style.height * 0.25 | ||
let dragDistance = value.translation.height | ||
|
||
if dragDistance > snapDistance { | ||
isPresented = false | ||
} else { | ||
offsetY = 0 | ||
} | ||
} | ||
) | ||
} | ||
.frame(maxWidth: .infinity, maxHeight: .infinity) | ||
} | ||
.ignoresSafeArea() | ||
} | ||
} | ||
|
||
#Preview { | ||
Home() | ||
} |
File renamed without changes.
File renamed without changes.
This file contains 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
Oops, something went wrong.