Skip to content

Commit

Permalink
Add KVO, Combine, and SwiftUI support (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
SergeyKuryanov committed Aug 2, 2020
1 parent 090e6e0 commit f7b255b
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 6 deletions.
Binary file added .github/storyboard_binding.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions LaunchAtLogin.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
D92CFD2224C5D909005B91BE /* Toggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92CFD2124C5D909005B91BE /* Toggle.swift */; };
E32E9B681EB87D7B000FEEE9 /* LaunchAtLogin.h in Headers */ = {isa = PBXBuildFile; fileRef = E32E9B661EB87D7B000FEEE9 /* LaunchAtLogin.h */; settings = {ATTRIBUTES = (Public, ); }; };
E32E9B6F1EB87DC5000FEEE9 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E32E9B6E1EB87DC5000FEEE9 /* LaunchAtLogin.swift */; };
E32E9B771EB87EA3000FEEE9 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = E32E9B761EB87EA3000FEEE9 /* main.swift */; };
Expand All @@ -26,6 +27,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
D92CFD2124C5D909005B91BE /* Toggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toggle.swift; sourceTree = "<group>"; };
E32E9B631EB87D7B000FEEE9 /* LaunchAtLogin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LaunchAtLogin.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E32E9B661EB87D7B000FEEE9 /* LaunchAtLogin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = LaunchAtLogin.h; sourceTree = "<group>"; usesTabs = 1; };
E32E9B671EB87D7B000FEEE9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -79,10 +81,11 @@
E32E9B651EB87D7B000FEEE9 /* LaunchAtLogin */ = {
isa = PBXGroup;
children = (
E32E9B6E1EB87DC5000FEEE9 /* LaunchAtLogin.swift */,
E32E9B661EB87D7B000FEEE9 /* LaunchAtLogin.h */,
E32E9B921EB889AE000FEEE9 /* copy-helper.sh */,
E32E9B671EB87D7B000FEEE9 /* Info.plist */,
E32E9B661EB87D7B000FEEE9 /* LaunchAtLogin.h */,
E32E9B6E1EB87DC5000FEEE9 /* LaunchAtLogin.swift */,
D92CFD2124C5D909005B91BE /* Toggle.swift */,
);
path = LaunchAtLogin;
sourceTree = "<group>";
Expand Down Expand Up @@ -220,6 +223,7 @@
buildActionMask = 2147483647;
files = (
E32E9B6F1EB87DC5000FEEE9 /* LaunchAtLogin.swift in Sources */,
D92CFD2224C5D909005B91BE /* Toggle.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
44 changes: 43 additions & 1 deletion LaunchAtLogin/LaunchAtLogin.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import Combine
import Foundation
import ServiceManagement

public struct LaunchAtLogin {
public enum LaunchAtLogin {
public static let kvo = KVO()

@available(macOS 10.15, *)
public static let observable = Observable()

@available(macOS 10.15, *)
private static var _publisher = CurrentValueSubject<Bool, Never>(isEnabled)
@available(macOS 10.15, *)
public static var publisher = _publisher.eraseToAnyPublisher()

private static let id = "\(Bundle.main.bundleIdentifier!)-LaunchAtLoginHelper"

public static var isEnabled: Bool {
Expand All @@ -15,7 +26,38 @@ public struct LaunchAtLogin {
return job?["OnDemand"] as? Bool ?? false
}
set {
if #available(macOS 10.15, *) {
observable.objectWillChange.send()
}

kvo.willChangeValue(for: \.isEnabled)
SMLoginItemSetEnabled(id as CFString, newValue)
kvo.didChangeValue(for: \.isEnabled)

if #available(macOS 10.15, *) {
_publisher.send(newValue)
}
}
}
}

// MARK: - LaunchAtLoginObservable
extension LaunchAtLogin {
@available(macOS 10.15, *)
public final class Observable: ObservableObject {
public var isEnabled: Bool {
get { LaunchAtLogin.isEnabled }
set { LaunchAtLogin.isEnabled = newValue }
}
}
}

// MARK: - LaunchAtLoginKVO
extension LaunchAtLogin {
public final class KVO: NSObject {
@objc dynamic public var isEnabled: Bool {
get { LaunchAtLogin.isEnabled }
set { LaunchAtLogin.isEnabled = newValue }
}
}
}
90 changes: 90 additions & 0 deletions LaunchAtLogin/Toggle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// LaunchAtLogin+Toggle.swift
// LaunchAtLogin
//
// Created by Sergey Kuryanov on 20.07.2020.
// Copyright © 2020 Sindre Sorhus. All rights reserved.
//

import SwiftUI

@available(macOS 10.15, *)
extension LaunchAtLogin {
/// A control that toggles launch at login between on and off states.
///
/// You create a toggle by providing a label. Set the label to a view
/// that visually describes the purpose of switching between
/// launch at login states. For example:
///
/// var body: some View {
/// LaunchAtLogin.Toggle {
/// Text("Launch at login")
/// }
/// }
///
/// For the common case of text-only labels, you can use the convenience
/// initializer that takes a title string (or localized string key) as its first
/// parameter, instead of a trailing closure:
///
/// var body: some View {
/// LaunchAtLogin.Toggle("Launch at login")
/// }
///
/// Default initializer will use "Launch at login" as a title.
///
/// var body: some View {
/// LaunchAtLogin.Toggle()
/// }
///
public struct Toggle<Label>: View where Label: View {
@ObservedObject private var launchAtLogin = LaunchAtLogin.observable

public let label: Label

/// Creates a launch at login toggle that displays a custom label.
///
/// - Parameters:
/// - label: A view that describes the purpose of the toggle.
public init(@ViewBuilder label: () -> Label) {
self.label = label()
}

public var body: some View {
SwiftUI.Toggle(isOn: $launchAtLogin.isEnabled) {
label
}
}
}
}

@available(macOS 10.15, *)
extension LaunchAtLogin.Toggle where Label == Text {
/// Creates a launch at login toggle that generates its label from a localized string key.
///
/// This initializer creates a ``Text`` view on your behalf with provided `titleKey`
///
/// - Parameters:
/// - titleKey: The key for the toggle's localized title, that describes
/// the purpose of the toggle.
public init(_ titleKey: LocalizedStringKey) {
label = Text(titleKey)
}

/// Creates a launch at login toggle that generates its label from a string.
///
/// This initializer creates a ``Text`` view on your behalf with provided `title`.
///
/// - Parameters:
/// - title: A string that describes the purpose of the toggle.
public init<S>(_ title: S) where S: StringProtocol {
label = Text(title)
}

/// Creates a launch at login toggle with default title.
///
/// This initializer creates a ``Text`` view on your behalf with ``Launch at login``
/// default title.
public init() {
self.init("Launch at login")
}
}
94 changes: 91 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ Add a new ["Run Script Phase"](http://stackoverflow.com/a/39633955/64949) **belo
"${PROJECT_DIR}/Carthage/Build/Mac/LaunchAtLogin.framework/Resources/copy-helper.sh"
```

Use it in your app:
### Use it in your app:

No need to store any state to UserDefaults.

*Note that the [Mac App Store guidelines](https://developer.apple.com/app-store/review/guidelines/) requires “launch at login” functionality to be enabled in response to a user action. This is usually solved by making it a preference that is disabled by default. Many apps also let the user activate it in a welcome screen.*

#### As static property:

```swift
import LaunchAtLogin
Expand All @@ -42,9 +48,91 @@ print(LaunchAtLogin.isEnabled)
//=> true
```

No need to store any state to UserDefaults.
#### SwiftUI:

*Note that the [Mac App Store guidelines](https://developer.apple.com/app-store/review/guidelines/) requires “launch at login” functionality to be enabled in response to a user action. This is usually solved by making it a preference that is disabled by default. Many apps also let the user activate it in a welcome screen.*
You can use `LaunchAtLogin.Toggle` view: a control that toggles launch at login between on and off states.

You create a toggle by providing a label. Set the label to a view that visually describes the purpose of switching between launch at login states. For example:

```swift
struct ContentView: View {
var body: some View {
LaunchAtLogin.Toggle {
Text("Launch at login")
}
}
}
```

For the common case of text-only labels, you can use the convenience initializer that takes a title string (or localized string key) as its first parameter, instead of a trailing closure:

```swift
struct ContentView: View {
var body: some View {
LaunchAtLogin.Toggle("Launch at login")
}
}
```
Default initializer will use "Launch at login" as a title.

```swift
struct ContentView: View {
var body: some View {
LaunchAtLogin.Toggle()
}
}
```

As alternative you can use `LaunchAtLogin.observable` as binding with `@ObservedObject`:

```swift
import SwiftUI
import LaunchAtLogin

struct ContentView: View {
@ObservedObject private var launchAtLogin = LaunchAtLogin.observable

var body: some View {
Toggle(isOn: $launchAtLogin.isEnabled) {
Text("Launch at login")
}
}
}
```
#### Combine:

Just subscribe to `LaunchAtLogin.publisher`:

```swift
import Combine
import LaunchAtLogin

final class ViewModel {
private var isLaunchAtLoginEnabled = LaunchAtLogin.isEnabled
private var cancellables = Set<AnyCancellable>()

func bind() {
LaunchAtLogin
.publisher
.assign(to: \.isLaunchAtLoginEnabled, on: self)
.store(in: &cancellables)
}
}
```

#### Storyboards:

Bind control to `LaunchAtLogin.kvo` exposed property:

```swift
import Cocoa
import LaunchAtLogin

final class ViewController: NSViewController {
@objc dynamic var launchAtLogin = LaunchAtLogin.kvo
}
```
![storyboard_binding](.github/storyboard_binding.png)

## How does it work?

Expand Down

0 comments on commit f7b255b

Please sign in to comment.