mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			141 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			141 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Swift
		
	
import UIKit
 | 
						|
 | 
						|
// Requirements:
 | 
						|
// • Links should show up properly and be tappable.
 | 
						|
// • Text should * not * be selectable.
 | 
						|
// • The long press interaction that shows the context menu should still work.
 | 
						|
 | 
						|
// See https://stackoverflow.com/questions/47983838/how-can-you-change-the-color-of-links-in-a-uilabel
 | 
						|
 | 
						|
public protocol TappableLabelDelegate: AnyObject {
 | 
						|
    func tapableLabel(_ label: TappableLabel, didTapUrl url: String, atRange range: NSRange)
 | 
						|
}
 | 
						|
 | 
						|
public class TappableLabel: UILabel {
 | 
						|
    public private(set) var links: [String: NSRange] = [:]
 | 
						|
    private lazy var highlightedMentionBackgroundView: HighlightMentionBackgroundView = HighlightMentionBackgroundView(targetLabel: self)
 | 
						|
    private(set) var layoutManager = NSLayoutManager()
 | 
						|
    private(set) var textContainer = NSTextContainer(size: CGSize.zero)
 | 
						|
    private(set) var textStorage = NSTextStorage() {
 | 
						|
        didSet {
 | 
						|
            textStorage.addLayoutManager(layoutManager)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public weak var delegate: TappableLabelDelegate?
 | 
						|
 | 
						|
    public override var attributedText: NSAttributedString? {
 | 
						|
        didSet {
 | 
						|
            guard let attributedText: NSAttributedString = attributedText else {
 | 
						|
                textStorage = NSTextStorage()
 | 
						|
                links = [:]
 | 
						|
                return
 | 
						|
            }
 | 
						|
 | 
						|
            textStorage = NSTextStorage(attributedString: attributedText)
 | 
						|
            findLinksAndRange(attributeString: attributedText)
 | 
						|
            highlightedMentionBackgroundView.maxPadding = highlightedMentionBackgroundView
 | 
						|
                .calculateMaxPadding(for: attributedText)
 | 
						|
            highlightedMentionBackgroundView.frame = self.bounds.insetBy(
 | 
						|
                dx: -highlightedMentionBackgroundView.maxPadding,
 | 
						|
                dy: -highlightedMentionBackgroundView.maxPadding
 | 
						|
            )
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public override var lineBreakMode: NSLineBreakMode {
 | 
						|
        didSet {
 | 
						|
            textContainer.lineBreakMode = lineBreakMode
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public override var numberOfLines: Int {
 | 
						|
        didSet {
 | 
						|
            textContainer.maximumNumberOfLines = numberOfLines
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    public var containsLinks: Bool {
 | 
						|
        return !links.isEmpty
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - Initialization
 | 
						|
 | 
						|
    public override init(frame: CGRect) {
 | 
						|
        super.init(frame: frame)
 | 
						|
        setup()
 | 
						|
    }
 | 
						|
 | 
						|
    public required init?(coder aDecoder: NSCoder) {
 | 
						|
        super.init(coder: aDecoder)
 | 
						|
        setup()
 | 
						|
    }
 | 
						|
 | 
						|
    private func setup() {
 | 
						|
        isUserInteractionEnabled = true
 | 
						|
        layoutManager.addTextContainer(textContainer)
 | 
						|
        textContainer.lineFragmentPadding = 0
 | 
						|
        textContainer.lineBreakMode = lineBreakMode
 | 
						|
        textContainer.maximumNumberOfLines  = numberOfLines
 | 
						|
        numberOfLines = 0
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - Layout
 | 
						|
    
 | 
						|
    public override func didMoveToSuperview() {
 | 
						|
        super.didMoveToSuperview()
 | 
						|
 | 
						|
        // Note: Because we want the 'highlight' content to appear behind the label we need
 | 
						|
        // to add the 'highlightedMentionBackgroundView' below it in the view hierarchy
 | 
						|
        //
 | 
						|
        // In order to try and avoid adding even more complexity to UI components which use
 | 
						|
        // this 'TappableLabel' we are going some view hierarchy manipulation and forcing
 | 
						|
        // these elements to maintain the same superview
 | 
						|
        highlightedMentionBackgroundView.removeFromSuperview()
 | 
						|
        superview?.insertSubview(highlightedMentionBackgroundView, belowSubview: self)
 | 
						|
    }
 | 
						|
    
 | 
						|
    public override func layoutSubviews() {
 | 
						|
        super.layoutSubviews()
 | 
						|
        
 | 
						|
        textContainer.size = bounds.size
 | 
						|
        highlightedMentionBackgroundView.frame = self.frame.insetBy(
 | 
						|
            dx: -highlightedMentionBackgroundView.maxPadding,
 | 
						|
            dy: -highlightedMentionBackgroundView.maxPadding
 | 
						|
        )
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - Functions
 | 
						|
 | 
						|
    private func findLinksAndRange(attributeString: NSAttributedString) {
 | 
						|
        links = [:]
 | 
						|
        let enumerationBlock: (Any?, NSRange, UnsafeMutablePointer<ObjCBool>) -> Void = { [weak self] value, range, isStop in
 | 
						|
            guard let strongSelf = self else { return }
 | 
						|
            if let value = value {
 | 
						|
                let stringValue = "\(value)"
 | 
						|
                strongSelf.links[stringValue] = range
 | 
						|
            }
 | 
						|
        }
 | 
						|
        attributeString.enumerateAttribute(.link, in: NSRange(0..<attributeString.length), options: [.longestEffectiveRangeNotRequired], using: enumerationBlock)
 | 
						|
        attributeString.enumerateAttribute(.attachment, in: NSRange(0..<attributeString.length), options: [.longestEffectiveRangeNotRequired], using: enumerationBlock)
 | 
						|
    }
 | 
						|
 | 
						|
    public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
 | 
						|
        guard let locationOfTouch = touches.first?.location(in: self) else {
 | 
						|
            return
 | 
						|
        }
 | 
						|
        
 | 
						|
        handleTouch(at: locationOfTouch)
 | 
						|
    }
 | 
						|
    
 | 
						|
    public func handleTouch(at point: CGPoint) {
 | 
						|
        textContainer.size = bounds.size
 | 
						|
        
 | 
						|
        let indexOfCharacter = layoutManager.glyphIndex(for: point, in: textContainer)
 | 
						|
        for (urlString, range) in links where NSLocationInRange(indexOfCharacter, range) {
 | 
						|
            delegate?.tapableLabel(self, didTapUrl: urlString, atRange: range)
 | 
						|
            return
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |