Skip to content

Conversation

@sangyup12
Copy link
Contributor

@sangyup12 sangyup12 commented Dec 3, 2025

🌴 작업한 브랜치

Style/#185

✅ 작업한 내용

자격증 상세페이지와 마이페이지에 사용되어서 Global/Components에 만들었습니다.

  • 시험 날짜 캘린더
  • 시험 장소 드롭 다운
  • 시험 시간 픽커
  • 자격증 상세에서 Sheet 연결
스크린샷 2025-12-02 오후 3 01 57 스크린샷 2025-12-02 오후 3 02 22 스크린샷 2025-12-02 오후 3 02 50
// 데이트픽커박스
DatePickerBox(selectedDate: $viewModel.CertificationPlanDate)

// 드롭 다운 메뉴
let placeMenuOptions = ["서울", "경기" ,"인천", "강원", "충남", "충북"]

DropdownMenu(selectedPlace: $viewModel.CertificationPlanPlaceDo,
             options: placeMenuOptions,
             menuPlaceholder: "시/도")

// 타임픽커
CertiTimePicker(isAM: $viewModel.isAM, hour: $viewModel.hour, minute: $viewModel.minute)

이런 식으루 쓰면 됩니다.

❗️PR Point

CertiTimePicker 구현. UIKit으로 커스텀 하여 SwiftUI 프로젝트에서 사용하기!

첫번째 사진은 스유에서 제공하는 기본 DatePicker이고 두 번째 사진이 우리 서티에서 사용하게될 Picker 입니다. 커스텀 내용으로는 각 컴포넌트에 들어갈 내용, 컴포넌트에 들어가는 텍스트의 폰트와 색상, 컴포넌트 너비와 높이, 선택된 컴포넌트 회색 배경 지우기, 그리고 선택된 값을 알려주는 line을 추가해주는 것까지가 되겠습니다.
스크린샷 2025-12-02 오후 3 28 55스크린샷 2025-12-02 오후 3 33 42
스유에서 제공되고 있는 DatePicker는 커스텀이 굉장히 제한되기 때문에 UIKit으로 커스텀을 해보았고 그 코드를 어떻게 스유에 붙였나 설명해드리겠습니다!

우선 UIKit을 통해서 UIPickerView를 만들어요.

import UIKit

final class CertiPickerView: UIPickerView {

그리고 line을 그려주고 layout을 잡아줬슴니다.
UIKit의 UIView를 SwiftUI로 에서 쓰려면 UIViewRepresentable 프로토콜을 사용해야 합니다. 이 프로토콜은 makeUIView(context:)updateUIView(uiView:, context:)를 준수해야함!

  • makeUIView는 SwiftUI 뷰가 생성될 때 호출돼서 UIView 인스턴스를 생성하고 초기화.
  • updateUIView는 SwiftUI 뷰가 업데이트 될 때 호출되어 변경된 데이터에 따라 UIView를 업데이트.

그리고 스유와 유킷간의 상태를 연결해주는 사람이 필요한데 Coordinator가 그 일을 합니다. 유킷에서 스유로의 데이터를 전달함(델리게이트 역할). makeCoordinator로 Coordinator 인스턴스를 만들어줌니다.

func makeCoordinator() -> Coordinator {
    Coordinator(self)
}

그러면 UIPickerView의 UIPickerViewDelegate와 UIPickerViewDataSource의 역할을 Coordinator가 하게 됩니다.

class Coordinator: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
    var parent: CustomTimePicker

Coordinator에서 각 컴포넌트에 들어갈 내용, 컴포넌트에 들어가는 텍스트의 폰트와 색상, 컴포넌트 너비와 높이를 정해줬습니다.

그리공 @Binding을 이용하여 SwiftUI에서 UIKit 방향으로 데이터를 업데이트할 수 있습니다.

struct CustomTimePicker: UIViewRepresentable {
    @Binding var isAM: Bool
    @Binding var hour: Int
    @Binding var minute: Int

이렇게 최종적으로

struct CertiTimePicker: View {
    @Binding var isAM: Bool
    @Binding var hour: Int
    @Binding var minute: Int
    
