Force page breaks and text containers using NSLayoutManager - swift

I'm trying to implement pagination using NSLayoutManager and multiple text containers. Creating NSTextView/UITextView instances works as expected, and the text flows from text view to another.
However, I'd like to force a page break, ie. determine myself where to break onto the next container. I'm working with parsed content, so I can't insert any additional ASCII control characters into the text.
I tried subclassing NSLayoutManager and overriding textContainer(glyphIndex:, effectiveRange:), and here's a very brute-force example:
override func textContainer(forGlyphAt glyphIndex: Int, effectiveRange effectiveGlyphRange: NSRangePointer?) -> NSTextContainer? {
if glyphIndex > 100 {
return self.textContainers[1]
} else {
return super.textContainer(forGlyphAt: glyphIndex, effectiveRange: effectiveGlyphRange)
}
}
I'd expect this to move any glyphs after 100th index onto the second container, but the results are weird:
I suppose I'd have to subclass NSTextContainer and tell the layout manager that it's already full of text. It has a method called lineFragmentRect(forProposedRect:at:writingDirection:remaining:) but the documentation is very sparse, and I can't find any working examples.
Existing documentation around displaying text is very outdated, so any ideas or hints are welcome. I'm very confused about this, but still hopeful there is a simple way of telling the layout manager where to cut off content in each container.
One possible solution
NSTextContainer.exclusionPaths could be used to rule out the rest of the possible space in containers.
let glyphIndex = layoutManager.glyphIndexForCharacter(at: 100)
var rect = layoutManager.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil)
// Get the line range and used rect
var lineRange = NSMakeRange(NSNotFound, 0)
var usedRect = layoutManager.lineFragmentUsedRect(forGlyphAt: gi, effectiveRange: NSRangePointer(&lineRange))
// Calculate the remainder of the line
let remainder = NSRange(location: glyphIndex, length: NSMaxRange(lineRange) - glyphIndex)
var rectCount:Int = 0
var breakRects = layoutManager.rectArray(forGlyphRange: remainder, withinSelectedGlyphRange: remainder, in: textContainer1, rectCount: UnsafeMutablePointer(&rectCount))
// Create the rect for the remainder of the line
var lineRect = breakRect!.pointee
lineRect.size.width = textContainer1.size.width - lineRect.origin.x
// Then create a rect to cover up the rest
var coverRest = NSMakeRect(0, lineRect.origin.y + lineRect.height, textContainer1.size.width, CGFloat.greatestFiniteMagnitude)
// Add exclusion paths
textContainer1.exclusionPaths = [NSBezierPath(rect: lineRect), NSBezierPath(rect: coverRest)]
This results in expected behavior:
This requires a ton of calculations, and text containers with exclusion paths are noticeably slower. The app could potentially have hundreds of text views, which makes this quite inefficient.

Related

NSTextView's jumps to the corresponding row via string

I wanted to complete a novel reader, but it wasn't ideal to jump to the corresponding location based on the catalog title.
I use the following method to jump, but sometimes I don't get to the right place, and sometimes I get stuck or fail when the string length is too long。“mySubString(to:)” is my custom method
func scrollToPointByCatalog(string: String) {
textView.isEditable = true
let layout:NSLayoutManager = textView.layoutManager!
let container = textView.textContainer
let cutString = textView.textStorage?.string.mySubString(to: string)
let focusRingFrame:CGRect = layout.boundingRect(forGlyphRange: NSMakeRange(0, cutString!.count), in: container!)
scrollView.documentView!.scroll(NSPoint(x: 0, y:focusRingFrame.height))
textView.isEditable = false
}
Is there a good way to do that? It is a macOS software.

CTFontGetGlyphsForCharacters Returns false when passing emoji

In a class conforming to NSLayoutManagerDelegate I implement this method:
func layoutManager(_ layoutManager: NSLayoutManager,
shouldGenerateGlyphs glyphs: UnsafePointer<CGGlyph>,
properties props: UnsafePointer<NSLayoutManager.GlyphProperty>,
characterIndexes charIndexes: UnsafePointer<Int>,
font aFont: UIFont,
forGlyphRange glyphRange: NSRange) -> Int {
// First, make sure we'll be able to access the NSTextStorage.
guard let textStorage = layoutManager.textStorage
else { return 0 }
// Get the first and last characters indexes for this glyph range,
// and from that create the characters indexes range.
let firstCharIndex = charIndexes[0]
let lastCharIndex = charIndexes[glyphRange.length - 1]
let charactersRange = NSRange(location: firstCharIndex, length: lastCharIndex - firstCharIndex + 1)
var bulletPointRanges = [NSRange]()
var hiddenRanges = [NSRange]()
let finalGlyphs = UnsafeMutablePointer<CGGlyph>(mutating: glyphs)
// Generate the Middle Dot glyph using aFont.
let middleDot: [UniChar] = [0x00B7] // Middle Dot: U+0x00B7
var myGlyphs: [CGGlyph] = [0]
// Get glyphs for `middleDot` character
guard CTFontGetGlyphsForCharacters(aFont, middleDot, &myGlyphs, middleDot.count) == true
else { fatalError("Failed to get the glyphs for characters \(middleDot).") }
}
The problem is that CTFontGetGlyphsForCharacters returns false when I type an emoji into the textview. I think it might have something to do with UTF-8 vs. UTF-16 but I'm kind of out of my depth a little here. Little help?
The font you are using does not have a glyph for that particular character.
The system maintains a list of "font fallbacks" for times when the specific font you are trying to look at does not have a glyph but another font might.
The list of fallbacks is given by CTFontCopyDefaultCascadeListForLanguages, but since you're at the point where you are being asked for the glyph from a particular font, it seems that fallback generation should be handled higher up in the chain.
You should probably return 0 to indicate that the layout manager should use it's default behavior.

