How do I change/modify the displayed title of an NSPopUpButton - swift

I would like an NSPopUpButton to display a different title than the title of the menu item that is selected.
Suppose I have an NSPopUpButton that lets the user pick a list of currencies, how can I have the collapsed/closed button show only the currencies abbreviation instead of the menu title of the selected currency (which is the full name of the currency)?
I imagine I can override draw in a subclass (of NSPopUpButtonCell) and draw the entire button myself, but I would prefer a more lightweight approach for now that reuses the system's appearance.
The menu items have the necessary information about the abbreviations, so that's not part of the question.

Subclass NSPopUpButtonCell, override drawTitle(_:withFrame:in:) and call super with the title you want.
override func drawTitle(_ title: NSAttributedString, withFrame frame: NSRect, in controlView: NSView) -> NSRect {
var attributedTitle = title
if let popUpButton = self.controlView as? NSPopUpButton {
if let object = popUpButton.selectedItem?.representedObject as? Dictionary<String, String> {
if let shortTitle = object["shortTitle"] {
attributedTitle = NSAttributedString(string:shortTitle, attributes:title.attributes(at:0, effectiveRange:nil))
}
}
}
return super.drawTitle(attributedTitle, withFrame:frame, in:controlView)
}
In the same way you can override intrinsicContentSize in a subclass of NSPopUpButton. Replace the menu, call super and put the original menu back.
override var intrinsicContentSize: NSSize {
if let popUpButtonCell = self.cell {
if let orgMenu = popUpButtonCell.menu {
let menu = NSMenu(title: "")
for item in orgMenu.items {
if let object = item.representedObject as? Dictionary<String, String> {
if let shortTitle = object["shortTitle"] {
menu.addItem(withTitle: shortTitle, action: nil, keyEquivalent: "")
}
}
}
popUpButtonCell.menu = menu
let size = super.intrinsicContentSize
popUpButtonCell.menu = orgMenu
return size
}
}
return super.intrinsicContentSize
}

Ok, so I found out how I can modify the title. I create a cell subclass where I override setting the title based on the selected item. In my case I check the represented object as discussed in the question.
final class MyPopUpButtonCell: NSPopUpButtonCell {
override var title: String! {
get {
guard let selectedCurrency = selectedItem?.representedObject as? ISO4217.Currency else {
return selectedItem?.title ?? ""
}
return selectedCurrency.rawValue
}
set {}
}
}
Then in my button subclass I set the cell (I use xibs)
override func awakeFromNib() {
guard let oldCell = cell as? NSPopUpButtonCell else { return }
let newCell = MyPopUpButtonCell()
newCell.menu = oldCell.menu
newCell.bezelStyle = oldCell.bezelStyle
newCell.controlSize = oldCell.controlSize
newCell.autoenablesItems = oldCell.autoenablesItems
newCell.font = oldCell.font
cell = newCell
}
A downside though is that I have to copy over all attributes of the cell I configured in Interface Builder. I can of course just set the cell class in Interface Builder, which makes this superfluous.
One thing I haven't figured out yet is how I can have the button have the correct intrinsic content size now. It still tries to be as wide as the longest regular title.
And the second thing I haven't figured out is how to make this work with bindings. If the buttons content is provided via Cocoa Bindings then I can only bind the contentValues, and the cell's title property is never called.

Related

Unable to pass on or retrieve data to another class

Trying to create a custom drop-down and facing issues in passing on data and retrieving the user selection from the custom drop-down. The solution however works when I use static var.
In the below code, I have a viewController class from where I am programmatically calling a popOver.
let countryDropDown = customDropdown()
#IBAction func btMultiCountry(_ sender: Any) {
ViewController.countryDropDown.removeAll() //fuction to clear the array before load
for c in g.country{ //Array from which all items are loaded on the custom dropdown
countryDropDown.addItems(labelText: c.countryName!, toggleState: 1) //passing the values in the add function to update the Array
}
countryDropDown.showDropdown(btMultiCountry) //programatically calling the function to display popover that has the collection view with the countries as items.
}
class customDropdown: NSViewController {
#IBOutlet weak var cvDropDown: NSCollectionView!
var pop = NSPopover() // if I put static var, the popover closes with the OK button as required, however, without static the OK button doest do anything
var dropDownLabelText = [String]() //if I put static var, the values are shown on the collection view without any issue. Without static the array is blank that was load from additems function
var dropDownToggle = [Int]() // same as above
var selectedItemsIndex: [Int] {
var a = [Int]()
for (i,t) in dropDownToggle.enumerated() {
if t == 1 {
a.append(i)
print(a)
}
}
return a
}
func showDropdown(_ sender: NSButton){
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
let VC = storyboard.instantiateController(withIdentifier: "dropDownForm") as? customDropdown
pop.contentViewController = VC
pop.behavior = NSPopover.Behavior.transient
pop.show(relativeTo: sender.bounds, of: sender, preferredEdge: NSRectEdge.minY)
print(pop)
}
#IBAction func btOK(_ sender: Any) { //this function does not close the popover if there is not static mention above
pop.close()
}
I tried putting static when declaring the variables and it worked. I do not want static and want to create different instances of the class that gives me selectedItemsIndex
For eg:
let countryDropDown = customDropdown()
let personDropDown = customDropdown()
I should be able to get a different values for countryDropDown.selectedItemsIndex and personDropDown.selectedItemsIndex
Any help is appreciated.

