Merge pull request #39 from RobotsAndPencils/installation-step-view

Add InstallationStepView
This commit is contained in:
Brandon Evans 2021-01-03 10:17:37 -07:00 committed by GitHub
commit 2052ff54ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 275 additions and 5 deletions

View file

@ -12,6 +12,7 @@
CA378F992466567600A58CE0 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA378F982466567600A58CE0 /* AppState.swift */; };
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */; };
CA44901F2463AD34003D8213 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA44901E2463AD34003D8213 /* Tag.swift */; };
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */; };
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */; };
CA61A6E0259835580008926E /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA61A6DF259835580008926E /* Xcode.swift */; };
CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA735108257BF96D00EA9CF8 /* AttributedText.swift */; };
@ -61,6 +62,9 @@
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA2B2592FBFC00380FEE /* Configure.swift */; };
CABFAA432593104F00380FEE /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA422593104F00380FEE /* AboutView.swift */; };
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */; };
CAC281C8259F97E100B8AB0B /* InstallationStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281C7259F97E100B8AB0B /* InstallationStepView.swift */; };
CAC281CD259F97FA00B8AB0B /* ObservingProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */; };
CAC281DA259F985100B8AB0B /* InstallationStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAC281D9259F985100B8AB0B /* InstallationStep.swift */; };
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD2E7A12449574E00113D76 /* XcodesApp.swift */; };
CAD2E7A42449574E00113D76 /* XcodeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD2E7A32449574E00113D76 /* XcodeListView.swift */; };
CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAD2E7A52449575000113D76 /* Assets.xcassets */; };
@ -123,6 +127,7 @@
CA378F982466567600A58CE0 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreButtonStyle.swift; sourceTree = "<group>"; };
CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = "<group>"; };
CA538A0C255A4F1A00E64DD7 /* AppleAPI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = AppleAPI; path = Xcodes/AppleAPI; sourceTree = "<group>"; };
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCodeTextView.swift; sourceTree = "<group>"; };
CA61A6DF259835580008926E /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = "<group>"; };
@ -179,6 +184,9 @@
CABFAA2B2592FBFC00380FEE /* Configure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Configure.swift; path = Xcodes/Backend/Configure.swift; sourceTree = SOURCE_ROOT; };
CABFAA422593104F00380FEE /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
CABFAA482593162500380FEE /* Bundle+InfoPlistValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+InfoPlistValues.swift"; sourceTree = "<group>"; };
CAC281C7259F97E100B8AB0B /* InstallationStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepView.swift; sourceTree = "<group>"; };
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservingProgressIndicator.swift; sourceTree = "<group>"; };
CAC281D9259F985100B8AB0B /* InstallationStep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStep.swift; sourceTree = "<group>"; };
CAD2E79E2449574E00113D76 /* Xcodes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Xcodes.app; sourceTree = BUILT_PRODUCTS_DIR; };
CAD2E7A12449574E00113D76 /* XcodesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesApp.swift; sourceTree = "<group>"; };
CAD2E7A32449574E00113D76 /* XcodeListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListView.swift; sourceTree = "<group>"; };
@ -236,7 +244,9 @@
63EAA4E9259944340046AB8F /* Common */ = {
isa = PBXGroup;
children = (
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */,
63EAA4EA259944450046AB8F /* ProgressButton.swift */,
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */,
);
path = Common;
sourceTree = "<group>";
@ -307,6 +317,7 @@
CAE42486259A68A300B8B246 /* XcodeListCategory.swift */,
CAD2E7A32449574E00113D76 /* XcodeListView.swift */,
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */,
CAC281C7259F97E100B8AB0B /* InstallationStepView.swift */,
);
path = XcodeList;
sourceTree = "<group>";
@ -329,6 +340,7 @@
CABFA9AC2592EEE900380FEE /* Foundation.swift */,
CA9FF8F425959CE000E47BAF /* HelperInstaller.swift */,
CA9FF9352595B44700E47BAF /* HelperClient.swift */,
CAC281D9259F985100B8AB0B /* InstallationStep.swift */,
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */,
CABFA9AE2592EEE900380FEE /* Path+.swift */,
@ -624,6 +636,7 @@
CABFA9BB2592EEEA00380FEE /* DateFormatter+.swift in Sources */,
CABFA9BD2592EEEA00380FEE /* Environment.swift in Sources */,
CABFA9C32592EEEA00380FEE /* Downloads.swift in Sources */,
CAC281DA259F985100B8AB0B /* InstallationStep.swift in Sources */,
CA378F992466567600A58CE0 /* AppState.swift in Sources */,
CAD2E7A42449574E00113D76 /* XcodeListView.swift in Sources */,
CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */,
@ -632,6 +645,8 @@
CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */,
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */,
CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */,
CAC281C8259F97E100B8AB0B /* InstallationStepView.swift in Sources */,
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */,
CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */,
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */,
@ -644,6 +659,7 @@
CA9FF8F525959CE000E47BAF /* HelperInstaller.swift in Sources */,
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */,
CAC281CD259F97FA00B8AB0B /* ObservingProgressIndicator.swift in Sources */,
CAFBDC68259A308B003DCC5A /* InfoPane.swift in Sources */,
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
CAFBDC6C259A3098003DCC5A /* View+Conditional.swift in Sources */,

