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 demonstrated | Technology demonstrated | Github 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
- Create Xcode project
- Modify
Package.swiftfile- Add a dependency on the
Swift4Jpackage, which provides the necessary functionality to expose Swift code as a Java API. - Add a dependency on the
Alamofire.swiftpackage.
- Add a dependency on the
- Create class WeatherServiceAlamofire, import
Swift4J, and add@JVMannotation
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¤t=temperature_2m
let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=\(latitude)&longitude=\(longitude)¤t=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
- Create a new Android project
- Set up the Gradle config file in the
appdirectory- 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.WeatherServiceAlamofireNext, 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
temperatureTextholds the temperature string for UI display. - The UI is set up using Jetpack Compose. The
Temperaturecomposable function displays the current temperature. - The Swift library is loaded via
System.loadLibrary("WeatherServiceAlamofire"). - An instance of
WeatherServiceAlamofireis created, and its methodcurrentTemperature(city:callback:)is called (with"Berlin"as an example). The callback updatestemperatureTextwith 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
