Skip to content

Commit

Permalink
Merge pull request #80 from score-dev/feat/#79-feat-map-view
Browse files Browse the repository at this point in the history
[Feat/Style] MapView 뷰 및 기능을 일부 구현했습니다.
  • Loading branch information
soletree authored Aug 19, 2024
2 parents f667ab0 + b652f35 commit c964e41
Show file tree
Hide file tree
Showing 7 changed files with 432 additions and 2 deletions.
30 changes: 30 additions & 0 deletions Score/Score.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
FD8B63A12BD2016D000500BF /* ServicePolicyFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8B63A02BD2016D000500BF /* ServicePolicyFeature.swift */; };
FD8B63A32BD201CC000500BF /* UnregisterFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8B63A22BD201CC000500BF /* UnregisterFeature.swift */; };
FD8B63A62BD21802000500BF /* ContactFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8B63A52BD21802000500BF /* ContactFeature.swift */; };
FD8D001C2C6BB391001C126B /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8D001B2C6BB391001C126B /* MapView.swift */; };
FD8D1C012C09C91700B87C43 /* GoogleAuthStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8D1C002C09C91700B87C43 /* GoogleAuthStore.swift */; };
FD8D1C042C09C92600B87C43 /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = FD8D1C032C09C92600B87C43 /* GoogleSignIn */; };
FD8D1C062C09C92600B87C43 /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD8D1C052C09C92600B87C43 /* GoogleSignInSwift */; };
Expand All @@ -100,6 +101,10 @@
FD9E070D2BCB4E66000109FD /* SCNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9E070C2BCB4E66000109FD /* SCNavigationBar.swift */; };
FD9E070F2BCB58BC000109FD /* SCLineTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9E070E2BCB58BC000109FD /* SCLineTabBar.swift */; };
FD9E07112BCC99DE000109FD /* DismissButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9E07102BCC99DE000109FD /* DismissButton.swift */; };
FDA1AF212C6CD4FA008A1312 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1AF202C6CD4F6008A1312 /* MapViewController.swift */; };
FDA1AF252C6CD6AB008A1312 /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1AF242C6CD6AB008A1312 /* LocationManager.swift */; };
FDA1AF272C6D0B3A008A1312 /* SCMapMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1AF262C6D0B3A008A1312 /* SCMapMarker.swift */; };
FDA1AF2A2C6E64B4008A1312 /* MapFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1AF292C6E64AF008A1312 /* MapFeature.swift */; };
FDC09D352C18327B00D5CF5C /* TermsAgreementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC09D342C18327B00D5CF5C /* TermsAgreementView.swift */; };
FDC09D372C18522E00D5CF5C /* TermsAgreementFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC09D362C18522E00D5CF5C /* TermsAgreementFeature.swift */; };
FDC09D392C18644000D5CF5C /* SelectProfileImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC09D382C18644000D5CF5C /* SelectProfileImageView.swift */; };
Expand Down Expand Up @@ -223,6 +228,7 @@
FD8B63A02BD2016D000500BF /* ServicePolicyFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicePolicyFeature.swift; sourceTree = "<group>"; };
FD8B63A22BD201CC000500BF /* UnregisterFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnregisterFeature.swift; sourceTree = "<group>"; };
FD8B63A52BD21802000500BF /* ContactFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactFeature.swift; sourceTree = "<group>"; };
FD8D001B2C6BB391001C126B /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = "<group>"; };
FD8D1C002C09C91700B87C43 /* GoogleAuthStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthStore.swift; sourceTree = "<group>"; };
FD8D1C092C09C9A300B87C43 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
FD8FC3AF2BC8251700B80B3D /* CalendarFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarFeature.swift; sourceTree = "<group>"; };
Expand All @@ -244,6 +250,10 @@
FD9E070C2BCB4E66000109FD /* SCNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCNavigationBar.swift; sourceTree = "<group>"; };
FD9E070E2BCB58BC000109FD /* SCLineTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCLineTabBar.swift; sourceTree = "<group>"; };
FD9E07102BCC99DE000109FD /* DismissButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissButton.swift; sourceTree = "<group>"; };
FDA1AF202C6CD4F6008A1312 /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = "<group>"; };
FDA1AF242C6CD6AB008A1312 /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
FDA1AF262C6D0B3A008A1312 /* SCMapMarker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCMapMarker.swift; sourceTree = "<group>"; };
FDA1AF292C6E64AF008A1312 /* MapFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapFeature.swift; sourceTree = "<group>"; };
FDC09D342C18327B00D5CF5C /* TermsAgreementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsAgreementView.swift; sourceTree = "<group>"; };
FDC09D362C18522E00D5CF5C /* TermsAgreementFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsAgreementFeature.swift; sourceTree = "<group>"; };
FDC09D382C18644000D5CF5C /* SelectProfileImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectProfileImageView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -403,6 +413,7 @@
FD4A3B3F2BBADBDE00D2DFFA /* Card */,
FD4A3B3E2BBADBCE00D2DFFA /* Label */,
FD4A3B3D2BBADBBE00D2DFFA /* Button */,
FDA1AF282C6D0DAC008A1312 /* MapMarker */,
);
path = UIComponent;
sourceTree = "<group>";
Expand Down Expand Up @@ -432,6 +443,9 @@
isa = PBXGroup;
children = (
FD1C54422C40493000419364 /* TodayWorkOutRecordView.swift */,
FD8D001B2C6BB391001C126B /* MapView.swift */,
FDA1AF202C6CD4F6008A1312 /* MapViewController.swift */,
FDA1AF242C6CD6AB008A1312 /* LocationManager.swift */,
);
path = TodayWorkOut;
sourceTree = "<group>";
Expand Down Expand Up @@ -784,6 +798,14 @@
path = Navigation;
sourceTree = "<group>";
};
FDA1AF282C6D0DAC008A1312 /* MapMarker */ = {
isa = PBXGroup;
children = (
FDA1AF262C6D0B3A008A1312 /* SCMapMarker.swift */,
);
path = MapMarker;
sourceTree = "<group>";
};
FDA9FDC12BFF3B1A00AC4EB7 /* DTO */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -952,6 +974,7 @@
FDFD3D032BD85E5C00629B8C /* Record */ = {
isa = PBXGroup;
children = (
FDA1AF292C6E64AF008A1312 /* MapFeature.swift */,
FDFD3D042BD85E6800629B8C /* RecordMainFeature.swift */,
FD584E422C4285AC00E81E07 /* CameraFeature.swift */,
FD443C242C4DF49C00C2E62E /* Camera.swift */,
Expand Down Expand Up @@ -1115,6 +1138,7 @@
FD9E06FF2BC97DF6000109FD /* ProfileEditView.swift in Sources */,
FD3428F92BDC24AE0021E542 /* CGFloat+.swift in Sources */,
FD2A260A2BAC837000F7B317 /* SCRadioButton.swift in Sources */,
FD8D001C2C6BB391001C126B /* MapView.swift in Sources */,
FD1C54462C40529A00419364 /* AttributedText.swift in Sources */,
FDE5C8C52C11F31D0021C8E3 /* TypeUserInfoMainView.swift in Sources */,
FD995E6D2C3AE1A200376823 /* SelectSchoolNameSheet.swift in Sources */,
Expand Down Expand Up @@ -1178,6 +1202,8 @@
FD208FBC2BB6F2A600DD4D3B /* EdgeBorder.swift in Sources */,
FD8B639F2BD20124000500BF /* PrivacyPolicyFeature.swift in Sources */,
FDEDB94A2BFBBA6D0040E313 /* APIProtocol.swift in Sources */,
FDA1AF2A2C6E64B4008A1312 /* MapFeature.swift in Sources */,
FDA1AF272C6D0B3A008A1312 /* SCMapMarker.swift in Sources */,
FDC09D372C18522E00D5CF5C /* TermsAgreementFeature.swift in Sources */,
FD38E9932BD53D0900D3BDB7 /* SignOutFeature.swift in Sources */,
FD408A9F2C11CFEB00DD8EAF /* TypeNickNameView.swift in Sources */,
Expand Down Expand Up @@ -1207,6 +1233,7 @@
FD443C232C4DF1AC00C2E62E /* CameraPreview.swift in Sources */,
FD2E8CE12BD93238001957F3 /* MyPageMainView.swift in Sources */,
FDFD3D082BD85E8900629B8C /* SchoolGroupMainFeature.swift in Sources */,
FDA1AF252C6CD6AB008A1312 /* LocationManager.swift in Sources */,
FD1C54402C40393800419364 /* CameraView.swift in Sources */,
FDEDB9642BFC17DC0040E313 /* AuthToken.swift in Sources */,
FD38E98E2BD5297E00D3BDB7 /* PolicyView.swift in Sources */,
Expand All @@ -1220,6 +1247,7 @@
FD2E8CE72BD94F37001957F3 /* SCIconButton.swift in Sources */,
FD208FC52BB9C45700DD4D3B /* SCNumberIcon.swift in Sources */,
FD208FCB2BB9EDCD00DD4D3B /* SCInformationCard.swift in Sources */,
FDA1AF212C6CD4FA008A1312 /* MapViewController.swift in Sources */,
FD408AA12C11D12200DD8EAF /* TypeUserInfoFeature.swift in Sources */,
FD443C252C4DF4A000C2E62E /* Camera.swift in Sources */,
FD208FBA2BB634AF00DD4D3B /* SCTextField.swift in Sources */,
Expand Down Expand Up @@ -1377,6 +1405,7 @@
INFOPLIST_FILE = Score/Resource/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "스코어";
INFOPLIST_KEY_NSCameraUsageDescription = "스코어에서 운동 기록 촬영을 위해 카메라에 접근합니다.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "스코어에서 운동 기록을 위해 사용자의 위치 정보에 접근합니다.";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "스코어에서 운동 기록 사진 저장을 위해 갤러리에 접근합니다.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "스코어에서 운동 기록 사진 저장을 위해 갤러리에 접근합니다.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
Expand Down Expand Up @@ -1421,6 +1450,7 @@
INFOPLIST_FILE = Score/Resource/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "스코어";
INFOPLIST_KEY_NSCameraUsageDescription = "스코어에서 운동 기록 촬영을 위해 카메라에 접근합니다.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "스코어에서 운동 기록을 위해 사용자의 위치 정보에 접근합니다.";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "스코어에서 운동 기록 사진 저장을 위해 갤러리에 접근합니다.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "스코어에서 운동 기록 사진 저장을 위해 갤러리에 접근합니다.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
Expand Down
8 changes: 6 additions & 2 deletions Score/Score/Source/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation
import GoogleSignIn
import KakaoSDKAuth
import KakaoSDKCommon
import NMapsMap

//MARK: - AppDelegate

Expand All @@ -21,16 +22,19 @@ class AppDelegate: NSObject,
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
/// KakaoSDK 초기화
// KakaoSDK 초기화
KakaoSDK.initSDK(
appKey: self.apiKeys.Kakao.appID.getValueFromBundle() ?? "none"
)

/// Google Client ID 초기화
// Google Client ID 초기화
GIDSignIn.sharedInstance.configuration = .init(
clientID: self.apiKeys.Google.clientID.getValueFromBundle() ?? "none"
)

// Naver Client ID 지정
NMFAuthManager.shared().clientId = self.apiKeys.Naver.clientID.getValueFromBundle() ?? "none"

return false
}

Expand Down
33 changes: 33 additions & 0 deletions Score/Score/Source/Reducer/Record/MapFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// MapFeature.swift
// Score
//
// Created by sole on 8/16/24.
//

import CoreLocation
import ComposableArchitecture
import NMapsMap

@Reducer
struct MapFeature {
struct State: Equatable {
var locations: [NMGLatLng] = []
}

enum Action {
case updatingLocations(locations: [CLLocation])
}



var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .updatingLocations(let locations):
state.locations += locations.map{ .init(lat: $0.coordinate.latitude, lng: $0.coordinate.longitude)}
return .none
}
}
}
}
51 changes: 51 additions & 0 deletions Score/Score/Source/View/Record/TodayWorkOut/LocationManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// LocationManager.swift
// Score
//
// Created by sole on 8/14/24.
//

