포스트

킥보드 프로젝트 4일차

프로젝트 4일차다.

점점 눈으로 보이기 시작한다.

어제에 이어, Annotation Pin관련 기능을 구현하는게 가장 중요하므로. 오늘은 이부분을 구현하면 될것같다.

AnnotationView의 경우, 우리가 주변에 있는 킥보드를 클릭했을때 간단한 정보 + 대여버튼이 있으면 좋을 것 같아서, 구현하려한다.

자료를 찾던 중 유튜브에 너무 코드 흐름이 잘 되어있어서, 이걸 기반으로 하나하나 작성해가면서 그 과정을 적어 보려 한다.

Custom Annotation Pin, View 구현

우선 Annotation Pin은 지금은 그냥 일반적인 pin이다.

이걸 킥보드의 이미지로 보여주면 더 좋을 것 같아서 수정하기로 결정.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// MARK: - AnnotationView
extension MapViewController: MKMapViewDelegate {
    
    // AnnotaionView 생성
    func mapView(_ mapView: MKMapView, viewFor annotation: any MKAnnotation) -> MKAnnotationView? {
        guard !(annotation is MKUserLocation) else {
            return nil
        }
        
        var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "customPin")
        
        if annotationView == nil {
            // view 생성
            
            annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "customPin")
            annotationView?.canShowCallout = true
            
            let btn = UIButton(type: .infoLight)
            annotationView?.rightCalloutAccessoryView = btn
            let click = UITapGestureRecognizer(target: self, action: #selector(showbtn))
            annotationView?.addGestureRecognizer(click)
            let btnClick = UITapGestureRecognizer(target: self, action: #selector(showView))
            btn.addGestureRecognizer(btnClick)
            
            
        } else {
            annotationView?.annotation = annotation
        }
        
        // pin image 조절 및 등록
        let pinImage = UIImage(named: "scooterPin")
        let size = CGSize(width: 40, height: 40)
        UIGraphicsBeginImageContext(size)
        pinImage!.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
        
        annotationView?.image = resizedImage
        
        return annotationView
    }
    
}

지도에 스쿠터모양의 pin이 생긴다.

simulator_screenshot_10F6C5B3-8379-42A0-8536-F03B1AAE4A3A

기능구현에 집중을 하다보니 결국 과정보단 결과를 기준으로 글을 작성하게 되었다.

위의 코드를 프로젝트 완료가 된 시점에서 리뷰를 하며 적는다

우선 CustomCell 처럼 이것도 역시 커스텀이 가능하지만, 위의 영상대로 이렇게도 나름 원하는대로 조정이 가능하기에 선택을 하게 되었다.

MKAnnotationView 란?

Annotation Object를 비주얼적으로 보여준다.

만드는 방법은 아주 다양하다, 위에서 언급한대로 별도의 Class를 사용하여 조금 더 디테일하게 만들 수도 있다.

아마 다음에 지도를 또 사용하게 된다면 사용할것같다.

다시 돌아와서, UIGestureRecognizer를 사용하여, 클릭을 할때 Event가 발생하게 하였다.

우선 마커들을 클릭했을때 정보를 표현하고 싶었기에, annotationView?.canShowCallout = true로 설정하였다.

그러면 예전 키오스크에서는 마커를 클릭해도 간단하게 확대가되고, 밑에 subtitle이 보였던것으로 기억하는데, 이제는 클릭을 하면 말풍선 같은게 생기면서 좀더 마커에 정보를 담을 수 있게한다.

Pin 추가 기능

생각해보니 핀에대한 언급이 없어서 여기에 적기로한다. (이부분은 사실 5일차에 디테일하게 구현한 내용.) 4일차에는 핀만 추가 되어있었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// pin 추가.
    func addMark(coordinate: CLLocationCoordinate2D, serial: String) {
        
        let pin = MKPointAnnotation()
        let address = CLGeocoder.init()
        address.reverseGeocodeLocation(CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)) { (placemarks, error) in
            var placeMark: CLPlacemark!
            placeMark = placemarks?[0]
            
            guard let address = placeMark else { return }
            
            pin.title = "기기번호: \(serial)"
            pin.subtitle = "현재위치: \(address.thoroughfare ?? "Apple Store")"
            
            pin.coordinate = coordinate
            self.mapView.addAnnotation(pin)
        }
        
    }

