├── .gitignore
├── iOS
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── AppDelegate.swift
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Info.plist
└── SceneDelegate.swift
├── macOS
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── macOS.entitlements
├── AppDelegate.swift
├── Info.plist
└── Base.lproj
│ └── Main.storyboard
├── Extensions.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── project.pbxproj
├── LICENSE
└── Extensions
├── Shared
├── Extensions-Geometry.swift
└── Extensions-Shared.swift
├── iOS
└── Extensions-iOS.swift
└── macOS
└── Extensions-macOS.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | xcuserdata
2 |
--------------------------------------------------------------------------------
/iOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/macOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Extensions.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/iOS/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/macOS/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Extensions.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/macOS/macOS.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/macOS/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // macOS
4 | //
5 | // Created by Tyler Hall on 10/11/20.
6 | //
7 |
8 | import Cocoa
9 |
10 | @NSApplicationMain
11 | class AppDelegate: NSObject, NSApplicationDelegate {
12 |
13 |
14 |
15 |
16 | func applicationDidFinishLaunching(_ aNotification: Notification) {
17 | // Insert code here to initialize your application
18 | }
19 |
20 | func applicationWillTerminate(_ aNotification: Notification) {
21 | // Insert code here to tear down your application
22 | }
23 |
24 |
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/macOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSMainStoryboardFile
26 | Main
27 | NSPrincipalClass
28 | NSApplication
29 |
30 |
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Tyler Hall
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "author" : "xcode",
56 | "version" : 1
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/iOS/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // iOS
4 | //
5 | // Created by Tyler Hall on 10/11/20.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 |
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | // MARK: UISceneSession Lifecycle
21 |
22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
23 | // Called when a new scene session is being created.
24 | // Use this method to select a configuration to create the new scene with.
25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
26 | }
27 |
28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
29 | // Called when the user discards a scene session.
30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
32 | }
33 |
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/iOS/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 |
--------------------------------------------------------------------------------
/iOS/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 |
32 |
33 |
--------------------------------------------------------------------------------
/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 | UISceneStoryboardFile
37 | Main
38 |
39 |
40 |
41 |
42 | UIApplicationSupportsIndirectInputEvents
43 |
44 | UILaunchStoryboardName
45 | LaunchScreen
46 | UIMainStoryboardFile
47 | Main
48 | UIRequiredDeviceCapabilities
49 |
50 | armv7
51 |
52 | UISupportedInterfaceOrientations
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationLandscapeLeft
56 | UIInterfaceOrientationLandscapeRight
57 |
58 | UISupportedInterfaceOrientations~ipad
59 |
60 | UIInterfaceOrientationPortrait
61 | UIInterfaceOrientationPortraitUpsideDown
62 | UIInterfaceOrientationLandscapeLeft
63 | UIInterfaceOrientationLandscapeRight
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/iOS/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // iOS
4 | //
5 | // Created by Tyler Hall on 10/11/20.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
19 | guard let _ = (scene as? UIWindowScene) else { return }
20 | }
21 |
22 | func sceneDidDisconnect(_ scene: UIScene) {
23 | // Called as the scene is being released by the system.
24 | // This occurs shortly after the scene enters the background, or when its session is discarded.
25 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
27 | }
28 |
29 | func sceneDidBecomeActive(_ scene: UIScene) {
30 | // Called when the scene has moved from an inactive state to an active state.
31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
32 | }
33 |
34 | func sceneWillResignActive(_ scene: UIScene) {
35 | // Called when the scene will move from an active state to an inactive state.
36 | // This may occur due to temporary interruptions (ex. an incoming phone call).
37 | }
38 |
39 | func sceneWillEnterForeground(_ scene: UIScene) {
40 | // Called as the scene transitions from the background to the foreground.
41 | // Use this method to undo the changes made on entering the background.
42 | }
43 |
44 | func sceneDidEnterBackground(_ scene: UIScene) {
45 | // Called as the scene transitions from the foreground to the background.
46 | // Use this method to save data, release shared resources, and store enough scene-specific state information
47 | // to restore the scene back to its current state.
48 | }
49 |
50 |
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/Extensions/Shared/Extensions-Geometry.swift:
--------------------------------------------------------------------------------
1 | import CoreGraphics
2 |
3 | extension CGPoint {
4 |
5 | func scaled(_ size: CGSize) -> CGPoint {
6 | return CGPoint(x: x * size.width, y: y * size.height)
7 | }
8 |
9 | func offsetBy(_ dx: CGFloat, dy: CGFloat) -> CGPoint {
10 | return CGPoint(x: x + dx, y: y + dy)
11 | }
12 |
13 | func scaledAroundCenterPoint(_ scale: CGFloat, centerPoint: CGPoint) -> CGPoint {
14 | let xNew = (scale * (x - centerPoint.x)) + centerPoint.x
15 | let yNew = (scale * (y - centerPoint.y)) + centerPoint.y
16 | return CGPoint(x: xNew, y: yNew)
17 | }
18 |
19 | func rotatedAroundCenterPoint(_ radians: CGFloat, centerPoint: CGPoint) -> CGPoint {
20 | let s = sin(radians)
21 | let c = cos(radians)
22 |
23 | var px = self.x - centerPoint.x
24 | var py = self.y - centerPoint.y
25 |
26 | let newX = px * c - py * s
27 | let newY = px * s + py * c
28 |
29 | px = newX + centerPoint.x
30 | py = newY + centerPoint.y
31 |
32 | return CGPoint(x: px, y: py)
33 | }
34 | }
35 |
36 | extension CGFloat {
37 | var valueOrZero: CGFloat {
38 | return CGFloat.maximum(0, self)
39 | }
40 | }
41 |
42 | extension Array where Element == CGPoint {
43 |
44 | func pointsAdjacentTo(index: Int) -> (Int, Int)? {
45 | guard count >= 3 else { return nil }
46 |
47 | if index == 0 {
48 | return (count - 1, 1)
49 | } else if index == (count - 1) {
50 | return (index - 1, 0)
51 | } else {
52 | return (index - 1, index + 1)
53 | }
54 | }
55 |
56 | // This only works on arrays with 4 points
57 | func oppositeVertexIndex(vertexIndex: Int) -> Int {
58 | guard count == 4 else { fatalError() }
59 | switch vertexIndex {
60 | case 0:
61 | return 2
62 | case 1:
63 | return 3
64 | case 2:
65 | return 0
66 | case 3:
67 | return 1
68 | default:
69 | fatalError()
70 | }
71 | }
72 | }
73 |
74 | struct Line {
75 | var p1: CGPoint
76 | var p2: CGPoint
77 |
78 | var midPoint: CGPoint {
79 | return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2)
80 | }
81 |
82 | var length: CGFloat {
83 | return sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y))
84 | }
85 |
86 | var slope: CGFloat? {
87 | guard p2.x - p1.x != 0 else { return nil }
88 | return (p2.y - p1.y) / (p2.x - p1.x)
89 | }
90 |
91 | var yIntercept: CGFloat? {
92 | if let slope = slope {
93 | return p1.y - slope * p1.x
94 | }
95 | return nil
96 | }
97 |
98 | func pointAt(x: CGFloat) -> CGPoint? {
99 | if let slope = slope, let yIntercept = yIntercept {
100 | return CGPoint(x: x, y: slope * x + yIntercept)
101 | }
102 | return nil
103 | }
104 |
105 | func intercetionWith(line: Line) -> CGPoint? {
106 | // y = ax + c
107 | // y = bx + d
108 | // ax + c = bx + d
109 | // ax - bx = d - c
110 | // x = (d - c) / (a - b)
111 |
112 | guard let a = slope, let c = yIntercept else {
113 | return nil
114 | }
115 |
116 | guard let b = line.slope, let d = line.yIntercept else {
117 | return nil
118 | }
119 |
120 | // Check for parallel lines
121 | if a == b {
122 | return nil
123 | }
124 |
125 | if a - b == 0 {
126 | return nil
127 | }
128 |
129 | if d - c == 0 {
130 | return nil
131 | }
132 |
133 | let x = (d - c) / (a - b)
134 | let y = a * x + c
135 |
136 | let p = CGPoint(x: x, y: y)
137 | if hasXCoord(x: p.x) && hasYCoord(y: p.y) && line.hasXCoord(x: p.x) && line.hasYCoord(y: p.y) {
138 | return p
139 | }
140 |
141 | return nil
142 | }
143 |
144 | func hasXCoord(x: CGFloat) -> Bool {
145 | let xMin = min(p1.x, p2.x)
146 | let xMax = max(p1.x, p2.x)
147 | return (xMin <= x) && (x <= xMax)
148 | }
149 |
150 | func hasYCoord(y: CGFloat) -> Bool {
151 | let yMin = min(p1.y, p2.y)
152 | let yMax = max(p1.y, p2.y)
153 | return (yMin <= y) && (y <= yMax)
154 | }
155 |
156 | func hasPoint(point: CGPoint) -> Bool {
157 | return hasXCoord(x: point.x) && hasYCoord(y: point.y)
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/Extensions/iOS/Extensions-iOS.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIColor {
4 | public static func hex(_ str: String?) -> UIColor? {
5 | guard let hex = str, (hex.count == 6) || (hex.count == 7) else { return nil }
6 |
7 | var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
8 |
9 | if (cString.hasPrefix("#")) {
10 | cString.remove(at: cString.startIndex)
11 | }
12 |
13 | if ((cString.count) != 6) {
14 | return UIColor.gray
15 | }
16 |
17 | var rgbValue:UInt64 = 0
18 | Scanner(string: cString).scanHexInt64(&rgbValue)
19 |
20 | return UIColor(
21 | red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
22 | green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
23 | blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
24 | alpha: CGFloat(1.0)
25 | )
26 | }
27 | }
28 |
29 | extension UIBezierPath {
30 | var points: [CGPoint]? {
31 | var bezierPoints = NSMutableArray()
32 | let path = cgPath
33 | path.apply(info: &bezierPoints) { info, element in
34 | guard let resultingPoints = info?.assumingMemoryBound(to: NSMutableArray.self) else { return }
35 |
36 | let points = element.pointee.points
37 | let type = element.pointee.type
38 |
39 | switch type {
40 | case .moveToPoint:
41 | resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
42 |
43 | case .addLineToPoint:
44 | resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
45 |
46 | case .addQuadCurveToPoint:
47 | resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
48 | resultingPoints.pointee.add([NSNumber(value: Float(points[1].x)), NSNumber(value: Float(points[1].y))])
49 |
50 | case .addCurveToPoint:
51 | resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
52 | resultingPoints.pointee.add([NSNumber(value: Float(points[1].x)), NSNumber(value: Float(points[1].y))])
53 | resultingPoints.pointee.add([NSNumber(value: Float(points[2].x)), NSNumber(value: Float(points[2].y))])
54 |
55 | case .closeSubpath:
56 | break
57 | @unknown default:
58 | fatalError()
59 | }
60 | }
61 |
62 | var points = [CGPoint]()
63 | for p in bezierPoints {
64 | let arr = p as! [NSNumber]
65 | points.append(CGPoint(x: arr[0] as! CGFloat, y: arr[1] as! CGFloat))
66 | }
67 |
68 | return points
69 | }
70 |
71 | static func pathFromPoints(_ points: [CGPoint]) -> UIBezierPath? {
72 | guard let firstPoint = points.first else { return nil }
73 |
74 | let path = UIBezierPath()
75 |
76 | path.move(to: firstPoint)
77 |
78 | for i in 1.. UIBezierPath {
88 | let pixelRect = CGRect(origin: point, size: CGSize(width: width, height: width))
89 | return UIBezierPath(rect: pixelRect)
90 | }
91 | }
92 |
93 | extension UIImage {
94 | static func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
95 | guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil) else { return nil }
96 | guard let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { return nil }
97 |
98 | let context = CGContext(data: nil,
99 | width: Int(size.width),
100 | height: Int(size.height),
101 | bitsPerComponent: image.bitsPerComponent,
102 | bytesPerRow: image.bytesPerRow,
103 | space: CGColorSpaceCreateDeviceRGB(), // image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,
104 | bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)
105 | context?.interpolationQuality = .high
106 | context?.draw(image, in: CGRect(origin: .zero, size: size))
107 |
108 | guard let scaledImage = context?.makeImage() else { print("bad image"); return nil }
109 |
110 | return UIImage(cgImage: scaledImage)
111 | }
112 | }
113 |
114 | extension UIView {
115 | var localCenter: CGPoint {
116 | return CGPoint(x: bounds.midX, y: bounds.midY)
117 | }
118 |
119 | func pinEdges(to other: UIView, offset: CGFloat = 0) {
120 | leadingAnchor.constraint(equalTo: other.leadingAnchor, constant: offset).isActive = true
121 | trailingAnchor.constraint(equalTo: other.trailingAnchor).isActive = true
122 | topAnchor.constraint(equalTo: other.topAnchor).isActive = true
123 | bottomAnchor.constraint(equalTo: other.bottomAnchor).isActive = true
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/Extensions/macOS/Extensions-macOS.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension NSEvent {
4 | var isRightClick: Bool {
5 | let rightClick = (self.type == .rightMouseDown)
6 | let controlClick = self.modifierFlags.contains(.control)
7 | return rightClick || controlClick
8 | }
9 | }
10 |
11 | extension NSFont {
12 | func withTraits(_ traits: NSFontDescriptor.SymbolicTraits) -> NSFont {
13 | let fd = fontDescriptor.withSymbolicTraits(traits)
14 | if let font = NSFont(descriptor: fd, size: pointSize) {
15 | return font
16 | } else {
17 | return self
18 | }
19 | }
20 |
21 | func italics() -> NSFont {
22 | return withTraits(.italic)
23 | }
24 |
25 | func bold() -> NSFont {
26 | return withTraits(.bold)
27 | }
28 |
29 | func boldItalics() -> NSFont {
30 | return withTraits([.bold, .italic])
31 | }
32 | }
33 |
34 | extension NSImage {
35 | func writeJPGToURL(_ url: URL, quality: Float = 0.85) -> Bool {
36 | let properties = [NSBitmapImageRep.PropertyKey.compressionFactor: quality]
37 | guard let imageData = self.tiffRepresentation else { return false }
38 | guard let imageRep = NSBitmapImageRep(data: imageData) else { return false }
39 | guard let fileData = imageRep.representation(using: .jpeg, properties: properties) else { return false }
40 |
41 | do {
42 | try fileData.write(to: url)
43 | } catch {
44 | return false
45 | }
46 |
47 | return true
48 | }
49 |
50 | func writePNGToURL(_ url: URL) -> Bool {
51 | guard let imageData = self.tiffRepresentation else { return false }
52 | guard let imageRep = NSBitmapImageRep(data: imageData) else { return false }
53 | guard let fileData = imageRep.representation(using: .png, properties: [:]) else { return false }
54 |
55 | do {
56 | try fileData.write(to: url)
57 | } catch {
58 | return false
59 | }
60 |
61 | return true
62 | }
63 |
64 | func tint(color: NSColor) -> NSImage {
65 | let image = self.copy() as! NSImage
66 | image.lockFocus()
67 |
68 | color.set()
69 |
70 | let imageRect = NSRect(origin: NSZeroPoint, size: image.size)
71 | imageRect.fill(using: .sourceAtop)
72 |
73 | image.unlockFocus()
74 |
75 | return image
76 | }
77 |
78 | func scaleBy(factor: CGFloat = 0.5) -> NSImage {
79 | let newSize = NSMakeSize(size.width * factor, size.height * factor)
80 | let scaledImage = NSImage(size: newSize)
81 | scaledImage.lockFocus()
82 | draw(in: NSMakeRect(0, 0, newSize.width, newSize.height))
83 | scaledImage.unlockFocus()
84 | return scaledImage
85 | }
86 | }
87 |
88 | extension NSTableView {
89 | func reloadOnMainThread(_ complete: (() -> ())? = nil) {
90 | DispatchQueue.main.async {
91 | self.reloadData()
92 | complete?()
93 | }
94 | }
95 |
96 | func reloadMaintainingSelection(_ complete: (() -> ())? = nil) {
97 | let oldSelectedRowIndexes = selectedRowIndexes
98 | reloadOnMainThread {
99 | if oldSelectedRowIndexes.count == 0 {
100 | if self.numberOfRows > 0 {
101 | self.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
102 | }
103 | } else {
104 | self.selectRowIndexes(oldSelectedRowIndexes, byExtendingSelection: false)
105 | }
106 | }
107 | }
108 |
109 | func selectFirstPossibleRow() {
110 | for i in 0..= 0) && (selectedRow < numberOfRows) {
140 | return item(atRow: selectedRow)
141 | } else {
142 | return nil
143 | }
144 | }
145 | }
146 |
147 | var selectedView: NSView? {
148 | get {
149 | if (selectedRow >= 0) && (selectedRow < numberOfRows) {
150 | return view(atColumn: 0, row: self.selectedRow, makeIfNecessary: false)
151 | } else {
152 | return nil
153 | }
154 | }
155 | }
156 |
157 | func expandAll() {
158 | DispatchQueue.main.async {
159 | self.expandItem(nil, expandChildren: true)
160 | }
161 | }
162 | }
163 |
164 | extension NSView {
165 | func pinEdges(to other: NSView, offset: CGFloat = 0, animate: Bool = false) {
166 | if animate {
167 | animator().leadingAnchor.constraint(equalTo: other.leadingAnchor, constant: offset).isActive = true
168 | } else {
169 | leadingAnchor.constraint(equalTo: other.leadingAnchor, constant: offset).isActive = true
170 | trailingAnchor.constraint(equalTo: other.trailingAnchor).isActive = true
171 | topAnchor.constraint(equalTo: other.topAnchor).isActive = true
172 | bottomAnchor.constraint(equalTo: other.bottomAnchor).isActive = true
173 | }
174 | }
175 | }
176 |
177 | extension NSWindow {
178 | func toolbarHeight() -> CGFloat {
179 | if let windowFrameHeight = contentView?.frame.height {
180 | let contentLayoutRectHeight = contentLayoutRect.height
181 | let fullSizeContentViewNoContentAreaHeight = windowFrameHeight - contentLayoutRectHeight
182 | return fullSizeContentViewNoContentAreaHeight
183 | }
184 |
185 | return 0
186 | }
187 | }
188 |
189 | extension String {
190 | // I should really just stop using these and switch to one of the better, full-featured attributed string
191 | // libraries, but meh. This stuff works for now.
192 | func boldString(textColor: NSColor = NSColor.textColor) -> NSMutableAttributedString {
193 | let attrStr = NSMutableAttributedString(string: self)
194 | attrStr.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: NSRange(self.startIndex..., in: self))
195 | attrStr.addAttribute(NSAttributedString.Key.font, value: NSFont.boldSystemFont(ofSize: NSFont.systemFontSize), range: NSRange(self.startIndex..., in: self))
196 | return attrStr
197 | }
198 |
199 | func coloredAttributedString(textColor: NSColor = NSColor.textColor) -> NSMutableAttributedString {
200 | let attrStr = NSMutableAttributedString(string: self)
201 | attrStr.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: NSRange(self.startIndex..., in: self))
202 | attrStr.addAttribute(NSAttributedString.Key.font, value: NSFont.systemFont(ofSize: NSFont.systemFontSize), range: NSRange(self.startIndex..., in: self))
203 | return attrStr
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/Extensions/Shared/Extensions-Shared.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CryptoKit
3 | import CommonCrypto
4 | import NaturalLanguage
5 |
6 | extension Array {
7 | func shuffled() -> [Element] {
8 | if count < 2 { return self }
9 | var list = self
10 | for i in 0..<(list.count - 1) {
11 | let j = Int(arc4random_uniform(UInt32(list.count - i))) + i
12 | if i != j {
13 | list.swapAt(i, j)
14 | }
15 | }
16 | return list
17 | }
18 |
19 | // Creates an array containing all combinations of two arrays.
20 | static func createAllCombinations(from lhs: Array, and rhs: Array) -> Array<(T, U)> {
21 | let result: [(T, U)] = lhs.reduce([]) { (accum, t) in
22 | let innerResult: [(T, U)] = rhs.reduce([]) { (innerAccum, u) in
23 | return innerAccum + [(t, u)]
24 | }
25 | return accum + innerResult
26 | }
27 | return result
28 | }
29 |
30 | mutating func move(from oldIndex: Index, to newIndex: Index) {
31 | // Don't work for free and use swap when indices are next to each other - this
32 | // won't rebuild array and will be super efficient.
33 | if oldIndex == newIndex { return }
34 | if abs(newIndex - oldIndex) == 1 { return self.swapAt(oldIndex, newIndex) }
35 | self.insert(self.remove(at: oldIndex), at: newIndex)
36 | }
37 | }
38 |
39 | extension Array where Element: Equatable {
40 | mutating func move(_ element: Element, to newIndex: Index) {
41 | if let oldIndex: Int = self.firstIndex(of: element) { self.move(from: oldIndex, to: newIndex) }
42 | }
43 | }
44 |
45 | extension Collection {
46 | subscript(optional i: Index) -> Iterator.Element? {
47 | return self.indices.contains(i) ? self[i] : nil
48 | }
49 | }
50 |
51 | extension Data {
52 | var hexString: String {
53 | let hexString = map { String(format: "%02.2hhx", $0) }.joined()
54 | return hexString
55 | }
56 | }
57 |
58 | extension Date {
59 | static var yesterday: Date { return Date().dayBefore }
60 | static var tomorrow: Date { return Date().dayAfter }
61 |
62 | var dayBefore: Date {
63 | return Calendar.current.date(byAdding: .day, value: -1, to: noon)!
64 | }
65 |
66 | var dayAfter: Date {
67 | return Calendar.current.date(byAdding: .day, value: 1, to: noon)!
68 | }
69 |
70 | var noon: Date {
71 | return Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: self)!
72 | }
73 |
74 | // let d = Date() -> Mar 12, 2020 at 1:51 PM
75 | // d.stringify() -> "1584039099.486827"
76 | func stringify() -> String {
77 | return timeIntervalSince1970.stringValue()
78 | }
79 |
80 | // Date.unstringify("1584039099.486827") -> Mar 12, 2020 at 1:51 PM
81 | static func unstringify(_ ts: String) -> Date? {
82 | if let dbl = Double(ts) {
83 | return Date(timeIntervalSince1970: dbl)
84 | }
85 | return nil
86 | }
87 |
88 | func addMonth(n: Int) -> Date? {
89 | return Calendar.current.date(byAdding: .month, value: n, to: self)
90 | }
91 |
92 | func addDay(n: Int) -> Date? {
93 | return Calendar.current.date(byAdding: .day, value: n, to: self)
94 | }
95 |
96 | var month: Int {
97 | return Calendar.current.component(.month, from: self)
98 | }
99 |
100 | var year: Int {
101 | return Calendar.current.component(.year, from: self)
102 | }
103 |
104 | var day: Int {
105 | return Calendar.current.component(.day, from: self)
106 | }
107 |
108 | var startOfDay: Date {
109 | return Calendar.current.startOfDay(for: self)
110 | }
111 |
112 | var endOfDay: Date {
113 | var components = DateComponents()
114 | components.day = 1
115 | components.second = -1
116 | return Calendar.current.date(byAdding: components, to: startOfDay)!
117 | }
118 |
119 | var startOfMonth: Date {
120 | let components = Calendar.current.dateComponents([.year, .month], from: startOfDay)
121 | return Calendar.current.date(from: components)!
122 | }
123 |
124 | var endOfMonth: Date {
125 | var components = DateComponents()
126 | components.month = 1
127 | components.second = -1
128 | return Calendar.current.date(byAdding: components, to: startOfMonth)!
129 | }
130 |
131 | func numberOfDaysInMonth() -> Int {
132 | let range = Calendar.current.range(of: .day, in: .month, for: self)!
133 | return range.count
134 | }
135 |
136 | var isLastDayOfMonth: Bool {
137 | return dayAfter.month != month
138 | }
139 |
140 | var midnight: Date {
141 | return Calendar.current.startOfDay(for: self)
142 | }
143 |
144 | var startOfWeek: Date {
145 | return Calendar.current.date(from: Calendar.current.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self))!
146 | }
147 | }
148 |
149 | extension Dictionary {
150 | // Combines self with another dictionary.
151 | mutating func merge(dict: [Key: Value]){
152 | for (k, v) in dict {
153 | updateValue(v, forKey: k)
154 | }
155 | }
156 | }
157 |
158 | extension Double {
159 | // It's dumb, but I swear I end up having to dump a number into some type
160 | // of storage that only accepts a String way more often than I care to think about.
161 | func stringValue() -> String {
162 | return String(format:"%f", self)
163 | }
164 |
165 | func formatAsCurrency() -> String {
166 | let nf = NumberFormatter()
167 | nf.numberStyle = .currency
168 | return nf.string(from: NSNumber(floatLiteral: self))!
169 | }
170 | }
171 |
172 | extension FileManager {
173 | // Given a basename such as "My Picture" and fileExtension "jpg",
174 | // it will produce a unique, seqential filename such as "My Picuture 1.jpg"
175 | // that does not exist in directoryURL. If the directory already contained
176 | // "My Picuture.jpg", "My Picuture 1.jpg", "My Picuture 2.jpg", this will
177 | // return "My Picuture 3.jpg".
178 | func uniqueFileURL(directoryURL: URL, basename: String, fileExtension: String?) -> URL {
179 | var fullPathURL = directoryURL.appendingPathComponent(basename)
180 | if let ext = fileExtension {
181 | fullPathURL = fullPathURL.appendingPathExtension(ext)
182 | }
183 |
184 | var i = 0
185 | while FileManager.default.fileExists(atPath: fullPathURL.path) {
186 | i += 1
187 | let newBasename = "\(basename) \(i)"
188 | fullPathURL = directoryURL.appendingPathComponent(newBasename)
189 | if let ext = fileExtension {
190 | fullPathURL = fullPathURL.appendingPathExtension(ext)
191 | }
192 | }
193 |
194 | return fullPathURL
195 | }
196 | }
197 |
198 | extension NSAttributedString {
199 | // Haphazard solution that returns the range of a line of text at a given position,
200 | // where a line is delimited by newlines.
201 | func rangeOfLineAtLocation(_ location: Int) -> NSRange {
202 | if string.character(location).isNewline {
203 | var start = location
204 | while(start > 0 && !string.character(start - 1).isNewline) {
205 | start -= 1
206 | }
207 | return NSMakeRange(start, location-start)
208 | }
209 |
210 | var start: Int = location
211 | while(start > 0 && !string.character(start - 1).isNewline) {
212 | start -= 1
213 | }
214 |
215 | var end = location
216 | while(end < string.count - 1 && !string.character(end + 1).isNewline) {
217 | end += 1
218 | }
219 |
220 | return NSMakeRange(start, end - start)
221 | }
222 |
223 | func attributedStringByTrimmingCharacterSet(charSet: CharacterSet) -> NSAttributedString {
224 | let modifiedString = NSMutableAttributedString(attributedString: self)
225 | modifiedString.trimCharactersInSet(charSet: charSet)
226 | return NSAttributedString(attributedString: modifiedString)
227 | }
228 | }
229 |
230 | extension NSMutableAttributedString {
231 | func trimCharactersInSet(charSet: CharacterSet) {
232 | var range = (string as NSString).rangeOfCharacter(from: charSet as CharacterSet)
233 |
234 | // Trim leading characters from character set.
235 | while range.length != 0 && range.location == 0 {
236 | replaceCharacters(in: range, with: "")
237 | range = (string as NSString).rangeOfCharacter(from: charSet)
238 | }
239 |
240 | // Trim trailing characters from character set.
241 | range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
242 | while range.length != 0 && NSMaxRange(range) == length {
243 | replaceCharacters(in: range, with: "")
244 | range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
245 | }
246 | }
247 | }
248 |
249 | extension NSNotification.Name {
250 | func post(_ object: Any? = nil, userInfo: [AnyHashable: Any]? = nil) {
251 | NotificationCenter.default.post(name: self, object: object, userInfo: userInfo)
252 | }
253 | }
254 |
255 | extension NSRange {
256 | init?(string: String, lowerBound: String.Index, upperBound: String.Index) {
257 | let utf16 = string.utf16
258 |
259 | if let lowerBound = lowerBound.samePosition(in: utf16), let upperBound = upperBound.samePosition(in: utf16) {
260 | let location = utf16.distance(from: utf16.startIndex, to: lowerBound)
261 | let length = utf16.distance(from: lowerBound, to: upperBound)
262 |
263 | self.init(location: location, length: length)
264 | }
265 | return nil
266 | }
267 |
268 | init?(range: Range, in string: String) {
269 | self.init(string: string, lowerBound: range.lowerBound, upperBound: range.upperBound)
270 | }
271 |
272 | init?(range: ClosedRange, in string: String) {
273 | self.init(string: string, lowerBound: range.lowerBound, upperBound: range.upperBound)
274 | }
275 | }
276 |
277 | extension String {
278 | // An incredibly lenient and forgiving way to get a numeric String
279 | // out of another string - typically one provided by the user.
280 | // You shouldn't rely on this for anything truly mission critical.
281 | func numberString() -> String? {
282 | let strippedStr = trimmingCharacters(in: .whitespacesAndNewlines)
283 | let isNegative = strippedStr.hasPrefix("-")
284 | let allowedCharSet = CharacterSet(charactersIn: ".,0123456789")
285 | let filteredStr = components(separatedBy: allowedCharSet.inverted).joined()
286 | if (count(of: ".") + count(of: ",")) > 1 { return nil }
287 | return (isNegative ? "-" : "") + filteredStr
288 | }
289 |
290 | // Number of times a character occurs within a string.
291 | func count(of needle: Character) -> Int {
292 | return reduce(0) {
293 | $1 == needle ? $0 + 1 : $0
294 | }
295 | }
296 |
297 | // Returns an array of substrings delimited by whitespace - and also
298 | // combines tokens inside matching quotes into a single token. I don't
299 | // claim this to be pefect in every edge case - but I haven't encountered
300 | // a bug yet 🤷♀️.
301 | // "My name is Tim Apple".tokenize() -> ["My", "name", "is", "Tim", "Apple"]
302 | // "I hope the \"SF Giants\" have a \"better season\" this year" -> ["I", "hope", "the", "SF Giants", "have", "a", "better season", "this", "year"]
303 | @available(OSX 10.15, iOS 13, *)
304 | func tokenize() -> [String] {
305 | enum State {
306 | case Normal
307 | case InsideAQuote
308 | }
309 |
310 | let theString = self.trimmingCharacters(in: .whitespacesAndNewlines)
311 |
312 | var tokens = [String]()
313 | var state = State.Normal
314 | let delimeters = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: "\""))
315 | let quote = CharacterSet(charactersIn: "\"")
316 |
317 | let scanner = Scanner(string: theString)
318 | scanner.charactersToBeSkipped = .none
319 |
320 | while !scanner.isAtEnd {
321 | if state == .Normal {
322 | if let token = scanner.scanCharacters(from: delimeters.inverted) {
323 | tokens.append(token.trimmingCharacters(in: .whitespacesAndNewlines))
324 | } else if let delims = scanner.scanCharacters(from: delimeters) {
325 | if delims.hasSuffix("\"") {
326 | state = .InsideAQuote
327 | }
328 | }
329 | } else {
330 | if let token = scanner.scanCharacters(from: quote.inverted) {
331 | tokens.append(token.trimmingCharacters(in: .whitespacesAndNewlines))
332 | state = .Normal
333 | }
334 | }
335 | }
336 |
337 | return tokens
338 | }
339 |
340 | func number() -> NSNumber? {
341 | let amountStr = self.trimmingCharacters(in: .whitespacesAndNewlines)
342 |
343 | let nf = NumberFormatter()
344 |
345 | nf.numberStyle = .currency
346 | if let num = nf.number(from: amountStr) {
347 | return num
348 | }
349 |
350 | nf.numberStyle = .none
351 | if let num = nf.number(from: amountStr) {
352 | return num
353 | }
354 |
355 | return nil
356 | }
357 |
358 | func formatAsCurrency() -> String? {
359 | return number()?.doubleValue.formatAsCurrency()
360 | }
361 |
362 | func substring(to : Int) -> String {
363 | let toIndex = self.index(self.startIndex, offsetBy: to)
364 | return String(self[...toIndex])
365 | }
366 |
367 | func substring(from : Int) -> String {
368 | let fromIndex = self.index(self.startIndex, offsetBy: from)
369 | return String(self[fromIndex...])
370 | }
371 |
372 | func substring(_ r: Range) -> String {
373 | let fromIndex = self.index(self.startIndex, offsetBy: r.lowerBound)
374 | let toIndex = self.index(self.startIndex, offsetBy: r.upperBound)
375 | let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex))
376 | return String(self[indexRange])
377 | }
378 |
379 | func character(_ at: Int) -> Character {
380 | return self[self.index(self.startIndex, offsetBy: at)]
381 | }
382 |
383 | func lastIndexOfCharacter(_ c: Character) -> Int? {
384 | guard let index = range(of: String(c), options: .backwards)?.lowerBound else { return nil }
385 | return distance(from: startIndex, to: index)
386 | }
387 |
388 | func deletingPrefix(_ prefix: String) -> String {
389 | guard self.hasPrefix(prefix) else { return self }
390 | return String(self.dropFirst(prefix.count))
391 | }
392 |
393 | func deletingSuffix(_ suffix: String) -> String {
394 | guard self.hasSuffix(suffix) else { return self }
395 | return String(self.dropLast(suffix.count))
396 | }
397 |
398 | func lowerTrimmed() -> String {
399 | return self.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
400 | }
401 |
402 | func keyVal() -> (String, String?)? {
403 | if let colonIndex = self.firstIndex(of: ":") {
404 | let key = self[.. [UInt8] in
415 | var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
416 | CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash)
417 | return hash
418 | }
419 | return hash.map { String(format: "%02x", $0) }.joined()
420 | }
421 |
422 | var sha256: String {
423 | let data = Data(self.utf8)
424 | let hashed = SHA256.hash(data: data)
425 | return hashed.compactMap { String(format: "%02x", $0) }.joined()
426 | }
427 |
428 | func matchingStrings(regex: String) -> [[String]] {
429 | guard let regex = try? NSRegularExpression(pattern: regex, options: [.dotMatchesLineSeparators]) else { return [] }
430 | let nsString = self as NSString
431 | let results = regex.matches(in: self, options: [.withoutAnchoringBounds], range: NSMakeRange(0, nsString.length))
432 | return results.map { result in
433 | (0.. String? {
443 | guard let leftRange = range(of: left) else { return nil }
444 | guard let rightRange = range(of: right, options: .backwards) else { return nil }
445 | guard leftRange.upperBound <= rightRange.lowerBound else { return nil }
446 |
447 | let sub = self[leftRange.upperBound...]
448 | let closestToLeftRange = sub.range(of: right)!
449 | return String(sub[..) -> NSRange {
453 | guard let lower = UTF16View.Index(range.lowerBound, within: utf16) else { return .init() }
454 | guard let upper = UTF16View.Index(range.upperBound, within: utf16) else { return .init() }
455 | return NSRange(location: utf16.distance(from: utf16.startIndex, to: lower), length: utf16.distance(from: lower, to: upper))
456 | }
457 |
458 | func lemmatize() -> [String] {
459 | let tagger = NSLinguisticTagger(tagSchemes: [.lemma], options: 0)
460 | tagger.string = self
461 | let range = NSMakeRange(0, self.utf16.count)
462 | let options: NSLinguisticTagger.Options = [.omitWhitespace, .omitPunctuation]
463 |
464 | var results = [String]()
465 | tagger.enumerateTags(in: range, unit: .word, scheme: .lemma, options: options) { (tag, tokenRange, stop) in
466 | if let lemma = tag?.rawValue {
467 | results.append(lemma)
468 | }
469 | }
470 |
471 | return (results.count > 0) ? [self] : results
472 | }
473 | }
474 |
475 | extension String {
476 | struct Summary {
477 | var characters = 0
478 | var nonWhitespaceCharacters = 0
479 | var words = 0
480 | var sentences = 0
481 | var lines = 0
482 | var averageReadingTime: TimeInterval = 0
483 | var aloudReadingTime: TimeInterval = 0
484 | }
485 |
486 | var summary: Summary {
487 | var s = Summary(characters: countCharacters(),
488 | nonWhitespaceCharacters: countNonWhitespaceCharacters(),
489 | words: countWords(),
490 | sentences: countSentences(),
491 | lines: countLines())
492 | s.averageReadingTime = calculateAverageReadingTime(words: s.words)
493 | s.aloudReadingTime = calculateAloudReadingTime(words: s.words)
494 | return s
495 | }
496 |
497 | // https://digest.bps.org.uk/2019/06/13/most-comprehensive-review-to-date-suggests-the-average-persons-reading-speed-is-slower-than-commonly-thought/
498 | func calculateAverageReadingTime(words: Int) -> TimeInterval {
499 | return TimeInterval(words) / 238.0 * 60.0
500 | }
501 |
502 | // https://www.visualthesaurus.com/cm/wc/seven-ways-to-write-a-better-speech/
503 | func calculateAloudReadingTime(words: Int) -> TimeInterval {
504 | return TimeInterval(words) / 125.0 * 60.0
505 | }
506 |
507 | func countCharacters() -> Int {
508 | return count
509 | }
510 |
511 | func countNonWhitespaceCharacters() -> Int {
512 | components(separatedBy: CharacterSet.whitespacesAndNewlines).joined().count
513 | }
514 |
515 | func countWords() -> Int {
516 | let words = components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty }
517 | return words.count
518 | }
519 |
520 | func countSentences() -> Int {
521 | let s = self
522 | var r = [Range]()
523 | let t = s.linguisticTags(in: s.startIndex.. Int {
536 | return components(separatedBy: .newlines).count
537 | }
538 | }
539 |
540 | extension TimeInterval {
541 | // let foo: TimeInterval = 6227
542 | // foo.durationString() -> "2h"
543 | // foo.durationString(2) -> "1h 44m"
544 | // foo.durationString(3) -> "1h 43m 47s"
545 | func durationString() -> String {
546 | let formatter = DateComponentsFormatter()
547 | formatter.allowedUnits = [.day, .hour, .minute, .second]
548 | formatter.unitsStyle = .abbreviated
549 | formatter.maximumUnitCount = 1
550 | return formatter.string(from: self)!
551 | }
552 | }
553 |
554 | extension URL {
555 | var isAccessibleFile: Bool {
556 | var isDir: ObjCBool = false
557 | let exists = FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir)
558 | if !exists || isDir.boolValue {
559 | return false
560 | }
561 | return FileManager.default.isReadableFile(atPath: self.path)
562 | }
563 |
564 | var isAccessibleDirectory: Bool {
565 | var isDir: ObjCBool = false
566 | let exists = FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir)
567 | if !exists || !isDir.boolValue {
568 | return false
569 | }
570 | return FileManager.default.isReadableFile(atPath: self.path)
571 | }
572 |
573 | func dumbGET(_ results: ((Data?) -> ())? = nil) {
574 | DispatchQueue.global(qos: .userInitiated).async {
575 | do {
576 | let data = try Data(contentsOf: self)
577 | results?(data)
578 | } catch {
579 | results?(nil)
580 | }
581 | }
582 | }
583 | }
584 |
--------------------------------------------------------------------------------
/Extensions.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | C655844B2534053500A440FD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655844A2534053500A440FD /* AppDelegate.swift */; };
11 | C655844F2534053600A440FD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C655844E2534053600A440FD /* Assets.xcassets */; };
12 | C65584522534053600A440FD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C65584502534053600A440FD /* Main.storyboard */; };
13 | C65584612534057400A440FD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65584602534057400A440FD /* AppDelegate.swift */; };
14 | C65584632534057400A440FD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65584622534057400A440FD /* SceneDelegate.swift */; };
15 | C65584682534057400A440FD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C65584662534057400A440FD /* Main.storyboard */; };
16 | C655846A2534057600A440FD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C65584692534057600A440FD /* Assets.xcassets */; };
17 | C655846D2534057600A440FD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C655846B2534057600A440FD /* LaunchScreen.storyboard */; };
18 | C6558478253405D800A440FD /* Extensions-Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6558477253405D800A440FD /* Extensions-Shared.swift */; };
19 | C6558479253405D800A440FD /* Extensions-Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6558477253405D800A440FD /* Extensions-Shared.swift */; };
20 | C655847D253405DF00A440FD /* Extensions-macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C655847C253405DF00A440FD /* Extensions-macOS.swift */; };
21 | C6558481253405E500A440FD /* Extensions-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6558480253405E500A440FD /* Extensions-iOS.swift */; };
22 | C6558485253405F300A440FD /* Extensions-Geometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6558484253405F300A440FD /* Extensions-Geometry.swift */; };
23 | C6558486253405F300A440FD /* Extensions-Geometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6558484253405F300A440FD /* Extensions-Geometry.swift */; };
24 | /* End PBXBuildFile section */
25 |
26 | /* Begin PBXFileReference section */
27 | C65584472534053500A440FD /* macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = macOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
28 | C655844A2534053500A440FD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
29 | C655844E2534053600A440FD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
30 | C65584512534053600A440FD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
31 | C65584532534053600A440FD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
32 | C65584542534053600A440FD /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; };
33 | C655845E2534057300A440FD /* iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
34 | C65584602534057400A440FD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
35 | C65584622534057400A440FD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
36 | C65584672534057400A440FD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
37 | C65584692534057600A440FD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
38 | C655846C2534057600A440FD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
39 | C655846E2534057600A440FD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
40 | C6558477253405D800A440FD /* Extensions-Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extensions-Shared.swift"; sourceTree = ""; };
41 | C655847C253405DF00A440FD /* Extensions-macOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extensions-macOS.swift"; sourceTree = ""; };
42 | C6558480253405E500A440FD /* Extensions-iOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extensions-iOS.swift"; sourceTree = ""; };
43 | C6558484253405F300A440FD /* Extensions-Geometry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Extensions-Geometry.swift"; sourceTree = ""; };
44 | /* End PBXFileReference section */
45 |
46 | /* Begin PBXFrameworksBuildPhase section */
47 | C65584442534053500A440FD /* Frameworks */ = {
48 | isa = PBXFrameworksBuildPhase;
49 | buildActionMask = 2147483647;
50 | files = (
51 | );
52 | runOnlyForDeploymentPostprocessing = 0;
53 | };
54 | C655845B2534057300A440FD /* Frameworks */ = {
55 | isa = PBXFrameworksBuildPhase;
56 | buildActionMask = 2147483647;
57 | files = (
58 | );
59 | runOnlyForDeploymentPostprocessing = 0;
60 | };
61 | /* End PBXFrameworksBuildPhase section */
62 |
63 | /* Begin PBXGroup section */
64 | C655843C2534051500A440FD = {
65 | isa = PBXGroup;
66 | children = (
67 | C6558476253405CB00A440FD /* Extensions */,
68 | C65584492534053500A440FD /* macOS */,
69 | C655845F2534057400A440FD /* iOS */,
70 | C65584482534053500A440FD /* Products */,
71 | );
72 | sourceTree = "";
73 | };
74 | C65584482534053500A440FD /* Products */ = {
75 | isa = PBXGroup;
76 | children = (
77 | C65584472534053500A440FD /* macOS.app */,
78 | C655845E2534057300A440FD /* iOS.app */,
79 | );
80 | name = Products;
81 | sourceTree = "";
82 | };
83 | C65584492534053500A440FD /* macOS */ = {
84 | isa = PBXGroup;
85 | children = (
86 | C655844A2534053500A440FD /* AppDelegate.swift */,
87 | C655844E2534053600A440FD /* Assets.xcassets */,
88 | C65584502534053600A440FD /* Main.storyboard */,
89 | C65584532534053600A440FD /* Info.plist */,
90 | C65584542534053600A440FD /* macOS.entitlements */,
91 | );
92 | path = macOS;
93 | sourceTree = "";
94 | };
95 | C655845F2534057400A440FD /* iOS */ = {
96 | isa = PBXGroup;
97 | children = (
98 | C65584602534057400A440FD /* AppDelegate.swift */,
99 | C65584622534057400A440FD /* SceneDelegate.swift */,
100 | C65584662534057400A440FD /* Main.storyboard */,
101 | C65584692534057600A440FD /* Assets.xcassets */,
102 | C655846B2534057600A440FD /* LaunchScreen.storyboard */,
103 | C655846E2534057600A440FD /* Info.plist */,
104 | );
105 | path = iOS;
106 | sourceTree = "";
107 | };
108 | C6558476253405CB00A440FD /* Extensions */ = {
109 | isa = PBXGroup;
110 | children = (
111 | C6558489253405F800A440FD /* Shared */,
112 | C65584902534062200A440FD /* macOS */,
113 | C65584912534062900A440FD /* iOS */,
114 | );
115 | path = Extensions;
116 | sourceTree = "";
117 | };
118 | C6558489253405F800A440FD /* Shared */ = {
119 | isa = PBXGroup;
120 | children = (
121 | C6558477253405D800A440FD /* Extensions-Shared.swift */,
122 | C6558484253405F300A440FD /* Extensions-Geometry.swift */,
123 | );
124 | path = Shared;
125 | sourceTree = "";
126 | };
127 | C65584902534062200A440FD /* macOS */ = {
128 | isa = PBXGroup;
129 | children = (
130 | C655847C253405DF00A440FD /* Extensions-macOS.swift */,
131 | );
132 | path = macOS;
133 | sourceTree = "";
134 | };
135 | C65584912534062900A440FD /* iOS */ = {
136 | isa = PBXGroup;
137 | children = (
138 | C6558480253405E500A440FD /* Extensions-iOS.swift */,
139 | );
140 | path = iOS;
141 | sourceTree = "";
142 | };
143 | /* End PBXGroup section */
144 |
145 | /* Begin PBXNativeTarget section */
146 | C65584462534053500A440FD /* macOS */ = {
147 | isa = PBXNativeTarget;
148 | buildConfigurationList = C65584552534053600A440FD /* Build configuration list for PBXNativeTarget "macOS" */;
149 | buildPhases = (
150 | C65584432534053500A440FD /* Sources */,
151 | C65584442534053500A440FD /* Frameworks */,
152 | C65584452534053500A440FD /* Resources */,
153 | );
154 | buildRules = (
155 | );
156 | dependencies = (
157 | );
158 | name = macOS;
159 | productName = macOS;
160 | productReference = C65584472534053500A440FD /* macOS.app */;
161 | productType = "com.apple.product-type.application";
162 | };
163 | C655845D2534057300A440FD /* iOS */ = {
164 | isa = PBXNativeTarget;
165 | buildConfigurationList = C655846F2534057600A440FD /* Build configuration list for PBXNativeTarget "iOS" */;
166 | buildPhases = (
167 | C655845A2534057300A440FD /* Sources */,
168 | C655845B2534057300A440FD /* Frameworks */,
169 | C655845C2534057300A440FD /* Resources */,
170 | );
171 | buildRules = (
172 | );
173 | dependencies = (
174 | );
175 | name = iOS;
176 | productName = iOS;
177 | productReference = C655845E2534057300A440FD /* iOS.app */;
178 | productType = "com.apple.product-type.application";
179 | };
180 | /* End PBXNativeTarget section */
181 |
182 | /* Begin PBXProject section */
183 | C655843D2534051500A440FD /* Project object */ = {
184 | isa = PBXProject;
185 | attributes = {
186 | LastSwiftUpdateCheck = 1200;
187 | LastUpgradeCheck = 1200;
188 | TargetAttributes = {
189 | C65584462534053500A440FD = {
190 | CreatedOnToolsVersion = 12.0.1;
191 | };
192 | C655845D2534057300A440FD = {
193 | CreatedOnToolsVersion = 12.0.1;
194 | };
195 | };
196 | };
197 | buildConfigurationList = C65584402534051500A440FD /* Build configuration list for PBXProject "Extensions" */;
198 | compatibilityVersion = "Xcode 9.3";
199 | developmentRegion = en;
200 | hasScannedForEncodings = 0;
201 | knownRegions = (
202 | en,
203 | Base,
204 | );
205 | mainGroup = C655843C2534051500A440FD;
206 | productRefGroup = C65584482534053500A440FD /* Products */;
207 | projectDirPath = "";
208 | projectRoot = "";
209 | targets = (
210 | C65584462534053500A440FD /* macOS */,
211 | C655845D2534057300A440FD /* iOS */,
212 | );
213 | };
214 | /* End PBXProject section */
215 |
216 | /* Begin PBXResourcesBuildPhase section */
217 | C65584452534053500A440FD /* Resources */ = {
218 | isa = PBXResourcesBuildPhase;
219 | buildActionMask = 2147483647;
220 | files = (
221 | C655844F2534053600A440FD /* Assets.xcassets in Resources */,
222 | C65584522534053600A440FD /* Main.storyboard in Resources */,
223 | );
224 | runOnlyForDeploymentPostprocessing = 0;
225 | };
226 | C655845C2534057300A440FD /* Resources */ = {
227 | isa = PBXResourcesBuildPhase;
228 | buildActionMask = 2147483647;
229 | files = (
230 | C655846D2534057600A440FD /* LaunchScreen.storyboard in Resources */,
231 | C655846A2534057600A440FD /* Assets.xcassets in Resources */,
232 | C65584682534057400A440FD /* Main.storyboard in Resources */,
233 | );
234 | runOnlyForDeploymentPostprocessing = 0;
235 | };
236 | /* End PBXResourcesBuildPhase section */
237 |
238 | /* Begin PBXSourcesBuildPhase section */
239 | C65584432534053500A440FD /* Sources */ = {
240 | isa = PBXSourcesBuildPhase;
241 | buildActionMask = 2147483647;
242 | files = (
243 | C6558478253405D800A440FD /* Extensions-Shared.swift in Sources */,
244 | C6558485253405F300A440FD /* Extensions-Geometry.swift in Sources */,
245 | C655844B2534053500A440FD /* AppDelegate.swift in Sources */,
246 | C655847D253405DF00A440FD /* Extensions-macOS.swift in Sources */,
247 | );
248 | runOnlyForDeploymentPostprocessing = 0;
249 | };
250 | C655845A2534057300A440FD /* Sources */ = {
251 | isa = PBXSourcesBuildPhase;
252 | buildActionMask = 2147483647;
253 | files = (
254 | C65584612534057400A440FD /* AppDelegate.swift in Sources */,
255 | C65584632534057400A440FD /* SceneDelegate.swift in Sources */,
256 | C6558486253405F300A440FD /* Extensions-Geometry.swift in Sources */,
257 | C6558479253405D800A440FD /* Extensions-Shared.swift in Sources */,
258 | C6558481253405E500A440FD /* Extensions-iOS.swift in Sources */,
259 | );
260 | runOnlyForDeploymentPostprocessing = 0;
261 | };
262 | /* End PBXSourcesBuildPhase section */
263 |
264 | /* Begin PBXVariantGroup section */
265 | C65584502534053600A440FD /* Main.storyboard */ = {
266 | isa = PBXVariantGroup;
267 | children = (
268 | C65584512534053600A440FD /* Base */,
269 | );
270 | name = Main.storyboard;
271 | sourceTree = "";
272 | };
273 | C65584662534057400A440FD /* Main.storyboard */ = {
274 | isa = PBXVariantGroup;
275 | children = (
276 | C65584672534057400A440FD /* Base */,
277 | );
278 | name = Main.storyboard;
279 | sourceTree = "";
280 | };
281 | C655846B2534057600A440FD /* LaunchScreen.storyboard */ = {
282 | isa = PBXVariantGroup;
283 | children = (
284 | C655846C2534057600A440FD /* Base */,
285 | );
286 | name = LaunchScreen.storyboard;
287 | sourceTree = "";
288 | };
289 | /* End PBXVariantGroup section */
290 |
291 | /* Begin XCBuildConfiguration section */
292 | C65584412534051500A440FD /* Debug */ = {
293 | isa = XCBuildConfiguration;
294 | buildSettings = {
295 | };
296 | name = Debug;
297 | };
298 | C65584422534051500A440FD /* Release */ = {
299 | isa = XCBuildConfiguration;
300 | buildSettings = {
301 | };
302 | name = Release;
303 | };
304 | C65584562534053600A440FD /* Debug */ = {
305 | isa = XCBuildConfiguration;
306 | buildSettings = {
307 | ALWAYS_SEARCH_USER_PATHS = NO;
308 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
309 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
310 | CLANG_ANALYZER_NONNULL = YES;
311 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
313 | CLANG_CXX_LIBRARY = "libc++";
314 | CLANG_ENABLE_MODULES = YES;
315 | CLANG_ENABLE_OBJC_ARC = YES;
316 | CLANG_ENABLE_OBJC_WEAK = YES;
317 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
318 | CLANG_WARN_BOOL_CONVERSION = YES;
319 | CLANG_WARN_COMMA = YES;
320 | CLANG_WARN_CONSTANT_CONVERSION = YES;
321 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
322 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
323 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
324 | CLANG_WARN_EMPTY_BODY = YES;
325 | CLANG_WARN_ENUM_CONVERSION = YES;
326 | CLANG_WARN_INFINITE_RECURSION = YES;
327 | CLANG_WARN_INT_CONVERSION = YES;
328 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
329 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
330 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
331 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
332 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
333 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
334 | CLANG_WARN_STRICT_PROTOTYPES = YES;
335 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
336 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
337 | CLANG_WARN_UNREACHABLE_CODE = YES;
338 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
339 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
340 | CODE_SIGN_STYLE = Automatic;
341 | COMBINE_HIDPI_IMAGES = YES;
342 | COPY_PHASE_STRIP = NO;
343 | DEBUG_INFORMATION_FORMAT = dwarf;
344 | DEVELOPMENT_TEAM = 3A6K89K388;
345 | ENABLE_HARDENED_RUNTIME = YES;
346 | ENABLE_STRICT_OBJC_MSGSEND = YES;
347 | ENABLE_TESTABILITY = YES;
348 | GCC_C_LANGUAGE_STANDARD = gnu11;
349 | GCC_DYNAMIC_NO_PIC = NO;
350 | GCC_NO_COMMON_BLOCKS = YES;
351 | GCC_OPTIMIZATION_LEVEL = 0;
352 | GCC_PREPROCESSOR_DEFINITIONS = (
353 | "DEBUG=1",
354 | "$(inherited)",
355 | );
356 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
357 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
358 | GCC_WARN_UNDECLARED_SELECTOR = YES;
359 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
360 | GCC_WARN_UNUSED_FUNCTION = YES;
361 | GCC_WARN_UNUSED_VARIABLE = YES;
362 | INFOPLIST_FILE = macOS/Info.plist;
363 | LD_RUNPATH_SEARCH_PATHS = (
364 | "$(inherited)",
365 | "@executable_path/../Frameworks",
366 | );
367 | MACOSX_DEPLOYMENT_TARGET = 10.15;
368 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
369 | MTL_FAST_MATH = YES;
370 | ONLY_ACTIVE_ARCH = YES;
371 | PRODUCT_BUNDLE_IDENTIFIER = io.tyler.macOS;
372 | PRODUCT_NAME = "$(TARGET_NAME)";
373 | SDKROOT = macosx;
374 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
375 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
376 | SWIFT_VERSION = 5.0;
377 | };
378 | name = Debug;
379 | };
380 | C65584572534053600A440FD /* Release */ = {
381 | isa = XCBuildConfiguration;
382 | buildSettings = {
383 | ALWAYS_SEARCH_USER_PATHS = NO;
384 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
385 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
386 | CLANG_ANALYZER_NONNULL = YES;
387 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
388 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
389 | CLANG_CXX_LIBRARY = "libc++";
390 | CLANG_ENABLE_MODULES = YES;
391 | CLANG_ENABLE_OBJC_ARC = YES;
392 | CLANG_ENABLE_OBJC_WEAK = YES;
393 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
394 | CLANG_WARN_BOOL_CONVERSION = YES;
395 | CLANG_WARN_COMMA = YES;
396 | CLANG_WARN_CONSTANT_CONVERSION = YES;
397 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
398 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
399 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
400 | CLANG_WARN_EMPTY_BODY = YES;
401 | CLANG_WARN_ENUM_CONVERSION = YES;
402 | CLANG_WARN_INFINITE_RECURSION = YES;
403 | CLANG_WARN_INT_CONVERSION = YES;
404 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
405 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
406 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
407 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
408 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
409 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
410 | CLANG_WARN_STRICT_PROTOTYPES = YES;
411 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
412 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
413 | CLANG_WARN_UNREACHABLE_CODE = YES;
414 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
415 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
416 | CODE_SIGN_STYLE = Automatic;
417 | COMBINE_HIDPI_IMAGES = YES;
418 | COPY_PHASE_STRIP = NO;
419 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
420 | DEVELOPMENT_TEAM = 3A6K89K388;
421 | ENABLE_HARDENED_RUNTIME = YES;
422 | ENABLE_NS_ASSERTIONS = NO;
423 | ENABLE_STRICT_OBJC_MSGSEND = YES;
424 | GCC_C_LANGUAGE_STANDARD = gnu11;
425 | GCC_NO_COMMON_BLOCKS = YES;
426 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
427 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
428 | GCC_WARN_UNDECLARED_SELECTOR = YES;
429 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
430 | GCC_WARN_UNUSED_FUNCTION = YES;
431 | GCC_WARN_UNUSED_VARIABLE = YES;
432 | INFOPLIST_FILE = macOS/Info.plist;
433 | LD_RUNPATH_SEARCH_PATHS = (
434 | "$(inherited)",
435 | "@executable_path/../Frameworks",
436 | );
437 | MACOSX_DEPLOYMENT_TARGET = 10.15;
438 | MTL_ENABLE_DEBUG_INFO = NO;
439 | MTL_FAST_MATH = YES;
440 | PRODUCT_BUNDLE_IDENTIFIER = io.tyler.macOS;
441 | PRODUCT_NAME = "$(TARGET_NAME)";
442 | SDKROOT = macosx;
443 | SWIFT_COMPILATION_MODE = wholemodule;
444 | SWIFT_OPTIMIZATION_LEVEL = "-O";
445 | SWIFT_VERSION = 5.0;
446 | };
447 | name = Release;
448 | };
449 | C65584702534057600A440FD /* Debug */ = {
450 | isa = XCBuildConfiguration;
451 | buildSettings = {
452 | ALWAYS_SEARCH_USER_PATHS = NO;
453 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
454 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
455 | CLANG_ANALYZER_NONNULL = YES;
456 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
457 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
458 | CLANG_CXX_LIBRARY = "libc++";
459 | CLANG_ENABLE_MODULES = YES;
460 | CLANG_ENABLE_OBJC_ARC = YES;
461 | CLANG_ENABLE_OBJC_WEAK = YES;
462 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
463 | CLANG_WARN_BOOL_CONVERSION = YES;
464 | CLANG_WARN_COMMA = YES;
465 | CLANG_WARN_CONSTANT_CONVERSION = YES;
466 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
467 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
468 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
469 | CLANG_WARN_EMPTY_BODY = YES;
470 | CLANG_WARN_ENUM_CONVERSION = YES;
471 | CLANG_WARN_INFINITE_RECURSION = YES;
472 | CLANG_WARN_INT_CONVERSION = YES;
473 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
474 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
475 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
476 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
477 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
478 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
479 | CLANG_WARN_STRICT_PROTOTYPES = YES;
480 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
481 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
482 | CLANG_WARN_UNREACHABLE_CODE = YES;
483 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
484 | CODE_SIGN_STYLE = Automatic;
485 | COPY_PHASE_STRIP = NO;
486 | DEBUG_INFORMATION_FORMAT = dwarf;
487 | DEVELOPMENT_TEAM = 3A6K89K388;
488 | ENABLE_STRICT_OBJC_MSGSEND = YES;
489 | ENABLE_TESTABILITY = YES;
490 | GCC_C_LANGUAGE_STANDARD = gnu11;
491 | GCC_DYNAMIC_NO_PIC = NO;
492 | GCC_NO_COMMON_BLOCKS = YES;
493 | GCC_OPTIMIZATION_LEVEL = 0;
494 | GCC_PREPROCESSOR_DEFINITIONS = (
495 | "DEBUG=1",
496 | "$(inherited)",
497 | );
498 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
499 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
500 | GCC_WARN_UNDECLARED_SELECTOR = YES;
501 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
502 | GCC_WARN_UNUSED_FUNCTION = YES;
503 | GCC_WARN_UNUSED_VARIABLE = YES;
504 | INFOPLIST_FILE = iOS/Info.plist;
505 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
506 | LD_RUNPATH_SEARCH_PATHS = (
507 | "$(inherited)",
508 | "@executable_path/Frameworks",
509 | );
510 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
511 | MTL_FAST_MATH = YES;
512 | ONLY_ACTIVE_ARCH = YES;
513 | PRODUCT_BUNDLE_IDENTIFIER = io.tyler.iOS;
514 | PRODUCT_NAME = "$(TARGET_NAME)";
515 | SDKROOT = iphoneos;
516 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
517 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
518 | SWIFT_VERSION = 5.0;
519 | TARGETED_DEVICE_FAMILY = "1,2";
520 | };
521 | name = Debug;
522 | };
523 | C65584712534057600A440FD /* Release */ = {
524 | isa = XCBuildConfiguration;
525 | buildSettings = {
526 | ALWAYS_SEARCH_USER_PATHS = NO;
527 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
528 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
529 | CLANG_ANALYZER_NONNULL = YES;
530 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
531 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
532 | CLANG_CXX_LIBRARY = "libc++";
533 | CLANG_ENABLE_MODULES = YES;
534 | CLANG_ENABLE_OBJC_ARC = YES;
535 | CLANG_ENABLE_OBJC_WEAK = YES;
536 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
537 | CLANG_WARN_BOOL_CONVERSION = YES;
538 | CLANG_WARN_COMMA = YES;
539 | CLANG_WARN_CONSTANT_CONVERSION = YES;
540 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
541 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
542 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
543 | CLANG_WARN_EMPTY_BODY = YES;
544 | CLANG_WARN_ENUM_CONVERSION = YES;
545 | CLANG_WARN_INFINITE_RECURSION = YES;
546 | CLANG_WARN_INT_CONVERSION = YES;
547 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
548 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
549 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
550 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
551 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
552 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
553 | CLANG_WARN_STRICT_PROTOTYPES = YES;
554 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
555 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
556 | CLANG_WARN_UNREACHABLE_CODE = YES;
557 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
558 | CODE_SIGN_STYLE = Automatic;
559 | COPY_PHASE_STRIP = NO;
560 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
561 | DEVELOPMENT_TEAM = 3A6K89K388;
562 | ENABLE_NS_ASSERTIONS = NO;
563 | ENABLE_STRICT_OBJC_MSGSEND = YES;
564 | GCC_C_LANGUAGE_STANDARD = gnu11;
565 | GCC_NO_COMMON_BLOCKS = YES;
566 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
567 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
568 | GCC_WARN_UNDECLARED_SELECTOR = YES;
569 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
570 | GCC_WARN_UNUSED_FUNCTION = YES;
571 | GCC_WARN_UNUSED_VARIABLE = YES;
572 | INFOPLIST_FILE = iOS/Info.plist;
573 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
574 | LD_RUNPATH_SEARCH_PATHS = (
575 | "$(inherited)",
576 | "@executable_path/Frameworks",
577 | );
578 | MTL_ENABLE_DEBUG_INFO = NO;
579 | MTL_FAST_MATH = YES;
580 | PRODUCT_BUNDLE_IDENTIFIER = io.tyler.iOS;
581 | PRODUCT_NAME = "$(TARGET_NAME)";
582 | SDKROOT = iphoneos;
583 | SWIFT_COMPILATION_MODE = wholemodule;
584 | SWIFT_OPTIMIZATION_LEVEL = "-O";
585 | SWIFT_VERSION = 5.0;
586 | TARGETED_DEVICE_FAMILY = "1,2";
587 | VALIDATE_PRODUCT = YES;
588 | };
589 | name = Release;
590 | };
591 | /* End XCBuildConfiguration section */
592 |
593 | /* Begin XCConfigurationList section */
594 | C65584402534051500A440FD /* Build configuration list for PBXProject "Extensions" */ = {
595 | isa = XCConfigurationList;
596 | buildConfigurations = (
597 | C65584412534051500A440FD /* Debug */,
598 | C65584422534051500A440FD /* Release */,
599 | );
600 | defaultConfigurationIsVisible = 0;
601 | defaultConfigurationName = Release;
602 | };
603 | C65584552534053600A440FD /* Build configuration list for PBXNativeTarget "macOS" */ = {
604 | isa = XCConfigurationList;
605 | buildConfigurations = (
606 | C65584562534053600A440FD /* Debug */,
607 | C65584572534053600A440FD /* Release */,
608 | );
609 | defaultConfigurationIsVisible = 0;
610 | defaultConfigurationName = Release;
611 | };
612 | C655846F2534057600A440FD /* Build configuration list for PBXNativeTarget "iOS" */ = {
613 | isa = XCConfigurationList;
614 | buildConfigurations = (
615 | C65584702534057600A440FD /* Debug */,
616 | C65584712534057600A440FD /* Release */,
617 | );
618 | defaultConfigurationIsVisible = 0;
619 | defaultConfigurationName = Release;
620 | };
621 | /* End XCConfigurationList section */
622 | };
623 | rootObject = C655843D2534051500A440FD /* Project object */;
624 | }
625 |
--------------------------------------------------------------------------------
/macOS/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
--------------------------------------------------------------------------------