Swift - Networking - ISS Location
The location of the International Space Station can be fetched with a very simple request from api.open-notify.org. With MapKit it is very easy to show it on a map.
The location is fetched from the endpoint `http://api.open-notify.org/iss-now.json` using `URLSession.shared.data(from:)`. To decode the JSON some helper types `ISSNowResponse` etc. can be used.
To display the location on a map use a map view from MapKit: `Map(initialPosition:,content:)`.
Note that the request for the ISS location uses HTTP and the connection is therefore not encrypted. Beginning with iOS 9 non-encrypted connections must be allowed by adding an App Transport Security exception to the Info.plist file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>api.open-notify.org</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
</dict>
</plist>
import SwiftUI
import MapKit
class ISSPositionViewModel: ObservableObject {
enum Err: Error {
case invalidURL
case invalidCoordinates
var localizedDescription: String {
switch self {
case .invalidURL: return "Invalid URL"
case .invalidCoordinates: return "Invalid coordinates"
}
}
}
/*
Response format for iss-now from api.open-notify.org:
{
"message": "success",
"timestamp": UNIX_TIME_STAMP,
"iss_position": {
"latitude": CURRENT_LATITUDE,
"longitude": CURRENT_LONGITUDE
}
}
*/
struct ISSNowResponse: Decodable {
let iss_position: ISSPosition
}
struct ISSPosition: Decodable {
let latitude: String
let longitude: String
}
@Published var coordinate: CLLocationCoordinate2D?
@Published var isLoading = false
@Published var errorMessage: String?
func fetchISSPosition() async {
await MainActor.run {
self.isLoading = true
self.errorMessage = nil
}
do {
let loc = try await fetchISSPosition_impl()
await MainActor.run {
self.coordinate = loc
self.isLoading = false
}
} catch {
await MainActor.run {
self.errorMessage = error.localizedDescription
self.isLoading = false
}
}
}
func fetchISSPosition_impl() async throws -> CLLocationCoordinate2D? {
guard let url = URL(string: "http://api.open-notify.org/iss-now.json") else {
throw Err.invalidURL
}
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode(ISSNowResponse.self, from: data)
guard
let lat = Double(decoded.iss_position.latitude),
let lon = Double(decoded.iss_position.longitude)
else {
throw Err.invalidCoordinates
}
return CLLocationCoordinate2D(latitude: lat, longitude: lon)
}
}
struct SampleISSPositionView: View {
@ObservedObject private var viewModel = ISSPositionViewModel()
var body: some View {
VStack(spacing: 16) {
if let error = viewModel.errorMessage {
Text("Error: \(error)").foregroundStyle(.red)
}
ZStack {
Color(white: 0.9)
if let coordinate = viewModel.coordinate {
Map(initialPosition: .region(
MKCoordinateRegion(
center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 180, longitudeDelta: 360)
)
), content: {
Marker("ISS", coordinate: coordinate)
.tint(.red)
})
.mapStyle(.hybrid)
} else if viewModel.isLoading {
ProgressView("Fetching ...")
} else {
Text("No position available")
}
}
.clipShape(RoundedRectangle(cornerRadius: 24))
}
.navigationTitle("ISS Position")
.navigationBarTitleDisplayMode(.large)
.task {
await viewModel.fetchISSPosition()
}
.padding()
}
}
#Preview {
SampleISSPositionView()
}