Chuyển tới nội dung chính

Advanced Formatting Guidelines

We will go through advanced formatting situations, architectural patterns, and complex Swift/Objective-C features for the SideStore project.

Advanced Swift Patterns

Protocol-Oriented Programming

Protocol Definitions

  • Keep protocols focused and cohesive
  • Use associated types for generic protocols
  • Provide default implementations in extensions when appropriate
// ✅ Good
protocol AppInstalling {
associatedtype AppType: App

func install(_ app: AppType) async throws
func uninstall(_ app: AppType) async throws
}

extension AppInstalling {
func validateApp(_ app: AppType) -> Bool {
return !app.identifier.isEmpty && app.version.isValid
}
}

// ❌ Bad
protocol AppManager {
func install(_ app: Any) -> Bool
func uninstall(_ app: Any) -> Bool
func update(_ app: Any) -> Bool
func backup(_ app: Any) -> Bool
func restore(_ app: Any) -> Bool
// Too many responsibilities
}

Protocol Composition

  • Use protocol composition for complex requirements
  • Keep individual protocols small and focused
// ✅ Good
protocol Downloadable {
var downloadURL: URL { get }
func download() async throws -> Data
}

protocol Installable {
var installationRequirements: InstallationRequirements { get }
func install() async throws
}

typealias DeployableApp = App & Downloadable & Installable

class AppDeployer {
func deploy<T: DeployableApp>(_ app: T) async throws {
let data = try await app.download()
try await app.install()
}
}

Generics and Type Constraints

Generic Type Definitions

  • Use meaningful constraint names
  • Prefer protocol constraints over class constraints
  • Use where clauses for complex constraints
// ✅ Good
struct Repository<Entity: Identifiable & Codable> {
private var entities: [Entity.ID: Entity] = [:]

func save<T>(_ entity: T) where T == Entity {
entities[entity.id] = entity
}

func find<ID>(byId id: ID) -> Entity? where ID == Entity.ID {
return entities[id]
}
}

// ❌ Bad
struct Repository<T> {
private var items: [String: T] = [:]
// No type safety
}

Advanced Generic Constraints

  • Use conditional conformance appropriately
  • Leverage phantom types when needed
// ✅ Good
extension Array: AppCollection where Element: App {
var installedApps: [Element] {
return filter { $0.isInstalled }
}

func sortedByInstallDate() -> [Element] {
return sorted { $0.installDate < $1.installDate }
}
}

// Phantom types for type safety
struct AppState<Status> {
let app: App
}

enum Downloaded {}
enum Installed {}

typealias DownloadedApp = AppState<Downloaded>
typealias InstalledApp = AppState<Installed>

Async/Await Patterns

Async Function Design

  • Use async/await consistently
  • Structure concurrent operations clearly
  • Handle cancellation appropriately
// ✅ Good
actor AppInstallationManager {
private var activeInstallations: [String: Task<Void, Error>] = [:]

func installApp(_ app: App) async throws {
// Prevent duplicate installations
if activeInstallations[app.identifier] != nil {
throw InstallationError.alreadyInstalling
}

let task = Task {
try await performInstallation(app)
}

activeInstallations[app.identifier] = task

defer {
activeInstallations.removeValue(forKey: app.identifier)
}

try await task.value
}

private func performInstallation(_ app: App) async throws {
// Check for cancellation at key points
try Task.checkCancellation()

let data = try await downloadApp(app)

try Task.checkCancellation()

try await installData(data, for: app)
}
}

// ❌ Bad
func installApp(_ app: App, completion: @escaping (Error?) -> Void) {
DispatchQueue.global().async {
// Mixing old completion handler style with new async code
let result = await self.downloadApp(app)
DispatchQueue.main.async {
completion(nil)
}
}
}

Structured Concurrency

  • Use task groups for related concurrent operations
  • Prefer structured concurrency over unstructured tasks
// ✅ Good
func installMultipleApps(_ apps: [App]) async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
for app in apps {
group.addTask {
try await self.installApp(app)
}
}

// Wait for all installations to complete
try await group.waitForAll()
}
}

// For independent results
func downloadMultipleApps(_ apps: [App]) async throws -> [App: Data] {
try await withThrowingTaskGroup(of: (App, Data).self) { group in
for app in apps {
group.addTask {
let data = try await self.downloadApp(app)
return (app, data)
}
}

var results: [App: Data] = [:]
for try await (app, data) in group {
results[app] = data
}
return results
}
}

