TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

NSTokenField For Swift 5 & MacOS Only

Forums > SwiftUI

NSTokenField for Swift 5 & macOS was written by me with help from ChatGPT to fix code errors and add code comments. It's not perfect, but it works.

Please let me know if there's anything I can improve upon.

//
//  TokenField.swift
//  TokenField
//
//  Created by DJABHipHop on 1/18/24.
//
import Combine
import SwiftUI
import AppKit
import Foundation
import Cocoa

struct TokenField: NSViewRepresentable {
    var placeholder: String = ""
    var placeholderAttributedString: NSAttributedString = NSAttributedString("")

    @Binding var tokens: [String]
    var preString: String
    var activity: PassthroughSubject<Date, Never>

    var tooltip: String = ""
    @Environment(\.tokenFieldDrawsBackground) private var drawsBackground: Bool
    @Environment(\.tokenFieldTextColor) private var textColor: NSColor
    @Environment(\.tokenFieldTokenStyle) private var tokenStyle: NSTokenField.TokenStyle
    @Environment(\.tokenFieldBackgroundColor) private var backgroundColor: NSColor
    @Environment(\.tokenFieldBezelStyle) private var bezelStyle: NSTextField.BezelStyle
    @Environment(\.tokenFieldAllowsExpansionToolTips) private var allowsExpansionToolTips: Bool
    @Environment(\.tokenFieldAllowsEditingTextAttributes) private var allowsEditingTextAttributes: Bool
    @Environment(\.tokenFieldAllowsCharacterPickerTouchBarItem) private var allowsCharacterPickerTouchBarItem: Bool
    @Environment(\.tokenFieldAllowsDefaultTighteningForTruncation) private var allowsDefaultTighteningForTruncation: Bool
    @Environment(\.tokenFieldAlignment) private var alignment: NSTextAlignment
    @Environment(\.tokenFieldAutoresizingMask) private var autoresizingMask: NSView.AutoresizingMask
    @Environment(\.controlSize) private var controlSize: ControlSize
    @Environment(\.tokenFieldContentType) private var contentType: NSTextContentType
    @Environment(\.tokenFieldLineBreakMode) private var lineBreakMode: NSLineBreakMode
    @Environment(\.tokenFieldLineBreakStrategy) private var lineBreakStrategy: NSParagraphStyle.LineBreakStrategy
    @Environment(\.tokenFieldMaximumNumberOfLines) private var maximumNumberOfLines: Int
    @Environment(\.tokenFieldFocusRingType) private var focusRingType: NSFocusRingType
    @Environment(\.tokenFieldUsesSingleLineMode) private var usesSingleLineMode: Bool

    init(_ placeholder: String, tokens: Binding<[String]>, preString: String, activity: PassthroughSubject<Date, Never>) {
        self.placeholder = placeholder
        self._tokens = tokens
        self.preString = preString
        self.activity = activity
    }

    init(_ placeholderAttributedString: NSAttributedString, tokens: Binding<[String]>, preString: String, activity: PassthroughSubject<Date, Never>) {
        self.placeholderAttributedString = placeholderAttributedString
        self._tokens = tokens
        self.preString = preString
        self.activity = activity
    }

    init(_ placeholder: String, tokens: Binding<[String]>, preString: String, tooltip: String, activity: PassthroughSubject<Date, Never>) {
        self.placeholder = placeholder
        self._tokens = tokens
        self.preString = preString
        self.tooltip = tooltip
        self.activity = activity
    }

