IOS 13 UITextView with Html Attributed String embedded in UITableViewCell scrolling lag - ios

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
}
}
}

Related

Detect clicked mutable attributed string

I have this code to write a paragraph like book with numbering for each sentence , the problem I'm facing is i can't find how to color one sentence when the user clicks in any word from it
import UIKit
let descender: CGFloat = UIFont.systemFont(ofSize: 25).descender
class ViewController: UIViewController , UITextViewDelegate, UIGestureRecognizerDelegate {
var all = [NSMutableAttributedString]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let style = NSMutableParagraphStyle()
style.alignment = NSTextAlignment.justified
style.baseWritingDirection = .rightToLeft
style.lineBreakMode = .byWordWrapping
let myAttribute = [ NSAttributedString.Key.font: UIFont.systemFont(ofSize: 25)] // ,
// NSAttributedString.Key.paragraphStyle: style ,
// NSAttributedString.Key.baselineOffset: NSNumber(value: 0)]
let textView = UITextView(frame:CGRect(x: 20, y: 100, width: UIScreen.main.bounds.width - 40 , height: UIScreen.main.bounds.height))
let attributedString = NSMutableAttributedString()
Array(1..<50).forEach {
let small = $0 % 2 == 0 ? " long text part one long text part one long text part one long text part one long text part one long text part one long text part one long text part one long text part one " : "long text part two long text part twolong text part twolong text part twolong text part twolong text part twolong text part twolong text part two "
let attributedString2 = NSMutableAttributedString(string: small,attributes: myAttribute)
attributedString.append(attributedString2)
let textAttachment11 = SubTextAttachment()
textAttachment11.image = generateImageWithText(text: "\($0)")
let attrStringWithImage11 = NSAttributedString(attachment: textAttachment11)
attributedString.append(attrStringWithImage11)
}
textView.attributedText = attributedString;
self.view.addSubview(textView)
textView.isEditable = false
textView.isSelectable = true
textView.delegate = self
let tap = UITapGestureRecognizer(target: self, action: #selector(self.textTapped(_:)))
tap.delegate = self
textView.isUserInteractionEnabled = true
textView.addGestureRecognizer(tap)
}
func generateImageWithText(text: String) -> UIImage? {
let image = UIImage(named: "qqq")!
print(text," ",image.size)
let imageView = UIImageView(image: image)
imageView.backgroundColor = UIColor.clear
imageView.frame = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let label = UILabel(frame: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
label.font = UIFont.systemFont(ofSize: 50)
label.backgroundColor = UIColor.clear
label.textAlignment = .center
label.textColor = UIColor.black
label.text = text
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0)
imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
label.layer.render(in: UIGraphicsGetCurrentContext()!)
let imageWithText = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return imageWithText
}
#objc func textTapped(_ sender:UITapGestureRecognizer) {
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
return true
}
}
class SubTextAttachment:NSTextAttachment {
override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect {
let height = lineFrag.size.height
var scale: CGFloat = 1.0;
let imageSize = image!.size
if (height < imageSize.height) {
scale = height / imageSize.height
}
let value = CGRect(x: 0, y: descender, width: imageSize.width * scale, height: imageSize.height * scale)
return value
}
}
I know how to change the foreground color of any sub attributed string , but how i can know that the clicked part belong to the one to be colored ?
Also is there any better way to build this UI (in terms of performance ) as with tableView/CollectionView there is a dequeueing but here there isn't ?
So any hep is greatly appreciated
With NSAttributedString , you can use CoreText to render.
Convert NSAttributedString to CTFrame, then render it.
The key part
when you click a word in paragraph,
with override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
you can get a CGPoint
with that CGPoint & CTFrame, you can know the text range clicked in the text.
then rebuild the NSAttributedString 、CTFrame & rerender
here is the code you can refer
import UIKit
import CoreText
class TextRenderView: UIView {
let frameRef:CTFrame
let theSize: CGSize
let keyOne = //...
let keyTwo = //...
let rawTxt: String
let contentPage: NSAttributedString
let keyRanges: [Range<String.Index>]
override init(frame: CGRect){
rawTxt = //...
var tempRanges = [Range<String.Index>]()
if let rangeOne = rawTxt.range(of: keyOne){
tempRanges.append(rangeOne)
}
if let rangeTwo = rawTxt.range(of: keyTwo){
tempRanges.append(rangeTwo)
}
keyRanges = tempRanges
contentPage = NSAttributedString(string: rawTxt, attributes: [NSAttributedString.Key.font: UIFont.regular(ofSize: 15), NSAttributedString.Key.foregroundColor: UIColor.black])
let calculatedSize = contentPage.boundingRect(with: CGSize(width: UI.std.width - CGFloat(15 * 2), height: UI.std.height), options: [.usesFontLeading, .usesLineFragmentOrigin], context: nil).size
let padding: CGFloat = 10
theSize = CGSize(width: calculatedSize.width, height: calculatedSize.height + padding)
let framesetter = CTFramesetterCreateWithAttributedString(contentPage)
let path = CGPath(rect: CGRect(origin: CGPoint.zero, size: theSize), transform: nil)
frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
super.init(frame: frame)
backgroundColor = UIColor.white
}
required init?(coder: NSCoder) {
fatalError()
}
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else{
return
}
ctx.textMatrix = CGAffineTransform.identity
ctx.translateBy(x: 0, y: bounds.size.height)
ctx.scaleBy(x: 1.0, y: -1.0)
CTFrameDraw(frameRef, ctx)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else{
return
}
let pt = touch.location(in: self)
guard let offset = parserRect(with: pt, frame: frameRef), let pos = rawTxt.index(rawTxt.startIndex, offsetBy: offset, limitedBy: rawTxt.endIndex) else{
return
}
if keyRanges[0].contains(pos){
print(0)
}
else if keyRanges[1].contains(pos){
print(1)
}
}
func parserRect(with point: CGPoint, frame textFrame: CTFrame) -> Int?{
var result: Int? = nil
let path: CGPath = CTFrameGetPath(textFrame)
let bounds = path.boundingBox
guard let lines = CTFrameGetLines(textFrame) as? [CTLine] else{
return result
}
let lineCount = lines.count
guard lineCount > 0 else {
return result
}
var origins = [CGPoint](repeating: CGPoint.zero, count: lineCount)
CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), &origins)
for i in 0..<lineCount{
let baselineOrigin = origins[i]
let line = lines[i]
var ascent: CGFloat = 0
var descent: CGFloat = 0
var linegap: CGFloat = 0
let lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &linegap)
let lineFrame = CGRect(x: baselineOrigin.x, y: bounds.height-baselineOrigin.y-ascent, width: CGFloat(lineWidth), height: ascent+descent+linegap + 10)
if lineFrame.contains(point){
result = CTLineGetStringIndexForPosition(line, point)
break
}
}
return result
}
}
helper method:
extension String {
func range(ns inner: String) -> NSRange{
return (self as NSString).range(of: inner)
}
}
here is the github code you can refer

