Created
June 16, 2021 02:36
-
-
Save yoxisem544/37530ba0c824d5f205f64fd51ca3cf31 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
// | |
// 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