비전공자 4명이 팀을 이루어 진행한 프로젝트입니다. 다소 어설픈 부분이 (많이)있을 수 있습니다.
프로젝트는 깃허브에서 보실 수 있습니다.
저는 프로젝트에서 회원가입/로그인 화면 직후 진입할 수 있는 지도 뷰를 작성했습니다.
//
// ViewController.swift
// NBCSuper8oard
//
// Created by 영현 on 1/21/24.
//
import UIKit
import CoreLocation
import NMapsMap
class MapViewController: UIViewController, UIViewControllerTransitioningDelegate, NMFMapViewTouchDelegate, CLLocationManagerDelegate {
lazy var mapView = NMFNaverMapView(frame: view.frame)
var locationManager = CLLocationManager()
weak var tabBarVC: TabBarController?
lazy var boardList = [Board]()
var numberOfDummyData = 30
var ridingBoardNumber: Int?
lazy var inUseLabel: UILabel = {
let label = UILabel()
label.text = "이용 중"
label.font = UIFont.systemFont(ofSize: 20)
label.textColor = .white
label.backgroundColor = .systemBlue
label.textAlignment = .center
label.frame = CGRect(x: 150, y: 50, width: 100, height: 40)
label.layer.cornerRadius = 20
label.isHidden = true
return label
}()
lazy var returnButton: UIButton = {
let button = UIButton()
button.setTitle("반납하기", for: .normal)
button.backgroundColor = .systemBlue
button.tintColor = .white
button.frame = CGRect(x: 150, y: 700, width: 100, height: 40)
button.isHidden = true
button.layer.cornerRadius = 10
button.addTarget(self, action: #selector(returnBoard), for: .touchUpInside)
return button
}()
lazy var searchButton: UIButton = {
let search = UIButton()
search.setTitle("주소 검색", for: .normal)
search.tintColor = .white
search.backgroundColor = .systemBlue
search.frame = CGRect(x: 280, y: 50, width: 100, height: 40)
search.layer.cornerRadius = 10
search.addTarget(self, action: #selector(searchButtonTapped), for: .touchUpInside)
return search
}()
//MARK: Marker Click Event
lazy var markerTapEvent = { [weak self] (overlay: NMFOverlay) -> Bool in
let marker = overlay
var tappedBoard: Board?
for board in self!.boardList {
if marker.userInfo["id"] as? Int == board.boardNumber {
tappedBoard = board
} else { continue }
}
let detailVC = DetailViewController(selectedBoard: tappedBoard, user: self?.tabBarVC?.user)
detailVC.isRented = { [weak self] board in
for i in 0..<self!.boardList.count {
if self?.boardList[i].boardNumber == board.boardNumber {
self?.boardList[i].isAvailable = board.isAvailable
}
}
}
detailVC.hideMarker = { [weak self] isAvailable in
marker.mapView = isAvailable ? self?.mapView.mapView : nil
self?.tabBarVC?.user?.isRiding = !isAvailable
self?.inUseLabel.isHidden = isAvailable
self?.returnButton.isHidden = isAvailable
}
self?.ridingBoardNumber = tappedBoard?.boardNumber
self?.present(detailVC, animated: true, completion: nil)
return true
}
@objc func returnBoard() {
let returnAlert = UIAlertController(title: "반납하기", message: "반납하시겠습니까?", preferredStyle: .alert)
let returnAction = UIAlertAction(title: "반납", style: .default) { _ in
self.inUseLabel.isHidden = true
self.tabBarVC?.user?.isRiding = false
if let ridingBoardNumber = self.ridingBoardNumber {
let currentLocation = self.setCurrentLocation()
let marker = NMFMarker(position: currentLocation)
marker.iconImage = NMFOverlayImage(image: UIImage(named: "BoardMarkerIcon")!.resized(to: CGSize(width: 30, height: 30)))
marker.touchHandler = self.markerTapEvent
marker.minZoom = 10
marker.userInfo["id"] = String(ridingBoardNumber)
marker.mapView = self.mapView.mapView
self.returnButton.isHidden = true
for i in 0..<self.boardList.count {
if self.boardList[i].boardNumber == ridingBoardNumber {
self.boardList[i].isAvailable = true
}
}
}
}
let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil)
returnAlert.addAction(returnAction)
returnAlert.addAction(cancelAction)
present(returnAlert, animated: true)
}
@objc func searchButtonTapped() {
let searchVC = SearchViewController()
present(searchVC, animated: true)
searchVC.setCameraLocation = { lat, lng in
let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: lat, lng: lng), zoomTo: 12)
self.mapView.mapView.moveCamera(cameraUpdate)
cameraUpdate.animation = .easeIn
}
}
func makeDummyData() {
for i in 0..<numberOfDummyData {
let tempLoc = generateRandomNMGLatLng()
let data = Board(boardType: "ninebot", boardNumber: i, boardBattery: 100, boardPrice: Int.random(in: 150...180), boardLocation: tempLoc, isAvailable: true)
boardList.append(data)
}
}
override func viewDidLoad() {
super.viewDidLoad()
mapView.mapView.touchDelegate = self
locationManager.delegate = self
switch locationManager.authorizationStatus {
case .denied:
print("위치 비허용")
case .notDetermined, .restricted:
locationManager.requestWhenInUseAuthorization()
default:
break
}
switch locationManager.accuracyAuthorization {
case .reducedAccuracy:
locationManager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: "")
case .fullAccuracy:
break
@unknown default:
break
}
view.addSubview(mapView)
setLocationData()
mapView.showZoomControls = true
mapView.showLocationButton = true
mapView.showCompass = true
mapView.mapView.minZoomLevel = 10
mapView.mapView.maxZoomLevel = 15
makeDummyData()
for board in self.boardList {
placeBoardOnMap(board: board).mapView = self.mapView.mapView
}
mapView.addSubview(inUseLabel)
mapView.addSubview(returnButton)
mapView.addSubview(searchButton)
tabBarVC = parent as? TabBarController
}
}
//MARK: Marker Config Methods
extension MapViewController {
private func placeBoardOnMap(board: Board) -> NMFMarker {
let marker = NMFMarker(position: board.boardLocation)
marker.iconImage = NMFOverlayImage(image: UIImage(named: "BoardMarkerIcon")!.resized(to: CGSize(width: 30, height: 30)))
marker.touchHandler = markerTapEvent
marker.minZoom = 1
marker.userInfo["id"] = board.boardNumber
return marker
}
private func generateRandomNMGLatLng() -> NMGLatLng {
return NMGLatLng(lat: Double.random(in: 37.3200...37.3700), lng: Double.random(in: 127.0800...127.1300))
}
}
//MARK: Location Config
extension MapViewController {
func setCurrentLocation() -> NMGLatLng {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
let latitude = locationManager.location?.coordinate.latitude ?? 0
let longitude = locationManager.location?.coordinate.longitude ?? 0
let result = NMGLatLng(lat: latitude, lng: longitude)
return result
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
print("현재 위치 권한 : \(manager.authorizationStatus)")
switch manager.authorizationStatus {
case .authorizedWhenInUse:
break
case .restricted, .denied:
break
case .notDetermined:
break
default:
break
}
}
func setLocationData() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.startUpdatingLocation()
let latitude = locationManager.location?.coordinate.latitude ?? 0
let longitude = locationManager.location?.coordinate.longitude ?? 0
print(latitude, longitude)
let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: latitude, lng: longitude), zoomTo: 12)
mapView.mapView.moveCamera(cameraUpdate)
cameraUpdate.animation = .easeIn
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
print("위도: \(location.coordinate.latitude)")
print("경도: \(location.coordinate.longitude)")
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("error: \(error)")
}
}
NMapsMap
, 사용자의 현재 위치 좌표를 사용하기 위해서 CoreLocation
을 프로젝트에 사용했습니다.Board
자료형을 요소로 가지는 배열에 더미데이터를 세팅하고, 지도 위의 마커를 생성하는데 사용합니다.//
// DetailViewController.swift
// NBCSuper8oard
//
// Created by 영현 on 1/21/24.
//
import UIKit
class DetailViewController: UIViewController {
var selectedBoard: Board?
var user: User?
var isRented: ((Board) -> ())?
var isHidden: ((Board) -> ())?
var hideMarker: ((Bool) -> ())?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let boardImage = UIImageView(frame: CGRect(x: 10, y: 20, width: 100, height: 100))
boardImage.image = UIImage(named: "Kickboard")
view.addSubview(boardImage)
let typeLabel = UILabel(frame: CGRect(x: 120, y: 20, width: view.frame.width - 120, height: 20))
typeLabel.text = "킥보드 종류 : \(selectedBoard?.boardType ?? "")"
typeLabel.textAlignment = .left
view.addSubview(typeLabel)
let priceLabel = UILabel(frame: CGRect(x: 120, y: 50, width: view.frame.width - 120, height: 20))
priceLabel.text = "대여 가격: 분당 \(selectedBoard?.boardPrice ?? 0)₩"
priceLabel.textAlignment = .left
view.addSubview(priceLabel)
let isAvailableLabel = UILabel(frame: CGRect(x: 120, y: 80, width: view.frame.width - 120, height: 20))
isAvailableLabel.text = "대여 가능 여부: \(selectedBoard?.isAvailable ?? false)"
isAvailableLabel.textAlignment = .left
view.addSubview(isAvailableLabel)
let rentButton = UIButton(type: .system)
rentButton.setTitle("대여하기", for: .normal)
rentButton.tintColor = .white
rentButton.backgroundColor = .systemBlue
rentButton.frame = CGRect(x: 0, y: 120, width: view.frame.width, height: 40)
rentButton.layer.cornerRadius = 10
rentButton.addTarget(self, action: #selector(rentButtonTapped), for: .touchUpInside)
rentButton.isEnabled = selectedBoard?.isAvailable ?? false
if let user = self.user {
if user.isRiding {
rentButton.isEnabled = false
} else {
rentButton.isEnabled = true
}
}
view.addSubview(rentButton)
}
init(selectedBoard: Board? = nil, user: User? = nil) {
self.selectedBoard = selectedBoard
self.user = user
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func rentButtonTapped() {
let confirmAlert = UIAlertController(title: "대여", message: "선택한 킥보드로 대여를 진행합니다.", preferredStyle: .alert)
let rentAction = UIAlertAction(title: "확인", style: .default) { [self] _ in
guard var temp = selectedBoard else { return }
temp.isAvailable = temp.isAvailable ? false : true
isRented?(temp)
isHidden?(temp)
hideMarker?(temp.isAvailable)
dismiss(animated: true)
}
confirmAlert.addAction(rentAction)
present(confirmAlert, animated: true)
}
}
UIImageView, UILabel
을 위치시켰습니다.Board, User
자료를 받아와 사용자가 대여 중인 킥보드가 있는지와, 대여 후 킥보드의 대여가능 여부(Board.isAvailable
)값을 처리합니다.//
// SearchViewController.swift
// NBCSuper8oard
//
// Created by 영현 on 1/21/24.
//
import UIKit
class SearchViewController: UIViewController {
var searchTextField: UITextField!
var currentLatitude: Double?
var currentLongitude: Double?
var setCameraLocation: ((Double, Double) -> ())?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let searchLabel = UILabel(frame: CGRect(x: 10, y: 40, width: view.frame.width - 20, height: 20))
searchLabel.text = "주소를 입력하세요."
searchLabel.textAlignment = .left
searchLabel.layer.cornerRadius = 5
view.addSubview(searchLabel)
searchTextField = UITextField(frame: CGRect(x: 10, y: 80, width: view.frame.width - 60, height: 40))
searchTextField.placeholder = "검색하고 싶은 주소를 입력하세요."
searchTextField.layer.cornerRadius = 5
view.addSubview(searchTextField)
let searchButton = UIButton(frame: CGRect(x: view.frame.width - 50, y: 80, width: 40, height: 40))
searchButton.tintColor = .white
searchButton.backgroundColor = .systemBlue
searchButton.setTitle("검색", for: .normal)
searchButton.layer.cornerRadius = 10
searchButton.addTarget(self, action: #selector(moveCamera), for: .touchUpInside)
view.addSubview(searchButton)
}
@objc func moveCamera() {
NMGeocoding.shared.getGeoXY(searchTextField.text ?? "") { geoXY in
if let geoXY = geoXY {
geoXY.addresses.forEach { address in
self.currentLatitude = Double(address.y)
self.currentLongitude = Double(address.x)
print(address)
}
}
if let lat = self.currentLatitude, let lng = self.currentLongitude {
self.setCameraLocation?(lat, lng)
self.dismiss(animated: true)
} else {
let cannotMoveAlert = UIAlertController(title: "입력된 주소를 확인해주세요.", message: nil, preferredStyle: .alert)
let confirm = UIAlertAction(title: "확인", style: .default, handler: nil)
cannotMoveAlert.addAction(confirm)
self.present(cannotMoveAlert, animated: true)
}
}
}
}
UITextField
에 이동하고 싶은 주소를 적어넣고 검색버튼을 누르면, 모달로 올라왔던 SearchView가 내려가며, 지도가 해당 위치를 비춰줍니다.//
// NMGeocoding.swift
// NBCSuper8oard
//
// Created by 영현 on 1/21/24.
//
import Foundation
struct GeoXY: Decodable {
let addresses: [Address]
struct Address: Decodable {
let x: String
let y: String
}
}
class NMGeocoding {
static let shared = NMGeocoding()
let NAVER_CLIENT_ID = "*********"
let NAVER_CLIENT_SECRET = "**************************"
let baseURL = "https://naveropenapi.apigw.ntruss.com/map-geocode/v2/geocode"
func getGeoXY(_ address: String, completion: @escaping (GeoXY?) -> Void) {
var components = URLComponents(string: baseURL)!
components.queryItems = [ URLQueryItem(name: "query", value: address) ]
guard let url = components.url else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue(NAVER_CLIENT_ID, forHTTPHeaderField: "X-NCP-APIGW-API-KEY-ID")
request.addValue(NAVER_CLIENT_SECRET, forHTTPHeaderField: "X-NCP-APIGW-API-KEY")
let session = URLSession.shared
let task = session.dataTask(with: request) { (data, response, error) in
guard error == nil else {
completion(nil)
return
}
guard let responseData = data else {
completion(nil)
return
}
do {
let response = try JSONDecoder().decode(GeoXY.self, from: responseData)
DispatchQueue.main.async {
completion(response)
}
} catch {
DispatchQueue.main.async {
completion(nil)
}
}
}
task.resume()
}
}
shared
객체를 생성해서 어디서든 접근할 수 있도록 만들었습니다.