    init(_ placeholderAttributedString: NSAttributedString, tokens: Binding<[String]>, preString: String, tooltip: String, activity: PassthroughSubject<Date, Never>) {
        self.placeholderAttributedString = placeholderAttributedString
        self._tokens = tokens
        self.preString = preString
        self.tooltip = tooltip
        self.activity = activity
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    class Coordinator: NSObject, NSTokenFieldDelegate {
        var parent: TokenField

        init(parent: TokenField) {
            self.parent = parent
        }

        func tokenField(_ tokenField: NSTokenField, completionsForSubstring substring: String, indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer<Int>?) -> [Any]? {
            // Implement token suggestions based on your logic
            return withAnimation(.default){[substring]}
        }

        func tokenField(_ tokenField: NSTokenField, shouldAdd tokens: [Any], at index: Int) -> [Any] {
            // Validate and modify tokens before adding them
            let modifiedTokens = tokens.map { token -> Any in
                if let stringToken = token as? String {
                    let modifiedString = stringToken.replacingOccurrences(of: parent.preString, with: "")
                    return withAnimation(.default){
                         parent.preString.appending(modifiedString).uppercased()
                    }
                }
                return withAnimation(.default){token}
            }
            return withAnimation(.default){modifiedTokens}
        }

        func tokenField(_ tokenField: NSTokenField, displayStringForRepresentedObject representedObject: Any) -> String? {
            parent.tokens = tokenField.objectValue as! [String]
            return tokenField.objectValue as? String
        }

        func controlTextDidEndEditing(_ obj: Notification) {
            guard let tokenField = obj.object as? NSTokenField else {
                return
            }

            // Assuming tokens property is of type [String]
            if let newTokens = tokenField.objectValue as? [String] {
                self.parent.tokens = newTokens
            }

            // Add the following line if you want to send the current date through the activity publisher
            self.parent.activity.send(Date())
        }
    }

    func makeNSView(context: Context) -> NSTokenField {
        let tokenField = NSTokenField()
        tokenField.delegate = context.coordinator
        tokenField.placeholderString = placeholder
        tokenField.placeholderAttributedString = placeholderAttributedString
        tokenField.toolTip = tooltip
        tokenField.alignment = alignment
        tokenField.autoresizingMask = autoresizingMask
        tokenField.controlSize = getNSControlSize()
        tokenField.contentType = contentType
        tokenField.lineBreakMode = lineBreakMode
        tokenField.lineBreakStrategy = lineBreakStrategy
        tokenField.usesSingleLineMode = usesSingleLineMode
        tokenField.maximumNumberOfLines = maximumNumberOfLines
        tokenField.focusRingType = focusRingType
        tokenField.autoresizesSubviews = false
//        tokenField.setf
        tokenField.completionDelay = 0
        tokenField.allowedTouchTypes = .indirect
        tokenField.textColor = textColor
        tokenField.tokenStyle = tokenStyle
        tokenField.drawsBackground = drawsBackground
        tokenField.backgroundColor = backgroundColor
        tokenField.bezelStyle = bezelStyle
        tokenField.allowsExpansionToolTips = allowsExpansionToolTips
        tokenField.allowsEditingTextAttributes = allowsEditingTextAttributes
        tokenField.allowsCharacterPickerTouchBarItem = allowsCharacterPickerTouchBarItem
        tokenField.allowsDefaultTighteningForTruncation = allowsDefaultTighteningForTruncation
        tokenField.becomeFirstResponder()
        tokenField.becomeFirstResponder()
        return tokenField
    }

    func updateNSView(_ nsView: NSTokenField, context: Context) {
        nsView.objectValue = tokens
        nsView.becomeFirstResponder()
        nsView.drawFocusRingMask()
        nsView.resignFirstResponder()
    }

    func textView(_ textView: NSTextView, dragOperationForDraggingInfo dragInfo: NSDraggingInfo, type: NSPasteboard.PasteboardType) -> NSDragOperation {

        // if option is pressed, always return .copy
        if NSApp.currentEvent?.modifierFlags.intersection(.deviceIndependentFlagsMask) == .option {
            return .copy
        }

        // if the source and destination are the same, we want default NSTokenField behavior
        if let source = dragInfo.draggingSource as? NSTextView, textView == source {
            return .generic
        }

        // default to .move
        return .move
    }

    private func getNSControlSize() -> NSControl.ControlSize {
        switch controlSize {
        case .mini:
            return .mini
        case .small:
            return .small
        case .large:
            return .large
        default:
            return .regular
        }
    }
}

struct TokenFieldProperties {
    var drawsBackground: Bool = true
    var textColor: NSColor = .textColor
    var tokenStyle: NSTokenField.TokenStyle = .rounded
    var backgroundColor: NSColor = .textBackgroundColor
    var bezelStyle: NSTextField.BezelStyle = .roundedBezel
    var allowsExpansionToolTips: Bool = false
    var allowsEditingTextAttributes: Bool = true
    var allowsCharacterPickerTouchBarItem: Bool = true
    var allowsDefaultTighteningForTruncation: Bool = true
    var alignment: NSTextAlignment = .natural
    var autoresizingMask: NSView.AutoresizingMask = .none
    var controlSize: NSControl.ControlSize = .large
    var contentType: NSTextContentType = .URL
    var lineBreakMode: NSLineBreakMode = .byClipping
    var lineBreakStrategy: NSParagraphStyle.LineBreakStrategy = .pushOut
    var maximumNumberOfLines: Int = 1
    var focusRingType: NSFocusRingType = .default
    var usesSingleLineMode: Bool = false
}

private struct TokenFieldPropertiesKey: EnvironmentKey {
    static let defaultValue = TokenFieldProperties()
}

extension EnvironmentValues {
    var tokenFieldDrawsBackground: Bool {
        get { self[TokenFieldPropertiesKey.self].drawsBackground }
        set { self[TokenFieldPropertiesKey.self].drawsBackground = newValue }
    }