    var body: some View {
        ZStack {
            CustomTimePicker(isAM: $isAM, hour: $hour, minute: $minute)
                .frame(height: 180)
            
            Text(":")
                .applyCertiFont(.caption_semibold_14)
                .foregroundColor(.grayscale600)
                .offset(x: 49, y: 0)
        }
    }
}

ZStack에서 점찍어서 CertiTimePicker를 사용할 수 있게 되었습니다.
이름이 좀 헷갈리는데
CertiPickerView (UIKit): 순수하게 UIPickerView를 상속받아 디자인만 수정한 진짜 유킷.
CustomTimePicker (중간 다리): UIViewRepresentable입니다. UIKit인 CertiPickerView를 감싸서 스유에서 쓸 수 있게 변환해주는 역할.
CertiTimePicker (SwiftUI 컴포넌트): 최종적으로 사용자가 화면에 배치하는 스유 뷰입니다. 내부에 ":" 텍스트와 CustomTimePicker를 배치함.

여기서 Binding이 또 있는 이유는 부모뷰 -> 스유 컴포넌트 -> 유킷뷰 로 전달되고 있습니다.
설명이 헷갈릴 수 있는데 질문 마니 부탁드립니다~

📸 스크린샷

기능 스크린샷
GIF

iOS 26에서 돌리니까 밤티 모달이 되.

📟 관련 이슈

@sangyup12 sangyup12 added this to the [CERTI] 1차 스프린트 milestone Dec 3, 2025
@sangyup12 sangyup12 self-assigned this Dec 3, 2025
@sangyup12 sangyup12 added Style 🖼️ UI 작업 상엽 🌼 상엽 작업 labels Dec 3, 2025
@sangyup12 sangyup12 linked an issue Dec 3, 2025 that may be closed by this pull request
3 tasks
Copy link
Contributor

@Yeonnies Yeonnies left a comment

Choose a reason for hiding this comment

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

잘봤습니다!!
커스텀 타임피커를 어떻게 구조적으로 분리해야할지를 잘 모르겠는데 좀더 분리를 해주면 더 조을거 가타요 유킷은 그렇다 치고 코디네이터 같은 건 분리가 가능할 거 같아서!!
글고 밤티 모달 진짜 어카냐..

고생 많으셨습니닷!!!!

얼음오니

// MARK: - Layout Methods

private extension CertiPickerView {
func hideIndicator() {
Copy link
Contributor

Choose a reason for hiding this comment

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

얘는 무슨 함수인가요?? 인디케이터가 머지요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

pr에 보시믄 기본 datePicker에서 사용되는 중앙에 선택된 시간을 알려주는 회색 바(?) 예요. 우리 서티 픽커에서는 이게 안 보여야해서 안 보이게 해주는 메서드입니다.

}
}

func layoutLines() {
Copy link
Contributor

Choose a reason for hiding this comment

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

레이아웃을 이런식으로 잡는군요! 이렇게 잡으면 오토레이아웃이 잘 적용될듯..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이게 좋아보이지는 않는데 유킷적 관점에서 괜찮나요?

}

struct CustomTimePicker: UIViewRepresentable {
@Binding var isAM: Bool
Copy link
Contributor

Choose a reason for hiding this comment

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

이거 변수명 실환가요..ㅋㅋ
p3.
Meridiem Indicator나 Cycle, Daypart 어떠신지 조심스럽게 추천해봅니다..
근데 덜 직관적이라면 지금처럼 가는 것도 조은거 가타요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

불형이라서 어쩔 수 없시 isAM이라고 했어여... 왜냐면 Daypart가 true이면 오전인지 오후인지 모르잔아여...... 아닌감

Copy link
Member

Choose a reason for hiding this comment

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

p3

저어는 지금 상태가 조금 더 직관적인 것 같긴 합니다..ㅎㅎ

}
}

struct CustomTimePicker: UIViewRepresentable {
Copy link
Contributor

Choose a reason for hiding this comment

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

p3.
여기서부터는 다른 구조체가 시작되는데, 파일 분리해주는 것도 좋을 거 같습니다

Copy link
Contributor Author

@sangyup12 sangyup12 Dec 18, 2025

Choose a reason for hiding this comment

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

지금 이 컴포넌트가 Global/Components 폴더에 위치해 있는데 파일을 분리해도 괜찮을까요? 오직 이 서티 타임 피커를 위한 구조체라서요. 하나의 파일에서 관리하려고 했습니다. 파일을 따로 분리하는 게 더 이해하기 편할까요?

Copy link
Member

Choose a reason for hiding this comment

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

p3

저는 한 곳에서 관리하는 게 더 이해하기 편할 것 같긴해요!
CertiTimePicker를 구현하기 위한 CertiPickerView 를 SwiftUI에서 사용할 수 있게 해주는 UIViewRepresentable 이니까 꼭 분리할 필요는 없지 않을까...
다만 지금은 UIKit으로 구현한 CertiPickerView가 파일 상단에 위치하고 하단에 우리 프로젝트에서 실질적으로 사용하는 SwiftUI 구조체가 위치해 있는데, 둘의 순서를 바꾸는 건 어떨지! 의견만 내봅니다

Copy link
Contributor Author

Choose a reason for hiding this comment

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

좋아요 수정했습니다!

@Binding var minute: Int

func makeCoordinator() -> Coordinator {
Coordinator(self)
Copy link
Contributor

Choose a reason for hiding this comment

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

바보 질문 죄송한데 self가 무슨 의미인지 잘 이해가 안돼서 설명 한번만..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

CustomTimePicker 구조체 입니다. 스유와 유킷을 연결해주는 코디네이터에게 자신을 알려줌으로써 변경되는 @binding 변수인 시간, 분을 업데이트 해줄 수 있게 되는 것입니다.

uiView.selectRow(minute / 5, inComponent: 2, animated: false)
}

class Coordinator: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
Copy link
Contributor

Choose a reason for hiding this comment

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

구조체 안에 클래스를 바로 넣는 건 처음바요..
분리할 수 있는 방법 없을까요..?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

스유와 유킷을 연결하기 위해서 이렇게 되더라고영. 헷갈릴 소지가 있을 거 같아서 extension으로 빼두었습니다.
참고한 글: https://ios-development.tistory.com/1043

Copy link
Member

Choose a reason for hiding this comment

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

p2

Coordinator 라는 네이밍을 사용하신 이유가 있을까요??
비록 이 구조체 내에서만 사용되기는 하지만, iOS에서 Coordinator 라고 하면 네비게이션 로직과 관련된 작업을 하는 객체를 떠올리는 게 일반적이라고 생각해요.
심지어 Certi 프로젝트는 Coordinator 패턴을 채택해서 화면전환을 전담하는 객체로서 사용하고 있기 때문에 용도에 대한 혼동이 생길 수 있을 것 같아요.

만약 이 네이밍이 전통적이고 필수적인게 아니라면 Adapter 같은 다른 네이밍을 고려해보시는 건 어떨까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

제가 임의로 정한 건 아니고 애플 공식문서에서 유킷 델리게이트 역할을 담당하는 애를 Coordinator라고 사용하고 있어용. UIViewRepresentable에 makeCoordinator()라는 메서드도 구현해야 하기 때문에 다른 네이밍을 쓰기보다 애플에서 정의한대로 작성하는 게 적절하다고 생각합니다!

}
}

