Skip to content

Commit

Permalink
Update to Swift 6
Browse files Browse the repository at this point in the history
Refactor the Cask view model for Swift 6 compatibility
  • Loading branch information
milanvarady committed Dec 29, 2024
1 parent 1586f56 commit 00cb819
Show file tree
Hide file tree
Showing 19 changed files with 183 additions and 135 deletions.
10 changes: 7 additions & 3 deletions Applite.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
4192566B2D1F286B00D9EF10 /* Cask+BrewFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192566A2D1F286B00D9EF10 /* Cask+BrewFunctions.swift */; };
4192566E2D1F293700D9EF10 /* Cask+LaunchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192566D2D1F293700D9EF10 /* Cask+LaunchApp.swift */; };
419256702D1F299E00D9EF10 /* Cask+ProtocolConformances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4192566F2D1F299E00D9EF10 /* Cask+ProtocolConformances.swift */; };
419256832D22055200D9EF10 /* CaskInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419256822D22055200D9EF10 /* CaskInfo.swift */; };
419506A42964A27F00FE5802 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419506A32964A27F00FE5802 /* SetupView.swift */; };
419506A62964A5EF00FE5802 /* BrewPathSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */; };
4196C8F528F9CB2600EADDDA /* DiscoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4196C8F428F9CB2600EADDDA /* DiscoverView.swift */; };
Expand Down Expand Up @@ -192,6 +193,7 @@
4192566A2D1F286B00D9EF10 /* Cask+BrewFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cask+BrewFunctions.swift"; sourceTree = "<group>"; };
4192566D2D1F293700D9EF10 /* Cask+LaunchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cask+LaunchApp.swift"; sourceTree = "<group>"; };
4192566F2D1F299E00D9EF10 /* Cask+ProtocolConformances.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cask+ProtocolConformances.swift"; sourceTree = "<group>"; };
419256822D22055200D9EF10 /* CaskInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaskInfo.swift; sourceTree = "<group>"; };
419506A32964A27F00FE5802 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
419506A52964A5EF00FE5802 /* BrewPathSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPathSelectorView.swift; sourceTree = "<group>"; };
419506A729696A5300FE5802 /* Applite-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Applite-Info.plist"; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -369,8 +371,9 @@
children = (
419256692D1F284100D9EF10 /* Cask */,
418F332528EC921D0023D76F /* CaskData.swift */,
4166EE7C28F73B2300CE305A /* BrewAnalytics.swift */,
419256822D22055200D9EF10 /* CaskInfo.swift */,
4191392B29159B5C00F1D75D /* CaskDTO.swift */,
4166EE7C28F73B2300CE305A /* BrewAnalytics.swift */,
);
path = "Cask Models";
sourceTree = "<group>";
Expand Down Expand Up @@ -749,6 +752,7 @@
413E60C22BBFF98A00978F6A /* AppIconView.swift in Sources */,
418989AD2A33A5C4004AC23B /* BrewManagementView.swift in Sources */,
418989B22A35D651004AC23B /* isBrewPathValid.swift in Sources */,
419256832D22055200D9EF10 /* CaskInfo.swift in Sources */,
4192560D2D1CA02C00D9EF10 /* ShellError.swift in Sources */,
4192566B2D1F286B00D9EF10 /* Cask+BrewFunctions.swift in Sources */,
4192561A2D1DEB2100D9EF10 /* AppView+OpenAndManageView.swift in Sources */,
Expand Down Expand Up @@ -931,7 +935,7 @@
PRODUCT_BUNDLE_IDENTIFIER = dev.aerolite.Applite;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
};
name = Debug;
};
Expand Down Expand Up @@ -966,7 +970,7 @@
PRODUCT_BUNDLE_IDENTIFIER = dev.aerolite.Applite;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
};
name = Release;
};
Expand Down
40 changes: 23 additions & 17 deletions Applite/Model/Cask Models/Cask/Cask+BrewFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension Cask {
resetProgressState(caskData: caskData)
}

Self.logger.info("Cask \"\(self.id)\" installation started")
Self.logger.info("Cask \"\(self.info.id)\" installation started")

