Swift: Center NSAttributedString in View horizontally and vertically - swift

Hi there,
I have a class CardButton: UIButton . In the draw method of this CardButton, I would like to add and center(vertically and horizontally) NSAttributed String, which is basically just one Emoji, inside of it. The result would look something like this:
However, NSAttributedString can be only aligned to center in horizontal dimension inside the container.
My idea for solution:
create a containerView inside of CardButton
center containerView both vertically and horizontally in it's container(which is CardButton)
add NSAttributedString inside the containerView and size containerView to fit the string's font.
So the result would look something like this:
My attempt for this to happen looks like this:
class CardButton: UIButton {
override func draw(){
//succesfully drawing the CardButton
let stringToDraw = attributedString(symbol, fontSize: symbolFontSize) //custom method to create attributed string
let containerView = UIView()
containerView.backgroundColor = //green
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
let centerXConstraint = containerView.centerXAnchor.constraint(equalTo: (self.centerXAnchor)).isActive = true
let centerYConstraint = containerView.centerYAnchor.constraint(equalTo: (self.centerYAnchor)).isActive = true
stringToDraw.draw(in: containerView.bounds)
containerView.sizeToFit()
}
}
Long story short, I failed terribly. I first tried to add containerView to cardButton, made the background green, gave it fixed width and height jut to make sure that it got properly added as a subview. It did. But once I try to active constraints on it, it totally disappears.
Any idea how to approach this?

