Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
Binary file not shown.
69 changes: 69 additions & 0 deletions SpotifyClone/Core/Configuration/AppConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// AppConfiguration.swift
// SpotifyClone
//
// Created for code improvements
//

import Foundation

/// Centralized configuration for the application
enum AppConfiguration {
/// Spotify API configuration
enum Spotify {
static let baseURL = "https://api.spotify.com/v1"
static let authURL = "https://accounts.spotify.com"
static var tokenURL: String {
return "\(authURL)/api/token"
}

static var clientID: String {
guard let path = Bundle.main.path(forResource: "Config", ofType: "plist"),
let plist = NSDictionary(contentsOfFile: path) as? [String: Any],
let id = plist["SpotifyClientID"] as? String, !id.isEmpty else {
fatalError("SpotifyClientID not found in Config.plist")
}
return id
}

static var clientSecret: String {
guard let path = Bundle.main.path(forResource: "Config", ofType: "plist"),
let plist = NSDictionary(contentsOfFile: path) as? [String: Any],
let secret = plist["SpotifyClientSecret"] as? String, !secret.isEmpty else {
fatalError("SpotifyClientSecret not found in Config.plist")
}
return secret
}

static var redirectURI: String {
guard let path = Bundle.main.path(forResource: "Config", ofType: "plist"),
let plist = NSDictionary(contentsOfFile: path) as? [String: Any],
let uri = plist["SpotifyRedirectURI"] as? String else {
return "http://localhost:3000/callback"
}
return uri
}
}

/// UserDefaults keys
enum UserDefaultsKeys {
static let accessToken = "access_token"
static let refreshToken = "refresh_token"
static let expirationDate = "expiration_date"
}

/// Cache configuration
enum Cache {
static let maxMemorySize = 50 * 1024 * 1024 // 50MB
static let maxDiskSize = 200 * 1024 * 1024 // 200MB
static let expirationInterval: TimeInterval = 3600 // 1 hour
}

/// Network configuration
enum Network {
static let timeoutInterval: TimeInterval = 30
static let maxRetries = 3
static let retryBaseDelay: TimeInterval = 1.0
}
}

66 changes: 65 additions & 1 deletion SpotifyClone/Core/Error/ApiError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ enum ApiError: LocalizedError {
case apiError(String)
case decodingError(String)
case unknownError(String)
case rateLimitExceeded
case rateLimitExceeded(retryAfter: String? = nil)
case jsonSerializationFailed
case jsonParsingFailed

Expand Down Expand Up @@ -59,4 +59,68 @@ enum ApiError: LocalizedError {
return "JSON parsing failed"
}
}

/// User-friendly error message for display in UI
var userMessage: String {
switch self {
case .code:
return "Authentication failed. Please try logging in again."
case .tokenNotFound:
return "Your session has expired. Please log in again."
case .invalidInput:
return "Invalid input. Please check your search and try again."
case .invalidURL:
return "Something went wrong. Please try again."
case .failedToGetData:
return "Unable to load data. Please check your internet connection."
case .decodeError, .decodingError:
return "Unable to process the response. Please try again."
case .rateLimitExceeded(let retryAfter):
if let retryAfter = retryAfter, let seconds = Int(retryAfter) {
return "Too many requests. Please wait \(seconds) seconds and try again."
}
return "Too many requests. Please wait a moment and try again."
case .invalidResponse(let statusCode):
switch statusCode {
case 401:
return "Your session has expired. Please log in again."
case 403:
return "You don't have permission to access this."
case 404:
return "The requested item was not found."
case 429:
return "Too many requests. Please wait a moment."
case 500...599:
return "Server error. Please try again later."
default:
return "Something went wrong. Please try again."
}
case .noGenresAvailable:
return "No genres are currently available."
case .apiError(let message):
return "Error: \(message)"
case .encodingError(let message):
return "Encoding error: \(message)"
case .unknownError(let message):
return "An unexpected error occurred: \(message)"
case .jsonSerializationFailed, .jsonParsingFailed:
return "Unable to process the data. Please try again."
}
}

/// Suggested recovery action for the error
var recoveryAction: String? {
switch self {
case .tokenNotFound, .code:
return "Log In"
case .failedToGetData:
return "Retry"
case .rateLimitExceeded:
return "Wait"
case .invalidResponse(let statusCode) where statusCode == 401:
return "Log In"
default:
return nil
}
}
}
126 changes: 126 additions & 0 deletions SpotifyClone/Core/Logging/Logger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// Logger.swift
// SpotifyClone
//
// Created for centralized logging
//

import Foundation
import os.log

