Skip to content

Conversation

@youz2me
Copy link
Member

@youz2me youz2me commented Mar 9, 2025

👻 PULL REQUEST

📄 작업 내용

  • 경고, 로딩, 완료, 에러 상태를 Enum 케이스로 분리해 간편하게 사용할 수 있는 커스텀 ToastView를 구현했어요.
경고 토스트 로딩 토스트 완료 토스트 에러 토스트
  • 디자인 명세에 따라 네비게이션 바를 Enum 케이스로 분리해 TabBarController 내에서 간편하게 사용할 수 있는 커스텀 NavigationView를 구현했어요.
IPhone 13 mini NavigationView 테스트 코드 (네이밍은 무시 부탁드립니다 ㅎㅎ ...)
  • 디자인 명세에 따라 홈, 커뮤니티, 소식, 뷰잇, 프로필로 구성된 TabBarController를 구현하고 SceneDelegate에 연결했어요.
IPhone 13 mini SceneDelegate 코드
  • ConstraintMaker 익스텐션을 선언해 SnapKit을 이용한 제약조건 설정 시 adjustedWidth와 adjustedHeight를 자동으로 적용할 수 있는 widthEqualTo, heightEqualTo 메서드를 구현했어요.

💻 주요 코드 설명

커스텀 ToastView 구현

  • ToastView에서는 ToastType Enum을 이용해 토스트 메시지 타입을 정의합니다.
  • 로딩, 완료, 경고, 오류 4가지 상태로 이루어집니다.
// MARK: - Toast Types

/// 토스트 메시지 타입을 정의하는 `ToastType` 열거형.
///
/// - `loading`: 로딩 중 상태를 나타내는 토스트
/// - `complete`: 작업 완료를 나타내는 토스트
/// - `caution`: 경고를 나타내는 토스트
/// - `error`: 오류 상태를 나타내는 토스트
enum ToastType {
    case loading
    case complete
    case caution
    case error
}
  • 사용 시에는 토스트 상태와 표시될 에러 메시지를 파라미터로 받습니다.
  • 에러 메시지는 2줄까지 작성할 수 있으며, 제약조건을 Label 크기에 맞게 토스트 높이가 늘어나도록 구현했습니다.
/// 제약조건 설정
    private func setupConstraint() {
        statusImageView.snp.makeConstraints {
            $0.centerY.equalToSuperview()
            $0.leading.equalToSuperview().offset(12)
            $0.widthEqualTo(32)
            $0.heightEqualTo(32)
        }
        
        statusLabel.snp.makeConstraints {
            $0.verticalEdges.equalToSuperview().inset(8)
            $0.leading.equalTo(statusImageView.snp.trailing).offset(6)
        }
    }

...

func show() {
        guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
              let window = windowScene.windows.first
        else { return }
        
        window.addSubview(self)
        
        self.snp.makeConstraints {
            $0.top.equalToSuperview().offset(76)
            $0.horizontalEdges.equalToSuperview().inset(16)
            $0.height.greaterThanOrEqualTo(48.adjustedHeight)
        }
        
        animate()
    }
  • 사용 예시는 아래와 같습니다. 따로 뷰 추가나 제약 조건 설정 없이 show 메서드만 호출하시면 됩니다.
let toast = ToastView(status: .complete, message: "완료되었습니다")
toast.show()

커스텀 NavigationViewTabBarController 구현

  • 디자인 명세에 따라 네비게이션 바 타입을 정의하고, 해당 타입에 따라 Enum 케이스를 분리했습니다.
// MARK: - Navigation Types

/// `UIView`의 네비게이션 타입을 정의하는 `NavigationType` 열거형.
///
/// - `home`: 메인 화면에 사용되는 네비게이션 타입
///   - `hasNewNotification`: 새로운 알림 존재 여부, 해당 값에 따라 알림 버튼 이미지 변경
/// - `flow`: 단계별 흐름(온보딩, 프로세스 등)에 사용되는 네비게이션 타입
/// - `page`: 일반적인 페이지 화면에 사용되는 네비게이션 타입
///   - `type`: 페이지의 타입 (plain, detail, profile)
///   - `text`: 네비게이션 바에 표시될 제목
/// - `hub`: 탭 기반 메인 화면에 사용되는 네비게이션 타입
///   - `text`: 네비게이션 바에 표시될 제목 (기본값: "")
///   - `isBeta`: 베타 기능 표시 여부 (기본값: false)
enum NavigationType {
    case home(hasNewNotification: Bool)
    case flow
    case page(type: PageType, text: String)
    case hub(text: String = "", isBeta: Bool = false)
    
