diff --git a/Source/DDS/Component/TextField/Extension/View+advancedFocus.swift b/Source/DDS/Component/TextField/Extension/View+advancedFocus.swift new file mode 100644 index 0000000..0e1b56f --- /dev/null +++ b/Source/DDS/Component/TextField/Extension/View+advancedFocus.swift @@ -0,0 +1,27 @@ +// +// File.swift +// +// +// Created by hhhello0507 on 8/21/24. +// + +import SwiftUI + +struct AdvancedFocusViewModifier: ViewModifier { + + @FocusState private var focused: Bool + + func body(content: Content) -> some View { + content + .focused($focused) + .onTapGesture { + focused = true + } + } +} + +public extension View { + func advancedFocus() -> some View { + self.modifier(AdvancedFocusViewModifier()) + } +} diff --git a/Source/DDS/Component/TextField/Internal/BaseTextField.swift b/Source/DDS/Component/TextField/Internal/BaseTextField.swift new file mode 100644 index 0000000..6b25678 --- /dev/null +++ b/Source/DDS/Component/TextField/Internal/BaseTextField.swift @@ -0,0 +1,103 @@ +// +// File.swift +// +// +// Created by hhhello0507 on 8/21/24. +// + +import SwiftUI + +internal struct BaseTextField: View { + + @FocusState private var focused + @State private var animatedFocusing: Bool = false + + private var isHighlighted: Bool { + animatedFocusing || !text.isEmpty + } + + // MARK: - Parameters + // text + private let hint: String + @Binding private var text: String + private let font: Font + private let supportText: String? + + // state + private let isSecured: Bool + private let isEnabled: Bool + private let isError: Bool + + // interaction + private let isFirstResponder: Bool + + // style + private let colors: TextFieldColors + + init( + _ hint: String, + text: Binding, + font: Font, + supportText: String?, + isSecured: Bool, + isEnabled: Bool, + isError: Bool, + isFirstResponder: Bool, + colors: TextFieldColors + ) { + self.hint = hint + self._text = text + self.font = font + self.supportText = supportText + self.isSecured = isSecured + self.isEnabled = isEnabled + self.isError = isError + self.isFirstResponder = isFirstResponder + self.colors = colors + } + + var body: some View { + Group { + if isSecured { + SecureField("", text: $text) + } else { + TextField("", text: $text) + } + } + .focused($focused) + .overlay { + Text(hint) + .foreground(colors.hintColor) + .scaleEffect( + isHighlighted ? 0.75 : 1, + anchor: .topLeading + ) + .padding(.top, isHighlighted ? -30 : 0) + .frame(maxWidth: .infinity, alignment: .leading) + } + // style + .font(font) + .foreground(colors.foregroundColor) + .tint( + isError + ? colors.errorColor + : colors.primaryColor + ) + // interaction + .disabled(!isEnabled) + .onAppear { + focused = true + } + .onChange(of: focused) { newValue in + withAnimation(.spring(duration: 0.1)) { + animatedFocusing = newValue + } + } + // optimization +#if canImport(UIKit) + .textInputAutocapitalization(.never) +#endif + .autocorrectionDisabled() + .textContentType(.init(rawValue: "")) + } +} diff --git a/Source/DDS/Component/TextField/Internal/TextFieldIcon.swift b/Source/DDS/Component/TextField/Internal/TextFieldIcon.swift new file mode 100644 index 0000000..8f50eb4 --- /dev/null +++ b/Source/DDS/Component/TextField/Internal/TextFieldIcon.swift @@ -0,0 +1,50 @@ +// +// File.swift +// +// +// Created by hhhello0507 on 8/21/24. +// + +import SwiftUI + +internal struct TextFieldIcon: View { + + let isHide: Bool + let isSecured: Bool + let isEnabled: Bool + let isError: Bool + let colors: TextFieldColors + let action: () -> Void + + var body: some View { + Button { + if isEnabled { + action() + } + } label: { + Image( + icon: isError + ? .exclamationmarkCircle + : isSecured + ? isHide ? .eyeSlash : .eye + : .xmarkCircle + ) + .resizable() + .renderingMode(.template) + .foreground( + isError + ? colors.errorColor + : colors.iconColor + ) + .frame(width: 24, height: 24) + .padding(4) + .opacity( + isError + ? 1 + : isEnabled + ? 0.5 + : 0 + ) + } + } +} diff --git a/Source/DDS/Component/TextField/TextField.swift b/Source/DDS/Component/TextField/TextField.swift index d717d5e..c73fdad 100644 --- a/Source/DDS/Component/TextField/TextField.swift +++ b/Source/DDS/Component/TextField/TextField.swift @@ -3,110 +3,165 @@ import SwiftUI @available(macOS 12, iOS 15, *) public struct DodamTextField: View { + @State private var isHide = true + @FocusState private var focused + + // MARK: - parameters private let title: String + @Binding private var text: String + private let font: Font + private let supportText: String? private let isSecured: Bool + private let isEnabled: Bool + private let isError: Bool private let isFirstResponder: Bool - @Binding private var text: String + private let colors: TextFieldColors private init( - title: String, - isSecured: Bool, + title: String = "", + text: Binding, + font: Font = .headline(.medium), + supportText: String? = nil, + isSecured: Bool = false, + isEnabled: Bool = true, + isError: Bool = false, isFirstResponder: Bool = false, - text: Binding + colors: TextFieldColors = .default ) { self.title = title + self._text = text + self.font = font + self.supportText = supportText self.isSecured = isSecured + self.isEnabled = isEnabled + self.isError = isError self.isFirstResponder = isFirstResponder - self._text = text - } - - public func makeFirstResponder() -> Self { - .init(title: title, - isSecured: isSecured, - isFirstResponder: true, - text: $text) + self.colors = colors } public static func `default`( - title: String, - text: Binding + title: String = "", + text: Binding, + font: Font = .headline(.medium), + supportText: String? = nil, + isEnabled: Bool = true, + isError: Bool = false, + isFirstResponder: Bool = false, + colors: TextFieldColors = .default ) -> Self { .init( title: title, + text: text, + font: font, + supportText: supportText, isSecured: false, - text: text + isEnabled: isEnabled, + isError: isError, + isFirstResponder: isFirstResponder, + colors: colors ) } public static func secured( - title: String, - text: Binding + title: String = "", + text: Binding, + font: Font = .headline(.medium), + supportText: String? = nil, + isEnabled: Bool = true, + isError: Bool = false, + isFirstResponder: Bool = false, + colors: TextFieldColors = .default ) -> Self { .init( title: title, + text: text, + font: font, + supportText: supportText, isSecured: true, - text: text + isEnabled: isEnabled, + isError: isError, + isFirstResponder: isFirstResponder, + colors: colors ) } - @FocusState private var isFocused: Bool - @State private var animatedFocusing: Bool = false - - private var isHighlighted: Bool { - animatedFocusing || !text.isEmpty + public func makeFirstResponder(_ condition: Bool = true) -> Self { + .init( + title: title, + text: _text, + font: font, + supportText: supportText, + isSecured: isSecured, + isEnabled: isEnabled, + isError: isError, + isFirstResponder: condition, + colors: colors + ) } - + + // MARK: - View public var body: some View { - VStack(spacing: 12) { - ZStack(alignment: .leading) { - Text(title) - .scaleEffect( - isHighlighted ? 0.75 : 1, - anchor: .topLeading - ) - .padding(.top, isHighlighted ? -30 : 0) - HStack { - Group { - if isSecured { - SecureField("", text: $text) - } else { - TextField("", text: $text) - } - } - .focused($isFocused) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .foregroundStyle(Color(.label)) - if isFocused && !text.isEmpty { - Button { - text = "" - } label: { - Dodam.icon(.xmarkCircle) - } - .foreground(DodamColor.Label.assistive) - .padding(.vertical, -2) - } + HStack(spacing: 0) { + BaseTextField( + title, + text: $text, + font: font, + supportText: supportText, + isSecured: isSecured && isHide, + isEnabled: isEnabled, + isError: isError, + isFirstResponder: isFirstResponder, + colors: colors + ) + TextFieldIcon( + isHide: isHide, + isSecured: isSecured, + isEnabled: !text.isEmpty && focused, + isError: isError, + colors: colors + ) { + if isSecured { + isHide.toggle() + } else { + text = "" } } - .frame(height: 41, alignment: .bottomLeading) - .body1(.medium) + } + .frame(height: 43) + .padding(.vertical, 4) + // Layout + .overlay { Rectangle() + .foreground( + isError + ? colors.errorColor + : focused + ? colors.primaryColor + : colors.strokeColor + ) .frame(height: 1) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) } - .foreground(isFocused ? DodamColor.Primary.normal : DodamColor.Label.alternative) - .onChange(of: isFocused) { newValue in - withAnimation(.spring(duration: 0.1)) { - animatedFocusing = newValue - } - } - .onAppear { - if isFirstResponder { - isFocused = true + .overlay { + if let supportText { + Text(supportText) + .label(.medium) + .foreground( + isError + ? colors.errorColor + : colors.foregroundColor + ) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading) + .offset(y: 24) } } + .focused($focused) + .advancedFocus() + .opacity(isEnabled ? 1 : 0.5) } } + #Preview { struct DodamTextFieldPreview: View { diff --git a/Source/DDS/Component/TextField/TextFieldColors.swift b/Source/DDS/Component/TextField/TextFieldColors.swift new file mode 100644 index 0000000..609bd13 --- /dev/null +++ b/Source/DDS/Component/TextField/TextFieldColors.swift @@ -0,0 +1,33 @@ +// +// File.swift +// +// +// Created by hhhello0507 on 8/21/24. +// + +import Foundation + +public struct TextFieldColors { + // defaut + public let hintColor: DodamColorable + public let strokeColor: DodamColorable + + // unfocused + public let foregroundColor: DodamColorable + public let iconColor: DodamColorable + + // focused + public let primaryColor: DodamColorable // for indicator color + + // error + public let errorColor: DodamColorable + + public static let `default` = TextFieldColors( + hintColor: DodamColor.Label.assistive, + strokeColor: DodamColor.Line.normal, + foregroundColor: DodamColor.Label.strong, + iconColor: DodamColor.Label.alternative, + primaryColor: DodamColor.Primary.normal, + errorColor: DodamColor.Status.negative + ) +} diff --git a/Source/DDS/Foundation/Iconography/Iconography.swift b/Source/DDS/Foundation/Iconography/Iconography.swift index e4874ae..7d61946 100644 --- a/Source/DDS/Foundation/Iconography/Iconography.swift +++ b/Source/DDS/Foundation/Iconography/Iconography.swift @@ -12,6 +12,7 @@ public enum DodamIconography: String { case checkmarkCircle = "CheckmarkCircle" case chevronLeft = "ChevronLeft" case chevronRight = "ChevronRight" + case close = "Close" case colorfulBus = "ColorfulBus" case colorfulCalender = "ColorfulCalender" case convenienceStore = "ConvenienceStore" diff --git a/Source/DDS/Foundation/Iconography/Iconography.xcassets/Close.imageset/Close.svg b/Source/DDS/Foundation/Iconography/Iconography.xcassets/Close.imageset/Close.svg new file mode 100644 index 0000000..309eed2 --- /dev/null +++ b/Source/DDS/Foundation/Iconography/Iconography.xcassets/Close.imageset/Close.svg @@ -0,0 +1,3 @@ + + + diff --git a/Source/DDS/Foundation/Iconography/Iconography.xcassets/Close.imageset/Contents.json b/Source/DDS/Foundation/Iconography/Iconography.xcassets/Close.imageset/Contents.json new file mode 100644 index 0000000..581989f --- /dev/null +++ b/Source/DDS/Foundation/Iconography/Iconography.xcassets/Close.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Close.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Source/DDS/Foundation/Typography/Extension/ViewExt.swift b/Source/DDS/Foundation/Typography/Extension/ViewExt.swift index e7bb93a..fbc6977 100644 --- a/Source/DDS/Foundation/Typography/Extension/ViewExt.swift +++ b/Source/DDS/Foundation/Typography/Extension/ViewExt.swift @@ -30,7 +30,7 @@ private struct DodamFontViewModifier: ViewModifier { @available(macOS 12, iOS 15, *) public extension View { - private func dodamFont(_ type: DodamTypography) -> some View { + internal func dodamFont(_ type: DodamTypography) -> some View { self.modifier(DodamFontViewModifier(font: type)) }