    var tokenFieldTextColor: NSColor {
        get { self[TokenFieldPropertiesKey.self].textColor }
        set { self[TokenFieldPropertiesKey.self].textColor = newValue }
    }

    var tokenFieldTokenStyle: NSTokenField.TokenStyle {
        get { self[TokenFieldPropertiesKey.self].tokenStyle }
        set { self[TokenFieldPropertiesKey.self].tokenStyle = newValue }
    }

    var tokenFieldBackgroundColor: NSColor {
        get { self[TokenFieldPropertiesKey.self].backgroundColor }
        set { self[TokenFieldPropertiesKey.self].backgroundColor = newValue }
    }

    var tokenFieldBezelStyle: NSTextField.BezelStyle {
        get { self[TokenFieldPropertiesKey.self].bezelStyle }
        set { self[TokenFieldPropertiesKey.self].bezelStyle = newValue }
    }

    var tokenFieldAllowsExpansionToolTips: Bool {
        get { self[TokenFieldPropertiesKey.self].allowsExpansionToolTips }
        set { self[TokenFieldPropertiesKey.self].allowsExpansionToolTips = newValue }
    }

    var tokenFieldAllowsEditingTextAttributes: Bool {
        get { self[TokenFieldPropertiesKey.self].allowsEditingTextAttributes }
        set { self[TokenFieldPropertiesKey.self].allowsEditingTextAttributes = newValue }
    }

    var tokenFieldAllowsCharacterPickerTouchBarItem: Bool {
        get { self[TokenFieldPropertiesKey.self].allowsCharacterPickerTouchBarItem }
        set { self[TokenFieldPropertiesKey.self].allowsCharacterPickerTouchBarItem = newValue }
    }

    var tokenFieldAllowsDefaultTighteningForTruncation: Bool {
        get { self[TokenFieldPropertiesKey.self].allowsDefaultTighteningForTruncation }
        set { self[TokenFieldPropertiesKey.self].allowsDefaultTighteningForTruncation = newValue }
    }

    var tokenFieldAlignment: NSTextAlignment {
        get { self[TokenFieldPropertiesKey.self].alignment }
        set { self[TokenFieldPropertiesKey.self].alignment = newValue }
    }

    var tokenFieldAutoresizingMask: NSView.AutoresizingMask {
        get { self[TokenFieldPropertiesKey.self].autoresizingMask }
        set { self[TokenFieldPropertiesKey.self].autoresizingMask = newValue }
    }

    var tokenFieldControlSize: NSControl.ControlSize {
        get { self[TokenFieldPropertiesKey.self].controlSize }
        set { self[TokenFieldPropertiesKey.self].controlSize = newValue }
    }

    var tokenFieldContentType: NSTextContentType {
        get { self[TokenFieldPropertiesKey.self].contentType }
        set { self[TokenFieldPropertiesKey.self].contentType = newValue }
    }

    var tokenFieldLineBreakMode: NSLineBreakMode {
        get { self[TokenFieldPropertiesKey.self].lineBreakMode }
        set { self[TokenFieldPropertiesKey.self].lineBreakMode = newValue }
    }

    var tokenFieldLineBreakStrategy: NSParagraphStyle.LineBreakStrategy {
        get { self[TokenFieldPropertiesKey.self].lineBreakStrategy }
        set { self[TokenFieldPropertiesKey.self].lineBreakStrategy = newValue }
    }

    var tokenFieldUsesSingleLineMode: Bool {
        get { self[TokenFieldPropertiesKey.self].usesSingleLineMode }
        set { self[TokenFieldPropertiesKey.self].usesSingleLineMode = newValue }
    }

    var tokenFieldMaximumNumberOfLines: Int {
        get { self[TokenFieldPropertiesKey.self].maximumNumberOfLines }
        set { self[TokenFieldPropertiesKey.self].maximumNumberOfLines = newValue }
    }

