iOS TextView is saved blurry when scaled - ios

I tried save textview as image with not device scale. I implemented a method to save an image by adding an arbitrary textview according to the UI value. Because when I tried save image using drawHierarchy method in up scale, image was blurry.
Condition when textview is saved blurry
not device scale (up scale)
1-1. isScrollEnabled = false and height of textview is more than 128.
1-2. isScrollEnabled = true (always text is blurry)
here is my code
func drawQuoteImage() {
var campusSize = view.frame.size
var scale = UIScreen.main.scale + 2
// 1. Create View
let quoteView = UIView(frame: CGRect(x: 0, y: 0, width: campusSize.width, height: campusSize.height))
let textview = UITextView()
textview.attributedText = NSAttributedString(string: quoteLabel.text, attributes: textAttributes as [NSAttributedString.Key : Any])
textview.frame = transfromFrame(originalFrame: quoteLabel.frame, campusSize: campusSize)
quoteView.addSubview(textview)
// 2. Render image
UIGraphicsBeginImageContextWithOptions(quoteView.frame.size, false, scale)
let context = UIGraphicsGetCurrentContext()!
context.setRenderingIntent(.relativeColorimetric)
context.interpolationQuality = .high
quoteView.drawHierarchy(in: quoteView.frame, afterScreenUpdates: true)
quoteView.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
quoteImage = image
}
private func transfromFrame(originalFrame: CGRect, campusSize: CGSize) -> CGRect
{
if UIDevice.current.screenType == .iPhones_X_XS {
return CGRect(x: round(originalFrame.origin.x), y: round(originalFrame.origin.y), width: round(originalFrame.width), height: round(originalFrame.height))
}
else {
var frame = CGRect()
let ratioBasedOnWidth = campusSize.width / editView.frame.width
let ratioBasedOnHeight = campusSize.height / editView.frame.height
frame.size.width = round(originalFrame.width * ratioBasedOnWidth)
frame.size.height = round(originalFrame.height * ratioBasedOnHeight)
frame.origin.x = round(originalFrame.origin.x * ratioBasedOnWidth)
frame.origin.y = round(originalFrame.origin.y * ratioBasedOnHeight)
return frame
}
}
Wired Point
when height of textview is more than 128, textview is save blurry. I found related value when I put textview default height is 128.
Height is 128 or less (when isScrollEnabled is false), textview is saved always clear. But when height is more than 128, it looks blurry.
Height 128
Height 129
I'd like to know how to clearly draw image with textview at #5x scale. (textview height is bigger than 128)

Here's a quick example using a UIView extension from this accepted answer: https://stackoverflow.com/a/51944513/6257435
We'll create a UITextView with a size of 240 x 129. Then add 4 buttons to capture the text view at 1x, 2x, 5x and 10x scale.
It looks like this when running:
and the result...
At 1x scale - 240 x 129 pixels:
At 2x scale - 480 x 258 pixels:
At 5x scale - 1200 x 645 pixels (just showing a portion):
At 10x scale - 2400 x 1290 pixels (just showing a portion):
The extension:
extension UIView {
func scale(by scale: CGFloat) {
self.contentScaleFactor = scale
for subview in self.subviews {
subview.scale(by: scale)
}
}
func getImage(scale: CGFloat? = nil) -> UIImage {
let newScale = scale ?? UIScreen.main.scale
self.scale(by: newScale)
let format = UIGraphicsImageRendererFormat()
format.scale = newScale
let renderer = UIGraphicsImageRenderer(size: self.bounds.size, format: format)
let image = renderer.image { rendererContext in
self.layer.render(in: rendererContext.cgContext)
}
return image
}
}
Sample controller code:
class TextViewCapVC: UIViewController {
let textView = UITextView()
let resultLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
// add a stack view with buttons
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 12
[1, 2, 5, 10].forEach { i in
let btn = UIButton()
btn.setTitle("Create Image at \(i)x scale", for: [])
btn.setTitleColor(.white, for: .normal)
btn.setTitleColor(.lightGray, for: .highlighted)
btn.backgroundColor = .systemBlue
btn.tag = i
btn.addTarget(self, action: #selector(gotTap(_:)), for: .touchUpInside)
stack.addArrangedSubview(btn)
}
[textView, stack, resultLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// text view 280x240, 20-points from top, centered horizontally
textView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
textView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
textView.widthAnchor.constraint(equalToConstant: 240.0),
textView.heightAnchor.constraint(equalToConstant: 129.0),
// stack view, 20-points from text view, same width, centered horizontally
stack.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 20.0),
stack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
stack.widthAnchor.constraint(equalTo: textView.widthAnchor),
// result label, 20-points from stack view
// 20-points from leading/trailing
resultLabel.topAnchor.constraint(equalTo: stack.bottomAnchor, constant: 20.0),
resultLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
resultLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
let string = "Test"
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.blue,
.font: UIFont.italicSystemFont(ofSize: 104.0),
]
let attributedString = NSMutableAttributedString(string: string, attributes: attributes)
textView.attributedText = attributedString
resultLabel.font = .systemFont(ofSize: 14, weight: .light)
resultLabel.numberOfLines = 0
resultLabel.text = "Results:"
// so we can see the view frames
textView.backgroundColor = .yellow
resultLabel.backgroundColor = .cyan
}
#objc func gotTap(_ sender: Any?) {
guard let btn = sender as? UIButton else { return }
let scaleFactor = CGFloat(btn.tag)
let img = textView.getImage(scale: scaleFactor)
var s: String = "Results:\n\n"
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fName: String = "\(btn.tag)xScale-\(img.size.width * img.scale)x\(img.size.height * img.scale).png"
let url = documents.appendingPathComponent(fName)
if let data = img.pngData() {
do {
try data.write(to: url)
} catch {
s += "Unable to Write Image Data to Disk"
resultLabel.text = s
return
}
} else {
s += "Could not get png data"
resultLabel.text = s
return
}
s += "Logical Size: \(img.size)\n\n"
s += "Scale: \(img.scale)\n\n"
s += "Pixel Size: \(CGSize(width: img.size.width * img.scale, height: img.size.height * img.scale))\n\n"
s += "File \"\(fName)\"\n\nsaved to Documents folder\n"
resultLabel.text = s
// print the path to documents in debug console
// so we can copy/paste into Finder to get to the files
print(documents.path)
}
}