import CoreLocation
import os.log
import ComposableArchitecture

final class LocationManager: CLLocationManager {
private var store: StoreOf<MapFeature>

init(store: StoreOf<MapFeature>) {
self.store = store
super.init()
self.delegate = self
}

/// 위치 접근 권한이 없으면 위치 접근 권한을 요청하는 메서드입니다.
func requestAuthorization() {
switch self.authorizationStatus {
case .notDetermined:
self.requestWhenInUseAuthorization()
logger.debug("\(#function) 위치 접근 권한이 설정되지 않았습니다.")
case .restricted:
logger.debug("\(#function) 위치 접근 권한이 제한적으로 설정되었습니다.")
case .denied:
// go to setting
logger.debug("\(#function) 위치 접근 권한이 거부되었습니다.")
case .authorizedAlways:
logger.debug("\(#function) 위치 접근 권한이 항상 허용되었습니다.")
case .authorizedWhenInUse:
logger.debug("\(#function) 위치 접근 권한이 허용되었습니다.")
@unknown default:
logger.debug("\(#function) 알 수 없는 위치 권한입니다.")
}
}
}

extension LocationManager: CLLocationManagerDelegate {
func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
store.send(.updatingLocations(locations: locations))
}
}

fileprivate let logger = Logger(subsystem: "sole.Score", category: "LocationManager")
28 changes: 28 additions & 0 deletions Score/Score/Source/View/Record/TodayWorkOut/MapView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// MapView.swift
// Score
//
// Created by sole on 8/14/24.
//

