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() }
