├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md └── Sources └── HyphenableText └── HyphenableText.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alessio Moiso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "HyphenableText", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | .watchOS(.v6), 12 | .tvOS(.v13), 13 | .macCatalyst(.v13) 14 | ], 15 | products: [ 16 | // Products define the executables and libraries a package produces, and make them visible to other packages. 17 | .library( 18 | name: "HyphenableText", 19 | targets: ["HyphenableText"]), 20 | ], 21 | dependencies: [ 22 | // Dependencies declare other packages that this package depends on. 23 | // .package(url: /* package url */, from: "1.0.0"), 24 | ], 25 | targets: [ 26 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 27 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 28 | .target( 29 | name: "HyphenableText", 30 | dependencies: []) 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HyphenableText 2 | HyphenableText is a `Text` view that displays one or multiple lines of text with support for hyphenation. 3 | 4 | ### The Problem 5 | In SwiftUI, when passing a longer text to a `Text`, it will automatically wrap its content to go on multiple lines, but when a word on its own is longer than the available space, it will just be broken without any indication. What Apple does in the system apps is to add [hyphenation]([Title](https://en.wikipedia.org/wiki/Hyphen)). 6 | 7 | This can be done in UIKit very easily by using a `AttributedString` and its `paragraphStyle` with a [`hyphenationFactor`](https://developer.apple.com/documentation/uikit/nsmutableparagraphstyle/1535553-hyphenationfactor) set to `1.0`. However, passing this `AttributedString` to a SwiftUI `Text` does nothing, as the component seems to be ignoring the paragraph style completely. 8 | 9 | ### The Solution 10 | HyphenableText resorts to Core Foundation to produce a string that contains [soft hyphens](https://en.wikipedia.org/wiki/Soft_hyphen). 11 | 12 | A soft hypen (Unicode character `\u{00AD}`) lets the text presenter know that, in that location, there is an opportunity to break a word: when this happens, the presenter will also automatically display a hyphen. 13 | 14 | In order to identify in which locations of the passed string there can be a hyphen, Core Foundation provides [`CFStringGetHyphenationLocationBeforeIndex`](https://developer.apple.com/documentation/corefoundation/1542693-cfstringgethyphenationlocationbe), which returns the next possible location for a hyphen, assuming that hyphenation is available for the current language (see [`CFStringIsHyphenationAvailableForLocale`](https://developer.apple.com/documentation/corefoundation/1543237-cfstringishyphenationavailablefo)). 15 | 16 | You can invoke this algorithm directly by using the `softHyphenated(withLocale:hyphenCharacter:)` extension, which also provides valid default values for all parameters and can be invoked on any `String`. 17 | 18 | ## Installation 19 | HyphenableText is available through [Swift Package Manager](https://swift.org/package-manager). 20 | 21 | ```swift 22 | .package(url: "https://github.com/MrAsterisco/HyphenableText", from: "") 23 | ``` 24 | 25 | ### Latest Release 26 | To find out the latest version, look at the Releases tab of this repository. 27 | 28 | ## Usage 29 | HyphenableText can be used as any other SwiftUI view: 30 | 31 | ```swift 32 | HyphenableText( 33 | "Antidisestablishmentarianism juxtaposed with ultramicroscopic-silicovolcanoconiosis presents an inextricable conundrum of lexical intricacy." 34 | ) 35 | ``` 36 | 37 | HyphenableText supports the same appearance modifiers that `Text` supports. 38 | 39 | ## Compatibility 40 | HyphenableText requires **iOS 13.0 or later**, **macOS 10.15 or later**, **watchOS 6.0 or later**, **tvOS 13 or later**, or **Mac Catalyst 13 or later**. 41 | 42 | ## Contributions 43 | All contributions to expand the library are welcome. Fork the repo, make the changes you want, and open a Pull Request. 44 | 45 | If you make changes to the codebase, I am not enforcing a coding style, but I may ask you to make changes based on how the rest of the library is made. 46 | 47 | ## Status 48 | This library is under **active development**. Even if most of the APIs are pretty straightforward, **they may change in the future**; but you don't have to worry about that, because releases will follow [Semantic Versioning 2.0.0](https://semver.org/). 49 | 50 | ## License 51 | HyphenableText is distributed under the MIT license. [See LICENSE](https://github.com/MrAsterisco/HyphenableText/blob/master/LICENSE) for details. 52 | -------------------------------------------------------------------------------- /Sources/HyphenableText/HyphenableText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HyphenableText.swift 3 | // 4 | // 5 | // Created by Alessio Moiso on 18.07.23. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | 11 | public extension String { 12 | /// The Unicode character of a soft hyphen. 13 | /// 14 | /// - see: [Wikipedia](https://en.wikipedia.org/wiki/Soft_hyphen). 15 | static let softHyphen = "\u{00AD}" 16 | 17 | /// Split the string using space as a separator and, for those substrings 18 | /// whose lengths are greater than or equal to minimumWordLength, 19 | /// replace them with a softHyphenated version. 20 | /// 21 | /// - note: This assumes that words are delineated by 22 | /// space characters, which may not be correct for all locales where 23 | /// CFStringIsHyphenationAvailableForLocale(_:) returns true. Consider 24 | /// using CFStringTokenizer to eliminate this assumption. 25 | func softHyphenateByWord(minimumWordLength: Int = 0, withLocale locale: Locale = .autoupdatingCurrent) -> Self { 26 | var substringArray: [String] = split(separator: " ", omittingEmptySubsequences: false).map({ String($0) }) 27 | 28 | for (i, substring) in substringArray.enumerated() { 29 | if substring.count >= minimumWordLength { 30 | substringArray[i] = substring.softHyphenated(withLocale: locale) 31 | } 32 | } 33 | 34 | return substringArray.joined(separator: " ") 35 | } 36 | 37 | /// Insert a soft-hyphen character at every possible location in the string. 38 | /// 39 | /// - note: Soft-hyphens are only displayed when needed in order to allow 40 | /// words that are longer than the available space to flow on multiple lines. 41 | func softHyphenated(withLocale locale: Locale = .autoupdatingCurrent, hyphenCharacter: String = Self.softHyphen) -> Self { 42 | let localeRef = locale as CFLocale 43 | guard CFStringIsHyphenationAvailableForLocale(localeRef) else { 44 | return self 45 | } 46 | 47 | let mutableSelf = NSMutableString(string: self) 48 | var hyphenationLocations = Array(repeating: false, count: count) 49 | let range = CFRangeMake(0, count) 50 | 51 | for i in 0..= 0 && nextLocation < count { 62 | hyphenationLocations[nextLocation] = true 63 | } 64 | } 65 | 66 | for i in (0..