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