How do I get UIConfigurationState without a reference to the cell?

Normally, when updating a cell's contentConfiguration for a particular cell's state you ask the cell for its contentConfiguration, then update it using updated(for:).
let content = cell.defaultContentConfiguration().updated(for: cell.configurationState)
However, in order to get this state you first need to have a reference to the cell. UIConfigurationState doesn't have an initializer. How can get the updated styling for a state without a reference to the cell?
For example, here I am trying to create a reusable configuration that adjusts itself for particular state
class Person {
let name: String
}
extension Person {
func listContentConfig(state: UICellConfigurationState) -> UIListContentConfiguration {
var content = UIListContentConfiguration.cell().updated(for: state)
content.text = self.name
return content
}
}
Then, during cell registration I can configure it with my reusable config.
extension UICollectionViewController {
func personCellRegistration(person: Person) -> UICollectionView.CellRegistration<UICollectionViewListCell, Person> {
return .init { cell, indexPath, person in
cell.contentConfiguration = person.listContentConfig(state: cell.configurationState)
}
}
}
That works fine, but what if I want to mix and match different properties for difference states? In order to actually get this state I need to first get the cell, update the state, then set it back. This is quite a few steps.
extension UICollectionViewController {
func personCellRegistration(person: Person) -> UICollectionView.CellRegistration<UICollectionViewListCell, Person> {
return .init { cell, indexPath, person in
// 1. Change the cell's state
cell.isUserInteractionEnabled = false
// 2. Grab my content config for the new state
let disabledConfig = person.listContentConfig(state: cell.configurationState)
// 3. Change the cell's state back
cell.isUserInteractionEnabled = true
// 4. Get the cell's default config
var defaultConfig = cell.defaultContentConfiguration()
// 5. Copy the pieces I want
defaultConfig.textProperties.color = disabledConfig.textProperties.color
}
}
}
What I'd like is to be able to do something like this:
extension Person {
func listContentConfig(state: UICellConfigurationState) -> UIListContentConfiguration {
let disabledState = UICellConfigurationState.disabled // no such property exists.
var content = UIListContentConfiguration.cell().updated(for: disabledState)
// customize...
}
}
I realize that I could pass in the cell itself to my reusable config, but this a) breaks encapsulation, b) defeats the purpose of configurations to be view agnostic, c) requires the same number of steps.
(FYI: The reason I am doing this is to allow the user to delete a cell that represents 'missing data'. The cell's style should appear disabled, but when setting isUserInteractionEnabled = false the delete accessory becomes unresponsive.)
Am I missing something?

How to add `toggleSidebar` NSToolbarItem in Catalyst?

In my app, I added a toggleSidebar item to the NSToolbar.
#if targetEnvironment(macCatalyst)
extension SceneDelegate: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.toggleSidebar, NSToolbarItem.Identifier.flexibleSpace, AddRestaurantButtonToolbarIdentifier]
}
}
#endif
However, when I compile my app to Catalyst, the button is disabled. Does anybody know what else I need to do to hook it up?
If you look at the documentation for .toggleSidebar/NSToolbarToggleSidebarItemIdentifier you will see:
The standard toolbar item identifier for a sidebar. It sends toggleSidebar: to firstResponder.
Adding that method to your view controller will enable the button in the toolbar:
Swift:
#objc func toggleSidebar(_ sender: Any) {
}
Objective-C:
- (void)toggleSidebar:(id)sender {
}
Your implementation will need to do whatever you want to do when the user taps the button in the toolbar.
Normally, under a real macOS app using an NSSplitViewController, this method is handled automatically by the split view controller and you don't need to add your own implementation of toggleSidebar:.
The target needs changed to self, this is shown in this Apple sample where it is done for the print item but can easily be changed to the toggle split item as I did after the comment.
/** This is an optional delegate function, called when a new item is about to be added to the toolbar.
This is a good spot to set up initial state information for toolbar items, particularly items
that you don't directly control yourself (like with NSToolbarPrintItemIdentifier).
The notification's object is the toolbar, and the "item" key in the userInfo is the toolbar item
being added.
*/
func toolbarWillAddItem(_ notification: Notification) {
let userInfo = notification.userInfo!
if let addedItem = userInfo["item"] as? NSToolbarItem {
let itemIdentifier = addedItem.itemIdentifier
if itemIdentifier == .print {
addedItem.toolTip = NSLocalizedString("print string", comment: "")
addedItem.target = self
}
// added code
else if itemIdentifier == .toggleSidebar {
addedItem.target = self
}
}
}
And then add the action to the scene delegate by adding the Swift equivalent of this:
- (IBAction)toggleSidebar:(id)sender{
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
[UIView animateWithDuration:0.2 animations:^{
splitViewController.preferredDisplayMode = (splitViewController.preferredDisplayMode != UISplitViewControllerDisplayModePrimaryHidden ? UISplitViewControllerDisplayModePrimaryHidden : UISplitViewControllerDisplayModeAllVisible);
}];
}
When configuring your UISplitViewController, set the primaryBackgroundStyle to .sidebar
let splitVC: UISplitViewController = //your application's split view controller
splitVC.primaryBackgroundStyle = .sidebar
This will enable your NSToolbarItem with the system identifier .toggleSidebar and it will work automatically with the UISplitViewController in Mac Catalyst without setting any target / action code.
This answer is mainly converting #malhal's answer to the latest Swift version
You will need to return [.toggleSidebar] in toolbarDefaultItemIdentifiers.
In toolbarWillAddItem you will write the following (just like the previous answer suggested):
func toolbarWillAddItem(_ notification: Notification) {
let userInfo = notification.userInfo!
if let addedItem = userInfo["item"] as? NSToolbarItem {
let itemIdentifier = addedItem.itemIdentifier
if itemIdentifier == .toggleSidebar {
addedItem.target = self
addedItem.action = #selector(toggleSidebar)
}
}
}
Finally, you will add your toggleSidebar method.
#objc func toggleSidebar() {
let splitController = self.window?.rootViewController as? MainSplitController
UIView.animate(withDuration: 0.2) {
splitController?.preferredDisplayMode = (splitController?.preferredDisplayMode != .primaryHidden ? .primaryHidden : .allVisible)
}
}
A few resources that might help:
Integrating a Toolbar and Touch Bar into Your App
Mac Catalyst: Adding a Toolbar
The easiest way to use the toggleSidebar toolbar item is to set primaryBackgroundStyle to .sidebar, as answered by #Craig Scrogie.
That has the side effect of enabling the toolbar item and hiding/showing the sidebar.
If you don't want to use the .sidebar background style, you have to implement toggling the sidebar and validating the toolbar item in methods on a class in your responder chain. I put these in a subclass of UISplitViewController.
#objc func toggleSidebar(_ sender: Any?) {
UIView.animate(withDuration: 0.2, animations: {
self.preferredDisplayMode =
(self.displayMode == .secondaryOnly) ?
.oneBesideSecondary : .secondaryOnly
})
}
#objc func validateToolbarItem(_ item: NSToolbarItem)
-> Bool {
if item.action == #selector(toggleSidebar) {
return true
}
return false
}

