Subclassing UINavigationBar with Swift - ios

I'm writing an app that gives a user tokens to spend and I want to display the user's current number of tokens in a UINavigationBar. What I would like to have is a label with the number of tokens, and an image of a coin in the top right corner of my navigation bar.
I've been searching for ways to customize the UINavigationBar, and have found plenty of posts related to adding an image to cover the entire bar, and changing the title. However, I can't find a simple way to do what I want.
I think I need to subclass UINavigationBar and add the text/image myself, but being new to iOS development and Swift, I was hoping that someone could point me in the right direction.

You can create a custom view contains subviews UILabel and UIImageView to show the token number and token image. Add it to right bar button item of the navigation controller.
It will look like:
Below code will create the custom view. Here you can observe that it is a local variable. However, you can manage global variable for custom view or create a whole new class and manage it independently for real-time updates to show token number.
// Custom to hold token number and image
let tokenView = UIView(frame: CGRect(x:0, y:0, width:100, height:44))
tokenView.backgroundColor = UIColor.yellow
// Label to show token number
let tokenLabel = UILabel(frame: CGRect(x:0, y:0, width:60, height:44))
tokenLabel.text = "1234"
tokenLabel.textAlignment = NSTextAlignment.right
let imageHeight = CGFloat(30)
let marginY = CGFloat((tokenView.frame.size.height / 2) - (imageHeight / 2))
// ImageView to display token image
let tokenImage = UIImageView(image: UIImage(named: "coin"))
tokenImage.frame = CGRect(x:70, y:marginY, width:30, height:30)
tokenView.addSubview(tokenLabel)
tokenView.addSubview(tokenImage)
// Add custom view as a right bar button item
let barButtonItem = UIBarButtonItem(customView: tokenView)
self.navigationItem.rightBarButtonItem = barButtonItem

To display the current number of tokens you can just set the title. In your view controller:
navigationItem.title = '\(numberOfCoins) coins"
If you want to do anything more fancy you can set your own title view instead of the standard UILabel:
navigationItem.titleView = UIView(...) // your custom view
To show the coin in the top right corner you'd set the rightBarButtonItem:
navigationItem.rightBarButtonItem = UIBarButtonItem(
image: UIImage(named:"Coin"),
style: .Plain,
target:self,
action:"tappedCoinButton:")

Related

In iOS 14 how do you tap a button programmatically?

