Swift - Networking - IP Location
The location of an IP-address can be fetched with a very simple request from ip-api.com. With MapKit it is very easy to show it on a map.
The location is fetched from the endpoint `http://ip-api.com/json` using `URLSession.shared.data(from:)`. To decode the JSON some helper type `IPLocation` 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 IP 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>ip-api.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
</dict>
</plist>
import SwiftUI
import MapKit
struct SampleIPLocationView: View {
enum Err: Error {
case invalidURL
case invalidCoordinates
case backendError
var localizedDescription: String {
switch self {
case .invalidURL: return "Invalid URL"
case .invalidCoordinates: return "Invalid coordinates"
case .backendError: return "Could not load location"
}
}
}
struct IPLocation: Decodable {
let status: String
let country: String
let regionName: String
let city: String
let lat: Double
let lon: Double
}
@State private var ipAddress: String = "8.8.8.8"
@State private var locationData: IPLocation? = nil
@State private var isLoading = true
@State private var errorMessage: String? = nil
@State private var location: CLLocationCoordinate2D?
var body: some View {
VStack(spacing: 16) {
HStack {
TextField("IP-Address", text: $ipAddress)
.padding(8)
Button(action: { Task { await fetchLocation() } }, label: { Image(systemName: "mappin.circle") })
.padding(8)
}
.background {
Color(white: 0.9)
}
.clipShape(RoundedRectangle(cornerRadius: 16))
if let errorMessage = errorMessage {
Text("Error: \(errorMessage)").foregroundStyle(.red)
}
ZStack {
Color(white: 0.9)
if let location = location {
VStack {
Map(initialPosition: .region(
MKCoordinateRegion(
center: location,
span: MKCoordinateSpan(latitudeDelta: 60, longitudeDelta: 60)
)
), content: {
Marker("IP: \(ipAddress)", coordinate: location)
.tint(.red)
})
.mapStyle(.hybrid)
if let locationData {
Form {
LabeledContent("City", value: locationData.city)
LabeledContent("Region", value: locationData.regionName)
LabeledContent("Country", value: locationData.country)
LabeledContent("Lat,Lon", value: "\(locationData.lat), \(locationData.lon)")
}
}
}
} else if isLoading {
ProgressView("Fetching ...")
} else {
Text("No position available")
}
}
.clipShape(RoundedRectangle(cornerRadius: 24))
}
.navigationTitle("IP Location")
.navigationBarTitleDisplayMode(.large)
.task {
await fetchLocation()
}
.padding()
}
func fetchLocation() async {
await MainActor.run {
isLoading = true
errorMessage = nil
}
do {
let locationData = try await fetchLocation_impl()
await MainActor.run {
if let locationData {
self.locationData = locationData
location = CLLocationCoordinate2D(latitude: locationData.lat, longitude: locationData.lon)
}
isLoading = false
errorMessage = nil
}
} catch {
await MainActor.run {
self.errorMessage = error.localizedDescription
self.isLoading = false
}
}
}
func fetchLocation_impl() async throws -> IPLocation? {
guard let url = URL(string: "http://ip-api.com/json/\(ipAddress)") else {
throw Err.invalidURL
}
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
let decoded = try decoder.decode(IPLocation.self, from: data)
if decoded.status == "success" {
return decoded
} else {
throw Err.backendError
}
}
}
#Preview {
SampleIPLocationView()
}