Update 2022:
Before iOS 15, Apple strictly followed soft hyphen, while in iOS 15 they now only consider those as hyphenation opportunities.
So basically now we should use languageIdentifier attribute, lesser opportunities for our own custom hyphenation rules (like remove ugly, one side short, hyphenation break), but works grammatically correctly.
https://developer.apple.com/documentation/foundation/attributescopes/foundationattributes/3802173-languageidentifier
Old answer:
In the end a real help was this extension by Frank Rausch
https://gist.github.com/frankrausch/0d2c91fad5e417a84aaa43bfb9c9aec8
Made some addition to detect dominant language of string and limit the words that should hyphenate by characters count.
import Foundation
import NaturalLanguage
extension String {
func detectedLanguage(for string: String) -> String? {
let recognizer = NLLanguageRecognizer()
recognizer.processString(string)
guard let languageCode = recognizer.dominantLanguage?.rawValue else { return nil }
let detectedLanguage = Locale.current.localizedString(forIdentifier: languageCode)
return detectedLanguage
}
func autoHyphenated() -> String {
return self.hyphenated(languageCode: detectedLanguage(for: self) ?? "")
}
func hyphenated(languageCode: String) -> String {
let locale = Locale(identifier: languageCode)
return self.hyphenated(locale: locale)
}
func hyphenated(locale: Locale, wordMinimumLenght: Int = 13) -> String {
guard CFStringIsHyphenationAvailableForLocale(locale as CFLocale) else { return self }
var s = self
var words = s.components(separatedBy: " ")
for index in 0..<words.count {
if words[index].count > wordMinimumLenght && !words[index].contains("-") {
let fullRange = CFRangeMake(0, words[index].utf16.count)
var hyphenationLocations = [CFIndex]()
for (i, _) in words[index].utf16.enumerated() {
let location: CFIndex = CFStringGetHyphenationLocationBeforeIndex(words[index] as CFString, i, fullRange, 0, locale as CFLocale, nil)
if hyphenationLocations.last != location {
hyphenationLocations.append(location)
}
}
for l in hyphenationLocations.reversed() {
guard l > 0 else { continue }
let strIndex = String.Index(utf16Offset: l, in: words[index])
words[index].insert("\u{00AD}", at: strIndex)
}
}
}
s = words.joined(separator: " ")
return s
}
}