Create UITextRange from NSRange - objective-c

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

Related

Has anyone found **legal** overrides to customize drawing of NSTabView?

BGHUDAppKit BGHUDTabView _drawThemeTab private API override now broken
For years, I have been using code originally based off of BGHUDAppKit, and found replacements for all of the private API that BGHUDAppKit overrides.
Except for one that I could not find a way to replace...
-[NSTabView _drawThemeTab:withState:inRect:]
(Note: I also use venerable PSMTabBarControl in many circumstances, so if all else fails I'll convert all my tab views to PSMTabBarControl)
Apple has now added the dark NSAppearance in 10.14 Mojave (so in ~10 years I can use it once we stop supporting High Sierra).
Whichever selfish dev at Apple writes NSTabView does not believe in making his view customizable, unlike all of the other NSControls which are customizable.
Here is part of the hackish overrides for custom drawing of NSTabView:
// until we can eliminate private API _drawThemeTab:, return nil for new NSAppearance
- (id) appearance { return nil; }
- (id) effectiveAppearance { return nil; }
-(void)_drawThemeTab:(id) tabItem withState:(NSUInteger) state inRect:(NSRect) aRect {
NSInteger idx = [self indexOfTabViewItem: tabItem];
int gradientAngle = 90;
NSBezierPath *path = nil;
aRect = NSInsetRect(aRect, 0.5f, 0.5f);
if([self tabViewType] == NSLeftTabsBezelBorder) {
gradientAngle = 0;
} else if([self tabViewType] == NSRightTabsBezelBorder) {
gradientAngle = 180;
}
NSColor *specialFillColor = [tabItem color];
NSColor *outlineColor = nil;
NSString *name = [specialFillColor description];
// MEC - added new prefix 12/15/17 to fix white border around last segment in High Sierra
if ( [name hasPrefix:#"NSNamedColorSpace System"] || [name hasPrefix:#"Catalog color: System controlColor"])
specialFillColor = nil;
else if ( [name isEqualToString: #"NSCalibratedWhiteColorSpace 0 1"] )
[specialFillColor set];
else
{
outlineColor = specialFillColor;
specialFillColor = nil;
}
... etc ...
It's probably preferrable to completely disable NSTabView's drawing (setting its tabViewType to NSNoTabsNoBorder), and create a custom segmented bar view to draw the selection separately (as a sibling view). This allows you to completely control the appearance, layout, and sizing of that custom implementation rather than relying on any details of NSTabView.
Looking at the view hierarchy of an NSTabViewController, you can see that it has this same approach by using an NSSegmentedControl as a separate subview managing selection from the NSTabView.

Truncate a string

I have a NSTableView that shows the path of files in one column. When the user resizes the tableview I want the pathname (e.g. /Users/name/testfile.m) to be resized, but I want the end of the pathname (e.g. ...name/testfile.m) to be visible and not the start (e.g. /Users/test/te...) of the path as happens by default. I wrote a function that successfully does what I want to do, but the tableview flickers while redrawing as the user scales the tableview. I think there must be a better, more elegant algorithm for doing this, but I have looked into the documentation for NSString and on Stackoverflow and I cant find anything that gives a better solution. If anyone has a more elegant solution to this problem that would be greatly appreciated. Thanks! Cheers, Trond
My current function:
-(NSString *) truncateString:(NSString *) myString withFontSize:(int) myFontSize withMaxWidth:(NSInteger) maxWidth
{
// Get the width of the current string for a given font
NSFont *font = [NSFont systemFontOfSize:myFontSize];
CGSize textSize = NSSizeToCGSize([myString sizeWithAttributes:[NSDictionary dictionaryWithObject:font forKey: NSFontAttributeName]]);
NSInteger lenURL =(int)textSize.width;
// Prepare for new truncated string
NSString *myStringShort;
NSMutableString *truncatedString = [[myString mutableCopy] autorelease];
// If the available width is smaller than the string, start truncating from first character
if (lenURL > maxWidth)
{
// Get range for first character in string
NSRange range = {0, 1};
while ([truncatedString sizeWithAttributes:[NSDictionary dictionaryWithObject:font forKey: NSFontAttributeName]].width > MAX(TKstringPad,maxWidth))
{
// Delete character at start of string
[truncatedString deleteCharactersInRange:range];
}
myStringShort = [NSString stringWithFormat:#"...%#",truncatedString];
}
else
{
myStringShort=myString;
}
return myStringShort;
}
The typical approach would be simply:
[tableViewCell setLineBreakMode:NSLineBreakByTruncatingHead];
As Dondragmer noted, this property may also be set in Xcode's NIB editor.

Does iPhone SDK Objective C support functions inside of functions?

I know that javascript, for example supports functions inside of functions, like so:
function doSomething(){
function doAnothingThing(){
//this function is redefined every time doSomething() is called and only exists inside doSomething()
}
//you can also stick it inside of conditions
if(yes){
function doSomethingElse(){
//this function only exists if yes is true
}
}
}
Does objective-c support this? Theoretical example:
-(void) doSomething:(id) sender{
-(void) respondToEvent: (id) sender{
//theoretically? ... please?
}
}
BONUS: What is the proper term for a "local" function?
A bit late, but you can place an inline block into a function, which kind of acts like your nested function questions.
-(int)addNumbers:(int)a withB:(int)b withC:(int)c {
// inline block
int(^inlineaddNumbers)(int, int) = ^(int a, int b) {
return a + b;
};
if( a == 0 ) return inlineaddNumbers(b,c);
else return inlineaddNumbers(a,c);
}
It's a bit messy, but it works!
The usual term is nested function. gcc supports nested functions as an extension to C (disabled by default). I don't think this option is available with Objective-C (or C++) with gcc though, and even if it were it's probably not a good idea to use it (portability etc).
gcc.gnu.org/onlinedocs/gcc/Nested-Functions.html
By default Xcode disallows nested functions.
If you want to switch them on, open up the Info for your project, go to the Build tab, and set "Other C flags" (under the section titled "GCC 4.2 - Language") to "-fnested-functions".
(This is stored in your project.pbxproj file as "OTHER_CFLAGS = "-fnested-functions";"
Expanding the answer provided by Gui13 a little bit, with object parameters.
The following code snippet demonstrates how to draw an 11x5 set of UILabels.
// inline block - to be called as a private function
UILabel *(^createLabel)(CGRect, NSString *, UIColor *) = ^UILabel *(CGRect rect, NSString *txt, UIColor *color) {
UILabel *lbl = [UILabel new];
lbl.frame = rect;
lbl.textColor = color;
lbl.backgroundColor = [UIColor whiteColor];
lbl.font = [UIFont systemFontOfSize:30.f];
lbl.textAlignment = NSTextAlignmentCenter;
lbl.text = txt;
return lbl;
};
// loop to create 11 rows of 5 columns over the whole screen
float w = CGRectGetWidth([UIScreen mainScreen].bounds);
float h = CGRectGetHeight([UIScreen mainScreen].bounds);
float top = h / 10; //start at 10% from top
float vOffset = h / 13; //space between rows: 7.6% of screen height
NSArray *xFrom, *xTo; //columns to start at 5%, 20%, 40%, 60%, 80%
xFrom = #[#(1.f/20), #(1.f/5), #(2.f/5), #(3.f/5), #(4.f/5)];
xTo = #[#(1.f/5-1.f/16), #(2.f/5-1.f/16), #(3.f/5-1.f/16), #(4.f/5-1.f/16), #(19.f/20)];
#define SFMT(format...) [NSString stringWithFormat:format]
for (int row=0; row<11; row++) {
for (int col=0; col<5; col++) {
CGRect rect = CGRectMake([xFrom[col] floatValue]*w, top+row*vOffset, [xTo[col] floatValue]*w-[xFrom[col] floatValue]*w, vOffset*0.9);
UILabel *lbl = createLabel(rect, SFMT(#"%i-%i", row, col), [UIColor blueColor]);
[<my-view> addSubview:lbl];
}
}
Here is the output for this code:
#Moshe You cannot actually provide the nested functions inside the Objective C. Instead you can use the feature in latest Swift 3 which enables this feature. It will be like below:
func someFunction(input:String)->String
{
var inputString = input;
func complexFunctionOnString()
{
inputString = "Hello" + input;
}
complexFunctionOnString();
return inputString;
}
someFunction("User");

Objective-C subclass initWithSuperClass

The common Superclass of Rectangle and Circle is Shape.
If I initialize some shapes, what is a good way of converting the shape into a circle later and keeping the same properties set while it was a shape? Should I implement a initWithShape in the subclasses that looks something like this?
- (id) initWithShape:(Shape*)aShape {
self = (id) aShape;
// set circle or rectangle specific values
return self;
}
Does anyone have an example that I can look at?
Don't do what you just did. Think about what happens when you do this:
Shape *shape = ...;
Rectangle *rect = [[Rectangle alloc] initWithShape:shape];
In the second line, an instance of Rectangle gets allocated. Then, the return value for initWithShape is just shape again, so the new Rectangle that we just allocated has been leaked!
The cast to id is also unnecessary—any Objective-C object can be implicitly cast to id.
I'm not entirely clear on what you're trying to do. Perhaps if you clarified your question, I could tell you what you should be doing.
You cannot change an object after it has been created, except by freeing it and creating a new one (which you can do in an init method, and is in fact quite often done for singletons or class clusters), but that is not really what you're after.
Give an existing Shape object, with some properties, your only real option is to create a new object based on the shape properties. Something like:
In Shape.m:
- (id) initWithShape:(Shape*)aShape {
self = [super init];
if (self != nil) {
// copy general properties
_x = aShape.x;
_y = aShape.y;
_color = [aShape.color copy];
}
return self;
}
In Circle.m:
- (id) initWithShape:(Shape*)aShape {
self = [super initWithShale:aShape];
if (self != nil) {
// base properties on the class of the shape
if ( [aShape isKindOfClass:[Oval class]] ) {
// average the short and long diameter to a radius
_radius = ([(Oval*)aShape shortDiameter] + [(Oval*)aShape longDiameter])/4;
} else {
// or use generic Shape methods
_radius = aShape.size / 2;
}
}
return self;
}
If you have a reference to a Shape, and it might be a Rectangle or Pentagram or whatever, and you want to 'convert' to a circle (I guess you mean a circle with the same bounding box?), you have to create a new object. You can't change the class of an object after it's been created (except through very nasty low-level hacks.)
So yes, you would create an -initWithShape: method in class Circle. But the method would look like a normal init method, setting up the instance variables of the new Circle object ('self'). It would access properties of the given Shape, like its position and size, and set up the new object accordingly.
Why not implement a method in your shapes to take properties from other shapes rather than trying to replace the instance of the object altogether. It's probably safer.
// for rectangle
- (void) takePropertiesFrom:(Shape *) aShape
{
if ([aShape isKindOfClass:[Circle class]])
{
float radius = [aShape radius];
[self setWidth:radius * 2];
[self setHeight:radius * 2];
}
else
{
[self setWidth:[aShape width]];
[self setHeight:[aShape height]];
}
}
// for circle
- (void) takePropertiesFrom:(Shape *) aShape
{
if ([aShape isKindOfClass:[Rectangle class]])
[self setRadius:[aShape width] / 2];
else
[self setRadius:[aShape radius]];
}
Obviously you would want to set up a public interface for Shape that exposes the basic properties of a shape, such as height and width, and then you won't need to hard-code property stealing based on class type.

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");