There is always more than one way to achieve any particular UI design goal, but the procedure below is relatively simple and has been adapted to suit the requirements as presented and understood in your question.
The UIButton.setImage method allows an image to be assigned to a UIButton without the need for creating a container explicitly.
The UIGraphicsImageRenderer method allows an image to be made from various components including NSAttributedText, and a host of custom shapes.
The process utilising these two tools to provide the rudiments for your project will be to:
Render an image with the appropriate components & size
Assign the rendered image to the button
A class could be created for this functionality, but that has not been explored here.
Additionally, your question mentions that when applying constraints the content disappears. This effect can be observed when image dimensions are too large for the container, constraints are positioning the content out of view and possibly a raft of other conditions.
The following code produces the above image:
func drawRectangleWithEmoji() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 512, height: 512))
let img = renderer.image { (ctx) in
// Create the outer square:
var rectangle = CGRect(x: 0, y: 0, width: 512, height: 512).insetBy(dx: 7.5, dy: 7.5)
var roundedRect = UIBezierPath(roundedRect: rectangle, cornerRadius: 50).cgPath
// MARK: .cgPath creates a CG representation of the path
ctx.cgContext.setFillColor(UIColor.white.cgColor)
ctx.cgContext.setStrokeColor(UIColor.blue.cgColor)
ctx.cgContext.setLineWidth(15)
ctx.cgContext.addPath(roundedRect)
ctx.cgContext.drawPath(using: .fillStroke)
// Create the inner square:
rectangle = CGRect(x: 0, y: 0, width: 512, height: 512).insetBy(dx: 180, dy: 180)
roundedRect = UIBezierPath(roundedRect: rectangle, cornerRadius: 10).cgPath
ctx.cgContext.setFillColor(UIColor.green.cgColor)
ctx.cgContext.setStrokeColor(UIColor.green.cgColor)
ctx.cgContext.setLineWidth(15)
ctx.cgContext.addPath(roundedRect)
ctx.cgContext.drawPath(using: .fillStroke)
// Add emoji:
var fontSize: CGFloat = 144
var attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: fontSize)//,
//.backgroundColor: UIColor.gray //uncomment to see emoji background bounds
]
var string = "❤️"
var attributedString = NSAttributedString(string: string, attributes: attrs)
let strWidth = attributedString.size().width
let strHeight = attributedString.size().height
attributedString.draw(at: CGPoint(x: 256 - strWidth / 2, y: 256 - strHeight / 2))
// Add NSAttributedString:
fontSize = 56
attrs = [
.font: UIFont.systemFont(ofSize: fontSize),
.foregroundColor: UIColor.brown
]
string = "NSAttributedString"
attributedString = NSAttributedString(string: string, attributes: attrs)
let textWidth = attributedString.size().width
let textHeight = attributedString.size().height
attributedString.draw(at: CGPoint(x: 256 - textWidth / 2, y: 384 - textHeight / 2))
}
return img
}
Activate the NSLayoutContraints and then the new image can be set for the button:
override func viewDidLoad() {
super.viewDidLoad()
view = UIView()
view.backgroundColor = .white
let buttonsView = UIView()
buttonsView.translatesAutoresizingMaskIntoConstraints = false
// buttonsView.backgroundColor = UIColor.gray
view.addSubview(buttonsView)
NSLayoutConstraint.activate([
buttonsView.widthAnchor.constraint(equalToConstant: CGFloat(buttonWidth)),
buttonsView.heightAnchor.constraint(equalToConstant: CGFloat(buttonWidth)),
buttonsView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
buttonsView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
let cardButton = UIButton(type: .custom)
cardButton.contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
cardButton.setImage(drawRectangleWithEmoji(), for: .normal)
let frame = CGRect(x: 0, y: 0, width: buttonWidth, height: buttonWidth)
cardButton.frame = frame
buttonsView.addSubview(cardButton)
}
Your comments will be appreciated as would constructive review of the code provided.

Related

How do I change the color of an SF Symbols icon in Swift?

I realize this question has been asked here twice but none of the solutions provided are working for me. My icon is showing up but it is showing with a blue outline rather than the red I have requested. Swift is telling me that setting the foreground color is not an option
let largeConfig = UIImage.SymbolConfiguration(pointSize: heartPointSize)
let emptyHeartIcon = UIImage(systemName: "heart", withConfiguration:largeConfig)?.withTintColor(.systemRed) // this did not work
emptyHeartIcon?.withTintColor(.systemRed) // this also did not work
emptyHeartIcon!.withTintColor(.systemRed) // thought maybe it was because I wasn't unwrapping it but this also didn't work
If you're so inclined here's the context of the symbol :)
func levelUpsShown(){
let levelUpShownStackView = UIStackView(arrangedSubviews: [])
levelUpShownStackView.frame = CGRect(x: 15, y: 15, width: levelUpsShownView.bounds.width - 30, height: levelUpsShownView.bounds.height - 30)
let heartPointSize = (levelUpsShownView.bounds.width - 30)/8
let heartLocation = (levelUpsShownView.bounds.width - 38)/12
(0..<6).forEach { (levelUpsBucket) in
let largeConfig = UIImage.SymbolConfiguration(pointSize: heartPointSize)
let emptyHeartIcon = UIImage(systemName: "heart", withConfiguration: largeConfig)
let emptyHeartView = UIImageView(image: emptyHeartIcon)
let levelUpSegment = UIView()
levelUpSegment.addSubview(emptyHeartView)
emptyHeartView.center = CGPoint(x: heartLocation, y: 40)
levelUpShownStackView.addArrangedSubview(levelUpSegment)
}
levelUpShownStackView.distribution = .fillEqually
levelUpShownStackView.spacing = 4
levelUpsShownView.addSubview(levelUpShownStackView)
}
Try to change the tint of the UIImageView instead of the UIImage.
emptyHeartView.tintColor = .systemRed
If you don't want an UIImageView, check this post;
https://stackoverflow.com/a/19152722/8258494
Basically applying a mask on your image.

How can I show the rest of a character that has gone slightly outside of UITextView?

I have a UITextView which changes size depending on the text the user inputs (the purple box), which is inside another UIView (the red box).
But when using a handwritten style font like this, the end character sometimes gets cut off at the edge:
I have tried used text1.clipsToBounds = false but that didn't show the edge of the character. Is there a way to show the full character without changing the width of the text view?
Also here is the code I am using to set up the text view:
let text1 = UITextView()
text1.text = ""
text1.font = UIFont(name: "Gotcha", size: 27)
text1.frame = CGRect(x: 10, y: 5, width: 70, height: 50)
text1.isScrollEnabled = false
text1.delegate = self
text1.textAlignment = .center
text1.isEditable = false
text1.isSelectable = false
holdingView.addSubview(text1)
The frame then gets updated with this function, and whenever the text is changed:
func adjustTextViewSize(_ textView: UITextView) {
let maxWidth = 300
let newSize = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude))
textView.frame = CGRect(x: (textView.frame.minX), y: (textView.frame.minY), width: newSize.width, height: newSize.height)
}
Thanks!
Update:
I solved this by adding an extra 30px to newSize.width for any font that is handwritten:
if fontFile?.isHandwritten == true {
currentView.widthConstraint?.constant = newSize.width + 30
currentTextHoldingView.widthConstraint?.constant = newSize.width + 30
}
call this function for get height according to string length
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: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.height)
}
}