View file

@ -0,0 +1,45 @@
import Foundation
/// A numbered step
enum InstallationStep: Equatable, CustomStringConvertible {
case downloading(progress: Progress)
case unarchiving
case moving(destination: String)
case trashingArchive
case checkingSecurity
case finishing
var description: String {
"(\(stepNumber)/\(stepCount)) \(message)"
}
var message: String {
switch self {
case .downloading:
return "Downloading"
case .unarchiving:
return "Unarchiving (This can take a while)"
case .moving(let destination):
return "Moving to \(destination)"
case .trashingArchive:
return "Moving archive to the Trash"
case .checkingSecurity:
return "Security verification"
case .finishing:
return "Finishing"
}
}
var stepNumber: Int {
switch self {
case .downloading: return 1
case .unarchiving: return 2
case .moving: return 3
case .trashingArchive: return 4
case .checkingSecurity: return 5
case .finishing: return 6
}
}
var stepCount: Int { 6 }
}

View file

@ -47,6 +47,6 @@ struct Xcode: Identifiable, CustomStringConvertible {
enum XcodeInstallState: Equatable {
case notInstalled
case installing(Progress)
case installing(InstallationStep)
case installed
}

View file

@ -0,0 +1,62 @@
import Combine
import SwiftUI
/// A ProgressIndicator that reflects the state of a Progress object.
/// This functionality is already built in to ProgressView,
/// but this implementation ensures that changes are received on the main thread.
@available(iOS 14.0, macOS 11.0, *)
public struct ObservingProgressIndicator: View {
let controlSize: NSControl.ControlSize
let style: NSProgressIndicator.Style
@StateObject private var progress: ProgressWrapper
public init(
_ progress: Progress,
controlSize: NSControl.ControlSize,
style: NSProgressIndicator.Style
) {
_progress = StateObject(wrappedValue: ProgressWrapper(progress: progress))
self.controlSize = controlSize
self.style = style
}
class ProgressWrapper: ObservableObject {
var progress: Progress
var cancellable: AnyCancellable!
init(progress: Progress) {
self.progress = progress
cancellable = progress
.publisher(for: \.fractionCompleted)
.receive(on: RunLoop.main)
.sink { [weak self] _ in self?.objectWillChange.send() }
}
}
public var body: some View {
ProgressIndicator(
minValue: 0.0,
maxValue: 1.0,
doubleValue: progress.progress.fractionCompleted,
controlSize: controlSize,
isIndeterminate: progress.progress.isIndeterminate,
style: style
)
}
}
@available(iOS 14.0, macOS 11.0, *)
struct ObservingProgressBar_Previews: PreviewProvider {
static var previews: some View {
Group {
ObservingProgressIndicator(
configure(Progress(totalUnitCount: 100)) {
$0.completedUnitCount = 40
},
controlSize: .small,
style: .spinning
)
}
.previewLayout(.sizeThatFits)
}
}

View file

@ -0,0 +1,40 @@
import SwiftUI
import AppKit
/// You probably want ProgressView unless you need more of NSProgressIndicator's API, which this exposes.
struct ProgressIndicator: NSViewRepresentable {
typealias NSViewType = NSProgressIndicator
let minValue: Double
let maxValue: Double
let doubleValue: Double
let controlSize: NSControl.ControlSize
let isIndeterminate: Bool
let style: NSProgressIndicator.Style
func makeNSView(context: Context) -> NSViewType {
NSProgressIndicator()
}
func updateNSView(_ nsView: NSViewType, context: Context) {
nsView.minValue = minValue
nsView.maxValue = maxValue
nsView.doubleValue = doubleValue
nsView.controlSize = controlSize
nsView.isIndeterminate = isIndeterminate
nsView.style = style
}
}
struct ProgressIndicator_Previews: PreviewProvider {
static var previews: some View {
ProgressIndicator(
minValue: 0,
maxValue: 1,
doubleValue: 0.4,
controlSize: .small,
isIndeterminate: false,
style: .spinning
)
}
}

View file

@ -0,0 +1,104 @@
import SwiftUI
struct InstallationStepView: View {
let installationStep: InstallationStep
let highlighted: Bool
let cancel: () -> Void
var body: some View {
HStack {
switch installationStep {
case let .downloading(progress):
// FB8955769 ProgressView.init(_: Progress) doesn't ensure that changes from the Progress object are applied to the UI on the main thread
// This Progress is vended by URLSession so I don't think we can control that.
// Use our own version of ProgressView that does this instead.
ObservingProgressIndicator(
progress,
controlSize: .small,
style: .spinning
)
case .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing:
ProgressView()
.scaleEffect(0.5)
}
Text("Step \(installationStep.stepNumber) of \(installationStep.stepCount): \(installationStep.message)")
.font(.footnote)
Button(action: { }) {
Label("Cancel", systemImage: "xmark.circle.fill")
.labelStyle(IconOnlyLabelStyle())
}
.buttonStyle(PlainButtonStyle())
.foregroundColor(highlighted ? .white : .secondary)
.help("Cancel installation")
}
.frame(minWidth: 80)
}
}
struct InstallView_Previews: PreviewProvider {
static var previews: some View {
Group {
ForEach(ColorScheme.allCases, id: \.self) { colorScheme in
Group {
InstallationStepView(
installationStep: .downloading(
progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 }
),
highlighted: false,
cancel: {}
)
InstallationStepView(
installationStep: .unarchiving,
highlighted: false,
cancel: {}
)
InstallationStepView(
installationStep: .moving(destination: "/Applications"),
highlighted: false,
cancel: {}
)
InstallationStepView(
installationStep: .trashingArchive,
highlighted: false,
cancel: {}
)
InstallationStepView(
installationStep: .checkingSecurity,
highlighted: false,
cancel: {}
)
InstallationStepView(
installationStep: .finishing,
highlighted: false,
cancel: {}
)
}
.padding()
.background(Color(.windowBackgroundColor))
.environment(\.colorScheme, colorScheme)
}
ForEach(ColorScheme.allCases, id: \.self) { colorScheme in
Group {
InstallationStepView(
installationStep: .downloading(
progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 }
),
highlighted: true,
cancel: {}
)
}
.padding()
.background(Color(.selectedContentBackgroundColor))
.environment(\.colorScheme, colorScheme)
}
}
}
}

View file

@ -72,14 +72,17 @@ struct XcodeListViewRow: View {
@ViewBuilder
private func installControl(for xcode: Xcode) -> some View {
if xcode.installed {
switch xcode.installState {
case .installed:
Button("OPEN") { appState.open(id: xcode.id) }
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: selected))
.help("Open this version")
} else {
Button("INSTALL") { print("Installing...") }
case .notInstalled:
Button("INSTALL") { appState.install(id: xcode.id) }
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: selected))
.help("Install this version")
case let .installing(installationStep):
InstallationStepView(installationStep: installationStep, highlighted: selected, cancel: {})
}
}
}
@ -98,7 +101,7 @@ struct XcodeListViewRow_Previews: PreviewProvider {
)
XcodeListViewRow(
xcode: Xcode(version: Version("12.1.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil),
xcode: Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, path: nil, icon: nil),
selected: false
)