    var isHub: Bool {
        if case .hub = self {
            return true
        } else {
            return false
        }
    }
}

// MARK: - Page Types

/// 페이지 타입을 정의하는 `PageType` 열거형.
///
/// - `plain`: 일반 페이지
/// - `detail`: 상세 페이지 (뒤로가기 버튼 포함)
/// - `profile`: 프로필 페이지 (메뉴 버튼 포함)
enum PageType {
    case plain
    case detail
    case profile
}
  • 모든 컴포넌트를 선언한 뒤 숨김 처리하고, 케이스에 따라 해당하는 컴포넌트만 숨김 처리를 해제하는 메서드 configureVisibleView()를 구현해 적용했습니다.
private extension NavigationView {
    func configureVisibleView() {
        var visibleViewList: [UIView] = []
        
        switch type {
        case .home(hasNewNotification: let hasNewNotification):
            notificationButton.setImage(hasNewNotification ? .icNotiBadge : .icNotiDefault, for: .normal)
            
            visibleViewList = [
                logoImageView,
                homeUnderLineView,
                notificationButton
            ]
        case .flow:
            visibleViewList = [
                backButton,
                dismissButton
            ]
        case .page(type: let type, text: let text):
            pageTitleLabel.attributedText = text.pretendardString(with: .body3)
            
            switch type {
            case .plain:
                visibleViewList = [pageTitleLabel]
            case .detail:
                visibleViewList = [
                    pageTitleLabel,
                    backButton,
                    pageUnderLineView
                ]
            case .profile:
                visibleViewList = [
                    pageTitleLabel,
                    menuButton,
                    pageUnderLineView
                ]
            }
        case .hub(text: let text, isBeta: let isBeta):
            backgroundColor = .wableBlack
            hubTitleLabel.attributedText = text.pretendardString(with: .head2)
            
            visibleViewList = [
                hubImageView,
                hubTitleLabel,
                homeUnderLineView
            ]
            
            isBeta ? visibleViewList.append(betaImageView) : nil
        }
        
        visibleViewList.forEach { $0.isHidden = false }
    }
}

ConstraintMaker 익스텐션

  • adjustedWidthadjustedHeight를 자동으로 적용할 수 있는 widthEqualTo, heightEqualTo 메서드를 구현했습니다.
  • 사용 예시는 아래와 같습니다.
view.snp.makeConstraints {
    $0.heightEqualTo(200) // $0.height.equalTo(200.adjustedHeight)와 같은 효과
}

👀 기타 더 이야기해볼 점

  • 충돌 및 PR 단위 거대화를 방지하기 위한 중간 PR입니다.

🔗 연결된 이슈

@youz2me youz2me added ✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌 labels Mar 9, 2025
@youz2me youz2me requested a review from JinUng41 March 9, 2025 21:51
@youz2me youz2me self-assigned this Mar 9, 2025
@youz2me youz2me changed the title [Feat] TabBarController, ToastView 및 ConstraintMaker 익스텐션 구현 [Feat] TabBarController, ToastView 작성 및 ConstraintMaker 익스텐션 구현 Mar 9, 2025
Copy link
Collaborator

@JinUng41 JinUng41 left a comment

Choose a reason for hiding this comment

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

종류 정의와 그에 따른 매핑 코드가 이해하기 쉽도록 구성되어져서 사용하기에 간편할 것 같습니다.
정말 좋은 코드를 작성해 주신 것 같아요!

몇 가지 코멘트만 확인 부탁드려요~
Figma에 반영이 되지 않은게 자꾸 나와서 참으로 속상하네요..ㅠㅠ

