Skip to content

Commit

Permalink
Merge branch 'textcoord'
Browse files Browse the repository at this point in the history
  • Loading branch information
cbpowell committed Aug 7, 2021
2 parents 93a86ff + 494d004 commit 1d788f5
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 6 deletions.
2 changes: 1 addition & 1 deletion MarqueeLabel/MLHeader.xib
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<constraints>
<constraint firstAttribute="height" constant="600" id="Ons-nq-GwC"/>
</constraints>
<blurEffect style="systemThinMaterial"/>
<blurEffect style="regular"/>
</visualEffectView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xk6-CO-BR2">
<rect key="frame" x="0.0" y="83" width="600" height="1"/>
Expand Down
1 change: 1 addition & 0 deletions MarqueeLabel/MarqueeLabel.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@
</navigationItem>
<connections>
<outlet property="demoLabel" destination="CG3-Ca-wbs" id="PH8-47-4Gk"/>
<outlet property="labelLabel" destination="SrV-q0-UjC" id="BOg-sx-yye"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Fr6-R5-Jcq" userLabel="First Responder" sceneMemberID="firstResponder"/>
Expand Down
67 changes: 66 additions & 1 deletion MarqueeLabel/MarqueeLabelDemoPushController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,30 @@ import UIKit

class MarqueeLabelDemoPushController: UIViewController {
@IBOutlet weak var demoLabel: MarqueeLabel!
@IBOutlet weak var labelLabel: UILabel!

override func viewDidLoad() {
super.viewDidLoad()

demoLabel.type = .continuous
demoLabel.type = MarqueeLabel.MarqueeType.allCases.randomElement() ?? .continuous
// Set label label text
labelLabel.text = { () -> String in
switch demoLabel.type {
case .continuous:
return "Continuous scrolling"
case .continuousReverse:
return "Continuous Reverse scrolling"
case .left:
return "Left-Only scrolling"
case .leftRight:
return "Left-Right scrolling"
case .right:
return "Right-Only scrolling"
case .rightLeft:
return "Right-Left scrolling"
}
}()

demoLabel.speed = .duration(15)
demoLabel.animationCurve = .easeInOut
demoLabel.fadeLength = 10.0
Expand All @@ -28,5 +47,51 @@ class MarqueeLabelDemoPushController: UIViewController {
"Be a yardstick of quality. Some people aren't used to an environment where excellence is expected."]

demoLabel.text = strings[Int(arc4random_uniform(UInt32(strings.count)))]

let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.didTap(_:)))
tapRecognizer.numberOfTapsRequired = 1
tapRecognizer.numberOfTouchesRequired = 1
demoLabel.addGestureRecognizer(tapRecognizer)
demoLabel.isUserInteractionEnabled = true
}

