Skip to content

Instantly share code, notes, and snippets.

@zats
Created March 15, 2024 12:27
Show Gist options
  • Select an option

  • Save zats/da8f1ba3c800ed1b05dad18b8ac02057 to your computer and use it in GitHub Desktop.

Select an option

Save zats/da8f1ba3c800ed1b05dad18b8ac02057 to your computer and use it in GitHub Desktop.

Revisions

  1. zats created this gist Mar 15, 2024.
    143 changes: 143 additions & 0 deletions TokenTextField.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,143 @@
    //

    import UIKit
    import UniformTypeIdentifiers

    protocol TokenTextFieldDelegate: AnyObject {
    func tokenizedTextField(_ sender: TokenTextField, didTapTokenView: UIView)
    }

    final class TokenTextField: UITextView {
    private static let tokenFileType = UTType.plainText.identifier

    enum Model {
    case token(String)
    case text(String)
    }

    var data: [Model] = [] {
    didSet {
    let attributedText = attributedText(for: data)
    self.attributedText = attributedText
    }
    }

    var tokenDelegate: TokenTextFieldDelegate?

    override init(frame: CGRect, textContainer: NSTextContainer?) {
    super.init(frame: frame, textContainer: textContainer)
    NSTextAttachment.registerViewProviderClass(TokenAttachmentViewProvider.self, forFileType: Self.tokenFileType)
    }

    required init?(coder: NSCoder) { fatalError() }

    private func attributedText(for data: [Model]) -> NSAttributedString {
    let result = NSMutableAttributedString()
    data.forEach { data in
    switch data {
    case .token(let value):
    let attachment = NSTextAttachment(data: value.data(using: .utf8), ofType: Self.tokenFileType)
    let substring = NSMutableAttributedString(attachment: attachment)
    result.append(substring)
    case .text(let value):
    result.append(NSAttributedString(string: value, attributes: [
    .font: UIFont.preferredFont(forTextStyle: .body),
    .foregroundColor: UIColor.label
    ]))
    }
    }
    return result
    }
    }


    #Preview {
    final class MyDelegate: NSObject, TokenTextFieldDelegate, UITextViewDelegate {
    func tokenizedTextField(_ sender: TokenTextField, didTapTokenView token: UIView) {
    token.layer.add(shake(), forKey: "shake")
    }

    private func shake() -> CAAnimation {
    let animation = CAKeyframeAnimation(keyPath: "position.x")
    animation.values = [0, 10, -10, 10, 0]
    animation.keyTimes = [0, 0.1, 0.3, 0.5, 0.7, 0.9, 1]
    animation.duration = 0.6
    animation.isAdditive = true
    animation.timingFunctions = [
    CAMediaTimingFunction(name: .linear),
    CAMediaTimingFunction(name: .easeInEaseOut),
    CAMediaTimingFunction(name: .easeInEaseOut),
    CAMediaTimingFunction(name: .easeInEaseOut)
    ]

    return animation
    }
    }

    let textView = TokenTextField(frame: CGRect(x: 0, y: 0, width: 250, height: 120))
    textView.data = [
    .text("And it's a "),
    .token("good"),
    .text(" day for shinin' your shoes\n"),
    .text("And it's a good day for losin' the blues\n"),
    .text("Everything to "),
    .token("gain"),
    .text(" and nothing to lose\n"),
    .text("A good day from morning 'til night"),
    ]
    let delegate = MyDelegate()
    textView.tokenDelegate = delegate
    textView.delegate = delegate
    textView.layer.cornerRadius = 8

    let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial))
    blurView.frame = textView.bounds
    blurView.alpha = 0.5
    textView.addSubview(blurView)
    textView.sendSubviewToBack(blurView)

    let container = UIView(frame: UIScreen.main.bounds)
    container.addSubview(textView)
    textView.center = container.center
    return container
    }

    class TokenAttachmentViewProvider: NSTextAttachmentViewProvider {
    override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: any NSTextLocation) {
    super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location)
    guard let data = textAttachment.contents, let token = String(bytes: data, encoding: .utf8) else {
    return
    }
    tracksTextAttachmentViewBounds = true

    let button = UIButton(type: .custom)
    button.setTitle(token, for: .normal)
    button.configuration = .borderedProminent()
    button.addAction(UIAction(handler: { [weak self, weak button] _ in
    guard let self,
    let button,
    let textView = textView(for: parentView) else { return }
    textView.tokenDelegate?.tokenizedTextField(textView, didTapTokenView: button)
    }), for: .touchUpInside)
    button.sizeToFit()
    let container = UIView(frame: button.bounds)
    button.frame.origin.y += 10
    container.addSubview(button)
    self.view = container
    }

    override func attachmentBounds(for attributes: [NSAttributedString.Key : Any], location: any NSTextLocation, textContainer: NSTextContainer?, proposedLineFragment: CGRect, position: CGPoint) -> CGRect {
    return self.view?.bounds ?? .zero
    }

    private func textView(for view: UIView?) -> TokenTextField? {
    var current: UIView? = view
    while current != nil {
    if let textView = current as? TokenTextField {
    return textView
    }
    current = current?.superview
    }
    return nil
    }
    }