Thursday, 17 October 2024

Setting Up Multiple App Targets in Xcode from a Single Codebase

 To create two different apps (like "Light" and "Regular") from the same codebase in Xcode, you can follow these steps by leveraging targets and build configurations. This allows you to create multiple versions of your app with variations such as different branding, themes, or configurations.

Step-by-Step Guide:

1. Create a New Target for the Second App

  • Open your Xcode project.
  • In the Project Navigator, select your project.
  • Go to the Targets section and right-click on the current target (the one for the original app).
  • Select Duplicate Target.
  • Rename the duplicated target (e.g., "Light" or "Regular" depending on the new app version).
  • Xcode will ask you to create a new scheme for the new target. Choose Yes.

2. Adjust the Target Settings

  • Select the new target and change the settings (like Bundle Identifier, Display Name, etc.):
    • Bundle Identifier: Ensure this is unique for the new app version (e.g., com.yourcompany.app.light vs. com.yourcompany.app.regular).
    • Display Name: Set a different name for the app version so they can be installed side by side on devices.

3. Modify the Info.plist for Each Target

  • You’ll need separate Info.plist files for each app version.
    • Right-click your project in the Project Navigator, select New File, and choose Property List.
    • Name it something like Info-light.plist or Info-regular.plist.
  • Assign each Info.plist to the corresponding target:
    • Select the target, go to the Build Settings tab.
    • Search for Info.plist File and point it to the correct Info.plist for the target (e.g., Info-light.plist for the Light version).

4. Set Up Custom Assets (Optional)

If you have different assets (like icons, splash screens, etc.):

  • Add separate asset catalogs for each version (e.g., Assets-light.xcassets and Assets-regular.xcassets).
  • In the Build Phases of each target, include the appropriate assets for that target.

5. Conditional Compilation (Optional)

If there are code differences between the two versions (e.g., features, themes), you can use preprocessor macros in the code:

  • In Build Settings, search for Other Swift Flags.
  • Add flags like -DLIGHT for the Light target and -DREGULAR for the Regular target.
  • In your Swift code, you can now use:
    #if LIGHT
    // Code specific to the Light version #else // Code specific to the Regular version #endif

6. Build Configurations (Optional)

If you need different build settings (e.g., debug vs. release or different environment variables):

  • Go to Project Settings -> Info.
  • Add new build configurations if necessary, and assign them to the correct targets.

7. Configure Schemes

  • Open the Scheme Editor (Product > Scheme > Edit Scheme).
  • Create a separate scheme for each target (if Xcode didn’t create them during duplication).
  • Select the appropriate target and configuration for each scheme (e.g., one for Light and one for Regular).

8. Test and Build

  • Now you should have two distinct apps ("Light" and "Regular") that can be built and installed side-by-side.
  • Select the appropriate scheme (e.g., "Light" or "Regular") and run the app.
By using targets and schemes, you can efficiently maintain multiple apps with shared code, but with enough flexibility to customize each version.

For more details, you can refer to Xcode documentation on managing targets and schemes 










Tuesday, 11 June 2024

Swift Testing: Simplifying Your Swift Code Testing

 Swift Testing is a package with expressive and intuitive APIs that make testing your Swift code a breeze.

  • Testing Swift code can often be a daunting task, but with Swift Testing, it becomes a breeze. 

  • This package offers expressive and intuitive APIs that make writing and running tests straightforward and efficient. 

  • Whether you're a senior developer or new to Swift, this guide will help you get started with Swift Testing.


Setting Up Your Test Target

To begin, you'll need to set up a new testing target in your Xcode project:

  1. File -> New Target -> Select "Unit Testing Bundle"
  2. In Testing System: Select "Swift Testing"

This setup allows you to integrate Swift Testing into your project seamlessly.





Attributes and Annotations

The @Test Attribute

The cornerstone of Swift Testing is the @Test attribute, which marks functions as tests. Here’s an example:

import Testing

@Test func checkValue() {

    // Test code goes here

}

Key points about @Test:

  1. Recognition: Once you add @Test, Xcode recognizes it and shows a Run icon alongside it.
  2. Functionality: Test functions are ordinary Swift functions annotated with @Test.
  3. Flexibility: They can be global functions or methods in a type, marked as async, throws, or isolated to a global actor such as @MainActor.
  4. Dependencies: To use any other class or struct, you need to import it as shown below:
@testable import className

Macros in Swift Testing

Swift Testing includes two powerful macros: #expect and #require.

#expect Macro

The #expect macro performs expectations and is essential for validating conditions in your tests.

#expect(x == y)

