Decrease UILabel Font size to exact point using CGAffineTransform - ios

I have a UITableView and a UINavigationBar with its custom title label. I want to decrease the title label font size on scroll down and increase it on scroll up.
Here is my code
override func viewDidLoad() {
super.viewDidLoad()
myLabel.font = UIFont(name: "Arial", size: 17)
}
Here I set the font size for my label.
And then, I transform the font size like so
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
let scale = min(max(1.0 - offset / 200.0, 0.0), 1.0)
myLabel.transform = CGAffineTransform(scaleX: scale, y: scale)
}
The problem is, the minimum point is set to 0 this way, but I want my label to decrease to 11 points, etc.
Can anyone help me edit my code?

calculate the final scale with this formula
let defualtFontSize = 17.0
let minFontSize = 11.0
let finalFontSize = CGFloat(minFontSize / defualtFontSize)
then use finalFontSize here
let scale = min(max(1.0 - offset / 200.0, finalFontSize), 1.0)
complete code
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let defualtFontSize = 17.0
let minFontSize = 11.0
let finalFontSize = CGFloat(minFontSize / defualtFontSize)
let offset = scrollView.contentOffset.y
let scale = min(max(1.0 - offset / 200.0, finalFontSize), 1.0)
myLabel.transform = CGAffineTransform(scaleX: scale, y: scale)
}
but write the calculation somewhere out of scrollViewDidScroll, avoiding CPU Usage.

Related

Building a circular facepile of profile pictures in Swift: how to have the last photo tucked under the first?