Related

How to fade out end of last line in multiline label?

Note, that it must work with different number of lines in UILabel - 1,2,3 etc.
I've already found solution for 1 line label, where you mask UILabel's layer with CAGradientLayer, but it doesn't work for multiline labels, as it masks the whole layer and fades out all lines.
I tried to make another CALayer with position calculated to be in the position of last line with desired width and used CAGradientLayer as mask and add this layer as sublayer of UILabel, it worked for static objects, but i use this UILabel in UITableViewCell and when it's tapped - it changes color to gray and i can see my layer, because it uses background color of UILabel when view layout its subviews, and also something wrong with x position calculation:
extension UILabel {
func fadeOutLastLineEnd() { //Call in layoutSubviews
guard bounds.width > 0 else { return }
lineBreakMode = .byCharWrapping
let tmpLayer = CALayer()
let gradientWidth: CGFloat = 32
let numberOfLines = CGFloat(numberOfLines)
tmpLayer.backgroundColor = UIColor.white.cgColor
tmpLayer.frame = CGRect(x: layer.frame.width - gradientWidth,
y: layer.frame.height / numberOfLines,
width: gradientWidth,
height: layer.frame.height / numberOfLines)
let tmpGrLayer = CAGradientLayer()
tmpGrLayer.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]
tmpGrLayer.startPoint = CGPoint(x: 1, y: 0)
tmpGrLayer.endPoint = CGPoint(x: 0, y: 0)
tmpGrLayer.frame = tmpLayer.bounds
tmpLayer.mask = tmpGrLayer
layer.addSublayer(tmpLayer)
}
}
So, i need :
which can be multiline
end of last line needs to be faded out (gradient?)
works in UITableViewCell, when the whole object changes color
There are various ways to do this -- here's one approach.
We can mask a view by setting the layer.mask. The opaque areas of the mask will show-through, and the transparent areas will not.
So, what we need is a custom layer subclass that will look like this:
This is an example that I'll call InvertedGradientLayer:
class InvertedGradientLayer: CALayer {
public var lineHeight: CGFloat = 0
public var gradWidth: CGFloat = 0
override func draw(in inContext: CGContext) {
// fill all but the bottom "line height" with opaque color
inContext.setFillColor(UIColor.gray.cgColor)
var r = self.bounds
r.size.height -= lineHeight
inContext.fill(r)
// can be any color, we're going from Opaque to Clear
let colors = [UIColor.gray.cgColor, UIColor.gray.withAlphaComponent(0.0).cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations)!
// start the gradient "grad width" from right edge
let startPoint = CGPoint(x: bounds.maxX - gradWidth, y: 0.5)
// end the gradient at the right edge, but
// probably want to leave the farthest-right 1 or 2 points
// completely transparent
let endPoint = CGPoint(x: bounds.maxX - 2.0, y: 0.5)
// gradient rect starts at the bottom of the opaque rect
r.origin.y = r.size.height - 1
// gradient rect height can extend below the bounds, becuase it will be clipped
r.size.height = bounds.height
inContext.addRect(r)
inContext.clip()
inContext.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsBeforeStartLocation)
}
}
Next, we'll make a UILabel subclass that implements that InvertedGradientLayer as a layer mask:
class CornerFadeLabel: UILabel {
let ivgLayer = InvertedGradientLayer()
override func layoutSubviews() {
super.layoutSubviews()
guard let f = self.font, let t = self.text else { return }
// we only want to fade-out the last line if
// it would be clipped
let constraintRect = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
let boundingBox = t.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font : f], context: nil)
if boundingBox.height <= bounds.height {
layer.mask = nil
return
}
layer.mask = ivgLayer
ivgLayer.lineHeight = f.lineHeight
ivgLayer.gradWidth = 60.0
ivgLayer.frame = bounds
ivgLayer.setNeedsDisplay()
}
}
and here is a sample view controller showing it in use:
class FadeVC: UIViewController {
let wordWrapFadeLabel: CornerFadeLabel = {
let v = CornerFadeLabel()
v.numberOfLines = 1
v.lineBreakMode = .byWordWrapping
return v
}()
let charWrapFadeLabel: CornerFadeLabel = {
let v = CornerFadeLabel()
v.numberOfLines = 1
v.lineBreakMode = .byCharWrapping
return v
}()
let normalLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 1
return v
}()
let numLinesLabel: UILabel = {
let v = UILabel()
v.textAlignment = .center
return v
}()
var numLines: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let sampleText = "This is some example text that will wrap onto multiple lines and fade-out the bottom-right corner instead of truncating or clipping a last line."
wordWrapFadeLabel.text = sampleText
charWrapFadeLabel.text = sampleText
normalLabel.text = sampleText
let stack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.spacing = 8
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let bStack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.spacing = 8
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let btnUP: UIButton = {
let v = UIButton()
let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
let img = UIImage(systemName: "chevron.up.circle.fill", withConfiguration: cfg)
v.setImage(img, for: [])
v.tintColor = .systemGreen
v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
v.addTarget(self, action: #selector(btnUpTapped), for: .touchUpInside)
return v
}()
let btnDown: UIButton = {
let v = UIButton()
let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
let img = UIImage(systemName: "chevron.down.circle.fill", withConfiguration: cfg)
v.setImage(img, for: [])
v.tintColor = .systemGreen
v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
v.addTarget(self, action: #selector(btnDownTapped), for: .touchUpInside)
return v
}()
bStack.addArrangedSubview(btnUP)
bStack.addArrangedSubview(numLinesLabel)
bStack.addArrangedSubview(btnDown)
let v1 = UILabel()
v1.text = "Word-wrapping"
v1.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let v2 = UILabel()
v2.text = "Character-wrapping"
v2.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
let v3 = UILabel()
v3.text = "Normal Label (Truncate Tail)"
v3.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
stack.addArrangedSubview(bStack)
stack.addArrangedSubview(v1)
stack.addArrangedSubview(wordWrapFadeLabel)
stack.addArrangedSubview(v2)
stack.addArrangedSubview(charWrapFadeLabel)
stack.addArrangedSubview(v3)
stack.addArrangedSubview(normalLabel)
stack.setCustomSpacing(20, after: bStack)
stack.setCustomSpacing(20, after: wordWrapFadeLabel)
stack.setCustomSpacing(20, after: charWrapFadeLabel)
view.addSubview(stack)
// dashed border views so we can see the lable frames
let wordBorderView = DashedView()
let charBorderView = DashedView()
let normalBorderView = DashedView()
wordBorderView.translatesAutoresizingMaskIntoConstraints = false
charBorderView.translatesAutoresizingMaskIntoConstraints = false
normalBorderView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(wordBorderView)
view.addSubview(charBorderView)
view.addSubview(normalBorderView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
wordBorderView.topAnchor.constraint(equalTo: wordWrapFadeLabel.topAnchor, constant: 0.0),
wordBorderView.leadingAnchor.constraint(equalTo: wordWrapFadeLabel.leadingAnchor, constant: 0.0),
wordBorderView.trailingAnchor.constraint(equalTo: wordWrapFadeLabel.trailingAnchor, constant: 0.0),
wordBorderView.bottomAnchor.constraint(equalTo: wordWrapFadeLabel.bottomAnchor, constant: 0.0),
charBorderView.topAnchor.constraint(equalTo: charWrapFadeLabel.topAnchor, constant: 0.0),
charBorderView.leadingAnchor.constraint(equalTo: charWrapFadeLabel.leadingAnchor, constant: 0.0),
charBorderView.trailingAnchor.constraint(equalTo: charWrapFadeLabel.trailingAnchor, constant: 0.0),
charBorderView.bottomAnchor.constraint(equalTo: charWrapFadeLabel.bottomAnchor, constant: 0.0),
normalBorderView.topAnchor.constraint(equalTo: normalLabel.topAnchor, constant: 0.0),
normalBorderView.leadingAnchor.constraint(equalTo: normalLabel.leadingAnchor, constant: 0.0),
normalBorderView.trailingAnchor.constraint(equalTo: normalLabel.trailingAnchor, constant: 0.0),
normalBorderView.bottomAnchor.constraint(equalTo: normalLabel.bottomAnchor, constant: 0.0),
])
// set initial number of lines to 1
btnUpTapped()
}
#objc func btnUpTapped() {
numLines += 1
numLinesLabel.text = "Num Lines: \(numLines)"
wordWrapFadeLabel.numberOfLines = numLines
charWrapFadeLabel.numberOfLines = numLines
normalLabel.numberOfLines = numLines
}
#objc func btnDownTapped() {
if numLines == 1 { return }
numLines -= 1
numLinesLabel.text = "Num Lines: \(numLines)"
wordWrapFadeLabel.numberOfLines = numLines
charWrapFadeLabel.numberOfLines = numLines
normalLabel.numberOfLines = numLines
}
}
When running, it looks like this:
The red dashed borders are there just so we can see the frames of the labels. Tapping the up/down arrows will increment/decrement the max number of lines to show in each label.
You should create a CATextLayer with the same text properties as your UILabel.
Fill it with the end of your text you wish to fade.
Then calculate the position of this text segment in your UILabel.
Finally overlay the two.
Here are some aspect explained.

