├── images └── banner.png ├── Tests ├── octoface@2x.png ├── octoface@3x.png ├── ReferenceImages_64 │ └── Tests.SnapTests │ │ ├── test_lorem_100@3x.png │ │ ├── test_lorem_200@3x.png │ │ ├── test_lorem_300@3x.png │ │ ├── test_clearBackground@3x.png │ │ ├── test_complexBuilder@3x.png │ │ ├── test_maxNumberOfLinesLimited@3x.png │ │ ├── test_maxNumberOfLinesUnlimited@3x.png │ │ ├── test_addingImageWithTint_withCenter@3x.png │ │ ├── test_addingImageWithTint_withNoOptions@3x.png │ │ └── test_addingImageWithTint_withBaseOptions@3x.png ├── Info.plist ├── StyledTextStringTests.swift ├── StyledTextTests.swift ├── StyledTextBuilderTests.swift ├── TextStyleTests.swift └── LRUCacheTests.swift ├── UITestsApp ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── AppDelegate.swift ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard └── ViewController.swift ├── Example ├── Example │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Example │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── ViewController.swift │ │ ├── Info.plist │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── AppDelegate.swift │ ├── Example.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── ViewController.swift │ ├── AppDelegate.swift │ ├── BudingAttributedStringViewController.swift │ ├── RenderingTextBitmapsViewController.swift │ ├── Info.plist │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── BackgroundRenderingViewController.swift │ └── FastScrollingTableViewController.swift ├── Pods │ ├── Target Support Files │ │ ├── Pods-Example │ │ │ ├── Pods-Example.modulemap │ │ │ ├── Pods-Example-dummy.m │ │ │ ├── Pods-Example-umbrella.h │ │ │ ├── Pods-Example.debug.xcconfig │ │ │ ├── Pods-Example.release.xcconfig │ │ │ ├── Info.plist │ │ │ ├── Pods-Example-acknowledgements.markdown │ │ │ ├── Pods-Example-acknowledgements.plist │ │ │ ├── Pods-Example-resources.sh │ │ │ └── Pods-Example-frameworks.sh │ │ └── StyledTextKit │ │ │ ├── StyledTextKit.modulemap │ │ │ ├── StyledTextKit-dummy.m │ │ │ ├── StyledTextKit-prefix.pch │ │ │ ├── StyledTextKit-umbrella.h │ │ │ ├── StyledTextKit.xcconfig │ │ │ └── Info.plist │ ├── Manifest.lock │ └── Local Podspecs │ │ └── StyledTextKit.podspec.json ├── Example.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Example.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Podfile └── Podfile.lock ├── Pods ├── Target Support Files │ ├── Pods-Tests │ │ ├── Pods-Tests.modulemap │ │ ├── Pods-Tests-dummy.m │ │ ├── Pods-Tests-umbrella.h │ │ ├── Info.plist │ │ ├── Pods-Tests-Info.plist │ │ ├── Pods-Tests.debug.xcconfig │ │ ├── Pods-Tests.release.xcconfig │ │ ├── Pods-Tests-acknowledgements.markdown │ │ ├── Pods-Tests-acknowledgements.plist │ │ ├── Pods-Tests-resources.sh │ │ └── Pods-Tests-frameworks.sh │ └── iOSSnapshotTestCase │ │ ├── iOSSnapshotTestCase.modulemap │ │ ├── iOSSnapshotTestCase-dummy.m │ │ ├── iOSSnapshotTestCase-prefix.pch │ │ ├── iOSSnapshotTestCase-umbrella.h │ │ ├── iOSSnapshotTestCase.xcconfig │ │ └── iOSSnapshotTestCase-Info.plist ├── Manifest.lock └── iOSSnapshotTestCase │ ├── FBSnapshotTestCase │ ├── Categories │ │ ├── UIImage+Snapshot.h │ │ ├── UIImage+Diff.h │ │ ├── UIImage+Compare.h │ │ ├── UIImage+Snapshot.m │ │ └── UIImage+Diff.m │ ├── FBSnapshotTestCasePlatform.m │ ├── FBSnapshotTestCasePlatform.h │ └── SwiftSupport.swift │ ├── LICENSE │ └── README.md ├── Podfile ├── StyledTextKit.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Tests.xcscheme │ ├── UITests.xcscheme │ ├── UITestsApp.xcscheme │ └── StyledTextKit.xcscheme ├── .travis.yml ├── StyledTextKit.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Source ├── UIScreen+Static.swift ├── CGSize+LRUCachable.swift ├── NSAttributedStringAttributesType.swift ├── CGImage+LRUCachable.swift ├── NSAttributedStringKey+StyledText.swift ├── StyledTextRenderCacheKey.swift ├── Hashable+Combined.swift ├── Font.swift ├── StyledTextKit.h ├── CGSize+Utility.swift ├── Info.plist ├── UIContentSizeCategory+Scaling.swift ├── NSLayoutManager+Render.swift ├── StyledTextString.swift ├── NSAttributedString+Trim.swift ├── TextStyle.swift ├── LRUCache.swift ├── StyledText.swift ├── StyledTextBuilder.swift └── StyledTextRenderer.swift ├── Podfile.lock ├── StyledTextKit.podspec ├── UITests ├── Info.plist └── UITests.swift ├── LICENSE ├── .gitignore └── README.md /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/images/banner.png -------------------------------------------------------------------------------- /Tests/octoface@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/octoface@2x.png -------------------------------------------------------------------------------- /Tests/octoface@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/octoface@3x.png -------------------------------------------------------------------------------- /UITestsApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Example/Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /UITestsApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ReferenceImages_64/Tests.SnapTests/test_lorem_100@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/ReferenceImages_64/Tests.SnapTests/test_lorem_100@3x.png -------------------------------------------------------------------------------- /Tests/ReferenceImages_64/Tests.SnapTests/test_lorem_200@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/ReferenceImages_64/Tests.SnapTests/test_lorem_200@3x.png -------------------------------------------------------------------------------- /Tests/ReferenceImages_64/Tests.SnapTests/test_lorem_300@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/ReferenceImages_64/Tests.SnapTests/test_lorem_300@3x.png -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Pods-Tests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Tests { 2 | umbrella header "Pods-Tests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ReferenceImages_64/Tests.SnapTests/test_clearBackground@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/ReferenceImages_64/Tests.SnapTests/test_clearBackground@3x.png -------------------------------------------------------------------------------- /Tests/ReferenceImages_64/Tests.SnapTests/test_complexBuilder@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/ReferenceImages_64/Tests.SnapTests/test_complexBuilder@3x.png -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Pods-Tests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Tests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Tests 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Example/Pods-Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Example { 2 | umbrella header "Pods-Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/StyledTextKit/StyledTextKit.modulemap: -------------------------------------------------------------------------------- 1 | framework module StyledTextKit { 2 | umbrella header "StyledTextKit-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ReferenceImages_64/Tests.SnapTests/test_maxNumberOfLinesLimited@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/ReferenceImages_64/Tests.SnapTests/test_maxNumberOfLinesLimited@3x.png -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Example/Pods-Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Tests/ReferenceImages_64/Tests.SnapTests/test_maxNumberOfLinesUnlimited@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/ReferenceImages_64/Tests.SnapTests/test_maxNumberOfLinesUnlimited@3x.png -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/StyledTextKit/StyledTextKit-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_StyledTextKit : NSObject 3 | @end 4 | @implementation PodsDummy_StyledTextKit 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/iOSSnapshotTestCase/iOSSnapshotTestCase.modulemap: -------------------------------------------------------------------------------- 1 | framework module FBSnapshotTestCase { 2 | umbrella header "iOSSnapshotTestCase-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Tests/ReferenceImages_64/Tests.SnapTests/test_addingImageWithTint_withCenter@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/ReferenceImages_64/Tests.SnapTests/test_addingImageWithTint_withCenter@3x.png -------------------------------------------------------------------------------- /Tests/ReferenceImages_64/Tests.SnapTests/test_addingImageWithTint_withNoOptions@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/ReferenceImages_64/Tests.SnapTests/test_addingImageWithTint_withNoOptions@3x.png -------------------------------------------------------------------------------- /Pods/Target Support Files/iOSSnapshotTestCase/iOSSnapshotTestCase-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_iOSSnapshotTestCase : NSObject 3 | @end 4 | @implementation PodsDummy_iOSSnapshotTestCase 5 | @end 6 | -------------------------------------------------------------------------------- /Tests/ReferenceImages_64/Tests.SnapTests/test_addingImageWithTint_withBaseOptions@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitHawkApp/StyledTextKit/HEAD/Tests/ReferenceImages_64/Tests.SnapTests/test_addingImageWithTint_withBaseOptions@3x.png -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | use_frameworks! 3 | inhibit_all_warnings! 4 | 5 | workspace 'StyledTextKit' 6 | 7 | target 'Tests' do 8 | use_frameworks! 9 | pod 'iOSSnapshotTestCase' 10 | end 11 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /StyledTextKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode10 3 | script: 4 | - xcodebuild clean test -workspace StyledTextKit.xcworkspace -scheme StyledTextKit -destination "platform=iOS Simulator,name=iPhone X,OS=latest" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO -------------------------------------------------------------------------------- /Example/Example.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/StyledTextKit/StyledTextKit-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/iOSSnapshotTestCase/iOSSnapshotTestCase-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /StyledTextKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /StyledTextKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /StyledTextKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Source/UIScreen+Static.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScreen+Static.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/14/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // grab this once and avoid touching this on the main thread 12 | public let StyledTextScreenScale = UIScreen.main.scale 13 | -------------------------------------------------------------------------------- /Source/CGSize+LRUCachable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+LRUCachable.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/14/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension CGSize: LRUCachable { 12 | 13 | public var cachedSize: Int { 14 | return 1 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Source/NSAttributedStringAttributesType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedStringAttributesType.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 6/9/19. 6 | // Copyright © 2019 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias NSAttributedStringAttributesType = [NSAttributedString.Key: AnyHashable] 12 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'Example' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for Example 9 | 10 | pod 'StyledTextKit', :path => '../StyledTextKit.podspec' 11 | 12 | 13 | end 14 | -------------------------------------------------------------------------------- /Source/CGImage+LRUCachable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGImage+LRUCachable.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/14/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension CGImage: LRUCachable { 12 | 13 | public var cachedSize: Int { 14 | return height * bytesPerRow 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - StyledTextKit (0.1.1) 3 | 4 | DEPENDENCIES: 5 | - StyledTextKit (from `../StyledTextKit.podspec`) 6 | 7 | EXTERNAL SOURCES: 8 | StyledTextKit: 9 | :path: "../StyledTextKit.podspec" 10 | 11 | SPEC CHECKSUMS: 12 | StyledTextKit: 7fb706b7dce1bf5adcf7df6054b5c321b94ebfc0 13 | 14 | PODFILE CHECKSUM: cce33de777ba45b96bae71bcdc8c7705f353b112 15 | 16 | COCOAPODS: 1.5.0 17 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - StyledTextKit (0.1.1) 3 | 4 | DEPENDENCIES: 5 | - StyledTextKit (from `../StyledTextKit.podspec`) 6 | 7 | EXTERNAL SOURCES: 8 | StyledTextKit: 9 | :path: "../StyledTextKit.podspec" 10 | 11 | SPEC CHECKSUMS: 12 | StyledTextKit: 7fb706b7dce1bf5adcf7df6054b5c321b94ebfc0 13 | 14 | PODFILE CHECKSUM: cce33de777ba45b96bae71bcdc8c7705f353b112 15 | 16 | COCOAPODS: 1.5.0 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Pods-Tests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_TestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_TestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Source/NSAttributedStringKey+StyledText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedStringKey+StyledText.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 5/29/18. 6 | // Copyright © 2018 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension NSAttributedString.Key { 12 | 13 | static let highlight = NSAttributedString.Key("com.whoisryannystrom.styledtextkit.highlight") 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Example/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Marcus Wu on 2018/6/14. 6 | // Copyright © 2018年 Marcus Wu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UITableViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | title = "StyledTextKit Example" 17 | } 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Example/Pods-Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/StyledTextKit/StyledTextKit-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double StyledTextKitVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char StyledTextKitVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Source/StyledTextRenderCacheKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyledTextKitRenderCacheKey.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/14/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal struct StyledTextRenderCacheKey: Hashable, Equatable { 12 | 13 | let width: CGFloat 14 | let attributedText: NSAttributedString 15 | let backgroundColor: UIColor? 16 | let maximumNumberOfLines: Int? 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Source/Hashable+Combined.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hashable+Combined.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/12/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Hashable { 12 | 13 | public func combineHash(with hashableOther: T) -> Int { 14 | let ownHash = self.hashValue 15 | let otherHash = hashableOther.hashValue 16 | return (ownHash << 5) &+ ownHash &+ otherHash 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/StyledTextKit/StyledTextKit.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/StyledTextKit 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Marcus Wu on 2018/6/14. 6 | // Copyright © 2018年 Marcus Wu. 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: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | 19 | return true 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - iOSSnapshotTestCase (6.0.3): 3 | - iOSSnapshotTestCase/SwiftSupport (= 6.0.3) 4 | - iOSSnapshotTestCase/Core (6.0.3) 5 | - iOSSnapshotTestCase/SwiftSupport (6.0.3): 6 | - iOSSnapshotTestCase/Core 7 | 8 | DEPENDENCIES: 9 | - iOSSnapshotTestCase 10 | 11 | SPEC REPOS: 12 | https://github.com/cocoapods/specs.git: 13 | - iOSSnapshotTestCase 14 | 15 | SPEC CHECKSUMS: 16 | iOSSnapshotTestCase: 944a73f6d9676302811a86c0cf35f0e6ef5ab2a0 17 | 18 | PODFILE CHECKSUM: 99cb3cf124245f4a0036e3f5a1414809234f4427 19 | 20 | COCOAPODS: 1.7.1 21 | -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - iOSSnapshotTestCase (6.0.3): 3 | - iOSSnapshotTestCase/SwiftSupport (= 6.0.3) 4 | - iOSSnapshotTestCase/Core (6.0.3) 5 | - iOSSnapshotTestCase/SwiftSupport (6.0.3): 6 | - iOSSnapshotTestCase/Core 7 | 8 | DEPENDENCIES: 9 | - iOSSnapshotTestCase 10 | 11 | SPEC REPOS: 12 | https://github.com/cocoapods/specs.git: 13 | - iOSSnapshotTestCase 14 | 15 | SPEC CHECKSUMS: 16 | iOSSnapshotTestCase: 944a73f6d9676302811a86c0cf35f0e6ef5ab2a0 17 | 18 | PODFILE CHECKSUM: 99cb3cf124245f4a0036e3f5a1414809234f4427 19 | 20 | COCOAPODS: 1.7.1 21 | -------------------------------------------------------------------------------- /Pods/Target Support Files/iOSSnapshotTestCase/iOSSnapshotTestCase-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | #import "FBSnapshotTestCase.h" 14 | #import "FBSnapshotTestCasePlatform.h" 15 | #import "FBSnapshotTestController.h" 16 | 17 | FOUNDATION_EXPORT double FBSnapshotTestCaseVersionNumber; 18 | FOUNDATION_EXPORT const unsigned char FBSnapshotTestCaseVersionString[]; 19 | 20 | -------------------------------------------------------------------------------- /Source/Font.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Font.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 2/19/18. 6 | // Copyright © 2018 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public enum Font: Hashable, Equatable { 12 | 13 | public enum SystemFont: Hashable, Equatable { 14 | case `default` 15 | case bold 16 | case italic 17 | case weighted(UIFont.Weight) 18 | case monospaced(UIFont.Weight) 19 | } 20 | 21 | case name(String) 22 | case descriptor(UIFontDescriptor) 23 | case system(SystemFont) 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Source/StyledTextKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // StyledTextKitKit.h 3 | // StyledTextKitKit 4 | // 5 | // Created by Ryan Nystrom on 12/12/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for StyledTextKit. 12 | FOUNDATION_EXPORT double StyledTextKitVersionNumber; 13 | 14 | //! Project version string for StyledTextKit. 15 | FOUNDATION_EXPORT const unsigned char StyledTextKitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /StyledTextKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'StyledTextKit' 3 | spec.version = '0.2.0' 4 | spec.license = { :type => 'MIT' } 5 | spec.homepage = 'https://github.com/GitHawkApp/StyledTextKit' 6 | spec.authors = { 'Ryan Nystrom' => 'rnystrom@whoisryannystrom.com' } 7 | spec.summary = 'Declarative building and fast rendering attributed string library.' 8 | spec.source = { :git => 'https://github.com/GitHawkApp/StyledTextKit.git', :tag => spec.version.to_s } 9 | spec.source_files = 'Source/*.swift' 10 | spec.platform = :ios, '10.0' 11 | spec.swift_version = '5.0' 12 | end 13 | -------------------------------------------------------------------------------- /Example/Example/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Marcus Wu on 2018/6/14. 6 | // Copyright © 2018年 Marcus Wu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | override func didReceiveMemoryWarning() { 19 | super.didReceiveMemoryWarning() 20 | // Dispose of any resources that can be recreated. 21 | } 22 | 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/StyledTextKit.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "StyledTextKit", 3 | "version": "0.1.1", 4 | "license": { 5 | "type": "MIT" 6 | }, 7 | "homepage": "https://github.com/GitHawkApp/StyledTextKit", 8 | "authors": { 9 | "Ryan Nystrom": "rnystrom@whoisryannystrom.com" 10 | }, 11 | "summary": "Declarative building and fast rendering attributed string library.", 12 | "source": { 13 | "git": "https://github.com/GitHawkApp/StyledTextKit/StyledTextKit.git", 14 | "tag": "0.1.1" 15 | }, 16 | "source_files": "Source/*.swift", 17 | "platforms": { 18 | "ios": "10.0" 19 | }, 20 | "swift_version": "4.0" 21 | } 22 | -------------------------------------------------------------------------------- /Source/CGSize+Utility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+Utility.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/13/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension CGSize { 12 | 13 | func snapped(scale: CGFloat) -> CGSize { 14 | var size = self 15 | size.width = ceil(size.width * scale) / scale 16 | size.height = ceil(size.height * scale) / scale 17 | return size 18 | } 19 | 20 | func resized(inset: UIEdgeInsets) -> CGSize { 21 | var size = self 22 | size.width += inset.left + inset.right 23 | size.height += inset.top + inset.bottom 24 | return size 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Example/Pods-Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/StyledTextKit" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/StyledTextKit/StyledTextKit.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "StyledTextKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Example/Pods-Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/StyledTextKit" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/StyledTextKit/StyledTextKit.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "StyledTextKit" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /UITests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Pods/Target Support Files/iOSSnapshotTestCase/iOSSnapshotTestCase.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/iOSSnapshotTestCase 2 | ENABLE_BITCODE = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "QuartzCore" -framework "UIKit" -framework "XCTest" 6 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_ROOT = ${SRCROOT} 10 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/iOSSnapshotTestCase 11 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 12 | SKIP_INSTALL = YES 13 | -------------------------------------------------------------------------------- /UITests/UITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class UITests: XCTestCase { 4 | override func setUp() { 5 | continueAfterFailure = false 6 | XCUIApplication().launch() 7 | } 8 | 9 | override func tearDown() { 10 | } 11 | 12 | func test_tapLink() { 13 | let app = XCUIApplication() 14 | let textviewElement = app.otherElements["textView"] 15 | let statelabelElement = app.otherElements["stateLabel"] 16 | 17 | textviewElement.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)) 18 | .tap() 19 | XCTAssertEqual(statelabelElement.label, "didTap: Link1") 20 | 21 | textviewElement.coordinate(withNormalizedOffset: CGVector(dx: 0.1, dy: 0.4)) 22 | .tap() 23 | XCTAssertEqual(statelabelElement.label, "didTap: Link2") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Source/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Source/UIContentSizeCategory+Scaling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIContentSizeCategory+Scaling.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/12/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension UIContentSizeCategory { 12 | 13 | func scaledFontSize(forTextStyle style: UIFont.TextStyle) -> CGFloat { 14 | let scaledFont = UIFont.preferredFont(forTextStyle: style, 15 | compatibleWith: UITraitCollection(preferredContentSizeCategory: self)) 16 | return scaledFont.pointSize 17 | } 18 | 19 | func approximateScaleFactor(forTextStyle style: UIFont.TextStyle) -> CGFloat { 20 | let defaultFontSize = UIContentSizeCategory.large.scaledFontSize(forTextStyle: style) 21 | return scaledFontSize(forTextStyle: style) / defaultFontSize 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Pods/iOSSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Snapshot.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018, Uber Technologies, Inc. 3 | * Copyright (c) 2015-2018, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface UIImage (Snapshot) 15 | 16 | /// Uses renderInContext: to get a snapshot of the layer. 17 | + (nullable UIImage *)fb_imageForLayer:(CALayer *)layer; 18 | 19 | /// Uses renderInContext: to get a snapshot of the view layer. 20 | + (nullable UIImage *)fb_imageForViewLayer:(UIView *)view; 21 | 22 | /// Uses drawViewHierarchyInRect: to get a snapshot of the view and adds the view into a window if needed. 23 | + (nullable UIImage *)fb_imageForView:(UIView *)view; 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /Example/Example/BudingAttributedStringViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BudingAttributedStringViewController.swift 3 | // Example 4 | // 5 | // Created by Marcus Wu on 2018/6/14. 6 | // Copyright © 2018年 Marcus Wu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StyledTextKit 11 | 12 | class BudingAttributedStringViewController: UIViewController { 13 | 14 | @IBOutlet weak var label: UILabel! 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | title = "Building NSAttributedStrings" 20 | 21 | let attributedString = StyledTextBuilder(text: "Foo ") 22 | .save() 23 | .add(text: "bar", traits: [.traitBold]) 24 | .restore() 25 | .add(text: " baz!") 26 | .build() 27 | .render(contentSizeCategory: .extraExtraExtraLarge) 28 | 29 | label.attributedText = attributedString 30 | } 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/StyledTextKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.1.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Pods-Tests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/iOSSnapshotTestCase/iOSSnapshotTestCase-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 6.0.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Tests/StyledTextStringTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyledTextStringTests.swift 3 | // Tests 4 | // 5 | // Created by Ryan Nystrom on 11/10/18. 6 | // Copyright © 2018 Ryan Nystrom. All rights reserved. 7 | // 8 | import XCTest 9 | @testable import StyledTextKit 10 | 11 | class StyledTextStringTests: XCTestCase { 12 | 13 | func test_cacheKeysEqual() { 14 | let text1 = StyledTextBuilder(text: "foo").add(text: "bar", traits: [.traitBold]).build() 15 | let text2 = StyledTextBuilder(text: "foo").add(text: "bar", traits: [.traitBold]).build() 16 | 17 | let key1 = StyledTextRenderCacheKey( 18 | width: 100, 19 | attributedText: text1.render(contentSizeCategory: .medium), 20 | backgroundColor: nil, 21 | maximumNumberOfLines: nil 22 | ) 23 | let key2 = StyledTextRenderCacheKey( 24 | width: 100, 25 | attributedText: text2.render(contentSizeCategory: .medium), 26 | backgroundColor: nil, 27 | maximumNumberOfLines: nil 28 | ) 29 | XCTAssertEqual(key1, key2) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Ryan Nystrom http://whoisryannystrom.com/ 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Pods-Tests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" "${PODS_CONFIGURATION_BUILD_DIR}/iOSSnapshotTestCase" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/iOSSnapshotTestCase/FBSnapshotTestCase.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/iOSSnapshotTestCase/FBSnapshotTestCase.framework/Headers" -iframework "$(PLATFORM_DIR)/Developer/Library/Frameworks" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/iOSSnapshotTestCase" 7 | OTHER_LDFLAGS = $(inherited) -framework "FBSnapshotTestCase" -framework "Foundation" -framework "QuartzCore" -framework "UIKit" -framework "XCTest" 8 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 9 | PODS_BUILD_DIR = ${BUILD_DIR} 10 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 11 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 12 | PODS_ROOT = ${SRCROOT}/Pods 13 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Pods-Tests.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" "${PODS_CONFIGURATION_BUILD_DIR}/iOSSnapshotTestCase" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/iOSSnapshotTestCase/FBSnapshotTestCase.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/iOSSnapshotTestCase/FBSnapshotTestCase.framework/Headers" -iframework "$(PLATFORM_DIR)/Developer/Library/Frameworks" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/iOSSnapshotTestCase" 7 | OTHER_LDFLAGS = $(inherited) -framework "FBSnapshotTestCase" -framework "Foundation" -framework "QuartzCore" -framework "UIKit" -framework "XCTest" 8 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 9 | PODS_BUILD_DIR = ${BUILD_DIR} 10 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 11 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 12 | PODS_ROOT = ${SRCROOT}/Pods 13 | -------------------------------------------------------------------------------- /Pods/iOSSnapshotTestCase/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018, Uber Technologies, Inc. 4 | Copyright (c) 2013-2018, Facebook, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Source/NSLayoutManager+Render.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSLayoutManager+Render.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/13/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension NSLayoutManager { 12 | 13 | func size(textContainer: NSTextContainer, width: CGFloat, scale: CGFloat) -> CGSize { 14 | textContainer.size = CGSize(width: width, height: 0) 15 | let bounds = usedRect(for: textContainer) 16 | return bounds.size.snapped(scale: scale) 17 | } 18 | 19 | func render( 20 | size: CGSize, 21 | textContainer: NSTextContainer, 22 | scale: CGFloat, 23 | backgroundColor: UIColor? = nil 24 | ) -> CGImage? { 25 | textContainer.size = size 26 | 27 | UIGraphicsBeginImageContextWithOptions(size, backgroundColor != nil, scale) 28 | defer { UIGraphicsEndImageContext() } 29 | 30 | if let backgroundColor = backgroundColor { 31 | backgroundColor.setFill() 32 | UIBezierPath(rect: CGRect(origin: .zero, size: size)).fill() 33 | } 34 | 35 | let range = glyphRange(for: textContainer) 36 | drawBackground(forGlyphRange: range, at: .zero) 37 | drawGlyphs(forGlyphRange: range, at: .zero) 38 | return UIGraphicsGetImageFromCurrentImageContext()?.cgImage 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Example/Pods-Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## StyledTextKit 5 | 6 | The MIT License 7 | 8 | Copyright (c) 2017 Ryan Nystrom http://whoisryannystrom.com/ 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | 28 | Generated by CocoaPods - https://cocoapods.org 29 | -------------------------------------------------------------------------------- /Example/Example/RenderingTextBitmapsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RenderingTextBitmapsViewController.swift 3 | // Example 4 | // 5 | // Created by Marcus Wu on 2018/6/14. 6 | // Copyright © 2018年 Marcus Wu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StyledTextKit 11 | 12 | class RenderingTextBitmapsViewController: UIViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | title = "Rendering Text Bitmaps" 18 | 19 | let style = TextStyle( 20 | size: 16, 21 | attributes: [.foregroundColor: UIColor.black] 22 | ) 23 | 24 | let styleLarge = TextStyle( 25 | size: 24, 26 | attributes: [.foregroundColor: UIColor.green] 27 | ) 28 | 29 | let foo = StyledText(storage: .text("foo"), style: style) 30 | let bar = StyledText(storage: .text(" bar"), style: styleLarge) 31 | let good = StyledText(storage: .text("👍"), style: styleLarge) 32 | let string = StyledTextString(styledTexts: [foo, bar, good]) 33 | let renderer = StyledTextRenderer( 34 | string: string, 35 | contentSizeCategory: .large 36 | ) 37 | let result = renderer.render(for: UIScreen.main.bounds.width) 38 | 39 | view.layer.contents = result.image 40 | view.layer.contentsGravity = kCAGravityCenter 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Pods-Tests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## iOSSnapshotTestCase 5 | 6 | MIT License 7 | 8 | Copyright (c) 2017-2018, Uber Technologies, Inc. 9 | Copyright (c) 2013-2018, Facebook, Inc. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | 29 | Generated by CocoaPods - https://cocoapods.org 30 | -------------------------------------------------------------------------------- /Source/StyledTextString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyledTextKitResult.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 3/17/18. 6 | // Copyright © 2018 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public struct StyledTextString: Hashable, Equatable { 13 | 14 | public enum RenderMode { 15 | case trimWhitespaceAndNewlines 16 | case trimWhitespace 17 | case preserve 18 | } 19 | 20 | public let styledTexts: [StyledText] 21 | public let renderMode: RenderMode 22 | 23 | public init(styledTexts: [StyledText], renderMode: RenderMode = .trimWhitespaceAndNewlines) { 24 | self.styledTexts = styledTexts 25 | self.renderMode = renderMode 26 | } 27 | 28 | public var allText: String { 29 | return styledTexts.reduce("", { $0 + $1.text }) 30 | } 31 | 32 | public func render(contentSizeCategory: UIContentSizeCategory) -> NSAttributedString { 33 | let result = NSMutableAttributedString() 34 | styledTexts.forEach { result.append($0.render(contentSizeCategory: contentSizeCategory)) } 35 | switch renderMode { 36 | case .trimWhitespaceAndNewlines: return result.trimCharactersInSet(charSet: CharacterSet.whitespacesAndNewlines) 37 | case .trimWhitespace: return result.trimCharactersInSet(charSet: CharacterSet.whitespaces) 38 | case .preserve: return result 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Source/NSAttributedString+Trim.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+Trim.swift 3 | // Freetime 4 | // 5 | // Created by Ryan Nystrom on 6/14/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // https://stackoverflow.com/a/38738940/940936 12 | internal extension NSAttributedString { 13 | 14 | func attributedStringByTrimmingCharacterSet(charSet: CharacterSet) -> NSAttributedString { 15 | let modifiedString = NSMutableAttributedString(attributedString: self) 16 | return modifiedString.trimCharactersInSet(charSet: charSet) 17 | } 18 | 19 | } 20 | 21 | internal extension NSMutableAttributedString { 22 | 23 | func trimCharactersInSet(charSet: CharacterSet) -> NSMutableAttributedString { 24 | var range = (string as NSString).rangeOfCharacter(from: charSet) 25 | 26 | // Trim leading characters from character set. 27 | while range.length != 0 && range.location == 0 { 28 | replaceCharacters(in: range, with: "") 29 | range = (string as NSString).rangeOfCharacter(from: charSet) 30 | } 31 | 32 | // Trim trailing characters from character set. 33 | range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards) 34 | while range.length != 0 && NSMaxRange(range) == length { 35 | replaceCharacters(in: range, with: "") 36 | range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards) 37 | } 38 | 39 | return self 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /UITestsApp/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/Example/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/Example/Example/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/Example/Example/Base.lproj/Main.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 | -------------------------------------------------------------------------------- /Pods/iOSSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Diff.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Gabriel Handford on 3/1/09. 3 | // Copyright 2009-2013. All rights reserved. 4 | // Created by John Boiles on 10/20/11. 5 | // Copyright (c) 2011. All rights reserved 6 | // Modified by Felix Schulze on 2/11/13. 7 | // Copyright 2013. All rights reserved. 8 | // 9 | // Permission is hereby granted, free of charge, to any person 10 | // obtaining a copy of this software and associated documentation 11 | // files (the "Software"), to deal in the Software without 12 | // restriction, including without limitation the rights to use, 13 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the 15 | // Software is furnished to do so, subject to the following 16 | // conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be 19 | // included in all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | // OTHER DEALINGS IN THE SOFTWARE. 29 | // 30 | 31 | #import 32 | 33 | NS_ASSUME_NONNULL_BEGIN 34 | 35 | @interface UIImage (Diff) 36 | 37 | - (UIImage *)fb_diffWithImage:(UIImage *)image; 38 | 39 | @end 40 | 41 | NS_ASSUME_NONNULL_END 42 | -------------------------------------------------------------------------------- /UITestsApp/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 | -------------------------------------------------------------------------------- /Example/Example/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 | -------------------------------------------------------------------------------- /Example/Example/Example/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## OSX 6 | .DS_Store 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | .build/ 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | # Pods/ 52 | 53 | # Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | # Carthage/Checkouts 57 | 58 | Carthage/Build 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 63 | # screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots 70 | fastlane/test_output 71 | node_modules 72 | 73 | # Jekyll 74 | _site/ 75 | .sass-cache/ 76 | .jekyll-metadata 77 | 78 | # secrets 79 | Resources/*.xcconfig 80 | *Secrets.swift* 81 | 82 | 83 | # FBSnapshotTestCase Failure Diffs 84 | FailureDiffs/ 85 | -------------------------------------------------------------------------------- /UITestsApp/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 | } -------------------------------------------------------------------------------- /Example/Example/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 | } -------------------------------------------------------------------------------- /Example/Example/Example/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 | } -------------------------------------------------------------------------------- /Pods/iOSSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Compare.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Gabriel Handford on 3/1/09. 3 | // Copyright 2009-2013. All rights reserved. 4 | // Created by John Boiles on 10/20/11. 5 | // Copyright (c) 2011. All rights reserved 6 | // Modified by Felix Schulze on 2/11/13. 7 | // Copyright 2013. All rights reserved. 8 | // 9 | // Permission is hereby granted, free of charge, to any person 10 | // obtaining a copy of this software and associated documentation 11 | // files (the "Software"), to deal in the Software without 12 | // restriction, including without limitation the rights to use, 13 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the 15 | // Software is furnished to do so, subject to the following 16 | // conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be 19 | // included in all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | // OTHER DEALINGS IN THE SOFTWARE. 29 | // 30 | 31 | #import 32 | 33 | NS_ASSUME_NONNULL_BEGIN 34 | 35 | @interface UIImage (Compare) 36 | 37 | /** 38 | Compares the image against another given image. 39 | 40 | @param image The other image to compare against. 41 | @param perPixelTolerance How much (in percentage) any given pixel's colors are allowed to change from the pixel in the reference image. 42 | @param overallTolerance The overall percentage of pixels that are allowed to change from the pixels in the reference image. 43 | @return A BOOL which represents if the image is the same or not. 44 | */ 45 | - (BOOL)fb_compareWithImage:(UIImage *)image perPixelTolerance:(CGFloat)perPixelTolerance overallTolerance:(CGFloat)overallTolerance; 46 | 47 | @end 48 | 49 | NS_ASSUME_NONNULL_END 50 | -------------------------------------------------------------------------------- /Example/Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Marcus Wu on 2018/6/14. 6 | // Copyright © 2018年 Marcus Wu. 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: [UIApplicationLaunchOptionsKey: 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 invalidate graphics rendering callbacks. 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 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // 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. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /UITestsApp/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import StyledTextKit 3 | 4 | class ViewController: UIViewController { 5 | @IBOutlet weak var textView: StyledTextView! 6 | @IBOutlet weak var stateLabel: UILabel! 7 | 8 | private let style = TextStyle( 9 | size: 20, 10 | attributes: [.foregroundColor: UIColor.red] 11 | ) 12 | 13 | private let styleLarge = TextStyle( 14 | size: 32, 15 | attributes: [.foregroundColor: UIColor.green] 16 | ) 17 | 18 | private let styleTapable = TextStyle( 19 | size: 16, 20 | attributes: [.foregroundColor: UIColor.orange, 21 | .underlineStyle: 1] 22 | ) 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | let builder = StyledTextBuilder(text: "So ") 28 | .save() 29 | .add(style: self.style) 30 | .add(text: "good") 31 | .restore() 32 | .save() 33 | .add(style: self.styleLarge) 34 | .add(text: "👍!") 35 | .restore() 36 | .save() 37 | .add(style: self.styleTapable) 38 | .add(text: "Link1", attributes: [.highlight: "Link1"]) 39 | .restore() 40 | .save() 41 | .add(text: "\n") 42 | .restore() 43 | .save() 44 | .add(style: self.styleTapable) 45 | .add(text: "Link2 ", attributes: [.highlight: "Link2"]) 46 | .restore() 47 | 48 | let renderer = StyledTextRenderer(string: builder.build(), 49 | contentSizeCategory: .medium) 50 | 51 | textView.configure(with: renderer, width: 240) 52 | textView.delegate = self 53 | } 54 | } 55 | 56 | extension ViewController: StyledTextViewDelegate { 57 | 58 | func didTap(view: StyledTextView, attributes: NSAttributedStringAttributesType, point: CGPoint) { 59 | guard let linkContent = attributes[.highlight] as? String else { return } 60 | stateLabel.text = "didTap: \(linkContent)" 61 | } 62 | 63 | func didLongPress(view: StyledTextView, attributes: NSAttributedStringAttributesType, point: CGPoint) { 64 | guard let linkContent = attributes[.highlight] as? String else { return } 65 | stateLabel.text = "didLongPress: \(linkContent)" 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Example/Pods-Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | The MIT License 18 | 19 | Copyright (c) 2017 Ryan Nystrom http://whoisryannystrom.com/ 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in 29 | all copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 37 | THE SOFTWARE. 38 | 39 | License 40 | MIT 41 | Title 42 | StyledTextKit 43 | Type 44 | PSGroupSpecifier 45 | 46 | 47 | FooterText 48 | Generated by CocoaPods - https://cocoapods.org 49 | Title 50 | 51 | Type 52 | PSGroupSpecifier 53 | 54 | 55 | StringsTable 56 | Acknowledgements 57 | Title 58 | Acknowledgements 59 | 60 | 61 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Pods-Tests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | MIT License 18 | 19 | Copyright (c) 2017-2018, Uber Technologies, Inc. 20 | Copyright (c) 2013-2018, Facebook, Inc. 21 | 22 | Permission is hereby granted, free of charge, to any person obtaining a copy 23 | of this software and associated documentation files (the "Software"), to deal 24 | in the Software without restriction, including without limitation the rights 25 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 26 | copies of the Software, and to permit persons to whom the Software is 27 | furnished to do so, subject to the following conditions: 28 | 29 | The above copyright notice and this permission notice shall be included in all 30 | copies or substantial portions of the Software. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 35 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 37 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 38 | SOFTWARE. 39 | 40 | License 41 | MIT 42 | Title 43 | iOSSnapshotTestCase 44 | Type 45 | PSGroupSpecifier 46 | 47 | 48 | FooterText 49 | Generated by CocoaPods - https://cocoapods.org 50 | Title 51 | 52 | Type 53 | PSGroupSpecifier 54 | 55 | 56 | StringsTable 57 | Acknowledgements 58 | Title 59 | Acknowledgements 60 | 61 | 62 | -------------------------------------------------------------------------------- /StyledTextKit.xcodeproj/xcshareddata/xcschemes/Tests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 55 | 56 | 58 | 59 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Pods/iOSSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Snapshot.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018, Uber Technologies, Inc. 3 | * Copyright (c) 2015-2018, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | #import 11 | 12 | @implementation UIImage (Snapshot) 13 | 14 | + (UIImage *)fb_imageForLayer:(CALayer *)layer 15 | { 16 | CGRect bounds = layer.bounds; 17 | NSAssert1(CGRectGetWidth(bounds), @"Zero width for layer %@", layer); 18 | NSAssert1(CGRectGetHeight(bounds), @"Zero height for layer %@", layer); 19 | 20 | UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0); 21 | CGContextRef context = UIGraphicsGetCurrentContext(); 22 | NSAssert1(context, @"Could not generate context for layer %@", layer); 23 | CGContextSaveGState(context); 24 | [layer layoutIfNeeded]; 25 | [layer renderInContext:context]; 26 | CGContextRestoreGState(context); 27 | 28 | UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext(); 29 | UIGraphicsEndImageContext(); 30 | return snapshot; 31 | } 32 | 33 | + (UIImage *)fb_imageForViewLayer:(UIView *)view 34 | { 35 | [view layoutIfNeeded]; 36 | return [self fb_imageForLayer:view.layer]; 37 | } 38 | 39 | + (UIImage *)fb_imageForView:(UIView *)view 40 | { 41 | // If the input view is already a UIWindow, then just use that. Otherwise wrap in a window. 42 | UIWindow *window = [view isKindOfClass:[UIWindow class]] ? (UIWindow *)view : view.window; 43 | BOOL removeFromSuperview = NO; 44 | if (!window) { 45 | window = [[UIApplication sharedApplication] keyWindow]; 46 | } 47 | 48 | if (!view.window && view != window) { 49 | [window addSubview:view]; 50 | removeFromSuperview = YES; 51 | } 52 | 53 | [view layoutIfNeeded]; 54 | 55 | CGRect bounds = view.bounds; 56 | NSAssert1(CGRectGetWidth(bounds), @"Zero width for view %@", view); 57 | NSAssert1(CGRectGetHeight(bounds), @"Zero height for view %@", view); 58 | 59 | UIGraphicsBeginImageContextWithOptions(bounds.size, NO, 0); 60 | [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES]; 61 | 62 | UIImage *snapshot = UIGraphicsGetImageFromCurrentImageContext(); 63 | UIGraphicsEndImageContext(); 64 | 65 | if (removeFromSuperview) { 66 | [view removeFromSuperview]; 67 | } 68 | 69 | return snapshot; 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /Tests/StyledTextTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyledTextKitTests.swift 3 | // StyledTextKitTests 4 | // 5 | // Created by Ryan Nystrom on 12/12/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import StyledTextKit 11 | 12 | class StyledTextTests: XCTestCase { 13 | 14 | func test_renderingStyledText_fromString_toAttributedString() { 15 | let style = TextStyle( 16 | size: 12, 17 | attributes: [.foregroundColor: UIColor.white] 18 | ) 19 | let text = StyledText(storage: .text("foo"), style: style) 20 | let render = text.render(contentSizeCategory: .large) 21 | XCTAssertEqual(render.string, "foo") 22 | 23 | let attributes = render.attributes(at: 1, effectiveRange: nil) 24 | XCTAssertEqual(attributes[.foregroundColor] as! UIColor, UIColor.white) 25 | 26 | let font = attributes[.font] as! UIFont 27 | XCTAssertEqual(font.familyName, UIFont.systemFont(ofSize: 1).familyName) 28 | XCTAssertEqual(font.pointSize, 12) 29 | } 30 | 31 | func test_renderingStyledText_fromNSAttributedString_toAttributedString() { 32 | let style = TextStyle( 33 | size: 12, 34 | attributes: [ 35 | .foregroundColor: UIColor.white, 36 | .font: UIFont.systemFont(ofSize: 10), 37 | ] 38 | ) 39 | let attributedString = NSAttributedString(string: "foo", attributes: [ 40 | .foregroundColor: UIColor.red, 41 | .font: UIFont.boldSystemFont(ofSize: 20), 42 | ]) 43 | let text = StyledText(storage: .attributedText(attributedString), style: style) 44 | let render = text.render(contentSizeCategory: .large) 45 | XCTAssertEqual(render.string, "foo") 46 | 47 | let attributes = render.attributes(at: 1, effectiveRange: nil) 48 | XCTAssertEqual(attributes[.foregroundColor] as! UIColor, UIColor.red) 49 | XCTAssertEqual(attributes[.font] as! UIFont, UIFont.boldSystemFont(ofSize: 20)) 50 | } 51 | 52 | func test_renderingStyledText_fromNSAttributedString_toAttributedString_whenEmpty() { 53 | let attributedString = NSAttributedString(string: "", attributes: [ 54 | .foregroundColor: UIColor.red, 55 | .font: UIFont.boldSystemFont(ofSize: 20), 56 | ]) 57 | let text = StyledText(storage: .attributedText(attributedString), style: TextStyle()) 58 | let render = text.render(contentSizeCategory: .large) 59 | XCTAssertEqual(render.string, "") 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Pods/iOSSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Diff.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Gabriel Handford on 3/1/09. 3 | // Copyright 2009-2013. All rights reserved. 4 | // Created by John Boiles on 10/20/11. 5 | // Copyright (c) 2011. All rights reserved 6 | // Modified by Felix Schulze on 2/11/13. 7 | // Copyright 2013. All rights reserved. 8 | // 9 | // Permission is hereby granted, free of charge, to any person 10 | // obtaining a copy of this software and associated documentation 11 | // files (the "Software"), to deal in the Software without 12 | // restriction, including without limitation the rights to use, 13 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the 15 | // Software is furnished to do so, subject to the following 16 | // conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be 19 | // included in all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | // OTHER DEALINGS IN THE SOFTWARE. 29 | // 30 | 31 | #import 32 | 33 | @implementation UIImage (Diff) 34 | 35 | - (UIImage *)fb_diffWithImage:(UIImage *)image 36 | { 37 | if (!image) { 38 | return nil; 39 | } 40 | CGSize imageSize = CGSizeMake(MAX(self.size.width, image.size.width), MAX(self.size.height, image.size.height)); 41 | UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0); 42 | CGContextRef context = UIGraphicsGetCurrentContext(); 43 | [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)]; 44 | CGContextSetAlpha(context, 0.5); 45 | CGContextBeginTransparencyLayer(context, NULL); 46 | [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; 47 | CGContextSetBlendMode(context, kCGBlendModeDifference); 48 | CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); 49 | CGContextFillRect(context, CGRectMake(0, 0, self.size.width, self.size.height)); 50 | CGContextEndTransparencyLayer(context); 51 | UIImage *returnImage = UIGraphicsGetImageFromCurrentImageContext(); 52 | UIGraphicsEndImageContext(); 53 | return returnImage; 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /Source/TextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextStyle.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/12/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct TextStyle: Hashable, Equatable { 12 | 13 | public let font: Font 14 | public let size: CGFloat 15 | public let attributes: NSAttributedStringAttributesType 16 | public let minSize: CGFloat 17 | public let maxSize: CGFloat 18 | public let scalingTextStyle: UIFont.TextStyle 19 | 20 | public init( 21 | font: Font = .system(.default), 22 | size: CGFloat = UIFont.systemFontSize, 23 | attributes: NSAttributedStringAttributesType = [:], 24 | minSize: CGFloat = 0, 25 | maxSize: CGFloat = .greatestFiniteMagnitude, 26 | scalingTextStyle: UIFont.TextStyle = .body 27 | ) { 28 | self.font = font 29 | self.size = size 30 | self.attributes = attributes 31 | self.minSize = minSize 32 | self.maxSize = maxSize 33 | self.scalingTextStyle = scalingTextStyle 34 | } 35 | 36 | public func font(contentSizeCategory: UIContentSizeCategory) -> UIFont { 37 | let calculatedSize: CGFloat 38 | 39 | if #available(iOS 11.0, *) { 40 | let metrics = UIFontMetrics(forTextStyle: scalingTextStyle) 41 | calculatedSize = metrics.scaledValue(for: size, compatibleWith: UITraitCollection(preferredContentSizeCategory: contentSizeCategory)) 42 | } else { 43 | calculatedSize = size * contentSizeCategory.approximateScaleFactor(forTextStyle: scalingTextStyle) 44 | } 45 | 46 | let preferredSize = min(max(calculatedSize, minSize), maxSize) 47 | 48 | switch font { 49 | case .name(let name): 50 | guard let font = UIFont(name: name, size: preferredSize) else { 51 | print("WARNING: Font with name \"\(name)\" not found. Falling back to system font.") 52 | return UIFont.systemFont(ofSize: preferredSize) 53 | } 54 | return font 55 | case .descriptor(let descriptor): return UIFont(descriptor: descriptor, size: preferredSize) 56 | case .system(let system): 57 | switch system { 58 | case .default: return UIFont.systemFont(ofSize: preferredSize) 59 | case .bold: return UIFont.boldSystemFont(ofSize: preferredSize) 60 | case .italic: return UIFont.italicSystemFont(ofSize: preferredSize) 61 | case .weighted(let weight): return UIFont.systemFont(ofSize: preferredSize, weight: weight) 62 | case .monospaced(let weight): return UIFont.monospacedDigitSystemFont(ofSize: preferredSize, weight: weight) 63 | } 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Pods/iOSSnapshotTestCase/FBSnapshotTestCase/FBSnapshotTestCasePlatform.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018, Uber Technologies, Inc. 3 | * Copyright (c) 2015-2018, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | #import 11 | #import 12 | 13 | BOOL FBSnapshotTestCaseIs64Bit(void) 14 | { 15 | #if __LP64__ 16 | return YES; 17 | #else 18 | return NO; 19 | #endif 20 | } 21 | 22 | NSOrderedSet *FBSnapshotTestCaseDefaultSuffixes(void) 23 | { 24 | NSMutableOrderedSet *suffixesSet = [[NSMutableOrderedSet alloc] init]; 25 | [suffixesSet addObject:@"_32"]; 26 | [suffixesSet addObject:@"_64"]; 27 | if (FBSnapshotTestCaseIs64Bit()) { 28 | return [suffixesSet reversedOrderedSet]; 29 | } 30 | return [suffixesSet copy]; 31 | } 32 | 33 | NSString *FBFileNameIncludeNormalizedFileNameFromOption(NSString *fileName, FBSnapshotTestCaseFileNameIncludeOption option) 34 | { 35 | if ((option & FBSnapshotTestCaseFileNameIncludeOptionDevice) == FBSnapshotTestCaseFileNameIncludeOptionDevice) { 36 | UIDevice *device = [UIDevice currentDevice]; 37 | fileName = [fileName stringByAppendingFormat:@"_%@", device.model]; 38 | } 39 | 40 | if ((option & FBSnapshotTestCaseFileNameIncludeOptionOS) == FBSnapshotTestCaseFileNameIncludeOptionOS) { 41 | UIDevice *device = [UIDevice currentDevice]; 42 | NSString *os = device.systemVersion; 43 | fileName = [fileName stringByAppendingFormat:@"_%@", os]; 44 | } 45 | 46 | if ((option & FBSnapshotTestCaseFileNameIncludeOptionScreenSize) == FBSnapshotTestCaseFileNameIncludeOptionScreenSize) { 47 | UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; 48 | CGSize screenSize = keyWindow.bounds.size; 49 | fileName = [fileName stringByAppendingFormat:@"_%.0fx%.0f", screenSize.width, screenSize.height]; 50 | } 51 | 52 | NSMutableCharacterSet *invalidCharacters = [NSMutableCharacterSet new]; 53 | [invalidCharacters formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]]; 54 | [invalidCharacters formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]]; 55 | NSArray *validComponents = [fileName componentsSeparatedByCharactersInSet:invalidCharacters]; 56 | fileName = [validComponents componentsJoinedByString:@"_"]; 57 | 58 | if ((option & FBSnapshotTestCaseFileNameIncludeOptionScreenScale) == FBSnapshotTestCaseFileNameIncludeOptionScreenScale) { 59 | CGFloat screenScale = [[UIScreen mainScreen] scale]; 60 | fileName = [fileName stringByAppendingFormat:@"@%.fx", screenScale]; 61 | } 62 | 63 | return fileName; 64 | } 65 | -------------------------------------------------------------------------------- /Tests/StyledTextBuilderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyledTextKitBuilderTests.swift 3 | // StyledTextKitTests 4 | // 5 | // Created by Ryan Nystrom on 12/12/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import StyledTextKit 11 | 12 | class StyledTextBuilderTests: XCTestCase { 13 | 14 | func test_whenOneLevel() { 15 | let render = StyledTextBuilder(text: "foo") 16 | .build() 17 | .render(contentSizeCategory: .large) 18 | XCTAssertEqual(render.string, "foo") 19 | } 20 | 21 | func test_whenAddingString() { 22 | let render = StyledTextBuilder(text: "foo") 23 | .add(text: " bar") 24 | .build() 25 | .render(contentSizeCategory: .large) 26 | XCTAssertEqual(render.string, "foo bar") 27 | } 28 | 29 | func test_whenAddingAttributes() { 30 | let render = StyledTextBuilder(styledText: StyledText(storage: .text("foo"), style: TextStyle(font: .system(.bold)))) 31 | .add(styledText: StyledText(storage: .text(" bar"), style: TextStyle(font: .system(.italic)))) 32 | .build() 33 | .render(contentSizeCategory: .large) 34 | XCTAssertEqual(render.string, "foo bar") 35 | 36 | let font1 = render.attributes(at: 1, effectiveRange: nil)[.font] as! UIFont 37 | XCTAssertTrue(font1.fontDescriptor.symbolicTraits.contains(.traitBold)) 38 | 39 | let font2 = render.attributes(at: 5, effectiveRange: nil)[.font] as! UIFont 40 | XCTAssertTrue(font2.fontDescriptor.symbolicTraits.contains(.traitItalic)) 41 | } 42 | 43 | func test_whenAddingAttributes_withSavingState_thenRestoring() { 44 | let string = StyledTextBuilder(styledText: StyledText(storage: .text("foo"), style: TextStyle(font: .system(.bold)))) 45 | .save() 46 | .add(styledText: StyledText(storage: .text(" bar"), style: TextStyle(font: .system(.italic)))) 47 | .restore() 48 | .add(text: " baz") 49 | .build() 50 | XCTAssertEqual(string.allText, "foo bar baz") 51 | 52 | let render = string.render(contentSizeCategory: .large) 53 | XCTAssertEqual(render.string, "foo bar baz") 54 | 55 | let font1 = render.attributes(at: 1, effectiveRange: nil)[.font] as! UIFont 56 | XCTAssertTrue(font1.fontDescriptor.symbolicTraits.contains(.traitBold)) 57 | XCTAssertFalse(font1.fontDescriptor.symbolicTraits.contains(.traitItalic)) 58 | 59 | let font2 = render.attributes(at: 5, effectiveRange: nil)[.font] as! UIFont 60 | XCTAssertFalse(font2.fontDescriptor.symbolicTraits.contains(.traitBold)) 61 | XCTAssertTrue(font2.fontDescriptor.symbolicTraits.contains(.traitItalic)) 62 | 63 | let font3 = render.attributes(at: 9, effectiveRange: nil)[.font] as! UIFont 64 | XCTAssertTrue(font3.fontDescriptor.symbolicTraits.contains(.traitBold)) 65 | XCTAssertFalse(font3.fontDescriptor.symbolicTraits.contains(.traitItalic)) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Pods/iOSSnapshotTestCase/FBSnapshotTestCase/FBSnapshotTestCasePlatform.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018, Uber Technologies, Inc. 3 | * Copyright (c) 2015-2018, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | #import 11 | 12 | #ifdef __cplusplus 13 | extern "C" { 14 | #endif 15 | 16 | NS_ASSUME_NONNULL_BEGIN 17 | 18 | /** 19 | An option mask that allows you to cherry pick which parts you want to include in the snapshot file name. 20 | 21 | - FBSnapshotTestCaseFileNameIncludeOptionNone: Don't include any of these options at all. 22 | - FBSnapshotTestCaseFileNameIncludeOptionDevice: The file name should include the device name, as returned by UIDevice.currentDevice.model. 23 | - FBSnapshotTestCaseFileNameIncludeOptionOS: The file name should include the OS version, as returned by UIDevice.currentDevice.systemVersion. 24 | - FBSnapshotTestCaseFileNameIncludeOptionScreenSize: The file name should include the screen size of the current keyWindow, as returned by UIApplication.sharedApplication.keyWindow.bounds.size. 25 | - FBSnapshotTestCaseFileNameIncludeOptionScreenScale: The file name should include the scale of the current device, as returned by UIScreen.mainScreen.scale. 26 | */ 27 | typedef NS_OPTIONS(NSUInteger, FBSnapshotTestCaseFileNameIncludeOption) { 28 | FBSnapshotTestCaseFileNameIncludeOptionNone = 1 << 0, 29 | FBSnapshotTestCaseFileNameIncludeOptionDevice = 1 << 1, 30 | FBSnapshotTestCaseFileNameIncludeOptionOS = 1 << 2, 31 | FBSnapshotTestCaseFileNameIncludeOptionScreenSize = 1 << 3, 32 | FBSnapshotTestCaseFileNameIncludeOptionScreenScale = 1 << 4 33 | }; 34 | 35 | /** 36 | Returns a Boolean value that indicates whether the snapshot test is running in 64Bit. 37 | This method is a convenience for creating the suffixes set based on the architecture 38 | that the test is running. 39 | 40 | @returns @c YES if the test is running in 64bit, otherwise @c NO. 41 | */ 42 | BOOL FBSnapshotTestCaseIs64Bit(void); 43 | 44 | /** 45 | Returns a default set of strings that is used to append a suffix based on the architectures. 46 | @warning Do not modify this function, you can create your own and use it with @c FBSnapshotVerifyViewWithOptions() 47 | 48 | @returns An @c NSOrderedSet object containing strings that are appended to the reference images directory. 49 | */ 50 | NSOrderedSet *FBSnapshotTestCaseDefaultSuffixes(void); 51 | 52 | /** 53 | Returns a fully normalized file name as per the provided option mask. Strips punctuation and spaces and replaces them with @c _. 54 | 55 | @param fileName The file name to normalize. 56 | @param option File Name Include options to use before normalization. 57 | @return An @c NSString object containing the passed @c fileName and optionally, with the device model and/or OS and/or screen size and/or screen scale appended at the end. 58 | */ 59 | NSString *FBFileNameIncludeNormalizedFileNameFromOption(NSString *fileName, FBSnapshotTestCaseFileNameIncludeOption option); 60 | 61 | NS_ASSUME_NONNULL_END 62 | 63 | #ifdef __cplusplus 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /Example/Example/BackgroundRenderingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundRenderingViewController.swift 3 | // Example 4 | // 5 | // Created by Marcus Wu on 2018/6/14. 6 | // Copyright © 2018年 Marcus Wu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StyledTextKit 11 | import SafariServices 12 | 13 | extension NSAttributedStringKey { 14 | 15 | public static let tapable = NSAttributedStringKey(rawValue: "tapable") 16 | 17 | } 18 | 19 | class BackgroundRenderingViewController: UIViewController { 20 | 21 | @IBOutlet weak var containter: UIView! 22 | 23 | lazy var styledTextView: StyledTextView = { 24 | let textView = StyledTextView() 25 | 26 | textView.delegate = self 27 | 28 | return textView 29 | }() 30 | 31 | let contentSizeCategory = UIApplication.shared.preferredContentSizeCategory 32 | 33 | let style = TextStyle( 34 | size: 20, 35 | attributes: [.foregroundColor: UIColor.red] 36 | ) 37 | 38 | let styleLarge = TextStyle( 39 | size: 32, 40 | attributes: [.foregroundColor: UIColor.green] 41 | ) 42 | 43 | let styleTapable = TextStyle( 44 | size: 16, 45 | attributes: [.foregroundColor: UIColor.orange, 46 | .underlineStyle: 1] 47 | ) 48 | 49 | override func viewDidLoad() { 50 | super.viewDidLoad() 51 | 52 | title = "Background Rendering" 53 | 54 | containter.addSubview(styledTextView) 55 | 56 | DispatchQueue.global().async { 57 | let builder = StyledTextBuilder(text: "So ") 58 | .save() 59 | .add(style: self.style) 60 | .add(text: "good") 61 | .restore() 62 | .save() 63 | .add(style: self.styleLarge) 64 | .add(text: "👍!") 65 | .restore() 66 | .save() 67 | .add(style: self.styleTapable) 68 | .add(text: "Tap me to StyledTextKit GitHub", attributes: [.tapable: #selector(self.tapAction), .highlight: NSObject()]) 69 | 70 | let renderer = StyledTextRenderer(string: builder.build(), contentSizeCategory: self.contentSizeCategory) 71 | .warm(width: 240) // warms the size cache 72 | 73 | DispatchQueue.main.async { 74 | self.styledTextView.configure(with: renderer, width: 240) 75 | } 76 | } 77 | } 78 | 79 | @objc private func tapAction() { 80 | present(SFSafariViewController(url: URL(string: "https://github.com/GitHawkApp/StyledTextKit")!), animated: true, completion: nil) 81 | } 82 | 83 | } 84 | 85 | extension BackgroundRenderingViewController: StyledTextViewDelegate { 86 | 87 | func didTap(view: StyledTextView, attributes: [NSAttributedStringKey : Any], point: CGPoint) { 88 | guard let action = attributes[.tapable] as? Selector else { return } 89 | 90 | perform(action) 91 | } 92 | 93 | func didLongPress(view: StyledTextView, attributes: [NSAttributedStringKey : Any], point: CGPoint) {} 94 | 95 | } 96 | -------------------------------------------------------------------------------- /Pods/iOSSnapshotTestCase/FBSnapshotTestCase/SwiftSupport.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017-2018, Uber Technologies, Inc. 3 | * Copyright (c) 2015-2018, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | public extension FBSnapshotTestCase { 11 | func FBSnapshotVerifyView(_ view: UIView, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), perPixelTolerance: CGFloat = 0, overallTolerance: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { 12 | FBSnapshotVerifyViewOrLayer(view, identifier: identifier, suffixes: suffixes, perPixelTolerance: perPixelTolerance, overallTolerance: overallTolerance, file: file, line: line) 13 | } 14 | 15 | func FBSnapshotVerifyLayer(_ layer: CALayer, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), perPixelTolerance: CGFloat = 0, overallTolerance: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { 16 | FBSnapshotVerifyViewOrLayer(layer, identifier: identifier, suffixes: suffixes, perPixelTolerance: perPixelTolerance, overallTolerance: overallTolerance, file: file, line: line) 17 | } 18 | 19 | private func FBSnapshotVerifyViewOrLayer(_ viewOrLayer: AnyObject, identifier: String = "", suffixes: NSOrderedSet = FBSnapshotTestCaseDefaultSuffixes(), perPixelTolerance: CGFloat = 0, overallTolerance: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { 20 | let envReferenceImageDirectory = self.getReferenceImageDirectory(withDefault: FB_REFERENCE_IMAGE_DIR) 21 | let envImageDiffDirectory = self.getImageDiffDirectory(withDefault: IMAGE_DIFF_DIR) 22 | var error: NSError? 23 | var comparisonSuccess = false 24 | 25 | for suffix in suffixes { 26 | let referenceImagesDirectory = "\(envReferenceImageDirectory)\(suffix)" 27 | let imageDiffDirectory = envImageDiffDirectory 28 | if viewOrLayer.isKind(of: UIView.self) { 29 | do { 30 | try compareSnapshot(of: viewOrLayer as! UIView, referenceImagesDirectory: referenceImagesDirectory, imageDiffDirectory: imageDiffDirectory, identifier: identifier, perPixelTolerance: perPixelTolerance, overallTolerance: overallTolerance) 31 | comparisonSuccess = true 32 | } catch let error1 as NSError { 33 | error = error1 34 | comparisonSuccess = false 35 | } 36 | } else if viewOrLayer.isKind(of: CALayer.self) { 37 | do { 38 | try compareSnapshot(of: viewOrLayer as! CALayer, referenceImagesDirectory: referenceImagesDirectory, imageDiffDirectory: imageDiffDirectory, identifier: identifier, perPixelTolerance: perPixelTolerance, overallTolerance: overallTolerance) 39 | comparisonSuccess = true 40 | } catch let error1 as NSError { 41 | error = error1 42 | comparisonSuccess = false 43 | } 44 | } else { 45 | assertionFailure("Only UIView and CALayer classes can be snapshotted") 46 | } 47 | 48 | assert(recordMode == false, message: "Test ran in record mode. Reference image is now saved. Disable record mode to perform an actual snapshot comparison!", file: file, line: line) 49 | 50 | if comparisonSuccess || recordMode { 51 | break 52 | } 53 | 54 | assert(comparisonSuccess, message: "Snapshot comparison failed: \(String(describing: error))", file: file, line: line) 55 | } 56 | } 57 | 58 | func assert(_ assertion: Bool, message: String, file: StaticString, line: UInt) { 59 | if !assertion { 60 | XCTFail(message, file: file, line: line) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /StyledTextKit.xcodeproj/xcshareddata/xcschemes/UITests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 66 | 67 | 73 | 74 | 80 | 81 | 82 | 83 | 85 | 86 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /Example/Example/FastScrollingTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FastScrollingTableViewController.swift 3 | // Example 4 | // 5 | // Created by Ryan Nystrom on 7/8/18. 6 | // Copyright © 2018 Marcus Wu. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StyledTextKit 11 | 12 | extension UITableView { 13 | var contentInsetWidth: CGFloat { 14 | return bounds.width - safeAreaInsets.left - safeAreaInsets.right 15 | } 16 | } 17 | 18 | class StyledTextTableViewCell: UITableViewCell { 19 | 20 | let textView = StyledTextView() 21 | 22 | override func layoutSubviews() { 23 | super.layoutSubviews() 24 | textView.reposition(for: contentView.bounds.width) 25 | } 26 | 27 | func configure(_ renderer: StyledTextRenderer) { 28 | if textView.superview != contentView { 29 | contentView.addSubview(textView) 30 | } 31 | textView.configure(with: renderer, width: contentView.bounds.width) 32 | } 33 | 34 | } 35 | 36 | class FastScrollingTableViewController: UITableViewController { 37 | 38 | var data = [StyledTextRenderer]() 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | tableView.register(StyledTextTableViewCell.self, forCellReuseIdentifier: "cell") 43 | 44 | // capture the width and content size while on the main queue 45 | let contentSizeCategory = UIApplication.shared.preferredContentSizeCategory 46 | let width = tableView.contentInsetWidth 47 | 48 | DispatchQueue.global().async { 49 | 50 | var tmp = [StyledTextRenderer]() 51 | for _ in 0..<1000 { 52 | let builder = StyledTextBuilder(styledText: StyledText( 53 | text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.", 54 | style: TextStyle(size: 18) 55 | )) 56 | let renderer = StyledTextRenderer( 57 | string: builder.build(), 58 | contentSizeCategory: contentSizeCategory, 59 | inset: UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15) 60 | ) 61 | .warm(width: width) // calling warm pre-sizes the text for the given width 62 | tmp.append(renderer) 63 | } 64 | 65 | DispatchQueue.main.async { [weak self] in 66 | self?.data = tmp 67 | self?.tableView.reloadData() 68 | } 69 | 70 | } 71 | } 72 | 73 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 74 | coordinator.animate(alongsideTransition: { context in 75 | // fixes layout issues with safeAreaInsets not changing alongside tableView's orientation (bounds.width) 76 | self.tableView.reloadData() 77 | }) 78 | } 79 | 80 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 81 | return data.count 82 | } 83 | 84 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 85 | return data[indexPath.row].viewSize(in: tableView.contentInsetWidth).height 86 | } 87 | 88 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 89 | let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) 90 | if let cell = cell as? StyledTextTableViewCell { 91 | cell.configure(data[indexPath.row]) 92 | } 93 | return cell 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /StyledTextKit.xcodeproj/xcshareddata/xcschemes/UITestsApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Source/LRUCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyledTextKitRenderCache.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/13/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol LRUCachable { 12 | var cachedSize: Int { get } 13 | } 14 | 15 | public final class LRUCache { 16 | 17 | internal class Node { 18 | 19 | // for reverse lookup in map 20 | let key: Key 21 | // mutable b/c you can change the value for an existing key 22 | var value: Value 23 | // 2-way linked list 24 | weak var previous: Node? = nil 25 | var next: Node? = nil 26 | 27 | init(key: Key, value: Value) { 28 | self.key = key 29 | self.value = value 30 | } 31 | 32 | var tail: Node? { 33 | var t: Node? = self 34 | while let next = t?.next { 35 | t = next 36 | } 37 | return t 38 | } 39 | } 40 | 41 | public enum Compaction { 42 | case `default` 43 | case percent(Double) 44 | } 45 | 46 | // thread safety 47 | private var lock = os_unfair_lock_s() 48 | 49 | // mutable collection state 50 | internal var map = [Key: Node]() 51 | internal var size: Int = 0 52 | internal var head: Node? 53 | 54 | public let maxSize: Int 55 | public let compaction: Compaction 56 | 57 | public init(maxSize: Int, compaction: Compaction = .default, clearOnWarning: Bool = false) { 58 | self.maxSize = maxSize 59 | 60 | switch compaction { 61 | case .default: self.compaction = compaction 62 | case .percent(let percent): 63 | if percent <= 0 || percent > 1 { 64 | self.compaction = .default 65 | } else { 66 | self.compaction = compaction 67 | } 68 | } 69 | 70 | if clearOnWarning { 71 | NotificationCenter.default.addObserver( 72 | self, 73 | selector: #selector(clear), 74 | name: UIApplication.didReceiveMemoryWarningNotification, 75 | object: nil 76 | ) 77 | } 78 | } 79 | 80 | public func get(_ key: Key) -> Value? { 81 | os_unfair_lock_lock(&lock) 82 | defer { os_unfair_lock_unlock(&lock) } 83 | 84 | let node = map[key] 85 | newHead(node: node) 86 | return node?.value 87 | } 88 | 89 | public func set(_ key: Key, value: Value?) { 90 | guard let value = value else { return } 91 | 92 | os_unfair_lock_lock(&lock) 93 | defer { os_unfair_lock_unlock(&lock) } 94 | 95 | let node: Node 96 | if let existingNode = map[key] { 97 | size -= existingNode.value.cachedSize 98 | existingNode.value = value 99 | node = existingNode 100 | } else { 101 | node = Node(key: key, value: value) 102 | map[key] = node 103 | } 104 | 105 | size += value.cachedSize 106 | newHead(node: node) 107 | compact() 108 | } 109 | 110 | public subscript(key: Key) -> Value? { 111 | get { 112 | return get(key) 113 | } 114 | set(newValue) { 115 | set(key, value: newValue) 116 | } 117 | } 118 | 119 | @objc public func clear() { 120 | os_unfair_lock_lock(&lock) 121 | defer { os_unfair_lock_unlock(&lock) } 122 | 123 | head = nil 124 | map.removeAll() 125 | size = 0 126 | } 127 | 128 | // unsafe to call w/out nested in lock 129 | private func newHead(node: Node?) { 130 | // fill in the gap and break cycles 131 | node?.previous?.next = node?.next 132 | 133 | head?.previous = node 134 | 135 | node?.next = head 136 | node?.previous = nil 137 | 138 | head = node 139 | } 140 | 141 | // unsafe to call w/out nested in lock 142 | private func compact() { 143 | guard size > maxSize else { return } 144 | 145 | var tail = head?.tail 146 | 147 | let compactSize: Int 148 | switch compaction { 149 | case .default: compactSize = maxSize 150 | case .percent(let percent): compactSize = Int(Double(maxSize) * percent) 151 | } 152 | 153 | while size > compactSize, let next = tail { 154 | size -= next.value.cachedSize 155 | map.removeValue(forKey: next.key) 156 | tail = next.previous 157 | } 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /UITestsApp/Base.lproj/Main.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 | 27 | 28 | 29 | 30 | 31 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | `StyledTextKit` is a declarative attributed string library for fast rendering and easy string building. It serves as a simple replacement to `NSAttributedString` and `UILabel` for background-thread sizing and bitmap caching. 6 | 7 | ## Features 8 | 9 | - Declarative attributed string building API 10 | - Find text sizes on a background thread without sanitizer warnings 11 | - Cache rendered text bitmaps for improved performance 12 | - Custom attribute interaction handling (link taps, etc) 13 | 14 | ## Installation 15 | 16 | Just add `StyledTextKit` to your Podfile and install. Done! 17 | 18 | ```ruby 19 | pod 'StyledTextKit' 20 | ``` 21 | 22 | ## Usage 23 | 24 | ### Building `NSAttributedString`s 25 | 26 | `StyledTextKit` lets you build complex `NSAttributedString`s: 27 | 28 | - Append `NSAttributedString`s or `String`s while re-using the string's current attributes, saving you from repetitive `.font` and `.foregroundColor` styling. 29 | - Intermix complex font traits like **bold** and _italics_ to get _**bold italics**_. 30 | - Handle dynamic text size at string render time. Lets you build the string once and re-render it on device text-size changes. 31 | - Call `save()` and `restore()` to push/pop style settings, letting you build complex text styles without complex code. 32 | 33 | ```swift 34 | let attributedString = StyledTextBuilder(text: "Foo ") 35 | .save() 36 | .add(text: "bar", traits: [.traitBold]) 37 | .restore() 38 | .add(text: " baz!") 39 | .build() 40 | .render(contentSizeCategory: .large) 41 | ``` 42 | 43 | > Foo **bar** baz! 44 | 45 | The basic steps are: 46 | 47 | - Create a `StyledTextBuilder` 48 | - Add `StyledText` objects 49 | - Call `build()` when finished to generate a `StyledTextString` object 50 | - Call `render(contentSizeCategory:)` to create an `NSAttributedString` 51 | 52 | ### Rendering Text Bitmaps 53 | 54 | Create a `StyledTextRenderer` for sizing and rendering text by initializing it with a `StyledTextString` and a `UIContentSizeCategory`. 55 | 56 | ```swift 57 | let renderer = StyledTextRenderer( 58 | string: string, 59 | contentSizeCategory: .large 60 | ) 61 | ``` 62 | 63 | Once created, you can easily get the size of the text constrained to a width: 64 | 65 | ```swift 66 | let size = renderer.size(in: 320) 67 | ``` 68 | 69 | You can also get a bitmap of the text: 70 | 71 | ```swift 72 | let result = renderer.render(for: 320) 73 | view.layer.contents = result.image 74 | ``` 75 | 76 | ### StyledTextView 77 | 78 | To make rendering and layout of text in a `UIView` simpler, use `StyledTextView` to manage display as well as interactions. All you need is a `StyledTextRenderer` and a width and you're set! 79 | 80 | ```swift 81 | let view = StyledTextView() 82 | view.configure(with: renderer, width: 320) 83 | ``` 84 | 85 | Set a delegate on the view to handle tap and long presses: 86 | 87 | ```swift 88 | view.delegate = self 89 | 90 | // StyledTextViewDelegate 91 | func didTap(view: StyledTextView, attributes: [NSAttributedStringKey: Any], point: CGPoint) { 92 | guard let link = attributes[.link] else { return } 93 | show(SFSafariViewController(url: link)) 94 | } 95 | ``` 96 | 97 | ## Background Rendering 98 | 99 | `StyledTextKit` exists to do background sizing and rendering of text content so that scrolling large amounts of text is buttery smooth. The typical pipeline to do this is: 100 | 101 | 1. Get the current width and `UIContentSizeCategory` 102 | 2. Go to a background queue 103 | 3. Build text 104 | 4. Warm caches 105 | 5. Return to the main queue 106 | 6. Configure your views 107 | 108 | ```swift 109 | // ViewController.swift 110 | 111 | let width = view.bounds.width 112 | let contentSizeCategory = UIApplication.shared.preferredContentSizeCategory 113 | 114 | DispatchQueue.global().async { 115 | let builder = StyledTextBuilder(...) 116 | let renderer = StyledTextRenderer(string: builder.build(), contentSizeCategory: contentSizeCategory) 117 | .warm(width: width) // warms the size cache 118 | 119 | DispatchQueue.main.async { 120 | self.textView.configure(with: renderer, width: width) 121 | } 122 | } 123 | ``` 124 | 125 | ## FAQ 126 | 127 | > Why not use `UITextView`? 128 | 129 | Prior to iOS 7, `UITextView` just used WebKit under the hood and was terribly slow. Now that it uses TextKit, it's significantly faster but still requires all sizing and rendering be done on the main thread. 130 | 131 | For apps with lots of text embedded in `UITableViewCell`s or `UICollectionViewCell`s, `UITextView` bring scrolling to a grinding halt. 132 | 133 | ## Acknowledgements 134 | 135 | - [@ocrickard](https://github.com/ocrickard) who built [ComponentTextKit](https://github.com/facebook/componentkit/tree/master/ComponentTextKit) and taught me the basics. 136 | - Created with ❤️ by [Ryan Nystrom](https://twitter.com/_ryannystrom) 137 | -------------------------------------------------------------------------------- /StyledTextKit.xcodeproj/xcshareddata/xcschemes/StyledTextKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 74 | 75 | 81 | 82 | 83 | 84 | 88 | 89 | 90 | 91 | 92 | 93 | 99 | 100 | 106 | 107 | 108 | 109 | 111 | 112 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /Pods/iOSSnapshotTestCase/README.md: -------------------------------------------------------------------------------- 1 | iOSSnapshotTestCase (previously named FBSnapshotTestCase) 2 | ====================== 3 | 4 | [![Build Status](https://travis-ci.org/uber/ios-snapshot-test-case.svg)](https://travis-ci.org/uber/ios-snapshot-test-case) 5 | [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/iOSSnapshotTestCase.svg)](https://img.shields.io/cocoapods/v/iOSSnapshotTestCase.svg) 6 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | 8 | What it does 9 | ------------ 10 | 11 | A "snapshot test case" takes a configured `UIView` or `CALayer` and uses the 12 | `renderInContext:` method to get an image snapshot of its contents. It 13 | compares this snapshot to a "reference image" stored in your source code 14 | repository and fails the test if the two images don't match. 15 | 16 | Why? 17 | ---- 18 | 19 | We write a lot of UI code. There are a lot of edge 20 | cases that we want to handle correctly when you are creating `UIView` instances: 21 | 22 | - What if there is more text than can fit in the space available? 23 | - What if an image doesn't match the size of an image view? 24 | - What should the highlighted state look like? 25 | 26 | It's straightforward to test logic code, but less obvious how you should test 27 | views. You can do a lot of rectangle asserts, but these are hard to understand 28 | or visualize. Looking at an image diff shows you exactly what changed and how 29 | it will look to users. 30 | 31 | `iOSSnapshotTestCase` was developed to make snapshot tests easy. 32 | 33 | Installation with CocoaPods 34 | --------------------------- 35 | 36 | 1. Add the following lines to your Podfile: 37 | 38 | ```ruby 39 | target "Tests" do 40 | use_frameworks! 41 | pod 'iOSSnapshotTestCase' 42 | end 43 | ``` 44 | 45 | If your test target is Objective-C only use `iOSSnapshotTestCase/Core` instead, which doesn't contain Swift support. 46 | 47 | Replace "Tests" with the name of your test project. 48 | 49 | 2. There are [three ways](https://github.com/uber/ios-snapshot-test-case/blob/master/FBSnapshotTestCase/FBSnapshotTestCase.h#L19-L29) of setting reference image directories, the recommended one is to define `FB_REFERENCE_IMAGE_DIR` in your scheme. This should point to the directory where you want reference images to be stored. We normally use this: 50 | 51 | |Name|Value| 52 | |:---|:----| 53 | |`FB_REFERENCE_IMAGE_DIR`|`$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages`| 54 | |`IMAGE_DIFF_DIR`|`$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/FailureDiffs`| 55 | 56 | Define the `IMAGE_DIFF_DIR` to the directory where you want to store diffs of failed snapshots. There are also [three ways](https://github.com/uber/ios-snapshot-test-case/blob/master/FBSnapshotTestCase/FBSnapshotTestCase.h#L34-L43) to set failed image diff directories. 57 | 58 | ![](FBSnapshotTestCaseDemo/Scheme_FB_REFERENCE_IMAGE_DIR.png) 59 | 60 | Creating a snapshot test 61 | ------------------------ 62 | 63 | 1. Subclass `FBSnapshotTestCase` instead of `XCTestCase`. 64 | 2. From within your test, use `FBSnapshotVerifyView`. 65 | 3. Run the test once with `self.recordMode = YES;` in the test's `-setUp` 66 | method. (This creates the reference images on disk.) 67 | 4. Remove the line enabling record mode and run the test. 68 | 69 | Features 70 | -------- 71 | 72 | - Automatically names reference images on disk according to test class and 73 | selector. 74 | - Prints a descriptive error message to the console on failure. (Bonus: 75 | failure message includes a one-line command to see an image diff if 76 | you have [Kaleidoscope](http://www.kaleidoscopeapp.com) installed.) 77 | - Supply an optional "identifier" if you want to perform multiple snapshots 78 | in a single test method. 79 | - Support for `CALayer` via `FBSnapshotVerifyLayer`. 80 | - `usesDrawViewHierarchyInRect` to handle cases like `UIVisualEffect`, `UIAppearance` and Size Classes. 81 | - `fileNameOptions` to control appending the device model (`iPhone`, `iPad`, `iPod Touch`, etc), OS version, screen size and screen scale to the images (allowing to have multiple tests for the same «snapshot» for different `OS`s and devices). 82 | 83 | Notes 84 | ----- 85 | 86 | Your unit tests _should_ be inside an "application" bundle, not a "logic/library" test bundle. (That is, it 87 | should be run within the Simulator so that it has access to UIKit.) 88 | 89 | In Xcode 5 90 | and later new projects only offer application tests, but older projects will 91 | have separate targets for the two types. 92 | 93 | *However*, if you are writing snapshot tests inside a library/framework, you might want to keep your test bundle as a library test bundle without a Test Host. 94 | 95 | Read more on this [here](docs/LibraryVsApplicationTestBundles.md). 96 | 97 | Authors 98 | ------- 99 | 100 | `iOSSnapshotTestCase` was written at Facebook by 101 | [Jonathan Dann](https://facebook.com/j.p.dann) with significant contributions by 102 | [Todd Krabach](https://facebook.com/toddkrabach). 103 | 104 | Today it is maintained by [Uber](https://github.com/uber) and [Alan Zeino](https://github.com/alanzeino). 105 | 106 | License 107 | ------- 108 | 109 | `iOSSnapshotTestCase` is MIT–licensed. See `LICENSE`. 110 | -------------------------------------------------------------------------------- /Source/StyledText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyledTextKit.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/12/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public class StyledText: Hashable, Equatable { 13 | 14 | public struct ImageFitOptions: OptionSet { 15 | public let rawValue: Int 16 | 17 | public init(rawValue: Int) { 18 | self.rawValue = rawValue 19 | } 20 | 21 | public static let fit = ImageFitOptions(rawValue: 1 << 0) 22 | public static let center = ImageFitOptions(rawValue: 2 << 0) 23 | } 24 | 25 | public enum Storage: Hashable, Equatable { 26 | case text(String) 27 | case attributedText(NSAttributedString) 28 | case image(UIImage, [ImageFitOptions]) 29 | 30 | // MARK: Hashable 31 | 32 | public func hash(into hasher: inout Hasher) { 33 | switch self { 34 | case .text(let text): hasher.combine(text) 35 | case .attributedText(let text): hasher.combine(text) 36 | case .image(let image, _): hasher.combine(image) 37 | } 38 | } 39 | 40 | // MARK: Equatable 41 | 42 | public static func ==(lhs: Storage, rhs: Storage) -> Bool { 43 | switch lhs { 44 | case .text(let lhsText): 45 | switch rhs { 46 | case .text(let rhsText): return lhsText == rhsText 47 | case .attributedText, .image: return false 48 | } 49 | case .attributedText(let lhsText): 50 | switch rhs { 51 | case .text, .image: return false 52 | case .attributedText(let rhsText): return lhsText == rhsText 53 | } 54 | case .image(let lhsImage, let lhsOptions): 55 | switch rhs { 56 | case .text, .attributedText: return false 57 | case .image(let rhsImage, let rhsOptions): 58 | return lhsImage == rhsImage && lhsOptions == rhsOptions 59 | } 60 | } 61 | } 62 | 63 | } 64 | 65 | public let storage: Storage 66 | public let style: TextStyle 67 | 68 | public init(storage: Storage = .text(""), style: TextStyle = TextStyle()) { 69 | self.storage = storage 70 | self.style = style 71 | } 72 | 73 | public convenience init(text: String, style: TextStyle = TextStyle()) { 74 | self.init(storage: .text(text), style: style) 75 | } 76 | 77 | internal var text: String { 78 | switch storage { 79 | case .text(let text): return text 80 | case .attributedText(let text): return text.string 81 | case .image: return "" 82 | } 83 | } 84 | 85 | internal func render(contentSizeCategory: UIContentSizeCategory) -> NSAttributedString { 86 | var attributes = style.attributes 87 | let font = style.font(contentSizeCategory: contentSizeCategory) 88 | attributes[.font] = font 89 | switch storage { 90 | case .text(let text): 91 | return NSAttributedString(string: text, attributes: attributes) 92 | case .attributedText(let text): 93 | guard text.length > 0 else { return text } 94 | let mutable = text.mutableCopy() as? NSMutableAttributedString ?? NSMutableAttributedString() 95 | let range = NSRange(location: 0, length: mutable.length) 96 | for (k, v) in attributes { 97 | // avoid overwriting attributes set by the stored string 98 | if mutable.attribute(k, at: 0, effectiveRange: nil) == nil { 99 | mutable.addAttribute(k, value: v, range: range) 100 | } 101 | } 102 | return mutable 103 | case .image(let image, let options): 104 | let attachment = NSTextAttachment() 105 | attachment.image = image 106 | 107 | var bounds = attachment.bounds 108 | let size = image.size 109 | if options.contains(.fit) { 110 | let ratio = size.width / size.height 111 | let fontHeight = min(ceil(font.pointSize), size.height) 112 | bounds.size.width = ratio * fontHeight 113 | bounds.size.height = fontHeight 114 | } else { 115 | bounds.size = size 116 | } 117 | 118 | if options.contains(.center) { 119 | bounds.origin.y = round((font.capHeight - bounds.height) / 2) 120 | } 121 | attachment.bounds = bounds 122 | 123 | // non-breaking space so the color hack doesn't wrap 124 | let attributedString = NSMutableAttributedString(string: "\u{00A0}") 125 | attributedString.append(NSAttributedString(attachment: attachment)) 126 | // replace attributes to 0 size font so no actual space taken 127 | attributes[.font] = UIFont.systemFont(ofSize: 0) 128 | // override all attributes so color actually tints image 129 | attributedString.addAttributes( 130 | attributes, 131 | range: NSRange(location: 0, length: attributedString.length) 132 | ) 133 | return attributedString 134 | } 135 | } 136 | 137 | // MARK: Hashable 138 | 139 | public func hash(into hasher: inout Hasher) { 140 | hasher.combine(storage) 141 | hasher.combine(style) 142 | } 143 | 144 | // MARK: Equatable 145 | 146 | public static func ==(lhs: StyledText, rhs: StyledText) -> Bool { 147 | return lhs === rhs 148 | || lhs.storage == rhs.storage 149 | && lhs.style == rhs.style 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /Source/StyledTextBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyledTextKitBuilder.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/12/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | public final class StyledTextBuilder: Hashable, Equatable { 13 | 14 | internal var styledTexts: [StyledText] 15 | internal var savedStyles = [TextStyle]() 16 | 17 | public convenience init(styledText: StyledText) { 18 | self.init(styledTexts: [styledText]) 19 | } 20 | 21 | public convenience init(text: String) { 22 | self.init(styledText: StyledText(storage: .text(text))) 23 | } 24 | 25 | public convenience init(attributedText: NSAttributedString) { 26 | self.init(styledText: StyledText(storage: .attributedText(attributedText))) 27 | } 28 | 29 | public init(styledTexts: [StyledText]) { 30 | self.styledTexts = styledTexts 31 | } 32 | 33 | public var tipAttributes: NSAttributedStringAttributesType? { 34 | return styledTexts.last?.style.attributes 35 | } 36 | 37 | public var count: Int { 38 | return styledTexts.count 39 | } 40 | 41 | @discardableResult 42 | public func save() -> StyledTextBuilder { 43 | if let last = styledTexts.last?.style { 44 | savedStyles.append(last) 45 | } 46 | return self 47 | } 48 | 49 | @discardableResult 50 | public func restore() -> StyledTextBuilder { 51 | guard let last = savedStyles.last else { return self } 52 | savedStyles.removeLast() 53 | return add(styledText: StyledText(style: last)) 54 | } 55 | 56 | @discardableResult 57 | public func add(styledTexts: [StyledText]) -> StyledTextBuilder { 58 | self.styledTexts += styledTexts 59 | return self 60 | } 61 | 62 | @discardableResult 63 | public func add(styledText: StyledText) -> StyledTextBuilder { 64 | return add(styledTexts: [styledText]) 65 | } 66 | 67 | @discardableResult 68 | public func add(style: TextStyle) -> StyledTextBuilder { 69 | return add(styledText: StyledText(style: style)) 70 | } 71 | 72 | @discardableResult 73 | public func add( 74 | text: String, 75 | traits: UIFontDescriptor.SymbolicTraits? = nil, 76 | attributes: NSAttributedStringAttributesType? = nil 77 | ) -> StyledTextBuilder { 78 | return add(storage: .text(text), traits: traits, attributes: attributes) 79 | } 80 | 81 | @discardableResult 82 | public func add( 83 | attributedText: NSAttributedString, 84 | traits: UIFontDescriptor.SymbolicTraits? = nil, 85 | attributes: NSAttributedStringAttributesType? = nil 86 | ) -> StyledTextBuilder { 87 | return add(storage: .attributedText(attributedText), traits: traits, attributes: attributes) 88 | } 89 | 90 | @discardableResult 91 | public func add( 92 | storage: StyledText.Storage = .text(""), 93 | traits: UIFontDescriptor.SymbolicTraits? = nil, 94 | attributes: NSAttributedStringAttributesType? = nil 95 | ) -> StyledTextBuilder { 96 | guard let tip = styledTexts.last else { return self } 97 | 98 | var nextAttributes = tip.style.attributes 99 | if let attributes = attributes { 100 | for (k, v) in attributes { 101 | nextAttributes[k] = v 102 | } 103 | } 104 | 105 | let nextStyle: TextStyle 106 | if let traits = traits { 107 | 108 | let tipFontDescriptor: UIFontDescriptor 109 | switch tip.style.font { 110 | case .descriptor(let descriptor): tipFontDescriptor = descriptor 111 | default: tipFontDescriptor = tip.style.font(contentSizeCategory: .medium).fontDescriptor 112 | } 113 | 114 | nextStyle = TextStyle( 115 | font: .descriptor(tipFontDescriptor.withSymbolicTraits(traits) ?? tipFontDescriptor), 116 | size: tip.style.size, 117 | attributes: nextAttributes, 118 | minSize: tip.style.minSize, 119 | maxSize: tip.style.maxSize 120 | ) 121 | } else { 122 | nextStyle = TextStyle( 123 | font: tip.style.font, 124 | size: tip.style.size, 125 | attributes: nextAttributes, 126 | minSize: tip.style.minSize, 127 | maxSize: tip.style.maxSize 128 | ) 129 | } 130 | 131 | return add(styledText: StyledText(storage: storage, style: nextStyle)) 132 | } 133 | 134 | @discardableResult 135 | public func add( 136 | image: UIImage, 137 | options: [StyledText.ImageFitOptions] = [.fit, .center], 138 | attributes: NSAttributedStringAttributesType? = nil 139 | ) -> StyledTextBuilder { 140 | return add(storage: .image(image, options), attributes: attributes) 141 | } 142 | 143 | @discardableResult 144 | public func clearText() -> StyledTextBuilder { 145 | guard let tipStyle = styledTexts.last?.style else { return self } 146 | styledTexts.removeAll() 147 | return add(styledText: StyledText(style: tipStyle)) 148 | } 149 | 150 | public func build(renderMode: StyledTextString.RenderMode = .trimWhitespaceAndNewlines) -> StyledTextString { 151 | return StyledTextString(styledTexts: styledTexts, renderMode: renderMode) 152 | } 153 | 154 | // MARK: Hashable 155 | 156 | public func hash(into hasher: inout Hasher) { 157 | styledTexts.forEach { 158 | hasher.combine($0) 159 | } 160 | } 161 | 162 | // MARK: Equatable 163 | 164 | public static func ==(lhs: StyledTextBuilder, rhs: StyledTextBuilder) -> Bool { 165 | return lhs === rhs 166 | || lhs.styledTexts == rhs.styledTexts 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /Tests/TextStyleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextStyleTests.swift 3 | // StyledTextKitTests 4 | // 5 | // Created by Ryan Nystrom on 12/12/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import StyledTextKit 11 | 12 | class TextStyleTests: XCTestCase { 13 | 14 | func test_initializersPassed() { 15 | let style = TextStyle( 16 | font: .name("name"), 17 | size: 12, 18 | attributes: [.foregroundColor: UIColor.white], 19 | minSize: 11, 20 | maxSize: 13 21 | ) 22 | XCTAssertEqual(style.size, 12) 23 | XCTAssertEqual(style.attributes[.foregroundColor] as! UIColor, UIColor.white) 24 | XCTAssertEqual(style.minSize, 11) 25 | XCTAssertEqual(style.maxSize, 13) 26 | switch style.font { 27 | case .name(let name): XCTAssertEqual(name, "name") 28 | default: XCTFail() 29 | } 30 | } 31 | 32 | func test_whenObjectsSame_thatHashHits() { 33 | XCTAssertEqual( 34 | TextStyle( 35 | font: .name("name"), 36 | size: 12, 37 | attributes: [.foregroundColor: UIColor.white], 38 | minSize: 11, 39 | maxSize: 13 40 | ).hashValue, 41 | TextStyle( 42 | font: .name("name"), 43 | size: 12, 44 | attributes: [.foregroundColor: UIColor.white], 45 | minSize: 11, 46 | maxSize: 13 47 | ).hashValue 48 | ) 49 | } 50 | 51 | func test_whenNameDiffers_thatHashMisses() { 52 | XCTAssertNotEqual( 53 | TextStyle( 54 | size: 12, 55 | attributes: [.foregroundColor: UIColor.white], 56 | minSize: 11, 57 | maxSize: 13 58 | ).hashValue, 59 | TextStyle( 60 | font: .name("name"), 61 | size: 12, 62 | attributes: [.foregroundColor: UIColor.white], 63 | minSize: 11, 64 | maxSize: 13 65 | ).hashValue 66 | ) 67 | } 68 | 69 | func test_whenSizeDiffers_thatHashMisses() { 70 | XCTAssertNotEqual( 71 | TextStyle( 72 | font: .name("name"), 73 | size: 12, 74 | attributes: [.foregroundColor: UIColor.white], 75 | minSize: 11, 76 | maxSize: 13 77 | ).hashValue, 78 | TextStyle( 79 | font: .name("name"), 80 | size: 13, 81 | attributes: [.foregroundColor: UIColor.white], 82 | minSize: 11, 83 | maxSize: 13 84 | ).hashValue 85 | ) 86 | } 87 | 88 | func test_whenAttributeCountsDiffer_thatHashMisses() { 89 | XCTAssertNotEqual( 90 | TextStyle( 91 | font: .name("name"), 92 | size: 12, 93 | attributes: [.foregroundColor: UIColor.white], 94 | minSize: 11, 95 | maxSize: 13 96 | ).hashValue, 97 | TextStyle( 98 | font: .name("name"), 99 | size: 12, 100 | attributes: [.foregroundColor: UIColor.white, .backgroundColor: UIColor.black], 101 | minSize: 11, 102 | maxSize: 13 103 | ).hashValue 104 | ) 105 | } 106 | 107 | func test_whenAttributesDiffer_thatNotEqual() { 108 | XCTAssertNotEqual( 109 | TextStyle( 110 | font: .name("name"), 111 | size: 12, 112 | attributes: [.foregroundColor: UIColor.white], 113 | minSize: 11, 114 | maxSize: 13 115 | ), 116 | TextStyle( 117 | font: .name("name"), 118 | size: 12, 119 | attributes: [.foregroundColor: UIColor.black], 120 | minSize: 11, 121 | maxSize: 13 122 | ) 123 | ) 124 | } 125 | 126 | func test_whenTypesDiffer_thatHashMisses() { 127 | XCTAssertNotEqual( 128 | TextStyle( 129 | font: .name("name"), 130 | size: 12, 131 | attributes: [.foregroundColor: UIColor.white], 132 | minSize: 11, 133 | maxSize: 13 134 | ).hashValue, 135 | TextStyle( 136 | font: .system(.default), 137 | size: 12, 138 | attributes: [.foregroundColor: UIColor.white], 139 | minSize: 11, 140 | maxSize: 13 141 | ).hashValue 142 | ) 143 | } 144 | 145 | func test_whenMinSizeDiffers_thatHashMisses() { 146 | XCTAssertNotEqual( 147 | TextStyle( 148 | font: .name("name"), 149 | size: 12, 150 | attributes: [.foregroundColor: UIColor.white], 151 | minSize: 11, 152 | maxSize: 13 153 | ).hashValue, 154 | TextStyle( 155 | font: .name("name"), 156 | size: 12, 157 | attributes: [.foregroundColor: UIColor.white], 158 | minSize: 12, 159 | maxSize: 13 160 | ).hashValue 161 | ) 162 | } 163 | 164 | func test_whenMaxSizeDiffers_thatHashMisses() { 165 | XCTAssertNotEqual( 166 | TextStyle( 167 | font: .name("name"), 168 | size: 12, 169 | attributes: [.foregroundColor: UIColor.white], 170 | minSize: 11, 171 | maxSize: 13 172 | ).hashValue, 173 | TextStyle( 174 | font: .name("name"), 175 | size: 12, 176 | attributes: [.foregroundColor: UIColor.white], 177 | minSize: 11, 178 | maxSize: 14 179 | ).hashValue 180 | ) 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Example/Pods-Example-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH+x} ]; then 7 | # If UNLOCALIZED_RESOURCES_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # resources to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 13 | 14 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 15 | > "$RESOURCES_TO_COPY" 16 | 17 | XCASSET_FILES=() 18 | 19 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 20 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 21 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 22 | 23 | case "${TARGETED_DEVICE_FAMILY:-}" in 24 | 1,2) 25 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 26 | ;; 27 | 1) 28 | TARGET_DEVICE_ARGS="--target-device iphone" 29 | ;; 30 | 2) 31 | TARGET_DEVICE_ARGS="--target-device ipad" 32 | ;; 33 | 3) 34 | TARGET_DEVICE_ARGS="--target-device tv" 35 | ;; 36 | 4) 37 | TARGET_DEVICE_ARGS="--target-device watch" 38 | ;; 39 | *) 40 | TARGET_DEVICE_ARGS="--target-device mac" 41 | ;; 42 | esac 43 | 44 | install_resource() 45 | { 46 | if [[ "$1" = /* ]] ; then 47 | RESOURCE_PATH="$1" 48 | else 49 | RESOURCE_PATH="${PODS_ROOT}/$1" 50 | fi 51 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 52 | cat << EOM 53 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 54 | EOM 55 | exit 1 56 | fi 57 | case $RESOURCE_PATH in 58 | *.storyboard) 59 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 60 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 61 | ;; 62 | *.xib) 63 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 64 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 65 | ;; 66 | *.framework) 67 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 68 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 69 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 70 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 71 | ;; 72 | *.xcdatamodel) 73 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true 74 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 75 | ;; 76 | *.xcdatamodeld) 77 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true 78 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 79 | ;; 80 | *.xcmappingmodel) 81 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true 82 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 83 | ;; 84 | *.xcassets) 85 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 86 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 87 | ;; 88 | *) 89 | echo "$RESOURCE_PATH" || true 90 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 91 | ;; 92 | esac 93 | } 94 | 95 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 97 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 98 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 99 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 100 | fi 101 | rm -f "$RESOURCES_TO_COPY" 102 | 103 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ] 104 | then 105 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 106 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 107 | while read line; do 108 | if [[ $line != "${PODS_ROOT}*" ]]; then 109 | XCASSET_FILES+=("$line") 110 | fi 111 | done <<<"$OTHER_XCASSETS" 112 | 113 | if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then 114 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 115 | else 116 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${TARGET_BUILD_DIR}/assetcatalog_generated_info.plist" 117 | fi 118 | fi 119 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Pods-Tests-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH+x} ]; then 7 | # If UNLOCALIZED_RESOURCES_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # resources to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 13 | 14 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 15 | > "$RESOURCES_TO_COPY" 16 | 17 | XCASSET_FILES=() 18 | 19 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 20 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 21 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 22 | 23 | case "${TARGETED_DEVICE_FAMILY:-}" in 24 | 1,2) 25 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 26 | ;; 27 | 1) 28 | TARGET_DEVICE_ARGS="--target-device iphone" 29 | ;; 30 | 2) 31 | TARGET_DEVICE_ARGS="--target-device ipad" 32 | ;; 33 | 3) 34 | TARGET_DEVICE_ARGS="--target-device tv" 35 | ;; 36 | 4) 37 | TARGET_DEVICE_ARGS="--target-device watch" 38 | ;; 39 | *) 40 | TARGET_DEVICE_ARGS="--target-device mac" 41 | ;; 42 | esac 43 | 44 | install_resource() 45 | { 46 | if [[ "$1" = /* ]] ; then 47 | RESOURCE_PATH="$1" 48 | else 49 | RESOURCE_PATH="${PODS_ROOT}/$1" 50 | fi 51 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 52 | cat << EOM 53 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 54 | EOM 55 | exit 1 56 | fi 57 | case $RESOURCE_PATH in 58 | *.storyboard) 59 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 60 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 61 | ;; 62 | *.xib) 63 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 64 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 65 | ;; 66 | *.framework) 67 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 68 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 69 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 70 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 71 | ;; 72 | *.xcdatamodel) 73 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true 74 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 75 | ;; 76 | *.xcdatamodeld) 77 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true 78 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 79 | ;; 80 | *.xcmappingmodel) 81 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true 82 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 83 | ;; 84 | *.xcassets) 85 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 86 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 87 | ;; 88 | *) 89 | echo "$RESOURCE_PATH" || true 90 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 91 | ;; 92 | esac 93 | } 94 | 95 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 97 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 98 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 99 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 100 | fi 101 | rm -f "$RESOURCES_TO_COPY" 102 | 103 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ] 104 | then 105 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 106 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 107 | while read line; do 108 | if [[ $line != "${PODS_ROOT}*" ]]; then 109 | XCASSET_FILES+=("$line") 110 | fi 111 | done <<<"$OTHER_XCASSETS" 112 | 113 | if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then 114 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 115 | else 116 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist" 117 | fi 118 | fi 119 | -------------------------------------------------------------------------------- /Tests/LRUCacheTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LRUCacheTests.swift 3 | // StyledTextKitTests 4 | // 5 | // Created by Ryan Nystrom on 12/13/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import StyledTextKit 11 | 12 | struct TestValue: LRUCachable { 13 | let size: Int 14 | let value: String 15 | var cachedSize: Int { 16 | return size 17 | } 18 | } 19 | 20 | class LRUCacheTests: XCTestCase { 21 | 22 | func test_whenInitializingCache_thatMaxSizeSet() { 23 | let cache = LRUCache(maxSize: 10) 24 | XCTAssertEqual(cache.maxSize, 10) 25 | } 26 | 27 | func test_whenGettingFromEmptyCache_thatNilReturned() { 28 | let cache = LRUCache(maxSize: 10) 29 | XCTAssertNil(cache.get("foo")) 30 | } 31 | 32 | func test_whenSetting_gettingReturnsSameObject() { 33 | let cache = LRUCache(maxSize: 10) 34 | cache.set("foo", value: TestValue(size: 1, value: "cat")) 35 | XCTAssertEqual(cache.get("foo")?.value, "cat") 36 | } 37 | 38 | func test_whenSettingMultipleTimes_thatHeadBumped() { 39 | let cache = LRUCache(maxSize: 10) 40 | 41 | cache.set("foo", value: TestValue(size: 1, value: "cat")) 42 | cache.set("bar", value: TestValue(size: 1, value: "dog")) 43 | cache.set("baz", value: TestValue(size: 3, value: "rat")) 44 | cache.set("bang", value: TestValue(size: 1, value: "bug")) 45 | 46 | XCTAssertEqual(cache.map.count, 4) 47 | XCTAssertEqual(cache.size, 6) 48 | XCTAssertEqual(cache.head?.value.value, "bug") 49 | XCTAssertEqual(cache.head?.key, "bang") 50 | 51 | XCTAssertEqual(cache.get("foo")?.value, "cat") 52 | 53 | XCTAssertEqual(cache.map.count, 4) 54 | XCTAssertEqual(cache.head?.value.value, "cat") 55 | XCTAssertEqual(cache.head?.key, "foo") 56 | } 57 | 58 | func test_whenExceedingMaxSize_withDefaultCompaction_thatCachePurged() { 59 | let cache = LRUCache(maxSize: 10) 60 | 61 | cache.set("foo", value: TestValue(size: 1, value: "cat")) 62 | cache.set("bar", value: TestValue(size: 2, value: "dog")) 63 | cache.set("baz", value: TestValue(size: 3, value: "rat")) 64 | cache.set("bang", value: TestValue(size: 4, value: "bug")) 65 | 66 | XCTAssertEqual(cache.map.count, 4) 67 | XCTAssertEqual(cache.size, 10) 68 | 69 | cache.set("bop", value: TestValue(size: 3, value: "pig")) 70 | 71 | XCTAssertEqual(cache.map.count, 3) 72 | XCTAssertEqual(cache.size, 10) 73 | XCTAssertEqual(cache.head?.value.value, "pig") 74 | XCTAssertEqual(cache.head?.key, "bop") 75 | } 76 | 77 | func test_whenPercentCompactionOOB_thatInitWithDefault() { 78 | let zeroCompact = LRUCache(maxSize: 10, compaction: .percent(0)).compaction 79 | switch zeroCompact { 80 | case .percent: 81 | XCTFail() 82 | default: break 83 | } 84 | 85 | let negativeCompact = LRUCache(maxSize: 10, compaction: .percent(-1)).compaction 86 | switch negativeCompact { 87 | case .percent: 88 | XCTFail() 89 | default: break 90 | } 91 | 92 | let tooBigCompact = LRUCache(maxSize: 10, compaction: .percent(1.1)).compaction 93 | switch tooBigCompact { 94 | case .percent: 95 | XCTFail() 96 | default: break 97 | } 98 | } 99 | 100 | func test_whenExceedingMaxSize_with50PercentCompaction_thatCachePurged() { 101 | let cache = LRUCache(maxSize: 10, compaction: .percent(0.5)) 102 | 103 | cache.set("foo", value: TestValue(size: 4, value: "cat")) 104 | cache.set("bar", value: TestValue(size: 3, value: "dog")) 105 | cache.set("baz", value: TestValue(size: 3, value: "rat")) 106 | 107 | cache.set("bang", value: TestValue(size: 3, value: "bug")) 108 | 109 | XCTAssertEqual(cache.map.count, 1) 110 | XCTAssertEqual(cache.size, 3) 111 | XCTAssertEqual(cache.head?.value.value, "bug") 112 | XCTAssertEqual(cache.head?.key, "bang") 113 | } 114 | 115 | func test_whenOverwritingExistingKey_thatSizeUpdates() { 116 | let cache = LRUCache(maxSize: 10) 117 | 118 | cache.set("foo", value: TestValue(size: 1, value: "cat")) 119 | XCTAssertEqual(cache.map.count, 1) 120 | XCTAssertEqual(cache.size, 1) 121 | XCTAssertEqual(cache.get("foo")?.value, "cat") 122 | 123 | cache.set("foo", value: TestValue(size: 3, value: "dog")) 124 | XCTAssertEqual(cache.map.count, 1) 125 | XCTAssertEqual(cache.size, 3) 126 | XCTAssertEqual(cache.get("foo")?.value, "dog") 127 | } 128 | 129 | func test_whenClearingCache_thatValuesReset() { 130 | let cache = LRUCache(maxSize: 10) 131 | 132 | cache.set("foo", value: TestValue(size: 4, value: "cat")) 133 | cache.set("bar", value: TestValue(size: 3, value: "dog")) 134 | cache.set("baz", value: TestValue(size: 3, value: "rat")) 135 | 136 | XCTAssertEqual(cache.map.count, 3) 137 | XCTAssertEqual(cache.size, 10) 138 | 139 | cache.clear() 140 | 141 | XCTAssertEqual(cache.map.count, 0) 142 | XCTAssertEqual(cache.size, 0) 143 | XCTAssertNil(cache.head) 144 | } 145 | 146 | func test_whenConfiguredForMemoryWarning_thatCacheClears() { 147 | let cache = LRUCache(maxSize: 10, clearOnWarning: true) 148 | 149 | cache.set("foo", value: TestValue(size: 4, value: "cat")) 150 | cache.set("bar", value: TestValue(size: 3, value: "dog")) 151 | cache.set("baz", value: TestValue(size: 3, value: "rat")) 152 | 153 | XCTAssertEqual(cache.map.count, 3) 154 | XCTAssertEqual(cache.size, 10) 155 | 156 | NotificationCenter.default.post(Notification(name: UIApplication.didReceiveMemoryWarningNotification)) 157 | 158 | XCTAssertEqual(cache.map.count, 0) 159 | XCTAssertEqual(cache.size, 0) 160 | XCTAssertNil(cache.head) 161 | } 162 | 163 | func test_whenNotForMemoryWarning_thatCacheUnchanged() { 164 | let cache = LRUCache(maxSize: 10, clearOnWarning: false) 165 | 166 | cache.set("foo", value: TestValue(size: 4, value: "cat")) 167 | cache.set("bar", value: TestValue(size: 3, value: "dog")) 168 | cache.set("baz", value: TestValue(size: 3, value: "rat")) 169 | 170 | XCTAssertEqual(cache.map.count, 3) 171 | XCTAssertEqual(cache.size, 10) 172 | 173 | NotificationCenter.default.post(Notification(name: UIApplication.didReceiveMemoryWarningNotification)) 174 | 175 | XCTAssertEqual(cache.map.count, 3) 176 | XCTAssertEqual(cache.size, 10) 177 | } 178 | 179 | func test_whenGettingExistingObject_thatItDoesntLoop() { 180 | let cache = LRUCache(maxSize: 10, clearOnWarning: false) 181 | 182 | cache.set("foo", value: TestValue(size: 4, value: "cat")) 183 | cache.set("bar", value: TestValue(size: 3, value: "dog")) 184 | 185 | let _ = cache.get("foo") 186 | 187 | XCTAssertNotNil(cache.head?.tail) 188 | XCTAssertEqual(cache.head?.key, "foo") 189 | XCTAssertEqual(cache.head?.next?.key, "bar") 190 | XCTAssertNotNil(cache.head?.tail?.key, "bar") 191 | XCTAssertNil(cache.head?.next?.next) 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /Source/StyledTextRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyledTextKitRenderer.swift 3 | // StyledTextKit 4 | // 5 | // Created by Ryan Nystrom on 12/13/17. 6 | // Copyright © 2017 Ryan Nystrom. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class StyledTextRenderer { 12 | 13 | internal let layoutManager: NSLayoutManager 14 | internal let textContainer: NSTextContainer 15 | 16 | public let scale: CGFloat 17 | public let inset: UIEdgeInsets 18 | public let string: StyledTextString 19 | public let backgroundColor: UIColor? 20 | 21 | private var map = [UIContentSizeCategory: NSTextStorage]() 22 | private var lock = os_unfair_lock_s() 23 | private var contentSizeCategory: UIContentSizeCategory 24 | 25 | internal static let globalSizeCache = LRUCache( 26 | maxSize: 1000, // CGSize cache size always 1, treat as item count 27 | compaction: .default, 28 | clearOnWarning: true 29 | ) 30 | 31 | internal static let globalBitmapCache = LRUCache( 32 | maxSize: 1024 * 1024 * 20, // 20mb 33 | compaction: .default, 34 | clearOnWarning: true 35 | ) 36 | 37 | internal let sizeCache: LRUCache 38 | internal let bitmapCache: LRUCache 39 | 40 | public convenience init( 41 | string: StyledTextString, 42 | contentSizeCategory: UIContentSizeCategory, 43 | inset: UIEdgeInsets = .zero, 44 | backgroundColor: UIColor? = nil, 45 | layoutManager: NSLayoutManager = NSLayoutManager(), 46 | scale: CGFloat = StyledTextScreenScale, 47 | maximumNumberOfLines: Int = 0 48 | ) { 49 | self.init( 50 | string: string, 51 | contentSizeCategory: contentSizeCategory, 52 | inset: inset, 53 | backgroundColor: backgroundColor, 54 | layoutManager: layoutManager, 55 | scale: scale, 56 | maximumNumberOfLines: maximumNumberOfLines, 57 | sizeCache: nil, 58 | bitmapCache: nil 59 | ) 60 | } 61 | 62 | internal init( 63 | string: StyledTextString, 64 | contentSizeCategory: UIContentSizeCategory, 65 | inset: UIEdgeInsets, 66 | backgroundColor: UIColor?, 67 | layoutManager: NSLayoutManager, 68 | scale: CGFloat, 69 | maximumNumberOfLines: Int, 70 | sizeCache: LRUCache?, 71 | bitmapCache: LRUCache? 72 | ) { 73 | self.string = string 74 | self.contentSizeCategory = contentSizeCategory 75 | self.inset = inset 76 | self.backgroundColor = backgroundColor 77 | self.scale = scale 78 | self.sizeCache = sizeCache ?? StyledTextRenderer.globalSizeCache 79 | self.bitmapCache = bitmapCache ?? StyledTextRenderer.globalBitmapCache 80 | 81 | textContainer = NSTextContainer() 82 | textContainer.exclusionPaths = [] 83 | textContainer.maximumNumberOfLines = maximumNumberOfLines 84 | textContainer.lineFragmentPadding = 0 85 | 86 | self.layoutManager = layoutManager 87 | layoutManager.allowsNonContiguousLayout = false 88 | layoutManager.hyphenationFactor = 0 89 | layoutManager.showsInvisibleCharacters = false 90 | layoutManager.showsControlCharacters = false 91 | layoutManager.usesFontLeading = true 92 | layoutManager.addTextContainer(textContainer) 93 | } 94 | 95 | internal var storage: NSTextStorage { 96 | if let storage = map[contentSizeCategory] { 97 | return storage 98 | } 99 | let storage = NSTextStorage(attributedString: string.render(contentSizeCategory: contentSizeCategory)) 100 | storage.addLayoutManager(layoutManager) 101 | map[contentSizeCategory] = storage 102 | return storage 103 | } 104 | 105 | // not thread safe 106 | private func _size(_ key: StyledTextRenderCacheKey) -> CGSize { 107 | if let cached = sizeCache[key] { 108 | // always update the container to requested size 109 | textContainer.size = cached 110 | return cached 111 | } 112 | let insetWidth = max(key.width - inset.left - inset.right, 0) 113 | let size = layoutManager.size(textContainer: textContainer, width: insetWidth, scale: scale) 114 | sizeCache[key] = size 115 | return size 116 | } 117 | 118 | public func size(in width: CGFloat = .greatestFiniteMagnitude) -> CGSize { 119 | os_unfair_lock_lock(&lock) 120 | defer { os_unfair_lock_unlock(&lock) } 121 | return _size(StyledTextRenderCacheKey(width: width, attributedText: storage, backgroundColor: backgroundColor, maximumNumberOfLines: textContainer.maximumNumberOfLines)) 122 | } 123 | 124 | public func viewSize(in width: CGFloat = .greatestFiniteMagnitude) -> CGSize { 125 | return size(in: width).resized(inset: inset) 126 | } 127 | 128 | public func cachedRender(for width: CGFloat) -> (image: CGImage?, size: CGSize?) { 129 | os_unfair_lock_lock(&lock) 130 | defer { os_unfair_lock_unlock(&lock) } 131 | 132 | let key = StyledTextRenderCacheKey( 133 | width: width, 134 | attributedText: storage, 135 | backgroundColor: backgroundColor, 136 | maximumNumberOfLines: 0 137 | ) 138 | return (bitmapCache[key], sizeCache[key]) 139 | } 140 | 141 | public func render(for width: CGFloat) -> (image: CGImage?, size: CGSize) { 142 | os_unfair_lock_lock(&lock) 143 | defer { os_unfair_lock_unlock(&lock) } 144 | 145 | let key = StyledTextRenderCacheKey(width: width, attributedText: storage, backgroundColor: backgroundColor, maximumNumberOfLines: textContainer.maximumNumberOfLines) 146 | let size = _size(key) 147 | let cache = bitmapCache 148 | if let cached = cache[key] { 149 | return (cached, size) 150 | } 151 | 152 | let contents = layoutManager.render( 153 | size: size, 154 | textContainer: textContainer, 155 | scale: scale, 156 | backgroundColor: backgroundColor 157 | ) 158 | cache[key] = contents 159 | return (contents, size) 160 | } 161 | 162 | public func attributes(at point: CGPoint) -> (attributes: NSAttributedStringAttributesType, index: Int)? { 163 | os_unfair_lock_lock(&lock) 164 | defer { os_unfair_lock_unlock(&lock) } 165 | var fractionDistance: CGFloat = 1.0 166 | let index = layoutManager.characterIndex( 167 | for: point, 168 | in: textContainer, 169 | fractionOfDistanceBetweenInsertionPoints: &fractionDistance 170 | ) 171 | if index != NSNotFound, 172 | fractionDistance < 1.0, 173 | let attributes = layoutManager.textStorage?.attributes(at: index, effectiveRange: nil) as? NSAttributedStringAttributesType { 174 | return (attributes, index) 175 | } 176 | return nil 177 | } 178 | 179 | public enum WarmOption { 180 | case size 181 | case bitmap 182 | } 183 | 184 | public func warm( 185 | _ option: WarmOption = .size, 186 | width: CGFloat 187 | ) -> StyledTextRenderer { 188 | switch option { 189 | case .size: let _ = size(in: width) 190 | case .bitmap: let _ = render(for: width) 191 | } 192 | return self 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-Example/Pods-Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 7 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # frameworks to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 13 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 14 | 15 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 16 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 17 | 18 | # Used as a return value for each invocation of `strip_invalid_archs` function. 19 | STRIP_BINARY_RETVAL=0 20 | 21 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 22 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 23 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 24 | 25 | # Copies and strips a vendored framework 26 | install_framework() 27 | { 28 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 29 | local source="${BUILT_PRODUCTS_DIR}/$1" 30 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 31 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 32 | elif [ -r "$1" ]; then 33 | local source="$1" 34 | fi 35 | 36 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 37 | 38 | if [ -L "${source}" ]; then 39 | echo "Symlinked..." 40 | source="$(readlink "${source}")" 41 | fi 42 | 43 | # Use filter instead of exclude so missing patterns don't throw errors. 44 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 45 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 46 | 47 | local basename 48 | basename="$(basename -s .framework "$1")" 49 | binary="${destination}/${basename}.framework/${basename}" 50 | if ! [ -r "$binary" ]; then 51 | binary="${destination}/${basename}" 52 | fi 53 | 54 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 55 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 56 | strip_invalid_archs "$binary" 57 | fi 58 | 59 | # Resign the code if required by the build settings to avoid unstable apps 60 | code_sign_if_enabled "${destination}/$(basename "$1")" 61 | 62 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 63 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 64 | local swift_runtime_libs 65 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) 66 | for lib in $swift_runtime_libs; do 67 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 68 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 69 | code_sign_if_enabled "${destination}/${lib}" 70 | done 71 | fi 72 | } 73 | 74 | # Copies and strips a vendored dSYM 75 | install_dsym() { 76 | local source="$1" 77 | if [ -r "$source" ]; then 78 | # Copy the dSYM into a the targets temp dir. 79 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 80 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 81 | 82 | local basename 83 | basename="$(basename -s .framework.dSYM "$source")" 84 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 85 | 86 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 87 | if [[ "$(file "$binary")" == *"Mach-O dSYM companion"* ]]; then 88 | strip_invalid_archs "$binary" 89 | fi 90 | 91 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 92 | # Move the stripped file into its final destination. 93 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 94 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 95 | else 96 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 97 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 98 | fi 99 | fi 100 | } 101 | 102 | # Signs a framework with the provided identity 103 | code_sign_if_enabled() { 104 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 105 | # Use the current code_sign_identitiy 106 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 107 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 108 | 109 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 110 | code_sign_cmd="$code_sign_cmd &" 111 | fi 112 | echo "$code_sign_cmd" 113 | eval "$code_sign_cmd" 114 | fi 115 | } 116 | 117 | # Strip invalid architectures 118 | strip_invalid_archs() { 119 | binary="$1" 120 | # Get architectures for current target binary 121 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 122 | # Intersect them with the architectures we are building for 123 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 124 | # If there are no archs supported by this binary then warn the user 125 | if [[ -z "$intersected_archs" ]]; then 126 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 127 | STRIP_BINARY_RETVAL=0 128 | return 129 | fi 130 | stripped="" 131 | for arch in $binary_archs; do 132 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 133 | # Strip non-valid architectures in-place 134 | lipo -remove "$arch" -output "$binary" "$binary" || exit 1 135 | stripped="$stripped $arch" 136 | fi 137 | done 138 | if [[ "$stripped" ]]; then 139 | echo "Stripped $binary of architectures:$stripped" 140 | fi 141 | STRIP_BINARY_RETVAL=1 142 | } 143 | 144 | 145 | if [[ "$CONFIGURATION" == "Debug" ]]; then 146 | install_framework "${BUILT_PRODUCTS_DIR}/StyledTextKit/StyledTextKit.framework" 147 | fi 148 | if [[ "$CONFIGURATION" == "Release" ]]; then 149 | install_framework "${BUILT_PRODUCTS_DIR}/StyledTextKit/StyledTextKit.framework" 150 | fi 151 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 152 | wait 153 | fi 154 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Tests/Pods-Tests-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | 23 | # Used as a return value for each invocation of `strip_invalid_archs` function. 24 | STRIP_BINARY_RETVAL=0 25 | 26 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 27 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 28 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 29 | 30 | # Copies and strips a vendored framework 31 | install_framework() 32 | { 33 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 34 | local source="${BUILT_PRODUCTS_DIR}/$1" 35 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 36 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 37 | elif [ -r "$1" ]; then 38 | local source="$1" 39 | fi 40 | 41 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 42 | 43 | if [ -L "${source}" ]; then 44 | echo "Symlinked..." 45 | source="$(readlink "${source}")" 46 | fi 47 | 48 | # Use filter instead of exclude so missing patterns don't throw errors. 49 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 50 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 51 | 52 | local basename 53 | basename="$(basename -s .framework "$1")" 54 | binary="${destination}/${basename}.framework/${basename}" 55 | 56 | if ! [ -r "$binary" ]; then 57 | binary="${destination}/${basename}" 58 | elif [ -L "${binary}" ]; then 59 | echo "Destination binary is symlinked..." 60 | dirname="$(dirname "${binary}")" 61 | binary="${dirname}/$(readlink "${binary}")" 62 | fi 63 | 64 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 65 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 66 | strip_invalid_archs "$binary" 67 | fi 68 | 69 | # Resign the code if required by the build settings to avoid unstable apps 70 | code_sign_if_enabled "${destination}/$(basename "$1")" 71 | 72 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 73 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 74 | local swift_runtime_libs 75 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 76 | for lib in $swift_runtime_libs; do 77 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 78 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 79 | code_sign_if_enabled "${destination}/${lib}" 80 | done 81 | fi 82 | } 83 | 84 | # Copies and strips a vendored dSYM 85 | install_dsym() { 86 | local source="$1" 87 | if [ -r "$source" ]; then 88 | # Copy the dSYM into a the targets temp dir. 89 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 90 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 91 | 92 | local basename 93 | basename="$(basename -s .framework.dSYM "$source")" 94 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 95 | 96 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 97 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 98 | strip_invalid_archs "$binary" 99 | fi 100 | 101 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 102 | # Move the stripped file into its final destination. 103 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 104 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 105 | else 106 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 107 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 108 | fi 109 | fi 110 | } 111 | 112 | # Copies the bcsymbolmap files of a vendored framework 113 | install_bcsymbolmap() { 114 | local bcsymbolmap_path="$1" 115 | local destination="${BUILT_PRODUCTS_DIR}" 116 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 117 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 118 | } 119 | 120 | # Signs a framework with the provided identity 121 | code_sign_if_enabled() { 122 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 123 | # Use the current code_sign_identity 124 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 125 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 126 | 127 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 128 | code_sign_cmd="$code_sign_cmd &" 129 | fi 130 | echo "$code_sign_cmd" 131 | eval "$code_sign_cmd" 132 | fi 133 | } 134 | 135 | # Strip invalid architectures 136 | strip_invalid_archs() { 137 | binary="$1" 138 | # Get architectures for current target binary 139 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 140 | # Intersect them with the architectures we are building for 141 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 142 | # If there are no archs supported by this binary then warn the user 143 | if [[ -z "$intersected_archs" ]]; then 144 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 145 | STRIP_BINARY_RETVAL=0 146 | return 147 | fi 148 | stripped="" 149 | for arch in $binary_archs; do 150 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 151 | # Strip non-valid architectures in-place 152 | lipo -remove "$arch" -output "$binary" "$binary" 153 | stripped="$stripped $arch" 154 | fi 155 | done 156 | if [[ "$stripped" ]]; then 157 | echo "Stripped $binary of architectures:$stripped" 158 | fi 159 | STRIP_BINARY_RETVAL=1 160 | } 161 | 162 | 163 | if [[ "$CONFIGURATION" == "Debug" ]]; then 164 | install_framework "${BUILT_PRODUCTS_DIR}/iOSSnapshotTestCase/FBSnapshotTestCase.framework" 165 | fi 166 | if [[ "$CONFIGURATION" == "Release" ]]; then 167 | install_framework "${BUILT_PRODUCTS_DIR}/iOSSnapshotTestCase/FBSnapshotTestCase.framework" 168 | fi 169 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 170 | wait 171 | fi 172 | --------------------------------------------------------------------------------