Result Builders and DSLs

Custom Result Builders

  • Create focused, single-purpose result builders
  • Provide clear syntax for domain-specific operations
// ✅ Good
@resultBuilder
struct AppConfigurationBuilder {
static func buildBlock(_ components: AppConfigurationComponent...) -> AppConfiguration {
return AppConfiguration(components: components)
}

static func buildOptional(_ component: AppConfigurationComponent?) -> AppConfigurationComponent? {
return component
}

static func buildEither(first component: AppConfigurationComponent) -> AppConfigurationComponent {
return component
}

static func buildEither(second component: AppConfigurationComponent) -> AppConfigurationComponent {
return component
}
}

// Usage
func configureApp(@AppConfigurationBuilder builder: () -> AppConfiguration) -> App {
let config = builder()
return App(configuration: config)
}

let app = configureApp {
AppName("SideStore")
AppVersion("1.0.0")
if debugMode {
DebugSettings()
}
Permissions {
NetworkAccess()
FileSystemAccess()
}
}

Advanced Objective-C Patterns

Category Organization

  • Use categories to organize related functionality
  • Keep category names descriptive and specific
// ✅ Good
@interface NSString (SSValidation)
- (BOOL)ss_isValidAppIdentifier;
- (BOOL)ss_isValidVersion;
@end

@interface UIViewController (SSAppInstallation)
- (void)ss_presentAppInstallationViewController:(SSApp *)app;
- (void)ss_showInstallationProgress:(SSInstallationProgress *)progress;
@end

// ❌ Bad
@interface NSString (Helpers)
- (BOOL)isValid; // Too generic
- (NSString *)cleanup; // Unclear purpose
@end

Advanced Memory Management

  • Use proper patterns for delegate relationships
  • Handle complex object graphs correctly
// ✅ Good
@interface SSAppInstaller : NSObject
@property (nonatomic, weak) id<SSAppInstallerDelegate> delegate;
@property (nonatomic, strong) NSOperationQueue *installationQueue;
@end

@implementation SSAppInstaller

- (instancetype)init {
self = [super init];
if (self) {
_installationQueue = [[NSOperationQueue alloc] init];
_installationQueue.maxConcurrentOperationCount = 3;
_installationQueue.name = @"com.sidestore.installation";
}
return self;
}

- (void)installApp:(SSApp *)app completion:(void (^)(NSError *))completion {
__weak typeof(self) weakSelf = self;
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;

NSError *error = nil;
[strongSelf performInstallationForApp:app error:&error];

dispatch_async(dispatch_get_main_queue(), ^{
completion(error);
});
}];

[self.installationQueue addOperation:operation];
}

@end

Block Usage Patterns

  • Use proper block patterns for asynchronous operations
  • Handle memory management in blocks correctly
// ✅ Good
typedef void (^SSInstallationProgressBlock)(float progress);
typedef void (^SSInstallationCompletionBlock)(SSApp *app, NSError *error);

@interface SSAppDownloader : NSObject
- (NSURLSessionTask *)downloadApp:(SSApp *)app
progress:(SSInstallationProgressBlock)progressBlock
completion:(SSInstallationCompletionBlock)completion;
@end

@implementation SSAppDownloader

- (NSURLSessionTask *)downloadApp:(SSApp *)app
progress:(SSInstallationProgressBlock)progressBlock
completion:(SSInstallationCompletionBlock)completion {

NSURLRequest *request = [NSURLRequest requestWithURL:app.downloadURL];

__weak typeof(self) weakSelf = self;
NSURLSessionDownloadTask *task = [[NSURLSession sharedSession]
downloadTaskWithRequest:request
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;

if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, error);
});
return;
}

// Process downloaded file
[strongSelf processDownloadedFile:location
forApp:app
completion:completion];
}];

[task resume];
return task;
}

@end

Architectural Patterns

MVVM Implementation

  • Separate concerns clearly between Model, View, and ViewModel
  • Use proper data binding patterns
// ✅ Good
// Model
struct App {
let identifier: String
let name: String
let version: String
let isInstalled: Bool
}

