How do I take a "screenshot" of an NSView? - objective-c

I need to take the contents of an NSView and put them in an NSImage, for an experimental project. Is this possible? I did some Googling, tried two methods that I found - but they didn't really work. Any suggestions?

From WWDC 2012 Session 245 (translated to Swift):
let viewToCapture = self.window!.contentView!
let rep = viewToCapture.bitmapImageRepForCachingDisplay(in: viewToCapture.bounds)!
viewToCapture.cacheDisplay(in: viewToCapture.bounds, to: rep)
let img = NSImage(size: viewToCapture.bounds.size)
img.addRepresentation(rep)

[[NSImage alloc] initWithData:[view dataWithPDFInsideRect:[view bounds]]];

let dataOfView = view.dataWithPDFInsideRect(view.bounds)
let imageOfView = NSImage(data: dataOfView)

NSView.bitmapImageRepForCachingDisplay() (mentioned in this answer) doesn't render the colors correctly on some views.
CGWindowListCreateImage() works perfectly for me.
Here's my implementation:
extension NSView {
#objc func takeScreenshot() -> NSImage? {
let screenRect = self.rectInQuartzScreenCoordinates()
guard let window = self.window else {
assert(false); return nil
}
let windowID = CGWindowID(window.windowNumber)
guard let screenshot = CGWindowListCreateImage(screenRect, .optionIncludingWindow, windowID, []) else {
assert(false); return nil
}
return NSImage(cgImage: screenshot, size: self.frame.size)
}
}
This code uses a method NSView.rectInQuartzScreenCoordinates(). To implement it you'll first have to convert the bounds of your view to screenCoordinates using NSView and NSWindow methods and then you need to flip the coordinates like this.

Related

