Examples

Weather Service with Alamofire

Retrieve the current temperature for any city or location using Alamofire

Overview

This example demonstrates how to build a weather service application that retrieves current temperature information for any city or geographic location using Alamofire for networking. The app showcases real-world usage of networking, JSON parsing, and asynchronous calls by integrating with external APIs.

Functionality demonstratedTechnology demonstratedGithub Project
• Retrieving current temperature by city name
• Geocoding city names into coordinates
• Fetching weather data using coordinates
• Asynchronous network requests with Alamofire
• Dynamic UI update with fetched temperature
• Reusing Swift logic inside Android UI
• Swift Android compiler
• Swift4J
• SwiftPM Gradle for Android
• Alamofire networking
• Swift async/await
iOS Example
Android Example

The Weather Service with Alamofire example highlights the most commonly used features from the Foundation framework and Alamofire:

  • Networking: Performing asynchronous HTTP requests using Alamofire
  • JSON Parsing: Processing JSON responses using JSONSerialization
  • Geocoding: Converting city names into latitude and longitude using OpenStreetMap Nominatim

APIs used:

  • Open-Meteo - retrieves weather data
  • OpenStreetMap Nominatim - geocodes city names into coordinates

Architecture: The task is split into two parts:

  • Core App Logic: Developed in Swift using Alamofire
  • User Interface: Created in Android Studio to target the Swift logic

It is strongly recommended that you complete the HelloWorld example first, as it covers essential concepts that are only briefly mentioned in this example.

Develop in Xcode

Set up the Swift project as demonstrated in the HelloWorld tutorial

  1. Create Xcode project
  2. Modify Package.swift file
    • Add a dependency on the Swift4J package, which provides the necessary functionality to expose Swift code as a Java API.
    • Add a dependency on the Alamofire.swift package.
  3. Create class WeatherServiceAlamofire, import Swift4J, and add @JVM annotation
    See the HelloWorld for details.

The resulting Package.swift file:

// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import Foundation
import PackageDescription

let package = Package(
    name: "WeatherService",
    platforms: [.macOS(.v13)],

    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "WeatherService",
            type: .dynamic,
            targets: ["WeatherServiceAlamofire"])
    ],

    dependencies: [      
      .package(url: "https://github.com/scade-platform/swift4j.git", from: "1.3.0"),
      .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.10.2")
    ],

    targets: [
        .target(
            name: "WeatherServiceAlamofire",
            dependencies: [
              .product(name: "Swift4j", package: "swift4j"),
              .product(name: "Alamofire", package: "Alamofire")
            ]
        )
    ]
)

WeatherServiceAlamofire class

Create Sources/WeatherServiceAlamofire/WeatherServiceAlamofire.swift file:

import Foundation
import Swift4j
import Alamofire
import os

struct HexEncodingOptions: OptionSet {
    let rawValue: Int
    static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
}

extension Sequence where Element == UInt8 {
    func hexEncodedString(options: HexEncodingOptions = []) -> String {
        let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
        return self.map { String(format: format, $0) }.joined()
    }
}

/// A service for fetching current weather information using online APIs and Alamofire.
///
/// This class provides asynchronous methods to retrieve the current temperature
/// for a given city or geographic coordinates. It uses Open-Meteo for weather data
/// and OpenStreetMap Nominatim for geocoding city names to coordinates.
///
/// This class uses Alamofire for network requests.
@jvm
final class WeatherServiceAlamofire: Sendable {
    
    let weatherLog = OSLog(subsystem: "WeatherApp", category: "WeatherService")
    
    /// Fetches the current temperature for the specified latitude and longitude.
    ///
    /// - Parameters:
    ///   - latitude: The latitude of the location.
    ///   - longitude: The longitude of the location.
    ///   - response: A closure called with the temperature value and its units.
    ///
    /// This method makes a network request to the Open-Meteo API to retrieve the
    /// current temperature at the given coordinates. The result is returned via the
    /// response closure. If the request fails or the data cannot be parsed, the closure is not called.
    private func currentTemperature(latitude: Double, longitude: Double, _ callback: @escaping (Float, String) -> Void) {
        // Log coordinates
        os_log("currentTemperature: latitude=%f, longitude=%f", log: self.weatherLog, latitude, longitude)
        
        // Construct the API URL for the given coordinates
        // https://api.open-meteo.com/v1/forecast?latitude=52.510885&longitude=13.3989367&current=temperature_2m
        let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=\(latitude)&longitude=\(longitude)&current=temperature_2m")!
        
        // Perform the network request asynchronously
        // NOTE: disable gzip compression
        var headers = HTTPHeaders()
        headers.add(HTTPHeader(name: "Accept-Encoding", value: "identity"));
        AF.request(url, headers: headers).response { response in
            // Log the HTTP response and headers
            os_log("Response: %@, headers: %@", log: self.weatherLog, String(describing: response), String(describing: response.response?.allHeaderFields ?? [:]))
            
            if let error = response.error {
                // Log network request errors
                os_log("Failed to get temperature: %@", log: self.weatherLog, error.localizedDescription)
                return
            }
            
            let data = response.data!
            
            do {
                // Parse the JSON response to extract temperature and units
                guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                      let current = json["current"] as? [String: Any],
                      let current_units = json["current_units"] as? [String: Any],
                      let current_temp = current["temperature_2m"] as? Double,
                      let current_temp_units = current_units["temperature_2m"] as? String else { return }
                
                // Call the response closure with the temperature and units
                callback(Float(current_temp), current_temp_units)
            }
            catch {
                // Log coordinate parsing errors
                os_log("Cannot parse current temperature: %@", log: self.weatherLog, error.localizedDescription)
            }
        }
    }
    