iOS -How to To Set Anchors to Adjust Spacing for Line Break with Self Sizing CollectionView Cells

When adjusting a label's text for self seizing collectionView cells I use the following function in sizeForItem:
func estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) -> CGFloat {
let size = CGSize(width: width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attributes = [NSAttributedStringKey.font: font]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
The function works fine and my cells expand accordingly.
What has happened though is there are several line breaks inside some of the text from the pricesLabel that get fed into the function. The line breaks are too "tight" so I followed this answer when creating my label to expand the spacing in between the line breaks.
let attributedString = NSMutableAttributedString(string: pricesText)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 7 // if I set this at 2 I have no problems
attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedString.length))
The problem is the text from pricesLabel inside the cell expands to far downwards. Using paragraphStyle.lineSpacing = 7 creates the space I want but it causes problems. If I set this to paragraphStyle.lineSpacing = 2 have no problems but the spacing is to tight.
As you can see in the picture the cell sizes the way it's supposed to but the the line break spacing in between the $8.00 and $12.00 makes the text expand to far and the text of $20.00 from the computedTotalLabel gets obscured.
I called sizeToFit() in layoutSubViews() but it made no difference:
override func layoutSubviews() {
super.layoutSubviews()
pricesLabel.sizeToFit()
computedTotalLabel.sizeToFit()
}
How can I make the pricesLabel text adjusted with the line breaks size itself accordingly
class MyCell: UICollectionViewCell {
let pricesLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .right
label.sizeToFit()
label.font = UIFont.systemFont(ofSize: 15.5)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5
label.numberOfLines = 0
label.sizeToFit()
return label
}()
let computedTotalLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .right
label.textColor = .black
label.sizeToFit()
label.font = UIFont.boldSystemFont(ofSize: 15.5)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5
label.numberOfLines = 1
label.sizeToFit()
return label
}()
let staticTotalLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Total"
label.textAlignment = .left
label.textColor = .black
label.font = UIFont.boldSystemFont(ofSize: 15.5)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5
label.numberOfLines = 1
label.sizeToFit()
return label
}()
let separatorLine: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .lightGray
return view
}()
override func layoutSubviews() {
super.layoutSubviews()
pricesLabel.sizeToFit()
computedTotalLabel.sizeToFit()
}
var myObject: MyObject? {
didSet {
// text is "$8.00\n$12.00\n"
let pricesText = myObject?.myText ?? "error"
let attributedString = NSMutableAttributedString(string: pricesText, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 15.5)])
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 7
attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedString.length))
pricesLabel.attributedText = attributedString
computedTotalLabel.text = functionThatTalliesUpAllThePrices(pricesText)
configureAnchors()
}
}
func configureAnchors() {
addSubview(pricesLabel)
addSubview(totalLabel)
addSubview(staticTotalLabel) // this is the label on the left side of the pic that says Total:
addSubview(separatorLine)
pricesLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 12).isActive = true
pricesLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
staticTotalLabel.lastBaselineAnchor.constraint(equalTo: totalLabel.lastBaselineAnchor).isActive = true
staticTotalLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
staticTotalLabel.rightAnchor.constraint(equalTo: totalLabel.leftAnchor, constant: -10).isActive = true
computedTotalLabel.topAnchor.constraint(equalTo: pricesLabel.bottomAnchor, constant: 0).isActive = true
computedTotalLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
separatorLine.topAnchor.constraint(equalTo: computedTotalLabel.bottomAnchor, constant: 12).isActive = true
separatorLine.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 10).isActive = true
separatorLine.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
separatorLine.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
separatorLine.heightAnchor.constraint(equalToConstant: 1).isActive = true
}
}
This is the sizeForItem inside the collectionView cell. Not sure if this makes a difference to the problem so I added it anyway
class MyClass: UIViewController {
let tableData = [MyObect]()
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let myObect = tableData[indexPath.item]
// text is "$8.00\n$12.00\n"
let pricesText = myObject?.myText ?? "error"
let width = collectionView.frame.width
let pricesLabelHeight = estimatedLabelHeight(text: pricesText, width: width, font: UIFont.systemFont(ofSize: 15.5))
let total = functionThatTalliesUpAllThePrices(pricesText)
let totalLabelHeight = estimatedLabelHeight(text: functionThatAddsUp, width: width, font: UIFont.boldSystemFont(ofSize: 15.5))
// the 12 + 0 + 12 + 1 are the constant sizes I use inside the cell's configureAnchors functions
let cellHeight = 12 + pricesLabelHeight + 0 + totalLabelHeight + 12 + 1
return CGSize(width: width, height: ceil(cellHeight))
}
}
1st. I had to place the same estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) inside the collectionView cell itself.
2nd. Inside the configureAnchors functions at the bottom of it I call pricesLabel.sizeToFit() and pricesLabel.layoutSubviews() and then I call the above function from step 1 to get the height of the pricesLabel from it's text.
3rd. I set the pricesLabel.heightAnchor.constraint(equalToConstant:) to the the height returned from step 2.
class MyCell: UICollectionViewCell {
// step 1. place this function inside the collectionView cell
func estimatedLabelHeight(text: String, width: CGFloat, font: UIFont) -> CGFloat {
let size = CGSize(width: width, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union([.usesLineFragmentOrigin, .usesFontLeading])
let attributes = [NSAttributedStringKey.font: font]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes, context: nil).height
return rectangleHeight
}
func configureAnchors() {
// all the other anchors are here
pricesLabel.sizeToFit()
computedTotalLabel.sizeToFit()
computedTotalLabel.layoutIfNeeded()
pricesLabel.layoutIfNeeded()
let pricesLabelText = pricesLabel.text ?? "error"
let width = self.frame.width
// step 2.
let pricesLabelHeight = estimatedLabelHeight(text: pricesLabelText, width: width, font: UIFont.systemFont(ofSize: 15.5))
// step 3.
pricesLabel.heightAnchor.constraint(equalToConstant: pricesLabelHeight).isActive = true
}
}

