forked from AcaMate/AcaMate_iOS
[✨] 드롭다운 컴포넌트 생성 및 회원가입 화면에 적용중
This commit is contained in:
parent
f11d3b417a
commit
4875427e24
Binary file not shown.
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
"직접 입력"]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user