Application list UI enhancements

This commit is contained in:
Daniil Vinogradov 2025-03-29 22:48:52 +01:00
parent 56054b75fb
commit 2816cf2729

View file

@ -11,84 +11,113 @@ struct InstalledAppsListView: View {
@StateObject private var viewModel = InstalledAppsViewModel()
@State private var appIcons: [String: UIImage] = [:]
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
@AppStorage("recentApps") var recentApps: [String] = []
var onSelectApp: (String) -> Void
var body: some View {
NavigationView {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
onSelectApp(bundleID)
}
}) {
HStack(spacing: 16) {
// App Icon
if let image = appIcons[bundleID] {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.cornerRadius(12)
.shadow(color: colorScheme == .dark ? Color.black.opacity(0.2) : Color.gray.opacity(0.2),
radius: 3, x: 0, y: 1)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color(UIColor.systemGray5))
.frame(width: 60, height: 60)
.overlay(
Image(systemName: "app")
.font(.system(size: 26))
.foregroundColor(.gray)
)
.shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1)
.onAppear {
loadAppIcon(for: bundleID)
List {
if !recentApps.isEmpty {
Section {
ForEach(recentApps, id: \.self) { bundleID in
AppButton(bundleID: bundleID, appName: viewModel.apps[bundleID] ?? "", recentApps: $recentApps, appIcons: $appIcons, onSelectApp: onSelectApp)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
withAnimation {
recentApps.removeAll(where: { $0 == bundleID })
}
} label: {
Label("Delete", systemImage: "trash")
}
}
// App Name and Bundle ID
VStack(alignment: .leading, spacing: 4) {
Text(appName)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(Color.blue)
Text(bundleID)
.font(.system(size: 15))
.foregroundColor(Color.gray)
.lineLimit(1)
}
Spacer()
}
.padding(.vertical, 12)
.padding(.horizontal, 20)
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
if appName != viewModel.apps.sorted(by: { $0.key < $1.key }).last?.key {
Divider()
.padding(.leading, 96)
.padding(.trailing, 20)
.opacity(0.4)
}
} header: {
Text("Recents")
}
}
Section {
ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in
AppButton(bundleID: bundleID, appName: appName, recentApps: $recentApps, appIcons: $appIcons, onSelectApp: onSelectApp)
}
} header: {
if !recentApps.isEmpty {
Text("All applications")
} else {
EmptyView()
}
}
.background(Color(UIColor.systemBackground))
}
.background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all))
.listStyle(.plain)
.navigationTitle("Installed Apps")
.navigationBarItems(leading: Button("Done") {
dismiss()
.toolbar(content: {
Button("Done") {
dismiss()
}
})
}
.background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all))
}
}
struct AppButton: View {
@State var bundleID: String
@State var appName: String
@Binding var recentApps: [String]
@Binding var appIcons: [String: UIImage]
var onSelectApp: (String) -> Void
@Environment(\.colorScheme) private var colorScheme
var body: some View {
Button(action: {
recentApps.removeAll(where: { $0 == bundleID })
recentApps = [bundleID] + recentApps
onSelectApp(bundleID)
}) {
HStack(spacing: 16) {
// App Icon
if let image = appIcons[bundleID] {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.cornerRadius(12)
.shadow(color: colorScheme == .dark ? Color.black.opacity(0.2) : Color.gray.opacity(0.2),
radius: 3, x: 0, y: 1)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color(UIColor.systemGray5))
.frame(width: 60, height: 60)
.overlay(
Image(systemName: "app")
.font(.system(size: 26))
.foregroundColor(.gray)
)
.shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1)
.onAppear {
loadAppIcon(for: bundleID)
}
}
// App Name and Bundle ID
VStack(alignment: .leading, spacing: 4) {
Text(appName)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(Color(.label))
Text(bundleID)
.font(.system(size: 15))
.foregroundColor(Color.gray)
.lineLimit(1)
}
Spacer()
}
.font(.system(size: 17, weight: .regular))
.foregroundColor(.blue))
}
}
// Helper method to load app icon
private func loadAppIcon(for bundleID: String) {
AppStoreIconFetcher.getIcon(for: bundleID) { image in
@ -103,6 +132,26 @@ struct InstalledAppsListView: View {
}
}
extension Array: @retroactive RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
#Preview {
InstalledAppsListView { _ in }
}