├── .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..