// ViewModel
@MainActor
class AppListViewModel: ObservableObject {
@Published var apps: [App] = []
@Published var isLoading = false
@Published var errorMessage: String?

private let appService: AppService

init(appService: AppService) {
self.appService = appService
}

func loadApps() async {
isLoading = true
errorMessage = nil

do {
apps = try await appService.fetchAvailableApps()
} catch {
errorMessage = error.localizedDescription
}

isLoading = false
}

func installApp(_ app: App) async {
do {
try await appService.installApp(app)
await loadApps() // Refresh the list
} catch {
errorMessage = "Failed to install \(app.name): \(error.localizedDescription)"
}
}
}

// View
struct AppListView: View {
@StateObject private var viewModel: AppListViewModel

init(appService: AppService) {
_viewModel = StateObject(wrappedValue: AppListViewModel(appService: appService))
}

var body: some View {
NavigationView {
List(viewModel.apps, id: \.identifier) { app in
AppRowView(app: app) {
Task {
await viewModel.installApp(app)
}
}
}
.navigationTitle("Available Apps")
.overlay {
if viewModel.isLoading {
ProgressView()
}
}
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
}
.task {
await viewModel.loadApps()
}
}
}

Dependency Injection

  • Use dependency injection for testability and flexibility
  • Consider using a DI container for complex applications
// ✅ Good
protocol AppService {
func fetchAvailableApps() async throws -> [App]
func installApp(_ app: App) async throws
}

protocol NetworkService {
func downloadData(from url: URL) async throws -> Data
}

class DefaultAppService: AppService {
private let networkService: NetworkService
private let deviceService: DeviceService

init(networkService: NetworkService, deviceService: DeviceService) {
self.networkService = networkService
self.deviceService = deviceService
}

func fetchAvailableApps() async throws -> [App] {
let data = try await networkService.downloadData(from: appsURL)
return try JSONDecoder().decode([App].self, from: data)
}

func installApp(_ app: App) async throws {
guard deviceService.hasSpace(for: app) else {
throw InstallationError.insufficientStorage
}

let appData = try await networkService.downloadData(from: app.downloadURL)
try await deviceService.installApp(data: appData)
}
}

// DI Container
class ServiceContainer {
static let shared = ServiceContainer()

private init() {}

lazy var networkService: NetworkService = DefaultNetworkService()
lazy var deviceService: DeviceService = DefaultDeviceService()
lazy var appService: AppService = DefaultAppService(
networkService: networkService,
deviceService: deviceService
)
}

Error Handling Architecture

  • Create comprehensive error handling strategies
  • Use typed errors for better error handling
// ✅ Good
enum SideStoreError: Error {
case network(NetworkError)
case installation(InstallationError)
case device(DeviceError)
case validation(ValidationError)
}

enum NetworkError: Error {
case noConnection
case timeout
case serverError(Int)
case invalidResponse
}

enum InstallationError: Error {
case insufficientStorage
case incompatibleDevice
case corruptedFile
case alreadyInstalled
}

extension SideStoreError: LocalizedError {
var errorDescription: String? {
switch self {
case .network(let networkError):
return "Network error: \(networkError.localizedDescription)"
case .installation(let installError):
return "Installation error: \(installError.localizedDescription)"
case .device(let deviceError):
return "Device error: \(deviceError.localizedDescription)"
case .validation(let validationError):
return "Validation error: \(validationError.localizedDescription)"
}
}
}

// Error handling in services
class AppService {
func installApp(_ app: App) async throws {
do {
try validateApp(app)
} catch {
throw SideStoreError.validation(error as! ValidationError)
}

do {
try await performInstallation(app)
} catch let error as NetworkError {
throw SideStoreError.network(error)
} catch let error as InstallationError {
throw SideStoreError.installation(error)
}
}
}

Performance Considerations

Memory Optimization

  • Use lazy loading for expensive resources
  • Implement proper caching strategies
// ✅ Good
class AppImageCache {
private let cache = NSCache<NSString, UIImage>()
private let downloadQueue = DispatchQueue(label: "image-download", qos: .utility)

init() {
cache.countLimit = 50
cache.totalCostLimit = 50 * 1024 * 1024 // 50MB
}

func image(for app: App) async -> UIImage? {
let key = app.identifier as NSString

// Check cache first
if let cachedImage = cache.object(forKey: key) {
return cachedImage
}

// Download if not cached
return await withCheckedContinuation { continuation in
downloadQueue.async { [weak self] in
guard let self = self else {
continuation.resume(returning: nil)
return
}

do {
let data = try Data(contentsOf: app.iconURL)
let image = UIImage(data: data)

if let image = image {
self.cache.setObject(image, forKey: key)
}

continuation.resume(returning: image)
} catch {
continuation.resume(returning: nil)
}
}
}
}
}

