Albedo App Icon

Albedo

Track satellites with precision. Get weather-based visibility predictions. Experience the cosmos from your pocket.

Download on the App Store

Why I Built Albedo

I wanted to build a relatively advanced app right from the start. My goal was to dive deep into the code, explore Apple's documentation in detail, challenge myself by running into bugs and learning to solve them, and develop disciplined, well-organized code. In terms of design, I aimed to keep the look and feel very native, drawing inspiration from the button styles used in Apple Maps.

As for the app's focus, I'm passionate about astronomy. I often use the Heavens Above website to track satellites, and I wanted to create something just as precise; but with a polished and enjoyable mobile experience.

Technical Challenge

One key challenge was implementing the complex physics calculations to determine the exact minute when satellites enter and exit Earth's shadow, plus calculating magnitude variations throughout each pass. Obviously the architecture wasn't simple, I was at the beginning of my learning journey. I had to break and rebuild the app several times until I truly understood SwiftUI architecture.

Live Ground Tracks & Pass Prediction

Watch real-time satellite ground tracks with MapKit integration, fully optimized for dark mode. See the next pass for your selected satellite and get precise visibility chances based on weather conditions.

public static func calculateOrbitPaths(for satellite: Satellite) throws -> [CLLocationCoordinate2D] {
    var coordinates: [CLLocationCoordinate2D] = []
    let startDate = Date()

    for minute in 0...90 {
        let futureDate = startDate.addingTimeInterval(Double(minute) * 60)
        let position = try satellite.geoPosition(julianDays: futureDate.julianDate)
        let coordinate = CLLocationCoordinate2D(
            latitude: position.lat,
            longitude: position.lon - floor((position.lon + 180) / 360) * 360
        )
        coordinates.append(coordinate)
    }
    return coordinates
}

Ground tracks calculation method: SGP4 for low Earth orbit satellites (90-minute revolutions), SDP4 for high orbit satellites. Implementation from my custom Swift Package Manager AlbedoKit.

Custom recipe creationIngredient creation

Deep Dive into Pass Details

Explore every aspect of a satellite pass across 7 days of visibility. Get rise and set times, detailed weather conditions, satellite magnitude variations, and astronomical context.

do {
    async let hourlyForecast = weatherService.weather(
        for: userLocation,
        including: .hourly(startDate: nowDate, endDate: sevenDaysAhead)
    )
    async let dailyForecast = weatherService.weather(
        for: userLocation,
        including: .daily(startDate: nowDate, endDate: sevenDaysAhead)
    )

    let (hourly, daily) = try await (hourlyForecast, dailyForecast)
    return (hourly, daily)

} catch {
    throw WeatherError.failedToFetchWeather
}

Albedo powered by WeatherKit for pass forecasting, visibility probability calculations, and weather conditions analysis.

Custom recipe creationIngredient creation

Visual Tracking & Navigation

Swift Charts integration provides clear visual representations of satellite data. An integrated compass helps you track exactly where the satellite will pass in the sky.

switch model.selectionState {
case .selected(let selectedMagnitude):
    RuleMarkSelection(selectedMagnitude: selectedMagnitude, shadowEvent: model.pass.shadowEvent)
case .unselectedWithShadowEvent(let shadowState):
    ShadowRuleMark(shadowState: shadowState)
case .unselected:
    EmptyRuleMark()
}

Chart selection switcher using chartXSelection to manage interactive data visualization.

Visual Tracking & Navigation

What's under the hood

Technologies and frameworks powering Albedo's precision

Apple Frameworks

MapKit

Ground tracks

WeatherKit

Visibility forecasts

Swift Charts

Data visualization

StoreKit

In-app purchases

CoreLocation

Position tracking

Other Packages

I initially created my own package, AlbedoKit, to handle magnitude calculations and Earth shadow computations. I proposed integrating it into SatelliteKit, but the maintainer preferred to keep that library focused on its core functionality. Instead, he suggested contributing to SatelliteUtilities, a project specialized in satellite processing such as ground tracks, where I successfully added my work.

In addition, I built a custom backend with Vapor to implement intelligent caching. This allows users to share cached API data, reducing redundant API calls and improving performance.

Vapor

Backend

SatelliteKit

TLE parsing & mechanics

TinyMoon

Moon illumination

SatelliteUtilities

Coordinate transforms

Architecture

Throughout Albedo's development, I iteratively refactored the application architecture to deeply understand SwiftUI patterns. I evolved from basic MV to MVVM, then embraced Clean Architecture principles for better separation of concerns. The final iteration features a horizontal, modular architecture built with Swift Package Manager, enabling excellent scalability, comprehensive testing, and seamless feature additions without disrupting the entire codebase.

BarTinder App Icon

BarTinder

Swipe through ingredients. Discover cocktails. Create your own recipes. The modern way to explore mixology.

Why I Built BarTinder

Since Albedo is an app focused on REST APIs and physical calculations, I also wanted to create a simpler CRUD application that allows adding, updating and deleting items using Swift Data.