    var tokenFieldFocusRingType: NSFocusRingType {
        get { self[TokenFieldPropertiesKey.self].focusRingType }
        set { self[TokenFieldPropertiesKey.self].focusRingType = newValue }
    }
}

extension View {
    func tokenFieldDrawsBackground(_ drawsBackground: Bool) -> some View {
        environment(\.tokenFieldDrawsBackground, drawsBackground)
    }

    func tokenFieldTextColor(_ textColor: NSColor) -> some View {
        environment(\.tokenFieldTextColor, textColor)
    }

    func tokenStyle(_ tokenStyle: NSTokenField.TokenStyle) -> some View {
        environment(\.tokenFieldTokenStyle, tokenStyle)
    }

    func tokenFieldBackgroundColor(_ backgroundColor: NSColor) -> some View {
        environment(\.tokenFieldBackgroundColor, backgroundColor)
    }

    func tokenFieldBezelStyle(_ bezelStyle: NSTextField.BezelStyle) -> some View {
        environment(\.tokenFieldBezelStyle, bezelStyle)
    }

    func tokenFieldAllowsExpansionToolTips(_ allowsExpansionToolTips: Bool) -> some View {
        environment(\.tokenFieldAllowsExpansionToolTips, allowsExpansionToolTips)
    }

    func tokenFieldAllowsEditingTextAttributes(_ allowsEditingTextAttributes: Bool) -> some View {
        environment(\.tokenFieldAllowsEditingTextAttributes, allowsEditingTextAttributes)
    }

    func tokenFieldAllowsCharacterPickerTouchBarItem(_ allowsCharacterPickerTouchBarItem: Bool) -> some View {
        environment(\.tokenFieldAllowsCharacterPickerTouchBarItem, allowsCharacterPickerTouchBarItem)
    }

    func tokenFieldAllowsDefaultTighteningForTruncation(_ allowsDefaultTighteningForTruncation: Bool) -> some View {
        environment(\.tokenFieldAllowsDefaultTighteningForTruncation, allowsDefaultTighteningForTruncation)
    }

    func tokenAlignment(_ alignment: NSTextAlignment) -> some View {
        environment(\.tokenFieldAlignment, alignment)
    }

    func tokenFieldAutoresizingMask(_ autoresizingMask: NSView.AutoresizingMask) -> some View {
        environment(\.tokenFieldAutoresizingMask, autoresizingMask)
    }

    func tokenFieldControlSize(_ controlSize: NSControl.ControlSize) -> some View {
        environment(\.tokenFieldControlSize, controlSize)
    }

    func tokenContentType(_ contentType: NSTextContentType) -> some View {
        environment(\.tokenFieldContentType, contentType)
    }

    func tokenLineBreakMode(_ lineBreakMode: NSLineBreakMode) -> some View {
        environment(\.tokenFieldLineBreakMode, lineBreakMode)
    }

    func tokenLineBreakStrategy(_ lineBreakStrategy: NSParagraphStyle.LineBreakStrategy) -> some View {
        environment(\.tokenFieldLineBreakStrategy, lineBreakStrategy)
    }

