-
Notifications
You must be signed in to change notification settings - Fork 1
Refactor/#264 테스트3 #267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor/#264 테스트3 #267
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,7 +34,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { | |
| UserDefaults.standard.set(true, forKey: "isAppAlarmOn") | ||
|
|
||
| // MARK: - 카카오 로그인 설정 | ||
|
|
||
| KakaoSDK.initSDK(appKey: Config.kakaoNativeAppKey) | ||
|
|
||
| let result = KeyChainService.loadTokens(accessKey: Config.accessTokenKey, refreshKey: Config.refreshTokenKey) | ||
|
|
@@ -50,8 +50,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { | |
|
|
||
| switch loginType { | ||
| case Config.appleLogin: | ||
| checkAppleLogin { [weak self] result in | ||
| self?.isLogin = result | ||
| Task { [weak self] in | ||
| let result = await self?.checkAppleLogin() ?? false | ||
| await MainActor.run { [weak self] in | ||
| self?.isLogin = result | ||
| } | ||
| } | ||
| case Config.kakaoLogin: | ||
| checkKakaoLogin { [weak self] result in | ||
|
|
@@ -108,48 +111,55 @@ class AppDelegate: UIResponder, UIApplicationDelegate { | |
| } | ||
| } | ||
|
|
||
| func checkAppleLogin(completion: @escaping (Bool) -> Void) { | ||
| func checkAppleLogin() async -> Bool { | ||
| let appleIDProvider = ASAuthorizationAppleIDProvider() | ||
| appleIDProvider.getCredentialState(forUserID: UserDefaults.standard.string(forKey: Config.appleUserID) ?? "") { (credentialState, error) in | ||
| switch credentialState { | ||
| case .authorized: | ||
| print("해당 ID는 연동되어있습니다.") | ||
| completion(true) | ||
| case .revoked: | ||
| print("해당 ID는 연동되어있지않습니다.") | ||
| completion(false) | ||
| case .notFound: | ||
| print("해당 ID를 찾을 수 없습니다.") | ||
| completion(false) | ||
| default: | ||
| break | ||
| return await withCheckedContinuation { continuation in | ||
| appleIDProvider.getCredentialState(forUserID: UserDefaults.standard.string(forKey: Config.appleUserID) ?? "") { credentialState, _ in | ||
| switch credentialState { | ||
| case .authorized: | ||
| print("해당 ID는 연동되어있습니다.") | ||
| continuation.resume(returning: true) | ||
| case .revoked: | ||
| print("해당 ID는 연동되어있지 않습니다.") | ||
| continuation.resume(returning: false) | ||
| case .notFound: | ||
| print("해당 ID를 찾을 수 없습니다.") | ||
| continuation.resume(returning: false) | ||
| default: | ||
| continuation.resume(returning: false) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - MessagingDelegate | ||
|
|
||
| extension AppDelegate: MessagingDelegate { | ||
|
|
||
| // FCM 토큰을 받았을 때 실행 | ||
| func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { | ||
| nonisolated func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { | ||
| if let fcmToken { | ||
| let _ = KeyChainService.saveFCMToken(fcmToken: fcmToken, key: Config.fcmTokenKey) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension AppDelegate: UNUserNotificationCenterDelegate { | ||
|
|
||
| func userNotificationCenter(_ center: UNUserNotificationCenter, | ||
| didReceive response: UNNotificationResponse, | ||
| withCompletionHandler completionHandler: @escaping () -> Void) { | ||
|
|
||
| if let navigationViewController = UIApplication.shared.keyWindow?.rootViewController as? ToasterNavigationController { | ||
| navigationViewController.popToRootViewController(animated: false) | ||
| if let tabBarController = navigationViewController.topViewController as? TabBarController { | ||
| tabBarController.selectedIndex = 0 | ||
| nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, | ||
| didReceive response: UNNotificationResponse, | ||
| withCompletionHandler completionHandler: @escaping () -> Void) { | ||
| Task { @MainActor in | ||
| if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, | ||
| let navigationViewController = windowScene.windows | ||
| .first(where: { $0.isKeyWindow })? | ||
| .rootViewController as? ToasterNavigationController { | ||
| navigationViewController.popToRootViewController(animated: false) | ||
| if let tabBarController = navigationViewController.topViewController as? TabBarController { | ||
| tabBarController.selectedIndex = 0 | ||
| } | ||
| } | ||
| } | ||
| completionHandler() | ||
| } | ||
|
Comment on lines
+149
to
164
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Consider awaiting the Task completion or moving the completion handler inside the Task: 🐛 Proposed fix nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: `@escaping` () -> Void) {
Task { `@MainActor` in
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let navigationViewController = windowScene.windows
.first(where: { $0.isKeyWindow })?
.rootViewController as? ToasterNavigationController {
navigationViewController.popToRootViewController(animated: false)
if let tabBarController = navigationViewController.topViewController as? TabBarController {
tabBarController.selectedIndex = 0
}
}
+ completionHandler()
}
- completionHandler()
}🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
|
|
||
| import Foundation | ||
|
|
||
| @MainActor | ||
| protocol CoordinatorFinishOutput { | ||
| var onFinish: (() -> Void)? { get set } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,7 +16,7 @@ final class APIInterceptor: RequestInterceptor { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 재발급을 받고 다시 기존 API 를 호출해야하기 때문에 Requset 의 Header 를 변경해야 한다. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 해당 프로퍼티가 true 일 경우 기존의 Requset 의 Header 를 변경 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// 재발급을 받은 적이 없는 false 의 경우 TargetType 을 통해 Header 를 설정한 Requset 를 기본으로 사용 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var isTokenRefreshed = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nonisolated(unsafe) private var isTokenRefreshed = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "APIInterceptor.swift" -type fRepository: Link-MIND/TOASTER-iOS Length of output: 112 🏁 Script executed: cat -n TOASTER-iOS/Network/Base/APIInterceptor.swiftRepository: Link-MIND/TOASTER-iOS Length of output: 4203 Remove the The singleton Suggested fixRemove the flag and instead read the token from KeyChain on every request: final class APIInterceptor: RequestInterceptor {
- nonisolated(unsafe) private var isTokenRefreshed = false
static let shared = APIInterceptor()
func adapt(_ urlRequest: URLRequest, for session: Session, completion: `@escaping` (Result<URLRequest, Error>) -> Void) {
print("adapt 진입")
var modifiedRequest = urlRequest
- if isTokenRefreshed {
- print("토큰 재발급 후 URLRequset 변경")
- modifiedRequest.setValue(KeyChainService.loadAccessToken(key: Config.accessTokenKey), forHTTPHeaderField: "accessToken")
-
- isTokenRefreshed = false
- }
+ modifiedRequest.setValue(KeyChainService.loadAccessToken(key: Config.accessTokenKey), forHTTPHeaderField: "accessToken")
completion(.success(modifiedRequest))
}
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: `@escaping` (RetryResult) -> Void) {
NetworkService.shared.authService.postRefreshToken { [weak self] result in
switch result {
case .success(let result):
guard let serverAccessToken = result?.data.accessToken, let serverRefreshToken = result?.data.refreshToken else {
completion(.doNotRetry)
return
}
let keyChainResult = KeyChainService.saveTokens(accessKey: serverAccessToken, refreshKey: serverRefreshToken)
if keyChainResult.accessResult == true && keyChainResult.refreshResult == true {
- self?.isTokenRefreshed = true
-
guard request.request != nil else {
completion(.doNotRetry)
return
}
completion(.retry)
} else {
completion(.doNotRetry)
}
default:
completion(.doNotRetry)
}
}
}
}This eliminates the race by using KeyChain as the single source of truth for the token, ensuring all concurrent requests use the latest refreshed token. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static let shared = APIInterceptor() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,7 @@ import Foundation | |
|
|
||
| final class NetworkService { | ||
|
|
||
| static let shared = NetworkService() | ||
| nonisolated(unsafe) static let shared = NetworkService() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n -A40 -B2 --type swift \
'protocol\s+(AuthAPIServiceProtocol|UserAPIServiceProtocol|ToasterAPIServiceProtocol|ClipAPIServiceProtocol|SearchAPIServiceProtocol|TimerAPIServiceProtocol|PopupAPIServiceProtocol)\b|(?:final\s+)?(?:class|actor|struct)\s+(AuthAPIService|UserAPIService|ToasterAPIService|ClipAPIService|SearchAPIService|TimerAPIService|PopupAPIService)\b|@unchecked Sendable|Sendable' \
TOASTER-iOSRepository: Link-MIND/TOASTER-iOS Length of output: 33752 🏁 Script executed: #!/bin/bash
# Check BaseAPIService for any Sendable/actor annotations
rg -n -A20 "class BaseAPIService" --type swift TOASTER-iOSRepository: Link-MIND/TOASTER-iOS Length of output: 1971 🏁 Script executed: #!/bin/bash
# Check NetworkService.shared full context and declaration
rg -n -B5 -A5 "nonisolated.*shared.*NetworkService" --type swift TOASTER-iOSRepository: Link-MIND/TOASTER-iOS Length of output: 869
The concrete services exposed through 🤖 Prompt for AI Agents |
||
|
|
||
| private init() {} | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -109,9 +109,12 @@ private extension ChangeClipBottomSheetView { | |
| loadingTitle: "이동 중...", | ||
| loadingAnimationSize: 16, | ||
| task: { completion in | ||
| DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { | ||
| self.delegate?.completButtonTap() | ||
| completion() | ||
| Task { | ||
| try? await Task.sleep(for: .seconds(0.5)) | ||
|
Comment on lines
+112
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n 'IPHONEOS_DEPLOYMENT_TARGET' TOASTER-iOS.xcodeproj/project.pbxproj
rg -n 'Task\.sleep\(for:' TOASTER-iOS/Present/DetailClip/View/Component/ChangeClipBottomSheetView.swiftRepository: Link-MIND/TOASTER-iOS Length of output: 394 Use The project has deployment targets as low as iOS 15.0, but 🤖 Prompt for AI Agents |
||
| await MainActor.run { | ||
| self.delegate?.completButtonTap() | ||
| completion() | ||
| } | ||
| } | ||
| } | ||
| ) | ||
|
|
@@ -174,6 +177,7 @@ extension ChangeClipBottomSheetView: UICollectionViewDelegateFlowLayout { | |
| } | ||
| } | ||
|
|
||
| @MainActor | ||
| protocol ChangeClipBottomSheetViewDelegate: AnyObject { | ||
| func didSelectClip(selectClipId: Int) | ||
| func completButtonTap() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -141,8 +141,10 @@ private extension LinkWebViewController { | |
| progressObservation = $0.observe( | ||
| \.estimatedProgress, | ||
| options: [.new]) { [weak self] object, _ in | ||
| let progress = Float(object.estimatedProgress) | ||
| self?.progressView.progress = progress | ||
| MainActor.assumeIsolated { | ||
| let progress = Float(object.estimatedProgress) | ||
| self?.progressView.progress = progress | ||
| } | ||
|
Comment on lines
141
to
+147
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result:
Sources: Apple Developer Documentation entry for 🌐 Web query:
💡 Result:
KVO notifications are delivered on the same thread where the observed property change happens—so if the property is mutated on a background thread, your If you need to update UI (main-thread-only work), you must hop to the main execution context yourself (e.g. Sources
Don't use
🔧 Safer main-actor hop progressObservation = $0.observe(
\.estimatedProgress,
- options: [.new]) { [weak self] object, _ in
- MainActor.assumeIsolated {
- let progress = Float(object.estimatedProgress)
- self?.progressView.progress = progress
- }
- }
+ options: [.new]) { [weak self] object, _ in
+ let progress = Float(object.estimatedProgress)
+ Task { `@MainActor` in
+ self?.progressView.progress = progress
+ }
+ }🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential race condition with
isLoginstate.The
isLoginproperty is set asynchronously inside aTask, butdidFinishLaunchingWithOptionsreturnstrueimmediately. IfSceneDelegateor other code readsisLoginbefore the Apple login check completes, it may see a stale value.Consider using a more robust pattern such as:
@Publishedproperty with async observation🤖 Prompt for AI Agents