Get PDF from a UILabel text mask

I'm trying to get PDF from UIView with UILabel text mask.
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 200 ))
label.text = "Label Text"
label.font = UIFont.systemFont(ofSize: 25)
label.textAlignment = .center
label.textColor = UIColor.white
let overlayView = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 200 ))
overlayView.image = UIImage(named: "erasemask.png")
label.mask = overlayView
view_process.addSubview(label)
}
func exportAsPdfFromView(){
let pdfPageFrame = CGRect(x: 0, y: 0, width: view_process.bounds.size.width, height: view_process.bounds.size.height)
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, pdfPageFrame, nil)
UIGraphicsBeginPDFPageWithInfo(pdfPageFrame, nil)
guard let pdfContext = UIGraphicsGetCurrentContext() else { return "" }
view_process.layer.render(in: pdfContext)
UIGraphicsEndPDFContext()
let path = self.saveViewPdf(data: pdfData)
print(path)
}
func saveViewPdf(data: NSMutableData) -> String {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let docDirectoryPath = paths[0]
let pdfPath = docDirectoryPath.appendingPathComponent("viewPdf.pdf")
if data.write(to: pdfPath, atomically: true) {
return pdfPath.path
} else {
return ""
}
}
but I do not get PDF with mask. I don't want to convert UIView to UImage and then convert UImage to PDF. I want to editable PDF so don't want to convert into UIImage.
Can anyone help me How to convert Masked UILabel to PDF ?
here erasemask.png
You have to draw text yourself in the pdf to make it editable.
I have slightly modified your code, and it works. You can simply copy/paste in a new app view controller to check the result.
Here is a screenshot of the document opened in AffinityDesigner. The mask is correctly applied as a layer mask, and the text is editable.
It needs some tweaking to respect exact layout.
import UIKit
class ViewController: UIViewController {
var labelFrame: CGRect { return CGRect(x: 0, y: 0, width: 200, height: 200 )}
lazy var label: UILabel = {
let label = UILabel(frame: labelFrame)
label.text = "Label Text"
label.font = UIFont.systemFont(ofSize: 25)
label.textAlignment = .center
label.textColor = UIColor.black
return label
}()
var maskImage = UIImage(named: "erasemask.png")
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(label)
let overlayView = UIImageView(frame: labelFrame)
overlayView.image = maskImage
label.mask = overlayView
exportAsPdfFromView()
}
func exportAsPdfFromView() {
let pdfPageFrame = CGRect(x: 0, y: 0, width: view.bounds.size.width, height: view.bounds.size.height)
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, pdfPageFrame, nil)
UIGraphicsBeginPDFPageWithInfo(pdfPageFrame, nil)
guard let context = UIGraphicsGetCurrentContext() else { return }
// Clip context
if let overlayImage = maskImage?.cgImage {
context.clip(to: labelFrame, mask: overlayImage)
}
// Draw String
let string: String = label.text ?? ""
let attributes: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.foregroundColor: label.textColor ?? .black,
NSAttributedString.Key.font: label.font ?? UIFont.systemFont(ofSize: 25)
]
let stringRectSize = string.size(withAttributes: attributes)
let x = (labelFrame.width - stringRectSize.width) / 2
let y = (labelFrame.height - stringRectSize.height) / 2
let stringRect = CGRect(origin: CGPoint(x: x, y: y), size: stringRectSize)
string.draw(in: stringRect, withAttributes: attributes)
UIGraphicsEndPDFContext()
saveViewPdf(data: pdfData)
}
func saveViewPdf(data: NSMutableData) {
guard let docDirectoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let pdfPath = docDirectoryPath.appendingPathComponent("viewPdf.pdf")
print(pdfPath)
data.write(to: pdfPath, atomically: true)
}
}
EDITED
As a comparison, I have added the result by using the Apple view.render function, using code from the question ( with a blue background so we can see white text ). It clearly shows that this function does not support editable text and masking.
It exports the document as a stack of flat images and extra - useless - groups.
So I guess the only solution to keep pdf entities types is to compose the document by rendering each object.
If you need to export complex forms, it must not be too hard to make a helper that crawls in the view hierarchy and render each object content.
If you need to also render pdf with interactive buttons, you will probably need to use Annotations( available in PDFKit ). By the way you can also create editable fields with annotations, but I'm not sure it supports masking.
LAST EDIT:
Since you use an external framework to render pdf ( PDFGenerator ), it is different. You can only override the view.layer.render function that is used by the framework and use the context.clip function.
To export ui components as editable, I think they must have no superview. As soon as there is a view hierarchy, a backing bitmap is created, and all render calls are made with this bitmap as parameter, and not the PDFContext that was used to call the first render.
That's how PDFGenerator works, it crawls in the view hierarchy an render views by removing them from superview, render to the context, and move them back in hierarchy.
The big drawback of this is that it leads to bugs when the view is moved back in the hierarchy. Probably because constraints are lost, or some views like UIStackView behave differently. There is numerous opened issues around this on their github.
What I don't really understand yet is why the label below is rendered as editable even in a view hierarchy. I guess it's due to how the render function is done by Apple. Can't go further on this right now..
Custom Field Example:
It may need more work to respect exact field layout, but this is a basis for a view that is rendered as editable text. It gives the exact same result as in the first part of the answer.
It works with any mask view, not only UIImage.
It is rendered as editable even in a hierarchy.
So if you use this label instead of UILabel in the initial code of this question, it works.
// Extension to get mask view as an image
extension UIView {
var cgImage: CGImage? {
UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, contentScaleFactor);
guard let context = UIGraphicsGetCurrentContext() else { return nil }
layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image?.cgImage
}
}
class PMLayer: CALayer {
// Needed to access text style
weak var label: UILabel?
override init() { super.init() }
required init?(coder: NSCoder) { super.init(coder: coder) }
override init(layer: Any) {
super.init(layer: layer)
if let pm = layer as? PMLayer {
label = pm.label
}
}
override func render(in ctx: CGContext) {
guard let label = label else { return }
// Clip context
if let mask = label.mask?.cgImage {
ctx.clip(to: label.frame, mask: mask)
}
// Draw String
let string: String = label.text ?? ""
let attributes: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.foregroundColor: label.textColor ?? .black,
NSAttributedString.Key.font: label.font ?? UIFont.systemFont(ofSize: 25)
]
let stringRectSize = string.size(withAttributes: attributes)
let x = (label.frame.width - stringRectSize.width) / 2
let y = (label.frame.height - stringRectSize.height) / 2
let stringRect = CGRect(origin: CGPoint(x: x, y: y), size: stringRectSize)
string.draw(in: stringRect, withAttributes: attributes)
}
}
class PMLabel: UILabel {
override class var layerClass: AnyClass { return PMLayer.self }
// Be sure the label is correctly linked to the layer when we access it
override var layer: CALayer {
(super.layer as? PMLayer)?.label = self
return super.layer
}
}
this is my decision
import UIKit
import PDFKit
class ViewController: UIViewController {
#IBOutlet weak var view_process: UIView!
#IBOutlet weak var pdf_view: UIView!
let documentInteractionController = UIDocumentInteractionController()
#IBAction func bClick(_ sender: Any) {
exportAsPdfFromView()
}
override func viewDidLoad() {
super.viewDidLoad()
documentInteractionController.delegate = self
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 200 ))
label.text = "Label Text"
label.font = UIFont.systemFont(ofSize: 25)
label.textAlignment = .center
label.textColor = UIColor.blue
let overlayView = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 200 ))
overlayView.image = UIImage(named: "erasemask.png")
label.mask = overlayView
view_process.addSubview(label)
}
func exportAsPdfFromView(){
let pdfPageFrame = view_process.bounds//.CGRect(x: 0, y: 0, width: view_process.bounds.size.width, height: view_process.bounds.size.height)
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, pdfPageFrame, nil)
UIGraphicsBeginPDFPageWithInfo(pdfPageFrame, nil)
let pdfContext = UIGraphicsGetCurrentContext()!
view_process.layer.render(in: pdfContext)
UIGraphicsEndPDFContext()
let path = self.saveViewPdf(data: pdfData)
print(path)
self.share(url: URL(string: "file://\(path)")!)
}
func saveViewPdf(data: NSMutableData) -> String {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let docDirectoryPath = paths[0]
let pdfPath = docDirectoryPath.appendingPathComponent("viewPdf.pdf")
if data.write(to: pdfPath, atomically: true) {
return pdfPath.path
} else {
return ""
}
}
func openPdf(path: String) {
let pdfView = PDFView(frame: self.view.bounds)
pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
pdf_view.addSubview(pdfView)
// Fit content in PDFView.
pdfView.autoScales = true
// Load Sample.pdf file from app bundle.
let fileURL = URL(string: path)
pdfView.document = PDFDocument(url: fileURL!)
}
func share(url: URL) {
documentInteractionController.url = url
// documentInteractionController.uti = url.typeIdentifier ?? "public.data, public.content"
// documentInteractionController.name = url.localizedName ?? url.lastPathComponent
documentInteractionController.presentPreview(animated: true)
}
}
extension ViewController: UIDocumentInteractionControllerDelegate {
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
guard let navVC = self.navigationController else {
return self
}
return navVC
}
}

