diff --git a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj index 531149f4..f513f5f4 100644 --- a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj +++ b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj @@ -1703,7 +1703,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2024.0208.0315; + CURRENT_PROJECT_VERSION = 2024.0312.0041; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8Q4H7X3Q58; GENERATE_INFOPLIST_FILE = NO; @@ -1726,7 +1726,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.runnect.Runnect-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.runnect.Runnect-iOS"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.runnect-Runnect-iOS"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -1747,7 +1747,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2024.0208.0315; + CURRENT_PROJECT_VERSION = 2024.0312.0041; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8Q4H7X3Q58; GENERATE_INFOPLIST_FILE = NO; @@ -1770,7 +1770,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.runnect.Runnect-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.runnect.Runnect-iOS"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.runnect-Runnect-iOS"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; diff --git a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIViewController+.swift b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIViewController+.swift index 547d088a..1ee0ea48 100644 --- a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIViewController+.swift +++ b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIViewController+.swift @@ -8,6 +8,7 @@ import UIKit import SnapKit +import FirebaseDynamicLinks extension UIViewController { @@ -89,10 +90,84 @@ extension UIViewController { } extension UIViewController { + + /** + ### Description: 공유 기능에 해당하는 정보를 넣어줍니다. + 2025년 8월 25일에 동적링크는 만기 됩니다. + + - courseTitle : 타이틀 이름 + - courseId : 코스 아이디 + - courseImageURL : 코스 사진 + - minimumAppVersion : 공유 기능을 사용할 수 있는 최소 버전 + - descriptionText : 내용 + - parameter : 공유 기능에 필요한 파라미터 + */ + /// 공유 기능 메서드 + /// + func shareCourse( + courseTitle: String, + courseId: Int, + courseImageURL: String, + minimumAppVersion: String, + descriptionText: String? = nil, + parameter: String + ) { + let dynamicLinksDomainURIPrefix = "https://rnnt.page.link" + let courseParameter = parameter + guard let link = URL(string: "\(dynamicLinksDomainURIPrefix)/?\(courseParameter)=\(courseId)") else { + print("Invalid link.") + return + } + + guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else { + print("Failed to create link builder.") + return + } + + linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.runnect.Runnect-iOS") + linkBuilder.iOSParameters?.appStoreID = "1663884202" + linkBuilder.iOSParameters?.minimumAppVersion = minimumAppVersion + + linkBuilder.androidParameters = DynamicLinkAndroidParameters(packageName: "com.runnect.runnect") + + linkBuilder.socialMetaTagParameters = DynamicLinkSocialMetaTagParameters() + linkBuilder.socialMetaTagParameters?.imageURL = URL(string: courseImageURL) + linkBuilder.socialMetaTagParameters?.title = courseTitle + linkBuilder.socialMetaTagParameters?.descriptionText = descriptionText ?? "" + + linkBuilder.shorten { [weak self] url, _, error in + guard let shortDynamicLink = url?.absoluteString else { + if let error = error { + print("Error shortening dynamic link: \(error)") + } + return + } + + print("Short URL is: \(shortDynamicLink)") + DispatchQueue.main.async { + self?.presentShareActivity(with: shortDynamicLink) + } + } + } + + private func presentShareActivity(with url: String) { + let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil) + activityVC.popoverPresentationController?.sourceView = self.view + self.present(activityVC, animated: true, completion: nil) + } +} + +extension UIViewController { + /** + - Description: 뷰컨에서 GA(구글 애널리틱스) 스크린 , 버튼 이벤트 사용 사는 메서드 입니다. + */ + + /// 스크린 이벤트 func analyze(screenName: String) { GAManager.shared.logEvent(eventType: .screen(screenName: screenName)) } + /// 버튼 이벤트 func analyze(buttonName: String) { GAManager.shared.logEvent(eventType: .button(buttonName: buttonName)) } diff --git a/Runnect-iOS/Runnect-iOS/Global/Supports/SceneDelegate.swift b/Runnect-iOS/Runnect-iOS/Global/Supports/SceneDelegate.swift index 455a21bb..954dc637 100644 --- a/Runnect-iOS/Runnect-iOS/Global/Supports/SceneDelegate.swift +++ b/Runnect-iOS/Runnect-iOS/Global/Supports/SceneDelegate.swift @@ -12,11 +12,15 @@ import FirebaseDynamicLinks import FirebaseCore import FirebaseCoreInternal +// 들어온 링크가 공유된 코스인지, 개인 보관함에 있는 코스인지 나타내기 위한 타입입니다. +enum CourseType { + case publicCourse, privateCourse +} + class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let _ = (scene as? UIWindowScene) else { return } @@ -39,30 +43,33 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { if let incomingURL = userActivity.webpageURL { - let linkHandled = DynamicLinks.dynamicLinks() + DynamicLinks.dynamicLinks() .handleUniversalLink(incomingURL) { dynamicLink, error in - if let courseId = self.handleDynamicLink(dynamicLink) { - guard let _ = (scene as? UIWindowScene) else { return } + if let (courseType, courseId) = self.handleDynamicLink(dynamicLink) { + guard let windowScene = scene as? UIWindowScene else { return } + let window = UIWindow(windowScene: windowScene) + let navigationController = UINavigationController() - if let windowScene = scene as? UIWindowScene { - let window = UIWindow(windowScene: windowScene) - + switch courseType { + case .publicCourse: let courseDetailVC = CourseDetailVC() - courseDetailVC.getUploadedCourseDetail(courseId: Int(courseId)) - - let tabBarController = TabBarController() - let navigationController = UINavigationController(rootViewController: tabBarController) - navigationController.navigationBar.isHidden = true + courseDetailVC.getUploadedCourseDetail(courseId: courseId) navigationController.pushViewController(courseDetailVC, animated: false) - - // 코스 발견 view 로 이동 - tabBarController.selectedIndex = 2 - window.rootViewController = navigationController - window.makeKeyAndVisible() - self.window = window - + case .privateCourse: + let privateCourseDetailVC = RunningWaitingVC() + privateCourseDetailVC.setData(courseId: courseId, publicCourseId: nil) + navigationController.pushViewController(privateCourseDetailVC, animated: false) } + + let tabBarController = TabBarController() + navigationController.navigationBar.isHidden = true + navigationController.viewControllers = [tabBarController, navigationController.viewControllers.last].compactMap { $0 } + + tabBarController.selectedIndex = 2 + window.rootViewController = navigationController + window.makeKeyAndVisible() + self.window = window } } } @@ -106,17 +113,26 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // to restore the scene back to its current state. } - func handleDynamicLink(_ dynamicLink: DynamicLink?) -> String? { + func handleDynamicLink(_ dynamicLink: DynamicLink?) -> (courseType: CourseType, courseId: Int)? { if let dynamicLink = dynamicLink, let url = dynamicLink.url, let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems { + var courseId: Int? + var courseType: CourseType? + for item in queryItems { - if item.name == "courseId", let courseId = item.value { - // 동적링크 핸들링 하여 courseId 추출 - - return courseId + if item.name == "courseId", let id = item.value, let idInt = Int(id) { + courseId = idInt + courseType = .publicCourse + } else if item.name == "privateCourseId", let id = item.value, let idInt = Int(id) { + courseId = idInt + courseType = .privateCourse } } + + if let courseId = courseId, let courseType = courseType { + return (courseType, courseId) + } } return nil } diff --git a/Runnect-iOS/Runnect-iOS/Info.plist b/Runnect-iOS/Runnect-iOS/Info.plist index d7b63c1f..104548fc 100644 --- a/Runnect-iOS/Runnect-iOS/Info.plist +++ b/Runnect-iOS/Runnect-iOS/Info.plist @@ -19,7 +19,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.0.0 + 2.0.1 + ITSAppUsesNonExemptEncryption + CFBundleURLTypes @@ -44,7 +46,7 @@ CFBundleVersion - 2024.0208.0315 + 2024.0312.0041 LSApplicationQueriesSchemes kakaokompassauth diff --git a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseStorageDto/ResponseDto/PrivateCourseResponseDto.swift b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseStorageDto/ResponseDto/PrivateCourseResponseDto.swift index b0e5d0d1..60cf57e0 100644 --- a/Runnect-iOS/Runnect-iOS/Network/Dto/CourseStorageDto/ResponseDto/PrivateCourseResponseDto.swift +++ b/Runnect-iOS/Runnect-iOS/Network/Dto/CourseStorageDto/ResponseDto/PrivateCourseResponseDto.swift @@ -17,6 +17,7 @@ struct PrivateCourseResponseDto: Codable { struct PrivateCourse: Codable { let id: Int + let isNowUser: Bool? let title: String let image, createdAt: String let distance: Float? diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift index a829a2b4..374a2559 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDetail/VC/CourseDetailVC.swift @@ -2,7 +2,7 @@ // CourseDetailVC.swift // Runnect-iOS // -// Created by 몽이 누나 on 2023/01/05. +// Created by 이명진 on 2023/10/09. // import UIKit @@ -13,9 +13,6 @@ import NMapsMap import Moya import SafariServices import KakaoSDKCommon -import FirebaseCore -import FirebaseDynamicLinks -import KakaoSDKShare import KakaoSDKTemplate import DropDown @@ -167,9 +164,7 @@ extension CourseDetailVC { scrapCourse(scrapTF: !sender.isSelected) delegate?.didUpdateScrapState(publicCourseId: publicCourseId, isScrapped: !sender.isSelected) /// 코스 발견 UI Update 부분 - marathonDelegate?.didUpdateMarathonScrapState(publicCourseId: publicCourseId, isScrapped: !sender.isSelected) // 마라톤 코스 UI Update 부분 - - /// print("CourseDetailVC 스크랩 탭🔥publicCourseId=\(publicCourseId), isScrapped은 \(!sender.isSelected) 요렇게 변경 ") + marathonDelegate?.didUpdateMarathonScrapState(publicCourseId: publicCourseId, isScrapped: !sender.isSelected) // 마라톤 코스 UI } @objc private func shareButtonTapped() { @@ -177,58 +172,18 @@ extension CourseDetailVC { return } - analyze(buttonName: GAEvent.Button.clickShare) - let publicCourse = model.publicCourse - let title = publicCourse.title - let courseId = publicCourse.id // primaryKey - let description = publicCourse.description - let courseImage = publicCourse.image - - let dynamicLinksDomainURIPrefix = "https://rnnt.page.link" - guard let link = URL(string: "\(dynamicLinksDomainURIPrefix)/?courseId=\(courseId)") else { - return - } - - print("‼️link= \(link)") - - guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else { - return - } - - linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "com.runnect.Runnect-iOS") - linkBuilder.iOSParameters?.appStoreID = "1663884202" - linkBuilder.iOSParameters?.minimumAppVersion = "1.0.4" - - linkBuilder.androidParameters = DynamicLinkAndroidParameters(packageName: "com.runnect.runnect") - linkBuilder.socialMetaTagParameters = DynamicLinkSocialMetaTagParameters() - linkBuilder.socialMetaTagParameters?.imageURL = URL(string: courseImage) - linkBuilder.socialMetaTagParameters?.title = title - linkBuilder.socialMetaTagParameters?.descriptionText = description - - guard let longDynamicLink = linkBuilder.url else { - return - } - print("The long URL is: \(longDynamicLink)") + analyze(buttonName: GAEvent.Button.clickShare) - /// 짧은 Dynamic Link로 변환하는 부분 입니다. - linkBuilder.shorten { [weak self] url, _, error in // warning 파라미터 와일드 카드 - guard let shortDynamicLink = url?.absoluteString else { - if let error = error { - print("❌Error shortening dynamic link: \(error)") - } - return - } - - print("🔥The short URL is: \(shortDynamicLink)") - - DispatchQueue.main.async { - let activityVC = UIActivityViewController(activityItems: [shortDynamicLink], applicationActivities: nil) - activityVC.popoverPresentationController?.sourceView = self?.view - self?.present(activityVC, animated: true, completion: nil) - } - } + self.shareCourse( + courseTitle: publicCourse.title, + courseId: publicCourse.id, + courseImageURL: publicCourse.image, + minimumAppVersion: "1.0.4", + descriptionText: publicCourse.description, + parameter: "courseId" + ) } @objc private func pushToUserProfileVC() { @@ -479,10 +434,6 @@ extension CourseDetailVC { $0.width.height.equalTo(37) } -// profileNameLabel.snp.makeConstraints { -// $0.leading.equalTo(profileImageView.snp.trailing).offset(12) -// } - firstHorizontalDivideLine.snp.makeConstraints { $0.top.equalTo(mapImageView.snp.bottom).offset(62) $0.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(14) diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift index f39f141b..48daf273 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift @@ -107,9 +107,8 @@ extension CourseStorageVC { guard let self = self else { return } analyze(buttonName: GAEvent.Button.clickScrapPageStartCourse) // 코스 발견_스크랩코스 상세페이지 시작하기 Event - let title = self.privateCourseList[index].title let runningWaitingVC = RunningWaitingVC() - runningWaitingVC.setData(courseId: self.privateCourseList[index].id, publicCourseId: nil, courseTitle: title) + runningWaitingVC.setData(courseId: self.privateCourseList[index].id, publicCourseId: nil) /// 코스 이름을 여기서 가져오는 로직 runningWaitingVC.hidesBottomBarWhenPushed = true diff --git a/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunningWaitingVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunningWaitingVC.swift index 99bf636f..9c8de928 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunningWaitingVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunningWaitingVC.swift @@ -13,6 +13,8 @@ import Moya import SnapKit import Then +import FirebaseDynamicLinks + final class RunningWaitingVC: UIViewController { // MARK: - Properties @@ -28,7 +30,12 @@ final class RunningWaitingVC: UIViewController { // MARK: - UI Components - private lazy var naviBar = CustomNavigationBar(self, type: .titleWithLeftButton).setTitle(courseTitle ?? "Test Code") + private lazy var naviBar = CustomNavigationBar(self, type: .titleWithLeftButton) + + private let shareButton = UIButton(type: .system).then { + $0.setImage(ImageLiterals.icShareButton, for: .normal) + $0.tintColor = .g1 + } private let moreButton = UIButton(type: .system).then { $0.setImage(ImageLiterals.icMore, for: .normal) @@ -81,10 +88,9 @@ final class RunningWaitingVC: UIViewController { // MARK: - Methods extension RunningWaitingVC { - func setData(courseId: Int, publicCourseId: Int?, courseTitle: String) { + func setData(courseId: Int, publicCourseId: Int?) { self.courseId = courseId self.publicCourseId = publicCourseId - self.courseTitle = courseTitle getCourseDetail(courseId: courseId) } @@ -92,10 +98,16 @@ extension RunningWaitingVC { private func setCourseData(courseModel: Course) { self.courseModel = courseModel + guard let isMyCourse = courseModel.isNowUser else { return } + self.isMyCourse(courseOwner: isMyCourse) + + self.courseTitle = courseModel.title + self.naviBar.setTitle(self.courseTitle ?? "타이틀 없음") + guard let path = courseModel.path, let distance = courseModel.distance else { return } let locations = path.map { NMGLatLng(lat: $0[0], lng: $0[1]) } self.makePath(locations: locations) - self.distanceLabel.text = String(distance) + self.distanceLabel.text = String(format: "%.1f", distance) } private func makePath(locations: [NMGLatLng]) { @@ -104,7 +116,14 @@ extension RunningWaitingVC { private func setAddTarget() { self.startButton.addTarget(self, action: #selector(startButtonDidTap), for: .touchUpInside) - moreButton.addTarget(self, action: #selector(moreButtonDidTap), for: .touchUpInside) + self.moreButton.addTarget(self, action: #selector(moreButtonDidTap), for: .touchUpInside) + self.shareButton.addTarget(self, action: #selector(shareButtonDidTap), for: .touchUpInside) + } + + private func isMyCourse(courseOwner: Bool) { + print("💪💪💪💪💪💪💪💪💪💪💪💪💪💪💪💪💪💪") + self.shareButton.isHidden = !courseOwner + self.moreButton.isHidden = !courseOwner } } @@ -127,8 +146,24 @@ extension RunningWaitingVC { self.navigationController?.pushViewController(countDownVC, animated: true) } + @objc private func shareButtonDidTap() { + guard let model = self.courseModel else { + return + } + analyze(buttonName: GAEvent.Button.clickShare) + + self.shareCourse( + courseTitle: model.title, + courseId: model.id, + courseImageURL: model.image, + minimumAppVersion: "2.0.1", + descriptionText: "이 코스는 링크로만 들어올 수 있어요!", + parameter: "privateCourseId" + ) + } + @objc private func moreButtonDidTap() { - guard let courseModel = self.courseModel else {return} + guard let courseModel = self.courseModel else { return } let items = ["수정하기", "삭제하기"] let imageArray: [UIImage] = [ImageLiterals.icModify, ImageLiterals.icRemove] @@ -171,6 +206,7 @@ extension RunningWaitingVC { private func setLayout() { view.addSubviews(naviBar, moreButton, + shareButton, mapView, distanceContainerView, startButton) @@ -182,10 +218,17 @@ extension RunningWaitingVC { view.bringSubviewToFront(naviBar) + shareButton.snp.makeConstraints { + $0.trailing.equalTo(moreButton.snp.leading) + $0.centerY.equalTo(naviBar) + } + moreButton.snp.makeConstraints { $0.trailing.equalTo(self.view.safeAreaLayoutGuide) $0.centerY.equalTo(naviBar) } + + view.bringSubviewToFront(shareButton) view.bringSubviewToFront(moreButton) mapView.snp.makeConstraints { diff --git a/Runnect-iOS/fastlane/Matchfile b/Runnect-iOS/fastlane/Matchfile index 55ed04f7..671dbbf8 100644 --- a/Runnect-iOS/fastlane/Matchfile +++ b/Runnect-iOS/fastlane/Matchfile @@ -1,10 +1,10 @@ git_url("https://github.com/thingineeer/fastlane-match-runnect") -# git_branch("Runnect") 수정 금지 +git_branch("master") storage_mode("git") type("development") # The default type, can be: appstore, adhoc, enterprise or development -# app_identifier("com.runnect.Runnect-iOS") +app_identifier("com.runnect.Runnect-iOS") # username("user@fastlane.tools") # Your Apple Developer Portal username # For all available options run `fastlane match --help`