Storing Button Pressed In Swift

I have collection view where you can select 4 buttons, it is like a quiz with A, B, C, D. I need to store which one they clicked before they go to the next question (They will swipe to go to the next question since it is a collection view) The controller looks like this:
First: Essentially the code used to display the image above, I have created a database which is parsed using this:
struct Question {
let fact: String
let question: String
let answers: [String: String]
let correctAnswer: String
let revenue: String
init?(with dictionary: [String: Any]) {
guard let fact = dictionary["fact"] as? String,
let question = dictionary["question"] as? String,
let answerA = dictionary["answer_a"] as? String,
let answerB = dictionary["answer_b"] as? String,
let answerC = dictionary["answer_c"] as? String,
let answerD = dictionary["answer_d"] as? String,
let revenue = dictionary["revenue"] as? String,
let correctAnswer = dictionary["correctAnswer"] as? String else { return nil }
self.fact = fact
self.question = question
self.revenue = revenue
var answersDict = [String: String]()
answersDict["answer_a"] = answerA
answersDict["answer_b"] = answerB
answersDict["answer_c"] = answerC
answersDict["answer_d"] = answerD
self.answers = answersDict
self.correctAnswer = correctAnswer
}
Second: Then I display using this code:
extension QuestionCell {
func configure(with model: Question) {
factLabel.text = model.fact
questionLabel.text = model.question
revenueLabel.text = model.revenue
let views = answersStack.arrangedSubviews
for view in views {
view.removeFromSuperview()
}
for (id, answer) in model.answers {
print(index)
print(id)
let answerLabel = UILabel()
answerLabel.text = answer
answersStack.addArrangedSubview(answerLabel)
let answerButton = UIButton()
let imageNormal = UIImage(named: "circle_empty")
answerButton.setImage(imageNormal, for: .normal)
let imageSelected = UIImage(named: "circle_filled")
answerButton.setImage(imageSelected, for: .selected)
answerButton.setTitleColor(.black, for: .normal)
answerButton.addTarget(self, action: #selector(answerPressed(_:)), for: .touchUpInside)
answersStack.addArrangedSubview(answerButton)
}
}
}
Is there a way to store the button I clicked? Thanks
Well there is one sure shot way out of this situation.
Make a custom cell for the collectionView.
Add button outlets and action the the customCell's class
Create and make use of delegate methods in customCell's class and when implementing the customCell in your ViewController set the delegate to
self.
Trigger the delegate methods when button actions are done (inside your custom cell).
Provide your customCell the current indexpath when using it in cellForItemAt method.
Make use of that indexPath to decide which button was triggered.
You should be thinking something close to this approach for a robust solution.
The way I've handled this in the past is to use the tag on a UIButton and just keep track of which tag is currently selected. With this approach I can use the same IBAction for every button and all I need to do is pull the tag from the sender in the function body. While maybe not as flexible and robust is an approach using subclassing, it's a bit quicker to implement.
First set your tags when you create your buttons (I use 100-104 to avoid conflicts with other buttons). Since you're creating your buttons in a CollectionView, you'll need to set the tag in your configure() function:
func configure(with model: Question) {
...
for (id, answer) in model.answers {
...
answerButton.setImage(imageSelected, for: .selected)
answerButton.tag = index
answerButton.setTitleColor(.black, for: .normal)
answerButton.addTarget(self, action: #selector(answerPressed(_:)), for: .touchUpInside)
}
}
Create an instance variable:
var selectedAnswerIndex = -1
Then assign this IBAction to each of you buttons:
func answerPressed(_ sender: UIButton){
selectedAnswerIndex = sender.tag
hilightNewOne(sender: sender)
}

Remove/Hide UITabBarItem in Swift

I have looked really hard for this solution in Swift but am not coming up with one that works for me. I am trying to hide my "Admin" TabBarItem based on the permissions of the person that logs in to the app. I can disable it but it still shows up on the bar. I want to be able to show it for certain people and hide it for others. Also, when I print self.tabBarController?.viewControllers I get nil.
class TabBarMenuController: UITabBarController {
let ref = Firebase(url: "")
var position = ""
func getPosition() {
let userRef = ref.childByAppendingPath("users/\(ref.authData.uid)")
userRef.observeSingleEventOfType(.Value, withBlock: {snapshot in
if snapshot.value["position"] as! String != "Staff" {
self.position = snapshot.value["position"] as! String
}
})
}
override func viewWillAppear(animated: Bool) {
getPosition()
print(self.tabBarController?.viewControllers)
if position != "Staff" {
if let tabBarController = self.tabBarController {
let indexToRemove = 3
if indexToRemove < tabBarController.viewControllers?.count {
var viewControllers = tabBarController.viewControllers
viewControllers?.removeAtIndex(indexToRemove)
tabBarController.setViewControllers(viewControllers, animated: true)
}
}
}
}
Also, I keep reading that this is against Apple's intended use. Is that true still? Is there a better workflow to accomplish that type of functionality?
I would create a tab that opens up the user's account and have a button in the user VC tab that opens up a page for admins only. you can show and hide the button as needed using adminButton.hidden = true or adminButton.hidden = false.