오늘은 위도 경도 값을 가지고 경로선을 그려볼 것이다.
다른 지도 API는 어떤지 모르겠지만, 내가 사용해본 네이버 지도 API는 경로선 그리는 기능을 제공한다. 하지만 지도 위에 그리는 것이 아니라 경로선만 필요한 경우 직접 그려줘야 한다. 이 작업은 `Canvas`와 `Path`를 이용하려고 한다.
Canvas란?
Canvas는 SwiftUI에서 제공하는 뷰로, 벡터 기반의 그래픽 작업을 간단하게 구현할 수 있도록 설계되었다.
Canvas 기본 구조
- context: 그리기 컨텍스트로, `stroke`나 `fill`같은 그리기 작업을 하는데 사용한다.
- size: Canvas의 사이즈로, 너비와 높이를 나타내는 CGSize 구조체다.
Canvas { context, size in
}
Path란?
Path는 SwiftUI에서 벡터 기반의 그래픽을 그리기 위한 도형을 정의하는 구조체다. Path를 통해 선, 곡선, 다각형, 원, 사각형 등을 그리고, 다양한 스타일로 채우거나 테두리를 설정할 수 있다.
Path로 도형을 그릴 수 있다면 Canvas는 왜 사용해야 해 ???
라고 한다면 Path는 단일 도형을 그리지만 Canvas는 Path, Text, Image 등을 하나의 그리기 작업으로 묶을 수 있다.
구현하기
경로 그리기
이제 `Canvas`와 `Path`를 가지고 좌표 경로선을 그려보자.
먼저 테스트를 위한 더미데이터를 생성해두고,
struct Location {
let latitude: Double
let longitude: Double
}
let coordinates = [
Location(latitude: 37.547975, longitude: 126.910687),
Location(latitude: 37.547976, longitude: 126.910682),
Location(latitude: 37.547977, longitude: 126.910677),
Location(latitude: 37.547978, longitude: 126.910670),
Location(latitude: 37.547980, longitude: 126.910660),
Location(latitude: 37.547985, longitude: 126.910650),
Location(latitude: 37.547990, longitude: 126.910640),
Location(latitude: 37.547995, longitude: 126.910630),
Location(latitude: 37.548000, longitude: 126.910620),
Location(latitude: 37.548005, longitude: 126.910610),
Location(latitude: 37.548010, longitude: 126.910600)
]
Canvas와 Path를 생성해준다.
struct Path: View {
var body: some View {
VStack {
Canvas { context, size in
var path = Path()
}
}
}
}
다음으로 Path를 그릴 시작점을 옮겨주고
if let firstPoint = coordinates.first {
path.move(to: CGPoint(x: firstPoint.longitude, y: firstPoint.latitude))
}
각 좌표 사이의 선을 그어준 다음
for point in coordinates {
path.addLine(to: CGPoint(x: point.longitude, y: point.latitude))
}
그려주기만 하면 된다!
context.stroke(path, with: .color(.blue), lineWidth: 4)
하지만 여기서 중요한 점은 우리는 실제 좌표값을 비율에 맞게 한 화면에 담아야 한다.
즉, 좌표를 화면 사이즈에 맞게 정규화를 해야 한다.
좌표 정규화하기
좌표를 기기 사이즈에 맞추려면 현재 그리려는 좌표의 범위를 알아야 한다.
범위는 이런식으로 구현해봤다. `coordinatesRange`의 `minLat`와 `maxLat`가 0인 경우는 한 번도 업데이트가 된 적이 없는 것이므로 가장 처음 들어온 좌표 값을 위도 경도에 각각 넣어주었다. 위도 경도는 각각 `-90 ~ 90`, `-180 ~ 180`의 값을 가지는데, 사실 우리나라만 생각하자면 대략 범위가 나오긴 한다. ㅎㅎ
위도 경도 범위를 구할 때 `0.000002` 값을 +- 한 것은 그린 경로 뷰 내부에 padding을 넣어줬다고 생각하면 될 것 같다.
var coordinatesRange: (minLat: Double, maxLat: Double, minLng: Double, maxLng: Double) = (0,0,0,0)
func calculateCoordinateRange() {
for coordi in coordinates {
// first update
if coordinatesRange.minLat == 0.0 && coordinatesRange.maxLat == 0.0 {
coordinatesRange = (coordi.latitude, coordi.latitude, coordi.longitude, coordi.longitude)
}
// 위도 경도 범위 구하기
coordinatesRange.maxLat = max(coordinatesRange.maxLat, coordi.latitude + 0.00002)
coordinatesRange.minLat = min(coordinatesRange.minLat, coordi.latitude - 0.00002)
coordinatesRange.maxLng = max(coordinatesRange.maxLng, coordi.longitude + 0.00002)
coordinatesRange.minLng = min(coordinatesRange.minLng, coordi.longitude - 0.00002)
}
}
범위를 가지고 정규화를 해보자
내가 그리려는 좌표가 있을 때, 주어진 범위에서 현재 내가 어느 위치인지 알려면 `(current-min)/(max - min)` 와 같은 식을 사용할 수 있다.
이처럼 `normalizedX`와 `normalizedY`는 현재 좌표 `point`의 위도 경도 값을 0~1 값으로 정규화한 것이다. 위도의 경우 화면 좌표계와 다르게 지도의 위도는 아래쪽이 0이므로 `1 -` 를 통해 y 좌표가 반전되도록 한다.
우리가 구하고자하는 화면 상의 좌표는 정규화된 값에 스크린 사이즈를 곱해주면 된다.
func coordinateToCGPoint(point: Location, size: CGSize) -> CGPoint {
let normalizedX = (point.longitude - coordinatesRange.minLng) / (coordinatesRange.maxLng - coordinatesRange.minLng)
let normalizedY = 1 - (point.latitude - coordinatesRange.minLat) / (coordinatesRange.maxLat - coordinatesRange.minLat)
let lng = normalizedX * size.width
let lat = normalizedY * size.height
return CGPoint(x: lng, y: lat)
}
이제 위 함수를 이용해서 경로 그리는 코드를 수정하면 아래와 같다.
Canvas { context, size in
var path = Path()
// 시작점
if let firstPoint = coordinates.first {
let startPoint = coordinateToCGPoint(point: firstPoint, size: size)
path.move(to: startPoint)
}
// 이어 그리기
for point in coordinates {
let point = coordinateToCGPoint(point: point, size: size)
path.addLine(to: point)
}
context.stroke(path, with: .color(.blue), lineWidth: 4)
}
실행해보면 이런 경로가 나온다.
열심히 구현했는데 달랑 선 하나 있는 느낌이 들긴 하지만 완성은 했다 !
'iOS > SwiftUI' 카테고리의 다른 글
[SwiftUI] 버튼 사이즈 유지한 채로 터치 영역만 넓히기 (0) | 2025.05.29 |
---|---|
[SwiftUI] NavigationStack과 NavigationPath로 화면 스택 관리하기 (1) | 2025.04.16 |
[SwiftUI] 카카오 로그인 구현하기 (3) | 2024.09.06 |