Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SwiftUI at SATS

SwiftUI at SATS

A tour of how we used SwiftUI in the SATS app.

A talk I gave for a local meetup https://www.meetup.com/swift-and-friends/events/307061032/

Avatar for Felipe Espinoza

Felipe Espinoza

April 21, 2025
Tweet

More Decks by Felipe Espinoza

Other Decks in Technology

Transcript

  1. iPhone 􀟜 􀟠 iPad 􀟤 Apple Watch Available on 🇳🇴

    🇸🇪 🇩🇰 🇫🇮 4 countries 2 Brands
  2. iPhone 􀟜 􀟠 iPad 􀟤 Apple Watch Available on 🇳🇴

    🇸🇪 🇩🇰 🇫🇮 4 countries 2 Brands
  3. iOS

  4. PM

  5. import Foundation import SATSSnapshots import SwiftUI import Testing import SATSCore

    @testable import Booking @MainActor struct BookingLandingViewTests { @Test( "Danish New Member with no GX, only PT trial", arguments: SnapshotVariant.fixedHeightVariants( height: 900, navigationTitle: "Danish New Member with no GX, only PT trial", backgroundColor: .backgroundSecondaryDefault ) ) func danishNewMemberNoGxOnlyPt(_ variant: SnapshotVariant) async throws { let view = BookingLandingView(viewData: .sampleDanishNewMemberWithoutGxOnlyPtTrial()) expectSnapshot(of: view, on: variant) } }
  6. Device iPhone, iPad (+ Model) Locale En, Nb, … Dynamic

    Type Large, Extra Large, … Region Norway, Chile, … Interface Style Light, Dark Orientation Portrait, Landscape, … Split screen Full screen, half screen, … Device Con fi guration View states Loading Error Empty Content Normal Content Content Situation #1 Content Situation #2 Content Situation #3
  7. How it helps? We can test UI in multiple settings

    easily Alerts unintentional changes in the UI, useful with design systems We can register and see the full content of scrollable content
  8. import Foundation import SATSSnapshots import SwiftUI import Testing import SATSCore

    @testable import Booking @MainActor struct BookingLandingViewTests { @Test( "Danish New Member with no GX, only PT trial", arguments: SnapshotVariant.fixedHeightVariants( height: 900, navigationTitle: "Danish New Member with no GX, only PT trial", backgroundColor: .backgroundSecondaryDefault ) ) func danishNewMemberNoGxOnlyPt(_ variant: SnapshotVariant) async throws { let view = BookingLandingView(viewData: .sampleDanishNewMemberWithoutGxOnlyPtTrial()) expectSnapshot(of: view, on: variant) } }
  9. New booking landing page Multiple di ff erent modules Which

    modules appear depend on user, the same as the content of said modules Polymorphic data structures "Smartness" part of the backend, the clients just render that JSON
  10. New booking landing page Multiple di ff erent modules Which

    modules appear depend on user, the same as the content of said modules Polymorphic data structures "Smartness" part of the backend, the clients just render that JSON
  11. "type": "Recommendations", "content": { "title": "What are you looking for?",

    "tabs": [ { "name": "For you", "items": [ { "type": "GroupExerciseClass", "content": { "id": "1234", "durationInMinutes": 45, "startTime": { "dateTime": "2025-08-21T00:00:00+00:00", "timeZone": "Europe/Oslo" }, "club": { "id": "1234566", "name": "Nydalen", "fullName": "SATS Nydalen" }, "instructorName": "Sandshrew", "classType": { "id": "123", "name": "Indoor Running", "description": "This is a class where you box things", "imageUrl": "https://images.ctfassets.net/bton54gi9dnn/1RLKjJn5thgmg6Jri7Orvn/09962b5059f4c218d23a9f2b51378eb0/Yoga_General.jpg? "fitnessScore": [ { "name": "Strength", "score": 1 } ] }, "bookingInfo": { "capacity": 10, "bookedCount": 15, "waitingListCount": 5, "bookingState": { "type": "BookedOnWaitingList", "content": { "waitingListPosition": 3, "participationProbability": "Low" }
  12. "id": "123", "name": "Indoor Running", "description": "This is a class

    where you box things", "imageUrl": "https://images.ctfassets.net/bton54gi9dnn/1RLKjJn5thgmg6Jri7Orvn/09962b5059f4c218d23a9f2b51378eb0/Yoga_General.jpg? "fitnessScore": [ { "name": "Strength", "score": 1 } ] }, "bookingInfo": { "capacity": 10, "bookedCount": 15, "waitingListCount": 5, "bookingState": { "type": "BookedOnWaitingList", "content": { "waitingListPosition": 3, "participationProbability": "Low" } } }, "friendBookings": [ { "member": { "id": "1234", "firstName": "Cezinando", "lastName": "Scicily", "imageUrl": "https://picsum.photos/512" }, "bookingState": { "type": "BookedOnWaitingList", "content": { "waitingListPosition": 3, "participationProbability": "Low" } } } ] } },
  13. struct RecommendationGxCardViewData: Identifiable { let id: GX.GroupExerciseID let name: String

    let startTime: String let clubName: String let durationInMinutes: String let image: ImageViewData? let friendsJoining: FriendJoiningViewData? let tag: TagViewData? let destination: Destination }
  14. struct RecommendationGxCardViewData: Identifiable { let id: GX.GroupExerciseID let name: String

    let startTime: String let clubName: String let durationInMinutes: String let image: ImageViewData? let friendsJoining: FriendJoiningViewData? let tag: TagViewData? let destination: Destination } public enum ImageViewData: Equatable { case empty case remote(url: URL) case image(_ image: Image) }
  15. extension RecommendationGxCardViewData { static func previewValue( name: String = "Indoor

    Running", startTime: String = "Tomorrow 08:00", clubName: String = "Nydalen", durationInMinutes: String = "45 min", image: ImageViewData? = .image(Image("gxClass1", bundle: .module)), friendsJoining: FriendJoiningViewData? = .previewValue(), tag: TagViewData? = nil ) -> Self { .init( id: .randomPreviewId(), name: name, startTime: startTime, clubName: clubName, durationInMinutes: durationInMinutes, image: image, friendsJoining: friendsJoining, tag: tag, destination: .booking ) } }
  16. import Foundation import SATSSnapshots import SwiftUI import Testing import SATSCore

    @testable import Booking @MainActor struct BookingLandingViewTests { @Test( "Landing page with all components", arguments: SnapshotVariant.fixedHeightVariants( height: 2400, navigationTitle: "Booking", backgroundColor: .backgroundSecondaryDefault ) ) func landingPageWithAllComponents(_ variant: SnapshotVariant) { let view = BookingLandingView(viewData: .previewValue()) expectSnapshot(of: view, on: variant) } }
  17. import Foundation import SATSSnapshots import SwiftUI import Testing import SATSCore

    @testable import Booking @MainActor struct BookingLandingViewTests { @Test( "Landing page with all components", arguments: SnapshotVariant.fixedHeightVariants( height: 2400, navigationTitle: "Booking", backgroundColor: .backgroundSecondaryDefault ) ) func landingPageWithAllComponents(_ variant: SnapshotVariant) { let view = BookingLandingView(viewData: .previewValue()) expectSnapshot(of: view, on: variant) } }
  18. Data Loading States Idle Loading Data Loaded Error Input Same

    fl ow for load of any kind of data The request fi res Received data Encountered an error Reloading Retry
  19. Basic Architecture #1 FeatureScreen FeatureView FeatureViewModel FeatureViewData ActionsProtocol Unidirectional data

    fl ow Split between stateful and stateless views Mutations will trigger a new view data value that will update the view We don’t use this pattern when it doesn’t fi t the problem
  20. Modularizing with SPM packages We use local packages, then we

    don’t pay integration costs Help code reuse across targets Faster compilation/iteration Explicit boundaries in code, less global sharing
  21. Base Infrastructure Features Targets • Models • Design Constants •

    Tracking Events • Networking • Design System • Navigation • Booking • Friends • Member Pro fi le • iOS • watchOS • Widgets Low High Complexity
  22. Base Infrastructure Features Targets • Models • Design Constants •

    Tracking Events • Networking • Design System • Navigation • Booking • Friends • Member Pro fi le • iOS • watchOS • Widgets Extracted from Figma Need dependency injection
  23. Base Infrastructure Features Targets • Models • Design Constants •

    Tracking Events • Networking • Design System • Navigation • Booking • Friends • Member Pro fi le • iOS • watchOS • Widgets
  24. Conclusion SwiftUI is excellent to render content and make multiple

    components Snapshot testing drives you to build better UI SwiftUI is quite fl exible when it comes to the choice of architecture Dividing code in SPM packages has multiple bene fi ts
  25. Building UI that is easy to Preview and Test with

    SwiftUI 17:30 Advanced Navigation for SwiftUI apps 12:24 Snapshot testing for iOS apps in Xcode Cloud 14:00 Recommended videos More details related to this talk