我正在斯威夫图伊创建一款旅游应用,用户可以在其中跟踪他们go 过哪些国家.我想要做到这一点,允许用户从列表中 Select 他们访问过的国家,然后显示世界 map 的SVG,该 map 将访问的国家填充为蓝色.下面是一个例子:app example

下面是SVG文件的一个示例.每条道路代表一个国家,都有一个ID.我希望能够通过ID找到一条路径,并将其 colored颜色 更改为蓝色.用javascrip很容易做到这一点,但我很难在网上找到任何针对Swift的文档.

<path d="m 479.68275,331.6274 -0.077,0.025 -0.258,0.155 -0.147,0.054 -0.134,0.027 -0.105,-0.011 -0.058,-0.091 0.006,-0.139 -0.024,-0.124 -0.02,-0.067 0.038,-0.181 0.086,-0.097 0.119,-0.08 0.188,0.029 0.398,0.116 0.083,0.109 10e-4,0.072 -0.073,0.119 z" title="Andorra" id="AD" />

我使用SVGKit(https://github.com/SVGKit/SVGKit)将SVG导入到我的swftui视图中.下面是我用来在我的视图中简单显示SVG的代码.

我的SVGImageView struct :

struct SVGImageView: View {
    let svgImage: SVGKImage

    init(svgImage: SVGKImage) {
        self.svgImage = svgImage
    }

    var body: some View {
        Image(uiImage: svgImage.uiImage)
            .resizable()
            .scaledToFit()
    }
}

在斯威夫图伊的视野里:

if let svgImage = SVGKImage(named: "world") {
     SVGImageView(svgImage: svgImage)
          .frame(width: UIScreen.main.bounds.width * 0.85)
          .padding(.top)
}

这是目前的情况:my app screenshot

我还没有try 任何东西,我一直在GitHub上寻找不同的回购,但还没有任何运气,所以我决定在这里询问.

推荐答案

我以前也做过类似的事情,但只是为了中国.我使用的是GeoJSON数据,而不是SVG,因为我找不到适合中国 map 的SVG.使用GeoJSON的另一个优点是,您可以轻松更改 map 投影,并且文件大小可以小得多.

以下是我使用的绘图代码,适用于SwiftUI.我使用CodableGeoJSON包只是为了添加Codable个GeoJSON类型.您完全可以通过判断JSON的 struct 来自己编写这些类型,因此如果您不想要的话,就不必使用这个包了.

struct CountryProperties: Codable {
    let name: String
}

typealias MapFeatureCollection = GeoJSONFeatureCollection<MultiPolygonGeometry, CountryProperties>
struct GeoJSONMapDrawer {
    let featureCollection: MapFeatureCollection?
    
    let colorDict: [GeoJSONFeatureIdentifier: Color]
    
    func drawMap(borderColor: Color, borderWidth: CGFloat, size: CGSize, context: GraphicsContext) {
        func transformProjectedPoint(_ point: CGPoint) -> CGPoint {
            point
                .applying(CGAffineTransform(scaleX: size.width, y: size.height))
        }
        
        guard let featureCollection = self.featureCollection else { return }
        let features = featureCollection.features
        for feature in features {
            guard let multipolygon = feature.geometry?.coordinates else { continue }
            var multiPolygonPath = Path()
            let fillColor = colorDict[feature.id ?? ""] ?? .clear
            for polygon in multipolygon {
                let firstLinearRing = polygon.first!
                for (index, position) in firstLinearRing.enumerated() {
                    if index == 0 {
                        multiPolygonPath.move(to:
                            transformProjectedPoint(project(long: position.longitude, lat: position.latitude))
                        )
                    } else {
                        multiPolygonPath.addLine(to:
                            transformProjectedPoint(project(long: position.longitude, lat: position.latitude))
                        )
                    }
                }
                multiPolygonPath.closeSubpath()
            }
            context.fill(multiPolygonPath, with: .color(fillColor))
            context.stroke(multiPolygonPath, with: .color(borderColor), lineWidth: borderWidth)
        }
        print("Done!")
    }
    
    // this converts a longitude and latitude into a coordinate in a unit square
    func project(long: Double, lat: Double) -> CGPoint {
        let lowestLongitude: Double = -180
        let longitudeRange: Double = 360
        // the top and bottom of the map needs to be truncated,
        // because of how the mercator projection works
        // here I truncated the top 10 degrees and the bottom 24 degrees, as is standard
        let lowestLatitudeMercator: Double = mercator(-66)
        let latitudeRangeMercator: Double = mercator(80) - mercator(-66)
        let projectedLong = CGFloat((long - lowestLongitude) / longitudeRange)
        let projectedLat = CGFloat(1 - ((mercator(lat) - lowestLatitudeMercator) / latitudeRangeMercator))
        return CGPoint(x: projectedLong, y: projectedLat)
    }
    
    func mercator(_ lat: Double) -> Double {
        asinh(tan(lat * .pi / 180))
    }
}

正如您在代码中看到的,我使用mercator projection绘制 map ,原因很简单,因为经度根本不需要更改.

然后,您可以使用SwiftUI Canvas绘制此图:

struct ContentView: View {
    
    var body: some View {
        Canvas(rendersAsynchronously: true) { context, size in
            // as an example, I coloured Russia green
            let drawer = GeoJSONMapDrawer(featureCollection: loadGeojson(), colorDict: [
                // the keys in this dictionary corresponds to the "id" property in each feature
                "RU": .green
            ])
            drawer.drawMap(borderColor: .black, borderWidth: 1, size: size, context: context)
        }
        // from a quick google, 1.65 is apparently best for a mercator map
        .aspectRatio(1.65, contentMode: .fit)
    }
    
    func loadGeojson() -> MapFeatureCollection {
        let data = try! Data(contentsOf: Bundle.main.url(forResource: "world", withExtension: "json")!)
        return try! JSONDecoder().decode(MapFeatureCollection.self, from: data)
    }
}

我使用的GeoJSON文件是:https://gist.githubusercontent.com/markmarkoh/2969317/raw/15c2e3dee7769bb77b62d2a202548e7cce039bce/gistfile1.js请注意,在文件的开头有额外的var countries_data =,您应该删除它.

输出:

enter image description here

Ios相关问答推荐

将图案UI视图动画化以模拟进度条

ITMS—91053:Flutter Project中缺少API声明

当包含UITextView的inputAccessoryView显示键盘时,iOS UITableView内容会出现UINavigationBar奇怪的错误

如何使用参与者允许并行读取,但阻止对SWIFT中资源的并发读取和写入?

仅更改 AttributedString 的字符串(例如 UIButton 配置 attributeTitle)

NSError 错误领域: "CBATTErrorDomain" 代码: 128 0x0000000282b111a0

在 Swift 中,对象如何被准确地从内存中移除?

如何更改 SwiftUI 弹出视图的大小?

UIControl 子类 - 按下时显示 UIMenu

iOS SwiftUI - 使用 PhotosPicker 从图库中 Select 图像或视频

xcode-select:找不到clang

Swift 5.7 - 使存在的任何协议符合 Hashable

UIImageView 使用 CGAffineTransform 错误旋转

未找到签名证书​​iOS Distribution

Xcode 4.1 致命错误:自构建预编译头文件后修改了 stdlib

如何像在 Facebook 应用程序中一样以编程方式打开设置?

NSURLSession:如何增加 URL 请求的超时时间?

是否可以将 Socket.io 与 AWS Lambda 一起使用?

ARC - __unsafe_unretained 的含义?

/usr/bin/codedesign 失败,退出代码为 1