I'm trying to create a UI in which the user can select/deselect multiple items.
I choose to use an segemented control to do such a thing and it's perfect for that use.
The behavior I want it to have is
1) an item must be set to disabled when clicked on
2) an item must be set to enabled when clicked on
I implemented step 1 but can't do step 2 as when the cell is set to disabled it won't send any event
Here's the code
#IBOutlet weak var segBtnStrings: NSSegmentedControl!
#IBAction func setStringState(sender: AnyObject) {
var segment = sender.selectedSegment
var enable = segBtnStrings.isEnabledForSegment(segment)
segBtnStrings.setEnabled(!enable, forSegment: segment)
}
Related
I have a basic Cocoa app using NSUndoManager to support undo/redo. My data model's title can be updated by editing an NSTextField. Out of the box NSTextField supports coalescing changes into a single "Undo Typing" undo action so that when the user presses Cmd-Z, the text is reverted in full instead of only the last letter.
I'd like to have a single "Undo Changing Model Title" undo action that undoes multiple changes to my model's title (made in a NSTextField). I tried grouping changes myself, but my app crashes (see below).
I can't use the default undo behavior of NSTextField, because I need to update my model and the text field might be gone by the time the user tries to undo an action (the text field is in a popup window).
NSUndoManager by default groups changes that occur within a single run loop cycle, but also allows to disable this behavior and to create custom "undo groups". So I tried the following:
Set undoManager.groupsByEvent = false
In controlTextDidBeginEditing(), begin a new undo group
In controlTextDidChange(), register an undo action
In controlTextDidEndEditing(), end the undo group
This works as long as I end editing the text field by pressing Enter. If I type "abc" and press Cmd-Z to undo before ending editing, the app crashes, because the undo grouping was not closed:
[General] undo: NSUndoManager 0x60000213b1b0 is in invalid state, undo was called
with too many nested undo groups
From the docs, undo() is supposed to close the undo grouping automatically, if needed. The grouping level is 1 in my case.
undo() [...] This method also invokes endUndoGrouping() if the nesting level is 1
However, the undo group is not closed by undo(), no matter if I set groupsByEvent to true or false. My app always crashes.
What is interesting:
I observed the default behavior of NSTextField. When I type the first letter, it begins AND ends an undo group right away. It does not create any other undo groups for subsequent changes after the first change.
To reproduce:
Create a new Cocoa project and paste in the code for the App Delegate (see below)
Type "a" + "b" + "c" + Cmd-Z
Expected:
Text field value should be set to empty string
Actual:
Crash
Alternatively:
Type "a" + "b" + "c" + Enter + Cmd-Z
Result:
The above works, because this time, the undo group is ended. All grouped changes are undone properly.
The problem is that the user can press Cmd-Z at any time while editing. I can't end the undo group after each change or changes cannot be undone all at once.
undo() not closing the undo group might be a bug, but nonetheless, grouping changes and undoing while typing must be possible, because that's what NSTextField does out of the box (and it works).
Source:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSTextFieldDelegate {
#IBOutlet weak var window: NSWindow!
private var textField:NSTextField!
private let useCustomUndo = true
private var myModelTitle = ""
func applicationDidFinishLaunching(_ aNotification: Notification) {
//useCustomUndo = false
observeUndoManger()
setup()
NSLog("Window undo manager: \(Unmanaged.passUnretained(window.undoManager!).toOpaque())")
NSLog("NSTextField undo manager: \(Unmanaged.passUnretained(textField.undoManager!).toOpaque())")
}
func controlTextDidBeginEditing(_ obj: Notification) {
NSLog("Did begin editing & starting undo group")
// With or without grouping the app crashes on undo if the group was not ended:
window.undoManager?.groupsByEvent = true
window.undoManager?.beginUndoGrouping()
}
func controlTextDidEndEditing(_ obj: Notification) {
NSLog("Did end editing & ending undo group")
window.undoManager?.endUndoGrouping()
}
func controlTextDidChange(_ obj: Notification) {
NSLog("Text did change")
setModelTitleWithUndo(title: textField.stringValue)
}
private func setModelTitleWithUndo(title:String) {
NSLog("Current groupingLevel: \(window.undoManager!.groupingLevel)")
window.undoManager?.registerUndo(withTarget: self, handler: {[oldValue = myModelTitle, weak self] _ in
guard let self = self, let undoManager = self.window.undoManager else { return }
NSLog("\(undoManager.isUndoing ? "Undo" : "Redo") from current model : '\(self.myModelTitle)' to: '\(oldValue)'")
NSLog( " from current textfield: '\(self.textField.stringValue)' to: '\(oldValue)'")
self.setModelTitleWithUndo(title: oldValue)
})
window.undoManager?.setActionName("Change Title")
myModelTitle = title
if window.undoManager?.isUndoing ?? false || window.undoManager?.isRedoing ?? false
{
textField.stringValue = myModelTitle
}
NSLog("Model: '\(myModelTitle)'")
}
private func observeUndoManger() {
for i in [(NSNotification.Name.NSUndoManagerCheckpoint , "<checkpoint>" ),
(NSNotification.Name.NSUndoManagerDidOpenUndoGroup , "<did open undo group>" ),
(NSNotification.Name.NSUndoManagerDidCloseUndoGroup, "<did close undo group>"),
(NSNotification.Name.NSUndoManagerDidUndoChange , "<did undo change>" ),
(NSNotification.Name.NSUndoManagerDidRedoChange , "<did redo change>" )]
{
NotificationCenter.default.addObserver(forName: i.0, object: nil, queue: nil) {n in
let undoManager = n.object as! UndoManager
NSLog("\(Unmanaged.passUnretained(undoManager).toOpaque()) \(i.1) grouping level: \(undoManager.groupingLevel), groups by event: \(undoManager.groupsByEvent)")
}
}
}
private func setup() {
textField = NSTextField(string: myModelTitle)
textField.translatesAutoresizingMaskIntoConstraints = false
window.contentView!.addSubview(textField)
textField.leadingAnchor.constraint(equalTo: window.contentView!.leadingAnchor).isActive = true
textField.topAnchor.constraint(equalTo: window.contentView!.topAnchor, constant: 50).isActive = true
textField.trailingAnchor.constraint(equalTo: window.contentView!.trailingAnchor).isActive = true
if useCustomUndo
{
textField.cell?.allowsUndo = false
textField.delegate = self
}
}
}
UPDATE:
I observed the behavior of NSTextField some more. NSTextField does not group across multiple run loop cycles. NSTextField groups by event and uses groupsByEvent = true. It creates a new undo group when I type the first letter, closes the group, and does not create any additional undo groups for the next letters I type. Very strange...
I have a five tab, tab bar controller in my app, and I want to only show the 5th item if a manager is logged into the app (instead of employee).
I currently have this code which disables the 5th item but I can still see it (its just grayed out and is not clickable).
self.tabBarController!.tabBar.items![4].enabled = false
Is there a way to only show the first four items and evenly space them if a non manager is logged in?
Swift 3
if let tabBarController = self.tabBarController {
let indexToRemove = 3
if indexToRemove < tabBarController.viewControllers!.count {
var viewControllers = tabBarController.viewControllers
viewControllers?.remove(at: indexToRemove)
tabBarController.viewControllers = viewControllers
}
}
In Xcode 7 with swift, I'm trying to use icons from my assets folder to display in the icon slot in a normal prototype tableview cell.
My code compiles and the app runs fine, displaying all the proper row text titles in the table, but all the cells use an icon that I (perhaps stupidly) entered into the attributes inspector for the prototype table cell.
By default, the style=Basic table cell does not have an icon to the left of the Title in the storyboard. BUT... if you add the name of an icon image from the assets folder in the image=MyAssetsIconName, the UI builder automatically adds a UIImageView to the left end of the table row cell. And of course all rows display the image that you selected. Which is what my app currently does.
But I would like to change the icon image for each row, so that the image on any row matches the text rows that I display. So I tried to assign a new image (from the assets folder) to each row cell as it was created. I did the assignment right after I assigned the text title to the cell.
Here is my code, which runs fine (but doesn't display the images that I want).
class TsbReportsCell: UITableViewCell {
#IBOutlet weak var Title: UILabel!
#IBOutlet weak var Detail: UILabel!
#IBOutlet weak var Icon: UIImageView!
...
}
In the table view controller:
In the table view controller class:
// define datasources
var reportnames = [String]()
var reportimages = [String]()
override func viewDidLoad() {
super.viewDidLoad()
reportnames = ["Balance Report",
"Routine Report",
"Low Quality Report"]
reportimages = ["enter.png",
"exterior.png",
"export.png"]
}
override func tableView(tableView:.... {
// all this code works fine
let cell = tableView.dequeueReusableCellWithIdentifier...
let row = indexPath.row
cell.Title.text = reportnames[row]
// here are the lines that seem to have no effect at all
let rowimage = UIImage(named: reportimages[row])
cell.Icon = UIImageView(image: rowimage)
return cell
}
Any ideas what I'm doing wrong? I named an image in the attributes for the table cell row just to get the UI builder to add the imageview placeholder for me. But maybe I'm not allowed to ever override that image, even though I can create an outlet for it.
This seems so simple a problem, but I searched at least 20 posts, the net, the doc, and I still couldn't find anything. Thanks
It's very simple. You are saying:
cell.Icon = UIImageView(image: rowimage)
But that does not put this image view into the interface! It merely assigns it to the cell's property.
Instead, you need to have an image view in the cell and hook an outlet from the cell's Icon property to that image view. As a result, cell.Icon is the image view in the interface. Now you say
cell.Icon.image = rowimage
Man, are you ever fast at answering questions, Matt. I'm sure I'm not the only one who appreciates that.
It's funny how taking a half hour to write up a decent question for this site makes you start thinking about other things to try. I took a seg fault on one of my tries, and was poking around in the error messages. From them, I got the idea that an ImageView was a structure that had an "image" field in it (or something like that), so I tried some UIImageView x UIImage crosses to see what would happen.
Sure enough, I was trying to assign an ImageView to an Image (I think; I could still be wrong). Here's the code line that worked for me.
cell.Icon.image = UIImage(named: reportimages[row])
So you can see I'm assigning an image to an image here, which works properly. AND... it must be right, because it's the same code as Matt's!! :-)
I've a really simple UI with a single NSPopUpButton. Its selectedIndex is bound to an int value ViewController.self.my_selection. As soon as I change the selection from the UI (i.e. a select the third item of the NSPopUpButton) I see that my_selection value changes. So far so good, what I'm trying to obtain is the opposed direction though. I want to change the my_selection value programmatically and see the NSPopUpButton selecting the item a the index that I've defined in my_selection. I erroneously supposed that behaviour was the default behaviour for bindings...
This is what I'm obtaining now:
NSPoPUpButton ---> select item at index 2 ----> my_selection becomes equal to 2
This is what I want to achieve (keeping also the previous behaviour)
my_selection ---> set value to 3----> NSPoPUpButton selected index = 3
Without a bit more info (see my comment) it's hard to see exactly what you're doing wrong. Here's how I got it working: First, create a simple class...
// Create a simple class
class Beatle: NSObject {
convenience init(name: String) {
self.init()
self.name = name
}
dynamic var name: String?
}
Then, in the AppDelegate I created a four-item array called beatles:
dynamic var beatles: [Beatle]?
func applicationDidFinishLaunching(aNotification: NSNotification) {
beatles = [Beatle(name: "John"),
Beatle(name: "Paul"),
Beatle(name: "Ringo"),
Beatle(name: "George")]
}
In Interface Builder I set things up so that this array provides the pop-up with its content:
This class also has a selectedIndex property that is bound to the pop-up button's selectedIndex binding - this property provides read-write access to the pop-up button's selection:
// Set the pop-up selection by changing the value of this variable.
// (Make sure you mark it as dynamic.)
dynamic var selectedIndex: Int = 0
I have an array of buttons and an array of associated NSTextViews (embedded in NSScrollViews), all created programmatically. All this in a program (a calendar) that compiles and works fine as far as it does.
However, I would like clicking a button to cause the associated text field to behave as if I had clicked directly in it (cursor and focus ring appear and it accepts text). Right now, I have to click the button AND the text box before I begin entering text.
It appears that “makeFirstResponder” should do what I want, but it won’t compile as I am trying to do it.
Here is the relevant code:
(All in a single View Controller)
Global Declarations:
var arrayOfButtons:[NSButton] = []
var arrayOfFields: [NSTextView!] = []
var arrayOfWindows: [NSScrollView!] = []
Creation of array of fields:
var i = 0
var myLocalText: NSTextView! = NSTextView(…
var myLocalWindow: NSScrollView! = NSScrollView(…
for i = 0; i <= 6; i++ {
var myLocalText: NSTextView! = NSTextView(…
var myLocalWindow: NSScrollView! = NSScrollView(…
view.addSubview(myLocalWindow)
myLocalWindow.hasVerticalScroller = false
myLocalWindow.focusRingType = NSFocusRingType(rawValue: UInt(2))! // I didn’t get a focus ring until I did this
myLocalWindow.addSubview(myLocalText)
myLocalWindow.documentView = myLocalText
myLocalText.editable = true
myLocalText.selectable = true
myLocalText.verticallyResizable = true
self.arrayOfFields.append(myLocalText)
}
Routine that responds to clicking a button:
…
view.makeFirstResponder(arrayOfFields[tag]) // THIS IS THE SUBJECT OF MY QUESTION
// IT GIVES A COMPILER ERROR AS FOLLOWS: “Cannot invoke ‘makeFirstResponder’ with an argument list of type ’NSTextView’”
// as far as I can tell from the documentation, makeFirstResponder should accept an argument type of NSResponder, and NSTextView should inherit from that
In Mac OS X, makeFirstResponder is an NSWindow method, not an NSView method.