Creating Font Descriptors with Fractions in Objective C - objective-c

I am having trouble displaying fractions in Objective C, even though the equivalent code works in Swift. I must be missing something very obvious??
Swift Code:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//: Playground - noun: a place where people can play
let pointSize = self.label.font.pointSize
let systemFontDesc = UIFont.systemFont(ofSize: pointSize, weight: UIFontWeightLight).fontDescriptor
let fractionFontDesc = systemFontDesc.addingAttributes(
[
UIFontDescriptorFeatureSettingsAttribute: [
[
UIFontFeatureTypeIdentifierKey: kFractionsType,
UIFontFeatureSelectorIdentifierKey: kDiagonalFractionsSelector,
], ]
] )
print(fractionFontDesc)
self.label.font = UIFont(descriptor: fractionFontDesc, size:pointSize)
print("label.font.descriptor: \(self.label.font.fontDescriptor)")
}
Result:
equivalent code in Objective C
-(void) viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CGFloat pointSize = _label.font.pointSize;
UIFontDescriptor *systemFontDesc = [UIFont systemFontOfSize:pointSize weight:UIFontWeightLight].fontDescriptor;
UIFontDescriptor *fractionDescriptor = [systemFontDesc fontDescriptorByAddingAttributes:#{ UIFontDescriptorFeatureSettingsAttribute : #{
UIFontFeatureTypeIdentifierKey: #(11), // kFractionsType
UIFontFeatureSelectorIdentifierKey: #(2)}}]; // kDiagonalFractionsSelector
NSLog(#"%#\n\n", fractionDescriptor);
UIFont *fracFont = [UIFont fontWithDescriptor:fractionDescriptor size:pointSize];
NSLog(#"fracFont.fontDescriptor: %#\n\n", fracFont.fontDescriptor);
[_label setFont: fracFont];
NSLog(#"label.font.descriptor: %#\n\n", _label.font.fontDescriptor);
}
Result:

The problem is the expression
fontDescriptorByAddingAttributes:#{
UIFontDescriptorFeatureSettingsAttribute : #{
That last #{ indicates that your UIFontDescriptorFeatureSettingsAttribute is a dictionary. That is wrong. It needs to be an array of dictionaries. (Look carefully at your original Swift code and you will see that this is so.)
You would do much better, in my opinion, to form your dictionary in one line, make an array of it in another line, and call fontDescriptorByAddingAttributes in a third line. That way you'll be clear on what you're doing. Right now you're just confusing the heck out of yourself with all those nested literals...

Related

again convert textKit from Objective-C to Swift

The following code was used in my app to change the state for text in a textview with strikeThrough. Now i wrote a small sample-app, in Objective-C and Swift. Again the result is frustrating as u can see in the screenshots. Any help is welcome so much.
I just use a TextView and try to show some text with StrikeThrough-Layout (other styles like Bold, Italic, Underline... have the same result)
First objc, that is ok, although the font-size of the striked part is very small
and now with Swith. The font is small as with objc, but there is no strikethrough :-)
And now again (dont know another way) the test-code:
objc Part 1: set the Font for a Range and call makeStrikeThrough()
- (void) setFont
{
NSRange range = NSMakeRange(11, 24);
[self makeStrikeThrough:range];
}
same in swift:
func setFont() {
let range = NSMakeRange(11, 24)
self.makeStrikeThrough(range)
}
objc Part 2: the strikeThrough
- (void) makeStrikeThrough:(NSRange)selectedRange
{
NSMutableDictionary *dict = [self getDict:selectedRange];
[_textView.textStorage beginEditing];
[_textView.textStorage setAttributes:dict range:selectedRange];
[_textView.textStorage endEditing];
}
and in Swift:
func makeStrikeThrough(selectedRange: NSRange) {
let dict = self.getDict(selectedRange)
self.textView.textStorage.beginEditing()
textView.textStorage.setAttributes([String() : dict], range: selectedRange)
self.textView.textStorage.endEditing()
}
objc Part 3: the help-method getDict() to buid a dictionary with the StrikeThrough
- (NSMutableDictionary*) getDict:(NSRange)range
{
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:[NSNumber numberWithInt:2] forKey:NSStrikethroughStyleAttributeName];
return dict;
}
and again in Swift
func getDict(range: NSRange) -> NSMutableDictionary {
let dict = NSMutableDictionary()
dict[NSStrikethroughStyleAttributeName] = NSNumber(integer: NSUnderlineStyle.StyleDouble.rawValue)
return dict
}
I´ve tried to reduce the problem to the root. Perhaps u vote me down ;-)
But i need a solution...
Or should i use AttributedStrings?
The procts to download
objc
swift
It boils down to one line of code in your Swift function:
func makeStrikeThrough(selectedRange: NSRange) {
let dict = self.getDict(selectedRange)
self.textView.textStorage.beginEditing()
textView.textStorage.setAttributes([String() : dict], range: selectedRange) // error
self.textView.textStorage.endEditing()
}
it should have been just dict:
func makeStrikeThrough(selectedRange: NSRange) {
let dict = self.getDict(selectedRange)
self.textView.textStorage.beginEditing()
textView.textStorage.setAttributes(dict, range: selectedRange)
self.textView.textStorage.endEditing()
}
And you need to change your getDict function too:
func getDict() -> [String: AnyObject] {
return [NSStrikethroughStyleAttributeName: 2]
}