Return type of UIImage in Swift [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 6 years ago.
Improve this question
I have a function which returns image if condition is satisfied else it returns nil
- (UIImage *)getProfilePic{
if (doc.userProperties != nil) {
UIImage *image = [UIImage imageNamed:#"placeholder.png"];
return image;
}
else {
return nil;
}
}
I want to convert this is in swift. I have tried this but it shows error while returning nil and also it crashes by showing error as unwrapping nil.
func getProfilePic(){
var image : UIImage?
if doc!.userProperties != nil {
image = UIImage(named: "placeholder.png")!
return image!
}
else {
return nil
}
}
at "return nil" line it shows nil is not compatible with return type ' uiimage'
func getProfilePic(){
var image : UIImage?
if doc!.userProperties != nil {
image = UIImage(named: "placeholder.png")!
return image!
}
else {
return nil
}
}
Right, so there are a few problems here. First, your code doesn't have a return type. If we ignore the body of your method and look just at the signature, the Objective-C equivalent would look like this:
- (void)getProfilePic;
So Swift & Objective-C would be complaining about the same thing here: what you're trying to return and the declared return type of the method do not match.
In case it's helpful since you seem perhaps more familiar with Objective-C than Swift, here's what your Swift method would look like if we translated it back into Objective-C:
- (void)getProfilePic {
UIImage *image;
if (doc.userProperties) {
image = UIImage(named: #"placeholder.png");
return image;
}
else {
return nil;
}
}
And again, this would generate the same or similar compile time warnings or errors, because the return type does not match the method signature. But Objective-C would not crash for unwrapping nil (but Swift will).
What you're actually trying to return is a UIImage?, so we need to update our method signature.
func getProfilePic() -> UIImage? {
if doc?.userProperties != nil {
return UIImage(named: "placeholder.png")
}
return nil
}
Assuming that userProperties holds perhaps a URL to an image you want to download or maybe the image itself, in the future we're going to want a slightly different construct... something more like this:
func getProfilePic() -> UIImage? {
guard let userProperties = doc?.userProperties else {
return nil
}
// extract the image from userProperties and return it
}
Try this code sample :
func getProfilePic() -> UIImage? {
let imageName = "placeholder.png"
var image: UIImage?
if (doc.userProperties != nil) {
image = UIImage(named: imageName)
}
return image
}

Create UITextRange from NSRange

I need to find the pixel-frame for different ranges in a textview. I'm using the - (CGRect)firstRectForRange:(UITextRange *)range; to do it. However I can't find out how to actually create a UITextRange.
Basically this is what I'm looking for:
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
UITextRange*range2 = [UITextRange rangeWithNSRange:range]; //DOES NOT EXIST
CGRect rect = [textView firstRectForRange:range2];
return rect;
}
Apple says one has to subclass UITextRange and UITextPosition in order to adopt the UITextInput protocol. I don't do that, but I tried anyway, following the doc's example code and passing the subclass to firstRectForRange which resulted in crashing.
If there is a easier way of adding different colored UILables to a textview, please tell me. I have tried using UIWebView with content editable set to TRUE, but I'm not fond of communicating with JS, and coloring is the only thing I need.
Thanks in advance.
You can create a text range with the method textRangeFromPosition:toPosition. This method requires two positions, so you need to compute the positions for the start and the end of your range. That is done with the method positionFromPosition:offset, which returns a position from another position and a character offset.
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView
{
UITextPosition *beginning = textView.beginningOfDocument;
UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
UITextPosition *end = [textView positionFromPosition:start offset:range.length];
UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
CGRect rect = [textView firstRectForRange:textRange];
return [textView convertRect:rect fromView:textView.textInputView];
}
It is a bit ridiculous that seems to be so complicated.
A simple "workaround" would be to select the range (accepts NSRange) and then read the selectedTextRange (returns UITextRange):
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
textView.selectedRange = range;
UITextRange *textRange = [textView selectedTextRange];
CGRect rect = [textView firstRectForRange:textRange];
return rect;
}
This worked for me even if the textView is not first responder.
If you don't want the selection to persist, you can either reset the selectedRange:
textView.selectedRange = NSMakeRange(0, 0);
...or save the current selection and restore it afterwards
NSRange oldRange = textView.selectedRange;
// do something
// then check if the range is still valid and
textView.selectedRange = oldRange;
Swift 4 of Andrew Schreiber's answer for easy copy/paste
extension NSRange {
func toTextRange(textInput:UITextInput) -> UITextRange? {
if let rangeStart = textInput.position(from: textInput.beginningOfDocument, offset: location),
let rangeEnd = textInput.position(from: rangeStart, offset: length) {
return textInput.textRange(from: rangeStart, to: rangeEnd)
}
return nil
}
}
To the title question, here is a Swift 2 extension that creates a UITextRange from an NSRange.
The only initializer for UITextRange is a instance method on the UITextInput protocol, thus the extension also requires you pass in UITextInput such as UITextField or UITextView.
extension NSRange {
func toTextRange(textInput textInput:UITextInput) -> UITextRange? {
if let rangeStart = textInput.positionFromPosition(textInput.beginningOfDocument, offset: location),
rangeEnd = textInput.positionFromPosition(rangeStart, offset: length) {
return textInput.textRangeFromPosition(rangeStart, toPosition: rangeEnd)
}
return nil
}
}
Swift 4 of Nicolas Bachschmidt's answer as an UITextView extension using swifty Range<String.Index> instead of NSRange:
extension UITextView {
func frame(ofTextRange range: Range<String.Index>?) -> CGRect? {
guard let range = range else { return nil }
let length = range.upperBound.encodedOffset-range.lowerBound.encodedOffset
guard
let start = position(from: beginningOfDocument, offset: range.lowerBound.encodedOffset),
let end = position(from: start, offset: length),
let txtRange = textRange(from: start, to: end)
else { return nil }
let rect = self.firstRect(for: txtRange)
return self.convert(rect, to: textInputView)
}
}
Possible use:
guard let rect = textView.frame(ofTextRange: text.range(of: "awesome")) else { return }
let awesomeView = UIView()
awesomeView.frame = rect.insetBy(dx: -3.0, dy: 0)
awesomeView.layer.borderColor = UIColor.black.cgColor
awesomeView.layer.borderWidth = 1.0
awesomeView.layer.cornerRadius = 3
self.view.insertSubview(awesomeView, belowSubview: textView)
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
UITextRange *textRange = [[textView _inputController] _textRangeFromNSRange:range]; // Private
CGRect rect = [textView firstRectForRange:textRange];
return rect;
}
Here is explain.
A UITextRange object represents a range of characters in a text
container; in other words, it identifies a starting index and an
ending index in string backing a text-entry object.
Classes that adopt the UITextInput protocol must create custom
UITextRange objects for representing ranges within the text managed by
the class. The starting and ending indexes of the range are
represented by UITextPosition objects. The text system uses both
UITextRange and UITextPosition objects for communicating text-layout
information. There are two reasons for using objects for text ranges
rather than primitive types such as NSRange:
Some documents contain nested elements (for example, HTML tags and
embedded objects) and you need to track both absolute position and
position in the visible text.
The WebKit framework, which the iPhone text system is based on,
requires that text indexes and offsets be represented by objects.
If you adopt the UITextInput protocol, you must create a custom
UITextRange subclass as well as a custom UITextPosition subclass.
For example like in those sources