Threading Best Practices

  • Use appropriate queue priorities
  • Minimize context switching
// ✅ Good
actor BackgroundProcessor {
private let processingQueue = DispatchQueue(
label: "background-processing",
qos: .utility,
attributes: .concurrent
)

func processLargeDataSet(_ data: [LargeDataItem]) async -> [ProcessedItem] {
return await withTaskGroup(of: ProcessedItem?.self, returning: [ProcessedItem].self) { group in
let chunkSize = max(data.count / ProcessInfo.processInfo.activeProcessorCount, 1)

for chunk in data.chunked(into: chunkSize) {
group.addTask {
return await self.processChunk(chunk)
}
}

var results: [ProcessedItem] = []
for await result in group {
if let processed = result {
results.append(processed)
}
}

return results
}
}

private func processChunk(_ chunk: [LargeDataItem]) async -> ProcessedItem? {
// CPU-intensive processing
return await withCheckedContinuation { continuation in
processingQueue.async {
let result = chunk.map { self.expensiveOperation($0) }
continuation.resume(returning: ProcessedItem(results: result))
}
}
}
}

Testing Patterns

Protocol-Based Testing

  • Use protocols for dependency injection in tests
  • Create focused test doubles
// ✅ Good
class MockAppService: AppService {
var shouldFailInstallation = false
var installedApps: [App] = []

func fetchAvailableApps() async throws -> [App] {
return [
App(identifier: "test.app1", name: "Test App 1", version: "1.0.0"),
App(identifier: "test.app2", name: "Test App 2", version: "2.0.0")
]
}

func installApp(_ app: App) async throws {
if shouldFailInstallation {
throw InstallationError.insufficientStorage
}
installedApps.append(app)
}
}

class AppListViewModelTests: XCTestCase {
private var mockAppService: MockAppService!
private var viewModel: AppListViewModel!

override func setUp() {
super.setUp()
mockAppService = MockAppService()
viewModel = AppListViewModel(appService: mockAppService)
}

@MainActor
func testLoadAppsSuccess() async {
await viewModel.loadApps()

XCTAssertEqual(viewModel.apps.count, 2)
XCTAssertFalse(viewModel.isLoading)
XCTAssertNil(viewModel.errorMessage)
}

@MainActor
func testInstallAppFailure() async {
mockAppService.shouldFailInstallation = true

let testApp = App(identifier: "test", name: "Test", version: "1.0")
await viewModel.installApp(testApp)

XCTAssertNotNil(viewModel.errorMessage)
XCTAssertTrue(viewModel.errorMessage!.contains("insufficient storage"))
}
}

Documentation Standards

Complex API Documentation

  • Document complex behaviors and edge cases
  • Provide usage examples for non-trivial APIs
/**
* A thread-safe manager for handling app installations with automatic retry logic.
*
* This class manages the installation process for iOS applications, handling
* network downloads, signature verification, and device communication.
* It provides automatic retry functionality for transient failures and
* comprehensive error reporting.
*
* ## Usage Example
* ```swift
* let installer = AppInstallationManager()
*
* do {
* let result = try await installer.installApp(
* from: app.sourceURL,
* identifier: app.bundleID,
* maxRetries: 3
* )
* print("Installation completed: \(result.installedPath)")
* } catch InstallationError.insufficientStorage {
* // Handle storage error
* } catch {
* // Handle other errors
* }
* ```
*
* ## Thread Safety
* This class is thread-safe and can be called from any queue. All completion
* handlers are called on the main queue unless otherwise specified.
*
* ## Error Handling
* Installation failures are categorized into recoverable and non-recoverable
* errors. Recoverable errors (network timeouts, temporary device issues) will
* be automatically retried up to the specified limit. Non-recoverable errors
* (invalid signatures, incompatible devices) will fail immediately.
*/
@MainActor
class AppInstallationManager {
// Implementation
}

Remember: These advanced patterns should be used judiciously. Always prioritize code clarity and maintainability over clever implementations. When in doubt, choose the simpler approach that still meets the requirements.