Merge pull request #126 from RobotsAndPencils/Notifications

Adds MacOS notifications
This commit is contained in:
Matt Kiazyk 2021-05-02 10:16:45 -05:00 committed by GitHub
commit fc7560d95f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 165 additions and 0 deletions

View file

@ -101,7 +101,9 @@
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; };
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; };
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; };
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; };
/* End PBXBuildFile section */
@ -266,7 +268,9 @@
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = "<group>"; };
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = "<group>"; };
E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = "<group>"; };
E89342F925EDCC17007CF557 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
E8977EA225C11E1500835F80 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -431,6 +435,7 @@
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
CAA8587B25A2B37900ACF8C0 /* IsTesting.swift */,
CA42DD6D25AEA8B200BC0B0C /* Logger.swift */,
E89342F925EDCC17007CF557 /* NotificationManager.swift */,
CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */,
CABFA9AE2592EEE900380FEE /* Path+.swift */,
CABFA9B42592EEEA00380FEE /* Process.swift */,
@ -552,6 +557,7 @@
CAFE4AAB25B7D2C70064FE51 /* GeneralPreferencePane.swift */,
CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */,
E8977EA225C11E1500835F80 /* PreferencesView.swift */,
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */,
);
path = Preferences;
sourceTree = "<group>";
@ -795,6 +801,7 @@
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */,
CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */,
CA25192A25A9644800F08414 /* XcodeInstallState.swift in Sources */,
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */,
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
@ -822,6 +829,7 @@
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */,
63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */,
536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */,
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */,
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */,
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */,
CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */,

View file

@ -41,6 +41,7 @@ extension AppState {
private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher<InstalledXcode, Error> {
Logger.appState.info("Using \(downloader) downloader")
return getXcodeArchive(installationType, downloader: downloader)
.flatMap { xcode, url -> AnyPublisher<InstalledXcode, Swift.Error> in
self.installArchivedXcode(xcode, at: url)
@ -442,6 +443,9 @@ extension AppState {
DispatchQueue.main.async {
guard let index = self.allXcodes.firstIndex(where: { $0.version.isEquivalent(to: version) }) else { return }
self.allXcodes[index].installState = .installing(step)
let xcode = self.allXcodes[index]
Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal)
}
}
}

View file

@ -16,6 +16,9 @@ class AppState: ObservableObject {
@Published var authenticationState: AuthenticationState = .unauthenticated
@Published var availableXcodes: [AvailableXcode] = [] {
willSet {
if newValue.count > availableXcodes.count && availableXcodes.count != 0 {
Current.notificationManager.scheduleNotification(title: "New Xcode versions", body: "New Xcode versions are available to download.", category: .normal)
}
updateAllXcodes(
availableXcodes: newValue,
installedXcodes: Current.files.installedXcodes(Path.root/"Applications"),
@ -297,6 +300,7 @@ class AppState: ObservableObject {
func install(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
installationPublishers[id] = signInIfNeeded()
.flatMap { [unowned self] in
// signInIfNeeded might finish before the user actually authenticates if UI is involved.

View file

@ -19,6 +19,7 @@ public struct Environment {
public var defaults = Defaults()
public var date: () -> Date = Date.init
public var helper = Helper()
public var notificationManager = NotificationManager()
}
public var Current = Environment()
@ -228,6 +229,11 @@ public struct Defaults {
public func removeObject(forKey key: String) {
removeObject(key)
}
public var get: (String) -> Any? = { UserDefaults.standard.value(forKey: $0) }
public func get(forKey key: String) -> Any? {
get(key)
}
}
private let helperClient = HelperClient()

View file

@ -0,0 +1,100 @@
import Foundation
import os.log
import UserNotifications
/// Representation of the 3 states of the Notifications permission prompt which may either have not been shown, or was shown and denied or accepted
/// Unknown is value to indicate that we have not yet determined the status and should not be used other than as a default value before determining the actual status
public enum NotificationPermissionPromptStatus: Int {
case unknown, notShown, shownAndDenied, shownAndAccepted
}
public enum XcodesNotificationCategory: String {
case normal
case error
}
public enum XcodesNotificationType: String, Identifiable, CaseIterable, CustomStringConvertible {
case newVersionAvailable
case finishedInstalling
public var id: Self { self }
public var description: String {
switch self {
case .newVersionAvailable:
return "New version is available"
case .finishedInstalling:
return "Finished installing"
}
}
}
public class NotificationManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
private let notificationCenter = UNUserNotificationCenter.current()
@Published var notificationStatus = NotificationPermissionPromptStatus.unknown
public override init() {
super.init()
loadNotificationStatus()
notificationCenter.delegate = self
}
public func loadNotificationStatus() {
UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { [weak self] (settings) in
let status = NotificationManager.systemPromptStatusFromSettings(settings)
self?.notificationStatus = status
})
}
private class func systemPromptStatusFromSettings(_ settings: UNNotificationSettings) -> NotificationPermissionPromptStatus {
switch settings.authorizationStatus {
case .notDetermined:
return .notShown
case .authorized, .provisional:
return .shownAndAccepted
case .denied:
return .shownAndDenied
@unknown default:
return .unknown
}
}
public func requestAccess() {
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in
DispatchQueue.main.async {
if let error = error {
// Handle the error here.
Logger.appState.error("Error requesting notification accesss: \(error.legibleLocalizedDescription)")
} else {
Logger.appState.log("User has \(granted ? "Granted" : "NOT GRANTED") notification permission")
}
self?.loadNotificationStatus()
}
}
}
func scheduleNotification(title: String?, body: String, category: XcodesNotificationCategory) {
let content = UNMutableNotificationContent()
if let title = title {
content.title = title
}
content.body = body
content.sound = .default
content.categoryIdentifier = category.rawValue
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.3, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
notificationCenter.add(request)
}
// MARK: UNUserNotificationCenterDelegate
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler( [.banner, .badge, .sound])
}
}

View file

@ -14,6 +14,12 @@ struct GeneralPreferencePane: View {
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())
Divider()
GroupBox(label: Text("Notifications")) {
NotificationsView().environmentObject(appState)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
.frame(width: 400)
}

View file

@ -0,0 +1,37 @@
import SwiftUI
struct NotificationsView: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack(alignment: .leading) {
switch Current.notificationManager.notificationStatus {
case .shownAndAccepted:
Text("Access Granted. You will receive notifications from Xcodes.")
.fixedSize(horizontal: false, vertical: true)
case .shownAndDenied:
Text("⚠️ Access Denied ⚠️\n\nPlease open your Notification Settings and select Xcodes if you wish to allow access.")
.fixedSize(horizontal: false, vertical: true)
Button("Notification Settings", action: {
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.notifications")!)
})
default:
Button("Enable Notifications", action: {
Current.notificationManager.requestAccess()
})
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct NotificationsView_Previews: PreviewProvider {
static var previews: some View {
Group {
NotificationsView()
}
}
}