    func tokenlineLimit(_ lineLimit: Int) -> some View {
        environment(\.tokenFieldMaximumNumberOfLines, lineLimit)
            .environment(\.tokenFieldUsesSingleLineMode, lineLimit == 1 ? true : false)
    }
}

2      

Here the Updated Code

//
//  TokenField.swift
//  TokenField
//
//  Created by DJABHipHop on 1/18/24.
//

import Cocoa
import AppKit
import Combine
import SwiftUI
import Foundation

enum TokenFieldStyle {
    case plain, rounded, squard
}

extension NSTokenField {
    var tokenFieldStyle: TokenFieldStyle {
        get {
            // Determine the style based on the current properties of NSTokenField
            if self.bezelStyle == .roundedBezel {
                return .rounded
            } else if !self.isBezeled && !self.isBordered && self.isHighlighted &&
                      self.isAutomaticTextCompletionEnabled && self.isVerticalContentSizeConstraintActive &&
                      self.isHorizontalContentSizeConstraintActive {
                return .plain
            } else {
                // Add more conditions or return a default style
                return .rounded
            }
        }
        set {
            // Set NSTokenField properties based on the provided style
            switch newValue {
            case .rounded:
                self.bezelStyle = .roundedBezel
            case .plain:
                self.isBezeled = false
                self.isBordered = false
                self.focusRingType = .none
                self.drawsBackground = false
            // Add more cases if needed
            case .squard:
                self.bezelStyle = .squareBezel
            }
        }
    }
}

struct TokenFieldProperties {
    var drawsBackground: Bool = true
    var textColor: NSColor = .textColor
    var tokenStyle: NSTokenField.TokenStyle = .rounded
    var backgroundColor: NSColor = .textBackgroundColor
    var tokenFieldStyle: TokenFieldStyle = .rounded
    var allowsExpansionToolTips: Bool = false
    var allowsEditingTextAttributes: Bool = true
    var allowsCharacterPickerTouchBarItem: Bool = true
    var allowsDefaultTighteningForTruncation: Bool = true
    var alignment: NSTextAlignment = .natural
    var autoresizingMask: NSView.AutoresizingMask = .none
    var controlSize: NSControl.ControlSize = .large
    var contentType: NSTextContentType = .URL
    var lineBreakMode: NSLineBreakMode = .byClipping
    var lineBreakStrategy: NSParagraphStyle.LineBreakStrategy = .pushOut
    var maximumNumberOfLines: Int = 1
    var focusRingType: NSFocusRingType = .default
    var isHighlighted: Bool = false
    var usesSingleLineMode: Bool = false
    var isAutomaticTextCompletionEnabled: Bool = false
    var isVerticalContentSizeConstraintActive: Bool = false
    var isHorizontalContentSizeConstraintActive: Bool = false
}

private struct TokenFieldPropertiesKey: EnvironmentKey {
    static let defaultValue = TokenFieldProperties()
}

extension EnvironmentValues {
    var tokenFieldTextColor: NSColor {
        get { self[TokenFieldPropertiesKey.self].textColor }
        set { self[TokenFieldPropertiesKey.self].textColor = newValue }
    }

    var tokenFieldTokenStyle: NSTokenField.TokenStyle {
        get { self[TokenFieldPropertiesKey.self].tokenStyle }
        set { self[TokenFieldPropertiesKey.self].tokenStyle = newValue }
    }

    var tokenFieldBackgroundColor: NSColor {
        get { self[TokenFieldPropertiesKey.self].backgroundColor }
        set { self[TokenFieldPropertiesKey.self].backgroundColor = newValue }
    }

    var tokenFieldStyle: TokenFieldStyle {
        get { self[TokenFieldPropertiesKey.self].tokenFieldStyle }
        set { self[TokenFieldPropertiesKey.self].tokenFieldStyle = newValue }
    }

    var tokenFieldAllowsExpansionToolTips: Bool {
        get { self[TokenFieldPropertiesKey.self].allowsExpansionToolTips }
        set { self[TokenFieldPropertiesKey.self].allowsExpansionToolTips = newValue }
    }

    var tokenFieldAllowsEditingTextAttributes: Bool {
        get { self[TokenFieldPropertiesKey.self].allowsEditingTextAttributes }
        set { self[TokenFieldPropertiesKey.self].allowsEditingTextAttributes = newValue }
    }

    var tokenFieldAllowsCharacterPickerTouchBarItem: Bool {
        get { self[TokenFieldPropertiesKey.self].allowsCharacterPickerTouchBarItem }
        set { self[TokenFieldPropertiesKey.self].allowsCharacterPickerTouchBarItem = newValue }
    }

    var tokenFieldAllowsDefaultTighteningForTruncation: Bool {
        get { self[TokenFieldPropertiesKey.self].allowsDefaultTighteningForTruncation }
        set { self[TokenFieldPropertiesKey.self].allowsDefaultTighteningForTruncation = newValue }
    }

    var tokenFieldAlignment: NSTextAlignment {
        get { self[TokenFieldPropertiesKey.self].alignment }
        set { self[TokenFieldPropertiesKey.self].alignment = newValue }
    }

    var tokenFieldAutoresizingMask: NSView.AutoresizingMask {
        get { self[TokenFieldPropertiesKey.self].autoresizingMask }
        set { self[TokenFieldPropertiesKey.self].autoresizingMask = newValue }
    }

    var tokenFieldControlSize: NSControl.ControlSize {
        get { self[TokenFieldPropertiesKey.self].controlSize }
        set { self[TokenFieldPropertiesKey.self].controlSize = newValue }
    }

    var tokenFieldContentType: NSTextContentType {
        get { self[TokenFieldPropertiesKey.self].contentType }
        set { self[TokenFieldPropertiesKey.self].contentType = newValue }
    }