I am trying to build a UIView that has a few UIImageViews arranged in a circular, overlapping manner (see image below). Let's say we have N images. Drawing out the first N - 1 is easy, just use sin/cos functions to arrange the centers of the UIImageViews around a circle. The problem is with the last image that seemingly has two z-index values! I know this is possible since kik messenger has similar group profile photos.
The best idea I have come up so far is taking the last image, split into something like "top half" and "bottom half" and assign different z-values for each. This seems doable when the image is the left-most one, but what happens if the image is the top most? In this case, I would need to split left and right instead of top and bottom.
Because of this problem, it's probably not top, left, or right, but more like a split across some imaginary axis from the center of the overall facepile through the center of the UIImageView. How would I do that?!
Below Code Will Layout UIImageView's in Circle
You would need to import SDWebImage and provide some image URLs to run the code below.
import Foundation
import UIKit
import SDWebImage
class EventDetailsFacepileView: UIView {
static let dimension: CGFloat = 66.0
static let radius: CGFloat = dimension / 1.68
private var profilePicViews: [UIImageView] = []
var profilePicURLs: [URL] = [] {
didSet {
updateView()
}
}
func updateView() {
self.profilePicViews = profilePicURLs.map({ (profilePic) -> UIImageView in
let imageView = UIImageView()
imageView.sd_setImage(with: profilePic)
imageView.roundImage(imageDimension: EventDetailsFacepileView.dimension, showsBorder: true)
imageView.sd_imageTransition = .fade
return imageView
})
self.profilePicViews.forEach { (imageView) in
self.addSubview(imageView)
}
self.setNeedsLayout()
self.layer.borderColor = UIColor.green.cgColor
self.layer.borderWidth = 2
}
override func layoutSubviews() {
super.layoutSubviews()
let xOffset: CGFloat = 0
let yOffset: CGFloat = 0
let center = CGPoint(x: self.bounds.size.width / 2, y: self.bounds.size.height / 2)
let radius: CGFloat = EventDetailsFacepileView.radius
let angleStep: CGFloat = 2 * CGFloat(Double.pi) / CGFloat(profilePicViews.count)
var count = 0
for profilePicView in profilePicViews {
let xPos = center.x + CGFloat(cosf(Float(angleStep) * Float(count))) * (radius - xOffset)
let yPos = center.y + CGFloat(sinf(Float(angleStep) * Float(count))) * (radius - yOffset)
profilePicView.frame = CGRect(origin: CGPoint(x: xPos, y: yPos),
size: CGSize(width: EventDetailsFacepileView.dimension, height: EventDetailsFacepileView.dimension))
count += 1
}
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let requiredSize = EventDetailsFacepileView.dimension + EventDetailsFacepileView.radius
return CGSize(width: requiredSize,
height: requiredSize)
}
}
I don't think you'll have much success trying to split images to get over/under z-indexes.
One approach is to use masks to make it appear that the image views are overlapped.
The general idea would be:
subclass UIImageView
in layoutSubviews()
apply cornerRadius to layer to make the image round
get a rect from the "overlapping view"
convert that rect to local coordinates
expand that rect by the desired width of the "outline"
get an oval path from that rect
combine it with a path from self
apply it as a mask layer
Here is an example....
I was not entirely sure what your sizing calculations were doing... trying to use your EventDetailsFacepileView as-is gave me small images in the lower-right corner of the view?
So, I modified your EventDetailsFacepileView in a couple ways:
uses local images named "pro1" through "pro5" (you should be able to replace with your SDWebImage)
uses auto-layout constraints instead of explicit frames
uses MyOverlapImageView class to handle the masking
Code - no #IBOutlet connections, so just set a blank view controller to OverlapTestViewController:
class OverlapTestViewController: UIViewController {
let facePileView = MyFacePileView()
override func viewDidLoad() {
super.viewDidLoad()
facePileView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(facePileView)
facePileView.dimension = 120
let sz = facePileView.sizeThatFits(.zero)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
facePileView.widthAnchor.constraint(equalToConstant: sz.width),
facePileView.heightAnchor.constraint(equalTo: facePileView.widthAnchor),
facePileView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
facePileView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
facePileView.profilePicNames = [
"pro1", "pro2", "pro3", "pro4", "pro5"
]
}
}
class MyFacePileView: UIView {
var dimension: CGFloat = 66.0
lazy var radius: CGFloat = dimension / 1.68
private var profilePicViews: [MyOverlapImageView] = []
var profilePicNames: [String] = [] {
didSet {
updateView()
}
}
func updateView() {
self.profilePicViews = profilePicNames.map({ (profilePic) -> MyOverlapImageView in
let imageView = MyOverlapImageView()
if let img = UIImage(named: profilePic) {
imageView.image = img
}
return imageView
})
// add MyOverlapImageViews to self
// and set width / height constraints
self.profilePicViews.forEach { (imageView) in
self.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.widthAnchor.constraint(equalToConstant: dimension).isActive = true
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
}
// start at "12 o'clock"
var curAngle: CGFloat = .pi * 1.5
// angle increment
let incAngle: CGFloat = ( 360.0 / CGFloat(self.profilePicViews.count) ) * .pi / 180.0
// calculate position for each image view
// set center constraints
self.profilePicViews.forEach { imgView in
let xPos = cos(curAngle) * radius
let yPos = sin(curAngle) * radius
imgView.centerXAnchor.constraint(equalTo: centerXAnchor, constant: xPos).isActive = true
imgView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: yPos).isActive = true
curAngle += incAngle
}
// set "overlapView" property for each image view
let n = self.profilePicViews.count
for i in (1..<n).reversed() {
self.profilePicViews[i].overlapView = self.profilePicViews[i-1]
}
self.profilePicViews[0].overlapView = self.profilePicViews[n - 1]
self.layer.borderColor = UIColor.green.cgColor
self.layer.borderWidth = 2
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let requiredSize = dimension * 2.0 + radius / 2.0
return CGSize(width: requiredSize,
height: requiredSize)
}
}
class MyOverlapImageView: UIImageView {
// reference to the view that is overlapping me
weak var overlapView: MyOverlapImageView?
// width of "outline"
var outlineWidth: CGFloat = 6
override func layoutSubviews() {
super.layoutSubviews()
// make image round
layer.cornerRadius = bounds.size.width * 0.5
layer.masksToBounds = true
let mask = CAShapeLayer()
if let v = overlapView {
// get bounds from overlapView
// converted to self
// inset by outlineWidth (negative numbers will make it grow)
let maskRect = v.convert(v.bounds, to: self).insetBy(dx: -outlineWidth, dy: -outlineWidth)
// oval path from mask rect
let path = UIBezierPath(ovalIn: maskRect)
// path from self bounds
let clipPath = UIBezierPath(rect: bounds)
// append paths
clipPath.append(path)
mask.path = clipPath.cgPath
mask.fillRule = .evenOdd
// apply mask
layer.mask = mask
}
}
}
Result:
(I grabbed random images by searching google for sample profile pictures)