    /// Fetches the current temperature for the specified city name.
    ///
    /// - Parameters:
    ///   - city: The name of the city to fetch the temperature for.
    ///   - response: A closure called with the temperature value and its units.
    ///
    /// This method first geocodes the city name to latitude and longitude using the
    /// OpenStreetMap Nominatim API, then fetches the current temperature for those coordinates.
    /// If the city cannot be found or the request fails, the closure is not called.
    func currentTemperature(city: String, _ callback: @escaping (Float, String) -> Void) async {
        // Construct the geocoding API URL for the city
        let url = URL(string: "https://nominatim.openstreetmap.org/search?city=\(city)&format=json")!
        
        // Perform the network request asynchronously
        // NOTE: set custom User-Agent header to avoid blocking by OSM service
        var headers = HTTPHeaders()
        headers.add(HTTPHeader(name: "User-Agent", value: "SCADE Weather Example"));
        AF.request(url, headers: headers).response { response in
            let data = response.data!
            
            do {
                // Parse the JSON response to extract latitude and longitude
                guard let json = try JSONSerialization.jsonObject(with: data) as? [Any],
                      let cityData = json.first as? [String: Any] else { return }
                
                guard let lat = cityData["lat"] as? String,
                      let lon = cityData["lon"] as? String else { return }
                
                // Convert latitude and longitude to Double and fetch temperature
                if let lat = Double(lat), let lon = Double(lon) {
                    self.currentTemperature(latitude: lat, longitude: lon, callback)
                } else {
                    // Log coordinate parsing errors
                    os_log("Cannot parse coordinates: %@, %@", log: self.weatherLog, lat, lon)
                }
            } catch {
                // Log errors
                os_log("Error: %@", log: self.weatherLog, error.localizedDescription)
            }
        }
    }
}

Explanation:

  • Uses Alamofire for all network requests
  • Private method fetches temperature by coordinates
  • Public method fetches temperature by city name and calls the private method
  • Exposed to Android via @jvm annotation

Set up the Android project as demonstrated in the HelloWorld tutorial

  1. Create a new Android project
  2. Set up the Gradle config file in the app directory
    • Add the Swift Packages for Gradle plugin to the project.
    • Add a configuration block for the plugin.

See the HelloWorld for details.

The resulting build.gradle.kts file:

plugins {
		// The first two plugins are generated by the Android Studio wizard
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)

		// We add the Swift Packages for Gradle plugin to the project
    id("io.scade.gradle.plugins.android.swiftpm") version "1.3.0"
}

android {
	// This part remains untouched and is generated by the Android Studio wizard
}

dependencies {
	// We add a dependency to the native SQLite3 library, which is required by GRDB.swift
	// This library is provided within an AAR archive and is built from official SQLite3 sources 
	implementation("io.scade.android.lib:sqlite3:3.49.0")

	// This part remains untouched and is generated by the Android Studio wizard
}

swiftpm {
    path = file("../../../Packages/WeatherService")
    product = "WeatherService"
    javaVersion = 8
    scdAutoUpdate = true
}

Bind Swift logic to Android UI control

Now, we can open the MainActivity.kt file and implement the user interface logic. For the sake of simplicity we just add a label that will show the current temperature for a given city. First, we add an import statement for the WeatherService class exported from the Swift package previously at the top of the file:

WeatherServiceAlamofire.WeatherServiceAlamofire

Next, we add the User-Interface related logic and calling the WeatherServiceAlamofire to the MainActivity class. The complete code of the MainActivity.kt file is presented below:

class MainActivity : ComponentActivity() {
    private val temperatureText = mutableStateOf("Current temperature: retrieving...")
     private lateinit var weather: WeatherServiceAlamofire

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        enableEdgeToEdge()
        setContent {
            WeatherAppTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Temperature(
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }

        // ----- Calling swift code -----

        System.loadLibrary("WeatherService")
         weather = WeatherServiceAlamofire()

        weather.currentTemperature("Berlin") { temp, units ->
            temperatureText.value = "The current temperature in Berlin is: $temp $units"
        }
    }

    @Composable
    fun Temperature(modifier: Modifier = Modifier) {
        val text by temperatureText

        val parts = text.split(": ")
        val description = parts.getOrNull(0) ?: ""
        val weatherInfo = parts.getOrNull(1) ?: ""

        androidx.compose.foundation.layout.Column(
            modifier = modifier
                .fillMaxSize(),
            horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
            verticalArrangement = androidx.compose.foundation.layout.Arrangement.Center
        ) {
            Text(text = description)
            Text(
                text = weatherInfo,
                fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
            )
        }
    }

}

Explanation:

  • A mutable state variable temperatureText holds the temperature string for UI display.
  • The UI is set up using Jetpack Compose. The Temperature composable function displays the current temperature.
  • The Swift library is loaded via System.loadLibrary("WeatherServiceAlamofire").
  • An instance of WeatherServiceAlamofire is created, and its method currentTemperature(city:callback:) is called (with "Berlin" as an example). The callback updates temperatureText with the retrieved temperature.
  • The Swift class uses Alamofire internally to perform asynchronous network requests to both Open-Meteo and OpenStreetMap Nominatim APIs.
  • Note that a Kotlin lambda is passed to the Swift function and is invoked asynchronously when the Alamofire network requests complete and the data is parsed.

Run the native Swift app on Android

img

Weather Service with Alamofire | Scade.io