SKSpriteNode isn't responding when I try to hide it (Spoiler: SKAction timing issue)

OK, so I've been working on doing an RPG-style dialog box for a project, and while most of it is going smoothly, the one thing that's tripping me up right now is the little icon in the corner of the box to let you know there's more.
I tried to figure out how to get draw the shape, but not having any luck getting Core Graphics to draw triangles I decided to just use a PNG image of one instead. The code below shows everything relevant to how it's been set up and managed.
That being figured out, I'm now trying to get it to hide the marker when updating the box and show it again afterward. Here's what I've tried so far:
Method 1: Use .alpha = 0 to hide it from view during updates, restore with .alpha = 1
Method 2: Remove it from the node tree
Method 3: Place it behind the box background (located at .zPosition = -1)
The result has been consistent across all 3 methods: The triangle just stays in place, unresponsive when invoked.
class DialogBox: SKNode {
private var continueMarker = SKSpriteNode(imageNamed: "continueTriangle") // The triangle that shows in the lower-right to show there's more to read
init() {
/// Setup and placement. It appears in the proper position if I draw it and don't try to hide anything
continueMarker.size.width = 50
continueMarker.size.height = 25
continueMarker.position = CGPoint(x: ((width / 2) - (continueMarker.size.width * 0.9)), y: ((continueMarker.size.height * 0.9) - (height - margin)))
addChild(continueMarker)
}
func updateContent(forceAnimation: Bool = false) {
/// Determine what content to put into the box
hideContinueMarker()
/// Perform the content update in the box (which works as it should)
showContinueMarker()
}
func showContinueMarker() {
// continueMarker.alpha = 1 /// Method 1: Use .alpha to hide it from view during updates
// if (continueMarker.parent == nil) { // Method 2: Remove it from the tree
// addChild(continueMarker)
// }
continueMarker.zPosition = -2 /// Method 3: place it behind the box background (zPosition -1)
}
func hideContinueMarker() {
// continueMarker.alpha = 0 /// Method 1
// if (continueMarker.parent != nil) { /// Method 2
// continueMarker.removeFromParent()
// }
continueMarker.zPosition = 2 /// Method 3
}
}
OK, so while typing this one up I had some more ideas and ended up solving my own problem, so I figured I'd share the solution here, rather than pull a DenverCoder9 on everyone.
On the plus side, you get a look at a simple way to animate text in SpriteKit! Hooray!
In a final check to make sure I wasn't losing my mind, I added some print statements to showContinueMarker() and hideContinueMarker() and noticed that they always appeared simultaneously.
What's that mean? SKAction is likely at fault. Here's a look at the code for animating updates to the box:
private func animatedContentUpdate(contentBody: String, speaker: String? = nil) {
if let speaker = speaker {
// Update speaker box, if provided
speakerLabel.text = speaker
}
var updatedText = "" // Text shown so far
var actionSequence: [SKAction] = []
for char in contentBody {
updatedText += "\(char)"
dialogTextLabel.text = updatedText
// Set up a custom action to update the label with the new text
let updateLabelAction = SKAction.customAction(withDuration: animateUpdateSpeed.rawValue, actionBlock: { [weak self, updatedText] (node, elapsed) in
self?.dialogTextLabel.text = updatedText
})
// Queue up the action so we can run the batch afterward
actionSequence.append(updateLabelAction)
}
/// HERE'S THE FIX
// We needed to add another action to the end of the sequence so that showing the marker again didn't occur concurrent with the update sequence.
let showMarker = SKAction.customAction(withDuration: animateUpdateSpeed.rawValue, actionBlock: { [weak self] (node, elapsed) in
self?.showContinueMarker()
})
// Run the sequence
actionSequence.append(showMarker)
removeAction(forKey: "animatedUpdate") // Cancel any animated updates already in progress
run(SKAction.sequence(actionSequence), withKey: "animatedUpdate") // Start the update
}
In case you missed it in the big block there, here's the specific bit in isolation
let showMarker = SKAction.customAction(withDuration: animateUpdateSpeed.rawValue, actionBlock: { [weak self] (node, elapsed) in
self?.showContinueMarker()
})
Basically, we needed to add showing the triangle as an action at the end of the update sequence instead of just assuming it would occur after the update since the function was invoked at a later time.
And since all 3 methods work equally well now that the timing has been fixed, I've gone back to the .alpha = 0 method to keep it simple.

