diff --git a/Runnect-iOS/Podfile b/Runnect-iOS/Podfile index 9c98c78e..861373ac 100644 --- a/Runnect-iOS/Podfile +++ b/Runnect-iOS/Podfile @@ -18,8 +18,17 @@ target 'Runnect-iOS' do pod 'FirebaseDynamicLinks' pod 'KakaoSDKTalk' pod 'DropDown', :git => 'https://github.com/thingineeer/DropDown.git', :commit => '95ee36f7bd925d466033c2c169979f1c574bf3b9' + pod 'CombineCocoa' - +end # Pods for Runnect-iOS +post_install do |installer| + installer.generated_projects.each do |project| + project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + end + end + end end diff --git a/Runnect-iOS/Podfile.lock b/Runnect-iOS/Podfile.lock index 31f52b6c..327732e9 100644 --- a/Runnect-iOS/Podfile.lock +++ b/Runnect-iOS/Podfile.lock @@ -1,39 +1,40 @@ PODS: - - Alamofire (5.7.1) + - Alamofire (5.8.1) + - CombineCocoa (0.4.1) - DropDown (2.3.13) - - FirebaseCore (10.14.0): + - FirebaseCore (10.17.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreInternal (10.14.0): + - FirebaseCoreInternal (10.17.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseDynamicLinks (10.14.0): + - FirebaseDynamicLinks (10.17.0): - FirebaseCore (~> 10.0) - - GoogleUtilities/Environment (7.11.5): + - GoogleUtilities/Environment (7.12.0): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Logger (7.12.0): - GoogleUtilities/Environment - - "GoogleUtilities/NSData+zlib (7.11.5)" - - KakaoSDKAuth (2.16.0): - - KakaoSDKCommon (= 2.16.0) - - KakaoSDKCommon (2.16.0): - - KakaoSDKCommon/Common (= 2.16.0) - - KakaoSDKCommon/Network (= 2.16.0) - - KakaoSDKCommon/Common (2.16.0) - - KakaoSDKCommon/Network (2.16.0): + - "GoogleUtilities/NSData+zlib (7.12.0)" + - KakaoSDKAuth (2.18.2): + - KakaoSDKCommon (= 2.18.2) + - KakaoSDKCommon (2.18.2): + - KakaoSDKCommon/Common (= 2.18.2) + - KakaoSDKCommon/Network (= 2.18.2) + - KakaoSDKCommon/Common (2.18.2) + - KakaoSDKCommon/Network (2.18.2): - Alamofire (~> 5.1) - - KakaoSDKCommon/Common (= 2.16.0) - - KakaoSDKShare (2.16.0): - - KakaoSDKCommon (= 2.16.0) - - KakaoSDKTemplate (= 2.16.0) - - KakaoSDKTalk (2.16.0): - - KakaoSDKTemplate (= 2.16.0) - - KakaoSDKUser (= 2.16.0) - - KakaoSDKTemplate (2.16.0): - - KakaoSDKCommon/Common (= 2.16.0) - - KakaoSDKUser (2.16.0): - - KakaoSDKAuth (= 2.16.0) - - Kingfisher (7.9.0) + - KakaoSDKCommon/Common (= 2.18.2) + - KakaoSDKShare (2.18.2): + - KakaoSDKCommon (= 2.18.2) + - KakaoSDKTemplate (= 2.18.2) + - KakaoSDKTalk (2.18.2): + - KakaoSDKTemplate (= 2.18.2) + - KakaoSDKUser (= 2.18.2) + - KakaoSDKTemplate (2.18.2): + - KakaoSDKCommon/Common (= 2.18.2) + - KakaoSDKUser (2.18.2): + - KakaoSDKAuth (= 2.18.2) + - Kingfisher (7.10.0) - Moya (15.0.0): - Moya/Core (= 15.0.0) - Moya/Core (15.0.0): @@ -46,6 +47,7 @@ PODS: - Then (3.0.0) DEPENDENCIES: + - CombineCocoa - DropDown (from `https://github.com/thingineeer/DropDown.git`, commit `95ee36f7bd925d466033c2c169979f1c574bf3b9`) - FirebaseDynamicLinks - KakaoSDKAuth @@ -63,6 +65,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - Alamofire + - CombineCocoa - FirebaseCore - FirebaseCoreInternal - FirebaseDynamicLinks @@ -92,19 +95,20 @@ CHECKOUT OPTIONS: :git: https://github.com/thingineeer/DropDown.git SPEC CHECKSUMS: - Alamofire: 0123a34370cb170936ae79a8df46cc62b2edeb88 + Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7 + CombineCocoa: e5210dbfb480ff251078929459ac17e750e7af1d DropDown: bf260fd688978138019ad7d94f63a2362dad85ba - FirebaseCore: 6fc17ac9f03509d51c131298aacb3ee5698b4f02 - FirebaseCoreInternal: d558159ee6cc4b823c2296ecc193de9f6d9a5bb3 - FirebaseDynamicLinks: 0eaabff2d0e5d0e576c0227227b00771aa2f3aaf - GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 - KakaoSDKAuth: 1b85ed7c41b0517bfd1fc9dc46c292c75b8cb610 - KakaoSDKCommon: d6579aa2e9d963d74e13d741cbf1cce48b8b0c17 - KakaoSDKShare: efc0415c4f33274232604eeaf96fb03641facdca - KakaoSDKTalk: 44545bd9a1ebc144795eb6e927960b10cf8252f5 - KakaoSDKTemplate: 7e478a3cda6f5879b475546bfcd667b464fbb6d0 - KakaoSDKUser: 427e5b3884abd19ee6d9a7bd6e3f3029eb2968b8 - Kingfisher: 59f908b6d2f403b0a3e539debb0eec05cb27002c + FirebaseCore: 534544dd98cabcf4bf8598d88ec683b02319a528 + FirebaseCoreInternal: 2cf9202e226e3f78d2bf6d56c472686b935bfb7f + FirebaseDynamicLinks: 66878206d55d4aba6e63744f57151a80d76e472e + GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 + KakaoSDKAuth: 3daf72ec463876e0fe6d0d37c7d6b0b2729169f1 + KakaoSDKCommon: 516f68bc6b223ed502c34a362d6ecc9145e0dcc0 + KakaoSDKShare: 8980e83a3a80bd805ab859c102dd41ed6801424e + KakaoSDKTalk: 567d2697b419b4f01f2f8bdb28e13100817c4090 + KakaoSDKTemplate: 0fcfbbb7cc05986fe6690b85e19dd8de0fc634e6 + KakaoSDKUser: 5b0826033381d314f550dd90fbf70d3612044ad6 + Kingfisher: a18f05d3b6d37d8650ee4a3e61d57a28fc6207f6 Moya: 138f0573e53411fb3dc17016add0b748dfbd78ee NMapsGeometry: 53c573ead66466681cf123f99f698dc8071a4b83 NMapsMap: a5b909a31b6f3d27a670f6eb2ddc913c38975474 @@ -112,6 +116,6 @@ SPEC CHECKSUMS: SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25 Then: 844265ae87834bbe1147d91d5d41a404da2ec27d -PODFILE CHECKSUM: 5bd73b39108f7bc1302e8303d79fefc7783805a5 +PODFILE CHECKSUM: a1846e0d9fa164fc8d0a63bb343a6b7ba18187b9 COCOAPODS: 1.12.1 diff --git a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj index 6360f34f..6e8707da 100644 --- a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj +++ b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj @@ -8,9 +8,14 @@ /* Begin PBXBuildFile section */ 0AEBD608F3973389E8E1C6D6 /* Pods_Runnect_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 015778D02D5CDE0838284CD7 /* Pods_Runnect_iOS.framework */; }; + 2349CD532B026EA500EE7C4A /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2349CD512B024E5B00EE7C4A /* GoogleService-Info.plist */; }; + 23EE06C12AC1AD5200CB3FF8 /* LocationSelectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE06C02AC1AD5200CB3FF8 /* LocationSelectView.swift */; }; + 23EE06C52AC1AE1900CB3FF8 /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE06C42AC1AE1900CB3FF8 /* BaseView.swift */; }; + 23EE06C92AC1DED100CB3FF8 /* GesturePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE06C82AC1DED100CB3FF8 /* GesturePublisher.swift */; }; + 23EE06CB2AC2AF3E00CB3FF8 /* KakaoAddressSearchingResponseDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE06CA2AC2AF3E00CB3FF8 /* KakaoAddressSearchingResponseDto.swift */; }; + 23EE06D12AC2F44E00CB3FF8 /* TmapAddressSearchingResponseDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE06D02AC2F44E00CB3FF8 /* TmapAddressSearchingResponseDto.swift */; }; 712F661D2A7B7BAB00D9539B /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 712F661C2A7B7BAB00D9539B /* Config.swift */; }; 7136BF8A2AF921A900679364 /* CustomBottomSheetVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7136BF892AF921A900679364 /* CustomBottomSheetVC.swift */; }; - 71DBF23E2ABB255A0013415B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 71DBF23D2ABB255A0013415B /* GoogleService-Info.plist */; }; A3305A97296EF58C000B1A10 /* GoalRewardInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3305A96296EF58C000B1A10 /* GoalRewardInfoDto.swift */; }; A3BC2F2B2962C3D500198261 /* GoalRewardInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BC2F2A2962C3D500198261 /* GoalRewardInfoVC.swift */; }; A3BC2F2D2962C3F200198261 /* ActivityRecordInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BC2F2C2962C3F200198261 /* ActivityRecordInfoVC.swift */; }; @@ -163,6 +168,12 @@ /* Begin PBXFileReference section */ 015778D02D5CDE0838284CD7 /* Pods_Runnect_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runnect_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2349CD512B024E5B00EE7C4A /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runnect-iOS/GoogleService-Info.plist"; sourceTree = ""; }; + 23EE06C02AC1AD5200CB3FF8 /* LocationSelectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSelectView.swift; sourceTree = ""; }; + 23EE06C42AC1AE1900CB3FF8 /* BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = ""; }; + 23EE06C82AC1DED100CB3FF8 /* GesturePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GesturePublisher.swift; sourceTree = ""; }; + 23EE06CA2AC2AF3E00CB3FF8 /* KakaoAddressSearchingResponseDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KakaoAddressSearchingResponseDto.swift; sourceTree = ""; }; + 23EE06D02AC2F44E00CB3FF8 /* TmapAddressSearchingResponseDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TmapAddressSearchingResponseDto.swift; sourceTree = ""; }; 3C3033C911343B5C57EB68E7 /* Pods-Runnect-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runnect-iOS.debug.xcconfig"; path = "Target Support Files/Pods-Runnect-iOS/Pods-Runnect-iOS.debug.xcconfig"; sourceTree = ""; }; 7110A6032AA337DD009A7E99 /* Runnect-iOSDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Runnect-iOSDebug.entitlements"; sourceTree = ""; }; 712F661C2A7B7BAB00D9539B /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; @@ -343,6 +354,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 23EE06C32AC1AE0600CB3FF8 /* Base */ = { + isa = PBXGroup; + children = ( + 23EE06C42AC1AE1900CB3FF8 /* BaseView.swift */, + ); + path = Base; + sourceTree = ""; + }; 3F7098551CF7A77F3FE7EB2B /* Pods */ = { isa = PBXGroup; children = ( @@ -669,6 +688,7 @@ isa = PBXGroup; children = ( CEB8416D2962C45300BF8080 /* LocationSearchResultTVC.swift */, + 23EE06C02AC1AD5200CB3FF8 /* LocationSelectView.swift */, ); path = Views; sourceTree = ""; @@ -761,6 +781,8 @@ children = ( CE10065429680F7000FD31FB /* DepartureSearchingResponseDto.swift */, CE320F37296C8FAB009F89A7 /* CourseDrawingResponseData.swift */, + 23EE06CA2AC2AF3E00CB3FF8 /* KakaoAddressSearchingResponseDto.swift */, + 23EE06D02AC2F44E00CB3FF8 /* TmapAddressSearchingResponseDto.swift */, ); path = ResponseDto; sourceTree = ""; @@ -776,6 +798,7 @@ CE4545BC295D7AF4003201E1 = { isa = PBXGroup; children = ( + 2349CD512B024E5B00EE7C4A /* GoogleService-Info.plist */, CE665614295D989A00C64E12 /* .swiftlint.yml */, CE4545C7295D7AF4003201E1 /* Runnect-iOS */, CE4545C6295D7AF4003201E1 /* Products */, @@ -884,6 +907,7 @@ CE6655AA295D7FAE00C64E12 /* Global */ = { isa = PBXGroup; children = ( + 23EE06C32AC1AE0600CB3FF8 /* Base */, CE6655B6295D803C00C64E12 /* UIComponents */, CE6655B5295D803800C64E12 /* Literal */, CE6655B4295D803400C64E12 /* Resource */, @@ -991,6 +1015,7 @@ CE58759F29601500005D967E /* Toast.swift */, CE6655C9295D84DD00C64E12 /* UserDefaultKeyList.swift */, CE29D583296416D800F47542 /* caculateStatusBarHeight.swift */, + 23EE06C82AC1DED100CB3FF8 /* GesturePublisher.swift */, ); path = Utils; sourceTree = ""; @@ -1249,9 +1274,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2349CD532B026EA500EE7C4A /* GoogleService-Info.plist in Resources */, CE665615295D989A00C64E12 /* .swiftlint.yml in Resources */, CE17F0342961BEF800E1DED0 /* Pretendard-Bold.otf in Resources */, - 71DBF23E2ABB255A0013415B /* GoogleService-Info.plist in Resources */, CE17F0352961BEF800E1DED0 /* Pretendard-SemiBold.otf in Resources */, CE17F0332961BEF800E1DED0 /* Pretendard-Medium.otf in Resources */, CE6655BF295D82E200C64E12 /* .gitkeep in Resources */, @@ -1343,6 +1368,7 @@ CE6655F4295D898400C64E12 /* UIViewController+.swift in Sources */, CE14677829658C7200DCEA1B /* Stopwatch.swift in Sources */, CE21C026299E5FF300F62AF5 /* CourseRouter.swift in Sources */, + 23EE06C52AC1AE1900CB3FF8 /* BaseView.swift in Sources */, CE40BB22296806140030ABCA /* NetworkHelper.swift in Sources */, CE5645162961B72E000A2856 /* ImageLiterals.swift in Sources */, CE4942AD296FCD2300736701 /* UploadedCourseDetailResponseDto.swift in Sources */, @@ -1398,6 +1424,7 @@ CE6655FE295D912300C64E12 /* calculateTopInset.swift in Sources */, CEEC6B492961C5E200D00E1E /* SplashVC.swift in Sources */, CE6655D0295D85FF00C64E12 /* CancelBag.swift in Sources */, + 23EE06C12AC1AD5200CB3FF8 /* LocationSelectView.swift in Sources */, CE102C4A29DBAD3D00E23E69 /* AuthInterceptor.swift in Sources */, A3BC2F432966A93100198261 /* CourseDetailVC.swift in Sources */, DAD5A3D8296C6D9600C8166B /* AdImageCollectionViewCell.swift in Sources */, @@ -1405,8 +1432,10 @@ CE6655DC295D873500C64E12 /* UIButton+.swift in Sources */, CEFA9A2F29FC263700F2D0CF /* UserDeleteResponseDto.swift in Sources */, CE21C02C299E601000F62AF5 /* ScrapRouter.swift in Sources */, + 23EE06D12AC2F44E00CB3FF8 /* TmapAddressSearchingResponseDto.swift in Sources */, A3D1A78029CF142E00DD54EC /* UserManager.swift in Sources */, A3D1A77929CF03D200DD54EC /* AuthRouter.swift in Sources */, + 23EE06C92AC1DED100CB3FF8 /* GesturePublisher.swift in Sources */, A3C38FED2A13212300FF196D /* SetInfoLayout.swift in Sources */, CE6655D4295D865B00C64E12 /* Publisher+UIControl.swift in Sources */, CE0D9FD329648DA300CEB5CD /* CustomAlertVC.swift in Sources */, @@ -1464,6 +1493,7 @@ CE102C4829DB1D6B00E23E69 /* GetNewTokenResponseDto.swift in Sources */, CE6655CA295D84DD00C64E12 /* UserDefaultKeyList.swift in Sources */, 712F661D2A7B7BAB00D9539B /* Config.swift in Sources */, + 23EE06CB2AC2AF3E00CB3FF8 /* KakaoAddressSearchingResponseDto.swift in Sources */, CE6655F2295D894D00C64E12 /* UIView+.swift in Sources */, CE9291252965C9FB0010959C /* CourseDetailInfoView.swift in Sources */, CE665600295D915D00C64E12 /* getClassName.swift in Sources */, diff --git a/Runnect-iOS/Runnect-iOS/Global/Base/BaseView.swift b/Runnect-iOS/Runnect-iOS/Global/Base/BaseView.swift new file mode 100644 index 00000000..e151b1d5 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Base/BaseView.swift @@ -0,0 +1,41 @@ +// +// BaseView.swift +// Runnect-iOS +// +// Created by Sojin Lee on 2023/09/25. +// + +import UIKit +import SnapKit + +protocol BaseViewProtocol { + func setDelegate() + func setUI() + func setLayout() +} + +class BaseView: UIView, BaseViewProtocol { + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + + setDelegate() + setUI() + setLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup Methods + + func setDelegate() { } + + func setUI() { } + + func setLayout() { } +} + diff --git a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIGestureRecognizer+.swift b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIGestureRecognizer+.swift new file mode 100644 index 00000000..bc91c87b --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIGestureRecognizer+.swift @@ -0,0 +1,82 @@ +// +// UIGestureRecognizer+.swift +// Runnect-iOS +// +// Created by Sojin Lee on 2023/09/26. +// + +import UIKit +import Combine + +enum GestureType { + case tap(UITapGestureRecognizer = .init()) + case swipe(UISwipeGestureRecognizer = .init()) + case longPress(UILongPressGestureRecognizer = .init()) + case pan(UIPanGestureRecognizer = .init()) + case pinch(UIPinchGestureRecognizer = .init()) + case edge(UIScreenEdgePanGestureRecognizer = .init()) + func get() -> UIGestureRecognizer { + switch self { + case let .tap(tapGesture): + return tapGesture + case let .swipe(swipeGesture): + return swipeGesture + case let .longPress(longPressGesture): + return longPressGesture + case let .pan(panGesture): + return panGesture + case let .pinch(pinchGesture): + return pinchGesture + case let .edge(edgePanGesture): + return edgePanGesture + } + } +} + +extension UIGestureRecognizer { + struct GesturePublisher: Publisher { + typealias Output = GestureType + typealias Failure = Never + private let view: UIView + private let gestureType: GestureType + init(view: UIView, gestureType: GestureType) { + self.view = view + self.gestureType = gestureType + } + func receive(subscriber: S) where S : Subscriber, + GesturePublisher.Failure == S.Failure, GesturePublisher.Output + == S.Input { + let subscription = GestureSubscription( + subscriber: subscriber, + view: view, + gestureType: gestureType + ) + subscriber.receive(subscription: subscription) + } + } + + class GestureSubscription: Subscription where S.Input == GestureType, S.Failure == Never { + private var subscriber: S? + private var gestureType: GestureType + private var view: UIView + init(subscriber: S, view: UIView, gestureType: GestureType) { + self.subscriber = subscriber + self.view = view + self.gestureType = gestureType + configureGesture(gestureType) + } + private func configureGesture(_ gestureType: GestureType) { + let gesture = gestureType.get() + gesture.addTarget(self, action: #selector(handler)) + view.addGestureRecognizer(gesture) + } + func request(_ demand: Subscribers.Demand) { } + func cancel() { + subscriber = nil + } + @objc + private func handler() { + _ = subscriber?.receive(gestureType) + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIView+.swift b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIView+.swift index 5b0f12ee..c8bbabb5 100644 --- a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIView+.swift +++ b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIView+.swift @@ -36,6 +36,12 @@ extension UIView { layer.cornerRadius = cornerRadius layer.maskedCorners = CACornerMask(arrayLiteral: maskedCorners) } + + /// view에 tap gesture를 연결해주는 함수 + func gesture(_ gestureType: GestureType = .tap()) -> + GesturePublisher { + .init(view: self, gestureType: gestureType) + } } class XibView: UIView { diff --git a/Runnect-iOS/Runnect-iOS/Global/Literal/ImageLiterals.swift b/Runnect-iOS/Runnect-iOS/Global/Literal/ImageLiterals.swift index 1bcbf738..95e00765 100644 --- a/Runnect-iOS/Runnect-iOS/Global/Literal/ImageLiterals.swift +++ b/Runnect-iOS/Runnect-iOS/Global/Literal/ImageLiterals.swift @@ -52,6 +52,8 @@ enum ImageLiterals { static var icModify: UIImage {.load(named: "ic_modify")} static var icRemove: UIImage {.load(named: "ic_remove")} static var icReport: UIImage {.load(named: "ic_report")} + static var icSmallMap: UIImage { .load(named: "ic_small_map")} + static var icDirection: UIImage { .load(named: "ic_direction")} // img static var imgBackground: UIImage { .load(named: "img_background") } diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/Contents.json b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/Contents.json new file mode 100644 index 00000000..22e30a56 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "ic_direction.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_direction@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_direction@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/ic_direction.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/ic_direction.png new file mode 100644 index 00000000..adb76a7b Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/ic_direction.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/ic_direction@2x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/ic_direction@2x.png new file mode 100644 index 00000000..fbe68588 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/ic_direction@2x.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/ic_direction@3x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/ic_direction@3x.png new file mode 100644 index 00000000..f36dbb77 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_direction.imageset/ic_direction@3x.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/Contents.json b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/Contents.json new file mode 100644 index 00000000..11ba48c0 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "ic_small_map.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_small_map@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_small_map@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/ic_small_map.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/ic_small_map.png new file mode 100644 index 00000000..77793ba2 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/ic_small_map.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/ic_small_map@2x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/ic_small_map@2x.png new file mode 100644 index 00000000..4433c070 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/ic_small_map@2x.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/ic_small_map@3x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/ic_small_map@3x.png new file mode 100644 index 00000000..fb9d5ab9 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_small_map.imageset/ic_small_map@3x.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomNavigationBar.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomNavigationBar.swift index 132cd403..88fcf77c 100644 --- a/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomNavigationBar.swift +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomNavigationBar.swift @@ -160,6 +160,13 @@ extension CustomNavigationBar { self.reportButton.isHidden = true return self } + + @discardableResult + func changeTitleWithLeftButton(_ font: UIFont, _ color: UIColor) -> Self { + centerTitleLabel.font = font + centerTitleLabel.textColor = color + return self + } } // MARK: - @objc Function diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift index d76afa7e..1f72ab51 100644 --- a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift @@ -19,6 +19,8 @@ final class RNMapView: UIView { @Published var pathDistance: Double = 0 @Published var markerCount = 0 + var eventSubject = PassthroughSubject, Never>() + private let screenWidth = UIScreen.main.bounds.width private let screenHeight = UIScreen.main.bounds.height @@ -95,7 +97,11 @@ extension RNMapView { /// 지정 위치에 startMarker와 출발 infoWindow 생성 (기존의 startMarker는 제거) @discardableResult func makeStartMarker(at location: NMGLatLng, withCameraMove: Bool = false) -> Self { - self.startMarker.position = location + /// 지도에서 선택한 경우 가상의 마커를 보여주기 때문에 분기처리 + if SelectedInfo.shared.type == .other { + self.startMarker.position = location + } + self.startMarker.mapView = self.map.mapView self.startMarker.showInfoWindow() if withCameraMove { @@ -386,10 +392,13 @@ extension RNMapView: NMFMapViewCameraDelegate, NMFMapViewTouchDelegate { self.makeMarker(at: latlng) } - func mapView(_ mapView: NMFMapView, cameraDidChangeByReason reason: Int, animated: Bool) { - let locationOverlay = map.mapView.locationOverlay - if locationOverlay.icon != locationOverlayIcon { - setLocationOverlay() + // 지도 이동 멈췄을 때 호출되는 메서드 + func mapViewCameraIdle(_ mapView: NMFMapView) { + let latitude = mapView.cameraPosition.target.lat + let longitude = mapView.cameraPosition.target.lng + + if SelectedInfo.shared.type == .map { + eventSubject.send([latitude, longitude]) } } } diff --git a/Runnect-iOS/Runnect-iOS/Global/Utils/GesturePublisher.swift b/Runnect-iOS/Runnect-iOS/Global/Utils/GesturePublisher.swift new file mode 100644 index 00000000..5c7d7fbc --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Utils/GesturePublisher.swift @@ -0,0 +1,80 @@ +// +// GesturePublisher.swift +// Runnect-iOS +// +// Created by Sojin Lee on 2023/09/26. +// + +import UIKit +import Combine + +enum GestureType { + case tap(UITapGestureRecognizer = .init()) + case swipe(UISwipeGestureRecognizer = .init()) + case longPress(UILongPressGestureRecognizer = .init()) + case pan(UIPanGestureRecognizer = .init()) + case pinch(UIPinchGestureRecognizer = .init()) + case edge(UIScreenEdgePanGestureRecognizer = .init()) + func get() -> UIGestureRecognizer { + switch self { + case let .tap(tapGesture): + return tapGesture + case let .swipe(swipeGesture): + return swipeGesture + case let .longPress(longPressGesture): + return longPressGesture + case let .pan(panGesture): + return panGesture + case let .pinch(pinchGesture): + return pinchGesture + case let .edge(edgePanGesture): + return edgePanGesture + } + } +} + +struct GesturePublisher: Publisher { + typealias Output = GestureType + typealias Failure = Never + private let view: UIView + private let gestureType: GestureType + init(view: UIView, gestureType: GestureType) { + self.view = view + self.gestureType = gestureType + } + func receive(subscriber: S) where S : Subscriber, + GesturePublisher.Failure == S.Failure, GesturePublisher.Output + == S.Input { + let subscription = GestureSubscription( + subscriber: subscriber, + view: view, + gestureType: gestureType + ) + subscriber.receive(subscription: subscription) + } +} + +class GestureSubscription: Subscription where S.Input == GestureType, S.Failure == Never { + private var subscriber: S? + private var gestureType: GestureType + private var view: UIView + init(subscriber: S, view: UIView, gestureType: GestureType) { + self.subscriber = subscriber + self.view = view + self.gestureType = gestureType + configureGesture(gestureType) + } + private func configureGesture(_ gestureType: GestureType) { + let gesture = gestureType.get() + gesture.addTarget(self, action: #selector(handler)) + view.addGestureRecognizer(gesture) + } + func request(_ demand: Subscribers.Demand) { } + func cancel() { + subscriber = nil + } + @objc + private func handler() { + _ = subscriber?.receive(gestureType) + } +} diff --git a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDrawingDto/ResponseDto/KakaoAddressSearchingResponseDto.swift b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDrawingDto/ResponseDto/KakaoAddressSearchingResponseDto.swift new file mode 100644 index 00000000..af288f89 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDrawingDto/ResponseDto/KakaoAddressSearchingResponseDto.swift @@ -0,0 +1,75 @@ +// +// DepartureAddressSearchingResponseDto.swift +// Runnect-iOS +// +// Created by Sojin Lee on 2023/09/26. +// + +import Foundation + +// MARK: - DepartureAddressSearchingResponseDto +struct KakaoAddressSearchingResponseDto: Codable { + let meta: Meta + let documents: [Document] + + func toDepartureLocationModel(latitude: Double, longitude: Double) -> DepartureLocationModel { + let roadName = self.documents[0].roadAddress.buildingName ?? "내가 설정한 출발지" + + return DepartureLocationModel(departureName: roadName, departureAddress: self.documents[0].address.addressName, latitude: String(latitude), longitude: String(longitude)) + } +} + +// MARK: - Document +struct Document: Codable { + let roadAddress: RoadAddress + let address: Address + + enum CodingKeys: String, CodingKey { + case roadAddress = "road_address" + case address + } +} + +// MARK: - Address +struct Address: Codable { + let addressName, region1DepthName, region2DepthName, region3DepthName: String + let mountainYn, mainAddressNo: String + + enum CodingKeys: String, CodingKey { + case addressName = "address_name" + case region1DepthName = "region_1depth_name" + case region2DepthName = "region_2depth_name" + case region3DepthName = "region_3depth_name" + case mountainYn = "mountain_yn" + case mainAddressNo = "main_address_no" + } +} + +// MARK: - RoadAddress +struct RoadAddress: Codable { + let addressName, region1DepthName, region2DepthName, region3DepthName: String + let roadName, undergroundYn, mainBuildingNo, subBuildingNo: String + let buildingName, zoneNo: String + + enum CodingKeys: String, CodingKey { + case addressName = "address_name" + case region1DepthName = "region_1depth_name" + case region2DepthName = "region_2depth_name" + case region3DepthName = "region_3depth_name" + case roadName = "road_name" + case undergroundYn = "underground_yn" + case mainBuildingNo = "main_building_no" + case subBuildingNo = "sub_building_no" + case buildingName = "building_name" + case zoneNo = "zone_no" + } +} + +// MARK: - Meta +struct Meta: Codable { + let totalCount: Int + + enum CodingKeys: String, CodingKey { + case totalCount = "total_count" + } +} diff --git a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDrawingDto/ResponseDto/TmapAddressSearchingResponseDto.swift b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDrawingDto/ResponseDto/TmapAddressSearchingResponseDto.swift new file mode 100644 index 00000000..185464fb --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseDrawingDto/ResponseDto/TmapAddressSearchingResponseDto.swift @@ -0,0 +1,35 @@ +// +// TmapAddressSearchingResponseDto.swift +// Runnect-iOS +// +// Created by Sojin Lee on 2023/09/26. +// + +import Foundation + +// MARK: - TmapAddressSearchingResponseDto +struct TmapAddressSearchingResponseDto: Codable { + let addressInfo: AddressInfo + + func toDepartureLocationModel(latitude: Double, longitude: Double) -> DepartureLocationModel { + let buildingName = self.addressInfo.buildingName.isEmpty ? "내가 설정한 출발지" : self.addressInfo.buildingName + + return DepartureLocationModel(departureName: buildingName, departureAddress: addressInfo.fullAddress, latitude: String(latitude), longitude: String(longitude)) + } +} + +// MARK: - AddressInfo +struct AddressInfo: Codable { + let fullAddress, addressType, cityDo, guGun: String + let eupMyun, adminDong, adminDongCode, legalDong: String + let legalDongCode, ri, bunji, roadName: String + let buildingIndex, buildingName, mappingDistance, roadCode: String + + enum CodingKeys: String, CodingKey { + case fullAddress, addressType + case cityDo = "city_do" + case guGun = "gu_gun" + case eupMyun = "eup_myun" + case adminDong, adminDongCode, legalDong, legalDongCode, ri, bunji, roadName, buildingIndex, buildingName, mappingDistance, roadCode + } +} diff --git a/Runnect-iOS/Runnect-iOS/Network/Router/CourseDrawingRouter/DepartureSearchingRouter.swift b/Runnect-iOS/Runnect-iOS/Network/Router/CourseDrawingRouter/DepartureSearchingRouter.swift index ada453b0..4d55a07e 100644 --- a/Runnect-iOS/Runnect-iOS/Network/Router/CourseDrawingRouter/DepartureSearchingRouter.swift +++ b/Runnect-iOS/Runnect-iOS/Network/Router/CourseDrawingRouter/DepartureSearchingRouter.swift @@ -11,14 +11,39 @@ import Moya enum DepartureSearchingRouter { case getAddress(keyword: String) + case getLocationAddress(latitude: Double, longitude: Double) // kakao + case getLocationTmapAddress(latitude: Double, longitude: Double) // tmap +} + +/// 현재 위치 | 검색 | 지도에서 선택 중 분기처리를 해주기 위함 +enum SelectedType { + case other + case map +} + +final class SelectedInfo { + static let shared = SelectedInfo() + var type: SelectedType = .other + + private init() {} } extension DepartureSearchingRouter: TargetType { var baseURL: URL { - guard let url = URL(string: Config.kakaoAddressBaseURL) else { - fatalError("baseURL could not be configured") + var urlString: String + + switch self { + case .getAddress: + urlString = Config.kakaoAddressBaseURL + case .getLocationAddress: + urlString = "https://dapi.kakao.com/v2/local/geo" + case .getLocationTmapAddress: + urlString = Config.tmapAddressBaseURL } + guard let url = URL(string: urlString) else { + fatalError("baseURL could not be configured") + } return url } @@ -26,12 +51,16 @@ extension DepartureSearchingRouter: TargetType { switch self { case .getAddress: return "/keyword.json" + case .getLocationAddress: + return "/coord2address.json" + case .getLocationTmapAddress: + return "/reversegeocoding" } } var method: Moya.Method { switch self { - case .getAddress: + case .getAddress, .getLocationAddress, .getLocationTmapAddress: return .get } } @@ -40,6 +69,10 @@ extension DepartureSearchingRouter: TargetType { switch self { case .getAddress(let keyword): return .requestParameters(parameters: ["query": keyword], encoding: URLEncoding.default) + case .getLocationAddress(let latitude, let longitude): + return .requestParameters(parameters: ["x": longitude, "y": latitude], encoding: URLEncoding.default) + case .getLocationTmapAddress(let latitude, let longitude): + return .requestParameters(parameters: ["lat": latitude, "lon":longitude, "addressType": "A04","appKey": Config.tmapAPIKey], encoding: URLEncoding.default) } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift index 2f1d6cb9..d387214f 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift @@ -15,6 +15,7 @@ final class CourseDrawingVC: UIViewController { // MARK: - Properties private let courseProvider = Providers.courseProvider + private let departureSearchingProvider = Providers.departureSearchingProvider private var departureLocationModel: DepartureLocationModel? @@ -29,9 +30,10 @@ final class CourseDrawingVC: UIViewController { $0.backgroundColor = .w1 } - private lazy var naviBar = CustomNavigationBar(self, type: .search) - .setTextFieldText(text: "검색 결과") + private lazy var naviBar = CustomNavigationBar(self, type: SelectedInfo.shared.type == .map ? .titleWithLeftButton : .search) .hideRightButton() + .changeTitleWithLeftButton(.b1, .g1) + .setTitle("지도에서 선택") private lazy var naviBarForEditing = CustomNavigationBar(self, type: .titleWithLeftButton) .then { @@ -46,6 +48,20 @@ final class CourseDrawingVC: UIViewController { $0.axis = .vertical } + private let underlineView = UIView().then { + $0.backgroundColor = .g4 + } + + private let aboutMapNoticeView = UIView().then { + $0.backgroundColor = .w1 + } + + private let aboutMapNoticeLabel = UILabel().then { + $0.font = .b4 + $0.textColor = .g2 + $0.text = "지도를 움직여 출발지를 설정해 주세요" + } + private let mapView = RNMapView().makeNaverLogoMargin(inset: UIEdgeInsets(top: 52, left: 0, bottom: 0, right: 0)) private let departureLocationLabel = UILabel().then { @@ -100,7 +116,20 @@ final class CourseDrawingVC: UIViewController { $0.setImage(ImageLiterals.icCancel, for: .normal) } - private let completeButton = CustomButton(title: "완성하기").setEnabled(false) + private let completeButton = CustomButton(title: "다음으로").setEnabled(false) + + private let startMarkUIImage = UIImageView().then { + $0.image = ImageLiterals.icMapDeparture + } + private let startLabelUIImage = UIImageView().then { + $0.image = ImageLiterals.icMapStart + } + private lazy var startMarkStackView = UIStackView().then { + $0.addArrangedSubviews(startLabelUIImage, startMarkUIImage) + $0.axis = .vertical + $0.alignment = .center + $0.spacing = 1 + } // MARK: - View Life Cycle @@ -119,13 +148,23 @@ final class CourseDrawingVC: UIViewController { extension CourseDrawingVC { func setData(model: DepartureLocationModel) { self.departureLocationModel = model - self.naviBar.setTextFieldText(text: model.departureName) self.mapView.makeStartMarker(at: model.toNMGLatLng(), withCameraMove: true) self.departureLocationLabel.text = model.departureName self.departureDetailLocationLabel.text = model.departureAddress } + func updateData(model: DepartureLocationModel) { + self.departureLocationModel = model + self.naviBar.setTextFieldText(text: model.departureName) + self.departureLocationLabel.text = model.departureName + self.departureDetailLocationLabel.text = model.departureAddress + + if SelectedInfo.shared.type == .other { + self.naviBar.setTextFieldText(text: model.departureName) + } + } + private func setAddTarget() { self.decideDepartureButton.addTarget(self, action: #selector(decideDepartureButtonDidTap), for: .touchUpInside) self.undoButton.addTarget(self, action: #selector(undoButtonDidTap), for: .touchUpInside) @@ -151,6 +190,11 @@ extension CourseDrawingVC { self.pathImage = image self.uploadCourseDrawing() }.store(in: cancelBag) + + mapView.eventSubject.sink { [weak self] arr in + guard let self = self else { return } + self.searchLocationTmapAddress(latitude: arr[0], longitude: arr[1]) + }.store(in: cancelBag) } private func setNavigationGesture(_ isEnabled: Bool) { @@ -184,6 +228,12 @@ extension CourseDrawingVC { self.present(alertVC, animated: false) } + + /// 수정 필요 - 바텀시트에서 완료 버튼을 누른 경우 + /// 아래의 코드를 호출해주어야함 + /// guard let self = self else { return } + /// guard handleVisitor() else { return } + /// self.mapView.capturePathImage() } // MARK: - @objc Function @@ -191,7 +241,13 @@ extension CourseDrawingVC { extension CourseDrawingVC { @objc private func decideDepartureButtonDidTap() { showHiddenViews(withDuration: 0.7) - + + if SelectedInfo.shared.type == .map { + startMarkStackView.isHidden = true + guard let model = self.departureLocationModel else { return } + mapView.makeStartMarker(at: model.toNMGLatLng(), withCameraMove: true) + } + mapView.setDrawMode(to: true) } @@ -199,9 +255,11 @@ extension CourseDrawingVC { mapView.undo() } + /// 수정 필요 - 다음으로 버튼 눌린 경우 호출될 함수 @objc private func completeButtonDidTap() { - guard handleVisitor() else { return } - mapView.capturePathImage() +// let bottomSheetVC = CourseNameBottomSheetVC() +// bottomSheetVC.modalPresentationStyle = .overFullScreen +// self.present(bottomSheetVC, animated: false) } } @@ -213,11 +271,13 @@ extension CourseDrawingVC { self.naviBarForEditing.backgroundColor = .clear self.departureInfoContainerView.layer.applyShadow(alpha: 0.35, x: 0, y: 3, blur: 10) self.distanceContainerView.layer.applyShadow(alpha: 0.2, x: 2, y: 4, blur: 9) + self.startMarkStackView.isHidden = SelectedInfo.shared.type == .map ? false : true } private func setLayout() { setHiddenViewsLayout() self.view.addSubviews(naviBarContainerStackView, mapView, departureInfoContainerView) + self.view.addSubview(startMarkStackView) self.departureInfoContainerView.addSubviews(departureLocationLabel, departureDetailLocationLabel, decideDepartureButton) view.bringSubviewToFront(naviBarContainerStackView) @@ -242,6 +302,21 @@ extension CourseDrawingVC { make.edges.equalToSuperview() } + startLabelUIImage.snp.makeConstraints { make in + make.height.equalTo(34) + make.width.equalTo(58) + } + startMarkUIImage.snp.makeConstraints { make in + make.height.width.equalTo(65) + } + + startMarkStackView.snp.makeConstraints { make in + make.height.equalTo(100) + make.width.equalTo(65) + make.centerX.equalToSuperview() + make.centerY.equalToSuperview().offset(-17) + } + departureInfoContainerView.snp.makeConstraints { make in make.leading.bottom.trailing.equalToSuperview() make.height.equalTo(172) @@ -262,6 +337,25 @@ extension CourseDrawingVC { make.leading.trailing.equalToSuperview().inset(16) make.height.equalTo(44) } + + if SelectedInfo.shared.type == .map { + self.aboutMapNoticeView.addSubview(aboutMapNoticeLabel) + self.naviBarContainerStackView.addArrangedSubviews(underlineView,aboutMapNoticeView) + + underlineView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(1) + } + + aboutMapNoticeView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(32) + } + + aboutMapNoticeLabel.snp.makeConstraints { + $0.centerX.centerY.equalToSuperview() + } + } } private func setHiddenViewsLayout() { @@ -374,4 +468,30 @@ extension CourseDrawingVC { } } } + + private func searchLocationTmapAddress(latitude: Double, longitude: Double) { + departureSearchingProvider + .request(.getLocationTmapAddress(latitude: latitude, longitude: longitude)) { [weak self] response in + guard let self = self else { return } + switch response { + case .success(let result): + let status = result.statusCode + if 200..<300 ~= status { + do { + let responseDto = try result.map(TmapAddressSearchingResponseDto.self) + self.updateData(model: responseDto.toDepartureLocationModel(latitude: latitude, longitude: longitude)) + } catch { + print(error.localizedDescription) + } + } + if status >= 400 { + print("400 error") + } + case .failure(let error): + print(error.localizedDescription) + self.showToast(message: "네트워크 통신 실패") + } + } + } } + diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift index d135000a..1365441f 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift @@ -7,15 +7,19 @@ import UIKit +import Combine import Moya +import CoreLocation -final class DepartureSearchVC: UIViewController { +final class DepartureSearchVC: UIViewController, CLLocationManagerDelegate { // MARK: - Properties private let departureSearchingProvider = Providers.departureSearchingProvider private var addressList = [KakaoAddressResult]() + private var cancelBag = CancelBag() + private var locationManager = CLLocationManager() // MARK: - UI Components @@ -27,6 +31,21 @@ final class DepartureSearchVC: UIViewController { $0.backgroundColor = .g5 } + private let selectDirectionView = LocationSelectView(type: .current) + private let selectMapView = LocationSelectView(type: .map) + + private lazy var locationSelectStackView = UIStackView().then { + $0.addArrangedSubview(selectDirectionView) + $0.addArrangedSubview(selectMapView) + $0.axis = .horizontal + $0.distribution = .fillEqually + $0.alignment = .center + } + + private let thinDividerView = UIView().then { + $0.backgroundColor = .g5 + } + private let locationTableView = UITableView(frame: .zero, style: .plain).then { $0.backgroundColor = .white $0.separatorStyle = .none @@ -59,6 +78,7 @@ final class DepartureSearchVC: UIViewController { self.setLayout() self.setDelegate() self.registerCell() + self.setBinding() } } @@ -81,6 +101,41 @@ extension DepartureSearchVC { emptyDataView.isHidden = !data.isEmpty locationTableView.reloadData() } + + private func setBinding() { + selectDirectionView.gesture().sink { _ in + SelectedInfo.shared.type = .other + self.setLocation() + }.store(in: cancelBag) + + selectMapView.gesture().sink { _ in + SelectedInfo.shared.type = .map + self.setLocation() + }.store(in: cancelBag) + } + +} + +// MARK: - Location +extension DepartureSearchVC { + /// 현재 위도, 경도에 따른 주소 받아오는 함수 + private func setLocation() { + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyBest + + DispatchQueue.global().async { [self] in + if CLLocationManager.locationServicesEnabled() { + self.locationManager.startUpdatingLocation() + + guard let latitude = locationManager.location?.coordinate.latitude, + let longitude = locationManager.location?.coordinate.longitude else { return } + searchLocationTmapAddress(latitude: latitude, longitude: longitude) + } + else { + locationManager.startUpdatingLocation() + } + } + } } // MARK: - UI & Layout @@ -92,7 +147,7 @@ extension DepartureSearchVC { } private func setLayout() { - view.addSubviews(naviBar, dividerView, locationTableView) + view.addSubviews(naviBar, dividerView, locationSelectStackView, thinDividerView, locationTableView) naviBar.snp.makeConstraints { make in make.leading.top.trailing.equalTo(view.safeAreaLayoutGuide) @@ -105,8 +160,30 @@ extension DepartureSearchVC { make.height.equalTo(6) } - locationTableView.snp.makeConstraints { make in + locationSelectStackView.snp.makeConstraints { make in make.top.equalTo(dividerView.snp.bottom) + make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(8) + make.height.equalTo(50) + } + + selectDirectionView.snp.makeConstraints { make in + make.height.equalTo(locationSelectStackView.snp.height) + make.centerY.equalTo(locationSelectStackView) + } + + selectMapView.snp.makeConstraints { make in + make.height.equalTo(locationSelectStackView.snp.height) + make.centerY.equalTo(locationSelectStackView) + } + + thinDividerView.snp.makeConstraints { make in + make.top.equalTo(locationSelectStackView.snp.bottom) + make.leading.trailing.equalTo(view.safeAreaLayoutGuide) + make.height.equalTo(1) + } + + locationTableView.snp.makeConstraints { make in + make.top.equalTo(thinDividerView.snp.bottom) make.leading.bottom.trailing.equalTo(view.safeAreaLayoutGuide) } @@ -142,6 +219,7 @@ extension DepartureSearchVC: UITableViewDelegate, UITableViewDataSource { let departureLocationModel = addressList[indexPath.item].toDepartureLocationModel() courseDrawingVC.setData(model: departureLocationModel) + SelectedInfo.shared.type = .other courseDrawingVC.hidesBottomBarWhenPushed = true self.navigationController?.pushViewController(courseDrawingVC, animated: true) @@ -190,4 +268,34 @@ extension DepartureSearchVC { } } } + + private func searchLocationTmapAddress(latitude: Double, longitude: Double) { + departureSearchingProvider + .request(.getLocationTmapAddress(latitude: latitude, longitude: longitude)) { [weak self] response in + guard let self = self else { return } + switch response { + case .success(let result): + let status = result.statusCode + if 200..<300 ~= status { + do { + let responseDto = try result.map(TmapAddressSearchingResponseDto.self) + let courseDrawingVC = CourseDrawingVC() + + courseDrawingVC.setData(model: responseDto.toDepartureLocationModel(latitude: latitude, longitude: longitude)) + + courseDrawingVC.hidesBottomBarWhenPushed = true + self.navigationController?.pushViewController(courseDrawingVC, animated: true) + } catch { + print(error.localizedDescription) + } + } + if status >= 400 { + print("400 error") + } + case .failure(let error): + print(error.localizedDescription) + self.showToast(message: "네트워크 통신 실패") + } + } + } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/Views/LocationSelectView.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/Views/LocationSelectView.swift new file mode 100644 index 00000000..3a44a77f --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/Views/LocationSelectView.swift @@ -0,0 +1,88 @@ +// +// LocationSelectView.swift +// Runnect-iOS +// +// Created by Sojin Lee on 2023/09/25. +// + +import UIKit +import Then +import SnapKit + +enum locationType { + case current + case map + + var icon: UIImage { + switch self { + case .current: + return ImageLiterals.icDirection + case .map: + return ImageLiterals.icSmallMap + } + } + + var text: String { + switch self { + case .current: + return "현재 위치에서 시작하기" + case .map: + return "지도에서 선택하기" + } + } + + var width: Int { + switch self { + case .current: + return 167 + case .map: + return 138 + } + } +} + +class LocationSelectView: BaseView { + let type: locationType + + private lazy var view = UIView() + private lazy var icon = UIImageView().then { + $0.image = type.icon + } + + private lazy var label = UILabel().then { + $0.text = type.text + $0.font = .h5 + $0.textColor = .g3 + $0.sizeToFit() + } + + init(type: locationType) { + self.type = type + + super.init(frame: .zero) + } + + override func setLayout() { + super.setLayout() + + view.addSubviews(icon, label) + addSubviews(view) + + icon.snp.makeConstraints { + $0.width.height.equalTo(20) + $0.leading.equalToSuperview() + $0.centerY.equalToSuperview() + } + + label.snp.makeConstraints { + $0.leading.equalTo(icon.snp.trailing).offset(5) + $0.centerY.equalToSuperview() + } + + view.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalToSuperview().offset(20) + $0.width.equalTo(type.width) + } + } +}