struct CertiTimePicker: View {
Copy link
Contributor

Choose a reason for hiding this comment

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

p3.
얘두 분리 가능하면 파일 분리 해주면 조을 거 같슴당

Copy link
Contributor Author

Choose a reason for hiding this comment

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

파일 분리에 대해서 위에 의견 남겨주세요!!

case 0:
parent.isAM = (row == 0)
case 1:
parent.hour = Self.hoursInfinite[row % 12]
Copy link
Contributor

Choose a reason for hiding this comment

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

이거 한번만 설명해주세용

Copy link
Contributor Author

Choose a reason for hiding this comment

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

애플식 무한스크롤을 구현하기 위해서 hoursInfinite 는 1부터 12까지 Int를 100번 반복한 배열이에요. 이 배열의 값을 12로 나눈 나머지값이 입력되도록 했습니다. 사실 반복한 배열 자체가 1~12 값만 있기 때문에 12로 안 나눠도 괜찮은데 혹시 몰라 배열 범위를 벗어나는 것을 방지하기 위해서 적용했습니다.
아래에 minutesInfinite도 같은 방식입니다

Copy link
Member

Choose a reason for hiding this comment

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

Image

p1

로그를 찍어보니까 무한 스크롤이다보니 라벨이 계속해서 생성되고 있더라구요!!

제가 알기론 UILabel()을 호출할 때마다 시스템은 힙(Heap) 영역에 메모리 공간을 새로 확보하고, 객체를 초기화합니다.
피커를 빠르게 휙 돌리면 1초에 수십 개의 row가 지나가는데, 그때마다 수십 번의 메모리 할당과 해제가 반복되며 성능상 부담이 갈 수 있을 것 같아요!

그래서 저는 두가지 방안을 고려해보셨으면 좋을 것 같습니다!

