Related
im working with reaction like message, here is what i did (using customProvider from contextMenu)
below is the code when i added emoji action to customProvider controller
private func addReactionList(_ items : [Reaction]) -> UIView{
let reactionView = UIView()
var arrangedSubviews: [UIView] = []
for reaction in items {
let imgView = UIImageView(image: AppUtils.reactionToImage(reaction: reaction))
imgView.isUserInteractionEnabled = true
imgView.frame = CGRect(x: 0, y: 0, width: iconHeight, height: iconHeight)
imgView.layer.cornerRadius = (imgView.frame.height) / 2
imgView.layer.masksToBounds = true
imgView.layer.borderColor = UIColor.clear.cgColor
imgView.layer.borderWidth = 0
imgView.contentMode = .scaleAspectFit
imgView.restorationIdentifier = reaction.Type
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
tapGesture.delegate = self
tapGesture.numberOfTapsRequired = 1
tapGesture.delaysTouchesBegan = true
tapGesture.cancelsTouchesInView = false
imgView.isUserInteractionEnabled = true
imgView.addGestureRecognizer(tapGesture)
arrangedSubviews.append(imgView)
}
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
stackView.distribution = .fillEqually
stackView.spacing = padding
stackView.layoutMargins = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding)
stackView.isLayoutMarginsRelativeArrangement = true
let width = (CGFloat(items.count) * iconHeight) + (CGFloat(items.count+1) * padding)
reactionView.frame = CGRect(x: 0, y: 0, width: width, height: iconHeight + 2 * padding)
reactionView.layer.cornerRadius = 12
reactionView.addSubview(stackView)
stackView.frame = reactionView.frame
stackView.isUserInteractionEnabled = false
reactionView.isUserInteractionEnabled = false
return reactionView
}
then i added this to custom provider vc:
let actionsReacts = addReactionList(likeReactions)
let emojiReacts = addReactionList(emojiReactions)
if let snap = customView.snapshotView(afterScreenUpdates: true) as UIView?{
view.addSubview(snap)
view.addSubview(actionsReacts)
view.addSubview(emojiReacts)
let maxWidth = snap.frame.size.width >= actionsReacts.frame.size.width ? snap.frame.size.width : actionsReacts.frame.size.width
var currentSnapFrame = snap.frame
currentSnapFrame = CGRect(Int((maxWidth - snap.frame.size.width)) / 2, 0 , Int(snap.frame.size.width), Int(snap.frame.size.height))
snap.frame = currentSnapFrame
var currentActionsReactFrame = actionsReacts.frame
currentActionsReactFrame = CGRect(0, Int(snap.frame.size.height), Int(actionsReacts.frame.size.width), Int(actionsReacts.frame.size.height))
actionsReacts.frame = currentActionsReactFrame
var currentEmojiReactFrame = actionsReacts.frame
currentEmojiReactFrame = CGRect(0, Int(snap.frame.size.height) + Int(emojiReacts.frame.size.height), Int(emojiReacts.frame.size.width), Int(emojiReacts.frame.size.height))
emojiReacts.frame = currentEmojiReactFrame
preferredContentSize = CGSize(width: maxWidth, height: snap.frame.size.height + actionsReacts.frame.size.height + emojiReacts.frame.size.height)
}
But the gesture can not fire, i tried to change img to button, but still not get click
#objc func handleTapGesture(_ sender : UITapGestureRecognizer){
let tappedImage = sender.view as! UIImageView
let reactionType = tappedImage.restorationIdentifier ?? ""
onSelectedReaction?(reactionType)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
i do not know why i can not click any where, even i tried to add gesture click to viewcontroller view. Can anyone explain it? Thanks in advance
i changed to present new VC with hero animation, so bored, but it is my best choice for now
I tried to reconstruct your demo app from what you have provided, but it is still not clear to me what your code is or how it behaves.
One thing I note that you are creating n gesture-recognizers and hooking each to an imageview.
Try instead using 1 gesture recognizer. Hook it to your parent view, and on tap-event, get the subview which is being hit.
Some code:
let tap = UITapGestureRecognizer(target: self, action: #selector(self.onViewTap))
parentView.addGestureRecognizer(tap)
#objc func onViewTap(_ sender: UITapGestureRecognizer) {
let point = sender.location(in: containerView)
if let hitSubview = parentView.hitTest(point, with: nil) {
// check which subview (imageView) is being hit by checking its tag or similar
}
I am trying to print properties from an array containing properties of a class when tapping on that button, I have read some answers but none are working. apparently println() use to work but it seems to have been depreciated. Below is the code in question:
this is the buttons class
class Buttons{
var nature = UIButton(type: UIButton.ButtonType.custom)
var name: String
init ( name: String) {
self.name = name
}
}
this is the array in the main class
var buttonsArray=[
Buttons(name: "below")
,
Buttons(name: "front")
,
Buttons(name: "right")
]
mapping:
for val in buttonsArray{
val.nature.frame = CGRect(x: 2, y: 10, width: 20, height:20)
val.nature.layer.cornerRadius = 0.5 * 0.12*bounds.size.width
val.nature.clipsToBounds = true
val.nature.backgroundColor = .blue
sceneView.addSubview(val.nature)
val.nature.adjustsImageWhenHighlighted = false
val.nature.addTarget(self, action: #selector(profileButtonClicked), for: UIControl.Event.touchDown)
val.nature.addTarget(self, action: #selector(profileButtonOut), for: UIControl.Event.touchUpInside)
val.nature.addTarget(self, action: #selector(profileButtonOut), for: UIControl.Event.touchDragExit)
}
assigned func
#objc func profileButtonClicked(button: self){
print(button.name)
}
I get an error in the function thought but the point is to print the tapped button name.
Thanks for your time
You can change the function parameter like
#objc func profileButtonClicked(button: Button){ print(button.name) }
For reference, you can see
[1]: What is "self" used for in Swift?
You also mentioned you are getting error. It would be better if you share the error message.
Since you are getting bad access. In that case I have two solutions for you, I tested both and worked well.
First one, you need to modify your profileButtonClicked() function like below
#objc func profileButtonClicked(_ button: Buttons){
for x in buttonsArray {
if x.nature == button {
print(x.name)
break
}
}
}
Or I prefer this one,
Main class change would be,
var buttonList = [
ButtonTest(name: "below"),
ButtonTest(name: "front"),
ButtonTest(name: "right")]
for button in buttonList {
sceneView.addSubview(button)
}
You can replace Button class with ButtonTest,
class ButtonTest: UIButton {
var name: String
init(name: String){
self.name = name
super.init(frame:.zero)
self.frame = CGRect(x: 2, y: 10, width: 100, height:40)
self.layer.cornerRadius = 0.5 * 0.12 * 200
self.clipsToBounds = true
self.backgroundColor = .blue
//self.setTitle(name, for: .normal)
self.adjustsImageWhenHighlighted = false
//self.contentEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 0, right: -25.0)
//view.addSubview(val.nature)
self.addTarget(self, action: #selector(profileButtonClicked), for: UIControl.Event.touchDown)
//
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func profileButtonClicked(button: ButtonTest){
print(button.name)
}
}
I am trying to drag an UIImage using UIDragInteraction. I am trying to implement the procedure as documented in Apple's documentation. My code is below. I am trying to perform the drag feature but currently my image is not moving even when i hold the left mouse button on the image and try to drag it (I have yet to implement the Drop implementation as i am trying to do this one step at a time). I am executing the code on a simulator.
my code:
class StartGameViewController: UIViewController{
var dragInteraction: UIDragInteraction!
var dragInteractionDelegate: UIDragInteractionDelegate!
var dragSourceImgView: UIImageView = {
let imgView = UIImageView()
imgView.backgroundColor = UIColor.red.withAlphaComponent(0.5)
imgView.contentMode = UIImageView.ContentMode.scaleToFill
return imgView
}()
var dropImgSourceView: UIImageView = {
let imgView = UIImageView()
imgView.contentMode = UIImageView.ContentMode.scaleAspectFit
imgView.backgroundColor = UIColor.blue.withAlphaComponent(0.5)
return imgView
}()
override func viewDidLoad() {
super.viewDidLoad()
dragSourceImgView.image = UIImage.init(named: "rider")
let frame = CGRect.init(x: 50, y: 50, width: 100, height: 100)
dragSourceImgView.frame = frame
container1.addSubview(dragSourceImgView)
let frameDrop = CGRect.init(x: 200, y: 50, width: 100, height: 100)
dropImgSourceView.frame = frameDrop
container1.addSubview(dropImgSourceView)
// Enable imageView as a drag source
dragInteraction = UIDragInteraction.init(delegate: self)
dragSourceImgView.addInteraction(dragInteraction)
}
}
// DRAG
extension StartGameViewController: UIDragInteractionDelegate{
// Create a Drag Item
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
guard let image = dragSourceImgView.image else{return []}
let itemProvider = NSItemProvider.init(object: image)
let dragItem = UIDragItem.init(itemProvider: itemProvider)
return [dragItem]
}
}
You should set your UIImageView to allow user interaction:
dragSourceImageView.userInteractionEnabled = true
If you are running on an iPhone you must also set the UIDragInteraction to be enabled:
dragInteraction.isEnabled = true
I am trying to display badge on my notification button, in app as displayed on AppIcon.
So far whatever i have researched is related to Obj. C, but nothing that specifically discussed way to implement that solution into Swift,
Please help to find a solution to add a custom class / code to achieve Badge on UiBarbutton and UiButton.
Researched so far:
https://github.com/Marxon13/M13BadgeView
along with MKBadge class etc.
There is a more elegant solution with an extension for UIButtonItem
extension CAShapeLayer {
func drawCircleAtLocation(location: CGPoint, withRadius radius: CGFloat, andColor color: UIColor, filled: Bool) {
fillColor = filled ? color.cgColor : UIColor.white.cgColor
strokeColor = color.cgColor
let origin = CGPoint(x: location.x - radius, y: location.y - radius)
path = UIBezierPath(ovalIn: CGRect(origin: origin, size: CGSize(width: radius * 2, height: radius * 2))).cgPath
}
}
private var handle: UInt8 = 0
extension UIBarButtonItem {
private var badgeLayer: CAShapeLayer? {
if let b: AnyObject = objc_getAssociatedObject(self, &handle) as AnyObject? {
return b as? CAShapeLayer
} else {
return nil
}
}
func addBadge(number: Int, withOffset offset: CGPoint = CGPoint.zero, andColor color: UIColor = UIColor.red, andFilled filled: Bool = true) {
guard let view = self.value(forKey: "view") as? UIView else { return }
badgeLayer?.removeFromSuperlayer()
// Initialize Badge
let badge = CAShapeLayer()
let radius = CGFloat(7)
let location = CGPoint(x: view.frame.width - (radius + offset.x), y: (radius + offset.y))
badge.drawCircleAtLocation(location: location, withRadius: radius, andColor: color, filled: filled)
view.layer.addSublayer(badge)
// Initialiaze Badge's label
let label = CATextLayer()
label.string = "\(number)"
label.alignmentMode = CATextLayerAlignmentMode.center
label.fontSize = 11
label.frame = CGRect(origin: CGPoint(x: location.x - 4, y: offset.y), size: CGSize(width: 8, height: 16))
label.foregroundColor = filled ? UIColor.white.cgColor : color.cgColor
label.backgroundColor = UIColor.clear.cgColor
label.contentsScale = UIScreen.main.scale
badge.addSublayer(label)
// Save Badge as UIBarButtonItem property
objc_setAssociatedObject(self, &handle, badge, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
func updateBadge(number: Int) {
if let text = badgeLayer?.sublayers?.filter({ $0 is CATextLayer }).first as? CATextLayer {
text.string = "\(number)"
}
}
func removeBadge() {
badgeLayer?.removeFromSuperlayer()
}
}
This great code was created by Stefano Vettor and you can find all the details at:
https://gist.github.com/freedom27/c709923b163e26405f62b799437243f4
Working Solution :
Step 1:
Firstly create new swift file which is a subclass to UIButton as follows:
import UIKit
class BadgeButton: UIButton {
var badgeLabel = UILabel()
var badge: String? {
didSet {
addbadgetobutton(badge: badge)
}
}
public var badgeBackgroundColor = UIColor.red {
didSet {
badgeLabel.backgroundColor = badgeBackgroundColor
}
}
public var badgeTextColor = UIColor.white {
didSet {
badgeLabel.textColor = badgeTextColor
}
}
public var badgeFont = UIFont.systemFont(ofSize: 12.0) {
didSet {
badgeLabel.font = badgeFont
}
}
public var badgeEdgeInsets: UIEdgeInsets? {
didSet {
addbadgetobutton(badge: badge)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
addbadgetobutton(badge: nil)
}
func addbadgetobutton(badge: String?) {
badgeLabel.text = badge
badgeLabel.textColor = badgeTextColor
badgeLabel.backgroundColor = badgeBackgroundColor
badgeLabel.font = badgeFont
badgeLabel.sizeToFit()
badgeLabel.textAlignment = .center
let badgeSize = badgeLabel.frame.size
let height = max(18, Double(badgeSize.height) + 5.0)
let width = max(height, Double(badgeSize.width) + 10.0)
var vertical: Double?, horizontal: Double?
if let badgeInset = self.badgeEdgeInsets {
vertical = Double(badgeInset.top) - Double(badgeInset.bottom)
horizontal = Double(badgeInset.left) - Double(badgeInset.right)
let x = (Double(bounds.size.width) - 10 + horizontal!)
let y = -(Double(badgeSize.height) / 2) - 10 + vertical!
badgeLabel.frame = CGRect(x: x, y: y, width: width, height: height)
} else {
let x = self.frame.width - CGFloat((width / 2.0))
let y = CGFloat(-(height / 2.0))
badgeLabel.frame = CGRect(x: x, y: y, width: CGFloat(width), height: CGFloat(height))
}
badgeLabel.layer.cornerRadius = badgeLabel.frame.height/2
badgeLabel.layer.masksToBounds = true
addSubview(badgeLabel)
badgeLabel.isHidden = badge != nil ? false : true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addbadgetobutton(badge: nil)
fatalError("init(coder:) is not implemented")
}
}
Step 2:
Create a function in your base file which u can use in each View Controller :
func addBadge(itemvalue: String) {
let bagButton = BadgeButton()
bagButton.frame = CGRect(x: 0, y: 0, width: 44, height: 44)
bagButton.tintColor = UIColor.darkGray
bagButton.setImage(UIImage(named: "ShoppingBag")?.withRenderingMode(.alwaysTemplate), for: .normal)
bagButton.badgeEdgeInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 15)
bagButton.badge = itemvalue
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: bagButton)
}
Step 3 :
Use above function from any View Controller in this way :
self.addBadge(itemvalue: localStorage.string(forKey: "total_products_in_cart") ?? "0")
First create label, then right bar button. On right bar button add subview which will be badge count. Finally add navigation right bar button.
SWIFT 5
let badgeCount = UILabel(frame: CGRect(x: 22, y: -05, width: 20, height: 20))
badgeCount.layer.borderColor = UIColor.clear.cgColor
badgeCount.layer.borderWidth = 2
badgeCount.layer.cornerRadius = badgeCount.bounds.size.height / 2
badgeCount.textAlignment = .center
badgeCount.layer.masksToBounds = true
badgeCount.textColor = .white
badgeCount.font = badgeCount.font.withSize(12)
badgeCount.backgroundColor = .red
badgeCount.text = "4"
let rightBarButton = UIButton(frame: CGRect(x: 0, y: 0, width: 35, height: 35))
rightBarButton.setBackgroundImage(UIImage(named: "NotificationBell"), for: .normal)
rightBarButton.addTarget(self, action: #selector(self.onBtnNotification), for: .touchUpInside)
rightBarButton.addSubview(badgeCount)
let rightBarButtomItem = UIBarButtonItem(customView: rightBarButton)
navigationItem.rightBarButtonItem = rightBarButtomItem
I had the same task. I didn't want to use third-party libraries. Firstly, I tried Stefano's solution and it's great however I decided to implement my own way to solve it.
In my humble opinion, there are simple steps described below briefly:
Create UIView instance within .xib file and put necessary items like UILabel or UIImageView instance depending on your design requirements.
The final action I did in this step is putting invisible button in the top of view's hierarchy.
Create YourCustomView.swift and link all #IBOutlets from xib to current file inside your custom view class implementation.
Next, implement class function in YourCustomView class which will load custom view from xib and return it as YourCustomView instance.
Finally, add your custom badge to your custom view controller instance!
My result is..
P.S. If you need to implement #IBActions I recommend to link your custom view and custom view controller through the delegate pattern.
using M13BadgeView.. use this code
(im using fontawesome.swift for buttons :: https://github.com/thii/FontAwesome.swift)
let rightButton = UIButton(frame: CGRect(x:0,y:0,width:30,height:30))
rightButton.titleLabel?.font = UIFont.fontAwesome(ofSize: 22)
rightButton.setTitle(String.fontAwesomeIcon(name: .shoppingBasket), for: .normal)
let rightButtonItem : UIBarButtonItem = UIBarButtonItem(customView: rightButton)
let badgeView = M13BadgeView()
badgeView.text = "1"
badgeView.textColor = UIColor.white
badgeView.badgeBackgroundColor = UIColor.red
badgeView.borderWidth = 1.0
badgeView.borderColor = UIColor.white
badgeView.horizontalAlignment = M13BadgeViewHorizontalAlignmentLeft
badgeView.verticalAlignment = M13BadgeViewVerticalAlignmentTop
badgeView.hidesWhenZero = true
rightButton.addSubview(badgeView)
self.navigationItem.rightBarButtonItem = rightButtonItem
Good answer #Julio Bailon (https://stackoverflow.com/a/45948819/1898973)!
Here is the author's site with full explanation: http://www.stefanovettor.com/2016/04/30/adding-badge-uibarbuttonitem/.
It seems not to be working on iOS 11, maybe because the script try to access the "view" property of the UIBarButtonItem. I made it work:
By creating a UIButton and then creating the UIBarButtonItem using the UIButton as a customView:
navigationItem.rightBarButtonItem = UIBarButtonItem.init(
customView: shoppingCartButton)
By replacing the line in the UIBarButtonItem extension:
guard let view = self.value(forKey: "view") as? UIView else { return }
with the following:
guard let view = self.customView else { return }
Seems elegant to me and, best of all, it worked!
You can set below constraints to UILabel with respect to UIButton
align UILabel's top and trailing to UIButton
And when you need to show badge set text to UILabel and when you don't want to show badge then set empty string to UILabel
Download This
For BarButtonItem : Drag and Drop UIBarButtonItem+Badge.h and UIBarButtonItem+Badge.m class in project.
Write this code for set Badges:
self.navigationItem.rightBarButtonItem.badgeValue = "2"
self.navigationItem.rightBarButtonItem.badgeBGColor = UIColor.black
For UIButtton : Drag and Drop UIButton+Badge.h and UIButton+Badge.m class in project.
self.notificationBtn.badgeValue = "2"
self.notificationBtn.badgeBGColor = UIColor.black
Answer with extension from Julio will not work.
Starting from iOS 11 this code will not work cause line of code below will not cast UIView. Also it's counting as private API and seems to be will not pass AppStore review.
guard let view = self.value(forKey: "view") as? UIView else { return }
Thread on Apple Developer Forum
Second thing that this snippet always draws circle, so it can't fit numbers bigger than 9.
Here the simplified version by using custom view
Easy and clear solution if you are looking for only adding the red dot without the number;
private var handle: UInt8 = 0;
extension UIBarButtonItem {
private var badgeLayer: CAShapeLayer? {
if let b: AnyObject = objc_getAssociatedObject(self, &handle) as AnyObject? {
return b as? CAShapeLayer
} else {
return nil
}
}
func setBadge(offset: CGPoint = .zero, color: UIColor = .red, filled: Bool = true, fontSize: CGFloat = 11) {
badgeLayer?.removeFromSuperlayer()
guard let view = self.value(forKey: "view") as? UIView else {
return
}
var font = UIFont.systemFont(ofSize: fontSize)
if #available(iOS 9.0, *) {
font = UIFont.monospacedDigitSystemFont(ofSize: fontSize, weight: .regular)
}
//Size of the dot
let badgeSize = UILabel(frame: CGRect(x: 22, y: -05, width: 10, height: 10))
// initialize Badge
let badge = CAShapeLayer()
let height = badgeSize.height
let width = badgeSize.width
// x position is offset from right-hand side
let x = view.frame.width + offset.x - 17
let y = view.frame.height + offset.y - 34
let badgeFrame = CGRect(origin: CGPoint(x: x, y: y), size: CGSize(width: width, height: height))
badge.drawRoundedRect(rect: badgeFrame, andColor: color, filled: filled)
view.layer.addSublayer(badge)
// initialiaze Badge's label
let label = CATextLayer()
label.alignmentMode = .center
label.font = font
label.fontSize = font.pointSize
label.frame = badgeFrame
label.foregroundColor = filled ? UIColor.white.cgColor : color.cgColor
label.backgroundColor = UIColor.clear.cgColor
label.contentsScale = UIScreen.main.scale
badge.addSublayer(label)
// save Badge as UIBarButtonItem property
objc_setAssociatedObject(self, &handle, badge, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
// bring layer to front
badge.zPosition = 1_000
}
private func removeBadge() {
badgeLayer?.removeFromSuperlayer()
}
}
// MARK: - Utilities
extension CAShapeLayer {
func drawRoundedRect(rect: CGRect, andColor color: UIColor, filled: Bool) {
fillColor = filled ? color.cgColor : UIColor.white.cgColor
strokeColor = color.cgColor
path = UIBezierPath(roundedRect: rect, cornerRadius: 7).cgPath
}
}
The source of the code:
https://gist.github.com/freedom27/c709923b163e26405f62b799437243f4
I only made a few changes to eliminate the number.
The MIBadgeButton-Swift is working also on UIBarButtonItems.
Here is my code after the navigation bar is created:
let rightBarButtons = self.navigationItem.rightBarButtonItems
let alarmsBarButton = rightBarButtons?.last
let alarmsButton = alarmsBarButton.customView as! MIBadgeButton?
alarmsButton.badgeString = "10"
You can do it programmatically with
self.tabBarItem.badgeColor = .red
or use the storyboard. See:
I am using the following codes to add two button to self.navigationItem.rightBarButtonItems, and I think in iOS7, the space between two buttons are too wide, is there a way to decrease the space between these two buttons?
UIBarButtonItem *saveStyleButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:#"save.png"] style:UIBarButtonItemStyleBordered target:self action:#selector(saveStyle)];
UIBarButtonItem *shareStyleButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:#selector(shareStyle)];
NSArray *arr= [[NSArray alloc] initWithObjects:shareStyleButton,saveStyleButton,nil];
self.navigationItem.rightBarButtonItems=arr;
Appreciate any hint or idea.
Updated at Jul 2015
A better way to do this is to use storyboard (tested in Xcode 6.4). First, add a UINavigationItem; secondly, add a Bar Button Item; thirdly, add a view to the Bar Button Item you just created in step 2; fourthly, add as many buttons as you wish into that view you just dragged in; lastly, adjust the space with your mouse and constraints.
Related Questions
Can't assign multiple Buttons to UINavigationItem when using Storyboard with iOS 5
How to add buttons to navigation controller visible after segueing?
Old Answer (Only acceptable for small insets)
Use imageInsets property:
leftButton.imageInsets = UIEdgeInsetsMake(0.0, 0.0, 0, -15);
rightButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, 0);
for three or more buttons, the middle one(s) get both insets:
leftButton.imageInsets = UIEdgeInsetsMake(0.0, 0.0, 0, -15);
middleButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, -15);
rightButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, 0);
For the right side buttons, be careful: the FIRST button in the item array is the RIGHT one:
rightButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, 0);
middleButton.imageInsets = UIEdgeInsetsMake(0.0, -15, 0, -15);
leftButton.imageInsets = UIEdgeInsetsMake(0.0, 0.0, 0, -15);
IMPORTANT: Split the inset between the two neighbors; if apply the entire inset to one edge, it will become obvious that the buttons are overlapping in the "blank" space - one button gets all of the "gap" touches. Even when "split" the adjustment like this, at -40 on both edges, the tap will definitely go to wrong button sometimes. -15 or -20 is the most to consider using with this technique.
By applying this method, the button could even be moved around in four directions.
My solution is using a custom view for right bar buttons.
Create a horizontal stackview with equal spacing and add any number of buttons as subview.
Sample code:
func addRightBarButtonItems()
{
let btnSearch = UIButton.init(type: .custom)
btnSearch.setImage(UIImage(systemName: "magnifyingglass"), for: .normal)
btnSearch.addTarget(self, action: #selector(MyPageContainerViewController.searchButtonPressed), for: .touchUpInside)
let btnEdit = UIButton.init(type: .custom)
btnEdit.setImage(UIImage(systemName: "pencil"), for: .normal)
btnEdit.addTarget(self, action: #selector(MyPageContainerViewController.editButtonPressed), for: .touchUpInside)
let stackview = UIStackView.init(arrangedSubviews: [btnEdit, btnSearch])
stackview.distribution = .equalSpacing
stackview.axis = .horizontal
stackview.alignment = .center
stackview.spacing = 8
let rightBarButton = UIBarButtonItem(customView: stackview)
self.navigationItem.rightBarButtonItem = rightBarButton
}
Swift 5
In your AppDelegate add this code:
let stackViewAppearance = UIStackView.appearance(whenContainedInInstancesOf: [UINavigationBar.self])
stackViewAppearance.spacing = -10
This will work with no additional code in more recent SDK versions as UIBarButtonItems are already contained in a horizontal UIStackView
First:
For UIBarButtonItem you must use constructor init(customView: UIView)
Second:
Use fixedSpace for set space between buttons
example:
let firstButton = UIButton()
let firstButtonItem = UIBarButtonItem(customView: firstButton)
let secondButton = UIButton()
let secondButtonItem = UIBarButtonItem(customView: secondButton)
let space = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
space.width = WIDTH
self.navigationItem.rightBarButtonItems = [firstButtonItem, space, secondButtonItem]
One line of code is all you need to decrease the space between buttons in the navigation bar:
UIStackView.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).spacing = -10
You must place this line in your code before you add the buttons to the navigation bar.
If you are looking to have 2 buttons on the top right with no space in between them or on the right, this has worked for me.
let imgLeft = UIImage(named: "buttonLeft")?.imageWithRenderingMode(.AlwaysOriginal)
let bLeft = UIBarButtonItem(image: imgLeft, style: UIBarButtonItemStyle.Done, target: self, action: "action1:")
let space = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FixedSpace, target: nil, action: nil)
space.width = -16.0
bLeft.imageInsets = UIEdgeInsetsMake(0, 0, 0, -25.0)
let imgRight = UIImage(named: "buttonRight")?.imageWithRenderingMode(.AlwaysOriginal)
let bRight = UIBarButtonItem(image: imgRight, style: UIBarButtonItemStyle.Done, target: self, action: "action2:")
bRight.imageInsets = UIEdgeInsetsMake(0, -25, 0, 0)
self.navigationItem.rightBarButtonItems = [space,bLeft,bRight ]
My situation was about giving horizontal space to logOut Button to the right edge.
func addLogOutButtonToNavigationBar(triggerToMethodName: String)
{
let button: UIButton = UIButton()
button.setImage(UIImage(named: "logOff.png"), forState: .Normal)
button.frame = CGRectMake(20, 0, 30, 25)
button.contentEdgeInsets = UIEdgeInsets.init(top: 0, left: 10, bottom: 0, right: -10)
button .addTarget(self, action:Selector(triggerToMethodName), forControlEvents: UIControlEvents.TouchUpInside)
let rightItem:UIBarButtonItem = UIBarButtonItem()
rightItem.customView = button
self.navigationItem.rightBarButtonItem = rightItem
}
Might be a bit late for this answer however this can help the newest IOS+Swift combination (IOS 10 and Swift 3 in my case). Here I describe a general approach for how to move items right/left for rightBarButtonItems/leftBarButtonItems:
The property you we have use here to move a barButtonItem is "imageEdgeInsets" . So, Here how to use this property -
yourBarButtonItem.imageEdgeInsets = UIEdgeInsetsMake(top, left, bottom, right)
These top, left, bottom, right are of type CGFloat and these are basically margin value that pushes your item from/to each other.
For decreasing a space, we can just use minus (-) values like this " -10 ".
So, for example if we want to use this for a group of leftBatButtonItems and say, if we want to move a item to the a bit right, then we can do this -
ourBarButtonItem.imageEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, 0.0, -15)
I hope we get the general idea here and hope it helps :)
Without any code. I just put another UIBarButtonItem in-between the buttons that need spacing in storyboard. The button is just a placeholder for spacing and the UIBarButton should have UIView as subview of the UIBarButtonItem. adjust the view's width for your spacing. See Screen shots.
Create a UIBarButtonItem with type flexible or fixed space. Set the width and add it to the array of barbuttonitems. Try using a negative width, see if that works.
Or, you could maybe adjust your image. The system buttons i think have a fixed size, and might include some transparent part, so even when packed together the still seem spaced.
Swift 5
If you want to add space between two Bar Button items then add a flexible space in between, the two buttons will be pushed to the left and right edge as the flexible space expands to take up most of the toolbar.
For Example:
let toolBar = UIToolbar()
var items = [UIBarButtonItem]()
let backBarButton = UIBarButtonItem(image: UIImage(named: "icon-back.png"), style: .done, target: self, action: #selector(backButtonTapped))
let nextBarButton = UIBarButtonItem(image: UIImage(named: "icon-next.png"), style: .done, target: self, action: #selector(nextButtonTapped))
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
items.append(backBarButton)
items.append(spacer)
items.append(nextBarButton)
toolBar.setItems(items, animated: true)
To accomplish this in code without adding an extra container view, use a UIBarButtonItem with the system item type set to FixedSpace. Then set the width of the fixed space to -10 and place it between the two buttons.
another answer :
It works in ios 9 - 12.
You should call fixNavigationItemsMargin(margin:) in function viewDidAppear(_ animated: Bool) and viewDidLayoutSubviews().
fixNavigationItemsMargin(margin:) would modify the UINavigationController stack.
you could call fixNavigationItemsMargin(margin:) in BaseNavigationController ,do the common work. And call fixNavigationItemsMargin(margin:) in UIViewController do precise layout.
// do common initilizer
class BaseNavigationController: UINavigationController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
fixNavigationItemsMargin()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
fixNavigationItemsMargin()
}
}
extension UINavigationController {
func fixNavigationItemsMargin(_ margin: CGFloat = 8) {
let systemMajorVersion = ProcessInfo.processInfo.operatingSystemVersion.majorVersion
if systemMajorVersion >= 11 {
// iOS >= 11
guard let contentView = navigationBar.subviews
.first(
where: { sub in
String(describing: sub).contains("ContentView")
}) else { return }
// refer to: https://www.matrixprojects.net/p/uibarbuttonitem-ios11/
// if rightBarButtonItems has not any custom views, then margin would be 8(320|375)/12(414)
// should use customView
let needAdjustRightItems: Bool
if let currentVC = viewControllers.last,
let rightItems = currentVC.navigationItem.rightBarButtonItems,
rightItems.count > 0,
rightItems.filter({ $0.customView != nil }).count > 0 {
needAdjustRightItems = true
} else {
print("Use 8(320|375)/12(414), if need precious margin ,use UIBarButtonItem(customView:)!!!")
needAdjustRightItems = false
}
let needAdjustLeftItems: Bool
if let currentVC = viewControllers.last,
let leftItems = currentVC.navigationItem.leftBarButtonItems,
leftItems.count > 0,
leftItems.filter({ $0.customView != nil }).count > 0 {
needAdjustLeftItems = true
} else {
print("Use 8(320|375)/12(414), if need precious margin ,use UIBarButtonItem(customView:)!!!")
needAdjustLeftItems = false
}
let layoutMargins: UIEdgeInsets
if #available(iOS 11.0, *) {
let directionInsets = contentView.directionalLayoutMargins
layoutMargins = UIEdgeInsets(
top: directionInsets.top,
left: directionInsets.leading,
bottom: directionInsets.bottom,
right: directionInsets.trailing)
} else {
layoutMargins = contentView.layoutMargins
}
contentView.constraints.forEach(
{ cst in
// iOS 11 the distance between rightest item and NavigationBar should be margin
// rightStackView trailing space is -margin / 2
// rightestItem trailing to rightStackView trailing is -margin / 2
let rightConstant = -margin / 2
switch (cst.firstAttribute, cst.secondAttribute) {
case (.leading, .leading), (.trailing, .trailing):
if let stackView = cst.firstItem as? UIStackView,
stackView.frame.minX < navigationBar.frame.midX {
// is leftItems
if needAdjustLeftItems {
cst.constant = margin - layoutMargins.left
}
} else if let layoutGuide = cst.firstItem as? UILayoutGuide,
layoutGuide.layoutFrame.minX < navigationBar.frame.midX {
// is leftItems
if needAdjustLeftItems {
cst.constant = margin - layoutMargins.left
}
}
if let stackView = cst.firstItem as? UIStackView,
stackView.frame.maxX > navigationBar.frame.midX {
// is rightItems
if needAdjustRightItems {
cst.constant = rightConstant
}
} else if let layoutGuide = cst.firstItem as? UILayoutGuide,
layoutGuide.layoutFrame.maxX > navigationBar.frame.midX {
// is rightItems
if needAdjustRightItems {
cst.constant = rightConstant
}
}
default: break
}
})
// ensure items space == 8, minispcae
contentView.subviews.forEach(
{ subsub in
guard subsub is UIStackView else { return }
subsub.constraints.forEach(
{ cst in
guard cst.firstAttribute == .width
|| cst.secondAttribute == .width
else { return }
cst.constant = 0
})
})
} else {
// iOS < 11
let versionItemsCount: Int
if systemMajorVersion == 10 {
// iOS 10 navigationItem.rightBarButtonItems == 0
// space = 16(320|375) / 20(414)
// should adjust margin
versionItemsCount = 0
} else {
// iOS 9 navigationItem.rightBarButtonItems == 0
// space = 8(320|375) / 12(414)
// should not adjust margin
versionItemsCount = 1
}
let spaceProducer = { () -> UIBarButtonItem in
let spaceItem = UIBarButtonItem(
barButtonSystemItem: .fixedSpace,
target: nil,
action: nil)
spaceItem.width = margin - 16
return spaceItem
}
if let currentVC = viewControllers.last,
var rightItems = currentVC.navigationItem.rightBarButtonItems,
rightItems.count > versionItemsCount,
let first = rightItems.first {
// ensure the first BarButtonItem is NOT fixedSpace
if first.title == nil && first.image == nil && first.customView == nil {
print("rightBarButtonItems SPACE SETTED!!! SPACE: ", abs(first.width))
} else {
rightItems.insert(spaceProducer(), at: 0)
// arranged right -> left
currentVC.navigationItem.rightBarButtonItems = rightItems
}
}
if let currentVC = viewControllers.last,
var leftItems = currentVC.navigationItem.leftBarButtonItems,
leftItems.count > versionItemsCount,
let first = leftItems.first {
if first.title == nil && first.image == nil && first.customView == nil {
print("leftBarButtonItems SPACE SETTED!!! SPACE: ", abs(first.width))
} else {
leftItems.insert(spaceProducer(), at: 0)
// arranged left -> right
currentVC.navigationItem.leftBarButtonItems = leftItems
}
}
}
}
}
// do precise layout
class ViewController: UIViewController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
navigationController?.fixNavigationItemsMargin(40)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.fixNavigationItemsMargin(40)
}
Found a crazy idea that works.
func createCustomToolbar(items: [UIBarButtonItem]) -> UIToolbar
{
// no spacing between bar buttons
let customToolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: items.count*45, height: 44))
customToolbar.items = items
customToolbar.barStyle = UIBarStyle(rawValue: -1)!
customToolbar.clearsContextBeforeDrawing = false
customToolbar.backgroundColor = UIColor.clearColor()
customToolbar.tintColor = UIColor.clearColor()
customToolbar.translucent = true
return customToolbar
}
let customToolbar = createCustomToolbar([item0,item1,item2,item3])
navigationItem.rightBarButtonItems = [UIBarButtonItem(customView: customToolbar)]
Tested on iOS7 and upper. Even this is written in swift the concept is clear.
I gave up with fighting this bug, and came up with the following extension:
import UIKit
extension UIBarButtonItem {
convenience init(buttonImage: UIImage?, target: Any?, action: Selector?) {
let button = UIButton(type: .system)
button.frame = CGRect(origin: CGPoint.zero, size: buttonImage != nil ? buttonImage!.size : CGSize.zero)
button.setImage(buttonImage, for: .normal)
if let action = action {
button.addTarget(target, action: action, for: .touchUpInside)
}
self.init(customView: button)
}
public func updateButton(image: UIImage?) {
if let button = self.customView as? UIButton {
button.setImage(image, for: .normal)
let size = image != nil ? image!.size : CGSize.zero
let frame = button.frame
button.frame = frame.insetBy(dx: ceil((frame.size.width - size.width) / 2), dy: ceil((frame.size.height - size.height) / 2))
}
}
}