Key features:

  1. Ordinary Expressions: It expects ordinary expressions and language operators.
  2. Detailed Failure: It captures the source code and values of subexpressions if it fails, providing detailed results.

Examples:

#expect(1 == 2)

#expect(user.name == "vishnu")

#expect(!array.isEmpty)

#expect(array.contains(3))




#require Macro

The #require macro is similar to assertions and is used when you want to exit early if an expectation fails.

try #require(session.isValid)
session.isInvalidate()

Key features:

  1. Early Exit: Throws an error if the expression is false, allowing for early exits.
  2. Optional Chaining: Can be used with optional chaining to safely unwrap values.

Example:
let method = try #require(arr.first)
#expect(method.value == 3)


Traits and Test Suites

Traits

Traits allow you to add descriptive names to test methods, improving readability and organization.

@Test("Operators") func checkValue() {

    #expect(1 == 2)

}



Test Suites

A collection of test methods within a type is known as a test suite. Test suites help organize and group related tests.

struct OperatorTests {

    let x = 2

    let y = 2

   @Test("Operators") func checkEqual() {

        #expect(x == y)

    }

    @Test("Operators") func checkSumValue() {

        #expect(x + y == 4)

    }

}



Common Testing Patterns:

1. Test with Conditions
2. Test with Common Charatecterstics
3. Test with different arguments.


Test with Conditions

Organize your tests to check various conditions, ensuring comprehensive coverage.


Test with Common Characteristics

Use tags to group related test cases. This approach enables you to execute specific tests as needed.


We can group all the related test cases in to struct and add a common tag to it.




Test with Different Arguments

Parameterized testing allows you to test a function with multiple sets of arguments without loops. This method automatically tests all elements and provides detailed information if any check fails.




Running Tests in Xcode 16

With Xcode 16, you can run individual arguments by clicking the run button in the test navigator, providing more control over your test execution.



Swift Testing and XCTest:









Conclusion:

 - Swift Testing and XCTest provide robust tools for testing Swift code. 

 - With the expressive and intuitive APIs of Swift Testing, you can write, organize, and run tests more efficiently. 

 - By leveraging attributes like @Test, macros like #expect and #require, and structuring your tests with traits and test suites, you can ensure your code is reliable and bug-free.








Monday, 20 May 2024

Video Task Manager System design in iOS(Download, Upload, Cancel Multiple videos)

 Designing a high-level architecture for a task manager that handles downloading, deleting, uploading, and canceling tasks for multiple videos simultaneously on iOS involves several key components. This includes defining the system architecture, identifying core modules and their interactions, and ensuring concurrency management. Here's an outline of the design:

System Architecture

  1. MVC or MVVM Pattern: Use Model-View-Controller (MVC) or Model-View-ViewModel (MVVM) design pattern to separate concerns and facilitate testability and maintainability.

  2. Background Task Management: Utilize iOS background task capabilities such as URLSession, BackgroundTasks, and OperationQueue for handling concurrent operations efficiently.

  3. Persistence: CoreData or Realm can be used for local storage to keep track of video tasks' states (e.g., pending, in-progress, completed, failed).

  4. Notifications and Delegates: Use notifications and delegate patterns to communicate between components.

Core Modules

  1. Task Manager: Central manager to handle all tasks.
  2. Download Manager: Handles video download tasks.
  3. Upload Manager: Manages video upload tasks.
  4. Task Queue: Manages the order and concurrency of tasks.
  5. Persistence Layer: Stores task states and information.
  6. UI Layer: Displays task status and controls to the user.

Detailed Design

TaskManager

  • Responsibilities:

    • Coordinate between different task managers (download, upload, delete).
    • Maintain a list of current tasks and their statuses.
    • Provide methods to start, cancel, pause, and resume tasks.
  • Key Methods:

    • startTask(task: VideoTask)
    • cancelTask(task: VideoTask)
    • deleteTask(task: VideoTask)
    • uploadTask(task: VideoTask)
    • pauseTask(task: VideoTask)
    • resumeTask(task: VideoTask)

DownloadManager

  • Responsibilities:

    • Handle the downloading of videos using URLSession.
    • Manage download progress and states.
  • Key Methods:

    • startDownload(video: Video)
    • pauseDownload(video: Video)
    • resumeDownload(video: Video)
    • cancelDownload(video: Video)

UploadManager

  • Responsibilities:

    • Handle the uploading of videos.
    • Manage upload progress and states.
  • Key Methods:

    • startUpload(video: Video)
    • pauseUpload(video: Video)
    • resumeUpload(video: Video)
    • cancelUpload(video: Video)