In iOS 14 I have configured a button to display a menu of questions. On its own the button is working perfectly. Here’s how it’s configured:
let button = UIButton()
button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
button.showsMenuAsPrimaryAction = true
button.menu = self.menu
I have it set on a text field’s rightView. This is how it looks when the menu is displayed.
When a menu item is tapped, that question appears in a label just above the text field. It works great. Except….
I also want the menu to appear when the user taps in the text field. I don’t want them to have to tap on the button specifically. In the old days of target-action this was easy: button.sendActions(for: .touchUpInside). I tried that and .menuActionTriggered and .primaryActionTriggered but none of those work. I think sendActions() is looking for the old selector based actions.
But there’s a new sendActions(for: UIControl.Event) method. Here’s what I’m trying in textFieldShouldBeginEditing():
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
let tempAction = UIAction { action in
print("sent action")
}
menuButton.sendAction(tempAction)
print("textField tapped")
return false
}
Sure enough, “sent action” and “textField tapped” appear in the console when the text field is tapped. But I have no idea what UIAction I could send that means “display your menu”. I wish there was this method: send(_: UIControl.Event).
Any ideas on how to get the button to display its menu when the text field is tapped?
ps. yes, textFieldShouldBeginEditing will need to know if a question has been selected and in that case will need to allow editing. That part is easy.
From what I read, you cannot programmatically trigger UIContextMenuInteraction as interactions seem to be handled internally by itself unlike send(_: UIControl.Event)
I see this mentioned on this SO post here
Also in the docs it seems that we don't have access to interaction management Apple needs to decide 3D touch is available or default back to long tap
From the docs
A context menu interaction object tracks Force Touch gestures on devices that support 3D Touch, and long-press gestures on devices that don't support it.
Workaround
I can propose the following workaround for your use case. My example was created using frame instead of auto layout as faster and easier to demo the concept this way, however you will need to make adjustments using autolayout
1. Create the UI elements
// I assume you have a question label, answer text field and drop down button
// Set up should be adjusted in case of autolayout
let questionLabel = UILabel(frame: CGRect(x: 10, y: 100, width: 250, height: 20))
let answerTextField = UITextField(frame: CGRect(x: 10, y: 130, width: 250, height: 50))
let dropDownButton = UIButton()
2. Regular setup of the label and the text view first
// Just regular set up
private func configureQuestionLabel()
{
questionLabel.textColor = .white
view.addSubview(questionLabel)
}
// Just regular set up
private func configureAnswerTextField()
{
let placeholderAttributes = [NSAttributedString.Key.foregroundColor :
UIColor.lightGray]
answerTextField.backgroundColor = .white
answerTextField.attributedPlaceholder = NSAttributedString(string: "Tap to select a question",
attributes: placeholderAttributes)
answerTextField.textColor = .black
view.addSubview(answerTextField)
}
3. Add the button to the text view
// Here we have something interesting
private func configureDropDownButton()
{
// Regular set up
dropDownButton.setImage(UIImage(systemName: "chevron.down"), for: .normal)
dropDownButton.showsMenuAsPrimaryAction = true
// 1. Create the menu options
// 2. When an option is selected update the question label
// 3. When an option is selected, update the button frame
// Update button width function explained later
dropDownButton.menu = UIMenu(title: "Select a question", children: [
UIAction(title: "Favorite movie") { [weak self] action in
self?.questionLabel.text = action.title
self?.answerTextField.placeholder = "Add your answer"
self?.answerTextField.text = ""
self?.updateButtonWidthIfRequired()
},
UIAction(title: "Car make") { [weak self] action in
self?.questionLabel.text = action.title
self?.answerTextField.placeholder = "Add your answer"
self?.answerTextField.text = ""
self?.updateButtonWidthIfRequired()
},
])
// I right align the content and set some insets to get some padding from
// the right of the text field
dropDownButton.contentHorizontalAlignment = .right
dropDownButton.contentEdgeInsets = UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: 0.0,
right: 20.0)
// The button initially will stretch across the whole text field hence we
// right aligned the content above
dropDownButton.frame = answerTextField.bounds
answerTextField.addSubview(dropDownButton)
}
// Update the button width if a menu option was selected or not
func updateButtonWidthIfRequired()
{
// Button takes the text field's width if the question isn't selected
if let questionText = questionLabel.text, questionText.isEmpty
{
dropDownButton.frame = answerTextField.bounds
return
}
// Reduce button width to right edge as a question was selected
dropDownButton.frame = CGRect(x: answerTextField.frame.width - 50.0,
y: answerTextField.bounds.origin.y,
width: 50,
height: answerTextField.frame.height)
}
4. End Result
Start with a similar view to yours
Then I tap in the middle of the text field
It displays the menu as intended
After selecting an option, the question shows in the label and the placeholder updates
Now I can start typing my answer using the text field and the button is only active on the right side since it was resized
And the button is still active
Final thoughts
Could be better to put this into a UIView / UITextField subclass
This was an example using frames with random values, adjustments need to made for autolayout
Edits from OP (too long for a comment):
I had tried setting contentEdgeInsets so the button was way over to the left but it covered up the placeholder text.
Your idea of simply adding the button as a subview of the text field was the key, but...
After selecting a question and resizing the button, if the text field was the first responder, tapping the button had no effect. The button was in the view hierarchy but the text field got the tap.
So, when a question is selected, I remove the button from its superview (the textfield) and add it to the textField's rightView. Then it would accept a tap even if the textField was the first responder.
And, as you suspected, I had to pin the button to the textField with constraints.

How to add a UIView into navigationItem.backBarButtonItem