Swift - How to get all occurrences of a range?

I'm trying to use regex and range for the first time in Swift. I want to see if the letter the user enters into the textfield will match the word that they have to guess. If it does match the matching letter or letters will be displayed in a UILabel (similar to how you play hangman, if you guess the correct letter once and there are multiple occurrences of that letter, all occurrences will show). When a button is clicked the method below is called. It works fine when finding the matching letters, and inserting them at the right location, BUT when the UILabel is updated after the loop it only updates the label with the result of the second/final loop. How can I get a combination of the result from all the iterations of the loop? Any help would be appreciated. Thank you
func findLetter(displayedWord toSearchin: String, userInput toSearchFor: String) {
let ranges: [NSRange]
var labelUpdate = String()
do {
let regex = try NSRegularExpression(pattern: toSearchFor, options: [])
let displayedWord = toSearchin as NSString
let rangeOfSearch = NSMakeRange(0, displayedWord.length)
ranges = regex.matches(in: toSearchin, range: rangeOfSearch).map {$0.range}
let nsStringlabel = wordLabel.text as NSString?
for range in ranges {
labelUpdate = (nsStringlabel?.replacingCharacters(in: range, with: toSearchFor))!
print(labelUpdate)
//the word is lavenders, so this prints:
//___e_____
//______e__
// I want:
//___e__e__
}
DispatchQueue.main.async(execute: {
self.wordLabel.text = labelUpdate
})
}
catch {
ranges = []
}
}
As stated in a comment, you’re always updating the original nsStringlabel variable, thus always overriding the previous modification in the previous loop run.
I’d recommend you init labelUpdate with wordLabel.text as NSString? and completely remove nsStringlabel. This should solve your problem.
That being said, there are a lot of other problems that could be fixed in this function.
In particular, why use regexes? It’s expensive and not useful there.
Also, you’re dispatching before setting the label value, which is good, but not before retrieving the value… either a dispatch is needed or it is not, but it cannot be needed at one place and not at the other. If you call your function from the main thread (as a response to a user input for instance), you should be good and not need a dispatch.
Here what I would have done (should be safer and faster):
func updateLabel(withDestinationWord destinationWord: String, userInput: String) {
var labelText = wordLabel.text
var startIndex = labelText.characters.startIndex
while let r = destinationWord.range(of: userInput, options: .caseInsensitive, range: startIndex..<labelText.characters.endIndex) {
labelText.replaceSubrange(r, with: userInput)
startIndex = labelText.characters.index(after: r.lowerBound)
}
wordLabel.text = labelText
}
Be sure to have the same length for wordLabel.text and destinationWord!

Get all available characters from a font

I am developing an iOS app in Swift 3.
In this app I am listing all available fonts (system provided) but I would like to list all available characters for them too.
For example I am using Font Awesome to and I want the user to be able to select any of the characters/symbols from a list. How can I do this?
This is how I get an array of the fonts. How can I get an array of all characters for a selected font?
UIFont.familyNames.map({ UIFont.fontNames(forFamilyName: $0)}).reduce([]) { $0 + $1 }
For each UIFont, you have to get characterSet of that font. For example, I take first UIFont.
let firsttFont = UIFont.familyNames.first
let first = UIFont(name: firsttFont!, size: 14)
let fontDescriptor = first!.fontDescriptor
let characterSet : NSCharacterSet = fontDescriptor.object(forKey: UIFontDescriptorCharacterSetAttribute) as! NSCharacterSet
Then, use this extension to get all characters of that NSCharacterSet:
extension NSCharacterSet {
var characters:[String] {
var chars = [String]()
for plane:UInt8 in 0...16 {
if self.hasMemberInPlane(plane) {
let p0 = UInt32(plane) << 16
let p1 = (UInt32(plane) + 1) << 16
for c:UTF32Char in p0..<p1 {
if self.longCharacterIsMember(c) {
var c1 = c.littleEndian
let s = NSString(bytes: &c1, length: 4, encoding: String.Encoding.utf32LittleEndian.rawValue)!
chars.append(String(s))
}
}
}
}
return chars
}
}
(Ref: NSArray from NSCharacterset)
So, at last, just call characterSet.characters to get all characters (in String)
I don't think you'll be able to without a lot of coding. Here's a few links to Apple documentation:
In their main font page you'll have to scroll down a bit to get to a list of documentation, but in that list is their TrueType reference manual. The characters are stored as glyphs, meaning they are vector-based to allow for clean font sizes. (I believe the simple drop-down of font sizes in IB are merely "suggestions", and you can type in any size you care to.)
In that second link, scroll down to the lengthy list of font tables. One looks promising - the cmap table. But reading through this, it's possible to (a) have foreign characters like "umlaut A" or Chinese and (b) omit characters in each font. Also, this is just a lookup table - you'll then maybe need to use the mapping table to get the location of the glyph.
If you are targeting English only, you might be better off finding a way to check if the letters "Aa" exist for the font and display them.