Conversation
WalkthroughSceneDelegate에 유니버설 링크 진입점을 추가하고, 해당 링크를 .universalWebLink로 변환해 ApplicationCoordinator로 전달하도록 라우팅을 확장했습니다. 앱 및 데모 타겟에 Associated Domains entitlements가 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Scene as SceneDelegate
participant Coordinator as ApplicationCoordinator
participant Notifier as NotificationHandler
participant TabBar as TabBarController
User->>Scene: Universal link 클릭 (NSUserActivity)
Scene->>Scene: scene(_:continue:) / willConnectTo 검사
Scene->>Scene: webpageURL 추출
Scene->>Coordinator: start(with: .universalWebLink(url))
Coordinator->>Notifier: receive(webLink: url)
Coordinator->>Notifier: sendNotificationIfNeeded()
Notifier->>TabBar: webLink 재전송
TabBar->>TabBar: 내비게이션 수행
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@SOPT-iOS/Projects/Demo/Sources/Application/SceneDelegate.swift`:
- Around line 48-59: The app currently handles universal links only in
scene(_:continue:) and misses cold-start links passed via
scene(_:willConnectTo:options:); update scene(_:willConnectTo:options:) to
inspect connectionOptions.userActivities for an NSUserActivity with activityType
== NSUserActivityTypeBrowsingWeb, extract its webpageURL, and call
handleUniversalLink(_:) with the URL.absoluteString (keeping the existing guard
logic used in scene(_:continue:)); reference scene(_:willConnectTo:options:),
connectionOptions.userActivities, NSUserActivityTypeBrowsingWeb, and
handleUniversalLink(_:) when making the change.
In `@SOPT-iOS/Projects/Features/RootFeature/Sources/ApplicationCoordinator.swift`:
- Around line 108-118: The deepLink and notificationLinkError subscriptions use
childCoordinators.contains(where: { $0 is DefaultTabBarCoordinator }) which
always returns false because DefaultTabBarCoordinator is never appended in
runTabBarFlow; update those subscriptions to use the same existence check as
webLink (self.tabBarController != nil) or alternatively ensure runTabBarFlow
appends the DefaultTabBarCoordinator to childCoordinators; adjust the filters
for deepLink and notificationLinkError to use self.tabBarController != nil (or
append DefaultTabBarCoordinator in runTabBarFlow) so handleDeepLink and
handleNotificationLinkError can actually be invoked.
In `@SOPT-iOS/Projects/SOPT-iOS/SOPT-iOS.entitlements`:
- Around line 11-14: The entitlements file references $(APPLINK) but APPLINK is
not defined anywhere, and the xcconfig files referenced by Configurations.swift
(xcconfigs/Base/Projects/Project-{Development,Test,QA,PROD}.xcconfig) are
missing; to fix, add or restore those xcconfig files and define APPLINK in each
configuration (Development, Test, QA, PROD) or alternatively set the APPLINK
build setting via your Tuist project manifest so the SOPT-iOS.entitlements key
com.apple.developer.associated-domains resolves correctly; ensure
Configurations.swift points to the actual xcconfig paths or update Tuist to
inject the APPLINK value for each build configuration.
🧹 Nitpick comments (1)
SOPT-iOS/Projects/Features/RootFeature/Sources/NotificationHelpers/NotificationHandler.swift (1)
111-118: 중복 발행 방지 로직을 고려해주세요.
sendNotificationIfNeeded()가 여러 번 호출되면 동일 링크가 재발행되어 중복 네비게이션이 발생할 수 있습니다. 한 번만 발행되도록 플래그 가드를 두는 방식을 고려해주세요.♻️ 예시 수정안
public final class NotificationHandler: NSObject, UNUserNotificationCenterDelegate { public let deepLink = CurrentValueSubject<DeepLinkComponentsExecutable?, Never>(nil) public let webLink = CurrentValueSubject<String?, Never>(nil) public let notificationLinkError = CurrentValueSubject<NotificationLinkError?, Never>(nil) + private var didSendPendingLinks = false @@ public func sendNotificationIfNeeded() { + guard !didSendPendingLinks else { return } + didSendPendingLinks = true if let wLink = self.webLink.value { self.webLink.send(wLink) } if let dLink = self.deepLink.value { self.deepLink.send(dLink) } } }
SOPT-iOS/Projects/Features/RootFeature/Sources/ApplicationCoordinator.swift
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@SOPT-iOS/Projects/Features/RootFeature/Sources/ApplicationCoordinator.swift`:
- Around line 108-115: The subscription on notificationHandler.webLink is
dropping the first real URL because dropFirst() is applied after compactMap;
move dropFirst() to before compactMap (i.e., call .dropFirst() directly on
webLink) or remove dropFirst() entirely so that the first non-nil emission from
CurrentValueSubject<String?, Never>(nil) is not discarded; update the chain
around notificationHandler.webLink, .dropFirst(), .compactMap { $0 },
.receive(on:), .filter { self.tabBarController != nil }, .sink { [weak self] url
in ... } accordingly.
In `@SOPT-iOS/Projects/SOPT-iOS/Sources/Application/SceneDelegate.swift`:
- Around line 40-44: The current flow calls
handleUniversalLinkWithUserAcitivity(userActivity) for any userActivity, but if
that method early-returns when the activity.type !=
NSUserActivityTypeBrowsingWeb it never falls back to starting the app; update
the logic so non-browsing activities still start the app: either (A) in the
SceneDelegate where you check connectionOptions.userActivities.first, inspect
userActivity.activityType and only call handleUniversalLinkWithUserAcitivity if
activityType == NSUserActivityTypeBrowsingWeb and otherwise call
self.appCoordinator.start(), or (B) modify handleUniversalLinkWithUserAcitivity
to call self.appCoordinator.start() when its guard for
NSUserActivityTypeBrowsingWeb fails; reference
handleUniversalLinkWithUserAcitivity(userActivity),
NSUserActivityTypeBrowsingWeb, and appCoordinator.start().
SOPT-iOS/Projects/Features/RootFeature/Sources/ApplicationCoordinator.swift
Show resolved
Hide resolved
| if let userActivity = connectionOptions.userActivities.first { | ||
| handleUniversalLinkWithUserAcitivity(userActivity) | ||
| } else { | ||
| self.appCoordinator.start() | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "SceneDelegate.swift" -type fRepository: sopt-makers/SOPT-iOS
Length of output: 367
🏁 Script executed:
cat -n SOPT-iOS/Projects/SOPT-iOS/Sources/Application/SceneDelegate.swift | head -80Repository: sopt-makers/SOPT-iOS
Length of output: 2756
🏁 Script executed:
rg "handleUniversalLinkWithUserAcitivity|handleUniversalLink" -B 2 -A 10Repository: sopt-makers/SOPT-iOS
Length of output: 8318
유니버설 링크가 아닌 userActivity에서 앱 시작이 누락됩니다.
userActivity가 존재하지만 웹 브라우징 activity가 아니면 handleUniversalLinkWithUserAcitivity() 메서드가 early return되어 appCoordinator.start()가 호출되지 않습니다. NSUserActivityTypeBrowsingWeb 확인에 실패한 경우 기본 start로 폴백해야 합니다.
수정 제안
if let userActivity = connectionOptions.userActivities.first {
handleUniversalLinkWithUserAcitivity(userActivity)
} else {
self.appCoordinator.start()
}또는 handleUniversalLinkWithUserAcitivity 메서드 내에서 guard 실패 시 self.appCoordinator.start() 호출:
func handleUniversalLinkWithUserAcitivity(_ userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL else {
print("❌ Not a universal link")
+ self.appCoordinator.start()
return
}
handleUniversalLink(incomingURL.absoluteString)
}🤖 Prompt for AI Agents
In `@SOPT-iOS/Projects/SOPT-iOS/Sources/Application/SceneDelegate.swift` around
lines 40 - 44, The current flow calls
handleUniversalLinkWithUserAcitivity(userActivity) for any userActivity, but if
that method early-returns when the activity.type !=
NSUserActivityTypeBrowsingWeb it never falls back to starting the app; update
the logic so non-browsing activities still start the app: either (A) in the
SceneDelegate where you check connectionOptions.userActivities.first, inspect
userActivity.activityType and only call handleUniversalLinkWithUserAcitivity if
activityType == NSUserActivityTypeBrowsingWeb and otherwise call
self.appCoordinator.start(), or (B) modify handleUniversalLinkWithUserAcitivity
to call self.appCoordinator.start() when its guard for
NSUserActivityTypeBrowsingWeb fails; reference
handleUniversalLinkWithUserAcitivity(userActivity),
NSUserActivityTypeBrowsingWeb, and appCoordinator.start().
There was a problem hiding this comment.
handleUniversalLinkWithUserActivity 메서드 내에서 확인하고 있는 것으로 보임
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg "handleUniversalLinkWithUserActivity|handleUniversalLinkWithUserAcitivity|appCoordinator.start" SOPT-iOS/Projects/SOPT-iOS/Sources/Application/SceneDelegate+HandleURL.swift -n -A 5 -B 2
Length of output: 682
@yungu0010, 맞습니다! 확인 결과 handleUniversalLinkWithUserActivity 메서드 내부(Line 72–76)에서 guard 실패 시 appCoordinator.start()를 호출하는 폴백이 이미 구현되어 있습니다:
func handleUniversalLinkWithUserActivity(_ userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL else {
print("❌ Not a universal link")
appCoordinator.start() // ✅ 폴백 처리됨
return
}
handleUniversalLink(incomingURL.absoluteString)
}제가 처음에 메서드 내부 구현을 잘못 판단했습니다. 지적해주셔서 감사합니다!
🧠 Learnings used
Learnt from: dlwogus0128
Repo: sopt-makers/SOPT-iOS PR: 699
File: SOPT-iOS/Projects/Features/RootFeature/Sources/ApplicationCoordinator.swift:576-581
Timestamp: 2025-09-23T04:48:57.229Z
Learning: In SOPT-iOS coordinator refactoring: Legacy coordinators require addDependency() calls for proper lifecycle management, but the new coordinator architecture handles dependency management differently and does not require explicit addDependency() calls.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
SOPT-iOS/Projects/Features/RootFeature/Sources/ApplicationCoordinator.swift (1)
97-106:⚠️ Potential issue | 🔴 Critical
deepLink필터가 여전히DefaultTabBarCoordinator체크를 사용하고 있어 항상 false를 반환합니다.
webLink(Line 113)와notificationLinkError(Line 124)는self.tabBarController != nil로 업데이트되었지만,deepLink는 여전히self.childCoordinators.contains(where: { $0 is DefaultTabBarCoordinator })를 사용합니다.이전 리뷰에서 지적된 대로
TabBarCoordinator가childCoordinators에 추가되지 않기 때문에 이 필터는 항상false를 반환하여handleDeepLink가 절대 실행되지 않습니다.🐛 수정 제안
self.notificationHandler.deepLink .compactMap { $0 } .receive(on: DispatchQueue.main) .filter { _ in - self.childCoordinators.contains(where: { $0 is DefaultTabBarCoordinator }) + self.tabBarController != nil } .sink { [weak self] deepLinkComponent in self?.handleDeepLink(deepLink: deepLinkComponent) self?.notificationHandler.clearNotificationRecord() }.store(in: cancelBag)
🤖 Fix all issues with AI agents
In `@SOPT-iOS/Projects/Demo/Sources/Application/SceneDelegate`+HandleURL.swift:
- Line 71: Rename the misspelled method handleUniversalLinkWithUserAcitivity to
handleUniversalLinkWithUserActivity and update all call sites accordingly;
specifically rename the method declaration in SceneDelegate+HandleURL (the
function currently named handleUniversalLinkWithUserAcitivity) and change every
invocation in SceneDelegate files that call handleUniversalLinkWithUserAcitivity
to call handleUniversalLinkWithUserActivity so signatures match.
| appCoordinator.start(with: .universalWebLink(url: url)) | ||
| } | ||
|
|
||
| func handleUniversalLinkWithUserAcitivity(_ userActivity: NSUserActivity) { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "handleUniversalLinkWithUserAcitivity" --type swiftRepository: sopt-makers/SOPT-iOS
Length of output: 895
🏁 Script executed:
rg -n "handleUniversalLinkWithUserActivity" --type swiftRepository: sopt-makers/SOPT-iOS
Length of output: 46
메서드 이름의 오타를 수정해야 합니다.
handleUniversalLinkWithUserAcitivity → handleUniversalLinkWithUserActivity ("Acitivity" → "Activity")
다음 위치들에서 모두 수정이 필요합니다:
SOPT-iOS/Projects/SOPT-iOS/Sources/Application/SceneDelegate+HandleURL.swift:71(메서드 정의)SOPT-iOS/Projects/SOPT-iOS/Sources/Application/SceneDelegate.swift:41, 55(호출 부분)SOPT-iOS/Projects/Demo/Sources/Application/SceneDelegate+HandleURL.swift:71(메서드 정의)SOPT-iOS/Projects/Demo/Sources/Application/SceneDelegate.swift:42, 56(호출 부분)
🤖 Prompt for AI Agents
In `@SOPT-iOS/Projects/Demo/Sources/Application/SceneDelegate`+HandleURL.swift at
line 71, Rename the misspelled method handleUniversalLinkWithUserAcitivity to
handleUniversalLinkWithUserActivity and update all call sites accordingly;
specifically rename the method declaration in SceneDelegate+HandleURL (the
function currently named handleUniversalLinkWithUserAcitivity) and change every
invocation in SceneDelegate files that call handleUniversalLinkWithUserAcitivity
to call handleUniversalLinkWithUserActivity so signatures match.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@SOPT-iOS/Projects/SOPT-iOS/Sources/Application/SceneDelegate`+HandleURL.swift:
- Around line 71-80: The guard failure currently always calls
appCoordinator.start(), which will restart the coordinator when
handleUniversalLinkWithUserActivity(_:) is invoked from scene(_:continue:),
causing unwanted navigation; change handleUniversalLinkWithUserActivity(_:) to
accept a caller-context flag (e.g., isInitialLaunch: Bool = false) or add an
explicit parameter (keep function name handleUniversalLinkWithUserActivity) and
only call appCoordinator.start() when isInitialLaunch is true (the
scene(_:willConnectTo:options:) caller should pass true, scene(_:continue:)
should pass false); also update both callers (scene(_:willConnectTo:options:)
and scene(_:continue:)) to pass the appropriate flag so scene(_:continue:)
returns without restarting the coordinator on guard failure.
| func handleUniversalLinkWithUserActivity(_ userActivity: NSUserActivity) { | ||
| guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, | ||
| let incomingURL = userActivity.webpageURL else { | ||
| print("❌ Not a universal link") | ||
| appCoordinator.start() | ||
| return | ||
| } | ||
|
|
||
| handleUniversalLink(incomingURL.absoluteString) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n -B5 -A5 'handleUniversalLinkWithUserActivity' --type swiftRepository: sopt-makers/SOPT-iOS
Length of output: 6922
scene(_:continue:)에서 호출될 때 guard 실패 처리 개선 필요
이 함수는 앱 시작 시 scene(_:willConnectTo:options:)과 앱이 이미 실행 중일 때 scene(_:continue:)에서 호출됩니다. 현재 guard 실패 시 두 경로 모두에서 appCoordinator.start()를 호출하고 있으므로, scene(_:continue:)에서 호출되는 경우 이미 활성화된 coordinator를 재시작하게 되어 예상치 못한 네비게이션 상태 변경이나 시각적 결함을 유발할 수 있습니다.
guard 실패 시 호출 컨텍스트에 따라 다르게 처리하거나, 최소한 scene(_:continue:)에서는 coordinator를 재시작하지 않고 단순 반환하도록 개선하세요.
🤖 Prompt for AI Agents
In `@SOPT-iOS/Projects/SOPT-iOS/Sources/Application/SceneDelegate`+HandleURL.swift
around lines 71 - 80, The guard failure currently always calls
appCoordinator.start(), which will restart the coordinator when
handleUniversalLinkWithUserActivity(_:) is invoked from scene(_:continue:),
causing unwanted navigation; change handleUniversalLinkWithUserActivity(_:) to
accept a caller-context flag (e.g., isInitialLaunch: Bool = false) or add an
explicit parameter (keep function name handleUniversalLinkWithUserActivity) and
only call appCoordinator.start() when isInitialLaunch is true (the
scene(_:willConnectTo:options:) caller should pass true, scene(_:continue:)
should pass false); also update both callers (scene(_:willConnectTo:options:)
and scene(_:continue:)) to pass the appropriate flag so scene(_:continue:)
returns without restarting the coordinator on guard failure.
yungu0010
left a comment
There was a problem hiding this comment.
고생하셨습니다! 몇 가지 질문 남겼습니다!
SceneDelegate나 새로 추가된 함수에 대해 주석을 달아두는건 어떨까요? 추후 유니버셜 링크 작업을 유지보수하는 데 도움이 될 것 같아요~~
| }.store(in: cancelBag) | ||
|
|
||
| self.notificationHandler.webLink | ||
| .dropFirst() |
There was a problem hiding this comment.
dropFirst를 추가해준 이유가 궁금합니다!
기존 웹링크 알림 로직도 정상적으로 동작하나요?
| self.notificationLinkError.send(nil) | ||
| } | ||
|
|
||
| public func sendNotificationIfNeeded() { |
There was a problem hiding this comment.
해당 함수가 왜 필요한지 궁금합니다! handleWebLink와 handleDeepLink를 사용하면 안되나요 ? ?
There was a problem hiding this comment.
유니버설 링크의 경우 앱이 초기화 되기 전에 (Coordinator, TabBarCon..등) 들어와서 NotificationHandelr에 저장되고 있는데, 들어온 당시에는 bindNotification이 처리되기 전이라서 이벤트를 수신하지 못하고 있었습니다 ! 따라서 모두 bind한 뒤에 만약 저장된 웹링크가 있다면 재방출해줄 수 있도록 처리했습니다.
handleWebLink와 handleDeepLink는 파싱하는 역할을 해주고 있어서 새로운 함수를 통해 재방출을 구현했습니다.
| if let userActivity = connectionOptions.userActivities.first { | ||
| handleUniversalLinkWithUserAcitivity(userActivity) | ||
| } else { | ||
| self.appCoordinator.start() | ||
| } |
There was a problem hiding this comment.
handleUniversalLinkWithUserActivity 메서드 내에서 확인하고 있는 것으로 보임
🌴 PR 요약
외부 링크(문자, 카카오톡 등)에서 SOPT 앱으로 직접 진입할 수 있도록 Universal Links를 구현했습니다.
🌱 작업한 브랜치
🌱 PR Point
scene(_:continue:)메서드 추가universalWebLink옵션 처리 추가NotificationHandler를 통한 웹링크 전달sendNotificationIfNeeded()메서드 사용📌 참고 사항
서버 설정 필요
dev/prod 도메인 둘 다 AASA 파일 배포를 부탁드린 상태입니다.
📮 관련 이슈