I am creating an app with a timestamp feature that displays times next to paragraphs in a UITextView
.
I have worked out to create singular timestamps but I am unsure how to save multiple ones. Basically they’re CGRect
and button objects that would need to be updated on command. The 2 options I can think of are:
1.Save Timestamp button + Character count ID
- This might make it hard to redraw all
CGrect
's on open as I would have to calculate based on Character count ID and if a paragraph moves I would have to update every single object...(just worried with a-lot of paragraphs how long this would take).
2.Save Timestamp button + CGRect
- I can use the
CGRect
to draw every object without re-calculating on open, - When text is edited I would just need a way to check if it's a new paragraph or not and if it is then create a timestamp, if not then leave it.
EDIT: I Just realised this also wouldn't work as when I update the views I would still need someway to hold the data in the buttons as the array is replaced on every function call.
I feel like the second option is wrong as it uses the object to make the UI but am not sure how the first option would work! haha. Can anyone help me with some suggestions on how I would accomplish this function? Am I just understanding this wrong lol
Code used to create paragraph components and timeStamps now:
var paragraphMarkers: [UIView] = [UIView]()
@objc func updateParagraphMarkers() -> Void {
// clear previous paragraph marker views
paragraphMarkers.forEach {
$0.removeFromSuperview()
}
// reset paraMarkers array
paragraphMarkers.removeAll()
// probably not needed, but this will make sure the the text container has updated
textView.layoutManager.ensureLayout(for: textView.textContainer)
// make sure we have some text
guard let str = textView.text else { return }
// get the full range
let textRange = str.startIndex..<str.endIndex
// we want to enumerate by paragraphs
let opts:NSString.EnumerationOptions = .byParagraphs
var i = 0
str.enumerateSubstrings(in: textRange, options: opts) {
(substring, substringRange, enclosingRange, _) in
// get the bounding rect for the sub-rects in each paragraph
if let boundRect = self.textView.boundingFrame(ofTextRange: enclosingRange) {
// create a UIView
let paragraphView = UIView()
// give it a background color from our array of colors
paragraphView.backgroundColor = .red
// init the frame
paragraphView.frame = boundRect
// position views
paragraphView.frame.origin.y += self.textView.frame.origin.y
paragraphView.frame.origin.x = self.timeStampView.frame.origin.x
paragraphView.frame.size.width = self.timeStampView.frame.width
// add it to the view
let timeText = self.calculateFirstLabelFromNSTimeInterval(currentTime: self.audioPlayer.currentTime)
let currentTimeText = "\(timeText.minute):\(timeText.second)"
let timeButton = UIButton(frame: CGRect(x: 0, y: 0, width: 30, height: 20))
timeButton.setTitle(currentTimeText, for: .normal)
timeButton.setTitleColor(UIColor.white, for: .normal)
timeButton.titleLabel?.font = UIFont.systemFont(ofSize:10, weight: UIFont.Weight.medium)
timeButton.backgroundColor = UIColor.gray
timeButton.autoresizingMask = [.flexibleRightMargin, .flexibleBottomMargin]
self.timeStampView.addSubview(paragraphView)
// save a reference to this UIView in our array of markers
self.paragraphMarkers.append(paragraphView)
}
}
extension UITextView {
func boundingFrame(ofTextRange range: Range<String.Index>?) -> CGRect?
{
guard let range = range else { return nil }
let length = range.upperBound.utf16Offset(in: self.text)-range.lowerBound.utf16Offset(in: self.text)
guard
let start = position(from: beginningOfDocument, offset: range.lowerBound.utf16Offset(in: self.text)),
let end = position(from: start, offset: length),
let txtRange = textRange(from: start, to: end)
else { return nil }
// we now have a UITextRange, so get the selection rects for that range
let rects = selectionRects(for: txtRange)
// init our return rect
var returnRect = CGRect.zero
// for each selection rectangle
for thisSelRect in rects {
// if it's the first one, just set the return rect
if thisSelRect == rects.first {
returnRect = thisSelRect.rect
} else {
// ignore selection rects with a width of Zero
if thisSelRect.rect.size.width > 0 {
// we only care about the top (the minimum origin.y) and the
// sum of the heights
returnRect.origin.y = min(returnRect.origin.y, thisSelRect.rect.origin.y)
returnRect.size.height += thisSelRect.rect.size.height
}
}
}
return returnRect
}
}