[] 드롭다운 컴포넌트 생성 및 회원가입 화면에 적용중

This commit is contained in:
김선규 2025-03-26 17:58:05 +09:00
parent f11d3b417a
commit 4875427e24
4 changed files with 137 additions and 41 deletions

View File

@ -6,12 +6,13 @@
// //
import SwiftUI import SwiftUI
class DropdownManager: ObservableObject {
static let shared = DropdownManager()
class DropdownManager: ObservableObject {
@Published var isPresented = false @Published var isPresented = false
@Published var items: [String] = [] @Published var items: [String] = []
@Published var anchor: CGRect = .zero @Published var anchor: CGRect = .zero
@Published var font: Font = .body
var onSelect: ((String) -> Void)? var onSelect: ((String) -> Void)?
func show(items: [String], anchor: CGRect, onSelect: @escaping (String) -> Void) { func show(items: [String], anchor: CGRect, onSelect: @escaping (String) -> Void) {
@ -26,30 +27,49 @@ class DropdownManager: ObservableObject {
} }
} }
/// .coordinateSpace(name: "dropdownArea")
/// GlobalDropdownOverlay(manager: dropdownManager) ZStack
struct DropdownButton: View { struct DropdownButton: View {
let title: String @ObservedObject var manager: DropdownManager
@State var title: String
let items: [String] let items: [String]
init(manager: DropdownManager, title: String, items: [String]) {
self.manager = manager
self.title = title
self.items = items
}
@State private var frame: CGRect = .zero @State private var frame: CGRect = .zero
var body: some View { var body: some View {
Button { Button {
DropdownManager.shared.show(items: items, anchor: frame) { selected in manager.show(items: items, anchor: frame) { selected in
print("선택한 항목:", selected) print("선택한 항목:", selected)
title = selected
} }
} label: { } label: {
HStack { HStack(alignment: .center) {
Spacer()
Text(title) Text(title)
Image(systemName: "chevron.down") .font(manager.font)
.lineLimit(1)
.minimumScaleFactor(0.5)
Image(systemName: /*manager.isPresented ? "chevron.up" : */ "chevron.down")
.resizable()
.frame(width: 8, height: 4)
Spacer()
} }
.padding() .padding(4)
.background(RoundedRectangle(cornerRadius: 8).stroke(Color.gray)) .background(RoundedRectangle(cornerRadius: 8).stroke(Color(.Normal.normal)))
} }
.buttonStyle(.plain)
.background( .background(
GeometryReader { geo in GeometryReader { geo in
Color.clear Color.clear
.onAppear { .onAppear {
frame = geo.frame(in: .global) frame = geo.frame(in: .named("dropdownArea"))
} }
} }
) )
@ -58,38 +78,78 @@ struct DropdownButton: View {
struct GlobalDropdownOverlay: View { struct GlobalDropdownOverlay: View {
@ObservedObject var manager = DropdownManager.shared @ObservedObject var manager: DropdownManager
@State private var scrollOffset: CGPoint = .zero
private var maxVisibleItemCount: Int {
if manager.items.count > 5 {
return 5
} else { return manager.items.count }
}
private let itemHeight: CGFloat = 42
private var heightForDropBox: CGFloat {
CGFloat(maxVisibleItemCount) * itemHeight
}
var body: some View { var body: some View {
if manager.isPresented { if manager.isPresented {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
Color.black.opacity(0.001) Color.black.opacity(0.1)
.ignoresSafeArea() .ignoresSafeArea()
.onTapGesture { .onTapGesture {
manager.dismiss() manager.dismiss()
} }
VStack(alignment: .leading, spacing: 0) { VStack(spacing: 0) {
ForEach(manager.items, id: \.self) { item in if manager.items.count > maxVisibleItemCount {
Button { OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { _ in
manager.onSelect?(item) dropdownContent()
manager.dismiss()
} label: {
Text(item)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
} }
.buttonStyle(.plain) } else {
.background(Color.white) dropdownContent()
} }
} }
.frame(width: manager.anchor.width) .frame(width: manager.anchor.width, height: heightForDropBox)
.background(RoundedRectangle(cornerRadius: 8).stroke(Color.gray)) .background{
.shadow(radius: 5) RoundedRectangle(cornerRadius: 8)
.position(x: manager.anchor.midX, y: manager.anchor.maxY + 10) .fill(Color(.Normal.normal))
.stroke(Color(.Second.normal))
.strokeBorder(lineWidth: 2)
}
.position(x: manager.anchor.midX,
y: manager.anchor.maxY+heightForDropBox/2)
.animation(.easeOut(duration: 0.25), value: manager.isPresented)
}
}
}
@ViewBuilder
private func dropdownContent() -> some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(manager.items.enumerated()), id: \.offset) { index, item in
Button {
manager.onSelect?(item)
manager.dismiss()
} label: {
Text(item)
.font(manager.font)
.lineLimit(1)
.minimumScaleFactor(0.5)
.frame(maxWidth: .infinity, minHeight: 40, maxHeight: 40, alignment: .center)
.padding([.leading,.trailing], 2)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
if index < manager.items.count - 1 {
Rectangle()
.fill(Color(.Second.normal).opacity(0.3))
.frame(maxWidth: .infinity, maxHeight: 2)
}
} }
} }
} }
} }

View File

@ -28,11 +28,13 @@ struct RegisterView: View {
@State private var isSelectAddr: Bool = false @State private var isSelectAddr: Bool = false
@StateObject private var dropdownManager = DropdownManager()
private let addressBtnID = UUID() private let addressBtnID = UUID()
private let registerBtnID = UUID() private let registerBtnID = UUID()
@State private var selected = "" @State private var selected = ""
let options = ["Swift", "Kotlin", "Dart", "JavaScript"] let options = ["Swift", "Kotlin", "Dart", "JavaScript", "C#","C++","C"]
var body: some View { var body: some View {
// MARK: TO-DO // MARK: TO-DO
@ -51,9 +53,6 @@ struct RegisterView: View {
} }
.padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 16)) .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 16))
DropdownButton(title: "언어 선택", items: ["Swift", "Dart", "Kotlin"])
OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in OffsetObservableScrollView(showsIndicators: false, scrollOffset: $scrollOffset) { proxy in
// //
@ -66,6 +65,7 @@ struct RegisterView: View {
.foregroundStyle(Color(.Other.red)) .foregroundStyle(Color(.Other.red))
} }
.frame(width: 60, alignment: .center) .frame(width: 60, alignment: .center)
.padding(.trailing,12)
Spacer(minLength: 1) Spacer(minLength: 1)
CustomTextField(placeholder: "최대 10글자 입력", text: $registerVM.nameText, alignment: .center) CustomTextField(placeholder: "최대 10글자 입력", text: $registerVM.nameText, alignment: .center)
.frame(maxWidth: .infinity,maxHeight: 48) .frame(maxWidth: .infinity,maxHeight: 48)
@ -82,6 +82,7 @@ struct RegisterView: View {
Text("생일") Text("생일")
.font(.nps(size: 16)) .font(.nps(size: 16))
.frame(width: 60, alignment: .center) .frame(width: 60, alignment: .center)
.padding(.trailing,12)
Spacer(minLength: 1) Spacer(minLength: 1)
DatePicker("", selection: $registerVM.selectDate, displayedComponents: [.date]) DatePicker("", selection: $registerVM.selectDate, displayedComponents: [.date])
.datePickerStyle(.compact) .datePickerStyle(.compact)
@ -99,10 +100,10 @@ struct RegisterView: View {
.font(.nps(size: 16)) .font(.nps(size: 16))
.foregroundStyle(Color(.Other.red)) .foregroundStyle(Color(.Other.red))
} }
.frame(width: 60, alignment: .center)
.padding(.trailing,12)
Spacer(minLength: 1) Spacer(minLength: 1)
CustomTextField(placeholder: "앞부분 입력", text: $registerVM.emailFrontText, alignment: .center) CustomTextField(placeholder: "앞부분 입력", text: $registerVM.emailFrontText, alignment: .center)
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20))
.background { .background {
@ -114,13 +115,25 @@ struct RegisterView: View {
.font(.nps(size: 16)) .font(.nps(size: 16))
.padding([.leading, .trailing], 4) .padding([.leading, .trailing], 4)
// Spacer(minLength: 1) // Spacer(minLength: 1)
CustomTextField(placeholder: "뒷부분 입력", text: $registerVM.emailTailText, alignment: .center)
.frame(maxWidth: .infinity,maxHeight: 48) DropdownButton(manager: dropdownManager, title: "도메인 선택",
items: registerVM.emailTailList)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
//
/*
CustomTextField(placeholder: "뒷부분 입력", text: $registerVM.emailFrontText, alignment: .center)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20))
.background { .background {
RoundedRectangle(cornerRadius: 24) RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light)) .foregroundStyle(Color(.Normal.light))
} }
*/
} }
.padding() .padding()
@ -133,10 +146,17 @@ struct RegisterView: View {
.font(.nps(size: 16)) .font(.nps(size: 16))
.foregroundStyle(Color(.Other.red)) .foregroundStyle(Color(.Other.red))
} }
.frame(width: 60, alignment: .center)
CustomTextField(placeholder: "000", text: $registerVM.nameText, alignment: .center) .padding(.trailing,12)
.frame(maxWidth: .infinity,maxHeight: 48) // DropdownButton(manager: dropdownManager, title: "", items: options)
.padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) // CustomTextField(placeholder: "000", text: $registerVM.nameText, alignment: .center)
// .frame(maxWidth: .infinity,maxHeight: 48)
// .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20))
// .background {
// RoundedRectangle(cornerRadius: 24)
// .foregroundStyle(Color(.Normal.light))
// }
DropdownButton(manager: dropdownManager, title: "선택", items: registerVM.numberHeadList)
.background { .background {
RoundedRectangle(cornerRadius: 24) RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light)) .foregroundStyle(Color(.Normal.light))
@ -144,6 +164,7 @@ struct RegisterView: View {
Text("-") Text("-")
.font(.nps(size: 16)) .font(.nps(size: 16))
.padding([.leading, .trailing], 4) .padding([.leading, .trailing], 4)
CustomTextField(placeholder: "0000", text: $registerVM.nameText, alignment: .center) CustomTextField(placeholder: "0000", text: $registerVM.nameText, alignment: .center)
.frame(maxWidth: .infinity,maxHeight: 48) .frame(maxWidth: .infinity,maxHeight: 48)
.padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20))
@ -168,6 +189,7 @@ struct RegisterView: View {
Text("주소") Text("주소")
.font(.nps(size: 16)) .font(.nps(size: 16))
.frame(width: 60, alignment: .center) .frame(width: 60, alignment: .center)
.padding(.trailing,12)
Spacer(minLength: 1) Spacer(minLength: 1)
VStack(spacing: 0) { VStack(spacing: 0) {
@ -189,8 +211,9 @@ struct RegisterView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
// .background(Color(.Normal.light)) // .background(Color(.Normal.light))
} }
GlobalDropdownOverlay() GlobalDropdownOverlay(manager: dropdownManager)
} }
.coordinateSpace(name: "dropdownArea")
.sheet(isPresented: $showWebView) { .sheet(isPresented: $showWebView) {
WebView(url: URL(string: "https://sean-59.github.io/Kakao-Postcode/")!, WebView(url: URL(string: "https://sean-59.github.io/Kakao-Postcode/")!,
showWebView: $showWebView, showWebView: $showWebView,
@ -217,6 +240,7 @@ struct RegisterView: View {
btnVM.setAction(for: addressBtnID) { btnVM.setAction(for: addressBtnID) {
self.showWebView.toggle() self.showWebView.toggle()
} }
dropdownManager.font = .nps(size: 16)
} }
.onChange(of: registerVM.addressText) { _, new in .onChange(of: registerVM.addressText) { _, new in

View File

@ -28,5 +28,17 @@ class RegisterViewModel: ObservableObject {
@Published var addressText: String = "주소 입력" @Published var addressText: String = "주소 입력"
@State var addrDetailText: String = "" @State var addrDetailText: String = ""
let numberHeadList = ["010","011","016","017","018","019"]
let emailTailList = ["gmail.com",
"naver.com",
"daum.net",
"hanmail.net",
"nate.com",
"outlook.com",
"icloud.com",
"kakao.com",
"yahoo.com",
"protonmail.com",
"직접 입력"]
} }