Resize font along with frame of label using pinch gesture on UILabel?

Increase or decrease font size smoothly whenever user resize label using pinch gesture on it.
Note
Without compromising quality of font
Not only transforming the scale of UILabel
With support of multiline text
Rotation gesture should work proper with pinch gesture
Reference: SnapChat or Instagram Text Editor tool
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: UIFont(name: font.fontName, size: font.pointSize)!], context: nil)
return ceil(boundingBox.height)
}
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: UIFont(name: font.fontName, size: font.pointSize)!], context: nil)
return ceil(boundingBox.width)
}
}
func resizeLabelToText(textLabel : UILabel)
{
let labelFont = textLabel.font
let labelString = textLabel.text
let labelWidth : CGFloat = labelString!.width(withConstrainedHeight: textLabel.frame.size.height, font: labelFont!)
let labelHeight : CGFloat = labelString!.height(withConstrainedWidth: labelWidth, font: labelFont!)
textLabel.frame = CGRect(x: textLabel.frame.origin.x, y: textLabel.frame.origin.y, width: labelWidth, height: labelHeight)
textLabel.font = labelFont
}
func pinchedRecognize(_ pinchGesture: UIPinchGestureRecognizer) {
guard pinchGesture.view != nil else {return}
if (pinchGesture.view is UILabel) {
let selectedTextLabel = pinchGesture.view as! UILabel
if pinchGesture.state == .began || pinchGesture.state == .changed {
let pinchScale = round(pinchGesture.scale * 1000) / 1000.0
if (pinchScale < 1) {
selectedTextLabel.font = selectedTextLabel.font.withSize(selectedTextLabel.font.pointSize - pinchScale)
}
else {
selectedTextLabel.font = selectedTextLabel.font.withSize(selectedTextLabel.font.pointSize + pinchScale)
}
resizeLabelToText(textLabel: selectedTextLabel)
}
}
}
I solved the problem with following code which is working fine with every aspect which are mentioned in question, similar to Snapchat and Instagram:
var pointSize: CGFloat = 0
#objc func pinchRecoginze(_ pinchGesture: UIPinchGestureRecognizer) {
guard pinchGesture.view != nil else {return}
let view = pinchGesture.view!
if (pinchGesture.view is UILabel) {
let textLabel = view as! UILabel
if pinchGesture.state == .began {
let font = textLabel.font
pointSize = font!.pointSize
pinchGesture.scale = textLabel.font!.pointSize * 0.1
}
if 1 <= pinchGesture.scale && pinchGesture.scale <= 10 {
textLabel.font = UIFont(name: textLabel.font!.fontName, size: pinchGesture.scale * 10)
resizeLabelToText(textLabel: textLabel)
}
}
}
func resizeLabelToText(textLabel : UILabel) {
let labelSize = textLabel.intrinsicContentSize
textLabel.bounds.size = labelSize
}
Call following method every time after UILabel size changes.
func labelSizeHasBeenChangedAfterPinch(_ label:UILabel, currentSize:CGSize){
let MAX = 25
let MIN = 8
let RATE = -1
for proposedFontSize in stride(from: MAX, to: MIN, by: RATE){
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attribute = [NSAttributedString.Key.font:UIFont.systemFont(ofSize: CGFloat(proposedFontSize))]
// let context = IF NEEDED ...
let rect = NSString(string: label.text ?? "").boundingRect(with: currentSize, options: options, attributes: attribute, context: nil)
let labelSizeThatFitProposedFontSize = CGSize(width: rect.width , height: rect.height)
if (currentSize.height > labelSizeThatFitProposedFontSize.height) && (currentSize.width > labelSizeThatFitProposedFontSize.width){
DispatchQueue.main.async {
label.font = UIFont.systemFont(ofSize: CGFloat(proposedFontSize))
}
break
}
}
}
you can try:
1 - Set maximum font size for this label
2 - Set line break to Truncate Tail
3 - Set Autoshrink to Minimum font size (minimum size)

