Skip to content

Instantly share code, notes, and snippets.

@zats
Created March 15, 2024 12:27
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save zats/da8f1ba3c800ed1b05dad18b8ac02057 to your computer and use it in GitHub Desktop.
Save zats/da8f1ba3c800ed1b05dad18b8ac02057 to your computer and use it in GitHub Desktop.
//
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
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment