mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
193 lines
7.2 KiB
Swift
193 lines
7.2 KiB
Swift
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import SwiftUI
|
|
import Combine
|
|
import UIKit
|
|
|
|
public struct SessionTextField<ExplanationView>: View where ExplanationView: View {
|
|
@Binding var text: String
|
|
@Binding var error: String?
|
|
@State var lastErroredText: String?
|
|
@State var textThemeColor: ThemeValue = .textPrimary
|
|
@State fileprivate var textChanged: ((String) -> Void)?
|
|
|
|
@FocusState private var isFirstResponder: Bool
|
|
|
|
public enum SessionTextFieldType {
|
|
case thin
|
|
case normal
|
|
}
|
|
|
|
let explanationView: () -> ExplanationView
|
|
let placeholder: String
|
|
let font: Font
|
|
let type: SessionTextFieldType
|
|
let accessibility: Accessibility
|
|
let inputChecker: ((String) -> String?)?
|
|
var isErrorMode: Bool { error?.isEmpty == false }
|
|
|
|
let height: CGFloat
|
|
let padding: CGFloat
|
|
let cornerRadius: CGFloat
|
|
|
|
public init(
|
|
_ text: Binding<String>,
|
|
placeholder: String,
|
|
font: Font = .system(size: Values.smallFontSize),
|
|
error: Binding<String?>,
|
|
type: SessionTextFieldType = .normal,
|
|
accessibility: Accessibility = Accessibility(identifier: "SessionTextField"),
|
|
inputChecker: ((String) -> String?)? = nil,
|
|
@ViewBuilder explanationView: @escaping () -> ExplanationView = {
|
|
EmptyView()
|
|
}
|
|
) {
|
|
self._text = text
|
|
self.placeholder = placeholder
|
|
self.font = font
|
|
self.type = type
|
|
self.accessibility = accessibility
|
|
self._error = error
|
|
self.inputChecker = inputChecker
|
|
self.explanationView = explanationView
|
|
switch self.type {
|
|
case .thin:
|
|
self.height = isIPhone5OrSmaller ? CGFloat(40) : CGFloat(45)
|
|
self.padding = Values.mediumSpacing
|
|
self.cornerRadius = CGFloat(7)
|
|
case .normal:
|
|
self.height = isIPhone5OrSmaller ? CGFloat(48) : CGFloat(80)
|
|
self.padding = Values.largeSpacing
|
|
self.cornerRadius = CGFloat(13)
|
|
}
|
|
UITextView.appearance().backgroundColor = .clear
|
|
}
|
|
|
|
public var body: some View {
|
|
VStack (
|
|
alignment: .center,
|
|
spacing: Values.smallSpacing
|
|
) {
|
|
// Text input
|
|
ZStack(alignment: .leading) {
|
|
if text.isEmpty {
|
|
Text(placeholder)
|
|
.font(.system(size: Values.smallFontSize))
|
|
.foregroundColor(themeColor: .textSecondary)
|
|
.allowsHitTesting(false)
|
|
}
|
|
|
|
if #available(iOS 16.0, *) {
|
|
TextField(
|
|
"",
|
|
text: $text,
|
|
axis: .vertical
|
|
)
|
|
.font(font)
|
|
.foregroundColor(themeColor: textThemeColor)
|
|
.accessibility(self.accessibility)
|
|
.focused($isFirstResponder)
|
|
|
|
} else {
|
|
ZStack {
|
|
TextEditor(text: $text)
|
|
.font(font)
|
|
.foregroundColor(themeColor: textThemeColor)
|
|
.textViewTransparentScrolling()
|
|
.accessibility(self.accessibility)
|
|
.frame(maxHeight: self.height)
|
|
.padding(.all, -4)
|
|
.focused($isFirstResponder)
|
|
|
|
// FIXME: This is a workaround for dynamic height of the TextEditor.
|
|
Text(text.isEmpty ? placeholder : text)
|
|
.font(font)
|
|
.opacity(0)
|
|
.padding(.all, 4)
|
|
.frame(
|
|
maxWidth: .infinity,
|
|
maxHeight: self.height
|
|
)
|
|
}
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
.padding(.horizontal, self.padding)
|
|
.padding(.vertical, Values.smallSpacing)
|
|
.framing(
|
|
maxWidth: .infinity,
|
|
minHeight: self.type == .thin ? self.height : nil,
|
|
height: self.type == .thin ? nil : self.height
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: self.cornerRadius)
|
|
.stroke(themeColor: isErrorMode ? .danger : .borderSeparator)
|
|
)
|
|
.contentShape(RoundedRectangle(cornerRadius: self.cornerRadius))
|
|
.onTapGesture { isFirstResponder = !isFirstResponder } // Added hit test to launch keyboard, currently textfield's hit area is too small
|
|
.onChange(of: text) { newText in
|
|
error = inputChecker?(newText)
|
|
textThemeColor = ((newText == lastErroredText || error?.isEmpty == false) ? .danger : .textPrimary)
|
|
}
|
|
.onChange(of: error) { newError in
|
|
if newError != nil {
|
|
lastErroredText = text
|
|
textThemeColor = .danger
|
|
}
|
|
}
|
|
|
|
// Error message
|
|
switch self.type {
|
|
case .thin:
|
|
if isErrorMode {
|
|
Text(error ?? "")
|
|
.bold()
|
|
.font(.system(size: Values.smallFontSize))
|
|
.foregroundColor(themeColor: .danger)
|
|
.multilineTextAlignment(.center)
|
|
.accessibility(
|
|
Accessibility(
|
|
identifier: "Error message"
|
|
)
|
|
)
|
|
}
|
|
case .normal:
|
|
ZStack {
|
|
if isErrorMode {
|
|
Text(error ?? "")
|
|
.bold()
|
|
.font(.system(size: Values.smallFontSize))
|
|
.foregroundColor(themeColor: .danger)
|
|
.multilineTextAlignment(.center)
|
|
.accessibility(
|
|
Accessibility(
|
|
identifier: "Error message"
|
|
)
|
|
)
|
|
} else {
|
|
explanationView()
|
|
}
|
|
}
|
|
.frame(
|
|
height: 54,
|
|
alignment: .top
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SessionTextField_Previews: PreviewProvider {
|
|
@State static var text: String = "test"
|
|
@State static var error: String? = "test error"
|
|
@State static var emptyText: String = ""
|
|
@State static var emptyError: String? = nil
|
|
static var previews: some View {
|
|
VStack {
|
|
SessionTextField($text, placeholder: "Placeholder", error: $error, explanationView: {})
|
|
SessionTextField($emptyText, placeholder: "Placeholder", error: $emptyError, explanationView: {})
|
|
}
|
|
|
|
}
|
|
}
|