[] 드롭다운 최종 완성, 커스텀 텍스트 필드 전체 변경, 회원가입 로직 추가 중

This commit is contained in:
김선규 2025-03-27 17:55:32 +09:00
parent 4875427e24
commit 63031172a1
8 changed files with 266 additions and 112 deletions

View File

@ -45,6 +45,7 @@ struct CircleBtnView: View {
}
.frame(width: state.width, height: state.height)
.onTapGesture {
endTextEditing()
guard let action = state.action else {return}
action()
}

View File

@ -23,6 +23,7 @@ struct SimpleBtnView: View {
.frame(width: state.width, height: state.height)
.onTapGesture {
if state.isUsable {
endTextEditing()
guard let action = state.action else { return }
action()
}
@ -30,6 +31,7 @@ struct SimpleBtnView: View {
}
else {
Button{
endTextEditing()
guard let action = state.action else { return }
action()
} label: {

View File

@ -13,9 +13,10 @@ class DropdownManager: ObservableObject {
@Published var anchor: CGRect = .zero
@Published var font: Font = .body
var onSelect: ((String) -> Void)?
var onSelect: ((Int) -> Void)?
func show(items: [String], anchor: CGRect, onSelect: @escaping (String) -> Void) {
func show(items: [String], anchor: CGRect,
onSelect: @escaping (Int) -> Void) {
self.items = items
self.anchor = anchor
self.onSelect = onSelect
@ -32,22 +33,28 @@ class DropdownManager: ObservableObject {
struct DropdownButton: View {
@ObservedObject var manager: DropdownManager
@State var title: String
var onSelect: ((Int) -> Void)?
let items: [String]
init(manager: DropdownManager, title: String, items: [String]) {
init(manager: DropdownManager, title: String, items: [String],
onSelect: @escaping (Int) -> Void) {
self.manager = manager
self.title = title
self.items = items
self.onSelect = onSelect
}
@State private var frame: CGRect = .zero
var body: some View {
Button {
manager.show(items: items, anchor: frame) { selected in
print("선택한 항목:", selected)
title = selected
endTextEditing()
manager.show(items: items, anchor: frame) { index in
self.onSelect?(index)
print("선택한 항목: \(items[index])")
title = items[index]
}
} label: {
HStack(alignment: .center) {
@ -62,7 +69,10 @@ struct DropdownButton: View {
Spacer()
}
.padding(4)
.background(RoundedRectangle(cornerRadius: 8).stroke(Color(.Normal.normal)))
.background{
RoundedRectangle(cornerRadius: 8)
.fill(Color.clear)
}
}
.buttonStyle(.plain)
.background(
@ -131,16 +141,16 @@ struct GlobalDropdownOverlay: View {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(manager.items.enumerated()), id: \.offset) { index, item in
Button {
manager.onSelect?(item)
manager.onSelect?(index)
manager.dismiss()
} label: {
Text(item)
.font(manager.font)
.lineLimit(1)
.minimumScaleFactor(0.5)
.minimumScaleFactor(0.4)
.padding([.leading, .trailing], 8)
.frame(maxWidth: .infinity, minHeight: 40, maxHeight: 40, alignment: .center)
.padding([.leading,.trailing], 2)
.contentShape(Rectangle())
.contentShape(Rectangle())
}
.buttonStyle(.plain)

View File

@ -13,12 +13,10 @@ struct RegisterView: View {
@StateObject var btnVM = ButtonViewModel()
@StateObject var registerVM: RegisterViewModel
private let responseValue: (SNSLoginType, String)
// private let responseValue: (SNSLoginType, String)
init(_ appVM: AppViewModel, type: SNSLoginType, snsID: String) {
_registerVM = StateObject(wrappedValue: RegisterViewModel(appVM))
self.responseValue.0 = type
self.responseValue.1 = snsID
_registerVM = StateObject(wrappedValue: RegisterViewModel(appVM, type: type, snsID: snsID))
}
@ -30,11 +28,9 @@ struct RegisterView: View {
@StateObject private var dropdownManager = DropdownManager()
private let addressBtnID = UUID()
private let registerBtnID = UUID()
@State private var selected = ""
let options = ["Swift", "Kotlin", "Dart", "JavaScript", "C#","C++","C"]
@State private var onDomainTxf = false
var body: some View {
// MARK: TO-DO
@ -67,13 +63,13 @@ struct RegisterView: View {
.frame(width: 60, alignment: .center)
.padding(.trailing,12)
Spacer(minLength: 1)
CustomTextField(placeholder: "최대 10글자 입력", text: $registerVM.nameText, alignment: .center)
CustomTxfView(placeholder: "이름 입력 (10 글자)", text: $registerVM.nameText, maxLength: 10, alignment: .center)
.frame(maxWidth: .infinity,maxHeight: 48)
.padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20))
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
}.clipped()
}
.padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16))
@ -103,37 +99,48 @@ struct RegisterView: View {
.frame(width: 60, alignment: .center)
.padding(.trailing,12)
Spacer(minLength: 1)
CustomTextField(placeholder: "앞부분 입력", text: $registerVM.emailFrontText, alignment: .center)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20))
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
// Spacer(minLength: 1)
Text("@")
.font(.nps(size: 16))
.padding([.leading, .trailing], 4)
// Spacer(minLength: 1)
DropdownButton(manager: dropdownManager, title: "도메인 선택",
items: registerVM.emailTailList)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
VStack (spacing: 4) {
CustomTxfView(placeholder: "이메일 입력(30 글자)", text: $registerVM.emailFrontText, maxLength: 30, alignment: .center)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.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))
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
HStack(spacing: 0) {
Text("@")
.font(.nps(size: 16))
.padding([.leading, .trailing], 4)
// Spacer(minLength: 1)
DropdownButton(manager: dropdownManager, title: "도메인 선택",
items: registerVM.emailTailList){ index in
if registerVM.emailTailList.count-1 == index {
onDomainTxf = true
} else {
registerVM.emailTailText = registerVM.emailTailList[index]
onDomainTxf = false
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
}
*/
if onDomainTxf {
CustomTxfView(placeholder: "도메인 직접 입력(30 글자)", text: $registerVM.emailTailText, maxLength: 30, alignment: .center)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
}
}
}
.padding()
@ -148,26 +155,20 @@ struct RegisterView: View {
}
.frame(width: 60, alignment: .center)
.padding(.trailing,12)
// DropdownButton(manager: dropdownManager, title: "", items: options)
// 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 {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
DropdownButton(manager: dropdownManager, title: "선택", items: registerVM.numberHeadList){ index in
registerVM.phoneTextSet.0 = registerVM.numberHeadList[index]
}
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
Text("-")
.font(.nps(size: 16))
.padding([.leading, .trailing], 4)
CustomTextField(placeholder: "0000", text: $registerVM.nameText, alignment: .center)
CustomTxfView(placeholder: "0000", text: $registerVM.phoneTextSet.1, maxLength: 4, alignment: .center)
.frame(maxWidth: .infinity,maxHeight: 48)
.padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20))
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
@ -175,9 +176,9 @@ struct RegisterView: View {
Text("-")
.font(.nps(size: 16))
.padding([.leading, .trailing], 4)
CustomTextField(placeholder: "0000", text: $registerVM.nameText, alignment: .center)
.frame(maxWidth: .infinity,maxHeight: 48)
.padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20))
CustomTxfView(placeholder: "0000", text: $registerVM.phoneTextSet.2, maxLength: 4, alignment: .center)
.frame(maxWidth: .infinity, maxHeight: 48)
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
@ -185,31 +186,44 @@ struct RegisterView: View {
}
.padding()
HStack(spacing: 0){
HStack(alignment: .center, spacing: 0){
Text("주소")
.font(.nps(size: 16))
.frame(width: 60, alignment: .center)
.padding(.trailing,12)
Spacer(minLength: 1)
VStack(spacing: 0) {
SimpleBtnView(vm: btnVM, id: addressBtnID)
.padding(.bottom, isSelectAddr ? 4:0)
CustomTextField(placeholder: "상세 주소 입력", text: $registerVM.addrDetailText, alignment: .center)
.frame(maxWidth: .infinity,
maxHeight: isSelectAddr ? 48 : 0)
.padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20))
SimpleBtnView(vm: btnVM, id: registerVM.addressBtnID)
.frame(maxWidth: .infinity, maxHeight: 48)
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
.opacity(isSelectAddr ? 1.0 : 0.0)
.padding(.bottom, isSelectAddr ? 4:0)
if isSelectAddr {
CustomTxfView(placeholder: "상세 주소 입력 (50 글자)", text: $registerVM.addrDetailText, maxLength: 50, alignment: .center)
.frame(maxWidth: .infinity,
maxHeight: isSelectAddr ? 48 : 0)
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.background {
RoundedRectangle(cornerRadius: 24)
.foregroundStyle(Color(.Normal.light))
}
}
}
}
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
// .background(Color(.Normal.light))
SimpleBtnView(vm: btnVM, id: registerVM.registerBtnID)
.frame(maxWidth: .infinity, maxHeight: 48)
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
// .background {
// RoundedRectangle(cornerRadius: 24)
// .foregroundStyle(Color(.Normal.light))
// }
}
GlobalDropdownOverlay(manager: dropdownManager)
}
@ -221,30 +235,36 @@ struct RegisterView: View {
if let result = result as? (String, String) {
print(result.0)
isSelectAddr = true
// printLog(registerVM.addressText)
registerVM.addressText = result.0
btnVM.setSize(for: addressBtnID, newWidth: .infinity, newHeight: .infinity)
// printLog(registerVM.addressText)
btnVM.setText(for: addressBtnID, newText: "\(registerVM.addressText)", newFont: .nps(size: 16))
btnVM.setSize(for: registerVM.addressBtnID, newWidth: .infinity, newHeight: .infinity)
btnVM.setText(for: registerVM.addressBtnID, newText: "\(registerVM.addressText)", newFont: .nps(size: 16))
}
}
}
.onAppear {
topVM.titleName = "회원가입"
topVM.setLeftBtn(Image(.Icon.left), size: CGPoint(x: 40, y: 40), action: leftAct)
topVM.setRightBtn(size: CGPoint(x: 40, y: 40), action: rightAct)
btnVM.setSize(for: addressBtnID, newWidth: 80, newHeight: 24)
btnVM.setText(for: addressBtnID, newText: "\(registerVM.addressText)", newFont: .nps(size: 16))
btnVM.setAction(for: addressBtnID) {
self.showWebView.toggle()
}
btnVM.setSize(for: registerVM.addressBtnID, newWidth: 80, newHeight: 24)
btnVM.setText(for: registerVM.addressBtnID, newText: "\(registerVM.addressText)", newFont: .nps(size: 16))
btnVM.setTextColor(for: registerVM.addressBtnID, newColor: Color.Text.black)
btnVM.setAction(for: registerVM.addressBtnID) { self.showWebView.toggle() }
btnVM.setSize(for: registerVM.registerBtnID, newWidth: .infinity, newHeight: 48)
btnVM.setText(for: registerVM.registerBtnID, newText: "회원가입", newFont: .nps(font: .bold, size: 24))
btnVM.setTextColor(for: registerVM.registerBtnID, newColor: Color.Point.dark)
btnVM.setAction(for: registerVM.registerBtnID) { registerVM.registerUser() }
dropdownManager.font = .nps(size: 16)
}
.onChange(of: registerVM.addressText) { _, new in
}
// .onChange(of: dropdownManager.onSelect) { _, new in
// printLog("CHANGE: \(new)")
//
// }
}
func leftAct() {

View File

@ -16,7 +16,7 @@ class AppViewModel: ObservableObject {
@Published var menuName: MenuName = .Home
@Published var naviState: NaviState = .init(act: .NONE, path: .Intro)
var alertData: AlertData = .init(body: "")
@Published var alertData: AlertData = .init(body: "")
///
let alertAction = CurrentValueSubject<String?, Never>(nil)

View File

@ -9,24 +9,38 @@ import SwiftUI
import Combine
class RegisterViewModel: ObservableObject {
private let appVM: AppViewModel
private var cancellables = Set<AnyCancellable>()
private let appVM: AppViewModel
private let responseValue: (SNSLoginType, String)
init(_ appVM: AppViewModel) {
init(_ appVM: AppViewModel, type: SNSLoginType, snsID: String) {
self.appVM = appVM
self.responseValue = (type, snsID)
}
@State var selectDate: Date = {
let calendar = Calendar.current
return calendar.date(byAdding: .year, value: -12, to: Date()) ?? Date()
}()
let addressBtnID = UUID()
let registerBtnID = UUID()
@State var nameText: String = ""
@State var emailFrontText: String = ""
@State var emailTailText: String = ""
@State var phoneArray: [Int] = []
@Published var selectDate: Date = {
let calendar = Calendar.current
return calendar.date(byAdding: .year, value: 0, to: Date()) ?? Date()
}() {
didSet {
if selectDate != oldValue {
changeDate = true
}
}
}
@Published var changeDate: Bool = false
@Published var nameText: String = ""
@Published var emailFrontText: String = ""
@Published var emailTailText: String = ""
@Published var numberHead: String = ""
@Published var phoneTextSet: (String,String,String) = ("","","")
@Published var addressText: String = "주소 입력"
@State var addrDetailText: String = ""
@Published var addrDetailText: String = ""
let numberHeadList = ["010","011","016","017","018","019"]
let emailTailList = ["gmail.com",
@ -41,4 +55,30 @@ class RegisterViewModel: ObservableObject {
"protonmail.com",
"직접 입력"]
func registerUser() {
//
if nameText != "" && emailFrontText != "" && emailTailText != "" && phoneTextSet.0 != "" && phoneTextSet.1 != "" && phoneTextSet.2 != "" {
if !changeDate || addressText == "주소 입력" {
appVM.alertData = AlertData(
title: "알림",
body: "\(changeDate ? "":"[생일]")\((addressText != "주소 입력") ? "":"[주소]")의 내용이 없습니다.\n 계속해서 진행할까요?",
button: [ButtonType(name: "확인", role: .cancel, function: nil)])
appVM.showAlert.toggle()
//
} else {
//
}
}
else {
appVM.alertData = AlertData(
title: "경고", body: "필수 입력 사항이 누락되었습니다.",
button: [ButtonType(name: "확인", role: .cancel, function: nil)])
appVM.showAlert.toggle()
}
}
}

View File

@ -8,10 +8,69 @@
import SwiftUI
import UIKit
struct CustomTextField: UIViewRepresentable {
struct FixedSizeWrapper<Content: UIView>: UIViewRepresentable {
let content: () -> Content
let width: CGFloat
let height: CGFloat
func makeUIView(context: Context) -> Content {
let view = content()
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalToConstant: width),
view.heightAnchor.constraint(equalToConstant: height)
])
return view
}
func updateUIView(_ uiView: Content, context: Context) {}
}
struct CustomTxfView: View {
var placeholder: String
@Binding var text: String
var maxLength: Int = 100
var isSecure: Binding<Bool> = .constant(false)
var alignment: TextAlignment = .leading
var textColor = Color(.Text.detail)
var font: Font = .nps(size:16)
// UIFont(name: "NPS-font-Regular", size: 16)
var body: some View {
TextField(placeholder, text: $text)
// Binding<String>(
// get: { text },
// set: { newValue in
// text = String(newValue.prefix(maxLength))
// }
// ))
.font(font)
.tint(textColor)
.lineLimit(1)
.multilineTextAlignment(alignment)
.minimumScaleFactor(0.5)
.truncationMode(.tail)
.clipped()
.onChange(of: text) { old, new in
if new.count > maxLength {
text = String(new.prefix(maxLength))
}
}
}
}
struct CustomTextField: UIViewRepresentable {
var placeholder: String
@Binding var text: String
var maxLength: Int?
var isSecure: Binding<Bool> = .constant(false)
var textColor = UIColor(Color(.Text.detail))
@ -21,24 +80,31 @@ struct CustomTextField: UIViewRepresentable {
// [] UIView
func makeUIView(context: Context) -> UITextField {
let txf = UITextField()
// txf.placeholder = placeholder
txf.attributedPlaceholder = NSAttributedString(string: "\(placeholder)", attributes: [NSAttributedString.Key.foregroundColor : UIColor(.Text.border)])
// txf.placeholder = placeholder
txf.attributedPlaceholder = NSAttributedString(
string: "\(placeholder)",
attributes: [NSAttributedString.Key.foregroundColor : UIColor(.Text.border)])
txf.isSecureTextEntry = isSecure.wrappedValue
txf.adjustsFontSizeToFitWidth = true
txf.minimumFontSize = 4
txf.textColor = textColor
txf.font = font
txf.translatesAutoresizingMaskIntoConstraints = false
let height = (font?.lineHeight ?? 10) + 8
txf.frame.size.height = height
txf.borderStyle = .none
txf.autocorrectionType = .no
txf.autocapitalizationType = .none
txf.smartInsertDeleteType = .no
txf.textContentType = .oneTimeCode
txf.textAlignment = self.alignment
txf.translatesAutoresizingMaskIntoConstraints = false
txf.setContentHuggingPriority(.required, for: .horizontal)
txf.setContentCompressionResistancePriority(.required, for: .horizontal)
txf.delegate = context.coordinator
return txf
@ -51,19 +117,34 @@ struct CustomTextField: UIViewRepresentable {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
Coordinator(self, max: maxLength)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: CustomTextField
var maxLength: Int?
init(_ parent: CustomTextField) {
init(_ parent: CustomTextField, max: Int?) {
self.parent = parent
self.maxLength = max
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let current = textField.text ?? ""
guard let stringRange = Range(range, in: current) else { return false}
let updated = current.replacingCharacters(in: stringRange, with: string)
return updated.count <= (maxLength ?? 9999)
}
// func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// let currentText = textField.text ?? ""
// guard let stringRange = Range(range, in: currentText) else { return false }
// let updatedText = currentText.replacingCharacters(in: stringRange, with: string)
// return updatedText.count <= maxLength
// }
}
}