Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "chevron_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "chevron_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "chevron_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "plus_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "plus_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "plus_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
170 changes: 170 additions & 0 deletions Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!! 혹시 sheetPresentationController를 고려해보셨을까요?
따로 커스텀 바텀 시트를 VC를 구현하신 이유가 있는지 궁금합니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 혹시 sheetPresentationController 이것도 MaxHeight를 설정할 수 있나요 ??
디자인 요구사항에서 딱 최대 높이 이상은 못올라가게 했으면 좋겠다는 의견이 있어서 CustomBottomSheet를 구현했습니다 !! ㅠㅠ

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 알기론느 detent를 이용해서 최대 높이를 정해줄 수 있는것으로 알고 있습니다! 다만 iOS 버전에 따라 조금 다를 수 있을거 같아서 확인이 필요할 것 같네요!
그런 제약 조건이 있는걸 몰랐어요.!.! 커스텀 시트 구현해주셔서 오히려 좋아입니다~

Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//
// CustomBottomSheet.swift
// Presentation
//
// Created by 최정인 on 7/14/25.
//

import SnapKit
import UIKit

final class CustomBottomSheet: UIViewController {

private enum Layout {
static let bottomSheetCornerRadius: CGFloat = 20
static let dragHandleTopSpacing: CGFloat = 16
static let dragHandleCornerRadius: CGFloat = 2
static let dragHandleHeight: CGFloat = 4
static let dragHandleWidth: CGFloat = 32
static let contentViewTopSpacing: CGFloat = 16
}

private let maxHeight: CGFloat
private let contentViewController: UIViewController

private let dimmedView = UIView()
private let bottomSheetView = UIView()
private let dragHandle = UIView()
private let contentView = UIView()
private var bottomSheetBottomConstraint: Constraint?

init(contentViewController: UIViewController, maxHeight: CGFloat = 300) {
self.contentViewController = contentViewController
self.maxHeight = maxHeight
super.init(nibName: nil, bundle: nil)

modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
configureAttribute()
configureLayout()
setupContent()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
showBottomSheet()
}

private func configureAttribute() {
dimmedView.do {
$0.backgroundColor = UIColor.black.withAlphaComponent(0.7)
$0.alpha = 0
}

bottomSheetView.do {
$0.backgroundColor = UIColor.systemBackground
$0.layer.cornerRadius = Layout.bottomSheetCornerRadius
$0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
}

dragHandle.do {
$0.backgroundColor = UIColor.systemGray4
$0.layer.cornerRadius = Layout.dragHandleCornerRadius
}

contentView.do {
$0.backgroundColor = .clear
}

let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissBottomSheet))
dimmedView.addGestureRecognizer(tapGesture)

let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
bottomSheetView.addGestureRecognizer(panGesture)
}

private func configureLayout() {
view.addSubview(dimmedView)
view.addSubview(bottomSheetView)
bottomSheetView.addSubview(dragHandle)
bottomSheetView.addSubview(contentView)

dimmedView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}

bottomSheetView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.height.equalTo(maxHeight)
self.bottomSheetBottomConstraint = make.bottom.equalToSuperview().offset(maxHeight).constraint
}

dragHandle.snp.makeConstraints { make in
make.top.equalToSuperview().offset(Layout.dragHandleTopSpacing)
make.centerX.equalToSuperview()
make.width.equalTo(Layout.dragHandleWidth)
make.height.equalTo(Layout.dragHandleHeight)
}

contentView.snp.makeConstraints { make in
make.top.equalTo(dragHandle.snp.bottom).offset(Layout.contentViewTopSpacing)
make.leading.trailing.equalToSuperview()
make.bottom.equalToSuperview()
}
}

private func setupContent() {
addChild(contentViewController)
contentView.addSubview(contentViewController.view)
contentViewController.didMove(toParent: self)

contentViewController.view.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}