UIView Height Change Not Reflected in UI

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.

Force custom view to redraw (or invalidate) using a timer

I've created a custom widget, which is animated. Now my problem is that I can't redraw the view, when the corresponding data gets updated.
Just don't hold anything against me in the code. This is my first piece of code in swift and I haven't worked with neither swift nor with Objective-C :-D
And also I've read the following questions, but they didn't help me:
How to force a view to render itself?
what-is-the-most-robust-way-to-force-a-uiview-to-redraw
p.s. : I can see the output of print(digit.phase) in the console.
p.s.s: I've also used performSelectorOnMainThread for calling the setNeedsDisplay function
The code:
import UIKit
struct Digit {
var targetDigit: Int
var currentDigit: Int
var phase: Float
}
#IBDesignable class RollerCounter: UIView {
var view: UIView!
var viewRect: CGRect!
var intNumber: Int
var digits = [Digit]()
let baseY = 20
var timer: NSTimer?
#IBInspectable var number: Int {
get {
return intNumber
}
set(number) {
intNumber = number
digits = []
var tempNumber:Int = intNumber
while tempNumber > 0 {
digits.append(Digit(targetDigit: tempNumber % 10, currentDigit: Int(rand()) % 10, phase: 0.0))
tempNumber /= 10
}
}
}
//init
override init(frame: CGRect) {
// set properties:
intNumber = 1111
super.init(frame: frame)
// setup the thing!
setup()
}
required init?(coder aDecoder: NSCoder) {
intNumber = 1111
super.init(coder: aDecoder)
// setup the thing
setup()
}
// Inital setup
func setup() {
let viewRect = CGRect(x: 0, y: 0, width: 280, height: 40)
view = UIView(frame: viewRect)
view.frame = bounds
view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
addSubview(view)
self.setNeedsDisplay()
backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.0)
}
func animate() {
timer = NSTimer.scheduledTimerWithTimeInterval(0.016, target: self, selector: Selector("tick"), userInfo: nil, repeats: true)
}
func tick() {
for var digit in digits {
digit.phase += Float(rand() % 100) / 100
print(digit.phase)
}
setNeedsDisplay()
//TEST: Also tested this
// if let rect = viewRect {
// drawRect(rect)
// } else {
// viewRect = CGRect(x: 0, y: 0, width: 280, height: 40)
// drawRect(viewRect
// }
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
var tempNumber: Int = number
let strTempNumber = String(tempNumber)
var index: Int = 1
let width = Float(rect.width)
let charWidth: Float = Float(rect.width) / Float(strTempNumber.characters.count)
let charHeight: CGFloat = 36
let color = UIColor.blackColor()
let font: UIFont = UIFont(name: "Helvetica Neue", size: charHeight)!
let paraStyle = NSMutableParagraphStyle()
paraStyle.lineSpacing = 6.0
let skew = 0.1
let textAttribs = [
NSForegroundColorAttributeName: color,
NSParagraphStyleAttributeName: paraStyle,
NSObliquenessAttributeName: skew,
NSFontAttributeName: font
]
for digit in digits {
let strCurrentDigit: NSString = String(digit.currentDigit) as NSString
let strNextDigit: NSString = String(digit.currentDigit - 1) as NSString
let xPos = width - Float(index) * charWidth
let yPos = Float(baseY) + Float(charHeight) * digit.phase
let point: CGPoint = CGPoint(x: Int(xPos), y: Int(yPos))
strCurrentDigit.drawAtPoint(point, withAttributes: textAttribs)
let nextDigitYPos = yPos - Float(charHeight) * 1.2
let nextDigitPoint: CGPoint = CGPoint(x: Int(xPos), y: Int(nextDigitYPos))
strNextDigit.drawAtPoint(nextDigitPoint, withAttributes: textAttribs)
index++
tempNumber /= 10
}
}
}
Sorry folks. My bad :-(
There's nothing wrong with the invalidation system. Here's what's wrong:
for var digit in digits {
digit.phase += Float(rand() % 100) / 100
print(digit.phase)
}
As it turns out, the changes to phase only get reflected in the local digit instance inside the for loop
But just to be clear, the setNeedsDisplay() call inside the tick method is crucial for the view to be updated.

Resources