UIButton title and image positions are swapped unexpectedly on clicked - ios

I'd like to make a button that the image is on the right instead of the left side.
This is the expected layout:
I'm able to satisfy the layouts with the following code:
import UIKit
class ViewController: UIViewController {
private lazy var leftButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(didTap), for: .touchUpInside)
button.setImage(UIImage(systemName: "trash"), for: .normal)
button.setTitle("Trash", for: .normal)
button.setTitleColor(.systemBlue, for: .normal)
if let imageView = button.imageView {
imageView.contentMode = .scaleAspectFit
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: 8),
imageView.heightAnchor.constraint(equalToConstant: 8)
])
}
button.contentHorizontalAlignment = .left
button.contentVerticalAlignment = .center
button.semanticContentAttribute = .forceRightToLeft
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: leftButton)
}
#objc private func didTap() {
}
}
Everything looked exactly what I wanted at the beginning until I clicked the button. The positions of the title and the image were swapped. Please take a look at the gif:
Does anyone have any clue on why this happened? And how can I improve my code?
I guess it may have something to do with the navigation item.

Try it like this:
let button = UIButton(type: .system)
button.setTitle("Trash", for: .normal)
button.setImage(UIImage(systemName: "trash"), for: .normal)
button.setPreferredSymbolConfiguration(
.init(scale: .small), forImageIn: .normal
)
button.widthAnchor.constraint(equalToConstant: 60).isActive = true
button.imageEdgeInsets.right = -75
button.titleEdgeInsets.left = -35
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: button)
However, if you can confine yourself to iOS 15 and later, this is a lot more simple and coherent, because the notion of a trailing image is built right in:
var config = UIButton.Configuration.plain()
config.title = "Trash"
config.image = UIImage(systemName: "trash")?
.applyingSymbolConfiguration(.init(scale: .small))
config.imagePlacement = .trailing
let button = UIButton(configuration: config)
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: button)

Related

UIButton titleLabel has padding

UIButton title label has top and bottom padding, I want to remove padding.
Set UIButton content mode did not work.
Here is my code
lazy var button: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(ThemeColor.red, for: .normal)
button.setTitle("Push", for: .normal)
button.addTarget(self, action: #selector(buttonDidTap), for: .touchUpInside)
button.backgroundColor = .blue
button.contentHorizontalAlignment = .fill
button.contentVerticalAlignment = .fill
button.contentMode = .scaleAspectFill
return button
}()
and it looks like
How can I remove the padding space!
As Matt pointed out, you can fix this by adjusting the button's contentEdgeInsets
However, one thing I noticed, if you set the contentEdgeInsets to 0 all around:
button.contentEdgeInsets = UIEdgeInsets(top: 0,
left: 0,
bottom: 0,
right: 0)
You still get the the vertical padding for some reason.
I remember seeing an answer which I cannot find now where it suggested to set an extremely small edge inset and this should work:
lazy var button: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(.red, for: .normal)
button.setTitle("Push", for: .normal)
button.backgroundColor = .blue
button.contentHorizontalAlignment = .fill
button.contentVerticalAlignment = .fill
button.contentMode = .scaleAspectFill
button.contentEdgeInsets = UIEdgeInsets(top: .leastNormalMagnitude,
left: .leastNormalMagnitude,
bottom: .leastNormalMagnitude,
right: .leastNormalMagnitude)
return button
}()
You can use UIButton.Configuration and then set its contentInsets to .zero
lazy var button: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
var configuration = UIButton.Configuration.plain()
configuration.background.backgroundColor = .blue
configuration.background.cornerRadius = 0
configuration.baseForegroundColor = .red
configuration.title = "Push"
configuration.contentInsets = .zero
button.configuration = configuration
return button
}()
You can use the button's configuration to get more precise control over its appearance like so:
lazy var myButton: UIButton = {
let newButton = UIButton()
newButton.translatesAutoresizingMaskIntoConstraints = false
newButton.contentMode = .scaleAspectFill
newButton.addTarget(self, action: #selector(buttonDidTap), for: .touchUpInside)
// 'configurationUpdateHandler' property can be used to set appearance depending on its state
newButton.configurationUpdateHandler = { button in
switch button.state { // here i'll just use default so it's the same over all states
default:
button.configuration?.title = "Push"
button.configuration?.baseBackgroundColor = .blue
// remove padding here
button.configuration?.contentInsets.top = 0
button.configuration?.contentInsets.bottom = 0
}
}
return newButton
}()
Of course you don't need to use updateHandler, you can just access the configuration directly and just set it there
button.configuration?.contentInsets.top = 0
button.configuration?.contentInsets.bottom = 0
See if this solves the problem...

How to navigate to a tableview embed inside a navigation Controller from a different viewController?

I have a tablview embed in a navigation controller.
I created a new viewController outside of the navigation controller.
In the new viewController I created the following code to navigate to the tableview but got "unable to dequeue a cell with identifier datacell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard" error.
Where did I go wrong?
override func viewDidLoad() {
super.viewDidLoad()
let startButton: UIButton = {
let button = UIButton()
button.titleLabel?.font = .boldSystemFont(ofSize: 25)
button.setTitle(" START ", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .blue
button.setTitleColor(.yellow, for: .highlighted)
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.black.cgColor
button.layer.cornerRadius = 10
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(goToHeritageSelection), for: .touchUpInside)
return button
}()
view.addSubview(startButton)
startButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 30).isActive = true
startButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
startButton.widthAnchor.constraint(equalToConstant: 300).isActive = true
}
#objc private func goToHeritageSelection() {
let vc = AncientHeritageTableViewController()
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true, completion: nil)
}