@objc func didTap(_ recognizer: UIGestureRecognizer) {
let label = recognizer.view as! MarqueeLabel
if recognizer.state == .ended {
label.isPaused ? label.unpauseLabel() : label.pauseLabel()
// Convert tap points
let tapPoint = recognizer.location(in: label)
print("Frame coord: \(tapPoint)")
guard let textPoint = label.textCoordinateForFramePoint(tapPoint) else {
return
}
print(" Text coord: \(textPoint)")

// Thanks to Toomas Vahter for the basis of the below
// https://augmentedcode.io/2020/12/20/opening-hyperlinks-in-uilabel-on-ios/
// Create layout manager
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: label.textLayoutSize())
textContainer.lineFragmentPadding = 0
// Create text storage
guard let text = label.text else { return }
let textStorage = NSTextStorage(string: "")
textStorage.setAttributedString(label.attributedText!)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineBreakMode = label.lineBreakMode
textContainer.size = label.textRect(forBounds: CGRect(origin: .zero, size:label.textLayoutSize()), limitedToNumberOfLines: label.numberOfLines).size

let characterIndex = layoutManager.characterIndex(for: textPoint, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard characterIndex >= 0, characterIndex != NSNotFound else {
print("No character at point found!")
return
}

let stringIndex = text.index(text.startIndex, offsetBy: characterIndex)
// Print character under touch point
print("Character under touch point: \(text[stringIndex])")
}
}

}
3 changes: 2 additions & 1 deletion MarqueeLabel/MarqueeLabelDemoViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,10 @@ class MarqueeLabelDemoViewController : UIViewController {

// Continuous, with tap to pause
demoLabel5.tag = 501
demoLabel5.type = .continuous
demoLabel5.type = .continuousReverse
demoLabel5.speed = .duration(10)
demoLabel5.fadeLength = 10.0
demoLabel5.leadingBuffer = 40.0
demoLabel5.trailingBuffer = 30.0
demoLabel5.text = "This text is long, and can be paused with a tap - handled via a UIGestureRecognizer!"

Expand Down
73 changes: 70 additions & 3 deletions Sources/MarqueeLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate {
- Continuous: Continuously scrolls left (with a pause at the original position if animationDelay is set).
- ContinuousReverse: Continuously scrolls right (with a pause at the original position if animationDelay is set).
*/
public enum MarqueeType {
public enum MarqueeType: CaseIterable {
case left
case leftRight
case right
Expand Down Expand Up @@ -684,7 +684,7 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate {
sublabel.minimumScaleFactor = 0.0

// Spacing between primary and second sublabel must be at least equal to leadingBuffer, and at least equal to the fadeLength
let minTrailing = max(max(leadingBuffer, trailingBuffer), fadeLength)
let minTrailing = minimumTrailingDistance

// Determine positions and generate scroll steps
let sequence: [MarqueeStep]
Expand Down Expand Up @@ -783,11 +783,24 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate {
}

override open func sizeThatFits(_ size: CGSize) -> CGSize {
return sizeThatFits(size, withBuffers: true)
}

open func sizeThatFits(_ size: CGSize, withBuffers: Bool) -> CGSize {
var fitSize = sublabel.sizeThatFits(size)
fitSize.width += leadingBuffer
if withBuffers {
fitSize.width += leadingBuffer
}
return fitSize
}

/**
Returns the unconstrained size of the specified label text (for a single line).
*/
open func textLayoutSize() -> CGSize {
return sublabel.desiredSize()
}

//
// MARK: - Animation Handling
//
Expand Down Expand Up @@ -1263,6 +1276,11 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate {
}
}

private var minimumTrailingDistance: CGFloat {
// Spacing between primary and second sublabel must be at least equal to leadingBuffer, and at least equal to the fadeLengt
return max(max(leadingBuffer, trailingBuffer), fadeLength)
}

fileprivate enum MarqueeKeys: String {
case Restart = "MLViewControllerRestart"
case Labelize = "MLShouldLabelize"
Expand Down Expand Up @@ -1429,6 +1447,49 @@ open class MarqueeLabel: UILabel, CAAnimationDelegate {
}
}

open func textCoordinateForFramePoint(_ point:CGPoint) -> CGPoint? {
// Check for presentation layer, if none return input point
guard let presentationLayer = sublabel.layer.presentation() else { return point }
// Convert point from MarqueeLabel main layer to sublabel's presentationLayer
let presentationPoint = presentationLayer.convert(point, from: self.layer)
// Check if point overlaps into 2nd instance of a continuous type label
let textPoint: CGPoint?
let presentationX = presentationPoint.x
let labelWidth = sublabel.frame.size.width

var containers: [Range<CGFloat>] = []
switch type {
case .continuous:
// First label frame range
let firstLabel = 0.0 ..< sublabel.frame.size.width
// Range from end of first label to the minimum trailining distance (i.e. the separator)
let minTrailing = firstLabel.rangeForExtension(minimumTrailingDistance)
// Range of second label instance, from end of separator to length
let secondLabel = minTrailing.rangeForExtension(labelWidth)
// Add valid ranges to array to check
containers += [firstLabel, secondLabel]
case .continuousReverse:
// First label frame range
let firstLabel = 0.0 ..< sublabel.frame.size.width
// Range of second label instance, from end of separator to length
let secondLabel = -sublabel.frame.size.width ..< -minimumTrailingDistance
// Add valid ranges to array to check
containers += [firstLabel, secondLabel]
case .left, .leftRight, .right, .rightLeft:
// Only label frame range
let firstLabel = 0.0 ..< sublabel.frame.size.width
containers.append(firstLabel)
}

// Determine which range contains the point, or return nil if in a buffer/margin area
guard let container = containers.filter({ (rng) -> Bool in
return rng.contains(presentationX)
}).first else { return nil }

textPoint = CGPoint(x: (presentationX - container.lowerBound), y: presentationPoint.y)
return textPoint
}

/**
Called when the label animation is about to begin.
Expand Down Expand Up @@ -1826,6 +1887,12 @@ fileprivate extension UILabel {
}
}

fileprivate extension Range where Bound == CGFloat {
func rangeForExtension(_ ext: CGFloat) -> Range {
return self.upperBound..<(self.upperBound + ext)
}
}

fileprivate extension UIResponder {
// Thanks to Phil M
// http://stackoverflow.com/questions/1340434/get-to-uiviewcontroller-from-uiview-on-iphone
Expand Down

0 comments on commit 1d788f5

Please sign in to comment.