Add animated Gif image in Iphone UIImageView

I need to load an animated Gif image from a URL in UIImageview.
When I used the normal code, the image didn't load.
Is there any other way to load animated Gif images?
UIImageView* animatedImageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
animatedImageView.animationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"image1.gif"],
[UIImage imageNamed:#"image2.gif"],
[UIImage imageNamed:#"image3.gif"],
[UIImage imageNamed:#"image4.gif"], nil];
animatedImageView.animationDuration = 1.0f;
animatedImageView.animationRepeatCount = 0;
[animatedImageView startAnimating];
[self.view addSubview: animatedImageView];
You can load more than one gif images.
You can split your gif using the following ImageMagick command:
convert +adjoin loading.gif out%d.gif
This has found an accepted answered, but I recently came across the UIImage+animatedGIF UIImage extension. It provides the following category:
+[UIImage animatedImageWithAnimatedGIFURL:(NSURL *)url]
allowing you to simply:
#import "UIImage+animatedGIF.h"
UIImage* mygif = [UIImage animatedImageWithAnimatedGIFURL:[NSURL URLWithString:#"http://en.wikipedia.org/wiki/File:Rotating_earth_(large).gif"]];
Works like magic.
Here is the best solution to use Gif Image.
Add SDWebImage from Github in your project.
#import "UIImage+GIF.h"
_imageViewAnimatedGif.image= [UIImage sd_animatedGIFNamed:#"thumbnail"];
If you don't want to use 3rd party library,
extension UIImageView {
func setGIFImage(name: String, repeatCount: Int = 0 ) {
DispatchQueue.global().async {
if let gif = UIImage.makeGIFFromCollection(name: name, repeatCount: repeatCount) {
DispatchQueue.main.async {
self.setImage(withGIF: gif)
self.startAnimating()
}
}
}
}
private func setImage(withGIF gif: GIF) {
animationImages = gif.images
animationDuration = gif.durationInSec
animationRepeatCount = gif.repeatCount
}
}
extension UIImage {
class func makeGIFFromCollection(name: String, repeatCount: Int = 0) -> GIF? {
guard let path = Bundle.main.path(forResource: name, ofType: "gif") else {
print("Cannot find a path from the file \"\(name)\"")
return nil
}
let url = URL(fileURLWithPath: path)
let data = try? Data(contentsOf: url)
guard let d = data else {
print("Cannot turn image named \"\(name)\" into data")
return nil
}
return makeGIFFromData(data: d, repeatCount: repeatCount)
}
class func makeGIFFromData(data: Data, repeatCount: Int = 0) -> GIF? {
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
print("Source for the image does not exist")
return nil
}
let count = CGImageSourceGetCount(source)
var images = [UIImage]()
var duration = 0.0
for i in 0..<count {
if let cgImage = CGImageSourceCreateImageAtIndex(source, i, nil) {
let image = UIImage(cgImage: cgImage)
images.append(image)
let delaySeconds = UIImage.delayForImageAtIndex(Int(i),
source: source)
duration += delaySeconds
}
}
return GIF(images: images, durationInSec: duration, repeatCount: repeatCount)
}
class func delayForImageAtIndex(_ index: Int, source: CGImageSource!) -> Double {
var delay = 0.0
// Get dictionaries
let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil)
let gifPropertiesPointer = UnsafeMutablePointer<UnsafeRawPointer?>.allocate(capacity: 0)
if CFDictionaryGetValueIfPresent(cfProperties, Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque(), gifPropertiesPointer) == false {
return delay
}
let gifProperties:CFDictionary = unsafeBitCast(gifPropertiesPointer.pointee, to: CFDictionary.self)
// Get delay time
var delayObject: AnyObject = unsafeBitCast(
CFDictionaryGetValue(gifProperties,
Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque()),
to: AnyObject.self)
if delayObject.doubleValue == 0 {
delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties,
Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque()), to: AnyObject.self)
}
delay = delayObject as? Double ?? 0
return delay
}
}
class GIF: NSObject {
let images: [UIImage]
let durationInSec: TimeInterval
let repeatCount: Int
init(images: [UIImage], durationInSec: TimeInterval, repeatCount: Int = 0) {
self.images = images
self.durationInSec = durationInSec
self.repeatCount = repeatCount
}
}
To use,
override func viewDidLoad() {
super.viewDidLoad()
imageView.setGIFImage(name: "gif_file_name")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
imageView.stopAnimating()
}
Make sure you add the gif file in the project, not in the .xcassets folder.
Check this link
https://github.com/mayoff/uiimage-from-animated-gif/blob/master/uiimage-from-animated-gif/UIImage%2BanimatedGIF.h
and import these clases UIImage+animatedGIF.h,UIImage+animatedGIF.m
Use this code
NSURL *urlZif = [[NSBundle mainBundle] URLForResource:#"dots64" withExtension:#"gif"];
NSString *path=[[NSBundle mainBundle]pathForResource:#"bar180" ofType:#"gif"];
NSURL *url=[[NSURL alloc] initFileURLWithPath:path];
imageVw.image= [UIImage animatedImageWithAnimatedGIFURL:url];
Hope this is helpfull
This doesn't meet the requirement of using a UIImageView, but maybe this would simplify things for you. Have you considered using a UIWebView?
NSString *gifUrl = #"http://gifs.com";
NSURL *url = [NSURL URLWithString: gifUrl];
[webView loadRequest: [NSURLRequest requestWithURL:url]
If you want, instead of linking to a URL that requires Internet, you could import an HTML file into your Xcode project and set the root in the string.
Here is an interesting library: https://github.com/Flipboard/FLAnimatedImage
I tested the demo example and it's working great. It's a child of UIImageView. So I think you can use it in your Storyboard directly as well.
Cheers
I know that an answer has already been approved, but its hard not to try to share that I've created an embedded framework that adds Gif support to iOS that feels just like if you were using any other UIKit Framework class.
Here's an example:
UIGifImage *gif = [[UIGifImage alloc] initWithData:imageData];
anUiImageView.image = gif;
Download the latest release from https://github.com/ObjSal/UIGifImage/releases
-- Sal
If you must load the gif image from URL, you can always embed the gif in an image tag in a UIWebView.
SWIFT 3
Here is the update for those who need the Swift version!.
A few days ago i needed to do something like this. I load some data from a server according specific parameters and in the meanwhile i wanted to show a different gif image of "loading". I was looking for an option to do it with an UIImageView but unfortunately i didn't find something to do it without splitting the .gif images. So i decided to implement a solution using a UIWebView and i want to shared it:
extension UIView{
func animateWithGIF(name: String){
let htmlString: String = "<!DOCTYPE html><html><head><title></title></head>" +
"<body style=\"background-color: transparent;\">" +
"<img src=\""+name+"\" align=\"middle\" style=\"width:100%;height:100%;\">" +
"</body>" +
"</html>"
let path: NSString = Bundle.main.bundlePath as NSString
let baseURL: URL = URL(fileURLWithPath: path as String) // to load images just specifying its name without full path
let frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
let gifView = UIWebView(frame: frame)
gifView.isOpaque = false // The drawing system composites the view normally with other content.
gifView.backgroundColor = UIColor.clear
gifView.loadHTMLString(htmlString, baseURL: baseURL)
var s: [UIView] = self.subviews
for i in 0 ..< s.count {
if s[i].isKind(of: UIWebView.self) { s[i].removeFromSuperview() }
}
self.addSubview(gifView)
}
func animateWithGIF(url: String){
self.animateWithGIF(name: url)
}
}
I made an extension for UIView which adds a UIWebView as subview and displays the .gif images just passing its name.
Now in my UIViewController i have a UIView named 'loadingView' which is my 'loading' indicator and whenever i wanted to show the .gif image, i did something like this:
class ViewController: UIViewController {
#IBOutlet var loadingView: UIView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
configureLoadingView(name: "loading.gif")
}
override func viewDidLoad() {
super.viewDidLoad()
// .... some code
// show "loading" image
showLoadingView()
}
func showLoadingView(){
loadingView.isHidden = false
}
func hideLoadingView(){
loadingView.isHidden = true
}
func configureLoadingView(name: String){
loadingView.animateWithGIF(name: "name")// change the image
}
}
when I wanted to change the gif image, simply called the function configureLoadingView() with the name of my new .gif image and calling showLoadingView(), hideLoadingView() properly everything works fine!.
BUT...
... if you have the image splitted then you can animate it in a single line with a UIImage static method called UIImage.animatedImageNamed like this:
imageView.image = UIImage.animatedImageNamed("imageName", duration: 1.0)
From the docs:
This method loads a series of files by appending a series of numbers to the base file name provided in the name parameter. All images included in the animated image should share the same size and scale.
Or you can make it with the UIImage.animatedImageWithImages method like this:
let images: [UIImage] = [UIImage(named: "imageName1")!,
UIImage(named: "imageName2")!,
...,
UIImage(named: "imageNameN")!]
imageView.image = UIImage.animatedImage(with: images, duration: 1.0)
From the docs:
Creates and returns an animated image from an existing set of images. All images included in the animated image should share the same size and scale.
With Swift and KingFisher
lazy var animatedPart: AnimatedImageView = {
let img = AnimatedImageView()
if let src = Bundle.main.url(forResource: "xx", withExtension: "gif"){
img.kf.setImage(with: src)
}
return img
}()
You Can use https://github.com/Flipboard/FLAnimatedImage
#import "FLAnimatedImage.h"
NSData *dt=[NSData dataWithContentsOfFile:path];
imageView1 = [[FLAnimatedImageView alloc] init];
FLAnimatedImage *image1 = [FLAnimatedImage animatedImageWithGIFData:dt];
imageView1.animatedImage = image1;
imageView1.frame = CGRectMake(0, 5, 168, 80);
[self.view addSubview:imageView1];
Swift 3:
As suggested above I'm using FLAnimatedImage with an FLAnimatedImageView. And I'm loading the gif as a data set from xcassets. This allows me to provide different gifs for iphone and ipad for appearance and app slicing purposes. This is far more performant than anything else I've tried. It's also easy to pause using .stopAnimating().
if let asset = NSDataAsset(name: "animation") {
let gifData = asset.data
let gif = FLAnimatedImage(animatedGIFData: gifData)
imageView.animatedImage = gif
}

How to programmatically open an NSComboBox's list?

I've been around this for a while.. I thought this should be an easy task, but it isn't =D
What I am trying to do, is to display the combobox's list when the user clicks the combobox but not specifically in the button.
Any Idea?
Thanks in advance!
This answer fits the title of the question, but not question itself. Omer wanted to touch a text field and have the box popup.
This solution shows the popup when the user enters text.
I found this answer on cocoabuilder from Jens Alfke. I reposted his code here. Thanks Jens.
original cocoabuilder post: (http://www.cocoabuilder.com/archive/cocoa)
#interface NSComboBox (MYExpansionAPI)
#property (getter=isExpanded) BOOL expanded;
#end
#implementation NSComboBox (MYExpansionAPI)
- (BOOL) isExpanded
{
id ax = NSAccessibilityUnignoredDescendant(self);
return [[ax accessibilityAttributeValue:
NSAccessibilityExpandedAttribute] boolValue];
}
- (void) setExpanded: (BOOL)expanded
{
id ax = NSAccessibilityUnignoredDescendant(self);
[ax accessibilitySetValue: [NSNumber numberWithBool: expanded]
forAttribute: NSAccessibilityExpandedAttribute];
}
I used this code in my controlTextDidChange: method.
- (void) controlTextDidChange:(NSNotification *) aNotification {
NSTextField *textField = [aNotification object];
NSString *value = [textField stringValue];
NSComboBox *box = [self comboBox];
if (value == nil || [value length] == 0) {
if ([box isExpanded]) { [box setExpanded:NO]; }
} else {
if (![box isExpanded]) { [box setExpanded:YES]; }
}
}
Returns true if the NSComboBox's list is expanded
comboBox.cell?.isAccessibilityExpanded() ?? false
Open the NSComboBox's list
comboBox.cell?.setAccessibilityExpanded(true)
Close the NSComboBox's list
comboBox.cell?.setAccessibilityExpanded(false)
Ref. jmoody’s answer.
You can use the following code line:
[(NSComboBoxCell*)self.acomboBox.cell performSelector:#selector(popUp:)];
Put
comboBoxCell.performSelector(Selector("popUp:"))
Into
override func controlTextDidChange(obj: NSNotification) {}
is what I ended up with. Thanks #Ahmed Lotfy
Here's the full code, it works for me on OSX 10.11
override func controlTextDidChange(obj: NSNotification) {
if let comboBoxCell = self.comboBox.cell as? NSComboBoxCell {
comboBoxCell.performSelector(Selector("popUp:"))
}
}
Thanks to jmoody and Jens Alfke mentioned above. Here is a SWIFT translation of the above solution.
import Cocoa
class CComboBoxEx: NSComboBox {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
// Drawing code here.
}
func isExpanded() -> Bool{
if let ax:AnyObject? = NSAccessibilityUnignoredDescendant(self) {
if ax!.accessibilityAttributeValue(NSAccessibilityExpandedAttribute) != nil {
return true
}
}
return false
}
func setExpanded (bExpanded:Bool) {
if let ax:AnyObject? = NSAccessibilityUnignoredDescendant(self) {
ax!.accessibilitySetValue(NSNumber(bool: bExpanded), forAttribute: NSAccessibilityExpandedAttribute)
}
}
}
NSComboBox was not designed to work this way. Because the user may want to edit the text in the control, they'll need to be able to click it without unexpectedly popping up the choices.
You would need to subclass NSComboBoxCell and change this behavior ... but then you'd have a standard-looking control that does not behave in a standard way. If you're determined to do this, take a look at the open source version of NSComboBoxCell. The interesting methods appear to be -popUpForComboBoxCell: and friends.
Based on the other answers I wrote this solution (tested with Xcode 10.2.1, Swift 5). It uses the same ideas but it's a little shorter.
// Put this extension for NSComboBox somewhere in your project
import Cocoa
public extension NSComboBox {
var isExpanded: Bool{
set {
cell?.setAccessibilityExpanded(newValue)
}
get {
return cell?.isAccessibilityExpanded() ?? false
}
}
}
// Set your corresponding NSViewController as NSComboBoxDelegate
// in the storyboard and add this piece of code
// to expand the combobox when the user types
class MyViewController: NSViewController, NSComboBoxDelegate {
func controlTextDidChange(_ notification: Notification) {
guard let comboBox = notification.object as? NSComboBox else { return }
if comboBox.isExpanded == false {
comboBox.isExpanded = true
}
}
}

Wrap NSButton title

Any way to have a NSButton title to wrap when it's width is longer than the button width, instead of getting clipped?
I'm trying to have a radio button with a text that can be long and have multiple lines. One way I thought about having it work is to have an NSButton of type NSRadioButton but can't get multiple lines of text to work.
Maybe my best alternative is to have an NSButton followed by an NSTextView with the mouseDown delegate function on it triggering the NSButton state?
I don't believe you can. You'd have to subclass NSButtonCell to add support for this.
That said, it's typically a bad idea to have multiple lines of text on a button. A button label should concisely represent the action performed:
The label on a push button should be a verb or verb phrase that describes the action it performs—Save, Close, Print, Delete, Change Password, and so on. If a push button acts on a single setting, label the button as specifically as possible; “Choose Picture…,” for example, is more helpful than “Choose…” Because buttons initiate an immediate action, it shouldn’t be necessary to use “now” (Scan Now, for example) in the label.
What are you trying to do?
I`m incredibly late, but I still feel obliged to share what I`ve found.
Just add a newline character before and after the button title before you assign it to the actual button — and voilà! It now wraps automatically.
The downside of this approach is that, for reasons unknown to me, apps compiled on a certain version of OS X shift button titles one line down when run on newer versions.
Well here's my excuse for needing multiline buttons: I'm writing an emulator for an IBM 701, complete with front panel, and, bless their hearts, the designers of that front panel used multi-line labels. Here's my code. You only have to subclass NSButtonCell (not NSButton), and only one method needs to be overridden.
// In Xcode 4.6 (don't know about earlier versions): Place NSButton, then double-click it
// and change class NSButtonCell to ButtonMultiLineCell.
#interface ButtonMultiLineCell : NSButtonCell
#end
#implementation ButtonMultiLineCell
- (NSRect)drawTitle:(NSAttributedString *)title withFrame:(NSRect)frame inView:(NSView *)controlView
{
NSAttributedString *as = [[NSAttributedString alloc] initWithString:[title.string stringByReplacingOccurrencesOfString:#" " withString:#"\n"]];
NSFont *sysFont = [NSFont systemFontOfSize:10];
NSMutableParagraphStyle *paragraphStyle = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
[paragraphStyle setAlignment:NSCenterTextAlignment];
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
sysFont, NSFontAttributeName,
paragraphStyle, NSParagraphStyleAttributeName,
nil];
NSSize textSize = [as.string sizeWithAttributes:attributes];
NSRect textBounds = NSMakeRect(0, 0, textSize.width, textSize.height);
// using frame argument seems to produce text in wrong place
NSRect f = NSMakeRect(0, (controlView.frame.size.height - textSize.height) / 2, controlView.frame.size.width, textSize.height);
[as.string drawInRect:f withAttributes:attributes];
return textBounds; // not sure what rectangle to return or what is done with it
}
#end
Even later, but I also feel obliged to share. You can set the attributedTitle property of NSButton to achieve manual wrapping.
In my case, I wanted the button title to wrap if it was greater than 6 characters (Swift 3):
if button.title.characters.count > 6 {
var wrappedTitle = button.title
wrappedTitle.insert("\n", at: wrappedTitle.index(wrappedTitle.startIndex, offsetBy: 6))
let style = NSMutableParagraphStyle()
style.alignment = .center
let attributes = [NSFontAttributeName: NSFont.systemFont(ofSize: 19), NSParagraphStyleAttributeName: style] as [String : Any]
button.attributedTitle = NSAttributedString(string: wrappedTitle, attributes: attributes)
}
I'm with Sören; If you need a longer description, think about using a tool tip or placing descriptive text in a wrapped text field using the small system font below the radio choices if the descriptive text is only a few lines. Otherwise, you could provide more information in a help document.
Figuring out a way to say what you need to say in a concise way is your best bet, though.
As of today, I'm seeing this can be done simply with a property on the cell of NSButton:
myButton.cell?.wraps = true
I had the same problem and tried, with a sinking heart, the solutions in this post. (While I appreciate advice that one generally should keep button titles short, I'm writing a game, and I want multi-line answers to behave like buttons).
Sometimes, you don't get there from here. My ideal was an NSButton with a multi-line label, but since I can't get that without considerable hassle, I have created a PseudoButton: an NSControl subclass that behaves like a button. It has a hand cursor to indicate 'you can click here' and it gives feedback: when you click the mouse, it changes to selectedControlColor, when you release the mouse, it returns to normal. And unlike solutions that try to stack buttons and labels, there is no problem with having labels and images on top of the view: the whole of the view is the clickable area.
import Cocoa
#IBDesignable
class PseudoButton: NSControl {
#IBInspectable var backgroundColor: NSColor = NSColor.white{
didSet{
self.needsDisplay = true
}
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path = NSBezierPath(rect: dirtyRect)
backgroundColor.setFill()
path.fill()
NSColor.black.setStroke()
path.lineWidth = 2
path.stroke()
}
override func mouseDown(with event: NSEvent) {
self.backgroundColor = NSColor.selectedControlColor
}
override func mouseUp(with event: NSEvent) {
self.backgroundColor = NSColor.clear
guard let action = action else {return}
tryToPerform(action, with: self)
//#IBAction func pseudobuttonClicked(_ sender: PseudoButton) in the ViewController class
}
override func resetCursorRects() {
addCursorRect(bounds, cursor: .pointingHand)
}
}
You use this like any other control in the storyboard: drag a Pseudobutton in, decorate it at will, and connect it to an appropriate IBAction in your viewController class.
I like this better than meddling with NSCell. (On past experience, NSCell-based hacks are more likely to break).
A little bit late here, here's my code to insert new line in title:
private func calculateMultipleLineTitle(_ title: String) -> String {
guard !title.isEmpty else { return title }
guard let cell = cell as? NSButtonCell else { return title }
let titleRect = cell.titleRect(forBounds: bounds)
let attr = attributedTitle.attributes(at: 0, effectiveRange: nil)
let indent = (attr[.paragraphStyle] as? NSMutableParagraphStyle)?.firstLineHeadIndent ?? 0
let titleTokenArray = title.components(separatedBy: " ") // word wrap break mode
guard !titleTokenArray.isEmpty else { return title }
var multipleLineTitle = titleTokenArray[0]
var multipleLineAttrTitle = NSMutableAttributedString(string: multipleLineTitle, attributes: attr)
var index = 1
while index < titleTokenArray.count {
multipleLineAttrTitle = NSMutableAttributedString(
string: multipleLineTitle + " " + titleTokenArray[index],
attributes: attr
)
if titleRect.minX+indent+multipleLineAttrTitle.size().width > bounds.width {
multipleLineTitle += " \n" + titleTokenArray[index]
} else {
multipleLineTitle += " " + titleTokenArray[index]
}
index += 1
}
return multipleLineTitle
}
Just pass the original title as parameter, it will return multiple line title.
I added an "\n" at the end of the title and I am setting the title using the NSAttributedString. this fixed the problem for me.
I am on MacOS Big Sur 11.7.2, Xcode 13.12.1
private NSAttributedString GetAttributedString(string text)
{
var paragraph = new NSMutableParagraphStyle();
paragraph.Alignment = NSTextAlignment.Center;
paragraph.LineBreakMode = NSLineBreakMode.ByWordWrapping;
var attrString = new NSAttributedString
(
text + "\n",
font: NSFont.FromFontName("Arial", 50.0f),
foregroundColor: NSColor.White,
backgroundColor: NSColor.FromCalibratedRgba(0, 0, 0, 0.0f),
paragraphStyle: paragraph
);
return attrString;
}
textButton.AttributedTitle = GetAttributedString("some text");