I am trying to achieve this
this is my code
let contactName=UILabel()
contactName.text=chat?.Name
contactName.font=UIFont(name: "System", size: 17)
contactName.sizeToFit()
let contactImg=UIImageView()
contactImg.image=UIImage(named: (chat?.Image)!)
contactImg.frame.size.width=20
contactImg.frame.size.height=20
contactImg.layer.cornerRadius=contactImg.frame.height/2
let backButtonView=UIView()
backButtonView.addSubview(contactImg)
backButtonView.addSubview(contactName)
backButtonView.frame.size.width=20+contactName.frame.width
backButtonView.frame.size.height=max(contactImg.frame.height,contactName.frame.height)
navigationItem.backBarButtonItem=UIBarButtonItem(customView: backButtonView)
but when I run app it shows the default backbarbutton
what am I doing wrong here please guide.
let profileImage = UIImage(named: "profile_image")
self.navigationController?.navigationBar.backIndicatorImage = profileImage
self.navigationController?.navigationBar.backIndicatorTransitionMaskImage = profileImage
self.navigationController?.navigationBar.backItem?.title = "Jhon"
Aditya is right, but here's a little more explanation. From the Apple docs:
When configuring your bar button item, do not assign a custom view to it; the navigation item ignores custom views in the back bar button anyway.
https://developer.apple.com/documentation/uikit/uinavigationitem/1624958-backbarbuttonitem
Also, the UIBarButtonItem is "...specialized for placement on a toolbar or tab bar. You typically use Interface Builder to create and configure bar button items."
https://developer.apple.com/documentation/uikit/uibarbuttonitem

Why do my objects get placed and sized so perfectly when I add them to a UIKitView?

So I have the following UIKit elements I am adding to my UIViewController to control the simulation that appears. I expected to have to write a lot of placement code but instead everything appears perfectly no-mater what device... my question is why?
let menuButton = UIButton()
let statusLabel = UILabel()
let segmentedLabel = UISegmentedControl(items: ["None", "Glow", "Cloud"])
func initializeUI() {
//The menu button that opens up the options for the simulations
menuButton.layer.borderColor = UIColor.lightGray.cgColor
menuButton.layer.borderWidth = 1
menuButton.layer.cornerRadius = 5
menuButton.layer.backgroundColor = UIColor.darkGray.cgColor
menuButton.showsTouchWhenHighlighted = true
menuButton.imageView?.contentMode = UIViewContentMode.scaleAspectFit
menuButton.setImage(UIImage(named: "hamburger.png"), for: UIControlState.normal)
menuButton.addTarget(self, action: #selector(menuPress), for: UIControlEvents.touchDown)
view.addSubview(menuButton)
//Will display the status of the simulation
statusLabel.text = "Particle Simulation"
statusLabel.textColor = UIColor.darkGray
view.addSubview(statusLabel)
//Will display visual options
view.addSubview(segmentedLabel)
}
The size of each of the elements is perfect and they dont overlap. The label is in the bottom left corner the button is in the bottom right and the segmented view is in the top left corner (of my landscape app).
An additional question I have is if I wanted to start placing these objects programmatically how would I do so? The elements don't have an attribute position that I can work with and if I do something like menuButton.frame.size.height *= 20 that does not make the menu button super tall.
The reason this works is that all of your UI items in your example have an “intrinsic” content size so they are able to create their own frame. Add a standard UIView the same way and you will be out of luck. Also the views are creating their own constraints based on initial frames using Auto Resizing Masks(Also known as Springs and Struts).
Finally to place items you would set the frame directly. Just math. Good luck.
Super important to understand intrinsic content size. Some frame changes might require you to turn off automatic constraints but this is usually not the case but could be the problem with your button
UIView.translatesAutoresizingMaskIntoConstraints = false

Swift - Custom SearchController and SearchBar

I am trying to recreate the search field as seen in the Yahoo Finance app. I followed an online tutorial for customizing the UISearchBar and UISearchController, however I still have some problems. If any of you could open up my project and see where Im going wrong / where I need to add these lines with even one of these that would be really great.
My attempted solution project can be found here: https://github.com/jordanw421/yahoofinance
1) How to get the search bar active (with text to the left, and typing indicator blinking) as soon as view presents itself? In the link below, you can see what I am talking about, when the button is pressed the search view is loaded and the search bar is instantly active.
https://youtu.be/tRtXm-m1hX0
I tried using:
customSearchController.definesPresentationContext = true
customSearchController.isActive = true
customSearchController.searchBar.becomeFirstResponder()
but that didn't work. Should I be setting these in the initial view controller with prepareForSegue?
2) How to set the keyboard appearance (to dark), and to add a keyboard toolbar with a button?
I was able to get this working for a non-custom search bar, but for some reason these don't work now:
customSearchController.searchBar.keyboardAppearance = .dark
and
func addKeyboardButton() {
let keyboardToolbar = UIToolbar()
keyboardToolbar.sizeToFit()
keyboardToolbar.isTranslucent = false
keyboardToolbar.barTintColor = UIColor.blue
let addButton = UIButton(type: .custom)
addButton.frame = CGRect(x: keyboardToolbar.frame.size.width / 2, y: keyboardToolbar.frame.size.height / 2, width: 50, height: 30)
addButton.addTarget(self, action: #selector(clickMe), for: .touchUpInside)
let item = UIBarButtonItem(customView: addButton)
keyboardToolbar.items = [item]
customSearchController.searchBar.inputAccessoryView = keyboardToolbar
}
and calling,
addKeyboardButton()
in the search bar configure function.
3) How to prevent the search bar / table view header from scrolling, but still allow the tableView to scroll?
If you look at my attempted solution you can see that for some reason the tableview header scrolls with the table view. When I use a non-custom search bar the header remains static.
I know there are a lot of questions here, but I've been stuck on this for awhile and could really use some help. Thank you.

