Categoría: GitHub

  • Dip your toes in middle of TCA ocean

    Dip your toes in middle of TCA ocean

    TCA, or The Composable Architecture, is a framework for iOS development that provides a structured and scalable approach to building robust, maintainable applications. Created by Brandon Williams and Stephen Celis, TCA leverages functional programming principles and Swift’s powerful type system to offer a modern solution for iOS app architecture.

    In this post, we’ll explore how to migrate our Rick and Morty iOS app to TC

    The architecture

    TCA consists of five main components:

    1. State: A single type that represents the entire state of an app or feature.
    2. Actions: An enumeration of all possible events that can occur in the app.
    3. Environment: A type that wraps all dependencies of the app or feature.
    4. Reducer: A function that transforms the current state to the next state based on a given action.
    5. Store: The runtime that powers the feature and manages the state.

    TCA offers several advantages for iOS development:

    • Unidirectional data flow: This makes it easy to understand how changes in state occur, simplifying debugging and preventing unexpected side effects.
    • Improved testability: TCA encourages writing features that are testable by default.
    • Modularity: It allows for composing separate features, enabling developers to plan, build, and test each part of the app independently.
    • Scalability: TCA is particularly useful for complex applications with many states and interactions.

    Configure XCode project

    To get started with this architecture, integrate the ComposableArchitecture library from its GitHub repository.

    Downloading might take some time.

    Character feature

    The component that we have to change implentation is basically the ViewModel component. In this case will be renamed as CharacterFeature.

    import ComposableArchitecture
    
    @Reducer
    struct CharatersFeature {
        @ObservableState
        struct State: Equatable {
            var characters: [Character] = []
            var isLoading: Bool = false
        }
    
        enum Action {
            case fetchCharacters
            case fetchCharactersSuccess([Character])
        }
    
        var body: some ReducerOf<Self> {
            Reduce { state, action in
                switch action {
                case .fetchCharacters:
                    state.isLoading = true
                    state.characters = []
                    return .run { send in
                        let result = await currentApp.dataManager.fetchCharacters(CharacterService())
                        switch result {
                        case .success(let characters):
                            //state.characters = characters
                            await send(.fetchCharactersSuccess(characters))
                        case .failure(let error):
                            print(error)
                        }
                    }
                case .fetchCharactersSuccess(let characters):
                    state.isLoading = false
                    state.characters = characters
                    return .none
                }
            }
        }
    }

    This code defines a feature using the Composable Architecture (TCA) framework in Swift. Let’s break down what this code does:

    1. Import and Structure:
      • It imports the ComposableArchitecture framework.
      • It defines a CharatersFeature struct with the @Reducer attribute, indicating it’s a reducer in the TCA pattern.
    2. State:
      • The State struct is marked with @ObservableState, making it observable for SwiftUI views.
      • It contains two properties:
        • characters: An array of Character objects.
        • isLoading: A boolean to track if data is being loaded.
    3. Actions:
      • The Action enum defines two possible actions:
        • fetchCharacters: Triggers the character fetching process.
        • fetchCharactersSuccess: Handles successful character fetching.
    4. Reducer:
      • The body property defines the reducer logic.
      • It uses a Reduce closure to handle state changes based on actions.
    5. Action Handling:
      • For .fetchCharacters:
        • Sets isLoading to true and clears the characters array.
        • Runs an asynchronous operation to fetch characters.
        • On success, it dispatches a .fetchCharactersSuccess action.
        • On failure, it prints the error.
      • For .fetchCharactersSuccess:
        • Sets isLoading to false.
        • Updates the characters array with the fetched data.
    6. Asynchronous Operations:
      • It uses .run for handling asynchronous operations within the reducer.
      • The character fetching is done using currentApp.dataManager.fetchCharacters(CharacterService()).

    This code essentially sets up a state management system for fetching and storing character data, with loading state handling. It’s designed to work with SwiftUI and the Composable Architecture, providing a structured way to manage application state and side effects.

    View

    The view is almost the same as before:

    struct CharacterView: View {
        let store: StoreOf<CharatersFeature>
        
        var body: some View {
            NavigationView {
                ZStack {
                    if store.isLoading {
                        ProgressView()
                    }
                ScrollView {
                        ForEach(store.characters) { character in
                            NavigationLink {
                                DetailView(character: character)
                            } label: {
                                HStack {
                                    characterImageView(character.imageUrl)
                                    Text("\(character.name)")
                                    Spacer()
                                }
                            }
                        }
                    }
                }
            }
            .padding()
            .onAppear {
                store.send(.fetchCharacters)
            }
        }

    The store holds observable items used by the view to present either the progression view or the character list. When the view appears, it triggers the .fetchCharacters action, prompting the reducer to fetch the character list.

    Unit test

    Unit testing with TCA differs significantly from my expectations:

        @Test func example() async throws {
            // Write your test here and use APIs like `#expect(...)` to check expected conditions.
            let store = await TestStore(initialState: CharatersFeature.State()) {
                CharatersFeature()
            }
            
            await store.send(.fetchCharacters) {
              $0.isLoading = true
            }
            
            await store.receive(\.fetchCharactersSuccess, timeout: .seconds(1)) {
              $0.isLoading = false
                $0.characters = expCharacters
            }
            
            await store.finish()
        }

    In TCA, testing often focuses on asserting the state transitions and effects of the reducer. Instead of traditional XCTest assertions like XCTAssertEqual, TCA provides its own mechanism for testing reducers using TestStore, which is a utility designed to test state changes, actions, and effects in a deterministic way.

    Conclusions

    This is a very minimalistic example just to get in touch with this architecture. With more complex applications, I meain with some flows and many screens reducer would become a huge chunk of code, so god approach would be implement this pattern per app flow.You can find source code used for writing this post in following repository.

  • Force Update iOS Apps When Backend Require It

    Force Update iOS Apps When Backend Require It

    In the mobile native (iOS/Android) app production ecosystem, multiple frontend versions often coexist and interact with the same backend. Frontend updates are typically adopted gradually; while it’s possible to enforce an update, this approach is generally considered disruptive and is used only in exceptional circumstances.

    This post aims to demonstrate a method for controlling request responses based on the frontend version specified in the request. The backend implementation will use Vapor, and the frontend will be an iOS app. Links to the GitHub repositories hosting the source code are provided at the end of this post.

    Keep request under control

    Including the client’s frontend version in backend requests is crucial for several reasons:

    1. Version-Specific Responses: The backend can tailor its responses to ensure compatibility and optimal functionality for each frontend version.

    2. API Versioning: It helps the backend serve the appropriate API version, supporting backward compatibility while enabling updates and improvements.

    3. Feature Support: Frontend versions may differ in their feature sets. The backend can adjust responses to include or exclude functionality based on the client’s capabilities.

    4. Performance Optimization: Backend processing and payloads can be optimized for the specific requirements of each frontend version, improving system performance.

    5. Error Handling: Knowing the frontend version allows for more relevant error messages and effective resolution of version-specific issues.

    6. Security Enhancements: Version-specific security protocols or restrictions can be implemented, boosting system security.

    By including the frontend version in client requests, developers can build robust, efficient, and maintainable systems that adapt to evolving requirements while maintaining compatibility with legacy clients.

    Vapor backend

    Vapor is an open-source web framework written in Swift, designed for building server-side applications. It offers a powerful and asynchronous platform for developing web applications, APIs, and backend services, all using Swift as the server-side language.

    This post is not a «build your first server-side app» tutorial. However, don’t worry—at the end of the post, I’ll share the tutorials I followed to gain a deeper understanding of this technology.

    To get started, we’ll create a new Vapor project. For this project, we won’t be working with databases, so you can safely answer «No» to all related prompts during the setup process.

    We will create an endpoint specifically for checking the minimum required versions compatible with the backend and determining whether a forced update is necessary. The endpoint will use the GET method, and the path will be /minversion.

    struct MainController: RouteCollection {
        func boot(routes: any Vapor.RoutesBuilder) throws {
            let minversionRoutesGrouped = routes.grouped("minversion")
            minversionRoutesGrouped.get(use: minVersion)

    And the associated function to perform this will be as follows.

        @Sendable
        func minVersion(req: Request) async throws -> VersionResponse {
            
            let currentVersion = "2.0.0"
            let minimumVersion = "1.5.0"
            let forceUpdate = true // o false dependiendo de la lógica de negocio
    
            // Devuelve la respuesta como JSON
            return VersionResponse(
                currentVersion: currentVersion,
                minimumVersion: minimumVersion,
                forceUpdate: forceUpdate
            )
        }

    Structure: We need to include the following information:

    1. Minimal Version: The minimum version of the application that the backend can handle.
    2. Current Version: The current version supported by the backend.
    3. Force Update: Whether a forced update is required.

    Instructions:
    Run the project, and check the log console to confirm that the server is ready.

    Use the curl command to call the specified endpoint.

    The API returns a JSON object containing the minimum and current versions, as well as a force-update flag.

    To simplify the backend’s ability to check frontend versions, we will add an additional attribute to each endpoint. This attribute will provide information about the frontend version. To illustrate this approach, we will create a sample POST endpoint that includes this feature.

    struct MainController: RouteCollection {
        func boot(routes: any Vapor.RoutesBuilder) throws {
            let minversionRoutesGrouped = routes.grouped("minversion")
            minversionRoutesGrouped.get(use: minVersion)
            
            let sampleRoutesGrouped = routes.grouped("sample")
            sampleRoutesGrouped.post(use: sample)
        }
    And its functionality is encapsulated in the following endpoint.
        @Sendable
        func sample(req: Request) async throws -> SampleResponse {
            let payload = try req.content.decode(SampleRequestData.self)
            let isLatestVersion =  await payload.version == VersionResponse.current().currentVersion
            let isForceUpdate = await VersionResponse.current().forceUpdate
            guard  isLatestVersion ||
                   !isForceUpdate else {
                throw Abort(.upgradeRequired) // Force update flag set
            }
    
            guard await isVersion(payload.version, inRange: (VersionResponse.current().minimumVersion, VersionResponse.current().currentVersion)) else {
                throw Abort(.upgradeRequired) // Version out of valid range
            }
            
            return SampleResponse(data: "Some data...")
        }

    The first thing the function does is validate that the version adheres to the X.Y.Z syntax.

        struct SampleRequestData: Content {
            let version: String
            
            mutating func afterDecode() throws {
                guard isValidVersionString(version) else {
                    throw Abort(.badRequest, reason: "Wrong version format")
                }
            }
            
            private func isValidVersionString(_ version: String) -> Bool {
                let versionRegex = #"^\d+\.\d+\.\d+$"#
                let predicate = NSPredicate(format: "SELF MATCHES %@", versionRegex)
                return predicate.evaluate(with: version)
            }
        }

    Later on, the process involves validating the version of a client application against a server-defined versioning policy. If the version check is successful, a simple JSON response with sample data is returned.

    Returning to the command line, we execute the sample using valid version values:

    We received a valid sample endpoint response, along with current, minimum version and wether forced update is being required.

    However, when we set a version lower than the required minimum, we encountered an error requesting an upgrade.

    While the implementation is theoretically complete, handling version updates on the front end is not difficult, but any mistakes in production can have dramatic consequences. For this reason, it is mandatory to implement a comprehensive set of unit tests to cover the implementation and ensure that when versions are updated, consistency is maintained.

    From now on, every new endpoint implemented by the server must perform this frontend version check, along with other checks, before proceeding. Additionally, the code must be data race-safe.

    At the time of writing this post, I encountered several issues while compiling the required libraries for Vapor. As a result, I had to revert these settings to continue writing this post. Apologies for the back-and-forth.

    IOS frontend

    The iOS app frontend we are developing will primarily interact with a sample POST API. This API accepts JSON data, which includes the current frontend version.

    • If the frontend version is within the supported range, the backend responds with the expected output for the sample POST API, along with information about the versions supported by the backend.
    • If the frontend version falls below the minimum supported version and a forced update is required, the backend will return an «update required» error response.

    To ensure compliance with Swift 6, make sure that Strict Concurrency Checking is set to Complete.

    … and Swift language version to Swift 6.

    Before we start coding, let’s set the app version. The version can be defined in many places, which can be quite confusing. Our goal is to set it in a single, consistent location.

    This is the unique place where you need to set the version number. For the rest of the target, we will inherit that value. When we set the version in the target, a default value (1.0) is already set, and it is completely isolated from the project. We are going to override this by setting MARKETING_VERSION to $(MARKETING_VERSION), so the value will be taken from the project’s MARKETING_VERSION.

    Once set, you will see that the value is adopted. One ring to rule them all.

    The application is not very complicated, and if you’re looking for implementation details, you can find the GitHub repository at the end of the post. Essentially, what it does is perform a sample request as soon as the view is shown.

    Make sure the Vapor server is running before launching the app on a simulator (not a real device, as you’re targeting localhost). You should see something like this:

    Simulator Screenshot - iPhone 16 Pro Max - 2024-12-05 at 12.05.23

    The current app version is 1.7.0, while the minimum supported backend version is 1.5.0, and the backend is currently at version 2.0.0. No forced update is required. Therefore, the UI displays a message informing users that they are within the supported version range, but it also indicates that an update to the latest version is available.

    Once we configure the Vapor backend to enforce a forced update:

            let versionResponse = VersionResponse(currentVersion: "2.0.0",
                                                  minimumVersion: "1.5.0",
                                                  forceUpdate: true)
            

    Re-run vapor server:

    Screenshot

    Re-run the app:

    Simulator Screenshot - iPhone 16 Pro Max - 2024-12-05 at 12.16.03

    The front-end needs to be updated, and users are required to update the app. Please provide a link to the Apple Store page for downloading the update.

    Conclusions

    In this post, I have demonstrated a method for versioning API communication between the backend and frontend. I acknowledge that my explanation of the implementation is brief, but you can find the backend and frontend repositories linked here.

    References

  • Firebase Authentication in Your iOS App

    Firebase Authentication in Your iOS App

    User authentication is often a cumbersome task that becomes essential as an application grows more robust. Firebase Authentication simplifies this process by handling authentication for you. It supports several authentication methods, but for the purpose of this post, we will focus on email and password authentication.

    Firebase console

    The first step is to create a dashboard to manage your app’s Firebase capabilities. To do this, open the Firebase console and create a new project. Use the name of your app as the project name for better organization and clarity. For the purposes of this post, I will not enable analytics. With these steps completed, your project should be ready to use.

    Later on, we will revisit the setup-specific issues for your iOS app.

    Creating iOS App scaffolder

    Let’s set Firebase aside for a moment and create a blank application. The only important detail in this process is to pay attention to the Bundle Identifier, as we’ll need it in the upcoming steps.

    Connect Firebase to your app

    Return to Firebase to add it to your app, and select the iOS option

    This is the moment to enter your app’s iOS Bundle Identifier.

    The next step is to download the configuration file and incorporate it into your project.

    The final step is incorporating the Firebase SDK using Swift Package Manager (SPM). To do this, select your project, go to Package Dependencies, and click Add Package Dependency.

    Enter the GitHub repository URL provided in Step 3 of the Firebase configuration into the search input box.

    Don’t forget to add FirebaseAuth to your target application.

    Continue through the Firebase configuration steps, and it will eventually provide the code you need to connect your app to Firebase.

    Incorporate this code into your iOS app project, then build and run the project to ensure everything is working properly.

    Implement authentication

    Get back to Firebase console to set up Authentication

    As you can see, there are many authentication methods, but for the purpose of this post, we will only work with the email and password method.

    As you can see, there are many authentication methods, but for the purpose of this post, we will only focus on the email and password method.

    For this post, I have implemented basic views for user registration, login, logout, and password recovery. All authentication functionality has been wrapped into a manager called AuthenticatorManager. The code is very basic but fully functional and compliant with Swift 6.0.

    Don’t worry if I go too fast; the link to the code repository is at the end of this post. You’ll see that the code is very easy to read. Simply run the project, register an account, and log in.

    You can find the project code at following repository.

    Conclusions

    Firebase provides a fast and efficient way to handle user authentication as your app scales.

  • Streamlining Your Xcode Projects with GitHub Actions

    Streamlining Your Xcode Projects with GitHub Actions

    Having good practices is one of the key points for successfully steering your project to completion, especially when working as part of a team. In this post, I will explain how to implement these tasks in a CI/CD environment such as GitHub.

    First, we will set up essential tasks like unit testing and linting locally, and then apply these tasks as requirements for integration approvals.

    Executing unit test through command line console

    At this point, we assume that the unit test target is properly configured in your project. This section is important because we will need to use the command in the future.»

    xcodebuild is a command-line tool provided by Apple as part of Xcode, it allows developers to build and manage Xcode projects and workspaces from the terminal, providing flexibility for automating tasks, running continuous integration (CI) pipelines, and scripting.

    Simply execute the command to validate that everything is working correctly.

    Linting your code

    Linting your code not only improves quality, prevents errors, and increases efficiency, but it also facilitates team collaboration by reducing time spent on code reviews. Additionally, linting tools can be integrated into CI pipelines to ensure that checks are part of the build and deployment process.

    The tool we will use for linting is SwiftLint. Here, you will find information on how to install it on your system. Once it is properly installed on your system:

    Go to project root folder and create file .swiftlint.yml, this is default configuration, you can check following link to know more about the defined rules.

    disabled_rules:
    - trailing_whitespace
    opt_in_rules:
    - empty_count
    - empty_string
    excluded:
    - Carthage
    - Pods
    - SwiftLint/Common/3rdPartyLib
    line_length:
        warning: 150
        error: 200
        ignores_function_declarations: true
        ignores_comments: true
        ignores_urls: true
    function_body_length:
        warning: 300
        error: 500
    function_parameter_count:
        warning: 6
        error: 8
    type_body_length:
        warning: 300
        error: 500
    file_length:
        warning: 1000
        error: 1500
        ignore_comment_only_lines: true
    cyclomatic_complexity:
        warning: 15
        error: 25
    reporter: "xcode"
    

    Now, let’s integrate this in Xcode. Select your target, go to Build Phases, click the plus (+) button, and choose ‘New Run Script Phase’.

    Rename the script name to ‘swiftlint’ for readability, and make sure to uncheck ‘Based on…’ and ‘Show environment…’.

    Paste the following script.

    echo ">>>>>>>>>>>>>>>>>>>>>>>> SWIFTLINT (BEGIN) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
    if [[ "$(uname -m)" == arm64 ]]; then
        export PATH="/opt/homebrew/bin:$PATH"
    fi
    
    if which swiftlint > /dev/null; then
      swiftlint
    else
      echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
    fi
    echo "<<<<<<<<<<<<<<<<<<<<<<<<< SWIFTLINT (END) <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"

    Select the target and build (Cmd+B). If you review the build log, you will see a new warnings triggered.

    GitHub actions

    GitHub Actions is a powerful CI/CD  platform integrated within GitHub, allowing developers to automate, customize, and streamline their software workflows directly from their repositories. It uses YAML configuration files to define tasks or workflows that run in response to events, such as pushing code, opening pull requests, or setting schedules. With its flexibility, developers can automate building, testing or deploying applications.

    Workflow for executing unit test

    At this point, we assume that we are in the root folder of a repository cloned from GitHub. Navigate to (or create) the following folder: ./github/workflows. In that folder, we will place our first GitHub Action for executing the unit tests. In my case, it was a file called UTest.yml with the following content:

    name: utests-workflow
    
    on:
      pull_request:
        branches: [main, develop]
    jobs:
      utests-job:
        runs-on: macos-latest
    
        steps:
          - name: Check out the repository
            uses: actions/checkout@v4
    
          - name: Set to XCode 16.0
            uses: maxim-lobanov/setup-xcode@v1
            with:
               xcode-version: '16.0'
    
          - name: Execute Unit tessts (iOS target)
            run: xcodebuild test -scheme 'EMOM timers' -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest'
    
          - name: Execute Unit tessts (AW target)
            run: xcodebuild test -scheme 'EMOM timers Watch App' -destination 'platform=watchOS Simulator,name=Apple Watch Series 10 (42mm),OS=latest'
    

    The first tag is name, which contains the given name for the workflow. Next, the on tag defines which event can trigger the workflow to run. In this case, we are interested in executing the workflow when a pull request targets the main or develop branch (for available events, please review the documentation).

    Finally, we find the jobs section, which describes the tasks we want to execute. utest-job is the designated name for this job, and the environment where it will be executed is specified as macos-latest.

    Next, we find the steps, which outline the sequence of actions to be executed. The first step is to check out the repository. In this step, we specify a shell command, but instead, we see an action being referenced. An action is a piece of code created by the community to perform specific tasks. It is highly likely that someone has already written the action you need, so be sure to review GitHub Actions or GitHub Actions Marketplace.  The checkout action was defined in the GitHub Actions   repositoty, and we can see that its popularity is good, so let’s give it a try.

    The second step is to set the Xcode version to one that is compatible with your project. My project is currently working with Xcode 16.0, so we need to set it to that version. I found this action in the GitHub Action marketplace.

    The final two steps involve executing unit tests for each target. Now, we can commit and push our changes.

    Create a pull request on GitHub for your project, and at the end, you should see that the workflow is being executed.

    After few minutes…

    I aim you to force fail some unit test and push changes. Did allow you to integrate branch?

    Workflow for linting

    We are going to create a second workflow to verify that static syntax analysis (linting) is correct. For this purpose, we have created the following .yml GitHub Action workflow script:

    name: lint
    
    # Especifica en qué ramas se ejecutará el workflow
    on:
      pull_request:
        branches: [main, develop]
    jobs:
      lint:
        runs-on: macos-latest
        steps:
          - name: Checkout code
            uses: actions/checkout@v4
    
          - name: Install SwiftLint
            run: brew install swiftlint
    
          - name: Run SwiftLint
            run: swiftlint

    In this workflow, the name and job refer to the linting process, with the only differences being in the last two steps.

    The first step, as in the previous workflow, is to check out the branch. The next step is to install SwiftLint via Homebrew, and the final step is to run SwiftLint. In this case, we will deliberately trigger a linting error.

    Once we commit and push the change, let’s proceed to review the pull request. The lint workflow has been executed, and a linting error has been triggered. However, it is still possible to merge the pull request, which is what we want to avoid. In the next section, we’ll address this issue.

    Setup branch rules

    Now it’s time to block any merges on the develop branch. Go to your repository settings.

    In the Branches section, click Add rule under Branch protection rules and select Add a classic branch protection rule.

    Enter the branch name where the rule applies. In this case, it is develop. Check «Require status checks to pass before merging» and «Require branch to be up to date before merging». In the search box below, type the workflow name. In this case, we want the unit tests and linting to succeed before proceeding with the pull request.

    When we return to the pull request page (and possibly refresh), we will see that we are not allowed to merge. This was our goal: to block any pull request that does not meet the minimum quality requirements.

    This rule has been applied to the development branch. I leave it to the reader to apply the same rule to the main branch.

    Conclusions

    By integrating GitHub Actions into our team development process, we can automate tasks that help us avoid increasing technical debt.

    Related links