Wrapping an iOS UILabel as a block within a constrained UIView

I have a couple of UILabels within an UIView.
I constrain that containing view to 100px. If both the UILabels have an intrinsic width of 75px each (because of their content) what I would like is that the second label drops below the first because it cannot display without wrapping it's own text.
Is there a containing View in iOS that would support that behaviour?
Here is one example of "wrapping" labels based on fitting into the width of a parent view.
You can run this directly in a Playground page... Tap the red "Tap Me" button to toggle the text in the labels, to see how they "fit".
import UIKit
import PlaygroundSupport
// String extension for easy text width/height calculations
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: [.font: font], 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: [.font: font], context: nil)
return ceil(boundingBox.width)
}
}
class TestViewController : UIViewController {
let btn: UIButton = {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Tap Me", for: .normal)
b.backgroundColor = .red
return b
}()
let cView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
return v
}()
let labelA: UILabel = {
let v = UILabel()
// we will be explicitly setting the label's frame
v.translatesAutoresizingMaskIntoConstraints = true
v.backgroundColor = .yellow
return v
}()
let labelB: UILabel = {
let v = UILabel()
// we will be explicitly setting the label's frame
v.translatesAutoresizingMaskIntoConstraints = true
v.backgroundColor = .cyan
return v
}()
// spacing between labels when both fit on one line
let spacing: CGFloat = 8.0
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(btn)
btn.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
btn.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
btn.topAnchor.constraint(equalTo: view.topAnchor, constant: 20.0).isActive = true
// add the "containing" view
view.addSubview(cView)
// add the two labels to the containing view
cView.addSubview(labelA)
cView.addSubview(labelB)
// constrain containing view Left-20, Top-20 (below the button)
cView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
cView.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0).isActive = true
// containing view has a fixed width of 100
cView.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
// constrain bottom of containing view to bottom of labelB (so the height auto-sizes)
cView.bottomAnchor.constraint(equalTo: labelB.bottomAnchor, constant: 0.0).isActive = true
// initial text in the labels - both will fit "on one line"
labelA.text = "First"
labelB.text = "Short"
}
func updateLabels() -> Void {
// get the label height based on its font
if let h = labelA.text?.height(withConstrainedWidth: CGFloat.greatestFiniteMagnitude, font: labelA.font) {
// get the calculated width of each label
if let wA = labelA.text?.width(withConstrainedHeight: h, font: labelA.font),
let wB = labelB.text?.width(withConstrainedHeight: h, font: labelB.font) {
// labelA frame will always start at 0,0
labelA.frame = CGRect(x: 0.0, y: 0.0, width: wA, height: h)
// will both labels + spacing fit in the containing view's width?
if wA + wB + spacing <= cView.frame.size.width {
// yes, so place labelB to the right of labelA (put them on "one line")
labelB.frame = CGRect(x: wA + spacing, y: 0.0, width: wB, height: h)
} else {
// no, so place labelB below labelA ("wrap" labelB "to the next line")
labelB.frame = CGRect(x: 0.0, y: h, width: wB, height: h)
}
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateLabels()
}
#objc func didTap(_ sender: Any?) -> Void {
// toggle labelB's text
if labelB.text == "Short" {
labelB.text = "Longer text"
} else {
labelB.text = "Short"
}
// adjust size / position of labels
updateLabels()
}
}
let vc = TestViewController()
vc.view.backgroundColor = .white
PlaygroundPage.current.liveView = vc

