I have a textfield with a hidden keyboard (since I'm using it with bluetooth). However, in iOS9 the shortcut bar keeps appearing.
Is there a way to hide it too?
Thank you so much!
You can pass your textfield name in place of userNameTextField for which you want to remove shortcut bar.
UITextInputAssistantItem* item = [userNameTextField inputAssistantItem];
item.leadingBarButtonGroups = #[];
item.trailingBarButtonGroups = #[];
In Swift 2.0
if #available(iOS 9.0, *) {
let item : UITextInputAssistantItem = yourTextView.inputAssistantItem
item.leadingBarButtonGroups = []
item.trailingBarButtonGroups = []
} else {
// Fallback on earlier versions
}
I had the same issue. And so starts a search of SO. So the above helped me out, but the whole, "if iOS9 thing" might be best framed like this:
if ([self respondsToSelector:#selector(inputAssistantItem)]) {
// iOS9.
UITextInputAssistantItem* item = [self inputAssistantItem];
item.leadingBarButtonGroups = #[];
item.trailingBarButtonGroups = #[];
}
Happily, I'd created a sub-class of a UITextField, (CHTextField) and was in use everywhere. So it was a very easy fix to whack this in the over-ridden "init" method.
Hope it helps.
Alternatively, just create an extension for UITextField in Swift 2.0 like this.
extension UITextField
{
public func hideAssistantBar()
{
if #available(iOS 9.0, *) {
let assistant = self.inputAssistantItem;
assistant.leadingBarButtonGroups = [];
assistant.trailingBarButtonGroups = [];
}
}
}
Then you can just call hideAssistantBar() on any text field you like.
#IBOutlet weak var myTextField: UITextField?;
override public func viewDidLoad() {
super.viewDidLoad();
myTextField?.hideAssistantbar();
}
In Swift 3.0 and 4.0
self.textField.inputAssistantItem.leadingBarButtonGroups.removeAll()
self.textField.inputAssistantItem.trailingBarButtonGroups.removeAll()
An easy way to do this for all text fields in your app is to create a category on UITextInputAssistantItem and override the getters for leadingBarButtonGroups and trailingBarButtonGroups like this:
#implementation UITextInputAssistantItem (RemoveBars)
- (NSArray<UIBarButtonItemGroup *> *)leadingBarButtonGroups
{
return #[];
}
- (NSArray<UIBarButtonItemGroup *> *)trailingBarButtonGroups
{
return #[];
}
#end
This worked for me on iOS 9.x and 8.x, no need for any conditional code.
Be careful with this though, this overrides those properties for EVERYTHING that uses UITextInputAssistantItem
Just to expand on the other answers here. I cobbled together some Swift 2.0 code that will loop through all subviews of a given view and disable the UITextInputAssistantItems for all UITextFields and UISearchBars.
func hideTheAssistantBar(view:UIView) {
//Check this view
for case let textField as UITextField in view.subviews {
let item : UITextInputAssistantItem = textField.inputAssistantItem
item.leadingBarButtonGroups = []
item.trailingBarButtonGroups = []
}
for case let searchBar as UISearchBar in view.subviews {
let item : UITextInputAssistantItem = searchBar.inputAssistantItem
item.leadingBarButtonGroups = []
item.trailingBarButtonGroups = []
}
//Now find this views subviews
let subviews = view.subviews
for subview : AnyObject in subviews {
if subview.isKindOfClass(UIView) {
hideTheAssistantBar(subview as! UIView)
}
}
}
You can then call this function passing in whatever view you would like to start at. I call this inside of my ViewDidLoad() method and pass in self.view like hideTheAssistantBar(self.view).
I actually went a step further for my needs and added this function to a helper class I use for common code. Therefore inside of my viewDidLoad() function I actually just call helper.hideTheAssistantBar(self.view) and then I don't have to put that function in every file.
Hope this helps someone coming along looking for an easy way to remove the assistant bar from all UITextFields and UISearchBars in one fail swoop.
Thanks to #Arkader for the swift code to recursively find all subviews. Swift List Subviews
Just to build on what Pranavan posted because setting the bar button groups to an empty array doesn't seem to work in iOS 12 or 13 using Xcode 11.
let inputAssistantItem = textFieldForTypingText.inputAssistantItem
inputAssistantItem.leadingBarButtonGroups.removeAll()
inputAssistantItem.trailingBarButtonGroups.removeAll()
I placed the above code in the viewDidLoad() function.
You can also give the option to the user:
inputAssistantItem.allowsHidingShortcuts = true
In the case letting the user hide it, if the text field becomes first responder again, they'll have to hide it again.
Related
The Real Question
How do you update the mainMenu in SwiftUI so that it actually works?
I have built a MacOS Document Based application in SwiftUI which includes all of the in-built File menu commands (i.e. Close, Save, Duplicate. Rename... etc.)
Before saving the document, I validate the structure and would like to present a modal dialog to the user if there are any validation errors.
The modal dialog is just a simple OK/Cancel dialog - 'OK' meaning that the user is happy to save the file with validation errors, 'Cancel' would need to stop the save operation.
So the question is: "How do I intercept the in-built 'Save' menu command to present this dialog?
I have tried to overwrite the .saveItem CommandGroup - but this replaces all of the menu items and I only want to override a couple of the commands ('Save' and 'Save As') and don't want to re-implement them all (and I am not sure that I have the skills to do so)
.commands {
CommandGroup(replacing: .saveItem) {
// code goes here - but removes all of the in-built menus
}
}
I have tried this solution (In a SwiftUI Document App, how to save a document from within a function)
and have put it into my AppDelegate
public func applicationDidBecomeActive(_ notification: Notification) {
let menu = NSApplication.shared.mainMenu!.items.first(where: { $0.title == "File" })!
let submenu = menu.submenu!.items.first(where: { $0.title == "Save" })!
submenu.action = #selector(showDialog)
}
#objc func showDialog() {
var retVal: Int = 0
let thisWindow: NSWindow? = NSApplication.shared.mainWindow
let nsAlert: NSAlert = NSAlert()
let cancelButton: NSButton = nsAlert.addButton(withTitle: "Cancel")
cancelButton.tag = 1
let okButton: NSButton = nsAlert.addButton(withTitle: "OK")
okButton.tag = 0
// The below code is replaced
nsAlert.beginSheetModal(for: thisWindow!) { modalResponse in
print(modalResponse)
retVal = modalResponse.rawValue
if retVal == 0 {
print("save")
} else {
print("cancel")
}
}
}
However it doesn't actually call the showDialog function.
Edit/Update
I am still having difficulties updating the menus, but in the above example the call to beginModalSheet is incorrect as the process will run in the background. Updated the call to runModal() which will stop any background process writing the file.
#objc func showDialog() {
let nsAlert: NSAlert = NSAlert()
let cancelButton: NSButton = nsAlert.addButton(withTitle: "Cancel")
cancelButton.tag = 1
let okButton: NSButton = nsAlert.addButton(withTitle: "OK")
okButton.tag = 0
let response: Int = nsAlert.runModal().rawValue
if response == 0 {
print("save")
NSApp.sendAction(#selector(NSDocument.save(_:)), to: nil, from: nil)
} else {
print("cancel")
}
}
I have read somewhere that you need to set the menu before the window appears, and I have also read that you need to set the menus before the AppDelegate is set.
Yet another edit
See this post Hiding Edit Menu of a SwiftUI / MacOS app
and this comment
Thoughts: SwiftUI either has a bug or they really don't want you to remove the top level menus in NSApp.mainMenu. SwiftUI seems to reset the whole menu with no way to override or customize most details currently (Xcode 13.4.1). The CommandGroup(replacing: .textEditing) { }-esque commands don't let you remove or clear a whole menu. Assigning a new NSApp.mainMenu just gets clobbered when SwiftUI wants even if you specify no commands.
XCode 14.1
Swift 5
After a lot of super frustrating searching an attempts and lots of code - I reduced the problem to being just trying to change the name of the save menu item - If I could do this - then I can change the action for it as well.
Here is how I did it
My Tetsing App is called YikesRedux
Steps:
Register the AppDelegate
Override the applicationWillUpdate method
Put the menu updating in a DispatchQueue.main.async closure
Cry tears of joy that you have solved this problem after days of searching
YikesAppRedux.swift
import SwiftUI
#main
struct YikesReduxApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate // <- Don't forget the AppDelegate
var body: some Scene {
DocumentGroup(newDocument: YikesReduxDocument()) { file in
ContentView(document: file.$document)
}
}
}
AppDelegate.swift
import Foundation
import AppKit
public class AppDelegate: NSObject, NSApplicationDelegate {
public func applicationWillUpdate(_ notification: Notification) {
DispatchQueue.main.async {
let currentMainMenu = NSApplication.shared.mainMenu
let fileMenu: NSMenuItem? = currentMainMenu?.item(withTitle: "File")
if nil != fileMenu {
let saveMenu = fileMenu?.submenu!.item(withTitle: "Save")
if nil != saveMenu {
print("updated menu")
saveMenu?.title = "Save Updated"
}
}
}
}
}
I put this down as a bit kludgey - as it runs on every application update (which is not a lot, but you can see the print out in the console "updated menu" when it does occur)
I did try to keep a state variable as to whether the menu was updated, to try and not do it again - but in a multi document window environment you would need to keep track of every window... (Also swift just clobbers the menu whenever it wants - so it didn't work as well as expected.)
I put the menu updating code in almost everywhere I could think of
Every single AppDelegate function override
init methods for the App, the ContentView
on the document read function/write function
You name it - I put it in there (I even had a hosting controller, a NSViewRepresentable)
I then removed them one by one until I found the solution.
I would be happy if there was a less kludgey way to do this.
I have a NSWindow, on which i apply this:
window.styleMask = window.styleMask | NSFullSizeContentViewWindowMask
window.titleVisibility = NSWindowTitleVisibility.Hidden;
window.titlebarAppearsTransparent = true;
I then add a NSView behind the titlebar to simulate a bigger one.
Now it looks like this:
I want to be able to move the window, by dragging the light-blue view. I have already tried to subclass NSView and always returning true for mouseDownCanMoveWindow using this code:
class LSViewD: NSView {
override var mouseDownCanMoveWindow:Bool {
get {
return true
}
}
}
This didn't work.
After some googling i found this INAppStoreWindow on GitHub. However it doesn't support OS X versions over 10.9, so it's completely useless for me.
Edit1
This is how it looks in the Interface Builder.
How can i move the window, by dragging on this NSView?
None of the answers here worked for me. They all either don't work at all, or make the whole window draggable (note that OP is not asking for this).
Here's how to actually achieve this:
To make a NSView control the window with it's drag events, simply subclass it and override the mouseDown as such:
class WindowDragView: NSView {
override public func mouseDown(with event: NSEvent) {
window?.performDrag(with: event)
}
}
That's it. The mouseDown function will transfer further event tracking to it's parent window.
No need for window masks, isMovableByWindowBackground or mouseDownCanMoveWindow.
Try setting the window's movableByWindowBackground property to true.
There are two ways to do this. The first one would be to set the NSTexturedBackgroundWindowMask as well as the windows background color to the one of your view. This should work.
Otherwise you can take a look at this Sample Code
I somehow managed to solve my problem, i don't really know how, but here are some screenshots.
In the AppDelegate file where i edit the properties of my window, i added an IBOutlet of my contentView. This IBOutlet is a subclass of NSView, in which i've overriden the variable mouseDownCanMoveWindow so it always returns false.
I tried this before in only one file, but it didn't work. This however solved the problem.
Thanks to Ken Thomases and Max for leading me into the right direction.
Swift3.0 Version
override func viewDidAppear() {
//for hide the TitleBar
self.view.window?.styleMask = .borderless
self.view.window?.titlebarAppearsTransparent = true
self.view.window?.titleVisibility = .hidden
//for Window movable with NSView
self.view.window?.isMovableByWindowBackground = true
}
Swift 3:
I needed this but dynamically. It's a little long but well worth it (IMHO).
So I decided to enable this only while the command key is down. This is achieved by registering a local key handler in the delegate:
// MARK:- Local key monitor
var localKeyDownMonitor : Any? = nil
var commandKeyDown : Bool = false {
didSet {
let notif = Notification(name: Notification.Name(rawValue: "commandKeyDown"),
object: NSNumber(booleanLiteral: commandKeyDown))
NotificationCenter.default.post(notif)
}
}
func keyDownMonitor(event: NSEvent) -> Bool {
switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
case [.command]:
self.commandKeyDown = true
return true
default:
self.commandKeyDown = false
return false
}
}
which is enabled within the delegate startup:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Watch local keys for window movenment, etc.
localKeyDownMonitor = NSEvent.addLocalMonitorForEvents(matching: NSEventMask.flagsChanged) { (event) -> NSEvent? in
return self.keyDownMonitor(event: event) ? nil : event
}
}
and its removal
func applicationWillTerminate(_ aNotification: Notification) {
// Forget key down monitoring
NSEvent.removeMonitor(localKeyDownMonitor!)
}
Note that when the commandKeyDown value is changed by the key down handler. This value change is caught by the didset{} to post a notification. This notification is registered by any view you wish to have its window so moved - i.e., in the view delegate
override func viewDidLoad() {
super.viewDidLoad()
// Watch command key changes
NotificationCenter.default.addObserver(
self,
selector: #selector(ViewController.commandKeyDown(_:)),
name: NSNotification.Name(rawValue: "commandKeyDown"),
object: nil)
}
and discarded when the viewWillDisappear() (delegate) or the window controller windowShouldClose(); add this
<your-view>.removeObserver(self, forKeyPath: "commandKeyDown")
So sequence goes like this:
key pressed/release
handler called
notification posted
The view's window isMovableByWindowBackground property is changed by notification - placed within view controller / delegate or where you registered the observer.
internal func commandKeyDown(_ notification : Notification) {
let commandKeyDown : NSNumber = notification.object as! NSNumber
if let window = self.view.window {
window.isMovableByWindowBackground = commandKeyDown.boolValue
Swift.print(String(format: "command %#", commandKeyDown.boolValue ? "v" : "^"))
}
}
Remove the tracer output when happy. See it in action in SimpleViewer on github.
I have this for-in loop:
for button in view.subviews {
}
Now I want button to be cast into a custom class so I can use its properties.
I tried this: for button in view.subviews as AClass
But it doesnt work and gives me an error:'AClass' does not conform to protocol 'SequenceType'
And I tried this: for button:AClass in view.subviews
But neither does that work.
For Swift 2 and later:
Swift 2 adds case patterns to for loops, which makes it even easier and safer to type cast in a for loop:
for case let button as AClass in view.subviews {
// do something with button
}
Why is this better than what you could do in Swift 1.2 and earlier? Because case patterns allow you to pick your specific type out of the collection. It only matches the type you are looking for, so if your array contains a mixture, you can operate on only a specific type.
For example:
let array: [Any] = [1, 1.2, "Hello", true, [1, 2, 3], "World!"]
for case let str as String in array {
print(str)
}
Output:
Hello
World!
For Swift 1.2:
In this case, you are casting view.subviews and not button, so you need to downcast it to the array of the type you want:
for button in view.subviews as! [AClass] {
// do something with button
}
Note: If the underlying array type is not [AClass], this will crash. That is what the ! on as! is telling you. If you're not sure about the type you can use a conditional cast as? along with optional binding if let:
if let subviews = view.subviews as? [AClass] {
// If we get here, then subviews is of type [AClass]
for button in subviews {
// do something with button
}
}
For Swift 1.1 and earlier:
for button in view.subviews as [AClass] {
// do something with button
}
Note: This also will crash if the subviews aren't all of type AClass. The safe method listed above also works with earlier versions of Swift.
This option is more secure:
for case let button as AClass in view.subviews {
}
or swifty way:
view.subviews
.compactMap { $0 as AClass }
.forEach { .... }
The answers provided are correct, I just wanted to add this as an addition.
When using a for loop with force casting, the code will crash (as already mentioned by others).
for button in view.subviews as! [AClass] {
// do something with button
}
But instead of using an if-clause,
if let subviews = view.subviews as? [AClass] {
// If we get here, then subviews is of type [AClass]
...
}
another way is to use a while-loop:
/* If you need the index: */
var iterator = view.subviews.enumerated().makeIterator()
while let (index, subview) = iterator.next() as? (Int, AClass) {
// Use the subview
// ...
}
/* If you don't need the index: */
var iterator = view.subviews.enumerated().makeIterator()
while let subview = iterator.next().element as? AClass {
// Use the subview
// ...
}
Which seems to be more convenient if some elements (but not all) of the array might be of type AClass.
Although for now (as of Swift 5), I'd go for the for-case loop:
for case let (index, subview as AClass) in view.subviews.enumerated() {
// ...
}
for case let subview as AClass in view.subviews {
// ...
}
You can also use a where clause
for button in view.subviews where button is UIButton {
...
}
The answer provided by vacawama was correct in Swift 1.0. And no longer works with Swift 2.0.
If you try, you will get an error similar to:
'[AnyObject]' is not convertible to '[AClass]';
In Swift 2.0 you need to write like:
for button in view.subviews as! [AClass]
{
}
You can perform the casting and be safe at the same time with this:
for button in view.subviews.compactMap({ $0 as? AClass }) {
}
If you want to have an index in your loop:
for case let (index, button as AClass) in view.subviews.enumerated() {
}
The officially recommended method for XIB/Storyboard localization is to create .xib and .storyboard files inside xx.lproj (where xx is the two letter language ID) for each localization you want to support.
This creates a problem because you have multiple files that in many cases share the same UI, that are prone to change. If you wanted to re-design the UI for one view, you'll have to do it multiple times (worse if you entered the localizable string values in the xib itself). This goes against the DRY principle.
It seems way more efficient to call NSLocalizedString() where you need it, and just use one XIB or Storyboard for one base localization.
So, why should(n't) I create localized XIB/Storyboard files?
You can make a category on UILabel, UIButton etc. like this:
#import "UILabel+Localization.h"
#implementation UILabel (Localization)
- (void)setLocalizeKey:(NSString*)key
{
self.text = NSLocalizedString(key, nil);
}
#end
and after that on your xib file use User Defined Runtime Attributes to link the UILabel (or UIButton etc.) to a key saved in your Localizable.strings file
This way you can have all your strings in one file and you do not have to create a separate xib for each language.
For just changing text labels I did something like this
+(void) replaceTextWithLocalizedTextInSubviewsForView:(UIView*)view
{
for (UIView* v in view.subviews)
{
if (v.subviews.count > 0)
{
[self replaceTextWithLocalizedTextInSubviewsForView:v];
}
if ([v isKindOfClass:[UILabel class]])
{
UILabel* l = (UILabel*)v;
l.text = NSLocalizedString(l.text, nil);
[l sizeToFit];
}
if ([v isKindOfClass:[UIButton class]])
{
UIButton* b = (UIButton*)v;
[b setTitle:NSLocalizedString(b.titleLabel.text, nil) forState:UIControlStateNormal];
}
}
}
call this function in your viewDidLoad: like this:
[[self class] replaceTextWithLocalizedTextInSubviewsForView:self.view];
It saved me a lot of work declaring and connecting IBOutlets when all you want is localized labels.
Flax's solution works well, one thing to note is that if you have UILabels or UIButtons which are are contained in UICollectionViewCells in UICollectionViews (or similar) and these collections change frequently in the current view, eg due to user action or being populated by an asynchronous request, then to keep the labels updated with the correct localization strings you can call the localization function in viewDidLayoutSubviews instead of viewDidLoad (which is called only once):
- (void)viewDidLayoutSubviews
{
[LocalizationHelper replaceTextWithLocalizedTextInSubviewsForView:self.view];
}
As can be seen from this code, I keep the localization method in a static helper class (as the other chap suggested):
#implementation LocalizationHelper
+(void) replaceTextWithLocalizedTextInSubviewsForView:(UIView*)view
{
for (UIView* v in view.subviews)
... <code from above> ...
}
#end
Would have added this as a comment to the above solution, but I ain't got no 'rep!
As explained by Leszek S you can create a category.
Here I'll give you an example in swift 3 with extension for UILabel and UIButton:
First of all create a "StringExtension.swift" file
Add on it this code:
extension String {
func localized() -> String {
let bundle = Bundle.main
return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "")
}
}
Then create another new file with the name you want (for example) "LocalizableObjectsExtensions.swift"
Add on it an extension for UIButton and one for UILabel like this (of course you can create extension for what you want, UITextField...):
extension UIButton {
var localizedText: String {
set (key) { setTitle(key.localized(), for: .normal) }
get { return titleLabel!.text! }
}
}
extension UILabel {
var localizedText: String {
set (key) { text = key.localized() }
get { return text! }
}
}
Now go in your Storyboard and for your button and/or you label that you want localize just add in the identity inspector of you object this:
FYI: here Key Path it's the name of the function you added in your extensions (UIlabel and UIButton) and Value is the name of the key that you want translate automatically which is in your Localizable.strings file. For example in your Localizable.strings (French) you have the key/value "ourOffers" = "NOS OFFRES";
Now build & Run. Your Object will be translated in the language of your device if you have the key/value in your Localizable.string. Enjoy :)
you can automate a lot of it with ibtool. this is a decent introduction: http://www.bdunagan.com/2009/03/15/ibtool-localization-made-easy/
Every place I look says that you have to replicate the entire xib file for each localization instance, even though you really only wanted to rip the text out and replicate the text in a different language for each localization instance.
If anyone knows of a method to replicate only the user visible text of an xib (in a different language) without replicating the entire xib file for each language, please let us know.
Useful post, much easier than multiple XIBs. I extended the code to handle UISegmentedControl:
if ([v isKindOfClass:[UISegmentedControl class]]) {
UISegmentedControl* s = (UISegmentedControl*)v;
for (int i = 0; i < s.numberOfSegments; i++) {
[s setTitle:NSLocalizedString([s titleForSegmentAtIndex:i],nil) forSegmentAtIndex:i];
}
}
I was looking for the exactly answer given by Flax, marked as right, but I needed it in Swift. So I translated into it. Thanks Flax.
func replaceTextWithLocalizedTextInSubviewsForView(view: UIView) {
for v in view.subviews {
if v.subviews.count > 0 {
self.replaceTextWithLocalizedTextInSubviewsForView(v)
}
if (v.isKindOfClass(UILabel)) {
let myLabel: UILabel = v as! UILabel
myLabel.text = NSLocalizedString(myLabel.text!, comment: "Text to translate.")
myLabel.sizeToFit()
}
if (v.isKindOfClass(UIButton)) {
let myButton: UIButton = v as! UIButton
myButton.setTitle(NSLocalizedString((myButton.titleLabel?.text)!, comment: "Text to translate.") as String, forState: .Normal)
myButton.sizeToFit()
}
}
}
That works for Swift 2.1
I used a similar approach as Leszek Szary described for my views in Swift.
Using a Boolean value as opposed to the localization keys, I added an "On/Off" drop down menu that determines whether the initial text values should be localized or not. This allows for the Storyboard to be kept clean without any extra upkeep.
When a value is selected, a single Runtime Attribute is added to the view and is used as a condition from within it's setter.
Here is the code from my .swift file which extends UIButton, UILabel, UITabBarItem and UITextField, including the text field placeholder and button control states:
import UIKit
extension String {
public var localize: String {
return NSLocalizedString(self, comment: "")
}
}
extension UIButton {
#IBInspectable public var Localize: Bool {
get { return false }
set { if (newValue) {
setTitle( title(for:.normal)?.localize, for:.normal)
setTitle( title(for:.highlighted)?.localize, for:.highlighted)
setTitle( title(for:.selected)?.localize, for:.selected)
setTitle( title(for:.disabled)?.localize, for:.disabled)
}}
}
}
extension UILabel {
#IBInspectable public var Localize: Bool {
get { return false }
set { if (newValue) { text = text?.localize }}
}
}
extension UITabBarItem {
#IBInspectable public var Localize: Bool {
get { return false }
set { if (newValue) { title = title?.localize }}
}
}
extension UITextField {
#IBInspectable public var Localize: Bool {
get { return false }
set { if (newValue) {
placeholder = placeholder?.localize
text = text?.localize
}}
}
}
You could also use the new property to easily translate values that are set while your program is running like this:
let button = UIButton()
button.setTitle("Normal Text", for: .normal)
button.setTitle("Selected Text", for: .selected)
button.Localize = true
I came across this post and several others while trying to make xib localization easier for myself. I posted my method of including IBOutles for labels/buttons on this question, worked great for me, keeps all changes limited to the Localization.strings files.
https://stackoverflow.com/a/15485572/1449834
IMHO Xcode has one among the worst localization features available around...
I really don't like developing for Android but I must admit Android Studio has a better localization system.
That said, because I really cannot stand anymore to recreate Storyboard.strings after each mod (you know, Xcode won't update them for you...), this is how I do :
I have a couple of extensions to loop subviews (and subviews of subviews) and I deal with each of the main objects (labels, textfield, buttons...) by localizing their main properties (text, placeholde...) through a simple helper (AltoUtil.ls) which is a "short" version for NSLocalizedString.
Then I insert texts and placeholders with underscores (for example "_first_name", "_email_address") in my storyboard/xibs and I add those strings to each Localizable.strings file.
Now I just need to call the localize() function in viewDidLoad (or whereber I need it) so that I can have the whole view controller localized. For cells I just call the localize() inside the awakeFromNib() method for example.
I'm sure this is not the fastest method (due to subviews loop) but I don't get any slowdown compared to other methods I used to use and it's pretty productive.
import UIKit
extension UIView {
func localize()
{
for view in self.allSubviews()
{
if let label = view as? UILabel
{
label.text = AltoUtil.ls(label.text)
}
else if let textField = view as? UITextField
{
textField.text = AltoUtil.ls(textField.text)
textField.placeholder = AltoUtil.ls(textField.placeholder)
}
else if let button = view as? UIButton
{
button.setTitle(AltoUtil.ls(button.title(for: UIControl.State.normal)), for: UIControl.State.normal)
}
else if let searchBar = view as? UISearchBar
{
searchBar.placeholder = AltoUtil.ls(searchBar.placeholder)
}
}
}
func allSubviews() -> [UIView]
{
return subviews + subviews.flatMap { $0.allSubviews() }
}
}
The second extension is needed to localize view controllers title and tab bar items in view controllers. You can add any item you need to localize.
import UIKit
extension UIViewController {
func localize()
{
self.title = AltoUtil.ls(self.navigationItem.title)
self.tabBarItem?.title = AltoUtil.ls(self.tabBarItem?.title)
self.view.localize()
}
}
I want to replace RBSplitView with NSSplitView in my existing project. The application is now leopard only and I would like to replace RBSplitView with the new NSSplitView shipped with Leopard.
However, I'm missing RBSplitView's handy methods expand and collapse in NSSplitView. How can I expand and collapse parts of NSSplitView programmatically?
Simply hide the subview you want to collapse, e.g.
[aSubViewToCollapse setHidden:YES];
You might also want to implement the delegate method -(BOOL)splitView:shouldHideDividerAtIndex: to return YES to hide the divider when a collapsed.
I just got programmatic expanding and collapsing of NSSplitView to work. I've also configured my NSSplitView to expand/collapse a subview whenever the divider is double-clicked, so I wanted this to play nice with that feature (and it seems to). This is what I did:
(in this example, splitView is the NSSplitView itself, splitViewSubViewLeft is the subview I wish to expand/collapse and lastSplitViewSubViewLeftWidth is an instance variable of type CGFloat.)
// subscribe to splitView's notification of subviews resizing
// (I do this in -(void)awakeFromNib)
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(mainSplitViewWillResizeSubviewsHandler:)
name:NSSplitViewWillResizeSubviewsNotification
object:splitView
];
// this is the handler the above snippet refers to
- (void) mainSplitViewWillResizeSubviewsHandler:(id)object
{
lastSplitViewSubViewLeftWidth = [splitViewSubViewLeft frame].size.width;
}
// wire this to the UI control you wish to use to toggle the
// expanded/collapsed state of splitViewSubViewLeft
- (IBAction) toggleLeftSubView:(id)sender
{
[splitView adjustSubviews];
if ([splitView isSubviewCollapsed:splitViewSubViewLeft])
[splitView
setPosition:lastSplitViewSubViewLeftWidth
ofDividerAtIndex:0
];
else
[splitView
setPosition:[splitView minPossiblePositionOfDividerAtIndex:0]
ofDividerAtIndex:0
];
}
I tried the solution above, and found it did not work, as isSubviewCollapsed never returned YES
A combination of the suggestions yielded a result which works
if ([splitViewTop isHidden]) {
[splitViewTop setHidden:NO];
[split
setPosition:previousSplitViewHeight
ofDividerAtIndex:0];
}
else {
[splitViewTop setHidden:YES];
}
[split adjustSubviews];
In El Capitan, this did the trick for me.
splitViewItem.collapsed = YES;
After some experimenting with the suggestions this was the easiest solution I found:
-(void)toggleCollapsibleView:(ib)sender {
[collapsibleView setHidden:![splitView isSubviewCollapsed:collapsibleView]];
[splitView adjustSubviews];
}
The function is a user defined first-responder action. It is triggered by a menu-item (or keystroke).
The collapsibleView is a subview in the splitView both of which are connected in IB with their properties.
In macOS Sierra, the collapsed property is changed to isCollapsed. Is straight forward just setting the property to true or false. The following code is from my WindowController, where I have two SplitViewItems.
#IBAction func toggleMap(_ sender: Any) {
if let splitViewController = contentViewController as? NSSplitViewController {
let splitViewItem = splitViewController.splitViewItems
if splitViewItem.first!.isCollapsed {
splitViewItem.first!.isCollapsed = false
} else if splitViewItem.last!.isCollapsed {
splitViewItem.last!.isCollapsed = false
} else {
if splitViewItem.first!.isCollapsed {
splitViewItem.first!.isCollapsed = false
}
splitViewItem.last!.isCollapsed = true
}
}
}
NSSplitView actually has a private method -(void)_setSubview:(NSView *)view isCollapsed:(BOOL)collapsed that does this. Those who would like to ignore all warnings against using private methods, behold:
- (void)toggleSubview:(NSView *)view {
SEL selector = #selector(_setSubview:isCollapsed:);
NSMethodSignature *signature = [NSSplitView instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = selector;
[invocation setArgument:&view atIndex:2];
BOOL arg = ![self isSubviewCollapsed:view];
[invocation setArgument:&arg atIndex:3];
[invocation invoke];
}
I implemented this as a category on NSSplitView. The only issue is that Xcode gives a warning about _setSubview:isCollapsed: being undeclared... I'm not really sure how to get around that.
El Capitan Update
I haven't written any code for OS X in ~2 years now so I haven't been able to verify this, but according to lemonmojo in the comments below, _setSubview:isCollapsed: was renamed in El Capitan to _setArrangedView:isCollapsed:.
In swift this works
func togglePanel() {
let splitViewItem = self.mySplitView.arrangedSubviews
if mySplitView.isSubviewCollapsed(outline.view){
splitViewItem[0].hidden = false
} else {
splitViewItem[0].hidden = true
}
call this from IBAction,
outline is an OutlineViewController with own xib and we need the view hence outline.view, keeping it simple but hope you get the idea
#IBAction func segmentAction(sender: NSSegmentedControl) {
splitVC?.togglePanel(sender.selectedSegment)
}
and
func togglePanel(segmentID: Int) {
let splitViewItem = self.mySplitView.arrangedSubviews
switch segmentID {
case segmentID:
if mySplitView.isSubviewCollapsed(splitViewItem[segmentID]) {
splitViewItem[segmentID].hidden = false
} else {
splitViewItem[segmentID].hidden = true
}
default:
break
}
}
And implement delegate
func splitView(splitView: NSSplitView, shouldHideDividerAtIndex dividerIndex: Int) -> Bool {
return true
}
And with 10.11 you might just use toggleSidebar action method.
How to toggle visibility of NSSplitView subView + hide Pane Splitter divider?
https://github.com/Dis3buted/SplitViewController
I recommend to use NSSplitViewController instead, and NSSplitViewItem.isCollapsed to control them. This just work.
let item: NSSplitViewItem = ...
item.isCollapsed = true
To make this to work properly, you have to configure split-UI components with mainly view-controllers. Otherwise, it can be broken.
You could try Brandon Walkin's BWToolKit.
The BWSplitView class has a method
- (IBAction)toggleCollapse:(id)sender;
#IBOutlet weak var horizontalSplitView: NSSplitView!
var splitViewItem : [NSView]?
var isSplitViewHidden: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// To Hide Particular Sub-View.
splitViewItem = self.horizontalSplitView.arrangedSubviews
splitViewItem?[0].isHidden = true
isSplitViewHidden = true
}
//MARK: View / Manage All Jobs Button Tapped.
#IBAction func actionManageScheduleJobsButtonTapped(_ sender: Any) {
if isSplitViewHidden == true {
isSplitViewHidden = false
splitViewItem?[0].isHidden = false
} else {
isSplitViewHidden = true
splitViewItem?[0].isHidden = true
}
}
--------- OR ----------
//MARK: View / Manage All Jobs Button Tapped.
#IBAction func actionManageScheduleJobsButtonTapped(_ sender: Any) {
if splitViewItem?[0].isHidden == true {
splitViewItem?[0].isHidden = false
} else {
splitViewItem?[0].isHidden = true
}
}