Comment on lines 27 to 30
@discardableResult
func widthEqualTo(_ float: CGFloat) -> ConstraintMakerEditable {
return self.width.equalTo(float.adjustedWidth)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

메서드 명을 adaptiveWidthEqualTo 혹은 adjustedWidthEqualTo라고 변경가능함도 생각해 보았습니다.
물론 지금의 방식도 상관없을 것 같아요.
그냥 그런 생각이 들었기에 말씀드려봅니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

저도 adjustedWidthEqualTo가 조금 더 메서드의 의도를 잘 나타내는 것 같다는 생각이 드네요! 좋은 의견 감사합니다.
widthEqualTo를 가져갔을 때 문제가 되는 상황도 있을까요? 일단 adjustedWidthEqualTo로 리팩토링 진행해보겠습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

사실 괜한 걱정일수도 있는데요.
모르는 사람이 사용되는 코드만 보면 SnapKit의 equalTo가 있는데, widthEqualTo는 왜 구현했을까 하는 생각이 들 수도 있겠다는 생각이 들어서요.

Copy link
Member Author

Choose a reason for hiding this comment

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

이미 있는 메서드랑 네이밍이 중복돼서 더 그렇게 느껴질 수도 있겠네요 !! 좋은 의견 감사드립니다 ㅎㅎ 리뷰 반영했습니다!

Comment on lines -30 to 53

extension CGFloat: ScreenAdjustable {
extension CGFloat {
/// 기준 해상도에 대한 너비 비율 (기본값: 375pt)
private static let widthRatio: CGFloat = UIScreen.main.bounds.width / 375
Copy link
Collaborator

Choose a reason for hiding this comment

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

extension의 영역이 fileprivate으로 제한된다면, 외부에서 CGFloat 값을 선언 후 사용할 때 adjusted 와 같은 내용을 몰라도 되어서 은닉화를 이룰 수 있을 것 같습니다만, 어떻게 생각하실까요?

fileprivate extension CGFloat {}

Copy link
Member Author

Choose a reason for hiding this comment

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

앗 제가 이 부분을 본문에 적어둔다는 걸 깜빡했네요 ...!! UIView 익스텐션의roundCorners 메서드에서 cornerRadius를 설정하는 부분을 adjustedWidth에 맞춰 변경했는데요. filePrivate 키워드를 사용하면 해당 익스텐션에서 adjustedWidth를 사용할 수 없기 때문에 키워드를 사용하지 않았습니다.

func roundCorners(_ corners: [Corner], radius: CGFloat) {
        layer.cornerRadius = radius.adjustedWidth
        layer.masksToBounds = true

Comment on lines 41 to 52
// MARK: - Page Types

/// 페이지 타입을 정의하는 `PageType` 열거형.
///
/// - `plain`: 일반 페이지
/// - `detail`: 상세 페이지 (뒤로가기 버튼 포함)
/// - `profile`: 프로필 페이지 (메뉴 버튼 포함)
enum PageType {
case plain
case detail
case profile
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Navigation Type의 중첩 타입으로 PageType을 구현하는 것은 어떠실까요?
처음에 분리가 되있어, NavigationType * PageType을 조합하여 네비게이션 바를 이룰 수 있는건가 하는 의문이 생겼었습니다.
헷갈릴 수 있는 여지가 있을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

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

엇 저도 그 부분을 좀 우려했는데 중첩 타입으로 정의하면 타입 간 관계가 조금 더 명확해질 수 있을 것 같아요! 좋은 의견 감사합니다 ㅎㅎ 반영해보겠습니다!

Comment on lines +17 to +18
/// - `home`: 메인 화면에 사용되는 네비게이션 타입
/// - `hasNewNotification`: 새로운 알림 존재 여부, 해당 값에 따라 알림 버튼 이미지 변경
Copy link
Collaborator

Choose a reason for hiding this comment

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

안타깝게도 안드로이드와 아요 모두 마찬가지로 홈 네비게이션 바에 알림 아이콘은 존재하지 않아요.
아무래도 구현하는 중에 제외되었는데 미처 반영되지 않은 것 같아요.
이 부분은 제가 TL에게 직접 연락해서 반영되도록 하겠습니다.

Copy link
Member Author

Choose a reason for hiding this comment

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

이럴수가............................ 제 세상이 무너졌습니다
그러면 혹시 알림 화면으로 진입하는 부분이 어디인지도 알 수 있을까요 ??

Copy link
Collaborator

@JinUng41 JinUng41 Mar 10, 2025

Choose a reason for hiding this comment

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

유진님께서 설명하신 부분이 맞았습니다.
오히려 제가 미처 파악하지 못한 부분이었네요,, 죄송합니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

아닙니다 ㅎㅎ 다시 반영했습니다!

Comment on lines 146 to 147
else { return }

Copy link
Collaborator

Choose a reason for hiding this comment

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

아래와 같이 개행되어야 합니다!

else {
    return
}

Copy link
Member Author

Choose a reason for hiding this comment

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

엇 반영했습니다 !! 😳

@youz2me youz2me merged commit d28550e into develop Mar 10, 2025
@youz2me youz2me deleted the feat/#118-common-component branch March 10, 2025 11:55
@youz2me youz2me linked an issue Mar 10, 2025 that may be closed by this pull request
7 tasks
youz2me added a commit that referenced this pull request Oct 26, 2025
[Feat] TabBarController, ToastView 작성 및 ConstraintMaker 익스텐션 구현
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feat 기능 또는 객체 구현 🦉 유진 🛌🛌🛌🛌🛌🛌🛌🛌🛌🛌

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 공통으로 사용되는 컴포넌트 구현하기

3 participants