From 60794f23f3f7e05b6afb0152edf9cd3431c0432a Mon Sep 17 00:00:00 2001 From: Jason Garber Date: Sun, 14 Jun 2026 22:00:20 -0400 Subject: [PATCH 1/4] Animate player's draw and discard --- .../Utilities/Managers/NetworkManager.swift | 34 +-- Level10/Views/Screens/GameView.swift | 205 +++++++++++++++++- 2 files changed, 221 insertions(+), 18 deletions(-) diff --git a/Level10/Utilities/Managers/NetworkManager.swift b/Level10/Utilities/Managers/NetworkManager.swift index 241278f..1b7928f 100644 --- a/Level10/Utilities/Managers/NetworkManager.swift +++ b/Level10/Utilities/Managers/NetworkManager.swift @@ -211,12 +211,16 @@ final class NetworkManager { let params = DiscardParams(card: card, playerToSkip: playerToSkip) - channel.push(.discard, payload: params.toDict(), with: decoder) { (result: Result) in - switch result { - case .success(let payload): - NotificationCenter.default.post(name: .handDidUpdate, object: nil, userInfo: ["hand": payload.hand]) - case .failure(let error): - NotificationCenter.default.post(name: .didReceiveDiscardError, object: nil, userInfo: ["error": error]) + try await withCheckedThrowingContinuation { continuation in + channel.push(.discard, payload: params.toDict(), with: decoder) { (result: Result) in + switch result { + case .success(let payload): + NotificationCenter.default.post(name: .handDidUpdate, object: nil, userInfo: ["hand": payload.hand]) + continuation.resume() + case .failure(let error): + NotificationCenter.default.post(name: .didReceiveDiscardError, object: nil, userInfo: ["error": error]) + continuation.resume(throwing: error) + } } } } @@ -226,17 +230,21 @@ final class NetworkManager { - Throws: `NetworkError.socketNotConnected` if the socket wasn't properly created for some reason. */ - func drawCard(source: DrawSource) async throws { + func drawCard(source: DrawSource) async throws -> Card { guard let socket = socket, let channel = gameChannel else { throw NetworkError.socketNotConnected } if !socket.isConnected { try await connectSocket() } let params = DrawCardParams(source: source) - channel.push(.drawCard, payload: params.toDict(), with: decoder) { (result: Result) in - switch result { - case .success(let payload): - NotificationCenter.default.post(name: .didDrawCard, object: nil, userInfo: ["newCard": payload.card]) - case .failure(let error): - NotificationCenter.default.post(name: .didReceiveCardDrawError, object: nil, userInfo: ["error": error]) + return try await withCheckedThrowingContinuation { continuation in + channel.push(.drawCard, payload: params.toDict(), with: decoder) { (result: Result) in + switch result { + case .success(let payload): + NotificationCenter.default.post(name: .didDrawCard, object: nil, userInfo: ["newCard": payload.card]) + continuation.resume(returning: payload.card) + case .failure(let error): + NotificationCenter.default.post(name: .didReceiveCardDrawError, object: nil, userInfo: ["error": error]) + continuation.resume(throwing: error) + } } } } diff --git a/Level10/Views/Screens/GameView.swift b/Level10/Views/Screens/GameView.swift index f4bfd31..8f09603 100644 --- a/Level10/Views/Screens/GameView.swift +++ b/Level10/Views/Screens/GameView.swift @@ -7,9 +7,69 @@ import SwiftUI +private let gameCoordinateSpaceName = "gameCoordinateSpace" + +private struct FlyingCard: Identifiable { + let id = UUID() + let card: Card + let sourceCenter: CGPoint + let targetCenter: CGPoint + let sourceScale: CGFloat + let targetScale: CGFloat +} + +private struct DrawPileFramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect? + + static func reduce(value: inout CGRect?, nextValue: () -> CGRect?) { + value = nextValue() ?? value + } +} + +private struct HandCardFramePreferenceKey: PreferenceKey { + static var defaultValue: [Int: CGRect] = [:] + + static func reduce(value: inout [Int: CGRect], nextValue: () -> [Int: CGRect]) { + value.merge(nextValue(), uniquingKeysWith: { _, new in new }) + } +} + +private struct NewCardFramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect? + + static func reduce(value: inout CGRect?, nextValue: () -> CGRect?) { + value = nextValue() ?? value + } +} + +private struct DiscardPileFramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect? + + static func reduce(value: inout CGRect?, nextValue: () -> CGRect?) { + value = nextValue() ?? value + } +} + +private struct HandAreaFramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect? + + static func reduce(value: inout CGRect?, nextValue: () -> CGRect?) { + value = nextValue() ?? value + } +} + struct GameView: View { @EnvironmentObject var viewModel: GameViewModel @State var displayScores = false + @State private var drawPileFrame: CGRect? + @State private var selectedCardFrames: [Int: CGRect] = [:] + @State private var newCardFrame: CGRect? + @State private var discardPileFrame: CGRect? + @State private var freezeDiscardPileTopCard = false + @State private var frozenDiscardPileTopCard: Card? + @State private var handAreaFrame: CGRect? + @State private var flyingCard: FlyingCard? + @State private var flyingCardAtTarget = false private var smallSize = DeviceTypes.ScreenSize.maxLength < 800 @@ -71,13 +131,19 @@ struct GameView: View { } label: { CardBackView() } + .background { + GeometryReader { geometry in + Color.clear.preference(key: DrawPileFramePreferenceKey.self, + value: geometry.frame(in: .named(gameCoordinateSpaceName))) + } + } Spacer() Button { onTapDiscardPile() } label: { - if let discardTop = viewModel.discardPileTopCard { + if let discardTop = displayedDiscardPileTopCard { CardView(card: discardTop) } else { ZStack { @@ -92,6 +158,12 @@ struct GameView: View { .frame(width: 80, height: 112) } } + .background { + GeometryReader { geometry in + Color.clear.preference(key: DiscardPileFramePreferenceKey.self, + value: geometry.frame(in: .named(gameCoordinateSpaceName))) + } + } Spacer() } @@ -120,6 +192,12 @@ struct GameView: View { .overlay { viewModel.newCardSelected ? RoundedRectangle(cornerRadius: 13).fill(.white).opacity(0.5) : nil } .scaleEffect(0.65) .frame(width: 54, height: 74) + .background { + GeometryReader { geometry in + Color.clear.preference(key: NewCardFramePreferenceKey.self, + value: geometry.frame(in: .named(gameCoordinateSpaceName))) + } + } } } @@ -135,11 +213,23 @@ struct GameView: View { .overlay { viewModel.selectedIndices.contains(i) ? RoundedRectangle(cornerRadius: 13).fill(.white).opacity(0.5) : nil } .scaleEffect(0.65) .frame(width: 54, height: 74) + .background { + GeometryReader { geometry in + Color.clear.preference(key: HandCardFramePreferenceKey.self, + value: [i: geometry.frame(in: .named(gameCoordinateSpaceName))]) + } + } } } } .frame(width: 296) } + .background { + GeometryReader { geometry in + Color.clear.preference(key: HandAreaFramePreferenceKey.self, + value: geometry.frame(in: .named(gameCoordinateSpaceName))) + } + } .padding(.horizontal) .padding(.bottom) } @@ -228,6 +318,13 @@ struct GameView: View { } } } + .coordinateSpace(name: gameCoordinateSpaceName) + .onPreferenceChange(DrawPileFramePreferenceKey.self) { drawPileFrame = $0 } + .onPreferenceChange(HandCardFramePreferenceKey.self) { selectedCardFrames = $0 } + .onPreferenceChange(NewCardFramePreferenceKey.self) { newCardFrame = $0 } + .onPreferenceChange(DiscardPileFramePreferenceKey.self) { discardPileFrame = $0 } + .onPreferenceChange(HandAreaFramePreferenceKey.self) { handAreaFrame = $0 } + .overlay { flyingCardOverlay() } .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local).onEnded({ value in if value.translation.width > 50 { withAnimation { @@ -243,6 +340,22 @@ struct GameView: View { })) } + private func flyingCardOverlay() -> some View { + ZStack { + if let flyingCard = flyingCard { + CardView(card: flyingCard.card) + .scaleEffect(flyingCardAtTarget ? flyingCard.targetScale : flyingCard.sourceScale) + .position(flyingCardAtTarget ? flyingCard.targetCenter : flyingCard.sourceCenter) + .allowsHitTesting(false) + .zIndex(2) + } + } + } + + private var displayedDiscardPileTopCard: Card? { + freezeDiscardPileTopCard ? frozenDiscardPileTopCard : viewModel.discardPileTopCard + } + private func playerTable(_ playerId: String) -> some View { HStack(spacing: 6) { let levelGroups = viewModel.levelGroups(player: playerId) @@ -389,9 +502,24 @@ struct GameView: View { Task { do { - try await NetworkManager.shared.drawCard(source: source) + let sourceFrame = drawSourceFrame(source) + let drawnCard = try await NetworkManager.shared.drawCard(source: source) + + await MainActor.run { + let targetFrame = newCardFrame ?? handAreaFrame + if let sourceFrame = sourceFrame, let targetFrame = targetFrame { + startCardAnimation(card: drawnCard, + sourceFrame: sourceFrame, + targetFrame: targetFrame, + sourceScale: 1.0, + targetScale: 0.65) + } + } } catch { - withAnimation { viewModel.error = "An unexpected error occurred. Please try force quitting the app and then try again 🤯" } + if error is CardDrawError { return } + await MainActor.run { + withAnimation { viewModel.error = "An unexpected error occurred. Please try force quitting the app and then try again 🤯" } + } } } @@ -438,9 +566,34 @@ struct GameView: View { Task { do { - try await NetworkManager.shared.discardCard(card: viewModel.selectedCards.first!, playerToSkip: nil) + let discardedCard = viewModel.selectedCards.first! + let sourceFrame = selectedDiscardSourceFrame() + let targetFrame = discardPileFrame + let discardPileTopCardBeforeDiscard = viewModel.discardPileTopCard + + try await NetworkManager.shared.discardCard(card: discardedCard, playerToSkip: nil) + + await MainActor.run { + if let sourceFrame = sourceFrame, let targetFrame = targetFrame { + freezeDiscardPileTopCard = true + frozenDiscardPileTopCard = discardPileTopCardBeforeDiscard + startCardAnimation(card: discardedCard, + sourceFrame: sourceFrame, + targetFrame: targetFrame, + sourceScale: 0.65, + targetScale: 1.0) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { + freezeDiscardPileTopCard = false + frozenDiscardPileTopCard = nil + } + } + } } catch { - withAnimation { viewModel.error = "An unexpected error occurred. Please try force quitting the app and then try again 🤯" } + if error is DiscardError { return } + await MainActor.run { + withAnimation { viewModel.error = "An unexpected error occurred. Please try force quitting the app and then try again 🤯" } + } } } } @@ -473,6 +626,48 @@ struct GameView: View { guard viewModel.selectedIndices.count > 0 || viewModel.newCardSelected else { return } viewModel.addToTable(groupIndex) } + + private func drawSourceFrame(_ source: DrawSource) -> CGRect? { + switch source { + case .drawPile: + return drawPileFrame + case .discardPile: + return discardPileFrame + } + } + + private func selectedDiscardSourceFrame() -> CGRect? { + if viewModel.newCardSelected { + return newCardFrame + } + + guard let selectedIndex = viewModel.selectedIndices.first else { return nil } + return selectedCardFrames[selectedIndex] + } + + private func startCardAnimation(card: Card, + sourceFrame: CGRect, + targetFrame: CGRect, + sourceScale: CGFloat, + targetScale: CGFloat) { + flyingCardAtTarget = false + flyingCard = FlyingCard(card: card, + sourceCenter: CGPoint(x: sourceFrame.midX, y: sourceFrame.midY), + targetCenter: CGPoint(x: targetFrame.midX, y: targetFrame.midY), + sourceScale: sourceScale, + targetScale: targetScale) + + DispatchQueue.main.async { + withAnimation(.easeInOut(duration: 0.35)) { + flyingCardAtTarget = true + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { + flyingCard = nil + flyingCardAtTarget = false + } + } } struct GameView_Previews: PreviewProvider { From 52f07b5b0f60dde64868ee0f1efef8f7f4f7e1d8 Mon Sep 17 00:00:00 2001 From: Jason Garber Date: Sun, 14 Jun 2026 21:52:54 -0400 Subject: [PATCH 2/4] Animate other players' draw/discard --- Level10/Views/Screens/GameView.swift | 199 ++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 7 deletions(-) diff --git a/Level10/Views/Screens/GameView.swift b/Level10/Views/Screens/GameView.swift index 8f09603..77f7939 100644 --- a/Level10/Views/Screens/GameView.swift +++ b/Level10/Views/Screens/GameView.swift @@ -9,15 +9,25 @@ import SwiftUI private let gameCoordinateSpaceName = "gameCoordinateSpace" +private enum FlyingCardFace { + case front(Card) + case back +} + private struct FlyingCard: Identifiable { let id = UUID() - let card: Card + let face: FlyingCardFace let sourceCenter: CGPoint let targetCenter: CGPoint let sourceScale: CGFloat let targetScale: CGFloat } +private struct RecentDiscardPileDrawSource { + let id: UUID + let card: Card +} + private struct DrawPileFramePreferenceKey: PreferenceKey { static var defaultValue: CGRect? @@ -34,6 +44,14 @@ private struct HandCardFramePreferenceKey: PreferenceKey { } } +private struct PlayerNameFramePreferenceKey: PreferenceKey { + static var defaultValue: [String: CGRect] = [:] + + static func reduce(value: inout [String: CGRect], nextValue: () -> [String: CGRect]) { + value.merge(nextValue(), uniquingKeysWith: { _, new in new }) + } +} + private struct NewCardFramePreferenceKey: PreferenceKey { static var defaultValue: CGRect? @@ -63,6 +81,7 @@ struct GameView: View { @State var displayScores = false @State private var drawPileFrame: CGRect? @State private var selectedCardFrames: [Int: CGRect] = [:] + @State private var playerNameFrames: [String: CGRect] = [:] @State private var newCardFrame: CGRect? @State private var discardPileFrame: CGRect? @State private var freezeDiscardPileTopCard = false @@ -70,6 +89,11 @@ struct GameView: View { @State private var handAreaFrame: CGRect? @State private var flyingCard: FlyingCard? @State private var flyingCardAtTarget = false + @State private var lastHandCounts: [String: Int] = [:] + @State private var previousDiscardPileTopCard: Card? + @State private var recentDiscardPileDrawSource: RecentDiscardPileDrawSource? + @State private var pendingRemoteDraws: [String: UUID] = [:] + @State private var pendingRemoteDiscards: [String: UUID] = [:] private var smallSize = DeviceTypes.ScreenSize.maxLength < 800 @@ -103,6 +127,12 @@ struct GameView: View { .lineLimit(1) .frame(maxWidth: 100, alignment: .leading) .opacity(player.id == viewModel.currentPlayer ? 1.0 : 0.6) + .background { + GeometryReader { geometry in + Color.clear.preference(key: PlayerNameFramePreferenceKey.self, + value: [player.id: geometry.frame(in: .named(gameCoordinateSpaceName))]) + } + } Spacer() @@ -321,9 +351,20 @@ struct GameView: View { .coordinateSpace(name: gameCoordinateSpaceName) .onPreferenceChange(DrawPileFramePreferenceKey.self) { drawPileFrame = $0 } .onPreferenceChange(HandCardFramePreferenceKey.self) { selectedCardFrames = $0 } + .onPreferenceChange(PlayerNameFramePreferenceKey.self) { playerNameFrames = $0 } .onPreferenceChange(NewCardFramePreferenceKey.self) { newCardFrame = $0 } .onPreferenceChange(DiscardPileFramePreferenceKey.self) { discardPileFrame = $0 } .onPreferenceChange(HandAreaFramePreferenceKey.self) { handAreaFrame = $0 } + .onAppear { + lastHandCounts = viewModel.handCounts + previousDiscardPileTopCard = viewModel.discardPileTopCard + } + .onChange(of: viewModel.handCounts) { handCounts in + handleHandCountChange(handCounts) + } + .onChange(of: viewModel.discardPileTopCard) { discardPileTopCard in + handleDiscardPileTopChange(discardPileTopCard) + } .overlay { flyingCardOverlay() } .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local).onEnded({ value in if value.translation.width > 50 { @@ -343,11 +384,18 @@ struct GameView: View { private func flyingCardOverlay() -> some View { ZStack { if let flyingCard = flyingCard { - CardView(card: flyingCard.card) - .scaleEffect(flyingCardAtTarget ? flyingCard.targetScale : flyingCard.sourceScale) - .position(flyingCardAtTarget ? flyingCard.targetCenter : flyingCard.sourceCenter) - .allowsHitTesting(false) - .zIndex(2) + Group { + switch flyingCard.face { + case .front(let card): + CardView(card: card) + case .back: + CardBackView() + } + } + .scaleEffect(flyingCardAtTarget ? flyingCard.targetScale : flyingCard.sourceScale) + .position(flyingCardAtTarget ? flyingCard.targetCenter : flyingCard.sourceCenter) + .allowsHitTesting(false) + .zIndex(2) } } } @@ -627,6 +675,131 @@ struct GameView: View { viewModel.addToTable(groupIndex) } + private func handleHandCountChange(_ handCounts: [String: Int]) { + defer { lastHandCounts = handCounts } + + guard !lastHandCounts.isEmpty else { return } + + for (playerId, count) in handCounts { + guard playerId != UserManager.shared.id, + let previousCount = lastHandCounts[playerId] + else { continue } + + if count > previousCount { + scheduleRemoteDrawAnimation(playerId: playerId) + } else if count < previousCount { + scheduleRemoteDiscardAnimation(playerId: playerId) + } + } + } + + private func handleDiscardPileTopChange(_ discardPileTopCard: Card?) { + let oldDiscardPileTopCard = previousDiscardPileTopCard + defer { previousDiscardPileTopCard = discardPileTopCard } + + if let playerId = pendingRemoteDraws.keys.first { + pendingRemoteDraws.removeValue(forKey: playerId) + animateRemoteDraw(playerId: playerId, sourceFrame: discardPileFrame, face: remoteDrawDiscardFace(oldDiscardPileTopCard)) + return + } + + if let playerId = pendingRemoteDiscards.keys.first { + pendingRemoteDiscards.removeValue(forKey: playerId) + animateRemoteDiscard(playerId: playerId, + face: discardPileTopCard.map { .front($0) } ?? .back, + previousDiscardPileTopCard: oldDiscardPileTopCard) + return + } + + if let oldDiscardPileTopCard = oldDiscardPileTopCard { + rememberRecentDiscardPileDrawSource(card: oldDiscardPileTopCard) + } + } + + private func scheduleRemoteDrawAnimation(playerId: String) { + if let recentDiscardPileDrawSource = recentDiscardPileDrawSource { + self.recentDiscardPileDrawSource = nil + animateRemoteDraw(playerId: playerId, + sourceFrame: discardPileFrame, + face: .front(recentDiscardPileDrawSource.card)) + return + } + + let animationId = UUID() + pendingRemoteDraws[playerId] = animationId + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) { + guard pendingRemoteDraws[playerId] == animationId else { return } + pendingRemoteDraws.removeValue(forKey: playerId) + animateRemoteDraw(playerId: playerId, sourceFrame: drawPileFrame, face: .back) + } + } + + private func scheduleRemoteDiscardAnimation(playerId: String) { + let animationId = UUID() + pendingRemoteDiscards[playerId] = animationId + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) { + guard pendingRemoteDiscards[playerId] == animationId else { return } + pendingRemoteDiscards.removeValue(forKey: playerId) + animateRemoteDiscard(playerId: playerId, + face: .back, + previousDiscardPileTopCard: previousDiscardPileTopCard) + } + } + + private func rememberRecentDiscardPileDrawSource(card: Card) { + let candidate = RecentDiscardPileDrawSource(id: UUID(), card: card) + recentDiscardPileDrawSource = candidate + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { + guard recentDiscardPileDrawSource?.id == candidate.id else { return } + recentDiscardPileDrawSource = nil + } + } + + private func animateRemoteDraw(playerId: String, sourceFrame: CGRect?, face: FlyingCardFace) { + guard let sourceFrame = sourceFrame, + let targetFrame = playerNameFrames[playerId] + else { return } + + startCardAnimation(face: face, + sourceFrame: sourceFrame, + targetFrame: targetFrame, + sourceScale: 1.0, + targetScale: 0.25) + } + + private func animateRemoteDiscard(playerId: String, + face: FlyingCardFace, + previousDiscardPileTopCard: Card?) { + guard let sourceFrame = playerNameFrames[playerId], + let targetFrame = discardPileFrame + else { return } + + freezeDiscardPileTopCard = true + frozenDiscardPileTopCard = previousDiscardPileTopCard + + startCardAnimation(face: face, + sourceFrame: sourceFrame, + targetFrame: targetFrame, + sourceScale: 0.25, + targetScale: 1.0) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.45) { + freezeDiscardPileTopCard = false + frozenDiscardPileTopCard = nil + } + } + + private func remoteDrawDiscardFace(_ oldDiscardPileTopCard: Card?) -> FlyingCardFace { + if let oldDiscardPileTopCard = oldDiscardPileTopCard { + return .front(oldDiscardPileTopCard) + } else { + return .back + } + } + private func drawSourceFrame(_ source: DrawSource) -> CGRect? { switch source { case .drawPile: @@ -650,8 +823,20 @@ struct GameView: View { targetFrame: CGRect, sourceScale: CGFloat, targetScale: CGFloat) { + startCardAnimation(face: .front(card), + sourceFrame: sourceFrame, + targetFrame: targetFrame, + sourceScale: sourceScale, + targetScale: targetScale) + } + + private func startCardAnimation(face: FlyingCardFace, + sourceFrame: CGRect, + targetFrame: CGRect, + sourceScale: CGFloat, + targetScale: CGFloat) { flyingCardAtTarget = false - flyingCard = FlyingCard(card: card, + flyingCard = FlyingCard(face: face, sourceCenter: CGPoint(x: sourceFrame.midX, y: sourceFrame.midY), targetCenter: CGPoint(x: targetFrame.midX, y: targetFrame.midY), sourceScale: sourceScale, From fec81e1556c702c9105b4d7dc3de96dd4f230487 Mon Sep 17 00:00:00 2001 From: Jason Garber Date: Sun, 14 Jun 2026 22:27:58 -0400 Subject: [PATCH 3/4] Use game player list at round start --- Level10/GameViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Level10/GameViewModel.swift b/Level10/GameViewModel.swift index 5444c3a..6c0943a 100644 --- a/Level10/GameViewModel.swift +++ b/Level10/GameViewModel.swift @@ -477,7 +477,7 @@ class GameViewModel: ObservableObject { HapticManager.playWarning() SoundManager.shared.playNotify() let handCounts = Dictionary(uniqueKeysWithValues: game.players.map { ($0.id, 10) }) - let remainingPlayers = Set(players.map { $0.id }) + let remainingPlayers = Set(game.players.map { $0.id }) DispatchQueue.main.async { [self] in completedLevel = false From f961dd7f59ae853f42c6ab13bbb6d9bd7aa9e893 Mon Sep 17 00:00:00 2001 From: Jason Garber Date: Sun, 14 Jun 2026 22:28:17 -0400 Subject: [PATCH 4/4] Clear user id with missing token --- Level10/Utilities/Managers/UserManager.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Level10/Utilities/Managers/UserManager.swift b/Level10/Utilities/Managers/UserManager.swift index 18e02a4..75ab91b 100644 --- a/Level10/Utilities/Managers/UserManager.swift +++ b/Level10/Utilities/Managers/UserManager.swift @@ -37,6 +37,8 @@ final class UserManager { } else { let _ = removeToken() } + } else { + removeId() } } @@ -155,14 +157,21 @@ final class UserManager { let status = SecItemDelete(removeQuery) - if status == errSecSuccess { + if status == errSecSuccess || status == errSecItemNotFound { self.token = nil + removeId() return true } else { return false } } + private func removeId() { + self.id = nil + var configuration = Configuration() + UserDefaults.standard.removeObject(forKey: configuration.environment.userIdKey) + } + private func setId(_ id: String) { self.id = id var configuration = Configuration()