├── .ruby-version ├── .xcodesamplecode.plist ├── demo.jpg ├── demo2.jpg ├── .gitignore ├── SwiftUISampleApp ├── AttributedTextSample │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── AttributedTextSample.entitlements │ ├── AppDelegate.swift │ ├── ContentView.swift │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── AttributedText.swift │ └── SceneDelegate.swift ├── AttributedTextSampleTests │ ├── Swift_logo_color_rgb.jpg │ ├── ImageAttachmentTests.swift │ └── Info.plist └── AttributedTextSample.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ └── xcschemes │ │ └── AttributedTextSample.xcscheme │ └── project.pbxproj ├── Tests ├── LinuxMain.swift └── NSAttributedStringBuilderTests │ ├── XCTestManifests.swift │ ├── StaticComponentsTests.swift │ ├── NSAttributedStringBuilderTests.swift │ ├── ComponentBasicModifierTests.swift │ └── ComponentParagraphSylteModifierTests.swift ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ └── NSAttributedStringBuilder.xcscheme ├── .travis.yml ├── .github └── workflows │ └── swift.yml ├── Sources └── NSAttributedStringBuilder │ ├── Components │ ├── AText.swift │ ├── Link.swift │ ├── StaticComponents.swift │ ├── ImageAttachment.swift │ └── Component.swift │ └── NSAttributedStringBuilder.swift ├── CHANGELOG.md ├── LICENSE ├── Package.swift ├── NSAttributedStringBuilder13.podspec └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.0 2 | -------------------------------------------------------------------------------- /.xcodesamplecode.plist: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanhuang13/NSAttributedStringBuilder/HEAD/demo.jpg -------------------------------------------------------------------------------- /demo2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanhuang13/NSAttributedStringBuilder/HEAD/demo2.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata 6 | *.xcuserstate 7 | *.xcscmblueprint -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import NSAttributedStringBuilderTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += NSAttributedStringBuilderTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSampleTests/Swift_logo_color_rgb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanhuang13/NSAttributedStringBuilder/HEAD/SwiftUISampleApp/AttributedTextSampleTests/Swift_logo_color_rgb.jpg -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/NSAttributedStringBuilderTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | [ 6 | testCase(NSAttributedStringBuilderTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode11 3 | script: 4 | - xcodebuild -sdk iphonesimulator -project ./SwiftUISampleApp/AttributedTextSample.xcodeproj -scheme AttributedTextSample -destination 'platform=iOS Simulator,name=iPhone 11 Pro Max,OS=13.0' test 5 | after_success: 6 | - bash <(curl -s https://codecov.io/bash) 7 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: macOS-latest 8 | env: 9 | DEVELOPER_DIR: /Applications/Xcode_12.5.app/Contents/Developer 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Build 13 | run: swift build -v 14 | - name: Run tests 15 | run: swift test -v 16 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample/AttributedTextSample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/NSAttributedStringBuilder/Components/AText.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import UIKit 3 | #elseif canImport(AppKit) 4 | import AppKit 5 | #endif 6 | 7 | public typealias AText = NSAttributedString.AttrText 8 | 9 | public extension NSAttributedString { 10 | struct AttrText: Component { 11 | // MARK: Lifecycle 12 | 13 | public init(_ string: String, attributes: Attributes = [:]) { 14 | self.string = string 15 | self.attributes = attributes 16 | } 17 | 18 | // MARK: Public 19 | 20 | public let string: String 21 | public let attributes: Attributes 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSampleTests/ImageAttachmentTests.swift: -------------------------------------------------------------------------------- 1 | @testable import NSAttributedStringBuilder 2 | import XCTest 3 | 4 | final class ImageAttachmentTests: XCTestCase { 5 | func testSetImage_getAttachmentAttribute() { 6 | let testBundle = Bundle(for: ImageAttachmentTests.self) 7 | let testImage = UIImage(contentsOfFile: testBundle.path(forResource: "Swift_logo_color_rgb", ofType: "jpg")!)! 8 | 9 | let sut = NSAttributedString { 10 | ImageAttachment(testImage, size: CGSize(width: 40, height: 40)) 11 | LineBreak() 12 | } 13 | 14 | XCTAssertNotNil(sut.attributes(at: 0, effectiveRange: nil)[.attachment]) 15 | // TODO: Better test 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/NSAttributedStringBuilder/Components/Link.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import UIKit 3 | #elseif canImport(AppKit) 4 | import AppKit 5 | #endif 6 | 7 | public typealias Link = NSAttributedString.Link 8 | 9 | public extension NSAttributedString { 10 | struct Link: Component { 11 | // MARK: Lifecycle 12 | 13 | public init(_ string: String, url: URL, attributes: Attributes = [:]) { 14 | self.string = string 15 | self.url = url 16 | 17 | var attributes = attributes 18 | attributes[.link] = url 19 | self.attributes = attributes 20 | } 21 | 22 | // MARK: Public 23 | 24 | public let string: String 25 | public let url: URL 26 | public let attributes: Attributes 27 | 28 | public var attributedString: NSAttributedString { 29 | NSAttributedString(string: string, attributes: attributes) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/NSAttributedStringBuilder/Components/StaticComponents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias Empty = NSAttributedString.Empty 4 | public typealias Space = NSAttributedString.Space 5 | public typealias LineBreak = NSAttributedString.LineBreak 6 | 7 | public extension NSAttributedString { 8 | struct Empty: Component { 9 | // MARK: Lifecycle 10 | 11 | public init() {} 12 | 13 | // MARK: Public 14 | 15 | public let string: String = "" 16 | public let attributes: Attributes = [:] 17 | } 18 | 19 | struct Space: Component { 20 | // MARK: Lifecycle 21 | 22 | public init() {} 23 | 24 | // MARK: Public 25 | 26 | public let string: String = " " 27 | public let attributes: Attributes = [:] 28 | } 29 | 30 | struct LineBreak: Component { 31 | // MARK: Lifecycle 32 | 33 | public init() {} 34 | 35 | // MARK: Public 36 | 37 | public let string: String = "\n" 38 | public let attributes: Attributes = [:] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/NSAttributedStringBuilder/NSAttributedStringBuilder.swift: -------------------------------------------------------------------------------- 1 | // NSAttributedString does not support SwiftUI Font and color, we still need to use UI/NS Font/Color 2 | #if canImport(UIKit) 3 | import UIKit 4 | public typealias Font = UIFont 5 | public typealias Color = UIColor 6 | #elseif canImport(AppKit) 7 | import AppKit 8 | public typealias Font = NSFont 9 | public typealias Color = NSColor 10 | #endif 11 | 12 | public typealias Attributes = [NSAttributedString.Key: Any] 13 | 14 | @resultBuilder 15 | public enum NSAttributedStringBuilder { 16 | public static func buildBlock(_ components: Component...) -> NSAttributedString { 17 | let mas = NSMutableAttributedString(string: "") 18 | for component in components { 19 | mas.append(component.attributedString) 20 | } 21 | return mas 22 | } 23 | } 24 | 25 | public extension NSAttributedString { 26 | convenience init(@NSAttributedStringBuilder _ builder: () -> NSAttributedString) { 27 | self.init(attributedString: builder()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # NSAttributedStringBuilder CHANGELOG 2 | 3 | All notable changes to this project will be documented in this file. 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.4.1] - 2021-04-28 10 | - [Fixed] Documents 11 | 12 | ## [0.4] - 2021-04-28 13 | - [Changed] Update to Swift 5.4, replaced `@_functionBuilder` with `@resultBuilder` 14 | - [Fixed] Typo of `ImageAttachment` 15 | 16 | ## [0.3.3] - 2019-11-19 17 | - [Added] CocoaPods support macOS and tvOS 18 | 19 | ## [0.3.2] - 2019-10-17 20 | - [Added] CocoaPods support 21 | 22 | ## [0.3.1] - 2019-07-20 23 | - [Removed] OS version requirements of Package.swift #1 24 | 25 | ## [0.3.0] - 2019-07-18 26 | - [Changed] Replace UIKitForMac to macCatalyst to reflect the changes in beta 4 27 | - [Changed] Replace .color() with .foregroundColor() to reflect the changes in beta 4 28 | 29 | ## [0.2.0] - 2019-07-12 30 | - [Added] Static components 31 | - [Changed] Rename AttrText's typealias to AText 32 | 33 | ## [0.1.0] - 2019-07-11 34 | - Initial version 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ethan Huang 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 | -------------------------------------------------------------------------------- /Sources/NSAttributedStringBuilder/Components/ImageAttachment.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import UIKit 3 | 4 | #if !os(watchOS) 5 | public typealias ImageAttachment = NSAttributedString.ImageAttachment 6 | 7 | public extension NSAttributedString { 8 | struct ImageAttachment: Component { 9 | // MARK: Lifecycle 10 | 11 | public init(_ image: UIImage, size: Size? = nil) { 12 | let attachment = NSTextAttachment() 13 | attachment.image = image 14 | 15 | if let size = size { 16 | attachment.bounds.size = size 17 | } 18 | 19 | self.attachment = attachment 20 | } 21 | 22 | // MARK: Public 23 | 24 | public let string: String = "" 25 | public let attributes: Attributes = [:] 26 | 27 | public var attributedString: NSAttributedString { 28 | NSAttributedString(attachment: attachment) 29 | } 30 | 31 | // MARK: Private 32 | 33 | private let attachment: NSTextAttachment 34 | } 35 | } 36 | #endif 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 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: "NSAttributedStringBuilder", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "NSAttributedStringBuilder", 12 | targets: ["NSAttributedStringBuilder"] 13 | ), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 22 | .target( 23 | name: "NSAttributedStringBuilder", 24 | dependencies: [] 25 | ), 26 | .testTarget( 27 | name: "NSAttributedStringBuilderTests", 28 | dependencies: ["NSAttributedStringBuilder"] 29 | ), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 6 | // Override point for customization after application launch. 7 | return true 8 | } 9 | 10 | // MARK: UISceneSession Lifecycle 11 | 12 | func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { 13 | // Called when a new scene session is being created. 14 | // Use this method to select a configuration to create the new scene with. 15 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 16 | } 17 | 18 | func application(_: UIApplication, didDiscardSceneSessions _: Set) { 19 | // Called when the user discards a scene session. 20 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 21 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/NSAttributedStringBuilderTests/StaticComponentsTests.swift: -------------------------------------------------------------------------------- 1 | @testable import NSAttributedStringBuilder 2 | import XCTest 3 | 4 | final class StaticComponentsTests: XCTestCase { 5 | func testEmpty() { 6 | let testData: NSAttributedString = { 7 | let mas = NSMutableAttributedString(string: "") 8 | mas.append(NSAttributedString(string: "")) 9 | return mas 10 | }() 11 | 12 | let sut = NSAttributedString { 13 | Empty() 14 | Empty() 15 | } 16 | 17 | XCTAssertEqual(sut, testData) 18 | } 19 | 20 | func testSpace() { 21 | let testData: NSAttributedString = { 22 | let mas = NSMutableAttributedString(string: " ") 23 | return mas 24 | }() 25 | 26 | let sut = NSAttributedString { 27 | Empty() 28 | Space() 29 | } 30 | 31 | XCTAssertEqual(sut, testData) 32 | } 33 | 34 | func testLineBreak() { 35 | let testData: NSAttributedString = { 36 | let mas = NSMutableAttributedString(string: "") 37 | mas.append(NSAttributedString(string: "\n")) 38 | mas.append(NSAttributedString(string: "")) 39 | return mas 40 | }() 41 | 42 | let sut = NSAttributedString { 43 | Empty() 44 | LineBreak() 45 | Empty() 46 | } 47 | 48 | XCTAssertEqual(sut, testData) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample/ContentView.swift: -------------------------------------------------------------------------------- 1 | import NSAttributedStringBuilder 2 | import SwiftUI 3 | 4 | let image = UIImage(named: "Swift_logo_color_rgb.jpg")! 5 | 6 | struct ContentView: View { 7 | var body: some View { 8 | VStack(alignment: .leading) { 9 | Text("Text Title") 10 | .font(.largeTitle) 11 | Text("Text Subtitle") 12 | .font(.headline) 13 | Text("Text Link") 14 | .font(.body) 15 | .underline() 16 | .foregroundColor(.blue) 17 | Image(uiImage: image) 18 | .padding(.bottom) 19 | 20 | // UITextView: UIViewRepresentable 21 | AttributedText { 22 | AText("AttributedText Title") 23 | .font(.preferredFont(forTextStyle: .largeTitle)) 24 | LineBreak() 25 | AText("AttributedText Subtitle") 26 | .font(.preferredFont(forTextStyle: .headline)) 27 | LineBreak() 28 | Link("Attributed Link", url: URL(string: "https://www.apple.com")!) 29 | .font(.preferredFont(forTextStyle: .body)) 30 | LineBreak() 31 | ImageAttachment(image) 32 | } 33 | } 34 | } 35 | } 36 | 37 | #if DEBUG 38 | struct ContentView_Previews: PreviewProvider { 39 | static var previews: some View { 40 | ContentView() 41 | } 42 | } 43 | #endif 44 | -------------------------------------------------------------------------------- /Tests/NSAttributedStringBuilderTests/NSAttributedStringBuilderTests.swift: -------------------------------------------------------------------------------- 1 | @testable import NSAttributedStringBuilder 2 | import XCTest 3 | 4 | final class NSAttributedStringBuilderTests: XCTestCase { 5 | static var allTests = [ 6 | ("testInitWithTextAndLink", testInitWithTextAndLink), 7 | ] 8 | 9 | func testInitWithTwoAText() { 10 | let testData: NSAttributedString = { 11 | let mas = NSMutableAttributedString(string: "Hello world") 12 | mas.append(NSAttributedString(string: " with Swift")) 13 | return mas 14 | }() 15 | 16 | let sut = NSAttributedString { 17 | AText("Hello world") 18 | AText(" with Swift") 19 | } 20 | 21 | XCTAssertEqual(sut, testData) 22 | } 23 | 24 | func testInitWithTextAndLink() { 25 | let testData: NSAttributedString = { 26 | let mas = NSMutableAttributedString(string: "") 27 | mas.append(NSAttributedString(string: "Here is a link to ", 28 | attributes: [.foregroundColor: Color.brown])) 29 | mas.append(NSAttributedString(string: "Apple", 30 | attributes: [.link: URL(string: "https://www.apple.com")!])) 31 | return mas 32 | }() 33 | 34 | let sut = NSAttributedString { 35 | AText("Here is a link to ") 36 | .foregroundColor(.brown) 37 | Link("Apple", url: URL(string: "https://www.apple.com")!) 38 | } 39 | 40 | XCTAssertEqual(sut, testData) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /NSAttributedStringBuilder13.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint NSAttributedStringBuilder.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'NSAttributedStringBuilder13' 11 | s.version = '0.4.1' 12 | s.summary = 'Composing NSAttributedString with SwiftUI-style syntax, powered by Result Builder.' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | Composing NSAttributedString with SwiftUI-style syntax, powered by Result Builder. 22 | 23 | Project Link: https://github.com/ethanhuang13/NSAttributedStringBuilder 24 | DESC 25 | 26 | s.homepage = 'https://github.com/ethanhuang13/NSAttributedStringBuilder' 27 | s.swift_versions = '5.4' 28 | # s.screenshots = 'https://github.com/ethanhuang13/NSAttributedStringBuilder/blob/master/demo2.jpg', 29 | 'https://github.com/ethanhuang13/NSAttributedStringBuilder/blob/master/demo.jpg' 30 | s.license = { :type => 'MIT', :file => 'LICENSE' } 31 | s.author = { 'ethanhuang13' => 'blesserx@gmail.com' } 32 | s.source = { :git => 'https://github.com/ethanhuang13/NSAttributedStringBuilder.git', :tag => s.version.to_s } 33 | s.social_media_url = 'https://twitter.com/ethanhuang13' 34 | 35 | s.ios.deployment_target = '8.0' 36 | s.osx.deployment_target = '10.10' 37 | s.tvos.deployment_target = '9.0' 38 | 39 | s.source_files = 'Sources/NSAttributedStringBuilder/*', 40 | 'Sources/NSAttributedStringBuilder/Components/*' 41 | end 42 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample/AttributedText.swift: -------------------------------------------------------------------------------- 1 | import NSAttributedStringBuilder 2 | import SwiftUI 3 | 4 | /// A custom view to use NSAttributedString in SwiftUI 5 | public final class AttributedText: UIViewRepresentable { 6 | // MARK: Lifecycle 7 | 8 | private init(_ attributedString: NSAttributedString) { 9 | self.attributedString = attributedString 10 | } 11 | 12 | public convenience init(@NSAttributedStringBuilder _ builder: () -> NSAttributedString) { 13 | self.init(builder()) 14 | } 15 | 16 | // MARK: Public 17 | 18 | public func makeUIView(context _: UIViewRepresentableContext) -> UITextView { 19 | let textView = UITextView(frame: .zero) 20 | textView.attributedText = attributedString 21 | textView.isEditable = false 22 | textView.backgroundColor = .clear 23 | textView.textAlignment = .center 24 | return textView 25 | } 26 | 27 | public func updateUIView(_ textView: UITextView, context _: UIViewRepresentableContext) { 28 | textView.attributedText = attributedString 29 | } 30 | 31 | // MARK: Internal 32 | 33 | var attributedString: NSAttributedString 34 | } 35 | 36 | #if DEBUG 37 | struct AttributedText_Previews: PreviewProvider { 38 | static var previews: some View { 39 | AttributedText { 40 | ImageAttachment(UIImage(named: "Swift_logo_color_rgb.jpg")!, size: CGSize(width: 90, height: 90)) 41 | LineBreak() 42 | .lineSpacing(20) 43 | AText("Hello SwiftUI") 44 | .backgroundColor(.red) 45 | .baselineOffset(10) 46 | .font(.systemFont(ofSize: 20)) 47 | .foregroundColor(.yellow) 48 | .expansion(1) 49 | .kerning(3) 50 | .ligature(.none) 51 | .obliqueness(0.5) 52 | .shadow(color: .black, radius: 10, x: 4, y: 4) 53 | .strikethrough(style: .patternDash, color: .black) 54 | .stroke(width: -2, color: .green) 55 | .underline(.patternDashDotDot, color: .cyan) 56 | LineBreak() 57 | AText(" with fun") 58 | .paragraphSpacing(10, before: 60) 59 | .alignment(.right) 60 | } 61 | } 62 | } 63 | #endif 64 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKit 3 | 4 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 5 | var window: UIWindow? 6 | 7 | func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { 8 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 9 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 10 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 11 | 12 | // Use a UIHostingController as window root view controller 13 | if let windowScene = scene as? UIWindowScene { 14 | let window = UIWindow(windowScene: windowScene) 15 | window.rootViewController = UIHostingController(rootView: ContentView()) 16 | self.window = window 17 | window.makeKeyAndVisible() 18 | } 19 | } 20 | 21 | func sceneDidDisconnect(_: UIScene) { 22 | // Called as the scene is being released by the system. 23 | // This occurs shortly after the scene enters the background, or when its session is discarded. 24 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 25 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 26 | } 27 | 28 | func sceneDidBecomeActive(_: UIScene) { 29 | // Called when the scene has moved from an inactive state to an active state. 30 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 31 | } 32 | 33 | func sceneWillResignActive(_: UIScene) { 34 | // Called when the scene will move from an active state to an inactive state. 35 | // This may occur due to temporary interruptions (ex. an incoming phone call). 36 | } 37 | 38 | func sceneWillEnterForeground(_: UIScene) { 39 | // Called as the scene transitions from the background to the foreground. 40 | // Use this method to undo the changes made on entering the background. 41 | } 42 | 43 | func sceneDidEnterBackground(_: UIScene) { 44 | // Called as the scene transitions from the foreground to the background. 45 | // Use this method to save data, release shared resources, and store enough scene-specific state information 46 | // to restore the scene back to its current state. 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NSAttributedStringBuilder 2 | [![Build Status](https://github.com/ethanhuang13/NSAttributedStringBuilder/workflows/Swift/badge.svg)](https://github.com/ethanhuang13/NSAttributedStringBuilder/actions?workflow=Swift) 3 | [![codecov](https://codecov.io/gh/ethanhuang13/NSAttributedStringBuilder/branch/master/graph/badge.svg)](https://codecov.io/gh/ethanhuang13/NSAttributedStringBuilder) 4 | [![GitHub release](https://img.shields.io/github/release/ethanhuang13/nsattributedstringbuilder.svg)]() 5 | ![GitHub top language](https://img.shields.io/github/languages/top/ethanhuang13/nsattributedstringbuilder.svg) 6 | [![License](https://img.shields.io/github/license/ethanhuang13/nsattributedstringbuilder.svg)](https://github.com/ethanhuang13/ladybug/blob/master/LICENSE) 7 | [![Twitter](https://img.shields.io/badge/Twitter-%40ethanhuang13-blue.svg)](https://twitter.com/ethanhuang13) 8 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/ethanhuang13) 9 | 10 | Composing `NSAttributedString` with SwiftUI-style syntax, powered by [Result Builder](https://forums.swift.org/t/function-builders/25167). 11 | 12 | Project Link: [https://github.com/ethanhuang13/NSAttributedStringBuilder](https://github.com/ethanhuang13/NSAttributedStringBuilder) 13 | 14 | ## Features 15 | 16 | | | Features | 17 | | --- | --- | 18 | | 🐦 | Open source library written in Swift 5.4 | 19 | | 🍬 | SwiftUI-like syntax | 20 | | 💪 | Support most attributes in `NSAttributedString.Key` | 21 | | 📦 | Distribution with Swift Package Manager | 22 | | 🧪 | Fully tested code | 23 | | 🛠 | Continuously integrates in [Swift Source Compatibility Suite](https://github.com/apple/swift-source-compat-suite/pull/373) | 24 | 25 | ## How to use? 26 | 27 | Traditionally we compose a `NSAttributedString` like this: 28 | 29 | ```Swift 30 | let mas = NSMutableAttributedString(string: "") 31 | mas.append(NSAttributedString(string: "Hello world", attributes: [.font: UIFont.systemFont(ofSize: 24), .foregroundColor: UIColor.red])) 32 | mas.append(NSAttributedString(string: "\n")) 33 | mas.append(NSAttributedString(string: "with Swift", attributes: [.font: UIFont.systemFont(ofSize: 20), .foregroundColor: UIColor.orange])) 34 | 35 | ``` 36 | Now, with **NSAttributedStringBuilder**, we can use SwiftUI-like syntax to declare `NSAttributedString`: 37 | 38 | ```Swift 39 | let attributedString = NSAttributedString { 40 | AText("Hello world") 41 | .font(.systemFont(ofSize: 24)) 42 | .foregroundColor(.red) 43 | LineBreak() 44 | AText("with Swift") 45 | .font(.systemFont(ofSize: 20)) 46 | .foregroundColor(.orange) 47 | } 48 | 49 | ``` 50 | 51 | ## Requirements 52 | Xcode 12.5. This project uses Swift 5.4 feature [Result Builder](https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md). 53 | 54 | ## Installation 55 | 56 | ### Swift Package 57 | Open your project in Xcode 12, navigate to **Menu -> Swift Packages -> Add Package Dependency** and enter [https://github.com/ethanhuang13/NSAttributedStringBuilder](https://github.com/ethanhuang13/NSAttributedStringBuilder) to install. 58 | 59 | ### CocoaPods 60 | Add `pod 'NSAttributedStringBuilder13'` to your `Podfile`. 61 | 62 | ## SwiftUI Sample Project 63 | Besides clearer `NSAttributedString` syntax, since **NSAttributedStringBuilder** uses Result Builder it also enables API to build components in `UIViewRepresentable`(which embeds `UIView` in a SwiftUI `View`). 64 | 65 | Just like a SwiftUI `Text` takes a `String` as input, the purpose of `AttributedText` in the sample project is to take a `NSAttributedString` as input and render in SwiftUI. 66 | 67 | To achieve this, `AttributedText.swift` uses `@NSAttributedStringBuilder` to support SwiftUI-style syntax: 68 | 69 | ![AttributedText.swift](demo2.jpg) 70 | 71 | Then using an `AttributedText` will be like: 72 | ![ContentView.swift](demo.jpg) 73 | 74 | Open the sample in ***/SwiftUISampleApp/AttributedTextSample.xcodeproj*** and check `AttributedText`. It uses `UITextView`, you can also use `UILabel` or `NSTextView`. 75 | 76 | ## TODO 77 | * Better tests for image attachment 78 | 79 | ## Known Issue 80 | * `NSAttributedString` does not support link color, therefore `Link` component with a `.color()` modifier has no effect. Alternatively you need to specify in `UITextView.linkTextAttributes` or `.tintColor`. 81 | 82 | ## Others 83 | Initially discussed on this [Twitter thread](https://twitter.com/ethanhuang13/status/1148135534826442752). Some code are inspired by [zonble](https://github.com/zonble/NSAttributedStringBuilder)🙏. 84 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/NSAttributedStringBuilder.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 52 | 53 | 54 | 55 | 57 | 63 | 64 | 65 | 67 | 68 | 69 | 70 | 71 | 72 | 82 | 83 | 89 | 90 | 96 | 97 | 98 | 99 | 101 | 102 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample.xcodeproj/xcshareddata/xcschemes/AttributedTextSample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 46 | 47 | 48 | 49 | 51 | 57 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 81 | 83 | 89 | 90 | 91 | 92 | 98 | 100 | 106 | 107 | 108 | 109 | 111 | 112 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /Sources/NSAttributedStringBuilder/Components/Component.swift: -------------------------------------------------------------------------------- 1 | #if canImport(UIKit) 2 | import UIKit 3 | public typealias Size = CGSize 4 | #elseif canImport(AppKit) 5 | import AppKit 6 | public typealias Size = NSSize 7 | #endif 8 | 9 | public protocol Component { 10 | var string: String { get } 11 | var attributes: Attributes { get } 12 | var attributedString: NSAttributedString { get } 13 | } 14 | 15 | public enum Ligature: Int { 16 | case none = 0 17 | case `default` = 1 18 | 19 | #if canImport(AppKit) 20 | case all = 2 // Value 2 is unsupported on iOS 21 | #endif 22 | } 23 | 24 | public extension Component { 25 | private func build(_ string: String, attributes: Attributes) -> Component { 26 | AText(string, attributes: attributes) 27 | } 28 | 29 | var attributedString: NSAttributedString { 30 | NSAttributedString(string: string, attributes: attributes) 31 | } 32 | 33 | func attribute(_ newAttribute: NSAttributedString.Key, value: Any) -> Component { 34 | attributes([newAttribute: value]) 35 | } 36 | 37 | func attributes(_ newAttributes: Attributes) -> Component { 38 | var attributes = self.attributes 39 | for attribute in newAttributes { 40 | attributes[attribute.key] = attribute.value 41 | } 42 | return build(string, attributes: attributes) 43 | } 44 | } 45 | 46 | // MARK: Basic Modifiers 47 | 48 | public extension Component { 49 | func backgroundColor(_ color: Color) -> Component { 50 | attributes([.backgroundColor: color]) 51 | } 52 | 53 | func baselineOffset(_ baselineOffset: CGFloat) -> Component { 54 | attributes([.baselineOffset: baselineOffset]) 55 | } 56 | 57 | func font(_ font: Font) -> Component { 58 | attributes([.font: font]) 59 | } 60 | 61 | func foregroundColor(_ color: Color) -> Component { 62 | attributes([.foregroundColor: color]) 63 | } 64 | 65 | func expansion(_ expansion: CGFloat) -> Component { 66 | attributes([.expansion: expansion]) 67 | } 68 | 69 | func kerning(_ kern: CGFloat) -> Component { 70 | attributes([.kern: kern]) 71 | } 72 | 73 | func ligature(_ ligature: Ligature) -> Component { 74 | attributes([.ligature: ligature.rawValue]) 75 | } 76 | 77 | func obliqueness(_ obliqueness: Float) -> Component { 78 | attributes([.obliqueness: obliqueness]) 79 | } 80 | 81 | func shadow(color: Color? = nil, radius: CGFloat, x: CGFloat, y: CGFloat) -> Component { 82 | let shadow = NSShadow() 83 | shadow.shadowColor = color 84 | shadow.shadowBlurRadius = radius 85 | shadow.shadowOffset = .init(width: x, height: y) 86 | return attributes([.shadow: shadow]) 87 | } 88 | 89 | func strikethrough(style: NSUnderlineStyle, color: Color? = nil) -> Component { 90 | if let color = color { 91 | return attributes([.strikethroughStyle: style.rawValue, 92 | .strikethroughColor: color]) 93 | } else { 94 | return attributes([.strikethroughStyle: style.rawValue]) 95 | } 96 | } 97 | 98 | func stroke(width: CGFloat, color: Color? = nil) -> Component { 99 | if let color = color { 100 | return attributes([.strokeWidth: width, 101 | .strokeColor: color]) 102 | } else { 103 | return attributes([.strokeWidth: width]) 104 | } 105 | } 106 | 107 | func textEffect(_ textEffect: NSAttributedString.TextEffectStyle) -> Component { 108 | attributes([.textEffect: textEffect]) 109 | } 110 | 111 | func underline(_ style: NSUnderlineStyle, color: Color? = nil) -> Component { 112 | if let color = color { 113 | return attributes([.underlineStyle: style.rawValue, 114 | .underlineColor: color]) 115 | } else { 116 | return attributes([.underlineStyle: style.rawValue]) 117 | } 118 | } 119 | 120 | func writingDirection(_ writingDirection: NSWritingDirection) -> Component { 121 | attributes([.writingDirection: writingDirection.rawValue]) 122 | } 123 | 124 | #if canImport(AppKit) 125 | func vertical() -> Component { 126 | attributes([.verticalGlyphForm: 1]) 127 | } 128 | #endif 129 | } 130 | 131 | // MARK: - Paragraph Style Modifiers 132 | 133 | public extension Component { 134 | func paragraphStyle(_ paragraphStyle: NSParagraphStyle) -> Component { 135 | attributes([.paragraphStyle: paragraphStyle]) 136 | } 137 | 138 | func paragraphStyle(_ paragraphStyle: NSMutableParagraphStyle) -> Component { 139 | attributes([.paragraphStyle: paragraphStyle]) 140 | } 141 | 142 | private func getMutableParagraphStyle() -> NSMutableParagraphStyle { 143 | if let mps = attributes[.paragraphStyle] as? NSMutableParagraphStyle { 144 | return mps 145 | } else if let ps = attributes[.paragraphStyle] as? NSParagraphStyle, 146 | let mps = ps.mutableCopy() as? NSMutableParagraphStyle { 147 | return mps 148 | } else { 149 | return NSMutableParagraphStyle() 150 | } 151 | } 152 | 153 | func alignment(_ alignment: NSTextAlignment) -> Component { 154 | let paragraphStyle = getMutableParagraphStyle() 155 | paragraphStyle.alignment = alignment 156 | return self.paragraphStyle(paragraphStyle) 157 | } 158 | 159 | func firstLineHeadIndent(_ indent: CGFloat) -> Component { 160 | let paragraphStyle = getMutableParagraphStyle() 161 | paragraphStyle.firstLineHeadIndent = indent 162 | return self.paragraphStyle(paragraphStyle) 163 | } 164 | 165 | func headIndent(_ indent: CGFloat) -> Component { 166 | let paragraphStyle = getMutableParagraphStyle() 167 | paragraphStyle.headIndent = indent 168 | return self.paragraphStyle(paragraphStyle) 169 | } 170 | 171 | func tailIndent(_ indent: CGFloat) -> Component { 172 | let paragraphStyle = getMutableParagraphStyle() 173 | paragraphStyle.tailIndent = indent 174 | return self.paragraphStyle(paragraphStyle) 175 | } 176 | 177 | func lineBreakeMode(_ lineBreakMode: NSLineBreakMode) -> Component { 178 | let paragraphStyle = getMutableParagraphStyle() 179 | paragraphStyle.lineBreakMode = lineBreakMode 180 | return self.paragraphStyle(paragraphStyle) 181 | } 182 | 183 | func lineHeight(multiple: CGFloat = 0, maximum: CGFloat = 0, minimum: CGFloat) -> Component { 184 | let paragraphStyle = getMutableParagraphStyle() 185 | paragraphStyle.lineHeightMultiple = multiple 186 | paragraphStyle.maximumLineHeight = maximum 187 | paragraphStyle.minimumLineHeight = minimum 188 | return self.paragraphStyle(paragraphStyle) 189 | } 190 | 191 | func lineSpacing(_ spacing: CGFloat) -> Component { 192 | let paragraphStyle = getMutableParagraphStyle() 193 | paragraphStyle.lineSpacing = spacing 194 | return self.paragraphStyle(paragraphStyle) 195 | } 196 | 197 | func paragraphSpacing(_ spacing: CGFloat, before: CGFloat = 0) -> Component { 198 | let paragraphStyle = getMutableParagraphStyle() 199 | paragraphStyle.paragraphSpacing = spacing 200 | paragraphStyle.paragraphSpacingBefore = before 201 | return self.paragraphStyle(paragraphStyle) 202 | } 203 | 204 | func baseWritingDirection(_ baseWritingDirection: NSWritingDirection) -> Component { 205 | let paragraphStyle = getMutableParagraphStyle() 206 | paragraphStyle.baseWritingDirection = baseWritingDirection 207 | return self.paragraphStyle(paragraphStyle) 208 | } 209 | 210 | func hyphenationFactor(_ hyphenationFactor: Float) -> Component { 211 | let paragraphStyle = getMutableParagraphStyle() 212 | paragraphStyle.hyphenationFactor = hyphenationFactor 213 | return self.paragraphStyle(paragraphStyle) 214 | } 215 | 216 | @available(iOS 9.0, tvOS 9.0, watchOS 2.0, OSX 10.11, *) 217 | func allowsDefaultTighteningForTruncation() -> Component { 218 | let paragraphStyle = getMutableParagraphStyle() 219 | paragraphStyle.allowsDefaultTighteningForTruncation = true 220 | return self.paragraphStyle(paragraphStyle) 221 | } 222 | 223 | func tabsStops(_ tabStops: [NSTextTab], defaultInterval: CGFloat = 0) -> Component { 224 | let paragraphStyle = getMutableParagraphStyle() 225 | paragraphStyle.tabStops = tabStops 226 | paragraphStyle.defaultTabInterval = defaultInterval 227 | return self.paragraphStyle(paragraphStyle) 228 | } 229 | 230 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 231 | func textBlocks(_ textBlocks: [NSTextBlock]) -> Component { 232 | let paragraphStyle = getMutableParagraphStyle() 233 | paragraphStyle.textBlocks = textBlocks 234 | return self.paragraphStyle(paragraphStyle) 235 | } 236 | 237 | func textLists(_ textLists: [NSTextList]) -> Component { 238 | let paragraphStyle = getMutableParagraphStyle() 239 | paragraphStyle.textLists = textLists 240 | return self.paragraphStyle(paragraphStyle) 241 | } 242 | 243 | func tighteningFactorForTruncation(_ tighteningFactorForTruncation: Float) -> Component { 244 | let paragraphStyle = getMutableParagraphStyle() 245 | paragraphStyle.tighteningFactorForTruncation = tighteningFactorForTruncation 246 | return self.paragraphStyle(paragraphStyle) 247 | } 248 | 249 | func headerLevel(_ headerLevel: Int) -> Component { 250 | let paragraphStyle = getMutableParagraphStyle() 251 | paragraphStyle.headerLevel = headerLevel 252 | return self.paragraphStyle(paragraphStyle) 253 | } 254 | #endif 255 | } 256 | 257 | // MARK: - No 'modifiers' for these keys. Use .attributes() or .attribute(:value:) instead. 258 | 259 | /* 260 | #if canImport(UIKit) 261 | let iOSKeys: [NSAttributedString.Key] = [ 262 | .UIAccessibilitySpeechAttributeSpellOut, // iOS 13+ 263 | .UIAccessibilityTextAttributeContext, // iOS 13+ 264 | .accessibilitySpeechIPANotation, // iOS 11.0+ 265 | .accessibilitySpeechLanguage, // iOS 7.0+ 266 | .accessibilitySpeechPitch, // iOS 7.0+ 267 | .accessibilitySpeechPunctuation, // iOS 7.0+ 268 | .accessibilitySpeechQueueAnnouncement, // iOS 11.0+ 269 | .accessibilityTextCustom, // iOS 11.0+ 270 | .accessibilityTextHeadingLevel, // iOS 11.0+ 271 | ] 272 | #endif 273 | 274 | #if canImport(AppKit) && !targetEnvironment(UIKitForMac) 275 | let macOSKeys: [NSAttributedString.Key] = [ 276 | .accessibilityAlignment, // macOS 10.12+ 277 | .accessibilityAnnotationTextAttribute, // macOS 10.13+ 278 | .accessibilityAttachment, // macOS 10.4+, Deprecated 279 | .accessibilityAutocorrected, // macOS 10.7+ 280 | .accessibilityBackgroundColor, // macOS 10.4+ 281 | .accessibilityCustomText, // macOS 10.13+ 282 | .accessibilityFont, // macOS 10.4+ 283 | .accessibilityForegroundColor, // macOS 10.4+ 284 | .accessibilityLanguage, // macOS 10.13+ 285 | .accessibilityLink, // macOS 10.4+ 286 | .accessibilityListItemIndex, // macOS 10.11+ 287 | .accessibilityListItemLevel, // macOS 10.11+ 288 | .accessibilityListItemPrefix, // macOS 10.11+ 289 | .accessibilityMarkedMisspelled, // macOS 10.4+ 290 | .accessibilityMisspelled, // macOS 10.4+ 291 | .accessibilityShadow, // macOS 10.4+ 292 | .accessibilityStrikethrough, // macOS 10.4+ 293 | .accessibilityStrikethroughColor, // macOS 10.4+ 294 | .accessibilitySuperscript, // macOS 10.4+ 295 | .accessibilityUnderline, // macOS 10.4+ 296 | .accessibilityUnderlineColor, // macOS 10.4+ 297 | .cursor, // macOS 10.3+ 298 | .glyphInfo, // macOS 10.2+ 299 | .markedClauseSegment, // macOS 10.5+ 300 | .spellingState, // macOS 10.5+ 301 | .superscript, // macOS 10.0+ 302 | .textAlternatives, // macOS 10.8+ 303 | .toolTip, // macOS 10.3+ 304 | ] 305 | #endif 306 | */ 307 | -------------------------------------------------------------------------------- /Tests/NSAttributedStringBuilderTests/ComponentBasicModifierTests.swift: -------------------------------------------------------------------------------- 1 | @testable import NSAttributedStringBuilder 2 | import XCTest 3 | 4 | final class ComponentBasicModifierTests: XCTestCase { 5 | func testModifyWithSingleAttribute() { 6 | let testData: NSAttributedString = { 7 | let mas = NSMutableAttributedString(string: "Hello world", 8 | attributes: [.foregroundColor: Color.yellow]) 9 | mas.append(NSAttributedString(string: " with Swift")) 10 | return mas 11 | }() 12 | 13 | let sut = NSAttributedString { 14 | AText("Hello world") 15 | .attribute(.foregroundColor, value: Color.yellow) 16 | AText(" with Swift") 17 | } 18 | 19 | XCTAssertEqual(sut, testData) 20 | } 21 | 22 | func testModifyBackgroundColor() { 23 | let testData: NSAttributedString = { 24 | let mas = NSMutableAttributedString(string: "Hello world", 25 | attributes: [.backgroundColor: Color.red]) 26 | mas.append(NSAttributedString(string: " with Swift")) 27 | return mas 28 | }() 29 | 30 | let sut = NSAttributedString { 31 | AText("Hello world") 32 | .backgroundColor(.red) 33 | AText(" with Swift") 34 | } 35 | 36 | XCTAssertEqual(sut, testData) 37 | } 38 | 39 | func testModifyBaselineOffset() { 40 | let testData: NSAttributedString = { 41 | let mas = NSMutableAttributedString(string: "Hello world", 42 | attributes: [.baselineOffset: 10]) 43 | mas.append(NSAttributedString(string: " with Swift")) 44 | return mas 45 | }() 46 | 47 | let sut = NSAttributedString { 48 | AText("Hello world") 49 | .baselineOffset(10) 50 | AText(" with Swift") 51 | } 52 | 53 | XCTAssertEqual(sut, testData) 54 | } 55 | 56 | func testModifyFontAndColor() { 57 | let testData: NSAttributedString = { 58 | let mas = NSMutableAttributedString(string: "") 59 | mas.append(NSAttributedString(string: "Hello world", 60 | attributes: [ 61 | .font: Font.systemFont(ofSize: 20), 62 | .foregroundColor: Color.yellow, 63 | ])) 64 | mas.append(NSAttributedString(string: "\n")) 65 | mas.append(NSAttributedString(string: "Second line", 66 | attributes: [.font: Font.systemFont(ofSize: 24)])) 67 | return mas 68 | }() 69 | 70 | let sut = NSAttributedString { 71 | AText("Hello world") 72 | .font(.systemFont(ofSize: 20)) 73 | .foregroundColor(.yellow) 74 | LineBreak() 75 | AText("Second line") 76 | .font(.systemFont(ofSize: 24)) 77 | } 78 | 79 | XCTAssertEqual(sut, testData) 80 | } 81 | 82 | func testModifyExpansion() { 83 | let testData: NSAttributedString = { 84 | let mas = NSMutableAttributedString(string: "Hello world", 85 | attributes: [.expansion: 1]) 86 | mas.append(NSAttributedString(string: " with Swift")) 87 | return mas 88 | }() 89 | 90 | let sut = NSAttributedString { 91 | AText("Hello world") 92 | .expansion(1) 93 | AText(" with Swift") 94 | } 95 | 96 | XCTAssertEqual(sut, testData) 97 | } 98 | 99 | func testModifyKerning() { 100 | let testData: NSAttributedString = { 101 | let mas = NSMutableAttributedString(string: "Hello world", 102 | attributes: [.kern: 3]) 103 | mas.append(NSAttributedString(string: " with Swift")) 104 | return mas 105 | }() 106 | 107 | let sut = NSAttributedString { 108 | AText("Hello world") 109 | .kerning(3) 110 | AText(" with Swift") 111 | } 112 | 113 | XCTAssertEqual(sut, testData) 114 | } 115 | 116 | func testModifyLigature() { 117 | let testData: NSAttributedString = { 118 | let mas = NSMutableAttributedString(string: "Hello world", 119 | attributes: [.ligature: 0]) 120 | mas.append(NSAttributedString(string: " with Swift")) 121 | return mas 122 | }() 123 | 124 | let sut = NSAttributedString { 125 | AText("Hello world") 126 | .ligature(.none) 127 | AText(" with Swift") 128 | } 129 | 130 | XCTAssertEqual(sut, testData) 131 | } 132 | 133 | func testModifyObliqueness() { 134 | let testData: NSAttributedString = { 135 | let mas = NSMutableAttributedString(string: "Hello world", 136 | attributes: [.obliqueness: 0.5]) 137 | mas.append(NSAttributedString(string: " with Swift")) 138 | return mas 139 | }() 140 | 141 | let sut = NSAttributedString { 142 | AText("Hello world") 143 | .obliqueness(0.5) 144 | AText(" with Swift") 145 | } 146 | 147 | XCTAssertEqual(sut, testData) 148 | } 149 | 150 | func testModifyShadow() { 151 | let testData: NSAttributedString = { 152 | let shadow = NSShadow() 153 | shadow.shadowColor = Color.black 154 | shadow.shadowBlurRadius = 10 155 | shadow.shadowOffset = .init(width: 4, height: 4) 156 | 157 | let mas = NSMutableAttributedString(string: "Hello world", 158 | attributes: [.shadow: shadow]) 159 | mas.append(NSAttributedString(string: " with Swift")) 160 | return mas 161 | }() 162 | 163 | let sut = NSAttributedString { 164 | AText("Hello world") 165 | .shadow(color: .black, radius: 10, x: 4, y: 4) 166 | AText(" with Swift") 167 | } 168 | 169 | XCTAssertEqual(sut, testData) 170 | } 171 | 172 | func testModifyStrikethrough() { 173 | let testData: NSAttributedString = { 174 | let mas = NSMutableAttributedString(string: "Hello world", 175 | attributes: [.strikethroughStyle: NSUnderlineStyle.double.rawValue]) 176 | mas.append(NSAttributedString(string: " with Swift")) 177 | return mas 178 | }() 179 | 180 | let sut = NSAttributedString { 181 | AText("Hello world") 182 | .strikethrough(style: .double) 183 | AText(" with Swift") 184 | } 185 | 186 | XCTAssertEqual(sut, testData) 187 | } 188 | 189 | func testModifyStrikethroughWithColor() { 190 | let testData: NSAttributedString = { 191 | let mas = NSMutableAttributedString(string: "Hello world", 192 | attributes: [.strikethroughStyle: NSUnderlineStyle.patternDash.rawValue, 193 | .strikethroughColor: Color.black]) 194 | mas.append(NSAttributedString(string: " with Swift")) 195 | return mas 196 | }() 197 | 198 | let sut = NSAttributedString { 199 | AText("Hello world") 200 | .strikethrough(style: .patternDash, color: .black) 201 | AText(" with Swift") 202 | } 203 | 204 | XCTAssertEqual(sut, testData) 205 | } 206 | 207 | func testModifyStroke() { 208 | let testData: NSAttributedString = { 209 | let mas = NSMutableAttributedString(string: "Hello world", 210 | attributes: [.strokeWidth: -2]) 211 | mas.append(NSAttributedString(string: " with Swift")) 212 | return mas 213 | }() 214 | 215 | let sut = NSAttributedString { 216 | AText("Hello world") 217 | .stroke(width: -2) 218 | AText(" with Swift") 219 | } 220 | 221 | XCTAssertEqual(sut, testData) 222 | } 223 | 224 | func testModifyStrokeWithColor() { 225 | let testData: NSAttributedString = { 226 | let mas = NSMutableAttributedString(string: "Hello world", 227 | attributes: [.strokeWidth: -2, 228 | .strokeColor: Color.green]) 229 | mas.append(NSAttributedString(string: " with Swift")) 230 | return mas 231 | }() 232 | 233 | let sut = NSAttributedString { 234 | AText("Hello world") 235 | .stroke(width: -2, color: .green) 236 | AText(" with Swift") 237 | } 238 | 239 | XCTAssertEqual(sut, testData) 240 | } 241 | 242 | func testModifyTextEffect() { 243 | let testData: NSAttributedString = { 244 | let mas = NSMutableAttributedString(string: "Hello world", 245 | attributes: [.textEffect: NSAttributedString.TextEffectStyle.letterpressStyle]) 246 | mas.append(NSAttributedString(string: " with Swift")) 247 | return mas 248 | }() 249 | 250 | let sut = NSAttributedString { 251 | AText("Hello world") 252 | .textEffect(.letterpressStyle) 253 | AText(" with Swift") 254 | } 255 | 256 | XCTAssertEqual(sut, testData) 257 | } 258 | 259 | func testModifyUnderline() { 260 | let testData: NSAttributedString = { 261 | let mas = NSMutableAttributedString(string: "Hello world", 262 | attributes: [.underlineStyle: NSUnderlineStyle.patternDashDotDot.rawValue]) 263 | mas.append(NSAttributedString(string: " with Swift")) 264 | return mas 265 | }() 266 | 267 | let sut = NSAttributedString { 268 | AText("Hello world") 269 | .underline(.patternDashDotDot) 270 | AText(" with Swift") 271 | } 272 | 273 | XCTAssertEqual(sut, testData) 274 | } 275 | 276 | func testModifyUnderlineWithColor() { 277 | let testData: NSAttributedString = { 278 | let mas = NSMutableAttributedString(string: "Hello world", 279 | attributes: [.underlineStyle: NSUnderlineStyle.patternDashDotDot.rawValue, 280 | .underlineColor: Color.cyan]) 281 | mas.append(NSAttributedString(string: " with Swift")) 282 | return mas 283 | }() 284 | 285 | let sut = NSAttributedString { 286 | AText("Hello world") 287 | .underline(.patternDashDotDot, color: .cyan) 288 | AText(" with Swift") 289 | } 290 | 291 | XCTAssertEqual(sut, testData) 292 | } 293 | 294 | func testModifyWritingDirection() { 295 | let testData: NSAttributedString = { 296 | let mas = NSMutableAttributedString(string: "Hello world", 297 | attributes: [.writingDirection: NSWritingDirection.rightToLeft.rawValue]) 298 | mas.append(NSAttributedString(string: " with Swift")) 299 | return mas 300 | }() 301 | 302 | let sut = NSAttributedString { 303 | AText("Hello world") 304 | .writingDirection(.rightToLeft) 305 | AText(" with Swift") 306 | } 307 | 308 | XCTAssertEqual(sut, testData) 309 | } 310 | 311 | #if canImport(AppKit) 312 | func testModifyVertical() { 313 | let testData: NSAttributedString = { 314 | let mas = NSMutableAttributedString(string: "Hello world", 315 | attributes: [.verticalGlyphForm: 1]) 316 | mas.append(NSAttributedString(string: " with Swift")) 317 | return mas 318 | }() 319 | 320 | let sut = NSAttributedString { 321 | AText("Hello world") 322 | .vertical() 323 | AText(" with Swift") 324 | } 325 | 326 | XCTAssertEqual(sut, testData) 327 | } 328 | #endif 329 | 330 | func testChaining() { 331 | let testData: NSAttributedString = { 332 | let shadow = NSShadow() 333 | shadow.shadowColor = Color.black 334 | shadow.shadowBlurRadius = 10 335 | shadow.shadowOffset = .init(width: 4, height: 4) 336 | 337 | let mas = NSMutableAttributedString( 338 | string: "Hello world", 339 | attributes: [.backgroundColor: Color.red, 340 | .baselineOffset: 10, 341 | .font: Font.systemFont(ofSize: 20), 342 | .foregroundColor: Color.yellow, 343 | .expansion: 1, 344 | .kern: 3, 345 | .ligature: 0, 346 | .obliqueness: 0.5, 347 | .shadow: shadow, 348 | .strikethroughStyle: NSUnderlineStyle.patternDash.rawValue, 349 | .strikethroughColor: Color.black, 350 | .strokeWidth: -2, 351 | .strokeColor: Color.green, 352 | .textEffect: NSAttributedString.TextEffectStyle.letterpressStyle, 353 | .underlineStyle: NSUnderlineStyle.patternDashDotDot.rawValue, 354 | .underlineColor: Color.cyan, 355 | .writingDirection: NSWritingDirection.rightToLeft.rawValue] 356 | ) 357 | mas.append(NSAttributedString(string: " with Swift")) 358 | return mas 359 | }() 360 | 361 | let sut = NSAttributedString { 362 | AText("Hello world") 363 | .backgroundColor(.red) 364 | .baselineOffset(10) 365 | .font(.systemFont(ofSize: 20)) 366 | .foregroundColor(.yellow) 367 | .expansion(1) 368 | .kerning(3) 369 | .ligature(.none) 370 | .obliqueness(0.5) 371 | .shadow(color: .black, radius: 10, x: 4, y: 4) 372 | .strikethrough(style: .patternDash, color: .black) 373 | .stroke(width: -2, color: .green) 374 | .textEffect(.letterpressStyle) 375 | .underline(.patternDashDotDot, color: .cyan) 376 | .writingDirection(.rightToLeft) 377 | AText(" with Swift") 378 | } 379 | 380 | XCTAssertEqual(sut, testData) 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /Tests/NSAttributedStringBuilderTests/ComponentParagraphSylteModifierTests.swift: -------------------------------------------------------------------------------- 1 | @testable import NSAttributedStringBuilder 2 | import XCTest 3 | 4 | #if canImport(UIKit) 5 | import Foundation 6 | import UIKit 7 | #elseif canImport(AppKit) 8 | import AppKit 9 | #endif 10 | 11 | final class ComponentParagraphSylteModifierTests: XCTestCase { 12 | func testSetEmptyParagraphStyle() { 13 | let testData: NSAttributedString = { 14 | let ps = NSParagraphStyle() 15 | let mas = NSMutableAttributedString(string: "Hello world", 16 | attributes: [.paragraphStyle: ps]) 17 | mas.append(NSAttributedString(string: " with Swift")) 18 | return mas 19 | }() 20 | 21 | let ps = NSParagraphStyle() 22 | 23 | let sut = NSAttributedString { 24 | AText("Hello world") 25 | .paragraphStyle(ps) 26 | AText(" with Swift") 27 | } 28 | 29 | XCTAssertEqual(sut, testData) 30 | } 31 | 32 | func testModifyMutableParagraphStyle() { 33 | let testData: NSAttributedString = { 34 | let paragraphStyle = NSMutableParagraphStyle() 35 | paragraphStyle.alignment = .right 36 | 37 | let mas = NSMutableAttributedString(string: "Hello world", 38 | attributes: [.paragraphStyle: paragraphStyle]) 39 | mas.append(NSAttributedString(string: " with Swift")) 40 | return mas 41 | }() 42 | 43 | let mps = NSMutableParagraphStyle() 44 | mps.alignment = .right 45 | 46 | let sut = NSAttributedString { 47 | AText("Hello world") 48 | .paragraphStyle(mps) 49 | AText(" with Swift") 50 | } 51 | 52 | XCTAssertEqual(sut, testData) 53 | } 54 | 55 | func testModifyAlignment() { 56 | let testData: NSAttributedString = { 57 | let paragraphStyle = NSMutableParagraphStyle() 58 | paragraphStyle.alignment = .right 59 | 60 | let mas = NSMutableAttributedString(string: "Hello world", 61 | attributes: [.paragraphStyle: paragraphStyle]) 62 | mas.append(NSAttributedString(string: " with Swift")) 63 | return mas 64 | }() 65 | 66 | let sut = NSAttributedString { 67 | AText("Hello world") 68 | .alignment(.right) 69 | AText(" with Swift") 70 | } 71 | 72 | XCTAssertEqual(sut, testData) 73 | } 74 | 75 | func testModifyFirstHeadIndent() { 76 | let testData: NSAttributedString = { 77 | let paragraphStyle = NSMutableParagraphStyle() 78 | paragraphStyle.firstLineHeadIndent = 16 79 | 80 | let mas = NSMutableAttributedString(string: "Hello world", 81 | attributes: [.paragraphStyle: paragraphStyle]) 82 | mas.append(NSAttributedString(string: " with Swift")) 83 | return mas 84 | }() 85 | 86 | let sut = NSAttributedString { 87 | AText("Hello world") 88 | .firstLineHeadIndent(16) 89 | AText(" with Swift") 90 | } 91 | 92 | XCTAssertEqual(sut, testData) 93 | } 94 | 95 | func testModifyHeadIndent() { 96 | let testData: NSAttributedString = { 97 | let paragraphStyle = NSMutableParagraphStyle() 98 | paragraphStyle.headIndent = 13 99 | 100 | let mas = NSMutableAttributedString(string: "Hello world", 101 | attributes: [.paragraphStyle: paragraphStyle]) 102 | mas.append(NSAttributedString(string: " with Swift")) 103 | return mas 104 | }() 105 | 106 | let sut = NSAttributedString { 107 | AText("Hello world") 108 | .headIndent(13) 109 | AText(" with Swift") 110 | } 111 | 112 | XCTAssertEqual(sut, testData) 113 | } 114 | 115 | func testModifyTailIndent() { 116 | let testData: NSAttributedString = { 117 | let paragraphStyle = NSMutableParagraphStyle() 118 | paragraphStyle.tailIndent = 19 119 | 120 | let mas = NSMutableAttributedString(string: "Hello world", 121 | attributes: [.paragraphStyle: paragraphStyle]) 122 | mas.append(NSAttributedString(string: " with Swift")) 123 | return mas 124 | }() 125 | 126 | let sut = NSAttributedString { 127 | AText("Hello world") 128 | .tailIndent(19) 129 | AText(" with Swift") 130 | } 131 | 132 | XCTAssertEqual(sut, testData) 133 | } 134 | 135 | func testModifyLinebreakMode() { 136 | let testData: NSAttributedString = { 137 | let paragraphStyle = NSMutableParagraphStyle() 138 | paragraphStyle.lineBreakMode = .byWordWrapping 139 | 140 | let mas = NSMutableAttributedString(string: "Hello world", 141 | attributes: [.paragraphStyle: paragraphStyle]) 142 | mas.append(NSAttributedString(string: " with Swift")) 143 | return mas 144 | }() 145 | 146 | let sut = NSAttributedString { 147 | AText("Hello world") 148 | .lineBreakeMode(.byWordWrapping) 149 | AText(" with Swift") 150 | } 151 | 152 | XCTAssertEqual(sut, testData) 153 | } 154 | 155 | func testModifyLineHeight() { 156 | let testData: NSAttributedString = { 157 | let paragraphStyle = NSMutableParagraphStyle() 158 | paragraphStyle.lineHeightMultiple = 1 159 | paragraphStyle.maximumLineHeight = 22 160 | paragraphStyle.minimumLineHeight = 18 161 | 162 | let mas = NSMutableAttributedString(string: "Hello world", 163 | attributes: [.paragraphStyle: paragraphStyle]) 164 | mas.append(NSAttributedString(string: " with Swift")) 165 | return mas 166 | }() 167 | 168 | let sut = NSAttributedString { 169 | AText("Hello world") 170 | .lineHeight(multiple: 1, maximum: 22, minimum: 18) 171 | AText(" with Swift") 172 | } 173 | 174 | XCTAssertEqual(sut, testData) 175 | } 176 | 177 | func testModifyLineSpacing() { 178 | let testData: NSAttributedString = { 179 | let paragraphStyle = NSMutableParagraphStyle() 180 | paragraphStyle.lineSpacing = 7 181 | 182 | let mas = NSMutableAttributedString(string: "Hello world", 183 | attributes: [.paragraphStyle: paragraphStyle]) 184 | mas.append(NSAttributedString(string: " with Swift")) 185 | return mas 186 | }() 187 | 188 | let sut = NSAttributedString { 189 | AText("Hello world") 190 | .lineSpacing(7) 191 | AText(" with Swift") 192 | } 193 | 194 | XCTAssertEqual(sut, testData) 195 | } 196 | 197 | func testModifyParagraphSpacing() { 198 | let testData: NSAttributedString = { 199 | let paragraphStyle = NSMutableParagraphStyle() 200 | paragraphStyle.paragraphSpacing = 9.3 201 | paragraphStyle.paragraphSpacingBefore = 17.2 202 | 203 | let mas = NSMutableAttributedString(string: "Hello world", 204 | attributes: [.paragraphStyle: paragraphStyle]) 205 | mas.append(NSAttributedString(string: " with Swift")) 206 | return mas 207 | }() 208 | 209 | let sut = NSAttributedString { 210 | AText("Hello world") 211 | .paragraphSpacing(9.3, before: 17.2) 212 | AText(" with Swift") 213 | } 214 | 215 | XCTAssertEqual(sut, testData) 216 | } 217 | 218 | func testModifyBaseWritingDirection() { 219 | let testData: NSAttributedString = { 220 | let paragraphStyle = NSMutableParagraphStyle() 221 | paragraphStyle.baseWritingDirection = .rightToLeft 222 | 223 | let mas = NSMutableAttributedString(string: "Hello world", 224 | attributes: [.paragraphStyle: paragraphStyle]) 225 | mas.append(NSAttributedString(string: " with Swift")) 226 | return mas 227 | }() 228 | 229 | let sut = NSAttributedString { 230 | AText("Hello world") 231 | .baseWritingDirection(.rightToLeft) 232 | AText(" with Swift") 233 | } 234 | 235 | XCTAssertEqual(sut, testData) 236 | } 237 | 238 | func testModifyHyphenationFactor() { 239 | let testData: NSAttributedString = { 240 | let paragraphStyle = NSMutableParagraphStyle() 241 | paragraphStyle.hyphenationFactor = 0.3 242 | 243 | let mas = NSMutableAttributedString(string: "Hello world", 244 | attributes: [.paragraphStyle: paragraphStyle]) 245 | mas.append(NSAttributedString(string: " with Swift")) 246 | return mas 247 | }() 248 | 249 | let sut = NSAttributedString { 250 | AText("Hello world") 251 | .hyphenationFactor(0.3) 252 | AText(" with Swift") 253 | } 254 | 255 | XCTAssertEqual(sut, testData) 256 | } 257 | 258 | @available(iOS 9.0, tvOS 9.0, watchOS 2.0, OSX 10.11, *) 259 | func testAllowsDefaultTighteningForTruncation() { 260 | let testData: NSAttributedString = { 261 | let paragraphStyle = NSMutableParagraphStyle() 262 | paragraphStyle.allowsDefaultTighteningForTruncation = true 263 | 264 | let mas = NSMutableAttributedString(string: "Hello world", 265 | attributes: [.paragraphStyle: paragraphStyle]) 266 | mas.append(NSAttributedString(string: " with Swift")) 267 | return mas 268 | }() 269 | 270 | let sut = NSAttributedString { 271 | AText("Hello world") 272 | .allowsDefaultTighteningForTruncation() 273 | AText(" with Swift") 274 | } 275 | 276 | XCTAssertEqual(sut, testData) 277 | } 278 | 279 | func testModifyTabStops() { 280 | let testData: NSAttributedString = { 281 | let paragraphStyle = NSMutableParagraphStyle() 282 | paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: 6, options: [:]), 283 | NSTextTab(textAlignment: .right, location: 4, options: [:])] 284 | 285 | let mas = NSMutableAttributedString(string: "Hello world", 286 | attributes: [.paragraphStyle: paragraphStyle]) 287 | mas.append(NSAttributedString(string: " with Swift")) 288 | return mas 289 | }() 290 | 291 | let sut = NSAttributedString { 292 | AText("Hello world") 293 | .tabsStops([NSTextTab(textAlignment: .left, location: 6, options: [:]), 294 | NSTextTab(textAlignment: .right, location: 4, options: [:])]) 295 | AText(" with Swift") 296 | } 297 | 298 | XCTAssertEqual(sut, testData) 299 | } 300 | 301 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 302 | 303 | func testModifyTextBlocks() { 304 | let textBlock = NSTextBlock() 305 | textBlock.setWidth(30, type: .absoluteValueType, for: .border) 306 | 307 | let testData: NSAttributedString = { 308 | let paragraphStyle = NSMutableParagraphStyle() 309 | paragraphStyle.textBlocks = [textBlock] 310 | 311 | let mas = NSMutableAttributedString(string: "Hello world", 312 | attributes: [.paragraphStyle: paragraphStyle]) 313 | mas.append(NSAttributedString(string: " with Swift")) 314 | return mas 315 | }() 316 | 317 | let sut = NSAttributedString { 318 | AText("Hello world") 319 | .textBlocks([textBlock]) 320 | AText(" with Swift") 321 | } 322 | 323 | XCTAssertEqual(sut, testData) 324 | } 325 | 326 | @available(OSX 10.13, *) 327 | func testModifyTextList() { 328 | let textList = NSTextList(markerFormat: .box, options: 0) 329 | 330 | let testData: NSAttributedString = { 331 | let paragraphStyle = NSMutableParagraphStyle() 332 | paragraphStyle.textLists = [textList] 333 | 334 | let mas = NSMutableAttributedString(string: "Hello world", 335 | attributes: [.paragraphStyle: paragraphStyle]) 336 | mas.append(NSAttributedString(string: " with Swift")) 337 | return mas 338 | }() 339 | 340 | let sut = NSAttributedString { 341 | AText("Hello world") 342 | .textLists([textList]) 343 | AText(" with Swift") 344 | } 345 | 346 | XCTAssertEqual(sut, testData) 347 | } 348 | 349 | func testModifyTighteningFactorForTruncation() { 350 | let testData: NSAttributedString = { 351 | let paragraphStyle = NSMutableParagraphStyle() 352 | paragraphStyle.tighteningFactorForTruncation = 0.5 353 | 354 | let mas = NSMutableAttributedString(string: "Hello world", 355 | attributes: [.paragraphStyle: paragraphStyle]) 356 | mas.append(NSAttributedString(string: " with Swift")) 357 | return mas 358 | }() 359 | 360 | let sut = NSAttributedString { 361 | AText("Hello world") 362 | .tighteningFactorForTruncation(0.5) 363 | AText(" with Swift") 364 | } 365 | 366 | XCTAssertEqual(sut, testData) 367 | } 368 | 369 | func testModifyHeaderLevel() { 370 | let testData: NSAttributedString = { 371 | let paragraphStyle = NSMutableParagraphStyle() 372 | paragraphStyle.headerLevel = 2 373 | 374 | let mas = NSMutableAttributedString(string: "Hello world", 375 | attributes: [.paragraphStyle: paragraphStyle]) 376 | mas.append(NSAttributedString(string: " with Swift")) 377 | return mas 378 | }() 379 | 380 | let sut = NSAttributedString { 381 | AText("Hello world") 382 | .headerLevel(2) 383 | AText(" with Swift") 384 | } 385 | 386 | XCTAssertEqual(sut, testData) 387 | } 388 | #endif 389 | 390 | @available(iOS 9.0, tvOS 9.0, watchOS 2.0, OSX 10.11, *) 391 | func testChaining() { 392 | let testData: NSAttributedString = { 393 | let paragraphStyle = NSMutableParagraphStyle() 394 | paragraphStyle.alignment = .right 395 | paragraphStyle.firstLineHeadIndent = 16 396 | paragraphStyle.headIndent = 13 397 | paragraphStyle.tailIndent = 19 398 | paragraphStyle.lineBreakMode = .byWordWrapping 399 | paragraphStyle.lineHeightMultiple = 1 400 | paragraphStyle.maximumLineHeight = 22 401 | paragraphStyle.minimumLineHeight = 18 402 | paragraphStyle.lineSpacing = 7 403 | paragraphStyle.paragraphSpacing = 9.3 404 | paragraphStyle.paragraphSpacingBefore = 17.2 405 | paragraphStyle.baseWritingDirection = .rightToLeft 406 | paragraphStyle.hyphenationFactor = 0.3 407 | paragraphStyle.allowsDefaultTighteningForTruncation = true 408 | 409 | let mas = NSMutableAttributedString(string: "Hello world", 410 | attributes: [.paragraphStyle: paragraphStyle]) 411 | mas.append(NSAttributedString(string: " with Swift")) 412 | return mas 413 | }() 414 | 415 | let sut = NSAttributedString { 416 | AText("Hello world") 417 | .alignment(.right) 418 | .firstLineHeadIndent(16) 419 | .headIndent(13) 420 | .tailIndent(19) 421 | .lineBreakeMode(.byWordWrapping) 422 | .lineHeight(multiple: 1, maximum: 22, minimum: 18) 423 | .lineSpacing(7) 424 | .paragraphSpacing(9.3, before: 17.2) 425 | .baseWritingDirection(.rightToLeft) 426 | .hyphenationFactor(0.3) 427 | .allowsDefaultTighteningForTruncation() 428 | AText(" with Swift") 429 | } 430 | 431 | XCTAssertEqual(sut, testData) 432 | } 433 | 434 | @available(iOS 9.0, tvOS 9.0, watchOS 2.0, OSX 10.11, *) 435 | func testRandomChainingOrderEqualness() { 436 | let sut = NSAttributedString { 437 | AText("Hello world") 438 | .alignment(.right) 439 | .firstLineHeadIndent(16) 440 | .headIndent(13) 441 | .tailIndent(19) 442 | .lineBreakeMode(.byWordWrapping) 443 | .lineHeight(multiple: 1, maximum: 22, minimum: 18) 444 | .lineSpacing(7) 445 | .paragraphSpacing(9.3, before: 17.2) 446 | .baseWritingDirection(.rightToLeft) 447 | .hyphenationFactor(0.3) 448 | .allowsDefaultTighteningForTruncation() 449 | AText(" with Swift") 450 | } 451 | 452 | let sut2 = NSAttributedString { 453 | AText("Hello world") 454 | .firstLineHeadIndent(16) 455 | .headIndent(13) 456 | .alignment(.right) 457 | .allowsDefaultTighteningForTruncation() 458 | .tailIndent(19) 459 | .lineSpacing(7) 460 | .lineBreakeMode(.byWordWrapping) 461 | .hyphenationFactor(0.3) 462 | .lineHeight(multiple: 1, maximum: 22, minimum: 18) 463 | .paragraphSpacing(9.3, before: 17.2) 464 | .baseWritingDirection(.rightToLeft) 465 | AText(" with Swift") 466 | } 467 | 468 | XCTAssertTrue(sut.isEqual(sut2)) 469 | } 470 | 471 | func testSetEmptyParagraphStyleThenChaining() { 472 | let testData: NSAttributedString = { 473 | let mps = NSMutableParagraphStyle() 474 | mps.alignment = .justified 475 | let mas = NSMutableAttributedString(string: "Hello world", 476 | attributes: [.paragraphStyle: mps]) 477 | mas.append(NSAttributedString(string: " with Swift")) 478 | return mas 479 | }() 480 | 481 | let ps = NSParagraphStyle() 482 | 483 | let sut = NSAttributedString { 484 | AText("Hello world") 485 | .paragraphStyle(ps) 486 | .alignment(.justified) 487 | AText(" with Swift") 488 | } 489 | 490 | XCTAssertEqual(sut, testData) 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /SwiftUISampleApp/AttributedTextSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6C4BCC9222D74C9700A4F8D7 /* ImageAttachmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4BCC9122D74C9700A4F8D7 /* ImageAttachmentTests.swift */; }; 11 | 6C4BCC9322D74CAF00A4F8D7 /* Swift_logo_color_rgb.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 6C4BCC8122D74C0A00A4F8D7 /* Swift_logo_color_rgb.jpg */; }; 12 | 6C4BCC9422D74D8200A4F8D7 /* Swift_logo_color_rgb.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 6C4BCC8122D74C0A00A4F8D7 /* Swift_logo_color_rgb.jpg */; }; 13 | 6C65EA2822D489A300EF32FB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C65EA2722D489A300EF32FB /* AppDelegate.swift */; }; 14 | 6C65EA2A22D489A300EF32FB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C65EA2922D489A300EF32FB /* SceneDelegate.swift */; }; 15 | 6C65EA2C22D489A300EF32FB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C65EA2B22D489A300EF32FB /* ContentView.swift */; }; 16 | 6C65EA2E22D489A400EF32FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6C65EA2D22D489A400EF32FB /* Assets.xcassets */; }; 17 | 6C65EA3122D489A400EF32FB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6C65EA3022D489A400EF32FB /* Preview Assets.xcassets */; }; 18 | 6C65EA3422D489A400EF32FB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6C65EA3222D489A400EF32FB /* LaunchScreen.storyboard */; }; 19 | 6CF300AA22D48AC200D8C7DF /* NSAttributedStringBuilder in Frameworks */ = {isa = PBXBuildFile; productRef = 6CF300A922D48AC200D8C7DF /* NSAttributedStringBuilder */; }; 20 | 6CF300B522D4A36600D8C7DF /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF300B422D4A36600D8C7DF /* AttributedText.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 6C4BCC8C22D74C8D00A4F8D7 /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 6C65EA1C22D489A200EF32FB /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = 6C65EA2322D489A300EF32FB; 29 | remoteInfo = AttributedTextSample; 30 | }; 31 | /* End PBXContainerItemProxy section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 6C456A1122D6EEAE00A02325 /* NSAttributedStringBuilder */ = {isa = PBXFileReference; lastKnownFileType = folder; name = NSAttributedStringBuilder; path = ..; sourceTree = ""; }; 35 | 6C4BCC8122D74C0A00A4F8D7 /* Swift_logo_color_rgb.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Swift_logo_color_rgb.jpg; sourceTree = ""; }; 36 | 6C4BCC8722D74C8D00A4F8D7 /* AttributedTextSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AttributedTextSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 6C4BCC8B22D74C8D00A4F8D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 38 | 6C4BCC9122D74C9700A4F8D7 /* ImageAttachmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAttachmentTests.swift; sourceTree = ""; }; 39 | 6C65EA2422D489A300EF32FB /* AttributedTextSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AttributedTextSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | 6C65EA2722D489A300EF32FB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 41 | 6C65EA2922D489A300EF32FB /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 42 | 6C65EA2B22D489A300EF32FB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 43 | 6C65EA2D22D489A400EF32FB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 44 | 6C65EA3022D489A400EF32FB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 45 | 6C65EA3322D489A400EF32FB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 46 | 6C65EA3522D489A400EF32FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 47 | 6CF300AB22D48EA900D8C7DF /* AttributedTextSample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AttributedTextSample.entitlements; sourceTree = ""; }; 48 | 6CF300B422D4A36600D8C7DF /* AttributedText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedText.swift; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | 6C4BCC8422D74C8D00A4F8D7 /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | 6C65EA2122D489A300EF32FB /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | 6CF300AA22D48AC200D8C7DF /* NSAttributedStringBuilder in Frameworks */, 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | /* End PBXFrameworksBuildPhase section */ 68 | 69 | /* Begin PBXGroup section */ 70 | 6C4BCC8822D74C8D00A4F8D7 /* AttributedTextSampleTests */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 6C4BCC9122D74C9700A4F8D7 /* ImageAttachmentTests.swift */, 74 | 6C4BCC8122D74C0A00A4F8D7 /* Swift_logo_color_rgb.jpg */, 75 | 6C4BCC8B22D74C8D00A4F8D7 /* Info.plist */, 76 | ); 77 | path = AttributedTextSampleTests; 78 | sourceTree = ""; 79 | }; 80 | 6C65EA1B22D489A200EF32FB = { 81 | isa = PBXGroup; 82 | children = ( 83 | 6C456A1122D6EEAE00A02325 /* NSAttributedStringBuilder */, 84 | 6C65EA2622D489A300EF32FB /* AttributedTextSample */, 85 | 6C4BCC8822D74C8D00A4F8D7 /* AttributedTextSampleTests */, 86 | 6C65EA2522D489A300EF32FB /* Products */, 87 | 6CF300A822D48AC200D8C7DF /* Frameworks */, 88 | ); 89 | sourceTree = ""; 90 | }; 91 | 6C65EA2522D489A300EF32FB /* Products */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 6C65EA2422D489A300EF32FB /* AttributedTextSample.app */, 95 | 6C4BCC8722D74C8D00A4F8D7 /* AttributedTextSampleTests.xctest */, 96 | ); 97 | name = Products; 98 | sourceTree = ""; 99 | }; 100 | 6C65EA2622D489A300EF32FB /* AttributedTextSample */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 6CF300AB22D48EA900D8C7DF /* AttributedTextSample.entitlements */, 104 | 6CF300B422D4A36600D8C7DF /* AttributedText.swift */, 105 | 6C65EA2722D489A300EF32FB /* AppDelegate.swift */, 106 | 6C65EA2922D489A300EF32FB /* SceneDelegate.swift */, 107 | 6C65EA2B22D489A300EF32FB /* ContentView.swift */, 108 | 6C65EA2D22D489A400EF32FB /* Assets.xcassets */, 109 | 6C65EA3222D489A400EF32FB /* LaunchScreen.storyboard */, 110 | 6C65EA3522D489A400EF32FB /* Info.plist */, 111 | 6C65EA2F22D489A400EF32FB /* Preview Content */, 112 | ); 113 | path = AttributedTextSample; 114 | sourceTree = ""; 115 | }; 116 | 6C65EA2F22D489A400EF32FB /* Preview Content */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 6C65EA3022D489A400EF32FB /* Preview Assets.xcassets */, 120 | ); 121 | path = "Preview Content"; 122 | sourceTree = ""; 123 | }; 124 | 6CF300A822D48AC200D8C7DF /* Frameworks */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | ); 128 | name = Frameworks; 129 | sourceTree = ""; 130 | }; 131 | /* End PBXGroup section */ 132 | 133 | /* Begin PBXNativeTarget section */ 134 | 6C4BCC8622D74C8D00A4F8D7 /* AttributedTextSampleTests */ = { 135 | isa = PBXNativeTarget; 136 | buildConfigurationList = 6C4BCC8E22D74C8D00A4F8D7 /* Build configuration list for PBXNativeTarget "AttributedTextSampleTests" */; 137 | buildPhases = ( 138 | 6C4BCC8322D74C8D00A4F8D7 /* Sources */, 139 | 6C4BCC8422D74C8D00A4F8D7 /* Frameworks */, 140 | 6C4BCC8522D74C8D00A4F8D7 /* Resources */, 141 | ); 142 | buildRules = ( 143 | ); 144 | dependencies = ( 145 | 6C4BCC8D22D74C8D00A4F8D7 /* PBXTargetDependency */, 146 | ); 147 | name = AttributedTextSampleTests; 148 | productName = AttributedTextSampleTests; 149 | productReference = 6C4BCC8722D74C8D00A4F8D7 /* AttributedTextSampleTests.xctest */; 150 | productType = "com.apple.product-type.bundle.unit-test"; 151 | }; 152 | 6C65EA2322D489A300EF32FB /* AttributedTextSample */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = 6C65EA3822D489A400EF32FB /* Build configuration list for PBXNativeTarget "AttributedTextSample" */; 155 | buildPhases = ( 156 | 6C65EA2022D489A300EF32FB /* Sources */, 157 | 6C65EA2122D489A300EF32FB /* Frameworks */, 158 | 6C65EA2222D489A300EF32FB /* Resources */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | ); 164 | name = AttributedTextSample; 165 | packageProductDependencies = ( 166 | 6CF300A922D48AC200D8C7DF /* NSAttributedStringBuilder */, 167 | ); 168 | productName = AttributedTextSample; 169 | productReference = 6C65EA2422D489A300EF32FB /* AttributedTextSample.app */; 170 | productType = "com.apple.product-type.application"; 171 | }; 172 | /* End PBXNativeTarget section */ 173 | 174 | /* Begin PBXProject section */ 175 | 6C65EA1C22D489A200EF32FB /* Project object */ = { 176 | isa = PBXProject; 177 | attributes = { 178 | LastSwiftUpdateCheck = 1100; 179 | LastUpgradeCheck = 1100; 180 | ORGANIZATIONNAME = "Elaborapp Co., Ltd."; 181 | TargetAttributes = { 182 | 6C4BCC8622D74C8D00A4F8D7 = { 183 | CreatedOnToolsVersion = 11.0; 184 | TestTargetID = 6C65EA2322D489A300EF32FB; 185 | }; 186 | 6C65EA2322D489A300EF32FB = { 187 | CreatedOnToolsVersion = 11.0; 188 | }; 189 | }; 190 | }; 191 | buildConfigurationList = 6C65EA1F22D489A200EF32FB /* Build configuration list for PBXProject "AttributedTextSample" */; 192 | compatibilityVersion = "Xcode 9.3"; 193 | developmentRegion = en; 194 | hasScannedForEncodings = 0; 195 | knownRegions = ( 196 | en, 197 | Base, 198 | ); 199 | mainGroup = 6C65EA1B22D489A200EF32FB; 200 | packageReferences = ( 201 | ); 202 | productRefGroup = 6C65EA2522D489A300EF32FB /* Products */; 203 | projectDirPath = ""; 204 | projectRoot = ""; 205 | targets = ( 206 | 6C65EA2322D489A300EF32FB /* AttributedTextSample */, 207 | 6C4BCC8622D74C8D00A4F8D7 /* AttributedTextSampleTests */, 208 | ); 209 | }; 210 | /* End PBXProject section */ 211 | 212 | /* Begin PBXResourcesBuildPhase section */ 213 | 6C4BCC8522D74C8D00A4F8D7 /* Resources */ = { 214 | isa = PBXResourcesBuildPhase; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | 6C4BCC9322D74CAF00A4F8D7 /* Swift_logo_color_rgb.jpg in Resources */, 218 | ); 219 | runOnlyForDeploymentPostprocessing = 0; 220 | }; 221 | 6C65EA2222D489A300EF32FB /* Resources */ = { 222 | isa = PBXResourcesBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | 6C4BCC9422D74D8200A4F8D7 /* Swift_logo_color_rgb.jpg in Resources */, 226 | 6C65EA3422D489A400EF32FB /* LaunchScreen.storyboard in Resources */, 227 | 6C65EA3122D489A400EF32FB /* Preview Assets.xcassets in Resources */, 228 | 6C65EA2E22D489A400EF32FB /* Assets.xcassets in Resources */, 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | }; 232 | /* End PBXResourcesBuildPhase section */ 233 | 234 | /* Begin PBXSourcesBuildPhase section */ 235 | 6C4BCC8322D74C8D00A4F8D7 /* Sources */ = { 236 | isa = PBXSourcesBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | 6C4BCC9222D74C9700A4F8D7 /* ImageAttachmentTests.swift in Sources */, 240 | ); 241 | runOnlyForDeploymentPostprocessing = 0; 242 | }; 243 | 6C65EA2022D489A300EF32FB /* Sources */ = { 244 | isa = PBXSourcesBuildPhase; 245 | buildActionMask = 2147483647; 246 | files = ( 247 | 6CF300B522D4A36600D8C7DF /* AttributedText.swift in Sources */, 248 | 6C65EA2822D489A300EF32FB /* AppDelegate.swift in Sources */, 249 | 6C65EA2A22D489A300EF32FB /* SceneDelegate.swift in Sources */, 250 | 6C65EA2C22D489A300EF32FB /* ContentView.swift in Sources */, 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | /* End PBXSourcesBuildPhase section */ 255 | 256 | /* Begin PBXTargetDependency section */ 257 | 6C4BCC8D22D74C8D00A4F8D7 /* PBXTargetDependency */ = { 258 | isa = PBXTargetDependency; 259 | target = 6C65EA2322D489A300EF32FB /* AttributedTextSample */; 260 | targetProxy = 6C4BCC8C22D74C8D00A4F8D7 /* PBXContainerItemProxy */; 261 | }; 262 | /* End PBXTargetDependency section */ 263 | 264 | /* Begin PBXVariantGroup section */ 265 | 6C65EA3222D489A400EF32FB /* LaunchScreen.storyboard */ = { 266 | isa = PBXVariantGroup; 267 | children = ( 268 | 6C65EA3322D489A400EF32FB /* Base */, 269 | ); 270 | name = LaunchScreen.storyboard; 271 | sourceTree = ""; 272 | }; 273 | /* End PBXVariantGroup section */ 274 | 275 | /* Begin XCBuildConfiguration section */ 276 | 6C4BCC8F22D74C8D00A4F8D7 /* Debug */ = { 277 | isa = XCBuildConfiguration; 278 | buildSettings = { 279 | BUNDLE_LOADER = "$(TEST_HOST)"; 280 | CODE_SIGN_STYLE = Automatic; 281 | DEVELOPMENT_TEAM = SE9N6X5V7C; 282 | INFOPLIST_FILE = AttributedTextSampleTests/Info.plist; 283 | LD_RUNPATH_SEARCH_PATHS = ( 284 | "$(inherited)", 285 | "@executable_path/Frameworks", 286 | "@loader_path/Frameworks", 287 | ); 288 | PRODUCT_BUNDLE_IDENTIFIER = com.elaborapp.NSAttributedStringBuilder.AttributedTextSampleTests; 289 | PRODUCT_NAME = "$(TARGET_NAME)"; 290 | SWIFT_VERSION = 5.0; 291 | TARGETED_DEVICE_FAMILY = "1,2"; 292 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AttributedTextSample.app/AttributedTextSample"; 293 | }; 294 | name = Debug; 295 | }; 296 | 6C4BCC9022D74C8D00A4F8D7 /* Release */ = { 297 | isa = XCBuildConfiguration; 298 | buildSettings = { 299 | BUNDLE_LOADER = "$(TEST_HOST)"; 300 | CODE_SIGN_STYLE = Automatic; 301 | DEVELOPMENT_TEAM = SE9N6X5V7C; 302 | INFOPLIST_FILE = AttributedTextSampleTests/Info.plist; 303 | LD_RUNPATH_SEARCH_PATHS = ( 304 | "$(inherited)", 305 | "@executable_path/Frameworks", 306 | "@loader_path/Frameworks", 307 | ); 308 | PRODUCT_BUNDLE_IDENTIFIER = com.elaborapp.NSAttributedStringBuilder.AttributedTextSampleTests; 309 | PRODUCT_NAME = "$(TARGET_NAME)"; 310 | SWIFT_VERSION = 5.0; 311 | TARGETED_DEVICE_FAMILY = "1,2"; 312 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AttributedTextSample.app/AttributedTextSample"; 313 | }; 314 | name = Release; 315 | }; 316 | 6C65EA3622D489A400EF32FB /* Debug */ = { 317 | isa = XCBuildConfiguration; 318 | buildSettings = { 319 | ALWAYS_SEARCH_USER_PATHS = NO; 320 | CLANG_ANALYZER_NONNULL = YES; 321 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 322 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 323 | CLANG_CXX_LIBRARY = "libc++"; 324 | CLANG_ENABLE_MODULES = YES; 325 | CLANG_ENABLE_OBJC_ARC = YES; 326 | CLANG_ENABLE_OBJC_WEAK = YES; 327 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 328 | CLANG_WARN_BOOL_CONVERSION = YES; 329 | CLANG_WARN_COMMA = YES; 330 | CLANG_WARN_CONSTANT_CONVERSION = YES; 331 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 332 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 333 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 334 | CLANG_WARN_EMPTY_BODY = YES; 335 | CLANG_WARN_ENUM_CONVERSION = YES; 336 | CLANG_WARN_INFINITE_RECURSION = YES; 337 | CLANG_WARN_INT_CONVERSION = YES; 338 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 339 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 340 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 341 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 342 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 343 | CLANG_WARN_STRICT_PROTOTYPES = YES; 344 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 345 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 346 | CLANG_WARN_UNREACHABLE_CODE = YES; 347 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 348 | COPY_PHASE_STRIP = NO; 349 | DEBUG_INFORMATION_FORMAT = dwarf; 350 | ENABLE_STRICT_OBJC_MSGSEND = YES; 351 | ENABLE_TESTABILITY = YES; 352 | GCC_C_LANGUAGE_STANDARD = gnu11; 353 | GCC_DYNAMIC_NO_PIC = NO; 354 | GCC_NO_COMMON_BLOCKS = YES; 355 | GCC_OPTIMIZATION_LEVEL = 0; 356 | GCC_PREPROCESSOR_DEFINITIONS = ( 357 | "DEBUG=1", 358 | "$(inherited)", 359 | ); 360 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 361 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 362 | GCC_WARN_UNDECLARED_SELECTOR = YES; 363 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 364 | GCC_WARN_UNUSED_FUNCTION = YES; 365 | GCC_WARN_UNUSED_VARIABLE = YES; 366 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 367 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 368 | MTL_FAST_MATH = YES; 369 | ONLY_ACTIVE_ARCH = YES; 370 | SDKROOT = iphoneos; 371 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 372 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 373 | }; 374 | name = Debug; 375 | }; 376 | 6C65EA3722D489A400EF32FB /* Release */ = { 377 | isa = XCBuildConfiguration; 378 | buildSettings = { 379 | ALWAYS_SEARCH_USER_PATHS = NO; 380 | CLANG_ANALYZER_NONNULL = YES; 381 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 382 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 383 | CLANG_CXX_LIBRARY = "libc++"; 384 | CLANG_ENABLE_MODULES = YES; 385 | CLANG_ENABLE_OBJC_ARC = YES; 386 | CLANG_ENABLE_OBJC_WEAK = YES; 387 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 388 | CLANG_WARN_BOOL_CONVERSION = YES; 389 | CLANG_WARN_COMMA = YES; 390 | CLANG_WARN_CONSTANT_CONVERSION = YES; 391 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 392 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 393 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 394 | CLANG_WARN_EMPTY_BODY = YES; 395 | CLANG_WARN_ENUM_CONVERSION = YES; 396 | CLANG_WARN_INFINITE_RECURSION = YES; 397 | CLANG_WARN_INT_CONVERSION = YES; 398 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 399 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 400 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 401 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 402 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 403 | CLANG_WARN_STRICT_PROTOTYPES = YES; 404 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 405 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 406 | CLANG_WARN_UNREACHABLE_CODE = YES; 407 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 408 | COPY_PHASE_STRIP = NO; 409 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 410 | ENABLE_NS_ASSERTIONS = NO; 411 | ENABLE_STRICT_OBJC_MSGSEND = YES; 412 | GCC_C_LANGUAGE_STANDARD = gnu11; 413 | GCC_NO_COMMON_BLOCKS = YES; 414 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 415 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 416 | GCC_WARN_UNDECLARED_SELECTOR = YES; 417 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 418 | GCC_WARN_UNUSED_FUNCTION = YES; 419 | GCC_WARN_UNUSED_VARIABLE = YES; 420 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 421 | MTL_ENABLE_DEBUG_INFO = NO; 422 | MTL_FAST_MATH = YES; 423 | SDKROOT = iphoneos; 424 | SWIFT_COMPILATION_MODE = wholemodule; 425 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 426 | VALIDATE_PRODUCT = YES; 427 | }; 428 | name = Release; 429 | }; 430 | 6C65EA3922D489A400EF32FB /* Debug */ = { 431 | isa = XCBuildConfiguration; 432 | buildSettings = { 433 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 434 | CODE_SIGN_ENTITLEMENTS = AttributedTextSample/AttributedTextSample.entitlements; 435 | CODE_SIGN_STYLE = Automatic; 436 | DEVELOPMENT_ASSET_PATHS = "AttributedTextSample/Preview\\ Content"; 437 | DEVELOPMENT_TEAM = SE9N6X5V7C; 438 | ENABLE_PREVIEWS = YES; 439 | INFOPLIST_FILE = AttributedTextSample/Info.plist; 440 | LD_RUNPATH_SEARCH_PATHS = ( 441 | "$(inherited)", 442 | "@executable_path/Frameworks", 443 | ); 444 | PRODUCT_BUNDLE_IDENTIFIER = com.elaborapp.AttributedTextSample; 445 | PRODUCT_NAME = "$(TARGET_NAME)"; 446 | SUPPORTS_UIKITFORMAC = NO; 447 | SWIFT_VERSION = 5.0; 448 | TARGETED_DEVICE_FAMILY = "1,2"; 449 | }; 450 | name = Debug; 451 | }; 452 | 6C65EA3A22D489A400EF32FB /* Release */ = { 453 | isa = XCBuildConfiguration; 454 | buildSettings = { 455 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 456 | CODE_SIGN_ENTITLEMENTS = AttributedTextSample/AttributedTextSample.entitlements; 457 | CODE_SIGN_STYLE = Automatic; 458 | DEVELOPMENT_ASSET_PATHS = "AttributedTextSample/Preview\\ Content"; 459 | DEVELOPMENT_TEAM = SE9N6X5V7C; 460 | ENABLE_PREVIEWS = YES; 461 | INFOPLIST_FILE = AttributedTextSample/Info.plist; 462 | LD_RUNPATH_SEARCH_PATHS = ( 463 | "$(inherited)", 464 | "@executable_path/Frameworks", 465 | ); 466 | PRODUCT_BUNDLE_IDENTIFIER = com.elaborapp.AttributedTextSample; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | SUPPORTS_UIKITFORMAC = NO; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = "1,2"; 471 | }; 472 | name = Release; 473 | }; 474 | /* End XCBuildConfiguration section */ 475 | 476 | /* Begin XCConfigurationList section */ 477 | 6C4BCC8E22D74C8D00A4F8D7 /* Build configuration list for PBXNativeTarget "AttributedTextSampleTests" */ = { 478 | isa = XCConfigurationList; 479 | buildConfigurations = ( 480 | 6C4BCC8F22D74C8D00A4F8D7 /* Debug */, 481 | 6C4BCC9022D74C8D00A4F8D7 /* Release */, 482 | ); 483 | defaultConfigurationIsVisible = 0; 484 | defaultConfigurationName = Release; 485 | }; 486 | 6C65EA1F22D489A200EF32FB /* Build configuration list for PBXProject "AttributedTextSample" */ = { 487 | isa = XCConfigurationList; 488 | buildConfigurations = ( 489 | 6C65EA3622D489A400EF32FB /* Debug */, 490 | 6C65EA3722D489A400EF32FB /* Release */, 491 | ); 492 | defaultConfigurationIsVisible = 0; 493 | defaultConfigurationName = Release; 494 | }; 495 | 6C65EA3822D489A400EF32FB /* Build configuration list for PBXNativeTarget "AttributedTextSample" */ = { 496 | isa = XCConfigurationList; 497 | buildConfigurations = ( 498 | 6C65EA3922D489A400EF32FB /* Debug */, 499 | 6C65EA3A22D489A400EF32FB /* Release */, 500 | ); 501 | defaultConfigurationIsVisible = 0; 502 | defaultConfigurationName = Release; 503 | }; 504 | /* End XCConfigurationList section */ 505 | 506 | /* Begin XCSwiftPackageProductDependency section */ 507 | 6CF300A922D48AC200D8C7DF /* NSAttributedStringBuilder */ = { 508 | isa = XCSwiftPackageProductDependency; 509 | productName = NSAttributedStringBuilder; 510 | }; 511 | /* End XCSwiftPackageProductDependency section */ 512 | }; 513 | rootObject = 6C65EA1C22D489A200EF32FB /* Project object */; 514 | } 515 | --------------------------------------------------------------------------------