Skip to content

[week7] Watcha SwiftUI 클론#6

Open
gleamminn wants to merge 21 commits into
developfrom
week7
Open

[week7] Watcha SwiftUI 클론#6
gleamminn wants to merge 21 commits into
developfrom
week7

Conversation

@gleamminn
Copy link
Copy Markdown
Collaborator

@gleamminn gleamminn commented May 29, 2026

🔗 7주차 과제

📄 작업 내용

🍎 기본 과제

  • 왓챠 UI, 화면설계서를 확인하여 기능/view 구현
    • Tab Bar (탭바 간 이동 가능)
    • 가로 스크롤 View들
    • ‘메인으로’ 버튼과 메인 뷰 연결

🍏 심화 과제

  • Sticky Header
  • 신작 업데이트 플로팅 뷰
  • Welcome View (메인 뷰로 넘어가는 네비게이션)
구현 내용 IPhone 17 pro IPhone 13 mini
GIF

💻 주요 코드 설명 및 Trouble Shooting 🚀

파일은 아래와 같이 구성했습니다.

1) View 재사용

먼저 동일한 UI를 가진 부분을 파악했습니다.

1) 상단에 큰 포스터 캐러셀 뷰(월간남친 포스터 부분), 방금 막 도착한 신상 컨텐츠, 왓고리즘, 공개예정 콘텐츠의 아이템들은 모두 둥근 포스터 이미지로 크기와 cornerRadius만 다르고 동일합니다.
-> PosterCardView를 구현한 뒤 여러 뷰에서 재사용했습니다. (cornerRadius를 변수로 받음)

2) 공개 예정 콘텐츠 Header와 왓챠 파티 Header는 title 제외 동일합니다.
-> CommonHeaderView를 구현한 뒤 재사용했습니다. (title을 변수로 받음)

3) 왓고리즘의 가로 스크롤 뷰와 공개 예정 콘텐츠의 가로 스크롤 뷰는 동일합니다.
-> CommonView를 구현한 뒤 재사용했습니다.

2) Stack 활용

구독 탭의 전체 뷰는 세로 스크롤이 되지만 [상단 네비게이션바, 구독 스티키 헤더, 하단에 구독 시작하기 플로팅 뷰]는 스크롤 되면 안됩니다.
아래 코드와 같이, 가장 바깥 쪽에 ZStack을 두고 스크롤 되어야하는 부분은 ScrollView안에 VStack으로 감싸 넣어줬고, 스크롤 안되는 부분은 ZStack 안에 Vstack 안에 넣어줬습니다.

struct SubscriptionView: View {
// 생략
    var body: some View {
        ZStack(alignment: .top) {
            ScrollView(.vertical, showsIndicators: false) {
                VStack(spacing: 0) {
                    // 스크롤 되는 부분
                }
            }
            
            // 스크롤 안되는 부분
            VStack {
                NavigationTopBarView()
                StickyHeaderView(progress: progress)
            }
            
            VStack {
                FloatingUpdateView()
                    .padding(.bottom, 14)
            }
        }
        .background(.watchaBlack)
        .ignoresSafeArea(edges: .top)
    }
}

3) 상태 기반 Navigation

WelcomeView에서 메인으로 버튼을 누르면 WatchaTabBarView로 이동하도록 구현했습니다.

  • navigateToMain이라는 @State 바인딩 속성을 사용합니다.
  • 버튼이 클릭되면, 상태 값이 true로 변경되고 화면이 이동합니다.
struct WelcomeView: View {
    @State private var navigateToMain = false
    
    var body: some View {
        NavigationStack {
            ZStack {
          // 생략
                VStack(spacing: 0) {
                    WelcomeButton {
                        navigateToMain = true
                    }
                }
            }
            .navigationDestination(isPresented: $navigateToMain) {
                WatchaTabBarView()
            }
        }
    }
}

3) Sticky Header

스크롤 위치(Offset)의 변화에 따라 헤더의 크기와 텍스트 스타일이 변경되는 스티키 헤더를 구현했습니다.
UIKit으로 구현했을 때와 거의 동일한 방법으로 구현했어요 !

- GeometryReader & PreferenceKey 활용
최상단에 투명한 뷰를 배치하고 GeometryReader로 minY 좌표를 확인하고 PreferenceKey(ScrollOffsetPreferenceKey)를 통해 상위 뷰로 전달합니다.

- progress
스크롤 offset을 threshold: 50.0을 기준으로 변화율로 확인합니다.

// 스크롤 위치에 따라 진행률 계산
private var progress: CGFloat {
    let threshold: CGFloat = 50.0
    return min(1, max(0, -scrollOffset / threshold))
}

// 스크롤 시 헤더가 상단으로 올라가도록 offset 계산
private var topOffset: CGFloat {
    return -(31 * progress)
}

- 구독 헤더 뷰는 계산된 progress 값만 주입받아 스타일을 변경합니다.

// 스티키되면 폰트가 작아짐
private var titleFont: Font {
    return progress > 0.5 ? .head2 : .head1
}

// 스티키되면 회섹 선 올라감
private var bottomPadding: CGFloat {
    return 15 - (7 * progress)
}

// 스티키되면 텍스트 올라감
private var textYOffset: CGFloat {
    return 4 * progress
}

📚 참고자료

👀 To Reviewers

image

스유 만세...만세..만세..
7셈까지 다들 너무 고생 많았어요 !!!!! 코드 리뷰 많이 달아주세요 😘

@gleamminn gleamminn self-assigned this May 29, 2026
@gleamminn gleamminn linked an issue May 29, 2026 that may be closed by this pull request
7 tasks
Copy link
Copy Markdown

@sssthnnhee sssthnnhee left a comment

Choose a reason for hiding this comment

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

수민님~~~~ 이번주도 고생많으셨습니다!!!!
아주아주 많이 알아갑니다...

WatchaContent(imageResource: .main1), WatchaContent(imageResource: .main3)
]

static let partyMockData: [WatchaContent] = [
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저는. . 여기 하드코딩했는데...... 이렇게 직접 데이터를 넣으니까 아름답습니다

// MARK: - Body

var body: some View {
VStack(alignment: .leading, spacing: 0) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저 spacing 기본이 0인줄알았는데 이런 세상에
찾아보니까 8. 정도나 있다고 ...
지금 제. 코드가 고칠것투성이라는 것을 느낍니다

.resizable()
.scaledToFill()
.frame(width: 35, height: 35)
.padding([.top, .trailing], 8)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

패딩값이 같으면 [ ]로 묶을수있군요

.scrollTargetLayout()
.padding(.horizontal, sidePadding)
}
.scrollTargetBehavior(.viewAligned)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

우와아 이런 멋진 기능도 있군요

.frame(width: cardWidth, height: 180)
}
}
.padding(.horizontal, (screenWidth - cardWidth) / 2)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

와.. 이렇게 해서 scrollTargetBehavior(.viewAligned) 를 햇을때
맨첫번째랑 맨마지막 카드도 제대로 정렬될수잇는거군요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[week7] Watcha SwiftUI 클론

2 participants