Created
March 15, 2024 12:27
-
-
Save zats/da8f1ba3c800ed1b05dad18b8ac02057 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
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