How to Turn a UITextView into "Pages" like an E-reader [closed] - swift

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 4 years ago.
Improve this question
I have a Text View that contains a long passage. Rather than have a user scroll to keep reading, I want to separate the text into pages.
The screen would take up the full amount of text it can, and then the user can click a next or previous page. In other words, the behavior of any e-reader on the market.
I really am not sure where to start with. I would assume a few things I would have to do:
Get the character count of the string that will go in the text view.
Get the view width and height of the user's text view on his device
Figure out how many characters from the character count can be displayed on the text view based on the user's device dimensions.
With whatever formula I come up with, dynamically choose the text amount based on this.
let count = str.count
let userWidth = UIScreen.main.bounds.size.width
let userHeight = UIScreen.main.bounds.size.height
However, there is nothing I can come up with that works, and even this doesn't seem like it is the right path.
Any advice on how to start on creating e-reader page turning functionality? I can use a UIWebView instead of a UITextView if that makes it easier.
Thanks!

You're on the right track, but your implementation will only work for monospaced fonts (Menlo, Courier, etc) because the letter widths of proportional fonts (Helvetica, Times, etc) vary from letter to letter. Also, your plan would have the text breaking in the middle of words, which is probably sub-optimal.
TextKit has tools to make all this easier for you. If you want to do standard pagination, with wrapping at word boundaries instead of inside them, you can use the NSLayoutManager to do this for you.
A few notes:
You can't use the screen width to accurately calculate the size of the view, because you need to account for safe areas and the textContainerInset of the UITextView.
I'll leave pagination to you. You can either calculate the pages up from, or simple move forward or backwards through the ranges by trimming the previous page from the string.
func stringThatFitsOnScreen(originalString: String) -> String? {
// the visible rect area the text will fit into
let userWidth = textView.bounds.size.width - textView.textContainerInset.right - textView.textContainerInset.left
let userHeight = textView.bounds.size.height - textView.textContainerInset.top - textView.textContainerInset.bottom
let rect = CGRect(x: 0, y: 0, width: userWidth, height: userHeight)
// we need a new UITextView object to calculate the glyphRange. This is in addition to
// the UITextView that actually shows the text (probably a IBOutlet)
let tempTextView = UITextView(frame: self.textView.bounds)
tempTextView.font = textView.font
tempTextView.text = originalString
// get the layout manager and use it to layout the text
let layoutManager = tempTextView.layoutManager
layoutManager.ensureLayout(for: tempTextView.textContainer)
// get the range of text that fits in visible rect
let rangeThatFits = layoutManager.glyphRange(forBoundingRect: rect, in: tempTextView.textContainer)
// convert from NSRange to Range
guard let stringRange = Range(rangeThatFits, in: originalString) else {
return nil
}
// return the text that fits
let subString = originalString[stringRange]
return String(subString)
}
There are more complex ways to mix and match text views, layout managers, and text containers. You can investigate it as you get a handle on the easy stuff.

Related

Using Swift, how do I animate the .setPosition() method of an NSSplitView without visually stretching its contents?

