├── .gitignore
├── desk
├── table.png
├── Assets.xcassets
│ ├── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── desk.entitlements
├── EventMonitor.swift
├── Info.plist
├── AppDelegate.swift
├── DeskController
│ ├── ParticlePeripheral.swift
│ └── DeskConnect.swift
├── DeskViewController.swift
├── BinUtils.swift
└── Base.lproj
│ └── Main.storyboard
├── README.md
├── desk.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── forti.xcuserdatad
│ │ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── desk.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── podfile
├── deskTests
├── Info.plist
└── deskTests.swift
├── deskUITests
├── Info.plist
└── deskUITests.swift
└── Podfile.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | Pods
--------------------------------------------------------------------------------
/desk/table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fortidude/LinakDeskControl/HEAD/desk/table.png
--------------------------------------------------------------------------------
/desk/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LinakDeskControl
2 |
3 | Swift status bar app to control Ikea Idasen bluetooth desk
4 |
5 | Work in progress
6 |
--------------------------------------------------------------------------------
/desk.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/desk.xcodeproj/xcuserdata/forti.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/desk.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/desk.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/podfile:
--------------------------------------------------------------------------------
1 | # Podfile
2 | use_frameworks!
3 |
4 | target 'desk' do
5 | pod 'RxSwift', '~> 5'
6 | pod 'RxCocoa', '~> 5'
7 | end
8 |
9 | # RxTest and RxBlocking make the most sense in the context of unit/integration tests
10 | target 'deskTests' do
11 | pod 'RxBlocking', '~> 5'
12 | pod 'RxTest', '~> 5'
13 | end
--------------------------------------------------------------------------------
/desk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/desk/desk.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.device.bluetooth
8 |
9 | com.apple.security.files.user-selected.read-only
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/desk.xcodeproj/xcuserdata/forti.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | desk.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 7
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/desk/EventMonitor.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | public class EventMonitor {
4 | private var monitor: Any?
5 | private let mask: NSEvent.EventTypeMask
6 | private let handler: (NSEvent?) -> Void
7 |
8 | public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
9 | self.mask = mask
10 | self.handler = handler
11 | }
12 |
13 | deinit {
14 | stop()
15 | }
16 |
17 | public func start() {
18 | monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler)
19 | }
20 |
21 | public func stop() {
22 | if monitor != nil {
23 | NSEvent.removeMonitor(monitor!)
24 | monitor = nil
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/deskTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/deskUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - RxBlocking (5.1.1):
3 | - RxSwift (~> 5)
4 | - RxCocoa (5.1.1):
5 | - RxRelay (~> 5)
6 | - RxSwift (~> 5)
7 | - RxRelay (5.1.1):
8 | - RxSwift (~> 5)
9 | - RxSwift (5.1.1)
10 | - RxTest (5.1.1):
11 | - RxSwift (~> 5)
12 |
13 | DEPENDENCIES:
14 | - RxBlocking (~> 5)
15 | - RxCocoa (~> 5)
16 | - RxSwift (~> 5)
17 | - RxTest (~> 5)
18 |
19 | SPEC REPOS:
20 | trunk:
21 | - RxBlocking
22 | - RxCocoa
23 | - RxRelay
24 | - RxSwift
25 | - RxTest
26 |
27 | SPEC CHECKSUMS:
28 | RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4
29 | RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601
30 | RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9
31 | RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178
32 | RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa
33 |
34 | PODFILE CHECKSUM: 7ef378dbdd4c62f31072a67bf2f217617ea1e077
35 |
36 | COCOAPODS: 1.9.1
37 |
--------------------------------------------------------------------------------
/deskTests/deskTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // deskTests.swift
3 | // deskTests
4 | //
5 | // Created by Forti on 18/05/2020.
6 | // Copyright © 2020 Forti. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import desk
11 |
12 | class deskTests: XCTestCase {
13 |
14 | override func setUpWithError() throws {
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDownWithError() throws {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | }
21 |
22 | func testExample() throws {
23 | // This is an example of a functional test case.
24 | // Use XCTAssert and related functions to verify your tests produce the correct results.
25 | }
26 |
27 | func testPerformanceExample() throws {
28 | // This is an example of a performance test case.
29 | self.measure {
30 | // Put the code you want to measure the time of here.
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/desk/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 |
--------------------------------------------------------------------------------
/desk/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSBluetoothPeripheralUsageDescription
6 | App uses Bluetooth to control desk
7 | LSUIElement
8 |
9 | CFBundleDevelopmentRegion
10 | $(DEVELOPMENT_LANGUAGE)
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIconFile
14 |
15 | CFBundleIdentifier
16 | $(PRODUCT_BUNDLE_IDENTIFIER)
17 | CFBundleInfoDictionaryVersion
18 | 6.0
19 | CFBundleName
20 | $(PRODUCT_NAME)
21 | CFBundlePackageType
22 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
23 | CFBundleShortVersionString
24 | 1.0
25 | CFBundleVersion
26 | 1
27 | LSMinimumSystemVersion
28 | $(MACOSX_DEPLOYMENT_TARGET)
29 | NSHumanReadableCopyright
30 | Copyright © 2020 Forti. All rights reserved.
31 | NSMainStoryboardFile
32 | Main
33 | NSPrincipalClass
34 | NSApplication
35 | NSSupportsAutomaticTermination
36 |
37 | NSSupportsSuddenTermination
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/deskUITests/deskUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // deskUITests.swift
3 | // deskUITests
4 | //
5 | // Created by Forti on 18/05/2020.
6 | // Copyright © 2020 Forti. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class deskUITests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 |
16 | // In UI tests it is usually best to stop immediately when a failure occurs.
17 | continueAfterFailure = false
18 |
19 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
20 | }
21 |
22 | override func tearDownWithError() throws {
23 | // Put teardown code here. This method is called after the invocation of each test method in the class.
24 | }
25 |
26 | func testExample() throws {
27 | // UI tests must launch the application that they test.
28 | let app = XCUIApplication()
29 | app.launch()
30 |
31 | // Use recording to get started writing UI tests.
32 | // Use XCTAssert and related functions to verify your tests produce the correct results.
33 | }
34 |
35 | func testLaunchPerformance() throws {
36 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
37 | // This measures how long it takes to launch your application.
38 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) {
39 | XCUIApplication().launch()
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/desk/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // desk
4 | //
5 | // Created by Forti on 18/05/2020.
6 | // Copyright © 2020 Forti. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import SwiftUI
11 |
12 | @NSApplicationMain
13 | class AppDelegate: NSObject, NSApplicationDelegate {
14 |
15 | let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
16 | let popover = NSPopover()
17 |
18 | var eventMonitor: EventMonitor?
19 |
20 | func applicationDidFinishLaunching(_ aNotification: Notification) {
21 | if let button = statusItem.button {
22 | button.image = NSImage(named: NSImage.Name("table"))
23 | button.action = #selector(togglePopover(_:))
24 | }
25 |
26 | popover.contentViewController = DeskViewController.freshController()
27 |
28 | eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
29 | if let strongSelf = self, strongSelf.popover.isShown {
30 | strongSelf.closePopover(sender: event)
31 | }
32 | }
33 | }
34 |
35 |
36 | @objc func togglePopover(_ sender: Any?) {
37 | if popover.isShown {
38 | closePopover(sender: sender)
39 | } else {
40 | showPopover(sender: sender)
41 | }
42 | }
43 |
44 | func showPopover(sender: Any?) {
45 | if let button = statusItem.button {
46 | popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
47 | }
48 |
49 | eventMonitor?.start()
50 | }
51 |
52 | func closePopover(sender: Any?) {
53 | popover.performClose(sender)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/desk/DeskController/ParticlePeripheral.swift:
--------------------------------------------------------------------------------
1 | import CoreBluetooth
2 |
3 | class ParticlePeripheral: NSObject {
4 |
5 | /// MARK: - Particle services and charcteristics Identifiers
6 |
7 | //GENERIC_ACCESS = "00001800-0000-1000-8000-00805F9B34FB"
8 | //REFERENCE_INPUT = "99FA0030-338A-1024-8A49-009C0215F78A"
9 | //REFERENCE_OUTPUT = "99FA0020-338A-1024-8A49-009C0215F78A"
10 | //DPG = "99FA0010-338A-1024-8A49-009C0215F78A"
11 | //CONTROL = "99FA0001-338A-1024-8A49-009C0215F78A"
12 |
13 | // MOVE_1_DOWN = 70
14 | // MOVE_1_UP = 71
15 | //
16 | // UNDEFINED = 254 ## used as stop
17 | // STOP_MOVING = 255
18 |
19 | /**
20 | All commented are not working
21 | */
22 |
23 | // public static let serviceGenericAccess = CBUUID.init(string: "00001800-0000-1000-8000-00805F9B34FB")
24 | // public static let serviceReferenceInput = CBUUID.init(string: "99FA0030-338A-1024-8A49-009C0215F78A")
25 | // public static let serviceDPG = CBUUID.init(string: "99FA0010-338A-1024-8A49-009C0215F78A") // DPG (physical control
26 | public static let servicePosition = CBUUID.init(string: "99FA0020-338A-1024-8A49-009C0215F78A") // one
27 | public static let serviceControl = CBUUID.init(string: "99FA0001-338A-1024-8A49-009C0215F78A") // control
28 |
29 | // public static let characteristicDeviceName = CBUUID.init(string: "00002A00-0000-1000-8000-00805F9B34FB")
30 | // public static let characteristicServiceChanges = CBUUID.init(string: "00002A05-0000-1000-8000-00805F9B34FB")
31 | // public static let characteristicManufacturer = CBUUID.init(string: "00002A29-0000-1000-8000-00805F9B34FB")
32 | // public static let characteristicModelNumber = CBUUID.init(string: "00002A24-0000-1000-8000-00805F9B34FB")
33 | // public static let characteristicError = CBUUID.init(string: "99FA0003-338A-1024-8A49-009C0215F78A")
34 | // public static let characteristicDPG = CBUUID.init(string: "99FA0011-338A-1024-8A49-009C0215F78A")
35 | // public static let characteristicMove = CBUUID.init(string: "99FA0031-338A-1024-8A49-009C0215F78A")
36 | public static let characteristicPosition = CBUUID.init(string: "99FA0021-338A-1024-8A49-009C0215F78A")
37 | public static let characteristicControl = CBUUID.init(string: "99FA0002-338A-1024-8A49-009C0215F78A")
38 |
39 | public static let allServices = [
40 | // ParticlePeripheral.serviceGenericAccess,
41 | // ParticlePeripheral.serviceReferenceInput,
42 | // ParticlePeripheral.serviceDPG,
43 | ParticlePeripheral.servicePosition,
44 | ParticlePeripheral.serviceControl
45 | ]
46 |
47 | public static let allCharacteristics = [
48 | // ParticlePeripheral.characteristicDeviceName,
49 | // ParticlePeripheral.characteristicServiceChanges,
50 | // ParticlePeripheral.characteristicManufacturer,
51 | // ParticlePeripheral.characteristicModelNumber,
52 | // ParticlePeripheral.characteristicError,
53 | // ParticlePeripheral.characteristicDPG,
54 | // ParticlePeripheral.characteristicHighSpeed,
55 | ParticlePeripheral.characteristicPosition,
56 | ParticlePeripheral.characteristicControl
57 |
58 | ]
59 | }
60 |
--------------------------------------------------------------------------------
/desk/DeskViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeskViewcontroller.swift
3 | // desk
4 | //
5 | // Created by Forti on 18/05/2020.
6 | // Copyright © 2020 Forti. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class DeskViewController: NSViewController {
12 | private var deskConnect: DeskConnect!
13 | private var longClick: NSPressGestureRecognizer?
14 | private var userDefaults: UserDefaults?
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 | self.deskConnect = DeskConnect()
19 |
20 | self.buttonUp.isEnabled = false
21 | self.buttonDown.isEnabled = false
22 |
23 | self.deskConnect.currentPosition.asObservable().subscribe({ value in
24 | if let position = value.element {
25 | if (position > 0) {
26 | self.currentValue.stringValue = String(format:"%.1f", position)
27 | self.currentPosition = position
28 | }
29 | }
30 |
31 | }).disposed(by: self.deskConnect.dispose)
32 |
33 | self.deskConnect.deviceName.asObservable().subscribe({ value in
34 | self.deskName.stringValue = "\(value.element ?? "Unknown desk")"
35 | self.buttonUp.isEnabled = true
36 | self.buttonDown.isEnabled = true
37 | }).disposed(by: self.deskConnect.dispose)
38 |
39 | self.userDefaults = UserDefaults.init(suiteName: "positions")
40 | self.initSavedPositions()
41 |
42 | self.buttonUp.sendAction(on: .leftMouseDown)
43 | self.buttonUp.isContinuous = true
44 | self.buttonUp.setPeriodicDelay(0, interval: 0.7)
45 |
46 | self.buttonDown.sendAction(on: .leftMouseDown)
47 | self.buttonDown.isContinuous = true
48 | self.buttonDown.setPeriodicDelay(0, interval: 0.7)
49 | }
50 |
51 |
52 | var currentPosition: Double!
53 | @IBOutlet var currentValue: NSTextField!
54 | @IBOutlet var deskName: NSTextField!
55 | @IBOutlet var buttonUp: NSButton!
56 | @IBOutlet var buttonDown: NSButton!
57 |
58 | @IBOutlet var buttonMoveToSit: NSButton!
59 | @IBOutlet var buttonMoveToStand: NSButton!
60 |
61 | @IBOutlet var sitPosition: NSTextField!
62 | @IBOutlet var standPosition: NSTextField!
63 |
64 | var isMovingToPositionValue = false
65 | var moveToPositionValue = 70.0
66 |
67 | var isWaitingForSecondPress = false
68 | @objc func stopMoving() {
69 | self.deskConnect.stopMoving()
70 | self.isWaitingForSecondPress = true
71 | }
72 |
73 | @objc func clearIsWairingForSecondPress() {
74 | self.isWaitingForSecondPress = false
75 | }
76 |
77 | func handleStopMovingIfSingleClick() {
78 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(clearIsWairingForSecondPress), object: nil)
79 |
80 | if (self.isWaitingForSecondPress == false) {
81 | perform(#selector(stopMoving), with: nil, afterDelay: 0.18)
82 | }
83 |
84 | perform(#selector(clearIsWairingForSecondPress), with: nil, afterDelay: 0.2)
85 | }
86 |
87 | @IBAction func up(_ sender: NSButton) {
88 | self.deskConnect.moveUp()
89 | self.handleStopMovingIfSingleClick()
90 | }
91 |
92 | @IBAction func down(_ sender: NSButton) {
93 | self.deskConnect.moveDown()
94 | self.handleStopMovingIfSingleClick()
95 | }
96 |
97 | @IBAction func saveAsSitPosition(_ sender: NSButton) {
98 | self.userDefaults?.set(self.currentPosition, forKey: "sit-position")
99 | self.sitPosition.stringValue = String(format:"%.1f", self.currentPosition)
100 | }
101 |
102 | @IBAction func saveAsStandPosition(_ sender: NSButton) {
103 | self.userDefaults?.set(self.currentPosition, forKey: "stand-position")
104 | self.standPosition.stringValue = String(format:"%.1f", self.currentPosition)
105 | }
106 |
107 | @IBAction func moveToSitPosition(_ sender: NSButton) {
108 | let position = self.userDefaults?.double(forKey: "sit-position") ?? .nan
109 | if (position != .nan) {
110 | self.deskConnect.moveToPosition(position: position)
111 | }
112 | }
113 |
114 | @IBAction func moveToStandPosition(_ sender: NSButton) {
115 | let position = self.userDefaults?.double(forKey: "stand-position") ?? .nan
116 | if (position != .nan) {
117 | self.deskConnect.moveToPosition(position: position)
118 | }
119 | }
120 |
121 | @IBAction func stop(_ sender: NSButton) {
122 | self.deskConnect.stopMoving()
123 | }
124 |
125 | private func initSavedPositions() {
126 | if let sitPosition = self.userDefaults?.double(forKey: "sit-position") {
127 | self.sitPosition.stringValue = String(format:"%.1f", sitPosition)
128 | }
129 |
130 | if let standPosition = self.userDefaults?.double(forKey: "stand-position") {
131 | self.standPosition.stringValue = String(format:"%.1f", standPosition)
132 | }
133 | }
134 | }
135 |
136 | extension DeskViewController {
137 | // MARK: Storyboard instantiation
138 | static func freshController() -> DeskViewController {
139 | //1.
140 | let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
141 | //2.
142 | let identifier = NSStoryboard.SceneIdentifier("DeskViewController")
143 | //3.
144 | guard let viewcontroller = storyboard.instantiateController(withIdentifier: identifier) as? DeskViewController else {
145 | fatalError("Why cant i find QuotesViewController? - Check Main.storyboard")
146 | }
147 | return viewcontroller
148 | }
149 | }
150 |
151 |
--------------------------------------------------------------------------------
/desk/DeskController/DeskConnect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Connect.swift
3 | // desk
4 | //
5 | // Created by Forti on 05/06/2020.
6 | // Copyright © 2020 Forti. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CoreBluetooth
11 | import RxSwift
12 | import RxRelay
13 |
14 | let deviceNamePattern = #"Desk[\s0-9].*"#;
15 | class DeskConnect: NSObject, CBPeripheralDelegate, CBCentralManagerDelegate {
16 | private var centralManager: CBCentralManager!
17 | private var peripheral: CBPeripheral!
18 |
19 | private var characteristicPosition: CBCharacteristic!
20 | private var characteristicControl: CBCharacteristic!
21 |
22 | private var valueMoveUp = pack("(value: "")
30 | let currentPosition = BehaviorRelay(value: 0)
31 | let dispose = DisposeBag()
32 |
33 | let deskOffset = 62.5
34 |
35 | override init() {
36 | super.init()
37 | centralManager = CBCentralManager(delegate: self, queue: nil)
38 | }
39 |
40 | func centralManagerDidUpdateState(_ central: CBCentralManager) {
41 | if (central.state != .poweredOn) {
42 | print("Central is not powered on. Bluetooth disabled? @TODO")
43 | } else {
44 | self.centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
45 | }
46 | }
47 |
48 | /**
49 | ON DISCOVER
50 | */
51 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
52 | // @TODO
53 | // DeviceName is just a test. let's do collection list with option to connect by user
54 | let deviceName = (advertisementData as NSDictionary).object(forKey: CBAdvertisementDataLocalNameKey) as? NSString
55 | let isDesk = deviceName?.range(of: deviceNamePattern, options:.regularExpression)
56 | if (isDesk?.location != NSNotFound && deviceName != nil) {
57 | self.centralManager.stopScan()
58 |
59 | if let dn = deviceName {
60 | self.deviceName.accept(dn as String)
61 | }
62 |
63 | self.peripheral = peripheral
64 | self.peripheral.delegate = self
65 | self.centralManager.connect(self.peripheral, options: nil)
66 | }
67 | }
68 |
69 | /**
70 | ON CONNECT
71 | */
72 | func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
73 | print(peripheral)
74 |
75 | self.peripheral.discoverServices(ParticlePeripheral.allServices)
76 | }
77 |
78 | /**
79 | ON SERVICES
80 | */
81 | func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
82 | if let services = peripheral.services {
83 | for service in services {
84 | self.peripheral.discoverCharacteristics(ParticlePeripheral.allCharacteristics, for: service)
85 | }
86 | }
87 | }
88 |
89 | /**
90 | ON CHARACTERISTICS
91 | */
92 | func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
93 | if let characteristics = service.characteristics {
94 | for characteristic in characteristics {
95 | self.peripheral.readValue(for: characteristic)
96 | self.peripheral.setNotifyValue(true, for: characteristic)
97 |
98 |
99 | if (characteristic.uuid.uuidString == ParticlePeripheral.characteristicControl.uuidString) {
100 | self.characteristicControl = characteristic
101 | }
102 |
103 | if (characteristic.uuid.uuidString == ParticlePeripheral.characteristicPosition.uuidString) {
104 | self.characteristicPosition = characteristic
105 | self.updatePosition(characteristic: characteristic)
106 | }
107 | }
108 | }
109 | }
110 |
111 | func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
112 | self.updatePosition(characteristic: characteristic)
113 | }
114 |
115 | func turnOnContinous() {
116 |
117 | }
118 |
119 | func moveUp() {
120 | print("up")
121 | self.peripheral.writeValue(Data(self.valueMoveUp), for: self.characteristicControl, type: CBCharacteristicWriteType.withResponse)
122 | }
123 |
124 | func moveDown() {
125 | print("down")
126 | self.peripheral.writeValue(Data(self.valueMoveDown), for: self.characteristicControl, type: CBCharacteristicWriteType.withResponse)
127 | }
128 |
129 | private func updatePosition(characteristic: CBCharacteristic) {
130 | if (characteristic.value != nil && characteristic.uuid.uuidString == self.characteristicPosition.uuid.uuidString) {
131 | let byteArray = [UInt8](characteristic.value!)
132 | if (byteArray.indices.contains(0) && byteArray.indices.contains(1)) {
133 | do {
134 | let positionWrapped = try unpack(" (requiredPosition - 0.75) && formattedPosition < (requiredPosition + 0.75)) {
144 | self.moveToPositionTimer?.invalidate()
145 | self.moveToPositionValue = nil
146 | self.stopMoving()
147 | }
148 | }
149 | }
150 | } catch let error as NSError {
151 | print("Error, update position: \(error)")
152 | }
153 | }
154 | }
155 | }
156 |
157 | @objc func stopMoving() {
158 | self.peripheral.writeValue(Data(self.valueStopMove), for: self.characteristicControl, type: CBCharacteristicWriteType.withResponse)
159 | }
160 |
161 | func moveToPosition(position: Double) {
162 | self.moveToPositionValue = position
163 | self.handleMoveToPosition()
164 |
165 | self.moveToPositionTimer = Timer.scheduledTimer(withTimeInterval: 0.7, repeats: true) { (Timer) in
166 | print("moving", self.currentPosition.value)
167 | if (self.moveToPositionValue == nil) {
168 | Timer.invalidate()
169 | } else {
170 | self.handleMoveToPosition()
171 | }
172 | }
173 | }
174 |
175 | private func handleMoveToPosition() {
176 | let positionRequired = self.moveToPositionValue ?? .nan
177 | if (positionRequired < self.currentPosition.value) {
178 | self.moveDown()
179 | } else if (positionRequired > self.currentPosition.value) {
180 | self.moveUp()
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/desk/BinUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BinUtils.swift
3 | // BinUtils
4 | //
5 | // Created by Nicolas Seriot on 12/03/16.
6 | // Copyright © 2016 Nicolas Seriot. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CoreFoundation
11 |
12 | // MARK: protocol UnpackedType
13 |
14 | public protocol Unpackable {}
15 |
16 | extension NSString: Unpackable {}
17 | extension Bool: Unpackable {}
18 | extension Int: Unpackable {}
19 | extension Double: Unpackable {}
20 |
21 | // MARK: protocol DataConvertible
22 |
23 | protocol DataConvertible {}
24 |
25 | extension DataConvertible {
26 |
27 | init?(data: Data) {
28 | guard data.count == MemoryLayout.size else { return nil }
29 | self = data.withUnsafeBytes { $0.load(as: Self.self) }
30 | }
31 |
32 | init?(bytes: [UInt8]) {
33 | let data = Data(bytes)
34 | self.init(data:data)
35 | }
36 |
37 | var data: Data {
38 | var value = self
39 | return Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
40 | }
41 | }
42 |
43 | extension Bool : DataConvertible { }
44 |
45 | extension Int8 : DataConvertible { }
46 | extension Int16 : DataConvertible { }
47 | extension Int32 : DataConvertible { }
48 | extension Int64 : DataConvertible { }
49 |
50 | extension UInt8 : DataConvertible { }
51 | extension UInt16 : DataConvertible { }
52 | extension UInt32 : DataConvertible { }
53 | extension UInt64 : DataConvertible { }
54 |
55 | extension Float32 : DataConvertible { }
56 | extension Float64 : DataConvertible { }
57 |
58 | // MARK: String extension
59 |
60 | extension String {
61 | subscript (from:Int, to:Int) -> String {
62 | return NSString(string: self).substring(with: NSMakeRange(from, to-from))
63 | }
64 | }
65 |
66 | // MARK: Data extension
67 |
68 | extension Data {
69 | var bytes : [UInt8] {
70 | return [UInt8](self)
71 | }
72 | }
73 |
74 | // MARK: functions
75 |
76 | public func hexlify(_ data:Data) -> String {
77 |
78 | // similar to hexlify() in Python's binascii module
79 | // https://docs.python.org/2/library/binascii.html
80 |
81 | var s = String()
82 | var byte: UInt8 = 0
83 |
84 | for i in 0 ..< data.count {
85 | NSData(data: data).getBytes(&byte, range: NSMakeRange(i, 1))
86 | s = s.appendingFormat("%02x", byte)
87 | }
88 |
89 | return s as String
90 | }
91 |
92 | public func unhexlify(_ string:String) -> Data? {
93 |
94 | // similar to unhexlify() in Python's binascii module
95 | // https://docs.python.org/2/library/binascii.html
96 |
97 | let s = string.uppercased().replacingOccurrences(of: " ", with: "")
98 |
99 | let nonHexCharacterSet = CharacterSet(charactersIn: "0123456789ABCDEF").inverted
100 | if let range = s.rangeOfCharacter(from: nonHexCharacterSet) {
101 | print("-- found non hex character at range \(range)")
102 | return nil
103 | }
104 |
105 | var data = Data(capacity: s.count / 2)
106 |
107 | for i in stride(from: 0, to:s.count, by:2) {
108 | let byteString = s[i, i+2]
109 | let byte = UInt8(byteString.withCString { strtoul($0, nil, 16) })
110 | data.append([byte] as [UInt8], count: 1)
111 | }
112 |
113 | return data
114 | }
115 |
116 | func readIntegerType(_ type:T.Type, bytes:[UInt8], loc:inout Int) -> T {
117 | let size = MemoryLayout.size
118 | let sub = Array(bytes[loc..<(loc+size)])
119 | loc += size
120 | return T(bytes: sub)!
121 | }
122 |
123 | func readFloatingPointType(_ type:T.Type, bytes:[UInt8], loc:inout Int, isBigEndian:Bool) -> T {
124 | let size = MemoryLayout.size
125 | let sub = Array(bytes[loc..<(loc+size)])
126 | loc += size
127 | let sub_ = isBigEndian ? sub.reversed() : sub
128 | return T(bytes: sub_)!
129 | }
130 |
131 | func isBigEndianFromMandatoryByteOrderFirstCharacter(_ format:String) -> Bool {
132 |
133 | guard let firstChar = format.first else { assertionFailure("empty format"); return false }
134 |
135 | let s = NSString(string: String(firstChar))
136 | let c = s.substring(to: 1)
137 |
138 | if c == "@" { assertionFailure("native size and alignment is unsupported") }
139 |
140 | if c == "=" || c == "<" { return false }
141 | if c == ">" || c == "!" { return true }
142 |
143 | assertionFailure("format '\(format)' first character must be among '=<>!'")
144 |
145 | return false
146 | }
147 |
148 | // akin to struct.calcsize(fmt)
149 | func numberOfBytesInFormat(_ format:String) -> Int {
150 |
151 | var numberOfBytes = 0
152 |
153 | var n = 0 // repeat counter
154 |
155 | var mutableFormat = format
156 |
157 | while !mutableFormat.isEmpty {
158 |
159 | let c = mutableFormat.remove(at: mutableFormat.startIndex)
160 |
161 | if let i = Int(String(c)) , 0...9 ~= i {
162 | if n > 0 { n *= 10 }
163 | n += i
164 | continue
165 | }
166 |
167 | if c == "s" {
168 | numberOfBytes += max(n,1)
169 | n = 0
170 | continue
171 | }
172 |
173 | let repeatCount = max(n,1)
174 |
175 | switch(c) {
176 |
177 | case "@", "<", "=", ">", "!", " ":
178 | ()
179 | case "c", "b", "B", "x", "?":
180 | numberOfBytes += 1 * repeatCount
181 | case "h", "H":
182 | numberOfBytes += 2 * repeatCount
183 | case "i", "l", "I", "L", "f":
184 | numberOfBytes += 4 * repeatCount
185 | case "q", "Q", "d":
186 | numberOfBytes += 8 * repeatCount
187 | case "P":
188 | numberOfBytes += MemoryLayout.size * repeatCount
189 | default:
190 | assertionFailure("-- unsupported format \(c)")
191 | }
192 |
193 | n = 0
194 | }
195 |
196 | return numberOfBytes
197 | }
198 |
199 | func formatDoesMatchDataLength(_ format:String, data:Data) -> Bool {
200 | let sizeAccordingToFormat = numberOfBytesInFormat(format)
201 | let dataLength = data.count
202 | if sizeAccordingToFormat != dataLength {
203 | print("format \"\(format)\" expects \(sizeAccordingToFormat) bytes but data is \(dataLength) bytes")
204 | return false
205 | }
206 |
207 | return true
208 | }
209 |
210 | /*
211 | pack() and unpack() should behave as Python's struct module https://docs.python.org/2/library/struct.html BUT:
212 | - native size and alignment '@' is not supported
213 | - as a consequence, the byte order specifier character is mandatory and must be among "=<>!"
214 | - native byte order '=' assumes a little-endian system (eg. Intel x86)
215 | - Pascal strings 'p' and native pointers 'P' are not supported
216 | */
217 |
218 | public enum BinUtilsError: Error {
219 | case formatDoesMatchDataLength(format:String, dataSize:Int)
220 | case unsupportedFormat(character:Character)
221 | }
222 |
223 | public func pack(_ format:String, _ objects:[Any], _ stringEncoding:String.Encoding=String.Encoding.windowsCP1252) -> Data {
224 |
225 | var objectsQueue = objects
226 |
227 | var mutableFormat = format
228 |
229 | var mutableData = Data()
230 |
231 | var isBigEndian = false
232 |
233 | let firstCharacter = mutableFormat.remove(at: mutableFormat.startIndex)
234 |
235 | switch(firstCharacter) {
236 | case "<", "=":
237 | isBigEndian = false
238 | case ">", "!":
239 | isBigEndian = true
240 | case "@":
241 | assertionFailure("native size and alignment '@' is unsupported'")
242 | default:
243 | assertionFailure("unsupported format chacracter'")
244 | }
245 |
246 | var n = 0 // repeat counter
247 |
248 | while !mutableFormat.isEmpty {
249 |
250 | let c = mutableFormat.remove(at: mutableFormat.startIndex)
251 |
252 | if let i = Int(String(c)) , 0...9 ~= i {
253 | if n > 0 { n *= 10 }
254 | n += i
255 | continue
256 | }
257 |
258 | var o : Any = 0
259 |
260 | if c == "s" {
261 | o = objectsQueue.remove(at: 0)
262 |
263 | guard let stringData = (o as! String).data(using: .utf8) else { assertionFailure(); return Data() }
264 | var bytes = stringData.bytes
265 |
266 | let expectedSize = max(1, n)
267 |
268 | // pad ...
269 | while bytes.count < expectedSize { bytes.append(0x00) }
270 |
271 | // ... or trunk
272 | if bytes.count > expectedSize { bytes = Array(bytes[0.. [Unpackable] {
338 |
339 | assert(CFByteOrderGetCurrent() == 1 /* CFByteOrderLittleEndian */, "\(#file) assumes little endian, but host is big endian")
340 |
341 | let isBigEndian = isBigEndianFromMandatoryByteOrderFirstCharacter(format)
342 |
343 | if formatDoesMatchDataLength(format, data: data) == false {
344 | throw BinUtilsError.formatDoesMatchDataLength(format:format, dataSize:data.count)
345 | }
346 |
347 | var a : [Unpackable] = []
348 |
349 | var loc = 0
350 |
351 | let bytes = data.bytes
352 |
353 | var n = 0 // repeat counter
354 |
355 | var mutableFormat = format
356 |
357 | mutableFormat.remove(at: mutableFormat.startIndex) // consume byte-order specifier
358 |
359 | while !mutableFormat.isEmpty {
360 |
361 | let c = mutableFormat.remove(at: mutableFormat.startIndex)
362 |
363 | if let i = Int(String(c)) , 0...9 ~= i {
364 | if n > 0 { n *= 10 }
365 | n += i
366 | continue
367 | }
368 |
369 | if c == "s" {
370 | let length = max(n,1)
371 | let sub = Array(bytes[loc.. /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
333 | showEnvVarsInLog = 0;
334 | };
335 | B8BEB05CD77D4B2DB12B1C87 /* [CP] Check Pods Manifest.lock */ = {
336 | isa = PBXShellScriptBuildPhase;
337 | buildActionMask = 2147483647;
338 | files = (
339 | );
340 | inputFileListPaths = (
341 | );
342 | inputPaths = (
343 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
344 | "${PODS_ROOT}/Manifest.lock",
345 | );
346 | name = "[CP] Check Pods Manifest.lock";
347 | outputFileListPaths = (
348 | );
349 | outputPaths = (
350 | "$(DERIVED_FILE_DIR)/Pods-deskTests-checkManifestLockResult.txt",
351 | );
352 | runOnlyForDeploymentPostprocessing = 0;
353 | shellPath = /bin/sh;
354 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
355 | showEnvVarsInLog = 0;
356 | };
357 | C4CA290866A3904FBFBFFFFC /* [CP] Embed Pods Frameworks */ = {
358 | isa = PBXShellScriptBuildPhase;
359 | buildActionMask = 2147483647;
360 | files = (
361 | );
362 | inputFileListPaths = (
363 | "${PODS_ROOT}/Target Support Files/Pods-deskTests/Pods-deskTests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
364 | );
365 | name = "[CP] Embed Pods Frameworks";
366 | outputFileListPaths = (
367 | "${PODS_ROOT}/Target Support Files/Pods-deskTests/Pods-deskTests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
368 | );
369 | runOnlyForDeploymentPostprocessing = 0;
370 | shellPath = /bin/sh;
371 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-deskTests/Pods-deskTests-frameworks.sh\"\n";
372 | showEnvVarsInLog = 0;
373 | };
374 | F3D2B8907716502B0FF5881A /* [CP] Embed Pods Frameworks */ = {
375 | isa = PBXShellScriptBuildPhase;
376 | buildActionMask = 2147483647;
377 | files = (
378 | );
379 | inputFileListPaths = (
380 | "${PODS_ROOT}/Target Support Files/Pods-desk/Pods-desk-frameworks-${CONFIGURATION}-input-files.xcfilelist",
381 | );
382 | name = "[CP] Embed Pods Frameworks";
383 | outputFileListPaths = (
384 | "${PODS_ROOT}/Target Support Files/Pods-desk/Pods-desk-frameworks-${CONFIGURATION}-output-files.xcfilelist",
385 | );
386 | runOnlyForDeploymentPostprocessing = 0;
387 | shellPath = /bin/sh;
388 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-desk/Pods-desk-frameworks.sh\"\n";
389 | showEnvVarsInLog = 0;
390 | };
391 | /* End PBXShellScriptBuildPhase section */
392 |
393 | /* Begin PBXSourcesBuildPhase section */
394 | 89BCA3402472E51B009C815D /* Sources */ = {
395 | isa = PBXSourcesBuildPhase;
396 | buildActionMask = 2147483647;
397 | files = (
398 | 89B309CB2475D94300204950 /* BinUtils.swift in Sources */,
399 | 89BCA37B2472F5A5009C815D /* DeskViewController.swift in Sources */,
400 | 89BCA3482472E51B009C815D /* AppDelegate.swift in Sources */,
401 | 896C0E80248BE5D100578941 /* EventMonitor.swift in Sources */,
402 | 892FB916248AC2EA0055E2D1 /* DeskConnect.swift in Sources */,
403 | 89BCA37F24730B27009C815D /* ParticlePeripheral.swift in Sources */,
404 | );
405 | runOnlyForDeploymentPostprocessing = 0;
406 | };
407 | 89BCA3552472E51C009C815D /* Sources */ = {
408 | isa = PBXSourcesBuildPhase;
409 | buildActionMask = 2147483647;
410 | files = (
411 | 89BCA35E2472E51C009C815D /* deskTests.swift in Sources */,
412 | );
413 | runOnlyForDeploymentPostprocessing = 0;
414 | };
415 | 89BCA3602472E51C009C815D /* Sources */ = {
416 | isa = PBXSourcesBuildPhase;
417 | buildActionMask = 2147483647;
418 | files = (
419 | 89BCA3692472E51C009C815D /* deskUITests.swift in Sources */,
420 | );
421 | runOnlyForDeploymentPostprocessing = 0;
422 | };
423 | /* End PBXSourcesBuildPhase section */
424 |
425 | /* Begin PBXTargetDependency section */
426 | 89BCA35B2472E51C009C815D /* PBXTargetDependency */ = {
427 | isa = PBXTargetDependency;
428 | target = 89BCA3432472E51B009C815D /* desk */;
429 | targetProxy = 89BCA35A2472E51C009C815D /* PBXContainerItemProxy */;
430 | };
431 | 89BCA3662472E51C009C815D /* PBXTargetDependency */ = {
432 | isa = PBXTargetDependency;
433 | target = 89BCA3432472E51B009C815D /* desk */;
434 | targetProxy = 89BCA3652472E51C009C815D /* PBXContainerItemProxy */;
435 | };
436 | /* End PBXTargetDependency section */
437 |
438 | /* Begin PBXVariantGroup section */
439 | 89BCA3502472E51C009C815D /* Main.storyboard */ = {
440 | isa = PBXVariantGroup;
441 | children = (
442 | 89BCA3512472E51C009C815D /* Base */,
443 | );
444 | name = Main.storyboard;
445 | sourceTree = "";
446 | };
447 | /* End PBXVariantGroup section */
448 |
449 | /* Begin XCBuildConfiguration section */
450 | 89BCA36B2472E51C009C815D /* Debug */ = {
451 | isa = XCBuildConfiguration;
452 | buildSettings = {
453 | ALWAYS_SEARCH_USER_PATHS = NO;
454 | CLANG_ANALYZER_NONNULL = YES;
455 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
456 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
457 | CLANG_CXX_LIBRARY = "libc++";
458 | CLANG_ENABLE_MODULES = YES;
459 | CLANG_ENABLE_OBJC_ARC = YES;
460 | CLANG_ENABLE_OBJC_WEAK = YES;
461 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
462 | CLANG_WARN_BOOL_CONVERSION = YES;
463 | CLANG_WARN_COMMA = YES;
464 | CLANG_WARN_CONSTANT_CONVERSION = YES;
465 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
466 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
467 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
468 | CLANG_WARN_EMPTY_BODY = YES;
469 | CLANG_WARN_ENUM_CONVERSION = YES;
470 | CLANG_WARN_INFINITE_RECURSION = YES;
471 | CLANG_WARN_INT_CONVERSION = YES;
472 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
473 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
474 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
475 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
476 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
477 | CLANG_WARN_STRICT_PROTOTYPES = YES;
478 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
479 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
480 | CLANG_WARN_UNREACHABLE_CODE = YES;
481 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
482 | COPY_PHASE_STRIP = NO;
483 | DEBUG_INFORMATION_FORMAT = dwarf;
484 | ENABLE_STRICT_OBJC_MSGSEND = YES;
485 | ENABLE_TESTABILITY = YES;
486 | GCC_C_LANGUAGE_STANDARD = gnu11;
487 | GCC_DYNAMIC_NO_PIC = NO;
488 | GCC_NO_COMMON_BLOCKS = YES;
489 | GCC_OPTIMIZATION_LEVEL = 0;
490 | GCC_PREPROCESSOR_DEFINITIONS = (
491 | "DEBUG=1",
492 | "$(inherited)",
493 | );
494 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
495 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
496 | GCC_WARN_UNDECLARED_SELECTOR = YES;
497 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
498 | GCC_WARN_UNUSED_FUNCTION = YES;
499 | GCC_WARN_UNUSED_VARIABLE = YES;
500 | MACOSX_DEPLOYMENT_TARGET = 10.15;
501 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
502 | MTL_FAST_MATH = YES;
503 | ONLY_ACTIVE_ARCH = YES;
504 | SDKROOT = macosx;
505 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
506 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
507 | };
508 | name = Debug;
509 | };
510 | 89BCA36C2472E51C009C815D /* Release */ = {
511 | isa = XCBuildConfiguration;
512 | buildSettings = {
513 | ALWAYS_SEARCH_USER_PATHS = NO;
514 | CLANG_ANALYZER_NONNULL = YES;
515 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
516 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
517 | CLANG_CXX_LIBRARY = "libc++";
518 | CLANG_ENABLE_MODULES = YES;
519 | CLANG_ENABLE_OBJC_ARC = YES;
520 | CLANG_ENABLE_OBJC_WEAK = YES;
521 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
522 | CLANG_WARN_BOOL_CONVERSION = YES;
523 | CLANG_WARN_COMMA = YES;
524 | CLANG_WARN_CONSTANT_CONVERSION = YES;
525 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
526 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
527 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
528 | CLANG_WARN_EMPTY_BODY = YES;
529 | CLANG_WARN_ENUM_CONVERSION = YES;
530 | CLANG_WARN_INFINITE_RECURSION = YES;
531 | CLANG_WARN_INT_CONVERSION = YES;
532 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
533 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
534 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
535 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
536 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
537 | CLANG_WARN_STRICT_PROTOTYPES = YES;
538 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
539 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
540 | CLANG_WARN_UNREACHABLE_CODE = YES;
541 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
542 | COPY_PHASE_STRIP = NO;
543 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
544 | ENABLE_NS_ASSERTIONS = NO;
545 | ENABLE_STRICT_OBJC_MSGSEND = YES;
546 | GCC_C_LANGUAGE_STANDARD = gnu11;
547 | GCC_NO_COMMON_BLOCKS = YES;
548 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
549 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
550 | GCC_WARN_UNDECLARED_SELECTOR = YES;
551 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
552 | GCC_WARN_UNUSED_FUNCTION = YES;
553 | GCC_WARN_UNUSED_VARIABLE = YES;
554 | MACOSX_DEPLOYMENT_TARGET = 10.15;
555 | MTL_ENABLE_DEBUG_INFO = NO;
556 | MTL_FAST_MATH = YES;
557 | SDKROOT = macosx;
558 | SWIFT_COMPILATION_MODE = wholemodule;
559 | SWIFT_OPTIMIZATION_LEVEL = "-O";
560 | };
561 | name = Release;
562 | };
563 | 89BCA36E2472E51C009C815D /* Debug */ = {
564 | isa = XCBuildConfiguration;
565 | baseConfigurationReference = 9BA38D4D1449B8631FE186B6 /* Pods-desk.debug.xcconfig */;
566 | buildSettings = {
567 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
568 | CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
569 | CODE_SIGN_ENTITLEMENTS = desk/desk.entitlements;
570 | CODE_SIGN_STYLE = Automatic;
571 | COMBINE_HIDPI_IMAGES = YES;
572 | DEVELOPMENT_TEAM = R3V78XR6AA;
573 | ENABLE_HARDENED_RUNTIME = YES;
574 | ENABLE_PREVIEWS = YES;
575 | INFOPLIST_FILE = desk/Info.plist;
576 | LD_RUNPATH_SEARCH_PATHS = (
577 | "$(inherited)",
578 | "@executable_path/../Frameworks",
579 | );
580 | MACOSX_DEPLOYMENT_TARGET = 10.15;
581 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.desk;
582 | PRODUCT_NAME = "$(TARGET_NAME)";
583 | SWIFT_VERSION = 5.0;
584 | };
585 | name = Debug;
586 | };
587 | 89BCA36F2472E51C009C815D /* Release */ = {
588 | isa = XCBuildConfiguration;
589 | baseConfigurationReference = A9ADE9ECDE2FB81BC7FBA812 /* Pods-desk.release.xcconfig */;
590 | buildSettings = {
591 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
592 | CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
593 | CODE_SIGN_ENTITLEMENTS = desk/desk.entitlements;
594 | CODE_SIGN_STYLE = Automatic;
595 | COMBINE_HIDPI_IMAGES = YES;
596 | DEVELOPMENT_TEAM = R3V78XR6AA;
597 | ENABLE_HARDENED_RUNTIME = YES;
598 | ENABLE_PREVIEWS = YES;
599 | INFOPLIST_FILE = desk/Info.plist;
600 | LD_RUNPATH_SEARCH_PATHS = (
601 | "$(inherited)",
602 | "@executable_path/../Frameworks",
603 | );
604 | MACOSX_DEPLOYMENT_TARGET = 10.15;
605 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.desk;
606 | PRODUCT_NAME = "$(TARGET_NAME)";
607 | SWIFT_VERSION = 5.0;
608 | };
609 | name = Release;
610 | };
611 | 89BCA3712472E51C009C815D /* Debug */ = {
612 | isa = XCBuildConfiguration;
613 | baseConfigurationReference = 4DF47CCA12A7BD5C2D7AA387 /* Pods-deskTests.debug.xcconfig */;
614 | buildSettings = {
615 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
616 | BUNDLE_LOADER = "$(TEST_HOST)";
617 | CODE_SIGN_STYLE = Automatic;
618 | COMBINE_HIDPI_IMAGES = YES;
619 | DEVELOPMENT_TEAM = R3V78XR6AA;
620 | INFOPLIST_FILE = deskTests/Info.plist;
621 | LD_RUNPATH_SEARCH_PATHS = (
622 | "$(inherited)",
623 | "@executable_path/../Frameworks",
624 | "@loader_path/../Frameworks",
625 | );
626 | MACOSX_DEPLOYMENT_TARGET = 10.15;
627 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.deskTests;
628 | PRODUCT_NAME = "$(TARGET_NAME)";
629 | SWIFT_VERSION = 5.0;
630 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/desk.app/Contents/MacOS/desk";
631 | };
632 | name = Debug;
633 | };
634 | 89BCA3722472E51C009C815D /* Release */ = {
635 | isa = XCBuildConfiguration;
636 | baseConfigurationReference = 6268E187AE56B61FCA99580A /* Pods-deskTests.release.xcconfig */;
637 | buildSettings = {
638 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
639 | BUNDLE_LOADER = "$(TEST_HOST)";
640 | CODE_SIGN_STYLE = Automatic;
641 | COMBINE_HIDPI_IMAGES = YES;
642 | DEVELOPMENT_TEAM = R3V78XR6AA;
643 | INFOPLIST_FILE = deskTests/Info.plist;
644 | LD_RUNPATH_SEARCH_PATHS = (
645 | "$(inherited)",
646 | "@executable_path/../Frameworks",
647 | "@loader_path/../Frameworks",
648 | );
649 | MACOSX_DEPLOYMENT_TARGET = 10.15;
650 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.deskTests;
651 | PRODUCT_NAME = "$(TARGET_NAME)";
652 | SWIFT_VERSION = 5.0;
653 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/desk.app/Contents/MacOS/desk";
654 | };
655 | name = Release;
656 | };
657 | 89BCA3742472E51C009C815D /* Debug */ = {
658 | isa = XCBuildConfiguration;
659 | buildSettings = {
660 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
661 | CODE_SIGN_STYLE = Automatic;
662 | COMBINE_HIDPI_IMAGES = YES;
663 | DEVELOPMENT_TEAM = R3V78XR6AA;
664 | INFOPLIST_FILE = deskUITests/Info.plist;
665 | LD_RUNPATH_SEARCH_PATHS = (
666 | "$(inherited)",
667 | "@executable_path/../Frameworks",
668 | "@loader_path/../Frameworks",
669 | );
670 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.deskUITests;
671 | PRODUCT_NAME = "$(TARGET_NAME)";
672 | SWIFT_VERSION = 5.0;
673 | TEST_TARGET_NAME = desk;
674 | };
675 | name = Debug;
676 | };
677 | 89BCA3752472E51C009C815D /* Release */ = {
678 | isa = XCBuildConfiguration;
679 | buildSettings = {
680 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
681 | CODE_SIGN_STYLE = Automatic;
682 | COMBINE_HIDPI_IMAGES = YES;
683 | DEVELOPMENT_TEAM = R3V78XR6AA;
684 | INFOPLIST_FILE = deskUITests/Info.plist;
685 | LD_RUNPATH_SEARCH_PATHS = (
686 | "$(inherited)",
687 | "@executable_path/../Frameworks",
688 | "@loader_path/../Frameworks",
689 | );
690 | PRODUCT_BUNDLE_IDENTIFIER = com.forti.desk.deskUITests;
691 | PRODUCT_NAME = "$(TARGET_NAME)";
692 | SWIFT_VERSION = 5.0;
693 | TEST_TARGET_NAME = desk;
694 | };
695 | name = Release;
696 | };
697 | /* End XCBuildConfiguration section */
698 |
699 | /* Begin XCConfigurationList section */
700 | 89BCA33F2472E51B009C815D /* Build configuration list for PBXProject "desk" */ = {
701 | isa = XCConfigurationList;
702 | buildConfigurations = (
703 | 89BCA36B2472E51C009C815D /* Debug */,
704 | 89BCA36C2472E51C009C815D /* Release */,
705 | );
706 | defaultConfigurationIsVisible = 0;
707 | defaultConfigurationName = Release;
708 | };
709 | 89BCA36D2472E51C009C815D /* Build configuration list for PBXNativeTarget "desk" */ = {
710 | isa = XCConfigurationList;
711 | buildConfigurations = (
712 | 89BCA36E2472E51C009C815D /* Debug */,
713 | 89BCA36F2472E51C009C815D /* Release */,
714 | );
715 | defaultConfigurationIsVisible = 0;
716 | defaultConfigurationName = Release;
717 | };
718 | 89BCA3702472E51C009C815D /* Build configuration list for PBXNativeTarget "deskTests" */ = {
719 | isa = XCConfigurationList;
720 | buildConfigurations = (
721 | 89BCA3712472E51C009C815D /* Debug */,
722 | 89BCA3722472E51C009C815D /* Release */,
723 | );
724 | defaultConfigurationIsVisible = 0;
725 | defaultConfigurationName = Release;
726 | };
727 | 89BCA3732472E51C009C815D /* Build configuration list for PBXNativeTarget "deskUITests" */ = {
728 | isa = XCConfigurationList;
729 | buildConfigurations = (
730 | 89BCA3742472E51C009C815D /* Debug */,
731 | 89BCA3752472E51C009C815D /* Release */,
732 | );
733 | defaultConfigurationIsVisible = 0;
734 | defaultConfigurationName = Release;
735 | };
736 | /* End XCConfigurationList section */
737 | };
738 | rootObject = 89BCA33C2472E51B009C815D /* Project object */;
739 | }
740 |
--------------------------------------------------------------------------------
/desk/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 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
729 |
740 |
741 |
742 |
743 |
744 |
745 |
746 |
747 |
748 |
749 |
760 |
771 |
772 |
773 |
774 |
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 |
784 |
785 |
786 |
787 |
788 |
799 |
800 |
801 |
802 |
803 |
804 |
805 |
806 |
807 |
808 |
809 |
810 |
811 |
812 |
813 |
814 |
815 |
816 |
817 |
828 |
829 |
830 |
831 |
832 |
833 |
834 |
835 |
836 |
837 |
838 |
839 |
840 |
841 |
842 |
843 |
844 |
845 |
846 |
847 |
848 |
849 |
850 |
851 |
852 |
853 |
854 |
855 |
856 |
857 |
858 |
859 |
860 |
861 |
862 |
863 |
864 |
865 |
866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 |
874 |
875 |
876 |
877 |
878 |
879 |
880 |
881 |
882 |
883 |
884 |
885 |
886 |
887 |
888 |
889 |
890 |
891 |
892 |
893 |
894 |
895 |
896 |
897 |
898 |
899 |
900 |
901 |
902 |
903 |
904 |
905 |
906 |
907 |
908 |
909 |
910 |
911 |
912 |
913 |
914 |
915 |
916 |
917 |
918 |
919 |
920 |
921 |
922 |
923 |
924 |
925 |
926 |
927 |
928 |
929 |
930 |
931 |
932 |
933 |
934 |
935 |
936 |
937 |
938 |
939 |
940 |
941 |
942 |
943 |
944 |
945 |
--------------------------------------------------------------------------------