핀을 구현하는것 자체는 키오스크에서 썼던 방식과 크게 차이는 없다. 다만 가장 큰 차이라면 Subtitle에 들어가는 내용이 좌표를 기반으로한 현재 위치를 보여주는 것이다.

다만 주의사항이 있다면, 1분안에 많은 request가 발생할경우 지도에 핀이 보이지 않는다.

현재 좌표를 기반으로 주소를 호출하는 메서드인 reverseGeocodeLocation을 사용한다. 그리고 Completion Handler를 사용하는 이유는, 이것도 일종의 Apple의 REST API를 호출하는 개념이기 때문.

성공하면 Address정보가 나오게 된다. 주소를 길게 표현을 하는게 좋을까 하다가, 그냥 마지막 주소 예를 들면 한국이면 ~~로 이런식으로 표현을 하는게 좋을 것같아 이렇게 구현했다.

우선 시뮬레이터에서 일본, 미국으로 테스트했을때 둘의 지역표기가 달라서 thothoroughfare 단위를 사용했다.

그리고 pin의 좌표도 설정해주고, 지도에 핀을 박게 하였다.

pin 선택시 이벤트 구현

1. 선택시 거리를 기준으로 구현.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 func mapView(_ mapView: MKMapView, didSelect annotation: any MKAnnotation) {
        
        let pin = annotation
        let currentLocation = mapView.userLocation.location
        
        guard let distance = currentLocation?.distance(from: CLLocation(latitude: pin.coordinate.latitude, longitude: pin.coordinate.longitude)) else {
            fatalError("Can't get distance from Pin")
        }
        
        if distance > 100.0 {
            rentButton.isHidden = true
            let alert = UIAlertController(title: "구역 외 킥보드 접근", message: "100m 를 넘어선 킥보드는 이용이 불가합니다.", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "확인", style: .default))
            self.present(alert, animated: true)
        }

    }

Apr-25-2024 23-07-56

저때 거리는 123미터였다.

구현완료.

하지만 기본적으로는 보이게는 해야하기에 대여버튼을 눌렀을때 해당 로직이 구현이 되어야한다.

2. 대여 시 pin 삭제.

처음에는 무수히 많은 핀들중 어떻게 내가 선택한 핀만 확인할 수 있을까 고민을 했지만, 나의 배움의 부족으로 아이디어가 떠오르지 않았다.

그러다가 스택오버플로우에서 내가 원하는 걸 찾았다.

바로 didSelect Method를 활용하는것이다.

보자마자 아차 싶었고, 바로 적용을 했다.

1
2
3
4
5
6
7
8
9
10
11
var selectedAnnotation: MKPointAnnotation?

 func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
        self.selectedAnnotation = view.annotation as? MKPointAnnotation
    }


func completedRent(didSelect annotation: any MKAnnotation) {
        let pin = annotation
        mapView.removeAnnotation(pin)
    }

Apr-26-2024 01-55-04

구현 완료.

3. 주행기록 Data 저장.

실제 주행거리는 아니지만 좌표로 계산해서 이런 기능만 보여주려고 만들었다.

물론 일정한 시간마다 거리계산용 배열에 좌표값을 계속 추가해준다면, 실제 이동한 거리 구현도 가능하다.

하지만 튜터님께 여쭤본 결과 지금 프로젝트 단계에서는 이정도 구현도 충분하다고 하셨기에, 여기서 멈춘다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
var locations: [CLLocationCoordinate2D] = [] // 거리 계산용 배열

// 거리계산 함수.
    func calculateTripDistance() {
        
        var total: Double = 0.0
        for i in 0..<locations.count - 1 {
            let start = locations[i]
            let end = locations[i + 1]
            let distance = getDistance(from: start, to: end)
            total += distance
        }
       
    }
    
    func getDistance(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) -> CLLocationDistance {
        let from = CLLocation(latitude: from.latitude, longitude: from.longitude)
        let to = CLLocation(latitude: to.latitude, longitude: to.longitude)
        return from.distance(from: to)
    }