I would like to animate the appearance of a NSSplitViewItem using .setPosition() using Swift, Cocoa and storyboards. My app allows a student to enter a natural deduction proof. When it is not correct, an 'advice view' appears on the right. When it is correct, this advice view will disappear.
The code I'm using is the below, where the first function makes the 'advice' appear, and the second makes it disappear:
func showAdviceView() {
// Our window
let windowSize = view.window?.frame.size.width
// A CGFloat proportion currently held as a constant
let adviceViewProportion = BKPrefConstants.adviceWindowSize
// Position is window size minus the proportion, since
// origin is top left
let newPosition = windowSize! - (windowSize! * adviceViewProportion)
NSAnimationContext.runAnimationGroup { context in
context.allowsImplicitAnimation = true
context.duration = 0.75
splitView.animator().setPosition(newPosition, ofDividerAt: 1)
}
}
func hideAdviceView() {
let windowSize = view.window?.frame.size.width
let newPosition = windowSize!
NSAnimationContext.runAnimationGroup{ context in
context.allowsImplicitAnimation = true
context.duration = 0.75
splitView.animator().setPosition(newPosition, ofDividerAt: 1)
}
}
My problem is that the animation action itself is causing the text in the views to stretch, as you can see in this example: Current behaviour
What I really want is the text itself to maintain all proportions and slide gracefully in the same manner that we see when the user themselves moves the separator: Ideal behaviour (but to be achieved programmatically, not manually)
Thus far in my troubleshooting process, I've tried to animate this outside of NSAnimationContext; played with concurrent drawing and autoresizing of subviews in XCode; and looked generally into Cocoa's animation system (though much of what I've read doesn't seem to have direct application here, but I might well be misunderstanding it). I suspect what's going on is that the .animator() proxy object allows only alpha changes and stretches---redrawing so that text alignment is honoured during the animation might be too non-standard. My feeling is that I need to 'trick' the app into treating the animation as though it's being performed by the user, but I'm not sure how to go about that.
Any tips greatly appreciated...
Cheers

Cocoa printing with Swift: "Any" printer setup has irregular margins/page size, any workaround?

I'm working on a graphics app for Mac OS in Swift and want to have a professional WYSIWYG layout features for printing.
I've isolated a weird behavior of the Cocoa printing system and just wondered if anyone knows of a setup step I'm missing or a workaround. The issue is that the "Any" printer has irregular, non-centered margins even when horizontal+vertical centering is requested. In particular, the left margin of my landscape letter-size docuemnt is 18pt and the right margin is 40pt - bigger than 1/2", which I feel is an unacceptable contraint, not to mention the annoyance of not being able to rely on the requested centering.
My app is NSDocument-based and I set printInfo margins to 0, centered fields to true, and orientation to .landscape.
self.printInfo.topMargin = 0.0
self.printInfo.rightMargin = 0.0
self.printInfo.leftMargin = 0.0
self.printInfo.bottomMargin = 0.0
self.printInfo.isHorizontallyCentered = true
self.printInfo.isVerticallyCentered = true
self.printInfo.orientation = .landscape
let newPrintOp = NSPrintOperation(view: self.printView!, printInfo: self.printInfo)
newPrintOp.showsPrintPanel = true
return newPrintOp
I am using Page Setup in this test. I have an EPSON printer that gives me the choice of regular or borderless printing, and with either option, selecting US Letter landscape, I get reasonable imageablePageBounds reported to draw into.
With the borderless page setup, I get
... imageablePageBounds: (0.0, 0.0, 792.0, 612.0)
and with 'regular' letter/landscape I get
... imageablePageBounds: (8.4000244140625, 8.399999618530273, 775.199951171875, 595.1999759674072)
The latter is setting to some driver minimum, but if you double the 8.4 point offsets and add to height and width you still get 792x612 == 11in x 8.5in
If instead I select "Any" printer in Page Setup and select US Letter, landscape, imageablePageBounds is reported (and enforced, even when printing to PDF) as:
... imageablePageBounds: (18.0, 18.0, 734.0, 576.0)
This gives 1/4" (18-pt) margins left, bottom, and top, but forces a 40-pt margin to the right (since width is only 734 - it should be 756 for a 10.5" drawing area). And indeed, if I try to draw an 10-inch image centered with 36pt margins, the right edge very annoyingly gets clipped unless I scale or shift it. It even gets clipped off with PDF output with this setup. Here's an image in the print panel that shows this clipping - the outer black line is the imageable view bounds given by the printing system, the blue line (right edge cut off) is a centered 10 x 7.5 image.
Does anyone know if there's a fix for this weird behavior? The ideal fix would be to get a reasonable 1/4" border as a default for anything I'm trying to print, but even if I can't get the max default width above 734pt I still want it to be centered so that I can at least work within 1/2" margins without any clipping.
While the original behavior does seem like a bug, it appears that setting printInfo.paperSize field may provide a partial workaround, as long as the Mac has an installed printer available.
In addition to my other setup, if I add the line
self.printInfo.paperSize = CGSize(width: 792.0, height: 612.0)
for my US-Letter landscape document, and still use Page Setup to set printer "Any" and letter/landscape, when the print job actually runs it now shows up having selected the driver from the EPSON driver rather than leaving the buggy "Any" driver's margins - the result is nicely centered with minimal margins as desired.
I tested on a MacBook Pro with no printer set up, and this workaround did not correct the issue - it still shows up with off-center margin and clips the drawing. So the workaround requires some actual printer to be installed.
I've filed a bug via Apple's feedback assistant, will post an update if anything more is discovered or resolved.
Here's my test app, it prints the view on one page without margins, maybe it helps:
Document.swift
override func windowControllerDidLoadNib(_ windowController: NSWindowController) {
let newPrintInfo = NSPrintInfo()
newPrintInfo.leftMargin = 0.0
newPrintInfo.bottomMargin = 0.0
newPrintInfo.rightMargin = 0.0
newPrintInfo.topMargin = 0.0
newPrintInfo.horizontalPagination = .clip
newPrintInfo.verticalPagination = .clip
self.printInfo = newPrintInfo
}
override var printInfo: NSPrintInfo {
didSet {
view.printInfoChanged(printInfo)
}
}
override func printOperation(withSettings printSettings: [NSPrintInfo.AttributeKey : Any]) throws -> NSPrintOperation {
return NSPrintOperation(view: view, printInfo: printInfo)
}
MyView.swift
func printInfoChanged(_ newPrintInfo: NSPrintInfo) {
setFrameSize(newPrintInfo.paperSize)
}
override func locationOfPrintRect(_ rect: NSRect) -> NSPoint {
return NSZeroPoint
}
override func knowsPageRange(_ range: NSRangePointer) -> Bool {
range.pointee.location = 1
range.pointee.length = 1
return true
}
override func rectForPage(_ page: Int) -> NSRect {
return bounds
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSColor.green.set()
NSBezierPath(rect:bounds).stroke()
NSColor.red.set()
NSBezierPath(ovalIn:bounds).stroke()
}

