Fix build errors, resolve all warnings, and improve code quality
Widget Extension Fixes: - Create standalone WidgetDataProvider for widget data isolation - Add WIDGET_EXTENSION compiler flag for conditional compilation - Fix DataController references in widget-shared files - Sync widget version numbers with main app (23, 1.0.2) - Add WidgetBackground color to asset catalog Warning Resolutions: - Fix UIScreen.main deprecation in BGView and SharingListView - Fix Text '+' concatenation deprecation in PurchaseButtonView and SettingsTabView - Fix exhaustive switch in BiometricAuthManager (add .none case) - Fix var to let in ExportService (3 instances) - Fix unused result warning in NoteEditorView - Fix ForEach duplicate ID warnings in MonthView and YearView Code Quality Improvements: - Wrap bypassSubscription in #if DEBUG for security - Rename StupidAssCustomWidgetObservableObject to CustomWidgetStateViewModel - Add @MainActor to IconViewModel - Replace fatalError with graceful fallback in SharedModelContainer - Add [weak self] to closures in DayViewViewModel - Add OSLog-based AppLogger for production logging - Add ImageCache with NSCache for memory efficiency - Add AccessibilityHelpers with Reduce Motion support - Create DataControllerProtocol for dependency injection - Update .gitignore with secrets exclusions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
34
Shared/Services/AppLogger.swift
Normal file
34
Shared/Services/AppLogger.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// AppLogger.swift
|
||||
// Feels
|
||||
//
|
||||
// Centralized logging using OSLog for production-ready logging.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
/// Centralized logging utility using OSLog
|
||||
enum AppLogger {
|
||||
// MARK: - Loggers by Category
|
||||
|
||||
static let general = Logger(subsystem: subsystem, category: "General")
|
||||
static let iap = Logger(subsystem: subsystem, category: "IAP")
|
||||
static let healthKit = Logger(subsystem: subsystem, category: "HealthKit")
|
||||
static let liveActivity = Logger(subsystem: subsystem, category: "LiveActivity")
|
||||
static let notifications = Logger(subsystem: subsystem, category: "Notifications")
|
||||
static let photos = Logger(subsystem: subsystem, category: "Photos")
|
||||
static let export = Logger(subsystem: subsystem, category: "Export")
|
||||
static let settings = Logger(subsystem: subsystem, category: "Settings")
|
||||
static let biometrics = Logger(subsystem: subsystem, category: "Biometrics")
|
||||
static let ai = Logger(subsystem: subsystem, category: "AI")
|
||||
static let events = Logger(subsystem: subsystem, category: "Events")
|
||||
static let userDefaults = Logger(subsystem: subsystem, category: "UserDefaults")
|
||||
static let backgroundTasks = Logger(subsystem: subsystem, category: "BackgroundTasks")
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private static var subsystem: String {
|
||||
Bundle.main.bundleIdentifier ?? "com.tt.ifeel"
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,8 @@ class BiometricAuthManager: ObservableObject {
|
||||
|
||||
var biometricName: String {
|
||||
switch biometricType {
|
||||
case .none:
|
||||
return "Passcode"
|
||||
case .faceID:
|
||||
return "Face ID"
|
||||
case .touchID:
|
||||
@@ -58,6 +60,8 @@ class BiometricAuthManager: ObservableObject {
|
||||
|
||||
var biometricIcon: String {
|
||||
switch biometricType {
|
||||
case .none:
|
||||
return "lock.fill"
|
||||
case .faceID:
|
||||
return "faceid"
|
||||
case .touchID:
|
||||
|
||||
@@ -277,7 +277,7 @@ class ExportService {
|
||||
// MARK: - PDF Drawing: Mood Distribution Chart
|
||||
|
||||
private func drawMoodDistributionChart(at y: CGFloat, stats: ExportStats, margin: CGFloat, width: CGFloat, in context: UIGraphicsPDFRendererContext) -> CGFloat {
|
||||
var currentY = drawSectionTitle("Mood Distribution", at: y, margin: margin)
|
||||
let currentY = drawSectionTitle("Mood Distribution", at: y, margin: margin)
|
||||
|
||||
let chartHeight: CGFloat = 140
|
||||
let barHeight: CGFloat = 22
|
||||
@@ -334,7 +334,7 @@ class ExportService {
|
||||
// MARK: - PDF Drawing: Weekday Analysis
|
||||
|
||||
private func drawWeekdayAnalysis(at y: CGFloat, entries: [MoodEntryModel], margin: CGFloat, width: CGFloat, in context: UIGraphicsPDFRendererContext) -> CGFloat {
|
||||
var currentY = drawSectionTitle("Mood by Day of Week", at: y, margin: margin)
|
||||
let currentY = drawSectionTitle("Mood by Day of Week", at: y, margin: margin)
|
||||
|
||||
let weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
var weekdayAverages: [Double] = Array(repeating: 0, count: 7)
|
||||
@@ -488,7 +488,7 @@ class ExportService {
|
||||
// MARK: - PDF Drawing: Streaks Section
|
||||
|
||||
private func drawStreaksSection(at y: CGFloat, stats: ExportStats, margin: CGFloat, width: CGFloat, in context: UIGraphicsPDFRendererContext) -> CGFloat {
|
||||
var currentY = drawSectionTitle("Streaks & Consistency", at: y, margin: margin)
|
||||
let currentY = drawSectionTitle("Streaks & Consistency", at: y, margin: margin)
|
||||
|
||||
let cardWidth = (width - 15) / 2
|
||||
let cardHeight: CGFloat = 60
|
||||
|
||||
173
Shared/Services/ImageCache.swift
Normal file
173
Shared/Services/ImageCache.swift
Normal file
@@ -0,0 +1,173 @@
|
||||
//
|
||||
// ImageCache.swift
|
||||
// Feels
|
||||
//
|
||||
// In-memory image cache for thumbnail images to improve scrolling performance.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
/// Thread-safe in-memory image cache
|
||||
final class ImageCache {
|
||||
static let shared = ImageCache()
|
||||
|
||||
private let cache = NSCache<NSString, UIImage>()
|
||||
private let queue = DispatchQueue(label: "com.tt.ifeel.imagecache", qos: .userInitiated)
|
||||
|
||||
private init() {
|
||||
// Configure cache limits
|
||||
cache.countLimit = 100 // Max 100 images
|
||||
cache.totalCostLimit = 50 * 1024 * 1024 // 50 MB max
|
||||
|
||||
// Clear cache on memory warning
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: UIApplication.didReceiveMemoryWarningNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.clearCache()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// Get image from cache
|
||||
func image(forKey key: String) -> UIImage? {
|
||||
queue.sync {
|
||||
cache.object(forKey: key as NSString)
|
||||
}
|
||||
}
|
||||
|
||||
/// Store image in cache
|
||||
func setImage(_ image: UIImage, forKey key: String) {
|
||||
queue.async { [weak self] in
|
||||
let cost = Int(image.size.width * image.size.height * 4) // Approximate memory cost
|
||||
self?.cache.setObject(image, forKey: key as NSString, cost: cost)
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove image from cache
|
||||
func removeImage(forKey key: String) {
|
||||
queue.async { [weak self] in
|
||||
self?.cache.removeObject(forKey: key as NSString)
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all cached images
|
||||
func clearCache() {
|
||||
queue.async { [weak self] in
|
||||
self?.cache.removeAllObjects()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience Methods for Photo IDs
|
||||
|
||||
/// Get cached image for photo ID
|
||||
func image(forPhotoID id: UUID) -> UIImage? {
|
||||
image(forKey: id.uuidString)
|
||||
}
|
||||
|
||||
/// Store image for photo ID
|
||||
func setImage(_ image: UIImage, forPhotoID id: UUID) {
|
||||
setImage(image, forKey: id.uuidString)
|
||||
}
|
||||
|
||||
/// Remove cached image for photo ID
|
||||
func removeImage(forPhotoID id: UUID) {
|
||||
removeImage(forKey: id.uuidString)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cached Photo Loading
|
||||
|
||||
extension PhotoManager {
|
||||
/// Load thumbnail with caching
|
||||
func loadCachedThumbnail(id: UUID) -> UIImage? {
|
||||
// Check cache first
|
||||
if let cached = ImageCache.shared.image(forPhotoID: id) {
|
||||
return cached
|
||||
}
|
||||
|
||||
// Load from disk
|
||||
guard let image = loadThumbnail(id: id) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cache for future use
|
||||
ImageCache.shared.setImage(image, forPhotoID: id)
|
||||
return image
|
||||
}
|
||||
|
||||
/// Load full image with caching
|
||||
func loadCachedPhoto(id: UUID) -> UIImage? {
|
||||
let cacheKey = "\(id.uuidString)-full"
|
||||
|
||||
// Check cache first
|
||||
if let cached = ImageCache.shared.image(forKey: cacheKey) {
|
||||
return cached
|
||||
}
|
||||
|
||||
// Load from disk
|
||||
guard let image = loadPhoto(id: id) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cache for future use
|
||||
ImageCache.shared.setImage(image, forKey: cacheKey)
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SwiftUI Cached Image View
|
||||
|
||||
struct CachedAsyncImage: View {
|
||||
let photoID: UUID?
|
||||
let useThumbnail: Bool
|
||||
|
||||
@State private var image: UIImage?
|
||||
@State private var isLoading = false
|
||||
|
||||
init(photoID: UUID?, useThumbnail: Bool = true) {
|
||||
self.photoID = photoID
|
||||
self.useThumbnail = useThumbnail
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let image = image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} else if isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Color.gray.opacity(0.2)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
loadImage()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadImage() {
|
||||
guard let id = photoID else { return }
|
||||
isLoading = true
|
||||
|
||||
Task {
|
||||
let loadedImage = await Task.detached(priority: .userInitiated) {
|
||||
if useThumbnail {
|
||||
return await PhotoManager.shared.loadCachedThumbnail(id: id)
|
||||
} else {
|
||||
return await PhotoManager.shared.loadCachedPhoto(id: id)
|
||||
}
|
||||
}.value
|
||||
|
||||
await MainActor.run {
|
||||
self.image = loadedImage
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import os.log
|
||||
|
||||
@MainActor
|
||||
class PhotoManager: ObservableObject {
|
||||
@@ -26,7 +27,7 @@ class PhotoManager: ObservableObject {
|
||||
guard let containerURL = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: Constants.currentGroupShareId
|
||||
) else {
|
||||
print("PhotoManager: Failed to get app group container")
|
||||
AppLogger.photos.error("Failed to get app group container")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -37,7 +38,7 @@ class PhotoManager: ObservableObject {
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: photosURL, withIntermediateDirectories: true)
|
||||
} catch {
|
||||
print("PhotoManager: Failed to create photos directory: \(error)")
|
||||
AppLogger.photos.error("Failed to create photos directory: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -54,7 +55,7 @@ class PhotoManager: ObservableObject {
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: thumbnailsURL, withIntermediateDirectories: true)
|
||||
} catch {
|
||||
print("PhotoManager: Failed to create thumbnails directory: \(error)")
|
||||
AppLogger.photos.error("Failed to create thumbnails directory: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -76,14 +77,14 @@ class PhotoManager: ObservableObject {
|
||||
// Save full resolution
|
||||
let fullURL = photosDir.appendingPathComponent(filename)
|
||||
guard let fullData = image.jpegData(compressionQuality: compressionQuality) else {
|
||||
print("PhotoManager: Failed to create JPEG data")
|
||||
AppLogger.photos.error("Failed to create JPEG data")
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
try fullData.write(to: fullURL)
|
||||
} catch {
|
||||
print("PhotoManager: Failed to save photo: \(error)")
|
||||
AppLogger.photos.error("Failed to save photo: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -151,7 +152,7 @@ class PhotoManager: ObservableObject {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: fullURL)
|
||||
} catch {
|
||||
print("PhotoManager: Failed to delete photo: \(error)")
|
||||
AppLogger.photos.error("Failed to delete photo: \(error.localizedDescription)")
|
||||
success = false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user