iOS - UIButton image doesn't fit as expected

I have a UIButton that has a default image and if a property is set it changes to the property object (an image);
With the default image, everything works properly and the image fits perfectly but when I'm using the image, I don't know why the image doesn't fit.
Here's the code:
var user: User? {
didSet {
self.profileImageButton.setImage(self.user?.profileImage?.withRenderingMode(.alwaysOriginal) ?? #imageLiteral(resourceName: "plus_photo").withRenderingMode(.alwaysOriginal), for: .normal)
}
}
lazy var profileImageButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(#imageLiteral(resourceName: "plus_photo").withRenderingMode(.alwaysOriginal), for: .normal)
button.layer.cornerRadius = 80/2
button.contentMode = .scaleAspectFill
button.layer.masksToBounds = true
button.clipsToBounds = true
button.layer.borderWidth = 1
button.layer.borderColor = .white
button.addTarget(self, action: #selector(didTapPhoto), for: .touchUpInside)
return button
}()
I'm expecting something like this
But I'm getting this instead
Add this line to your button configuration
button.imageView?.contentMode = .scaleAspectFit

Change size of UIBarButtonItem (image) in Swift 3

I am trying to change the size of some icons in my navBar, but I am a little confused as to how to do this? My code so far is:
func setUpNavBarButtons() {
let moreButton = UIBarButtonItem (image: UIImage(named:"ic_more_vert_3")?.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(handleMore))
navigationItem.rightBarButtonItems = [moreButton]
let refreshButton = UIBarButtonItem (image: UIImage(named:"ic_refresh")?.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(refreshDataButton))
navigationItem.leftBarButtonItems = [refreshButton]
}
any help?
This is how I did it
iOS 10 and below
func setUpMenuButton(){
let menuBtn = UIButton(type: .custom)
menuBtn.frame = CGRect(x: 0.0, y: 0.0, width: 20, height: 20)
menuBtn.setImage(UIImage(named:"menuIcon"), for: .normal)
menuBtn.addTarget(self, action: #selector(vc.onMenuButtonPressed(_:)), for: UIControlEvents.touchUpInside)
let menuBarItem = UIBarButtonItem(customView: menuBtn)
self.navigationItem.leftBarButtonItem = menuBarItem
}
iOS 11 - Navigation bar come up with Autolayout so frame setting may not work
func setUpMenuButton(){
let menuBtn = UIButton(type: .custom)
menuBtn.frame = CGRect(x: 0.0, y: 0.0, width: 20, height: 20)
menuBtn.setImage(UIImage(named:"menuIcon"), for: .normal)
menuBtn.addTarget(self, action: #selector(vc.onMenuButtonPressed(_:)), for: UIControlEvents.touchUpInside)
let menuBarItem = UIBarButtonItem(customView: menuBtn)
let currWidth = menuBarItem.customView?.widthAnchor.constraint(equalToConstant: 24)
currWidth?.isActive = true
let currHeight = menuBarItem.customView?.heightAnchor.constraint(equalToConstant: 24)
currHeight?.isActive = true
self.navigationItem.leftBarButtonItem = menuBarItem
}
Extension for Swift 4.2
Just a different way of implementation the answer provided by anoop4real
However using button type .system allows the global tint to be applied to your icon
Usage:
navigationItem.leftBarButtonItem = UIBarButtonItem.menuButton(self, action: #selector(presentSettings), imageName: "settings")
Implementation:
extension UIBarButtonItem {
static func menuButton(_ target: Any?, action: Selector, imageName: String) -> UIBarButtonItem {
let button = UIButton(type: .system)
button.setImage(UIImage(named: imageName), for: .normal)
button.addTarget(target, action: action, for: .touchUpInside)
let menuBarItem = UIBarButtonItem(customView: button)
menuBarItem.customView?.translatesAutoresizingMaskIntoConstraints = false
menuBarItem.customView?.heightAnchor.constraint(equalToConstant: 24).isActive = true
menuBarItem.customView?.widthAnchor.constraint(equalToConstant: 24).isActive = true
return menuBarItem
}
}
You can configure the frame of you button like below:
let icon = UIImage(named: "imageName")
let iconSize = CGRect(origin: CGPoint.zero, size: CGSize(width: 50, height: 50))
let iconButton = UIButton(frame: iconSize)
iconButton.setBackgroundImage(icon, for: .normal)
let barButton = UIBarButtonItem(customView: iconButton)
iconButton.addTarget(self, action: #selector(foo), for: .touchUpInside)
navigationItem.leftBarButtonItem = barButton
#DogCoffee answer is a great and creative way to solve the problem. May I suggest some slightly mods in order to take into account size and tintColor
extension UIBarButtonItem {
static func menuButton(_ target: Any?,
action: Selector,
imageName: String,
size:CGSize = CGSize(width: 32, height: 32),
tintColor:UIColor?) -> UIBarButtonItem
{
let button = UIButton(type: .system)
button.tintColor = tintColor
button.setImage(UIImage(named: imageName), for: .normal)
button.addTarget(target, action: action, for: .touchUpInside)
let menuBarItem = UIBarButtonItem(customView: button)
menuBarItem.customView?.translatesAutoresizingMaskIntoConstraints = false
menuBarItem.customView?.heightAnchor.constraint(equalToConstant: size.height).isActive = true
menuBarItem.customView?.widthAnchor.constraint(equalToConstant: size.width).isActive = true
return menuBarItem
}
}
In the end I did it like this and it worked:
let moreButton = UIButton(frame: CGRect(x: 0, y: 0, width: 35, height: 35))
moreButton.setBackgroundImage(UIImage(named: "ic_more_vert_3"), for: .normal)
moreButton.addTarget(self, action: #selector(TableViewController.handleMore), for: .touchUpInside)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: moreButton)
Answer from: Change width of a UIBarButtonItem in a UINavigationBar in swift
If you are using SF symbol image, which is available from iOS 13, you could set the SF symbol image configuration.
let config = UIImage.SymbolConfiguration(pointSize: 30, weight: .light, scale: .default)
let image = UIImage(systemName: "plus.circle", withConfiguration: config)
Swift 5 extension using button closure, expanding on #valvoline 's answer.
As you can appreciate instead of using target and selector Button allows you to pass a closure.
extension UIBarButtonItem {
static func imageButton(
image: UIImage,
size: CGFloat = 24,
color: UIColor = .systemBackground,
action: #escaping () -> Void
) -> UIBarButtonItem {
// create ui button
let button = UIButton(type: .system)
// assign image
button.setImage(image, for: .normal)
button.tintColor = color
// assign action
button.addAction(.init(handler: { _ in action() }), for: .touchUpInside)
// create menu bar item using custom view
let menuBarItem = UIBarButtonItem(customView: button)
//use auto layout to assign bar button's size
menuBarItem.customView?.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[
menuBarItem.customView?.heightAnchor.constraint(equalToConstant: size),
menuBarItem.customView?.widthAnchor.constraint(equalToConstant: size)
].compactMap { $0 }
)
return menuBarItem
}
}
Usage Example:
let item = UIBarButtonItem.imageButton(image: image, color: .systemBlue) {
print("You pushed it!") }
You can configure the bar buttons using this function:
public convenience init(customView: UIView)
And You can init the custom view as You desire.
After that You can access the view if needed via UIBarButtonItem's:
open var customView: UIView?
Hint: Because UIButton is a child class of UIView, You can directly use it too.

Why isn't my UIButton in a UIBarButtonItem not showing in the Nav Bar?

I have a UINavigationController which changes the UIBarButtonItem in the top right corner, depending on the state of the app.
When view did load I initialize the buttons I need
var editBarButton: UIBarButtonItem!
var logoutBarButton: UIBarButtonItem!
var showRepositoryBarButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
editButton = UIButton()
editButton.setImage(UIImage(named: "icon pencil"), forState: .Normal)
editButton.frame = CGRectMake(0, 0, 30, 30)
editButton.addTarget(self, action: #selector(UserProfileNavigatorController.onEditClick), forControlEvents: .TouchUpInside)
editBarButton = UIBarButtonItem(customView: editButton)
let logoutButton = UIButton()
logoutButton.setImage(UIImage(named: "icon logout"), forState: .Normal)
logoutButton.frame = CGRectMake(0, 0, 30, 30)
logoutButton.addTarget(self, action: #selector(UserProfileNavigatorController.onLogoutClick), forControlEvents: .TouchUpInside)
logoutBarButton = UIBarButtonItem(customView: logoutButton)
let showRepositoryButton = UIButton()
logoutButton.setImage(UIImage(named: "icon logout"), forState: .Normal)
logoutButton.frame = CGRectMake(0, 0, 30, 30)
logoutButton.addTarget(self, action: #selector(UserProfileNavigatorController.onShowRepositoryOnWebClick), forControlEvents: .TouchUpInside)
showRepositoryBarButton = UIBarButtonItem(customView: showRepositoryButton)
presenter.viewDidLoad()
}
And depending on the situation, I change the buttons displayed in this way
func showUserProfile() {
navigationItem.setRightBarButtonItems([editBarButton, logoutBarButton], animated: false)
Router.showUserProfileInNavigatorController(self)
}
func showRepository(repository: Repository) {
navigationItem.setRightBarButtonItems([showRepositoryBarButton], animated: false)
Router.showRepository(self, repository: repository)
}
Even though this code is actually executed, nothing appears in the top right corner.
The problem was that that code was running in the UINavigationController. Instead, it's the UIViewController who must show the buttons in the UINavigationController.
So just moving the code inside the UIViewController is working.
Note that there are multiple problems with your code. First, note that while UIButton() will return a non-optional, UIImage may return an optional. So you really have no idea now whether you actually have typed the names of your images correctly. I created a simple project using your code above, with no images in it, and duplicated that if the images are nil, that nothing shows.
Then adding images, I only got two images showing. Looking closely at your code, the 3rd image re-defines loginButton but does not set any properties on showRepositoryButton.
Several suggestions:
us "do { ... }" to create a sub context to avoid unintended variable re-use
if using a UIButton, use the designated initializer (UIButton(type:...)
better still, use a UIBarButtonItem(image: ...)
pay really close attention to optionals, and let forced unwrapping crash at the appropriate time (or use an assert, guard, or precondition)
Without drastically modifying your code, here is what works fine for me:
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Howdie!"
do {
let button = UIButton()
let image = UIImage(named: "02-redo")!
button.setImage(image, forState: .Normal)
button.frame = CGRectMake(0, 0, 30, 30)
button.addTarget(self, action: #selector(onEditClick), forControlEvents: .TouchUpInside)
editBarButton = UIBarButtonItem(customView: button)
}
do {
let button = UIButton()
let image = UIImage(named: "03-loopback")!
button.setImage(image, forState: .Normal)
button.frame = CGRectMake(0, 0, 30, 30)
button.addTarget(self, action: #selector(onLogoutClick), forControlEvents: .TouchUpInside)
logoutBarButton = UIBarButtonItem(customView: button)
}
do {
let button = UIButton()
let image = UIImage(named: "04-squiggle")!
button.setImage(image, forState: .Normal)
button.frame = CGRectMake(0, 0, 30, 30)
button.addTarget(self, action: #selector(onShowRepositoryOnWebClick), forControlEvents: .TouchUpInside)
showRepositoryBarButton = UIBarButtonItem(customView: button)
}
navigationItem.setRightBarButtonItems([editBarButton, logoutBarButton, showRepositoryBarButton], animated: false)
}

Resources