How to Increase/Decrease font size with Pan Gesture?

I am applying pan gesture on UILabel. I use one finger scale to increase and decrease the size of the UILabel.
I tried using scale to add and subtract value from font size, but I am not getting the exact result.
#objc
func handleRotateGesture(_ recognizer: UIPanGestureRecognizer) {
let touchLocation = recognizer.location(in: self.superview)
let center = self.center
switch recognizer.state {
case .began:
self.deltaAngle = CGFloat(atan2f(Float(touchLocation.y - center.y), Float(touchLocation.x - center.x))) - CGAffineTransformGetAngle(self.transform)
self.initialBounds = self.bounds
self.initialDistance = CGPointGetDistance(point1: center, point2: touchLocation)
case .changed:
let angle = atan2f(Float(touchLocation.y - center.y), Float(touchLocation.x - center.x))
let angleDiff = Float(self.deltaAngle) - angle
self.transform = CGAffineTransform(rotationAngle: CGFloat(-angleDiff))
if let label = self.contentView as? UILabel {
var scale = CGPointGetDistance(point1: center, point2: touchLocation) / self.initialDistance
let minimumScale = CGFloat(self.minimumSize) / min(self.initialBounds.size.width, self.initialBounds.size.height)
scale = max(scale, minimumScale)
let scaledBounds = CGRectScale(self.initialBounds, wScale: scale, hScale: scale)
var pinchScale = scale
pinchScale = round(pinchScale * 1000) / 1000.0
var fontSize = label.font.pointSize
if(scale > minimumScale){
if (self.bounds.height > scaledBounds.height) {
// fontSize = fontSize - pinchScale
label.font = UIFont(name: label.font.fontName, size: fontSize - pinchScale)
}
else{
// fontSize = fontSize + pinchScale
label.font = UIFont(name: label.font.fontName, size: fontSize + pinchScale)
}
} else {
label.font = UIFont( name: label.font.fontName, size: fontSize)
}
print("PinchScale -- \(pinchScale), FontSize = \(fontSize)")
self.bounds = scaledBounds
} else {
var scale = CGPointGetDistance(point1: center, point2: touchLocation) / self.initialDistance
let minimumScale = CGFloat(self.minimumSize) / min(self.initialBounds.size.width, self.initialBounds.size.height)
scale = max(scale, minimumScale)
let scaledBounds = CGRectScale(self.initialBounds, wScale: scale, hScale: scale)
self.bounds = scaledBounds
}
self.setNeedsDisplay()
default:
break
}
}
However, we can achieve this using UIPinchGestureRecognizer. But how can we do the same effect with UIPanGestureRecognizer? Any help would be appreciated. Thanks.
Right now you’re adding and removing points to the label’s current font size. I’d suggest a simpler pattern, more consistent with what you’re doing elsewhere, namely just capturing the the initial point size at the start of the gesture and then, as the user’s finger moves, apply a scale to that saved value. So, I’d suggest:
Define property to save the initial font size:
var initialPointSize: CGFloat = 0
In the .began of the gesture, capture the current size
initialPointSize = (contentView as? UILabel)?.font?.pointSize ?? 0
In the .changed, adjust the font size:
let pinchScale = (scale * 1000).rounded() / 1000
label.font = label.font.withSize(initialPointSize * pinchScale)
As an aside, I’m not sure that it’s necessary to round the scale to three decimal places, but you had that in your original code snippet, so I preserved that.
Personally, I’d follow the same basic pattern with the transform:
Define a properties to capture the starting angle and the view’s current transform:
var initialAngle: CGFloat = 0
var initialTransform: CGAffineTransform = .identity
In the .began, capture the current the starting angle and existing transform:
initialAngle = atan2(touchLocation.y - center.y, touchLocation.x - center.x)
initialTransform = transform
In the .changed, update the transform:
let angle = atan2(touchLocation.y - center.y, touchLocation.x - center.x)
transform = initialTransform.rotated(by: angle - initialAngle)
This saves you from reverse engineering the angle associated with the current transform with CGAffineTransformGetAngle and instead just apply rotated(by:) to the saved transform.
So this, like the point size and the bounds, represents a consistent pattern: Capture the starting value in .began and just apply whatever change you need in .changed.
A few unrelated observations:
All those self. references are not needed. It just adds noise that makes it harder to read the code.
All of those casts between CGFloat and Float are not needed. If you use the atof2 function instead of atof2f, they all just work with CGFloat without any casting. E.g., instead of
let angle = atan2f(Float(touchLocation.y - center.y), Float(touchLocation.x - center.x))
let angleDiff = Float(self.deltaAngle) - angle
self.transform = CGAffineTransform(rotationAngle: CGFloat(-angleDiff))
You can just do:
let angle = atan2(touchLocation.y - center.y, touchLocation.x - center.x)
transform = CGAffineTransform(rotationAngle: angle - deltaAngle)
You actually don’t need any of those casts that are currently sprinkled throughout the code snippet.
All of those bounds.size.width and bounds.size.height can just be bounds.width and bounds.height respectively, again removing noise from the code.
When adjusting the font size, rather than:
label.font = UIFont(name: label.font.fontName, size: fontSize)
You should just use:
label.font = label.font.withSize(fontSize)
That way you preserve all of the essential characteristics of the font (the weight, etc.), and just adjust the size.
In your if let label = contentView as? UILabel test, the same five lines of code in your else clause appear in the if clause, too. You should just move those common lines before the if-else statement, and then you can lose the else clause entirely, simplifying your code.