How can I insert image to end of the string and insert more than one image in UITextView with swift?

I created an UITextView and I can add image with image picker into text view. But I have some problem about replacement image. I want add this image end of the text. And I want to add images more than one. (like: text + image + text...). How can I solve this problem ? Can anyone help me ?
let pickImage = UIImageView() // this is for imagepickercontroller
lazy var writePost: UITextView = {
let wpost = UITextView()
let images = pickImage
let attachment = NSTextAttachment()
let attString = NSAttributedString(attachment: attachment)
attachment.image = images.image
images.frame = CGRect(x: 0, y: 0, width: 220, height: 220)
images.contentMode = .scaleAspectFill
wpost.textStorage.insert(attString, at: wpost.selectedRange.location)
wpost.addSubview(images)
wpost.textAlignment = .center
wpost.textColor = UIColor.lightGray
wpost.font = UIFont(name: "AvenirNext-DemiBoldItalic", size: 16)
wpost.isEditable = true
wpost.isScrollEnabled = true
wpost.layer.borderWidth = 1.5
wpost.layer.cornerRadius = 7.0
wpost.layer.borderColor = UIColor.orange.cgColor
wpost.delegate = self
return wpost
}()
What you should do is use UITextView's textContainer's exclusionPaths property. The exclusionPaths property lets you assign an array of UIBezierPaths to your textContainer. When exclusionPaths are set, none of the UITextView's text will appear within these paths. You could then add a UIImageView as a subview of the UITextView's super view placed above the UITextView that has a frame equal to said exclusion path.
The end result will be a UITextView with a UIImageView placed above it. None of the UITextView's text will be blocked by the UIImageView as the UITextView's textContainer's exclusionPaths have instructed the text not to populate there.
Here is an example of some code I've done to do something similar, with variable names changed to match your code a bit:
let imageView: UIImageView!
func addImageView() {
imageView = UIImageView(frame: CGRect(x: textView.frame.maxX - 200, y: textView.frame.maxY - 150, width: 200, height: 150))
textView.superView.addSubview(imageView)
}
func setExclusionPath(for imageView: UIImageView) {
let imageViewPath = UIBezierPath(rect: CGRect(x: textView.frame.maxX - imageView.frame.width, y: textView.frame.maxY - imageView.frame.height, width: imageView.frame.width, height: imageView.frame.height))
textView.textContainer.exclusionPaths.append(imageViewPath)
}
func someMethod() {
addImageView()
setExclusionPath(for: self.imageView)
}
Resources:
exclusionPaths reference from Apple

How to get rid of border of UISearchBar?

