Button inside of TextField in SwiftUI works on iOS but not on MacOS


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
    }
}

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img