Swift: ScrollView not scrolling (how to set the constraints)?

I am trying to set up a scroll view with auto layout in my storyboard and populate it with a couple of buttons in code, but the scroll view doesn't scroll and I don't understand how to set up the constraints to make scrolling available?
Especially how to set the constraints to the content view (what's that?).
In storyboard the scroll view is placed at the bottom of the screen (20 pix to the safe area) and from leading to trailing. It has a size of 375x80.
Now in code this is how the scroll view is populated with buttons and how it is set up:
override func viewDidLoad() {
super.viewDidLoad()
var xCoord: CGFloat = 5
var yCoord: CGFloat = 5
let buttonWidth: CGFloat = 70
let buttonHeight: CGFloat = 70
let gapBetweenButtons: CGFloat = 5
var itemCount = 0
for i in 0..<CIFilterNames.count {
itemCount = 1
// Button properties
let filterButton = UIButton(type: .custom)
filterButton.frame = CGRect(x: xCoord, y: yCoord, width: buttonWidth, height: buttonHeight)
filterButton.tag = itemCount
filterButton.addTarget(self, action: #selector(ViewController.filterButtonTapped(sender:)), for: .touchUpInside)
filterButton.layer.cornerRadius = 6
filterButton.clipsToBounds = true
//Code for filters will be added here
let ciContext = CIContext(options: nil)
let coreImage = CIImage(image: originalImage.image!)
let filter = CIFilter(name: "\(CIFilterNames[i])")
filter!.setDefaults()
filter?.setValue(coreImage, forKey: kCIInputImageKey)
let filteredImageDate = filter!.value(forKey: kCIOutputImageKey) as! CIImage
let filteredImageRef = ciContext.createCGImage(filteredImageDate, from: filteredImageDate.extent)
let imageForButton = UIImage(cgImage: filteredImageRef!)
// Asign filtered image to the button
filterButton.setBackgroundImage(imageForButton, for: .normal)
// Add buttons in the scrollView
xCoord += buttonWidth + gapBetweenButtons
filterScrollView.addSubview(filterButton)
}
filterScrollView.contentSize = CGSize(width: buttonWidth * CGFloat(itemCount + 2), height: yCoord)
filterScrollView.isScrollEnabled = true
}
Depending on the device size 4 or 5 buttons are shown, but not more and scrolling is not possible.
What can be done to make scrolling possible?
Reason is in itemCount , you're settings it to itemCount + 2 will be 3 as itemCount is 1 from the for loop so , set it to equal array size
filterScrollView.contentSize = CGSize(width: buttonWidth * CIFilterNames.count , height: yCoord)
You will be much better off using auto-layout over calculating sizes - gives much more flexibility for future changes.
Here is an example you can run in a Playground page. I used the "count" as the button labels, since I don't have your images. If you add images, you can un-comment the appropriate lines and remove the .setTitle and .backgroundColor lines:
import UIKit
import PlaygroundSupport
import CoreImage
class TestViewController : UIViewController {
var CIFilterNames = [
"CIPhotoEffectChrome",
"CIPhotoEffectFade",
"CIPhotoEffectInstant",
"CIPhotoEffectNoir",
"CIPhotoEffectProcess",
"CIPhotoEffectTonal",
"CIPhotoEffectTransfer",
"CISepiaTone"
]
let buttonWidth: CGFloat = 70
let buttonHeight: CGFloat = 70
let gapBetweenButtons: CGFloat = 5
override func viewDidLoad() {
super.viewDidLoad()
// create a UIScrollView
let filterScrollView = UIScrollView()
// we will set the auto-layout constraints
filterScrollView.translatesAutoresizingMaskIntoConstraints = false
// set background color so we can see the scrollView when the images are scrolled
filterScrollView.backgroundColor = .orange
// add the scrollView to the view
view.addSubview(filterScrollView)
// pin scrollView 20-pts from bottom/leading/trailing
filterScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
filterScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true
filterScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20.0).isActive = true
// scrollView height is 80
filterScrollView.heightAnchor.constraint(equalToConstant: 80).isActive = true
// create a UIStackView
let stackView = UIStackView()
stackView.spacing = gapBetweenButtons
// we will set the auto-layout constraints
stackView.translatesAutoresizingMaskIntoConstraints = false
// add the stackView to the scrollView
filterScrollView.addSubview(stackView)
// with auto-layout, scroll views use the content's constraints to
// determine the contentSize,
// so pin the stackView to top/bottom/leading/trailing of the scrollView
// with padding to match the gapBetweenButtons
stackView.leadingAnchor.constraint(equalTo: filterScrollView.leadingAnchor, constant: gapBetweenButtons).isActive = true
stackView.topAnchor.constraint(equalTo: filterScrollView.topAnchor, constant: gapBetweenButtons).isActive = true
stackView.trailingAnchor.constraint(equalTo: filterScrollView.trailingAnchor, constant: -gapBetweenButtons).isActive = true
stackView.bottomAnchor.constraint(equalTo: filterScrollView.bottomAnchor, constant: -gapBetweenButtons).isActive = true
// loop through the images
for i in 0..<CIFilterNames.count {
// create a new UIButton
let filterButton = UIButton(type: .custom)
filterButton.tag = i
// we will set the auto-layout constraints, and allow the stackView
// to handle the placement
filterButton.translatesAutoresizingMaskIntoConstraints = false
// set the width and height constraints
filterButton.widthAnchor.constraint(equalToConstant: buttonWidth).isActive = true
filterButton.heightAnchor.constraint(equalToConstant: buttonHeight).isActive = true
filterButton.layer.cornerRadius = 6
filterButton.clipsToBounds = true
filterButton.addTarget(self, action: #selector(filterButtonTapped(_:)), for: .touchUpInside)
// this example doesn't have your images, so
// set button title and background color
filterButton.setTitle("\(i)", for: .normal)
filterButton.backgroundColor = .blue
// //Code for filters will be added here
//
// let ciContext = CIContext(options: nil)
// let coreImage = CIImage(image: originalImage.image!)
// let filter = CIFilter(name: "\(CIFilterNames[i])")
// filter!.setDefaults()
// filter?.setValue(coreImage, forKey: kCIInputImageKey)
// let filteredImageDate = filter!.value(forKey: kCIOutputImageKey) as! CIImage
// let filteredImageRef = ciContext.createCGImage(filteredImageDate, from: filteredImageDate.extent)
// let imageForButton = UIImage(cgImage: filteredImageRef!)
//
// // Asign filtered image to the button
// filterButton.setBackgroundImage(imageForButton, for: .normal)
// add the image view to the stackView
stackView.addArrangedSubview(filterButton)
}
}
func filterButtonTapped(_ sender: Any?) -> Void {
if let b = sender as? UIButton {
print("Tapped:", b.tag)
}
}
}
let vc = TestViewController()
vc.view.backgroundColor = .red
PlaygroundPage.current.liveView = vc

How to scroll images horizontally in iOS swift

I have an imageview called "cardImgView" in that I want to load two images by scrolling horizontally, I have tried the following way, in this case I can able to scroll only to up and down and the images also not changing, anyone
have idea how to do this correctly.
let img: UIImage = self.dataDict.object(forKey: kCardImgFront) as! UIImage
let img2:UIImage = self.dataDict.object(forKey: kCardImgBack) as! UIImage
imgArray = [img, img2]
for i in 0..<imgArray.count{
cardImgView?.image = imgArray[i]
scrollView.contentSize.width = scrollView.frame.width * CGFloat(i + 1)
scrollView.addSubview(cardImgView!)
}
thanks in advance.
First, as I commented, you are currently using a single UIImageView --- so each time through your for-loop you are just replacing the .image of that one image view.
Second, you will be much better off using auto-layout and constraints, instead of trying to explicitly set frames and the scrollView's contentSize.
Third, UIStackView is ideal for your use case - adding multiple images that you want to horizontally scroll.
So, the general idea is:
add a scroll view
add a stack view to the scroll view
use constraints to make the stack view control the scroll view's contentSize
create a new UIImageView for each image
add each image view to the stack view
Here is a simple example that you can run in a Playground page to see how it works. If you add your own images named image1.png and image2.png to the playground's resources, they will be used (otherwise, this example creates solid blue and solid green images):
import UIKit
import PlaygroundSupport
// UIImage extension to create a new, solid-color image
public extension UIImage {
public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)
}
}
class TestViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// create a UIScrollView
let scrollView = UIScrollView()
// we will set the auto-layout constraints
scrollView.translatesAutoresizingMaskIntoConstraints = false
// set background color so we can see the scrollView when the images are scrolled
scrollView.backgroundColor = .orange
// add the scrollView to the view
view.addSubview(scrollView)
// pin scrollView 20-pts from top/bottom/leading/trailing
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20.0).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20.0).isActive = true
// create an array of empty images in case this is run without
// valid images in the resources
var imgArray = [UIImage(color: .blue), UIImage(color: .green)]
// if these images exist, load them and replace the blank images in imgArray
if let img1: UIImage = UIImage(named: "image1"),
let img2: UIImage = UIImage(named: "image2") {
imgArray = [img1, img2]
}
// create a UIStackView
let stackView = UIStackView()
// we can use the default stackView properties
// but can change axis, alignment, distribution, spacing, etc if desired
// we will set the auto-layout constraints
stackView.translatesAutoresizingMaskIntoConstraints = false
// add the stackView to the scrollView
scrollView.addSubview(stackView)
// with auto-layout, scroll views use the content's constraints to
// determine the contentSize,
// so pin the stackView to top/bottom/leading/trailing of the scrollView
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0.0).isActive = true
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0.0).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0.0).isActive = true
// loop through the images
for img in imgArray {
// create a new UIImageView
let imgView = UIImageView(image: img)
// we will set the auto-layout constraints, and allow the stackView
// to handle the placement
imgView.translatesAutoresizingMaskIntoConstraints = false
// set image scaling as desired
imgView.contentMode = .scaleToFill
// add the image view to the stackView
stackView.addArrangedSubview(imgView)
// set imgView's width and height to the scrollView's width and height
imgView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1.0).isActive = true
imgView.heightAnchor.constraint(equalTo: scrollView.heightAnchor, multiplier: 1.0).isActive = true
}
}
}
let vc = TestViewController()
vc.view.backgroundColor = .red
PlaygroundPage.current.liveView = vc
I modified my code and tried as follows and its working now. with page contrlller
let imgArray = [UIImage]()
let img: UIImage = self.dataDict.object(forKey: kCardImgFront) as! UIImage
let img2:UIImage = self.dataDict.object(forKey: kCardImgBack) as! UIImage
imgArray = [img, img2]
for i in 0..<imgArray.count {
let imageView = UIImageView()
imageView.image = imgArray[i]
let xPosition = self.view.frame.width * CGFloat(i)
imageView.frame = CGRect(x: xPosition, y: 0, width:
self.scrollView.frame.width + 50, height: self.scrollView.frame.height)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(i + 1)
scrollView.addSubview(imageView)
}
self.scrollView.delegate = self
func scrollViewDidScroll(_ scrollView: UIScrollView){
pageController.currentPage = Int(self.scrollView.contentOffset.x /
CGFloat(4))
}
I think you need to set a proper frame for the cardImgView. It would be something like
cardImgView.frame = CGRect(x: scrollView.frame.width * CGFloat(i), y: 0, width: scrollView.frame.width, height: scrollView.frame.height)
Finally, after the for loop, you need to set scroll view's content size:
scrollView.contentSize.width = scrollView.frame.width * imgArray.count
Hope this helps.
I have written scrolling images horizontally in swift. Please check with this:
import UIKit
class ViewController: UIViewController,UIScrollViewDelegate {
#IBOutlet weak var Bannerview: UIView!
var spinner = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
var loadingView: UIView = UIView()
var loadinglabel: UILabel = UILabel()
var nextPage :Int!
var titlelab :UILabel!
var bannerimg :UIImageView!
var scroll :UIScrollView!
var viewPanel :UIView!
var pgCtr:UIPageControl!
var bannerArr:[String]!
var imgUrlstr :NSString!
var screenSize: CGRect!
var screenWidth: CGFloat!
var screenHeight: CGFloat!
func uicolorFromHex(rgbValue:UInt32)->UIColor
{
let red = CGFloat((rgbValue & 0xFF0000) >> 16)/256.0
let green = CGFloat((rgbValue & 0xFF00) >> 8)/256.0
let blue = CGFloat(rgbValue & 0xFF)/256.0
return UIColor(red:red, green:green, blue:blue, alpha:1.0)
}
override func viewWillAppear(_ animated: Bool)
{
screenSize = UIScreen.main.bounds
screenWidth = screenSize.width
screenHeight = screenSize.height
bannerArr = ["image1.jpeg","image2.jpeg","image3.jpeg","images4.jpeg","images5.jpeg"]
self.bannerview()
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.navigationController?.navigationBar.isTranslucent = false
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
}

Resources