/// Log levels for different types of messages
enum LogLevel {
case debug
case info
case warning
case error

var osLogType: OSLogType {
switch self {
case .debug: return .debug
case .info: return .info
case .warning: return .default
case .error: return .error
}
}

var prefix: String {
switch self {
case .debug: return "🔍 DEBUG"
case .info: return "ℹ️ INFO"
case .warning: return "⚠️ WARNING"
case .error: return "❌ ERROR"
}
}
}

/// Centralized logging utility
final class Logger {
static let shared = Logger()

private let subsystem: String
private let category = "SpotifyClone"
private let log: OSLog

#if DEBUG
private let isDebugMode = true
#else
private let isDebugMode = false
#endif

private init() {
subsystem = Bundle.main.bundleIdentifier ?? "com.spotifyclone"
log = OSLog(subsystem: subsystem, category: category)
}

/// Log a message with specified level
func log(
_ message: String,
level: LogLevel = .info,
file: String = #file,
function: String = #function,
line: Int = #line
) {
let fileName = (file as NSString).lastPathComponent
let logMessage = "[\(fileName):\(line)] \(function) - \(message)"

// Skip debug logs in release builds
if level == .debug && !isDebugMode {
return
}

// Log to OSLog (visible in Console.app)
os_log("%{public}@", log: log, type: level.osLogType, logMessage)

#if DEBUG
// Print to console in debug mode
print("\(level.prefix) - \(logMessage)")
#endif
}

/// Log a debug message
func debug(
_ message: String,
file: String = #file,
function: String = #function,
line: Int = #line
) {
log(message, level: .debug, file: file, function: function, line: line)
}

/// Log an info message
func info(
_ message: String,
file: String = #file,
function: String = #function,
line: Int = #line
) {
log(message, level: .info, file: file, function: function, line: line)
}

/// Log a warning message
func warning(
_ message: String,
file: String = #file,
function: String = #function,
line: Int = #line
) {
log(message, level: .warning, file: file, function: function, line: line)
}

/// Log an error message
func error(
_ message: String,
error: Error? = nil,
file: String = #file,
function: String = #function,
line: Int = #line
) {
var fullMessage = message
if let error = error {
fullMessage += " - Error: \(error.localizedDescription)"
}
log(fullMessage, level: .error, file: file, function: function, line: line)
}
}

59 changes: 59 additions & 0 deletions SpotifyClone/Core/Networking/NetworkMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// NetworkMonitor.swift
// SpotifyClone
//
// Created for network reachability monitoring
//

import Foundation
import Network

/// Delegate protocol for network status changes
protocol NetworkMonitorDelegate: AnyObject {
func networkStatusChanged(isConnected: Bool)
}

/// Monitors network connectivity status
final class NetworkMonitor {
static let shared = NetworkMonitor()

private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "com.spotifyclone.networkmonitor")

/// Current network connection status
private(set) var isConnected: Bool = false

/// Delegate to notify about network status changes
weak var delegate: NetworkMonitorDelegate?

private init() {
startMonitoring()
}

/// Start monitoring network connectivity
func startMonitoring() {
monitor.pathUpdateHandler = { [weak self] path in
let wasConnected = self?.isConnected ?? false
self?.isConnected = path.status == .satisfied

// Notify delegate if status changed
if wasConnected != self?.isConnected {
DispatchQueue.main.async {
self?.delegate?.networkStatusChanged(isConnected: self?.isConnected ?? false)
}
}
}
monitor.start(queue: queue)
}

/// Stop monitoring network connectivity
func stopMonitoring() {
monitor.cancel()
}

/// Check if network is currently available
func checkConnectivity() -> Bool {
return isConnected
}
}

46 changes: 46 additions & 0 deletions SpotifyClone/Extensions/UIViewController+ErrorDisplay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// UIViewController+ErrorDisplay.swift
// SpotifyClone
//
// Created for error display utilities
//

import UIKit

extension UIViewController {
/// Display an error alert with user-friendly message
func showError(_ error: Error, title: String = "Error", completion: (() -> Void)? = nil) {
let message: String
let recoveryAction: String?

if let apiError = error as? ApiError {
message = apiError.userMessage
recoveryAction = apiError.recoveryAction
} else {
message = error.localizedDescription
recoveryAction = nil
}

let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)

if let recoveryAction = recoveryAction {
alert.addAction(UIAlertAction(title: recoveryAction, style: .default) { _ in
completion?()
})
} else {
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completion?()
})
}

present(alert, animated: true)
}

/// Display a success message
func showSuccess(_ message: String, title: String = "Success") {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}

Loading