    var tokenFieldLineBreakMode: NSLineBreakMode {
        get { self[TokenFieldPropertiesKey.self].lineBreakMode }
        set { self[TokenFieldPropertiesKey.self].lineBreakMode = newValue }
    }

    var tokenFieldLineBreakStrategy: NSParagraphStyle.LineBreakStrategy {
        get { self[TokenFieldPropertiesKey.self].lineBreakStrategy }
        set { self[TokenFieldPropertiesKey.self].lineBreakStrategy = newValue }
    }

    var tokenFieldUsesSingleLineMode: Bool {
        get { self[TokenFieldPropertiesKey.self].usesSingleLineMode }
        set { self[TokenFieldPropertiesKey.self].usesSingleLineMode = newValue }
    }

    var tokenFieldMaximumNumberOfLines: Int {
        get { self[TokenFieldPropertiesKey.self].maximumNumberOfLines }
        set { self[TokenFieldPropertiesKey.self].maximumNumberOfLines = newValue }
    }

    var tokenFieldIsHighlighted: Bool {
        get { self[TokenFieldPropertiesKey.self].isHighlighted }
        set { self[TokenFieldPropertiesKey.self].isHighlighted = newValue }
    }

    var tokenFieldIsAutomaticTextCompletionEnabled: Bool {
        get { self[TokenFieldPropertiesKey.self].isAutomaticTextCompletionEnabled }
        set { self[TokenFieldPropertiesKey.self].isAutomaticTextCompletionEnabled = newValue }
    }

    var tokenFieldIsVerticalContentSizeConstraintActive: Bool {
        get { self[TokenFieldPropertiesKey.self].isVerticalContentSizeConstraintActive }
        set { self[TokenFieldPropertiesKey.self].isVerticalContentSizeConstraintActive = newValue }
    }

    var tokenFieldIsHorizontalContentSizeConstraintActive: Bool {
        get { self[TokenFieldPropertiesKey.self].isHorizontalContentSizeConstraintActive }
        set { self[TokenFieldPropertiesKey.self].isHorizontalContentSizeConstraintActive = newValue }
    }
}

extension View {
    func tokenFieldTextColor(_ textColor: NSColor) -> some View {
        environment(\.tokenFieldTextColor, textColor)
    }

    func tokenStyle(_ tokenStyle: NSTokenField.TokenStyle) -> some View {
        environment(\.tokenFieldTokenStyle, tokenStyle)
    }

    func tokenFieldBackgroundColor(_ backgroundColor: NSColor) -> some View {
        environment(\.tokenFieldBackgroundColor, backgroundColor)
    }

    func tokenFieldStyle(_ tokenFieldStyle: TokenFieldStyle) -> some View {
        environment(\.tokenFieldStyle, tokenFieldStyle)
    }

    func tokenFieldAllowsExpansionToolTips(_ allowsExpansionToolTips: Bool) -> some View {
        environment(\.tokenFieldAllowsExpansionToolTips, allowsExpansionToolTips)
    }

    func tokenFieldAllowsEditingTextAttributes(_ allowsEditingTextAttributes: Bool) -> some View {
        environment(\.tokenFieldAllowsEditingTextAttributes, allowsEditingTextAttributes)
    }

    func tokenFieldAllowsCharacterPickerTouchBarItem(_ allowsCharacterPickerTouchBarItem: Bool) -> some View {
        environment(\.tokenFieldAllowsCharacterPickerTouchBarItem, allowsCharacterPickerTouchBarItem)
    }

    func tokenFieldAllowsDefaultTighteningForTruncation(_ allowsDefaultTighteningForTruncation: Bool) -> some View {
        environment(\.tokenFieldAllowsDefaultTighteningForTruncation, allowsDefaultTighteningForTruncation)
    }

    func tokenAlignment(_ alignment: NSTextAlignment) -> some View {
        environment(\.tokenFieldAlignment, alignment)
    }

    func tokenFieldAutoresizingMask(_ autoresizingMask: NSView.AutoresizingMask) -> some View {
        environment(\.tokenFieldAutoresizingMask, autoresizingMask)
    }

    func tokenFieldControlSize(_ controlSize: NSControl.ControlSize) -> some View {
        environment(\.tokenFieldControlSize, controlSize)
    }

    func tokenContentType(_ contentType: NSTextContentType) -> some View {
        environment(\.tokenFieldContentType, contentType)
    }