TaskQueue

  • Responsibilities:

    • Manage the execution order of tasks.
    • Ensure tasks are executed concurrently within allowed limits.
  • Key Methods:

    • addTask(task: VideoTask)
    • removeTask(task: VideoTask)
    • prioritizeTask(task: VideoTask)
    • executeNextTask()

PersistenceLayer

  • Responsibilities:

    • Persist task information and state.
    • Retrieve task information on app launch.
  • Key Methods:

    • saveTaskState(task: VideoTask)
    • loadTaskStates() -> [VideoTask]

UILayer

  • Responsibilities:

    • Provide a user interface to display task statuses.
    • Allow user interactions to start, pause, resume, cancel, and delete tasks.
  • Components:

    • Task List View: Displays all tasks with their current status.
    • Task Detail View: Shows detailed progress and options for a selected task.

Concurrency Management

  • URLSession: For download and upload tasks, use URLSession with background configurations to handle tasks even when the app is in the background.
  • OperationQueue: Use OperationQueue for managing concurrent execution of tasks with dependencies.
  • DispatchQueue: Utilize DispatchQueue for thread-safe operations on shared resources.

Example Workflow

  1. User Initiates a Download:

    • The user selects a video to download.
    • TaskManager creates a VideoTask and adds it to TaskQueue.
    • DownloadManager starts the download using URLSession.
    • PersistenceLayer saves the task state.
  2. User Cancels a Task:

    • The user selects a task to cancel.
    • TaskManager invokes cancelTask on the respective manager (DownloadManager or UploadManager).
    • The manager cancels the operation and updates the task state.
    • TaskQueue is updated to remove the task.
  3. App Launches:

    • PersistenceLayer loads task states.
    • TaskManager reinitializes tasks based on their last known state.

Key Considerations

  • Error Handling: Ensure robust error handling and retries for failed tasks.
  • Data Consistency: Maintain consistency between the UI, task manager, and persistence layer.
  • User Feedback: Provide clear feedback to users about the status of their tasks, especially for long-running operations.

This high-level design outlines the major components and their interactions to create a robust task manager for handling multiple video operations simultaneously on iOS.



Creating a comprehensive example of the entire system is quite extensive, but I'll provide a simplified yet practical example focusing on key aspects: downloading videos using URLSession, managing tasks with OperationQueue, and providing real-time updates with a delegate pattern.

Step 1: Define Models

First, define the VideoTask model to represent tasks.

import Foundation

enum TaskState { case pending, inProgress, completed, failed, cancelled } struct VideoTask { let id: String let url: URL var state: TaskState var progress: Float }

Step 2: Task Manager

Next, create the TaskManager to manage video tasks.

import Foundation class TaskManager { static let shared = TaskManager() private var tasks: [String: VideoTask] = [:] private let queue = OperationQueue() weak var delegate: TaskManagerDelegate? private init() { queue.maxConcurrentOperationCount = 3 loadTasks() } func startDownload(task: VideoTask) { var task = task task.state = .inProgress tasks[task.id] = task PersistenceManager.shared.saveTask(task) let operation = DownloadOperation(task: task) { [weak self] updatedTask in self?.tasks[updatedTask.id] = updatedTask PersistenceManager.shared.updateTask(updatedTask) self?.delegate?.taskDidUpdate(updatedTask) } queue.addOperation(operation) } func cancelTask(taskId: String) { if var task = tasks[taskId] { task.state = .cancelled PersistenceManager.shared.updateTask(task) queue.operations .compactMap { $0 as? DownloadOperation } .first { $0.task.id == taskId }? .cancel() delegate?.taskDidUpdate(task) } } func getTask(taskId: String) -> VideoTask? { return tasks[taskId] } func getAllTasks() -> [VideoTask] { return Array(tasks.values) } private func loadTasks() { let savedTasks = PersistenceManager.shared.fetchTasks() for task in savedTasks { tasks[task.id] = task if task.state == .inProgress { startDownload(task: task) } } } }

Step 3: Download Operation

Create a custom Operation for downloading videos.

