Skip to content

Instantly share code, notes, and snippets.

@yoxisem544
Created June 16, 2021 02:36
Show Gist options
  • Save yoxisem544/37530ba0c824d5f205f64fd51ca3cf31 to your computer and use it in GitHub Desktop.
Save yoxisem544/37530ba0c824d5f205f64fd51ca3cf31 to your computer and use it in GitHub Desktop.
//
// InteractiveLabel.swift
// Hahow2C
//
// Created by David on 2020/7/23.
// Copyright Β© 2020 Hahow. All rights reserved.
//
import UIKit
import Siatke
// MARK: - Data modal of InteactiveTextView
public typealias TextComponentAction = (() -> Void)
public enum TextComponent {
case text(String)
case action(text: String, action: TextComponentAction)
}
fileprivate struct _TextComponent {
let text: String
let action: TextComponentAction?
fileprivate init(text: String, action: TextComponentAction? = nil) {
self.text = text
self.action = action
}
}
// MARK: - InteractiveTextView
final public class InteractiveTextView: UITextView {
// MARK: - πŸ“Œ Constants
// MARK: - πŸ”Ά Properties
private var actions: [(NSRange, TextComponentAction)] = []
/// Components to display on label
public var components: [TextComponent] = [] {
didSet {
renderContent(with: components)
}
}
/// Determine if clickable range should have a underline indicates this is a clickable range.
/// Default to true
public var showUnderlineStyleOnActionRange: Bool = true {
didSet { _reRenderContents() }
}
// MARK: - 🎨 Style
public struct BaseStyle {
public var fontStyle: UIFont.SteakerStyle = .body2
public var textColor: UIColor = .hahow(.textPrimary)
public var actionLabelColor: UIColor = .hahow(.textPrimary)
}
public var baseStyle = BaseStyle() {
// re-render if style have changed
didSet { _reRenderContents() }
}
public override var textAlignment: NSTextAlignment {
didSet { _reRenderContents() }
}
public var lineBreakMode: NSLineBreakMode = .byWordWrapping {
didSet {
_reRenderContents()
}
}
// MARK: - 🧩 Subviews
// MARK: - πŸ‘† Actions
// MARK: - πŸ”¨ Initialization
public convenience init() {
self.init(frame: .zero)
}
public override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
setupUI()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - πŸ— UI
private func setupUI() {
isScrollEnabled = false
textContainer.lineBreakMode = lineBreakMode
adjustsFontForContentSizeCategory = true
isUserInteractionEnabled = true
backgroundColor = .clear
textContainerInset = .zero
textContainer.lineFragmentPadding = .zero
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(labelTapped(_:))))
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// TODO: re-render attributed text here
renderContent(with: components)
}
// MARK: - 🚌 Public Methods
private func renderContent(with components: [TextComponent]) {
let string = NSMutableAttributedString()
var range = NSRange(location: 0, length: 0)
for item in components.mapStruct() {
var attr: [NSAttributedString.Key: Any] = .steakerAttributes(
for: baseStyle.fontStyle,
textColor: baseStyle.textColor,
textAlignment: textAlignment,
lineBreakMode: lineBreakMode
)
range.location = range.location + range.length
range.length = item.text.count
if let action = item.action {
attr[.foregroundColor] = baseStyle.actionLabelColor
if showUnderlineStyleOnActionRange {
attr[.underlineStyle] = NSUnderlineStyle.single.rawValue
}
actions.append((range, action))
}
let attributedString = NSAttributedString(string: item.text, attributes: attr)
string.append(attributedString)
}
attributedText = string
}
private func _reRenderContents() {
renderContent(with: components)
}
// MARK: - πŸ”’ Private Methods
@objc private func labelTapped(_ gesture: UITapGestureRecognizer) {
guard let view = gesture.view as? UITextView else { return }
var location = gesture.location(in: view)
location.x -= view.textContainerInset.left
location.y -= view.textContainerInset.top
let indexOfCharacter = view.layoutManager.characterIndex(for: location, in: view.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
for (range, action) in actions {
if NSLocationInRange(indexOfCharacter, range) {
action()
break
}
}
}
}
private extension TextComponent {
func mapStruct() -> _TextComponent {
switch self {
case .text(let text):
return _TextComponent(text: text)
case let .action(text: text, action: action):
return _TextComponent(text: text, action: action)
}
}
}
private extension Array where Element == TextComponent {
func mapStruct() -> [_TextComponent] {
map({ $0.mapStruct() })
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment