Skip to content

Commit

Permalink
Added Swift version of app with sandbox + entitlements and proper set…
Browse files Browse the repository at this point in the history
…up window
  • Loading branch information
sroebert committed Aug 24, 2022
0 parents commit 9c89cc9
Show file tree
Hide file tree
Showing 31 changed files with 2,722 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Packages
.build
xcuserdata
*.xcodeproj
DerivedData/
.DS_Store
db.sqlite
.swiftpm
/Data/
.env

159 changes: 159 additions & 0 deletions IKEADeskControl/App.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import SwiftUI
import AppKit
import Combine

//@main
//struct IKEADeskControlApp: App {
//
// @StateObject private var appModel = AppModel.shared
//
// var body: some Scene {
// MenuBarExtra("IKEA Desk Control", systemImage: "arrow.up.and.down") {
// Button("Quit IKEA Desk Control") {
// NSApp.terminate(nil)
// }
// .keyboardShortcut("Q", modifiers: .command)
// }
// }
//}

@main
struct IKEADeskControlApp {
static func main() {
let app = NSApplication.shared

let delegate = AppDelegate()
app.delegate = delegate

_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
}
}

class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {

// MARK: - Private Vars

private var appModel: AppModel!

private var statusItem: NSStatusItem!
private var setupResetMenuItem: NSMenuItem!

private var setupWindow: NSWindow?
private var setupViewModel: SetupViewModel?

// MARK: - NSApplicationDelegate

func applicationDidFinishLaunching(_ notification: Notification) {
appModel = AppModel.shared

statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let button = statusItem.button {
button.image = NSImage(
systemSymbolName: "arrow.up.and.down",
accessibilityDescription: "IKEA Desk Control"
)
}

setupMenu()

updateSetupResetMenuItem()
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return false
}

// MARK: - NSWindowDelegate

func windowShouldClose(_ sender: NSWindow) -> Bool {
setupWindow = nil
setupViewModel = nil
return true
}

// MARK: - Setup

private func setupMenu() {
setupResetMenuItem = NSMenuItem(
title: "",
action: #selector(AppDelegate.toggle),
keyEquivalent: ""
)

let statusMenu = NSMenu()
statusMenu.addItem(setupResetMenuItem)
statusMenu.addItem(NSMenuItem.separator())
statusMenu.addItem(NSMenuItem(
title: "Quit IKEA Desk Control",
action: #selector(NSApplication.terminate(_:)),
keyEquivalent: "Q"
))
statusItem.menu = statusMenu
}

// MARK: - Utils

private func updateSetupResetMenuItem() {
if appModel.isActive {
setupResetMenuItem.title = "Stop"
} else {
setupResetMenuItem.title = "Setup..."
}
}

@objc private func toggle() {
if appModel.isActive {
appModel.stop()
updateSetupResetMenuItem()
} else {
showSetupWindow()
}
}

private func showSetupWindow() {
if let existingWindow = setupWindow {
existingWindow.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return
}

let setupWindow = NSWindow(
contentRect: .zero,
styleMask: [.titled, .closable, .fullSizeContentView],
backing: .buffered,
defer: false
)
setupWindow.title = "Setup IKEADeskControl"
setupWindow.isReleasedWhenClosed = false

let setupViewModel = SetupViewModel { [weak self] in
self?.start(with: $0)
}

setupWindow.contentView = NSHostingView(
rootView: SetupView(viewModel: setupViewModel)
)
setupWindow.delegate = self
setupWindow.setFrameAutosaveName("com.roebert.IKEADeskControl.config")

setupWindow.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)

self.setupWindow = setupWindow
self.setupViewModel = setupViewModel
}

private func start(with configuration: SetupViewModel.StartConfiguration) {
setupWindow?.close()
setupWindow = nil
setupViewModel = nil

appModel.start(
mqttURL: configuration.mqttURL,
mqttUsername: configuration.mqttUsername,
mqttPassword: configuration.mqttPassword,
mqttIdentifier: configuration.mqttIdentifier
)
updateSetupResetMenuItem()
}
}
163 changes: 163 additions & 0 deletions IKEADeskControl/AppModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import Foundation
import SwiftUI
import Logging
import MQTTNIO

final class AppModel: ObservableObject {

// MARK: - Types

private struct Configuration: Codable {
var peripheralId: UUID?
var mqttURL: URL
var mqttUsername: String?
var mqttPassword: String?
var mqttIdentifier: String
}

// MARK: - Public Vars

static let shared: AppModel = AppModel()

var isActive: Bool {
return activeTask != nil
}

// MARK: - Private Vars

@KeychainItem("configuration")
private var configuration: Configuration?

private typealias ActiveTaskData = (DeskController, MQTTController)
private var activeTask: Task<ActiveTaskData, Never>?

// MARK: - Lifecycle

private init() {
Self.loggingBootstrap()

if let configuration = configuration {
activeTask = Task {
await setup(
peripheralId: configuration.peripheralId,
mqttURL: configuration.mqttURL,
mqttUsername: configuration.mqttUsername,
mqttPassword: configuration.mqttPassword,
mqttIdentifier: configuration.mqttIdentifier
)
}
}
}

private static func loggingBootstrap() {
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)

#if DEBUG
handler.logLevel = .debug
#else
handler.logLevel = .info
#endif

return handler
}
}

// MARK: - Start

func start(
mqttURL: URL,
mqttUsername: String? = nil,
mqttPassword: String? = nil,
mqttIdentifier: String
) {
stop()

configuration = .init(
mqttURL: mqttURL,
mqttUsername: mqttUsername,
mqttPassword: mqttPassword,
mqttIdentifier: mqttIdentifier
)

activeTask = Task {
await setup(
peripheralId: nil,
mqttURL: mqttURL,
mqttUsername: mqttUsername,
mqttPassword: mqttPassword,
mqttIdentifier: mqttIdentifier
)
}
}

func stop() {
activeTask?.cancel()
activeTask = nil
configuration = nil
}

private func setup(
peripheralId: UUID? = nil,
mqttURL: URL,
mqttUsername: String? = nil,
mqttPassword: String? = nil,
mqttIdentifier: String
) async -> ActiveTaskData {
let deskController = await DeskController(
peripheralId: peripheralId
)

let credentials: MQTTConfiguration.Credentials?
if let username = mqttUsername, let password = mqttPassword {
credentials = .init(username: username, password: password)
} else {
credentials = nil
}

let mqttController = await MQTTController(
identifier: mqttIdentifier,
url: mqttURL,
credentials: credentials
)

await deskController.onConnected { [weak self, weak mqttController] deskState in
self?.configuration?.peripheralId = deskState.peripheralId

await mqttController?.deskDidConnect(deskState: deskState)
}

await deskController.onDisconnected { [weak mqttController] in
await mqttController?.deskDidDisconnect()
}

await deskController.onDeskState { [weak mqttController] in
await mqttController?.didReceiveDeskState($0)
}

await mqttController.onCommand { [weak deskController] command in
guard let deskController = deskController else {
return
}

switch command {
case .stop:
try? await deskController.stop()

case .moveTo(let position):
try? await deskController.move(toPosition: position)

case .open:
try? await deskController.move(toPosition: DeskController.maximumDeskPosition)

case .close:
try? await deskController.move(toPosition: DeskController.minimumDeskPosition)
}
}

await mqttController.start()
await deskController.start()

return (deskController, mqttController)
}
}
11 changes: 11 additions & 0 deletions IKEADeskControl/Assets.xcassets/AccentColor.colorset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading

0 comments on commit 9c89cc9

Please sign in to comment.