// 대여시
@IBAction func didTapRentButton(_ sender: Any) {
        let rentProcessAlert = UIAlertController(title: "대여 진행", message: "해당 킥보드를 이용하시겠습니까?", preferredStyle: .alert)
        
        let rent = UIAlertAction(title: "대여하기", style: .default) { _ in
            self.completedRent(didSelect: self.selectedAnnotation!)
            
            if let coordinate = self.selectedAnnotation?.coordinate { // new
                self.locations.append(coordinate)
            }
            
            DispatchQueue.main.async {
                
                self.isUsed = true
                self.setbuttonHidden(isStatus: self.isUsed)
            }
        }
        
        let cancel = UIAlertAction(title: "취소", style: .cancel)
        
        rentProcessAlert.addAction(cancel)
        rentProcessAlert.addAction(rent)
        present(rentProcessAlert, animated: true, completion: nil)
        
        
        
    }
// 반납시
@IBAction func returnScooterBtn(_ sender: UIButton) {
        DispatchQueue.global().async { [weak self] in
            if CLLocationManager.locationServicesEnabled() {
                self?.locationManager.requestWhenInUseAuthorization()
                let currentLocation = self?.locationManager.location
                
                self?.addMark(coordinate: CLLocationCoordinate2D(latitude: currentLocation?.coordinate.latitude ?? 37.503702192, longitude: currentLocation?.coordinate.longitude ?? 127.025313873406))
                self?.locations.append(currentLocation!.coordinate)
            }
            
        }
        isUsed = false
        setbuttonHidden(isStatus: isUsed)
        
        let alert = UIAlertController(title: "반납완료", message: "킥보드 반납이 완료되었습니다.\n안녕히 가세요.", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "확인", style: .default))
        self.present(alert, animated: true)
        let distance = calculateTripDistance()
        let finTime = Date.now
        RecordSingleton.shared.array.append(RecordModel(distance: distance, time: finTime))
    }            

대여시 거리계산용 배열에 좌표를 추가하고, 반납시 좌표를 추가하여, 그 둘의 값을 계산하는 시퀀스로 이루어 진다고 생각하면 된다.

위에도 적었지만 이걸 일정한 간격마다 계속 추가하고 함수를 계속 사용하여 Dispatchqueue를 사용하여 해당 내용을 어떤 View에 추가를 한다면,

실시간 이동거리를 계산 할 수도 있다.

이부분은 프로젝트 개조할때 해보는걸로…

4. 반납시 기록을 저장할 배열을 싱글턴으로 구현

위에 이미 마지막에 추가하는게 적혀있지만 그래도 사용하게된 이유를 적어보자면, 우선 싱글턴으로 구현을 해서 지도와, 마이페이지의 주행거리 쪽을 눌러서 확인할때 데이터를 굳이 왔다갔다 하는게 의미가 있나 싶어서 싱글턴을 사용하면 좋겠다 싶어 바로 구현을 했다.

키오스크때 사용을 해서 그런가, 바로 구현을 했다.

1
2
3
4
5
6
7
8
9
10
11
12
import Foundation

class RecordSingleton {
    
    static let shared = RecordSingleton()
    
    var array: [RecordModel] = [RecordModel]()
    
    private init () {}
    
}

5. 추가 대여 방지 로직 구현.

사실 이건 Boolean을 통해 대여하기 버튼을 보이지 않는 형식으로 단순하게 했다.

var isUsed: Bool = false

isUsed라는 값을 통해 유져가 사용중인지? 아닌지를 판단하고 그에따라 버튼을 보여줄지 말지를 결정하게 했다.

사실 추가대여 방지는 사용할때 바로 버튼을 다 보이지않게 숨기고, 그다음에 사용을 다하게 되면 보여주는 방식이 더 좋았지만

현재 기능 테스트에서는 단순히 버튼만 보여주고 말고만 하게 하였다.

1
2
3
4
5
6
7
8
9
10
11
// 대여의 상태를 보고 버튼을 숨기거나 보여줌
    func setbuttonHidden(isStatus: Bool) {
        
        if !isStatus {  // false 대여를 하지 않은 상태
            rentButton.isHidden = true // true인 이유는 평상시에도 숨기다가 킥보드를 클릭했을때 보여주게 하기위함.
            returnButton.isHidden = true
        } else { // 대여를 한 상태라면
            rentButton.isHidden = true
            returnButton.isHidden = false
        }
    }

확인 완료.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.