I have a UIView subclass in which I have a UITextView. The view and the text view grow in width according to the text in the text view. When the width of the view reaches the width of the superview, the view stops growing, and the font of the text gets smaller inside the text view. Everything seems to work fine until the text starts to get smaller. The text (which is centred both horizontally and vertically) starts to lose its position and jump around after each letter I type in. Here's a visual demonstration:
https://gyazo.com/b2027ab0fa7956244842736868dbd87e.
Here are the code parts:
The delegate methods:
// After text was typed in
func textViewDidChange(textView: UITextView) {
if shouldUpdate {
updateTextFont(textView)
self.centerText()
shouldUpdate = false
}
self.centerText()
}
// Before text was typed in
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
if text == "\n" {
textView.resignFirstResponder()
}
let textSize = (text as NSString).sizeWithAttributes([NSFontAttributeName: textView.font!]) // Size of only ONE letter
if frame.width+textSize.width > superview!.bounds.width {
shouldUpdate = true
}
else {
updateFrames(textSize.width)
}
//centerText()
return true
}
Font size&position methods + frame resizing method:
func updateTextFont(textView: UITextView) {
if (textView.text.isEmpty || CGSizeEqualToSize(textView.bounds.size, CGSizeZero)) {
return;
}
let textViewSize = textView.bounds.size;
let fixedWidth = textView.bounds.width;
let expectSize = textView.sizeThatFits(CGSizeMake(fixedWidth, CGFloat(MAXFLOAT)));
var expectFont = textView.font;
if (expectSize.height > textViewSize.height) {
while (textView.sizeThatFits(CGSizeMake(fixedWidth, CGFloat(MAXFLOAT))).height > textViewSize.height) {
expectFont = textView.font!.fontWithSize(textView.font!.pointSize-1)
textView.font = expectFont
}
}
else {
while (textView.sizeThatFits(CGSizeMake(fixedWidth, CGFloat(MAXFLOAT))).height < textViewSize.height) {
print(expectFont!.pointSize)
expectFont = textView.font;
textView.font = textView.font!.fontWithSize(textView.font!.pointSize+1)
}
textView.font = expectFont;
}
}
func centerText() {
self.layoutIfNeeded()
let size = self.mainTextView.sizeThatFits(CGSizeMake(CGRectGetWidth(self.mainTextView.bounds), CGFloat(MAXFLOAT)))
var topoffset = (self.mainTextView.bounds.size.height - size.height * self.mainTextView.zoomScale) / 2.0
topoffset = topoffset < 0.0 ? 0.0 : topoffset
self.mainTextView.contentOffset = CGPointMake(0, -topoffset)
}
func updateFrames(param: CGFloat) {
bounds.size.width += param
mainTextView.center = CGPoint(x: bounds.width/2, y: bounds.height/2)
centerText()
}
Please notify me if I forgot to include any part of the code.
An interesting thing I found is that if I call centerText() after a delay in textViewDidChange the text does go back to place, but by default it doesn't.
What could be the problem?
Thanks in advance!
Related
In IOS 12 the scrolling of UITableView is smooth without lag even when html string contains image but in IOS 13 UITableView scrolling becomes laggy. Already try shouldresterizing = true but doesn't solve the problem. Simulator and Device are the same results.
class ProductTextViewCell:UITableViewCell {
#IBOutlet weak var textView: UITextView!
override func awakeFromNib() {
super.awakeFromNib()
textView.textContainer.lineFragmentPadding = 0
textView.text = "-"
textView.dataDetectorTypes = .all
}
func set(str:String) {
print(str)
if let attributed = str.html2AttributedString {
let mutable = NSMutableAttributedString(attributedString: attributed)
self.textView.attributedText = mutable
mutable.enumerateAttribute(NSAttributedStringKey.attachment, in: NSMakeRange(0, attributed.length), options: .init(rawValue: 0), using: { (value, range, stop) in
if let attachement = value as? NSTextAttachment {
if let image = attachement.image(forBounds: attachement.bounds, textContainer: NSTextContainer(), characterIndex: range.location) {
let screenSize: CGRect = UIScreen.main.bounds
let max = screenSize.width - 20
print(max)
print(image.size.width)
if image.size.width > max {
let scale = image.size.height / image.size.width
attachement.bounds = CGRect(x: 0, y: 0, width: max, height: max * scale)
}
}
}
})
} else {
self.textView.attributedText = nil
}
}
}
I want to set the vertical alignment on my UITextView. The text view is editable and scroll is disabled:
let textView = UITextView(frame: frame)
textView.backgroundColor = .clear
textView.attributedText = myAttributedString
textView.isUserInteractionEnabled = true
textView.isEditable = true
textView.allowsEditingTextAttributes = true
textView.isScrollEnabled = false
textView.textContainerInset = .zero
self.addSubview(textView)
So, I would like to do something like :
textView.verticalAlignment = .center
I already tried subclassing the text view and adding an attribute like this:
class MyTextView: UITextView {
public var verticalAlignment: UIControl.ContentVerticalAlignment = .center {
didSet {
let topCorrection: CGFloat
switch verticalAlignment {
case .center:
topCorrection = (bounds.size.height - contentSize.height * zoomScale) / 2.0
case .bottom:
topCorrection = (bounds.size.height - contentSize.height * zoomScale)
case .top, .fill:
topCorrection = 0
#unknown default:
topCorrection = 0
}
contentInset.top = max(0, topCorrection)
}
}
}
But it seems not to work with isScrollEnabled set to false.
Every other solution I found on the internet didn't work either and I'm a bit hopeless… Can you help me?
Thanks
Maybe you intended to use a UITextField?
let field = UITextField()
field.contentVerticalAlignment = UIControlContentVerticalAlignment.center
Or set it to .bottom, .top, etc.
Create a custom textView
class CustomTextView: UITextView {
override var canBecomeFirstResponder: Bool {
return false
}
override var selectedTextRange: UITextRange? {
get {
return nil
} set {
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer,
tapGestureRecognizer.numberOfTapsRequired == 1 {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
if let longPressGestureRecognizer = gestureRecognizer as? UILongPressGestureRecognizer,
longPressGestureRecognizer.minimumPressDuration < 0.325 {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
gestureRecognizer.isEnabled = false
return false
}
}
Use this textview as a baseclass for the textview
I have used pinchGesture to zoom-in and zoom-out textView using below code.
added pinchGesture to textView
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch))
pinchGesture.delegate = self
view.addGestureRecognizer(pinchGesture)
delagate
#IBAction func handlePinch(recognizer:UIPinchGestureRecognizer) {
if let view = recognizer.view as? UITextView {
view.transform = view.transform.scaledBy(x: recognizer.scale, y: recognizer.scale)
recognizer.scale = 1
}
}
Result
Here is the possible solution i have applied but still not able to find perfect solution.
#IBAction func handlePinch(recognizer:UIPinchGestureRecognizer) {
if let textView = recognizer.view as? UITextView {
let font = textView.font!
var pointSize = font.pointSize
let fontName = font.fontName
pointSize = ((recognizer.velocity > 0) ? 1 : -1) * 1 + pointSize;
if (pointSize < 13) {
pointSize = 13
}
if (pointSize > 100) {
pointSize = 100
}
textView.font = UIFont(name: fontName, size: pointSize)
}
}
Result
Using above solution i am successfully able to increase font size but textView frame is not updating so text is getting cut off because textView frame is smaller.
Expected Result
Font will get increased and also frame will get update so it will look like simple zoom-in and zoom-out but without blurry.
Looking for best possible solution to increase font size with frame like instagram and snapchat is doing.
Thanks.
Here is the code to resize font size along with frame on pinch zoom in/out using UITextView and isScrollEnabled = false
#objc func pinchRecoginze(_ pinchGesture: UIPinchGestureRecognizer) {
guard recognizer.view != nil, let view = recognizer.view else {return}
if view is UITextView {
let textView = view as! UITextView
if recognizer.state == .began {
let font = textView.font
let pointSize = font!.pointSize
recognizer.scale = pointSize * 0.1
}
if 1 <= recognizer.scale && recognizer.scale <= 10 {
textView.font = UIFont(name: textView.font!.fontName, size: recognizer.scale * 10)
let textViewSiSize = textView.intrinsicContentSize
textView.bounds.size = textViewSiSize
}
}
}
Updated answer to compatible with UITextView
Here is the to resize font and frame with pinchGesture when textView isScrollEnabled = false.
#IBAction func handlePinch(recognizer:UIPinchGestureRecognizer) {
if let view = recognizer.view {
if view is UITextView {
let textView = view as! UITextView
if textView.font!.pointSize * recognizer.scale < 90 {
let font = UIFont(name: textView.font!.fontName, size: textView.font!.pointSize * recognizer.scale)
textView.font = font
let sizeToFit = textView.sizeThatFits(CGSize(width: UIScreen.main.bounds.size.width,
height:CGFloat.greatestFiniteMagnitude))
textView.bounds.size = CGSize(width: textView.intrinsicContentSize.width,
height: sizeToFit.height)
} else {
let sizeToFit = textView.sizeThatFits(CGSize(width: UIScreen.main.bounds.size.width,
height:CGFloat.greatestFiniteMagnitude))
textView.bounds.size = CGSize(width: textView.intrinsicContentSize.width,
height: sizeToFit.height)
}
textView.setNeedsDisplay()
} else {
view.transform = view.transform.scaledBy(x: recognizer.scale, y: recognizer.scale)
}
recognizer.scale = 1
}
}
What if you try to enable scrolling in the beginning of your handlePinch method and disable it again at the end of pinching?:
#IBAction func handlePinch(recognizer:UIPinchGestureRecognizer) {
if let textView = recognizer.view as? UITextView {
textView.isScrollingEnabled = true
let font = textView.font!
var pointSize = font.pointSize
let fontName = font.fontName
pointSize = ((recognizer.velocity > 0) ? 1 : -1) * 1 + pointSize;
if (pointSize < 13) {
pointSize = 13
}
if (pointSize > 100) {
pointSize = 100
}
textView.font = UIFont(name: fontName, size: pointSize)
let width = view.frame.size.width
textView.frame.size = textView.sizeThatFits(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
textView.isScrollingEnabled = false
}
}
I have a UIView with embedded stack views containing labels and imageViews. This UIView is designed to expand in height when the text of one of the labels reaches a certain size. The label is set to lines = 0 and word wrap. I have confirmed that the height changes, but this isn't reflected in the UI.
This is the UIView with a standard size name label:
This is the UIView with an extended size name label. As you can see the "open" label is cut off:
This function determines the height of UIView:
func viewHeight(_ locationName: String) -> CGFloat {
let locationName = tappedLocation[0].name
var size = CGSize()
if let font = UIFont(name: ".SFUIText", size: 17.0) {
let fontAttributes = [NSAttributedStringKey.font: font]
size = (locationName as NSString).size(withAttributes: fontAttributes)
}
let normalCellHeight = CGFloat(96)
let extraLargeCellHeight = CGFloat(normalCellHeight + 20.33)
let textWidth = ceil(size.width)
let cellWidth = ceil(nameLabel.frame.width)
if textWidth > cellWidth {
return extraLargeCellHeight
} else {
return normalCellHeight
}
}
And this function applies it:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
annotation = view.annotation as! MKPointAnnotation
horizontalStackView.addBackground(color: UIColor.black)
// Add the tapped location to the tappedLocation array
for location in locations {
if location.latitude == annotation.coordinate.latitude && location.longitude == annotation.coordinate.longitude {
tappedLocation.append(location)
}
}
locationView.frame.size.height = viewHeight(tappedLocation[0].name)
print("locationView height = \(locationView.frame.height)")
print("locationView x = \(locationView.frame.origin.x)")
print("locationView y = \(locationView.frame.origin.y)")
print("Frame height: \(locationView.frame.size.height)")
print("Frame widthL \(locationView.frame.size.width)")
YelpClient.sharedInstance().loadImage(tappedLocation[0].imageUrl, completionHandler: { (image) in
performUIUpdatesOnMain {
self.thumbnailImageView.layer.cornerRadius = 10
self.thumbnailImageView.clipsToBounds = true
self.thumbnailImageView.layer.borderColor = UIColor.white.cgColor
self.thumbnailImageView.layer.borderWidth = 1
self.thumbnailImageView.image = image
self.nameLabel.text = self.tappedLocation[0].name
self.nameLabel.textColor = UIColor.white
self.priceLabel.text = self.tappedLocation[0].price
self.priceLabel.textColor = UIColor.white
self.displayRating(location: self.tappedLocation[0])
}
YelpClient.sharedInstance().getOpeningHoursFromID(id: self.tappedLocation[0].id, completionHandlerForOpeningHours: { (isOpenNow, error) in
if let error = error {
print("There was an error: \(String(describing: error))")
}
if let isOpenNow = isOpenNow {
performUIUpdatesOnMain {
if isOpenNow {
self.openLabel.text = "Open"
self.openLabel.textColor = UIColor.white
} else {
self.openLabel.text = "Closed"
self.openLabel.textColor = UIColor(red: 195/255, green: 89/255, blue: 75/255, alpha: 1.0)
self.openLabel.font = UIFont.systemFont(ofSize: 17.0, weight: .semibold)
}
}
}
})
})
locationView.isHidden = false
}
This print statement indicates the height of the UIView is changing height, but the x and y origins are not changing (the view should extend upwards to accommodate the word wrap in the name label):
Manual height manipulation doesn't work in autolayout. If you want to increase the height, create an IBOutlet to a height constraint and set its constant value. You can even animate it.
I have a custom UILabel based class, that does not display properly in the storyboard.
All it displays is a blank label. It happens with both XCode 6 and 7 beta.
import UIKit
#IBDesignable
class CCLabel: UILabel {
#IBInspectable var spacingChar: CGFloat = 0 {
didSet {
self.updateAttributed(self.text)
}
}
#IBInspectable var extraLineSpacing: CGFloat = 0 {
didSet {
self.updateAttributed(self.text)
}
}
override var bounds: CGRect {
didSet {
if (bounds.size.width != self.bounds.size.width) {
self.setNeedsUpdateConstraints()
}
super.bounds = bounds
}
}
override func updateConstraints() {
if (self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width
}
super.updateConstraints()
}
override var text: String? {
didSet {
if let txt = text {
self.updateAttributed(txt)
}
}
}
private func updateAttributed(text: String?) {
if (text == nil) {
return
}
if (extraLineSpacing > 0 || spacingChar > 0) {
let attrString = NSMutableAttributedString(string: text!)
let rng = NSMakeRange(0, attrString.length)
if (extraLineSpacing > 0) {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = extraLineSpacing
paragraphStyle.alignment = self.textAlignment
attrString.addAttribute(NSParagraphStyleAttributeName, value:paragraphStyle, range:rng)
}
if (spacingChar > 0) {
attrString.addAttribute(NSKernAttributeName, value:spacingChar, range:rng)
}
attrString.addAttribute(NSFontAttributeName, value: self.font, range: rng)
self.attributedText = attrString
}
}
}
That's the custom UILabel class in IB:
When I delete the name of the custom class label, it displays properly.
When I add it again, it does not disappear. However, the next time it's white again.
When you run the project, it's perfect. I suppose XCode is not rendering it properly, but cannot tell why.
I think you need to call this:
[super drawRect:rect];
When you subclass any other than UIView, you SHOULD CALL super.draw( _ ...) in your implementation. (https://developer.apple.com/documentation/uikit/uiview/1622529-drawrect)