├── .gitattributes ├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── AttributedString.podspec ├── AttributedString.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── AttributedString-iOS.xcscheme │ ├── AttributedString-macOS.xcscheme │ └── AttributedString-tvOS.xcscheme ├── AttributedString.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Demo-Mac ├── Demo-Mac.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── Demo-Mac.xcscheme └── Demo-Mac │ ├── AllViewController.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── huaji.imageset │ │ ├── Contents.json │ │ └── huaji.jpg │ ├── swift-icon.imageset │ │ ├── Contents.json │ │ └── swift-icon.png │ └── swift-image-1.imageset │ │ ├── Contents.json │ │ └── swift-image-1.jpg │ ├── Base.lproj │ └── Main.storyboard │ ├── Cell │ └── TableViewCell.swift │ ├── Demo_Mac.entitlements │ ├── Info.plist │ └── ViewController.swift ├── Demo-TV ├── Demo-TV.xcodeproj │ └── project.pbxproj └── Demo-TV │ ├── AllDetailViewController.swift │ ├── AllTableViewController.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon - App Store.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ ├── App Icon.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ └── Middle.imagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ └── Contents.json │ │ └── Top Shelf Image.imageset │ │ │ └── Contents.json │ ├── Contents.json │ ├── huaji.imageset │ │ ├── Contents.json │ │ └── huaji.jpg │ ├── swift-icon.imageset │ │ ├── Contents.json │ │ └── swift-icon.png │ └── swift-image-1.imageset │ │ ├── Contents.json │ │ └── swift-image-1.jpg │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Cell │ └── TableViewCell.swift │ ├── Info.plist │ └── ViewController.swift ├── Demo ├── Demo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Demo.xcscheme └── Demo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── huaji.imageset │ │ ├── Contents.json │ │ └── huaji.jpg │ ├── placeholder.imageset │ │ ├── Contents.json │ │ └── placeholder.png │ ├── swift-icon.imageset │ │ ├── Contents.json │ │ └── swift-icon.png │ └── swift-image-1.imageset │ │ ├── Contents.json │ │ └── swift-image-1.jpg │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Cell │ └── TableViewCell.swift │ ├── Debug │ ├── Debug.storyboard │ ├── Debug.swift │ ├── DebugLabelView.swift │ └── DebugLabelViewController.swift │ ├── Details │ ├── ActionViewController.swift │ ├── AttachmentViewController.swift │ ├── BackgroundColorViewController.swift │ ├── BaselineOffsetViewController.swift │ ├── CheckingViewController.swift │ ├── ExpansionViewController.swift │ ├── FontViewController.swift │ ├── ForegroundColorViewController.swift │ ├── KernViewController.swift │ ├── LigatureViewController.swift │ ├── LinkViewController.swift │ ├── ObliquenessViewController.swift │ ├── ParagraphStyleViewController.swift │ ├── ShadowViewController.swift │ ├── StrikethroughViewController.swift │ ├── StrokeViewController.swift │ ├── TextEffectViewController.swift │ ├── UnderlineViewController.swift │ ├── VerticalGlyphFormViewController.swift │ └── WritingDirectionViewController.swift │ ├── Info.plist │ ├── SimpleViewController.swift │ ├── VideoPlayerView.swift │ └── ViewController.swift ├── Gemfile ├── Gemfile.lock ├── Info.plist ├── LICENSE ├── Package.swift ├── README.md ├── README_CN.md ├── Resources ├── all.png ├── coding.gif ├── font.png ├── kern.png ├── logo.png ├── logo.sketch ├── simple.png └── stroke.png ├── Sources ├── Action.swift ├── Attachment.swift ├── Attribute.swift ├── AttributedString.h ├── AttributedString.swift ├── Checking.swift ├── Extension │ ├── AppKit │ │ └── NSTextFieldExtension.swift │ ├── ArrayExtension.swift │ ├── CoreGraphics │ │ ├── CGPointExtension.swift │ │ ├── CGRectExtension.swift │ │ └── CGSizeExtension.swift │ ├── Extension.swift │ ├── ObjectExtension.swift │ ├── ShadowExtension.swift │ ├── UIKit │ │ ├── ActionQueue.swift │ │ ├── UIButtonExtension.swift │ │ ├── UILabel │ │ │ ├── UILabelExtension.swift │ │ │ └── UILabelLayoutManagerDelegate.swift │ │ ├── UITextFieldExtension.swift │ │ └── UITextViewExtension.swift │ └── WatchKit │ │ ├── WKInterfaceButtonExtension.swift │ │ ├── WKInterfaceLabelExtension.swift │ │ └── WKInterfaceTextFieldExtension.swift ├── Interpolation.swift ├── ParagraphStyle.swift └── PrivacyInfo.xcprivacy └── Tests ├── AttributedString_iOS_Tests.swift ├── AttributedString_macOS_Tests.swift ├── AttributedString_tvOS_Tests.swift └── Info.plist /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ruby linguist-language=swift -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: "AttributedString CI" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - hotfix 8 | pull_request: 9 | branches: 10 | - '*' 11 | 12 | jobs: 13 | macOS: 14 | name: Test macOS 15 | runs-on: macOS-latest 16 | env: 17 | DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: macOS 21 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "AttributedString.xcodeproj" -scheme "AttributedString-macOS" -destination "platform=macOS" clean test | xcpretty 22 | iOS: 23 | name: Test iOS 24 | runs-on: macOS-latest 25 | env: 26 | DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer 27 | strategy: 28 | matrix: 29 | destination: ["OS=16.2,name=iPhone 14 Pro"] #, "OS=12.4,name=iPhone XS", "OS=11.4,name=iPhone X", "OS=10.3.1,name=iPhone SE"] 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: iOS - ${{ matrix.destination }} 33 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "AttributedString.xcodeproj" -scheme "AttributedString-iOS" -destination "${{ matrix.destination }}" clean test | xcpretty 34 | tvOS: 35 | name: Test tvOS 36 | runs-on: macOS-latest 37 | env: 38 | DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer 39 | strategy: 40 | matrix: 41 | destination: ["OS=16.1,name=Apple TV 4K (3rd generation)"] #, "OS=11.4,name=Apple TV 4K", "OS=10.2,name=Apple TV 1080p"] 42 | steps: 43 | - uses: actions/checkout@v2 44 | - name: tvOS - ${{ matrix.destination }} 45 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "AttributedString.xcodeproj" -scheme "AttributedString-tvOS" -destination "${{ matrix.destination }}" clean test | xcpretty 46 | watchOS: 47 | name: Build watchOS 48 | runs-on: macOS-latest 49 | env: 50 | DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer 51 | strategy: 52 | matrix: 53 | destination: ["OS=9.1,name=Apple Watch Series 8 (45mm)"] #, "OS=4.2,name=Apple Watch Series 3 - 42mm", "OS=3.2,name=Apple Watch Series 2 - 42mm"] 54 | steps: 55 | - uses: actions/checkout@v2 56 | - name: watchOS - ${{ matrix.destination }} 57 | run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "AttributedString.xcodeproj" -scheme "AttributedString-watchOS" -destination "${{ matrix.destination }}" clean build | xcpretty 58 | # spm: 59 | # name: Test with SPM 60 | # runs-on: macOS-latest 61 | # env: 62 | # DEVELOPER_DIR: /Applications/Xcode_11.4.app/Contents/Developer 63 | # steps: 64 | # - uses: actions/checkout@v2 65 | # - name: SPM Test 66 | # run: swift test -c debug 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AttributedString.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "AttributedString" 4 | s.version = "3.4.2" 5 | s.summary = "基于Swift字符串插值快速构建你想要的富文本, 支持点击按住等事件获取, 支持多种类型过滤" 6 | 7 | s.homepage = "https://github.com/lixiang1994/AttributedString" 8 | 9 | s.license = { :type => "MIT", :file => "LICENSE" } 10 | 11 | s.author = { "LEE" => "18611401994@163.com" } 12 | 13 | s.source = { :git => "https://github.com/lixiang1994/AttributedString.git", :tag => s.version } 14 | 15 | s.requires_arc = true 16 | 17 | s.swift_versions = ["5.0"] 18 | 19 | s.frameworks = "Foundation" 20 | s.ios.frameworks = "UIKit" 21 | s.osx.frameworks = "AppKit" 22 | s.tvos.frameworks = "UIKit" 23 | s.watchos.frameworks = "WatchKit" 24 | 25 | s.ios.deployment_target = '9.0' 26 | s.osx.deployment_target = "10.13" 27 | s.tvos.deployment_target = "11.0" 28 | s.watchos.deployment_target = "6.0" 29 | 30 | s.source_files = ["Sources/*.swift", "Sources/Extension/*.swift", "Sources/Extension/CoreGraphics/*.swift"] 31 | s.ios.source_files = ["Sources/Extension/UIKit/*.swift", "Sources/Extension/UIKit/UILabel/*.swift"] 32 | s.osx.source_files = ["Sources/Extension/AppKit/*.swift"] 33 | s.tvos.source_files = ["Sources/Extension/UIKit/*.swift"] 34 | s.watchos.source_files = ["Sources/Extension/WatchKit/*.swift"] 35 | 36 | s.subspec 'Privacy' do |ss| 37 | ss.resource_bundles = { 38 | "AttributedString" => 'Sources/PrivacyInfo.xcprivacy' 39 | } 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /AttributedString.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AttributedString.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AttributedString.xcodeproj/xcshareddata/xcschemes/AttributedString-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /AttributedString.xcodeproj/xcshareddata/xcschemes/AttributedString-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /AttributedString.xcodeproj/xcshareddata/xcschemes/AttributedString-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /AttributedString.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /AttributedString.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac.xcodeproj/xcshareddata/xcschemes/Demo-Mac.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo-Mac 4 | // 5 | // Created by Lee on 2019/11/18. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | func applicationDidFinishLaunching(_ aNotification: Notification) { 15 | // Insert code here to initialize your application 16 | } 17 | 18 | func applicationWillTerminate(_ aNotification: Notification) { 19 | // Insert code here to tear down your application 20 | } 21 | 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Assets.xcassets/huaji.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "huaji.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Assets.xcassets/huaji.imageset/huaji.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Demo-Mac/Demo-Mac/Assets.xcassets/huaji.imageset/huaji.jpg -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Assets.xcassets/swift-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "swift-icon.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Assets.xcassets/swift-icon.imageset/swift-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Demo-Mac/Demo-Mac/Assets.xcassets/swift-icon.imageset/swift-icon.png -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Assets.xcassets/swift-image-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "swift-image-1.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Assets.xcassets/swift-image-1.imageset/swift-image-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Demo-Mac/Demo-Mac/Assets.xcassets/swift-image-1.imageset/swift-image-1.jpg -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Cell/TableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCell.swift 3 | // Demo-Mac 4 | // 5 | // Created by Lee on 2020/4/9. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class TableViewCell: NSTableCellView { 12 | 13 | @IBOutlet weak var text: NSTextField! 14 | 15 | override func draw(_ dirtyRect: NSRect) { 16 | super.draw(dirtyRect) 17 | 18 | 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Demo_Mac.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2019 LEE. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | NSSupportsAutomaticTermination 32 | 33 | NSSupportsSuddenTermination 34 | 35 | UIUserInterfaceStyle 36 | Light 37 | NSAppTransportSecurity 38 | 39 | NSAllowsArbitraryLoads 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Demo-Mac/Demo-Mac/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo-Mac 4 | // 5 | // Created by Lee on 2019/11/18. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import AppKit 11 | import AttributedString 12 | 13 | class ViewController: NSViewController { 14 | 15 | @IBOutlet weak var label: NSTextField! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | /// 21 | /// .init( 22 | /// """ 23 | /// \(.image(#imageLiteral(resourceName: "swift-icon"), .custom(size: .init(width: 64, height: 64)))) 24 | /// \("Swift", .font(.systemFont(ofSize: 48, weight: .semibold))) 25 | /// 26 | /// \("The powerful programming language that is also easy to learn.", .font(.systemFont(ofSize: 32, weight: .medium))) 27 | /// 28 | /// \("Swift is a powerful and intuitive programming language for macOS, iOS, watchOS, tvOS and beyond. Writing Swift code is interactive and fun, the syntax is concise yet expressive, and Swift includes modern features developers love. Swift code is safe by design, yet also produces software that runs lightning-fast.", .font(.systemFont(ofSize: 21))) 29 | /// 30 | /// """, 31 | /// .paragraph(.alignment(.center)) 32 | /// ) 33 | /// 34 | /// Equivalent 35 | /// 36 | /// """ 37 | /// \(wrap: 38 | /// """ 39 | /// \(.image(#imageLiteral(resourceName: "swift-icon"), .custom(size: .init(width: 64, height: 64)))) 40 | /// \("Swift", .font(.systemFont(ofSize: 48, weight: .semibold))) 41 | /// 42 | /// \("The powerful programming language that is also easy to learn.", .font(.systemFont(ofSize: 32, weight: .medium))) 43 | /// 44 | /// \("Swift is a powerful and intuitive programming language for macOS, iOS, watchOS, tvOS and beyond. Writing Swift code is interactive and fun, the syntax is concise yet expressive, and Swift includes modern features developers love. Swift code is safe by design, yet also produces software that runs lightning-fast.", .font(.systemFont(ofSize: 21))) 45 | /// 46 | /// """ 47 | /// , .paragraph(.alignment(.center))) 48 | /// """ 49 | 50 | let array: [ASAttributedString] = [ 51 | .init( 52 | """ 53 | \(.image(#imageLiteral(resourceName: "swift-icon"), .custom(size: .init(width: 64, height: 64)))) 54 | \("Swift", .font(.systemFont(ofSize: 48, weight: .semibold))) 55 | 56 | \("The powerful programming language that is also easy to learn.", .font(.systemFont(ofSize: 32, weight: .medium))) 57 | 58 | \("Swift is a powerful and intuitive programming language for macOS, iOS, watchOS, tvOS and beyond. Writing Swift code is interactive and fun, the syntax is concise yet expressive, and Swift includes modern features developers love. Swift code is safe by design, yet also produces software that runs lightning-fast.", .font(.systemFont(ofSize: 21))) 59 | 60 | """, 61 | .paragraph(.alignment(.center)) 62 | ), 63 | """ 64 | \("Great First Language", .font(.systemFont(ofSize: 40, weight: .semibold))) 65 | 66 | \( 67 | """ 68 | Swift can open doors to the world of coding. In fact, it was designed to be anyone’s first programming language, whether you’re still in school or exploring new career paths. For educators, Apple created free curriculum to teach Swift both in and out of the classroom. First-time coders can download Swift Playgrounds—an app for iPad that makes getting started with Swift code interactive and fun. 69 | """, .font(.systemFont(ofSize: 17)) 70 | ) 71 | 72 | \(.image(#imageLiteral(resourceName: "swift-image-1"))) 73 | """, 74 | """ 75 | \("Features:", .font(.systemFont(ofSize: 30, weight: .semibold))) 76 | \("foregroundColor", .foreground(#colorLiteral(red: 0.5568627715, green: 0.3529411852, blue: 0.9686274529, alpha: 1))) 77 | \("backgroundColor", .background(#colorLiteral(red: 0.6642242074, green: 0.6642400622, blue: 0.6642315388, alpha: 1))) 78 | \("font", .font(.systemFont(ofSize: 18, weight: .semibold))) 79 | \("link", .link("https://www.apple.com/")) 80 | \("kern", .kern(5)) 81 | \("ligature", .ligature(true)) 82 | \("strikethrough", .strikethrough(.single, color: .darkGray)) 83 | \("underline", .underline(.double, color: .black)) 84 | \("baselineOffset", .baselineOffset(5)) +5 85 | \("shadow", .shadow(.init(offset: .init(width: 0, height: 3), radius: 4, color: .orange))) 86 | \("stroke", .stroke(3.0, color: .blue)) 87 | \("textEffect", .textEffect(.letterpressStyle)) 88 | \("obliqueness", .obliqueness(0.3)) 89 | \("expansion", .expansion(0.8)) 0.8 90 | \("writingDirection", .writingDirection(.RLO)) RLO 91 | \("verticalGlyphForm. Currently on iOS, it's always horizontal.", .verticalGlyphForm(true)) 92 | 93 | 94 | \("Paragraph:", .font(.systemFont(ofSize: 30, weight: .semibold))) 95 | \("alignment:center\nlineSpacing:10", .paragraph(.lineSpacing(10), .alignment(.center))) 96 | 97 | 98 | 99 | \("Wrap:", .font(.systemFont(ofSize: 30, weight: .semibold))) 100 | ----------- 101 | \(wrap: .embedding( 102 | """ 103 | Embedding 104 | 105 | fontSize: 16 106 | This is attributed text -> \("fontSize: 30", .font(.systemFont(ofSize: 30)), .foreground(#colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1))) 107 | This is attributed text -> \("underline: single", .underline(.single)) 108 | \(wrap: .embedding( 109 | "Test wrap color red \("fontSize: 40 medium", .font(.systemFont(ofSize: 40, weight: .medium)))" 110 | ), .font(.systemFont(ofSize: 20)), .foreground(.red)) 111 | """ 112 | ), .font(.systemFont(ofSize: 16)) 113 | ) 114 | ----------- 115 | \(wrap: .override( 116 | """ 117 | Override 118 | 119 | fontSize: 16 120 | This is attributed text -> \("fontSize: 30", .font(.systemFont(ofSize: 30)), .foreground(#colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1))) 121 | This is attributed text -> \("underline: single", .underline(.single)) 122 | \(wrap: .override( 123 | "Test wrap color red \("fontSize: 40 medium", .font(.systemFont(ofSize: 40, weight: .medium)))" 124 | ), .font(.systemFont(ofSize: 20)), .foreground(.red)) 125 | """ 126 | ), .font(.systemFont(ofSize: 16)) 127 | ) 128 | 129 | """ 130 | ] 131 | 132 | let string = array.reduce(into: ASAttributedString(stringLiteral: "")) { 133 | $0 += $1 + "\n" 134 | } 135 | label.attributed.string = string 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/AllDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllDetailViewController.swift 3 | // Demo-TV 4 | // 5 | // Created by Lee on 2020/4/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedString 11 | 12 | class AllDetailViewController: UIViewController { 13 | 14 | typealias Item = AllTableViewController.Item 15 | 16 | @IBOutlet weak var tableView: UITableView! 17 | 18 | private var list: [NSAttributedString] = [] 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | } 24 | 25 | func set(item: Item) { 26 | list = [ 27 | ASAttributedString(item.content, .font(.systemFont(ofSize: 38))).value, 28 | .init(string: item.code) 29 | ] 30 | 31 | tableView.reloadData() 32 | } 33 | } 34 | 35 | extension AllDetailViewController: UITableViewDelegate { 36 | 37 | func numberOfSections(in tableView: UITableView) -> Int { 38 | return 1 39 | } 40 | } 41 | 42 | extension AllDetailViewController: UITableViewDataSource { 43 | 44 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 45 | return list.count 46 | } 47 | 48 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 49 | let cell = tableView.dequeueReusableCell( 50 | withIdentifier: "DetailCell", 51 | for: indexPath 52 | ) as! DetailCell 53 | cell.set(list[indexPath.row]) 54 | cell.set(indexPath.row) 55 | return cell 56 | } 57 | } 58 | 59 | class DetailCell: UITableViewCell { 60 | 61 | @IBOutlet weak var label: UILabel! 62 | 63 | func set(_ text: NSAttributedString) { 64 | label.attributedText = text 65 | } 66 | 67 | func set(_ index: Int) { 68 | switch index { 69 | case 0: 70 | label.textColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) 71 | label.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) 72 | 73 | case 1: 74 | label.textColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) 75 | label.backgroundColor = #colorLiteral(red: 0.3333333433, green: 0.3333333433, blue: 0.3333333433, alpha: 1) 76 | 77 | default: break 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo-TV 4 | // 5 | // Created by Lee on 2020/4/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/huaji.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "huaji.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/huaji.imageset/huaji.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Demo-TV/Demo-TV/Assets.xcassets/huaji.imageset/huaji.jpg -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/swift-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "swift-icon.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/swift-icon.imageset/swift-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Demo-TV/Demo-TV/Assets.xcassets/swift-icon.imageset/swift-icon.png -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/swift-image-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "swift-image-1.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Assets.xcassets/swift-image-1.imageset/swift-image-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Demo-TV/Demo-TV/Assets.xcassets/swift-image-1.imageset/swift-image-1.jpg -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/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 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/Cell/TableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCell.swift 3 | // Demo-TV 4 | // 5 | // Created by Lee on 2020/4/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TableViewCell: UITableViewCell { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | @IBOutlet weak var textHeight: NSLayoutConstraint! 15 | 16 | override func awakeFromNib() { 17 | super.awakeFromNib() 18 | 19 | textView.textContainer.lineFragmentPadding = 0 20 | textView.textContainerInset = .zero 21 | } 22 | 23 | func set(_ string: NSAttributedString) { 24 | textView.attributedText = string 25 | } 26 | 27 | func set(_ height: CGFloat) { 28 | textHeight.constant = height 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/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 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | arm64 30 | 31 | UIUserInterfaceStyle 32 | Automatic 33 | 34 | 35 | -------------------------------------------------------------------------------- /Demo-TV/Demo-TV/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo-TV 4 | // 5 | // Created by Lee on 2020/4/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedString 11 | 12 | class ViewController: UIViewController { 13 | 14 | @IBOutlet weak var tableView: UITableView! 15 | 16 | private var list: [Model] = [] 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | /// 22 | /// .init( 23 | /// """ 24 | /// \(.image(#imageLiteral(resourceName: "swift-icon"), .custom(size: .init(width: 64, height: 64)))) 25 | /// \("Swift", .font(.systemFont(ofSize: 48, weight: .semibold))) 26 | /// 27 | /// \("The powerful programming language that is also easy to learn.", .font(.systemFont(ofSize: 32, weight: .medium))) 28 | /// 29 | /// \("Swift is a powerful and intuitive programming language for macOS, iOS, watchOS, tvOS and beyond. Writing Swift code is interactive and fun, the syntax is concise yet expressive, and Swift includes modern features developers love. Swift code is safe by design, yet also produces software that runs lightning-fast.", .font(.systemFont(ofSize: 21))) 30 | /// 31 | /// """, 32 | /// .paragraph(.alignment(.center)) 33 | /// ) 34 | /// 35 | /// Equivalent 36 | /// 37 | /// """ 38 | /// \(wrap: 39 | /// """ 40 | /// \(.image(#imageLiteral(resourceName: "swift-icon"), .custom(size: .init(width: 64, height: 64)))) 41 | /// \("Swift", .font(.systemFont(ofSize: 48, weight: .semibold))) 42 | /// 43 | /// \("The powerful programming language that is also easy to learn.", .font(.systemFont(ofSize: 32, weight: .medium))) 44 | /// 45 | /// \("Swift is a powerful and intuitive programming language for macOS, iOS, watchOS, tvOS and beyond. Writing Swift code is interactive and fun, the syntax is concise yet expressive, and Swift includes modern features developers love. Swift code is safe by design, yet also produces software that runs lightning-fast.", .font(.systemFont(ofSize: 21))) 46 | /// 47 | /// """ 48 | /// , .paragraph(.alignment(.center))) 49 | /// """ 50 | 51 | 52 | let array: [ASAttributedString] = [ 53 | .init( 54 | """ 55 | \(.image(#imageLiteral(resourceName: "swift-icon"), .custom(size: .init(width: 64, height: 64)))) 56 | \("Swift", .font(.systemFont(ofSize: 48, weight: .semibold))) 57 | 58 | \("The powerful programming language that is also easy to learn.", .font(.systemFont(ofSize: 32, weight: .medium))) 59 | 60 | \("Swift is a powerful and intuitive programming language for macOS, iOS, watchOS, tvOS and beyond. Writing Swift code is interactive and fun, the syntax is concise yet expressive, and Swift includes modern features developers love. Swift code is safe by design, yet also produces software that runs lightning-fast.", .font(.systemFont(ofSize: 21))) 61 | 62 | """, 63 | .paragraph(.alignment(.center)) 64 | ), 65 | """ 66 | \("Great First Language", .font(.systemFont(ofSize: 40, weight: .semibold))) 67 | 68 | \( 69 | """ 70 | Swift can open doors to the world of coding. In fact, it was designed to be anyone’s first programming language, whether you’re still in school or exploring new career paths. For educators, Apple created free curriculum to teach Swift both in and out of the classroom. First-time coders can download Swift Playgrounds—an app for iPad that makes getting started with Swift code interactive and fun. 71 | """, .font(.systemFont(ofSize: 17)) 72 | ) 73 | 74 | \(.image(#imageLiteral(resourceName: "swift-image-1"))) 75 | """, 76 | """ 77 | \("Features:", .font(.systemFont(ofSize: 30, weight: .semibold))) 78 | \("foregroundColor", .foreground(#colorLiteral(red: 0.5568627715, green: 0.3529411852, blue: 0.9686274529, alpha: 1))) 79 | \("backgroundColor", .background(#colorLiteral(red: 0.6642242074, green: 0.6642400622, blue: 0.6642315388, alpha: 1))) 80 | \("font", .font(.systemFont(ofSize: 18, weight: .semibold))) 81 | \("link", .link("https://www.apple.com/")) 82 | \("kern", .kern(5)) 83 | \("ligature", .ligature(true)) 84 | \("strikethrough", .strikethrough(.single, color: .darkGray)) 85 | \("underline", .underline(.double, color: .black)) 86 | \("baselineOffset", .baselineOffset(5)) +5 87 | \("shadow", .shadow(.init(offset: .init(width: 0, height: 3), radius: 4, color: .orange))) 88 | \("stroke", .stroke(3.0, color: .blue)) 89 | \("textEffect", .textEffect(.letterpressStyle)) 90 | \("obliqueness", .obliqueness(0.3)) 91 | \("expansion", .expansion(0.8)) 0.8 92 | \("writingDirection", .writingDirection(.RLO)) RLO 93 | \("verticalGlyphForm. Currently on iOS, it's always horizontal.", .verticalGlyphForm(true)) 94 | 95 | 96 | \("Paragraph:", .font(.systemFont(ofSize: 30, weight: .semibold))) 97 | \("alignment:center\nlineSpacing:10", .paragraph(.lineSpacing(10), .alignment(.center))) 98 | 99 | 100 | 101 | \("Wrap:", .font(.systemFont(ofSize: 30, weight: .semibold))) 102 | ----------- 103 | \(wrap: .embedding( 104 | """ 105 | Embedding 106 | 107 | fontSize: 16 108 | This is attributed text -> \("fontSize: 30", .font(.systemFont(ofSize: 30)), .foreground(#colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1))) 109 | This is attributed text -> \("underline: single", .underline(.single)) 110 | \(wrap: .embedding( 111 | "Test wrap color red \("fontSize: 40 medium", .font(.systemFont(ofSize: 40, weight: .medium)))" 112 | ), .font(.systemFont(ofSize: 20)), .foreground(.red)) 113 | """ 114 | ), .font(.systemFont(ofSize: 16)) 115 | ) 116 | ----------- 117 | \(wrap: .override( 118 | """ 119 | Override 120 | 121 | fontSize: 16 122 | This is attributed text -> \("fontSize: 30", .font(.systemFont(ofSize: 30)), .foreground(#colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1))) 123 | This is attributed text -> \("underline: single", .underline(.single)) 124 | \(wrap: .override( 125 | "Test wrap color red \("fontSize: 40 medium", .font(.systemFont(ofSize: 40, weight: .medium)))" 126 | ), .font(.systemFont(ofSize: 20)), .foreground(.red)) 127 | """ 128 | ), .font(.systemFont(ofSize: 16)) 129 | ) 130 | 131 | """ 132 | ] 133 | 134 | list = array.map { .init($0) } 135 | tableView.reloadData() 136 | } 137 | 138 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 139 | super.traitCollectionDidChange(previousTraitCollection) 140 | 141 | list = list.map { .init($0.content) } 142 | tableView.reloadData() 143 | } 144 | } 145 | 146 | extension ViewController: UITableViewDelegate { 147 | 148 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 149 | return list[indexPath.row].height + 20 150 | } 151 | 152 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 153 | return 0.001 154 | } 155 | 156 | func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 157 | return 0.001 158 | } 159 | } 160 | 161 | extension ViewController: UITableViewDataSource { 162 | 163 | func numberOfSections(in tableView: UITableView) -> Int { 164 | return 1 165 | } 166 | 167 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 168 | return list.count 169 | } 170 | 171 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 172 | let cell = tableView.dequeueReusableCell( 173 | withIdentifier: "TableViewCell", 174 | for: indexPath 175 | ) as! TableViewCell 176 | let model = list[indexPath.row] 177 | cell.set(model.content.value) 178 | cell.set(model.height) 179 | return cell 180 | } 181 | } 182 | 183 | extension ViewController { 184 | 185 | struct Model { 186 | let content: ASAttributedString 187 | let height: CGFloat 188 | 189 | init(_ content: ASAttributedString) { 190 | self.content = content 191 | self.height = content.value.boundingRect( 192 | with: .init( 193 | width: UIScreen.main.bounds.width - 20, 194 | height: .greatestFiniteMagnitude 195 | ), 196 | options: [ 197 | .usesLineFragmentOrigin, 198 | .usesFontLeading 199 | ], 200 | context: nil 201 | ).integral.size.height 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Demo/Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/18. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Demo/Demo/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 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/huaji.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "huaji.jpg" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/huaji.imageset/huaji.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Demo/Demo/Assets.xcassets/huaji.imageset/huaji.jpg -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "placeholder.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/placeholder.imageset/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Demo/Demo/Assets.xcassets/placeholder.imageset/placeholder.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/swift-icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "swift-icon.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/swift-icon.imageset/swift-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Demo/Demo/Assets.xcassets/swift-icon.imageset/swift-icon.png -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/swift-image-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "swift-image-1.jpg" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/swift-image-1.imageset/swift-image-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Demo/Demo/Assets.xcassets/swift-image-1.imageset/swift-image-1.jpg -------------------------------------------------------------------------------- /Demo/Demo/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 | -------------------------------------------------------------------------------- /Demo/Demo/Cell/TableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCell.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/18. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedString 11 | 12 | class TableViewCell: UITableViewCell { 13 | 14 | @IBOutlet weak var textView: UITextView! 15 | @IBOutlet weak var textHeight: NSLayoutConstraint! 16 | 17 | override func awakeFromNib() { 18 | super.awakeFromNib() 19 | 20 | textView.textContainer.lineFragmentPadding = 0 21 | textView.textContainerInset = .zero 22 | } 23 | 24 | func set(_ string: ASAttributedString) { 25 | textView.attributed.text = string 26 | } 27 | 28 | func set(_ height: CGFloat) { 29 | textHeight.constant = height 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Demo/Demo/Debug/Debug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Debug.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2020/8/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum Debug { 12 | 13 | } 14 | 15 | extension Debug { 16 | 17 | struct Label: Codable { 18 | // size 19 | var width: CGFloat? 20 | var height: CGFloat? 21 | 22 | // normal 23 | var font: UIFont? 24 | var numberOfLines: Int? 25 | var textAlignment: NSTextAlignment? 26 | var lineBreakMode: NSLineBreakMode? 27 | var adjustsFontSizeToFitWidth: Bool? 28 | var baselineAdjustment: UIBaselineAdjustment? 29 | var minimumScaleFactor: CGFloat? 30 | var allowsDefaultTighteningForTruncation: Bool? 31 | 32 | // paragraphs 33 | var lineSpacing: CGFloat? 34 | var lineHeightMultiple: CGFloat? 35 | var minimumLineHeight: CGFloat? 36 | var maximumLineHeight: CGFloat? 37 | var paragraphSpacing: CGFloat? 38 | var paragraphSpacingBefore: CGFloat? 39 | var firstLineHeadIndent: CGFloat? 40 | var headIndent: CGFloat? 41 | var tailIndent: CGFloat? 42 | 43 | init() { 44 | font = .systemFont(ofSize: 17.0) 45 | } 46 | 47 | enum CodingKeys: String, CodingKey { 48 | case width 49 | case height 50 | case font 51 | case numberOfLines 52 | case textAlignment 53 | case lineBreakMode 54 | case adjustsFontSizeToFitWidth 55 | case baselineAdjustment 56 | case minimumScaleFactor 57 | case allowsDefaultTighteningForTruncation 58 | case lineSpacing 59 | case lineHeightMultiple 60 | case minimumLineHeight 61 | case maximumLineHeight 62 | case paragraphSpacing 63 | case paragraphSpacingBefore 64 | case firstLineHeadIndent 65 | case headIndent 66 | case tailIndent 67 | } 68 | 69 | init(from decoder: Decoder) throws { 70 | let container = try decoder.container(keyedBy: CodingKeys.self) 71 | 72 | self.width = try container.decodeIfPresent(CGFloat.self, forKey: .width) 73 | self.height = try container.decodeIfPresent(CGFloat.self, forKey: .height) 74 | 75 | let fontDescriptorData = try container.decodeIfPresent(Data.self, forKey: .font) 76 | if let descriptor = fontDescriptorData?.map() { 77 | self.font = UIFont(descriptor: descriptor, size: 0) 78 | } 79 | 80 | self.numberOfLines = try container.decodeIfPresent(Int.self, forKey: .numberOfLines) 81 | 82 | if let value = try container.decodeIfPresent(Int.self, forKey: .textAlignment) { 83 | self.textAlignment = NSTextAlignment(rawValue: value) 84 | } 85 | 86 | if let value = try container.decodeIfPresent(Int.self, forKey: .lineBreakMode) { 87 | self.lineBreakMode = NSLineBreakMode(rawValue: value) 88 | } 89 | 90 | self.adjustsFontSizeToFitWidth = try container.decodeIfPresent(Bool.self, forKey: .adjustsFontSizeToFitWidth) 91 | 92 | if let value = try container.decodeIfPresent(Int.self, forKey: .baselineAdjustment) { 93 | self.baselineAdjustment = UIBaselineAdjustment(rawValue: value) 94 | } 95 | 96 | self.minimumScaleFactor = try container.decodeIfPresent(CGFloat.self, forKey: .minimumScaleFactor) 97 | 98 | self.allowsDefaultTighteningForTruncation = try container.decodeIfPresent(Bool.self, forKey: .allowsDefaultTighteningForTruncation) 99 | 100 | self.lineSpacing = try container.decodeIfPresent(CGFloat.self, forKey: .lineSpacing) 101 | 102 | self.lineHeightMultiple = try container.decodeIfPresent(CGFloat.self, forKey: .lineHeightMultiple) 103 | 104 | self.minimumLineHeight = try container.decodeIfPresent(CGFloat.self, forKey: .minimumLineHeight) 105 | 106 | self.maximumLineHeight = try container.decodeIfPresent(CGFloat.self, forKey: .maximumLineHeight) 107 | 108 | self.paragraphSpacing = try container.decodeIfPresent(CGFloat.self, forKey: .paragraphSpacing) 109 | 110 | self.paragraphSpacingBefore = try container.decodeIfPresent(CGFloat.self, forKey: .paragraphSpacingBefore) 111 | 112 | self.firstLineHeadIndent = try container.decodeIfPresent(CGFloat.self, forKey: .firstLineHeadIndent) 113 | 114 | self.headIndent = try container.decodeIfPresent(CGFloat.self, forKey: .headIndent) 115 | 116 | self.tailIndent = try container.decodeIfPresent(CGFloat.self, forKey: .tailIndent) 117 | } 118 | 119 | func encode(to encoder: Encoder) throws { 120 | var container = encoder.container(keyedBy: CodingKeys.self) 121 | try container.encodeIfPresent(width, forKey: .width) 122 | try container.encodeIfPresent(height, forKey: .height) 123 | try container.encodeIfPresent(font?.fontDescriptor.data, forKey: .font) 124 | try container.encodeIfPresent(numberOfLines, forKey: .numberOfLines) 125 | try container.encodeIfPresent(textAlignment?.rawValue, forKey: .textAlignment) 126 | try container.encodeIfPresent(lineBreakMode?.rawValue, forKey: .lineBreakMode) 127 | try container.encodeIfPresent(adjustsFontSizeToFitWidth, forKey: .adjustsFontSizeToFitWidth) 128 | try container.encodeIfPresent(baselineAdjustment?.rawValue, forKey: .baselineAdjustment) 129 | try container.encodeIfPresent(minimumScaleFactor, forKey: .minimumScaleFactor) 130 | try container.encodeIfPresent(allowsDefaultTighteningForTruncation, forKey: .allowsDefaultTighteningForTruncation) 131 | try container.encodeIfPresent(lineSpacing, forKey: .lineSpacing) 132 | try container.encodeIfPresent(lineHeightMultiple, forKey: .lineHeightMultiple) 133 | try container.encodeIfPresent(minimumLineHeight, forKey: .minimumLineHeight) 134 | try container.encodeIfPresent(maximumLineHeight, forKey: .maximumLineHeight) 135 | try container.encodeIfPresent(paragraphSpacing, forKey: .paragraphSpacing) 136 | try container.encodeIfPresent(paragraphSpacingBefore, forKey: .paragraphSpacingBefore) 137 | try container.encodeIfPresent(firstLineHeadIndent, forKey: .firstLineHeadIndent) 138 | try container.encodeIfPresent(headIndent, forKey: .headIndent) 139 | try container.encodeIfPresent(tailIndent, forKey: .tailIndent) 140 | } 141 | } 142 | } 143 | 144 | extension Debug.Label { 145 | 146 | static let fonts: [UIFont] = [ 147 | .systemFont(ofSize: 17.0), 148 | .systemFont(ofSize: 17.0, weight: .light), 149 | .systemFont(ofSize: 17.0, weight: .medium), 150 | .systemFont(ofSize: 17.0, weight: .semibold), 151 | .systemFont(ofSize: 17.0, weight: .black), 152 | UIFont(name: "Georgia", size: 17.0) ?? .systemFont(ofSize: 17.0), 153 | UIFont(name: "Helvetica", size: 17.0) ?? .systemFont(ofSize: 17.0), 154 | UIFont(name: "Helvetica Neue", size: 17.0) ?? .systemFont(ofSize: 17.0), 155 | UIFont(name: "Times New Roman", size: 17.0) ?? .systemFont(ofSize: 17.0) 156 | ] 157 | } 158 | 159 | private extension Data { 160 | 161 | func map() -> UIFontDescriptor? { 162 | let unarchiver = NSKeyedUnarchiver(forReadingWith: self) 163 | return unarchiver.decodeObject() as? UIFontDescriptor 164 | } 165 | } 166 | 167 | private extension UIFontDescriptor { 168 | 169 | var data: Data { 170 | let mutableData = NSMutableData() 171 | let archiver = NSKeyedArchiver(forWritingWith: mutableData) 172 | archiver.encode(self) 173 | archiver.finishEncoding() 174 | return .init(mutableData) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Demo/Demo/Debug/DebugLabelViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugLabelViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2020/8/7. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedString 11 | 12 | private let key = "com.debug.label" 13 | 14 | class DebugLabelViewController: ViewController { 15 | 16 | private var info: Debug.Label = .init() { 17 | didSet { set(info: info) } 18 | } 19 | 20 | private var attributes: [ASAttributedString.Attribute] = [] 21 | private var paragraphs: [ASAttributedString.Attribute.ParagraphStyle] = [] 22 | private var attributedString: ASAttributedString = """ 23 | 我的名字叫李响,我的手机号码是18611401994,我的电子邮件地址是18611401994@163.com,现在是2020/06/28 20:30。我的GitHub主页是https://github.com/lixiang1994。欢迎来Star! \("点击联系我", .action({ })) 24 | """ 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | setup() 30 | updateText() 31 | } 32 | 33 | private func setup() { 34 | guard 35 | let data = UserDefaults.standard.data(forKey: key), 36 | let info = try? JSONDecoder().decode(Debug.Label.self, from: data) else { 37 | return 38 | } 39 | self.info = info 40 | } 41 | 42 | private func set(info: Debug.Label) { 43 | 44 | func update(_ style: ASAttributedString.Attribute.ParagraphStyle) { 45 | paragraphs.removeAll(where: { $0 ~= style }) 46 | paragraphs.append(style) 47 | } 48 | 49 | func remove(_ style: ASAttributedString.Attribute.ParagraphStyle) { 50 | paragraphs.removeAll(where: { $0 ~= style }) 51 | } 52 | 53 | if let value = info.lineSpacing { 54 | update(.lineSpacing(value)) 55 | 56 | } else { 57 | remove(.lineSpacing(0)) 58 | } 59 | if let value = info.lineHeightMultiple { 60 | update(.lineHeightMultiple(value)) 61 | 62 | } else { 63 | remove(.lineHeightMultiple(0)) 64 | } 65 | if let value = info.minimumLineHeight { 66 | update(.minimumLineHeight(value)) 67 | 68 | } else { 69 | remove(.minimumLineHeight(0)) 70 | } 71 | if let value = info.maximumLineHeight { 72 | update(.maximumLineHeight(value)) 73 | 74 | } else { 75 | remove(.maximumLineHeight(0)) 76 | } 77 | if let value = info.paragraphSpacing { 78 | update(.paragraphSpacing(value)) 79 | 80 | } else { 81 | remove(.paragraphSpacing(0)) 82 | } 83 | if let value = info.paragraphSpacingBefore { 84 | update(.paragraphSpacingBefore(value)) 85 | 86 | } else { 87 | remove(.paragraphSpacingBefore(0)) 88 | } 89 | if let value = info.firstLineHeadIndent { 90 | update(.firstLineHeadIndent(value)) 91 | 92 | } else { 93 | remove(.firstLineHeadIndent(0)) 94 | } 95 | if let value = info.headIndent { 96 | update(.headIndent(value)) 97 | 98 | } else { 99 | remove(.headIndent(0)) 100 | } 101 | if let value = info.tailIndent { 102 | update(.tailIndent(value)) 103 | 104 | } else { 105 | remove(.tailIndent(0)) 106 | } 107 | container.set(info: info) 108 | updateText() 109 | } 110 | 111 | private func updateText() { 112 | container.set(text: .init( 113 | attributedString, 114 | with: attributes + [.paragraph(with: paragraphs)] 115 | )) 116 | } 117 | 118 | @IBAction func saveAction(_ sender: UIBarButtonItem) { 119 | guard let json = try? JSONEncoder().encode(info) else { return } 120 | UserDefaults.standard.setValue(json, forKey: key) 121 | } 122 | @IBAction func cleanAction(_ sender: UIBarButtonItem) { 123 | UserDefaults.standard.removeObject(forKey: key) 124 | info = .init() 125 | } 126 | 127 | @IBAction func pageControlAction(_ sender: UIPageControl) { 128 | container.set(page: sender.currentPage, scroll: true) 129 | } 130 | 131 | @IBAction func widthSwitchAction(_ sender: UISwitch) { 132 | info.width = sender.isOn ? container.labelWidth : .none 133 | } 134 | @IBAction func heightSwitchAction(_ sender: UISwitch) { 135 | info.height = sender.isOn ? container.labelHeight : .none 136 | } 137 | @IBAction func widthSliderAction(_ sender: UISlider) { 138 | info.width = .init(sender.value) 139 | } 140 | @IBAction func heightSliderAction(_ sender: UISlider) { 141 | info.height = .init(sender.value) 142 | } 143 | 144 | @IBAction func fontNameSliderAction(_ sender: UISlider) { 145 | info.font = Debug.Label.fonts[.init(sender.value)].withSize(info.font?.pointSize ?? 17.0) 146 | } 147 | @IBAction func fontSizeSliderAction(_ sender: UISlider) { 148 | info.font = info.font?.withSize(.init(sender.value)) 149 | } 150 | @IBAction func numberOfLinesSliderAction(_ sender: UISlider) { 151 | info.numberOfLines = .init(sender.value) 152 | } 153 | @IBAction func textAlignmentSliderAction(_ sender: UISlider) { 154 | info.textAlignment = NSTextAlignment(rawValue: .init(sender.value)) ?? .natural 155 | } 156 | @IBAction func lineBreakModeSliderAction(_ sender: UISlider) { 157 | info.lineBreakMode = NSLineBreakMode(rawValue: .init(sender.value)) ?? .byTruncatingTail 158 | } 159 | 160 | @IBAction func adjustsFontSizeToFitWidthSwitchAction(_ sender: UISwitch) { 161 | info.adjustsFontSizeToFitWidth = sender.isOn 162 | } 163 | @IBAction func baselineAdjustmentSegmentedAction(_ sender: UISegmentedControl) { 164 | info.baselineAdjustment = UIBaselineAdjustment(rawValue: sender.selectedSegmentIndex) ?? .alignBaselines 165 | } 166 | @IBAction func minimumScaleFactorSlider(_ sender: UISlider) { 167 | info.minimumScaleFactor = .init(sender.value) 168 | } 169 | @IBAction func allowsDefaultTighteningForTruncationSwitchAction(_ sender: UISwitch) { 170 | info.allowsDefaultTighteningForTruncation = sender.isOn 171 | } 172 | 173 | @IBAction func lineSpacingSliderAction(_ sender: UISlider) { 174 | info.lineSpacing = .init(sender.value) 175 | } 176 | @IBAction func lineHeightMultipleSliderAction(_ sender: UISlider) { 177 | info.lineHeightMultiple = .init(sender.value) 178 | } 179 | @IBAction func minimumLineHeightSliderAction(_ sender: UISlider) { 180 | info.minimumLineHeight = .init(sender.value) 181 | } 182 | @IBAction func maximumLineHeightSliderAction(_ sender: UISlider) { 183 | info.maximumLineHeight = .init(sender.value) 184 | } 185 | @IBAction func paragraphSpacingSliderAction(_ sender: UISlider) { 186 | info.paragraphSpacing = .init(sender.value) 187 | } 188 | @IBAction func paragraphSpacingBeforeSliderAction(_ sender: UISlider) { 189 | info.paragraphSpacingBefore = .init(sender.value) 190 | } 191 | @IBAction func firstLineHeadIndentSliderAction(_ sender: UISlider) { 192 | info.firstLineHeadIndent = .init(sender.value) 193 | } 194 | @IBAction func headIndentSliderAction(_ sender: UISlider) { 195 | info.headIndent = .init(sender.value) 196 | } 197 | @IBAction func tailIndentSliderAction(_ sender: UISlider) { 198 | info.tailIndent = .init(sender.value) 199 | } 200 | } 201 | 202 | extension DebugLabelViewController: UIScrollViewDelegate { 203 | 204 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 205 | let offset = (scrollView.contentOffset.x + scrollView.bounds.width * 0.5).rounded(.down) 206 | container.set(page: .init(offset / scrollView.bounds.width), scroll: false) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Demo/Demo/Details/ActionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2020/6/3. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedString 11 | 12 | class ActionViewController: UIViewController { 13 | 14 | @IBOutlet weak var label: UILabel! 15 | 16 | @IBOutlet weak var textView: UITextView! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | // 如果需要修改全局默认高亮样式 可以通过以下方式 22 | // Array.defalut = [.foreground(<#T##value: Color##Color#>)] 23 | 24 | func clicked(_ result: ASAttributedString.Action.Result) { 25 | switch result.content { 26 | case .string(let value): 27 | print("点击了文本: \n\(value) \nrange: \(result.range)") 28 | 29 | case .attachment(let value): 30 | print("点击了附件: \n\(value) \nrange: \(result.range)") 31 | } 32 | } 33 | 34 | func pressed(_ result: ASAttributedString.Action.Result) { 35 | switch result.content { 36 | case .string(let value): 37 | print("按住了文本: \n\(value) \nrange: \(result.range)") 38 | 39 | case .attachment(let value): 40 | print("按住了附件: \n\(value) \nrange: \(result.range)") 41 | } 42 | } 43 | 44 | let custom = ASAttributedString.Action(.press, highlights: [.background(#colorLiteral(red: 0.5568627715, green: 0.3529411852, blue: 0.9686274529, alpha: 1)), .foreground(#colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0))]) { (result) in 45 | switch result.content { 46 | case .string(let value): 47 | print("按住了文本: \n\(value) \nrange: \(result.range)") 48 | 49 | case .attachment(let value): 50 | print("按住了附件: \n\(value) \nrange: \(result.range)") 51 | } 52 | } 53 | 54 | label.attributed.text = """ 55 | This is \("Label", .font(.systemFont(ofSize: 50)), .action(clicked), .action(.press, pressed)) 56 | 57 | This is a picture -> \(.image(#imageLiteral(resourceName: "huaji"), .custom(size: .init(width: 100, height: 100))), action: clicked) -> Displayed in custom size. 58 | 59 | This is \("Long Press", .font(.systemFont(ofSize: 30)), .action(.press, pressed)) 60 | 61 | Please \("custom highlight style", .font(.systemFont(ofSize: 30)), .action(custom)). 62 | 63 | Please custom -> \(.image(#imageLiteral(resourceName: "swift-icon"), .original(.center)), action: custom). 64 | 65 | """ 66 | 67 | textView.attributed.text = """ 68 | This is \("TextView", .font(.systemFont(ofSize: 20)), .action(clicked)) 69 | 70 | This is a picture -> \(.image(#imageLiteral(resourceName: "huaji"), .custom(size: .init(width: 100, height: 100))), action: clicked) -> Displayed in custom size. 71 | 72 | This is \("Long Press", .font(.systemFont(ofSize: 30)), .action(.press, pressed)) 73 | 74 | Please \("custom highlight style", .font(.systemFont(ofSize: 30)), .action(custom)). 75 | 76 | Please custom highlight style -> \(.image(#imageLiteral(resourceName: "swift-icon"), .original(.center)), action: custom). 77 | 78 | """ 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Demo/Demo/Details/AttachmentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttachmentViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedString 11 | 12 | class AttachmentViewController: UIViewController { 13 | 14 | @IBOutlet weak var textView: UITextView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | textView.textContainer.lineFragmentPadding = 0 19 | textView.textContainerInset = .zero 20 | 21 | // 网络图片链接与占位图 22 | let url = URL(string: "https://avatars.githubusercontent.com/u/13112992?s=400&u=bb452b153d9d5342877ebd8179b04fbbae41f3d0&v=4") 23 | let placeholder = UIImage(named: "placeholder") 24 | 25 | 26 | // 创建一些自定义视图控件 27 | let customView = UIView(frame: .init(x: 0, y: 0, width: 100, height: 100)) 28 | customView.backgroundColor = .red 29 | 30 | let customImageView = UIImageView(image: #imageLiteral(resourceName: "swift-image-1")) 31 | customImageView.contentMode = .scaleAspectFill 32 | customImageView.sizeToFit() 33 | 34 | let customLabel = UILabel() 35 | customLabel.text = "1234567890" 36 | customLabel.font = .systemFont(ofSize: 30, weight: .medium) 37 | customLabel.backgroundColor = #colorLiteral(red: 0.9568627477, green: 0.6588235497, blue: 0.5450980663, alpha: 1) 38 | customLabel.sizeToFit() 39 | 40 | func clicked() { 41 | // 更改自定义视图的大小 (x y 无效) 42 | customView.frame = .init(x: 100, y: 0, width: .random(in: 100 ... 200), height: .random(in: 100 ... 200)) 43 | 44 | // 更改自定义图片视图的图片 45 | customImageView.image = #imageLiteral(resourceName: "swift-icon") 46 | customImageView.sizeToFit() 47 | // 更改自定义标签的文本 48 | customLabel.text = "45678" 49 | customLabel.sizeToFit() 50 | 51 | // 请主动调用刷新布局 52 | textView.attributed.layout() 53 | } 54 | 55 | textView.attributed.text = .init( 56 | """ 57 | 58 | This is a picture -> \(.image(#imageLiteral(resourceName: "huaji"))) -> Displayed in original size. 59 | 60 | This is a picture -> \(.image(#imageLiteral(resourceName: "swift-icon"), .custom(.center, size: .init(width: 50, height: 50)))) -> Displayed in custom size. 61 | 62 | This is the recommended size image -> \(.image(#imageLiteral(resourceName: "swift-icon"), .proposed(.center)))). 63 | 64 | ----------------------- 65 | 66 | AsyncImageAttachment only support UITextView (以下异步图片附件仅UITextView支持): 67 | 68 | This is a remote image URL -> \(.image(url, placeholder: placeholder)) 69 | 70 | ----------------------- 71 | 72 | \("ViewAttachment only support UITextView (以下视图附件仅UITextView支持):", .font(.systemFont(ofSize: 24, weight: .medium))) 73 | 74 | \("Change something", .foreground(.blue), .action(clicked)) 75 | 76 | aaaa\(.view(customView, .original(.center)))aaa 77 | 78 | bbbb\(.view(customLabel, .original(.origin)))bbb 79 | 80 | cccc\(.view(customImageView, .original(.origin)))ccc 81 | 82 | """, 83 | .font(.systemFont(ofSize: 18)) 84 | ) 85 | } 86 | } 87 | 88 | /* 89 | 90 | ASAttributedString.AsyncImageAttachment.Loader = AsyncImageAttachmentKingfisherLoader.self 91 | 92 | 93 | import Kingfisher 94 | 95 | public class AsyncImageAttachmentKingfisherLoader: NSObject, AsyncImageAttachmentLoader { 96 | 97 | private var downloadTask: Kingfisher.DownloadTask? 98 | 99 | public var isLoading: Bool { 100 | return downloadTask != nil 101 | } 102 | 103 | public func loadImage(with url: URL, completion: @escaping (Result) -> Void) { 104 | downloadTask = KingfisherManager.shared.retrieveImage(with: url) { [weak self] (result) in 105 | guard let self = self else { return } 106 | self.downloadTask = nil 107 | 108 | switch result { 109 | case .success(let value): 110 | completion(.success(value.image)) 111 | case .failure(let error): 112 | completion(.failure(error)) 113 | } 114 | } 115 | } 116 | 117 | public func cancel() { 118 | downloadTask?.cancel() 119 | downloadTask = nil 120 | } 121 | } 122 | 123 | */ 124 | -------------------------------------------------------------------------------- /Demo/Demo/Details/BackgroundColorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // backgroundColor ViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BackgroundColorViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | \(" backgroundColor ", .background(.white)) 21 | 22 | \(" backgroundColor ", .background(#colorLiteral(red: 0.7952535152, green: 0.7952535152, blue: 0.7952535152, alpha: 1))) 23 | 24 | \(" backgroundColor ", .background(#colorLiteral(red: 0.5723067522, green: 0.5723067522, blue: 0.5723067522, alpha: 1))) 25 | 26 | \(" backgroundColor ", .background(#colorLiteral(red: 0.3179988265, green: 0.3179988265, blue: 0.3179988265, alpha: 1))) 27 | 28 | \(" backgroundColor ", .background(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1))) 29 | 30 | \(" backgroundColor ", .background(#colorLiteral(red: 1, green: 0.4932718873, blue: 0.4739984274, alpha: 1))) 31 | 32 | \(" backgroundColor ", .background(#colorLiteral(red: 1, green: 0.8323456645, blue: 0.4732058644, alpha: 1))) 33 | 34 | \(" backgroundColor ", .background(#colorLiteral(red: 0.9995340705, green: 0.988355577, blue: 0.4726552367, alpha: 1))) 35 | 36 | \(" backgroundColor ", .background(#colorLiteral(red: 0.8321695924, green: 0.985483706, blue: 0.4733308554, alpha: 1))) 37 | 38 | \(" backgroundColor ", .background(#colorLiteral(red: 0.4500938654, green: 0.9813225865, blue: 0.4743030667, alpha: 1))) 39 | 40 | \(" backgroundColor ", .background(#colorLiteral(red: 0.4508578777, green: 0.9882974029, blue: 0.8376303315, alpha: 1))) 41 | 42 | \(" backgroundColor ", .background(#colorLiteral(red: 0.4513868093, green: 0.9930960536, blue: 1, alpha: 1))) 43 | 44 | \(" backgroundColor ", .background(#colorLiteral(red: 0.4620226622, green: 0.8382837176, blue: 1, alpha: 1))) 45 | 46 | \(" backgroundColor ", .background(#colorLiteral(red: 0.476841867, green: 0.5048075914, blue: 1, alpha: 1))) 47 | 48 | \(" backgroundColor ", .background(#colorLiteral(red: 0.8446564078, green: 0.5145705342, blue: 1, alpha: 1))) 49 | 50 | \(" backgroundColor ", .background(#colorLiteral(red: 1, green: 0.5212053061, blue: 1, alpha: 1))) 51 | 52 | \(" backgroundColor ", .background(#colorLiteral(red: 1, green: 0.5409764051, blue: 0.8473142982, alpha: 1))) 53 | 54 | """ 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Demo/Demo/Details/BaselineOffsetViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaselineOffsetViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaselineOffsetViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | baseline offset: none 21 | 22 | --------------------- 23 | 24 | baseline \("offset: 0", .baselineOffset(0)) 25 | 26 | --------------------- 27 | 28 | baseline \("offset: 1", .baselineOffset(1)) 29 | 30 | --------------------- 31 | 32 | baseline \("offset: 3", .baselineOffset(3)) 33 | 34 | --------------------- 35 | 36 | baseline \("offset: 5", .baselineOffset(5)) 37 | 38 | --------------------- 39 | 40 | baseline \("offset: -1", .baselineOffset(-1)) 41 | 42 | --------------------- 43 | 44 | baseline \("offset: -3", .baselineOffset(-3)) 45 | 46 | --------------------- 47 | 48 | baseline \("offset: -5", .baselineOffset(-5)) 49 | 50 | """ 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Demo/Demo/Details/CheckingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckingViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2020/6/28. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedString 11 | 12 | class CheckingViewController: ViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | // 添加电话号码类型监听 18 | container.label.attributed.observe(.phoneNumber, with: .init(.click, highlights: [.foreground(#colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1))], with: { (result) in 19 | print(result) 20 | })) 21 | // 添加默认类型监听 22 | container.textView.attributed.observe(highlights: [.foreground(#colorLiteral(red: 0.5568627715, green: 0.3529411852, blue: 0.9686274529, alpha: 1))]) { (result) in 23 | print(result) 24 | } 25 | // 移除监听 26 | //container.textView.attributed.remove(checking: .link) 27 | 28 | func clicked(_ result: ASAttributedString.Action.Result) { 29 | switch result.content { 30 | case .string(let value): 31 | print("点击了文本: \n\(value) \nrange: \(result.range)") 32 | 33 | case .attachment(let value): 34 | print("点击了附件: \n\(value) \nrange: \(result.range)") 35 | } 36 | } 37 | 38 | do { 39 | var string: ASAttributedString = """ 40 | 我的名字叫李响,我的手机号码是18611401994,我的电子邮件地址是18611401994@163.com,现在是2020/06/28 20:30。我的GitHub主页是https://github.com/lixiang1994。欢迎来Star! \("点击联系我", .action(clicked)) 41 | """ 42 | string.add(attributes: [.foreground(#colorLiteral(red: 0.9529411793, green: 0.6862745285, blue: 0.1333333403, alpha: 1)), .font(.systemFont(ofSize: 20, weight: .medium))], checkings: [.phoneNumber]) 43 | string.add(attributes: [.foreground(#colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1)), .font(.systemFont(ofSize: 20, weight: .medium))], checkings: [.link]) 44 | string.add(attributes: [.foreground(#colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1)), .font(.systemFont(ofSize: 20, weight: .medium))], checkings: [.date]) 45 | 46 | // 测试action 47 | // string.add(attributes: [.action { 48 | // print("11") 49 | // }], range: .init(location: 3, length: 6)) 50 | // string.add(attributes: [.action { 51 | // print("22") 52 | // },.action { 53 | // print("33") 54 | // }], checkings: [.link]) 55 | 56 | container.label.attributed.text = string 57 | } 58 | 59 | do { 60 | var string: ASAttributedString = """ 61 | My name is Li Xiang, my mobile phone number is 18611401994, my email address is 18611401994@163.com, I live in No.10 Xitucheng Road, Haidian District, Beijing, China, and it is now 20:30 on June 28, 2020. My GitHub homepage is https://github.com/lixiang1994. Welcome to star me! \("Contact me", .action(clicked)) 62 | """ 63 | string.add(attributes: [.foreground(#colorLiteral(red: 0.9529411793, green: 0.6862745285, blue: 0.1333333403, alpha: 1))], checkings: [.address]) 64 | string.add(attributes: [.foreground(#colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1))], checkings: [.link, .phoneNumber]) 65 | string.add(attributes: [.foreground(#colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1))], checkings: [.date]) 66 | string.add(attributes: [.foreground(#colorLiteral(red: 0.9098039269, green: 0.4784313738, blue: 0.6431372762, alpha: 1))], checkings: [.regex("Li Xiang")]) 67 | string.add(attributes: [.font(.systemFont(ofSize: 16, weight: .medium))], checkings: [.action]) 68 | 69 | // 测试action 70 | // string.add(attributes: [.action { 71 | // print("11") 72 | // }], range: .init(location: 3, length: 6)) 73 | // string.add(attributes: [.action { 74 | // print("22") 75 | // },.action { 76 | // print("33") 77 | // }], checkings: [.link]) 78 | 79 | container.textView.attributed.text = string 80 | } 81 | 82 | container.tintAdjustmentMode = .normal 83 | } 84 | 85 | @IBAction func changeTintAction(_ sender: Any) { 86 | container.tintAdjustmentMode = container.tintAdjustmentMode == .normal ? .dimmed : .normal 87 | } 88 | } 89 | 90 | class CheckingView: UIView { 91 | 92 | @IBOutlet weak var label: UILabel! 93 | @IBOutlet weak var textView: UITextView! 94 | 95 | override func tintColorDidChange() { 96 | super.tintColorDidChange() 97 | let isDimmed = tintAdjustmentMode == .dimmed 98 | let color = isDimmed ? #colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1) : #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1) 99 | label.attributed.text?.add(attributes: [.foreground(color)], checkings: [.action]) 100 | textView.attributed.text.add(attributes: [.foreground(color)], checkings: [.action]) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Demo/Demo/Details/ExpansionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpansionViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ExpansionViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | expansion: none 21 | 22 | \("expansion: 0", .expansion(0)) 23 | 24 | \("expansion: 0.1", .expansion(0.1)) 25 | 26 | \("expansion: 0.3", .expansion(0.3)) 27 | 28 | \("expansion: 0.5", .expansion(0.5)) 29 | 30 | \("expansion: -0.1", .expansion(-0.1)) 31 | 32 | \("expansion: -0.3", .expansion(-0.3)) 33 | 34 | \("expansion: -0.5", .expansion(-0.5)) 35 | 36 | """ 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Demo/Demo/Details/FontViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedString 11 | 12 | class FontViewController: UIViewController { 13 | 14 | @IBOutlet weak var textView: UITextView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | textView.attributed.text = """ 20 | 21 | \("fontSize: 13", .font(.systemFont(ofSize: 13))) 22 | 23 | \("fontSize: 20", .font(.systemFont(ofSize: 20))) 24 | 25 | \("fontSize: 22 weight: semibold", .font(.systemFont(ofSize: 22, weight: .semibold))) 26 | 27 | """ 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Demo/Demo/Details/ForegroundColorViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForegroundColorViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ForegroundColorViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | \("foregroundColor", .foreground(.white)) 21 | 22 | \("foregroundColor", .foreground(#colorLiteral(red: 0.7952535152, green: 0.7952535152, blue: 0.7952535152, alpha: 1))) 23 | 24 | \("foregroundColor", .foreground(#colorLiteral(red: 0.5723067522, green: 0.5723067522, blue: 0.5723067522, alpha: 1))) 25 | 26 | \("foregroundColor", .foreground(#colorLiteral(red: 0.3179988265, green: 0.3179988265, blue: 0.3179988265, alpha: 1))) 27 | 28 | \("foregroundColor", .foreground(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1))) 29 | 30 | \("foregroundColor", .foreground(#colorLiteral(red: 1, green: 0.4932718873, blue: 0.4739984274, alpha: 1))) 31 | 32 | \("foregroundColor", .foreground(#colorLiteral(red: 1, green: 0.8323456645, blue: 0.4732058644, alpha: 1))) 33 | 34 | \("foregroundColor", .foreground(#colorLiteral(red: 0.9995340705, green: 0.988355577, blue: 0.4726552367, alpha: 1))) 35 | 36 | \("foregroundColor", .foreground(#colorLiteral(red: 0.8321695924, green: 0.985483706, blue: 0.4733308554, alpha: 1))) 37 | 38 | \("foregroundColor", .foreground(#colorLiteral(red: 0.4500938654, green: 0.9813225865, blue: 0.4743030667, alpha: 1))) 39 | 40 | \("foregroundColor", .foreground(#colorLiteral(red: 0.4508578777, green: 0.9882974029, blue: 0.8376303315, alpha: 1))) 41 | 42 | \("foregroundColor", .foreground(#colorLiteral(red: 0.4513868093, green: 0.9930960536, blue: 1, alpha: 1))) 43 | 44 | \("foregroundColor", .foreground(#colorLiteral(red: 0.4620226622, green: 0.8382837176, blue: 1, alpha: 1))) 45 | 46 | \("foregroundColor", .foreground(#colorLiteral(red: 0.476841867, green: 0.5048075914, blue: 1, alpha: 1))) 47 | 48 | \("foregroundColor", .foreground(#colorLiteral(red: 0.8446564078, green: 0.5145705342, blue: 1, alpha: 1))) 49 | 50 | \("foregroundColor", .foreground(#colorLiteral(red: 1, green: 0.5212053061, blue: 1, alpha: 1))) 51 | 52 | \("foregroundColor", .foreground(#colorLiteral(red: 1, green: 0.5409764051, blue: 0.8473142982, alpha: 1))) 53 | 54 | """ 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Demo/Demo/Details/KernViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KernViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class KernViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | kern: default 21 | 22 | \("kern: 0", .kern(0)) 23 | 24 | \("kern: 2", .kern(2)) 25 | 26 | \("kern: 5", .kern(5)) 27 | 28 | \("kern: 10", .kern(10)) 29 | 30 | """ 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Demo/Demo/Details/LigatureViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LigatureViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LigatureViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | \("ligature: 1", .ligature(true)) 21 | 22 | \("ligature: 0", .ligature(false)) 23 | 24 | """ 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Demo/Demo/Details/LinkViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AttributedString 11 | 12 | class LinkViewController: UIViewController { 13 | 14 | @IBOutlet weak var textView: UITextView! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | textView.attributed.text = """ 20 | 21 | link: none 22 | 23 | \("link: https://www.apple.com", .link("https://www.apple.com")) 24 | 25 | """ 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Demo/Demo/Details/ObliquenessViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObliquenessViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ObliquenessViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | obliqueness: none 21 | 22 | \("obliqueness: 0.1", .obliqueness(0.1)) 23 | 24 | \("obliqueness: 0.3", .obliqueness(0.3)) 25 | 26 | \("obliqueness: 0.5", .obliqueness(0.5)) 27 | 28 | \("obliqueness: 1.0", .obliqueness(1.0)) 29 | 30 | \("obliqueness: -0.1", .obliqueness(-0.1)) 31 | 32 | \("obliqueness: -0.3", .obliqueness(-0.3)) 33 | 34 | \("obliqueness: -0.5", .obliqueness(-0.5)) 35 | 36 | \("obliqueness: -1.0", .obliqueness(-1.0)) 37 | 38 | """ 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Demo/Demo/Details/ParagraphStyleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParagraphStyleViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ParagraphStyleViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | \( 21 | """ 22 | lineSpacing: 10, lineSpacing: 10 23 | lineSpacing: 10, lineSpacing: 10 24 | lineSpacing: 10 25 | """, .paragraph(.lineSpacing(10)) 26 | ) 27 | 28 | ------------------------ 29 | 30 | \("alignment: center", .paragraph(.alignment(.center))) 31 | 32 | ------------------------ 33 | 34 | \( 35 | """ 36 | firstLineHeadIndent: 20, firstLineHeadIndent: 20, firstLineHeadIndent: 20, firstLineHeadIndent: 20, firstLineHeadIndent: 20, firstLineHeadIndent: 20, firstLineHeadIndent: 20, firstLineHeadIndent: 20 37 | """, .paragraph(.firstLineHeadIndent(20)) 38 | ) 39 | 40 | ------------------------ 41 | 42 | \( 43 | """ 44 | headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20, headIndent: 20 45 | """, .paragraph(.headIndent(20)) 46 | ) 47 | 48 | ------------------------ 49 | 50 | \( 51 | """ 52 | baseWritingDirection: rightToLeft 53 | """, .paragraph(.baseWritingDirection(.rightToLeft)) 54 | ) 55 | 56 | """ 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demo/Demo/Details/ShadowViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShadowViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ShadowViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | shadow: none 21 | 22 | \("shadow: defalut", .shadow(.init())) 23 | 24 | \("shadow: offset 0 radius: 4 color: nil", .shadow(.init(offset: .zero, radius: 4))) 25 | 26 | \("shadow: offset 0 radius: 4 color: .gray", .shadow(.init(offset: .zero, radius: 4, color: .gray))) 27 | 28 | \("shadow: offset 3 radius: 4 color: .gray", .shadow(.init(offset: .init(width: 0, height: 3), radius: 4, color: .gray))) 29 | 30 | \("shadow: offset 3 radius: 10 color: .gray", .shadow(.init(offset: .init(width: 0, height: 3), radius: 10, color: .gray))) 31 | 32 | \("shadow: offset 10 radius: 1 color: .gray", .shadow(.init(offset: .init(width: 0, height: 10), radius: 1, color: .gray))) 33 | 34 | \("shadow: offset 4 radius: 3 color: .red", .shadow(.init(offset: .init(width: 0, height: 4), radius: 3, color: .red))) 35 | 36 | """ 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Demo/Demo/Details/StrikethroughViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StrikethroughViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class StrikethroughViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | strikethrough: none 21 | 22 | \("strikethrough: single", .strikethrough(.single)) 23 | 24 | \("strikethrough: thick", .strikethrough(.thick)) 25 | 26 | \("strikethrough: double", .strikethrough(.double)) 27 | 28 | \("strikethrough: 1", .strikethrough(.init(rawValue: 1))) 29 | 30 | \("strikethrough: 2", .strikethrough(.init(rawValue: 2))) 31 | 32 | \("strikethrough: 3", .strikethrough(.init(rawValue: 3))) 33 | 34 | \("strikethrough: 4", .strikethrough(.init(rawValue: 4))) 35 | 36 | \("strikethrough: 5", .strikethrough(.init(rawValue: 5))) 37 | 38 | \("strikethrough: thick color: .lightGray", .strikethrough(.thick, color: .lightGray)) 39 | 40 | \("strikethrough: double color: .red", .strikethrough(.double, color: .red)) 41 | 42 | """ 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Demo/Demo/Details/StrokeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StrokeViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class StrokeViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | stroke: none 21 | 22 | \("stroke: 0", .stroke()) 23 | 24 | \("stroke: 1", .stroke(1)) 25 | 26 | \("stroke: 2", .stroke(2)) 27 | 28 | \("stroke: 3", .stroke(3)) 29 | 30 | \("stroke: 3 color: .black", .stroke(3, color: .black)) 31 | 32 | \("stroke: 3 color: .blue", .stroke(3, color: .blue)) 33 | 34 | \("stroke: 3 color: .red", .stroke(3, color: .red)) 35 | 36 | """ 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Demo/Demo/Details/TextEffectViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextEffectViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TextEffectViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | textEffect: none 21 | 22 | \("textEffect: .letterpressStyle", .textEffect(.letterpressStyle)) 23 | 24 | """ 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Demo/Demo/Details/UnderlineViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnderlineViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class UnderlineViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | underline: none 21 | 22 | \("underline: single", .underline(.single)) 23 | 24 | \("underline: thick", .underline(.thick)) 25 | 26 | \("underline: double", .underline(.double)) 27 | 28 | \("underline: byWord", .underline(.byWord)) 29 | 30 | \("underline: patternDot thick", .underline([.patternDot, .thick])) 31 | 32 | \("underline: patternDash thick", .underline([.patternDash, .thick])) 33 | 34 | \("underline: patternDashDot thick", .underline([.patternDashDot, .thick])) 35 | 36 | \("underline: patternDashDotDot thick", .underline([.patternDashDotDot, .thick])) 37 | 38 | \("underline: 1", .underline(.init(rawValue: 1))) 39 | 40 | \("underline: 2", .underline(.init(rawValue: 2))) 41 | 42 | \("underline: 3", .underline(.init(rawValue: 3))) 43 | 44 | \("underline: 4", .underline(.init(rawValue: 4))) 45 | 46 | \("underline: 5", .underline(.init(rawValue: 5))) 47 | 48 | \("underline: thick color: .lightGray", .underline([.patternDot, .thick], color: .lightGray)) 49 | 50 | \("underline: double color: .red", .underline([.patternDot, .double], color: .red)) 51 | 52 | """ 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Demo/Demo/Details/VerticalGlyphFormViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalGlyphFormViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class VerticalGlyphFormViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | verticalGlyphForm: none 21 | 22 | \("verticalGlyphForm: 1", .verticalGlyphForm(true)) 23 | 24 | \("verticalGlyphForm: 0", .verticalGlyphForm(false)) 25 | 26 | 27 | \("Currently on iOS, it's always horizontal.", .foreground(.lightGray)) 28 | 29 | """ 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Demo/Demo/Details/WritingDirectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WritingDirectionViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2019/11/19. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class WritingDirectionViewController: UIViewController { 12 | 13 | @IBOutlet weak var textView: UITextView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | textView.attributed.text = """ 19 | 20 | writingDirection: none 21 | 22 | \("writingDirection: LRE", .writingDirection(.LRE)) 23 | 24 | \("writingDirection: RLE", .writingDirection(.RLE)) 25 | 26 | \("writingDirection: LRO", .writingDirection(.LRO)) 27 | 28 | \("writingDirection: RLO", .writingDirection(.RLO)) 29 | 30 | """ 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Demo/Demo/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 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIMainStoryboardFile 31 | Main 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | UIUserInterfaceStyle 50 | Light 51 | 52 | 53 | -------------------------------------------------------------------------------- /Demo/Demo/VideoPlayerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoPlayerView.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2020/7/8. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class VideoPlayerView: UIView { 12 | 13 | private var updateContentMode: ((UIView.ContentMode) -> Void)? 14 | private var updateLayout: ((CGSize, CAAnimation?) -> Void)? 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | } 19 | 20 | init(_ add: (UIView) -> Void) { 21 | super.init(frame: .zero) 22 | clipsToBounds = true 23 | add(self) 24 | } 25 | 26 | func observe(contentMode: @escaping ((UIView.ContentMode) -> Void)) { 27 | updateContentMode = { (mode) in 28 | contentMode(mode) 29 | } 30 | updateContentMode?(self.contentMode) 31 | } 32 | 33 | func observe(layout: @escaping ((CGSize, CAAnimation?) -> Void)) { 34 | updateLayout = { (size, animation) in 35 | layout(size, animation) 36 | } 37 | layoutSubviews() 38 | } 39 | 40 | required init?(coder aDecoder: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | override public var contentMode: UIView.ContentMode { 45 | get { 46 | return super.contentMode 47 | } 48 | set { 49 | super.contentMode = newValue 50 | self.updateContentMode?(newValue) 51 | } 52 | } 53 | 54 | override public func layoutSubviews() { 55 | super.layoutSubviews() 56 | 57 | updateLayout?(bounds.size, layer.animation(forKey: "bounds.size")) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Demo/Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo 4 | // 5 | // Created by Lee on 2020/6/28. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | var container: Container { view as! Container } 14 | 15 | override func loadView() { 16 | super.loadView() 17 | if view is Container { 18 | return 19 | } 20 | view = Container() 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | } 26 | 27 | override var prefersHomeIndicatorAutoHidden: Bool { 28 | return true 29 | } 30 | 31 | override var preferredStatusBarStyle: UIStatusBarStyle { 32 | return .default 33 | } 34 | 35 | override var shouldAutorotate: Bool { 36 | return false 37 | } 38 | 39 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 40 | return .portrait 41 | } 42 | 43 | override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { 44 | return .portrait 45 | } 46 | 47 | deinit { print("deinit:\t\(classForCoder)") } 48 | } 49 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "fastlane" 6 | gem "jazzy" 7 | gem "cocoapods" 8 | gem "xcode-install" 9 | gem "addressable", ">= 2.8.0" 10 | gem "rexml", ">= 3.2.5" -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.1.4) 9 | base64 10 | bigdecimal 11 | concurrent-ruby (~> 1.0, >= 1.0.2) 12 | connection_pool (>= 2.2.5) 13 | drb 14 | i18n (>= 1.6, < 2) 15 | minitest (>= 5.1) 16 | mutex_m 17 | tzinfo (~> 2.0) 18 | addressable (2.8.7) 19 | public_suffix (>= 2.0.2, < 7.0) 20 | algoliasearch (1.27.5) 21 | httpclient (~> 2.8, >= 2.8.3) 22 | json (>= 1.5.1) 23 | artifactory (3.0.17) 24 | atomos (0.1.3) 25 | aws-eventstream (1.3.0) 26 | aws-partitions (1.977.0) 27 | aws-sdk-core (3.206.0) 28 | aws-eventstream (~> 1, >= 1.3.0) 29 | aws-partitions (~> 1, >= 1.651.0) 30 | aws-sigv4 (~> 1.9) 31 | jmespath (~> 1, >= 1.6.1) 32 | aws-sdk-kms (1.91.0) 33 | aws-sdk-core (~> 3, >= 3.205.0) 34 | aws-sigv4 (~> 1.5) 35 | aws-sdk-s3 (1.163.0) 36 | aws-sdk-core (~> 3, >= 3.205.0) 37 | aws-sdk-kms (~> 1) 38 | aws-sigv4 (~> 1.5) 39 | aws-sigv4 (1.10.0) 40 | aws-eventstream (~> 1, >= 1.0.2) 41 | babosa (1.0.4) 42 | base64 (0.2.0) 43 | bigdecimal (3.1.8) 44 | claide (1.1.0) 45 | cocoapods (1.15.2) 46 | addressable (~> 2.8) 47 | claide (>= 1.0.2, < 2.0) 48 | cocoapods-core (= 1.15.2) 49 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 50 | cocoapods-downloader (>= 2.1, < 3.0) 51 | cocoapods-plugins (>= 1.0.0, < 2.0) 52 | cocoapods-search (>= 1.0.0, < 2.0) 53 | cocoapods-trunk (>= 1.6.0, < 2.0) 54 | cocoapods-try (>= 1.1.0, < 2.0) 55 | colored2 (~> 3.1) 56 | escape (~> 0.0.4) 57 | fourflusher (>= 2.3.0, < 3.0) 58 | gh_inspector (~> 1.0) 59 | molinillo (~> 0.8.0) 60 | nap (~> 1.0) 61 | ruby-macho (>= 2.3.0, < 3.0) 62 | xcodeproj (>= 1.23.0, < 2.0) 63 | cocoapods-core (1.15.2) 64 | activesupport (>= 5.0, < 8) 65 | addressable (~> 2.8) 66 | algoliasearch (~> 1.0) 67 | concurrent-ruby (~> 1.1) 68 | fuzzy_match (~> 2.0.4) 69 | nap (~> 1.0) 70 | netrc (~> 0.11) 71 | public_suffix (~> 4.0) 72 | typhoeus (~> 1.0) 73 | cocoapods-deintegrate (1.0.5) 74 | cocoapods-downloader (2.1) 75 | cocoapods-plugins (1.0.0) 76 | nap 77 | cocoapods-search (1.0.1) 78 | cocoapods-trunk (1.6.0) 79 | nap (>= 0.8, < 2.0) 80 | netrc (~> 0.11) 81 | cocoapods-try (1.2.0) 82 | colored (1.2) 83 | colored2 (3.1.2) 84 | commander (4.6.0) 85 | highline (~> 2.0.0) 86 | concurrent-ruby (1.3.4) 87 | connection_pool (2.4.1) 88 | declarative (0.0.20) 89 | digest-crc (0.6.5) 90 | rake (>= 12.0.0, < 14.0.0) 91 | domain_name (0.6.20240107) 92 | dotenv (2.8.1) 93 | drb (2.2.1) 94 | emoji_regex (3.2.3) 95 | escape (0.0.4) 96 | ethon (0.16.0) 97 | ffi (>= 1.15.0) 98 | excon (0.111.0) 99 | faraday (1.10.3) 100 | faraday-em_http (~> 1.0) 101 | faraday-em_synchrony (~> 1.0) 102 | faraday-excon (~> 1.1) 103 | faraday-httpclient (~> 1.0) 104 | faraday-multipart (~> 1.0) 105 | faraday-net_http (~> 1.0) 106 | faraday-net_http_persistent (~> 1.0) 107 | faraday-patron (~> 1.0) 108 | faraday-rack (~> 1.0) 109 | faraday-retry (~> 1.0) 110 | ruby2_keywords (>= 0.0.4) 111 | faraday-cookie_jar (0.0.7) 112 | faraday (>= 0.8.0) 113 | http-cookie (~> 1.0.0) 114 | faraday-em_http (1.0.0) 115 | faraday-em_synchrony (1.0.0) 116 | faraday-excon (1.1.0) 117 | faraday-httpclient (1.0.1) 118 | faraday-multipart (1.0.4) 119 | multipart-post (~> 2) 120 | faraday-net_http (1.0.2) 121 | faraday-net_http_persistent (1.2.0) 122 | faraday-patron (1.0.0) 123 | faraday-rack (1.0.0) 124 | faraday-retry (1.0.3) 125 | faraday_middleware (1.2.0) 126 | faraday (~> 1.0) 127 | fastimage (2.3.1) 128 | fastlane (2.222.0) 129 | CFPropertyList (>= 2.3, < 4.0.0) 130 | addressable (>= 2.8, < 3.0.0) 131 | artifactory (~> 3.0) 132 | aws-sdk-s3 (~> 1.0) 133 | babosa (>= 1.0.3, < 2.0.0) 134 | bundler (>= 1.12.0, < 3.0.0) 135 | colored (~> 1.2) 136 | commander (~> 4.6) 137 | dotenv (>= 2.1.1, < 3.0.0) 138 | emoji_regex (>= 0.1, < 4.0) 139 | excon (>= 0.71.0, < 1.0.0) 140 | faraday (~> 1.0) 141 | faraday-cookie_jar (~> 0.0.6) 142 | faraday_middleware (~> 1.0) 143 | fastimage (>= 2.1.0, < 3.0.0) 144 | gh_inspector (>= 1.1.2, < 2.0.0) 145 | google-apis-androidpublisher_v3 (~> 0.3) 146 | google-apis-playcustomapp_v1 (~> 0.1) 147 | google-cloud-env (>= 1.6.0, < 2.0.0) 148 | google-cloud-storage (~> 1.31) 149 | highline (~> 2.0) 150 | http-cookie (~> 1.0.5) 151 | json (< 3.0.0) 152 | jwt (>= 2.1.0, < 3) 153 | mini_magick (>= 4.9.4, < 5.0.0) 154 | multipart-post (>= 2.0.0, < 3.0.0) 155 | naturally (~> 2.2) 156 | optparse (>= 0.1.1, < 1.0.0) 157 | plist (>= 3.1.0, < 4.0.0) 158 | rubyzip (>= 2.0.0, < 3.0.0) 159 | security (= 0.1.5) 160 | simctl (~> 1.6.3) 161 | terminal-notifier (>= 2.0.0, < 3.0.0) 162 | terminal-table (~> 3) 163 | tty-screen (>= 0.6.3, < 1.0.0) 164 | tty-spinner (>= 0.8.0, < 1.0.0) 165 | word_wrap (~> 1.0.0) 166 | xcodeproj (>= 1.13.0, < 2.0.0) 167 | xcpretty (~> 0.3.0) 168 | xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) 169 | ffi (1.17.0) 170 | fourflusher (2.3.1) 171 | fuzzy_match (2.0.4) 172 | gh_inspector (1.1.3) 173 | google-apis-androidpublisher_v3 (0.54.0) 174 | google-apis-core (>= 0.11.0, < 2.a) 175 | google-apis-core (0.11.3) 176 | addressable (~> 2.5, >= 2.5.1) 177 | googleauth (>= 0.16.2, < 2.a) 178 | httpclient (>= 2.8.1, < 3.a) 179 | mini_mime (~> 1.0) 180 | representable (~> 3.0) 181 | retriable (>= 2.0, < 4.a) 182 | rexml 183 | google-apis-iamcredentials_v1 (0.17.0) 184 | google-apis-core (>= 0.11.0, < 2.a) 185 | google-apis-playcustomapp_v1 (0.13.0) 186 | google-apis-core (>= 0.11.0, < 2.a) 187 | google-apis-storage_v1 (0.31.0) 188 | google-apis-core (>= 0.11.0, < 2.a) 189 | google-cloud-core (1.7.1) 190 | google-cloud-env (>= 1.0, < 3.a) 191 | google-cloud-errors (~> 1.0) 192 | google-cloud-env (1.6.0) 193 | faraday (>= 0.17.3, < 3.0) 194 | google-cloud-errors (1.4.0) 195 | google-cloud-storage (1.47.0) 196 | addressable (~> 2.8) 197 | digest-crc (~> 0.4) 198 | google-apis-iamcredentials_v1 (~> 0.1) 199 | google-apis-storage_v1 (~> 0.31.0) 200 | google-cloud-core (~> 1.6) 201 | googleauth (>= 0.16.2, < 2.a) 202 | mini_mime (~> 1.0) 203 | googleauth (1.8.1) 204 | faraday (>= 0.17.3, < 3.a) 205 | jwt (>= 1.4, < 3.0) 206 | multi_json (~> 1.11) 207 | os (>= 0.9, < 2.0) 208 | signet (>= 0.16, < 2.a) 209 | highline (2.0.3) 210 | http-cookie (1.0.7) 211 | domain_name (~> 0.5) 212 | httpclient (2.8.3) 213 | i18n (1.14.6) 214 | concurrent-ruby (~> 1.0) 215 | jazzy (0.15.1) 216 | cocoapods (~> 1.5) 217 | mustache (~> 1.1) 218 | open4 (~> 1.3) 219 | redcarpet (~> 3.4) 220 | rexml (>= 3.2.7, < 4.0) 221 | rouge (>= 2.0.6, < 5.0) 222 | sassc (~> 2.1) 223 | sqlite3 (~> 1.3) 224 | xcinvoke (~> 0.3.0) 225 | jmespath (1.6.2) 226 | json (2.7.2) 227 | jwt (2.9.0) 228 | base64 229 | liferaft (0.0.6) 230 | mini_magick (4.13.2) 231 | mini_mime (1.1.5) 232 | mini_portile2 (2.8.7) 233 | minitest (5.25.1) 234 | molinillo (0.8.0) 235 | multi_json (1.15.0) 236 | multipart-post (2.4.1) 237 | mustache (1.1.1) 238 | mutex_m (0.2.0) 239 | nanaimo (0.3.0) 240 | nap (1.1.0) 241 | naturally (2.2.1) 242 | netrc (0.11.0) 243 | nkf (0.2.0) 244 | open4 (1.3.4) 245 | optparse (0.5.0) 246 | os (1.1.4) 247 | plist (3.7.1) 248 | public_suffix (4.0.7) 249 | rake (13.2.1) 250 | redcarpet (3.6.0) 251 | representable (3.2.0) 252 | declarative (< 0.1.0) 253 | trailblazer-option (>= 0.1.1, < 0.2.0) 254 | uber (< 0.2.0) 255 | retriable (3.1.2) 256 | rexml (3.3.7) 257 | rouge (2.0.7) 258 | ruby-macho (2.5.1) 259 | ruby2_keywords (0.0.5) 260 | rubyzip (2.3.2) 261 | sassc (2.4.0) 262 | ffi (~> 1.9) 263 | security (0.1.5) 264 | signet (0.19.0) 265 | addressable (~> 2.8) 266 | faraday (>= 0.17.5, < 3.a) 267 | jwt (>= 1.5, < 3.0) 268 | multi_json (~> 1.10) 269 | simctl (1.6.10) 270 | CFPropertyList 271 | naturally 272 | sqlite3 (1.7.3) 273 | mini_portile2 (~> 2.8.0) 274 | terminal-notifier (2.0.0) 275 | terminal-table (3.0.2) 276 | unicode-display_width (>= 1.1.1, < 3) 277 | trailblazer-option (0.1.2) 278 | tty-cursor (0.7.1) 279 | tty-screen (0.8.2) 280 | tty-spinner (0.9.3) 281 | tty-cursor (~> 0.7) 282 | typhoeus (1.4.1) 283 | ethon (>= 0.9.0) 284 | tzinfo (2.0.6) 285 | concurrent-ruby (~> 1.0) 286 | uber (0.1.0) 287 | unicode-display_width (2.6.0) 288 | word_wrap (1.0.0) 289 | xcinvoke (0.3.0) 290 | liferaft (~> 0.0.6) 291 | xcode-install (2.8.1) 292 | claide (>= 0.9.1) 293 | fastlane (>= 2.1.0, < 3.0.0) 294 | xcodeproj (1.25.0) 295 | CFPropertyList (>= 2.3.3, < 4.0) 296 | atomos (~> 0.1.3) 297 | claide (>= 1.0.2, < 2.0) 298 | colored2 (~> 3.1) 299 | nanaimo (~> 0.3.0) 300 | rexml (>= 3.3.2, < 4.0) 301 | xcpretty (0.3.0) 302 | rouge (~> 2.0.7) 303 | xcpretty-travis-formatter (1.0.1) 304 | xcpretty (~> 0.2, >= 0.0.7) 305 | 306 | PLATFORMS 307 | ruby 308 | 309 | DEPENDENCIES 310 | addressable (>= 2.8.0) 311 | cocoapods 312 | fastlane 313 | jazzy 314 | rexml (>= 3.2.5) 315 | xcode-install 316 | 317 | BUNDLED WITH 318 | 2.2.3 319 | -------------------------------------------------------------------------------- /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 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 LEE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 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: "AttributedString", 8 | platforms: [.iOS(.v9), .macOS(.v10_13), .tvOS(.v11), .watchOS(.v5)], 9 | products: [ 10 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 11 | .library( 12 | name: "AttributedString", 13 | targets: ["AttributedString"]), 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: "AttributedString", 24 | dependencies: [], 25 | path: "Sources", 26 | resources: [.process("PrivacyInfo.xcprivacy")] 27 | ) 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /Resources/all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Resources/all.png -------------------------------------------------------------------------------- /Resources/coding.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Resources/coding.gif -------------------------------------------------------------------------------- /Resources/font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Resources/font.png -------------------------------------------------------------------------------- /Resources/kern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Resources/kern.png -------------------------------------------------------------------------------- /Resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Resources/logo.png -------------------------------------------------------------------------------- /Resources/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Resources/logo.sketch -------------------------------------------------------------------------------- /Resources/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Resources/simple.png -------------------------------------------------------------------------------- /Resources/stroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiang1994/AttributedString/d8a72a7e29e8699979b052b59659720087bc2ea0/Resources/stroke.png -------------------------------------------------------------------------------- /Sources/Attribute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedStringAttribute.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2019/11/18. 11 | // Copyright © 2019 LEE. All rights reserved. 12 | // 13 | 14 | #if os(macOS) 15 | import AppKit 16 | #else 17 | import UIKit 18 | #endif 19 | 20 | extension ASAttributedString { 21 | 22 | /// 属性 23 | public struct Attribute { 24 | let attributes: [NSAttributedString.Key: Any] 25 | } 26 | 27 | /// 包装模式 28 | public enum WrapMode { 29 | case embedding(ASAttributedString) // 嵌入模式 30 | case override(ASAttributedString) // 覆盖模式 31 | 32 | internal var value: ASAttributedString { 33 | switch self { 34 | case .embedding(let value): return value 35 | case .override(let value): return value 36 | } 37 | } 38 | } 39 | } 40 | 41 | extension ASAttributedString.Attribute { 42 | 43 | public static func custom(_ value: [NSAttributedString.Key: Any]) -> Self { 44 | return .init(attributes: value) 45 | } 46 | 47 | public static func font(_ value: ASFont) -> Self { 48 | return .init(attributes: [.font: value]) 49 | } 50 | 51 | public static func foreground(_ value: ASColor) -> Self { 52 | return .init(attributes: [.foregroundColor: value]) 53 | } 54 | 55 | public static func background(_ value: ASColor) -> Self { 56 | return .init(attributes: [.backgroundColor: value]) 57 | } 58 | 59 | public static func ligature(_ value: Bool) -> Self { 60 | return .init(attributes: [.ligature: value ? 1 : 0]) 61 | } 62 | 63 | public static func kern(_ value: CGFloat) -> Self { 64 | return .init(attributes: [.kern: value]) 65 | } 66 | 67 | public static func strikethrough(_ style: NSUnderlineStyle, color: ASColor? = nil) -> Self { 68 | var temp: [NSAttributedString.Key: Any] = [:] 69 | temp[.strikethroughColor] = color 70 | temp[.strikethroughStyle] = style.rawValue 71 | return .init(attributes: temp) 72 | } 73 | 74 | public static func underline(_ style: NSUnderlineStyle, color: ASColor? = nil) -> Self { 75 | var temp: [NSAttributedString.Key: Any] = [:] 76 | temp[.underlineColor] = color 77 | temp[.underlineStyle] = style.rawValue 78 | return .init(attributes: temp) 79 | } 80 | 81 | public static func link(_ value: String) -> Self { 82 | guard let url = URL(string: value) else { return .init(attributes: [:])} 83 | 84 | return link(url) 85 | } 86 | public static func link(_ value: URL) -> Self { 87 | return .init(attributes: [.link: value]) 88 | } 89 | 90 | public static func baselineOffset(_ value: CGFloat) -> Self { 91 | return .init(attributes: [.baselineOffset: value]) 92 | } 93 | 94 | public static func shadow(_ value: NSShadow) -> Self { 95 | return .init(attributes: [.shadow: value]) 96 | } 97 | 98 | public static func stroke(_ width: CGFloat = 0, color: ASColor? = nil) -> Self { 99 | var temp: [NSAttributedString.Key: Any] = [:] 100 | temp[.strokeColor] = color 101 | temp[.strokeWidth] = width 102 | return .init(attributes: temp) 103 | } 104 | 105 | public static func textEffect(_ value: String) -> Self { 106 | return .init(attributes: [.textEffect: value]) 107 | } 108 | public static func textEffect(_ value: NSAttributedString.TextEffectStyle) -> Self { 109 | return textEffect(value.rawValue) 110 | } 111 | 112 | public static func obliqueness(_ value: CGFloat = 0.1) -> Self { 113 | return .init(attributes: [.obliqueness: value]) 114 | } 115 | 116 | public static func expansion(_ value: CGFloat = 0.0) -> Self { 117 | return .init(attributes: [.expansion: value]) 118 | } 119 | 120 | public static func writingDirection(_ value: [Int]) -> Self { 121 | return .init(attributes: [.writingDirection: value]) 122 | } 123 | public static func writingDirection(_ value: WritingDirection) -> Self { 124 | return writingDirection(value.value) 125 | } 126 | 127 | public static func verticalGlyphForm(_ value: Bool) -> Self { 128 | return .init(attributes: [.verticalGlyphForm: value ? 1 : 0]) 129 | } 130 | } 131 | 132 | #if os(macOS) 133 | 134 | extension ASAttributedString.Attribute { 135 | 136 | public static func cursor(_ value: NSCursor) -> Self { 137 | return .init(attributes: [.cursor: value]) 138 | } 139 | 140 | public static func markedClauseSegment(_ value: Int) -> Self { 141 | return .init(attributes: [.markedClauseSegment: value]) 142 | } 143 | 144 | public static func spellingState(_ value: SpellingState) -> Self { 145 | return .init(attributes: [.spellingState: value.rawValue]) 146 | } 147 | 148 | public static func superscript(_ value: Int) -> Self { 149 | return .init(attributes: [.superscript: value]) 150 | } 151 | 152 | public static func textAlternatives(_ value: NSTextAlternatives) -> Self { 153 | return .init(attributes: [.textAlternatives: value]) 154 | } 155 | 156 | public static func toolTip(_ value: String) -> Self { 157 | return .init(attributes: [.toolTip: value]) 158 | } 159 | } 160 | 161 | extension ASAttributedString.Attribute { 162 | 163 | /** 164 | This enum controls the display of the spelling and grammar indicators on text, 165 | highlighting portions of the text that are flagged for spelling or grammar issues. 166 | This should be used with `Attribute.spellingState`. 167 | */ 168 | public enum SpellingState: Int { 169 | 170 | /// The spelling error indicator. 171 | case spelling = 1 172 | 173 | /// The grammar error indicator. 174 | case grammar = 2 175 | } 176 | } 177 | 178 | #endif 179 | 180 | extension ASAttributedString.Attribute { 181 | 182 | public enum WritingDirection { 183 | case LRE 184 | case RLE 185 | case LRO 186 | case RLO 187 | 188 | fileprivate var value: [Int] { 189 | switch self { 190 | case .LRE: return [NSWritingDirection.leftToRight.rawValue | NSWritingDirectionFormatType.embedding.rawValue] 191 | 192 | case .RLE: return [NSWritingDirection.rightToLeft.rawValue | NSWritingDirectionFormatType.embedding.rawValue] 193 | 194 | case .LRO: return [NSWritingDirection.leftToRight.rawValue | NSWritingDirectionFormatType.override.rawValue] 195 | 196 | case .RLO: return [NSWritingDirection.rightToLeft.rawValue | NSWritingDirectionFormatType.override.rawValue] 197 | } 198 | } 199 | } 200 | } 201 | 202 | extension ASAttributedStringInterpolation { 203 | 204 | public typealias Attribute = ASAttributedString.Attribute 205 | public typealias WrapMode = ASAttributedString.WrapMode 206 | 207 | public mutating func appendInterpolation(_ value: T, _ attributes: Attribute...) { 208 | appendInterpolation(value, with: attributes) 209 | } 210 | public mutating func appendInterpolation(_ value: T, with attributes: [Attribute]) { 211 | self.value.append(ASAttributedString("\(value)", with: attributes).value) 212 | } 213 | 214 | public mutating func appendInterpolation(_ value: NSAttributedString, _ attributes: Attribute...) { 215 | appendInterpolation(value, with: attributes) 216 | } 217 | public mutating func appendInterpolation(_ value: NSAttributedString, with attributes: [Attribute]) { 218 | self.value.append(ASAttributedString(value, with: attributes).value) 219 | } 220 | 221 | public mutating func appendInterpolation(_ value: ASAttributedString, _ attributes: Attribute...) { 222 | appendInterpolation(value, with: attributes) 223 | } 224 | public mutating func appendInterpolation(_ value: ASAttributedString, with attributes: [Attribute]) { 225 | self.value.append(ASAttributedString(value, with: attributes).value) 226 | } 227 | 228 | // 嵌套包装 229 | public mutating func appendInterpolation(wrap string: ASAttributedString, _ attributes: Attribute...) { 230 | appendInterpolation(wrap: string, with: attributes) 231 | } 232 | public mutating func appendInterpolation(wrap string: ASAttributedString, with attributes: [Attribute]) { 233 | self.value.append(ASAttributedString(string, with: attributes).value) 234 | } 235 | 236 | public mutating func appendInterpolation(wrap mode: WrapMode, _ attributes: Attribute...) { 237 | appendInterpolation(wrap: mode, with: attributes) 238 | } 239 | public mutating func appendInterpolation(wrap mode: WrapMode, with attributes: [Attribute]) { 240 | self.value.append(ASAttributedString(wrap: mode, with: attributes).value) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /Sources/AttributedString.h: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedString.h 3 | // AttributedString 4 | // 5 | // Created by Lee on 2019/11/18. 6 | // Copyright © 2019 LEE. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for AttributedString. 12 | FOUNDATION_EXPORT double AttributedStringVersionNumber; 13 | 14 | //! Project version string for AttributedString. 15 | FOUNDATION_EXPORT const unsigned char AttributedStringVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/AttributedString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedString.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2019/11/18. 11 | // Copyright © 2019 LEE. All rights reserved. 12 | // 13 | 14 | #if os(macOS) 15 | import AppKit 16 | public typealias ASImage = NSImage 17 | public typealias ASColor = NSColor 18 | public typealias ASFont = NSFont 19 | #else 20 | import UIKit 21 | public typealias ASImage = UIImage 22 | public typealias ASColor = UIColor 23 | public typealias ASFont = UIFont 24 | #endif 25 | 26 | public struct ASAttributedString { 27 | 28 | internal init(value: NSAttributedString) { 29 | self.value = value 30 | } 31 | 32 | 33 | public internal(set) var value: NSAttributedString 34 | 35 | public var length: Int { 36 | value.length 37 | } 38 | 39 | /// String 40 | 41 | public init(string value: String, _ attributes: Attribute...) { 42 | self.value = ASAttributedString(string: value, with: attributes).value 43 | } 44 | 45 | public init(string value: String, with attributes: [Attribute] = []) { 46 | self.value = ASAttributedString(.init(string: value), with: attributes).value 47 | } 48 | 49 | /// NSAttributedString 50 | 51 | public init(_ value: NSAttributedString) { 52 | self.value = value 53 | } 54 | 55 | public init(_ value: NSAttributedString, _ attributes: Attribute...) { 56 | self.value = ASAttributedString(value, with: attributes).value 57 | } 58 | 59 | public init?(_ value: NSAttributedString?, _ attributes: Attribute...) { 60 | guard let value = value else { return nil } 61 | self.value = ASAttributedString(value, with: attributes).value 62 | } 63 | 64 | public init(_ value: NSAttributedString, with attributes: [Attribute]) { 65 | self.value = ASAttributedString(.init(value), with: attributes).value 66 | } 67 | 68 | public init?(_ value: NSAttributedString?, with attributes: [Attribute] = []) { 69 | guard let value = value else { return nil } 70 | self.value = ASAttributedString(.init(value), with: attributes).value 71 | } 72 | 73 | /// AttributedString 74 | 75 | public init(_ string: ASAttributedString, _ attributes: Attribute...) { 76 | self.value = ASAttributedString(wrap: .embedding(string), with: attributes).value 77 | } 78 | 79 | public init(_ string: ASAttributedString, with attributes: [Attribute] = []) { 80 | self.value = ASAttributedString(wrap: .embedding(string), with: attributes).value 81 | } 82 | 83 | public init(wrap mode: WrapMode, _ attributes: Attribute...) { 84 | self.value = ASAttributedString(wrap: mode, with: attributes).value 85 | } 86 | 87 | public init(wrap mode: WrapMode, with attributes: [Attribute]) { 88 | guard !attributes.isEmpty else { 89 | self.value = mode.value.value 90 | return 91 | } 92 | 93 | #if os(iOS) || os(macOS) 94 | // 合并多个Action 95 | let attributes = attributes.mergedAction() 96 | 97 | #endif 98 | 99 | // 获取通用属性 100 | var temp: [NSAttributedString.Key: Any] = [:] 101 | attributes.forEach { temp.merge($0.attributes, uniquingKeysWith: { $1 }) } 102 | // 创建可变富文本 103 | let string: NSMutableAttributedString 104 | switch mode { 105 | case .embedding(let value): 106 | string = .init(attributedString: value.value) 107 | // 过滤后的属性以及范围 108 | var ranges: [([NSAttributedString.Key: Any], NSRange)] = [] 109 | // 遍历原属性 去除重复属性 防止覆盖 110 | string.enumerateAttributes( 111 | in: .init(location: 0, length: string.length), 112 | options: .longestEffectiveRangeNotRequired 113 | ) { (attributs, range, stop) in 114 | // 差集 从通用属性中过滤掉原本就存在的属性 115 | let keys = Set(temp.keys).subtracting(Set(attributs.keys)) 116 | ranges.append((temp.filter { keys.contains($0.key) }, range)) 117 | } 118 | // 添加过滤后的属性和相应的范围 119 | ranges.forEach { string.addAttributes($0, range: $1) } 120 | 121 | case .override(let value): 122 | string = .init(attributedString: value.value) 123 | string.addAttributes(temp, range: .init(location: 0, length: string.length)) 124 | } 125 | 126 | self.value = string 127 | } 128 | } 129 | 130 | extension ASAttributedString: ExpressibleByStringLiteral { 131 | 132 | public init(stringLiteral value: String) { 133 | self.value = .init(string: value) 134 | } 135 | } 136 | 137 | extension ASAttributedString: CustomStringConvertible { 138 | 139 | public var description: String { 140 | .init(describing: value) 141 | } 142 | } 143 | 144 | extension ASAttributedString: Equatable { 145 | 146 | public static func == (lhs: ASAttributedString, rhs: ASAttributedString) -> Bool { 147 | guard lhs.length == rhs.length else { 148 | return false 149 | } 150 | guard lhs.value.string == rhs.value.string else { 151 | return false 152 | } 153 | guard lhs.value.get(.init(location: 0, length: lhs.length)) == rhs.value.get(.init(location: 0, length: rhs.length)) else { 154 | return false 155 | } 156 | return true 157 | } 158 | 159 | /// 内容是否相等 160 | /// - Parameter other: 其他AttributedString 161 | /// - Returns: 结果 162 | public func isContentEqual(to other: ASAttributedString?) -> Bool { 163 | guard let other = other else { 164 | return false 165 | } 166 | guard length == other.length else { 167 | return false 168 | } 169 | return value.string == other.value.string 170 | } 171 | } 172 | 173 | extension ASAttributedString { 174 | 175 | public mutating func add(attributes: [Attribute], range: NSRange) { 176 | guard !attributes.isEmpty, range.length > 0 else { return } 177 | 178 | #if os(iOS) || os(macOS) 179 | // 合并多个Action 180 | let attributes = attributes.mergedAction() 181 | 182 | #endif 183 | 184 | var temp: [NSAttributedString.Key: Any] = [:] 185 | attributes.forEach { temp.merge($0.attributes, uniquingKeysWith: { $1 }) } 186 | let string = NSMutableAttributedString(attributedString: value) 187 | string.addAttributes(temp, range: range) 188 | value = string 189 | } 190 | 191 | public mutating func set(attributes: [Attribute], range: NSRange) { 192 | guard !attributes.isEmpty, range.length > 0 else { return } 193 | 194 | #if os(iOS) || os(macOS) 195 | // 合并多个Action 196 | let attributes = attributes.mergedAction() 197 | 198 | #endif 199 | 200 | var temp: [NSAttributedString.Key: Any] = [:] 201 | attributes.forEach { temp.merge($0.attributes, uniquingKeysWith: { $1 }) } 202 | let string = NSMutableAttributedString(attributedString: value) 203 | string.setAttributes(temp, range: range) 204 | value = string 205 | } 206 | } 207 | 208 | extension ASAttributedString { 209 | 210 | public func add(attributes: Attribute..., range: NSRange? = .none) -> Self { 211 | return add(attributes, range: range ?? .init(location: 0, length: length)) 212 | } 213 | 214 | public func add(_ attributes: [Attribute], range: NSRange) -> Self { 215 | var temp = self 216 | temp.add(attributes: attributes, range: range) 217 | return temp 218 | } 219 | 220 | public func set(attributes: Attribute..., range: NSRange? = .none) -> Self { 221 | return set(attributes, range: range ?? .init(location: 0, length: length)) 222 | } 223 | 224 | public func set(_ attributes: [Attribute], range: NSRange) -> Self { 225 | var temp = self 226 | temp.set(attributes: attributes, range: range) 227 | return temp 228 | } 229 | } 230 | 231 | fileprivate extension Dictionary where Key == NSAttributedString.Key, Value == Any { 232 | 233 | static func == (lhs: [NSAttributedString.Key: Any], rhs: [NSAttributedString.Key: Any]) -> Bool { 234 | lhs.keys == rhs.keys ? NSDictionary(dictionary: lhs).isEqual(to: rhs) : false 235 | } 236 | } 237 | 238 | fileprivate extension Dictionary where Key == NSRange, Value == [NSAttributedString.Key: Any] { 239 | 240 | static func == (lhs: [NSRange: [NSAttributedString.Key: Any]], rhs: [NSRange: [NSAttributedString.Key: Any]]) -> Bool { 241 | guard lhs.count == rhs.count else { 242 | return false 243 | } 244 | return zip(lhs, rhs).allSatisfy { (l, r) -> Bool in 245 | l.0 == r.0 && l.1 == r.1 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /Sources/Extension/ArrayExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayExtension.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2020/7/4. 11 | // Copyright © 2020 LEE. All rights reserved. 12 | // 13 | 14 | extension Array { 15 | 16 | /// 过滤重复元素 17 | /// - Parameter path: KeyPath条件 18 | func filtered(duplication path: KeyPath) -> [Element] { 19 | return reduce(into: [Element]()) { (result, e) in 20 | let contains = result.contains { $0[keyPath: path] == e[keyPath: path] } 21 | result += contains ? [] : [e] 22 | } 23 | } 24 | 25 | /// 过滤重复元素 26 | /// - Parameter closure: 过滤条件 27 | func filtered(duplication closure: (Element) throws -> E) rethrows -> [Element] { 28 | return try reduce(into: [Element]()) { (result, e) in 29 | let contains = try result.contains { try closure($0) == closure(e) } 30 | result += contains ? [] : [e] 31 | } 32 | } 33 | 34 | /// 过滤重复元素 35 | /// - Parameter path: KeyPath条件 36 | @discardableResult 37 | mutating func filter(duplication path: KeyPath) -> [Element] { 38 | self = filtered(duplication: path) 39 | return self 40 | } 41 | 42 | /// 过滤重复元素 43 | /// - Parameter closure: 过滤条件 44 | @discardableResult 45 | mutating func filter(duplication closure: (Element) throws -> E) rethrows -> [Element] { 46 | self = try filtered(duplication: closure) 47 | return self 48 | } 49 | } 50 | 51 | extension Array where Element: Equatable { 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Extension/CoreGraphics/CGPointExtension.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | extension CGPoint { 4 | 5 | /// Creates a point with unnamed arguments. 6 | init(_ x: CGFloat, _ y: CGFloat) { 7 | self.init() 8 | self.x = x 9 | self.y = y 10 | } 11 | 12 | /// Returns a copy with the x value changed. 13 | func with(x: CGFloat) -> CGPoint { 14 | return .init(x: x, y: y) 15 | } 16 | /// Returns a copy with the y value changed. 17 | func with(y: CGFloat) -> CGPoint { 18 | return .init(x: x, y: y) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Extension/CoreGraphics/CGRectExtension.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | extension CGRect { 4 | 5 | /// Creates a rect with unnamed arguments. 6 | init(_ origin: CGPoint = .zero, _ size: CGSize = .zero) { 7 | self.init() 8 | self.origin = origin 9 | self.size = size 10 | } 11 | 12 | /// Creates a rect with unnamed arguments. 13 | init(_ x: CGFloat, _ y: CGFloat, _ width: CGFloat, _ height: CGFloat) { 14 | self.init() 15 | self.origin = CGPoint(x: x, y: y) 16 | self.size = CGSize(width: width, height: height) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Extension/CoreGraphics/CGSizeExtension.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | extension CGSize { 4 | 5 | /// Creates a size with unnamed arguments. 6 | init(_ width: CGFloat, _ height: CGFloat) { 7 | self.init() 8 | self.width = width 9 | self.height = height 10 | } 11 | 12 | /// Returns a copy with the width value changed. 13 | func with(width: CGFloat) -> CGSize { 14 | return .init(width: width, height: height) 15 | } 16 | /// Returns a copy with the height value changed. 17 | func with(height: CGFloat) -> CGSize { 18 | return .init(width: width, height: height) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Extension/Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extension.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2019/11/18. 11 | // Copyright © 2019 LEE. All rights reserved. 12 | // 13 | 14 | import Foundation 15 | 16 | public class ASAttributedStringWrapper { 17 | let base: Base 18 | init(_ base: Base) { 19 | self.base = base 20 | } 21 | } 22 | 23 | public protocol ASAttributedStringCompatible { 24 | associatedtype ASAttributedStringCompatibleType 25 | var attributed: ASAttributedStringCompatibleType { get } 26 | } 27 | 28 | extension ASAttributedStringCompatible { 29 | 30 | public var attributed: ASAttributedStringWrapper { 31 | get { return ASAttributedStringWrapper(self) } 32 | } 33 | } 34 | 35 | extension ASAttributedString { 36 | 37 | /// Add a AttributedString to another AttributedString. 38 | /// 39 | /// - Parameters: 40 | /// - lhs: AttributedString to add to. 41 | /// - rhs: AttributedString to add. 42 | public static func += (lhs: inout ASAttributedString, rhs: ASAttributedString) { 43 | let string = NSMutableAttributedString(attributedString: lhs.value) 44 | string.append(rhs.value) 45 | lhs = .init(string) 46 | } 47 | 48 | /// Add a AttributedString to another AttributedString and return a new AttributedString instance. 49 | /// 50 | /// - Parameters: 51 | /// - lhs: AttributedString to add. 52 | /// - rhs: AttributedString to add. 53 | /// - Returns: New instance with added AttributedString. 54 | public static func + (lhs: ASAttributedString, rhs: ASAttributedString) -> ASAttributedString { 55 | let string = NSMutableAttributedString(attributedString: lhs.value) 56 | string.append(rhs.value) 57 | return .init(string) 58 | } 59 | 60 | /// Add a String to another AttributedString. 61 | /// 62 | /// - Parameters: 63 | /// - lhs: AttributedString to add to. 64 | /// - rhs: String to add. 65 | public static func += (lhs: inout ASAttributedString, rhs: String) { 66 | lhs += ASAttributedString(.init(string: rhs)) 67 | } 68 | 69 | /// Add a AttributedString to another String. 70 | /// 71 | /// - Parameters: 72 | /// - lhs: String to add to. 73 | /// - rhs: AttributedString to add. 74 | public static func += (lhs: inout String, rhs: ASAttributedString) { 75 | lhs += rhs.value.string 76 | } 77 | 78 | /// Add a String to another AttributedString and return a new AttributedString instance. 79 | /// 80 | /// - Parameters: 81 | /// - lhs: AttributedString to add. 82 | /// - rhs: String to add. 83 | /// - Returns: New instance with added NSAttributedString. 84 | public static func + (lhs: ASAttributedString, rhs: String) -> ASAttributedString { 85 | return lhs + ASAttributedString(.init(string: rhs)) 86 | } 87 | /// Add a AttributedString to another String and return a new AttributedString instance. 88 | /// 89 | /// - Parameters: 90 | /// - lhs: String to add. 91 | /// - rhs: AttributedString to add. 92 | /// - Returns: New instance with added NSAttributedString. 93 | public static func + (lhs: String, rhs: ASAttributedString) -> ASAttributedString { 94 | return ASAttributedString(.init(string: lhs)) + rhs 95 | } 96 | 97 | /// Add a NSAttributedString to another AttributedString. 98 | /// 99 | /// - Parameters: 100 | /// - lhs: AttributedString to add to. 101 | /// - rhs: NSAttributedString to add. 102 | public static func += (lhs: inout ASAttributedString, rhs: NSAttributedString) { 103 | lhs += ASAttributedString(rhs) 104 | } 105 | 106 | /// Add a AttributedString to another NSMutableAttributedString. 107 | /// 108 | /// - Parameters: 109 | /// - lhs: NSMutableAttributedString to add to. 110 | /// - rhs: AttributedString to add. 111 | public static func += (lhs: inout NSMutableAttributedString, rhs: ASAttributedString) { 112 | lhs.append(rhs.value) 113 | } 114 | 115 | /// Add a NSAttributedString to another AttributedString and return a new AttributedString instance. 116 | /// 117 | /// - Parameters: 118 | /// - lhs: AttributedString to add. 119 | /// - rhs: NSAttributedString to add. 120 | /// - Returns: New instance with added NSAttributedString. 121 | public static func + (lhs: ASAttributedString, rhs: NSAttributedString) -> ASAttributedString { 122 | return lhs + ASAttributedString(rhs) 123 | } 124 | 125 | /// Add a AttributedString to another NSAttributedString and return a new AttributedString instance. 126 | /// 127 | /// - Parameters: 128 | /// - lhs: NSAttributedString to add. 129 | /// - rhs: AttributedString to add. 130 | /// - Returns: New instance with added NSAttributedString. 131 | public static func + (lhs: NSAttributedString, rhs: ASAttributedString) -> ASAttributedString { 132 | return ASAttributedString(lhs) + rhs 133 | } 134 | 135 | /// Add a AttributedString.Attribute to another AttributedString. 136 | /// 137 | /// - Parameters: 138 | /// - lhs: AttributedString to add to. 139 | /// - rhs: AttributedString.Attribute to add. 140 | public static func += (lhs: inout ASAttributedString, rhs: ASAttributedString.Attribute) { 141 | lhs += (rhs, .init(location: 0, length: lhs.value.length)) 142 | } 143 | 144 | /// Add a AttributedString.Attribute to another AttributedString. 145 | /// 146 | /// - Parameters: 147 | /// - lhs: AttributedString to add to. 148 | /// - rhs: AttributedString.Attribute to add. 149 | public static func += (lhs: inout ASAttributedString, rhs: [ASAttributedString.Attribute]) { 150 | lhs += (rhs, .init(location: 0, length: lhs.value.length)) 151 | } 152 | 153 | /// Add a AttributedString.Attribute to another AttributedString. 154 | /// 155 | /// - Parameters: 156 | /// - lhs: AttributedString to add to. 157 | /// - rhs: AttributedString.Attribute to add. 158 | public static func += (lhs: inout ASAttributedString, rhs: (ASAttributedString.Attribute, NSRange)) { 159 | lhs += ([rhs.0], rhs.1) 160 | } 161 | 162 | /// Add a AttributedString.Attribute to another AttributedString. 163 | /// 164 | /// - Parameters: 165 | /// - lhs: AttributedString to add to. 166 | /// - rhs: AttributedString.Attribute to add. 167 | public static func += (lhs: inout ASAttributedString, rhs: ([ASAttributedString.Attribute], NSRange)) { 168 | lhs = lhs + rhs 169 | } 170 | 171 | /// Add a AttributedString.Attribute to another AttributedString and return a new AttributedString instance. 172 | /// 173 | /// - Parameters: 174 | /// - lhs: AttributedString to add. 175 | /// - rhs: AttributedString.Attribute to add. 176 | /// - Returns: New instance with added AttributedString.Attribute. 177 | public static func + (lhs: ASAttributedString, rhs: ASAttributedString.Attribute) -> ASAttributedString { 178 | return lhs + (rhs, .init(location: 0, length: lhs.value.length)) 179 | } 180 | 181 | /// Add a AttributedString.Attribute to another AttributedString and return a new AttributedString instance. 182 | /// 183 | /// - Parameters: 184 | /// - lhs: AttributedString to add. 185 | /// - rhs: AttributedString.Attribute to add. 186 | /// - Returns: New instance with added AttributedString.Attribute. 187 | public static func + (lhs: ASAttributedString, rhs: (ASAttributedString.Attribute, NSRange)) -> ASAttributedString { 188 | return lhs + ([rhs.0], rhs.1) 189 | } 190 | 191 | /// Add a AttributedString.Attribute to another AttributedString and return a new AttributedString instance. 192 | /// 193 | /// - Parameters: 194 | /// - lhs: AttributedString to add. 195 | /// - rhs: AttributedString.Attribute to add. 196 | /// - Returns: New instance with added AttributedString.Attribute. 197 | public static func + (lhs: ASAttributedString, rhs: ([ASAttributedString.Attribute], NSRange)) -> ASAttributedString { 198 | let string = NSMutableAttributedString(attributedString: lhs.value) 199 | rhs.0.forEach { string.addAttributes($0.attributes, range: rhs.1) } 200 | return .init(string) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/Extension/ObjectExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectExtension.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2020/6/1. 11 | // Copyright © 2020 LEE. All rights reserved. 12 | // 13 | 14 | import Foundation 15 | 16 | class AssociatedWrapper { 17 | let base: Base 18 | init(_ base: Base) { 19 | self.base = base 20 | } 21 | } 22 | 23 | protocol AssociatedCompatible { 24 | associatedtype AssociatedCompatibleType 25 | var associated: AssociatedCompatibleType { get } 26 | } 27 | 28 | extension AssociatedCompatible { 29 | 30 | var associated: AssociatedWrapper { 31 | get { return AssociatedWrapper(self) } 32 | } 33 | } 34 | 35 | extension NSObject: AssociatedCompatible { } 36 | 37 | extension AssociatedWrapper where Base: NSObject { 38 | 39 | enum Policy { 40 | case nonatomic 41 | case atomic 42 | } 43 | 44 | /// 获取关联值 45 | func get(_ key: UnsafeRawPointer) -> T? { 46 | guard let value = objc_getAssociatedObject(base, key) else { 47 | return nil 48 | } 49 | return (value as! T) 50 | // 💣 Xcode 14.0 iOS12 Release Mode Crash 疑似苹果编译器漏洞 51 | //objc_getAssociatedObject(base, key) as? T 52 | } 53 | 54 | /// 设置关联值 OBJC_ASSOCIATION_ASSIGN 55 | func set(assign key: UnsafeRawPointer, _ value: Any) { 56 | objc_setAssociatedObject(base, key, value, .OBJC_ASSOCIATION_ASSIGN) 57 | } 58 | 59 | /// 设置关联值 OBJC_ASSOCIATION_RETAIN_NONATOMIC / OBJC_ASSOCIATION_RETAIN 60 | func set(retain key: UnsafeRawPointer, _ value: Any?, _ policy: Policy = .nonatomic) { 61 | switch policy { 62 | case .nonatomic: 63 | objc_setAssociatedObject(base, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 64 | case .atomic: 65 | objc_setAssociatedObject(base, key, value, .OBJC_ASSOCIATION_RETAIN) 66 | } 67 | } 68 | 69 | /// 设置关联值 OBJC_ASSOCIATION_COPY_NONATOMIC / OBJC_ASSOCIATION_COPY 70 | func set(copy key: UnsafeRawPointer, _ value: Any?, _ policy: Policy = .nonatomic) { 71 | switch policy { 72 | case .nonatomic: 73 | objc_setAssociatedObject(base, key, value, .OBJC_ASSOCIATION_COPY_NONATOMIC) 74 | case .atomic: 75 | objc_setAssociatedObject(base, key, value, .OBJC_ASSOCIATION_COPY) 76 | } 77 | } 78 | } 79 | 80 | func swizzleMethod(for aClass: AnyClass, _ originalSelector: Selector, _ swizzledSelector: Selector) { 81 | guard 82 | let originalMethod = class_getInstanceMethod(aClass, originalSelector), 83 | let swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector) else { 84 | return 85 | } 86 | 87 | let didAddMethod = class_addMethod( 88 | aClass, 89 | originalSelector, 90 | method_getImplementation(swizzledMethod), 91 | method_getTypeEncoding(swizzledMethod) 92 | ) 93 | 94 | if didAddMethod { 95 | class_replaceMethod( 96 | aClass, 97 | swizzledSelector, 98 | method_getImplementation(originalMethod), 99 | method_getTypeEncoding(originalMethod) 100 | ) 101 | 102 | } else { 103 | method_exchangeImplementations(originalMethod, swizzledMethod) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Extension/ShadowExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShadowExtension.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2019/11/18. 11 | // Copyright © 2019 LEE. All rights reserved. 12 | // 13 | 14 | #if os(iOS) || os(tvOS) 15 | import UIKit 16 | #elseif os(macOS) 17 | import AppKit 18 | #elseif os(watchOS) 19 | import WatchKit 20 | #endif 21 | 22 | public extension NSShadow { 23 | 24 | convenience init(offset: CGSize, radius: CGFloat, color: ASColor? = .none) { 25 | self.init() 26 | self.shadowOffset = offset 27 | self.shadowBlurRadius = radius 28 | self.shadowColor = color 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Extension/UIKit/ActionQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionQueue.swift 3 | // AttributedString 4 | // 5 | // Created by 李响 on 2021/10/29. 6 | // Copyright © 2021 LEE. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// 通常分为两种情况 began -> action -> ended / action -> began -> ended, 通过队列保证 began -> action -> ended 的执行顺序 12 | class ActionQueue { 13 | 14 | static let main = ActionQueue() 15 | 16 | typealias Handle = () -> Void 17 | 18 | private var action: Handle? 19 | 20 | func began(_ handle: Handle) { 21 | handle() 22 | } 23 | 24 | func action(_ handle: @escaping Handle) { 25 | action = handle 26 | } 27 | 28 | func ended(_ handle: @escaping Handle) { 29 | action?() 30 | handle() 31 | } 32 | 33 | func cancelled(_ handle: @escaping Handle) { 34 | action = nil 35 | handle() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Extension/UIKit/UIButtonExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButtonExtension.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2019/11/18. 11 | // Copyright © 2019 LEE. All rights reserved. 12 | // 13 | 14 | #if os(iOS) || os(tvOS) 15 | 16 | import UIKit 17 | 18 | extension UIButton: ASAttributedStringCompatible { 19 | 20 | } 21 | 22 | extension ASAttributedStringWrapper where Base: UIButton { 23 | 24 | public func setTitle(_ title: ASAttributedString?, for state: UIControl.State) { 25 | base.setAttributedTitle(title?.value, for: state) 26 | } 27 | 28 | public func title(for state: UIControl.State) -> ASAttributedString? { 29 | ASAttributedString(base.attributedTitle(for: state)) 30 | } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/Extension/UIKit/UILabel/UILabelLayoutManagerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabelLayoutManagerDelegate.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2020/8/1. 11 | // Copyright © 2020 LEE. All rights reserved. 12 | // 13 | 14 | #if os(iOS) 15 | 16 | import UIKit 17 | 18 | class UILabelLayoutManagerDelegate: NSObject, NSLayoutManagerDelegate { 19 | 20 | // 当Label发生Scaled时 21 | let scaledMetrics: UILabel.ScaledMetrics? 22 | let baselineAdjustment: UIBaselineAdjustment 23 | 24 | init(_ scaledMetrics: UILabel.ScaledMetrics?, with baselineAdjustment: UIBaselineAdjustment) { 25 | self.scaledMetrics = scaledMetrics 26 | self.baselineAdjustment = baselineAdjustment 27 | super.init() 28 | } 29 | 30 | func layoutManager(_ layoutManager: NSLayoutManager, 31 | shouldSetLineFragmentRect lineFragmentRect: UnsafeMutablePointer, 32 | lineFragmentUsedRect: UnsafeMutablePointer, 33 | baselineOffset: UnsafeMutablePointer, 34 | in textContainer: NSTextContainer, 35 | forGlyphRange glyphRange: NSRange) -> Bool { 36 | /** 37 | From apple's doc: 38 | https://developer.apple.com/library/content/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/CustomTextProcessing/CustomTextProcessing.html 39 | In addition to returning the line fragment rectangle itself, the layout manager returns a rectangle called the used rectangle. This is the portion of the line fragment rectangle that actually contains glyphs or other marks to be drawn. By convention, both rectangles include the line fragment padding and the interline space (which is calculated from the font’s line height metrics and the paragraph’s line spacing parameters). However, the paragraph spacing (before and after) and any space added around the text, such as that caused by center-spaced text, are included only in the line fragment rectangle, and are not included in the used rectangle. 40 | */ 41 | guard let textStorage = layoutManager.textStorage else { 42 | return false 43 | } 44 | guard let maximum = getMaximum(layoutManager, with: textStorage, for: glyphRange) else { 45 | return false 46 | } 47 | 48 | // 段落前间距 49 | var paragraphSpacingBefore: CGFloat = 0 50 | if glyphRange.location > 0, let paragraph = maximum.paragraph, paragraph.paragraphSpacingBefore > .ulpOfOne { 51 | let lastIndex = layoutManager.characterIndexForGlyph(at: glyphRange.location - 1) 52 | let substring = textStorage.attributedSubstring(from: .init(location: lastIndex, length: 1)).string 53 | let isLineBreak = substring == "\n" 54 | paragraphSpacingBefore = isLineBreak ? paragraph.paragraphSpacingBefore : 0 55 | } 56 | 57 | // 段落间距 58 | var paragraphSpacing: CGFloat = 0 59 | if let paragraph = maximum.paragraph, paragraph.paragraphSpacing > .ulpOfOne { 60 | let lastIndex = layoutManager.characterIndexForGlyph(at: glyphRange.location + glyphRange.length - 1) 61 | let substring = textStorage.attributedSubstring(from: .init(location: lastIndex, length: 1)).string 62 | let isLineBreak = substring == "\n" 63 | paragraphSpacing = isLineBreak ? paragraph.paragraphSpacing : 0 64 | } 65 | 66 | var rect = lineFragmentRect.pointee 67 | var used = lineFragmentUsedRect.pointee 68 | 69 | // 当Label发生Scaled时 最大行数为1时 70 | if let scaledMetrics = scaledMetrics, textContainer.maximumNumberOfLines == 1 { 71 | switch baselineAdjustment { 72 | case .alignBaselines: 73 | // 原始的基线偏移 使用Scaled的尺寸高度 74 | var baseline = baselineOffset.pointee 75 | baseline = .init(scaledMetrics.baselineOffset) 76 | baselineOffset.pointee = baseline 77 | rect.size.height = scaledMetrics.scaledSize.height 78 | used.size.height = scaledMetrics.scaledSize.height 79 | 80 | case .alignCenters: 81 | print(scaledMetrics) 82 | // 居中的基线偏移 使用Scaled的尺寸高度 83 | var baseline = baselineOffset.pointee 84 | // 整行的占用高度 - 缩放的行高 = 上下边距; 上边距 = 上下边距 * 0.5; 居中的基线偏移 = 上边距 + 缩放的基线偏移 85 | let margin = (scaledMetrics.scaledSize.height - .init(scaledMetrics.scaledLineHeight)) * 0.5 86 | baseline = margin + .init(scaledMetrics.scaledBaselineOffset) 87 | baselineOffset.pointee = baseline 88 | rect.size.height = scaledMetrics.scaledSize.height 89 | used.size.height = scaledMetrics.scaledSize.height 90 | 91 | case .none: 92 | // 缩放的基线偏移 使用Scaled的尺寸高度 93 | var baseline = baselineOffset.pointee 94 | baseline = .init(scaledMetrics.scaledBaselineOffset) 95 | baselineOffset.pointee = baseline 96 | rect.size.height = scaledMetrics.scaledSize.height 97 | used.size.height = scaledMetrics.scaledSize.height 98 | 99 | default: 100 | break 101 | } 102 | 103 | } else { 104 | // 以最大的高度为准 (可解决附件问题), 同时根据最大行数是否为1来判断used是否需要增加行间距, 以解决1行时应该无行间距的问题. 105 | let temp = max(maximum.lineHeight, used.height) 106 | rect.size.height = temp + maximum.lineSpacing + paragraphSpacing + paragraphSpacingBefore 107 | used.size.height = temp 108 | } 109 | 110 | // 重新赋值最终结果 111 | lineFragmentRect.pointee = rect 112 | lineFragmentUsedRect.pointee = used 113 | 114 | /** 115 | From apple's doc: 116 | true if you modified the layout information and want your modifications to be used or false if the original layout information should be used. 117 | But actually returning false is also used. : ) 118 | We should do this to solve the problem of exclusionPaths not working. 119 | */ 120 | return false 121 | } 122 | 123 | // Implementing this method with a return value 0 will solve the problem of last line disappearing 124 | // when both maxNumberOfLines and lineSpacing are set, since we didn't include the lineSpacing in the lineFragmentUsedRect. 125 | func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat { 126 | return 0 127 | } 128 | } 129 | 130 | extension UILabelLayoutManagerDelegate { 131 | 132 | private struct Maximum { 133 | let font: UIFont 134 | let lineHeight: CGFloat 135 | let lineSpacing: CGFloat 136 | let paragraph: NSParagraphStyle? 137 | } 138 | 139 | private func getMaximum(_ layoutManager: NSLayoutManager, with textStorage: NSTextStorage, for glyphRange: NSRange) -> Maximum? { 140 | // 排除换行符, 系统不用它计算行. 141 | var glyphRange = glyphRange 142 | if glyphRange.length > 1 { 143 | let property = layoutManager.propertyForGlyph(at: glyphRange.location + glyphRange.length - 1) 144 | if property == .controlCharacter { 145 | glyphRange = .init(location: glyphRange.location, length: glyphRange.length - 1) 146 | } 147 | } 148 | 149 | let characterRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) 150 | 151 | var maximumLineHeightFont: UIFont? 152 | var maximumLineHeight: CGFloat = 0 153 | var maximumLineSpacing: CGFloat = 0 154 | var paragraph: NSParagraphStyle? 155 | textStorage.enumerateAttributes(in: characterRange, options: .longestEffectiveRangeNotRequired) { 156 | (attributes, range, stop) in 157 | // 使用 NSOriginalFont 的行高进行计算 https://juejin.im/post/6844903838252531725 158 | guard let font = (attributes[.originalFont] ?? attributes[.font]) as? UIFont else { return } 159 | paragraph = paragraph ?? attributes[.paragraphStyle] as? NSParagraphStyle 160 | 161 | let lineHeight = getLineHeight(font, with: paragraph) 162 | // 获取最大行高 163 | if lineHeight > maximumLineHeight { 164 | maximumLineHeightFont = font 165 | maximumLineHeight = lineHeight 166 | } 167 | // 获取最大行间距 168 | if let lineSpacing = paragraph?.lineSpacing, lineSpacing > maximumLineSpacing { 169 | maximumLineSpacing = lineSpacing 170 | } 171 | } 172 | 173 | guard let font = maximumLineHeightFont else { 174 | return nil 175 | } 176 | return .init( 177 | font: font, 178 | lineHeight: maximumLineHeight, 179 | lineSpacing: maximumLineSpacing, 180 | paragraph: paragraph 181 | ) 182 | } 183 | 184 | private func getLineHeight(_ font: UIFont, with paragraph: NSParagraphStyle? = .none) -> CGFloat { 185 | guard let paragraph = paragraph else { 186 | return font.lineHeight 187 | } 188 | 189 | var lineHeight = font.lineHeight 190 | 191 | if paragraph.lineHeightMultiple > .ulpOfOne { 192 | lineHeight *= paragraph.lineHeightMultiple 193 | } 194 | if paragraph.minimumLineHeight > .ulpOfOne { 195 | lineHeight = max(paragraph.minimumLineHeight, lineHeight) 196 | } 197 | if paragraph.maximumLineHeight > .ulpOfOne { 198 | lineHeight = min(paragraph.maximumLineHeight, lineHeight) 199 | } 200 | return lineHeight 201 | } 202 | } 203 | 204 | fileprivate extension NSAttributedString.Key { 205 | 206 | /// 参考: https://juejin.im/post/6844903838252531725 207 | static let originalFont: NSAttributedString.Key = .init("NSOriginalFont") 208 | } 209 | 210 | #endif 211 | -------------------------------------------------------------------------------- /Sources/Extension/UIKit/UITextFieldExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextFieldExtension.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2019/11/18. 11 | // Copyright © 2019 LEE. All rights reserved. 12 | // 13 | 14 | #if os(iOS) || os(tvOS) 15 | 16 | import UIKit 17 | 18 | extension UITextField: ASAttributedStringCompatible { 19 | 20 | } 21 | 22 | extension ASAttributedStringWrapper where Base: UITextField { 23 | 24 | public var text: ASAttributedString? { 25 | get { ASAttributedString(base.attributedText) } 26 | set { base.attributedText = newValue?.value } 27 | } 28 | 29 | public var placeholder: ASAttributedString? { 30 | get { ASAttributedString(base.attributedPlaceholder) } 31 | set { base.attributedPlaceholder = newValue?.value } 32 | } 33 | } 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /Sources/Extension/WatchKit/WKInterfaceButtonExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WKInterfaceButtonExtension.swift 3 | // AttributedString-watchOS 4 | // 5 | // Created by Lee on 2020/4/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | #if os(watchOS) 10 | 11 | import WatchKit 12 | 13 | extension WKInterfaceButton: ASAttributedStringCompatible { 14 | 15 | } 16 | 17 | extension ASAttributedStringWrapper where Base: WKInterfaceButton { 18 | 19 | public func set(title: ASAttributedString?) { 20 | base.setAttributedTitle(title?.value) 21 | } 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/Extension/WatchKit/WKInterfaceLabelExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WKInterfaceLabelExtension.swift 3 | // AttributedString-watchOS 4 | // 5 | // Created by Lee on 2020/4/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | #if os(watchOS) 10 | 11 | import WatchKit 12 | 13 | extension WKInterfaceLabel: ASAttributedStringCompatible { 14 | 15 | } 16 | 17 | extension ASAttributedStringWrapper where Base: WKInterfaceLabel { 18 | 19 | public func set(text: ASAttributedString?) { 20 | base.setAttributedText(text?.value) 21 | } 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Sources/Extension/WatchKit/WKInterfaceTextFieldExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WKInterfaceTextFieldExtension.swift 3 | // AttributedString-watchOS 4 | // 5 | // Created by Lee on 2020/4/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | #if os(watchOS) 10 | 11 | import WatchKit 12 | 13 | @available(watchOS 6.0, *) 14 | extension WKInterfaceTextField: ASAttributedStringCompatible { 15 | 16 | } 17 | 18 | @available(watchOS 6.0, *) 19 | extension ASAttributedStringWrapper where Base: WKInterfaceTextField { 20 | 21 | public func set(text: ASAttributedString?) { 22 | base.setAttributedText(text?.value) 23 | } 24 | 25 | public func setPlaceholder(text: ASAttributedString?) { 26 | base.setAttributedPlaceholder(text?.value) 27 | } 28 | } 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /Sources/Interpolation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedStringInterpolation.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2019/11/18. 11 | // Copyright © 2019 LEE. All rights reserved. 12 | // 13 | 14 | #if os(macOS) 15 | import AppKit 16 | #else 17 | import UIKit 18 | #endif 19 | 20 | extension ASAttributedString: ExpressibleByStringInterpolation { 21 | 22 | public init(stringInterpolation: ASAttributedStringInterpolation) { 23 | self.value = .init(attributedString: stringInterpolation.value) 24 | } 25 | } 26 | 27 | public struct ASAttributedStringInterpolation : StringInterpolationProtocol { 28 | 29 | let value: NSMutableAttributedString 30 | 31 | public init(literalCapacity: Int, interpolationCount: Int) { 32 | value = .init() 33 | } 34 | 35 | public mutating func appendLiteral(_ literal: String) { 36 | self.value.append(.init(string: literal)) 37 | } 38 | 39 | public mutating func appendInterpolation(_ value: NSAttributedString) { 40 | self.value.append(value) 41 | } 42 | 43 | public mutating func appendInterpolation(_ value: ASAttributedString) { 44 | self.value.append(value.value) 45 | } 46 | 47 | /// Interpolates the given value's textual representation into the 48 | /// attributed string literal being created. 49 | /// 50 | /// Do not call this method directly. It is used by the compiler when 51 | /// interpreting string interpolations. Instead, use string 52 | /// interpolation to create a new string by including values, literals, 53 | /// variables, or expressions enclosed in parentheses, prefixed by a 54 | /// backslash (`\(`...`, attributes: [:])`). 55 | /// 56 | /// let price = 2 57 | /// let number = 3 58 | /// let message: AttributedString = """ 59 | /// If one cookie costs \(price, attributes: [.foregroundColor: UIColor.red]) dollars, \ 60 | /// \(number, attributes: [.foregroundColor: UIColor.gray]) cookies cost \(price * number, attributes: [.foregroundColor: UIColor.blue]) dollars. 61 | /// """ 62 | /// 63 | public mutating func appendInterpolation(_ value: T, attributes: [NSAttributedString.Key: Any]) { 64 | self.value.append(.init(string: "\(value)", attributes: attributes)) 65 | } 66 | 67 | /// Interpolates the given value's textual representation into the 68 | /// attributed string literal being created. 69 | /// 70 | /// Do not call this method directly. It is used by the compiler when 71 | /// interpreting string interpolations. Instead, use string 72 | /// interpolation to create a new string by including values, literals, 73 | /// variables, or expressions enclosed in parentheses, prefixed by a 74 | /// backslash (`\(`...`)`). 75 | /// 76 | /// let price = 2 77 | /// let number = 3 78 | /// let message = """ 79 | /// If one cookie costs \(price) dollars, \ 80 | /// \(number) cookies cost \(price * number) dollars. 81 | /// """ 82 | /// print(message) 83 | /// 84 | /// // Prints "If one cookie costs 2 dollars, 3 cookies cost 6 dollars." 85 | /// 86 | public mutating func appendInterpolation(_ value: T) { 87 | self.value.append(.init(string: "\(value)")) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/ParagraphStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedStringParagraphStyle.swift 3 | // ┌─┐ ┌───────┐ ┌───────┐ 4 | // │ │ │ ┌─────┘ │ ┌─────┘ 5 | // │ │ │ └─────┐ │ └─────┐ 6 | // │ │ │ ┌─────┘ │ ┌─────┘ 7 | // │ └─────┐│ └─────┐ │ └─────┐ 8 | // └───────┘└───────┘ └───────┘ 9 | // 10 | // Created by Lee on 2019/11/18. 11 | // Copyright © 2019 LEE. All rights reserved. 12 | // 13 | 14 | #if os(macOS) 15 | import AppKit 16 | #else 17 | import UIKit 18 | #endif 19 | 20 | extension ASAttributedString.Attribute { 21 | 22 | /// 段落 23 | /// - Parameter value: 段落样式 24 | /// - Returns: 属性 25 | public static func paragraph(_ value: ParagraphStyle...) -> Self { 26 | return .init(attributes: value.isEmpty ? [:] : [.paragraphStyle: ParagraphStyle.get(value)]) 27 | } 28 | 29 | /// 段落 30 | /// - Parameter value: 段落样式 31 | /// - Returns: 属性 32 | public static func paragraph(with value: [ParagraphStyle]) -> Self { 33 | return .init(attributes: value.isEmpty ? [:] : [.paragraphStyle: ParagraphStyle.get(value)]) 34 | } 35 | } 36 | 37 | extension ASAttributedString.Attribute { 38 | 39 | public struct ParagraphStyle { 40 | 41 | fileprivate enum Key { 42 | case lineSpacing // CGFloat 43 | case paragraphSpacing // CGFloat 44 | case alignment // NSTextAlignment 45 | case firstLineHeadIndent // CGFloat 46 | case headIndent // CGFloat 47 | case tailIndent // CGFloat 48 | case lineBreakMode // NSLineBreakMode 49 | case minimumLineHeight // CGFloat 50 | case maximumLineHeight // CGFloat 51 | case baseWritingDirection // NSWritingDirection 52 | case lineHeightMultiple // CGFloat 53 | case paragraphSpacingBefore // CGFloat 54 | case hyphenationFactor // Float 55 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOSApplicationExtension 8.0, *) 56 | case usesDefaultHyphenation // Bool 57 | case tabStops // [NSTextTab] 58 | case defaultTabInterval // CGFloat 59 | case allowsDefaultTighteningForTruncation // Bool 60 | case lineBreakStrategy // NSParagraphStyle.LineBreakStrategy 61 | } 62 | 63 | fileprivate let style: [Key: Any] 64 | 65 | fileprivate static func get(_ attributes: [ParagraphStyle]) -> NSParagraphStyle { 66 | var temp: [Key: Any] = [:] 67 | attributes.forEach { temp.merge($0.style, uniquingKeysWith: { $1 }) } 68 | 69 | func fetch(_ key: Key, completion: (Value)->()) { 70 | guard let value = temp[key] as? Value else { return } 71 | completion(value) 72 | } 73 | 74 | let paragraph = NSMutableParagraphStyle() 75 | fetch(.lineSpacing) { paragraph.lineSpacing = $0 } 76 | fetch(.paragraphSpacing) { paragraph.paragraphSpacing = $0 } 77 | fetch(.alignment) { paragraph.alignment = $0 } 78 | fetch(.firstLineHeadIndent) { paragraph.firstLineHeadIndent = $0 } 79 | fetch(.headIndent) { paragraph.headIndent = $0 } 80 | fetch(.tailIndent) { paragraph.tailIndent = $0 } 81 | fetch(.lineBreakMode) { paragraph.lineBreakMode = $0 } 82 | fetch(.minimumLineHeight) { paragraph.minimumLineHeight = $0 } 83 | fetch(.maximumLineHeight) { paragraph.maximumLineHeight = $0 } 84 | fetch(.baseWritingDirection) { paragraph.baseWritingDirection = $0 } 85 | fetch(.lineHeightMultiple) { paragraph.lineHeightMultiple = $0 } 86 | fetch(.paragraphSpacingBefore) { paragraph.paragraphSpacingBefore = $0 } 87 | fetch(.hyphenationFactor) { paragraph.hyphenationFactor = $0 } 88 | if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOSApplicationExtension 8.0, *) { 89 | fetch(.usesDefaultHyphenation) { paragraph.usesDefaultHyphenation = $0 } 90 | } 91 | fetch(.tabStops) { paragraph.tabStops = $0 } 92 | fetch(.defaultTabInterval) { paragraph.defaultTabInterval = $0 } 93 | fetch(.allowsDefaultTighteningForTruncation) { paragraph.allowsDefaultTighteningForTruncation = $0 } 94 | fetch(.lineBreakStrategy) { paragraph.lineBreakStrategy = $0 } 95 | return paragraph 96 | } 97 | } 98 | } 99 | 100 | extension ASAttributedString.Attribute.ParagraphStyle { 101 | 102 | public static func lineSpacing(_ value: CGFloat) -> Self { 103 | return .init(style: [.lineSpacing: value]) 104 | } 105 | 106 | public static func paragraphSpacing(_ value: CGFloat) -> Self { 107 | return .init(style: [.paragraphSpacing: value]) 108 | } 109 | 110 | public static func alignment(_ value: NSTextAlignment) -> Self { 111 | return .init(style: [.alignment: value]) 112 | } 113 | 114 | public static func firstLineHeadIndent(_ value: CGFloat) -> Self { 115 | return .init(style: [.firstLineHeadIndent: value]) 116 | } 117 | 118 | public static func headIndent(_ value: CGFloat) -> Self { 119 | return .init(style: [.headIndent: value]) 120 | } 121 | 122 | public static func tailIndent(_ value: CGFloat) -> Self { 123 | return .init(style: [.tailIndent: value]) 124 | } 125 | 126 | public static func lineBreakMode(_ value: NSLineBreakMode) -> Self { 127 | return .init(style: [.lineBreakMode: value]) 128 | } 129 | 130 | public static func minimumLineHeight(_ value: CGFloat) -> Self { 131 | return .init(style: [.minimumLineHeight: value]) 132 | } 133 | 134 | public static func maximumLineHeight(_ value: CGFloat) -> Self { 135 | return .init(style: [.maximumLineHeight: value]) 136 | } 137 | 138 | public static func baseWritingDirection(_ value: NSWritingDirection) -> Self { 139 | return .init(style: [.baseWritingDirection: value]) 140 | } 141 | 142 | public static func lineHeightMultiple(_ value: CGFloat) -> Self { 143 | return .init(style: [.lineHeightMultiple: value]) 144 | } 145 | 146 | public static func paragraphSpacingBefore(_ value: CGFloat) -> Self { 147 | return .init(style: [.paragraphSpacingBefore: value]) 148 | } 149 | 150 | public static func hyphenationFactor(_ value: Float) -> Self { 151 | return .init(style: [.hyphenationFactor: value]) 152 | } 153 | 154 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOSApplicationExtension 8.0, *) 155 | public static func usesDefaultHyphenation(_ value: Bool) -> Self { 156 | return .init(style: [.usesDefaultHyphenation: value]) 157 | } 158 | 159 | public static func tabStops(_ value: [NSTextTab]) -> Self { 160 | return .init(style: [.tabStops: value]) 161 | } 162 | 163 | public static func defaultTabInterval(_ value: CGFloat) -> Self { 164 | return .init(style: [.defaultTabInterval: value]) 165 | } 166 | 167 | public static func allowsDefaultTighteningForTruncation(_ value: Bool) -> Self { 168 | return .init(style: [.allowsDefaultTighteningForTruncation: value]) 169 | } 170 | 171 | public static func lineBreakStrategy(_ value: NSParagraphStyle.LineBreakStrategy) -> Self { 172 | return .init(style: [.lineBreakStrategy: value]) 173 | } 174 | } 175 | 176 | extension ASAttributedString.Attribute.ParagraphStyle { 177 | 178 | public static func ~= (lhs: ASAttributedString.Attribute.ParagraphStyle, rhs: ASAttributedString.Attribute.ParagraphStyle) -> Bool { 179 | return lhs.style.keys == rhs.style.keys 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Sources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyTrackingDomains 10 | 11 | NSPrivacyCollectedDataTypes 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Tests/AttributedString_iOS_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedString_iOS_Tests.swift 3 | // AttributedString-iOS Tests 4 | // 5 | // Created by Lee on 2020/4/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | #if os(iOS) 10 | 11 | import XCTest 12 | 13 | class AttributedString_iOS_Tests: XCTestCase { 14 | 15 | override func setUpWithError() throws { 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDownWithError() throws { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | } 22 | 23 | func testExample() throws { 24 | // This is an example of a functional test case. 25 | // Use XCTAssert and related functions to verify your tests produce the correct results. 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /Tests/AttributedString_macOS_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedString_macOS_Tests.swift 3 | // AttributedString-macOS Tests 4 | // 5 | // Created by Lee on 2020/4/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | #if os(macOS) 10 | 11 | import XCTest 12 | 13 | class AttributedString_macOS_Tests: XCTestCase { 14 | 15 | override func setUpWithError() throws { 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDownWithError() throws { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | } 22 | 23 | func testExample() throws { 24 | // This is an example of a functional test case. 25 | // Use XCTAssert and related functions to verify your tests produce the correct results. 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /Tests/AttributedString_tvOS_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedString_tvOS_Tests.swift 3 | // AttributedString-tvOS Tests 4 | // 5 | // Created by Lee on 2020/4/10. 6 | // Copyright © 2020 LEE. All rights reserved. 7 | // 8 | 9 | #if os(tvOS) 10 | 11 | import XCTest 12 | 13 | class AttributedString_tvOS_Tests: XCTestCase { 14 | 15 | override func setUpWithError() throws { 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDownWithError() throws { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | } 22 | 23 | func testExample() throws { 24 | // This is an example of a functional test case. 25 | // Use XCTAssert and related functions to verify your tests produce the correct results. 26 | } 27 | 28 | func testPerformanceExample() throws { 29 | // This is an example of a performance test case. 30 | self.measure { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /Tests/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 | --------------------------------------------------------------------------------