Skip to content

Commit 1b0b301

Browse files
committed
textfield with keyboard
1 parent 39cc528 commit 1b0b301

File tree

4 files changed

+350
-0
lines changed

4 files changed

+350
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// BaseView.swift
3+
// YYTextfieldWithKeyboard
4+
//
5+
// Created by ChuanqingYang on 2024/12/5.
6+
//
7+
8+
import SwiftUI
9+
10+
@available(iOS 16.0, *)
11+
public struct BaseBlurView: UIViewRepresentable {
12+
@Environment(\.colorScheme) private var colorScheme
13+
public init(style: UIBlurEffect.Style? = nil) {
14+
self.style = style
15+
}
16+
17+
let style: UIBlurEffect.Style?
18+
19+
public func makeUIView(context: Context) -> UIVisualEffectView {
20+
if let style = self.style {
21+
return UIVisualEffectView(effect: UIBlurEffect(style: style))
22+
} else {
23+
let style: UIBlurEffect.Style = colorScheme == .dark ? .systemUltraThinMaterialDark : .systemUltraThinMaterialLight
24+
return UIVisualEffectView(effect: UIBlurEffect(style: style))
25+
}
26+
}
27+
28+
public func updateUIView( _ view: UIVisualEffectView, context _: Context) {
29+
view.effect = UIBlurEffect(style: colorScheme == .dark ? .systemUltraThinMaterialDark : .systemUltraThinMaterialLight)
30+
}
31+
32+
public func makeCoordinator() -> Coordinator {
33+
Coordinator(style: style ?? (self.colorScheme == .dark ? .systemUltraThinMaterialDark : .systemUltraThinMaterialLight))
34+
}
35+
}
36+
37+
@available(iOS 16.0, *)
38+
extension BaseBlurView {
39+
public final class Coordinator {
40+
fileprivate init(style: UIBlurEffect.Style) {
41+
self.style = style
42+
}
43+
44+
let style: UIBlurEffect.Style
45+
}
46+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// ExampleView.swift
3+
// YYTextfieldWithKeyboard
4+
//
5+
// Created by ChuanqingYang on 2024/12/5.
6+
//
7+
8+
import SwiftUI
9+
10+
@available(iOS 16.0, *)
11+
public struct ExampleView: View {
12+
@State private var text: String = ""
13+
@FocusState var isActive: Bool
14+
public var body: some View {
15+
YYTextFieldWithKeyboard {
16+
TextField("", text: $text)
17+
.padding(.horizontal, 10)
18+
.padding(.vertical, 6)
19+
.frame(width: 150)
20+
.background {
21+
RoundedRectangle(cornerRadius: 8)
22+
.fill(Color.secondary)
23+
}
24+
} keyboard: {
25+
CustomNumberKeyboard(text: $text, isActive: $isActive, config: .default)
26+
}
27+
28+
}
29+
}
30+
31+
@available(iOS 16.0, *)
32+
#Preview {
33+
ExampleView()
34+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
//
2+
// KeyboardViews.swift
3+
// YYTextfieldWithKeyboard
4+
//
5+
// Created by ChuanqingYang on 2024/12/5.
6+
//
7+
import SwiftUI
8+
9+
@available(iOS 16.0, *)
10+
public struct CustomNumberKeyboard: View {
11+
12+
@Binding public var text: String
13+
14+
@FocusState.Binding public var isActive: Bool
15+
16+
public var config: Config
17+
18+
public struct Config : Sendable{
19+
public static let `default`: Config = .init()
20+
21+
public var baseStyle: BaseStyle = .base(.default)
22+
23+
public enum BaseStyle: Sendable {
24+
case base(_ config: BaseStyleConfig)
25+
26+
public struct BaseStyleConfig: Sendable {
27+
public static let `default`: BaseStyleConfig = .init()
28+
29+
public var backgroundColor: Color = Color.gray.opacity(0.5)
30+
}
31+
}
32+
33+
public var layoutStyle: LayoutStyle = .default
34+
35+
public struct LayoutStyle : Sendable{
36+
public static let `default`: LayoutStyle = .init()
37+
38+
var keyboardVericalSpacing: CGFloat = 15
39+
40+
var keyboardVerticalPadding: CGFloat = 15
41+
42+
var buttonHorizontalPadding: CGFloat = 24
43+
}
44+
45+
var numberStyle: NumberStyle = .normal(.default)
46+
47+
public enum NumberStyle : Sendable{
48+
case normal(_ config: NumberStyleConfig)
49+
50+
public struct NumberStyleConfig: Sendable {
51+
public static let `default`: NumberStyleConfig = .init()
52+
53+
public var numberFont: Font = .title3
54+
public var fontWeight: Font.Weight = .semibold
55+
public var numberForegroundColor: Color = Color.white
56+
}
57+
}
58+
59+
var buttonStyle: ButtonStyle = .rounded(.default)
60+
61+
public enum ButtonStyle : Sendable{
62+
case capsule(_ config: ButtonStyleConfig)
63+
case rounded(_ config: ButtonStyleConfig)
64+
65+
public struct ButtonStyleConfig : Sendable{
66+
public static let `default`: ButtonStyleConfig = .init()
67+
68+
public var buttonTitle: String = "Confirm"
69+
public var buttonHeight: CGFloat = 48
70+
public var buttonCornerRadius: CGFloat = 8
71+
public var buttonTitleFont: Font = .title3
72+
public var buttonTitleFontWeight: Font.Weight = .semibold
73+
public var buttonForegroundColor: Color = Color.white
74+
public var buttonBackgroundColor: Color = Color.clear
75+
public var buttonBoarderColor: Color = Color.gray
76+
public var buttonBoarderWidth: CGFloat = 1
77+
}
78+
}
79+
}
80+
81+
public enum KeyboardType {
82+
case number
83+
case point
84+
case delete
85+
}
86+
87+
public init(text: Binding<String>, isActive: FocusState<Bool>.Binding, config: Config) {
88+
_text = text
89+
_isActive = isActive
90+
self.config = config
91+
}
92+
93+
public var body: some View {
94+
VStack(alignment: .leading, spacing: 12) {
95+
LazyVGrid(columns: Array(repeating: GridItem(spacing: 0), count: 3), spacing: config.layoutStyle.keyboardVericalSpacing) {
96+
ForEach(1...9, id: \.self) { index in
97+
ButtonView("\(index)", type: .number)
98+
}
99+
100+
ButtonView(".", type: .point)
101+
102+
ButtonView("0", type: .number)
103+
104+
ButtonView("delete.backward.fill", type: .delete)
105+
}
106+
.padding(.vertical, config.layoutStyle.keyboardVericalSpacing)
107+
108+
ConfirmButton()
109+
.padding(.horizontal, config.layoutStyle.buttonHorizontalPadding)
110+
111+
Spacer()
112+
}
113+
.background {
114+
switch config.baseStyle {
115+
case let .base(style):
116+
style.backgroundColor
117+
.ignoresSafeArea()
118+
}
119+
}
120+
}
121+
122+
@ViewBuilder
123+
func ConfirmButton() -> some View {
124+
Button {
125+
isActive = false
126+
} label: {
127+
switch config.buttonStyle {
128+
case .capsule(let style):
129+
Capsule()
130+
.stroke(style.buttonBoarderColor, lineWidth: style.buttonBoarderWidth)
131+
.background {
132+
style.buttonBackgroundColor
133+
}
134+
.overlay {
135+
Text(style.buttonTitle)
136+
.font(style.buttonTitleFont)
137+
.fontWeight(style.buttonTitleFontWeight)
138+
.foregroundStyle(style.buttonForegroundColor)
139+
}
140+
.frame(height: style.buttonHeight)
141+
case .rounded(let style):
142+
RoundedRectangle(cornerRadius: style.buttonCornerRadius, style: .continuous)
143+
.stroke(style.buttonBoarderColor, lineWidth: style.buttonBoarderWidth)
144+
.background {
145+
style.buttonBackgroundColor
146+
}
147+
.overlay {
148+
Text(style.buttonTitle)
149+
.font(style.buttonTitleFont)
150+
.fontWeight(style.buttonTitleFontWeight)
151+
.foregroundStyle(style.buttonForegroundColor)
152+
}
153+
.frame(height: style.buttonHeight)
154+
}
155+
}
156+
157+
}
158+
159+
@ViewBuilder
160+
func ButtonView(_ value: String, type: KeyboardType) -> some View {
161+
Button {
162+
switch type {
163+
case .number:
164+
if text.hasPrefix("0") && !text.hasPrefix("0.") && value == "0" {
165+
return
166+
}
167+
text += value
168+
case .point:
169+
if text.isEmpty && value == "." {
170+
text = "0."
171+
return
172+
}
173+
174+
if text.contains(".") && value == "." {
175+
return
176+
}
177+
178+
text += value
179+
case .delete:
180+
if !text.isEmpty {
181+
text.removeLast()
182+
}
183+
}
184+
} label: {
185+
switch type {
186+
case .number, .point:
187+
switch config.numberStyle {
188+
case .normal(let style):
189+
Text(value)
190+
.font(style.numberFont)
191+
.fontWeight(style.fontWeight)
192+
.foregroundStyle(style.numberForegroundColor)
193+
}
194+
case .delete:
195+
Image(systemName: value)
196+
.renderingMode(.template)
197+
.foregroundStyle(Color.white)
198+
}
199+
}
200+
201+
}
202+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,70 @@
11
// The Swift Programming Language
22
// https://docs.swift.org/swift-book
3+
4+
import SwiftUI
5+
6+
@available(iOS 16.0, *)
7+
public struct YYTextFieldWithKeyboard<TextField: View, Keyboard: View>: UIViewControllerRepresentable {
8+
9+
@ViewBuilder var textfield: TextField
10+
@ViewBuilder var keyboard: Keyboard
11+
12+
public init(
13+
@ViewBuilder textfield: () -> TextField,
14+
@ViewBuilder keyboard: () -> Keyboard
15+
) {
16+
self.textfield = textfield()
17+
self.keyboard = keyboard()
18+
}
19+
20+
public func makeUIViewController(context: Context) -> UIHostingController<TextField> {
21+
let controller = UIHostingController(rootView: textfield)
22+
23+
DispatchQueue.main.async {
24+
if let textfield = controller.view.allSubviews.first(where: { $0 is UITextField }) as? UITextField, textfield.inputView == nil {
25+
// place inputview
26+
let inputController = UIHostingController(rootView: keyboard)
27+
inputController.view.backgroundColor = .clear
28+
inputController.view.frame = .init(origin: .zero, size: CGSize(width: inputController.view.intrinsicContentSize.width, height: inputController.view.intrinsicContentSize.height + SafeAreaHelper.bottomSafeAreaHeight()))
29+
textfield.inputView = inputController.view
30+
textfield.reloadInputViews()
31+
}
32+
}
33+
34+
controller.view.backgroundColor = .clear
35+
return controller
36+
}
37+
38+
public func updateUIViewController(_ uiViewController: UIHostingController<TextField>, context: Context) {
39+
40+
}
41+
42+
public func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: UIHostingController<TextField>, context: Context) -> CGSize? {
43+
return uiViewController.view.intrinsicContentSize
44+
}
45+
}
46+
47+
// Finding UITextField from UIHostingController
48+
fileprivate extension UIView {
49+
var allSubviews: [UIView] {
50+
return self.subviews.flatMap({ [$0] + $0.allSubviews })
51+
}
52+
}
53+
54+
fileprivate struct SafeAreaHelper {
55+
@MainActor static func getSafeAreaInsets() -> UIEdgeInsets {
56+
guard let window = UIApplication.shared.windows.first else {
57+
return .zero
58+
}
59+
return window.safeAreaInsets
60+
}
61+
62+
@MainActor static func bottomSafeAreaHeight() -> CGFloat {
63+
return getSafeAreaInsets().bottom
64+
}
65+
66+
@MainActor static func topSafeAreaHeight() -> CGFloat {
67+
return getSafeAreaInsets().top
68+
}
69+
}
70+

0 commit comments

Comments
 (0)