Touch Slide over UIButton issue

Can someone tell me if I have translated the first 2 lines correctly to Swift and if the first part is correctly? Also, could anyone help me figure out the rest. I can't figure out how to translate the if statement at the bottom..
[C addTarget:self action:#selector(outsideOfKey: forEvent:) forControlEvents:UIControlEventTouchDragOutside|UIControlEventTouchDragInside];
[C addTarget:self action:#selector(keyGetsLeft: forEvent:) forControlEvents:UIControlEventTouchUpOutside | UIControlEventTouchUpInside];
-(void) outsideOfKey:(id)sender forEvent:(UIEvent *)event
{
for(UITouch *t in [event allTouches])
{
CGPoint touchPoint = [t locationInView:window];
if(CGRectContainsPoint(C.frame, touchPoint))
{
C.highlighted = YES;
}
else{
C.highlighted = NO;
}
Translated to swift
C.addTarget(self, action:Selector("outsideOfKey:forEvent:"), forControlEvents:.TouchDragOutside)
C.addTarget(self, action:Selector("outsideOfKey:forEvent:"), forControlEvents:.TouchDragInside)
C.addTarget(self, action:Selector("keyGetsLeft:forEvent:"), forControlEvents:.TouchUpOutside)
C.addTarget(self, action:Selector("keyGetsLeft:forEvent:"), forControlEvents:.TouchUpInside)
func outsideOfKey (sender: AnyObject, forEvent: UIEvent) {
let touch = event.allTouches() as? UITouch
for touch
{
var touchPoint : CGPoint = touch.locationInView(window)
if(CGRectContainsPoint(C.frame, touchPoint))
{
C.highlighted = YES;
}
else{
C.highlighted = NO;
}
}
Try something like:
C.addTarget(self, action:Selector("outsideOfKey:forEvent:"), forControlEvents:.TouchDragOutside | .TouchDragInside)
C.addTarget(self, action:Selector("keyGetsLeft:forEvent:"), forControlEvents:.TouchUpOutside | .TouchUpInside)
func outsideOfKey(sender: AnyObject, forEvent event: UIEvent) {
if let touches = event.allTouches()?.allObjects as? [UITouch] {
for touch in touches {
var touchPoint : CGPoint = touch.locationInView(window)
if CGRectContainsPoint(C.frame, touchPoint) == true {
C.highlighted = true;
} else {
C.highlighted = false;
}
}
}
}
Points:
You can still use the "|" operator on enums in swift (most of the time)
You don't put parentheses around the conditional clause in swift
YES and NO are not valid in swift, you much use true and false
Swift does not support sets so it is easiest to get the touches as an Array so you can iterate using a simple swift for...in loop
The "if let ..." pattern is called optional binding, if the right hand side is not nil "touches" will be set to the value and the code in the following braces will be executed, otherwise it will skip the block of code
The way "?" is used after ".allTouches()" is called optional chaining, if .allTouches() returns nil, the whole expression will return nil
Hope this is of some use to you, let me know if you have any more swift queries!

Append to NSTextView and scroll

OK, what I need should have been very simple. However, I've looked everywhere and I'm not sure I've found something that works 100% (and it's something that has troubled me in the past too).
So, here we are :
I want to be able to append to an NSTextView
After appending, the NSTextView should scroll down (so that that latest appended contents are visible)
Rather straightforward, huh?
So... any ideas? (A code example that performs exactly this simple "trick" would be more than ideal...)
After cross-referencing several answers and sources (with some tweaks), here's the answer that does work (given _myTextView is an NSTextView outlet) :
- (void)appendToMyTextView:(NSString*)text
{
dispatch_async(dispatch_get_main_queue(), ^{
NSAttributedString* attr = [[NSAttributedString alloc] initWithString:text];
[[_myTextView textStorage] appendAttributedString:attr];
[_myTextView scrollRangeToVisible:NSMakeRange([[_myTextView string] length], 0)];
});
}
The appendAttributedString and scrollToEndOfDocument are available starting in OS X 10.0 and 10.6 respectively
extension NSTextView {
func append(string: String) {
self.textStorage?.appendAttributedString(NSAttributedString(string: string))
self.scrollToEndOfDocument(nil)
}
}
Simply use this way :
for (NSInteger i=1; i<=100; i++) {
[self.textView setString:[NSString stringWithFormat:#"%#\n%#",[self.textView string],#(i)]];
}
[self.textView scrollRangeToVisible:NSMakeRange([[self.textView string] length], 0)];
Here's a Swift version of Anoop Vaidya's answer
extension NSTextView {
func append(string: String) {
let oldString = self.string == nil ? "" : self.string!
let newString = NSString(format: "%#%#", oldString, string)
self.string = newString
}
}
Here's a Swiftier solution:
extension NSTextView {
func appendString(string:String) {
self.string! += string
self.scrollRangeToVisible(NSRange(location:countElements(self.string!), length: 0))
}
}

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

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