// Appdir argument
let appdirOn = UserDefaults.standard.bool(forKey: Preferences.appdirOn.rawValue)
Expand All @@ -45,7 +45,7 @@ extension Cask {
let alertMessage = switch completeOutput {
// Already installed
case _ where completeOutput.contains("It seems there is already an App"):
String(localized: "\(self.name) is already installed. If you want to add it to \(Bundle.main.appName) click more options (chevron icon) and press Force Install.")
String(localized: "\(self.info.name) is already installed. If you want to add it to \(Bundle.main.appName) click more options (chevron icon) and press Force Install.")
// Network error
case _ where completeOutput.contains("Could not resolve host"):
String(localized: "Couldn't download app. No internet connection, or host is unreachable.")
Expand All @@ -56,15 +56,15 @@ extension Cask {
showFailure(
error: error,
output: completeOutput,
alertTitle: "Failed to install \(self.name)",
alertTitle: "Failed to install \(self.info.name)",
alertMessage: alertMessage
)
return
}

showSuccess(
logMessage: "Successfully installed cask \(self.id)",
alertTitle: "\(self.name) successfully installed!"
alertTitle: "\(self.info.name) successfully installed!"
)

// Update state
Expand All @@ -83,7 +83,7 @@ extension Cask {
progressState = .busy(withTask: "Uninstalling")
caskData.busyCasks.insert(self)

var arguments: [String] = [self.id]
var arguments: [String] = [self.info.id]

// Add -- zap argument
if zap {
Expand All @@ -98,15 +98,15 @@ extension Cask {
showFailure(
error: error,
output: output,
alertTitle: "Failed to uninstall \(self.name)",
alertTitle: "Failed to uninstall \(self.info.name)",
alertMessage: error.localizedDescription
)
return
}

showSuccess(
logMessage: "Successfully uninstalled \(self.id)",
alertTitle: "\(self.name) successfully uninstalled"
logMessage: "Successfully uninstalled \(self.info.id)",
alertTitle: "\(self.info.name) successfully uninstalled"
)

// Update state
Expand All @@ -125,20 +125,20 @@ extension Cask {
var output: String = ""

do {
output = try await Shell.runBrewCommand("uninstall", arguments: [self.id])
output = try await Shell.runBrewCommand("uninstall", arguments: [self.info.id])
} catch {
showFailure(
error: error,
output: output,
alertTitle: "Failed to update \(self.name)",
alertTitle: "Failed to update \(self.info.name)",
alertMessage: error.localizedDescription
)
return
}

showSuccess(
logMessage: "Successfully updated \(self.id)",
alertTitle: "\(self.name) successfully updated"
alertTitle: "\(self.info.name) successfully updated"
)

// Update state
Expand All @@ -157,20 +157,20 @@ extension Cask {
var output: String = ""

do {
output = try await Shell.runBrewCommand("uninstall", arguments: [self.id])
output = try await Shell.runBrewCommand("uninstall", arguments: [self.info.id])
} catch {
showFailure(
error: error,
output: output,
alertTitle: "Failed to reinstall \(self.name)",
alertTitle: "Failed to reinstall \(self.info.name)",
alertMessage: error.localizedDescription
)
return
}

showSuccess(
logMessage: "Successfully reinstalled \(self.id)",
alertTitle: "\(self.name) successfully reinstalled"
logMessage: "Successfully reinstalled \(self.info.id)",
alertTitle: "\(self.info.name) successfully reinstalled"
)
}

Expand Down Expand Up @@ -208,7 +208,10 @@ extension Cask {
alertMessage: String = ""
) {
Self.logger.info("\(logMessage)")
sendNotification(title: alertTitle, body: alertMessage, reason: .success)

Task {
await sendNotification(title: alertTitle, body: alertMessage, reason: .success)
}

// Show success for 2 seconds
progressState = .success
Expand Down Expand Up @@ -239,7 +242,10 @@ extension Cask {

// Send notification
let notificationTitle = notificationTitle ?? alertTitle
sendNotification(title: notificationTitle, body: notificationMessage, reason: .failure)

Task {
await sendNotification(title: notificationTitle, body: notificationMessage, reason: .failure)
}

// Set progress state to failed
progressState = .failed(output: output)
Expand Down
4 changes: 2 additions & 2 deletions Applite/Model/Cask Models/Cask/Cask+LaunchApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ extension Cask {
func launchApp() async throws {
let appPath: String

if self.pkgInstaller {
if self.info.pkgInstaller {
// Open PKG type app
var applicationsDirectory = "/Applications"

Expand All @@ -25,7 +25,7 @@ extension Cask {
}
}

appPath = "\"\(applicationsDirectory)/\(self.name).app\""
appPath = "\"\(applicationsDirectory)/\(self.info.name).app\""
} else {
// Open normal app
let brewDirectory = BrewPaths.currentBrewDirectory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import Foundation

extension Cask {
nonisolated var id: String {
self.info.id
}

// Equatable
nonisolated static func == (lhs: Cask, rhs: Cask) -> Bool {
lhs.id == rhs.id
Expand Down
58 changes: 20 additions & 38 deletions Applite/Model/Cask Models/Cask/Cask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,15 @@
import SwiftUI
import os

/// Holds all essential data of a Homebrew cask and provides methods to run brew commands on it (e.g. install, uninstall, update)
/// A view model that holds all essential data of a Homebrew cask and provides methods to run brew commands on it (e.g. install, uninstall, update)
@MainActor
final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
// MARK: - Static properties
final class Cask: ObservableObject, Identifiable, Hashable {
/// Static cask information
let info: CaskInfo

/// Unique id of the class, this is the same name you would use to download the cask with brew
let id: String
/// Longer format cask name
let name: String
/// Short description
let description: String
let homepageURL: URL?
/// Number of downloads in the last 365 days
var downloadsIn365days: Int = 0
/// Description of any caveats with the app
let caveats: String?
/// If true app has a .pkg installer
let pkgInstaller: Bool

let downloadsIn365days: Int

// MARK: - Published properties

@Published var isInstalled: Bool = false
Expand All @@ -51,27 +41,19 @@ final class Cask: Identifiable, Decodable, Hashable, ObservableObject {
case failed(output: String)
}

// MARK: - Initializers

nonisolated init(from decoder: Decoder) throws {
let rawData = try? CaskDTO(from: decoder)

let homepage: String = rawData?.homepage ?? "https://brew.sh/"

self.id = rawData?.token ?? "N/A"
self.name = rawData?.nameArray[0] ?? "N/A"
self.description = rawData?.desc ?? "N/A"
self.homepageURL = URL(string: homepage)
self.caveats = rawData?.caveats
self.pkgInstaller = rawData?.url.hasSuffix("pkg") ?? false
}

init() {
self.id = "test"
self.name = "Test app"
self.description = "An application to test this application"
self.homepageURL = URL(string: "https://aerolite.dev/")
self.caveats = nil
self.pkgInstaller = false
required init(info: CaskInfo, downloadsIn365days: Int, isInstalled: Bool = false, isOutdated: Bool = false) {
self.info = info
self.downloadsIn365days = downloadsIn365days
self.isInstalled = isInstalled
self.isOutdated = isOutdated
}

static let dummy = Cask(info: CaskInfo(
id: "test",
name: "Test",
description: "Test application",
homepageURL: URL(string: "https://aerolite.dev/"),
caveats: nil,
pkgInstaller: false
), downloadsIn365days: 100)
}
47 changes: 21 additions & 26 deletions Applite/Model/Cask Models/CaskData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ final class CaskData: ObservableObject {
/// Gets cask information from the Homebrew API and decodes it into a list of ``Cask`` objects
/// - Returns: List of ``Cask`` objects
@Sendable
func loadCaskObjects() async throws -> [Cask] {
func loadCaskInfo() async throws -> [CaskInfo] {
// Get json data from api
guard let casksURL = URL(string: "https://formulae.brew.sh/api/cask.json") else { return [] }

Expand All @@ -68,8 +68,8 @@ final class CaskData: ObservableObject {
// Chache json file
await cacheData(data: caskData, to: Self.caskCacheURL)

// Decode data
return try JSONDecoder().decode([Cask].self, from: caskData)
// Decode static cask data
return try JSONDecoder().decode([CaskInfo].self, from: caskData)
}

/// Gets cask analytics information from the Homebrew API and decodes it into a dictionary
Expand Down Expand Up @@ -182,7 +182,7 @@ final class CaskData: ObservableObject {
for category in categories {
// Filter casks
let filteredCasks = casks.filter {
category.casks.contains($0.id)
category.casks.contains($0.info.id)
}

// Sort by number of downloads
Expand All @@ -201,31 +201,26 @@ final class CaskData: ObservableObject {
}

// Get data components concurrently
async let caskData = loadCaskObjects()
async let caskInfo = loadCaskInfo()
async let analyticsDict = loadAnalyticsData()
async let installedCasks = getInstalledCasks()
async let outdatedCaskIDs = getOutdatedCasks()

// Combine data into a final list of `Cask` objects
do {
for i in try await caskData.indices {
try await caskData[i].downloadsIn365days = try await analyticsDict[try await caskData[i].id] ?? 0

if try await installedCasks.contains(try await caskData[i].id) {
try await caskData[i].isInstalled = true

if try await outdatedCaskIDs.contains(try await caskData[i].id) {
try await caskData[i].isOutdated = true
self.outdatedCasks.insert(try await caskData[i])
}
}
}
} catch {
Self.logger.error("Error while trying to combine cask data. Message: \(error.localizedDescription)")

var casks: [Cask] = []

for caskInfo in try await caskInfo {
let cask = Cask(
info: caskInfo,
downloadsIn365days: try await analyticsDict[caskInfo.id] ?? 0,
isInstalled: try await installedCasks.contains(caskInfo.id),
isOutdated: try await outdatedCaskIDs.contains(caskInfo.id)
)

casks.append(cask)
}
self.casks = try await caskData

self.casks = casks

Self.logger.info("Cask data loaded successfully!")

// Create category dicts
Expand All @@ -242,7 +237,7 @@ final class CaskData: ObservableObject {
.map({ $0.trimmingCharacters(in: .whitespacesAndNewlines) }) // Trim whitespace

for i in self.casks.indices {
if outdatedCaskIDs.contains(self.casks[i].id) && self.casks[i].isInstalled {
if outdatedCaskIDs.contains(self.casks[i].info.id) && self.casks[i].isInstalled {
self.casks[i].isOutdated = true
outdatedCasks.insert(casks[i])
}
Expand Down
Loading

0 comments on commit 00cb819

Please sign in to comment.