import SwiftUI
import NMapsMap

struct MapView: UIViewControllerRepresentable {
func makeUIViewController(
context: Context
) -> some UIViewController {
return MapViewController()
}

func updateUIViewController(
_ uiViewController: UIViewControllerType,
context: Context
) {

}
}

#Preview {
MapView()
}
125 changes: 125 additions & 0 deletions Score/Score/Source/View/Record/TodayWorkOut/MapViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//
// MapViewController.swift
// Score
//
// Created by sole on 8/14/24.
//

import ComposableArchitecture
import NMapsMap
import UIKit
import SwiftUI
import Combine

final class MapViewController: UIViewController {
private let store: StoreOf<MapFeature> = .init(initialState: .init(), reducer: { MapFeature() })
private var cancellable: Set<AnyCancellable> = .init()

private var naverMapView: NMFNaverMapView!
private var locationManager: LocationManager!

private let pathOverlay: NMFPath = {
let pathOverlay: NMFPath = .init()
pathOverlay.color = UIColor(.brandColor(color: .main))
pathOverlay.outlineWidth = 0
return pathOverlay
}()

init() {
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
self.setUpLocationManager()
self.setUpMapView()
self.setUpCamera()
self.setUpMapMarker()
view.addSubview(naverMapView)

store.publisher.locations
.sink{ [weak self] in
self?.overlayMapPath(locations: $0)
}
.store(in: &cancellable)
}

/// locationManager 관련 설정을 세팅합니다.
func setUpLocationManager() {
self.locationManager = .init(store: store)
self.locationManager.requestAuthorization()
self.locationManager.startUpdatingLocation()
}

/// 지도 관련 설정을 세팅합니다.
func setUpMapView() {
self.naverMapView = .init(frame: view.frame)
self.naverMapView.showCompass = false
self.naverMapView.showZoomControls = false
self.naverMapView.mapView.isScrollGestureEnabled = false
self.naverMapView.mapView.isRotateGestureEnabled = false
self.naverMapView.mapView.minZoomLevel = 5
self.naverMapView.mapView.maxZoomLevel = 18
self.naverMapView.mapView.positionMode = .direction
}

/// camera 위치를 현재 유저의 위치로 설정합니다.
func setUpCamera() {
guard let currentLocation = locationManager.location
else { return }

let position: NMFCameraPosition = .init(
.init(
lat: currentLocation.coordinate.latitude,
lng: currentLocation.coordinate.longitude
),
zoom: 18
)
let cameraUpdate: NMFCameraUpdate = .init(position: position)
self.naverMapView.mapView.moveCamera(cameraUpdate)
}

// FIXME: UIImage로 Rendering이 제대로 되지 않는 문제 해결 해야 함.
func setUpMapMarker() {
let locationOverlay = self.naverMapView.mapView.locationOverlay
let markerVC = SCMapMarker(isFocused: true).asUIViewController()
// let markerUIImage: UIImage = markerVC.view.asUIImage(bounds: self.view.bounds)
let markerUIImage: UIImage = .apple
let overlayImage: NMFOverlayImage = .init(image: markerUIImage)
locationOverlay.icon = overlayImage
}

/// 유저가 지나온 경로를 그립니다.
func overlayMapPath(locations: [NMGLatLng]) {
// point가 2개 이상인 경우에만 실행됩니다.
guard locations.count > 1
else { return }
self.pathOverlay.path = .init(points: locations)
self.pathOverlay.mapView = self.naverMapView.mapView
}

deinit {
cancellable.forEach{ $0.cancel() }
}
}

extension UIView {
func asUIImage(bounds: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
let uiImage = renderer.image { context in
self.layer.render(in: context.cgContext)
}
return uiImage
}
}


extension View {
func asUIViewController() -> UIViewController {
UIHostingController(rootView: self)
}
}
Loading

0 comments on commit c964e41

Please sign in to comment.