  1. 기획측과 무한 스크롤이 필요한가? 논의해보기 (비추)
  2. 지금 상태에서 메모리 사용량을 Instruments 툴을 통해 측정하고 캐싱 혹은 뷰 재사용등의 방식으로 개선해서 정량적 수치로 나타낼 수 있는 개선해본 경험 만들기 (추천)

.frame(width: 149, height: 20)
.padding(.leading, 20)
.padding(.trailing, 206)

Copy link
Contributor

Choose a reason for hiding this comment

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

여기 한 칸은 왜 띄운건가여?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

실수예요.. 지웠습니다_


@State private var opacity: Double = 1.0

@State private var isShowingSheet = false
Copy link
Contributor

Choose a reason for hiding this comment

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

p3.
이거 뷰모델에 넣는건 어떨까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

단순한 UI 상태인데 뷰에서 갖고 있는 게 낫지 않응가여. 뷰모델에서 가지고 있으면 어떤 점이 좋은가요?

Copy link
Member

@OneTen19 OneTen19 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다! 리뷰사항 남긴 것들 확인 부탁드려요!

그리고 pr 제목 오타 수정 ㄱ

private func setupLines() {
for _ in 0..<3 {
let topLine = UIView()
topLine.backgroundColor = UIColor(Color.purpleblue)
Copy link
Member

Choose a reason for hiding this comment

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

p3

topLine.backgroundColor = UIColor(.purpleblue) 혹은 topLine.backgroundColor = .purpleblue

이렇게 작성해도 정상적으로 빌드되더라구요! 혹시 Color.purpleblue로 사용하신 이유가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

엇 그러네요..? 색상 인식이 안 돼서 썼던 건데 ㅠ UIColor 다 지우고 .purple로 수정했습니다

}
}

struct CustomTimePicker: UIViewRepresentable {
Copy link
Member

Choose a reason for hiding this comment

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

p3

저는 한 곳에서 관리하는 게 더 이해하기 편할 것 같긴해요!
CertiTimePicker를 구현하기 위한 CertiPickerView 를 SwiftUI에서 사용할 수 있게 해주는 UIViewRepresentable 이니까 꼭 분리할 필요는 없지 않을까...
다만 지금은 UIKit으로 구현한 CertiPickerView가 파일 상단에 위치하고 하단에 우리 프로젝트에서 실질적으로 사용하는 SwiftUI 구조체가 위치해 있는데, 둘의 순서를 바꾸는 건 어떨지! 의견만 내봅니다

}

struct CustomTimePicker: UIViewRepresentable {
@Binding var isAM: Bool
Copy link
Member

Choose a reason for hiding this comment

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

p3

저어는 지금 상태가 조금 더 직관적인 것 같긴 합니다..ㅎㅎ

case 0:
parent.isAM = (row == 0)
case 1:
parent.hour = Self.hoursInfinite[row % 12]
Copy link
Member

Choose a reason for hiding this comment

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

Image

p1

로그를 찍어보니까 무한 스크롤이다보니 라벨이 계속해서 생성되고 있더라구요!!

제가 알기론 UILabel()을 호출할 때마다 시스템은 힙(Heap) 영역에 메모리 공간을 새로 확보하고, 객체를 초기화합니다.
피커를 빠르게 휙 돌리면 1초에 수십 개의 row가 지나가는데, 그때마다 수십 번의 메모리 할당과 해제가 반복되며 성능상 부담이 갈 수 있을 것 같아요!

그래서 저는 두가지 방안을 고려해보셨으면 좋을 것 같습니다!