import Foundation class DownloadOperation: Operation { private var task: VideoTask private let updateHandler: (VideoTask) -> Void private var urlSession: URLSessionDataTask? init(task: VideoTask, updateHandler: @escaping (VideoTask) -> Void) { self.task = task self.updateHandler = updateHandler } override func main() { guard !isCancelled else { task.state = .cancelled updateHandler(task) return } let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) urlSession = session.dataTask(with: task.url) urlSession?.resume() task.state = .inProgress updateHandler(task) while !isCancelled && task.state == .inProgress { Thread.sleep(forTimeInterval: 1) } if isCancelled { task.state = .cancelled updateHandler(task) } } override func cancel() { super.cancel() urlSession?.cancel() } } extension DownloadOperation: URLSessionDataDelegate { func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { // Handle data received task.progress += Float(data.count) / Float(dataTask.countOfBytesExpectedToReceive) updateHandler(task) } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let error = error { print("Download failed: \(error.localizedDescription)") self.task.state = .failed } else { self.task.state = .completed } updateHandler(self.task) } }

Step 4: UI Layer

Lastly, implement a simple UI layer to display and manage tasks. Here's a basic view controller using UITableView to list the tasks.

import UIKit class TaskViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { private var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() tableView = UITableView(frame: view.bounds) tableView.delegate = self tableView.dataSource = self view.addSubview(tableView) TaskManager.shared.delegate = self } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return TaskManager.shared.getAllTasks().count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil) let task = TaskManager.shared.getAllTasks()[indexPath.row] cell.textLabel?.text = task.url.lastPathComponent cell.detailTextLabel?.text = "\(task.state) - \(Int(task.progress * 100))%" return cell } } extension TaskViewController: TaskManagerDelegate { func taskDidUpdate(_ task: VideoTask) { DispatchQueue.main.async { self.tableView.reloadData() } } }

Adding a Task

To add a download task, you could add a button that triggers the following:

let videoURL = URL(string: "https://example.com/video.mp4")! let newTask = VideoTask(id: UUID().uuidString, url: videoURL, state: .pending, progress: 0.0) TaskManager.shared.startDownload(task: newTask)


To include a persistence layer, we'll use CoreData to store the states of tasks so that they can be restored when the app is relaunched. This involves defining a CoreData model, creating the persistence manager, and updating the TaskManager to interact with the persistence layer.

Step 5: Setting Up CoreData

First, create a new CoreData model (.xcdatamodeld) file and define an entity VideoTaskEntity with the following attributes:

  • id (String)
  • url (String)
  • state (String)
  • progress (Float)

Step 2: Persistence Manager

Create a persistence manager to handle saving and loading tasks.

import CoreData import UIKit class PersistenceManager { static let shared = PersistenceManager() private init() {} lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "VideoTaskModel") container.loadPersistentStores { _, error in if let error = error { fatalError("Unresolved error \(error)") } } return container }() var context: NSManagedObjectContext { return persistentContainer.viewContext } func saveContext() { if context.hasChanges { do { try context.save() } catch { let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } } func fetchTasks() -> [VideoTask] { let request: NSFetchRequest<VideoTaskEntity> = VideoTaskEntity.fetchRequest() do { let result = try context.fetch(request) return result.map { VideoTask(id: $0.id!, url: URL(string: $0.url!)!, state: TaskState(rawValue: $0.state!)!, progress: $0.progress) } } catch { print("Failed to fetch tasks: \(error)") return [] } } func saveTask(_ task: VideoTask) { let entity = VideoTaskEntity(context: context) entity.id = task.id entity.url = task.url.absoluteString entity.state = task.state.rawValue entity.progress = task.progress saveContext() } func updateTask(_ task: VideoTask) { let request: NSFetchRequest<VideoTaskEntity> = VideoTaskEntity.fetchRequest() request.predicate = NSPredicate(format: "id == %@", task.id) do { let result = try context.fetch(request) if let entity = result.first { entity.state = task.state.rawValue entity.progress = task.progress saveContext() } } catch { print("Failed to update task: \(error)") } } func deleteTask(_ task: VideoTask) { let request: NSFetchRequest<VideoTaskEntity> = VideoTaskEntity.fetchRequest() request.predicate = NSPredicate(format: "id == %@", task.id) do { let result = try context.fetch(request) if let entity = result.first { context.delete(entity) saveContext() } } catch { print("Failed to delete task: \(error)") } } }

Conclusion

This example demonstrates the basics of managing video tasks in an iOS app. The TaskManager handles the coordination, DownloadOperation manages the downloading process, and the view controller displays task status updates in real-time. To expand this further, you could add similar structures for uploading and deleting videos, enhance error handling, and improve UI feedback. With the addition of the PersistenceManager, the task manager can now save and load tasks, maintaining state across app launches. This example integrates CoreData to persist task information, ensuring continuity of tasks and progress tracking even after the app is closed and reopened.

Setting Up Multiple App Targets in Xcode from a Single Codebase

 To create two different apps (like "Light" and "Regular") from the same codebase in Xcode, you can follow these steps b...