The Problem
I have a UISearchController embedded within a UIView. The searchbar takes up the entire size of the view. However, there's this grey border that I can't figure how to get rid off. I want the search bar to take up the entire size of the view without that pesky grey border. How can I get rid of it?
I've tried
searchController?.searchBar.backgroundImage = UIImage()
But all that does is make the grey border clear (not to mention it makes the searchResultsController of the searchController not show up):
I want the border to be gone and the search bar take up the entire frame of the UIView.
let SEARCH_BAR_SEARCH_FIELD_KEY = "searchField"
let SEARCH_BAR_PLACEHOLDER_TEXT_KEY = "_placeholderLabel.textColor"
func customiseSearchBar() {
searchBar.isTranslucent = true
searchBar.backgroundImage = UIImage.imageWithColor(color: UIColor.yellow, size: CGSize(width: searchBar.frame.width,height: searchBar.frame.height))
//modify textfield font and color attributes
let textFieldSearchBar = searchBar.value(forKey: SEARCH_BAR_SEARCH_FIELD_KEY) as? UITextField
textFieldSearchBar?.backgroundColor = UIColor.yellow
textFieldSearchBar?.font = UIFont(name: "Helvetica Neue" as String, size: 18)
textFieldSearchBar?.setValue(UIColor.yellow, forKeyPath: SEARCH_BAR_PLACEHOLDER_TEXT_KEY)
}
extension UIImage {
class func imageWithColor(color: UIColor, size: CGSize) -> UIImage {
let rect: CGRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
This achieves the UI as per your requirement, let me know if you need more customization on this. This is in swift 3 by the way.

Programatically set frame size of labels inside UIButton

I have a custom UIButton that I created. Inside the button I have 2 labels, one above the other. The top one is the title so it is a little bigger, and the bottom one is a bit smaller.
My goal is to have both labels cover the whole button exactly, like this:
The top label will cover the top 2/3 part of the button, and the bottom will cover the rest 1/3 of the button.
My goal is not far from being reached, but I get weird behavior - the labels are a little out of the button, and in some cases they disappear(I can click on the button but cannot see the labels).
This is my custom UIButton for reference, I hope this code will help people regardless to my issue:
class ButtonWithStats: UIButton {
var num: Int
var name: String
var nameLabel: UILabel?
var numLabel: UILabel?
required init?(coder aDecoder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
self.num = 0
self.name = ""
self.numLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0)) //just to init the labels.
self.nameLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
self.numLabel?.textAlignment = .center
self.nameLabel?.textAlignment = .center
self.numLabel?.textColor = UIColor.purple
self.nameLabel?.textColor = UIColor.black
self.numLabel?.font = UIFont.init(name: "Helvetica", size: 11)
self.nameLabel?.font = UIFont.init(name: "Helvetica", size: 13)
super.init(coder: aDecoder)
}
func setButton(numInput: Int, nameInput: String){
self.num = numInput
self.name = nameInput
self.setLabels()
}
private func setLabels(){
numLabel?.text = String(self.num)
nameLabel?.text = self.name
let widthForName = self.frame.width
let heightForName = self.frame.height * 2 / 3
nameLabel?.center = self.center
nameLabel?.frame = CGRect(origin: self.frame.origin, size: CGSize(width: widthForName, height: heightForName))
let widthForNum = self.frame.width
let heightForNum = self.frame.height * 1 / 3
let yForNum = (self.frame.height * 2 / 3) //+ self.frame.origin.y
numLabel?.frame = CGRect(x: self.frame.origin.x, y: yForNum, width: widthForNum, height: heightForNum)
self.addSubview(nameLabel!)
self.addSubview(numLabel!)
print("#########################")
print("Button \(self.nameLabel?.text) frame is: \(self.frame)")
print("Label frame is: \(self.nameLabel?.frame)")
print("Num frame is: \(self.numLabel?.frame)")
print("#########################")
}
}
my console prints this data:
#########################
Button Optional("button one") frame is: (257.5, 0.0, 64.5, 33.0)
Label frame is: Optional((257.5, 0.0, 64.5, 22.0))
Num frame is: Optional((257.5, 22.0, 64.5, 11.0))
#########################
#########################
Button Optional("Button two") frame is: (129.0, 0.0, 64.0, 33.0)
Label frame is: Optional((129.0, 0.0, 64.0, 22.0))
Num frame is: Optional((129.0, 22.0, 64.0, 11.0))
#########################
#########################
Button Optional("Button three") frame is: (0.0, 0.0, 64.5, 33.0)
Label frame is: Optional((0.0, 0.0, 64.5, 22.0))
Num frame is: Optional((0.0, 22.0, 64.5, 11.0))
#########################
Rather than add 2 labels, how about setting the numberOfLines on the UIButtons's textLabel?
button.titleLabel?.numberOfLines = 2
Or alternatively, add a vertical UIStackView, with the 2 labels as subviews, and set stackView.frame = button.bounds
let stackView = UIStackView(arrangedSubviews: [numLabel, nameLabel])
stackView.frame = buttonView.bounds
addSubview(stackView)
You can adjust the stackView.spacing / layoutMargins etc, to position the labels as required
To make the numLabel twice the height of the nameLabel, you could use set constraints using…
numLabel.heightAnchor.constraint(equalTo: nameLabel.heightAnchor, multiplier: 2)
I'd recommend you do this in a storyboard / xib, but if you need to do it programatically, you might find it useful create a xib to experiment with the UIStackView settings.