I'm trying to create a Swift custom text editor (lite version) and got stuck on iOS 8 fontDescriptorWithSymbolicTraits bug, returning nil. Are there any Swift workarounds?
I've created a small project sample here , you can download and run the following scenario:
Select a text
Click on "B" button
Click on "I" button
The app crashes, fontDescriptorWithSymbolicTraits returns nil :(
private func addOrRemoveTraitWithName(traitName: String, traitValue: UInt32) {
let range = self.textEditor.selectedRange
let currentAttributesDict = self.textEditor.textStorage.attributesAtIndex(range.location, effectiveRange: nil)
let currentFont = currentAttributesDict[NSFontAttributeName]
let fontDescriptor = currentFont?.fontDescriptor()
let fontNameAttribute = fontDescriptor?.fontAttributes()[UIFontDescriptorNameAttribute]
var changedFontDescriptor: UIFontDescriptor?
if fontNameAttribute?.rangeOfString(traitName).location == NSNotFound {
let existingTraitsWithNewTrait = UIFontDescriptorSymbolicTraits(rawValue: fontDescriptor!.symbolicTraits.rawValue | traitValue)
changedFontDescriptor = fontDescriptor?.fontDescriptorWithSymbolicTraits(existingTraitsWithNewTrait)
} else {
let existingTraitsWithoutTrait = UIFontDescriptorSymbolicTraits(rawValue: fontDescriptor!.symbolicTraits.rawValue & ~traitValue)
changedFontDescriptor = fontDescriptor?.fontDescriptorWithSymbolicTraits(existingTraitsWithoutTrait)
}
let updatedFont = UIFont(descriptor: changedFontDescriptor!, size: 0.0)
var dict = [String : AnyObject]()
dict[NSFontAttributeName] = updatedFont
self.textEditor.textStorage.beginEditing()
self.textEditor.textStorage.setAttributes(dict, range: range)
self.textEditor.textStorage.endEditing()
}
I highly recommend to take a quick look at sample project
What options do I have?
I also seen that on Apple documentation the method that was used by other is missing from the SDK & documentation
What others say about this:
Font Descriptor returns nil in iOS 8
http://www.openradar.me/19922049
You're right about this. The bug was fixed in iOS 8.3, fortunately. The only reliable workaround is to drop down to the level of Core Text and perform the trait application there. It's a pain, but it solves the problem.
Thus, for instance, before iOS 8.3, this doesn't work:
if let body = UIFont(name: "GillSans", size: 15),
emphasis = body.fontDescriptor().fontDescriptorWithSymbolicTraits(.TraitItalic) {
fbody = body
femphasis = UIFont(descriptor: emphasis, size: 0)
}
So you have to import CoreText and drop down to that level:
if let body = UIFont(name: "GillSans", size: 15),
result = CTFontCreateCopyWithSymbolicTraits(body as CTFont, 0, nil, .ItalicTrait, .ItalicTrait) {
fbody = body
femphasis = result as UIFont
}
Fortunately, Swift 1.2 and later is aware that UIFont and CTFont are now toll-free bridged. Before Swift 1.2, this was even more complicated! And of course in earlier systems, they were not toll-free bridged and this was still more difficult.
Related
I was reading an article that talks about Core Services and decided to use one of them. I import Core Services and add the library on XCode, but it doesn't work.
Sample code:
import Foundation
import CoreServices.DictionaryServices
func define(_ word: String) -> String? {
let nsstring = word as NSString
let cfrange = CFRange(location: 0, length: nsstring.length)
guard let definition = DCSCopyTextDefinition(nil, nsstring, cfrange) else {
return nil
}
return String(definition.takeUnretainedValue())
}
define("apple") // "apple | ˈapəl | noun 1 the round fruit of a tree..."
It gives a "no such module" error when I import it. I also look at Apple's documentation but there is no explanation for how to implement and use it.
The Solution:
Thanks to people who answered below my question, I made a little research and found the solution. Apparently, I can use the iOS dictionary by calling the UIReferenceLibraryViewController, but the Apple documentation says that we should not use this making for a dictionary app. It is obvious that it is not sufficient to make a dictionary app because it uses its own ViewController and is not customizable.
Here is the sample working code:
let dic = UIReferenceLibraryViewController(term: textLabelOutlet.text as! String)
dic.modalPresentationStyle = .popover // add this
let popover = dic.popoverPresentationController
popover?.sourceView = view
popover?.sourceRect = CGRect(x: 32, y: 32, width: 64, height: 64)
present(dic, animated: true)
Source: UIReferenceLibraryViewController cannot be presented as popup (always covers full screen)
CoreServices may be available on all platforms but DictionaryServices seems to be available on macOS only.
https://developer.apple.com/documentation/coreservices/1446842-dcscopytextdefinition
SDK lists only: macOS 10.5+
CoreServices.DictionaryServices is a framework of OS X, not of iOS.
you could click CoreServices.DictionaryServices, and see the page in a mac app project.
/*
DictionaryServices.h
DictionaryServices framework
*/
/*!
#typedef DCSDictionaryRef
#abstract Opaque CF object that represents a dictionary file
*/
public class DCSDictionary {
}
#available(OSX 10.5, *)
public func DCSGetTermRangeInString(_ dictionary: DCSDictionary?, _ textString: CFString, _ offset: CFIndex) -> CFRange
So I am using the CZWeatherKit library to grab weather data from forecast.io.
When I get results, it sends a climacon UInt8 char, which should match to an icon if the climacon font is installed. I did that but it only shows the char, not the actual icon. Here is the code, it prints a quote i.e. " which is the correct mapping to ClimaconCloudSun, but the icon doesn't show. I followed these instructions to install the climacons.ttf font
request.sendWithCompletion { (data, error) -> Void in
if let error = error {
print(error)
} else if let weather = data {
let forecast = weather.dailyForecasts.first as! CZWeatherForecastCondition
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// I get back good results, this part works
let avgTempFloat = (forecast.highTemperature.f + forecast.lowTemperature.f) / 2
let avgTemp = NSDecimalNumber(float: avgTempFloat).decimalNumberByRoundingAccordingToBehavior(rounder)
self.temperatureLabel.text = String(avgTemp)
self.weatherLabel.text = forecast.summary
// this part does not work, it has the right char, but does not display icon
// I tried setting self.climaconLabel.font = UIFont(name: "Climacons-Font", size: 30) both in IB and programmatically
let climaChar = forecast.climacon.rawValue
let climaString = NSString(format: "%c", climaChar)
self.climaconLabel.text = String(climaString)
})
}
}
I solved the exact same issue, the problem was the font file. Replace your current font with the one provided here: https://github.com/comyar/Sol/blob/master/Sol/Sol/Resources/Fonts/Climacons.ttf
You've probably moved on from this problem by now, but I'll leave this here for future use.
You need to call setNeedsLayout on the label after you change the title text to the desired value, and the label will change to the corresponding icon.
This question already has answers here:
Download font .ttf file from web and store on iPhone
(5 answers)
Closed 7 years ago.
I have searched here for the answer to this but I haven't found any posts that I can use to get an answer. I have only found answers about pre-installing the font to the plist of the app which won't work for me.
What I am trying to do is download a Font from a URL, store the file locally to the app and then install or use that font in controls.
I am at the point of installing the font to the tablet to use. I can add it to CTFontManager without any issues using the code below.
private func InstallFont(dest:String)
{
let fontData = NSData(contentsOfFile: dest)!
let dataProvider = CGDataProviderCreateWithCFData(fontData)
let cgFont = CGFontCreateWithDataProvider(dataProvider)!
var error: Unmanaged<CFError>?
if !CTFontManagerRegisterGraphicsFont(cgFont, &error)
{
print("Error loading Font!")
}
}
The issue that I am facing is that I cannot seem to find the font I have installed anywhere in the app, I have tried to set it like below but just goes to default which tells me it can not be found.
self.font = UIFont(name: "Font Name", size: 20.0)
I have also looped through the font family's to see if it has been installed and no success there either.
for x in UIFont.familyNames()
{
print(x)
for z in UIFont.fontNamesForFamilyName(x)
{
print("== \(z)")
}
}
Is what I am trying to achieve even possible? If so what am I doing wrong?
It is possible. I created an example project in github. You have to just add the line below.
if !CTFontManagerRegisterGraphicsFont(cgFont, &error)
{
print("Error loading Font!")
}
else
{
let fontName = CGFontCopyPostScriptName(cgFont)
uiFont = UIFont(name: String(fontName) , size: 30)
}
Thank you for Michael Dautermann, he was right.
Github project link
According to the answers in this related question, you should be able to download your font and register it the way you are already doing (which it sounds like you are doing correctly), but you simply need to get the Postscript name and use that to create a corresponding UIFont:
var uiFont : UIFont?
let fontData = NSData(contentsOfFile: dest)!
let dataProvider = CGDataProviderCreateWithCFData(fontData)
let cgFont = CGFontCreateWithDataProvider(dataProvider)!
var error: Unmanaged<CFError>?
if !CTFontManagerRegisterGraphicsFont(cgFont, &error)
{
print("Error loading Font!")
} else {
let fontName = CGFontCopyPostScriptName(fontRef)
uiFont = UIFont(name: fontName, size: 30)
}
I have a Swift-based application that currently implements the Tesseract OCR framework (similar to the form in this tutorial: http://www.raywenderlich.com/93276/implementing-tesseract-ocr-ios). So upon taking a picture and employing Tesseract, I obtain the following output in a UITextField object:
Subtotal 155.60
Tax 14.02
Total 169.82
So now I would like to separate the text from the numbers in the UITextField. I was considering using the "contain" function built into Swift on a matrix containing all values in price format ([0.01 0.02, etc.]) but this will only return a boolean as outlined in this post (How to have a textfield scan for all values in an array individually in swift?). Does anyone have any suggestions on how to do this? Cheers!
Tesseract Implementation
func performImageRecognition(image: UIImage)
// 0
// 1
let tesseract = G8Tesseract()
// 2
tesseract.language = "eng"
// 3
tesseract.engineMode = .TesseractCubeCombined
// 4
tesseract.pageSegmentationMode = .Auto
// 5
tesseract.maximumRecognitionTime = 60.0
// 6
tesseract.image = image.g8_blackAndWhite()
tesseract.recognize()
// 7
textView.text = tesseract.recognizedText
textView.editable = true
Sounds like you might want to look into using Regular Expressions
func seperate (text: String) -> (text: String?, value: String?) {
// You might want to do an extra check here to ensure the whole string is valid
// i.e., nothing in between the two parts of the string
let textMatch = text.rangeOfString("^([A-Z]|[a-z])+", options: .RegularExpressionSearch)
let priceMatch = text.rangeOfString("[0-9]*.[0-9]{2}$", options: .RegularExpressionSearch)
// You might want to adjust regex to handle price edge cases, such as 15 (rather than 15.00) etc
if let textMatch = textMatch, priceMatch = priceMatch {
let textValue = text.substringWithRange(textMatch)
let priceValue = text.substringWithRange(priceMatch)
return(textValue, priceValue)
} else {
return (nil, nil)
}
}
seperate("Subtotal 155.60") // -> Subtotal, 155.60
I am reading strings out of a Localizable.strings which contains something like that which is basically what you have in an strings.xml of an Android app
"testShort" = "A <b>short</b>\ntest with another<b>bold text</b>";
The bold and and line feed are the only two formatting attributes I have in my texts. I am trying to develop a method like this for days now without success:
func ConvertText(inputText: String) -> NSAttributedString {
// here comes the conversion to a representation with Helvetica 14pt and Helvetica-Bold 14pt including line feeds.
}
My final goal is to display the text in an UITextView's attributedText.
Being kinda new to Swift and iOS without knowing Objective-C I found its very difficult to do String manipulations as they are quite different and complex and all examples are in Objective-C. What makes it even harder is that most API methods are not available or different in Swift than in Objective-C...
Here is what I tried so far for the method body with the help of a lot of other posts here:
var test = inputText.dataUsingEncoding(NSUnicodeStringEncoding, allowLossyConversion: true)!
attrStr = NSAttributedString(
data: test,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil,
error: nil)!
return attrStr
The main issues here are that \n isn't converted and the font is very small and different.
Next I tried to manually bold a part of a text. It seem to work like that:
var newText = NSMutableAttributedString(string: inputText)
newText.addAttribute(NSFontAttributeName, value: UIFont(name: "Helvetica-Bold", size: 14.0)!, range: NSRange(location:2,length:4))
Now I tried to search for the attributes in the text, deleting them and use the addAttribute kinda like that
// Start of bold text
var range = newText.rangeOfString("<b>")!
// End of bold text
var range2 = newText.rangeOfString("</b>")!
// replacing attributes
newText = newText.stringByReplacingCharactersInRange(range, withString: "")
newText = newText.stringByReplacingCharactersInRange(range2, withString: "")
// creating a range for bold => error "'String.Index' is not convertible to 'int'"
// how to do such a thing
var boldRange = NSMakeRange(range.startIndex, range2.endIndex -3)
// setting bold
newText.addAttribute(NSFontAttributeName, value: UIFont(name: "Helvetica-Bold", size: 14.0)!, range: boldRange)
This whole range thing is my main issue at the moment as its quite different to a simple position in the string.
This issue is a great example for the lack of (or well hidden) documentation:
The addAttribute wants an NSRange, the rangeOfString seems to deliver a generic Range according to an error message I get - but there is no info about it.
The Search Documentation button in Xcode on rangeOfString() leads to NSString.
Searching in there for rangeOfString()says it returns NSRange. Clicking on that leads to the info of a type alias for _NSRange which in turn has two NSUInteger properties named location and length. Where is the startIndex and endIndex property I see in XCode? Very confusing...
Would be great if you can give me some snippets or hints where I'm wrong here or even the method body as I'm still hoping its not too difficult if you know iOS and Swift well. I'm aiming for iOS 7.1 support but if its way easier with iOS 8 only its fine as well.
Regarding your first method with NSAttributedString:
The \n character in HTML is just ordinary white space. To get a line break you
would have to replace it by <br /> first.
The font attributes can be controlled by a HTML <span>, see Parsing HTML into NSAttributedText - how to set font?.
This gives (now updated for Swift 2):
func convertText(inputText: String) -> NSAttributedString {
var html = inputText
// Replace newline character by HTML line break
while let range = html.rangeOfString("\n") {
html.replaceRange(range, with: "<br />")
}
// Embed in a <span> for font attributes:
html = "<span style=\"font-family: Helvetica; font-size:14pt;\">" + html + "</span>"
let data = html.dataUsingEncoding(NSUnicodeStringEncoding, allowLossyConversion: true)!
let attrStr = try? NSAttributedString(
data: data,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil)
return attrStr!
}
Regarding your second method:
There are two different rangeOfString() methods, one for (Swift) String and one
for (Foundation) NSString. The String method returns a Range<String.Index>
and the NSString method returns an NSRange.
Converting between these two is possible but complicated. The reason is that in
a String each "extended grapheme cluster" counts as one character, whereas in
NSString each UTF-16 unit is counted. An extended grapheme cluster can be
one or more UTF-16 unit ("😄" is two UTF-16 units, "🇩🇪" is four).
The addAttribute() method accepts only an NSRange. The easiest method to solve
this problem is to convert the Swift string to NSString and work with NSRange
only. Then your method could look like this:
func convertText(inputText: String) -> NSAttributedString {
let attrString = NSMutableAttributedString(string: inputText)
let boldFont = UIFont(name: "Helvetica-Bold", size: 14.0)!
var r1 = (attrString.string as NSString).rangeOfString("<b>")
while r1.location != NSNotFound {
let r2 = (attrString.string as NSString).rangeOfString("</b>")
if r2.location != NSNotFound && r2.location > r1.location {
let r3 = NSMakeRange(r1.location + r1.length, r2.location - r1.location - r1.length)
attrString.addAttribute(NSFontAttributeName, value: boldFont, range: r3)
attrString.replaceCharactersInRange(r2, withString: "")
attrString.replaceCharactersInRange(r1, withString: "")
} else {
break
}
r1 = (attrString.string as NSString).rangeOfString("<b>")
}
return attrString
}