MTKView refresh issue

I am compositing an array of UIImages via an MTKView, and I am seeing refresh issues that only manifest themselves during the composite phase, but which go away as soon as I interact with the app. In other words, the composites are working as expected, but their appearance on-screen looks glitchy until I force a refresh by zooming in/translating, etc.
I posted two videos that show the problem in action: Glitch1, Glitch2
The composite approach I've chosen is that I convert each UIImage into an MTLTexture which I submit to a render buffer set to ".load" which renders a poly with this texture on it, and I repeat the process for each image in the UIImage array.
The composites work, but the screen feedback, as you can see from the videos is very glitchy.
Any ideas as to what might be happening? Any suggestions would be appreciated
Some pertinent code:
for strokeDataCurrent in strokeDataArray {
let strokeImage = UIImage(data: strokeDataCurrent.image)
let strokeBbox = strokeDataCurrent.bbox
let strokeType = strokeDataCurrent.strokeType
self.brushStrokeMetal.drawStrokeImage(paintingViewMetal: self.canvasMetalViewPainting, strokeImage: strokeImage!, strokeBbox: strokeBbox, strokeType: strokeType)
} // end of for strokeDataCurrent in strokeDataArray
...
func drawStrokeUIImage (strokeUIImage: UIImage, strokeBbox: CGRect, strokeType: brushTypeMode) {
// set up proper compositing mode fragmentFunction
self.updateRenderPipeline(stampCompStyle: drawStampCompMode)
let stampTexture = UIImageToMTLTexture(strokeUIImage: strokeUIImage)
let stampColor = UIColor.white
let stampCorners = self.stampSetVerticesFromBbox(bbox: strokeBbox)
self.stampAppendToVertexBuffer(stampUse: stampUseMode.strokeBezier, stampCorners: stampCorners, stampColor: stampColor)
self.renderStampSingle(stampTexture: stampTexture)
} // end of func drawStrokeUIImage (strokeUIImage: UIImage, strokeBbox: CGRect)
func renderStampSingle(stampTexture: MTLTexture) {
// this routine is designed to update metalDrawableTextureComposite one stroke at a time, taking into account
// whatever compMode the stroke requires. Note that we copy the contents of metalDrawableTextureComposite to
// self.currentDrawable!.texture because the goal will be to eventually display a resulting composite
let renderPassDescriptorSingleStamp: MTLRenderPassDescriptor? = self.currentRenderPassDescriptor
renderPassDescriptorSingleStamp?.colorAttachments[0].loadAction = .load
renderPassDescriptorSingleStamp?.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0)
renderPassDescriptorSingleStamp?.colorAttachments[0].texture = metalDrawableTextureComposite
// Create a new command buffer for each tessellation pass
let commandBuffer: MTLCommandBuffer? = commandQueue.makeCommandBuffer()
let renderCommandEncoder: MTLRenderCommandEncoder? = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptorSingleStamp!)
renderCommandEncoder?.label = "Render Command Encoder"
renderCommandEncoder?.setTriangleFillMode(.fill)
defineCommandEncoder(
renderCommandEncoder: renderCommandEncoder,
vertexArrayStamps: vertexArrayStrokeStamps,
metalTexture: stampTexture) // foreground sub-curve chunk
renderCommandEncoder?.endEncoding() // finalize renderEncoder set up
//begin presentsWithTransaction approach (needed to better synchronize with Core Image scheduling
copyTexture(buffer: commandBuffer!, from: metalDrawableTextureComposite, to: self.currentDrawable!.texture)
commandBuffer?.commit() // commit and send task to gpu
commandBuffer?.waitUntilScheduled()
self.currentDrawable!.present()
// end presentsWithTransaction approach
self.initializeStampArray(stampUse: stampUseMode.strokeBezier) // clears out the stamp array in preparation of next draw call
} // end of func renderStampSingle(stampTexture: MTLTexture)
First of all, the domain Metal is very deep, and it's use within the MTKView construct is sparsely documented, especially for any applications that fall outside the more traditional gaming paradigm. This is where I have found myself in the limited experience I have accumulated with Metal with the help from folks like #warrenm, #ken-thomases, and #modj, whose contributions have been so valuable to me, and to the Swift/Metal community at large. So a deep thank you to all of you.
Secondly, to anyone troubleshooting metal, please take note of the following: If you are getting the message:
[CAMetalLayerDrawable present] should not be called after already presenting this drawable. Get a nextDrawable instead
please don't ignore it. It mays seem harmless enough, especially if it only gets reported once. But beware that this is a sign that a part of your implementation is flawed, and must be addressed before you can troubleshoot any other Metal-related aspect of your app. At least this was the case for me. As you can see from the video posts, the symptoms of having this problem were pretty severe and caused unpredictable behavior that I was having a difficult time pinpointing the source of. The thing that was especially difficult for me to see was that I only got this message ONCE early on in the app cycle, but that single instance was enough to throw everything else graphically out of whack in ways that I thought were attributable to CoreImage and/or other totally unrelated design choices I had made.
So, how did I get rid of this warning? Well, in my case, I assumed that having the settings:
self.enableSetNeedsDisplay = true // needed so we can call setNeedsDisplay() to force a display update as soon as metal deems possible
self.isPaused = true // needed so the draw() loop does not get called once/fps
self.presentsWithTransaction = true // for better synchronization with CoreImage (such as simultaneously turning on a layer while also clearing MTKView)
meant that I could pretty much call currentDrawable!.present() or commandBuffer.presentDrawable(view.currentDrawable) directly whenever I wanted to refresh the screen. Well, this is not the case AT ALL. It turns out these calls should only be made within the draw() loop and only accessed via a setNeedsDisplay() call. Once I made this change, I was well on my way to solving my refresh riddle.
Furthermore, I found that the MTKView setting self.isPaused = true (so that I could make setNeedsDisplay() calls directly) still resulted in some unexpected behavior. So, instead, I settled for:
self.enableSetNeedsDisplay = false // needed so we can call setNeedsDisplay() to force a display update as soon as metal deems possible
self.isPaused = false // draw() loop gets called once/fps
self.presentsWithTransaction = true // for better synchronization with CoreImage
as well as modifying my draw() loop to drive what kind of update to carry out once I set a metalDrawableDriver flag AND call setNeedsDisplay():
override func draw(_ rect: CGRect) {
autoreleasepool(invoking: { () -> () in
switch metalDrawableDriver {
case stampRenderMode.canvasRenderNoVisualUpdates:
return
case stampRenderMode.canvasRenderClearAll:
renderClearCanvas()
case stampRenderMode.canvasRenderPreComputedComposite:
renderPreComputedComposite()
case stampRenderMode.canvasRenderStampArraySubCurve:
renderSubCurveArray()
} // end of switch metalDrawableDriver
}) // end of autoreleasepool
} // end of draw()
This may seem round-about, but it was the only mechanism I found to get consistent user-driven display updates.
It is my hope that this post describes an error-free and viable solution that Metal developers may find useful in the future.

