<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Radu Dan on Medium]]></title>
        <description><![CDATA[Stories by Radu Dan on Medium]]></description>
        <link>https://medium.com/@radu-ionut-dan?source=rss-dee343eb346a------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*KMhwBOLag716f_JxaV7tdg.jpeg</url>
            <title>Stories by Radu Dan on Medium</title>
            <link>https://medium.com/@radu-ionut-dan?source=rss-dee343eb346a------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 06 May 2026 12:29:00 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@radu-ionut-dan/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Xcode Migrations: From Stone Age to AI Mastery]]></title>
            <link>https://radu-ionut-dan.medium.com/xcode-migrations-from-stone-age-to-ai-mastery-33640d58eb5c?source=rss-dee343eb346a------2</link>
            <guid isPermaLink="false">https://medium.com/p/33640d58eb5c</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[tech]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[xcode]]></category>
            <dc:creator><![CDATA[Radu Dan]]></dc:creator>
            <pubDate>Wed, 27 Aug 2025 15:41:14 GMT</pubDate>
            <atom:updated>2025-08-27T15:42:06.166Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*h0XR4IJY0eI-Ev2bSlq6-g.png" /></figure><p>At Qonto, Xcode migrations used to paralyze our 60+ iOS engineers for 3+ weeks. Today we complete them in 1 day without disrupting anyone’s work. The secret? We removed CocoaPods entirely, built Swift-based automation tools, and created proactive monitoring instead of reactive chaos. Result: 75% faster migrations with zero team disruption.</p><p>Read the complete story here: <a href="https://medium.com/qonto-way/xcode-migrations-from-stone-age-to-ai-mastery-d2590657e809">https://medium.com/qonto-way/xcode-migrations-from-stone-age-to-ai-mastery-d2590657e809</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=33640d58eb5c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Cracking the Localizable Strings in iOS]]></title>
            <link>https://medium.com/geekculture/cracking-the-localizable-strings-in-ios-2c5f28d4a45b?source=rss-dee343eb346a------2</link>
            <guid isPermaLink="false">https://medium.com/p/2c5f28d4a45b</guid>
            <category><![CDATA[localization]]></category>
            <category><![CDATA[localizable]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[string]]></category>
            <category><![CDATA[swiftui]]></category>
            <dc:creator><![CDATA[Radu Dan]]></dc:creator>
            <pubDate>Sat, 01 Feb 2025 22:39:36 GMT</pubDate>
            <atom:updated>2025-02-02T11:54:19.883Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_p8tKotTOFeybFC29p0G0w.png" /><figcaption>AI-generated visual showing how tricky working with Localizations in Swift can be</figcaption></figure><blockquote>In this article, we will explore the modern approach to localizing strings in an iOS application. We will delve into Apple’s Foundation frameworks related to localization and learn how to effectively use them.</blockquote><p>The source code for this article is open source and available on <a href="https://github.com/radude89/localized-words">GitHub</a>. You can also read more about this topic on my <a href="https://www.radude89.com/blog/localizable-strings.html">personal blog</a>.️</p><h3>Motivation</h3><p>Recently, I have been working on an iOS application and sought an easy way to make it accessible to a global audience. While SwiftUI simplifies the process, I found myself puzzled by the various methods available for localizing text.</p><h3>The Fun Way to Work with Strings</h3><p>It all started simply. In my <strong>Localizable.xcstrings</strong> catalog, I have a key named <strong>main.onboarding.title.label</strong>, which I plan to use in a <strong>Text</strong> view on my main screen.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IeGbUwnX8yTvxdX1xu8NrA.png" /><figcaption>Strings catalog from Xcode</figcaption></figure><p>The main screen is built using <strong>SwiftUI</strong> and features a straightforward card containing an <strong>Image</strong> and a <strong>Text</strong> view.</p><pre>struct MainView: View {<br>    var body: some View {<br>        VStack {<br>            Image(systemName: &quot;sun.max.fill&quot;)<br>            Text(&quot;main.onboarding.title.label&quot;)<br>            ...<br>        }<br>        ...<br>    }<br>}</pre><p>When I build the project, I automatically receive the key <strong>main.onboarding.title.label</strong>, as shown in the previous image.</p><p>So far, so good. I can easily navigate to my <strong>Strings</strong> <strong>catalog</strong>, add a new language, and select <strong>French</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/224/1*sqivrzztTHHJj2LhLeGRBg.png" /><figcaption>Add a new language (French) to my Strings catalog</figcaption></figure><p>This process is seamless; you don’t need to worry about the underlying mechanics. You can add your key in the UI element, build the project, go to your <strong>Strings catalog</strong>, translate it, and you’re done.</p><p>Here is the complete main screen of the app displayed in both French and English languages.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/394/1*RE75m5_DeVQI8T4kJGVZsw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/394/1*PDcPCQhzmtzwhsesOiNIoQ.png" /><figcaption>Main screen in English and in French languages</figcaption></figure><h3>Understanding Localization is Hard</h3><p>As an iOS developer, you may encounter various methods for translating text in larger projects.</p><p>In one project, you might see this:</p><pre>let title = LocalizedStringResource(stringLiteral: &quot;weird.text&quot;)</pre><p>In another project, you could come across:</p><pre>let petDetailsScreenTitleKey = LocalizedStringKey(&quot;dog.details.title&quot;)</pre><p>Or this:</p><pre>let alertTitle = String(localized: &quot;login.alert.title.label&quot;)</pre><p>In older projects, you might find strings localized like this:</p><pre>let commentBoxText = NSLocalizedString(<br>    &quot;comment.box&quot;,<br>    comment: &quot;Users enter their comment in this box.&quot;<br>)</pre><p>And one more example:</p><pre>let placeholderLocalizationValue = String.LocalizationValue(<br>    &quot;textfield.placeholder&quot;<br>)</pre><p>That’s too much. What are these functions and classes doing to my lovely <strong>String</strong>? <br>Is my <strong>String</strong> even a <strong>String</strong> anymore or it lost the authenticity?! <br>Who knows?!</p><h3>Cracking the Code</h3><p>Don’t worry; things will become clearer shortly.</p><p>Let’s apply all of these localization methods to our key defined at the beginning of the article:</p><pre>static let localizationValue = String.LocalizationValue(<br>    &quot;main.onboarding.title.label&quot;<br>)<br>static let localizationStaticString: StaticString = &quot;main.onboarding.title.label&quot;<br><br>let localizedResourceWithStringLiteral = LocalizedStringResource(<br>    stringLiteral: &quot;main.onboarding.title.label&quot;<br>)<br><br>let localizedResourceWithLocalizationValue = LocalizedStringResource(<br>    localizationValue<br>)<br><br>let localizedResourceWithStaticString = LocalizedStringResource(<br>    localizationStaticString,<br>    defaultValue: localizationValue<br>)<br><br>let localizedString = String(localized: &quot;main.onboarding.title.label&quot;)<br><br>let localizedStringKey = LocalizedStringKey(&quot;main.onboarding.title.label&quot;)<br><br>let localizedStringOldWay = NSLocalizedString(<br>    &quot;main.onboarding.title.label&quot;,<br>    comment: &quot;Used to show the onboarding title.&quot;<br>)</pre><p>Believe it or not, the first three variables:</p><ul><li><strong>localizedResourceWithStringLiteral</strong>,</li><li><strong>localizedResourceWithLocalizationValue</strong>, and</li><li><strong>localizedResourceWithStaticString</strong></li></ul><p>are equivalent. They hold the same information and point to the same localized string resource, producing identical output when used in our app.</p><p>The same applies to <strong>localizedString</strong> and <strong>localizedStringOldWay</strong>, which will yield the translated version of your string.</p><p>Finally, using <strong>localizedStringKey</strong> in a SwiftUI <strong>Text</strong> view will also produce the translated string.</p><p>Now we understand that we have multiple ways to translate a string and present it to our users.</p><p>Let’s clarify when to use each method.</p><h4>LocalizedStringResource vs String(localized:)</h4><p>According to <a href="https://developer.apple.com/documentation/foundation/localizedstringresource">Apple’s documentation</a>, <strong>LocalizedStringResource</strong> is a <em>“reference to a localizable string, accessible from another process”</em>.<br>We should use <strong>LocalizedStringResource</strong> when we want to provide localizable strings with lookups deferred to a later time.</p><p>What does this mean? Why would we want to defer lookups?</p><p>Here are a few scenarios:</p><p><strong>1. When communicating between processes (like the main app and extensions),</strong></p><p><strong>2. When you want to change the locale before displaying the string,</strong></p><p><strong>3. When working with features like Siri or App Intents.</strong></p><p>Imagine a notification system that allows users to schedule messages for a later time. We want those messages to be in the user’s preferred language, which might differ from the app’s current language. For example, the app is in English, while the user’s device has been changed later on to French.</p><pre>struct NotificationScheduler {<br>    /// The localized greeting message key used in notifications.<br>    private static let greeting = LocalizedStringResource(&quot;main.onboarding.title.label&quot;)<br>    <br>    func scheduleNotification(forUserWithLocale locale: Locale) async {<br>        // Create a unique notification request with localized content and trigger.<br>        let request = UNNotificationRequest(<br>            identifier: UUID().uuidString,<br>            content: buildContent(locale: locale), // contains the `greeting` message<br>            trigger: buildTrigger() // creates the `UNTimeIntervalNotificationTrigger` object<br>        )<br><br>        // Get the shared notification center instance for managing notifications<br>        let notificationCenter = UNUserNotificationCenter.current()<br>        <br>        // Request user&#39;s permission to show notifications<br>        await requestAuthorization(notificationCenter: notificationCenter)<br>        <br>        // Schedule the notification with the notification center<br>        await scheduleNotification(<br>            request: request,<br>            notificationCenter: notificationCenter<br>        )<br>    }<br>    ...<br>}</pre><p>We can trigger the notification scheduler with a <strong>Task</strong>, as the code is asynchronous:</p><pre>Task {<br>    await scheduler.scheduleNotification(<br>        forUserWithLocale: Locale.current<br>    )<br>}</pre><p>This ensures that the notification displays the correct translation based on the device’s locale.</p><p>Understanding this concept is crucial. If we had used <strong>String(localized: “main.onboarding.title.label”)</strong> when setting the notification content, the message would have already been translated. If the user changed the locale or language before receiving the scheduled notification, they would see it in a different language.</p><p>This is shown in the below image, where the message of the notification is in English, but the phone is in French:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/394/1*iuP7sjxHndu0lUljJZgE1g.png" /><figcaption>Banner’s message is in English while the phone is in French</figcaption></figure><p>Seeing a real-world example applied to an app helps clarify this important concept.</p><p>Moreover, <a href="https://developer.apple.com/videos/play/wwdc2023/10155">Apple states</a> that <strong>LocalizedStringResource</strong> is the <strong><em>recommended</em></strong><em> type for </em><strong><em>representing and passing around</em></strong><em> localizable strings</em>. If you need to provide localized strings to another process that might be using a different locale, then <strong>LocalizedStringResource</strong> is the way to go.</p><p>In summary, this type is part of Apple’s efforts to streamline localization in iOS applications, making it easier for developers to create apps that are accessible to a global audience.</p><h4>What About String.LocalizationValue?</h4><p><a href="https://developer.apple.com/documentation/swift/string/localizationvalue">Apple recommends</a> using <em>“this type when the localization key is the localized string value in the development language”.</em> This essentially means that the string you use as a key is the same string that will appear in your default development language (usually English).</p><p>For example, consider the localization key “<strong>Sign In</strong>”, with translations in different languages:</p><ul><li>🇬🇧 English: “Sign In”,</li><li>🇪🇸 Spanish: “Iniciar Sesión”,</li><li>🇫🇷 French: “Se Connecter”.</li></ul><p>One drawback is that <strong>String.LocalizationValue</strong> does not conform to <strong>StringProtocol</strong>, so you can’t use it directly in SwiftUI <strong>Text</strong> views. As a key, you can use it with <strong>LocalizedStringResource(keyAndValue:)</strong> or with <strong>String(localized:)</strong> initializers, for example.</p><pre>// ❌ This does not work<br>Text(String.LocalizationValue(&quot;main.onboarding.title.label&quot;))<br><br>// ✅ This works<br>let localizationValue = String.LocalizationValue(&quot;main.onboarding.title.label&quot;)<br>Text(LocalizedStringResource(localizationValue))<br><br>// ✅ This also works<br>Text(String(localized: localizationValue))</pre><h4>How Is That Different from LocalizedStringKey?</h4><p><strong>LocalizedStringKey</strong> was integrated into SwiftUI from the beginning and works with many UI components and modifiers.</p><p>When you use this initializer, it searches for the <strong>key</strong> you provide in a localization table. If it finds the key, it displays the corresponding string in the text view. However, if the key isn’t found or if there’s no localization table available, the text view will simply show the key itself as a string.</p><p>Compared to <strong>String.LocalizationValue</strong>, a major advantage of <strong>LocalizedStringKey</strong> is that it can be used directly in SwiftUI views.</p><pre>// ✅ This works<br>Text(LocalizedStringKey(&quot;main.onboarding.title.label&quot;))</pre><h4>Oops, We Forgot NSLocalizedString</h4><p>We can safely assume that when we see something that starts with <strong>NS</strong>, it’s something old — specifically, Objective-C old. You are correct.</p><p><strong>NSLocalizedString</strong> was introduced in <strong>iOS 2.0</strong> 🙀, 16 years ago, during the era of the iPhone 3G when most programmers used <strong>NSLog</strong> to print messages to their console.</p><p>It works perfectly well with SwiftUI and can be safely used in our codebase. In Swift, we use its successor, <strong>String(localized:)</strong>.</p><pre>Text(<br>    NSLocalizedString(<br>        &quot;main.onboarding.title.label&quot;,<br>        comment: &quot;Used to show the onboarding title.&quot; // comment for translators<br>    )<br>)</pre><h3>God Mode IDDQD</h3><p>Consider the following challenge:</p><p><strong>You are working on the next-gen iOS puzzle game and have been assigned an important task: designing the “Special Hint” feature.</strong></p><p><strong>What is the “Special Hint” feature, you might ask? Let me explain.</strong></p><ol><li><strong>Users will see a text field to enter a keyword.</strong></li><li><strong>To progress to the next level, they must enter the correct keyword.</strong></li><li><strong>They have opted in to receive special hints based on the entered words that are related to the keyword.</strong></li><li><strong>If they enter a word that should trigger a special hint but is not the keyword, the game should display an alert with a localized message.</strong></li></ol><p><strong>📒 Note: The iOS puzzle game is localized in multiple languages.</strong></p><p><strong>Example:</strong></p><ul><li><strong>Pierre has his device set to French and enters the word: ‘<em>morse’</em>.</strong></li><li><strong>The word ‘<em>morse</em>’ is recognized as a trigger for a special hint.</strong></li><li><strong>The special hint in English is ‘<em>You will have to be more archaic’</em>, which can be translated approximately in French as <em>‘Vous devrez être plus archaïque’</em>. The message in French is shown to Pierre.</strong></li><li><strong>Jane has her phone set to English and enters the word: ‘<em>walrus</em>’. The special hint will be triggered, and Jane will see the message <em>‘You will have to be more archaic’.</em></strong></li></ul><p><strong>📒 Note: The game should have multiple special hints associated with the keyword that unlocks the next level (e.g., walrus, cone, banana). Each special hint will trigger a different message.</strong></p><p>Before jumping to the solution, I encourage you to resolve this problem. It will help you better understand the localization concepts we discussed in this article.</p><p>Here is one way to approach the implementation:</p><ol><li>The user is French and enters the special hint “<strong><em>morse</em></strong>”.</li><li>We have a predefined array of keys: <strong><em>[“walrus”, “banana”, “cone”]</em></strong>. These keys should be localized based on the entered word.</li><li>Translations in French for those keys are: <br><strong><em>“walrus” = “morse”; <br>“banana” = “banane”; <br>“cone” = “cône”</em></strong>.</li><li>For each key in the array, retrieve the localized string for the key. The mapped array will be <strong><em>[“morse”, “banane”, “cône”]</em></strong>.</li><li>If one of the keys matches the special hint <strong>“<em>morse</em>”</strong>, we should display the special hint message. To achieve this, we can create a convention for each key and add <strong>“<em>_value</em>”</strong> as a suffix: <strong><em>[“walrus_value”, “banana_value”, “cone_value”]</em></strong>.</li><li>Since we know <strong>“<em>morse</em>”</strong> is a key in the array and corresponds to the localized key <strong>“<em>walrus</em>”</strong>, we should display the localized text corresponding to the key <strong>“<em>walrus_value</em>”</strong>.</li></ol><p>Code snippet (<a href="https://github.com/radude89/localized-words">full source-code on GitHub</a>):</p><pre>struct ChallengeView: View {<br>    /// The message to be displayed in the alert when a special hint is triggered.<br>    @State private var alertMessage: LocalizedStringResource = &quot;&quot;<br>    <br>    /// A flag to control the visibility of the alert.<br>    @State private var showAlert = false<br>    <br>    /// The word entered by the user to check against accepted keys.<br>    @State private var enteredWord = &quot;&quot;<br><br>    /// An array of accepted localized keys that can trigger special hints.<br>    private let acceptedLocalizedKeys = [&quot;walrus&quot;, &quot;cone&quot;, &quot;banana&quot;]<br><br>    var body: some View {<br>        contentView<br>            .onChange(of: enteredWord) { _, newValue in<br>                checkWord(newValue) // Check the entered word for special hints.<br>            }<br>            .alert(...) // Display the alert if showAlert is true.<br>    }<br><br>    /// Checks the entered text against the accepted keys and updates the alert message if a match is found.<br>    /// - Parameter enteredText: The text entered by the user to check for special hints.<br>    private func checkWord(_ enteredText: String) {<br>        let localizedKeyValueWords = Dictionary(<br>            uniqueKeysWithValues: acceptedLocalizedKeys.map { key in<br>                let translatedCopy = String.LocalizationValue(key)<br>                return (key, String(localized: translatedCopy))<br>            }<br>        )<br>        <br>        // Invoke the use case to find a matching special hint.<br>        if let specialHint = FindWordInListCaseInsensitiveUseCase.invoke(<br>            input: enteredText,<br>            wordsDictionary: localizedKeyValueWords<br>        ) {<br>            // Update the alert message with the localized key for the special hint.<br>            alertMessage = LocalizedStringResource(<br>                stringLiteral: &quot;\(specialHint.localizedKey)\(Self.suffixForSpecialHint)&quot;<br>            )<br>            showAlert = true // Set the flag to show the alert.<br>        }<br>    }<br>}<br><br>// Enum to encapsulate the logic for finding a word in a case-insensitive manner.<br>enum FindWordInListCaseInsensitiveUseCase {<br>    /// Invokes the search for a matching word in the provided dictionary.<br>    /// - Parameters:<br>    ///   - input: The user input to compare against the dictionary.<br>    ///   - wordsDictionary: A dictionary of words to search through.<br>    /// - Returns: A tuple containing the localized key and the word if a match is found; otherwise, nil.<br>    static func invoke(<br>        input: String,<br>        wordsDictionary: [String: String]<br>    ) -&gt; (localizedKey: String, word: String)? {<br>        // Search for a matching word in the dictionary using case-insensitive comparison.<br>        if let (localizedKey, word) = wordsDictionary.first(where: { key, word in<br>            input.compare(<br>                word,<br>                options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive]<br>            ) == .orderedSame<br>        }) {<br>            return (localizedKey, word) // Return the found key and word.<br>        }<br>        return nil // Return nil if no match is found.<br>    }<br>}</pre><h3>Conclusion</h3><p>In conclusion, localizing strings in iOS applications is a crucial aspect of creating a user-friendly experience for a global audience. Throughout this article, we explored various methods for localization, including LocalizedStringResource, String(localized:), LocalizedStringKey, and NSLocalizedString. Each method has its own use cases and advantages, allowing developers to choose the most appropriate approach based on their specific needs.</p><p>Understanding the differences between these localization techniques is essential for effective app development. By leveraging the right tools, developers can ensure that their applications not only reach a wider audience but also resonate with users in their preferred languages.</p><p>As you continue to develop your iOS applications, remember the importance of localization and consider implementing these strategies to enhance accessibility and user satisfaction. Embracing localization will ultimately lead to a more inclusive and successful app experience for users around the world.</p><h3>Bonus Questions</h3><ol><li>What improvements can we make to the algorithm for the challenge?</li><li>What does the <strong><em>NS</em></strong> in <strong>NSLocalizedString</strong> stand for?</li></ol><h3>Resources</h3><p>Here are some helpful resources for further reading on localization in iOS:</p><ul><li><a href="https://developer.apple.com/documentation/swift/string/localizationvalue">Localization Value Documentation</a></li><li><a href="https://developer.apple.com/documentation/foundation/localizedstringresource">Localized String Resource Documentation</a></li><li><a href="https://github.com/radude89/localized-words">Localized Words GitHub Repository</a></li><li><a href="https://levelup.gitconnected.com/ios-localization-localizedstringresource-vs-localizedstringkey-vs-string-56cb519cf098">iOS Localization: LocalizedStringResource vs LocalizedStringKey</a></li><li><a href="https://developer.apple.com/documentation/foundation/nslocalizedstring">NSLocalizedString Documentation</a></li><li><a href="https://medium.com/turo-engineering/default-value-on-nslocalizedstring-d6e1a7c0e5ca">Default Value on NSLocalizedString</a></li><li><a href="https://www.swiftyplace.com/blog/localization-ios-app-xcode-15">Localization in iOS Apps</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2c5f28d4a45b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/geekculture/cracking-the-localizable-strings-in-ios-2c5f28d4a45b">Cracking the Localizable Strings in iOS</a> was originally published in <a href="https://medium.com/geekculture">Geek Culture</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Battle of the iOS Architecture Patterns: View Interactor Presenter (VIP)]]></title>
            <link>https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84?source=rss-dee343eb346a------2</link>
            <guid isPermaLink="false">https://medium.com/p/59ebdae86e84</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Radu Dan]]></dc:creator>
            <pubDate>Fri, 27 Aug 2021 07:05:59 GMT</pubDate>
            <atom:updated>2025-01-17T22:02:35.473Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dp6jPW3V6WOAOb9e2YDSvg.png" /><figcaption>Architecture Series — View Interactor Presenter (VIP)</figcaption></figure><h3>Motivation</h3><p>Before diving into iOS app development, it’s crucial to carefully consider the project’s architecture. We need to thoughtfully plan how different pieces of code will fit together, ensuring they remain comprehensible not just today, but months or years later when we need to revisit and modify the codebase. Moreover, a well-structured project helps establish a shared technical vocabulary among team members, making collaboration more efficient.</p><p>This article kicks off an exciting series where we’ll explore different architectural approaches by building the same application using various patterns. Throughout the series, we’ll analyze practical aspects like build times and implementation complexity, weigh the pros and cons of each pattern, and most importantly, examine real, production-ready code implementations. This hands-on approach will help you make informed decisions about which architecture best suits your project needs.</p><h3>Architecture Series Articles</h3><ul><li><a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">Model View Controller (MVC)</a></li><li><a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">Model View ViewModel (MVVM)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">Model View Presenter (MVP)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">Model View Presenter with Coordinators (MVP-C)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">View Interactor Presenter Entity Router (VIPER)</a></li><li><strong>View Interactor Presenter (VIP) — Current Article</strong></li></ul><p>If you’re eager to explore the implementation details directly, you can find the complete source code in our open-source repository <a href="https://github.com/radude89/footballgather-ios">here</a>.</p><h3>Why Your iOS App Needs a Solid Architecture Pattern</h3><p>The cornerstone of any successful iOS application is maintainability. A well-architected app clearly defines boundaries — you know exactly where view logic belongs, what responsibilities each view controller has, and which components handle business logic. This clarity isn’t just for you; it’s essential for your entire development team to understand and maintain these boundaries consistently.</p><p>Here are the key benefits of implementing a robust architecture pattern:</p><ul><li><strong>Maintainability</strong>: Makes code easier to update and modify over time</li><li><strong>Testability</strong>: Facilitates comprehensive testing of business logic through clear separation of concerns</li><li><strong>Team Collaboration</strong>: Creates a shared technical vocabulary and understanding among team members</li><li><strong>Clean Separation</strong>: Ensures each component has clear, single responsibilities</li><li><strong>Bug Reduction</strong>: Minimizes errors through better organization and clearer interfaces between components</li></ul><h3>Project Requirements Overview</h3><p><strong>Given</strong> a medium-sized iOS application consisting of 6–7 screens, we’ll demonstrate how to implement it using the most popular architectural patterns in the iOS ecosystem: MVC (Model-View-Controller), MVVM (Model-View-ViewModel), MVP (Model-View-Presenter), VIPER (View-Interactor-Presenter-Entity-Router), VIP (Clean Swift), and the Coordinator pattern. Each implementation will showcase the pattern’s strengths and potential challenges.</p><p>Our demo application, <strong>Football Gather</strong>, is designed to help friends organize and track their casual football matches. It’s complex enough to demonstrate real-world architectural challenges while remaining simple enough to clearly illustrate different patterns.</p><h3>Core Features and Functionality</h3><ul><li>Player Management: Add and maintain a roster of players in the application</li><li>Team Assignment: Flexibly organize players into different teams for each match</li><li>Player Customization: Edit player details and preferences</li><li>Match Management: Set and control countdown timers for match duration</li></ul><h3>Screen Mockups</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9OX6Xc0mZWJKFWFp.png" /></figure><h3>Backend</h3><p>The application is supported by a web backend developed using the Vapor framework, a popular choice for building modern REST APIs in Swift. You can explore the details of this implementation in our article <a href="https://www.radude89.com/blog/vapor.html">here</a>, which covers the fundamentals of building REST APIs with Vapor and Fluent in Swift. Additionally, we have documented the transition from Vapor 3 to Vapor 4, highlighting the enhancements and new features in our article <a href="https://www.radude89.com/blog/migrate-to-vapor4.html">here</a>.</p><h3>Cleaning Your Code Like a VIP</h3><p>VIP is not a widely adopted architecture pattern, but it offers unique advantages for clean and scalable iOS development. Invented by Raymond Law, VIP is an adaptation of Uncle Bob’s Clean Architecture principles tailored for iOS projects. For more details, visit <a href="https://clean-swift.com/">Clean Swift</a>.</p><p>The primary goal of VIP is to address the “Massive View Controller” problem often associated with MVC. VIP also aims to resolve challenges seen in other architecture patterns. For example, while VIPER places the Presenter at the center of the application, VIP simplifies this process by using a unidirectional flow of control, making it easier to manage method invocations across layers.</p><p>VIP organizes your app into distinct control cycles, ensuring a clear and consistent flow of data and actions.</p><h3>Example Scenario of Applying VIP:</h3><ol><li>A user taps a button to fetch a list of players, starting in the <strong>ViewController</strong>.</li><li>The associated IBAction triggers a method in the <strong>Interactor</strong>.</li><li>The <strong>Interactor</strong> processes the request, executes business logic (e.g., fetching players from a server), and sends the response to the <strong>Presenter</strong> to format it for display.</li><li>The <strong>Presenter</strong> passes the formatted data to the <strong>ViewController</strong>, which displays it to the user.</li></ol><h3>VIP Architecture Components</h3><h4>View/ViewController</h4><p>The <strong>View/ViewController</strong> layer has two primary responsibilities: sending user actions to the <strong>Interactor</strong> and displaying data received from the <strong>Presenter</strong>.</p><h4>Interactor</h4><p>Known as the “new Presenter,” the <strong>Interactor</strong> serves as the core of the VIP architecture. It handles tasks like network calls, error handling, and business logic computation.</p><h4>Worker</h4><p>In some cases (e.g., in Football Gather), we refer to Workers as “Services.” A <strong>Worker</strong> offloads specific tasks from the <strong>Interactor</strong>, such as managing network requests or database interactions.</p><h4>Presenter</h4><p>The <strong>Presenter</strong> processes data from the <strong>Interactor</strong> and formats it into a <strong>ViewModel</strong> suitable for display in the <strong>View</strong>.</p><h4>Router</h4><p>The <strong>Router</strong> manages scene transitions, similar to its role in VIPER.</p><h4>Models</h4><p>The <strong>Model</strong> layer encapsulates data, much like in other architectural patterns.</p><h4>Communication</h4><p>Communication flows in a unidirectional manner:</p><ul><li>The <strong>ViewController</strong> interacts with both the <strong>Router</strong> and the <strong>Interactor</strong>.</li><li>The <strong>Interactor</strong> processes data and communicates with the <strong>Presenter</strong>. It may also collaborate with <strong>Workers</strong> for specific tasks.</li><li>The <strong>Presenter</strong> formats the response from the <strong>Interactor</strong> into a <strong>ViewModel</strong> and sends it to the <strong>View/ViewController</strong>.</li></ul><h3>Advantages of VIP</h3><ul><li>Eliminates the “Massive View Controller” issue found in MVC.</li><li>Avoids the “Massive View Model” problem that can occur with incorrect MVVM implementation.</li><li>Solves control issues seen in VIPER by introducing the VIP cycle.</li><li>Prevents “Massive Presenters” often encountered with improper VIPER use.</li><li>Aligns with Clean Architecture principles, ensuring separation of concerns.</li><li>Facilitates handling of complex business logic by delegating to <strong>Workers</strong>.</li><li>Highly testable and compatible with TDD practices.</li><li>Offers good modularity and easier debugging.</li></ul><h3>Disadvantages of VIP</h3><ul><li>Introduces numerous layers, which can become tedious without code generation tools.</li><li>Involves writing significant amounts of code, even for simple actions.</li><li>Not well-suited for small applications due to its complexity.</li><li>Certain components may feel redundant, depending on the app’s use case.</li><li>Slightly increases app startup time.</li></ul><h3>VIP vs. VIPER</h3><ul><li>In VIP, the <strong>Interactor</strong> directly interacts with the <strong>ViewController</strong>.</li><li>The <strong>ViewController</strong> holds a reference to the <strong>Router</strong> in VIP.</li><li>VIPER’s flexibility can lead to “Massive Presenters” if not implemented correctly.</li><li>VIP maintains a unidirectional flow of control.</li><li>Services in VIPER are referred to as <strong>Workers</strong> in VIP.</li></ul><h3>Applying VIP to Our Code</h3><p>Transitioning an app from VIPER to VIP is not a straightforward process. The first step is to transform the <strong>Presenter</strong> into an <strong>Interactor</strong>. Afterward, extract the <strong>Router</strong> from the <strong>Presenter</strong> and integrate it into the <strong>ViewController</strong>.</p><p>The module assembly logic from VIPER can remain intact, simplifying the process.</p><h3>Login Scene</h3><p>Let’s start by applying these changes to the <strong>Login</strong> scene and iterating from there.</p><pre>final class LoginViewController: UIViewController, LoginViewable {<br><br>    // MARK: - Properties<br>    @IBOutlet private weak var usernameTextField: UITextField!<br>    @IBOutlet private weak var passwordTextField: UITextField!<br>    @IBOutlet private weak var rememberMeSwitch: UISwitch!<br><br>    lazy var loadingView = LoadingView.initToView(view)<br><br>    var interactor: LoginInteractorProtocol = LoginInteractor()<br>    var router: LoginRouterProtocol = LoginRouter()<br><br>    // MARK: - View life cycle<br>    override func viewDidLoad() {<br>        super.viewDidLoad()<br>        loadCredentials()<br>    }<br><br>    private func loadCredentials() {<br>        let request = Login.LoadCredentials.Request()<br>        interactor.loadCredentials(request: request)<br>    }<br><br>    // ...<br>}</pre><p>As you can see we no longer tell the <strong>Presenter</strong> that the view has been loaded. We now make a request to the <strong>Interactor</strong> to load the credentials.</p><p>The IBActions have been modified as below:</p><pre>final class LoginViewController: UIViewController, LoginViewable {<br><br>    // ...<br>    @IBAction private func login(_ sender: Any) {<br>        showLoadingView()<br>        let request = Login.Authenticate.Request(username: usernameTextField.text,<br>                                                  password: passwordTextField.text,<br>                                                  storeCredentials: rememberMeSwitch.isOn)<br>        interactor.login(request: request)<br>    }<br><br>    @IBAction private func register(_ sender: Any) {<br>        showLoadingView()<br>        let request = Login.Authenticate.Request(username: usernameTextField.text,<br>                                                  password: passwordTextField.text,<br>                                                  storeCredentials: rememberMeSwitch.isOn)<br>        interactor.register(request: request)<br>    }<br>    // ...<br>}</pre><p>We start the loading view, construct the request to the <strong>Interactor</strong> containing the username, password contents of the text fields and the state of the UISwitch for remembering the username.</p><p>Next, handling the viewDidLoad UI updates are made through the LoginViewConfigurable protocol:</p><pre>extension LoginViewController: LoginViewConfigurable {<br>    func displayStoredCredentials(viewModel: Login.LoadCredentials.ViewModel) {<br>        rememberMeSwitch.isOn = viewModel.rememberMeIsOn<br>        usernameTextField.text = viewModel.usernameText<br>    }<br>}</pre><p>Finally, when the logic service call has been completed we call from the <strong>Presenter</strong> the following method:</p><pre>func loginCompleted(viewModel: Login.Authenticate.ViewModel) {<br>    hideLoadingView()<br><br>    if viewModel.isSuccessful {<br>        router.showPlayerList()<br>    } else {<br>        handleError(title: viewModel.errorTitle!, message: viewModel.errorDescription!)<br>    }<br>}</pre><p>The <strong>Interactor</strong> looks the same as the one from the VIPER architecture. It has the same dependencies:</p><pre>final class LoginInteractor: LoginInteractable {<br><br>    var presenter: LoginPresenterProtocol<br><br>    private let loginService: LoginService<br>    private let usersService: StandardNetworkService<br>    private let userDefaults: FootballGatherUserDefaults<br>    private let keychain: FootbalGatherKeychain<br><br>    init(presenter: LoginPresenterProtocol = LoginPresenter(),<br>          loginService: LoginService = LoginService(),<br>          usersService: StandardNetworkService = StandardNetworkService(resourcePath: &quot;/api/users&quot;),<br>          userDefaults: FootballGatherUserDefaults = .shared,<br>          keychain: FootbalGatherKeychain = .shared) {<br>        self.presenter = presenter<br>        self.loginService = loginService<br>        self.usersService = usersService<br>        self.userDefaults = userDefaults<br>        self.keychain = keychain<br>    }<br>}</pre><p>The key thing here is that we now inject the <strong>Presenter</strong> through the initializer and it is no longer a weak variable.</p><p>Loading credentials is presented below. We first take the incoming request from the <strong>ViewController</strong>. We create a response for the presenter and call the function presentCredentials(response: response).</p><pre>func loadCredentials(request: Login.LoadCredentials.Request) {<br>    let rememberUsername = userDefaults.rememberUsername ?? true<br>    let username = keychain.username<br>    let response = Login.LoadCredentials.Response(rememberUsername: rememberUsername, username: username)<br>    presenter.presentCredentials(response: response)<br>}</pre><p>The login and register methods are the same, the exception being the Network service (<strong>Worker</strong>).</p><pre>func login(request: Login.Authenticate.Request) {<br>    guard let username = request.username, let password = request.password else {<br>        let response = Login.Authenticate.Response(error: .missingCredentials)<br>        presenter.authenticationCompleted(response: response)<br>        return<br>    }<br><br>    let requestModel = UserRequestModel(username: username, password: password)<br><br>    loginService.login(user: requestModel) { [weak self] result in<br>        DispatchQueue.main.async {<br>            switch result {<br>            case .failure(let error):<br>                let response = Login.Authenticate.Response(error: .loginFailed(error.localizedDescription))<br>                self?.presenter.authenticationCompleted(response: response)<br>            case .success(_):<br>                guard let self = self else { return }<br>                self.updateCredentials(username: username, shouldStore: request.storeCredentials)<br>                let response = Login.Authenticate.Response(error: nil)<br>                self.presenter.authenticationCompleted(response: response)<br>            }<br>        }<br>    }<br>}<br><br>private func updateCredentials(username: String, shouldStore: Bool) {<br>    keychain.username = shouldStore ? username : nil<br>    userDefaults.rememberUsername = shouldStore<br>}</pre><p>The <strong>Presenter</strong> doesn’t hold references to the <strong>Router</strong> or <strong>Interactor</strong>. We just keep the dependency of the <strong>View</strong>, which has to be weak to complete the VIP cycle and not have retain cycles.</p><p>The <strong>Presenter</strong> has been greatly simplified, exposing two methods of the public API:</p><pre>func presentCredentials(response: Login.LoadCredentials.Response) {<br>    let viewModel = Login.LoadCredentials.ViewModel(rememberMeIsOn: response.rememberUsername,<br>                                                    usernameText: response.username)<br>    view?.displayStoredCredentials(viewModel: viewModel)<br>}<br><br>func authenticationCompleted(response: Login.Authenticate.Response) {<br>    guard response.error == nil else {<br>        handleServiceError(response.error)<br>        return<br>    }<br><br>    let viewModel = Login.Authenticate.ViewModel(isSuccessful: true, errorTitle: nil, errorDescription: nil)<br>    view?.loginCompleted(viewModel: viewModel)<br>}<br><br>private func handleServiceError(_ error: LoginError?) {<br>    switch error {<br>    case .missingCredentials:<br>        let viewModel = Login.Authenticate.ViewModel(isSuccessful: false,<br>                                                      errorTitle: &quot;Error&quot;,<br>                                                      errorDescription: &quot;Both fields are mandatory.&quot;)<br>        view?.loginCompleted(viewModel: viewModel)<br>    case .loginFailed(let message), .registerFailed(let message):<br>        let viewModel = Login.Authenticate.ViewModel(isSuccessful: false,<br>                                                      errorTitle: &quot;Error&quot;,<br>                                                      errorDescription: String(describing: message))<br>        view?.loginCompleted(viewModel: viewModel)<br>    default:<br>        break<br>    }<br>}</pre><p>The <strong>Router</strong> layer remains the same.</p><p>We apply some minor updates to the <strong>Module</strong> assembly:</p><pre>extension LoginModule: AppModule {<br>    func assemble() -&gt; UIViewController? {<br>        presenter.view = view<br><br>        interactor.presenter = presenter<br>        view.interactor = interactor<br>        view.router     = router<br>        return view as? UIViewController<br>    }<br>}</pre><h4>PlayerList scene</h4><p>Next, we move to PlayerList scene.</p><p>The ViewController will be transformed in a similar way - the <strong>Presenter</strong> will be replaced by <strong>Interactor</strong> and we now hold a reference to the <strong>Router</strong>.</p><p>An interesting aspect in VIP is the fact we can have an array of view models inside the <strong>ViewController</strong>:</p><pre>var interactor: PlayerListInteractorProtocol = PlayerListInteractor()<br>var router: PlayerListRouterProtocol = PlayerListRouter()<br><br>private var displayedPlayers: [PlayerList.FetchPlayers.ViewModel.DisplayedPlayer] = []</pre><p>We no longer tell the <strong>Presenter</strong> that the <strong>View</strong> has been loaded. The <strong>ViewController</strong> will configure its UI elements in the initial state.</p><pre>override func viewDidLoad() {<br>    super.viewDidLoad()<br><br>    setupView()<br>    fetchPlayers()<br>}<br><br>private func setupView() {<br>    configureTitle(&quot;Players&quot;)<br>    setupBarButtonItem(title: &quot;Select&quot;)<br>    setBarButtonState(isEnabled: false)<br>    setupTableView()<br>}</pre><p>Similar to <strong>Login</strong>, the IBActions will construct a request and will call a method within the <strong>Interactor</strong>.</p><pre>// MARK: - Selectors<br>@objc private func selectPlayers() {<br>    let request = PlayerList.SelectPlayers.Request()<br>    interactor.selectPlayers(request: request)<br>}<br><br>@IBAction private func confirmOrAddPlayers(_ sender: Any) {<br>    let request = PlayerList.ConfirmOrAddPlayers.Request()<br>    interactor.confirmOrAddPlayers(request: request)<br>}</pre><p>When the data will be fetched and ready to be displayable, the <strong>Presenter</strong> will call the method from the <strong>ViewController</strong> displayFetchedPlayers.</p><p>The rest of the code is available in the <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIP">open-source repository</a>.</p><h3>Key Metrics</h3><h4>Lines of code — Protocols</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*raDn1AnctaH4D-CHiHoyHQ.png" /></figure><h4>Lines of code — View Controllers and Views</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*K7cWcvGH4mV_doOEX2pSHw.png" /></figure><h4>Lines of code — Modules</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Z9Od7ep3z6n-OhFo1C4ZeA.png" /></figure><h4>Lines of code — Local Models</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JoxAdRbHZTK1bhmm6FjJnA.png" /></figure><h4>Lines of code — Routers</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dA6_JILbj1tSakcEtCiNcA.png" /></figure><h4>Lines of code — Presenters</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FIP8UDw3Ta0olq6E7M03tw.png" /></figure><h4>Lines of code — Interactors</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zGFlcvd34d6G1zN2Wtbatw.png" /></figure><h4>Unit Tests</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Uflekkmz25mNegB4sepqJg.png" /></figure><h4>Build Times</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8C4GHs7PmQDoCvogGcE1MQ.png" /></figure><p><em>Tests were run on an 8-Core Intel Core i9, MacBook Pro, 2019. Xcode version: 12.5.1. macOS Big Sur.</em></p><h3>Conclusion</h3><p>We implemented VIP architecture in an application originally built with VIPER. The first noticeable improvement is the significant simplification and cleanliness of the <strong>Presenters</strong>. If the transition were made from an MVC application, the <strong>ViewControllers</strong> would have been reduced drastically due to better separation of concerns.</p><p>VIP introduces a unidirectional flow of control, making the invocation of methods across layers more straightforward and predictable. This results in a cleaner and more maintainable structure.</p><p>The average build times for VIP are comparable to those of VIPER and MVP, remaining around <strong>10 seconds</strong>. While the addition of more unit tests increases the overall test execution time, we found it to be slightly faster than VIPER during our testing process.</p><p>One key observation is the reduction in the size of <strong>Presenters</strong>. With VIP, <strong>Presenters</strong> were streamlined to only <strong>514</strong> lines of code, a substantial improvement over VIPER. However, this reduction is offset by an increase in the size of <strong>Interactors</strong>, which grew by <strong>508</strong> lines. Essentially, what was removed from the <strong>Presenters</strong> was redistributed to the <strong>Interactors</strong>, resulting in a similar overall code footprint.</p><p>Personally, I still lean towards VIPER. While VIP offers several advantages, there are aspects of the architecture that, in my view, deviate from the principles it claims to follow, including Uncle Bob’s Clean Architecture guidelines.</p><p>For instance, the necessity of constructing a Request object, even when there is no data to attach to it, seems redundant. While this step can technically be skipped, the <a href="https://github.com/Clean-Swift/CleanStore/blob/master/CleanStore/Scenes/ListOrders/ListOrdersViewController.swift#L79-L83">example repository</a> demonstrates numerous instances of empty Request objects, which add unnecessary boilerplate code.</p><p>Another challenge is the added complexity of maintaining an array of view models within the <strong>ViewController</strong>. This approach can lead to synchronization issues with the corresponding <strong>Worker</strong> models, making the architecture harder to manage.</p><p>That said, there’s room to customize and adapt VIP to better suit specific project needs. A personalized variation of VIP could mitigate many of these concerns and improve its practicality in certain contexts.</p><p>On a positive note, the concept of VIP cycles is compelling, and the architecture is well-suited for Test-Driven Development (TDD). However, adhering strictly to the prescribed layering rules can make even minor changes more cumbersome than necessary. After all, software development should aim to be adaptable — true to the spirit of “SOFTware.”</p><h3>Useful Links</h3><ul><li>The iOS App, Football Gather — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather">GitHub Repo Link</a></li><li>The web server application made in Vapor — <a href="https://github.com/radude89/footballgather-ws">GitHub Repo Link</a></li><li>Vapor 3 Backend APIs <a href="https://radu-ionut-dan.medium.com/using-vapor-and-fluent-to-create-a-rest-api-5f9a0dcffc7b">article link</a></li><li>Migrating to Vapor 4 <a href="https://radu-ionut-dan.medium.com/migrating-to-vapor-4-53a821c29203">article link</a></li><li>Model View Controller (MVC) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVC">GitHub Repo Link</a> and <a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">article link</a></li><li>Model View ViewModel (MVVM) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVVM">GitHub Repo Link</a> and <a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">article link</a></li><li>Model View Presenter (MVP) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">article link</a></li><li>Coordinator Pattern — MVP with Coordinators (MVP-C) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP-C">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">article link</a></li><li>View Interactor Presenter Entity Router (VIPER) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIPER">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">article link</a></li><li>View Interactor Presenter (VIP) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIP">GitHub Repo link</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=59ebdae86e84" width="1" height="1" alt=""><hr><p><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">Battle of the iOS Architecture Patterns: View Interactor Presenter (VIP)</a> was originally published in <a href="https://medium.com/geekculture">Geek Culture</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Battle of the iOS Architecture Patterns: View Interactor Presenter Entity Router (VIPER)]]></title>
            <link>https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960?source=rss-dee343eb346a------2</link>
            <guid isPermaLink="false">https://medium.com/p/8f76f1bdc960</guid>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[viper-architecture]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Radu Dan]]></dc:creator>
            <pubDate>Sat, 17 Jul 2021 09:07:03 GMT</pubDate>
            <atom:updated>2025-01-17T21:51:52.096Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zgBB7ut1sPe8Nd3FBM_hpw.png" /><figcaption>Architecture Series — View Interactor Presenter Entity Router (VIPER)</figcaption></figure><h3>Motivation</h3><p>Before diving into iOS app development, it’s crucial to carefully consider the project’s architecture. We need to thoughtfully plan how different pieces of code will fit together, ensuring they remain comprehensible not just today, but months or years later when we need to revisit and modify the codebase. Moreover, a well-structured project helps establish a shared technical vocabulary among team members, making collaboration more efficient.</p><p>This article kicks off an exciting series where we’ll explore different architectural approaches by building the same application using various patterns. Throughout the series, we’ll analyze practical aspects like build times and implementation complexity, weigh the pros and cons of each pattern, and most importantly, examine real, production-ready code implementations. This hands-on approach will help you make informed decisions about which architecture best suits your project needs.</p><h3>Architecture Series Articles</h3><ul><li><a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">Model View Controller (MVC)</a></li><li><a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">Model View ViewModel (MVVM)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">Model View Presenter (MVP)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">Model View Presenter with Coordinators (MVP-C)</a></li><li><strong>View Interactor Presenter Entity Router (VIPER) — Current Article</strong></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">View Interactor Presenter (VIP)</a></li></ul><p>If you’re eager to explore the implementation details directly, you can find the complete source code in our open-source repository <a href="https://github.com/radude89/footballgather-ios">here</a>.</p><h3>Why Your iOS App Needs a Solid Architecture Pattern</h3><p>The cornerstone of any successful iOS application is maintainability. A well-architected app clearly defines boundaries — you know exactly where view logic belongs, what responsibilities each view controller has, and which components handle business logic. This clarity isn’t just for you; it’s essential for your entire development team to understand and maintain these boundaries consistently.</p><p>Here are the key benefits of implementing a robust architecture pattern:</p><ul><li><strong>Maintainability</strong>: Makes code easier to update and modify over time</li><li><strong>Testability</strong>: Facilitates comprehensive testing of business logic through clear separation of concerns</li><li><strong>Team Collaboration</strong>: Creates a shared technical vocabulary and understanding among team members</li><li><strong>Clean Separation</strong>: Ensures each component has clear, single responsibilities</li><li><strong>Bug Reduction</strong>: Minimizes errors through better organization and clearer interfaces between components</li></ul><h3>Project Requirements Overview</h3><p><strong>Given</strong> a medium-sized iOS application consisting of 6–7 screens, we’ll demonstrate how to implement it using the most popular architectural patterns in the iOS ecosystem: MVC (Model-View-Controller), MVVM (Model-View-ViewModel), MVP (Model-View-Presenter), VIPER (View-Interactor-Presenter-Entity-Router), VIP (Clean Swift), and the Coordinator pattern. Each implementation will showcase the pattern’s strengths and potential challenges.</p><p>Our demo application, <strong>Football Gather</strong>, is designed to help friends organize and track their casual football matches. It’s complex enough to demonstrate real-world architectural challenges while remaining simple enough to clearly illustrate different patterns.</p><h3>Core Features and Functionality</h3><ul><li>Player Management: Add and maintain a roster of players in the application</li><li>Team Assignment: Flexibly organize players into different teams for each match</li><li>Player Customization: Edit player details and preferences</li><li>Match Management: Set and control countdown timers for match duration</li></ul><h3>Screen Mockups</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dZU-rPWOrSE_N7Se.png" /></figure><h3>Backend</h3><p>The application is supported by a web backend developed using the Vapor framework, a popular choice for building modern REST APIs in Swift. You can explore the details of this implementation in our article <a href="https://www.radude89.com/blog/vapor.html">here</a>, which covers the fundamentals of building REST APIs with Vapor and Fluent in Swift. Additionally, we have documented the transition from Vapor 3 to Vapor 4, highlighting the enhancements and new features in our article <a href="https://www.radude89.com/blog/migrate-to-vapor4.html">here</a>.</p><h3>Dude, where’s my VIPER?</h3><p><strong>VIPER</strong> stands for View-Interactor-Presenter-Entity-Router.</p><p>We saw in MVP what the <strong>Presenter</strong> layer is and what it does. This concept applies as well for VIPER, but has been enhanced with a new responsibility, to get data from the <strong>Interactor</strong> and based on the rules, it will update / configure the <strong>View</strong>.</p><h3>View</h3><p>Must be as dumb as possible. It forwards all events to the <strong>Presenter</strong> and mostly should do what the <strong>Presenter</strong> tells it to do, being passive.</p><h3>Interactor</h3><p>A new layer has been introduced, and in here we should put everything that has to do with the business rules and logic.</p><h3>Presenter</h3><p>Has the responsibility to get data from the <strong>Interactor</strong>, based on the user’s actions, and then handle the <strong>View</strong> updates.</p><h3>Entity</h3><p>Is the <strong>Model</strong> layer and is used to encapsulate data.</p><h3>Router</h3><p>Holds all navigation logic for our application. It looks more like a <strong>Coordinator</strong>, without the business logic.</p><h3>Communication</h3><p>When something happens in the view layer, for example when the user initiates an action, it is communicated to the <strong>Presenter</strong>.</p><p>The <strong>Presenter</strong> asks the <strong>Interactor</strong> for the data needed by the user. The <strong>Interactor</strong> provides the data.</p><p>The <strong>Presenter</strong> applies the needed UI transformation to display that data.</p><p>When the model / data has been changed, the <strong>Interactor</strong> will inform the <strong>Presenter</strong>.</p><p>The <strong>Presenter</strong> will configure or refresh the <strong>View</strong> based on the data it received.</p><p>When users navigate through different screens within the app or take a different route that will change the flow, the View will communicate it to the <strong>Presenter</strong>.</p><p>The <strong>Presenter</strong> will notify the <strong>Router</strong> to load the new screen or load the new flow (e.g. pushing a new view controller).</p><h3>Extended VIPER</h3><p>There are a few concepts that are commonly used with VIPER architecture pattern.</p><h4>Modules</h4><p>Is a good idea to separate the VIPER layers creation from the Router and introduce a new handler for module assembly. This is done most likely with a Factory method pattern.</p><pre>/// Defines the structure for the AppModule protocol, which requires an assemble method that returns an optional UIViewController.<br>protocol AppModule {<br>    func assemble() -&gt; UIViewController?<br>}<br><br>/// Defines the ModuleFactoryProtocol with methods to create specific modules like Login and PlayerList.<br>protocol ModuleFactoryProtocol {<br>    func makeLogin(using navigationController: UINavigationController) -&gt; LoginModule<br>    func makePlayerList(using navigationController: UINavigationController) -&gt; PlayerListModule<br>}</pre><p>And the concrete implementation for our app:</p><pre>/// ModuleFactory struct implements the ModuleFactoryProtocol, providing concrete methods to create modules.<br>struct ModuleFactory: ModuleFactoryProtocol {<br>    /// Creates the Login module with a provided or default navigation controller.<br>    func makeLogin(using navigationController: UINavigationController = UINavigationController()) -&gt; LoginModule {<br>        let router = LoginRouter(navigationController: navigationController, moduleFactory: self)<br>        let view: LoginViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        return LoginModule(view: view, router: router)<br>    }<br><br>    /// Creates the PlayerList module with a provided or default navigation controller.<br>    func makePlayerList(using navigationController: UINavigationController = UINavigationController()) -&gt; PlayerListModule {<br>        let router = PlayerListRouter(navigationController: navigationController, moduleFactory: self)<br>        let view: PlayerListViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        return PlayerListModule(view: view, router: router)<br>    }<br>}</pre><p>We will see later more source code.</p><h4>TDD</h4><p>This approach does a good job from a Clean Code perspective, and you develop the layers to have a good separation of concerns and follow the SOLID principles better.</p><p>So, TDD is easy to achieve using VIPER.</p><ul><li>The modules are decoupled.</li><li>There is a clear separation of concerns.</li><li>The modules are are neat and clean from a coding perspective.</li></ul><h4>Code generation tool</h4><p>As we add more modules, flows and functionality to our application, we will discover that we write a lot of code and most of it is repetitive.</p><p>There is a good idea to have a code generator tool for your VIPER modules.</p><h3>Solving the back problem</h3><p>We saw that when applying the <strong>Coordinator</strong> pattern we had a problem when navigating back in the stack, to a specific view controller.<br>In this case, we need to think of a way if in our app we need to go back or send data between different VIPER modules.</p><p>This problem can be easily solved with <strong>Delegation</strong>.</p><p>For example:</p><pre>protocol PlayerDetailsDelegate: AnyObject {<br>    func didUpdatePlayer(_ player: Player)<br>}<br><br>/// This extension makes the Presenter the delegate of PlayerDetailsPresenter.<br>/// This allows refreshing the UI when a player is updated.<br>extension PlayerListPresenter: PlayerDetailsDelegate {<br>    func didUpdatePlayer(_ player: Player) {<br>        viewState = .list<br>        configureView()<br>        view?.reloadData()<br>    }<br>}</pre><p>More practical examples we are going to see in the section Applying to our code.</p><h3>When to use VIPER</h3><p>VIPER should be used when you have some knowledge about Swift and iOS programming or you have experienced or more senior developers within your team.</p><p>If you are part of a small project, that will not scale, then VIPER might be too much. MVC should work just fine.</p><p>Use it when you are more interested in modularising and unit test the app giving you a high code coverage.<br>Don’t use it when you are a beginner or you don’t have that much experience into iOS development.<br>Be prepared to write more code.</p><p>From my point of view, VIPER is great and I really like how clean the code looks. Is easy to test, my classes are decoupled and the code is indeed <strong>SOLID</strong>.</p><p>For our app, we separated the <strong>View</strong> layer into two components: <strong>ViewController</strong> and the actual <strong>View</strong>.<br>The <strong>ViewController</strong> acts as a <strong>Coordinator</strong> / <strong>Router</strong> and holds a reference to the view, usually set as an IBOutlet.</p><h4>Advantages</h4><ul><li>The code is clean, SRP is at its core.</li><li>Unit tests are easy to write.</li><li>The code is decoupled.</li><li>Less bugs, especially if you are using TDD.</li><li>Very useful for complex projects, where it simplifies the business logic.</li><li>The modules can be reusable.</li><li>New features are easy to add.</li></ul><h4>Disadvantages</h4><ul><li>You may write a lot of boilerplate code.</li><li>Is not great for small apps.</li><li>You end up with a big codebase and a lot of classes.</li><li>Some of the components might be redundant based on your app use cases.</li><li>App startup will slightly increase.</li></ul><h3>Applying to our code</h3><p>There will be major changes to the app by applying VIPER.</p><p>We decided to not keep two separate layers for <strong>View</strong> and <strong>ViewController</strong>, because one of these layer will become very light and it didn’t serve much purpose.</p><p>All coordinators will be removed.</p><p>First, we start by creating an AppLoader that will load the first module, <strong>Login</strong>.</p><pre>struct AppLoader {<br>    private let window: UIWindow<br>    private let navigationController: UINavigationController<br>    private let moduleFactory: ModuleFactoryProtocol<br><br>    init(window: UIWindow = UIWindow(frame: UIScreen.main.bounds),<br>          navigationController: UINavigationController = UINavigationController(),<br>          moduleFactory: ModuleFactoryProtocol = ModuleFactory()) {<br>        self.window = window<br>        self.navigationController = navigationController<br>        self.moduleFactory = moduleFactory<br>    }<br><br>    /// This function is similar to the one we had for Coordinators, start().<br>    func build() {<br>        let module = moduleFactory.makeLogin(using: navigationController)<br>        let viewController = module.assemble()<br>        setRootViewController(viewController)<br>    }<br><br>    private func setRootViewController(_ viewController: UIViewController?) {<br>        window.rootViewController = navigationController<br>        if let viewController = viewController {<br>            navigationController.pushViewController(viewController, animated: true)<br>        }<br>        window.makeKeyAndVisible()<br>    }<br>}</pre><p>We allocate AppLoader in AppDelegate and call the function build() when the app did finish launching.</p><pre>class AppDelegate: UIResponder, UIApplicationDelegate {<br><br>    private lazy var loader = AppLoader()<br>    <br>    func application(<br>      _ application: UIApplication,<br>      didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?<br>    ) -&gt; Bool {<br>        loader.build()<br>        return true<br>    }<br>    ..<br>}</pre><p>We saw earlier how we use ModuleFactory to create VIPER modules. We provide an interface for all modules that require assembly in our app.</p><pre>protocol ModuleFactoryProtocol {<br>    func makeLogin(using navigationController: UINavigationController) -&gt; LoginModule<br>    func makePlayerList(using navigationController: UINavigationController) -&gt; PlayerListModule<br>    func makePlayerDetails(using navigationController: UINavigationController,<br>                            for player: PlayerResponseModel,<br>                            delegate: PlayerDetailDelegate) -&gt; PlayerDetailModule<br>    func makePlayerEdit(using navigationController: UINavigationController,<br>                        for playerEditable: PlayerEditable,<br>                        delegate: PlayerEditDelegate) -&gt; PlayerEditModule<br>    func makePlayerAdd(using navigationController: UINavigationController, delegate: PlayerAddDelegate) -&gt; PlayerAddModule<br>    func makeConfirmPlayers(using navigationController: UINavigationController,<br>                            playersDictionary: [TeamSection: [PlayerResponseModel]],<br>                            delegate: ConfirmPlayersDelegate) -&gt; ConfirmPlayersModule<br>    func makeGather(using navigationController: UINavigationController,<br>                    gather: GatherModel,<br>                    delegate: GatherDelegate) -&gt; GatherModule<br>}</pre><p>We have a struct ModuleFactory that is the concrete implementation of the above protocol.</p><pre>struct ModuleFactory: ModuleFactoryProtocol {<br>    func makeLogin(using navigationController: UINavigationController = UINavigationController()) -&gt; LoginModule {<br>        let router = LoginRouter(navigationController: navigationController, moduleFactory: self)<br>        let view: LoginViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        return LoginModule(view: view, router: router)<br>    }<br>    /// other functions<br>    …<br>}</pre><p>Let’s see how <strong>LoginModule</strong> is created.</p><pre>final class LoginModule {<br><br>    /// Set the dependencies<br>    private var view: LoginViewProtocol<br>    private var router: LoginRouterProtocol<br>    private var interactor: LoginInteractorProtocol<br>    private var presenter: LoginPresenterProtocol<br><br>    /// Optionally, provide default implementation for your protocols with concrete classes<br>    init(view: LoginViewProtocol = LoginViewController(),<br>          router: LoginRouterProtocol = LoginRouter(),<br>          interactor: LoginInteractorProtocol = LoginInteractor(),<br>          presenter: LoginPresenterProtocol = LoginPresenter()) {<br>        self.view = view<br>        self.router = router<br>        self.interactor = interactor<br>        self.presenter = presenter<br>    }<br>}<br><br>/// Reference your layers<br>extension LoginModule: AppModule {<br>    func assemble() -&gt; UIViewController? {<br>        presenter.view = view<br>        presenter.interactor = interactor<br>        presenter.router = router<br>        interactor.presenter = presenter<br>        view.presenter = presenter<br>        return view as? UIViewController<br>    }<br>}</pre><p>Every module will have a function assemble() that is needed when implementing the AppModule protocol.</p><p>In here, we create the references between the VIPER layers:</p><ul><li>We set the view to the presenter (weak link).</li><li><strong>Presenter</strong> holds a strong reference to the <strong>Interactor</strong>.</li><li><strong>Presenter</strong> holds a strong reference to the <strong>Router</strong>.</li><li><strong>Interactor</strong> holds a weak reference to the <strong>Presenter</strong>.</li><li>Our <strong>View</strong> holds a strong reference to the <strong>Presenter</strong>.</li></ul><p>We set the weak references to avoid, of course, retain cycles which can cause memory leaks.</p><p>Every VIPER module within our app is assembled in the same way.</p><p>LoginRouter has a simple job: present the players after the user logged in.</p><pre>final class LoginRouter {<br><br>    private let navigationController: UINavigationController<br>    private let moduleFactory: ModuleFactoryProtocol<br><br>    // We inject the module factory so we can create and assemble the next screen module (PlayerList).<br>    init(navigationController: UINavigationController = UINavigationController(),<br>         moduleFactory: ModuleFactoryProtocol = ModuleFactory()) {<br>        self.navigationController = navigationController<br>        self.moduleFactory = moduleFactory<br>    }<br>}<br><br>extension LoginRouter: LoginRouterProtocol {<br>    func showPlayerList() {<br>        let module = moduleFactory.makePlayerList(using: navigationController)<br>        if let viewController = module.assemble() {<br>            navigationController.pushViewController(viewController, animated: true)<br>        }<br>    }<br>}</pre><p>One important aspect that we missed when applying MVP to our code, was that we didn’t made our <strong>View</strong> passive. The <strong>Presenter</strong> acted more like a <strong>ViewModel</strong> in some cases.</p><p>Let’s correct that and make the <strong>View</strong> as passive and dumb as we can.</p><p>Another thing that we did, was to split the LoginViewProtocol into multiple small protocols, addressing the specific need:</p><pre>typealias LoginViewProtocol = LoginViewable &amp; Loadable &amp; LoginViewConfigurable &amp; ErrorHandler<br><br>protocol LoginViewable: AnyObject {<br>    var presenter: LoginPresenterProtocol { get set }<br>}<br><br>protocol LoginViewConfigurable: AnyObject {<br>    var rememberMeIsOn: Bool { get }<br>    var usernameText: String? { get }<br>    var passwordText: String? { get }<br><br>    func setRememberMeSwitch(isOn: Bool)<br>    func setUsername(_ username: String?)<br>}</pre><p>We combined all of them by using protocol composition and named them with a typealias. We use the same approach for all of our VIPER protocols.</p><p>The LoginViewController is described below:</p><pre>final class LoginViewController: UIViewController, LoginViewable {<br><br>    // MARK: - Properties<br>    @IBOutlet weak var usernameTextField: UITextField!<br>    @IBOutlet weak var passwordTextField: UITextField!<br>    @IBOutlet weak var rememberMeSwitch: UISwitch!<br><br>    lazy var loadingView = LoadingView.initToView(view)<br><br>    // We can remove the default implementation of LoginPresenter() and force-unwrap the presenter in the protocol definition. We used this approach for some modules.<br>    var presenter: LoginPresenterProtocol = LoginPresenter()<br><br>    // MARK: - View life cycle<br>    override func viewDidLoad() {<br>        super.viewDidLoad()<br>        presenter.viewDidLoad()<br>    }<br><br>    // MARK: - IBActions<br><br>    @IBAction private func login(_ sender: Any) {<br>        presenter.performLogin()<br>    }<br><br>    @IBAction private func register(_ sender: Any) {<br>        presenter.performRegister()<br>    }<br>}<br><br>extension LoginViewController: LoginViewConfigurable {<br>    // UIKit is not allowed to be referenced in the Presenter. We expose the value of our outlets by using abstraction.<br>    var rememberMeIsOn: Bool { rememberMeSwitch.isOn }<br>    var usernameText: String? { usernameTextField.text }<br>    var passwordText: String? { passwordTextField.text }<br><br>    func setRememberMeSwitch(isOn: Bool) {<br>        rememberMeSwitch.isOn = isOn<br>    }<br><br>    func setUsername(_ username: String?) {<br>        usernameTextField.text = username<br>    }<br>}<br><br>extension LoginViewController: Loadable {}<br><br>extension LoginViewController: ErrorHandler {}</pre><p>Loadable is the same helper protocol that we used in our previous versions of the codebase. It simply shows and hides a loading view, which comes in handy when doing some Network requests. It has a default implementation for classes of type UIView and UIViewController (example: extension Loadable where Self: UIViewController).</p><p>ErrorHandler is a new helper protocol that has one method:</p><pre>protocol ErrorHandler {<br>    func handleError(title: String, message: String)<br>}<br><br>extension ErrorHandler where Self: UIViewController {<br>    func handleError(title: String, message: String) {<br>        AlertHelper.present(in: self, title: title, message: message)<br>    }<br>}</pre><p>The default implementation uses the static method from AlertHelper to present an alert controller. We use it for displaying the Network errors.</p><p>We continue with the <strong>Presenter</strong> layer below:</p><pre>final class LoginPresenter: LoginPresentable {<br><br>    // MARK: - Properties<br>    weak var view: LoginViewProtocol?<br>    var interactor: LoginInteractorProtocol<br>    var router: LoginRouterProtocol<br><br>    // MARK: - Public API<br>    init(view: LoginViewProtocol? = nil,<br>         interactor: LoginInteractorProtocol = LoginInteractor(),<br>         router: LoginRouterProtocol = LoginRouter()) {<br>        self.view = view<br>        self.interactor = interactor<br>        self.router = router<br>    }<br>}</pre><p>We set our dependencies to be injected via the initialiser. Now, the presenter has two new dependencies: <strong>Interactor</strong> and <strong>Router</strong>.</p><p>After our <strong>ViewController</strong> finished to load the view, we notify the <strong>Presenter</strong>. We want to make the <strong>View</strong> more passive, so we let the <strong>Presenter</strong> to specify the <strong>View</strong> how to configure its UI elements with the information that we get from the <strong>Interactor</strong>:</p><pre>extension LoginPresenter: LoginPresenterViewConfiguration {<br>    func viewDidLoad() {<br>        // Fetch the UserDefaults and Keychain values by asking the Interactor. Configure the UI elements based on the values we got.<br>        let rememberUsername = interactor.rememberUsername<br><br>        view?.setRememberMeSwitch(isOn: rememberUsername)<br><br>        if rememberUsername {<br>            view?.setUsername(interactor.username)<br>        }<br>    }<br>}</pre><p>The service API calls to login and register are similar:</p><pre>extension LoginPresenter: LoginPresenterServiceInteractable {<br>    func performLogin() {<br>        guard validateCredentials() else { return }<br><br>        view?.showLoadingView()<br><br>        interactor.login(username: username!, password: password!)<br>    }<br><br>    func performRegister() {<br>        guard validateCredentials() else { return }<br>        view?.showLoadingView()<br>        interactor.register(username: username!, password: password!)<br>    }<br><br>    private func validateCredentials() -&gt; Bool {<br>        guard credentialsAreValid else {<br>            view?.handleError(title: &quot;Error&quot;, message: &quot;Both fields are mandatory.&quot;)<br>            return false<br>        }<br><br>        return true<br>    }<br><br>    private var credentialsAreValid: Bool {<br>        username?.isEmpty == false &amp;&amp; password?.isEmpty == false<br>    }<br><br>    private var username: String? {<br>        view?.usernameText<br>    }<br><br>    private var password: String? {<br>        view?.passwordText<br>    }<br>}</pre><p>When the API calls are finished, the <strong>Interactor</strong> calls the following methods from the <strong>Presenter</strong>:</p><pre>// MARK: - Service Handler<br>extension LoginPresenter: LoginPresenterServiceHandler {<br>    func serviceFailedWithError(_ error: Error) {<br>        view?.hideLoadingView()<br>        view?.handleError(title: &quot;Error&quot;, message: String(describing: error))<br>    }<br><br>    func didLogin() {<br>        handleAuthCompletion()<br>    }<br><br>    func didRegister() {<br>        handleAuthCompletion()<br>    }<br><br>    private func handleAuthCompletion() {<br>        storeUsernameAndRememberMe()<br>        view?.hideLoadingView()<br>        router.showPlayerList()<br>    }<br><br>    private func storeUsernameAndRememberMe() {<br>        let rememberMe = view?.rememberMeIsOn ?? true<br>        if rememberMe {<br>            interactor.setUsername(view?.usernameText)<br>        } else {<br>            interactor.setUsername(nil)<br>        }<br>    }<br>}</pre><p>The <strong>Interactor</strong> now holds the business logic:</p><pre>final class LoginInteractor: LoginInteractable {<br><br>    weak var presenter: LoginPresenterProtocol?<br><br>    private let loginService: LoginService<br>    private let usersService: StandardNetworkService<br>    private let userDefaults: FootballGatherUserDefaults<br>    private let keychain: FootbalGatherKeychain<br><br>    init(loginService: LoginService = LoginService(),<br>         usersService: StandardNetworkService = StandardNetworkService(resourcePath: &quot;/api/users&quot;),<br>         userDefaults: FootballGatherUserDefaults = .shared,<br>         keychain: FootbalGatherKeychain = .shared) {<br>        self.loginService = loginService<br>        self.usersService = usersService<br>        self.userDefaults = userDefaults<br>        self.keychain = keychain<br>    }<br>}</pre><p>We expose in our Public API the actual values for rememberMe and the username:</p><pre>// MARK: - Credentials handler<br>extension LoginInteractor: LoginInteractorCredentialsHandler {<br><br>    var rememberUsername: Bool { userDefaults.rememberUsername ?? true }<br>    var username: String? { keychain.username }<br><br>    func setRememberUsername(_ value: Bool) {<br>        userDefaults.rememberUsername = value<br>    }<br><br>    func setUsername(_ username: String?) {<br>        keychain.username = username<br>    }<br>}</pre><p>The service handlers are lighter than in previous architecture patterns:</p><pre>// MARK: - Services<br>extension LoginInteractor: LoginInteractorServiceRequester {<br>    func login(username: String, password: String) {<br>        let requestModel = UserRequestModel(username: username, password: password)<br>        loginService.login(user: requestModel) { [weak self] result in<br>            DispatchQueue.main.async {<br>                switch result {<br>                case .failure(let error):<br>                    self?.presenter?.serviceFailedWithError(error)<br><br>                case .success(_):<br>                    self?.presenter?.didLogin()<br>                }<br>            }<br>        }<br>    }<br><br>    func register(username: String, password: String) {<br>        guard let hashedPasssword = Crypto.hash(message: password) else {<br>            fatalError(&quot;Unable to hash password&quot;)<br>        }<br><br>        let requestModel = UserRequestModel(username: username, password: hashedPasssword)<br>        usersService.create(requestModel) { [weak self] result in<br>            DispatchQueue.main.async {<br>                switch result {<br>                case .failure(let error):<br>                    self?.presenter?.serviceFailedWithError(error)<br>                case .success(let resourceId):<br>                    print(&quot;Created user: \(resourceId)&quot;)<br>                    self?.presenter?.didRegister()<br>                }<br>            }<br>        }<br>    }<br>}</pre><p>When editing a player, we use delegation for refreshing the list of the players from the <strong>PlayerList</strong> module.</p><pre>struct ModuleFactory: ModuleFactoryProtocol {<br>    func makePlayerDetails(using navigationController: UINavigationController = UINavigationController(),<br>                           for player: PlayerResponseModel,<br>                           delegate: PlayerDetailDelegate) -&gt; PlayerDetailModule {<br>        let router = PlayerDetailRouter(navigationController: navigationController, moduleFactory: self)<br>        let view: PlayerDetailViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        let interactor = PlayerDetailInteractor(player: player)<br>        let presenter = PlayerDetailPresenter(interactor: interactor, delegate: delegate)<br><br>        return PlayerDetailModule(view: view, router: router, interactor: interactor, presenter: presenter)<br>    }<br><br>    func makePlayerEdit(using navigationController: UINavigationController = UINavigationController(),<br>                        for playerEditable: PlayerEditable,<br>                        delegate: PlayerEditDelegate) -&gt; PlayerEditModule {<br>        let router = PlayerEditRouter(navigationController: navigationController, moduleFactory: self)<br>        let view: PlayerEditViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        let interactor = PlayerEditInteractor(playerEditable: playerEditable)<br>        let presenter = PlayerEditPresenter(interactor: interactor, delegate: delegate)<br>        return PlayerEditModule(view: view, router: router, interactor: interactor, presenter: presenter)<br>    }<br>}</pre><h3>Navigating to Edit screen</h3><p>We show PlayerDetailsView by calling the router from PlayerListPresenter:</p><pre>func selectRow(at index: Int) {<br>    guard playersCollectionIsEmpty == false else {<br>        return<br>    }<br><br>    if isInListViewMode {<br>        let player = interactor.players[index]<br>        showDetailsView(for: player)<br>    } else {<br>        toggleRow(at: index)<br>        updateSelectedRows(at: index)<br>        reloadViewAfterRowSelection(at: index)<br>    }<br>}<br><br>private func showDetailsView(for player: PlayerResponseModel) {<br>    router.showDetails(for: player, delegate: self)<br>}</pre><p>PlayerListRouter is shown below:</p><pre>extension PlayerListRouter: PlayerListRouterProtocol {<br>    func showDetails(for player: PlayerResponseModel, delegate: PlayerDetailDelegate) {<br>        let module = moduleFactory.makePlayerDetails(using: navigationController, for: player, delegate: delegate)<br><br>        if let viewController = module.assemble() {<br>            navigationController.pushViewController(viewController, animated: true)<br>        }<br>    }<br>}</pre><p>Now, we use the same approach from <strong>Detail</strong> screen to <strong>Edit</strong> screen:</p><pre>func selectRow(at indexPath: IndexPath) {<br>    let player = interactor.player<br>    let rowDetails = sections[indexPath.section].rows[indexPath.row]<br>    let items = self.items(for: rowDetails.editableField)<br>    let selectedItemIndex = items.firstIndex(of: rowDetails.value.lowercased())<br>    let editablePlayerDetails = PlayerEditable(player: player,<br>                                                items: items,<br>                                                selectedItemIndex: selectedItemIndex,<br>                                                rowDetails: rowDetails)<br><br>    router.showEditView(with: editablePlayerDetails, delegate: self)<br>}</pre><p>And the router:</p><pre>extension PlayerDetailRouter: PlayerDetailRouterProtocol {<br>    func showEditView(with editablePlayerDetails: PlayerEditable, delegate: PlayerEditDelegate) {<br>        let module = moduleFactory.makePlayerEdit(using: navigationController, for: editablePlayerDetails, delegate: delegate)<br><br>        if let viewController = module.assemble() {<br>            navigationController.pushViewController(viewController, animated: true)<br>        }<br>    }<br>}</pre><h3>Navigating back to the List screen</h3><p>When the user confirms the changes to a player, we call our presenter delegate.</p><pre>extension PlayerEditPresenter: PlayerEditPresenterServiceHandler {<br>    func playerWasUpdated() {<br>        view?.hideLoadingView()<br>        delegate?.didUpdate(player: interactor.playerEditable.player)<br>        router.dismissEditView()<br>    }<br>}<br><br>// MARK: - PlayerEditDelegate<br>extension PlayerDetailPresenter: PlayerEditDelegate {<br>    func didUpdate(player: PlayerResponseModel) {<br>        interactor.updatePlayer(player)<br>        delegate?.didUpdate(player: player)<br>    }<br>}</pre><p>The delegate is PlayerDetailsPresenter:</p><p>Finally, we call the PlayerDetailDelegate (assigned to PlayerListPresenter) and refresh the list of players:</p><pre>// MARK: - PlayerEditDelegate<br>extension PlayerListPresenter: PlayerDetailDelegate {<br>    func didUpdate(player: PlayerResponseModel) {<br>        interactor.updatePlayer(player)<br>    }<br>}</pre><p>We follow the same approach for <strong>Confirm</strong> and <strong>Add</strong> modules:</p><pre>func confirmOrAddPlayers() {<br>    if isInListViewMode {<br>        showAddPlayerView()<br>    } else {<br>        showConfirmPlayersView()<br>    }<br>}<br><br>private var isInListViewMode: Bool {<br>    viewState == .list<br>}<br><br>private func showAddPlayerView() {<br>    router.showAddPlayer(delegate: self)<br>}<br><br>private func showConfirmPlayersView() {<br>    router.showConfirmPlayers(with: interactor.selectedPlayers(atRows: selectedRows), delegate: self)<br>}</pre><p>The <strong>Router</strong> class is presented below:</p><pre>extension PlayerListRouter: PlayerListRouterProtocol {<br>    func showAddPlayer(delegate: PlayerAddDelegate) {<br>        let module = moduleFactory.makePlayerAdd(using: navigationController, delegate: delegate)<br><br>        if let viewController = module.assemble() {<br>            navigationController.pushViewController(viewController, animated: true)<br>        }<br>    }<br><br>    func showConfirmPlayers(with playersDictionary: [TeamSection: [PlayerResponseModel]], delegate: ConfirmPlayersDelegate) {<br>        let module = moduleFactory.makeConfirmPlayers(using: navigationController, playersDictionary: playersDictionary, delegate: delegate)<br><br>        if let viewController = module.assemble() {<br>            navigationController.pushViewController(viewController, animated: true)<br>        }<br>    }<br>}</pre><p>Implementing the service handler in PlayerAddPresenter:</p><pre>extension PlayerAddPresenter: PlayerAddPresenterServiceHandler {<br>    func playerWasAdded() {<br>        view?.hideLoadingView()<br>        delegate?.didAddPlayer()<br>        router.dismissAddView()<br>    }<br>}</pre><p>Finally, delegation to the list of players:</p><pre>// MARK: - PlayerAddDelegate<br>extension PlayerListPresenter: PlayerAddDelegate {<br>    func didAddPlayer() {<br>        loadPlayers()<br>    }<br>}<br><br>// MARK: - ConfirmPlayersDelegate<br>extension PlayerListPresenter: ConfirmPlayersDelegate {<br>    func didEndGather() {<br>        viewState = .list<br>        configureView()<br>        view?.reloadData()<br>    }<br>}</pre><p>In this architecture pattern, we wanted to make the <strong>View</strong> as passive as we could (this concept should be applied to MVP, too).<br>For that we created for the table rows, a CellViewPresenter:</p><pre>protocol PlayerTableViewCellPresenterProtocol: AnyObject {<br>    var view: PlayerTableViewCellProtocol? { get set }<br>    var viewState: PlayerListViewState { get set }<br>    var isSelected: Bool { get set }<br><br>    func setupView()<br>    func configure(with player: PlayerResponseModel)<br>    func toggle()<br>}</pre><p>The concrete class described below:</p><pre>final class PlayerTableViewCellPresenter: PlayerTableViewCellPresenterProtocol {<br><br>    var view: PlayerTableViewCellProtocol?<br>    var viewState: PlayerListViewState<br>    var isSelected = false<br><br>    init(view: PlayerTableViewCellProtocol? = nil, viewState: PlayerListViewState = .list) {<br>        self.view = view<br>        self.viewState = viewState<br>    }<br><br>    func setupView() {<br>        if viewState == .list {<br>            view?.setupDefaultView()<br>        } else {<br>            view?.setupViewForSelection(isSelected: isSelected)<br>        }<br>    }<br><br>    func toggle() {<br>        isSelected.toggle()<br>        if viewState == .selection {<br>            view?.setupCheckBoxImage(isSelected: isSelected)<br>        }<br>    }<br><br>    func configure(with player: PlayerResponseModel) {<br>        view?.set(nameDescription: player.name)<br>        setPositionDescription(for: player)<br>        setSkillDescription(for: player)<br>    }<br><br>    private func setPositionDescription(for player: PlayerResponseModel) {<br>        let position = player.preferredPosition?.rawValue<br>        view?.set(positionDescription: &quot;Position: \(position ?? &quot;-&quot;)&quot;)<br>    }<br><br>    private func setSkillDescription(for player: PlayerResponseModel) {<br>        let skill = player.skill?.rawValue<br>        view?.set(skillDescription: &quot;Skill: \(skill ?? &quot;-&quot;)&quot;)<br>    }<br>}</pre><p>The presenter will update the CellView:</p><pre>final class PlayerTableViewCell: UITableViewCell, PlayerTableViewCellProtocol {<br>    @IBOutlet weak var checkboxImageView: UIImageView!<br>    @IBOutlet weak var playerCellLeftConstraint: NSLayoutConstraint!<br>    @IBOutlet weak var nameLabel: UILabel!<br>    @IBOutlet weak var positionLabel: UILabel!<br>    @IBOutlet weak var skillLabel: UILabel!<br><br>    private enum Constants {<br>        static let playerContentLeftPadding: CGFloat = 10.0<br>        static let playerContentAndIconLeftPadding: CGFloat = -20.0<br>    }<br><br>    func setupDefaultView() {<br>        playerCellLeftConstraint.constant = Constants.playerContentAndIconLeftPadding<br>        setupCheckBoxImage(isSelected: false)<br>        checkboxImageView.isHidden = true<br>    }<br><br>    func setupViewForSelection(isSelected: Bool) {<br>        playerCellLeftConstraint.constant = Constants.playerContentLeftPadding<br>        checkboxImageView.isHidden = false<br>        setupCheckBoxImage(isSelected: isSelected)<br>    }<br><br>    func setupCheckBoxImage(isSelected: Bool) {<br>        let imageName = isSelected ? &quot;ticked&quot; : &quot;unticked&quot;<br>        checkboxImageView.image = UIImage(named: imageName)<br>    }<br><br>    func set(nameDescription: String) {<br>        nameLabel.text = nameDescription<br>    }<br><br>    func set(positionDescription: String) {<br>        positionLabel.text = positionDescription<br>    }<br><br>    func set(skillDescription: String) {<br>        skillLabel.text = skillDescription<br>    }<br>}</pre><p>In PlayerViewController, we have the cellForRowAt method:</p><pre>func tableView(_ tableView: UITableView, <br>    cellForRowAt indexPath: IndexPath<br>) -&gt; UITableViewCell {<br>   guard let cell: PlayerTableViewCell = tableView.dequeueReusableCell(withIdentifier: &quot;PlayerTableViewCell&quot;) as? PlayerTableViewCell else {<br>       return UITableViewCell()<br>   }<br><br>   let index = indexPath.row<br>   let cellPresenter = presenter.cellPresenter(at: index)<br>   let player = presenter.player(at: index)<br>   cellPresenter.view = cell<br>   cellPresenter.setupView()<br>   cellPresenter.configure(with: player)<br>   return cell<br>}</pre><p>Inside the <strong>Presenter</strong> we cache the existing cell presenters:</p><pre>func cellPresenter(at index: Int) -&gt; PlayerTableViewCellPresenterProtocol {<br>   if let cellPresenter = cellPresenters[index] {<br>       cellPresenter.viewState = viewState<br>       return cellPresenter<br>   }<br><br>   let cellPresenter = PlayerTableViewCellPresenter(viewState: viewState)<br>   cellPresenters[index] = cellPresenter<br>   return cellPresenter<br>}</pre><p>The rest of the code is available in the <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIPER">open-source repository</a>.</p><h3>Key Metrics</h3><h4>Lines of code — Protocols</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Cae5Lwr2O-InZ2q6KJ5aPA.png" /></figure><h4>Lines of code — View Controllers and Views</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dDCc_nVSpRRAoqUlPo7UfA.png" /></figure><h4>Lines of code — Modules</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QOYTgf7ytqNROnEQsLLv6w.png" /></figure><h4>Lines of code — Routers</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YHwU1hg7Yort7vLpMWmR6A.png" /></figure><h4>Lines of code — Presenters</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SbQZza-TyvVFG7sbLksVsw.png" /></figure><h4>Lines of code — Interactors</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3DAaadeWdpDcTGi8ZA0_4Q.png" /></figure><h4>Lines of code — Local Models</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4gv-_pM9bOKp98yAOQCGgw.png" /></figure><h4>Unit Tests</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CEBGsvIQ9-I5s5o8ues79A.png" /></figure><h4>Build Times</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yvOwwQAoQGFTg3GiqrbPhg.png" /></figure><p><em>Tests were run on an 8-Core Intel Core i9, MacBook Pro, 2019. Xcode version: 12.5.1. macOS Big Sur.</em></p><h3>Conclusion</h3><p>VIPER is an excellent architectural choice if you prioritize clean and maintainable code. It allows for strict adherence to the Single Responsibility Principle, and we can even introduce additional layers to further refine our application structure.</p><p>One of the standout benefits of VIPER is how straightforward it makes writing unit tests. Its decoupled nature simplifies testability and ensures each component is independently verifiable.</p><p>However, VIPER’s modularity comes at a cost. The architecture introduces a significant number of files, protocols, and classes. When UI changes or updates are required, multiple components often need to be modified, which can be time-consuming.</p><p>In our specific case, transitioning from MVP-C to VIPER proved to be more challenging compared to other patterns. We had to first merge the <strong>View</strong> and <strong>ViewController</strong> layers, then refactor almost every class, and finally create several new files and classes. This transformation required considerable effort and time.</p><p>On the positive side, VIPER encourages the creation of small, focused functions, with most performing a single, well-defined task. This improves readability and maintainability.</p><p>Another advantage is the use of protocol files. These abstractions decouple the modules from the main .xcodeproj, making it easier to work with static frameworks.</p><p>Our <strong>ViewControllers</strong> saw a significant reduction in size. Collectively, they now total approximately <strong>800 lines of code</strong>, which is a dramatic improvement over the <strong>1627 lines</strong> we had under MVC. This reduction highlights the benefits of delegating responsibilities to other layers.</p><p>However, VIPER introduces new layers, such as:</p><ul><li><strong>Protocols</strong> — These define abstractions for the modules, specifying only the structure of the layers.</li><li><strong>Modules</strong> — These assemble the VIPER layers and are typically part of the <strong>Router</strong>, initialized via a factory.</li><li><strong>Interactors</strong> — These handle business logic, manage network calls, and orchestrate data flow.</li></ul><p>These new layers added <strong>1903 lines of code</strong> to our project, increasing its complexity.</p><p>Writing unit tests with VIPER was an enjoyable experience. The decoupled components made it easy to test various scenarios, and we achieved <strong>100% code coverage</strong>, a noteworthy milestone.</p><p>However, one downside is the increased build times. Clearing the <strong>Derived Data</strong> folder and cleaning the build folder adds <strong>10.43 seconds</strong> to the process, which is nearly one second more than when the app used MVVM or MVC. On the bright side, this added time is a small trade-off for the potential bugs we avoid with the improved architecture.</p><p>Executing unit tests after a clean build takes around <strong>20 seconds</strong>, with a total of <strong>46 tests</strong>. The additional files, classes, and dependencies naturally contribute to longer compile times.</p><p>Fortunately, we don’t need to clean the build or wipe out the Derived Data folder every time we run unit tests. This task can be delegated to the CI server, reducing the impact on developer productivity.</p><p>In conclusion, VIPER is an excellent choice for medium to large applications that are relatively stable and primarily focused on adding new features incrementally. Its advantages in maintainability and testability make it an attractive option for complex projects.</p><p>However, it does come with some drawbacks. Firstly, the amount of boilerplate code can feel excessive, and at times, you might question the need to go through multiple layers instead of directly handling tasks in the <strong>ViewController</strong>.</p><p>Secondly, VIPER is not a good fit for small applications where simplicity and rapid development are priorities. Adding redundant files for straightforward tasks can feel unnecessary.</p><p>Finally, adopting VIPER may result in longer app compilation and startup times, which can impact the overall development experience.</p><p>Thank you for reading until the end! We hope this analysis helps you decide if VIPER is the right choice for your project.</p><h3>Useful Links</h3><ul><li>The iOS App, Football Gather — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather">GitHub Repo Link</a></li><li>The web server application made in Vapor — <a href="https://github.com/radude89/footballgather-ws">GitHub Repo Link</a></li><li>Vapor 3 Backend APIs <a href="https://radu-ionut-dan.medium.com/using-vapor-and-fluent-to-create-a-rest-api-5f9a0dcffc7b">article link</a></li><li>Migrating to Vapor 4 <a href="https://radu-ionut-dan.medium.com/migrating-to-vapor-4-53a821c29203">article link</a></li><li>Model View Controller (MVC) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVC">GitHub Repo Link</a> and <a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">article link</a></li><li>Model View ViewModel (MVVM) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVVM">GitHub Repo Link</a> and <a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">article link</a></li><li>Model View Presenter (MVP) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">article link</a></li><li>Coordinator Pattern — MVP with Coordinators (MVP-C) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP-C">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">article link</a></li><li>View Interactor Presenter Entity Router (VIPER) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIPER">GitHub Repo link</a></li><li>View Interactor Presenter (VIP) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIP">GitHub Repo link</a> and <a href="https://radu-ionut-dan.medium.com/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">article link</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8f76f1bdc960" width="1" height="1" alt=""><hr><p><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">Battle of the iOS Architecture Patterns: View Interactor Presenter Entity Router (VIPER)</a> was originally published in <a href="https://medium.com/geekculture">Geek Culture</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Battle of the iOS Architecture Patterns: Model View Presenter with Coordinators (MVP-C)]]></title>
            <link>https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36?source=rss-dee343eb346a------2</link>
            <guid isPermaLink="false">https://medium.com/p/99edf7ab8c36</guid>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[coordinator]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Radu Dan]]></dc:creator>
            <pubDate>Wed, 07 Jul 2021 12:03:16 GMT</pubDate>
            <atom:updated>2025-01-17T21:38:10.750Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HqpjX0YQVyUXCuX9J3MMxQ.png" /><figcaption>Architecture Series — Model View Presenter with Coordinators (MVP-C)</figcaption></figure><h3>Motivation</h3><p>Before diving into iOS app development, it’s crucial to carefully consider the project’s architecture. We need to thoughtfully plan how different pieces of code will fit together, ensuring they remain comprehensible not just today, but months or years later when we need to revisit and modify the codebase. Moreover, a well-structured project helps establish a shared technical vocabulary among team members, making collaboration more efficient.</p><p>This article kicks off an exciting series where we’ll explore different architectural approaches by building the same application using various patterns. Throughout the series, we’ll analyze practical aspects like build times and implementation complexity, weigh the pros and cons of each pattern, and most importantly, examine real, production-ready code implementations. This hands-on approach will help you make informed decisions about which architecture best suits your project needs.</p><h3>Architecture Series Articles</h3><ul><li><a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">Model View Controller (MVC)</a></li><li><a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">Model View ViewModel (MVVM)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">Model View Presenter (MVP)</a></li><li><strong>Model View Presenter with Coordinators (MVP-C) — Current Article</strong></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">View Interactor Presenter Entity Router (VIPER)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">View Interactor Presenter (VIP)</a></li></ul><p>If you’re eager to explore the implementation details directly, you can find the complete source code in our open-source repository <a href="https://github.com/radude89/footballgather-ios">here</a>.</p><h3>Why Your iOS App Needs a Solid Architecture Pattern</h3><p>The cornerstone of any successful iOS application is maintainability. A well-architected app clearly defines boundaries — you know exactly where view logic belongs, what responsibilities each view controller has, and which components handle business logic. This clarity isn’t just for you; it’s essential for your entire development team to understand and maintain these boundaries consistently.</p><p>Here are the key benefits of implementing a robust architecture pattern:</p><ul><li><strong>Maintainability</strong>: Makes code easier to update and modify over time</li><li><strong>Testability</strong>: Facilitates comprehensive testing of business logic through clear separation of concerns</li><li><strong>Team Collaboration</strong>: Creates a shared technical vocabulary and understanding among team members</li><li><strong>Clean Separation</strong>: Ensures each component has clear, single responsibilities</li><li><strong>Bug Reduction</strong>: Minimizes errors through better organization and clearer interfaces between components</li></ul><h3>Project Requirements Overview</h3><p><strong>Given</strong> a medium-sized iOS application consisting of 6–7 screens, we’ll demonstrate how to implement it using the most popular architectural patterns in the iOS ecosystem: MVC (Model-View-Controller), MVVM (Model-View-ViewModel), MVP (Model-View-Presenter), VIPER (View-Interactor-Presenter-Entity-Router), VIP (Clean Swift), and the Coordinator pattern. Each implementation will showcase the pattern’s strengths and potential challenges.</p><p>Our demo application, <strong>Football Gather</strong>, is designed to help friends organize and track their casual football matches. It’s complex enough to demonstrate real-world architectural challenges while remaining simple enough to clearly illustrate different patterns.</p><h3>Core Features and Functionality</h3><ul><li>Player Management: Add and maintain a roster of players in the application</li><li>Team Assignment: Flexibly organize players into different teams for each match</li><li>Player Customization: Edit player details and preferences</li><li>Match Management: Set and control countdown timers for match duration</li></ul><h3>Screen Mockups</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*b8JK9mgAQAim110p.png" /></figure><h3>Backend</h3><p>The application is supported by a web backend developed using the Vapor framework, a popular choice for building modern REST APIs in Swift. You can explore the details of this implementation in our article <a href="https://www.radude89.com/blog/vapor.html">here</a>, which covers the fundamentals of building REST APIs with Vapor and Fluent in Swift. Additionally, we have documented the transition from Vapor 3 to Vapor 4, highlighting the enhancements and new features in our article <a href="https://www.radude89.com/blog/migrate-to-vapor4.html">here</a>.</p><h3>What Are Coordinators?</h3><p>The concept of Coordinators was first introduced by <a href="https://khanlou.com/2015/01/the-coordinator/">Soroush Khanlou in 2015</a> as a solution for managing flow logic within view controllers.</p><p>As your application grows in size and complexity, you may need to reuse view controllers in various contexts. However, coupling flow logic with the view controller makes this difficult to achieve. To implement the pattern effectively, you will need a high-level coordinator that manages the application’s flow, as suggested by Soroush.</p><p>Here are a few benefits of extracting flow logic into a coordinator:</p><ul><li>View controllers can focus solely on their primary responsibilities, based on the architecture pattern you’re using in your app (e.g., binding a model to a view).</li><li>The initialization of view controllers is moved to a separate layer, reducing clutter in individual view controllers.</li></ul><p>Coordinators help solve several common problems:</p><ul><li><strong>Overstuffed app delegates:</strong> App delegates tend to become overloaded with responsibilities. By using a base app coordinator, we can move some of that logic to a more appropriate layer.</li><li><strong>Excessive responsibilities for view controllers:</strong> In architectures like MVC, view controllers often handle a variety of tasks — such as model binding, view management, data fetching, and transformation. Coordinators help alleviate this burden.</li><li><strong>Smoother flow:</strong> Navigation logic is extracted from view controllers and placed into coordinators, creating a more streamlined process.</li></ul><p>The app coordinator is typically responsible for resolving the issue of an overloaded AppDelegate.<br>Here, you can allocate the window object, create your navigation controller, and initialize the first view controller. In <a href="https://www.amazon.com/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420"><strong>Martin Fowler&#39;s &quot;Patterns of Enterprise Application Architecture&quot;</strong></a>, this is referred to as the <strong>Application Controller</strong>.</p><p>A key rule for coordinators is that each coordinator maintains an array of its child coordinators. This prevents child coordinators from being deallocated prematurely.<br>In the case of a tab bar application, each navigation controller has its own coordinator, which is managed by its parent coordinator.</p><p>In addition to managing flow logic, coordinators also take over the responsibility of handling model mutations from view controllers.</p><h4>Advantages</h4><ul><li>Each view controller becomes more isolated and focused on its specific task.</li><li>View controllers become more reusable across different parts of the app.</li><li>Every task and sub-task in the app is encapsulated in a dedicated coordinator.</li><li>Coordinators separate the logic of display-binding from side effects.</li><li>Coordinators are fully under your control, making it easier to manage and extend your app’s navigation flow.</li></ul><h3>The Back Navigation Problem</h3><p>What happens when the user navigates back in the stack? While we can control custom back buttons, what about when the user swipes right to go back?</p><p>One way to solve this problem is to maintain a reference to the coordinator within the view controller and call its didFinish method inside viewDidDisappear. This solution works for simple apps, but it becomes problematic when multiple view controllers are managed by child coordinators.</p><p><a href="https://khanlou.com/2017/05/back-buttons-and-coordinators/">As Soroush mentions</a>, we can implement the UINavigationControllerDelegate protocol to gain control over these navigation events.</p><ol><li><strong>Implement </strong><strong>UINavigationControllerDelegate in your main app coordinator:</strong><br>Focus on the navigationController:didShowViewController:animated: method, which is called after the navigation controller displays a view controller. When this event is triggered (indicating that a view controller has been popped from the stack), you can deallocate the relevant coordinators.</li><li><strong>Subclass </strong><strong>UIViewController to manage coordinators:</strong><br>In this special subclass, you can maintain a dictionary of coordinators for your view controllers:<br>private var viewControllersToChildCoordinators: [UIViewController: Coordinator] = [:]<br>Implement the UINavigationControllerDelegate within this class. When a view controller is popped and exists in the dictionary, it will be removed and deallocated.<br>The main tradeoff with this approach is that your subclassed UIViewController ends up taking on more responsibilities than desired.</li></ol><h3>Applying to our code</h3><p>We start first with defining our application coordinators:</p><pre>protocol Coordinator: AnyObject {<br>    var childCoordinators: [Coordinator] { get set }<br>    var parent: Coordinator? { get set }<br><br>    func start()<br>}</pre><p>The start function takes care of allocating the view controller and pushing it into the navigation controller stack.</p><pre>protocol Coordinatable: AnyObject {<br>    var coordinator: Coordinator? { get set }<br>}</pre><p>We define a Coordinatable project that our view controllers will implement, so they can delegate to their coordinator specific navigation tasks (such as going back).</p><p>Next, we create our main app coordinator: AppCoordinator and initialise it within the AppDelegate.</p><pre>final class AppCoordinator: NSObject, Coordinator {<br>    weak var parent: Coordinator?<br>    var childCoordinators: [Coordinator] = []<br>    <br>    private let navController: UINavigationController<br>    private let window: UIWindow<br><br>    init(navController: UINavigationController = UINavigationController(),<br>          window: UIWindow = UIWindow(frame: UIScreen.main.bounds)) {<br>        self.navController = navController<br>        self.window = window<br>    }<br><br>    func start() {<br>        navController.delegate = self<br>        window.rootViewController = navController<br>        window.makeKeyAndVisible()<br>    }<br>}</pre><p>The AppDelegate now looks like this:</p><pre>@UIApplicationMain<br>class AppDelegate: UIResponder, UIApplicationDelegate {<br><br>    private lazy var appCoordinator = AppCoordinator()<br><br>    func application(<br>        _ application: UIApplication, <br>        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?<br>    ) -&gt; Bool {<br>        appCoordinator.start()<br>        return true<br>    }<br><br>    // ...<br>}</pre><p>Our first screen is <strong>Login</strong>. We do the following adjustments so it can support coordinators:</p><pre>final class LoginViewController: UIViewController, Coordinatable {<br><br>    @IBOutlet weak var loginView: LoginView!<br><br>    weak var coordinator: Coordinator?<br><br>    private var loginCoordinator: LoginCoordinator? {<br>      coordinator as? LoginCoordinator<br>    }<br>    // ...<br>}<br><br>extension LoginViewController: LoginViewDelegate {<br>    // ...<br>    func didLogin() {<br>        loginCoordinator?.navigateToPlayerList()<br>    }<br><br>    func didRegister() {<br>        loginCoordinator?.navigateToPlayerList()<br>    }<br>}</pre><p>The LoginCoordinator is presented below:</p><pre>final class LoginCoordinator: Coordinator {<br>    weak var parent: Coordinator?<br>    var childCoordinators: [Coordinator] = []<br><br>    private let navController: UINavigationController<br><br>    init(navController: UINavigationController, parent: Coordinator? = nil) {<br>        self.navController = navController<br>        self.parent = parent<br>    }<br><br>    func start() {<br>        let viewController: LoginViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        viewController.coordinator = self<br>        navController.pushViewController(viewController, animated: true)<br>    }<br><br>    func navigateToPlayerList() {<br>        let playerListCoordinator = PlayerListCoordinator(navController: navController, parent: self)<br>        playerListCoordinator.start()<br>        childCoordinators.append(playerListCoordinator)<br>    }<br>}</pre><p>An alternative to Coordinatable, is to use delegation i.e. create a LoginViewControllerDelegate that contains the method navigateToPlayerList, and make LoginCoordinator the delegate of this class.</p><p>And the final piece for <strong>LoginScreen</strong>, is to remove the segues from the storyboard.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*b-ey7Hbrhd8kYH8c.png" /></figure><p>As we are going to instantiate all of our view controllers from the storyboard, let’s define a convenient method to do that:</p><pre>enum Storyboard: String {<br>    case main = &quot;Main&quot;<br><br>    static var defaultStoryboard: UIStoryboard {<br>        return UIStoryboard(name: Storyboard.main.rawValue, bundle: nil)<br>    }<br>}<br><br>extension UIStoryboard {<br>    func instantiateViewController(withIdentifier identifier: String = String(describing: T.self)) -&gt; T {<br>        return instantiateViewController(withIdentifier: identifier) as! T<br>    }<br>}</pre><p>We now can allocate a view controller by setting the storyboard ID in the designated storyboard and use:</p><pre>let viewController: PlayerListViewController = Storyboard.defaultStoryboard.instantiateViewController()</pre><p><strong>PlayerList</strong> screen suffers the following adjustments:</p><ul><li>We removed PlayerListTogglable, the pop functionality resides completely in the responsibilities of Coordinators.</li><li>Make it implement Coordinatable so we can have a reference to the View Controller’s coordinator.</li><li>Remove the delegate methods from PlayerDetailViewControllerDelegate, AddPlayerDelegate and PlayerListTogglable.</li><li>Enhance the public API with the methods that will be required after a player is edited (reload data), added and after a gather finishes (toggleViewState).</li></ul><pre>weak var coordinator: Coordinator?<br>private var listCoordinator: PlayerListCoordinator? { coordinator as? PlayerListCoordinator }<br><br>/// .....<br>func reloadView() {<br>    playerListView.loadPlayers()<br>}<br><br>func didEdit(player: PlayerResponseModel) {<br>    playerListView.didEdit(player: player)<br>}<br><br>func toggleViewState() {<br>    playerListView.toggleViewState()<br>}</pre><p>To navigate to different screens from <strong>PlayerList</strong> (to Add or Edit screens, for example), we created the appropriate segue identifier in the <strong>Presenter</strong> and forward it to the <strong>ViewController</strong> using the <strong>View</strong> layer. We now deprecated the use of segue identifiers, all routing will be done using <strong>Coordinators</strong>. So, let’s implement these changes:</p><pre>protocol PlayerListViewDelegate: AnyObject {<br>    func didRequestToChangeTitle(_ title: String)<br>    func addRightBarButtonItem(_ barButtonItem: UIBarButtonItem)<br>    func presentAlert(title: String, message: String)<br>    func didRequestPlayerDeletion()<br>    func viewPlayerDetails(_ player: PlayerResponseModel)<br>    func addPlayer()<br>    func confirmPlayers(with playersDictionary: [TeamSection: [PlayerResponseModel]])<br>}<br><br>@IBAction private func confirmOrAddPlayers(_ sender: Any) {<br>    if presenter.isInListViewMode {<br>        delegate?.addPlayer()<br>    } else {<br>        delegate?.confirmPlayers(with: presenter.playersDictionary)<br>    }<br>}<br><br>func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {<br>    guard !presenter.playersCollectionIsEmpty else { return }<br><br>    if presenter.isInListViewMode {<br>        let player = presenter.selectPlayerForDisplayingDetails(at: indexPath)<br>        delegate?.viewPlayerDetails(player)<br>    } else {<br>        toggleCellSelection(at: indexPath)<br>        updateViewForPlayerSelection()<br>    }<br>}</pre><h4>PlayerListCoordinator</h4><p>The coordinator implementation is presented below:</p><pre>final class PlayerListCoordinator: Coordinator {<br><br>    weak var parent: Coordinator?<br>    var childCoordinators: [Coordinator] = []<br><br>    private let navController: UINavigationController<br>    private var playerListViewController: PlayerListViewController?<br><br>    init(navController: UINavigationController, parent: Coordinator? = nil) {<br>        self.navController = navController<br>        self.parent = parent<br>    }<br><br>    func start() {<br>        let viewController: PlayerListViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        viewController.coordinator = self<br>        navController.pushViewController(viewController, animated: true)<br>    }<br><br>    func navigateToPlayerDetails(player: PlayerResponseModel) {<br>        let playerDetailCoordinator = PlayerDetailCoordinator(navController: navController, parent: self, player: player)<br>        playerDetailCoordinator.delegate = self<br>        playerDetailCoordinator.start()<br>        childCoordinators.append(playerDetailCoordinator)<br>    }<br><br>    func navigateToPlayerAddScreen() {<br>        let playerAddCoordinator = PlayerAddCoordinator(navController: navController, parent: self)<br>        playerAddCoordinator.delegate = self<br>        playerAddCoordinator.start()<br>        childCoordinators.append(playerAddCoordinator)<br>    }<br><br>    func navigateToConfirmPlayersScreen(with playersDictionary: [TeamSection: [PlayerResponseModel]]) {<br>        let confirmPlayersCoordinator = ConfirmPlayersCoordinator(navController: navController, parent: self, playersDictionary: playersDictionary)<br>        confirmPlayersCoordinator.delegate = self<br>        confirmPlayersCoordinator.start()<br>        childCoordinators.append(confirmPlayersCoordinator)<br>    }</pre><p>There are quite a few changes we had to do to <strong>PlayerEdit</strong> and <strong>PlayerDetail</strong> screens.</p><p>Firstly, we had to make them implement Coordinatable, so we can have a reference to the coordinators, same as we did for <strong>PlayerList</strong>.</p><p>In <strong>PlayerDetails</strong>, we had to make setupTitle method public, because when we edit a player and change its name, we will need to communicate this change to the <strong>ViewController</strong> so it can refresh the navigation title. The title is actually the player name.</p><p>Same thing we did to reloadData(), and created a new function updateData(player) to communicate to the <strong>View</strong> the player changes.</p><pre>func reloadData() {<br>   playerDetailView.reloadData()<br>}<br><br>func updateData(player: PlayerResponseModel) {<br>   playerDetailView.updateData(player: player)<br>}</pre><p>We use PlayerDetailViewDelegate to listen to changes that happen in the <strong>View</strong> layer:</p><pre>extension PlayerDetailViewController: PlayerDetailViewDelegate {<br>    func didRequestEditView(with viewType: PlayerEditViewType,<br>                             playerEditModel: PlayerEditModel?,<br>                             playerItemsEditModel: PlayerItemsEditModel?) {<br>        detailCoordinator?.navigateToEditScreen(viewType: viewType,<br>                                                 playerEditModel: playerEditModel,<br>                                                 playerItemsEditModel: playerItemsEditModel)<br>    }<br>}</pre><p>PlayerDetailViewDelegate has now changed the simple didRequestEditView method, into the one that you see above.<br>This is called from didSelectRow table view&#39;s delegate:</p><pre>func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {<br>   presenter.selectPlayerRow(at: indexPath)<br><br>    delegate?.didRequestEditView(<br>        with: presenter.destinationViewType,<br>        playerEditModel: presenter.playerEditModel,<br>        playerItemsEditModel: presenter.playerItemsEditModel<br>    )<br>}</pre><h4>PlayerDetailCoordinator</h4><p>Full code below:</p><pre>protocol PlayerDetailCoordinatorDelegate: AnyObject {<br>    func didEdit(player: PlayerResponseModel)<br>}<br><br>final class PlayerDetailCoordinator: Coordinator {<br>    weak var parent: Coordinator?<br>    var childCoordinators: [Coordinator] = []<br>    weak var delegate: PlayerDetailCoordinatorDelegate?<br><br>    private let navController: UINavigationController<br>    private let player: PlayerResponseModel<br>    private var detailViewController: PlayerDetailViewController?<br><br>    init(navController: UINavigationController, parent: Coordinator? = nil, player: PlayerResponseModel) {<br>        self.navController = navController<br>        self.parent = parent<br>        self.player = player<br>    }<br><br>    func start() {<br>        let viewController: PlayerDetailViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        viewController.coordinator = self<br>        viewController.player = player<br>        detailViewController = viewController<br>        navController.pushViewController(viewController, animated: true)<br>    }<br><br>    func navigateToEditScreen(viewType: PlayerEditViewType,<br>                              playerEditModel: PlayerEditModel?,<br>                              playerItemsEditModel: PlayerItemsEditModel?) {<br>        let editCoordinator = PlayerEditCoordinator(navController: navController,<br>                                                    viewType: viewType,<br>                                                    playerEditModel: playerEditModel,<br>                                                    playerItemsEditModel: playerItemsEditModel)<br>        editCoordinator.delegate = self<br>        editCoordinator.start()<br>        childCoordinators.append(editCoordinator)<br>    }<br>}<br><br>extension PlayerDetailCoordinator: PlayerEditCoordinatorDelegate {<br>    func didFinishEditing(player: PlayerResponseModel) {<br>        detailViewController?.setupTitle()<br>        detailViewController?.updateData(player: player)<br>        detailViewController?.reloadData()<br>        delegate?.didEdit(player: player)<br>    }<br>}</pre><h4>PlayerEditCoordinator</h4><p>The implementation is pretty straightforward:</p><pre>protocol PlayerEditCoordinatorDelegate: AnyObject {<br>    func didFinishEditing(player: PlayerResponseModel)<br>}<br><br>final class PlayerEditCoordinator: Coordinator {<br>    weak var parent: Coordinator?<br>    var childCoordinators: [Coordinator] = []<br>    weak var delegate: PlayerEditCoordinatorDelegate?<br><br>    private let navController: UINavigationController<br>    private let viewType: PlayerEditViewType<br>    private let playerEditModel: PlayerEditModel?<br>    private let playerItemsEditModel: PlayerItemsEditModel?<br><br>    init(navController: UINavigationController,<br>         parent: Coordinator? = nil,<br>         viewType: PlayerEditViewType,<br>         playerEditModel: PlayerEditModel?,<br>         playerItemsEditModel: PlayerItemsEditModel?) {<br>        self.navController = navController<br>        self.parent = parent<br>        self.viewType = viewType<br>        self.playerEditModel = playerEditModel<br>        self.playerItemsEditModel = playerItemsEditModel<br>    }<br><br>    func start() {<br>        let viewController: PlayerEditViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        viewController.coordinator = self<br>        viewController.viewType = viewType<br>        viewController.playerEditModel = playerEditModel<br>        viewController.playerItemsEditModel = playerItemsEditModel<br>        navController.pushViewController(viewController, animated: true)<br>    }<br><br>    func didFinishEditingPlayer(_ player: PlayerResponseModel) {<br>        delegate?.didFinishEditing(player: player)<br>        navController.popViewController(animated: true)<br>    }<br>}</pre><h4>PlayerAddCoordinator</h4><p>The add players feature is impacted a little, because it’s very simple. The coordinator looks like this:</p><pre>protocol PlayerAddCoordinatorDelegate: AnyObject {<br>    func playerWasAdded()<br>}<br><br>final class PlayerAddCoordinator: Coordinator {<br>    weak var parent: Coordinator?<br><br>    var childCoordinators: [Coordinator] = []<br>    weak var delegate: PlayerAddCoordinatorDelegate?<br><br>    private let navController: UINavigationController<br><br>    init(navController: UINavigationController, parent: Coordinator? = nil) {<br>        self.navController = navController<br>        self.parent = parent<br>    }<br><br>    func start() {<br>        let viewController: PlayerAddViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        viewController.coordinator = self<br>        navController.pushViewController(viewController, animated: true)<br>    }<br><br>    func playerWasAdded() {<br>        delegate?.playerWasAdded()<br>        navController.popViewController(animated: true)<br>    }<br>}</pre><p>In PlayerAddViewController, we modify didAddPlayer (that is called from the <strong>View</strong> layer) as presented below:</p><pre>func didAddPlayer() {<br>    // The delegate is now the addCoordinator.<br>    // We remove navigationController?.popViewController(animated: true), because we handle this in addCoordinator.<br>    addCoordinator?.playerWasAdded()<br>}</pre><h4>ConfirmPlayersCoordinator</h4><p>In <strong>ConfirmPlayers</strong>, we take in the selected players dictionary, we choose a team for them and finally we start the gather.</p><p>ConfirmPlayersCoordinator looks like this:</p><pre>protocol ConfirmPlayersCoordinatorDelegate: AnyObject {<br>    func didEndGather()<br>}<br><br>final class ConfirmPlayersCoordinator: Coordinator {<br>    weak var parent: Coordinator?<br>    var childCoordinators: [Coordinator] = []<br>    weak var delegate: ConfirmPlayersCoordinatorDelegate?<br><br>    private let navController: UINavigationController<br>    private let playersDictionary: [TeamSection: [PlayerResponseModel]]<br><br>    init(navController: UINavigationController, parent: Coordinator? = nil, playersDictionary: [TeamSection: [PlayerResponseModel]] = [:]) {<br>        self.navController = navController<br>        self.parent = parent<br>        self.playersDictionary = playersDictionary<br>    }<br><br>    func start() {<br>        let viewController: ConfirmPlayersViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        viewController.coordinator = self<br>        viewController.playersDictionary = playersDictionary<br>        navController.pushViewController(viewController, animated: true)<br>    }<br><br>    func navigateToGatherScreen(with gatherModel: GatherModel) {<br>        let gatherCoordinator = GatherCoordinator(navController: navController, parent: self, gather: gatherModel)<br>        gatherCoordinator.delegate = self<br>        gatherCoordinator.start()<br>        childCoordinators.append(gatherCoordinator)<br>    }<br>}<br><br>extension ConfirmPlayersCoordinator: GatherCoordinatorDelegate {<br>    func didEndGather() {<br>        delegate?.didEndGather()<br>    }<br>}</pre><p>In ConfirmPlayersView we changed the method didStartGather() and passed the GatherModel in the parameter list: func didStartGather(_ gather: GatherModel).</p><h4>GatherCoordinator</h4><p>Finally, GatherCoordinator is detailed below:</p><pre>protocol GatherCoordinatorDelegate: AnyObject {<br>    func didEndGather()<br>}<br><br>final class GatherCoordinator: Coordinator {<br>    weak var parent: Coordinator?<br>    var childCoordinators: [Coordinator] = []<br>    weak var delegate: GatherCoordinatorDelegate?<br><br>    private let navController: UINavigationController<br>    private let gather: GatherModel<br><br>    init(navController: UINavigationController, parent: Coordinator? = nil, gather: GatherModel) {<br>        self.navController = navController<br>        self.parent = parent<br>        self.gather = gather<br>    }<br><br>    func start() {<br>        let viewController: GatherViewController = Storyboard.defaultStoryboard.instantiateViewController()<br>        viewController.coordinator = self<br>        viewController.gatherModel = gather<br>        navController.pushViewController(viewController, animated: true)<br>    }<br><br>    /// Called from the ViewController<br>    func didEndGather() {<br>        delegate?.didEndGather()<br>    }</pre><p>GatherViewController’s didEndGather method has reduced considerably from:</p><pre>func didEndGather() {<br>    guard let playerListTogglable = navigationController?.viewControllers.first(where: { $0 is PlayerListTogglable }) as? PlayerListTogglable else {<br>        return<br>    }<br><br>    playerListTogglable.toggleViewState()<br><br>    if let playerListViewController = playerListTogglable as? UIViewController {<br>        navigationController?.popToViewController(playerListViewController, animated: true)<br>    }<br>}</pre><p>To:</p><pre>func didEndGather() {<br>    gatherCoordinator?.didEndGather()<br>}</pre><h3>Key Metrics</h3><h4>Lines of code — Coordinators</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*23hA58kGPIEX2n8MXR5GsQ.png" /></figure><h4>Lines of code — View Controllers</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RwjSDrhu79JOrn7zCBDeqg.png" /></figure><h4>Lines of code — Views</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qj-n9Ck5WvuoU-MppVcOmw.png" /></figure><h4>Lines of code — Presenters</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bNUU4DcAn1MdBUGG3d747w.png" /></figure><h4>Lines of code — Local Models</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Ha2K6OjUiH9ngPwytXwrGg.png" /></figure><h4>Unit Tests</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WRzyiflsvblo0-E6XT-Nig.png" /></figure><h4>Build Times</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1gdmzWaIFVPyxCQ0N7NhGQ.png" /></figure><p><em>Tests were run in iPhone 8 Simulator, with iOS 14.4, using Xcode 12.5.1 and on an i9 MacBook Pro 2019.</em></p><h3>Conclusion</h3><p>And just like that, we’ve completed another <strong>Architecture Series</strong> implementation article. Congratulations!</p><p>Together, we explored how to integrate the <strong>Coordinator</strong> pattern into an existing application, simplifying the structure of our <strong>View Controllers</strong>.</p><p>Our journey began with removing all segues from the storyboards, leaving some screens seemingly isolated. If we opened the Main.storyboard again, we wouldn’t easily identify how the screens are connected. While it’s possible to infer the connections based on the positioning of view controllers, this approach isn’t always intuitive or clear.</p><p>Next, we introduced new classes at the <strong>Application</strong> level to define the main coordinator, providing a more structured flow.</p><p>Then, we took each module individually, applying the new pattern. This allowed us to streamline the passing of data between screens and manage the navigation flow more effectively. No longer did we need to perform segues or rely on the <strong>Presenter</strong> for maintaining references to the <strong>Model</strong> and View Controllers. We also removed the need for back-and-forth referencing between the <strong>Presenter</strong> and <strong>View Controller</strong> when preparing for a segue to the next screen.</p><p>Finally, we applied the Delegation pattern for communication between child and parent coordinators. For instance, when adding or editing players, the changes communicate back to the player list, triggering a refresh of the screen.</p><p>The Coordinator pattern is incredibly effective, and I believe it can be implemented in any app aiming to move away from segues and storyboards, leading to cleaner, more maintainable code.</p><p>Looking at the numbers, we introduced <strong>348</strong> new lines of code, but we also reduced <strong>64</strong> lines in the view controllers, improving readability and simplifying their logic.</p><p>Interestingly, the LoginViewController saw an increase of <strong>three</strong> lines of code. But why is that?</p><p>In the case of the LoginViewController, the view controller was quite simple, with just a few one-liners for segues. After adopting the Coordinator pattern, we added two new variables:</p><pre>weak var coordinator: Coordinator?<br>private var listCoordinator: PlayerListCoordinator? { coordinator as? PlayerListCoordinator }</pre><p>The <strong>Views</strong> and <strong>Presenters</strong> generally maintained the same number of lines of code. However, there was a small increase of <strong>3</strong> lines in the PlayerDetail module, where we added three new variables to be passed to the <strong>Edit</strong> screen (see didSelectRowAt method). On the plus side, we reduced the <strong>7</strong> lines of code in the PlayerListPresenter.</p><p>As anticipated, the primary beneficiaries of this pattern are the <strong>View Controllers</strong>.</p><p>In terms of build times, there was a slight increase due to the addition of new files, which the compiler now needs to process. With a clean build and the Derived Data folder wiped, we noticed a delay of about <strong>2 seconds</strong> compared to the MVP version without Coordinators, and a delay of over <strong>5 seconds</strong> compared to the MVC version.</p><p>However, this isn’t a major issue, as we typically rely on a Continuous Integration (CI) solution, and we don’t need to wait for builds locally before seeing all tests pass.</p><p>And with that, we wrap up this chapter of the series!</p><h3>Useful Links</h3><ul><li>The iOS App, Football Gather — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather">GitHub Repo Link</a></li><li>The web server application made in Vapor — <a href="https://github.com/radude89/footballgather-ws">GitHub Repo Link</a></li><li>Vapor 3 Backend APIs <a href="https://radu-ionut-dan.medium.com/using-vapor-and-fluent-to-create-a-rest-api-5f9a0dcffc7b">article link</a></li><li>Migrating to Vapor 4 <a href="https://radu-ionut-dan.medium.com/migrating-to-vapor-4-53a821c29203">article link</a></li><li>Model View Controller (MVC) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVC">GitHub Repo Link</a> and <a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">article link</a></li><li>Model View ViewModel (MVVM) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVVM">GitHub Repo Link</a> and <a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">article link</a></li><li>Model View Presenter (MVP) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">article link</a></li><li>Coordinator Pattern — MVP with Coordinators (MVP-C) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP-C">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">article link</a></li><li>View Interactor Presenter Entity Router (VIPER) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIPER">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">article link</a></li><li>View Interactor Presenter (VIP) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIP">GitHub Repo link</a> and <a href="https://radu-ionut-dan.medium.com/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">article link</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=99edf7ab8c36" width="1" height="1" alt=""><hr><p><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">Battle of the iOS Architecture Patterns: Model View Presenter with Coordinators (MVP-C)</a> was originally published in <a href="https://medium.com/geekculture">Geek Culture</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Battle of the iOS Architecture Patterns: Model View Presenter (MVP)]]></title>
            <link>https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e?source=rss-dee343eb346a------2</link>
            <guid isPermaLink="false">https://medium.com/p/f693f6efd23e</guid>
            <category><![CDATA[design-patterns]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[swift]]></category>
            <dc:creator><![CDATA[Radu Dan]]></dc:creator>
            <pubDate>Mon, 17 May 2021 17:00:30 GMT</pubDate>
            <atom:updated>2025-01-17T21:27:39.079Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3wLQDnhdyg0dGLTiZ0SmDA.png" /><figcaption>Architecture Series — Model View Presenter (MVP)</figcaption></figure><h3>Motivation</h3><p>Before diving into iOS app development, it’s crucial to carefully consider the project’s architecture. We need to thoughtfully plan how different pieces of code will fit together, ensuring they remain comprehensible not just today, but months or years later when we need to revisit and modify the codebase. Moreover, a well-structured project helps establish a shared technical vocabulary among team members, making collaboration more efficient.</p><p>This article kicks off an exciting series where we’ll explore different architectural approaches by building the same application using various patterns. Throughout the series, we’ll analyze practical aspects like build times and implementation complexity, weigh the pros and cons of each pattern, and most importantly, examine real, production-ready code implementations. This hands-on approach will help you make informed decisions about which architecture best suits your project needs.</p><h3>Architecture Series Articles</h3><ul><li><a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">Model View Controller (MVC)</a></li><li><a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">Model View ViewModel (MVVM)</a></li><li><strong>Model View Presenter (MVP) — Current Article</strong></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">Model View Presenter with Coordinators (MVP-C)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">View Interactor Presenter Entity Router (VIPER)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">View Interactor Presenter (VIP)</a></li></ul><p>If you’re eager to explore the implementation details directly, you can find the complete source code in our open-source repository <a href="https://github.com/radude89/footballgather-ios">here</a>.</p><h3>Why Your iOS App Needs a Solid Architecture Pattern</h3><p>The cornerstone of any successful iOS application is maintainability. A well-architected app clearly defines boundaries — you know exactly where view logic belongs, what responsibilities each view controller has, and which components handle business logic. This clarity isn’t just for you; it’s essential for your entire development team to understand and maintain these boundaries consistently.</p><p>Here are the key benefits of implementing a robust architecture pattern:</p><ul><li><strong>Maintainability</strong>: Makes code easier to update and modify over time</li><li><strong>Testability</strong>: Facilitates comprehensive testing of business logic through clear separation of concerns</li><li><strong>Team Collaboration</strong>: Creates a shared technical vocabulary and understanding among team members</li><li><strong>Clean Separation</strong>: Ensures each component has clear, single responsibilities</li><li><strong>Bug Reduction</strong>: Minimizes errors through better organization and clearer interfaces between components</li></ul><h3>Project Requirements Overview</h3><p><strong>Given</strong> a medium-sized iOS application consisting of 6–7 screens, we’ll demonstrate how to implement it using the most popular architectural patterns in the iOS ecosystem: MVC (Model-View-Controller), MVVM (Model-View-ViewModel), MVP (Model-View-Presenter), VIPER (View-Interactor-Presenter-Entity-Router), VIP (Clean Swift), and the Coordinator pattern. Each implementation will showcase the pattern’s strengths and potential challenges.</p><p>Our demo application, <strong>Football Gather</strong>, is designed to help friends organize and track their casual football matches. It’s complex enough to demonstrate real-world architectural challenges while remaining simple enough to clearly illustrate different patterns.</p><h3>Core Features and Functionality</h3><ul><li>Player Management: Add and maintain a roster of players in the application</li><li>Team Assignment: Flexibly organize players into different teams for each match</li><li>Player Customization: Edit player details and preferences</li><li>Match Management: Set and control countdown timers for match duration</li></ul><h3>Screen Mockups</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*fUEgbohBL6ZTJAr9.png" /></figure><h3>Backend</h3><p>The application is supported by a web backend developed using the Vapor framework, a popular choice for building modern REST APIs in Swift. You can explore the details of this implementation in our article <a href="https://www.radude89.com/blog/vapor.html">here</a>, which covers the fundamentals of building REST APIs with Vapor and Fluent in Swift. Additionally, we have documented the transition from Vapor 3 to Vapor 4, highlighting the enhancements and new features in our article <a href="https://www.radude89.com/blog/migrate-to-vapor4.html">here</a>.</p><h3>What is MVP?</h3><p>MVP (Model-View-Presenter) is a design pattern used in software development, similar to MVVM (Model-View-ViewModel), but with some key distinctions:</p><ul><li>It introduces a <strong>Presenter</strong> layer that mediates between the View and the Model.</li><li>The Presenter controls the View and handles the communication between the layers.</li></ul><h4>Model</h4><ul><li>The Model layer encapsulates business data and logic.</li><li>It acts as an interface responsible for managing domain-specific data.</li></ul><h4><strong>Communication:</strong></h4><ul><li>When a user interacts with the View (e.g., clicking a button), the action is communicated to the Presenter, which then interacts with the Model.</li><li>When the Model updates (e.g., fetching new data), the Presenter communicates these changes to the View, ensuring the user interface reflects the latest state.</li></ul><h4>View</h4><ul><li>The View is responsible for rendering the user interface and capturing user interactions.</li><li>Unlike MVVM, the View does not directly handle its state updates. These are managed by the Presenter.</li></ul><h4><strong>Communication:</strong></h4><ul><li>The View does not directly communicate with the Model. All communication flows through the Presenter.</li></ul><h4>Presenter</h4><ul><li>The Presenter handles events triggered by the View and performs the necessary operations with the Model.</li><li>It acts as the intermediary, connecting the View and the Model without embedding logic into the View itself.</li><li>Typically, each Presenter is mapped 1:1 with a View.</li></ul><h4><strong>Communication:</strong></h4><ul><li>The Presenter communicates with both the Model and the View.</li><li>It updates the View when data changes, ensuring the user interface reflects the current state.</li><li>All updates to the View are initiated by the Presenter.</li></ul><h3>When to Use MVP</h3><p>The MVP pattern is suitable in scenarios where:</p><ul><li>MVC or MVVM does not provide sufficient modularity or testability for your application.</li><li>You want to make your app more modular and improve code coverage with unit tests.</li></ul><p>However, it may not be ideal for beginners or developers with limited iOS development experience, as implementing MVP involves more boilerplate code.</p><p>In our app, we have separated the <strong>View</strong> layer into two components:</p><ul><li>The <strong>ViewController</strong>, which acts as a Coordinator/Router and holds a reference to the View, often set as an IBOutlet.</li><li>The actual <strong>View</strong>, which focuses solely on rendering the UI.</li></ul><h4>Advantages</h4><ul><li>Better separation of concerns compared to other patterns.</li><li>Most of the business logic can be unit tested.</li></ul><h4>Disadvantages</h4><ul><li>The “assembly problem” becomes more prominent, requiring additional layers like a Router or Coordinator for navigation and module assembly.</li><li>The Presenter can become overly large and complex due to its responsibilities.</li></ul><h3>Applying MVP to Our Code</h3><p>Implementing MVP in our app involves two major steps:</p><ol><li>Converting existing ViewModels into Presenters.</li><li>Separating the View from the ViewController to ensure modularity.</li></ol><p>The applied MVP pattern is outlined below:</p><pre>/// FooViewController is the main view controller handling the FooView.<br>final class FooViewController: UIViewController {<br><br>    /// The main view for FooViewController.<br>    @IBOutlet weak var fooView: FooView!<br><br>    /// Called after the controller&#39;s view is loaded into memory.<br>    override func viewDidLoad() {<br>        super.viewDidLoad()<br>        setupView()<br>    }<br><br>    /// Sets up the view by assigning the presenter and delegate.<br>    private func setupView() {<br>        let presenter = FooPresenter(view: fooView)<br>        fooView.delegate = self<br>        fooView.presenter = presenter<br>        fooView.setupView()<br>    }<br><br>    /// Prepares for a segue to another view controller.<br>    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {<br>    }<br>}<br><br>/// Conforms FooViewController to FooViewDelegate.<br>extension FooViewController: FooViewDelegate {<br>    func didRequestToNavigateToFooDetail() {<br>        // Perform segue to FooDetail<br>    }<br>}<br><br>/// A protocol defining delegate methods for FooView.<br>protocol FooViewDelegate: AnyObject {<br>    func didRequestToNavigateToFooDetail()<br>}<br><br>/// A protocol defining methods for setting up the view.<br>protocol FooViewProtocol: AnyObject {<br>    func setupView()<br>}<br><br>/// Represents the main view of Foo, conforming to FooViewProtocol.<br>final class FooView: UIView, FooViewProtocol {<br>    /// The presenter managing the view&#39;s business logic.<br>    var presenter: FooPresenterProtocol = FooPresenter()<br><br>    /// A delegate to handle user interactions.<br>    weak var delegate: FooViewDelegate?<br><br>    /// Sets up the view with necessary configurations.<br>    func setupView() {<br>    }<br><br>    /// Loads data into the view.<br>    func loadData() {<br>    }<br>}<br><br>/// A protocol defining methods for presenters managing FooView.<br>protocol FooPresenterProtocol: AnyObject {<br>    func loadData()<br>}<br><br>/// A presenter class implementing FooPresenterProtocol.<br>final class FooPresenter: FooPresenterProtocol {<br>    /// A weak reference to the view managed by the presenter.<br>    private(set) weak var view: FooViewProtocol?<br><br>    /// Initializes the presenter with an optional view.<br>    init(view: FooViewProtocol? = nil) {<br>        self.view = view<br>    }<br><br>    /// Loads data and updates the view.<br>    func loadData() {<br>    }<br>}</pre><h4>LoginPresenter</h4><p>Let’s see how the LoginPresenter looks like:</p><pre>/// Defines the public API<br>protocol LoginPresenterProtocol: AnyObject {<br>    var rememberUsername: Bool { get }<br>    var username: String? { get }<br>    func setRememberUsername(_ value: Bool)<br>    func setUsername(_ username: String?)<br>    func performLogin(withUsername username: String?, andPassword password: String?)<br>    func performRegister(withUsername username: String?, andPassword password: String?)<br>}</pre><p>All parameters will be injected through the initialiser.</p><pre>final class LoginPresenter: LoginPresenterProtocol {<br>    private weak var view: LoginViewProtocol?<br>    private let loginService: LoginService<br>    private let usersService: StandardNetworkService<br>    private let userDefaults: FootballGatherUserDefaults<br>    private let keychain: FootbalGatherKeychain<br><br>    init(view: LoginViewProtocol? = nil,<br>         loginService: LoginService = LoginService(),<br>         usersService: StandardNetworkService = StandardNetworkService(resourcePath: &quot;/api/users&quot;),<br>         userDefaults: FootballGatherUserDefaults = .shared,<br>         keychain: FootbalGatherKeychain = .shared) {<br>        self.view = view<br>        self.loginService = loginService<br>        self.usersService = usersService<br>        self.userDefaults = userDefaults<br>        self.keychain = keychain<br>    }</pre><p>The Keychain interactions are defined below:</p><pre>var rememberUsername: Bool {<br>    return userDefaults.rememberUsername ?? true<br>}<br><br>var username: String? {<br>    return keychain.username<br>}<br>func setRememberUsername(_ value: Bool) {<br>    userDefaults.rememberUsername = value<br>}<br>func setUsername(_ username: String?) {<br>    keychain.username = username<br>}</pre><p>And we have the two services:</p><pre>func performLogin(withUsername username: String?, andPassword password: String?) {<br>    guard let userText = username, !userText.isEmpty,<br>          let passwordText = password, !passwordText.isEmpty else {<br>        // Key difference between MVVM and MVP, the presenter now tells the view what should do.<br>        view?.handleError(title: &quot;Error&quot;, message: &quot;Both fields are mandatory.&quot;)<br>        return<br>    }<br><br>    // Presenter tells the view to present a loading indicator.<br>    view?.showLoadingView()<br>    let requestModel = UserRequestModel(username: userText, password: passwordText)<br>    loginService.login(user: requestModel) { [weak self] result in<br>        DispatchQueue.main.async {<br>            self?.view?.hideLoadingView()<br>            switch result {<br>            case .failure(let error):<br>                self?.view?.handleError(title: &quot;Error&quot;, message: String(describing: error))<br>            case .success(_):<br>                // Go to next screen<br>                self?.view?.handleLoginSuccessful()<br>            }<br>        }<br>    }<br>}</pre><p>The register function is basically the same as the login one:</p><pre>func performRegister(withUsername username: String?, andPassword password: String?) {<br>    guard let userText = username, !userText.isEmpty,<br>          let passwordText = password, !passwordText.isEmpty else {<br>        view?.handleError(title: &quot;Error&quot;, message: &quot;Both fields are mandatory.&quot;)<br>        return<br>    }<br><br>    guard let hashedPasssword = Crypto.hash(message: passwordText) else {<br>        fatalError(&quot;Unable to hash password&quot;)<br>    }<br><br>    view?.showLoadingView()<br><br>    let requestModel = UserRequestModel(username: userText, password: hashedPasssword)<br><br>    usersService.create(requestModel) { [weak self] result in<br>        DispatchQueue.main.async {<br>            self?.view?.hideLoadingView()<br>            switch result {<br>            case .failure(let error):<br>                self?.view?.handleError(title: &quot;Error&quot;, message: String(describing: error))<br>            case .success(let resourceId):<br>                print(&quot;Created user: \(resourceId)&quot;)<br>                self?.view?.handleRegisterSuccessful()<br>            }<br>        }<br>    }<br>}</pre><p>The LoginView has the following protocols:</p><pre>// MARK: - LoginViewDelegate<br>/// A protocol defining the delegate&#39;s responsibilities for communicating with the LoginViewController.<br>protocol LoginViewDelegate: AnyObject {<br>    /// Presents an alert with a given title and message.<br>    func presentAlert(title: String, message: String)<br><br>    /// Notifies that the login operation was successful.<br>    func didLogin()<br><br>    /// Notifies that the registration operation was successful.<br>    func didRegister()<br>}<br><br>// MARK: - LoginViewProtocol<br>/// A protocol defining the public API of the LoginView.<br>protocol LoginViewProtocol: AnyObject {<br>    /// Sets up the initial view state.<br>    func setupView()<br><br>    /// Displays a loading indicator.<br>    func showLoadingView()<br><br>    /// Hides the loading indicator.<br>    func hideLoadingView()<br><br>    /// Handles errors by presenting an alert with a given title and message.<br>    func handleError(title: String, message: String)<br><br>    /// Notifies the view that login was successful.<br>    func handleLoginSuccessful()<br><br>    /// Notifies the view that registration was successful.<br>    func handleRegisterSuccessful()<br>}<br><br>// MARK: - LoginView<br>/// The view responsible for managing the UI components and communicating with the presenter.<br>final class LoginView: UIView, Loadable {<br><br>    // MARK: - Properties<br>    @IBOutlet weak var usernameTextField: UITextField!<br>    @IBOutlet weak var passwordTextField: UITextField!<br>    @IBOutlet weak var rememberMeSwitch: UISwitch!<br>    lazy var loadingView = LoadingView.initToView(self)<br>    weak var delegate: LoginViewDelegate?<br>    var presenter: LoginPresenterProtocol = LoginPresenter()<br><br>    /// Configures the &quot;Remember Me&quot; switch and pre-fills the username if necessary.<br>    private func configureRememberMe() {<br>        rememberMeSwitch.isOn = presenter.rememberUsername<br>        if presenter.rememberUsername {<br>            usernameTextField.text = presenter.username<br>        }<br>    }<br><br>    /// Stores the username and the state of the &quot;Remember Me&quot; switch in the presenter.<br>    private func storeUsernameAndRememberMe() {<br>        presenter.setRememberUsername(rememberMeSwitch.isOn)<br>        if rememberMeSwitch.isOn {<br>            presenter.setUsername(usernameTextField.text)<br>        } else {<br>            presenter.setUsername(nil)<br>        }<br>    }<br><br>    /// Handles the login action and delegates the responsibility to the presenter.<br>    @IBAction private func login(_ sender: Any) {<br>        presenter.performLogin(withUsername: usernameTextField.text, andPassword: passwordTextField.text)<br>    }<br><br>    /// Handles the registration action and delegates the responsibility to the presenter.<br>    @IBAction private func register(_ sender: Any) {<br>        presenter.performRegister(withUsername: usernameTextField.text, andPassword: passwordTextField.text)<br>    }<br>}<br><br>// MARK: - LoginViewProtocol Implementation<br>extension LoginView: LoginViewProtocol {<br>    func setupView() {<br>        configureRememberMe()<br>    }<br><br>    func handleError(title: String, message: String) {<br>        delegate?.presentAlert(title: title, message: message)<br>    }<br><br>    func handleLoginSuccessful() {<br>        storeUsernameAndRememberMe()<br>        delegate?.didLogin()<br>    }<br><br>    func handleRegisterSuccessful() {<br>        storeUsernameAndRememberMe()<br>        delegate?.didRegister()<br>    }<br>}<br><br>// MARK: - LoginViewController<br>/// The controller responsible for managing the login view and handling navigation.<br>final class LoginViewController: UIViewController {<br><br>    // MARK: - Properties<br>    @IBOutlet weak var loginView: LoginView!<br>    override func viewDidLoad() {<br>        super.viewDidLoad()<br>        setupView()<br>    }<br><br>    /// Configures the view and initializes the presenter.<br>    private func setupView() {<br>        let presenter = LoginPresenter(view: loginView)<br>        loginView.delegate = self<br>        loginView.presenter = presenter<br>        loginView.setupView()<br>    }<br>}<br><br>// MARK: - LoginViewDelegate Implementation<br>extension LoginViewController: LoginViewDelegate {<br>    func presentAlert(title: String, message: String) {<br>        // Presents an alert to the user.<br>        AlertHelper.present(in: self, title: title, message: message)<br>    }<br><br>    func didLogin() {<br>        // Navigates to the player list screen after successful login.<br>        performSegue(withIdentifier: SegueIdentifier.playerList.rawValue, sender: nil)<br>    }<br><br>    func didRegister() {<br>        // Navigates to the player list screen after successful registration.<br>        performSegue(withIdentifier: SegueIdentifier.playerList.rawValue, sender: nil)<br>    }<br>}<br><br>// MARK: - PlayerListPresenter<br>/// The presenter responsible for handling PlayerList-specific logic.<br>func performPlayerDeleteRequest() {<br>    guard let indexPath = indexPathForDeletion else { return }<br>    view?.showLoadingView()<br>    requestDeletePlayer(at: indexPath) { [weak self] result in<br>        if result {<br>            self?.view?.handlePlayerDeletion(forRowAt: indexPath)<br>        }<br>    }<br>}</pre><p>Now, the check for player deletion is made inside the <strong>Presenter</strong> and not in the <strong>View/ViewController</strong>.</p><pre>// MARK: - Player Deletion Request<br>private func requestDeletePlayer(at indexPath: IndexPath, completion: @escaping (Bool) -&gt; Void) {<br>    let player = players[indexPath.row]<br>    var service = playersService<br><br>    // Request to delete the player<br>    service.delete(withID: ResourceID.integer(player.id)) { [weak self] result in<br>        DispatchQueue.main.async {<br>            // [1] Hide the loading spinner view.<br>            self?.view?.hideLoadingView()<br>            // Handle the result of the deletion<br>            switch result {<br>            case .failure(let error):<br>                // [2] Notify the view of the error<br>                self?.view?.handleError(title: &quot;Error&quot;, message: String(describing: error))<br>                completion(false)<br>            case .success(_):<br>                // [3] Notify completion of successful deletion<br>                completion(true)<br>            }<br>        }<br>    }<br>}</pre><p>If we look in the PlayerListView, at the table view&#39;s data source methods, we observe that the <strong>Presenter</strong> is behaving exactly as a <strong>ViewModel</strong>:</p><pre>// MARK: - Table View Data Source<br>func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {<br>    // The number of rows is determined by the presenter<br>    return presenter.numberOfRows<br>}<br><br>func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {<br>    guard let cell: PlayerTableViewCell = tableView.dequeueReusableCell(<br>        withIdentifier: &quot;PlayerTableViewCell&quot;<br>    ) as? PlayerTableViewCell else {<br>        return UITableViewCell()<br>    }<br><br>    // Check if the presenter is in list view mode<br>    if presenter.isInListViewMode {<br>        // Clear any selected player if necessary<br>        presenter.clearSelectedPlayerIfNeeded(at: indexPath)<br>        cell.setupDefaultView()<br>    } else {<br>        // Setup the cell for selection view<br>        cell.setupSelectionView()<br>    }<br><br>    // Configure cell with player information provided by the presenter<br>    cell.nameLabel.text = presenter.playerNameDescription(at: indexPath)<br>    cell.positionLabel.text = presenter.playerPositionDescription(at: indexPath)<br>    cell.skillLabel.text = presenter.playerSkillDescription(at: indexPath)<br>    cell.playerIsSelected = presenter.playerIsSelected(at: indexPath)<br>    return cell<br>}</pre><p>The PlayerListViewController now acts as a router between the Edit, Confirm, and Add screens.</p><pre>override func prepare(for segue: UIStoryboardSegue, sender: Any?) {<br>    switch segue.identifier {<br>    case SegueIdentifier.confirmPlayers.rawValue:<br>        // [1] Compose the selected players that will be added in the ConfirmPlayersPresenter.<br>        if let confirmPlayersViewController = segue.destination as? ConfirmPlayersViewController {<br>            confirmPlayersViewController.playersDictionary = playerListView.presenter.playersDictionary<br>        }<br><br>    case SegueIdentifier.playerDetails.rawValue:<br>        // [2] Set the player that we want to show the details.<br>        if let playerDetailsViewController = segue.destination as? PlayerDetailViewController,<br>          let player = playerListView.presenter.selectedPlayerForDetails {<br>            // [3] From the Details screen, we can edit a player. Using delegation, we listen for<br>            // such modifications and refresh this view with the updated details.<br>            playerDetailsViewController.delegate = self<br>            playerDetailsViewController.player = player<br>        }<br>    case SegueIdentifier.addPlayer.rawValue:<br>        (segue.destination as? PlayerAddViewController)?.delegate = self<br>    default:<br>        break<br>    }<br>}</pre><p>Breaking into responsibilities, the <strong>PlayerList</strong> module has the following components:</p><p>PlayerListViewController responsibilities:</p><ul><li>Implements the PlayerListTogglable protocol to return to the listView mode when a gather is completed (called from GatherViewController).</li><li>Holds an IBOutlet to PlayerListView.</li><li>Sets the presenter and view delegate, and instructs the view to set up.</li><li>Handles navigation logic and constructs models for Edit, Add, and Confirm screens.</li><li>Implements the PlayerListViewDelegate, performing operations such as:</li><li>Changing the title when requested (func didRequestToChangeTitle(_ title: String)).</li><li>Adding the right navigation bar button item (<strong>Select</strong> or <strong>Cancel</strong> selection of players).</li><li>Performing the appropriate segue with the identifier constructed in the <strong>Presenter</strong>.</li><li>Presenting alerts for service failures or delete confirmations.</li><li>Implements PlayerDetailViewControllerDelegate to refresh the <strong>View</strong> when a player is edited.</li><li>Implements AddPlayerDelegate to reload the list of players in the <strong>View</strong>.</li></ul><p>PlayerListView responsibilities:</p><ul><li>Exposes a public API via PlayerListViewProtocol. This layer should remain simple and avoid complex logic.</li></ul><p>PlayerListPresenter responsibilities:</p><ul><li>Exposes necessary methods for the <strong>View</strong>, such as barButtonItemTitle, barButtonItemIsEnabled, etc.</li></ul><p>PlayerListViewState responsibilities:</p><ul><li>Uses the Factory Method pattern to manage different states of PlayerListView, encapsulated in a separate file.</li></ul><h4>PlayerDetail screen</h4><p>For the <strong>PlayerDetail</strong> screen, the <strong>View</strong> is separated from the <strong>ViewController</strong>.</p><pre>// MARK: - PlayerDetailViewController<br>final class PlayerDetailViewController: UIViewController {<br><br>    // MARK: - Properties<br>    @IBOutlet weak var playerDetailView: PlayerDetailView!<br>    weak var delegate: PlayerDetailViewControllerDelegate?<br>    var player: PlayerResponseModel?<br>    // .. other methods<br>}</pre><p>Navigation to the Edit screen follows a delegation pattern:</p><ul><li>The user taps a row corresponding to a player property. The <strong>View</strong> informs the <strong>ViewController</strong> to edit that field, and the <strong>ViewController</strong> performs the segue. In the prepare(for segue:) method, required properties are allocated for editing.</li></ul><pre>extension PlayerDetailViewController: PlayerDetailViewDelegate {<br>    func didRequestEditView() {<br>        performSegue(withIdentifier: SegueIdentifier.editPlayer.rawValue, sender: nil)<br>    }<br>}</pre><p>Inside PlayerDetailViewController:</p><pre>override func prepare(for segue: UIStoryboardSegue, sender: Any?) {<br>    guard segue.identifier == SegueIdentifier.editPlayer.rawValue,<br>          let destinationViewController = segue.destination as? PlayerEditViewController else {<br>        return<br>    }<br><br>    let presenter = playerDetailView.presenter<br>    destinationViewController.viewType = presenter?.destinationViewType ?? .text<br>    destinationViewController.playerEditModel = presenter?.playerEditModel<br>    destinationViewController.playerItemsEditModel = presenter?.playerItemsEditModel<br>    destinationViewController.delegate = self<br>}</pre><p>PlayerDetailView:</p><pre>final class PlayerDetailView: UIView, PlayerDetailViewProtocol {<br><br>    // MARK: - Properties<br>    @IBOutlet weak var playerDetailTableView: UITableView!<br>    weak var delegate: PlayerDetailViewDelegate?<br>    var presenter: PlayerDetailPresenterProtocol!<br>    <br>    // MARK: - Public API<br>    var title: String {<br>        return presenter.title<br>    }<br><br>    func reloadData() {<br>        playerDetailTableView.reloadData()<br>    }<br><br>    func updateData(player: PlayerResponseModel) {<br>        presenter.updatePlayer(player)<br>        presenter.reloadSections()<br>    }<br>}</pre><p>The table view delegate and data source implementation:</p><pre>extension PlayerDetailView: UITableViewDelegate, UITableViewDataSource {<br>    func numberOfSections(in tableView: UITableView) -&gt; Int {<br>        return presenter.numberOfSections<br>    }<br><br>    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {<br>        return presenter.numberOfRowsInSection(section)<br>    }<br><br>    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {<br>        guard let cell = tableView.dequeueReusableCell(withIdentifier: &quot;PlayerDetailTableViewCell&quot;) as? PlayerDetailTableViewCell else {<br>            return UITableViewCell()<br>        }<br>        cell.leftLabel.text = presenter.rowTitleDescription(for: indexPath)<br>        cell.rightLabel.text = presenter.rowValueDescription(for: indexPath)<br>        return cell<br>    }<br><br>    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -&gt; String? {<br>        return presenter.titleForHeaderInSection(section)<br>    }<br><br>    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {<br>        presenter.selectPlayerRow(at: indexPath)<br>        delegate?.didRequestEditView()<br>    }<br>}</pre><p>Inside PlayerDetailViewController:</p><pre>/// Prepares for a segue by passing the necessary data to the destination view controller.<br>override func prepare(for segue: UIStoryboardSegue, sender: Any?) {<br>    // Ensure the segue identifier matches and cast the destination view controller.<br>    guard segue.identifier == SegueIdentifier.editPlayer.rawValue,<br>          let destinationViewController = segue.destination as? PlayerEditViewController else {<br>        return<br>    }<br><br>    // Get the presenter from the player detail view.<br>    let presenter = playerDetailView.presenter<br><br>    // [1] Show the textfield or the picker for editing a player field.<br>    destinationViewController.viewType = presenter?.destinationViewType ?? .text<br><br>    // [2] Pass the edit model to the destination view controller.<br>    destinationViewController.playerEditModel = presenter?.playerEditModel<br><br>    // [3] Pass the data source for picker mode if applicable.<br>    destinationViewController.playerItemsEditModel = presenter?.playerItemsEditModel<br><br>    // Set the delegate to self.<br>    destinationViewController.delegate = self<br>}</pre><p>PlayerDetailView is presented below:</p><pre>final class PlayerDetailView: UIView, PlayerDetailViewProtocol {<br><br>    // MARK: - Properties<br>    @IBOutlet weak var playerDetailTableView: UITableView!<br>    weak var delegate: PlayerDetailViewDelegate?<br>    var presenter: PlayerDetailPresenterProtocol!<br>    <br>    // MARK: - Public API<br>    /// Returns the title for the view.<br>    var title: String {<br>        return presenter.title<br>    }<br><br>    /// Reloads the table view data.<br>    func reloadData() {<br>        playerDetailTableView.reloadData()<br>    }<br><br>    /// Updates the player data and refreshes the view.<br>    func updateData(player: PlayerResponseModel) {<br>        presenter.updatePlayer(player)<br>        presenter.reloadSections()<br>    }<br>}</pre><p>And the table view delegate and data source implementation:</p><pre>extension PlayerDetailView: UITableViewDelegate, UITableViewDataSource {<br><br>    /// Returns the number of sections in the table view.<br>    func numberOfSections(in tableView: UITableView) -&gt; Int {<br>        return presenter.numberOfSections<br>    }<br><br>    /// Returns the number of rows in a given section.<br>    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {<br>        return presenter.numberOfRowsInSection(section)<br>    }<br><br>    /// Configures and returns the cell for a given index path.<br>    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {<br>        guard let cell = tableView.dequeueReusableCell(withIdentifier: &quot;PlayerDetailTableViewCell&quot;) as? PlayerDetailTableViewCell else {<br>            return UITableViewCell()<br>        }<br>        cell.leftLabel.text = presenter.rowTitleDescription(for: indexPath)<br>        cell.rightLabel.text = presenter.rowValueDescription(for: indexPath)<br>        return cell<br>    }<br><br>    /// Returns the title for the header of a section.<br>    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -&gt; String? {<br>        return presenter.titleForHeaderInSection(section)<br>    }<br><br>    /// Handles the selection of a row and requests the edit view.<br>    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {<br>        presenter.selectPlayerRow(at: indexPath)<br>        delegate?.didRequestEditView()<br>    }<br>}</pre><p>The PlayerDetailPresenter:</p><pre>final class PlayerDetailPresenter: PlayerDetailPresenterProtocol {<br><br>    // MARK: - Properties<br>    private(set) var player: PlayerResponseModel<br>    private lazy var sections = makeSections()<br>    private(set) var selectedPlayerRow: PlayerRow?<br><br>    // MARK: - Public API<br>    init(player: PlayerResponseModel) {<br>        self.player = player<br>    }<br><br>    // other methods<br>}</pre><h4>Edit Screen</h4><p>We follow the same approach for the remaining screens of the app.<br>Exemplifying below the PlayerEdit functionality. The PlayerEditView class is basically the new <strong>ViewController</strong>.</p><pre>final class PlayerEditView: UIView, Loadable {<br><br>    // MARK: - Properties<br>    @IBOutlet weak var playerEditTextField: UITextField!<br>    @IBOutlet weak var playerTableView: UITableView!<br><br>    private lazy var doneButton = UIBarButtonItem(title: &quot;Done&quot;, style: .done, target: self, action: #selector(doneAction))<br><br>    lazy var loadingView = LoadingView.initToView(self)<br>    weak var delegate: PlayerEditViewDelegate?<br>    var presenter: PlayerEditPresenterProtocol!<br>    <br>    // other methods<br>}</pre><p>The selectors are pretty straightforward:</p><pre>// MARK: - Selectors<br>@objc private func textFieldDidChange(textField: UITextField) {<br>    doneButton.isEnabled = presenter.doneButtonIsEnabled(newValue: playerEditTextField.text)<br>}<br><br>@objc private func doneAction(sender: UIBarButtonItem) {<br>    presenter.updatePlayerBasedOnViewType(inputFieldValue: playerEditTextField.text)<br>}</pre><p>And the Public API:</p><pre>extension PlayerEditView: PlayerEditViewProtocol {<br>    var title: String {<br>        return presenter.title<br>    }<br><br>    func setupView() {<br>        setupNavigationItems()<br>        setupPlayerEditTextField()<br>        setupTableView()<br>    }<br><br>    func handleError(title: String, message: String) {<br>        delegate?.presentAlert(title: title, message: message)<br>    }<br><br>    func handleSuccessfulPlayerUpdate() {<br>        delegate?.didFinishEditingPlayer()<br>    }<br>}</pre><p>Finally, the UITableViewDataSource and UITableViewDelegate methods:</p><pre>extension PlayerEditView: UITableViewDelegate, UITableViewDataSource {<br>    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {<br>        return presenter.numberOfRows<br>    }<br><br>    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {<br>        guard let cell = tableView.dequeueReusableCell(withIdentifier: &quot;ItemSelectionCellIdentifier&quot;) else {<br>            return UITableViewCell()<br>        }<br>        cell.textLabel?.text = presenter.itemRowTextDescription(indexPath: indexPath)<br>        cell.accessoryType = presenter.isSelectedIndexPath(indexPath) ? .checkmark : .none<br>        return cell<br>    }<br><br>    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {<br>        if let selectedItemIndex = presenter.selectedItemIndex {<br>            clearAccessoryType(forSelectedIndex: selectedItemIndex)<br>        }<br>        presenter.updateSelectedItemIndex(indexPath.row)<br>        tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark<br>        doneButton.isEnabled = presenter.doneButtonIsEnabled(selectedIndexPath: indexPath)<br>    }<br><br>    private func clearAccessoryType(forSelectedIndex selectedItemIndex: Int) {<br>        let indexPath = IndexPath(row: selectedItemIndex, section: 0)<br>        playerTableView.cellForRow(at: indexPath)?.accessoryType = .none<br>    }<br><br>    func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {<br>        tableView.cellForRow(at: indexPath)?.accessoryType = .none<br>    }<br>}</pre><p>PlayerEditPresenter handles the business logic and exposes the properties for updating the UI elements.</p><pre>final class PlayerEditPresenter: PlayerEditPresenterProtocol {<br><br>    // MARK: - Properties<br>    private weak var view: PlayerEditViewProtocol?<br>    private var playerEditModel: PlayerEditModel<br>    private var viewType: PlayerEditViewType<br>    private var playerItemsEditModel: PlayerItemsEditModel?<br>    private var service: StandardNetworkService<br><br>    // MARK: - Public API<br>    init(view: PlayerEditViewProtocol? = nil,<br>          viewType: PlayerEditViewType = .text,<br>          playerEditModel: PlayerEditModel,<br>          playerItemsEditModel: PlayerItemsEditModel? = nil,<br>          service: StandardNetworkService = StandardNetworkService(resourcePath: &quot;/api/players&quot;, authenticated: true)) {<br>        self.view = view<br>        self.viewType = viewType<br>        self.playerEditModel = playerEditModel<br>        self.playerItemsEditModel = playerItemsEditModel<br>        self.service = service<br>    }<br>    // other methods<br>}</pre><p>An API call is detailed below:</p><pre>func updatePlayerBasedOnViewType(inputFieldValue: String?) {<br>    // [1] Check if we updated something.<br>    guard shouldUpdatePlayer(inputFieldValue: inputFieldValue) else { return }<br><br>    // [2] Present a loading indicator.<br>    view?.showLoadingView()<br>    let fieldValue = isSelectionViewType ? selectedItemValue : inputFieldValue<br>    <br>    // [3] Make the Network call.<br>    updatePlayer(newFieldValue: fieldValue) { [weak self] updated in<br>        DispatchQueue.main.async {<br>            self?.view?.hideLoadingView()<br>            self?.handleUpdatedPlayerResult(updated)<br>        }<br>    }<br>}</pre><p><strong>PlayerAdd</strong>, <strong>Confirm</strong> and <strong>Gather</strong> screens follow the same approach.</p><h3>Testing our business logic</h3><p>The testing approach is 90% the same as we did for MVVM.</p><p>In addition, we need to mock the view and check if the appropriate methods were called. For example, when a service API call is made, check if the view reloaded its state or handled the error in case of failures.</p><p>Unit Testing below GatherPresenter:</p><pre>// [1] Basic setup<br>final class GatherPresenterTests: XCTestCase {<br><br>    // [2] Define the Mocked network classes.<br>    private let session = URLSessionMockFactory.makeSession()<br>    private let resourcePath = &quot;/api/gathers&quot;<br>    private let appKeychain = AppKeychainMockFactory.makeKeychain()<br><br>    // [3] Setup and clear the Keychain variables.<br>    override func setUp() {<br>        super.setUp()<br>        appKeychain.token = ModelsMock.token<br>    }<br><br>    override func tearDown() {<br>        appKeychain.storage.removeAll()<br>        super.tearDown()<br>    }<br>}</pre><p>Testing the countdownTimerLabelText:</p><pre>func testFormattedCountdownTimerLabelText_whenViewModelIsAllocated_returnsDefaultTime() {<br>    // given<br>    let gatherTime = GatherTime.defaultTime<br>    let expectedFormattedMinutes = gatherTime.minutes &lt; 10 ? &quot;0\(gatherTime.minutes)&quot; : &quot;\(gatherTime.minutes)&quot;<br>    let expectedFormattedSeconds = gatherTime.seconds &lt; 10 ? &quot;0\(gatherTime.seconds)&quot; : &quot;\(gatherTime.seconds)&quot;<br>    let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>    let sut = GatherPresenter(gatherModel: mockGatherModel)<br><br>    // when<br>    let formattedCountdownTimerLabelText = sut.formattedCountdownTimerLabelText<br><br>    // then<br>    XCTAssertEqual(formattedCountdownTimerLabelText, &quot;\(expectedFormattedMinutes):\(expectedFormattedSeconds)&quot;)<br>}<br><br>func testFormattedCountdownTimerLabelText_whenPresenterIsAllocated_returnsDefaultTime() {<br>    // given<br>    let gatherTime = GatherTime.defaultTime<br>    let expectedFormattedMinutes = gatherTime.minutes &lt; 10 ? &quot;0\(gatherTime.minutes)&quot; : &quot;\(gatherTime.minutes)&quot;<br>    let expectedFormattedSeconds = gatherTime.seconds &lt; 10 ? &quot;0\(gatherTime.seconds)&quot; : &quot;\(gatherTime.seconds)&quot;<br>    let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>    let sut = GatherPresenter(gatherModel: mockGatherModel)<br><br>    // when<br>    let formattedCountdownTimerLabelText = sut.formattedCountdownTimerLabelText<br><br>    // then<br>    XCTAssertEqual(formattedCountdownTimerLabelText, &quot;\(expectedFormattedMinutes):\(expectedFormattedSeconds)&quot;)<br>}<br><br>func testFormattedCountdownTimerLabelText_whenTimeIsZero_returnsZeroSecondsZeroMinutes() {<br>    // given<br>    let mockGatherTime = GatherTime(minutes: 0, seconds: 0)<br>    let mockGatherTimeHandler = GatherTimeHandler(selectedTime: mockGatherTime)<br>    let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>    let sut = GatherPresenter(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br><br>    // when<br>    let formattedCountdownTimerLabelText = sut.formattedCountdownTimerLabelText<br><br>    // then<br>    XCTAssertEqual(formattedCountdownTimerLabelText, &quot;00:00&quot;)<br>}<br><br>func testFormattedCountdownTimerLabelText_whenTimeHasMinutesAndZeroSeconds_returnsMinutesAndZeroSeconds() {<br>    // given<br>    let mockGatherTime = GatherTime(minutes: 10, seconds: 0)<br>    let mockGatherTimeHandler = GatherTimeHandler(selectedTime: mockGatherTime)<br>    let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>    let sut = GatherPresenter(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br><br>    // when<br>    let formattedCountdownTimerLabelText = sut.formattedCountdownTimerLabelText<br><br>    // then<br>    XCTAssertEqual(formattedCountdownTimerLabelText, &quot;10:00&quot;)<br>}<br><br>func testFormattedCountdownTimerLabelText_whenTimeHasSecondsAndZeroMinutes_returnsSecondsAndZeroMinutes() {<br>    // given<br>    let mockGatherTime = GatherTime(minutes: 0, seconds: 10)<br>    let mockGatherTimeHandler = GatherTimeHandler(selectedTime: mockGatherTime)<br>    let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>    let sut = GatherPresenter(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br><br>    // when<br>    let formattedCountdownTimerLabelText = sut.formattedCountdownTimerLabelText<br><br>    // then<br>    XCTAssertEqual(formattedCountdownTimerLabelText, &quot;00:10&quot;)<br>}</pre><p>Toggle timer becomes more interesting:</p><pre>func testToggleTimer_whenSelectedTimeIsNotValid_returns() {<br>    // given<br>    let mockGatherTime = GatherTime(minutes: -1, seconds: -1)<br>    let mockGatherTimeHandler = GatherTimeHandler(selectedTime: mockGatherTime)<br>    let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>    // [1] Allocate the mock view.<br>    let mockView = MockView()<br>    let sut = GatherPresenter(view: mockView, gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br><br>    // when<br>    sut.toggleTimer()<br><br>    // then<br>    // [2] configureSelectedTime() was not called.<br>    XCTAssertFalse(mockView.selectedTimeWasConfigured)<br>}<br><br>func testToggleTimer_whenSelectedTimeIsValid_updatesTime() {<br>    // given<br>    let numberOfUpdateCalls = 2<br>    let mockGatherTime = GatherTime(minutes: 0, seconds: numberOfUpdateCalls)<br>    let mockGatherTimeHandler = GatherTimeHandler(selectedTime: mockGatherTime)<br>    let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>    // [1] Configure the mock view parameters<br>    let exp = expectation(description: &quot;Waiting timer expectation&quot;)<br>    let mockView = MockView()<br>    mockView.numberOfUpdateCalls = numberOfUpdateCalls<br>    mockView.expectation = exp<br>    let sut = GatherPresenter(view: mockView, gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br><br>    // when<br>    sut.toggleTimer()<br><br>    // then<br>    // Selector should be called two times.<br>    waitForExpectations(timeout: 5) { _ in<br>        XCTAssertTrue(mockView.selectedTimeWasConfigured)<br>        XCTAssertEqual(mockView.actualUpdateCalls, numberOfUpdateCalls)<br>        sut.stopTimer()<br>    }<br>}</pre><p>And below is the mock view:</p><pre>/// This extension contains a mock view implementation used for testing purposes<br>private extension GatherPresenterTests {<br><br>    /// MockView conforms to GatherViewProtocol and helps in testing the GatherPresenter logic<br>    final class MockView: GatherViewProtocol {<br>        /// Indicates whether the selected time was configured in the view<br>        private(set) var selectedTimeWasConfigured = false<br><br>        weak var expectation: XCTestExpectation? = nil<br>        var numberOfUpdateCalls = 1<br>        private(set) var actualUpdateCalls = 0<br><br>        /// Configures the selected time when called<br>        func configureSelectedTime() {<br>            selectedTimeWasConfigured = true<br>            actualUpdateCalls += 1<br>            if expectation != nil &amp;&amp; numberOfUpdateCalls == actualUpdateCalls {<br>                expectation?.fulfill()<br>            }<br>        }<br><br>        /// Handles the successful end of the gather process and fulfills the expectation<br>        func handleSuccessfulEndGather() {<br>            expectation?.fulfill()<br>        }<br><br>        /// Sets up the view (no implementation for testing purposes)<br>        func setupView() {}<br><br>        /// Shows a loading view (no implementation for testing purposes)<br>        func showLoadingView() {}<br><br>        /// Hides the loading view (no implementation for testing purposes)<br>        func hideLoadingView() {}<br><br>        /// Handles an error and displays the message (no implementation for testing purposes)<br>        func handleError(title: String, message: String) {}<br><br>        /// Confirms the end of the gather process (no implementation for testing purposes)<br>        func confirmEndGather() {}<br>    }<br>}</pre><p>I’d say that testing the presenter is very cool. You don’t need to do magic stuff, and the methods are very small in size which is helping.<br>The complex thing comes with the fact that you will need to mock the View layer and check if some parameters are changing accordingly.</p><h3>Key Metrics</h3><h4>Lines of code — View Controllers</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PzMNxUTKsZmE_QMwAt1yYA.png" /></figure><h4>Lines of code — Views</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dtNeje9qe2xF0J-OXug2sQ.png" /></figure><h4>Lines of code — Presenters</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*czz4_-0788DNRNz2iuFKtg.png" /></figure><h4>Lines of code — Local Models</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4RZPNOWTPaA-hxns4i6Prw.png" /></figure><h4>Unit Tests</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dwDkLxEIGNt_jIhgjHLqbQ.png" /></figure><h4>Build Times</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pnf6a12PMClvw2oMbLp_vg.png" /></figure><p><em>Tests were run in iPhone 8 Simulator, with iOS 14.3, using Xcode 12.4 and on an i9 MacBook Pro 2019.</em></p><h3>Conclusion</h3><p>In this refactor, we successfully transitioned the application from the MVVM architecture to MVP. The process was straightforward: we replaced each <strong>ViewModel</strong> with a corresponding <strong>Presenter</strong> layer, ensuring that our application followed the new pattern seamlessly.</p><p>Additionally, we introduced a new <strong>View</strong> layer, separating it from the <strong>ViewController</strong> to further clarify the division of responsibilities. This made the codebase cleaner, with thinner view controllers and smaller, more focused classes and functions that adhere to the Single Responsibility Principle (SRP).</p><p>Personally, I find the MVP pattern more intuitive, especially for apps built with UIKit. It offers a more natural approach compared to MVVM in this context.</p><p>Looking at the key metrics, we can draw the following conclusions:</p><ul><li>The View Controllers are significantly thinner; we reduced their size by more than <strong>1,000</strong> lines of code.</li><li>A new <strong>View</strong> layer was introduced for managing UI updates, improving clarity and separation of concerns.</li><li>Presenters are larger than ViewModels due to their added responsibility of managing the views.</li><li>Unit testing was similar to the MVVM approach, resulting in almost identical code coverage of <strong>97.2%</strong>.</li><li>While the number of files and classes increased, the impact on build time was minimal, increasing by just <strong>530 ms</strong> compared to MVVM, and <strong>400 ms</strong> compared to MVC.</li><li>Surprisingly, the average unit test execution time was faster by <strong>1.36 seconds</strong> compared to MVVM.</li><li>Unit tests for business logic were considerably easier to write when compared to the MVC pattern.</li></ul><p>It’s exciting to see how transforming an app from MVVM to MVP can improve structure and maintainability. From my perspective, separating the <strong>View</strong> from the <strong>ViewController</strong> in MVP offers a cleaner and more powerful</p><h3>Useful Links</h3><ul><li>The iOS App, Football Gather — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather">GitHub Repo Link</a></li><li>The web server application made in Vapor — <a href="https://github.com/radude89/footballgather-ws">GitHub Repo Link</a></li><li>Vapor 3 Backend APIs <a href="https://radu-ionut-dan.medium.com/using-vapor-and-fluent-to-create-a-rest-api-5f9a0dcffc7b">article link</a></li><li>Migrating to Vapor 4 <a href="https://radu-ionut-dan.medium.com/migrating-to-vapor-4-53a821c29203">article link</a></li><li>Model View Controller (MVC) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVC">GitHub Repo Link</a> and <a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">article link</a></li><li>Model View ViewModel (MVVM) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVVM">GitHub Repo Link</a> and <a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">article link</a></li><li>Model View Presenter (MVP) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">article link</a></li><li>Coordinator Pattern — MVP with Coordinators (MVP-C) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP-C">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">article link</a></li><li>View Interactor Presenter Entity Router (VIPER) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIPER">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">article link</a></li><li>View Interactor Presenter (VIP) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIP">GitHub Repo link</a> and <a href="https://radu-ionut-dan.medium.com/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">article link</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f693f6efd23e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">Battle of the iOS Architecture Patterns: Model View Presenter (MVP)</a> was originally published in <a href="https://medium.com/geekculture">Geek Culture</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Battle of the iOS Architecture Patterns: A Look at Model-View-ViewModel (MVVM)]]></title>
            <link>https://medium.com/better-programming/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e?source=rss-dee343eb346a------2</link>
            <guid isPermaLink="false">https://medium.com/p/bdfd07d9395e</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[mobile-development]]></category>
            <dc:creator><![CDATA[Radu Dan]]></dc:creator>
            <pubDate>Fri, 26 Mar 2021 15:54:08 GMT</pubDate>
            <atom:updated>2025-01-17T21:13:28.968Z</atom:updated>
            <content:encoded><![CDATA[<h4>Build a real-world footballer iOS game using the popular architecture pattern</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Usnylwpnyh1VQ6gm1n7E9g.png" /><figcaption>Architecture Series — Model View ViewModel (MVVM)</figcaption></figure><h3>Motivation</h3><p>Before diving into iOS app development, it’s crucial to carefully consider the project’s architecture. We need to thoughtfully plan how different pieces of code will fit together, ensuring they remain comprehensible not just today, but months or years later when we need to revisit and modify the codebase. Moreover, a well-structured project helps establish a shared technical vocabulary among team members, making collaboration more efficient.</p><p>This article kicks off an exciting series where we’ll explore different architectural approaches by building the same application using various patterns. Throughout the series, we’ll analyze practical aspects like build times and implementation complexity, weigh the pros and cons of each pattern, and most importantly, examine real, production-ready code implementations. This hands-on approach will help you make informed decisions about which architecture best suits your project needs.</p><h3>Architecture Series Articles</h3><ul><li><a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">Model View Controller (MVC)</a></li><li><strong>Model View ViewModel (MVVM) — Current Article</strong></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">Model View Presenter (MVP)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">Model View Presenter with Coordinators (MVP-C)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">View Interactor Presenter Entity Router (VIPER)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">View Interactor Presenter (VIP)</a></li></ul><p>If you’re eager to explore the implementation details directly, you can find the complete source code in our open-source repository <a href="https://github.com/radude89/footballgather-ios">here</a>.</p><h3>Why Your iOS App Needs a Solid Architecture Pattern</h3><p>The cornerstone of any successful iOS application is maintainability. A well-architected app clearly defines boundaries — you know exactly where view logic belongs, what responsibilities each view controller has, and which components handle business logic. This clarity isn’t just for you; it’s essential for your entire development team to understand and maintain these boundaries consistently.</p><p>Here are the key benefits of implementing a robust architecture pattern:</p><ul><li><strong>Maintainability</strong>: Makes code easier to update and modify over time</li><li><strong>Testability</strong>: Facilitates comprehensive testing of business logic through clear separation of concerns</li><li><strong>Team Collaboration</strong>: Creates a shared technical vocabulary and understanding among team members</li><li><strong>Clean Separation</strong>: Ensures each component has clear, single responsibilities</li><li><strong>Bug Reduction</strong>: Minimizes errors through better organization and clearer interfaces between components</li></ul><h3>Project Requirements Overview</h3><p><strong>Given</strong> a medium-sized iOS application consisting of 6–7 screens, we’ll demonstrate how to implement it using the most popular architectural patterns in the iOS ecosystem: MVC (Model-View-Controller), MVVM (Model-View-ViewModel), MVP (Model-View-Presenter), VIPER (View-Interactor-Presenter-Entity-Router), VIP (Clean Swift), and the Coordinator pattern. Each implementation will showcase the pattern’s strengths and potential challenges.</p><p>Our demo application, <strong>Football Gather</strong>, is designed to help friends organize and track their casual football matches. It’s complex enough to demonstrate real-world architectural challenges while remaining simple enough to clearly illustrate different patterns.</p><h3>Core Features and Functionality</h3><ul><li>Player Management: Add and maintain a roster of players in the application</li><li>Team Assignment: Flexibly organize players into different teams for each match</li><li>Player Customization: Edit player details and preferences</li><li>Match Management: Set and control countdown timers for match duration</li></ul><h3>Screen Mockups</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*YylvYl3d-4GbYMBm.png" /></figure><h3>Backend</h3><p>The application is supported by a web backend developed using the Vapor framework, a popular choice for building modern REST APIs in Swift. You can explore the details of this implementation in our article <a href="https://www.radude89.com/blog/vapor.html">here</a>, which covers the fundamentals of building REST APIs with Vapor and Fluent in Swift. Additionally, we have documented the transition from Vapor 3 to Vapor 4, highlighting the enhancements and new features in our article <a href="https://www.radude89.com/blog/migrate-to-vapor4.html">here</a>.</p><h3>What is MVVM</h3><p><strong>MVVM</strong> stands for <strong>Model View ViewModel</strong>, an architecture pattern that is used naturally with RxSwift where you can bind your UI elements to the <strong>Model</strong> classes through the <strong>ViewModel</strong>.</p><p>It is a newer pattern, proposed in 2005 by John Gossman and has the role of extracting the <strong>Model</strong> from the <strong>ViewController</strong>. The interaction between the <strong>ViewController</strong> and the <strong>Model</strong> is done through a new layer, called <strong>ViewModel</strong>.</p><h4>Model</h4><ul><li>The same layer we had in MVC, and is used to encapsulate data and the business logic.</li></ul><h4><strong>Communication</strong></h4><ul><li>When something happens in the view layer, for example when the user initiates an action, it is communicated to the model through the ViewModel.</li><li>When the model is changed, for example when new data is made available and we need to update the UI, the model notifies the ViewModel.</li></ul><h4>View</h4><ul><li>View and ViewController are the layers where the visual elements reside.</li><li>The View contains the UI elements, such as buttons, labels, table views and the ViewController is the owner of the View.</li><li>This layer is the same as in MVC, but the ViewController is now part of it and will be changed to reference the ViewModel.</li></ul><h4><strong>Communication</strong></h4><ul><li>Views can’t communicate directly with the Model, everything is done through the ViewModel.</li></ul><h4>ViewModel</h4><ul><li>A new layer that sits between the View/View Controller and the Model.</li><li>Through binding, it updates the UI elements when something has changed in the Model.</li><li>Is a canonical representation of the View.</li><li>Provides interfaces to the View.</li></ul><h4><strong>Communication</strong></h4><ul><li>Can communicate with both layers, Model and View/View Controller.</li><li>Via binding, ViewModels trigger changes to the data of the Model layer</li><li>When data changes, it makes sure those changes are communicated to the user interface, updating the View (again through a binding).</li></ul><h3>Different flavours of MVVM</h3><p>The way you apply MVVM depends on how you choose to implement the binding:</p><ul><li>Using a 3rd party, such as RxSwift.</li><li>KVO — Key Value Observing.</li><li>Manually.</li></ul><p>In our demo app we will explore the manual approach.</p><h3>How and when to use MVVM</h3><p>When you see the ViewController does a lot of stuff and might turn to be massive, you can start looking at different patterns, such as MVVM.</p><p>Advantages:</p><ul><li>Slims down the ViewController.</li><li>Easier to test the business logic, because you now have a dedicated layer that handles data.</li><li>Provides a better separation of concerns</li></ul><p>Disadvantages:</p><ul><li>Same as in MVC, if is not applied correctly and you are not careful of SRP (Single Responsibility Principle), it can turn out into a Massive ViewModel.</li><li>Can be overkill and too complex for small projects (for example, in a Hackathon app/prototype).</li><li>Adopting a 3rd party increases the app size and can impact the performance.</li><li>Doesn’t feel natural to iOS app development with UIKit. On the other hand, for apps developed with SwiftUI makes perfect sense.</li></ul><p>Below you can find a collection of links that tell you more about this code architecture pattern:</p><ul><li><a href="https://www.raywenderlich.com/34-design-patterns-by-tutorials-mvvm">Book about MVVM on raywenderlich.</a></li><li><a href="https://medium.com/better-programming/mvvm-in-ios-from-net-perspective-580eb7f4f129">Article about MVVM in iOS</a></li><li><a href="https://medium.com/flawless-app-stories/how-to-use-a-model-view-viewmodel-architecture-for-ios-46963c67be1b">Article “How to not get desperate with MVVM implementation”</a></li><li><a href="https://docs.microsoft.com/en-us/archive/blogs/johngossman/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps">Introduction to Model/View/ViewModel pattern for building WPF apps</a></li><li><a href="https://www.appcoda.com/mvvm-vs-mvc/">MVVM vs MVC</a></li><li><a href="https://blog.pusher.com/mvvm-ios/">Using MVV in iOS</a></li><li><a href="https://medium.com/flawless-app-stories/practical-mvvm-rxswift-a330db6aa693">Practical MVVM + RxSwift</a></li><li><a href="https://academy.realm.io/posts/slug-max-alexander-mvvm-rxswift/">MVVM with RxSwift</a></li><li><a href="https://benoitpasquier.com/integrate-rxswift-in-mvvm/">How to integrate RxSwift in your MVVM architecture</a></li><li><a href="https://cocoacasts.com/what-are-the-benefits-of-model-view-viewmodel">What Are the Benefits of Model-View-ViewModel</a></li><li><a href="https://blogsnook.com/mvvm-pattern-advantages/">MVVM Pattern Advantages — Benefits of Using MVVM Model</a></li><li><a href="https://docs.microsoft.com/en-us/archive/blogs/johngossman/advantages-and-disadvantages-of-m-v-vm">Advantages and disadvantages of M-V-VM</a></li><li><a href="https://medium.com/swift-india/mvvm-1-a-general-discussion-764581a2d5d9">MVVM-1: A General Discussion</a></li></ul><h3>Applying to our code</h3><p>This is pretty straightforward. We go into each ViewController and extract the business logic into a new layer (ViewModel).</p><h4>Decoupling LoginViewController from business logic</h4><p><strong>Transformations</strong>:</p><ul><li>viewModel - A new layer that handles the view state and the model updates.</li><li>The services are now part of the <strong>ViewModel</strong> layer.</li></ul><p>In viewDidLoad method, we call configureRememberMe() function. Here, we can observe how the <strong>View</strong> asks the <strong>ViewModel</strong> for the values of the &quot;Remember Me&quot; UISwitch and the username:</p><pre>/// Configures the &quot;Remember Me&quot; functionality by setting up the switch state and username field<br>private func configureRememberMe() {<br>    // Set the switch state based on user&#39;s saved preference<br>    rememberMeSwitch.isOn = viewModel.rememberUsername<br>    <br>    // If &quot;Remember Me&quot; was enabled, populate the username field<br>    // with the previously stored username<br>    if viewModel.rememberUsername {<br>        usernameTextField.text = viewModel.username<br>    }<br>}</pre><p>For the <strong>Login</strong> and <strong>Register</strong> actions, we tell the <strong>ViewModel</strong> to handle the service requests. We use closures for updating the UI once the server API call finished.</p><pre>/// Handles the login button action<br>@IBAction func login(_ sender: Any) {<br>    // Validate that both username and password fields are filled<br>    guard let userText = usernameTextField.text, userText.isEmpty == false,<br>          let passwordText = passwordTextField.text, passwordText.isEmpty == false else {<br>        AlertHelper.present(in: self, title: &quot;Error&quot;, message: &quot;Both fields are mandatory.&quot;)<br>        return<br>    }<br>    <br>    // Show loading indicator while performing login<br>    showLoadingView()<br>    <br>    // Attempt to login using the ViewModel<br>    viewModel.performLogin(withUsername: userText, andPassword: passwordText) { [weak self] error in<br>        DispatchQueue.main.async {<br>            self?.hideLoadingView()<br>            self?.handleServiceResponse(error: error)<br>        }<br>    }<br>}<br><br>/// Handles the API response from login attempt<br>private func handleServiceResponse(error: Error?) {<br>    if let error = error {<br>        // Show error alert if login failed<br>        AlertHelper.present(in: self, title: &quot;Error&quot;, message: String(describing: error))<br>    } else {<br>        handleSuccessResponse()<br>    }<br>}<br>/// Handles successful login by storing credentials and navigating to PlayerList<br>private func handleSuccessResponse() {<br>    storeUsernameAndRememberMe()<br>    performSegue(withIdentifier: SegueIdentifier.playerList.rawValue, sender: nil)<br>}<br>/// Stores the username and remember me preference in the ViewModel<br>private func storeUsernameAndRememberMe() {<br>    // Update remember me preference<br>    viewModel.setRememberUsername(rememberMeSwitch.isOn)<br>    <br>    // Store or clear username based on remember me switch<br>    if rememberMeSwitch.isOn {<br>        viewModel.setUsername(usernameTextField.text)<br>    } else {<br>        viewModel.setUsername(nil)<br>    }<br>}</pre><p>The LoginViewModel is defined by the following properties:</p><pre>/// ViewModel responsible for handling login-related business logic and data management<br>struct LoginViewModel {<br>    // MARK: - Dependencies<br>    <br>    /// Service handling login authentication<br>    private let loginService: LoginService<br>    <br>    /// Service handling user-related network operations<br>    private let usersService: StandardNetworkService<br>    <br>    /// Persistent storage for user preferences<br>    private let userDefaults: FootballGatherUserDefaults<br>    <br>    /// Secure storage for sensitive user data<br>    private let keychain: FootbalGatherKeychain<br>}</pre><p>We have the services that were passed from LoginViewController (LoginService, StandardNetworkService used for registering the user and the <strong>Storage</strong> facilitators - UserDefaults and Keychain wrappers).<br>All of them are injected through the initializer:</p><pre>/// Initializes the LoginViewModel with customizable dependencies<br>init(<br>    loginService: LoginService = LoginService(),<br>    usersService: StandardNetworkService = StandardNetworkService(<br>        resourcePath: &quot;/api/users&quot;<br>    ),<br>    userDefaults: FootballGatherUserDefaults = .shared,<br>    keychain: FootbalGatherKeychain = .shared<br>) {<br>    self.loginService = loginService<br>    self.usersService = usersService<br>    self.userDefaults = userDefaults<br>    self.keychain = keychain<br>}</pre><p>This comes in handy for unit testing if we want to use our own Mocked services or storages.<br>The Public API is clean and simple:</p><pre>// MARK: - User Preferences<br><br>/// Returns whether the &quot;Remember Username&quot; feature is enabled<br>/// Defaults to true if not previously set<br>var rememberUsername: Bool {<br>    return userDefaults.rememberUsername ?? true<br>}<br><br>/// Returns the stored username if &quot;Remember Username&quot; is enabled<br>/// Returns nil if no username is stored or feature is disabled<br>var username: String? {<br>    return keychain.username<br>}<br><br>/// Updates the &quot;Remember Username&quot; preference<br>/// - Parameter value: Boolean indicating if username should be remembered<br>func setRememberUsername(_ value: Bool) {<br>    userDefaults.rememberUsername = value<br>}<br><br>/// Stores or clears the username in secure storage<br>/// - Parameter username: The username to store, or nil to clear<br>func setUsername(_ username: String?) {<br>    keychain.username = username<br>}</pre><p>And the two server API calls:</p><pre>/// Attempts to log in a user with the provided credentials<br>/// - Parameters:<br>///   - username: The user&#39;s username<br>///   - password: The user&#39;s password<br>///   - completion: Callback with optional error if login fails<br>func performLogin(<br>    withUsername username: String,<br>    andPassword password: String,<br>    completion: @escaping (Error?) -&gt; ()<br>) {<br>    // Create request model with user credentials<br>    let requestModel = UserRequestModel(username: username, password: password)<br>    <br>    // Attempt login with service<br>    loginService.login(user: requestModel) { result in<br>        switch result {<br>        case .failure(let error):<br>            completion(error)<br>            <br>        case .success(_):<br>            completion(nil)<br>        }<br>    }<br>}<br><br>/// Registers a new user with the provided credentials<br>/// - Parameters:<br>///   - username: The desired username<br>///   - password: The user&#39;s password (will be hashed)<br>///   - completion: Callback with optional error if registration fails<br>func performRegister(<br>    withUsername username: String,<br>    andPassword password: String,<br>    completion: @escaping (Error?) -&gt; ()<br>) {<br>    // Hash the password for security<br>    guard let hashedPasssword = Crypto.hash(message: password) else {<br>        fatalError(&quot;Unable to hash password&quot;)<br>    }<br>    <br>    // Create request model with hashed credentials<br>    let requestModel = UserRequestModel(<br>        username: username,<br>        password: hashedPasssword<br>    )<br>    <br>    // Attempt to create user with service<br>    usersService.create(requestModel) { result in<br>        switch result {<br>        case .failure(let error):<br>            completion(error)<br>            <br>        case .success(let resourceId):<br>            print(&quot;Created user: \(resourceId)&quot;)<br>            completion(nil)<br>        }<br>    }<br>}</pre><p>As you can see, the code looks much cleaner by separating the <strong>Model</strong> from the <strong>ViewController</strong>. Now, the <strong>View / ViewController</strong> asks the <strong>ViewModel</strong> for what it needs.</p><p>PlayerListViewController is much bigger, harder to refactor and to extract the business logic than the LoginViewController.<br>First, we want to leave just the outlets and all UIView objects we require for this class.<br>In viewDidLoad, we will do the setup and configuration of the initial state of the views, setting the view model delegate and trigger the player load through the view model.</p><p><strong>Loading players:</strong></p><pre>/// Loads players from the server and updates the UI accordingly<br>private func loadPlayers() {<br>    // Disable user interaction while loading<br>    view.isUserInteractionEnabled = false<br>    <br>    // Request players from ViewModel<br>    viewModel.fetchPlayers { [weak self] error in<br>        DispatchQueue.main.async {<br>            // Re-enable user interaction<br>            self?.view.isUserInteractionEnabled = true<br>            <br>            // Handle the response<br>            if let error = error {<br>                self?.handleServiceFailures(withError: error)<br>            } else {<br>                self?.handleLoadPlayersSuccessfulResponse()<br>            }<br>        }<br>    }<br>}</pre><p>The response handling is similar as we have in LoginViewController:</p><pre>/// Handles service failure by presenting an error alert to the user<br>private func handleServiceFailures(withError error: Error) {<br>    AlertHelper.present(in: self, title: &quot;Error&quot;, message: String(describing: error))<br>}<br><br>/// Handles successful loading of players by updating the UI<br>private func handleLoadPlayersSuccessfulResponse() {<br>    if viewModel.playersCollectionIsEmpty {<br>        // The players array is empty, show the empty view<br>        showEmptyView()<br>    } else {<br>        // Players are available, hide the empty view<br>        hideEmptyView()<br>    }<br>    // Reload the players table view to reflect the latest data<br>    playerTableView.reloadData()<br>}</pre><p>To display the model properties in the table view’s cell and configure it, we ask the <strong>ViewModel</strong> to give us the primitives and then we set them to the cell’s properties:</p><pre>/// Returns the number of rows in the section, which is the number of players<br>func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {<br>    return viewModel.numberOfRows<br>}<br><br>/// Configures and returns the cell for the given index path<br>func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {<br>    guard let cell = tableView.dequeueReusableCell(withIdentifier: &quot;PlayerTableViewCell&quot;) as? PlayerTableViewCell else {<br>        return UITableViewCell()<br>    }<br>    if viewModel.isInListViewMode {<br>        // Default view mode, showing the players<br>        viewModel.clearSelectedPlayerIfNeeded(at: indexPath)<br>        cell.setupDefaultView()<br>    } else {<br>        // Selection view mode for gathering players<br>        cell.setupSelectionView()<br>    }<br>    // Display the model properties in the cell&#39;s properties<br>    cell.nameLabel.text = viewModel.playerNameDescription(at: indexPath)<br>    cell.positionLabel.text = viewModel.playerPositionDescription(at: indexPath)<br>    cell.skillLabel.text = viewModel.playerSkillDescription(at: indexPath)<br>    cell.playerIsSelected = viewModel.playerIsSelected(at: indexPath)<br>    return cell<br>}</pre><p>To delete a player, we do the following:</p><pre>/// Determines if a row can be edited, only in list view mode<br>func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -&gt; Bool {<br>    return viewModel.isInListViewMode<br>}<br><br>/// Handles the commit editing style for deleting a player<br>func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {<br>    guard editingStyle == .delete else { return }<br><br>    // Present a confirmation alert<br>    let alertController = UIAlertController(title: &quot;Delete player&quot;, message: &quot;Are you sure you want to delete the selected player?&quot;, preferredStyle: .alert)<br>    let confirmAction = UIAlertAction(title: &quot;Delete&quot;, style: .destructive) { [weak self] _ in<br>        self?.handleDeletePlayerConfirmation(forRowAt: indexPath)<br>    }<br>    alertController.addAction(confirmAction)<br><br>    let cancelAction = UIAlertAction(title: &quot;Cancel&quot;, style: .cancel, handler: nil)<br>    alertController.addAction(cancelAction)<br>    present(alertController, animated: true, completion: nil)<br>}<br><br>/// Handles the confirmation of player deletion<br>private func handleDeletePlayerConfirmation(forRowAt indexPath: IndexPath) {<br>    requestDeletePlayer(at: indexPath) { [weak self] result in<br>        guard result, let self = self else { return }<br><br>        // In case the service succeeded, delete locally the player<br>        self.playerTableView.beginUpdates()<br>        self.viewModel.deleteLocallyPlayer(at: indexPath)<br><br>        self.playerTableView.deleteRows(at: [indexPath], with: .fade)<br>        self.playerTableView.endUpdates()<br><br>        // Check if we need to display the empty view in case we haven&#39;t any players left<br>        if self.viewModel.playersCollectionIsEmpty {<br>            self.showEmptyView()<br>        }<br>    }<br>}<br><br>/// Requests the deletion of a player from the server<br>private func requestDeletePlayer(at indexPath: IndexPath, completion: @escaping (Bool) -&gt; Void) {<br>    viewModel.requestDeletePlayer(at: indexPath) { [weak self] error in<br>        DispatchQueue.main.async {<br>            self?.hideLoadingView()<br>            if let error = error {<br>                self?.handleServiceFailures(withError: error)<br>                completion(false)<br>            } else {<br>                completion(true)<br>            }<br>        }<br>    }<br>}</pre><p>The navigation to <strong>Confirm / Detail</strong> and <strong>Add</strong> screens is done through performSegue. We choose PlayerListViewModel to be responsible to create the next screens view models and inject them in prepareForSegue.<br>This is not the best approach, because we violate the SRP principle, but we will see in the <strong>Coordinator</strong> article how we can solve this problem.</p><pre>/// Prepares for navigation by configuring the destination view controller<br>override func prepare(for segue: UIStoryboardSegue, sender: Any?) {<br>    switch segue.identifier {<br>    case SegueIdentifier.confirmPlayers.rawValue:<br>        if let confirmPlayersViewController = segue.destination as? ConfirmPlayersViewController {<br>            // Configure confirm players screen with its view model<br>            confirmPlayersViewController.viewModel = viewModel.makeConfirmPlayersViewModel()<br>        }<br><br>    case SegueIdentifier.playerDetails.rawValue:<br>        if let playerDetailsViewController = segue.destination as? PlayerDetailViewController,<br>            let player = viewModel.selectedPlayerForDetails {<br>            // Configure player details screen with delegate and view model<br>            playerDetailsViewController.delegate = self<br>            playerDetailsViewController.viewModel = PlayerDetailViewModel(player: player)<br>        }<br>    case SegueIdentifier.addPlayer.rawValue:<br>        // Configure add player screen with delegate<br>        (segue.destination as? PlayerAddViewController)?.delegate = self<br>    default:<br>        break<br>    }<br>}</pre><p>PlayerListViewModel is rather big and contains a lot of properties and methods that are exposed to the <strong>View</strong>, all of them mandatory.<br>For the sake of the demo, we will leave it like it is and let the desired refactoring as an exercise to the readers. You could:</p><ul><li>separate PlayerListViewController in multiple ViewControllers / ViewModels, all handled by a parent or container view controller.</li><li>split PlayerListViewModel in different components: by edit / list functions, service component, player selection.</li></ul><p>The ViewState (player selection and list modes) is implemented through <strong>Factory</strong> pattern:</p><pre>/// ViewModel responsible for managing player list state and interactions<br>final class PlayerListViewModel {<br>    <br>    // MARK: - Properties<br>    <br>    /// Current view state (list or selection mode)<br>    private var viewState: ViewState<br>    <br>    /// Details for the current view state, created on-demand<br>    private var viewStateDetails: LoginViewStateDetails {<br>        return ViewStateDetailsFactory.makeViewStateDetails(from: viewState)<br>    }<br>}<br><br>// MARK: - ViewState Definition<br>extension PlayerListViewModel {<br>    /// Represents the possible view states for the player list<br>    enum ViewState {<br>        /// Default state showing the list of players<br>        case list<br>        /// State for selecting players for a gather<br>        case selection<br>        <br>        /// Toggles between list and selection states<br>        mutating func toggle() {<br>            self = self == .list ? .selection : .list<br>        }<br>    }<br>}</pre><p>And the concrete classes for list and selection:</p><pre>/// Protocol defining the interface for view state details<br>protocol LoginViewStateDetails {<br>    /// Title for the navigation bar button<br>    var barButtonItemTitle: String { get }<br><br>    /// Whether the action button should be enabled<br>    var actionButtonIsEnabled: Bool { get }<br><br>    /// Title for the action button<br>    var actionButtonTitle: String { get }<br><br>    /// Identifier for the segue to be performed<br>    var segueIdentifier: String { get }<br>}<br><br>// MARK: - View State Implementations<br>fileprivate extension PlayerListViewModel {<br>    <br>    /// Details for list view mode<br>    struct ListViewStateDetails: LoginViewStateDetails {<br>        var barButtonItemTitle: String {<br>            return &quot;Select&quot;<br>        }<br>        <br>        var actionButtonIsEnabled: Bool {<br>            return false<br>        }<br>        <br>        var segueIdentifier: String {<br>            // Bound to the add player action<br>            return SegueIdentifier.addPlayer.rawValue<br>        }<br>        <br>        var actionButtonTitle: String {<br>            return &quot;Add player&quot;<br>        }<br>    }<br>    /// Details for selection view mode<br>    struct SelectionViewStateDetails: LoginViewStateDetails {<br>        var barButtonItemTitle: String {<br>            return &quot;Cancel&quot;<br>        }<br>        <br>        var actionButtonIsEnabled: Bool {<br>            return true<br>        }<br>        <br>        var segueIdentifier: String {<br>            return SegueIdentifier.confirmPlayers.rawValue<br>        }<br>        <br>        var actionButtonTitle: String {<br>            return &quot;Confirm players&quot;<br>        }<br>    }<br>    /// Factory for creating view state details<br>    enum ViewStateDetailsFactory {<br>        /// Creates the appropriate view state details based on the current view state<br>        static func makeViewStateDetails(from viewState: ViewState) -&gt; LoginViewStateDetails {<br>            switch viewState {<br>            case .list:<br>                return ListViewStateDetails()<br>                <br>            case .selection:<br>                return SelectionViewStateDetails()<br>            }<br>        }<br>    }<br>}</pre><p>The service methods are easy to read:</p><pre>/// Fetches players from the service and updates the local collection<br>func fetchPlayers(completion: @escaping (Error?) -&gt; ()) {<br>    playersService.get { [weak self] (result: Result&lt;[PlayerResponseModel], Error&gt;) in<br>        switch result {<br>        case .failure(let error):<br>            completion(error)<br>            <br>        case .success(let players):<br>            self?.players = players<br>            completion(nil)<br>        }<br>    }<br>}<br><br>/// Requests deletion of a player at the specified index path<br>func requestDeletePlayer(at indexPath: IndexPath, completion: @escaping (Error?) -&gt; Void) {<br>    let player = players[indexPath.row]<br>    var service = playersService<br>    <br>    service.delete(withID: ResourceID.integer(player.id)) { result in<br>        switch result {<br>        case .failure(let error):<br>            completion(error)<br>            <br>        case .success(_):<br>            completion(nil)<br>        }<br>    }<br>}</pre><h4>PlayerAddViewController - defines the Add players screen.</h4><p>Once a player was created, we use delegation pattern to notify <strong>Player Add</strong> screen and pop the view controller. The service call resides in the view model.</p><pre>/// Handles the done button action for creating a new player<br>@objc private func doneAction(sender: UIBarButtonItem) {<br>    guard let playerName = playerNameTextField.text else { return }<br><br>    // Show loading indicator while creating player<br>    showLoadingView()<br>    viewModel.requestCreatePlayer(name: playerName) { [weak self] playerWasCreated in<br>        DispatchQueue.main.async {<br>            self?.hideLoadingView()<br>            if !playerWasCreated {<br>                self?.handleServiceFailure()<br>            } else {<br>                self?.handleServiceSuccess()<br>            }<br>        }<br>    }<br>}<br><br>/// Handles service failure by showing an error alert<br>private func handleServiceFailure() {<br>    AlertHelper.present(in: self, title: &quot;Error update&quot;, message: &quot;Unable to create player. Please try again.&quot;)<br>}<br><br>/// Handles service success by notifying delegate and dismissing view<br>private func handleServiceSuccess() {<br>    delegate?.playerWasAdded()<br>    navigationController?.popViewController(animated: true)<br>}<br><br>// MARK: - ViewModel<br>/// ViewModel responsible for managing player creation<br>struct PlayerAddViewModel {<br>    /// Service used for player creation requests<br>    private let service: StandardNetworkService<br>    <br>    /// Initializes the view model with a network service<br>    init(service: StandardNetworkService = StandardNetworkService(resourcePath: &quot;/api/players&quot;, authenticated: true)) {<br>        self.service = service<br>    }<br>    <br>    /// The title displayed in the navigation bar<br>    var title: String {<br>        return &quot;Add Player&quot;<br>    }<br>    <br>    /// Requests the creation of a new player<br>    func requestCreatePlayer(name: String, completion: @escaping (Bool) -&gt; Void) {<br>        let player = PlayerCreateModel(name: name)<br>        service.create(player) { result in<br>            if case .success(_) = result {<br>                completion(true)<br>            } else {<br>                completion(false)<br>            }<br>        }<br>    }<br>    <br>    /// Determines if the done button should be enabled based on text input<br>    func doneButtonIsEnabled(forText text: String?) -&gt; Bool {<br>        return text?.isEmpty == false<br>    }<br>}</pre><h4>PlayerDetailViewController defines the Details screen</h4><p>The view model is created and passed in the PlayerListViewController&#39;s method, prepareForSegue.<br>We use the same approach when navigating to PlayerEditViewController:</p><pre>/// Prepares for navigation to the PlayerEditViewController<br>override func prepare(for segue: UIStoryboardSegue, sender: Any?) {<br>    guard segue.identifier == SegueIdentifier.editPlayer.rawValue,<br>          let destinationViewController = segue.destination as? PlayerEditViewController else {<br>        return<br>    }<br><br>    // Configure the edit player screen with its view model and delegate<br>    destinationViewController.viewModel = viewModel.makeEditViewModel()<br>    destinationViewController.delegate = self<br>}</pre><p>Displaying the player’s details is done similar as we have in <strong>PlayerList</strong> screen: the <strong>View</strong> asks the <strong>ViewModel</strong> for the properties and sets the labels’ text.</p><pre>// MARK: - UITableView DataSource &amp; Delegate<br>extension PlayerDetailViewController: UITableViewDelegate, UITableViewDataSource {<br>    /// Returns the number of sections in the table view<br>    func numberOfSections(in tableView: UITableView) -&gt; Int {<br>        return viewModel.numberOfSections<br>    }<br><br>    /// Returns the number of rows in the specified section<br>    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {<br>        return viewModel.numberOfRowsInSection(section)<br>    }<br>    <br>    /// Configures and returns a cell for the specified index path<br>    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {<br>        guard let cell = tableView.dequeueReusableCell(withIdentifier: &quot;PlayerDetailTableViewCell&quot;) as? PlayerDetailTableViewCell else {<br>            return UITableViewCell()<br>        }<br>        // Configure cell with view model data<br>        cell.leftLabel.text = viewModel.rowTitleDescription(for: indexPath)<br>        cell.rightLabel.text = viewModel.rowValueDescription(for: indexPath)<br>        return cell<br>    }<br><br>    /// Returns the header title for the specified section<br>    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -&gt; String? {<br>        return viewModel.titleForHeaderInSection(section)<br>    }<br><br>    /// Handles row selection by navigating to edit screen<br>    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {<br>        viewModel.selectPlayerRow(at: indexPath)<br>        performSegue(withIdentifier: SegueIdentifier.editPlayer.rawValue, sender: nil)<br>    }<br>}<br><br>// MARK: - PlayerEditViewController Delegate<br>extension PlayerDetailViewController: PlayerEditViewControllerDelegate {<br>    /// Handles completion of player editing<br>    func didFinishEditing(player: PlayerResponseModel) {<br>        setupTitle()                    // Update title if player name changed<br>        viewModel.updatePlayer(player)  // Update local player model<br>        viewModel.reloadSections()      // Rebuild sections data<br>        reloadData()                    // Refresh table view<br>        delegate?.didEdit(player: player) // Notify parent of update<br>    }<br>}</pre><p>PlayerDetailViewModel has the following properties:</p><pre>/// ViewModel responsible for managing player details display<br>final class PlayerDetailViewModel {<br>    <br>    // MARK: - Properties<br>    <br>    /// The player model being displayed in the screen<br>    private(set) var player: PlayerResponseModel<br>    <br>    /// Sections containing organized player data, created lazily<br>    private lazy var sections = makeSections()<br>    <br>    /// Currently selected player row information<br>    private(set) var selectedPlayerRow: PlayerRow?<br>    <br>    // MARK: - Initialization<br>    <br>    /// Initializes the view model with a player model<br>    init(player: PlayerResponseModel) {<br>        self.player = player<br>    }<br>}</pre><h4>PlayerEditViewController</h4><p>The segue to display the <strong>Edit</strong> screen is triggered from <strong>PlayerDetails</strong> screen. This is the place where you can edit the players details.<br>The <strong>ViewModel</strong> is passed from PlayerDetailsViewController.<br>Following the same approach, we moved all server API interaction, plus the model handling, in the <strong>ViewModel</strong>.</p><p>The edit text field is configured based on the <strong>ViewModel</strong>’s properties:</p><pre>/// Configures the player edit text field with initial values and behavior<br>private func setupPlayerEditTextField() {<br>    // Set initial text values<br>    playerEditTextField.placeholder = viewModel.playerRowValue<br>    playerEditTextField.text = viewModel.playerRowValue<br>    <br>    // Configure editing behavior<br>    playerEditTextField.addTarget(<br>        self, <br>        action: #selector(textFieldDidChange), <br>        for: .editingChanged<br>    )<br>    <br>    // Hide field if in selection mode<br>    playerEditTextField.isHidden = viewModel.isSelectionViewType<br>}</pre><p>When we are done with editing the player’s information, we ask the view model to perform the server updates and after it’s done, we handle the success or failure responses.<br>In case we have a failure, we inform the user, and in case the server call was successful, we notify the delegate and pop this view controller from the view controllers stack.</p><pre>/// Handles the done button action for updating player information<br>@objc private func doneAction(sender: UIBarButtonItem) {<br>    guard viewModel.shouldUpdatePlayer(<br>          inputFieldValue: playerEditTextField.text<br>    ) else { return }<br><br>    // Show loading indicator while updating<br>    showLoadingView()<br>    // Attempt to update player with current field value<br>    viewModel.updatePlayerBasedOnViewType(inputFieldValue: playerEditTextField.text) { [weak self] updated in<br>        DispatchQueue.main.async {<br>            self?.hideLoadingView()<br>            if updated {<br>                self?.handleSuccessfulPlayerUpdate()<br>            } else {<br>                self?.handleServiceError()<br>            }<br>        }<br>    }<br>}<br><br>/// Handles successful player update by notifying delegate and dismissing view<br>private func handleSuccessfulPlayerUpdate() {<br>    delegate?.didFinishEditing(player: viewModel.editablePlayer)<br>    navigationController?.popViewController(animated: true)<br>}<br><br>/// Handles update failure by showing an error alert<br>private func handleServiceError() {<br>    AlertHelper.present(<br>        in: self,<br>        title: &quot;Error update&quot;,<br>        message: &quot;Unable to update player. Please try again.&quot;<br>    )<br>}</pre><p>PlayerEditViewModel is similar with the rest, most important methods would be the player update ones:</p><pre>/// Checks if the entered value in the field is different from the old value<br>func shouldUpdatePlayer(inputFieldValue: String?) -&gt; Bool {<br>    if isSelectionViewType {<br>        return newValueIsDifferentFromOldValue(newFieldValue: selectedItemValue)<br>    }<br>    <br>    return newValueIsDifferentFromOldValue(newFieldValue: inputFieldValue)<br>}<br><br>private func newValueIsDifferentFromOldValue(newFieldValue: String?) -&gt; Bool {<br>    guard let newFieldValue = newFieldValue else { return false }<br>    <br>    return playerEditModel.playerRow.value.lowercased() != newFieldValue.lowercased()<br>}<br><br>/// There are two different ways to update player information.<br>/// One is through the input / textField where you can type, for example the name or age of the player<br>/// and the other one is through selection where you can choose a different option (applied to player&#39;s position or skill).<br>private var selectedItemValue: String? {<br>    guard let playerItemsEditModel = playerItemsEditModel else { return nil }<br>    <br>    return playerItemsEditModel.items[playerItemsEditModel.selectedItemIndex]<br>}<br><br>/// Decides what needs to be updated (if inputFieldValue is nil, then it will update the player through selection mode).<br>func updatePlayerBasedOnViewType(inputFieldValue: String?, completion: @escaping (Bool) -&gt; ()) {<br>    if isSelectionViewType {<br>        updatePlayer(newFieldValue: selectedItemValue, completion: completion)<br>    } else {<br>        updatePlayer(newFieldValue: inputFieldValue, completion: completion)<br>    }<br>}<br><br>private func updatePlayer(newFieldValue: String?, completion: @escaping (Bool) -&gt; ()) {<br>    guard let newFieldValue = newFieldValue else {<br>        completion(false)<br>        return<br>    }<br>    <br>    playerEditModel.player.update(usingField: playerEditModel.playerRow.editableField, value: newFieldValue)<br>    requestUpdatePlayer(completion: completion)<br>}<br><br>/// Performs the player service update call<br>private func requestUpdatePlayer(completion: @escaping (Bool) -&gt; ()) {<br>    let player = playerEditModel.player<br>    service.update(PlayerCreateModel(player), resourceID: ResourceID.integer(player.id)) { [weak self] result in<br>        if case .success(let updated) = result {<br>            self?.playerEditModel.player = player<br>            completion(updated)<br>        } else {<br>            completion(false)<br>        }<br>    }<br>}</pre><h4>ConfirmPlayersViewController</h4><p>Before reaching <strong>Gather</strong> screen, we have to confirm the selected players. This screen is defined by ConfirmPlayersViewController.<br>In viewDidLoad we setup the UI elements, such as the table view and configure the start gather button:</p><pre>/// Configures initial view states and appearance<br>func setupViews() {<br>    // Configure table view editing state<br>    playerTableView.isEditing = viewModel.playerTableViewIsEditing<br>    <br>    // Setup gather button configuration<br>    configureStartGatherButton()<br>}</pre><p>The server API call is presented below:</p><pre>/// Handles the action to start a new gather<br>@IBAction private func startGather(_ sender: Any) {<br>    // Show loading indicator while creating gather<br>    showLoadingView()<br><br>    viewModel.startGather { [weak self] result in<br>        DispatchQueue.main.async {<br>            self?.hideLoadingView()<br>            if !result {<br>                self?.handleServiceFailure()<br>            } else {<br>                self?.performSegue(<br>                    withIdentifier: SegueIdentifier.gather.rawValue,<br>                    sender: nil<br>                )<br>            }<br>        }<br>    }<br>}<br><br>/// Handles gather creation failure by showing an error alert<br>private func handleServiceFailure() {<br>    AlertHelper.present(<br>        in: self,<br>        title: &quot;Error&quot;,<br>        message: &quot;Unable to create gather.&quot;<br>    )<br>}</pre><p>And the <strong>TableView Delegate</strong> and <strong>DataSource</strong>:</p><pre>// MARK: - UITableViewDelegate &amp; UITableViewDataSource<br>extension ConfirmPlayersViewController: UITableViewDelegate, UITableViewDataSource {<br>    /// Returns the number of sections in the table view<br>    func numberOfSections(in tableView: UITableView) -&gt; Int {<br>        return viewModel.numberOfSections<br>    }<br><br>    /// Returns the header title for the specified section<br>    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -&gt; String? {<br>        return viewModel.titleForHeaderInSection(section)<br>    }<br><br>    /// Returns the number of rows in the specified section<br>    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {<br>        return viewModel.numberOfRowsInSection(section)<br>    }<br><br>    /// Returns the editing style for a row<br>    func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -&gt; UITableViewCell.EditingStyle {<br>        return .none<br>    }<br><br>    /// Determines if a row should be indented while editing<br>    func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -&gt; Bool {<br>        return false<br>    }<br><br>    /// Configures and returns a cell for the specified index path<br>    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {<br>        guard let cell = tableView.dequeueReusableCell(withIdentifier: &quot;PlayerChooseTableViewCellId&quot;) else {<br>            return UITableViewCell()<br>        }<br>        // Configure cell with view model data<br>        cell.textLabel?.text = viewModel.rowTitle(at: indexPath)<br>        cell.detailTextLabel?.text = viewModel.rowDescription(at: indexPath)<br>        return cell<br>    }<br><br>    /// Handles row movement within the table view<br>    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {<br>        viewModel.moveRowAt(sourceIndexPath: sourceIndexPath, to: destinationIndexPath)<br>        configureStartGatherButton()<br>    }<br>}</pre><p>ConfirmPlayersViewModel contains the playersDictionary with the selected players and their teams, the services needed to add players to a gather and to start the gather, the gatherUUID which is defined after a gather is created on the server and a dispatchGroup to orchestrate the multiple server calls.</p><pre>/// ViewModel responsible for managing player confirmation and gather creation<br>final class ConfirmPlayersViewModel {<br>    <br>    // MARK: - Properties<br>    <br>    /// Dictionary mapping teams to their selected players<br>    private var playersDictionary: [TeamSection: [PlayerResponseModel]]<br>    <br>    /// Service for adding players to a gather<br>    private var addPlayerService: AddPlayerToGatherService<br>    <br>    /// Service for gather-related network operations<br>    private let gatherService: StandardNetworkService<br>    <br>    /// Group for coordinating multiple player additions<br>    private let dispatchGroup = DispatchGroup()<br>    <br>    /// UUID of the created gather<br>    private var gatherUUID: UUID?<br>    <br>    // MARK: - Initialization<br>    <br>    /// Initializes the view model with players and services<br>    init(playersDictionary: [TeamSection: [PlayerResponseModel]] = [:],<br>         addPlayerService: AddPlayerToGatherService = AddPlayerToGatherService(),<br>         gatherService: StandardNetworkService = StandardNetworkService(<br>             resourcePath: &quot;/api/gathers&quot;,<br>             authenticated: true<br>         )) {<br>        self.playersDictionary = playersDictionary<br>        self.addPlayerService = addPlayerService<br>        self.gatherService = gatherService<br>    }<br>}</pre><p>The most complex thing from this class is the server API interaction when starting a gather:</p><pre>/// Initiates the gather creation process and adds selected players<br>func startGather(completion: @escaping (Bool) -&gt; ()) {<br>    createGather { [weak self] uuid in<br>        guard let gatherUUID = uuid else {<br>            completion(false)<br>            return<br>        }<br>        <br>        // Store UUID and add players to the created gather<br>        self?.gatherUUID = gatherUUID<br>        self?.addPlayersToGather(havingUUID: gatherUUID, completion: completion)<br>    }<br>}<br><br>/// Creates a new gather on the server<br>private func createGather(completion: @escaping (UUID?) -&gt; Void) {<br>    gatherService.create(GatherCreateModel()) { result in<br>        if case let .success(ResourceID.uuid(gatherUUID)) = result {<br>            completion(gatherUUID)<br>        } else {<br>            completion(nil)<br>        }<br>    }<br>}<br><br>/// Adds all selected players to the gather using a dispatch group for coordination<br>private func addPlayersToGather(havingUUID gatherUUID: UUID, completion: @escaping (Bool) -&gt; ()) {<br>    var serviceFailed = false<br>    <br>    playerTeamArray.forEach { playerTeam in<br>        dispatchGroup.enter()<br>        <br>        self.addPlayer(playerTeam.player, <br>                      toGatherHavingUUID: gatherUUID, <br>                      team: playerTeam.team) { [weak self] playerWasAdded in<br>            if !playerWasAdded {<br>                serviceFailed = true<br>            }<br>            <br>            self?.dispatchGroup.leave()<br>        }<br>    }<br>    <br>    // Wait for all player additions to complete<br>    dispatchGroup.notify(queue: DispatchQueue.main) {<br>        completion(serviceFailed)<br>    }<br>}<br><br>/// Converts the players dictionary into an array of player-team pairs<br>private var playerTeamArray: [PlayerTeamModel] {<br>    var players: [PlayerTeamModel] = []<br>    <br>    // Add Team A players<br>    players += self.playersDictionary<br>        .filter { $0.key == .teamA }<br>        .flatMap { $0.value }<br>        .map { PlayerTeamModel(team: .teamA, player: $0) }<br>    <br>    // Add Team B players<br>    players += self.playersDictionary<br>        .filter { $0.key == .teamB }<br>        .flatMap { $0.value }<br>        .map { PlayerTeamModel(team: .teamB, player: $0) }<br>    <br>    return players<br>}<br><br>/// Adds a single player to the gather with their team assignment<br>private func addPlayer(_ player: PlayerResponseModel,<br>                      toGatherHavingUUID gatherUUID: UUID,<br>                      team: TeamSection,<br>                      completion: @escaping (Bool) -&gt; Void) {<br>    addPlayerService.addPlayer(<br>        havingServerId: player.id,<br>        toGatherWithId: gatherUUID,<br>        team: PlayerGatherTeam(team: team.headerTitle)<br>    ) { result in<br>        if case let .success(resultValue) = result {<br>            completion(resultValue)<br>        } else {<br>            completion(false)<br>        }<br>    }<br>}</pre><h4>GatherViewController</h4><p>Finally, we have GatherViewController, belonging to the most important screen from FootballGather.<br>We manage to clean the properties and left the IBOutlets, plus the loading view and the view model:</p><pre>/// View controller responsible for managing the gather screen and its interactions<br>final class GatherViewController: UIViewController, Loadable {<br>    // MARK: - IBOutlets<br>    <br>    /// Table view displaying player information<br>    @IBOutlet weak var playerTableView: UITableView!<br>    <br>    /// View displaying the current score<br>    @IBOutlet weak var scoreLabelView: ScoreLabelView!<br>    <br>    /// Stepper control for adjusting scores<br>    @IBOutlet weak var scoreStepper: ScoreStepper!<br>    <br>    /// Label displaying the current timer value<br>    @IBOutlet weak var timerLabel: UILabel!<br>    <br>    /// Container view for timer-related controls<br>    @IBOutlet weak var timerView: UIView!<br>    <br>    /// Picker view for selecting timer duration<br>    @IBOutlet weak var timePickerView: UIPickerView!<br>    <br>    /// Button for controlling timer actions<br>    @IBOutlet weak var actionTimerButton: UIButton!<br>    <br>    // MARK: - Properties<br>    <br>    /// Loading indicator view<br>    lazy var loadingView = LoadingView.initToView(self.view)<br>    <br>    /// View model managing gather business logic<br>    var viewModel: GatherViewModel!<br>}</pre><p>In viewDidLoad, we setup and configure the views:</p><pre>override func viewDidLoad() {<br>    super.viewDidLoad()<br><br>    setupViewModel()<br>    setupTitle()<br>    configureSelectedTime()<br>    hideTimerView()<br>    configureTimePickerView()<br>    configureActionTimerButton()<br>    setupScoreStepper()<br>    reloadData()<br>}<br><br>/// Sets the view controller&#39;s title from the view model<br>private func setupTitle() {<br>    title = viewModel.title<br>}<br><br>/// Configures the view model delegate<br>private func setupViewModel() {<br>    viewModel.delegate = self<br>}<br><br>/// Configures the timer label with formatted text<br>private func configureSelectedTime() {<br>    timerLabel?.text = viewModel.formattedCountdownTimerLabelText<br>}<br><br>/// Configures the action timer button with formatted text<br>private func configureActionTimerButton() {<br>    actionTimerButton.setTitle(viewModel.formattedActionTitleText, for: .normal)<br>}<br><br>/// Hides the timer view<br>private func hideTimerView() {<br>    timerView.isHidden = true<br>}<br><br>/// Shows the timer view<br>private func showTimerView() {<br>    timerView.isHidden = false<br>}<br><br>/// Sets up the score stepper delegate<br>private func setupScoreStepper() {<br>    scoreStepper.delegate = self<br>}<br><br>/// Reloads data for the time picker and player table views<br>private func reloadData() {<br>    timePickerView.reloadAllComponents()<br>    playerTableView.reloadData()<br>}</pre><p>The timer related functions are looking neat:</p><pre>// MARK: - Timer Actions<br><br>/// Shows the timer picker view<br>@IBAction private func setTimer(_ sender: Any) {<br>    configureTimePickerView()<br>    showTimerView()<br>}<br><br>/// Cancels and resets the current timer<br>@IBAction private func cancelTimer(_ sender: Any) {<br>    viewModel.stopTimer()<br>    viewModel.resetTimer()<br>    configureSelectedTime()<br>    configureActionTimerButton()<br>    hideTimerView()<br>}<br><br>/// Toggles the timer between running and paused states<br>@IBAction private func actionTimer(_ sender: Any) {<br>    viewModel.toggleTimer()<br>    configureActionTimerButton()<br>}<br><br>/// Dismisses the timer picker view without saving<br>@IBAction private func timerCancel(_ sender: Any) {<br>    hideTimerView()<br>}<br><br>/// Saves the selected timer duration and starts the timer<br>@IBAction private func timerDone(_ sender: Any) {<br>    viewModel.stopTimer()<br>    viewModel.setTimerMinutes(selectedMinutesRow)<br>    viewModel.setTimerSeconds(selectedSecondsRow)<br>    configureSelectedTime()<br>    configureActionTimerButton()<br>    hideTimerView()<br>}<br><br>// MARK: - Timer Picker Helpers<br>/// Returns the currently selected minutes from the picker view<br>private var selectedMinutesRow: Int { <br>    timePickerView.selectedRow(inComponent: viewModel.minutesComponent) <br>}<br><br>/// Returns the currently selected seconds from the picker view<br>private var selectedSecondsRow: Int { <br>    timePickerView.selectedRow(inComponent: viewModel.secondsComponent) <br>}</pre><p>And the endGather API interaction:</p><pre>// MARK: - Gather Control Actions<br><br>/// Presents a confirmation alert before ending the gather<br>@IBAction private func endGather(_ sender: Any) {<br>    let alertController = UIAlertController(<br>        title: &quot;End Gather&quot;,<br>        message: &quot;Are you sure you want to end the gather?&quot;,<br>        preferredStyle: .alert<br>    )<br>    <br>    let confirmAction = UIAlertAction(title: &quot;Yes&quot;, style: .default) { [weak self] _ in<br>        self?.endGather()<br>    }<br>    alertController.addAction(confirmAction)<br>    let cancelAction = UIAlertAction(title: &quot;Cancel&quot;, style: .cancel, handler: nil)<br>    alertController.addAction(cancelAction)<br>    present(alertController, animated: true, completion: nil)<br>}<br><br>/// Ends the current gather and updates the final scores<br>private func endGather() {<br>    guard let scoreTeamAString = scoreLabelView.teamAScoreLabel.text,<br>          let scoreTeamBString = scoreLabelView.teamBScoreLabel.text else {<br>        return<br>    }<br>    showLoadingView()<br>    viewModel.endGather(<br>        teamAScoreLabelText: scoreTeamAString,<br>        teamBScoreLabelText: scoreTeamBString<br>    ) { [weak self] updated in<br>        DispatchQueue.main.async {<br>            self?.hideLoadingView()<br>            <br>            if !updated {<br>                self?.handleServiceFailure()<br>            } else {<br>                self?.handleServiceSuccess()<br>            }<br>        }<br>    }<br>}<br><br>/// Displays an error alert when the gather update fails<br>private func handleServiceFailure() {<br>    AlertHelper.present(<br>        in: self,<br>        title: &quot;Error update&quot;,<br>        message: &quot;Unable to update gather. Please try again.&quot;<br>    )<br>}<br><br>/// Handles successful gather completion by returning to the player list<br>private func handleServiceSuccess() {<br>    guard let playerListTogglable = navigationController?.viewControllers<br>        .first(where: { $0 is PlayerListTogglable }) as? PlayerListTogglable else {<br>        return<br>    }<br><br>    playerListTogglable.toggleViewState()<br>    if let playerListViewController = playerListTogglable as? UIViewController {<br>        navigationController?.popToViewController(<br>            playerListViewController,<br>            animated: true<br>        )<br>    }<br>}</pre><p>The table view’s <strong>DataSource</strong> and <strong>Delegate</strong> are looking great as well, clean and simple:</p><pre>// MARK: - UITableViewDelegate | UITableViewDataSource<br>extension GatherViewController: UITableViewDelegate, UITableViewDataSource {<br>    func numberOfSections(in tableView: UITableView) -&gt; Int {<br>        viewModel.numberOfSections<br>    }<br><br>    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -&gt; String? {<br>        viewModel.titleForHeaderInSection(section)<br>    }<br><br>    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {<br>        viewModel.numberOfRowsInSection(section)<br>    }<br><br>    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {<br>        guard let cell = tableView.dequeueReusableCell(withIdentifier: &quot;GatherCellId&quot;) else {<br>            return UITableViewCell()<br>        }<br>        let rowDescription = viewModel.rowDescription(at: indexPath)<br>        cell.textLabel?.text = rowDescription.title<br>        cell.detailTextLabel?.text = rowDescription.details<br>        return cell<br>    }<br>}</pre><p>And the rest of the methods:</p><pre>// MARK: - ScoreStepperDelegate<br>extension GatherViewController: ScoreStepperDelegate {<br>    func stepper(_ stepper: UIStepper, didChangeValueForTeam team: TeamSection, newValue: Double) {<br>        if viewModel.shouldUpdateTeamALabel(section: team) {<br>            scoreLabelView.teamAScoreLabel.text = viewModel.formatStepperValue(newValue)<br>        } else if viewModel.shouldUpdateTeamBLabel(section: team) {<br>            scoreLabelView.teamBScoreLabel.text = viewModel.formatStepperValue(newValue)<br>        }<br>    }<br>}<br><br>// MARK: - UIPickerViewDataSource<br>extension GatherViewController: UIPickerViewDataSource, UIPickerViewDelegate {<br>    func numberOfComponents(in pickerView: UIPickerView) -&gt; Int {<br>        viewModel.numberOfPickerComponents<br>    }<br><br>    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -&gt; Int {<br>        viewModel.numberOfRowsInPickerComponent(component)<br>    }<br><br>    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -&gt; String? {<br>        viewModel.titleForPickerRow(row, forComponent: component)<br>    }<br>}<br><br>// MARK: - GatherViewModelDelegate<br>extension GatherViewController: GatherViewModelDelegate {<br>    func didUpdateGatherTime() {<br>        configureSelectedTime()<br>    }<br>}</pre><p>Cleaning the <strong>ViewController</strong> came with some downsides in the <strong>ViewModel</strong> class. It has a lot of methods and the class is big (around 200 lines of code).</p><p>We decided to move out the <strong>Timer</strong> interaction into a new struct, called GatherTimeHandler.<br>In this struct, we expose selectedTime which is set from outside of the class, and has two more variables: the timer and a state variable (can be stopped, running or paused).<br>The public API has methods such as stop, reset and toggle timer, as well as decrementTime:</p><pre>mutating func decrementTime() {<br>    if selectedTime.seconds == 0 {<br>        decrementMinutes()<br>    } else {<br>        decrementSeconds()<br>    }<br><br>    if selectedTimeIsZero {<br>        stopTimer()<br>    }<br>}<br>private mutating func decrementMinutes() {<br>    selectedTime.minutes -= 1<br>    selectedTime.seconds = 59<br>}<br><br>private mutating func decrementSeconds() {<br>    selectedTime.seconds -= 1<br>}<br><br>private var selectedTimeIsZero: Bool {<br>    return selectedTime.seconds == 0 &amp;&amp; selectedTime.minutes == 0<br>}</pre><p>Overall, this is looking much better from the first iteration where we implemented the app via MVC.</p><h3>Testing our business logic</h3><p>The most important part is the <strong>ViewModel</strong>. Here, we have implemented the business logic.</p><p>Testing the title:</p><pre>func testTitle_whenViewModelIsAllocated_isNotEmpty() {<br>    // given<br>    let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>    let sut = GatherViewModel(gatherModel: mockGatherModel)<br>    <br>    // when<br>    let title = sut.title<br>    <br>    // then<br>    XCTAssertFalse(title.isEmpty)<br>}</pre><p>Testing the formatted countdown timer label text:</p><pre>func testFormattedCountdownTimerLabelText_whenViewModelIsAllocated_returnsDefaultTime() {<br>      // given<br>      let gatherTime = GatherTime.defaultTime<br>      // Define the expected values, format should be 00:00.<br>      let expectedFormattedMinutes = gatherTime.minutes &lt; 10 ? &quot;0\(gatherTime.minutes)&quot; : &quot;\(gatherTime.minutes)&quot;<br>      let expectedFormattedSeconds = gatherTime.seconds &lt; 10 ? &quot;0\(gatherTime.seconds)&quot; : &quot;\(gatherTime.seconds)&quot;<br>      let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>      let sut = GatherViewModel(gatherModel: mockGatherModel)<br>      <br>      // when<br>      let formattedCountdownTimerLabelText = sut.formattedCountdownTimerLabelText<br>      <br>      // then<br>      XCTAssertEqual(formattedCountdownTimerLabelText, &quot;\(expectedFormattedMinutes):\(expectedFormattedSeconds)&quot;)<br>  }<br>  <br>  func testFormattedCountdownTimerLabelText_whenTimeIsZero_returnsZeroSecondsZeroMinutes() {<br>      // given<br>      let mockGatherTime = GatherTime(minutes: 0, seconds: 0)<br>      let mockGatherTimeHandler = GatherTimeHandler(selectedTime: mockGatherTime)<br>      let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>      let sut = GatherViewModel(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br>      <br>      // when<br>      let formattedCountdownTimerLabelText = sut.formattedCountdownTimerLabelText<br>      <br>      // then<br>      XCTAssertEqual(formattedCountdownTimerLabelText, &quot;00:00&quot;)<br>  }<br>  <br>  func testFormattedCountdownTimerLabelText_whenTimeHasMinutesAndZeroSeconds_returnsMinutesAndZeroSeconds() {<br>      // given<br>      let mockGatherTime = GatherTime(minutes: 10, seconds: 0)<br>      let mockGatherTimeHandler = GatherTimeHandler(selectedTime: mockGatherTime)<br>      let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>      let sut = GatherViewModel(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br>      <br>      // when<br>      let formattedCountdownTimerLabelText = sut.formattedCountdownTimerLabelText<br>      <br>      // then<br>      XCTAssertEqual(formattedCountdownTimerLabelText, &quot;10:00&quot;)<br>  }<br>  <br>  func testFormattedCountdownTimerLabelText_whenTimeHasSecondsAndZeroMinutes_returnsSecondsAndZeroMinutes() {<br>      // given<br>      let mockGatherTime = GatherTime(minutes: 0, seconds: 10)<br>      let mockGatherTimeHandler = GatherTimeHandler(selectedTime: mockGatherTime)<br>      let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>      let sut = GatherViewModel(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br>      <br>      // when<br>      let formattedCountdownTimerLabelText = sut.formattedCountdownTimerLabelText<br>      <br>      // then<br>      XCTAssertEqual(formattedCountdownTimerLabelText, &quot;00:10&quot;)<br>  }</pre><p>Testing the action title text, that should be <strong>Start</strong>, <strong>Resume</strong> or <strong>Pause</strong>.</p><pre>// We set the state to be initially .paused<br>func testFormattedActionTitleText_whenStateIsPaused_returnsResume() {<br>    // given<br>    let mockGatherTimeHandler = GatherTimeHandler(state: .paused)<br>    let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>    let sut = GatherViewModel(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br><br>    // when<br>    let formattedActionTitleText = sut.formattedActionTitleText<br>    <br>    // then<br>    XCTAssertEqual(formattedActionTitleText, &quot;Resume&quot;)<br>}</pre><p>We follow the same approach for <strong>Pause</strong> and <strong>Start</strong>:</p><pre>func testFormattedActionTitleText_whenStateIsRunning_returnsPause() {<br>      // given<br>      let mockGatherTimeHandler = GatherTimeHandler(state: .running)<br>      let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>      let sut = GatherViewModel(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br>      <br>      // when<br>      let formattedActionTitleText = sut.formattedActionTitleText<br>      <br>      // then<br>      XCTAssertEqual(formattedActionTitleText, &quot;Pause&quot;)<br>}<br>      <br>func testFormattedActionTitleText_whenStateIsStopped_returnsStart() {<br>      // given<br>      let mockGatherTimeHandler = GatherTimeHandler(state: .stopped)<br>      let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>      let sut = GatherViewModel(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br>      <br>      // when<br>      let formattedActionTitleText = sut.formattedActionTitleText<br>      <br>      // then<br>      XCTAssertEqual(formattedActionTitleText, &quot;Start&quot;)<br>}</pre><p>For testing the stopTimer function, we mock the system to be in a running state</p><pre>func testStopTimer_whenStateIsRunning_updatesStateToStopped() {<br>      // given<br>      let mockGatherTimeHandler = GatherTimeHandler(state: .running)<br>      let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>      let sut = GatherViewModel(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br>      <br>      // when<br>      sut.stopTimer()<br>      <br>      // then<br>      let formattedActionTitleText = sut.formattedActionTitleText<br>      XCTAssertEqual(formattedActionTitleText, &quot;Start&quot;)<br>}</pre><p>The delegates of the pickerView and tableView are very easy to test. We exemplify some unit tests below:</p><pre>func testNumberOfRowsInSection_whenViewModelHasPlayers_returnsCorrectNumberOfPlayers() {<br>        // given<br>        let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 5)<br>        let teamAPlayersCount = mockGatherModel.players.filter { $0.team == .teamA}.count<br>        let teamBPlayersCount = mockGatherModel.players.filter { $0.team == .teamB}.count<br>        let sut = GatherViewModel(gatherModel: mockGatherModel)<br>        <br>        // when<br>        let numberOfRowsInSection0 = sut.numberOfRowsInSection(0)<br>        let numberOfRowsInSection1 = sut.numberOfRowsInSection(1)<br>        <br>        // then<br>        XCTAssertEqual(numberOfRowsInSection0, teamAPlayersCount)<br>        XCTAssertEqual(numberOfRowsInSection1, teamBPlayersCount)<br>}<br><br>func testNumberOfRowsInPickerComponent_whenComponentIsMinutes_returns60() {<br>        // given<br>        let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>        let sut = GatherViewModel(gatherModel: mockGatherModel)<br>        <br>        // when<br>        let numberOfRowsInPickerComponent = sut.numberOfRowsInPickerComponent(GatherTimeHandler.Component.minutes.rawValue)<br>        <br>        // then<br>        XCTAssertEqual(numberOfRowsInPickerComponent, 60)<br>}</pre><p>For ending a gather we use the mocked endpoint and models. We verify if the received response is true:</p><pre>func testEndGather_whenScoreIsSet_updatesGather() {<br>      // given<br>      let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>      let mockEndpoint = EndpointMockFactory.makeSuccessfulEndpoint(path: resourcePath)<br>      let mockService = StandardNetworkService(session: session, urlRequest: AuthURLRequestFactory(endpoint: mockEndpoint, keychain: appKeychain))<br>      let sut = GatherViewModel(gatherModel: mockGatherModel, updateGatherService: mockService)<br>      let exp = expectation(description: &quot;Update gather expectation&quot;)<br>      <br>      // when<br>      sut.endGather(teamAScoreLabelText: &quot;1&quot;, teamBScoreLabelText: &quot;1&quot;) { gatherUpdated in<br>          XCTAssertTrue(gatherUpdated)<br>          exp.fulfill()<br>      }<br>      <br>      // then<br>      waitForExpectations(timeout: 5, handler: nil)<br>  }</pre><p>To check if the timer is toggled, we use a MockViewModelDelegate:</p><pre>private extension GatherViewModelTests {<br>  final class MockViewModelDelegate: GatherViewModelDelegate {<br>      // [1] Used to check if the delegate was called (didUpdateGatherTime())<br>      private(set) var gatherTimeWasUpdated = false<br><br>      // [2] Is fulfilled when the numberOfUpdateCalls is equal to actualUpdateCalls.<br>      // This means that the selector for the timer was called as many times as we wanted.<br>      weak var expectation: XCTestExpectation? = nil<br>      var numberOfUpdateCalls = 1<br>      private(set) var actualUpdateCalls = 0<br>      <br>      func didUpdateGatherTime() {<br>          gatherTimeWasUpdated = true<br>          actualUpdateCalls += 1 // [3] Increment the number of calls to this method<br>          <br>          if expectation != nil &amp;&amp; numberOfUpdateCalls == actualUpdateCalls {<br>              expectation?.fulfill()<br>          }<br>      }<br>  }<br>}</pre><p>And the unit test:</p><pre>func testToggleTimer_whenSelectedTimeIsValid_updatesTime() {<br>      // given<br>      let numberOfUpdateCalls = 2<br>      let mockGatherTime = GatherTime(minutes: 0, seconds: numberOfUpdateCalls)<br>      let mockGatherTimeHandler = GatherTimeHandler(selectedTime: mockGatherTime)<br>      let mockGatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 2)<br>      <br>      let exp = expectation(description: &quot;Waiting timer expectation&quot;)<br>      let mockDelegate = MockViewModelDelegate()<br>      mockDelegate.numberOfUpdateCalls = numberOfUpdateCalls<br>      mockDelegate.expectation = exp<br><br>      let sut = GatherViewModel(gatherModel: mockGatherModel, timeHandler: mockGatherTimeHandler)<br>      <br>      // when<br>      sut.delegate = mockDelegate<br>      sut.toggleTimer()<br>      <br>      // then<br>      waitForExpectations(timeout: 5) { _ in<br>          XCTAssertTrue(mockDelegate.gatherTimeWasUpdated)<br>          XCTAssertEqual(mockDelegate.actualUpdateCalls, numberOfUpdateCalls)<br>          sut.stopTimer()<br>      }<br>  }</pre><p>Compared with testing the <strong>ViewController</strong> in the MVC architecture, life becomes easier when testing the <strong>ViewModel</strong> layer. The unit tests are easy to write, easier to understand and much simpler.</p><h3>Key Metrics</h3><h4>Lines of code — View Controllers</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Fr6rZszs7V6e7QxbciTJvA.png" /></figure><h4>Lines of code — View Models</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TdcTvtiBWqox0LBeFwaPJw.png" /></figure><h4>Unit tests</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IXa5Tgzzr2QWklrid-KBRA.png" /></figure><h4>Build times</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_QhRZQlgn1oRa6I-eafohg.png" /></figure><p><em>Tests were run in iPhone 8 Simulator, with iOS 14.3, using Xcode 12.4 and on an i9 MacBook Pro 2019.</em></p><h3>Conclusion</h3><p>In this article, we’ve documented our journey of transforming our application from MVC to MVVM architecture. By introducing a dedicated layer for business logic, we’ve successfully decoupled core functionality from the View Controller, resulting in a cleaner separation of responsibilities.</p><p>The MVVM pattern proved highly effective in reducing View Controller complexity, producing more maintainable code. A particularly notable improvement was the enhanced testability of our business logic, with unit tests becoming significantly more straightforward to implement.</p><p>However, it’s important to acknowledge that implementing MVVM with UIKit presents certain challenges, as the framework wasn&#39;t originally designed with this architectural pattern in mind.</p><h4>Key Metrics and Observations</h4><p><strong>Code Distribution</strong></p><ul><li>Achieved a substantial reduction of <strong>607</strong> lines in View Controller code</li><li>Introduced <strong>1113</strong> lines of View Model code</li><li>Net increase of <strong>506</strong> lines and <strong>7</strong> new files to the codebase</li></ul><p><strong>Testing Improvements</strong></p><ul><li>Achieved higher code coverage for the Gathers feature, increasing by <strong>1.6%</strong> to reach <strong>97.3%</strong></li><li>Significantly simplified unit test implementation for business logic</li><li>Minor trade-off in test execution time, with an increase of <strong>5.1</strong> seconds</li></ul><h4>Benefits and Trade-offs</h4><p>The adoption of MVVM has delivered several key advantages:</p><ul><li>Enhanced code organization and maintainability</li><li>Improved separation of concerns</li><li>Better testability of business logic</li><li>Reduced risk of errors through cleaner architecture</li></ul><p>While the implementation required additional code and slightly longer test execution times, the benefits in terms of maintainability, testability, and code organization make MVVM a valuable architectural choice for iOS applications, particularly as they grow in complexity.</p><p>This exercise in architectural transformation has resulted in a more robust, maintainable, and testable application. For teams considering MVVM, our experience suggests that the initial investment in additional code and setup time can pay significant dividends in long-term maintainability and reliability.</p><p>Thank you for following along with our architectural journey! Please explore the resources below for additional information and practical examples.</p><h3>Useful Links</h3><ul><li>The iOS App, Football Gather — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather">GitHub Repo Link</a></li><li>The web server application made in Vapor — <a href="https://github.com/radude89/footballgather-ws">GitHub Repo Link</a></li><li>Vapor 3 Backend APIs <a href="https://radu-ionut-dan.medium.com/using-vapor-and-fluent-to-create-a-rest-api-5f9a0dcffc7b">article link</a></li><li>Migrating to Vapor 4 <a href="https://radu-ionut-dan.medium.com/migrating-to-vapor-4-53a821c29203">article link</a></li><li>Model View Controller (MVC) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVC">GitHub Repo Link</a> and <a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">article link</a></li><li>Model View ViewModel (MVVM) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVVM">GitHub Repo Link</a> and <a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">article link</a></li><li>Model View Presenter (MVP) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">article link</a></li><li>Coordinator Pattern — MVP with Coordinators (MVP-C) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP-C">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">article link</a></li><li>View Interactor Presenter Entity Router (VIPER) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIPER">GitHub Repo link</a> and <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">article link</a></li><li>View Interactor Presenter (VIP) — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIP">GitHub Repo link</a> and <a href="https://radu-ionut-dan.medium.com/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">article link</a></li><li><a href="https://www.raywenderlich.com/34-design-patterns-by-tutorials-mvvm">Book about MVVM on raywenderlich.</a></li><li><a href="https://medium.com/better-programming/mvvm-in-ios-from-net-perspective-580eb7f4f129">Article about MVVM in iOS</a></li><li><a href="https://medium.com/flawless-app-stories/how-to-use-a-model-view-viewmodel-architecture-for-ios-46963c67be1b">Article “How to not get desperate with MVVM implementation”</a></li><li><a href="https://docs.microsoft.com/en-us/archive/blogs/johngossman/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps">Introduction to Model/View/ViewModel pattern for building WPF apps</a></li><li><a href="https://www.appcoda.com/mvvm-vs-mvc/">MVVM vs MVC</a></li><li><a href="https://blog.pusher.com/mvvm-ios/">Using MVV in iOS</a></li><li><a href="https://medium.com/flawless-app-stories/practical-mvvm-rxswift-a330db6aa693">Practical MVVM + RxSwift</a></li><li><a href="https://academy.realm.io/posts/slug-max-alexander-mvvm-rxswift/">MVVM with RxSwift</a></li><li><a href="https://benoitpasquier.com/integrate-rxswift-in-mvvm/">How to integrate RxSwift in your MVVM architecture</a></li><li><a href="https://cocoacasts.com/what-are-the-benefits-of-model-view-viewmodel">What Are the Benefits of Model-View-ViewModel</a></li><li><a href="https://blogsnook.com/mvvm-pattern-advantages/">MVVM Pattern Advantages — Benefits of Using MVVM Model</a></li><li><a href="https://docs.microsoft.com/en-us/archive/blogs/johngossman/advantages-and-disadvantages-of-m-v-vm">Advantages and disadvantages of M-V-VM</a></li><li><a href="https://medium.com/swift-india/mvvm-1-a-general-discussion-764581a2d5d9">MVVM-1: A General Discussion</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bdfd07d9395e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/better-programming/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">Battle of the iOS Architecture Patterns: A Look at Model-View-ViewModel (MVVM)</a> was originally published in <a href="https://betterprogramming.pub">Better Programming</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Battle of the iOS Architecture Patterns: Model View Controller (MVC)]]></title>
            <link>https://medium.com/better-programming/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6?source=rss-dee343eb346a------2</link>
            <guid isPermaLink="false">https://medium.com/p/442241b447f6</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[soft]]></category>
            <dc:creator><![CDATA[Radu Dan]]></dc:creator>
            <pubDate>Tue, 16 Mar 2021 04:37:29 GMT</pubDate>
            <atom:updated>2025-07-12T08:51:55.071Z</atom:updated>
            <content:encoded><![CDATA[<h4>Getting started with the most common architecture pattern for iOS development</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-KmoL6YZtao1mXk9GFnxVg.png" /><figcaption>Architecture Series — Model View Controller (MVC)</figcaption></figure><h3>Motivation</h3><p>Before diving into iOS app development, it’s crucial to carefully consider the project’s architecture. We need to thoughtfully plan how different pieces of code will fit together, ensuring they remain comprehensible not just today, but months or years later when we need to revisit and modify the codebase. Moreover, a well-structured project helps establish a shared technical vocabulary among team members, making collaboration more efficient.</p><p>This article kicks off an exciting series where we’ll explore different architectural approaches by building the same application using various patterns. Throughout the series, we’ll analyze practical aspects like build times and implementation complexity, weigh the pros and cons of each pattern, and most importantly, examine real, production-ready code implementations. This hands-on approach will help you make informed decisions about which architecture best suits your project needs.</p><h3>Architecture Series Articles</h3><ul><li><strong>Model View Controller (MVC) — Current Article</strong></li><li><a href="https://betterprogramming.pub/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">Model View ViewModel (MVVM)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">Model View Presenter (MVP)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">Model View Presenter with Coordinators (MVP-C)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">View Interactor Presenter Entity Router (VIPER)</a></li><li><a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">View Interactor Presenter (VIP)</a></li></ul><p>If you’re eager to explore the implementation details directly, you can find the complete source code in our open-source repository <a href="https://github.com/radude89/footballgather-ios">here</a>.</p><h3>Why Your iOS App Needs a Solid Architecture Pattern</h3><p>The cornerstone of any successful iOS application is maintainability. A well-architected app clearly defines boundaries — you know exactly where view logic belongs, what responsibilities each view controller has, and which components handle business logic. This clarity isn’t just for you; it’s essential for your entire development team to understand and maintain these boundaries consistently.</p><p>Here are the key benefits of implementing a robust architecture pattern:</p><ul><li><strong>Maintainability</strong>: Makes code easier to update and modify over time</li><li><strong>Testability</strong>: Facilitates comprehensive testing of business logic through clear separation of concerns</li><li><strong>Team Collaboration</strong>: Creates a shared technical vocabulary and understanding among team members</li><li><strong>Clean Separation</strong>: Ensures each component has clear, single responsibilities</li><li><strong>Bug Reduction</strong>: Minimizes errors through better organization and clearer interfaces between components</li></ul><h3>Project Requirements Overview</h3><p><strong>Given</strong> a medium-sized iOS application consisting of 6–7 screens, we’ll demonstrate how to implement it using the most popular architectural patterns in the iOS ecosystem: MVC (Model-View-Controller), MVVM (Model-View-ViewModel), MVP (Model-View-Presenter), VIPER (View-Interactor-Presenter-Entity-Router), VIP (Clean Swift), and the Coordinator pattern. Each implementation will showcase the pattern’s strengths and potential challenges.</p><p>Our demo application, <strong>Football Gather</strong>, is designed to help friends organize and track their casual football matches. It’s complex enough to demonstrate real-world architectural challenges while remaining simple enough to clearly illustrate different patterns.</p><h3>Core Features and Functionality</h3><ul><li>Player Management: Add and maintain a roster of players in the application</li><li>Team Assignment: Flexibly organize players into different teams for each match</li><li>Player Customization: Edit player details and preferences</li><li>Match Management: Set and control countdown timers for match duration</li></ul><h3>Screen Mockups</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3yuqsGyKu0dWuiCU.png" /></figure><h3>Backend</h3><p>The application is supported by a web backend developed using the Vapor framework, a popular choice for building modern REST APIs in Swift. You can explore the details of this implementation in our article <a href="https://www.radude89.com/blog/vapor.html">here</a>, which covers the fundamentals of building REST APIs with Vapor and Fluent in Swift. Additionally, we have documented the transition from Vapor 3 to Vapor 4, highlighting the enhancements and new features in our article <a href="https://www.radude89.com/blog/migrate-to-vapor4.html">here</a>.</p><h3>What is MVC</h3><p>Model-View-Controller (MVC) is arguably the most widely recognized architectural pattern in software development.</p><p>At its core, MVC consists of three distinct components: Model, View, and Controller. Let’s explore each one in detail.</p><h4>Model</h4><ul><li>Encompasses all data classes, helper utilities, and networking code</li><li>Contains application-specific data and the business logic that processes it</li><li>In our application, the Model layer includes everything within the Utils, Storage, and Networking groups</li><li>Supports various relationships between model objects (e.g., many-to-many between players and gathers, one-to-many between users and players/gathers)</li><li>Maintains independence from the View layer and should not be concerned with user interface details</li></ul><h4><strong>Communication Flow</strong></h4><ul><li>User actions in the View layer are communicated to the Model through the Controller</li><li>When the Model’s data changes, it notifies the Controller, which then updates the View accordingly</li></ul><h4>View</h4><ul><li>Represents the visual elements that users interact with on screen</li><li>Handles user input and interaction events</li><li>Displays Model data and facilitates user interactions</li><li>Built using Apple’s core frameworks: UIKit and AppKit</li><li>In our application: LoadingView, EmptyView, PlayerTableViewCell, and ScoreStepper are examples of View components</li></ul><h4><strong>Communication Flow</strong></h4><ul><li>Views never communicate directly with the Model — all interaction is mediated through the Controller</li></ul><h4>Controller</h4><ul><li>Acts as the central coordinator of the MVC architecture</li><li>Manages View updates and Model mutations</li><li>Processes Model changes and ensures View synchronization</li><li>Handles object lifecycle management and setup tasks</li></ul><h4><strong>Communication Flow</strong></h4><ul><li>Maintains bidirectional communication with both Model and View layers</li><li>Interprets user actions and orchestrates corresponding Model updates</li><li>Ensures UI consistency by propagating Model changes to the View</li></ul><h3>Evolution of MVC</h3><p>The traditional MVC pattern differs from Apple’s Cocoa MVC implementation. In the original pattern, View and Model layers could communicate directly, while the View remained stateless and was rendered by the Controller after Model updates.<br>First introduced in <a href="https://en.wikipedia.org/wiki/Smalltalk">Smalltalk-79</a>, MVC was built upon three fundamental design patterns: composite, strategy, and observer.</p><h3>Composite Pattern</h3><blockquote><strong>The view hierarchy in an application consists of nested view objects working together cohesively. These visual components span from windows to complex views like table views, down to basic UI elements such as buttons. User interactions can occur at any level within this composite structure.</strong></blockquote><p>Consider the UIView hierarchy in iOS development. Views serve as the fundamental building blocks of the user interface, capable of containing multiple subviews.<br>For instance, our LoginViewController&#39;s main view contains a hierarchy of stack views, which in turn contain text fields for credentials and a login button.</p><h3>Strategy Pattern</h3><blockquote><strong>Controllers implement specific strategies for view objects. While views focus solely on their visual presentation, controllers handle all application-specific logic and interface behavior decisions.</strong></blockquote><h3>Observer Pattern</h3><blockquote><strong>Model objects maintain and notify interested components — typically views — about changes in their state.</strong></blockquote><p>A significant drawback of traditional MVC is the tight coupling between all three layers, which can make testing, maintenance, and code reuse challenging.</p><h3>Applying to our code</h3><p>In FootballGather we implement each screen as a View Controller:</p><h4>LoginViewController</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/0*9GFZc7X78XTCqfDJ.png" /></figure><p><strong>Description</strong></p><ul><li>landing page where users can login with their credentials or create new users</li></ul><p><strong>UI elements</strong></p><ul><li><strong>usernameTextField</strong> — this is the text field where users enter their username</li><li><strong>passwordTextField</strong> — secure text field for entering passwords</li><li><strong>rememberMeSwitch</strong> — is an UISwitch for saving the username in Keychain after login and autopopulate the field next time we enter the app</li><li><strong>loadingView</strong> — is used to show an indicator while a server call is made</li></ul><p><strong>Services</strong></p><ul><li><strong>loginService</strong> — used to call the Login API with the entered credentials</li><li><strong>usersService</strong> — used to call the Register API, creating a new user</li></ul><p>As we can see, this class has three major functions: login, register and remember my username. Jumping to next screen is done via performSegue.</p><h4>Code snippet</h4><pre>@IBAction private func login(_ sender: Any) {<br>    // Validate required fields<br>    // Both username and password must be non-empty strings<br>    guard let userText = usernameTextField.text, userText.isEmpty == false,<br>        let passwordText = passwordTextField.text, passwordText.isEmpty == false else {<br>            AlertHelper.present(in: self, title: &quot;Error&quot;, message: &quot;Both fields are mandatory.&quot;)<br>            return<br>    }<br><br>    // Display loading indicator while authentication is in progress<br>        showLoadingView()<br>        // Prepare login credentials model for API request<br>        let requestModel = UserRequestModel(username: userText, password: passwordText)<br>        // Attempt to authenticate user with provided credentials<br>        // Response is handled asynchronously<br>        loginService.login(user: requestModel) { [weak self] result in<br>            guard let self = self else { return }<br>            DispatchQueue.main.async {<br>                // Hide loading indicator once response is received<br>                self.hideLoadingView()<br>                switch result {<br>                case .failure(let error):<br>                    // Authentication failed - Display error message to user<br>                    AlertHelper.present(in: self, title: &quot;Error&quot;, message: String(describing: error))<br>                case .success(_):<br>                    // Authentication successful:<br>                    // 1. Save &quot;Remember Me&quot; preference if enabled<br>                    // 2. Clear stored credentials if disabled<br>                    // 3. Navigate to main screen<br>                    self.handleSuccessResponse()<br>                }<br>            }<br>        }<br>  }</pre><h4>PlayerListViewController</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/0*zQtcC7iEe8dDMy6C.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/0*oXEUSCDCtZRgJFso.png" /></figure><p><strong>Description</strong></p><ul><li>shows the players for the logged in user. Consists of a main table view, each player is displayed in a separated row.</li></ul><p><strong>UI elements</strong></p><ul><li><strong>playerTableView</strong> — The table view that displays players</li><li><strong>confirmOrAddPlayersButton</strong> — Action button from the bottom of the view, that can either correspond to an add player action or confirms the selected players</li><li><strong>loadingView</strong> — is used to show an indicator while a server call is made</li><li><strong>emptyView</strong> — Shown when the user hasn’t added any players</li><li><strong>barButtonItem</strong> — The top right button that can have different states based on the view mode we are in. Has the title “Cancel” when we go into selection mode to choose the players we want for the gather or “Select” when we are in view mode.</li></ul><p><strong>Services</strong></p><ul><li><strong>playersService</strong> — Used to retrieve the list of players and to delete a player</li></ul><p><strong>Models</strong></p><ul><li><strong>players</strong> — An array of players created by the user. This are the rows we see in playerTableView</li><li><strong>selectedPlayersDictionary</strong> — A cache dictionary that stores the row index of the selected player as key and the selected player as value.</li></ul><p>If you open up Main.storyboard you can see that from this view controller you can perform three segues</p><ul><li><strong>ConfirmPlayersSegueIdentifier</strong> — After you select what players you want for your gather, you go to a confirmation screen where you assign the teams they will be part of.</li><li><strong>PlayerAddSegueIdentifier</strong> — Goes to a screen where you can create a new player</li><li><strong>PlayerDetailSegueIdentifier</strong> — Opens a screen where you can see the details of the player</li></ul><p>We have the following function to retrieve the model for this View Controller.</p><pre>private func loadPlayers() {<br>    // Disable user interaction during data fetch<br>    // Prevents multiple requests and shows loading state<br>    view.isUserInteractionEnabled = false<br><br>    // Fetch players from remote service<br>    // Returns array of PlayerResponseModel or error<br>    playersService.get { [weak self] (result: Result&lt;[PlayerResponseModel], Error&gt;) in<br>        DispatchQueue.main.async {<br>            // Re-enable user interaction after response<br>            self?.view.isUserInteractionEnabled = true<br>            switch result {<br>            case .failure(let error):<br>                // Handle service error:<br>                // Display error message and retry options to user<br>                self?.handleServiceFailures(withError: error)<br>            case .success(let players):<br>                // Update data model and refresh UI:<br>                // 1. Store retrieved players<br>                // 2. Update table view<br>                // 3. Handle empty states<br>                self?.players = players<br>                self?.handleLoadPlayersSuccessfulResponse()<br>            }<br>        }<br>    }<br>}</pre><p>And if we want to delete one player we do the following:</p><pre>func tableView(<br>    _ tableView: UITableView,<br>    commit editingStyle: UITableViewCell.EditingStyle,<br>    forRowAt indexPath: IndexPath<br>) {<br>    // Only handle deletion actions, ignore other editing styles<br>    guard editingStyle == .delete else { return }<br><br>    // Show a confirmation dialog before deleting the player<br>    let alertController = UIAlertController(<br>        title: &quot;Delete player&quot;,<br>        message: &quot;Are you sure you want to delete the selected player?&quot;,<br>        preferredStyle: .alert<br>    )<br>    <br>    // Configure delete action with destructive style<br>    let confirmAction = UIAlertAction(<br>        title: &quot;Delete&quot;,<br>        style: .destructive<br>    ) { [weak self] _ in<br>        self?.handleDeletePlayerConfirmation(forRowAt: indexPath)<br>    }<br>    alertController.addAction(confirmAction)<br>    // Configure cancel action to dismiss dialog<br>    let cancelAction = UIAlertAction(<br>        title: &quot;Cancel&quot;,<br>        style: .cancel,<br>        handler: nil<br>    )<br>    alertController.addAction(cancelAction)<br>    // Present the confirmation dialog<br>    present(alertController, animated: true, completion: nil)<br>}</pre><p>The service call is presented below:</p><pre>private func requestDeletePlayer(<br>    at indexPath: IndexPath,<br>    completion: @escaping (Bool) -&gt; Void<br>) {<br>    // Retrieve player to be deleted from data model<br>    let player = players[indexPath.row]<br>    var service = playersService<br><br>    // Perform delete request using player&#39;s ID<br>    service.delete(withID: ResourceID.integer(player.id)) { [weak self] result in<br>        DispatchQueue.main.async {<br>            switch result {<br>            case .failure(let error):<br>                // Handle deletion failure<br>                // Notify user of error and pass false to completion handler<br>                self?.handleServiceFailures(withError: error)<br>                completion(false)<br>            case .success(_):<br>                // Deletion successful<br>                // Pass true to completion handler<br>                completion(true)<br>            }<br>        }<br>    }<br>}</pre><h4>PlayerAddViewController</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/0*AycjPS04hw-UbwRc.png" /></figure><p><strong>Description</strong></p><ul><li>This screen is used for creating a player.</li></ul><p><strong>UI Elements</strong></p><ul><li><strong>playerNameTextField</strong> — Used to enter the name of the player</li><li><strong>doneButton</strong> — Bar button item that is used to confirm the player to be created and initiates a service call</li><li><strong>loadingView</strong> — Is used to show an indicator while a server call is made</li></ul><p><strong>Services</strong></p><ul><li>We use the StandardNetworkService that points to <strong>/api/players</strong>. To add players, we initiate a <strong>POST</strong> request.</li></ul><p><strong>Code snippet</strong></p><pre>private func createPlayer(<br>    _ player: PlayerCreateModel,<br>    completion: @escaping (Bool) -&gt; Void<br>) {<br>    // Initialize network service for player creation<br>    let service = StandardNetworkService(<br>        resourcePath: &quot;/api/players&quot;,<br>        authenticated: true<br>    )<br>    <br>    // Perform create request with player data<br>    service.create(player) { result in<br>        // Check result of the network request<br>        if case .success(_) = result {<br>            // Player creation successful<br>            completion(true)<br>        } else {<br>            // Player creation failed<br>            completion(false)<br>        }<br>    }<br>}</pre><h4>PlayerDetailViewController</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/0*RYL7noxnFKDXVDc_.png" /></figure><p><strong>Description</strong></p><ul><li>maps a screen that shows the details of a player (name, age, position, skill and favourite team)</li></ul><p><strong>UI elements</strong></p><ul><li><strong>playerDetailTableView</strong> — A tableview that displays the details of the player.</li></ul><p><strong>Model</strong></p><ul><li><strong>player</strong> — The model of the player as PlayerResponseModel</li></ul><p>We have no services in this ViewController. A request to update an information of player is received fromPlayerEditViewController and passed to PlayerListViewController through delegation.</p><p>The sections are made with a factory pattern:</p><pre>private func makeSections() -&gt; [PlayerSection] {<br>    // Create and return an array of PlayerSection objects<br>    return [<br>        PlayerSection(<br>            title: &quot;Personal&quot;,<br>            rows: [<br>                // Add player name row<br>                PlayerRow(<br>                    title: &quot;Name&quot;,<br>                    value: self.player?.name ?? &quot;&quot;,<br>                    editableField: .name<br>                ),<br>                // Add player age row<br>                PlayerRow(<br>                    title: &quot;Age&quot;,<br>                    value: self.player?.age != nil ? &quot;\(self.player!.age!)&quot; : &quot;&quot;,<br>                    editableField: .age<br>                )<br>            ]<br>        ),<br>        PlayerSection(<br>            title: &quot;Play&quot;,<br>            rows: [<br>                // Add preferred position row<br>                PlayerRow(<br>                    title: &quot;Preferred position&quot;,<br>                    value: self.player?.preferredPosition?.rawValue.capitalized ?? &quot;&quot;,<br>                    editableField: .position<br>                ),<br>                // Add skill row<br>                PlayerRow(<br>                    title: &quot;Skill&quot;,<br>                    value: self.player?.skill?.rawValue.capitalized ?? &quot;&quot;,<br>                    editableField: .skill<br>                )<br>            ]<br>        ),<br>        PlayerSection(<br>            title: &quot;Likes&quot;,<br>            rows: [<br>                // Add favourite team row<br>                PlayerRow(<br>                    title: &quot;Favourite team&quot;,<br>                    value: self.player?.favouriteTeam ?? &quot;&quot;,<br>                    editableField: .favouriteTeam<br>                )<br>            ]<br>        )<br>    ]<br>}</pre><h4>PlayerEditViewController</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/0*GxuBu8AxSOproVVn.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/0*ve4oYqkrVCp62SZH.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/0*JKXT9XSjhqyAC4E3.png" /></figure><p><strong>Description</strong></p><ul><li>Edits a player information.</li></ul><p><strong>UI Elements</strong></p><ul><li><strong>playerEditTextField</strong> — The field that is filled with the player’s detail we want to edit</li><li><strong>playerTableView</strong> — We wanted to have a similar behaviour and UI as we have in iOS General Settings for editing a details. This table view has either one row with a text field or multiple rows with a selection behaviour.</li><li><strong>loadingView</strong> — is used to show an indicator while a server call is made</li><li><strong>doneButton</strong> — An UIBarButtonItem that performs the action of editing.</li></ul><p><strong>Services</strong></p><ul><li>Update Player API, used as a StandardNetworkService:</li></ul><pre>private func updatePlayer(<br>    _ player: PlayerResponseModel,<br>    completion: @escaping (Bool) -&gt; Void<br>) {<br>    // Initialize network service for player update<br>    var service = StandardNetworkService(<br>        resourcePath: &quot;/api/players&quot;,<br>        authenticated: true<br>    )<br>    <br>    // Perform update request with player data<br>    // Use player&#39;s ID for resource identification<br>    service.update(<br>        PlayerCreateModel(player),<br>        resourceID: ResourceID.integer(player.id)<br>    ) { result in<br>        // Check result of the network request<br>        if case .success(let updated) = result {<br>            // Player update successful<br>            completion(updated)<br>        } else {<br>            // Player update failed<br>            completion(false)<br>        }<br>    }<br>}</pre><p><strong>Models</strong></p><ul><li><strong>viewType</strong> — An enum that can be .text (for player details that are entered via keyboard) or .selection (for player details that are selected by tapping one of the cells, for example the preferred position).</li><li><strong>player</strong> — The player we want to edit.</li><li><strong>items</strong> — An array of strings corresponding to all possible options for preferred positions or skill. This array is nil when a text entry is going to be edited.</li></ul><h4>ConfirmPlayersViewController</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/0*FUONGWbktfHnbcXe.png" /></figure><p><strong>Description</strong></p><ul><li>Before reaching the Gather screen we want to put the players in the desired teams</li></ul><p><strong>UI elements</strong></p><ul><li><strong>playerTableView</strong> — A table view split in three sections (Bench, Team A and Team B) that shows the selected players we want for the gather.</li><li><strong>startGatherButton</strong> — Initially disabled, when tapped triggers an action to perform the Network API calls required to start the gather and at last, it pushes the next screen.</li><li><strong>loadingView</strong> — is used to show an indicator while a server call is made.</li></ul><p><strong>Services</strong></p><ul><li><strong>Create Gather</strong> — Adds a new gather by making a POST request to <strong>/api/gathers</strong>.</li><li><strong>Add Player to Gather</strong> — After we are done with selecting teams for our players, we add them to the gather by doing a POST request to <strong>api/gathers/{gather_id}/players/{player_id}</strong>.</li></ul><p><strong>Models</strong></p><ul><li><strong>playersDictionary</strong> — Each team has an array of players, so the dictionary has the teams as keys (Team A, Team B or Bench) and for values we have the selected players (array of players).</li></ul><p>When we are done with the selection (UI), a new gather is created and each player is assigned a team.</p><pre>@IBAction func startGatherAction(_ sender: Any) {<br>    // Show loading indicator while creating gather<br>    showLoadingView()<br>    <br>    // Create new gather with authenticated request<br>    // Returns UUID on success, nil on failure<br>    createGather { [weak self] uuid in<br>        guard let self = self else { return }<br>        <br>        guard let gatherUUID = uuid else {<br>            // Handle gather creation failure<br>            self.handleServiceFailure()<br>            return<br>        }<br>        <br>        // Add selected players to the newly created gather<br>        // Players will be assigned to their respective teams<br>        self.addPlayersToGather(havingUUID: gatherUUID)<br>    }<br>}</pre><p>The for loop to add players is presented below:</p><pre>private func addPlayersToGather(havingUUID gatherUUID: UUID) {<br>    // Get array of players with their team assignments<br>    let players = self.playerTeamArray<br>    <br>    // Create dispatch group to handle multiple concurrent requests<br>    // This ensures all player additions complete before updating UI<br>    let dispatchGroup = DispatchGroup()<br>    var serviceFailed = false<br>    <br>    // Add each player to the gather with their assigned team<br>    players.forEach { playerTeamModel in<br>        dispatchGroup.enter()<br>        <br>        self.addPlayer(<br>            playerTeamModel.player,<br>            toGatherHavingUUID: gatherUUID,<br>            team: playerTeamModel.team,<br>            completion: { playerWasAdded in<br>                // Track if any player addition fails<br>                if !playerWasAdded {<br>                    serviceFailed = true<br>                }<br>                <br>                dispatchGroup.leave()<br>            }<br>        )<br>    }<br>    <br>    // Handle completion after all player additions finish<br>    dispatchGroup.notify(queue: DispatchQueue.main) {<br>        self.hideLoadingView()<br>        <br>        if serviceFailed {<br>            // Handle case where one or more players failed to add<br>            self.handleServiceFailure()<br>        } else {<br>            // All players added successfully - navigate to gather screen<br>            self.performSegue(<br>                withIdentifier: SegueIdentifiers.gather.rawValue,<br>                sender: GatherModel(<br>                    players: players,<br>                    gatherUUID: gatherUUID<br>                )<br>            )<br>        }<br>    }<br>}</pre><h4>GatherViewController</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/750/0*jG8jtQh2v2K2Kow1.png" /></figure><p><strong>Description</strong></p><ul><li>This is the core screen of the application, where you are in the gather mode and start / pause or stop the timer and in the end, finish the match.</li></ul><p><strong>UI elements</strong></p><ul><li><strong>playerTableView</strong> — Used to display the players in gather, split in two sections: Team A and Team B.</li><li><strong>scoreLabelView</strong> — A view that has two labels for displaying the score, one for Team A and the other one for Team B.</li><li><strong>scoreStepper</strong> — A view that has two steppers for the teams.</li><li><strong>timerLabel</strong> — Used to display the remaining time in the format <strong>mm:ss</strong>.</li><li><strong>timerView</strong> — An overlay view that has a UIPickerView to choose the time of the gather.</li><li><strong>timePickerView</strong> — The picker view with two components (minutes and seconds) for selecting the gather’s time.</li><li><strong>actionTimerButton</strong> — Different state button that manages the countdown timer (resume, pause and start).</li><li><strong>loadingView</strong> — is used to show an indicator while a server call is made.</li></ul><p><strong>Services</strong></p><ul><li><strong>Update Gather</strong> — when a gather is ended, we do a PUT request to update the winner team and the score</li></ul><p><strong>Models</strong></p><ul><li><strong>GatherTime</strong> — A tuple that has minutes and seconds as Int.</li><li><strong>gatherModel</strong> — Contains the gather ID and an array of player team model (the player response model and the team he belongs to). This is created and passed from ConfirmPlayersViewController.</li><li><strong>timer</strong> — Used to countdown the minutes and seconds of the gather.</li><li><strong>timerState</strong> — Can have three states <strong>stopped</strong>, <strong>running</strong> and <strong>paused</strong>. We observer when one of the values is set so we can change the actionTimerButton&#39;s title accordingly. When it&#39;s paused the button&#39;s title will be <strong>Resume</strong>. When it&#39;s running the button&#39;s title will be <strong>Pause</strong> and <strong>Start</strong> when the timer is stopped.</li></ul><p>When the actionTimerButton is tapped, we verify if we want to invalidate or start the timer:</p><pre>@IBAction func actionTimer(_ sender: Any) {<br>    // Check if the user selected a time more than 1 second<br>    guard selectedTime.minutes &gt; 0 || selectedTime.seconds &gt; 0 else {<br>        return<br>    }<br>    <br>    switch timerState {<br>    case .stopped, .paused:<br>        // Timer was stopped or paused, start running<br>        timerState = .running<br>    case .running:<br>        // Timer is running, pause it<br>        timerState = .paused<br>    }<br>    <br>    if timerState == .paused {<br>        // Stop the timer<br>        timer.invalidate()<br>    } else {<br>        // Start the timer and call updateTimer every second<br>        timer = Timer.scheduledTimer(<br>            timeInterval: 1,<br>            target: self,<br>            selector: #selector(updateTimer),<br>            userInfo: nil,<br>            repeats: true<br>        )	    <br>    }<br>}</pre><p>To cancel a timer we have the following action implemented:</p><pre>@IBAction func cancelTimer(_ sender: Any) {<br>    // Set timer state to stopped and invalidate the timer<br>    timerState = .stopped<br>    timer.invalidate()<br>    <br>    // Reset selected time to default (10 minutes)<br>    selectedTime = Constants.defaultTime<br>    timerView.isHidden = true<br>}</pre><p>The selector updateTimer is called each second:</p><pre>@objc func updateTimer(_ timer: Timer) {<br>    // Check if seconds are zero to decrement minutes<br>    if selectedTime.seconds == 0 {<br>        selectedTime.minutes -= 1<br>        selectedTime.seconds = 59<br>    } else {<br>        selectedTime.seconds -= 1<br>    }<br>    <br>    // Stop timer if time reaches zero<br>    if selectedTime.seconds == 0 &amp;&amp; selectedTime.minutes == 0 {<br>        timerState = .stopped<br>        timer.invalidate()<br>    }<br>}</pre><p>Before ending a gather, check the winner team:</p><pre>guard let scoreTeamAString = scoreLabelView.teamAScoreLabel.text,<br>      let scoreTeamBString = scoreLabelView.teamBScoreLabel.text,<br>      let scoreTeamA = Int(scoreTeamAString),<br>      let scoreTeamB = Int(scoreTeamBString) else {<br>    return<br>}<br><br>// Format the score as a string<br>let score = &quot;\(scoreTeamA)-\(scoreTeamB)&quot;<br>// Determine the winner team based on scores<br>var winnerTeam: String = &quot;None&quot;<br>if scoreTeamA &gt; scoreTeamB {<br>    winnerTeam = &quot;Team A&quot;<br>} else if scoreTeamA &lt; scoreTeamB {<br>    winnerTeam = &quot;Team B&quot;<br>}<br>// Create gather model with score and winner team<br>let gather = GatherCreateModel(score: score, winnerTeam: winnerTeam)</pre><p>And the service call:</p><pre>private func updateGather(<br>    _ gather: GatherCreateModel,<br>    completion: @escaping (Bool) -&gt; Void<br>) {<br>    // Verify gather model exists before proceeding<br>    guard let gatherModel = gatherModel else {<br>        completion(false)<br>        return<br>    }<br>    <br>    // Initialize network service for gather update<br>    var service = StandardNetworkService(<br>        resourcePath: &quot;/api/gathers&quot;,<br>        authenticated: true<br>    )<br>    <br>    // Perform update request with gather data<br>    // Use player&#39;s ID for resource identification<br>    service.update(<br>        gather,<br>        resourceID: ResourceID.uuid(gatherModel.gatherUUID)<br>    ) { result in<br>        // Check result of the network request<br>        if case .success(let updated) = result {<br>            // Update successful<br>            completion(updated)<br>        } else {<br>            // Update failed<br>            completion(false)<br>        }<br>    }<br>}</pre><p>The private method updateGather is called from endGather:</p><pre>let gather = GatherCreateModel(score: score, winnerTeam: winnerTeam)<br>  <br>showLoadingView()<br>updateGather(gather) { [weak self] gatherWasUpdated in<br>    guard let self = self else { return }<br>    <br>    DispatchQueue.main.async {<br>        self.hideLoadingView()<br>        <br>        if !gatherWasUpdated {<br>            // The server call failed, make sure we show an alert to the user<br>            AlertHelper.present(<br>                in: self,<br>                title: &quot;Error update&quot;,<br>                message: &quot;Unable to update gather. Please try again.&quot;<br>            )<br>        } else {<br>            guard let playerViewController = self.navigationController?.viewControllers<br>                .first(where: { $0 is PlayerListViewController }) as? PlayerListViewController else {<br>                return<br>            }<br>            <br>            // The PlayerListViewController is in a selection mode state<br>            // We make sure we turn it back to .list<br>            playerViewController.toggleViewState()<br>            <br>            // Pop to PlayerListViewController, skipping confirmation screen<br>            self.navigationController?.popToViewController(<br>                playerViewController,<br>                animated: true<br>            )<br>        }<br>    }<br>}</pre><h3>Testing our business logic</h3><p>We saw a first iteration of applying MVC to the demo app FootbalGather. Of course, we can refactor the code and make it better and decouple some of the logic, split it into different classes, but for the sake of the exercise we are going to keep this version of the codebase.</p><p>Let’s see how we can write unit tests for our classes. We are going to exemplify for GatherViewController and try to reach close to 100% code coverage.</p><p>First, we see GatherViewController is part of Main storyboard. To make our lives easier, we use an identifier and instantiate it with the method storyboard.instantiateViewController. Let&#39;s use the setUp method for this logic:</p><pre>final class GatherViewControllerTests: XCTestCase {<br>	  <br>    var sut: GatherViewController! // System Under Test (SUT)<br>    <br>    override func setUp() {<br>        super.setUp()<br>        <br>        // Load the storyboard named &quot;Main&quot;<br>        let storyboard = UIStoryboard(name: &quot;Main&quot;, bundle: nil)<br>        <br>        // Instantiate GatherViewController from the storyboard<br>        if let viewController = storyboard.instantiateViewController(identifier: &quot;GatherViewController&quot;) as? GatherViewController {<br>            sut = viewController // Assign to SUT<br>            sut.gatherModel = gatherModel // Set the gather model<br>            _ = sut.view // Load the view to trigger viewDidLoad<br>        } else {<br>            XCTFail(&quot;Unable to instantiate GatherViewController&quot;) // Fail the test if instantiation fails<br>        }<br>    }<br>//…<br>}</pre><p>For our first test, we verify all outlets are not nil:</p><pre>func testOutlets_whenViewControllerIsLoadedFromStoryboard_areNotNil() {<br>    // Verify all IBOutlets are properly connected from storyboard<br>    XCTAssertNotNil(sut.playerTableView)      // Table view showing players<br>    XCTAssertNotNil(sut.scoreLabelView)       // View displaying team scores<br>    XCTAssertNotNil(sut.scoreStepper)         // Stepper controls for adjusting scores<br>    XCTAssertNotNil(sut.timerLabel)           // Label showing countdown time<br>    XCTAssertNotNil(sut.timerView)            // Container view for timer controls<br>    XCTAssertNotNil(sut.timePickerView)       // Picker for setting match duration<br>    XCTAssertNotNil(sut.actionTimerButton)    // Button to start/pause/resume timer<br>}</pre><p>Now let’s see if viewDidLoad is called. The title is set and some properties are configured. We verify the public parameters:</p><pre>func testViewDidLoad_whenViewControllerIsLoadedFromStoryboard_setsVariables() {<br>    XCTAssertNotNil(sut.title)<br>    XCTAssertTrue(sut.timerView.isHidden)<br>    XCTAssertNotNil(sut.timePickerView.delegate)<br>}</pre><p>The variable timerView is a pop-up custom view where users set their match timer.</p><p>Moving forward let’s unit test our table view methods:</p><pre>func testViewDidLoad_whenViewControllerIsLoadedFromStoryboard_setsVariables() {<br>    // Verify initial view setup after loading<br>    XCTAssertNotNil(sut.title)                    // Check view controller title is set<br>    XCTAssertTrue(sut.timerView.isHidden)         // Timer view should be hidden initially<br>    XCTAssertNotNil(sut.timePickerView.delegate)  // Time picker should have delegate set<br>}</pre><p>The variable timerView is a pop-up custom view where users set their match timer.</p><p>Moving forward let’s unit test our table view methods:</p><pre>func testNumberOfSections_whenGatherModelIsSet_returnsTwoTeams() {<br>    // Verify table view has correct number of sections (Team A and Team B)<br>    XCTAssert(sut.playerTableView?.numberOfSections == Team.allCases.count - 1)<br>}</pre><p>We have just two teams: <strong>Team A</strong> and <strong>Team B</strong>. The <strong>Bench</strong> team is not visible and not part of this screen.</p><pre>func testTitleForHeaderInSection_whenSectionIsTeamAAndGatherModelIsSet_returnsTeamATitleHeader() {<br>    // Test header title for Team A section<br>    let teamASectionTitle = sut.tableView(sut.playerTableView, titleForHeaderInSection: 0)<br>    XCTAssertEqual(teamASectionTitle, Team.teamA.headerTitle)<br>}<br>    <br>func testTitleForHeaderInSection_whenSectionIsTeamBAndGatherModelIsSet_returnsTeamBTitleHeader() {<br>    // Test header title for Team B section<br>    let teamBSectionTitle = sut.tableView(sut.playerTableView, titleForHeaderInSection: 1)<br>    XCTAssertEqual(teamBSectionTitle, Team.teamB.headerTitle)<br>}</pre><p>Our tableview should have two sections with both header titles being set to the team names (Team A and Team B).</p><p>For checking the number of rows, we inject a mocked gather model:</p><pre>private let gatherModel = ModelsMockFactory.makeGatherModel(numberOfPlayers: 4)<br><br>static func makeGatherModel(numberOfPlayers: Int, gatherUUID: UUID = ModelsMock.gatherUUID) -&gt; GatherModel {<br>    // Get all possible player skills and positions<br>    let allSkills = PlayerSkill.allCases<br>    let allPositions = PlayerPosition.allCases<br>    <br>    // Initialize empty array to store player-team assignments<br>    var playerTeams: [PlayerTeamModel] = []<br>    <br>    // Create specified number of players with random attributes<br>    // For each player<br>    ...<br>        // Randomly assign skill and position<br>        // Alternate between Team A and Team B<br>        // Create player model with incremental attributes<br>    ...<br>    <br>    return GatherModel(players: playerTeams, gatherUUID: gatherUUID)<br>}</pre><p>Nil scenario when the section is invalid.</p><pre>func testNumberOfRowsInSection_whenGatherModelIsNil_returnsZero() {<br>    // Test edge case when gather model is nil<br>    sut.gatherModel = nil<br>    XCTAssertEqual(sut.tableView(sut.playerTableView, numberOfRowsInSection: -1), 0)<br>}<br><br>func testCellForRowAtIndexPath_whenSectionIsTeamA_setsCellDetails() {<br>    // Set up test data for Team A<br>    let indexPath = IndexPath(row: 0, section: 0)<br>    let playerTeams = gatherModel.players.filter({ $0.team == .teamA })<br>    let player = playerTeams[indexPath.row].player<br>    <br>    // Get cell from table view<br>    let cell = sut.playerTableView.cellForRow(at: indexPath)<br>    <br>    // Verify cell content matches player data<br>    XCTAssertEqual(cell?.textLabel?.text, player.name)<br>    XCTAssertEqual(cell?.detailTextLabel?.text, player.preferredPosition?.acronym)<br>}<br>  <br>func testCellForRowAtIndexPath_whenSectionIsTeamB_setsCellDetails() {<br>    // Set up test data for Team B<br>    let indexPath = IndexPath(row: 0, section: 1)<br>    let playerTeams = gatherModel.players.filter({ $0.team == .teamB })<br>    let player = playerTeams[indexPath.row].player<br>    <br>    // Get cell from table view<br>    let cell = sut.playerTableView.cellForRow(at: indexPath)<br>    <br>    // Verify cell content matches player data<br>    XCTAssertEqual(cell?.textLabel?.text, player.name)<br>    XCTAssertEqual(cell?.detailTextLabel?.text, player.preferredPosition?.acronym)<br>}<br><br>func testPickerViewNumberOfComponents_returnsAllCountDownCases() {<br>    // Verify picker view has correct number of components<br>    XCTAssertEqual(sut.timePickerView.numberOfComponents, <br>                    GatherViewController.GatherCountDownTimerComponent.allCases.count)<br>}<br><br>func testPickerViewNumberOfRowsInComponent_whenComponentIsMinutes_returns60() {<br>    // Test number of rows for minutes component<br>    let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue<br>    let numberOfRows = sut.pickerView(sut.timePickerView, numberOfRowsInComponent: minutesComponent)<br>    <br>    XCTAssertEqual(numberOfRows, 60)<br>}<br>  <br>func testPickerViewNumberOfRowsInComponent_whenComponentIsSecounds() {<br>    // Test number of rows for seconds component<br>    let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue<br>    let numberOfRows = sut.pickerView(sut.timePickerView, numberOfRowsInComponent: secondsComponent)<br>    <br>    XCTAssertEqual(numberOfRows, 60)<br>}<br><br>func testPickerViewTitleForRow_whenComponentIsMinutes_containsMin() {<br>    // Test title format for minutes component<br>    let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue<br>    let title = sut.pickerView(sut.timePickerView, titleForRow: 0, forComponent: minutesComponent)<br>    <br>    XCTAssertTrue(title!.contains(&quot;min&quot;))<br>}<br>  <br>func testPickerViewTitleForRow_whenComponentIsSeconds_containsSec() {<br>    // Test title format for seconds component<br>    let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue<br>    let title = sut.pickerView(sut.timePickerView, titleForRow: 0, forComponent: secondsComponent)<br>    <br>    XCTAssertTrue(title!.contains(&quot;sec&quot;))<br>}<br><br>func testSetTimer_whenActionIsSent_showsTimerView() {<br>    // Test timer view visibility when setting timer<br>    sut.setTimer(UIButton())<br>    XCTAssertFalse(sut.timerView.isHidden)<br>}<br><br>func testCancelTimer_whenActionIsSent_hidesTimerView() {<br>    // Test timer view visibility when canceling timer<br>    sut.cancelTimer(UIButton())<br>    XCTAssertTrue(sut.timerView.isHidden)<br>}<br><br>func testTimerCancel_whenActionIsSent_hidesTimerView() {<br>    // Test timer view visibility when canceling from overlay<br>    sut.timerCancel(UIButton())<br>    XCTAssertTrue(sut.timerView.isHidden)<br>}<br><br>func testTimerDone_whenActionIsSent_hidesTimerViewAndSetsMinutesAndSeconds() {<br>    // Test timer completion setup<br>    sut.timerDone(UIButton())<br>    <br>    // Get selected time components<br>    let minutes = sut.timePickerView.selectedRow(inComponent: <br>        GatherViewController.GatherCountDownTimerComponent.minutes.rawValue)<br>    let seconds = sut.timePickerView.selectedRow(inComponent: <br>        GatherViewController.GatherCountDownTimerComponent.seconds.rawValue)<br>    <br>    // Verify timer view state and time settings<br>    XCTAssertTrue(sut.timerView.isHidden)<br>    XCTAssertGreaterThan(minutes, 0)<br>    XCTAssertEqual(seconds, 0)<br>}<br><br>func testActionTimer_whenSelectedTimeIsZero_returns() {<br>    // Set up components<br>    let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue<br>    let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue<br>    <br>    // Set time to 00:00<br>    sut.timePickerView.selectRow(0, inComponent: minutesComponent, animated: false)<br>    sut.timePickerView.selectRow(0, inComponent: secondsComponent, animated: false)<br>    sut.timerDone(UIButton())<br>    sut.actionTimer(UIButton())<br>    <br>    // Verify time remains at 00:00<br>    XCTAssertEqual(sut.timePickerView.selectedRow(inComponent: minutesComponent), 0)<br>    XCTAssertEqual(sut.timePickerView.selectedRow(inComponent: secondsComponent), 0)<br>}<br><br>func testActionTimer_whenSelectedTimeIsSet_updatesTimer() {<br>    // Set up test components<br>    let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue<br>    let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue<br>    <br>    // Configure initial time (0:01)<br>    sut.timePickerView.selectRow(0, inComponent: minutesComponent, animated: false)<br>    sut.timePickerView.selectRow(1, inComponent: secondsComponent, animated: false)<br>    <br>    // Verify initial state<br>    sut.timerDone(UIButton())<br>    XCTAssertEqual(sut.actionTimerButton.title(for: .normal), &quot;Start&quot;)<br>    <br>    // Start timer<br>    sut.actionTimer(UIButton())<br>    XCTAssertEqual(sut.actionTimerButton.title(for: .normal), &quot;Pause&quot;)<br>    <br>    // Wait for timer completion<br>    let exp = expectation(description: &quot;Timer expectation&quot;)<br>    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {<br>        XCTAssertEqual(self.sut.actionTimerButton.title(for: .normal), &quot;Start&quot;)<br>        exp.fulfil()<br>    }<br>    <br>    waitForExpectations(timeout: 5, handler: nil)<br>}<br>func testActionTimer_whenTimerIsSetAndRunning_isPaused() {<br>    // Set up test components<br>    let sender = UIButton()<br>    let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue<br>    let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue<br>    <br>    // Configure initial time (0:03)<br>    sut.timePickerView.selectRow(0, inComponent: minutesComponent, animated: false)<br>    sut.timePickerView.selectRow(3, inComponent: secondsComponent, animated: false)<br>    <br>    // Start timer and verify initial state<br>    sut.timerDone(sender)<br>    XCTAssertEqual(sut.actionTimerButton.title(for: .normal), &quot;Start&quot;)<br>    sut.actionTimer(sender)<br>    <br>    // Pause timer after 1 second<br>    let exp = expectation(description: &quot;Timer expectation&quot;)<br>    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {<br>        self.sut.actionTimer(sender)<br>        XCTAssertEqual(self.sut.actionTimerButton.title(for: .normal), &quot;Resume&quot;)<br>        exp.fulfil()<br>    }<br>    <br>    waitForExpectations(timeout: 5, handler: nil)<br>}<br><br>func testUpdateTimer_whenSecondsReachZero_decrementsMinuteComponent() {<br>    // Set up test components<br>    let sender = UIButton()<br>    let timer = Timer()<br>    let minutesComponent = GatherViewController.GatherCountDownTimerComponent.minutes.rawValue<br>    let secondsComponent = GatherViewController.GatherCountDownTimerComponent.seconds.rawValue<br>    <br>    // Set initial time to 1:00<br>    sut.timePickerView.selectRow(1, inComponent: minutesComponent, animated: false)<br>    sut.timePickerView.selectRow(0, inComponent: secondsComponent, animated: false)<br>    sut.timerDone(sender)<br>    XCTAssertEqual(sut.timerLabel.text, &quot;01:00&quot;)<br>    <br>    // Update timer and verify decrement<br>    sut.updateTimer(timer)<br>    XCTAssertEqual(sut.timerLabel.text, &quot;00:59&quot;)<br>}</pre><p>In this test we checked if the seconds are decremented when the minutes component is reaching zero.</p><p>Having access to the outlets, we can easily verify the stepperDidChangeValue delegates:</p><pre>func testStepperDidChangeValue_whenTeamAScores_updatesTeamAScoreLabel() {<br>    // Simulate Team A scoring<br>    sut.scoreStepper.teamAStepper.value = 1<br>    sut.scoreStepper.teamAStepperValueChanged(UIButton())<br>    <br>    // Verify Team A&#39;s score label is updated<br>    XCTAssertEqual(sut.scoreLabelView.teamAScoreLabel.text, &quot;1&quot;)<br>    // Verify Team B&#39;s score label remains unchanged<br>    XCTAssertEqual(sut.scoreLabelView.teamBScoreLabel.text, &quot;0&quot;)<br>}<br><br>func testStepperDidChangeValue_whenTeamBScores_updatesTeamBScoreLabel() {<br>    // Simulate Team B scoring<br>    sut.scoreStepper.teamBStepper.value = 1<br>    sut.scoreStepper.teamBStepperValueChanged(UIButton())<br>    <br>    // Verify Team B&#39;s score label is updated<br>    XCTAssertEqual(sut.scoreLabelView.teamAScoreLabel.text, &quot;0&quot;)<br>    // Verify Team A&#39;s score label remains unchanged<br>    XCTAssertEqual(sut.scoreLabelView.teamBScoreLabel.text, &quot;1&quot;)<br>}<br><br>func testStepperDidChangeValue_whenTeamIsBench_scoreIsNotUpdated() {<br>    // Simulate score change for Bench team<br>    sut.stepper(UIStepper(), didChangeValueForTeam: .bench, newValue: 1)<br>    <br>    // Verify no score labels are updated<br>    XCTAssertEqual(sut.scoreLabelView.teamAScoreLabel.text, &quot;0&quot;)<br>    XCTAssertEqual(sut.scoreLabelView.teamBScoreLabel.text, &quot;0&quot;)<br>}</pre><p>Finally, the hardest and probably the most important method we have in GatherViewController is the endGather method. Here, we do a service call updating the gather model. We pass the winnerTeam and the score of the match.</p><p>It is a big method, does more than one thing and is private. (we use it as per example, functions should not be big and functions should do one thing!).</p><p>The responsibilities of this function are detailed below. endGather does the following:</p><ul><li>gets the score from scoreLabelViews</li><li>computes the winner team by comparing the score</li><li>creates the GatherModel for the service call</li><li>shows a loading spinner</li><li>does the updateGather service call</li><li>hides the loading spinner</li><li>handles success and failure</li><li>for success, the view controller is popped to PlayerListViewController (this view should be in the stack)</li><li>for failure, it presents an alert</li></ul><p>How we should test all of that? (Again, as best practice, this function should be splitted down into multiple functions).</p><p>Let’s take one step at a time.</p><p>Creating a mocked service and injecting it in our <strong>sut</strong>:</p><pre>// Set up mock networking components<br>private let session = URLSessionMockFactory.makeSession()<br>private let resourcePath = &quot;/api/gathers&quot;<br>private let appKeychain = AppKeychainMockFactory.makeKeychain()<br><br>// Configure mock endpoint and service<br>let endpoint = EndpointMockFactory.makeSuccessfulEndpoint(path: resourcePath)<br>sut.updateGatherService = StandardNetworkService(<br>    session: session, <br>    urlRequest: AuthURLRequestFactory(endpoint: endpoint, keychain: appKeychain)<br>)</pre><p>Testing the success handler. We use a protocol instead of the concrete class PlayerListViewController and we mock it in our test class:</p><pre>// Protocol definition for view state toggling<br>protocol PlayerListTogglable {<br>    func toggleViewState()<br>}<br><br>// Main view controller conforming to protocol<br>class PlayerListViewController: UIViewController, PlayerListTogglable { .. }<br><br>private extension GatherViewControllerTests {<br>    // Mock implementation for testing<br>    final class MockPlayerTogglableViewController: UIViewController, PlayerListTogglable {<br>        weak var viewStateExpectation: XCTestExpectation?<br>        private(set) var viewState = true<br>        <br>        func toggleViewState() {<br>            viewState = !viewState<br>            viewStateExpectation?.fulfil()<br>        }<br>    }<br>}</pre><p>This should be part of a navigation controller:</p><pre>// Set up view controller hierarchy<br>let playerListViewController = MockPlayerTogglableViewController()<br>let window = UIWindow()<br>let navController = UINavigationController(rootViewController: playerListViewController)<br>window.rootViewController = navController<br>window.makeKeyAndVisible()<br><br>// Trigger view loading and verify initial state<br>_ = playerListViewController.view<br>XCTAssertTrue(playerListViewController.viewState)<br>// Set up expectation for state change<br>let exp = expectation(description: &quot;Timer expectation&quot;)<br>playerListViewController.viewStateExpectation = exp<br>// Add test view controller to navigation stack<br>navController.pushViewController(sut, animated: false)</pre><p>We check the initial viewState. It should be true.</p><p>The rest of the unit test is presented below:</p><pre>// Set up mock score state<br>sut.scoreLabelView.teamAScoreLabel.text = &quot;1&quot;<br>sut.scoreLabelView.teamBScoreLabel.text = &quot;1&quot;<br><br>// Configure mock networking<br>let endpoint = EndpointMockFactory.makeSuccessfulEndpoint(path: resourcePath)<br>sut.updateGatherService = StandardNetworkService(<br>    session: session,<br>    urlRequest: AuthURLRequestFactory(endpoint: endpoint, keychain: appKeychain)<br>)<br>// Trigger gather end and handle alert<br>sut.endGather(UIButton())<br>let alertController = (sut.presentedViewController as! UIAlertController)<br>alertController.tapButton(atIndex: 0)<br>// Wait for and verify state change<br>waitForExpectations(timeout: 5) { _ in<br>    XCTAssertFalse(playerListViewController.viewState)<br>}</pre><p>Because endGather is a private method, we had to use the IBAction that calls this method. And for tapping on OK in the alert controller that was presented we had to use its private API:</p><pre>private extension UIAlertController {<br>    // Type definition for alert action handler<br>    typealias AlertHandler = @convention(block) (UIAlertAction) -&gt; Void<br>  <br>    func tapButton(atIndex index: Int) {<br>          // Access private handler using key-value coding<br>          guard let block = actions[index].value(forKey: &quot;handler&quot;) else { return }<br>          <br>          // Convert and execute handler<br>          let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self)<br>          handler(actions[index])<br>      }<br>}</pre><p>We don’t have the guarantee this unit test will work in future Swift versions. This is bad.</p><h3>Key Metrics</h3><h4>Lines of code</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XLaBxdYeJAhtOE1623nw5Q.png" /></figure><h4>Unit Tests</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hU5rZeUI0AcFTooKag6U3Q.png" /></figure><h4>Build Times</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HLBx5fFD1MrzRCv6MJobGw.png" /></figure><p>* tests were run on an 8-Core Intel Core i9, MacBook Pro, 2019</p><h3>Conclusion</h3><p>Model-View-Controller (MVC) remains one of the most widely adopted architectural patterns in iOS development, and for good reason. Throughout this article, we’ve explored its practical implementation in a real-world application.</p><p>In our demonstration, we took a straightforward approach by mapping each screen to a dedicated View Controller. While this implementation works for our sample application, it’s important to note that this simplified approach may not be suitable for more complex screens with numerous actions and responsibilities. In production applications, it’s often better to distribute responsibilities across multiple components, such as through the use of child view controllers.</p><p>We provided a comprehensive breakdown of each screen in our application, detailing:</p><ul><li>The functional role and purpose of each component</li><li>The UI elements and their interactions</li><li>The underlying data models and controller interactions</li><li>Key implementation methods with practical code examples</li></ul><p>Our experience with unit testing, particularly with the GatherViewController, revealed some challenges inherent to the MVC pattern. The need to rely on private APIs (such as with UIAlertController) highlighted potential maintenance risks, as these implementations could break with future iOS updates.</p><p>Despite these challenges, MVC remains a powerful architectural pattern when implemented thoughtfully. Its primary advantages include:</p><ul><li>Straightforward implementation for smaller applications</li><li>Lower initial complexity compared to other patterns</li><li>Strong integration with Apple’s frameworks</li><li>Familiar structure for most iOS developers</li></ul><p>While our metrics analysis is preliminary, we anticipate that MVC will show advantages in terms of code conciseness, as other architectural patterns typically introduce additional layers and complexity. However, a complete comparison with other patterns will be necessary to draw definitive conclusions.</p><p>In the upcoming articles in this series, we’ll explore alternative architectural patterns, providing a comprehensive comparison that will help you make informed decisions for your own iOS projects.</p><p>Thank you for following along with this deep dive into MVC! Be sure to check out the additional resources below for more information and practical examples.</p><h3>Useful Links</h3><ul><li>The iOS App — Football Gather — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather">GitHub Repo Link</a></li><li>The web server application made in Vapor — <a href="https://github.com/radude89/footballgather-ws"><br>GitHub Repo Link</a>,<br> <a href="https://www.radude89.com/blog/vapor.html">‘Building Modern REST APIs with Vapor and Fluent in Swift’ article link</a>, <br><a href="https://www.radude89.com/blog/migrate-to-vapor4.html">‘From Vapor 3 to 4: Elevate your server-side app’ article link</a></li><li>Model View Controller (MVC) — <a href="https://medium.com/better-programming/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">article</a> and <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVC">GitHub Repo Link</a></li><li>Model View ViewModel (MVVM) — <a href="https://medium.com/better-programming/battle-of-the-ios-architecture-patterns-a-look-at-model-view-viewmodel-mvvm-bdfd07d9395e">article</a> and <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVVM">GitHub Repo Link</a></li><li>Model View Presenter (MVP) — <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-mvp-f693f6efd23e">article</a> and <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP">GitHub Repo Link</a></li><li>Coordinator Pattern — MVP with Coordinators (MVP-C) — <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-model-view-presenter-with-coordinators-mvp-c-99edf7ab8c36">article</a> and <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/MVP-C">GitHub Repo Link</a></li><li>View Interactor Presenter Entity Router (VIPER) — <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-entity-router-viper-8f76f1bdc960">article</a> and <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIPER">GitHub Repo Link</a></li><li>View Interactor Presenter (VIP) — <a href="https://medium.com/geekculture/battle-of-the-ios-architecture-patterns-view-interactor-presenter-vip-59ebdae86e84">article</a> and <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/VIP">GitHub Repo Link</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=442241b447f6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/better-programming/battle-of-the-ios-architecture-patterns-model-view-controller-mvc-442241b447f6">Battle of the iOS Architecture Patterns: Model View Controller (MVC)</a> was originally published in <a href="https://betterprogramming.pub">Better Programming</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Migrating to Vapor 4]]></title>
            <link>https://radu-ionut-dan.medium.com/migrating-to-vapor-4-53a821c29203?source=rss-dee343eb346a------2</link>
            <guid isPermaLink="false">https://medium.com/p/53a821c29203</guid>
            <category><![CDATA[fluent]]></category>
            <category><![CDATA[development]]></category>
            <category><![CDATA[swift]]></category>
            <category><![CDATA[server-side-swift]]></category>
            <category><![CDATA[vapor]]></category>
            <dc:creator><![CDATA[Radu Dan]]></dc:creator>
            <pubDate>Tue, 10 Nov 2020 06:36:24 GMT</pubDate>
            <atom:updated>2025-01-17T16:41:05.564Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*v3WF_PrWTYFHGSiq-W483g.png" /><figcaption>Vapor 4 — Server Side Swift</figcaption></figure><h3>From Vapor 3 to 4: Elevate your server-side app</h3><p>In this article, we will explore how to migrate an application developed in Vapor 3 to the latest version, Vapor 4.</p><h3>Short recap</h3><p>We saw together in how we can develop a basic REST API in Vapor 3.</p><p>We covered in <a href="https://medium.com/@radu.ionut.dan/using-vapor-and-fluent-to-create-a-rest-api-5f9a0dcffc7b">this article</a> (ℹ️ or you can read it on my personal website — <a href="https://www.radude89.com/blog/migrate-to-vapor4.html">link here</a>) how to develop a basic REST API using Vapor 3.</p><p>The server-side app structure in Vapor 3 is as follows:</p><p>├── Public<br>├── Sources<br>│ ├── App<br>│ │ ├── Controllers<br>│ │ ├── Models<br>│ │ ├── boot.swift<br>│ │ ├── configure.swift<br>│ │ └── routes.swift<br>│ └── Run<br>│ └── main.swift<br>├── Tests<br>│ └── AppTests<br>└── Package.swift</p><h4><strong>Package.swift</strong></h4><ul><li>This file is the project’s manifest and defines all dependencies and targets for the app.<br>The project is set to use <strong>Vapor 3.3.0</strong>:<br>.package(url: &quot;https://github.com/vapor/vapor.git&quot;, from: &quot;3.3.0&quot;)</li></ul><h4><strong>Public</strong></h4><ul><li>Contains all public resources, such as images.</li></ul><h4>Sources</h4><ul><li>Contains two separate modules: App and Run.<br>The <strong>App</strong> folder is where you put all your developed code.<br>The <strong>Run</strong> folder contains the <strong>main.swift</strong> file.</li></ul><h4><strong>Models</strong></h4><ul><li>This is where you add your Fluent models. In this app, the models are: User, Player, Gather.</li></ul><h4><strong>Controllers</strong></h4><ul><li>Controllers contain the logic of your REST API, such as CRUD operations.<br>They are similar to iOS ViewControllers but handle requests and manage models.</li></ul><h4>routes.swift</h4><ul><li>Used to find the appropriate response for an incoming request.</li></ul><h4>configure.swift</h4><ul><li>This file is called before the app is initialized. It registers the router, middlewares, database, and model migrations.</li></ul><p>When the server app runs, the sequence is:<br>main.swift → app.swift (to create an instance of the app) → configure.swift (called before the app initializes) → routes.swift (handles route registration) → boot.swift (called after the app is initialized).</p><h3>Migration</h3><h4>Package</h4><p>A lot has changed from Vapor 3, and migrating to Vapor 4 is not as straightforward as it might seem.</p><p>Below, we present the differences in the Package.swift file. You can also check it out on <a href="https://github.com/radude89/footballgather-ws/commit/4c90a338af5f58756ed0a1606fae5057f125dd70#diff-a4b0339b87a790d65f8e5a754d1f547c81f819eb1a1109834b29d437763751d1">GitHub</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9POYVa2xlpOZXVubg4U59A.png" /><figcaption>Code diffs of <strong>Package.swift </strong>between Vapor 3 and 4</figcaption></figure><h3>Updating our Models</h3><p>Vapor 4 harnesses the full potential of <strong>Swift</strong> and includes property wrappers at its core.<br>The Model protocol now replaces the previous SQLiteTypeModel that was extended in Vapor 3.<br>Additionally, models no longer need to implement Migration.</p><p>We also remove the SQLiteUUIDPivot protocol and update PlayerGatherPivot to implement the standard Model protocol.</p><p>The foreign and private keys are now defined using the @Parent property wrapper.</p><p>You can see below the transformations applied to the codebase:</p><ul><li><strong>Gather</strong> model transformation — <a href="https://github.com/radude89/footballgather-ws/commit/4c90a338af5f58756ed0a1606fae5057f125dd70#diff-132b37146b8291ac6619b4c9c1742998">commit</a></li><li><strong>Player</strong> model transformation — <a href="https://github.com/radude89/footballgather-ws/commit/4c90a338af5f58756ed0a1606fae5057f125dd70#diff-b509dada4de4b816c5cac41dcc2f1cdb">commit</a></li><li><strong>Pivot</strong> model transformation — <a href="https://github.com/radude89/footballgather-ws/commit/4c90a338af5f58756ed0a1606fae5057f125dd70#diff-c979c7e2e5c0ebb7e7a4369c6a676225">commit</a></li><li><strong>Token</strong> model transformation — <a href="https://github.com/radude89/footballgather-ws/commit/4c90a338af5f58756ed0a1606fae5057f125dd70#diff-7200fca0ebef08da5b9bdb8e92f3cd1e">commit</a></li><li><strong>User</strong> model transformation — <a href="https://github.com/radude89/footballgather-ws/commit/4c90a338af5f58756ed0a1606fae5057f125dd70#diff-0ae67d4f3b860a3bca3c7a9f60103be8">commit</a></li></ul><p>Here is an example of the updated PlayerGatherPivot model:</p><pre>import Vapor<br>import FluentSQLiteDriver<br><br>// MARK: - Model<br>final class PlayerGatherPivot: Model {<br>    static let schema = &quot;player_gather&quot;<br>    @ID(key: .id)<br>    var id: UUID?<br>    @Parent(key: &quot;player_id&quot;)<br>    var player: Player<br>    @Parent(key: &quot;gather_id&quot;)<br>    var gather: Gather<br>    @Field(key: &quot;team&quot;)<br>    var team: String<br>    init() {}<br>    init(playerID: Player.IDValue,<br>         gatherID: Gather.IDValue,<br>         team: String) {<br>        self.$player.id = playerID<br>        self.$gather.id = gatherID<br>        self.team = team<br>    }<br>}<br>// MARK: - Migration<br>extension PlayerGatherPivot: Migration {<br>    func prepare(on database: Database) -&gt; EventLoopFuture {<br>        database.schema(PlayerGatherPivot.schema)<br>            .field(&quot;id&quot;, .uuid, .identifier(auto: true))<br>            .field(&quot;player_id&quot;, .int, .required)<br>            .field(&quot;gather_id&quot;, .uuid, .required)<br>            .field(&quot;team&quot;, .string, .required)<br>            .create()<br>    }<br>    func revert(on database: Database) -&gt; EventLoopFuture {<br>        database.schema(PlayerGatherPivot.schema).delete()<br>    }<br>}</pre><h3>Updating our Controllers</h3><p>Route collections still use the boot function, but the function’s parameter type has changed from Router to RoutesBuilder.</p><p>We no longer use Model.parameter. Instead, we now use :id as a parameter marker.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/756/0*KatgTvWVmGJKlq4S.png" /></figure><p>Fetching the authenticated user has changed from this in Vapor 3:</p><pre>let user = try req.requireAuthenticated(User.self)<br>return try user.gathers.query(on: req).all()</pre><p>To this in Vapor 4:</p><pre>let user = try req.auth.require(User.self)</pre><p>Regarding authentication, the middleware setup has also changed:</p><pre>let tokenAuthMiddleware = Token.authenticator()<br>let guardMiddleware = User.guardMiddleware() // same as Vapor 3<br>let tokenAuthGroup = gatherRoute.grouped(tokenAuthMiddleware, guardMiddleware)</pre><p>The token authentication middleware now implements a new protocol called <strong>ModelTokenAuthenticatable</strong>:</p><pre>extension Token: ModelTokenAuthenticatable {<br>    static let valueKey = \Token.$token<br>    static let userKey = \Token.$user<br><br>    var isValid: Bool { true }<br>}</pre><p>You can check all Vapor 4 controller transformations on GitHub:</p><ul><li>GatherController transformation - <a href="https://github.com/radude89/footballgather-ws/commit/4c90a338af5f58756ed0a1606fae5057f125dd70#diff-eaccbb095a0b78de13ad7c1556ab9795d97d47b1c2c67450013ba159d9b7f569">GitHub Link</a></li><li>PlayerController transformation - <a href="https://github.com/radude89/footballgather-ws/commit/4c90a338af5f58756ed0a1606fae5057f125dd70#diff-c4139b6f9f05a831bbc4ab0a1c8d251913a189714492c41568385417748472f7">GitHub Link</a></li><li>UserController transformation - <a href="https://github.com/radude89/footballgather-ws/commit/4c90a338af5f58756ed0a1606fae5057f125dd70#diff-55cf6bb35be98c756b21fb2557e2e68e9b04651746d01179b216aad81d74377f">GitHub Link</a></li></ul><p>The CRUD operations have undergone significant changes:</p><h4>GET All Gathers</h4><pre>func getGathersHandler(_ req: Request) throws -&gt; EventLoopFuture&lt;[GatherResponseData]&gt; {<br>    let user = try req.auth.require(User.self)<br>    return user.$gathers.query(on: req.db)<br>        .all()<br>        .flatMapEachThrowing {<br>            try GatherResponseData(<br>                id: $0.requireID(),<br>                userId: user.requireID(),<br>                score: $0.score,<br>                winnerTeam: $0.winnerTeam<br>            )<br>        }<br>}</pre><p>Note that flatMapEachThrowing is used to apply a closure to each element in the sequence, which is wrapped in an EventLoopFuture.</p><h4>CREATE a Gather</h4><pre>func createHandler(_ req: Request) throws -&gt; EventLoopFuture {<br>    let user = try req.auth.require(User.self)<br>    let gather = try Gather(userID: user.requireID())<br>    return gather.save(on: req.db).map {<br>        let response = Response()<br>        response.status = .created<br>        if let gatherID = gather.id?.description {<br>            let location = req.url.path + &quot;/&quot; + gatherID<br>            response.headers.replaceOrAdd(name: &quot;Location&quot;, value: location)<br>        }<br>        return response<br>   }<br>}</pre><h4>DELETE a Gather</h4><pre>func deleteHandler(_ req: Request) throws -&gt; EventLoopFuture {<br>    let user = try req.auth.require(User.self)<br><br>    guard let id = req.parameters.get(&quot;id&quot;, as: UUID.self) else {<br>        throw Abort(.badRequest)<br>    }<br>    return Gather.find(id, on: req.db).flatMap {<br>        guard let gather = $0 else {<br>            throw Abort(.notFound)<br>        }<br>        return gather.delete(on: req.db).transform(to: .noContent)<br>    }<br>}</pre><h4>UPDATE a gather</h4><pre>func updateHandler(_ req: Request) throws -&gt; EventLoopFuture {<br>    let user = try req.auth.require(User.self)<br>    let gatherUpdateDate = try req.content.decode(GatherUpdateData.self)<br><br>    guard let id = req.parameters.get(&quot;id&quot;, as: UUID.self) else {<br>        throw Abort(.badRequest)<br>    }<br><br>    return user.$gathers.get(on: req.db).flatMap { gathers in<br>        guard let gather = gathers.first(where: { $0.id == id }) else {<br>            return req.eventLoop.makeFailedFuture(Abort(.notFound))<br>        }<br><br>        gather.score = gatherUpdateDate.score<br>        gather.winnerTeam = gatherUpdateDate.winnerTeam<br><br>        return gather.save(on: req.db).transform(to: .noContent)<br>    }<br>}</pre><h4>GET players of a specified gather</h4><pre>func getPlayersHandler(_ req: Request) throws -&gt; EventLoopFuture&lt;[PlayerResponseData]&gt; {<br>    let user = try req.auth.require(User.self)<br>    guard let id = req.parameters.get(&quot;id&quot;, as: UUID.self) else {<br>        throw Abort(.badRequest)<br>    }<br><br>    return user.$gathers.get(on: req.db).flatMap { gathers in<br>        guard let gather = gathers.first(where: { $0.id == id }) else {<br>            return req.eventLoop.makeFailedFuture(Abort(.notFound))<br>        }<br><br>        return gather.$players.query(on: req.db)<br>            .all()<br>            .flatMapEachThrowing {<br>                try PlayerResponseData(<br>                    id: $0.requireID(),<br>                    name: $0.name,<br>                    age: $0.age,<br>                    skill: $0.skill,<br>                    preferredPosition: $0.preferredPosition,<br>                    favouriteTeam: $0.favouriteTeam<br>                )<br>            }<br>    }<br>}</pre><h4>POST player to a specified gather</h4><pre>func addPlayerHandler(_ req: Request) throws -&gt; EventLoopFuture {<br>      let user = try req.auth.require(User.self)<br>      let playerGatherData = try req.content.decode(PlayerGatherData.self)<br><br>      guard let gatherID = req.parameters.get(&quot;gatherID&quot;, as: UUID.self) else {<br>          throw Abort(.badRequest)<br>      }<br><br>      guard let playerID = req.parameters.get(&quot;playerID&quot;, as: Int.self) else {<br>          throw Abort(.badRequest)<br>      }<br><br>      let gather = user.$gathers.query(on: req.db)<br>          .filter(\.$id == gatherID)<br>          .first()<br><br>      let player = user.$players.query(on: req.db)<br>          .filter(\.$id == playerID)<br>          .first()<br><br>      return gather.and(player).flatMap { _ in<br>          let pivot = PlayerGatherPivot(<br>              playerID: playerID,<br>              gatherID: gatherID,<br>              team: playerGatherData.team<br>          )<br><br>          return pivot.save(on: req.db).transform(to: .ok)<br>      }<br>}</pre><h3>Models in Vapor 4</h3><p>The application has the following registered models: User, Gather, Player, PlayerGatherPivot, and Token.</p><h3>User Model</h3><p>The User model has three main properties:</p><ul><li>id: A UUID that serves as the primary key.</li><li>username: A unique name created during registration.</li><li>password: The hashed password of the user.</li></ul><p>An inner class, User.Public, is defined to control what public information (just the username) is exposed through methods such as <strong>GET</strong>.</p><p>The one-to-many relationships with Gather (a user can create multiple gatherings) and with Player (a user can create multiple players) are implemented using the Fluent @Children property wrapper:</p><pre>final class User: Model {<br>    static let schema = &quot;users&quot;<br><br>    @ID(key: .id)<br>    var id: UUID?<br><br>    @Field(key: &quot;username&quot;)<br>    var username: String<br><br>    @Field(key: &quot;password&quot;)<br>    var password: String<br><br>    @Children(for: \.$user)<br>    var gathers: [Gather]<br><br>    @Children(for: \.$user)<br>    var players: [Player]<br><br>    init() {}<br><br>    init(id: UUID? = nil,<br>         username: String,<br>         password: String) {<br>        self.id = id<br>        self.username = username<br>        self.password = password<br>    }<br>}</pre><p>We use extensions to wrap our functions for transforming a normal User to User.Public and vice versa:</p><pre>// MARK: - Public User<br>extension User {<br>    final class Public {<br>        var id: UUID?<br>        var username: String<br><br>        init(id: UUID?, username: String) {<br>            self.id = id<br>            self.username = username<br>        }<br>    }<br>}<br><br>extension User.Public: Content {}<br><br>extension User {<br>    func toPublicUser() -&gt; User.Public {<br>        return User.Public(id: id, username: username)<br>    }<br>}</pre><h3>Token Model</h3><p>The Token model defines a mapping between a generated 16-byte data (the actual token) that is base64 encoded and the user ID.</p><p>To generate the token, we use the CryptoRandom().generateData function, which relies on <strong>OpenSSL RAND_bytes</strong> to generate random data of the specified length.</p><p>The authentication pattern for the server application is Bearer token authentication.<br>Vapor simplifies this process by requiring the implementation of the BearerAuthenticatable protocol and specifying the key path of the token key:<br>static let tokenKey: TokenKey = \Token.token.</p><pre>final class Token: Model {<br>    static let schema = &quot;tokens&quot;<br><br>    @ID(key: .id)<br>    var id: UUID?<br><br>    @Field(key: &quot;token&quot;)<br>    var token: String<br><br>    @Parent(key: &quot;user_id&quot;)<br>    var user: User<br><br>    init() {}<br><br>    init(id: UUID? = nil,<br>         token: String,<br>         userID: User.IDValue) {<br>        self.id = id<br>        self.token = token<br>        self.$user.id = userID<br>    }<br>}<br><br>// MARK: - Authenticable<br>extension Token: ModelTokenAuthenticatable {<br>    static let valueKey = \Token.$token<br>    static let userKey = \Token.$user<br><br>    var isValid: Bool { true }<br>}</pre><h3>Gather Model</h3><p>The Gather model represents our football matches.</p><p>It has a parent identifier (the user ID) and two optional string parameters: the score and the winning team.</p><pre>final class Gather: Model {<br>    static let schema = &quot;gathers&quot;<br><br>    @ID(key: &quot;id&quot;)<br>    var id: UUID?<br><br>    @Parent(key: &quot;user_id&quot;)<br>    var user: User<br><br>    @OptionalField(key: &quot;score&quot;)<br>    var score: String?<br><br>    @OptionalField(key: &quot;winner_team&quot;)<br>    var winnerTeam: String?<br><br>    @Siblings(through: PlayerGatherPivot.self, from: \.$gather, to: \.$player)<br>    var players: [Player]<br><br>    init() {}<br><br>    init(id: UUID? = nil,<br>         userID: User.IDValue,<br>         score: String? = nil,<br>         winnerTeam: String? = nil) {<br>        self.id = id<br>        self.$user.id = userID<br>        self.score = score<br>        self.winnerTeam = winnerTeam<br>    }<br>}</pre><h3>Player Model</h3><p>The Player model is defined similarly to the Gather model:</p><ul><li>userID: The ID of the user who created this player (the parent).</li><li>name: Combines the first and last names.</li><li>age: An optional integer to store the player&#39;s age.</li><li>skill: An enum specifying the player&#39;s skill level (beginner, amateur, or professional).</li><li>preferredPosition: Represents the position on the field that the player prefers.</li><li>favouriteTeam: An optional string parameter to record the player&#39;s favorite team.</li></ul><pre>final class Player: Model {<br>    static let schema = &quot;players&quot;<br><br>    @ID(custom: \.$id)<br>    var id: Int?<br><br>    @Parent(key: &quot;user_id&quot;)<br>    var user: User<br><br>    @Field(key: &quot;name&quot;)<br>    var name: String<br><br>    @OptionalField(key: &quot;age&quot;)<br>    var age: Int?<br><br>    @OptionalField(key: &quot;skill&quot;)<br>    var skill: Skill?<br><br>    @OptionalField(key: &quot;position&quot;)<br>    var preferredPosition: Position?<br><br>    @OptionalField(key: &quot;favourite_team&quot;)<br>    var favouriteTeam: String?<br><br>    @Siblings(through: PlayerGatherPivot.self, from: \.$player, to: \.$gather)<br>    public var gathers: [Gather]<br><br>    convenience init() {<br>        self.init(userID: UUID(), name: &quot;&quot;)<br>    }<br><br>    init(id: Int? = nil,<br>         userID: User.IDValue,<br>         name: String,<br>         age: Int? = nil,<br>         skill: Skill? = nil,<br>         preferredPosition: Position? = nil,<br>         favouriteTeam: String? = nil) {<br>        self.id = id<br>        self.$user.id = userID<br>        self.name = name<br>        self.age = age<br>        self.skill = skill<br>        self.preferredPosition = preferredPosition<br>        self.favouriteTeam = favouriteTeam<br>    }<br>}</pre><h3>Relationships</h3><h4>Vapor 3</h4><p>The many-to-many relationship between gathers and players (where one gather can have multiple players and one player can be in multiple gathers) is implemented using Fluent pivots.</p><p>To achieve this, we create a new model class that extends SQLiteUUIDPivot (SQLite is the database we are currently using) and specify the key paths of the tables:</p><pre>static var leftIDKey: LeftIDKey = PlayerGatherPivot.playerId<br>static var rightIDKey: RightIDKey = PlayerGatherPivot.gatherId</pre><p>In our model classes, we can create convenient methods to access the gathers that a player has participated in, and conversely, the players that are part of a given gather:</p><pre>extension Player {<br>    var gathers: Siblings {<br>        return siblings()<br>    }<br>}<br><br>extension Gather {<br>    var players: Siblings {<br>        return siblings()<br>    }<br>}</pre><h4>Migrating to Vapor 4</h4><pre>final class PlayerGatherPivot: Model {<br>    static let schema = &quot;player_gather&quot;<br><br>    @ID(key: .id)<br>    var id: UUID?<br><br>    @Parent(key: &quot;player_id&quot;)<br>    var player: Player<br><br>    @Parent(key: &quot;gather_id&quot;)<br>    var gather: Gather<br><br>    @Field(key: &quot;team&quot;)<br>    var team: String<br><br>    init() {}<br><br>    init(playerID: Player.IDValue,<br>         gatherID: Gather.IDValue,<br>         team: String) {<br>        self.$player.id = playerID<br>        self.$gather.id = gatherID<br>        self.team = team<br>    }<br>}<br><br> // MARK: - Migration<br>extension Gather: Migration {<br>    func prepare(on database: Database) -&gt; EventLoopFuture {<br>        database.schema(Gather.schema)<br>            .field(&quot;id&quot;, .uuid, .identifier(auto: true))<br>            .field(&quot;user_id&quot;, .uuid, .required, .references(&quot;users&quot;, &quot;id&quot;))<br>            .foreignKey(&quot;user_id&quot;, references: &quot;users&quot;, &quot;id&quot;, onDelete: .cascade)<br>            .field(&quot;score&quot;, .string)<br>            .field(&quot;winner_team&quot;, .string)<br>            .create()<br>    }<br>}<br><br>extension Player: Migration {<br>    func prepare(on database: Database) -&gt; EventLoopFuture {<br>        database.schema(Player.schema)<br>            .field(&quot;id&quot;, .int, .identifier(auto: true))<br>            .field(&quot;user_id&quot;, .uuid, .required, .references(&quot;users&quot;, &quot;id&quot;))<br>            .foreignKey(&quot;user_id&quot;, references: &quot;users&quot;, &quot;id&quot;, onDelete: .cascade)<br>            .field(&quot;name&quot;, .string, .required)<br>            .field(&quot;age&quot;, .int)<br>            .field(&quot;skill&quot;, .string)<br>            .field(&quot;position&quot;, .string)<br>            .field(&quot;favourite_team&quot;, .string)<br>            .create()<br>    }<br>}</pre><h3>Controllers</h3><p>The service logic and routing are managed with RouteCollections.</p><p>In the boot function, we define the service paths and specify the methods to handle incoming requests.</p><p>For all collections, we define the resource path as api/{resource}. For example: api/users, api/gathers, or api/players.</p><p>Thus, if a GET request is made to <a href="https://foo.net/api/">https://foo.net/api/{resource}</a>, Vapor will look for the corresponding route and method. Remember to register it in routes.swift.</p><h4>UserController</h4><ul><li><strong>POST</strong> /api/users/login — Login functionality for users</li><li><strong>POST</strong> /api/users — Registers a new user</li><li><strong>GET</strong> /api/users — Gets the list of users</li><li><strong>GET</strong> /api/users/{userId} — Gets user by their ID</li><li><strong>DELETE</strong> /api/users/{user_id} — Deletes a user by the given ID</li></ul><p>Code snippet:</p><pre>extension UserController {<br>    func createHandler(_ req: Request) throws -&gt; EventLoopFuture {<br>        let user = try req.content.decode(User.self)<br>        user.password = try Bcrypt.hash(user.password)<br><br>        return user.save(on: req.db).map {<br>            let response = Response()<br>            response.status = .created<br><br>            if let userID = user.id?.description {<br>                let location = req.url.path + &quot;/&quot; + userID<br>                response.headers.replaceOrAdd(name: &quot;Location&quot;, value: location)<br>            }<br><br>            return response<br>        }<br>    }<br>}</pre><p>Before saving the user in the database, we hash the password using BCryptDigest.</p><p>After the user is saved, we retrieve the ID and return it as part of the Location response header, adhering to RESTful API practices by returning status codes instead of the actual resource.</p><p>The newly created resource can be associated with the unique ID provided in the Location header field. (<a href="https://restfulapi.net/http-status-codes/">https://restfulapi.net/http-status-codes/</a>).</p><p>The full implementation can be found on <a href="https://github.com/radude89/footballgather-ws/blob/master/FootballGatherServer/Sources/App/Controllers/UserController.swift">GitHub</a>.</p><h4>GatherController</h4><p>The GatherController contains the following methods:</p><p><strong>GET</strong> /api/gathers — Retrieves all gathers for the authenticated user</p><p><strong>POST</strong> /api/gathers — Creates a new gather for the authenticated user.</p><ul><li>The model for creation data is a subset of the actual <strong>Gather</strong> model, containing the score and the winner team, both parameters being optional.</li><li>This is similar to the User’s create method; if successful, we return the gather ID as part of the Location header.</li></ul><p><strong>DELETE</strong> /api/gathers/{gather_id} — Deletes the gather by its ID</p><p><strong>PUT</strong> /api/gathers/{gather_id} — Updates the gather associated with the given ID.</p><ul><li>We search through all saved gathers to find a match for the given ID.</li><li>If successful, we update the score and winner team of the gather.</li><li>Finally, we return <strong>204</strong>.</li></ul><p><strong>POST</strong> /api/gathers/{gather_id}/players/{player_id} — Adds a player to a gather.</p><ul><li>Many-to-many relationship.</li><li>We search for a gather with the given ID, similar to the update method.</li><li>If found, we retrieve it and create a new pivot.</li><li>Finally, we save the pivot model in the database and return <strong>200</strong>.</li></ul><p><strong>GET</strong> /api/gathers/{gather_id}/players — Returns the players for the given gather.</p><ul><li>Similar to the update and add player methods, we search for the gather with the given ID.</li><li>If found, we return all players associated with that gather.</li></ul><p>Code snippet:</p><pre>func addPlayerHandler(_ req: Request) throws -&gt; EventLoopFuture {<br>    let user = try req.auth.require(User.self)<br>    let playerGatherData = try req.content.decode(PlayerGatherData.self)<br><br>    guard let gatherID = req.parameters.get(&quot;gatherID&quot;, as: UUID.self) else {<br>        throw Abort(.badRequest)<br>    }<br><br>    guard let playerID = req.parameters.get(&quot;playerID&quot;, as: Int.self) else {<br>        throw Abort(.badRequest)<br>    }<br><br>    let gather = user.$gathers.query(on: req.db)<br>        .filter(\.$id == gatherID)<br>        .first()<br><br>    let player = user.$players.query(on: req.db)<br>        .filter(\.$id == playerID)<br>        .first()<br><br>    return gather.and(player).flatMap { _ in<br>        let pivot = PlayerGatherPivot(playerID: playerID,<br>                                      gatherID: gatherID,<br>                                      team: playerGatherData.team)<br><br>        return pivot.save(on: req.db).transform(to: .ok)<br>    }<br>}</pre><h3>Conclusion</h3><p>Vapor is an excellent framework for server-side programming, offering rich APIs for your web applications. More importantly, it is built on top of Swift, integrating the new features of the language into its core. This allows you to harness the true power of Swift, including features like property wrappers.</p><p>In this article, we examined a practical example of how to migrate an older application written in Vapor 3 to the latest version, Vapor 4.</p><p>The migration wasn’t as straightforward as we expected; many changes occurred between these versions.<br>The most notable change is the Fluent Models, which have now been moved to their own package in Vapor 4.</p><p>For a detailed overview of all changes, you can check this specific commit <a href="https://github.com/radude89/footballgather-ws/commit/4c90a338af5f58756ed0a1606fae5057f125dd70#diff-f04a00507d216ba6fa7d55d7516e184cca087439581bf921aabda31b1127b412">on GitHub</a>.</p><h3>Useful Links</h3><ul><li><a href="https://github.com/radude89/footballgather-ws">My repository with the example used in this article</a></li><li><a href="https://github.com/vapor/vapor">Vapor GitHub Repository</a></li><li><a href="https://github.com/vapor/fluent-kit">Fluent ORM Repository</a></li><li><a href="https://docs.vapor.codes/4.0/upgrading/">Upgrading to Vapor 4</a></li><li><a href="https://theswiftdev.com/articles/#vapor">The Swift Dev Blog Articles</a></li><li><a href="https://www.raywenderlich.com/11555468-getting-started-with-server-side-swift-with-vapor-4">Article on Ray Wenderlich — Getting Started with Server-Side Swift</a></li><li><a href="https://www.raywenderlich.com/books/server-side-swift-with-vapor/v3.0.ea1">Vapor 4 Book</a></li><li><a href="https://www.timc.dev/speaking/">Interesting talks by Tim Condon (member of Vapor’s Core Team)</a></li><li><a href="https://discord.gg/vapor">Vapor’s Discord Server</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=53a821c29203" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Stub your network responses with WireMock]]></title>
            <link>https://radu-ionut-dan.medium.com/stub-your-network-responses-with-wiremock-80cb33c17bf1?source=rss-dee343eb346a------2</link>
            <guid isPermaLink="false">https://medium.com/p/80cb33c17bf1</guid>
            <category><![CDATA[testing]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[wiremock]]></category>
            <category><![CDATA[continuous-integration]]></category>
            <category><![CDATA[swift-programming]]></category>
            <dc:creator><![CDATA[Radu Dan]]></dc:creator>
            <pubDate>Wed, 31 Jul 2019 07:35:33 GMT</pubDate>
            <atom:updated>2025-01-17T20:28:21.165Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KVY-rU0non5w-swzIhlwzg.png" /></figure><h3>Stubbing your network responses with WireMock</h3><h3>Motivation</h3><p>While working on unit tests for network responses, I was searching for a robust alternative to <a href="https://github.com/AliSoftware/OHHTTPStubs">OHHTTPStubs</a> when I discovered WireMock. What caught my attention was that it’s written in Java, runs as a standalone process, and offers a straightforward implementation.</p><p>Best of all, I wouldn’t need to make my project dependent on a third-party library by importing it directly into my unit test files.</p><h3>Understanding Stubbing vs Mocking</h3><p>Let’s start with stubbing: A stub provides predefined, consistent behavior. Think of it as a simple input-output mechanism — when you call method X, you’ll always get result Y. In our case, when working with API requests, we’ll create JSON files that serve as predetermined responses for specific endpoints.</p><p>Here’s a simple example:</p><p>POST /users {&quot;firstName&quot;: &quot;John&quot;, &quot;lastName&quot;: &quot;Smith&quot;}</p><p>When this request is made, our JSON file register-stub.json will consistently return a 200 OK response. It&#39;s predictable and straightforward.</p><p>Mocking, on the other hand, is more sophisticated. Mocks are what you configure as part of your test expectations, allowing you to verify behavior and interactions. They’re more flexible and can be programmed to respond differently based on various conditions.</p><p>Let’s look at a practical example with WireMock:</p><pre>{<br>    &quot;request&quot;: {<br>        &quot;method&quot;: &quot;POST&quot;,<br>        &quot;url&quot;: &quot;/api/users/login/success&quot;,<br>        &quot;headers&quot;: {<br>            &quot;Accept&quot;: {<br>                &quot;equalTo&quot;: &quot;application/json&quot;<br>            },<br>            &quot;Content-Type&quot;: {<br>                &quot;equalTo&quot;: &quot;application/json&quot;<br>            }<br>        },<br>        &quot;basicAuthCredentials&quot;: {<br>            &quot;username&quot;: &quot;demo-user&quot;,<br>            &quot;password&quot;: &quot;41bd876b085d6031cb0e04de35b88d77f83a4ba39f879fee40805ac19e356023&quot;<br>        }<br>    },<br>    &quot;response&quot;: {<br>        &quot;status&quot;: 200,<br>        &quot;headers&quot;: {<br>            &quot;Content-Type&quot;: &quot;application/json; charset=utf-8&quot;<br>        },<br>        &quot;bodyFileName&quot;: &quot;happy-path/response-200-users-login.json&quot;<br>    }<br>}</pre><p>Example mappings.user-login</p><pre>{<br>    &quot;request&quot;: {<br>        &quot;method&quot;: &quot;POST&quot;,<br>        &quot;url&quot;: &quot;/api/users/login/success&quot;,<br>        &quot;headers&quot;: {<br>            &quot;Accept&quot;: {<br>                &quot;equalTo&quot;: &quot;application/json&quot;<br>            },<br>            &quot;Content-Type&quot;: {<br>                &quot;equalTo&quot;: &quot;application/json&quot;<br>            }<br>        },<br>        &quot;basicAuthCredentials&quot;: {<br>            &quot;username&quot;: &quot;demo-user&quot;,<br>            &quot;password&quot;: &quot;41bd876b085d6031cb0e04de35b88d77f83a4ba39f879fee40805ac19e356023&quot;<br>        }<br>    },<br>    &quot;response&quot;: {<br>        &quot;status&quot;: 200,<br>        &quot;headers&quot;: {<br>            &quot;Content-Type&quot;: &quot;application/json; charset=utf-8&quot;<br>        },<br>        &quot;bodyFileName&quot;: &quot;happy-path/response-200-users-login.json&quot;<br>    }<br>}</pre><p>And __files/happy-path/response-200-users-login.json:</p><pre>{<br>    &quot;id&quot;: &quot;5D38234E-D67D-4DCF-BDB4-B9B7D21BA092&quot;,<br>    &quot;token&quot;: &quot;v2s4o0XcRgDHF/VojbAmGQ==&quot;,<br>    &quot;userID&quot;: &quot;07C3E7A9-7B0B-4CD8-97E0-93AEC7093862&quot;<br>}</pre><h3>Practical Use Case: Testing Your Networking Layer</h3><p>Let’s dive into a real-world scenario: You’ve just finished developing your networking framework, and now it’s time to ensure its reliability through comprehensive unit tests. This is where WireMock really shines.</p><p>To set the stage, here’s how the standard endpoint of your application might be structured:</p><pre>/// A standard implementation of the Endpoint protocol that represents<br>/// a network endpoint configuration<br>struct StandardEndpoint: Endpoint {<br>    /// The path component of the URL (e.g., &quot;/api/users&quot;)<br>    var path: String<br>    <br>    /// Optional query parameters to be added to the URL<br>    /// Example: [&quot;page&quot;: &quot;1&quot;, &quot;limit&quot;: &quot;10&quot;]<br>    var queryItems: [URLQueryItem]? = nil<br>    <br>    /// The URL scheme (defaults to &quot;https&quot;)<br>    var scheme: String? = &quot;https&quot;<br>    <br>    /// The host domain (defaults to &quot;foo.com&quot;)<br>    var host: String? = &quot;foo.com&quot;<br>    <br>    /// Optional port number for the URL<br>    /// Example: 8080 would result in foo.com:8080<br>    var port: Int? = nil<br><br>    /// Initializes a new endpoint with the given path<br>    /// - Parameter path: The URL path component<br>    init(path: String) {<br>        self.path = path<br>    }<br>}</pre><p>Now that we understand the basics, let’s create our test environment. We’ll use the mocks we discussed earlier (EndpointMock and URLSessionMockFactory) to set up our test scenario. Here&#39;s how we can structure our test file for maximum clarity and effectiveness:</p><p>You create your Mocks (EndpointMock and URLSessionMockFactory) as defined in the previous section. The tests file can be defined as below:</p><pre>import XCTest<br>@testable import FootballGather<br><br>/// Test suite for the LoginService class that handles user authentication<br>final class LoginServiceTests: XCTestCase {<br>    /// Mocked URLSession for simulating network requests<br>    private let session = URLSessionMockFactory.makeSession()<br>    <br>    /// API endpoint path for user login<br>    private let resourcePath = &quot;/api/users/login&quot;<br>    <br>    /// Mocked keychain for storing authentication tokens<br>    private let appKeychain = AppKeychainMockFactory.makeKeychain()<br>    <br>    /// Cleans up any stored data after each test<br>    override func tearDown() {<br>        appKeychain.storage.removeAll()<br>        super.tearDown()<br>    }<br>    <br>    /// Tests that a login request completes successfully and stores the token<br>    func test_request_completesSuccessfully() {<br>        let endpoint = EndpointMockFactory.makeSuccessfulEndpoint(path: resourcePath)<br>        let service = LoginService(<br>            session: session,<br>            urlRequest: StandardURLRequestFactory(endpoint: endpoint),<br>            appKeychain: appKeychain<br>        )<br>        let user = ModelsMockFactory.makeUser()<br>        let exp = expectation(description: &quot;Waiting response expectation&quot;)<br>        <br>        service.login(user: user) { [weak self] result in<br>            switch result {<br>            case .success(let success):<br>                XCTAssertTrue(success)<br>                XCTAssertEqual(self?.appKeychain.token!, ModelsMock.token)<br>                exp.fulfill()<br>            case .failure(_):<br>                XCTFail(&quot;Unexpected failure&quot;)<br>            }<br>        }<br>        <br>        // Wait for async operation to complete<br>        wait(for: [exp], timeout: TestConfigurator.defaultTimeout)<br>    }<br>    <br>    // other methods<br>}</pre><p>Where:</p><ul><li><strong>session</strong> — Is a mocked URLSession having an ephemeral configuration</li><li><strong>resourcePath</strong> — Is used for creating the endpoint URL for the User resources</li><li><strong>appKeychain</strong> — Is a mocked storage that acts as a Keychain for holding the token passed in the requests, after authentication. All data is hold in memory in a cache dictionary.</li></ul><p>In the test method, we define the mocks we are going to use in the login method.</p><p>The actual method is defined below:</p><pre>/// Authenticates a user and stores their token in the keychain<br>/// - Parameter user: The user credentials model for authentication<br>/// - Returns: A boolean indicating successful authentication<br>/// - Throws: ServiceError or network-related errors<br>func login(user: UserRequestModel) async throws -&gt; Bool {<br>    // Prepare the HTTP request<br>    var request = urlRequest.makeURLRequest()<br>    request.httpMethod = &quot;POST&quot;<br>    <br>    // Create and set Basic Authentication header<br>    let basicAuth = BasicAuth(<br>        username: user.username,<br>        password: Crypto.hash(message: user.password)!<br>    )<br>    request.setValue(<br>        &quot;Basic \(basicAuth.encoded)&quot;,<br>        forHTTPHeaderField: &quot;Authorization&quot;<br>    )<br>    <br>    do {<br>        // Perform the network request<br>        let (data, _) = try await session.data(for: request)<br>        <br>        // Validate response data<br>        guard !data.isEmpty else {<br>            throw ServiceError.expectedDataInResponse<br>        }<br>        <br>        // Decode the login response<br>        let loginResponse = try JSONDecoder().decode(<br>            LoginResponseModel.self,<br>            from: data<br>        )<br>        <br>        // Store the authentication token<br>        appKeychain.token = loginResponse.token<br>        <br>        return true<br>    } catch let decodingError as DecodingError {<br>        // Handle JSON decoding errors<br>        throw ServiceError.unexpectedResponse<br>    }<br>    // Other errors will propagate automatically<br>}</pre><p>For authentication, we send the username and password hash in the request header, using base64 encoding with a colon separator (:). This follows the standard <a href="https://en.wikipedia.org/wiki/Basic_access_authentication">basic access authentication principle</a>.</p><p>Upon successful authentication, the server returns a unique UUID token that we’ll use for subsequent network calls. This token is securely stored in the user’s keychain and includes an expiration mechanism for enhanced security.</p><p>Our unit test focuses on the “happy path” scenario, verifying two key aspects: successful request completion and proper token storage in the app’s keychain.</p><h3>Setting Up WireMock for Testing</h3><p>To ensure reliable testing, we need to properly initialize WireMock before our test suite runs and gracefully shut it down afterward. Here’s how to set this up:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*sPpgXbWjxVOSmcjA.png" /></figure><p>First, in Xcode, follow these steps:</p><ol><li>Open your project scheme (⌘ + &lt;)</li><li>Click ‘Edit Scheme’</li><li>In the left panel, select ‘Test’</li><li>Navigate to ‘Pre-actions’</li><li>Add the following startup script:</li></ol><p>java -jar &quot;${SRCROOT}/../stubs/wiremock-standalone-2.22.0.jar&quot; --port 9999 --root-dir &quot;${SRCROOT}/../stubs&quot; 2&gt;&amp;1 &amp;</p><p>Here’s the folder structure we’re using in this example:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*kGlT4topN_XD8vhV.png" /></figure><p>For proper cleanup, add this shutdown command to the Post-actions section:</p><p>curl -X POST <a href="http://localhost:9999/__admin/shutdown">http://localhost:9999/__admin/shutdown</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/812/0*0rXNDkdcy9Cno6f8.png" /></figure><p>With this setup in place, you can now run your unit tests and watch them pass successfully.</p><h3>Integrating with Continuous Integration</h3><p>Here’s a sample GitHub Actions workflow configuration (Note: You may need to adjust this based on your specific needs):</p><pre>name: iOS Tests<br>on:<br>  push:<br>    branches: [ main ]<br>  pull_request:<br>    branches: [ main ]<br><br>jobs:<br>  test:<br>    runs-on: macos-latest<br>    <br>    steps:<br>    - uses: actions/checkout@v3<br>    <br>    - name: Setup Java for WireMock<br>      uses: actions/setup-java@v3<br>      with:<br>        distribution: &#39;temurin&#39;<br>        java-version: &#39;11&#39;<br>    <br>    - name: Start WireMock<br>      run: ./scripts/start-wiremock.sh<br>    <br>    - name: Select Xcode<br>      uses: maxim-lobanov/setup-xcode@v1<br>      with:<br>        xcode-version: latest-stable<br>    <br>    - name: Run Tests<br>      run: ./scripts/run-tests.sh<br>      <br>    - name: Stop WireMock<br>      if: always()  # Ensures this runs even if tests fail<br>      run: ./scripts/stop-wiremock.sh</pre><p>You can take the scripts from <a href="https://github.com/radude89/footballgather-ios/tree/master/scripts">here</a>.</p><h3>Conclusion</h3><p>Throughout this article, we’ve explored how WireMock provides a robust solution for testing network interactions in iOS applications. We’ve covered everything from basic request stubbing to practical implementation in a login scenario, demonstrating how WireMock can be integrated into your testing workflow without adding dependencies to your main project.</p><p>Key takeaways from our exploration:</p><ul><li>WireMock runs as a standalone Java process, keeping your test code clean and dependency-free</li><li>It provides powerful stubbing capabilities for simulating various network scenarios</li><li>The setup integrates smoothly with both local development and CI/CD pipelines</li><li>You can effectively test both success and error scenarios in your networking layer</li></ul><p>For practical examples of implementation, you can explore the complete source code in my GitHub repository:</p><ul><li>Networking Unit Tests — <a href="https://github.com/radude89/footballgather-ios/tree/master/FootballGather/FootballGatherTests/Networking">GitHub link</a></li><li>WireMock Stubs — <a href="https://github.com/radude89/footballgather-ios/tree/master/stubs">GitHub link</a></li></ul><p>WireMock’s capabilities extend far beyond what we’ve covered here. You can simulate various network conditions, inject failures, define complex response patterns using regex, and leverage extensive logging capabilities for debugging. Whether you’re working on a simple networking layer or a complex API-driven application, WireMock provides the tools you need for comprehensive testing.</p><h3>References</h3><ul><li>For a comprehensive overview of network stubbing options in Swift, check out this excellent <a href="https://medium.com/xcblog/network-stubbing-options-for-xctest-and-xcuitest-in-swift-2e0dcce9a37d">guide on Medium</a></li><li>Martin Fowler’s seminal article “<a href="https://martinfowler.com/articles/mocksArentStubs.html">Mocks Aren’t Stubs</a>” provides essential background on test double patterns</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=80cb33c17bf1" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>