I am attempting to display individual characters in the exact positions that they would appear if displayed as a single string with kerning. The problem is that the characters' bounding boxes seem to be opaque, so that each newly added character covers some of the prior one. Where kerning is greater (e.g., in the combination "ToT"), the problem is obvious:
My setup is something like this: I have an SKView embedded in a container view. In an extension of the SKView's view controller, the following are set within a function in this order:
skView.allowsTransparency = true
scene.backgroundColor = .clear
charAttr – [NSAttributedStringKey.backgroundColor: UIColor.clear]
textView.isOpaque = false
textView.backgroundColor = UIColor.clear
Each UITextView is added successively as a subview to the view (which is an SKView).
I've looked all over my code for some clue as to what could be making the character's bounding boxes seem opaque, but I haven't found anything. The sad thing is that I solved this problem sometime last year, but don't remember what I did and don't have the code anymore.
Any insights or ideas would be appreciated.
After achieving the sought-after effect in a playground, I pasted this simple code into the extension where the original code was. It still worked, so I made it as close to identical to the original as possible, until it also exhibited the problem.
The SKView and extension aspects were irrelevant. The problem lies with how UITextView frames property deals with character widths. Here's the relevant code:
// charInfoArray contains (among other things) an attributed character, its origin,
// its size, and info about whether or not it should be displayed
// For each char, the origin and size info were derived from...
let currentCharRect = layoutManager.boundingRect(forGlyphRange: currentCharRange, in: textContainer)
// To display each (attributed) char of "ToT\n" . . .
for (index, charInfo) in charInfoArray.enumerated() {
guard charInfo.displayed == true else { continue }
let textView = UITextView()
textView.backgroundColor = UIColor.clear
textView.attributedText = charInfo.attrChar
textView.textContainerInset = UIEdgeInsets.zero
// let width = charInfo.size!.width
let width = 30 // arbitrary width
// Using the automatically generated charInfo.size!.width here
// appears to make the text view's background opaque!
textView.frame = CGRect(origin: charInfo.origin!,
size: CGSize(width: width, height: charInfo.size!.height))
textView.frame = textView.frame.offsetBy(dx: 0.0, dy: offsetToCenterY)
textView.textContainer.lineFragmentPadding = CGFloat(0.0)
textView.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 0.2)
textView.textColor = UIColor.black
view.addSubview(textView)
print(charInfo.size!.width)
}
Setting the width to width = charInfo.size!.width seems to make the text view's background opaque! This may be caused by the unusually large width assigned to newline char at the end of the sequence. To eliminate the problem, I'll have to deal with newline chars in another way. In any case, I have no idea why this causes the text view's background to turn opaque, but it does. Setting the width to an arbitrary value of, say, 30 eliminates problem.
Here are the results of using automatically generated and manually set widths, respectively:
The translucent red areas (on yellow backgrounds) show the bounding rectangles for each of the characters, including the newline character.
Related
I have been looking around for a way to set the alignment of the segmented control titles to the left but I don't seem to be able to achieve what I want.
I have created this little function to change the frame of the subviews of the segment control.
It works at first.
func modifyFrameOfSegment() {
for segment in segmentedControl.subviews {
guard segment.subviews.isNotEmpty else { return }
segment.contentMode = .left
for label in segment.subviews where label is UILabel {
label.frame = CGRect(x: 0, y: label.frame.origin.y, width: label.frame.size.width, height: label.frame.size.height)
(label as! UILabel).textAlignment = .left
}
}
}
But everytime I select a new segment it resets the frames of all the subviews and center align all the titles again.
Is there a way to achieve a permanent left alignment for the segment titles in a segmented control?
Any tips or advice would be greatly appreciated.
Thank you for your time.
Let's use this method
self.segmentedControl.setContentPositionAdjustment(UIOffset(horizontal: -20, vertical: 0), forSegmentType: .left, barMetrics: .default)
And you can do what you want (Of course, you can change the horizontal & vertical value by your needs). Here is the result:
Update:
There's apparently no way to set the alignment of the items, but you can fake it by adjusting the position of each individual item using setContentOffset(_ offset: CGSize, forSegmentAt segment: Int). Here's a kludgy example:
class LeftSegmentedControl: UISegmentedControl {
var margin : CGFloat = 10
override func layoutSubviews() {
super.layoutSubviews()
leftJustifyItems()
}
func leftJustifyItems() {
let fontAttributes = titleTextAttributes(for: .normal)
let segments = numberOfSegments - 1
let controlWidth = frame.size.width
let segmentWidth = controlWidth / CGFloat(numberOfSegments)
for segment in 0...segments {
let title = titleForSegment(at: segment)
setWidth(segmentWidth, forSegmentAt: segment)
if let t = title {
let titleSize = t.size(withAttributes: fontAttributes)
let offset = (segmentWidth - titleSize.width) / 2 - margin
self.setContentOffset(CGSize(width: -offset, height: 0), forSegmentAt: segment)
}
}
}
}
Here's what it looks like:
There are a few caveats:
This version sets the segments to all have equal width, which might not be what you want.
I used a fixed left margin of 10px because it seems unlikely that you'd want to vary that, but you can obviously change it or make it a settable property.
Just because you can do this doesn't mean you should. Personally, I don't think it looks great, and it suffers in the usability department too. Users expect segmented control items to be centered, and left-justifying the items will make it harder for them to know where to tap to hit the segment. That seems particularly true for short items like the one labelled "3rd" in the example. It's not terrible, it just seems a little weird.
Original answer:
UIControl (of which UISegmentedControl is a subclass) has a contentHorizontalAlignment property that's supposed to tell the control to align its content a certain way, so the logical thing to do would be to set it like this:
let segmented = UISegmentedControl(items: ["Yes", "No", "Maybe"])
segmented.frame = CGRect(x:75, y:250, width:250, height:35)
segmented.contentHorizontalAlignment = .left
But that doesn't work — you still get the labels centered. If you've got a compelling use case for left-aligned segments, you should send the request to Apple.
One way you could work around this problem is to render your labels into images and then use the images as the segment labels instead of plain strings. Starting from the code in How to convert a UIView to an image, you could easily subclass UISegmentedControl to create images from the item strings.
The image below shows Xcode graphical debug hierarchy for a UIViewController. It looks like I need to destroy additional data from UITextViews that are being recreated after editing in an array of UITextViews. Each time I make a change, I've set the array of UITextViews to [], then recreate it with the updated data. Even though I verify that the UITextView array is indeed being reset to zero elements, then recreated with the expected number of elements, these ghost images linger on screen and the debug hierarchy shows something isn't being removed.
I suspect there's some sort of housekeeping I need to do to find & destroy additional data related to the UITextViews, and that setting the array back to zero isn't clearing everything out of my subview, but I'm unsure what this might be. I'm hopeful that my mistake seems obvious to those with more experience & you'll point me in the right direction.
I also share some code below, but I suspect the visual may be the most direct clue for the experienced to set me straight.
var topOfViewFrame: CGFloat = 0
textViewArray = []
for textBlock in textBlocks.textBlocksArray { // a textBlock has formatting data for a UITextView
let textBlockHeight = CGFloat(textBlock.numberOfLines) * textBlock.blockFontSize
let viewFrame = CGRect(x: 0, y: topOfViewFrame, width: textBoxWidth, height: textBlockHeight)
var newTextView = UITextView(frame: viewFrame)
newTextView.center = CGPoint(x: screenView.frame.width/2, y: topOfViewFrame + (textBlockHeight/2)) // screenView is the 320x240 subView holding the [UITextViews]
let viewFont = UIFont(name: "AvenirNextCondensed-Medium", size: textBlock.blockFontSize)
newTextView.font = viewFont
newTextView.text = textBlock.blockText
newTextView = configureTextBlockView(textBoxView: newTextView, textBlock: textBlock)
textViewArray.append(newTextView)
screenView.addSubview(newTextView) // screenView is the subview holding the [UITextViews]
topOfViewFrame += newTextView.frame.height // put the next UITextView directly below the previous one
}
Thanks!
If you want to remove all textViews from 'screenView' its not enough just to clear the array.
You need to remove it from superview:
screenView.subviews.forEach({ $0.removeFromSuperview() })
I'm wanting to move my labels wherever I want on my external display. (eg. displayRunsLabel at the top and centered or perhaps 40px from right and top)
I have my labels showing on the screen. I'm currently using text alignment.
What's the best way to do this?
Here's my external display:
ExternalDisplayScreenshot
Here's the code:
// CUSTOMISE VIEW
secondScreenView.backgroundColor = UIColor.black
displayWicketLabel.textAlignment = NSTextAlignment.left
displayWicketLabel.font = UIFont(name: "Arial-BoldMT", size: 200.0)
displayWicketLabel.textColor = UIColor.white
displayWicketLabel.frame = secondScreenView.bounds
secondScreenView.addSubview(displayWicketLabel)
displayRunsLabel.textAlignment = NSTextAlignment.center
displayRunsLabel.font = UIFont(name: "Arial-BoldMT", size: 200.0)
displayRunsLabel.textColor = UIColor.white
displayRunsLabel.frame = secondScreenView.bounds
secondScreenView.addSubview(displayRunsLabel)
displayOversLabel.textAlignment = NSTextAlignment.right
displayOversLabel.font = UIFont(name: "Arial-BoldMT", size: 200.0)
displayOversLabel.textColor = UIColor.white
displayOversLabel.frame = secondScreenView.bounds
secondScreenView.addSubview(displayOversLabel)
}
The position of a label or any other UIView is the origin of its frame, which is a CGPoint representing its top left corner.
So, you are currently saying:
displayWicketLabel.frame = secondScreenView.bounds
displayRunsLabel.frame = secondScreenView.bounds
displayOversLabel.frame = secondScreenView.bounds
Well, that's pretty silly, because you have given all three labels the same frame! Thus they overlay one another, and (as you rightly say) you can only read them by giving them different text alignments.
Instead, give them each a different frame — one that puts the label where you want it. For example:
displayWicketLabel.sizeToFit()
displayWicketLabel.frame.origin = CGPoint(x:30, y:20) // or whatever
displayRunsLabel.sizeToFit()
displayRunsLabel.frame.origin = CGPoint(x:30, y:100) // or whatever
displayOversLabel.sizeToFit()
displayOversLabel.frame.origin = CGPoint(x:30, y:200) // or whatever
Now, having said all that, a clear flaw in the code I just showed you is that we have hard-coded the positions arbitrarily, without taking account of what the size of the window is (the external display). For that reason, a much better way would be for you to learn about auto layout, which will let you lay the three labels out in nice positions automatically based on the size of the window.
I have followed this tutorial: https://www.raywenderlich.com/410-core-graphics-tutorial-part-2-gradients-and-contexts
I am trying to add a label over each point with the Int for the point, but it wont show. Adding code in the //Draw the circles on top of graph stroke
//Draw the circles on top of graph stroke
for i in 0..<graphPoints.count {
var point = CGPoint(x:columnXPoint(i), y:columnYPoint(graphPoints[i]))
point.x -= Constants.circleDiameter / 2
point.y -= Constants.circleDiameter / 2
let circle = UIBezierPath(ovalIn: CGRect(origin: point, size: CGSize(width: Constants.circleDiameter, height: Constants.circleDiameter)))
circle.fill()
//let label = UILabel(frame: CGRect(origin: point, size: CGSize(width: Constants.circleDiameter, height: Constants.circleDiameter)))
let label = UILabel()
label.frame.origin = CGPoint(x:columnXPoint(i), y:columnYPoint(graphPoints[i]))
label.text = "TDDDDDDDE"
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = UIColor.black
self.addSubview(label)
self.view.addSubview(label)
}
Can anyone help me with adding the label? The label wont show, and I want it over the points.
Best guests, given there's not the full context. I see 3 possible reasons
1: You should replace
self.addSubview(label)
self.view.addSubview(label)
By
self.addSubview(label)
The second line will remove the label from view to add it to self.view, which might be a problem if they are different.
2: With the following line, you're telling the label to use autolayout ...but do not provide any constraint, so they might be there but not where you expected them to be.
label.translatesAutoresizingMaskIntoConstraints = false
To add constraints: see official Apple Doc, example below:
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: aConstant).isActive = true
But 3: your label is initialised without a frame/an empty frame. Call sizeToFit on your label after having set the content, fonts, etc if you don't use autolayout. If you stick to autolayout, this point is not applicable.
And another point indirectly linked to your question: If you manage to get your code to work, you'll probably have too much labels: your code seems to be in the draw method, which means that you'll be instantiating new labels each time the view is rendered ...and the previous ones won't be removed. A better practice would be instanciate the labels when you load the data (if they depend on the data points), to position them in layoutSubviews or to skip the labels entirely and use NSAttributedString draw functions to render the text directly in the view.
The best way to describe my situations is with images. What I have is a view which contains several UILabels and UIImage. The red box is a UILabel and if the content is too big it should go to the second line.
From the storyboard I have a working case when the content fits but the problem is that I am not sure how to handle the case when the last (red box) should go to the second line. I am using autolayout and cartography.
If someone can point me to the right direction I will be very grateful.
First calcululate width of text as per your current label's position.
If text width is more than current label's width then see my answer from below link:
Auto Layout how to move the view2 from right to the bottom?
Calculate width:
func widthForView1(_ text:String, font:UIFont, height:CGFloat) -> CGFloat
{
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: your_label_width, height: your_lable_height))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.text = text
label.font = font
label.sizeToFit()
return label.frame.width
}
You cannot do that with constraints only. To change the entire position of the element on the screen, you need to do it programmatically.
Use of tag View can solve this issue. TagListView is an external library.
When u add a view as subclass of taglistView, its height automatically increases accordingly.
ADD this to pod file : pod 'TagListView'
func addTags() {
let str1 = "Hi"
tagListView.addTag(str1)
let str2 = "Helloo"
tagListView.addTag(str2)
let str3 = "How Are u ? "
tagListView.addTag(str2)
tagListView.isUserInteractionEnabled = false
}