Added subview is positioned slightly off - how can I get it back into place?

UPDATE: Solved! While the contentMode for pianoNoteDisplayed and piano_background were indeed the same, apparently this wasn't true for the added subviews. I simply added the line subview.contentMode = superview.contentMode to the function vdmzz suggested, and now everything looks right on all 4 screen sizes.
There are two image views: one called "piano_background" holds a background image (a piano keyboard) and the other will be used to display highlighted notes. The second is constrained to the first:
(the width constraint is probably unnecessary, because the leading and trailing constraints are already set, right?)
To display multiple highlighted keys, I am programmatically adding subviews to the piano_note view and activating the NSLayoutConstraints to get it into place (otherwise it shows up way out of position) like so:
pianoNoteDisplayed.image = nil
if !notesAlreadyAttempted.contains(currentUserAnswer) {
let wrongNoteImageName = "large_\(currentUserAnswer)_wrong"
let wrongNoteImage = UIImage(named: wrongNoteImageName)
let wrongNoteImageView = UIImageView(image: wrongNoteImage!)
wrongNoteImageView.translatesAutoresizingMaskIntoConstraints = false
pianoNoteDisplayed.addSubview(wrongNoteImageView)
NSLayoutConstraint.activate([
wrongNoteImageView.widthAnchor.constraint(equalToConstant: pianoNoteDisplayed!.frame.width),
wrongNoteImageView.heightAnchor.constraint(equalToConstant: pianoNoteDisplayed!.frame.height)
])
}
notesAlreadyAttempted.append(currentUserAnswer)
}
The issue is that the subview is displayed slightly off, and I can't seem to figure out why:
(as you can see, the highlight looks slightly compressed vertically.. the top lands correctly, but the bottom doesn't reach far enough by about 5px)
I have tried centering and constraining the subview in multiple ways, using suggestions from about 5 different answers on stack, and a few other articles I found. The images I am using (the piano background and the overlaying note highlight subview) are identical sizes. I have tried adding more or fewer constraints in the interface builder, and I have tried adding subviews to the original piano_background view instead of the second pianoNoteDisplayed view - same result. Using the pianoNoteDisplayed view itself to display the highlighted note works fine by the way:
And these are displayed using the usual .image method:
pianoNoteDisplayed.image = UIImage(named: "large_\(currentCorrectAnswer)_right")
Any suggestions for how to troubleshoot the issue further?
First of all, as far as I understood pianoNoteDisplayed doesn't need to be an UIImageView.
Secondly, if you align piano_background and pianoNoteDisplayed by top, leading, trailing and bottom edges, one will be exactly on top of other. Or you could set them equal height, width and center positions.
The problem with your current set of constraints is that piano_background's Y position is determined by Safe Area and therefore might defer from pianoNoteDisplayed's Y position.
Try using this function:
func addSameSize(subview: UIView, onTopOf superview: UIView) {
superview.addSubview(subview)
subview.translatesAutoresizingMaskIntoConstraints = false
subview.centerXAnchor.constraint(equalTo: superview.centerXAnchor).isActive = true
subview.centerYAnchor.constraint(equalTo: superview.centerYAnchor).isActive = true
subview.widthAnchor.constraint(equalTo: superview.widthAnchor).isActive = true
subview.heightAnchor.constraint(equalTo: superview.heightAnchor).isActive = true
subview.contentMode = superview.contentMode
}
E.g.
addSameSize(subview: wrongNoteImageView, onTopOf: pianoNoteDisplayed)
It will add your image views exactly aligned on top of pianoNoteDisplayed view

SKScene Pausing briefly When SKLabelNode's text is changed

I am programming a small game using SpriteKit
I added a SKLabelNode to my SKScene with the initial text of just "0".
When I try and update the text of this SKLabel using:
func updateScoreLabel() {
scoreNumber++
scoreLabel.text = String(scoreNumber)
}
There is a short pause of the entire SKScene between when it gets called and then when it is updated.
However this only happens the first time it is called so If I am running this a second time as in updating the scoreLabel any subsequent time the Scene Pausing then the pause does not occur.
what is triggering the method call is... CC is an enum of physicsBody categoryBitMasks typed to Int
func isCollisionBetween(nodeTypeOne: CC, nodeTypeTwo: CC, contact: SKPhysicsContact) -> Bool {
let isAnodeTypeOne = contact.nodeA.physicsBody!.categoryBitMask == nodeTypeOne.rawValue
let isAnodeTypeTwo = contact.nodeA.physicsBody!.categoryBitMask == nodeTypeTwo.rawValue
let isBnodeTypeOne = contact.nodeB.physicsBody!.categoryBitMask == nodeTypeOne.rawValue
let isBnodeTypeTwo = contact.nodeB.physicsBody!.categoryBitMask == nodeTypeTwo.rawValue
if (isAnodeTypeOne && isBnodeTypeTwo) || (isAnodeTypeTwo && isBnodeTypeOne) {
return true
} else {
return false
}
}
then if
if isCollisionBetween(CC.TypeA, nodeTypeTwo: CC.TypeB, contact: contact) {
updateScoreLabel()
}
Can someone please point out the problem. The score updating does not pause the scene when the same collision is detected and a println statement is used to output the score so I think it is specific to changing the text of the SKLabelNode
You should check for two things which for sure can cause the lag:
for typos in a font name
that you are not loading entire font family, ie Arial instead of just Arial-BoldMT or Arial-ItalicMT. You want to be specific, because loading entire font family can make a delay when using certain fonts. List of iOS fonts could be found here.
If you need to list available fonts(and see real font names), you can use something like this:
for familyName in UIFont.familyNames() as [String] {
println("\(familyName)")
for fontName in UIFont.fontNamesForFamilyName(familyName) as [String] {
println("\tFont: \(fontName)")
}
}
When initializing label for the first time (say at the moment when collision occurs) the delay may happen if you are using custom fonts which are not available in iOS.
In that case try to "preload" a font before using the label. So before actual gameplay, you should instantiate SKLabelNode and set its text property to some value. You have to set a text property, because by doing that the font will be preloaded and ready for use. Otherwise it will be loaded at the time you set label's text property.
EDIT:
Sorry, I just noticed that you are said that you are initializing already a label with initial text. So the just ignore part of my answer related to that and look for typos and the part about loading specific font.
Hope this will take you somewhere. Good luck!