Built with the simplicity and modern approach of SwiftUI, BarTinder serves as an excellent learning resource for iOS developers at any level. The app leverages the latest iOS 26 APIs and features, including Foundation Models (Apple Intelligence), Liquid Glass design elements, SwiftData for CRUD operations, and Xcode 26's Default Actor Isolation and Approachable Concurrency settings.

Since I don't have a formal UI/UX design background, I drew heavy inspiration from Apple's stock applications and design language. I pay close attention to Apple's Human Interface Guidelines to ensure the app feels truly native.

I also wanted to build BarTinder with a clean, readable architecture while respecting proper separation of concerns. I wrote extensive tests using Swift Testing to practice modern testing techniques, with Xcode Cloud automatically catching any breaking changes. By focusing on the CRUD aspect, lists, scroll views and UI/UX, I was targeting a common application pattern that is widely used across many types of products.

Technical Challenge

Allowing users to cancel cocktail edits while using @Bindable presented a significant challenge. The solution required creating a draft SwiftData context to handle temporary changes, carefully managing potential conflicts between different contexts and @Query, all while maintaining clean and maintainable code architecture throughout the project.

Swipe Your Taste

Browse through ingredient cards with familiar swipe gestures. Love an ingredient? Swipe right. Not a fan? Swipe left.

private func handleSwipe(_ value: DragGesture.Value) {
    if value.translation.width >= threshold {
        offset = BarTinderApp.SwipingSettings.swipeOutDistance
        rotation = BarTinderApp.SwipingSettings.maxRotation
        Task { await model.swipeRight(card: cardIngredient) }
    } else if value.translation.width <= -threshold {
        offset = -BarTinderApp.SwipingSettings.swipeOutDistance
        rotation = -BarTinderApp.SwipingSettings.maxRotation
        Task { await model.swipeLeft(card: cardIngredient)  }
    } else {
        offset = 0
        rotation = 0
    }
}

Swipe gestures managed with threshold detection, offset calculations, and rotation animations for smooth card interactions.

Swipe Your Taste

Discover Perfect Matches

Based on your ingredient preferences, BarTinder shows you cocktails you can make right now with what you have. Sort them by name, glass type, or difficulty, and add your favorites to the Bar for quick access.

func executeUpdatePossibleCocktails() {
    let cocktails = repo.callGetContextContent()
    for cocktail in cocktails {
        let ingredientNames = Set(cocktail.ingredients.map(\.name))
        if selectedIngredients.isSuperset(of: ingredientNames) {
            cocktail.isPossible = true
        }
    }
    repo.callContextSave()
}

My first idea was to use Sets for easy ingredient comparison, and even deduce missing ingredients if needed.

Discover Perfect Matches

Create Your Signature

Create custom cocktails with your own photos from the library, choose your ingredients, quantities, glassware, and preparation steps. Edit or delete your creations anytime.

struct CreateEditCocktail: View {
    @Environment(\.modelContext) private var context
    @State private var model = CocktailCreationModel()
    @Bindable var cocktail: Cocktail

    var body: some View {
        List {
            Section {
                CocktailPreviewSection(selectedImage: $model.selectedPic, cocktail: cocktail)
            }
            // ...
        }
        .toolbar {
            CreationToolbar(cocktail: cocktail)
        }
        .navigationTitle(context.insertedModelsArray.isEmpty ? "Edit Cocktail" : "New Cocktail")
        .navigationBarBackButtonHidden()
        .navigationBarTitleDisplayMode(.inline)
        .scrollDismissesKeyboard(.interactively)
        .environment(model)
    }
}

Since @Model uses @Observable under the hood, I can directly bind cocktail properties to UI components like TextFields and Pickers. This allows me to pass a @Bindable cocktail to the view and use the same view for both creating and editing.

Custom recipe creationIngredient creation

Detailed Cocktail View

Dive deep into each cocktail with comprehensive details: ingredients list, glassware, and all characteristics. Add cocktails to your personal bar, edit recipes, or customize them to your taste.

@Model
final class Cocktail {
    #Index<Cocktail>([\.isInBar, \.isPossible])

    @Attribute(.unique)
    var name: String
    @Relationship(deleteRule: .cascade, inverse: \Ingredient.cocktail)
    var ingredients: [Ingredient]
    var isInBar: Bool
    var isPossible: Bool
    var imageName: String?
    @Attribute(.externalStorage)
    var imageData: Data?
    var style: CocktailStyle
    var glass: CocktailGlass
    var mixingTechnique: CocktailMixingTechnique
    var difficulty: CocktailDifficulty

    init(...) { ... }
}

@Model
final class Ingredient {
    var name: String
    var measure: String
    var unit: Units

    var cocktail: Cocktail?

    init(...) { ... }
}

A cocktail has a lot to offer. It includes glassware, style, technique, and difficulty.
Plus ingredients with measurements and units.

Detailed Cocktail View

What's under the hood

Technologies and frameworks powering BarTinder's cocktail experience

Apple Frameworks

SwiftData

Local persistence

Swift Testing

Unit testing

PhotosUI

Photo selection

FoundationModels

Apple Intelligence

Architecture & Design