    func tokenLineBreakMode(_ lineBreakMode: NSLineBreakMode) -> some View {
        environment(\.tokenFieldLineBreakMode, lineBreakMode)
    }

    func tokenLineBreakStrategy(_ lineBreakStrategy: NSParagraphStyle.LineBreakStrategy) -> some View {
        environment(\.tokenFieldLineBreakStrategy, lineBreakStrategy)
    }

    func tokenlineLimit(_ lineLimit: Int) -> some View {
        environment(\.tokenFieldMaximumNumberOfLines, lineLimit)
            .environment(\.tokenFieldUsesSingleLineMode, lineLimit == 1 ? true : false)
    }
}

struct TokenField: NSViewRepresentable {
    var placeholder: String = ""
    var placeholderAttributedString: NSAttributedString = NSAttributedString("")

    @Binding var tokens: [String]
    var preString: String
    var activity: PassthroughSubject<Date, Never>

    var tooltip: String = ""
    @Environment(\.tokenFieldTextColor) private var textColor: NSColor
    @Environment(\.tokenFieldTokenStyle) private var tokenStyle: NSTokenField.TokenStyle
    @Environment(\.tokenFieldBackgroundColor) private var backgroundColor: NSColor
    @Environment(\.tokenFieldStyle) private var tokenFieldStyle: TokenFieldStyle
    @Environment(\.tokenFieldAllowsExpansionToolTips) private var allowsExpansionToolTips: Bool
    @Environment(\.tokenFieldAllowsEditingTextAttributes) private var allowsEditingTextAttributes: Bool
    @Environment(\.tokenFieldAllowsCharacterPickerTouchBarItem) private var allowsCharacterPickerTouchBarItem: Bool
    @Environment(\.tokenFieldAllowsDefaultTighteningForTruncation) private var allowsDefaultTighteningForTruncation: Bool
    @Environment(\.tokenFieldAlignment) private var alignment: NSTextAlignment
    @Environment(\.tokenFieldAutoresizingMask) private var autoresizingMask: NSView.AutoresizingMask
    @Environment(\.controlSize) private var controlSize: ControlSize
    @Environment(\.tokenFieldContentType) private var contentType: NSTextContentType
    @Environment(\.tokenFieldLineBreakMode) private var lineBreakMode: NSLineBreakMode
    @Environment(\.tokenFieldLineBreakStrategy) private var lineBreakStrategy: NSParagraphStyle.LineBreakStrategy
    @Environment(\.tokenFieldMaximumNumberOfLines) private var maximumNumberOfLines: Int
    @Environment(\.tokenFieldUsesSingleLineMode) private var usesSingleLineMode: Bool
    @Environment(\.tokenFieldIsHighlighted) private var isHighlighted: Bool
    @Environment(\.tokenFieldIsAutomaticTextCompletionEnabled) private var isAutomaticTextCompletionEnabled: Bool
    @Environment(\.tokenFieldIsVerticalContentSizeConstraintActive) private var isVerticalContentSizeConstraintActive: Bool
    @Environment(\.tokenFieldIsHorizontalContentSizeConstraintActive) private var isHorizontalContentSizeConstraintActive: Bool

    init(_ placeholder: String, tokens: Binding<[String]>, preString: String, activity: PassthroughSubject<Date, Never>) {
        self.placeholder = placeholder
        self._tokens = tokens
        self.preString = preString
        self.activity = activity
    }

    init(_ placeholderAttributedString: NSAttributedString, tokens: Binding<[String]>, preString: String, activity: PassthroughSubject<Date, Never>) {
        self.placeholderAttributedString = placeholderAttributedString
        self._tokens = tokens
        self.preString = preString
        self.activity = activity
    }

    init(_ placeholder: String, tokens: Binding<[String]>, preString: String, tooltip: String, activity: PassthroughSubject<Date, Never>) {
        self.placeholder = placeholder
        self._tokens = tokens
        self.preString = preString
        self.tooltip = tooltip
        self.activity = activity
    }

