6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIで日本地図を描く

6
Last updated at Posted at 2025-12-03

はじめに

前々から日本地図描きたいな〜と思ってたので今回はSwiftUIで綺麗な日本地図を描きたいと思います。

最終的にはこんな感じの地図が描けます:point_down:

map.png

データの取得・加工については下記記事を参考にさせていただきました:pray:

手順

  1. 地図データをダウンロード
  2. 地図データを加工
  3. 地図データを軽量化
  4. SwiftUIで描く

地図データをダウンロード

下記のNatural Earthから地図データをダウンロードします。

ダウンロード手順。

  1. Downloads
  2. Large scale data, 1:10m
  3. Cultural
  4. Admin 1 – States, Provinces
  5. Download states and provinces

地図データを加工

下記のQGIS(3.44.5)を使って地図データを加工します。

SHPファイルを開く。

  1. レイヤ
  2. レイヤの追加
  3. ベクタレイヤの追加
  4. ダウンロードしたshpファイルを選択
  5. 追加

日本以外を削除。

  1. 編集モードに切り替え
  2. 地物の選択に切り替え
  3. 不要な地形を削除

沖縄を任意の位置に調整。

  1. 編集
  2. ジオメトリを編集
  3. 地物を移動

GeoJSONでエクスポート。

  1. レイヤ選択
  2. 右クリック
  3. エクスポート
  4. 新規ファイルに地物を保存
  5. 形式でGeoJson選択
  6. 保存

地図データを軽量化

このままだと描画にめちゃくちゃ時間がかかるので下記のmapshaperを使って地図データを軽量化します。

軽量化してエクスポート。

  1. Simplify
  2. スライダーで任意の値に軽量化
  3. Export
  4. GeoJson選択
  5. Export

SwiftUIで描く

エクスポートしたjsonファイルはこんな感じです。

{
  "type": "Feature",
  "geometry": { ... },   // 形(ポリゴン、線、点など)
  "properties": { ... }  // 属性情報(名前、コードなど)
}
"geometry": {
  "type": "Polygon",
  "coordinates": [
    [ [lon, lat], [lon, lat], ... ],  // 外周
  ]
}
"geometry": {
  "type": "MultiPolygon",
  "coordinates": [
    [
      [ [lon, lat], [lon, lat], ... ] // 外周
    ],
    [
      [ [lon, lat], ... ]              // 別の島など
    ]
  ]
}

あまり詳しく知らないですがおそらく内陸や飛地のない県はPolygonで島とかある県はMultiPolygonになるんだと思います。これを考慮して県ごとの緯度経度を取得します。

こんな感じです。

import Foundation

@Observable final class JapanMapStore {

    var polygons: [[[CGPoint]]] = []

    init() {
        loadData()
    }

    private func loadData() {
        guard let geoJSONURL = Bundle.main.url(forResource: "japan2", withExtension: "json"),
              let data = try? Data(contentsOf: geoJSONURL),
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let features = json["features"] as? [[String: Any]] else {
            return
        }

        let polygonPrefectures: [[[CGPoint]]] = features.compactMap { feature in
            guard let geometry = feature["geometry"] as? [String: Any],
                  let type = geometry["type"] as? String,
                  type == "Polygon",
                  let coordinates = geometry["coordinates"] as? [[[Double]]] else {
                return nil
            }

            let points: [CGPoint] = coordinates.flatMap { ring in
                ring.map { coord in
                    let lat = coord[1]
                    let lon = coord[0]
                    return CGPoint(x: lat, y: lon)
                }
            }
            return [points]
        }
       
        let multiPolygonPrefectures: [[[CGPoint]]] = features.compactMap { feature in
            guard let geometry = feature["geometry"] as? [String: Any],
                  let type = geometry["type"] as? String,
                  type == "MultiPolygon",
                  let coordinates = geometry["coordinates"] as? [[[[Double]]]] else {
                return nil
            }

            let polygons: [[CGPoint]] = coordinates.map { rings in
                rings.map { ring in
                    ring.map { coord in
                        let lat = coord[1]
                        let lon = coord[0]
                        return CGPoint(x: lat, y: lon)
                    }
                }
            }.flatMap { $0 }
            return polygons
        }

        polygons = polygonPrefectures + multiPolygonPrefectures
    }
}

次に描画範囲を指定して地図を描きます。何かと使いやすいので正方形の枠を作って描いていきます。

import SwiftUI

struct JpnMapView: View {

    @State var store = JapanMapStore()

    var body: some View {
        Canvas { context, size in
            let length = min(size.width, size.height)
            let drawRect = CGRect(
                x: (size.width - length) / 2,
                y: (size.height - length) / 2,
                width: length,
                height: length
            )
            
            store.polygons.forEach {
                var path = Path()
                $0.forEach { polygon in
                    path.addLines(polygon.map { point in
                        transformCoordinates(lat: point.x, lon: point.y, bounds: drawRect)
                    })
                    path.closeSubpath()
                }
                context.fill(path, with: .color(.green))
                context.stroke(path, with: .color(.black), lineWidth: 0.3)
            }
        }
    }

    private func transformCoordinates(lat: Double, lon: Double, bounds: CGRect) -> CGPoint {
        let geoBounds = (minLat: 30.0, maxLat: 46.0, minLon: 124.0, maxLon: 150.0)
        let x = (lon - geoBounds.minLon) / (geoBounds.maxLon - geoBounds.minLon) * bounds.width + bounds.origin.x
        let y = (1 - (lat - geoBounds.minLat) / (geoBounds.maxLat - geoBounds.minLat)) * bounds.height + bounds.origin.y
        return CGPoint(x: x, y: y)
    }
}

ポイントはここの緯度経度をViewの座標に変換しているところです。加工した地図はだいたい緯度30 ~ 46、経度124 ~ 150になるようにしてあります。ここの範囲は正方形になっていればいいのでおさまるようにいい感じに調整してください。

private func transformCoordinates(lat: Double, lon: Double, bounds: CGRect) -> CGPoint {
    let geoBounds = (minLat: 30.0, maxLat: 46.0, minLon: 124.0, maxLon: 150.0)
    let x = (lon - geoBounds.minLon) / (geoBounds.maxLon - geoBounds.minLon) * bounds.width + bounds.origin.x
    let y = (1 - (lat - geoBounds.minLat) / (geoBounds.maxLat - geoBounds.minLat)) * bounds.height + bounds.origin.y
    return CGPoint(x: x, y: y)
}

あとはJpnMapViewを呼び出すだけです。

import SwiftUI

struct ContentView: View {
    var body: some View {
        JpnMapView()
    }
}

完成:tada:

map.png

おわりに

これを色々やるとこんなこともできます:v:
map.gif

地図データを変えてやれば都道府県ごとの地図や標高マップも描けちゃいます:pencil2:

大阪府 標高マップ
osaka.png osaka_height.png

そうやって描いた地図を使って作成したアプリがこちらです:sunglasses:

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?