I'm new to Swift.
I would like to know if possible add a "favorite" Button programmatically inside a view controller that inherits from UITableView?
I need to say too that I wanna use this button to favorite a result from google API.
This is possible once the result comes from a search's result and not from a specific list?
PS: I'm doing my project in UIKIT.
Thank you.
Of course your view controller will then inherit from UITableViewController, not UITableView.
Here are the basics:
var favoriteButtonItem: UIBarButtonItem?
var isFavorite: Bool = <...initial value...>
var favoriteImage
{
return UIImage(systemName: "heart" + (isFavorite ? ".fill" : ""))
}
favoriteButtonItem = UIBarButtonItem(image: favoriteImage,
style: .plain,
target: self,
action: #selector(toggleFavorite))
navigationItem.rightBarButtonItem = favoriteButtonItem
#objc private func toggleFavorite()
{
isFavorite = !isFavorite
favoriteButtonItem.image = favoriteImage
<...logic to process change...>
}
You'll need to add logic to disable or not show yet favoriteButtonItem until the backend has returned the current value.
Related
iOS 14 adds the ability to display menus upon tapping or long pressing a UIBarButtonItem or UIButton, like so:
let menu = UIMenu(children: [UIAction(title: "Action", image: nil) { action in
//do something
}])
button.menu = menu
barButtonItem = UIBarButtonItem(title: "Show Menu", image: nil, primaryAction: nil, menu: menu)
This most often replaces action sheets (UIAlertController with actionSheet style). It's really common to have a dynamic action sheet where actions are only included or may be disabled based on some state at the time the user taps the button. But with this API, the menu is created at the time the button is created. How can you modify the menu prior to it being presented or otherwise make it dynamic to ensure the appropriate actions are available and in the proper state when it will appear?
You can store a reference to your bar button item or button and recreate the menu each time any state changes that affects the available actions in the menu. menu is a settable property so it can be changed any time after the button is created. You can also get the current menu and replace its children like so: button.menu = button.menu?.replacingChildren([])
For scenarios where you are not informed when the state changes for example, you really need to be able to update the menu right before it appears. There is a UIDeferredMenuElement API which allows the action(s) to be generated dynamically. It's a block where you call a completion handler providing an array of UIMenuElement. A placeholder with loading UI is added by the system and is replaced once you call the completion handler, so it supports asynchronous determination of menu items. However, this block is only called once and then it is cached and reused so this doesn't do what we need for this scenario. iOS 15 added a new uncached provider API which behaves the same way except the block is invoked every time the element is displayed, which is exactly what we need for this scenario.
barButtonItem.menu = UIMenu(children: [
UIDeferredMenuElement.uncached { [weak self] completion in
var actions = [UIMenuElement]()
if self?.includeTestAction == true {
actions.append(UIAction(title: "Test Action") { [weak self] action in
self?.performTestAction()
})
}
completion(actions)
}
])
Before this API existed, I did find for UIButton you can change the menu when the user touches down via target/action like so: button.addTarget(self, action: #selector(buttonTouchedDown(_:)), for: .touchDown). This worked only if showsMenuAsPrimaryAction was false so they had to long press to open the menu. I didn't find a solution for UIBarButtonItem, but you could use a UIButton as a custom view.
After some trial, I've found out that you can modify the UIButton 's .menu by setting the menu property to null first then set the new UIIMenu
here is the sample code that I made
#IBOutlet weak var button: UIButton!
func generateMenu(max: Int, isRandom: Bool = false) -> UIMenu {
let n = isRandom ? Int.random(in: 1...max) : max
print("GENERATED MENU: \(n)")
let items = (0..<n).compactMap { i -> UIAction in
UIAction(
title: "Menu \(i)",
image: nil
) {[weak self] _ in
guard let self = self else { return }
self.button.menu = nil // HERE
self.button.menu = self.generateMenu(max: 10, isRandom: true)
print("Tap")
}
}
let m = UIMenu(
title: "Test", image: nil,
identifier: UIMenu.Identifier(rawValue: "Hello.menu"),
options: .displayInline, children: items)
return m
}
override func viewDidLoad() {
super.viewDidLoad()
button.menu = generateMenu(max: 10)
button.showsMenuAsPrimaryAction = true
}
Found a solution for the case with UIBarButtonItem. My solution is based on Jordan H solution, but I am facing a bug - my menu update method regenerateContextMenu() was not called every time on menu appears, and I was getting irrelevant data in the menu. So I changed the code a bit:
private lazy var threePointBttn: UIButton = {
$0.setImage(UIImage(systemName: "ellipsis"), for: .normal)
// pay attention on UIControl.Event in next line
$0.addTarget(self, action: #selector(regenerateContextMenu), for: .menuActionTriggered)
$0.showsMenuAsPrimaryAction = true
return $0
}(UIButton(type: .system))
override func viewDidLoad() {
super.viewDidLoad()
threePointBttn.menu = createContextMenu()
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: threePointBttn)
}
private func createContextMenu() -> UIMenu {
let action1 = UIAction(title:...
// ...
return UIMenu(title: "Some title", children: [action1, action2...])
}
#objc private func regenerateContextMenu() {
threePointBttn.menu = createContextMenu()
}
tested on iOS 14.7.1
Modified Jordan H's version to separate the assignment and build action
This will build the menu on the fly every time the button is tapped
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem?.menu = UIMenu(children: [
// build menu every time the button is tapped
UIDeferredMenuElement.uncached { [weak self] completion in
if let menu = self?.buildMenu() as? UIMenu {
completion([menu])
}
}
])
}
func buildMenu() -> UIMenu {
var actions: [UIMenuElement] = []
// build actions
UIAction(title: "Filter", image: UIImage(systemName: "line.3.horizontal.decrease.circle")) { _ in
self.filterTapped()
}
actions.append(filterAction)
return UIMenu(options: .displayInline, children: actions)
}
I want to hide/show a UIBarButtonItem when a segmentedControl changes, this is my code:
#objc fileprivate func handleSegmentedChange() {
switch segmentedControl.selectedSegmentIndex {
case index0:
// Set the proper rightBarButtonItems, in the first load this bar button items will be nil, this is why we have to check first
self.navigationItem.rightBarButtonItems?.append(UIBarButtonItem(image: #imageLiteral(resourceName: "Filter2"), style: .plain, target: self, action: #selector(openBottomSheet)))
default:
self.navigationItem.rightBarButtonItems?.remove(at: 0)
}
}
However is not updating the views (hiding or showing anything).
Note I've also tried setting the rightBarButtonItems to nil before adding or removing items, however is not working.
How can I accomplish the desired effect?
If rightBarButtonItems is nil before you try to append or remove items to/from it, then nothing will happen as you cannot append or remove items to/from a non-existent array.
Instead of appending/removing to/from rightBarButtonItems, try just setting it directly to the items you want it to be, like this:
#objc fileprivate func handleSegmentedChange() {
switch segmentedControl.selectedSegmentIndex {
case 0:
let barButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "Filter2"),
style: .plain,
target: self,
action: #selector(openBottomSheet))
navigationItem.rightBarButtonItems = [barButtonItem]
// Note: If you're just dealing with one bar button item,
// you could also just use `navigationItem.rightBarButtonItem` like:
// navigationItem.rightBarButtonItem = barButtonItem
default:
navigationItem.rightBarButtonItems = nil // or `= []`
// Note: If you're just dealing with one bar button item,
// you could also just use `navigationItem.rightBarButtonItem` like:
// navigationItem.rightBarButtonItem = nil
}
}
I'm developing my first iOS app in Swift 2.2 and I have the following problem.
In an utility class, I have the following static method, called by some different UIViewController.
static func setNavigationControllerStatusBar(myView: UIViewController, title: String, color: CIColor, style: UIBarStyle) {
let navigation = myView.navigationController!
navigation.navigationBar.barStyle = style
navigation.navigationBar.barTintColor = UIColor(CIColor: color)
navigation.navigationBar.translucent = false
navigation.navigationBar.tintColor = UIColor.whiteColor()
myView.navigationItem.title = title
let menuButton = UIBarButtonItem(image: UIImage(named: "menu"),
style: UIBarButtonItemStyle.Plain ,
target: self, action: #selector("Utils.menuClicked(_:)"))
myView.navigationItem.leftBarButtonItem = menuButton
}
func menuClicked(sender: UIButton!) {
// do stuff
}
I'm trying in some different ways to associate a #selector for this button, however I always have the following error.
No quotes.
#selector(Utils.menuClicked(_:))
func menuClicked should be in your view controller class. But if for some reason it isn't, you can do
class Utils {
static let instance = Utils()
let menuButton = UIBarButtonItem(image: UIImage(named: "menu"),
style: UIBarButtonItemStyle.Plain ,
target: Utils.instance, action: #selector(Utils.menuClicked(_:)))
#objc func menuClicked(sender: UIBarButtonItem) {
// do stuff
}
}
Swift 2.2 deprecates using strings for selectors and instead introduces new syntax: #selector.
Using #selector will check your code at compile time to make sure the method you want to call actually exists. Even better, if the method doesn’t exist, you’ll get a compile error: Xcode will refuse to build your app, thus banishing to oblivion another possible source of bugs.
So remove the double quote for your method in #selector. It should work!
var toolBar: UIToolbar!
let nextBarButton = UIBarButtonItem(title: "Next", style: .Plain, target: self, action: "nextButtonPressed")
self.toolBar.setItems([nextBarButton], animated: true)
How to hide nextButton in ToolBar?
I used the following code and it did not work.
self.toolbar.items.indexOf(1).hidden = true
This answer is inspired by this answer.
I will improve on it and make all the work programmatically. No need to update/set the class of the UIBarButtonItem instance to use the new subclass anymore.
We could just add isHidden attribute to the UIBarButtonItem. Then just use it as you want.
extension UIBarButtonItem {
var isHidden: Bool = false {
didSet {
isEnabled = !isHidden
tintColor = isHidden ? UIColor.clear : UIColor.black
}
}
}
In your case, after you add the extension (outside any class). You could use it as:
self.toolbar.items.indexOf(1).isHidden = true
Looks like it's a 'next' button, the standard way to handle when there is nothing to move to next is to simply disable it like this:
nextBarButton.enabled = NO;
The icon is greyed out automatically too.
UIBarButton does not have a "hidden" property so you cannot hide. You can remove it. Since you only have one button on the toolbar, you can clear all the items on the toolbar.
toolBar.setItems([], animated: true)
If you wanted to remove a specific item, you can use the index.
var items = toolBar.items
items?.removeAtIndex(0)
toolBar.setItems(items, animated: true)
You can add simple function, for hiding state
extension UIBarButtonItem {
func set(hide: Bool) {
isEnabled = !hide
tintColor = hide ? .clear : .white
}
}
I have a text field in my view controller and I want to display a custom title for the back button. This is to represent changes made in the text field.
override func viewDidLoad() {
super.viewDidLoad()
// Update back button in the nav bar
updateBackButton()
// Text field delegation
nameTextField.delegate = self
nameTextField.addTarget(self, action: "updateBackButton", forControlEvents: .EditingChanged)
}
func updateBackButton() {
let backButton = UIBarButtonItem(
title: formHasChanged ? "Cancel" : "Back",
style: .Done,
target: nil,
action: nil
)
print(backButton.title)
navigationController?.navigationBar.topItem?.backBarButtonItem = backButton
}
This does effect the back button only once, in the viewDidLoad method. On subsequent calls to updateBackButton() there's no visible change, even though print(backButton.title) does print the appropriate output.
What's missing from my approach in order to have a dynamically updated back button title?
Output from the updateBackButton() method's print statement.
Optional("Back")
Optional("Cancel")
Optional("Back")
Optional("Cancel")
Optional("Back")
If you want to call a function as an action from any control's target then you'll have to define it as #IBAction and use : while calling it. Just like
override func viewDidLoad() {
super.viewDidLoad()
// Update back button in the nav bar
// updateBackButton() // Now you don't need to call it here, I guess..
// Text field delegation
nameTextField.delegate = self
nameTextField.addTarget(self, action: "updateBackButton:",forControlEvents: .EditingChanged)
}
#IBAction func updateBackButton(sender: AnyObject!) {
let backButton = UIBarButtonItem(
title: formHasChanged ? "Cancel" : "Back",
style: .Done,
target: nil,
action: nil
)
print(backButton.title!)
navigationController?.navigationBar.topItem?.backBarButtonItem = backButton
}
Update :
Try changing this line
navigationController?.navigationBar.topItem?.backBarButtonItem = backButton
with
navigationController?.navigationBar.topItem?.leftBarButtonItem = backButton or simply navigationItem.leftBarButtonItem = backButton
PS : It will not show you < symbol but I think you dont need it as you are dealing with cancel and done.
Its working now. Hope this will work for you too !