feat(mac): Responding to code review

This commit is contained in:
Eli Schantz 2026-08-12 19:42:06 -04:00
parent 47aa28b042
commit cfb1bedebc
6 changed files with 53 additions and 58 deletions

View file

@ -32,7 +32,7 @@ struct ConfigApp: App {
.environmentObject(installation)
}
Window("Installation", id: "install") {
ParentInstallView()
MainInstallView()
.environmentObject(installation)
}
.windowResizability(.contentSize)

View file

@ -20,9 +20,19 @@ struct GrantAccessibiltyPermissionView: View {
let namespace: Namespace.ID
let onContinue: () -> Void
/**
* The flow of this view depends on the following @State variables.
* Once the user presses "Open Settings" in order to toggle the security permission they will be allowed to continue.
* When they press "Continue," a loading symbol will run until the view receives a notification of whether the access is
* granted or not. If not granted, an error message will appear. If granted, the user will be moved to the next screen.
*/
// Tracks if the user clicked "Open Settings" (Enables the "Continue" button)
@State var openSettingsButtonPressed: Bool = false
// Tracks if the app is currently running the background permission check
@State var checkingPermission: Bool = false
@State var advancementRequestedAndPermissionNotGranted: Bool = false
// Tracks if the user clicked "Continue" but permission is still missing
@State var permissionNotGrantedAfterPrompt: Bool = false
var body: some View {
VStack {
@ -58,12 +68,13 @@ struct GrantAccessibiltyPermissionView: View {
if checkingPermission {
HStack {
// Shows spinner AKA ProgressView()
ProgressView()
.controlSize(.small)
Text("Checking...")
}
} else if advancementRequestedAndPermissionNotGranted {
} else if permissionNotGrantedAfterPrompt {
Text("Access has not been granted.")
.foregroundStyle(Color.red)
.padding(7)
@ -89,7 +100,9 @@ struct GrantAccessibiltyPermissionView: View {
.tint(.blue)
.clipShape(Capsule())
.matchedGeometryEffect(id: "actionButton", in: namespace)
Button {
// Trigger the system task to check for accessibility permission
checkingPermission = true
installation.executeCurrentInstallationTask()
} label: {
@ -101,16 +114,18 @@ struct GrantAccessibiltyPermissionView: View {
.clipShape(Capsule())
}
}
// Triggered when the system confirms accessibility has been granted.
.onReceive( NotificationCenter.default.publisher(for: .accessibilityGranted)) { notification in
withAnimation(.smooth) {
advancementRequestedAndPermissionNotGranted = true
onContinue()
permissionNotGrantedAfterPrompt = false
onContinue() // Moves the user to the next screen
}
}
// Triggered when the system confirms accessibility has not been granted.
.onReceive( NotificationCenter.default.publisher(for: .accessibilityNotGranted)) { notification in
withAnimation(.smooth) {
checkingPermission = false
advancementRequestedAndPermissionNotGranted = true
checkingPermission = false // Stops showing the loading spinner
permissionNotGrantedAfterPrompt = true // Shows the red error text
}
}
}

View file

@ -13,9 +13,7 @@ import AppKit
internal import UniformTypeIdentifiers
struct InitialRepairView: View {
@EnvironmentObject var installation: InstallationContainer
let namespace: Namespace.ID
let onContinue: () -> Void

View file

@ -1,18 +0,0 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Eli Schantz on 2026-07-21
*
* The @State variable currentPage is of type InstallPage.
* These pages are used to easily connect each installation phase with a SwiftUI view in ParentInstallView.
*/
enum InstallPage: String, CaseIterable {
case loading
case initialInstall
case initialRepair
case completed
case enableInputMethod
case allowSecurityPermission
case rerunInstaller
}

View file

@ -8,11 +8,33 @@
import SwiftUI
struct ParentInstallView: View {
enum InstallPage: String, CaseIterable {
case loading
case initialInstall
case initialRepair
case completed
case enableInputMethod
case allowSecurityPermission
case rerunInstaller
case restartMac
}
struct MainInstallView: View {
@EnvironmentObject var installation: InstallationContainer
/**
* A namespace is created here and passed to child views.
* Any subviews with the same string id and this namespace
* will animate smoothly when changing positions or states.
*/
@Namespace var animation
@State public var currentPage: InstallPage = .loading
/**
* chooseCurrentPage() will update the @State var currentPage according to the current task.
* If there is a task involved with the installationPhase, it will display the page associated with that task.
* If there is not a task involved with the installationPhase, it will display the page associated with that phase.
*/
func chooseCurrentPage() {
if installation.installationPhase.hasTasks {
switch installation.currentTask()?.taskType {
@ -21,6 +43,7 @@ struct ParentInstallView: View {
case .enableInputMethod: currentPage = .enableInputMethod
case .requestAccess: currentPage = .allowSecurityPermission
case .confirmAccess: currentPage = .allowSecurityPermission
case .requestRestart: currentPage = .restartMac
default: currentPage = .completed
}
} else {
@ -40,7 +63,9 @@ struct ParentInstallView: View {
}
var body: some View {
ZStack {
VStack {
// The switch statement below updates the view this VStack contains whenever currentPage changes value
switch currentPage {
case .loading: ProgressView()
case .initialInstall: InitialInstallView(namespace: animation,onContinue: {
@ -55,11 +80,13 @@ struct ParentInstallView: View {
case .enableInputMethod: EnableInputMethodView(namespace: animation, onContinue: chooseCurrentPage)
case .allowSecurityPermission: GrantAccessibiltyPermissionView(namespace: animation, onContinue: chooseCurrentPage)
case .rerunInstaller: RerunInstallerView(namespace: animation)
case .restartMac: RestartComputerView(namespace: animation)
}
}
// While the installer is evaluating the Keyman installation, the loading screen will be shown
.onAppear {
print("LOL ", installation.installationPhase)
print("LOL ", installation.currentTask()?.taskType ?? "no task available")
print("From MainInstallView onAppear: ", installation.installationPhase)
print("From MainInstallView onAppear: ", installation.currentTask()?.taskType ?? "no task available")
if installation.installationPhase == .evaluatingInstallation {
currentPage = .loading
@ -70,8 +97,6 @@ struct ParentInstallView: View {
await MainActor.run {
withAnimation(.smooth) {
chooseCurrentPage()
print("LOOL ", installation.installationPhase)
}
}
}

View file

@ -11,20 +11,6 @@ import SwiftUI
import AppKit
internal import UniformTypeIdentifiers
private func chooseAndOpenKeymanInstaller() {
let panel = NSOpenPanel()
panel.message = "Open the Keyman .pkg file."
panel.allowedContentTypes = [UTType(filenameExtension: "pkg")!]
panel.directoryURL = FileManager.default.urls(
for: .downloadsDirectory,
in: .userDomainMask
).first
if panel.runModal() == .OK, let url = panel.url {
NSWorkspace.shared.open(url)
}
}
struct RerunInstallerView: View {
@EnvironmentObject var installation: InstallationContainer
let namespace: Namespace.ID
@ -59,17 +45,6 @@ struct RerunInstallerView: View {
Text("Run Keyman installer")
.font(.title2)
.frame(maxWidth: .infinity, alignment: .leading)
Button {
chooseAndOpenKeymanInstaller()
} label: {
Text("Open Installer")
.padding(.horizontal, 16)
.padding(.vertical, 4)
}
.buttonStyle(.borderedProminent)
.tint(.blue)
.clipShape(Capsule())
.matchedGeometryEffect(id: "actionButton", in: namespace)
NavigationButton(action: .dismiss)
}
}