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