  1. 기획측과 무한 스크롤이 필요한가? 논의해보기 (비추)
  2. 지금 상태에서 메모리 사용량을 Instruments 툴을 통해 측정하고 캐싱 혹은 뷰 재사용등의 방식으로 개선해서 정량적 수치로 나타낼 수 있는 개선해본 경험 만들기 (추천)

uiView.selectRow(minute / 5, inComponent: 2, animated: false)
}

class Coordinator: NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
Copy link
Member

Choose a reason for hiding this comment

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

p2

Coordinator 라는 네이밍을 사용하신 이유가 있을까요??
비록 이 구조체 내에서만 사용되기는 하지만, iOS에서 Coordinator 라고 하면 네비게이션 로직과 관련된 작업을 하는 객체를 떠올리는 게 일반적이라고 생각해요.
심지어 Certi 프로젝트는 Coordinator 패턴을 채택해서 화면전환을 전담하는 객체로서 사용하고 있기 때문에 용도에 대한 혼동이 생길 수 있을 것 같아요.

만약 이 네이밍이 전통적이고 필수적인게 아니라면 Adapter 같은 다른 네이밍을 고려해보시는 건 어떨까요??

Comment on lines +237 to +240
Text(":")
.applyCertiFont(.caption_semibold_14)
.foregroundColor(.grayscale600)
.offset(x: 49, y: 0)
Copy link
Member

Choose a reason for hiding this comment

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

p3

offset으로 위치를 설정해놔서 그런디 혹시 13mini랑 17 pro max랑 둘 다 시뮬레이터 상으로 괜찮았나유

Comment on lines +6 to +10
//
import SwiftUI


struct DropdownMenu: View {
Copy link
Member

Choose a reason for hiding this comment

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

p1

개행 정리해줍시다!

Comment on lines +33 to +34
@Published var CertificationPlanPlaceDo: String? = nil
@Published var CertificationPlanPlaceSi: String? = nil
Copy link
Member

Choose a reason for hiding this comment

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

p1

CertificationPlanPlaceDo 랑 CertificationPlanPlaceSi가 뭐죠..
설마 경기도 할 때 '도' 랑 수원시 할 때 '시'...?
얘는 네이밍 뭔가 바꾸긴 해야할 듯...

struct CertificationDetailPlanModalView: View {
@ObservedObject var viewModel: CertificateDetailViewModel

@EnvironmentObject var tabRouter: CertiTabCoordinator
Copy link
Member

Choose a reason for hiding this comment

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

p1

tabRouter 뭐임??? 이거 gpt 썼지 ㅡㅡ
이 구조체에서 사용되지 않고 있는 것 같은데 지워주세요!

@sangyup12 sangyup12 changed the title [Style] #185 - 취득 예정 보달 뷰 UI 구현 [Style] #185 - 취득 예정 모달 뷰 UI 구현 Dec 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Style 🖼️ UI 작업 상엽 🌼 상엽 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SCRUM-38] Style: 취득예정 모달 뷰 UI 구현

4 participants