// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import SwiftUI import Combine import UIKit public struct SessionTextField: View where ExplanationView: View { @Binding var text: String @Binding var error: String? @State var previousError: String = "" @State var textThemeColor: ThemeValue = .textPrimary let explanationView: () -> ExplanationView let placeholder: String let accessibility: Accessibility var isErrorMode: Bool { guard previousError.isEmpty else { return true } if error?.isEmpty == false { return true } return false } let height: CGFloat = isIPhone5OrSmaller ? CGFloat(48) : CGFloat(80) let cornerRadius: CGFloat = 13 public init( _ text: Binding, placeholder: String, error: Binding, accessibility: Accessibility = Accessibility(), @ViewBuilder explanationView: @escaping () -> ExplanationView = { EmptyView() } ) { self._text = text self.placeholder = placeholder self.accessibility = accessibility self._error = error self.explanationView = explanationView 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: isErrorMode ? .danger : .textSecondary) } if #available(iOS 16.0, *) { SwiftUI.TextField( "", text: $text.onChange{ value in if error?.isEmpty == false && text != value { previousError = error! error = nil } }, axis: .vertical ) .font(.system(size: Values.smallFontSize)) .foregroundColor(themeColor: textThemeColor) .accessibility(self.accessibility) } else { ZStack { TextEditor( text: $text.onChange{ value in if error?.isEmpty == false && text != value { previousError = error! error = nil } } ) .font(.system(size: Values.smallFontSize)) .foregroundColor(themeColor: textThemeColor) .textViewTransparentScrolling() .accessibility(self.accessibility) .frame(maxHeight: self.height) .padding(.all, -4) // FIXME: This is a workaround for dynamic height of the TextEditor. Text(text.isEmpty ? placeholder : text) .font(.system(size: Values.smallFontSize)) .opacity(0) .padding(.all, 4) .frame( maxWidth: .infinity, maxHeight: self.height ) } .fixedSize(horizontal: false, vertical: true) } } .padding(.horizontal, Values.largeSpacing) .frame(maxWidth: .infinity) .frame(height: self.height) .overlay( RoundedRectangle( cornerSize: CGSize( width: self.cornerRadius, height: self.cornerRadius ) ) .stroke(themeColor: isErrorMode ? .danger : .borderSeparator) ) .onReceive(Just(error)) { newValue in textThemeColor = (newValue?.isEmpty == false) ? .danger : .textPrimary } // Error message ZStack { if isErrorMode { Text(error ?? previousError) .bold() .font(.system(size: Values.smallFontSize)) .foregroundColor(themeColor: .danger) .multilineTextAlignment(.center) .accessibility( Accessibility( identifier: "Error message", label: "Error message" ) ) } else { explanationView() } } .frame( height: 50, 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) {} SessionTextField($emptyText, placeholder: "Placeholder", error: $emptyError) {} } } }