can anyone help with this implementation of TextField on Swiftui?
I need the user to enter text to get the same result as in the video
After double exposure, this text was ordered in the usual format.
My code is presented below, it works better on iOS, but on a Mac it works disgustingly.
My implementation was written using the kavSoft video: https://www.youtube.com/watch?v=6aw1KaUg4MY&t=309s
import SwiftUI
struct Tag: Identifiable, Hashable {
var id = UUID()
var value: String
var isInitial: Bool = false
}
@available(iOS 17.0, macOS 14.0, *)
struct TagsTextField: View {
@Binding var tags: [Tag]
@State private var new: String = " "
var body: some View {
TagLayout(alignment: .leading) {
ForEach($tags) { $tag in
TagView(tag: $tag, allTags: $tags)
.onChange(of: tag.value) { newValue in
if newValue.last == " " {
if NSAttributedString(string: newValue) != unitTextField(str: newValue) && newValue.last == " " {
tag.value = unitTextField(str: newValue).string
new = unitTextField(str: newValue).string
} else {
if !tag.value.isEmpty {
tags.append(.init(value: ""))
}
}
}
print(tags.count)
}
.background(NSAttributedString(string: tag.value) == unitTextField(str: new) ? Color.purple : Color.clear, in: .rect(cornerRadius: 10, style: .continuous))
.background(highlightedText(str: tag.value) ? Color.blue : Color.clear, in: .rect(cornerRadius: 10, style: .continuous))
.padding(.horizontal, NSAttributedString(string: tag.value) != unitTextField(str: new) ? 0 : 10)
}
}
.clipped()
.padding(.vertical, 10)
.padding(.horizontal, 15)
.background(.bar, in: .rect(cornerRadius: 12))
.onAppear() {
if tags.isEmpty {
tags.append(.init(value: "", isInitial: true))
}
}
}
func highlightedText(str: String) -> Bool {
let pattern = "%\\d*\\$?@"
let unitPattern = "%#\\d*\\$?@[^@]*@"
do {
let regex = try NSRegularExpression(pattern: pattern, options: [])
let unitRegex = try NSRegularExpression(pattern: unitPattern, options: [])
let matches = regex.matches(in: str, options: [], range: NSRange(location: 0, length: str.utf16.count))
let unitMatches = unitRegex.matches(in: str, options: [], range: NSRange(location: 0, length: str.utf16.count))
let attributedString = NSMutableAttributedString(string: str)
for match in matches {
let range = Range(match.range, in: str)!
return true
}
return false
} catch {
return false
}
}
func unitTextField(str: String) -> NSAttributedString {
let unitPattern = "%#@(\\d*)\\$?([^@]*)@"
do {
let unitRegex = try NSRegularExpression(pattern: unitPattern, options: [])
let unitMatches = unitRegex.matches(in: str, options: [], range: NSRange(location: 0, length: str.utf16.count))
let attributedString = NSMutableAttributedString(string: str)
for unitMatch in unitMatches {
let fullRange = Range(unitMatch.range, in: str)!
let group2Range = Range(unitMatch.range(at: 2), in: str)!
let replacement = str[group2Range]
attributedString.replaceCharacters(in: NSRange(fullRange, in: str), with: "@" + String(replacement))
}
return attributedString
} catch {
return NSAttributedString(string: str)
}
}
}
@available(iOS 17.0, macOS 14.0, *)
fileprivate struct TagView: View {
@Binding var tag: Tag
@Binding var allTags: [Tag]
@FocusState private var isFocused: Bool
var body: some View {
BackSpaceListenerTextField(text: $tag.value, hint: "", onBackPressed: {
if allTags.count > 1 {
if tag.value.isEmpty {
allTags.removeAll(where: { $0.id == tag.id } )
if let lastIndex = allTags.indices.last {
allTags[lastIndex].isInitial = false
}
}
}
})
.focused($isFocused)
.padding(.vertical, 5)
.disabled(tag.isInitial)
.onChange(of: allTags, initial: true) { oldValue, newValue in
if newValue.last?.id == tag.id && !(newValue.last?.isInitial ?? false) && !isFocused {
isFocused = true
}
}
.overlay {
if tag.isInitial {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(.clear)
.contentShape(.rect)
.onTapGesture {
if allTags.last?.id == tag.id {
tag.isInitial = false
isFocused = true
}
}
}
}
.onChange(of: isFocused) { _ in
if !isFocused {
tag.isInitial = true
}
}
}
func highlightedText(str: String) -> NSAttributedString {
let unitPattern = "%#@(\\d*)\\$?([^@]*)@"
do {
let unitRegex = try NSRegularExpression(pattern: unitPattern, options: [])
let unitMatches = unitRegex.matches(in: str, options: [], range: NSRange(location: 0, length: str.utf16.count))
let attributedString = NSMutableAttributedString(string: str)
for unitMatch in unitMatches {
let fullRange = Range(unitMatch.range, in: str)!
let group1Range = Range(unitMatch.range(at: 1), in: str)!
let group2Range = Range(unitMatch.range(at: 2), in: str)!
attributedString.addAttribute(.backgroundColor, value: PlatformColor.systemPurple, range: NSRange(group1Range, in: str))
let replacement = str[group2Range]
attributedString.replaceCharacters(in: NSRange(fullRange, in: str), with: "@" + String(replacement))
}
return attributedString
} catch {
return NSAttributedString(string: str)
}
}
}
struct TagLayout: Layout {
var alignment: Alignment = .center
var spacing: CGFloat = 0
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxWidth = proposal.width ?? 0
let rows = generateRows(maxWidth, proposal, subviews)
var height: CGFloat = 0
for (index, row) in rows.enumerated() {
if index == (rows.count - 1) {
height += row.maxHeight(proposal)
} else {
height += row.maxHeight(proposal) + spacing
}
}
return .init(width: maxWidth, height: height)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var origin = bounds.origin
let maxWidth = bounds.width
let rows = generateRows(maxWidth, proposal, subviews)
for row in rows {
let leading: CGFloat = bounds.maxX - maxWidth
let trailing = bounds.maxX - (row.reduce(CGFloat.zero) { partialResult, view in
let width = view.sizeThatFits(proposal).width
if view == row.last {
return partialResult + width
}
return partialResult + width + spacing
})
let center = (trailing + leading) / 2
origin.x = (alignment == .leading ? leading : alignment == .trailing ? trailing : center)
for view in row {
let viewSize = view.sizeThatFits(proposal)
view.place(at: origin, proposal: proposal)
origin.x += (viewSize.width + spacing)
}
origin.y += (row.maxHeight(proposal) + spacing)
}
}
func generateRows(_ maxWidth: CGFloat, _ proposal: ProposedViewSize, _ subViews: Subviews) -> [[LayoutSubviews.Element]] {
var row: [LayoutSubviews.Element] = []
var rows: [[LayoutSubviews.Element]] = []
var origin = CGRect.zero.origin
for view in subViews {
let viewSize = view.sizeThatFits(proposal)
if (origin.x + viewSize.width + spacing) > maxWidth {
rows.append(row)
row.removeAll()
origin.x = 0
row.append(view)
origin.x += (viewSize.width + spacing)
} else {
row.append(view)
origin.x += (viewSize.width + spacing)
}
}
if !row.isEmpty {
rows.append(row)
row.removeAll()
}
return rows
}
}
extension [LayoutSubviews.Element] {
func maxHeight(_ proposal: ProposedViewSize) -> CGFloat {
return self.compactMap { view in
return view.sizeThatFits(proposal).height
}.max() ?? 0
}
}
#if os(iOS)
fileprivate struct BackSpaceListenerTextField: UIViewRepresentable {
@Binding var text: String
var hint: String = ""
var onBackPressed: () -> ()
func makeUIView(context: Context) -> CustomTextField {
let textField = CustomTextField()
textField.delegate = context.coordinator
textField.onBackPressed = onBackPressed
textField.placeholder = hint
textField.autocorrectionType = .no
textField.autocapitalizationType = .words
textField.backgroundColor = .clear
textField.addTarget(context.coordinator, action: #selector(Coordinator.textChange(textField:)), for: .editingChanged)
return textField
}
func updateUIView(_ uiView: CustomTextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView: CustomTextField, context: Context) -> CGSize? {
return uiView.intrinsicContentSize
}
class Coordinator: NSObject, UITextFieldDelegate {
@Binding var text: String
init(text: Binding<String>) {
self._text = text
}
@objc func textChange(textField: UITextField) {
text = textField.text ?? ""
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
}
}
}
fileprivate class CustomTextField: UITextField {
open var onBackPressed: (() -> ())?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func deleteBackward() {
onBackPressed?()
super.deleteBackward()
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}




