diff --git a/virtualOS.xcodeproj/project.pbxproj b/virtualOS.xcodeproj/project.pbxproj index 6b6c096..227d96e 100644 --- a/virtualOS.xcodeproj/project.pbxproj +++ b/virtualOS.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 0005A77A27E2809E0013BE83 /* VirtualMachineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0005A77927E2809E0013BE83 /* VirtualMachineView.swift */; }; 002E64922871B2DD00CE95A0 /* UserDefaults+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002E64912871B2DD00CE95A0 /* UserDefaults+Settings.swift */; }; - 0044A65527F601E60007988A /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0044A65427F601E60007988A /* MainViewModel.swift */; }; + 0044A65527F601E60007988A /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0044A65427F601E60007988A /* ViewModel.swift */; }; 0044A65A27F76BD30007988A /* URL+Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0044A65927F76BD30007988A /* URL+Paths.swift */; }; 006504E727F9D59300723BCA /* ConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 006504E627F9D59300723BCA /* ConfigurationView.swift */; }; 007987AF27E2487200960D74 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 007987AE27E2487200960D74 /* LICENSE */; }; @@ -49,7 +49,7 @@ /* Begin PBXFileReference section */ 0005A77927E2809E0013BE83 /* VirtualMachineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMachineView.swift; sourceTree = ""; }; 002E64912871B2DD00CE95A0 /* UserDefaults+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Settings.swift"; sourceTree = ""; }; - 0044A65427F601E60007988A /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = ""; }; + 0044A65427F601E60007988A /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; 0044A65927F76BD30007988A /* URL+Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Paths.swift"; sourceTree = ""; }; 006504E627F9D59300723BCA /* ConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationView.swift; sourceTree = ""; }; 007987AE27E2487200960D74 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; @@ -135,8 +135,8 @@ children = ( 00BA26AC2826DAF200E80B76 /* Info.plist */, 00989C6327E2340C0048776B /* virtualOSApp.swift */, - 00989C9127E236A10048776B /* Model */, 00989C9327E236A10048776B /* View */, + 00989C9127E236A10048776B /* Model */, 0090AF5F27E25F6F0077D35F /* Extension */, 00989C6727E2340D0048776B /* Assets.xcassets */, 00989C6C27E2340D0048776B /* virtualOS.entitlements */, @@ -174,7 +174,7 @@ isa = PBXGroup; children = ( 01FCAD8329AB707C00F12689 /* ApplicationDelegate.swift */, - 0044A65427F601E60007988A /* MainViewModel.swift */, + 0044A65427F601E60007988A /* ViewModel.swift */, 007987B027E24A8400960D74 /* VirtualMac.swift */, 00989C9927E238930048776B /* VirtualMacConfiguration.swift */, 01B042F129CD9F6A003CD5C2 /* Bookmark.swift */, @@ -338,7 +338,7 @@ 00989C9A27E238930048776B /* VirtualMacConfiguration.swift in Sources */, 01B042F229CD9F6A003CD5C2 /* Bookmark.swift in Sources */, 0090AF6127E25F6F0077D35F /* UInt64+Byte.swift in Sources */, - 0044A65527F601E60007988A /* MainViewModel.swift in Sources */, + 0044A65527F601E60007988A /* ViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -500,7 +500,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 12; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"virtualOS/Preview Content\""; DEVELOPMENT_TEAM = 2AD47BTDQ6; @@ -515,7 +515,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.3; + MARKETING_VERSION = 1.3.2; PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -533,7 +533,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 12; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"virtualOS/Preview Content\""; DEVELOPMENT_TEAM = 2AD47BTDQ6; @@ -548,7 +548,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.3; + MARKETING_VERSION = 1.3.2; PRODUCT_BUNDLE_IDENTIFIER = com.github.yep.ios.virtualOS; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/virtualOS/Model/Bookmark.swift b/virtualOS/Model/Bookmark.swift index e9ce4ee..c9f6917 100644 --- a/virtualOS/Model/Bookmark.swift +++ b/virtualOS/Model/Bookmark.swift @@ -35,15 +35,9 @@ struct Bookmark { previousURL.stopAccessingSecurityScopedResource() } - if accessedURLs[key] == bookmarkURL { - // resource already accessed, do nothing - } else { - // start access resource - if !bookmarkURL.startAccessingSecurityScopedResource() { - // access failed - bookmarkURL.stopAccessingSecurityScopedResource() - return nil - } + if accessedURLs[key] != bookmarkURL { + // resource not already accessed, start access + _ = bookmarkURL.startAccessingSecurityScopedResource() accessedURLs[key] = bookmarkURL } return bookmarkURL diff --git a/virtualOS/Model/MainViewModel.swift b/virtualOS/Model/ViewModel.swift similarity index 98% rename from virtualOS/Model/MainViewModel.swift rename to virtualOS/Model/ViewModel.swift index d6b096d..4c4908f 100644 --- a/virtualOS/Model/MainViewModel.swift +++ b/virtualOS/Model/ViewModel.swift @@ -1,5 +1,5 @@ // -// MainViewModel.swift +// ViewModel.swift // virtualOS // // Created by Jahn Bertsch on 31.03.22. @@ -11,7 +11,7 @@ import Foundation import Virtualization import OSLog -final class MainViewModel: NSObject, ObservableObject { +final class ViewModel: NSObject, ObservableObject { enum State: String { case Downloading case Installing @@ -342,7 +342,7 @@ final class MainViewModel: NSObject, ObservableObject { } } -extension MainViewModel: VZVirtualMachineDelegate { +extension ViewModel: VZVirtualMachineDelegate { func guestDidStop(_ vm: VZVirtualMachine) { state = .Stopped } diff --git a/virtualOS/View/ConfigurationView.swift b/virtualOS/View/ConfigurationView.swift index ce5f82b..3a0d276 100644 --- a/virtualOS/View/ConfigurationView.swift +++ b/virtualOS/View/ConfigurationView.swift @@ -19,7 +19,7 @@ struct ConfigurationView: View { case custom = 2 } - @ObservedObject var viewModel: MainViewModel + @ObservedObject var viewModel: ViewModel @State fileprivate var cpuCountSliderValue: Float = 0 { didSet { viewModel.virtualMac.parameters.cpuCount = Int(cpuCountSliderValue) @@ -125,7 +125,7 @@ struct ConfigurationView: View { Text("Shared Folder").frame(minWidth: textWidth, alignment: .leading) VStack(alignment: .leading, content: { Picker("", selection: $sharedFolderType) { - Text("None").tag(SharedFolderType.none) + Text("No shared folder").tag(SharedFolderType.none) Text("Custom").tag(SharedFolderType.custom) }.pickerStyle(.inline) Button("Select Shared Folder") { @@ -194,7 +194,7 @@ struct ConfigurationView: View { struct ConfigurationViewProvider_Previews: PreviewProvider { static var previews: some View { VStack { - ConfigurationView(viewModel: MainViewModel()) + ConfigurationView(viewModel: ViewModel()) } } } diff --git a/virtualOS/View/MainView.swift b/virtualOS/View/MainView.swift index 92f7cab..4da6bca 100644 --- a/virtualOS/View/MainView.swift +++ b/virtualOS/View/MainView.swift @@ -10,7 +10,7 @@ import SwiftUI struct MainView: View { - @ObservedObject var viewModel: MainViewModel + @ObservedObject var viewModel: ViewModel var body: some View { VStack { @@ -51,7 +51,7 @@ struct MainView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { - MainView(viewModel: MainViewModel()) + MainView(viewModel: ViewModel()) } } diff --git a/virtualOS/View/MenuCommands.swift b/virtualOS/View/MenuCommands.swift index 8fd34f1..076de54 100644 --- a/virtualOS/View/MenuCommands.swift +++ b/virtualOS/View/MenuCommands.swift @@ -10,7 +10,7 @@ import SwiftUI #if arch(arm64) struct MenuCommands: Commands { - @ObservedObject var viewModel: MainViewModel + @ObservedObject var viewModel: ViewModel var body: some Commands { CommandGroup(replacing: .appInfo) { @@ -30,7 +30,7 @@ struct MenuCommands: Commands { Divider() Button("Delete Restore Image") { viewModel.deleteRestoreImage() - }.disabled(!MainViewModel.restoreImageExists) + }.disabled(!ViewModel.restoreImageExists) Button("Delete Virtual Machine", action: { viewModel.deleteVirtualMachine() }) diff --git a/virtualOS/View/SettingsView.swift b/virtualOS/View/SettingsView.swift index 9a4ba4c..3cb55dd 100644 --- a/virtualOS/View/SettingsView.swift +++ b/virtualOS/View/SettingsView.swift @@ -9,49 +9,59 @@ import SwiftUI import UniformTypeIdentifiers +import AppKit struct SettingsView: View { - @ObservedObject var viewModel: MainViewModel - - enum HardDiskLocation: String, CaseIterable, Identifiable { + fileprivate enum HardDiskLocation: String, CaseIterable, Identifiable { case sandbox = "Sandbox" - case custom = "Select location where VM hard disk images will be stored." + case custom = "Select location where VM hard disk image will be stored." var id: Self { self } } - enum RestoreImageType: String, CaseIterable, Identifiable { + fileprivate enum RestoreImageType: String, CaseIterable, Identifiable { case latest = "Downloads latest restore image from Apple." - case custom = "Select custom restore image (.ipsw)\nFor example, download from [https://ipsw.me](https://ipsw.me/product/Mac)" + case custom = "Select custom restore image (.ipsw)\nFor example, download from https://ipsw.me/product/mac" var id: Self { self } } + fileprivate struct SizeConstants { + static let totalWidth = CGFloat(470) + static let infoWidth = CGFloat(300) + static let diskWidth = CGFloat(140) + static let locationWidth = CGFloat(450) + static let minTextHeight = CGFloat(28) + } - @State var diskSize = String(UserDefaults.standard.diskSize) - @State var hardDisk = HardDiskLocation.sandbox - @State var restoreImageType = RestoreImageType.latest - - var hardDiskLocationInfo: String { - if let customHardDiskURL = viewModel.customHardDiskURL { - return "Using \(customHardDiskURL.path)" - } else if hardDisk == .sandbox { + @ObservedObject var viewModel: ViewModel + @State fileprivate var diskSize = String(UserDefaults.standard.diskSize) + @State fileprivate var hardDiskLocation = HardDiskLocation.sandbox + @State fileprivate var restoreImageType = RestoreImageType.latest + @State fileprivate var showAlert = false + + fileprivate var hardDiskLocationString: String { + if hardDiskLocation == .sandbox { return URL.basePath } else { - return HardDiskLocation.custom.rawValue + if let customHardDiskURL = viewModel.customHardDiskURL { + return customHardDiskURL.path + } else { + return HardDiskLocation.custom.rawValue + } } } - var restoreImageInfo: String { + fileprivate var restoreImageInfoString: String { if let restoreImageURL = viewModel.customRestoreImageURL { - return "Using \(restoreImageURL.path)" + return restoreImageURL.path } else { return restoreImageType.rawValue } } var body: some View { - VStack { - Text("Settings") + VStack() { + Text("Settings").font(.headline) Form { HStack { TextField("Hard Disk Size:", text: $diskSize) - .frame(maxWidth: 130) + .frame(maxWidth: SizeConstants.diskWidth) .onChange(of: diskSize) { newValue in if let newDiskSize = Int(diskSize) { viewModel.diskSize = newDiskSize @@ -62,72 +72,70 @@ struct SettingsView: View { Text("(in GB)") } - Picker("Hard Disk Location:", selection: $hardDisk) { + Picker("Hard Disk Location:", selection: $hardDiskLocation) { Text("Default").tag(HardDiskLocation.sandbox) Text("Custom").tag(HardDiskLocation.custom) } .pickerStyle(.inline) - .onChange(of: hardDisk) { newValue in + .onChange(of: hardDiskLocation) { newValue in if newValue == .sandbox { UserDefaults.standard.hardDiskDirectoryBookmarkData = nil } } - + HStack { + Button("Show in Finder") { + NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: hardDiskLocationString) + }.disabled(hardDiskLocation != .sandbox && viewModel.customHardDiskURL == nil) + Button("Select Hard Disk Location") { selectCustomHardDiskLocation() - }.disabled(hardDisk == .sandbox) + }.disabled(hardDiskLocation == .sandbox) } - Text(.init("hardDiskLocationInfo")) + Text(.init(hardDiskLocationString)) .font(.caption) - .frame(maxWidth: 270, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: SizeConstants.infoWidth, minHeight: SizeConstants.minTextHeight, alignment: .topLeading) .lineLimit(nil) - .disabled(hardDisk == .sandbox) + .disabled(hardDiskLocation == .sandbox) + Picker("Restore Image:", selection: $restoreImageType) { Text("Latest").tag(RestoreImageType.latest) Text("Custom").tag(RestoreImageType.custom) }.pickerStyle(.inline) + Button("Select Restore Image") { selectRestoreImage() }.disabled(restoreImageType == .latest) - Text(restoreImageInfo) + + Text(restoreImageInfoString) .font(.caption) - .frame(maxWidth: 270, alignment: .leading) + .frame(maxWidth: SizeConstants.infoWidth, minHeight: SizeConstants.minTextHeight, alignment: .topLeading) .fixedSize(horizontal: false, vertical: true) .lineLimit(nil) .disabled(restoreImageType == .latest) }.padding(.bottom) - - Text("To open the hard disk location directory:\nIn Finder, in the 'Go' menu, select 'Go to Folder' and enter the path shown above.") - .frame(maxWidth: 370, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(nil) - .padding(.bottom) - .textSelection(.enabled) - .font(.caption) - + Button("OK") { viewModel.showSettings = !viewModel.showSettings }.keyboardShortcut(.defaultAction) } .padding() - .frame(minWidth: 420) + .frame(minWidth: SizeConstants.totalWidth, maxWidth: SizeConstants.totalWidth) .onAppear() { diskSize = String(viewModel.diskSize) if let hardDiskDirectoryBookmarkData = UserDefaults.standard.hardDiskDirectoryBookmarkData, let hardDiskURL = Bookmark.startAccess(data: hardDiskDirectoryBookmarkData, forType: .hardDisk) { - hardDisk = .custom + hardDiskLocation = .custom viewModel.customHardDiskURL = hardDiskURL } } } - + // MARK: - Private - + fileprivate func selectCustomHardDiskLocation() { let openPanel = NSOpenPanel() openPanel.directoryURL = URL(fileURLWithPath: URL.basePath, isDirectory: true) @@ -144,7 +152,7 @@ struct SettingsView: View { UserDefaults.standard.hardDiskDirectoryBookmarkData = hardDiskDirectoryBookmarkData } } - + fileprivate func selectRestoreImage() { guard let ipswContentType = UTType(filenameExtension: "ipsw") else { return @@ -159,13 +167,12 @@ struct SettingsView: View { viewModel.customRestoreImageURL = selectedURL } } - } struct SettingsViewProvider_Previews: PreviewProvider { static var previews: some View { VStack { - SettingsView(viewModel: MainViewModel()) + SettingsView(viewModel: ViewModel()) } } } diff --git a/virtualOS/virtualOS.entitlements b/virtualOS/virtualOS.entitlements index 38d08b4..cf5b2e0 100644 --- a/virtualOS/virtualOS.entitlements +++ b/virtualOS/virtualOS.entitlements @@ -10,7 +10,5 @@ com.apple.security.files.user-selected.read-write - com.apple.security.files.bookmarks.document-scope - diff --git a/virtualOS/virtualOSApp.swift b/virtualOS/virtualOSApp.swift index 181e874..152d047 100644 --- a/virtualOS/virtualOSApp.swift +++ b/virtualOS/virtualOSApp.swift @@ -17,7 +17,7 @@ struct virtualOSApp: App { static let logger = Logger(subsystem: "com.github.yep.virtualOS", category: "main") #if arch(arm64) - @StateObject var viewModel = MainViewModel() + @StateObject var viewModel = ViewModel() #endif @AppStorage("NSFullScreenMenuItemEverywhere") var fullScreenMenuItemEverywhere = false