private func showBottomSheet() {
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
self.dimmedView.alpha = 1
self.bottomSheetBottomConstraint?.update(offset: 0)
self.view.layoutIfNeeded()
}
}

@objc private func dismissBottomSheet() {
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
self.dimmedView.alpha = 0
self.bottomSheetBottomConstraint?.update(offset: self.maxHeight)
self.view.layoutIfNeeded()
} completion: { _ in
self.dismiss(animated: false)
}
}

@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: view)
let velocity = gesture.velocity(in: view)

switch gesture.state {
case .changed:
let offset = max(0, translation.y)
bottomSheetBottomConstraint?.update(offset: offset)

let progress = min(1, offset / maxHeight)
dimmedView.alpha = 1 - progress

case .ended:
let shouldDismiss = translation.y > maxHeight * 0.3 || velocity.y > 1000
if shouldDismiss {
dismissBottomSheet()
} else {
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
self.bottomSheetBottomConstraint?.update(offset: 0)
self.dimmedView.alpha = 1
self.view.layoutIfNeeded()
}
}

default:
break
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ enum BitnagilIcon {
static let kakaoIcon = UIImage(named: "kakao_icon", in: bundle, with: nil)
static let appleIcon = UIImage(named: "apple_icon", in: bundle, with: nil)
static let checkIcon = UIImage(named: "check_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysTemplate)
static let plusIcon = UIImage(named: "plus_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysTemplate)
static let chevronIcon = UIImage(named: "chevron_icon", in: bundle, with: nil)
static func chevronIcon(direction: Direction) -> UIImage? {
return BitnagilIcon.chevronIcon?.rotate(degrees: direction.rotation)?.withRenderingMode(.alwaysTemplate)
}

// MARK: - Tab Bar Icons
static let homeFillIcon = UIImage(named: "home_fill_icon", in: bundle, with: nil)
Expand All @@ -29,3 +34,19 @@ enum BitnagilIcon {
static let mypageFillIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil)
static let mypageEmptyIcon = UIImage(named: "mypage_empty_icon", in: bundle, with: nil)
}

enum Direction {
case up
case down
case left
case right

var rotation: Float {
switch self {
case .up: 90
case .down: -90
case .left: 0
case .right: 180
}
}
}
72 changes: 72 additions & 0 deletions Projects/Presentation/Sources/Common/Extension/UIImage+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// UIImage+.swift
// Presentation
//
// Created by 최정인 on 7/17/25.
//

import UIKit

extension UIImage {
// 받은 크기로 이미지 크기를 변경합니다. (원본 비율을 무시될 수 있습니다.)
func resize(to size: CGSize) -> UIImage? {
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { _ in
draw(in: CGRect(origin: .zero, size: size))
}
}

// 원본 비율 유지하면서 이미지 크기를 변경합니다. (aspect fit)
func resizeAspectFit(to size: CGSize) -> UIImage? {
let aspectRatio = self.size.width / self.size.height
let targetRatio = size.width / size.height

var newSize: CGSize
if aspectRatio > targetRatio {
newSize = CGSize(width: size.width, height: size.width / aspectRatio)
} else {
newSize = CGSize(width: size.height * aspectRatio, height: size.height)
}

let renderer = UIGraphicsImageRenderer(size: newSize)
return renderer.image { _ in
draw(in: CGRect(origin: .zero, size: newSize))
}
}

// 원본 비율 유지하면서 이미지 크기를 변경합니다. (aspect fill)
func resizeAspectFill(to size: CGSize) -> UIImage? {
let aspectRatio = self.size.width / self.size.height
let targetRatio = size.width / size.height

var newSize: CGSize
if aspectRatio > targetRatio {
newSize = CGSize(width: size.height * aspectRatio, height: size.height)
} else {
newSize = CGSize(width: size.width, height: size.width / aspectRatio)
}

let renderer = UIGraphicsImageRenderer(size: newSize)
return renderer.image { _ in
draw(in: CGRect(origin: .zero, size: newSize))
}
}

// 이미지를 회전합니다.
func rotate(radians: Float) -> UIImage? {
let rotatedSize = CGRect(origin: .zero, size: size)
.applying(CGAffineTransform(rotationAngle: CGFloat(radians)))
.integral.size

let renderer = UIGraphicsImageRenderer(size: rotatedSize)
return renderer.image { context in
context.cgContext.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2)
context.cgContext.rotate(by: CGFloat(radians))
draw(in: CGRect(origin: CGPoint(x: -size.width / 2, y: -size.height / 2), size: size))
}
}

func rotate(degrees: Float) -> UIImage? {
return rotate(radians: degrees * .pi / 180)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import UIKit

extension UIViewController {

// MARK: - NavigationBar
func configureNavigationBar(navigationStyle: NavigationBarStyle) {
switch navigationStyle {
case .hidden:
Expand Down Expand Up @@ -74,6 +74,12 @@ extension UIViewController {
let targetViewController = viewControllers[viewControllers.count - 3]
navigationController.popToViewController(targetViewController, animated: true)
}

// MARK: - BottomSheet
func presentCustomBottomSheet(contentViewController: UIViewController, maxHeight: CGFloat) {
let bottomSheet = CustomBottomSheet(contentViewController: contentViewController, maxHeight: maxHeight)
present(bottomSheet, animated: true)
}
}

enum NavigationBarStyle {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,9 @@ public struct PresentationDependencyAssembler: DependencyAssemblerProtocol {

return OnboardingViewModel(onboardingUseCase: onboardingUseCase)
}

DIContainer.shared.register(type: RecommendedRoutineViewModel.self) { _ in
return RecommendedRoutineViewModel()
}
}
}
12 changes: 4 additions & 8 deletions Projects/Presentation/Sources/Common/TabBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ final public class TabBarView: UITabBarController {
tabBar.tintColor = BitnagilColor.navy600
tabBar.unselectedItemTintColor = BitnagilColor.navy100

guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
fatalError("RecommendedViewModel 의존성이 등록되지 않았습니다.")
}
Comment on lines +41 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

의존성 해결 실패 시 더 안전한 에러 처리 방식을 고려해보세요.

fatalError를 사용하면 앱이 크래시되므로, 사용자 경험에 부정적인 영향을 줄 수 있습니다. 이전 학습 내용에 따르면 추후 CustomAlertView를 통한 에러 처리를 계획하고 계신 것으로 보이니, 현재는 임시적인 처리 방식으로 이해하겠습니다.

-        guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
-            fatalError("RecommendedViewModel 의존성이 등록되지 않았습니다.")
-        }
+        guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
+            BitnagilLogger.log(logType: .error, message: "RecommendedRoutineViewModel 의존성이 등록되지 않았습니다.")
+            // 임시 처리: 기본 뷰 또는 에러 뷰 표시
+            return
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
fatalError("RecommendedViewModel 의존성이 등록되지 않았습니다.")
}
guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
BitnagilLogger.log(logType: .error, message: "RecommendedRoutineViewModel 의존성이 등록되지 않았습니다.")
// 임시 처리: 기본 뷰 또는 에러 뷰 표시
return
}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/TabBarView.swift around lines 41 to 43,
replace the fatalError call used when dependency resolution for
RecommendedRoutineViewModel fails with a safer error handling approach. Instead
of crashing the app, implement a mechanism to show a user-friendly error
message, such as displaying a CustomAlertView or logging the error gracefully,
to improve user experience while maintaining app stability.

let homeView = HomeView()
let recommendView = RecommendView()
let recommendView = RecommendedRoutineView(viewModel: viewModel)
let reportView = ReportView()
let mypageView = MypageView()

Expand Down Expand Up @@ -80,13 +83,6 @@ final class HomeView: UIViewController {
}
}

final class RecommendView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
}
}

final class ReportView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Expand Down
Loading
Loading