Skip to content

Commit

Permalink
Merge pull request #44 from SOPT-all/style/#41-componentsInit
Browse files Browse the repository at this point in the history
[Style] 바텀시트 배치
  • Loading branch information
hooni0918 authored Jan 15, 2025
2 parents 1a7a722 + d4ebf8b commit 17c809b
Show file tree
Hide file tree
Showing 14 changed files with 353 additions and 295 deletions.
17 changes: 17 additions & 0 deletions Spoony-iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,22 @@
<string>Pretendard-Bold.otf</string>
<string>Pretendard-SemiBold.otf</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>
4 changes: 4 additions & 0 deletions Spoony-iOS/Spoony-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@
};
78C002172D1D9FC500DA101C /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = 78C0020A2D1D9FC400DA101C /* Spoony-iOS */;
baseConfigurationReferenceRelativePath = Config/Config.xcconfig;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
Expand Down Expand Up @@ -331,6 +333,8 @@
};
78C002182D1D9FC500DA101C /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = 78C0020A2D1D9FC400DA101C /* Spoony-iOS */;
baseConfigurationReferenceRelativePath = Config/Config.xcconfig;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
Expand Down
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()
}
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
}
}
}
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()
}
5 changes: 5 additions & 0 deletions Spoony-iOS/Spoony-iOS/Resource/Extension/View+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,9 @@ extension View {
self
}
}

//홈 바텀시트 코너 radius
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}
Loading

0 comments on commit 17c809b

Please sign in to comment.