    init(_ placeholderAttributedString: NSAttributedString, tokens: Binding<[String]>, preString: String, tooltip: String, activity: PassthroughSubject<Date, Never>) {
        self.placeholderAttributedString = placeholderAttributedString
        self._tokens = tokens
        self.preString = preString
        self.tooltip = tooltip
        self.activity = activity
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    class Coordinator: NSObject, NSTokenFieldDelegate {
        var parent: TokenField

        init(parent: TokenField) {
            self.parent = parent
        }

        func tokenField(_ tokenField: NSTokenField, completionsForSubstring substring: String, indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer<Int>?) -> [Any]? {
            // Implement token suggestions based on your logic
            return withAnimation(.default){[substring]}
        }

        func tokenField(_ tokenField: NSTokenField, shouldAdd tokens: [Any], at index: Int) -> [Any] {
            // Validate and modify tokens before adding them
            let modifiedTokens = tokens.map { token -> Any in
                if let stringToken = token as? String {
                    let modifiedString = stringToken.replacingOccurrences(of: parent.preString, with: "")
                    return withAnimation(.default){
                         parent.preString.appending(modifiedString).uppercased()
                    }
                }
                return withAnimation(.default){token}
            }
            return withAnimation(.default){modifiedTokens}
        }

        func tokenField(_ tokenField: NSTokenField, displayStringForRepresentedObject representedObject: Any) -> String? {
            return tokenField.objectValue as? String
        }

        func tokenField(_ tokenField: NSTokenField, styleForRepresentedObject representedObject: Any ) -> NSTokenField.TokenStyle {
            if (representedObject as? NSString)?.range(of: parent.preString).location == 0 {
                return parent.tokenStyle
            } else {
                return .none
            }
        }

        func controlTextDidEndEditing(_ obj: Notification) {
            guard let tokenField = obj.object as? NSTokenField else {
                return
            }

            // Assuming tokens property is of type [String]
            if let newTokens = tokenField.objectValue as? String {
                self.parent.tokens.append(newTokens)
                print(self.parent.tokens)
            }

            self.parent.activity.send(Date())
        }

        func control(_ control: NSControl, textView fieldEditor: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
            var retval = false

            if commandSelector == #selector(NSStandardKeyBindingResponding.insertNewline(_:)) {

                retval = true // causes Apple to NOT fire the default enter action

                self.parent.tokens.append(fieldEditor.string)
                self.parent.activity.send(Date())

                retval = false
            }
            return retval
        }
    }

    func makeNSView(context: Context) -> NSTokenField {
        let tokenField = NSTokenField()
        tokenField.delegate = context.coordinator
        if placeholder.isEmpty {
            tokenField.placeholderString = placeholder
        }
        if placeholderAttributedString.string.isEmpty {
            tokenField.placeholderAttributedString = placeholderAttributedString
        }
        tokenField.toolTip = tooltip
        tokenField.alignment = alignment
        tokenField.autoresizingMask = autoresizingMask
        tokenField.controlSize = getNSControlSize()
        tokenField.contentType = contentType
        tokenField.lineBreakMode = lineBreakMode
        tokenField.lineBreakStrategy = lineBreakStrategy
        tokenField.usesSingleLineMode = usesSingleLineMode
        tokenField.maximumNumberOfLines = maximumNumberOfLines
        tokenField.isContinuous = false
        tokenField.autoresizesSubviews = false
        tokenField.completionDelay = 0
        tokenField.allowedTouchTypes = .indirect
        tokenField.textColor = textColor
        tokenField.tokenStyle = tokenStyle
        tokenField.backgroundColor = backgroundColor
        tokenField.tokenFieldStyle = tokenFieldStyle
        tokenField.allowsExpansionToolTips = allowsExpansionToolTips
        tokenField.allowsEditingTextAttributes = allowsEditingTextAttributes
        tokenField.allowsCharacterPickerTouchBarItem = allowsCharacterPickerTouchBarItem
        tokenField.allowsDefaultTighteningForTruncation = allowsDefaultTighteningForTruncation
        return tokenField
    }

    func updateNSView(_ nsView: NSTokenField, context: Context) {
        nsView.objectValue = tokens
    }

    func textView(_ textView: NSTextView, dragOperationForDraggingInfo dragInfo: NSDraggingInfo, type: NSPasteboard.PasteboardType) -> NSDragOperation {

        // if option is pressed, always return .copy
        if NSApp.currentEvent?.modifierFlags.intersection(.deviceIndependentFlagsMask) == .option {
            return .copy
        }

        // if the source and destination are the same, we want default NSTokenField behavior
        if let source = dragInfo.draggingSource as? NSTextView, textView == source {
            return .generic
        }

        // default to .move
        return .move
    }

    private func getNSControlSize() -> NSControl.ControlSize {
        switch controlSize {
        case .mini:
            return .mini
        case .small:
            return .small
        case .large:
            return .large
        default:
            return .regular
        }
    }
}

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.