The text in UITextView is being cutoff when rotated

I am trying to allow the user to rotate text they place on a view inside UITextViews. The problem is when I rotate them using the transform property, the text gets cutoff. However, when I do this with UILabels, it works fine, so why not UITextViews? The bounds of UITextView (shown in green) never changes, so I am not sure why this is happening. The amount of cutoff seems related to the height of the text view.
The only work around I've found is to do the transform asynchronously. I don't know why it works, but I don't think it's a good solution. You can see this in action with the following code.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for xPos in stride(from: 80, to: view.bounds.width, by: 80) {
let point = CGPoint(x: xPos, y: 100)
let textView = makeTextView(at: point)
//DispatchQueue.main.async {
let angle = xPos / 400.0
textView.transform = CGAffineTransform(rotationAngle: angle)
//}
view.addSubview(textView)
}
}
private func makeTextView(at point: CGPoint) -> UITextView {
let textView = UITextView()
textView.text = "1234567890"
textView.isScrollEnabled = false
textView.backgroundColor = .green
textView.center = CGPoint(x: point.x, y: point.y)
textView.bounds.size = CGSize(width: 80, height: 24)
return textView
}
}
What is the correct way to rotate them?

How to make a UIImageView have a square size based on a given bounds/container

So I am trying to create a simple UIImageView to make it have a square frame/size with CGSize. Based on a given bounds. So for example if the bounds container is the width & height of the screen then. The function should resize the UIImageView to fit like a perfect square base on those bounds on the screen.
Code:
let myImageView = UIImageView()
myImageView.frame.origin.y = (self.view?.frame.height)! * 0.0
myImageView.frame.origin.x = (self.view?.frame.width)! * 0.0
myImageView.backgroundColor = UIColor.blue
self.view?.insertSubview(myImageView, at: 0)
//("self.view" is the ViewController view that is the same size as the devices screen)
MakeSquare(view: myImageView, boundsOf: self.view)
func MakeSquare(view passedview: UIImageView, boundsOf container: UIView) {
let ratio = container.frame.size.width / container.frame.size.height
if container.frame.width > container.frame.height {
let newHeight = container.frame.width / ratio
passedview.frame.size = CGSize(width: container.frame.width, height: newHeight)
} else{
let newWidth = container.frame.height * ratio
passedview.frame.size = CGSize(width: newWidth, height: container.frame.height)
}
}
The problem is its giving me back the same bounds/size of the container & not changed
Note: I really have know idea how to pull this off, but wanted to see if its possible. My function comes from a question here. That takes a UIImage and resizes its parent view to make the picture square.
This should do it (and will centre the image view in the containing view):
func makeSquare(view passedView: UIImageView, boundsOf container: UIView) {
let minSize = min(container.bounds.maxX, container.bounds.maxY)
passedView.bounds = CGRect(x: container.bounds.midX - minSize / 2.0,
y: container.bounds.midY - minSize / 2.0,
width: minSize, height: minSize)
}