Issue with section header and table view cell in UITableViewController

I've done an experiment of creating a screen with the use of UITableviewController instead of using scroll view reason is the fields in screen may get dramatically change that's why I do that
Screen :
All of those cells are static cells.
section header code
let backgroundImage = UIImage(named: "main_bg")
let imageView = UIImageView(image: backgroundImage)
self.tableView.backgroundView = imageView
header.backgroundColor = UIColor.clear
let lblheader : appLabel = appLabel(frame: CGRect(x: 0, y: 20, width: UIScreen.main.bounds.width, height: header.frame.height - 20))
lblheader.text = "SIGN UP"
lblheader.textFontsize = 14
lblheader.textFontType = 2
lblheader.textFontColor = 1
lblheader.textAlignment = .center
header.addSubview(lblheader)
let btnback : appButton = appButton(frame: CGRect(x: 0, y: lblheader.frame.origin.y, width: 50, height: lblheader.frame.height))
btnback.backgroundColor = UIColor.clear
btnback.setImage(UIImage(named: "back_arrow"), for: .normal)
btnback.addTarget(self, action: #selector(self.btnBackTapped), for: .touchUpInside)
header.addSubview(btnback)
Now when I scroll the table It looks as below:
I want to scroll static cell of the table under header section.
I know I can also use combination of UIViewController which contains Container and represents the Table view controller (Ref : See this i used it already)
If any solutions other than that.
Please guide me!
Thank you
alternatively you can use UITableView's tableHeaderView instead of using UITableView's viewForHeaderInSection because Section Headers WILL always stick to the top of the tableview unless otherwise if you do some simple hacks
If you make your SectionHeader opaque and not transparent/translucent you'll understand. Try making a Custom Project (test project) where you create a TableView with a lot of Sections With Different Headers
Perfect example without creating the Sample Project would be the Phone Application (the green one) in your iPhone where the Letters are Section Headers and they stick at the top of your screen when scrolling
This is an example in Swift 2.3
let someHeader = NSBundle.mainBundle().loadNibNamed("", owner:self, options nil!)!.first as! SomeView
//this is not a real function just what you wanna do stuff
someHeader.customizeIfNeeded ... yadda yadda set frame assign values add targets etc
tableView.tableHeaderView = someHeader
tableView.reloadData()
Use UIViewController instead of UITableViewControllerand add a UIView as header in top of tableView
Refer this maintain the header of a tableView fixed
Just an easy approach, hope this helps
If you have to use 'UITableViewController' look at below solution also
UITableViewController (static cells/keyboard handling) and have a fixed header

Resources