Get height of UIView based on specific width before showing view

I have a UIView which I have designed in Interface Builder. It basically consists out of a header image and some text (UILabel) below. The view is being shown modally with a custom Transition and doesn't fill the whole screen.
There is like a 20 pixels margin on the left and right and 40 px on the top. The UILabel gets filled with some text that's coming from the web. What I want do do, is to find (or should I say predict) the height of the whole view for a specific width. How can I do that?
I had similar problem, but instead of calculating font size and image height you can use this UIView extension that will autosize your view with max width:
extension UIView {
func autosize(maxWidth: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
let dummyContainerView = UIView(frame: CGRect(x: 0, y: 0, width: maxWidth, height: 10000000))
dummyContainerView.addSubview(self)
dummyContainerView.topAnchor.constraint(equalTo: topAnchor, constant: 0).isActive = true
dummyContainerView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0).isActive = true
dummyContainerView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0).isActive = true
setNeedsLayout()
layoutIfNeeded()
removeFromSuperview()
frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
translatesAutoresizingMaskIntoConstraints = true
}
}
Using this approuch you don't have to worry about your content inside the view.
To use it:
let customView: CustomView = ... //create your view
... // configure all data on your view, e.g. labels with correct text
customView.autosize(maxWidth: 150) // resize your view
view.addSubview(customView) // add your view to any view
You need to have both the picture and the label before calculating the aspected size.
I guess you should use something like this (maybe adding the vertical inter-distance between the imageView and the Label to the sum, and maybe removing the lateral margins from the width):
objective C :
- (CGFloat)preferredHeightFromWidth:(CGFloat)width text:(NSString *)text font:(UIFont *)font image:(UIImage *)image
{
// Calculate label height
CGFloat labelHeight = [text
boundingRectWithSize:CGSizeMake(width, 10000)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName:font}
context:[[NSStringDrawingContext alloc] init]
].size.height;
// Calculate image height
CGFloat ratio = image.size.height/ image.size.width;
CGFloat imageHeight = (ratio * width);
// Do the sum
return labelHeight + imageHeight;
}
Swift:
func preferredHeight(width: CGFloat, text: NSString, font: UIFont, image: UIImage) -> CGFloat {
// Calculate Label Height
let labelRect = text.boundingRect(
with: CGSize.init(width: width, height: 10000),
options: .usesLineFragmentOrigin,
attributes: [NSFontAttributeName : font],
context: NSStringDrawingContext())
let labelHeight = labelRect.height
// Calculate Image Height
let ratio = image.size.height / image.size.width
let imageHeight = ratio / width
// Calculate Total Height
let height = labelHeight + imageHeight
// Return Height Value
return height
}
(Thanks to Christopher Hannah for swift version)
Here is the same answer as Alberto, but I have changed it into Swift 3.
func preferredHeight(width: CGFloat, text: NSString, font: UIFont, image: UIImage) -> CGFloat {
// Calculate Label Height
let labelRect = text.boundingRect(
with: CGSize.init(width: width, height: 10000),
options: .usesLineFragmentOrigin,
attributes: [NSFontAttributeName : font],
context: NSStringDrawingContext())
let labelHeight = labelRect.height
// Calculate Image Height
let ratio = image.size.height / image.size.width
let imageHeight = ratio / width
// Calculate Total Height
let height = labelHeight + imageHeight
// Return Height Value
return height
}

Resources