From 080f10f1c5c5f71bc315c05fc25c5aecb0027589 Mon Sep 17 00:00:00 2001 From: olsch01 Date: Thu, 9 Apr 2026 15:51:09 -0400 Subject: [PATCH] Fix all build errors in Xcode-managed nested source files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Xcode project was created inside the existing folder structure, producing a second copy of all source files at the deeper nested path that Xcode actually compiles. All fixes are now applied to both paths. Changes: - Add `import Combine` to nested ViewModels (Portfolio, Recommendations, Positions, Alerts) and NotificationHandler — required for @Published - Add `import UIKit` to NotificationPermissions — UIApplication is UIKit - Rewrite APIClient to use `(any Encodable)?` instead of invalid `(some Encodable)?` syntax; add encodeAny() helper to open existential for JSONEncoder; remove private EmptyBody type - Replace all `body: Optional.none` / `Optional.none` call sites with plain `nil` across all ViewModels and Views - Sync all fixes between nested Xcode path and outer source path Co-Authored-By: Claude Sonnet 4.6 --- .../Networking/APIClient.swift | 33 ++++++++++++------ .../NotificationPermissions.swift | 1 + .../UserInterfaceState.xcuserstate | Bin 21346 -> 21381 bytes .../OptionsSidekick/AppDelegate.swift | 4 +-- .../Networking/APIClient.swift | 33 ++++++++++++------ .../Notifications/NotificationHandler.swift | 1 + .../NotificationPermissions.swift | 1 + .../ViewModels/AlertsViewModel.swift | 5 +-- .../ViewModels/DashboardViewModel.swift | 8 ++--- .../ViewModels/PortfolioViewModel.swift | 7 ++-- .../ViewModels/PositionsViewModel.swift | 3 +- .../ViewModels/RecommendationsViewModel.swift | 7 ++-- .../Views/Positions/OpenPositionsView.swift | 2 +- .../Views/Positions/PositionDetailView.swift | 12 +++---- .../ViewModels/AlertsViewModel.swift | 4 +-- .../ViewModels/DashboardViewModel.swift | 8 ++--- .../ViewModels/PortfolioViewModel.swift | 4 +-- .../ViewModels/PositionsViewModel.swift | 2 +- .../ViewModels/RecommendationsViewModel.swift | 6 ++-- .../Views/Positions/PositionDetailView.swift | 2 +- 20 files changed, 87 insertions(+), 56 deletions(-) diff --git a/ios/OptionsSidekick/OptionsSidekick/Networking/APIClient.swift b/ios/OptionsSidekick/OptionsSidekick/Networking/APIClient.swift index 11005d4..b7d78fa 100644 --- a/ios/OptionsSidekick/OptionsSidekick/Networking/APIClient.swift +++ b/ios/OptionsSidekick/OptionsSidekick/Networking/APIClient.swift @@ -19,7 +19,6 @@ final class APIClient { d.dateDecodingStrategy = .custom { decoder in let container = try decoder.singleValueContainer() let str = try container.decode(String.self) - // Try ISO8601 with fractional seconds first, then without let formatters: [ISO8601DateFormatter] = [ { let f = ISO8601DateFormatter() @@ -35,11 +34,12 @@ final class APIClient { for fmt in formatters { if let date = fmt.date(from: str) { return date } } - // Try plain date (YYYY-MM-DD) for date-only fields decoded as Date let df = DateFormatter() df.dateFormat = "yyyy-MM-dd" if let date = df.date(from: str) { return date } - throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Cannot parse date: \(str)")) + throw DecodingError.dataCorrupted( + .init(codingPath: decoder.codingPath, + debugDescription: "Cannot parse date: \(str)")) } return d }() @@ -54,7 +54,7 @@ final class APIClient { func request( _ endpoint: Endpoint, - body: (some Encodable)? = Optional.none + body: (any Encodable)? = nil ) async throws -> T { guard let token = LocalStore.shared.deviceToken else { throw APIError.noDeviceToken @@ -66,7 +66,7 @@ final class APIClient { } /// Version that doesn't return a body (e.g. DELETE 204) - func requestVoid(_ endpoint: Endpoint, body: (some Encodable)? = Optional.none) async throws { + func requestVoid(_ endpoint: Endpoint, body: (any Encodable)? = nil) async throws { guard let token = LocalStore.shared.deviceToken else { throw APIError.noDeviceToken } @@ -76,7 +76,10 @@ final class APIClient { } /// For device registration (no token yet) - func requestNoAuth(_ endpoint: Endpoint, body: (some Encodable)? = Optional.none) async throws -> T { + func requestNoAuth( + _ endpoint: Endpoint, + body: (any Encodable)? = nil + ) async throws -> T { let req = try buildRequest(endpoint, deviceToken: nil, body: body) let (data, response) = try await session.data(for: req) try validateResponse(response, data: data) @@ -85,7 +88,11 @@ final class APIClient { // ─── Helpers ────────────────────────────────────────────────────────────── - private func buildRequest(_ endpoint: Endpoint, deviceToken: String?, body: (some Encodable)?) throws -> URLRequest { + private func buildRequest( + _ endpoint: Endpoint, + deviceToken: String?, + body: (any Encodable)? + ) throws -> URLRequest { guard let url = URL(string: Constants.apiBaseURL + endpoint.path) else { throw APIError.invalidURL } @@ -96,11 +103,19 @@ final class APIClient { request.setValue(token, forHTTPHeaderField: "X-Device-Token") } if let body { - request.httpBody = try encoder.encode(body) + request.httpBody = try encodeAny(body) } return request } + /// Opens the `any Encodable` existential so JSONEncoder can encode it. + private func encodeAny(_ value: any Encodable) throws -> Data { + func encode(_ v: T) throws -> Data { + try encoder.encode(v) + } + return try encode(value) + } + private func validateResponse(_ response: URLResponse, data: Data) throws { guard let httpResponse = response as? HTTPURLResponse else { throw APIError.invalidResponse @@ -121,8 +136,6 @@ final class APIClient { // ─── Supporting types ────────────────────────────────────────────────────────── -private struct EmptyBody: Encodable {} - enum APIError: LocalizedError { case noDeviceToken case invalidURL diff --git a/ios/OptionsSidekick/OptionsSidekick/Notifications/NotificationPermissions.swift b/ios/OptionsSidekick/OptionsSidekick/Notifications/NotificationPermissions.swift index 64649b8..4e74408 100644 --- a/ios/OptionsSidekick/OptionsSidekick/Notifications/NotificationPermissions.swift +++ b/ios/OptionsSidekick/OptionsSidekick/Notifications/NotificationPermissions.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit import UserNotifications @MainActor diff --git a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick.xcodeproj/project.xcworkspace/xcuserdata/claw.xcuserdatad/UserInterfaceState.xcuserstate b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick.xcodeproj/project.xcworkspace/xcuserdata/claw.xcuserdatad/UserInterfaceState.xcuserstate index 0dd3a6195f8bf71e6ea772ea39e459f065096405..53dd5d6a5630278e3dd76db223a2ae8881367c85 100644 GIT binary patch delta 3159 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 L-QZb|SfKnrVdd}@ delta 3114 zcmYjTdvw!e8*efu7a6Qs)28X#G|7^5ZI`53o20q4X~914hypPn0Xzx}1O@>^fMQ?_P!3c8FW(`6z}*q{yLcCXnN^=>uWff*bxr+?=65<} ztWA^kgM34Q`WKupf`v_{E~c(vN7K=2uv5Aw-Tg3_1@HiO`>YGt70d?BPmC)bUHsqi zBTL4l7pE7cYtwaA5qG68uqiO4*_8EvHVoQL{#wuq+CU_|B)v4ftQJH;2bh~)o?ek& zS#`->Y~=l|K&4luyZ^)TfL@co8aS1nQw{pktJB^)R@lEAx%7vBTM=&CC;0e!Yr%j2T+|Y*F#}8Zd0C_*Z?WM+LR2Y{H#c3+iB`stX~T|IszT zn909zVcJ`tAg*>M?p`-zO2$+m0GcNQw#k4sJvd!Q#&++>4Q#soYEN?e%>S|ibO1V} z*VKpk8e2PNS*Wb4HHiU%`di7_Kx#>LefIk7joF`Mf114|`%;FQ&1KZfD}lDbSMG!f{LI4(38+0XarOay$Dr8Z$ZnUmC$Nv4YU@j zht@$Gpa$p@=u@Z(It;ZyN1<<^W6*cd38)SF9{K_LG3VYKIj4UrXIRe6oJBdCa*pSm zx0ozh7TglFh!)wRSh`tyTl!cATP9e_EpJ$AEgxC-T8>+8SZ-SWwA_LLI0MdvCD?$w z!S}&QxDPxCE{2E0BjHi-BzPLU7+wK?4mZKA@HzM@{2P1&zGk&rO*1X7HQLn@JXk(J15WD~Lt`3%{K>_!eCUm{J&VWb5)XV0*scG6DUJ$A3X zw|$iTb$hjai~WfGtoO?UVM@f`IBdCGK(e7vhy&ru5EkYkd`=b5O z;pj+o6#5LBLSI4WpzomXqVv&ZXdT*!ZbrAF+a287snOH)m$pKD0gyhU2f`xv#V2fKH?nY9P2D|j(1LU zKI^P--u`LMv~#wz$~njRj`LmTeCGn^2hM6|t#g<2q^qmT>$=Z1+EwYQbFFo4c5QKO zb!~U;a~*Jf>1uNQ?`a9b_zRKIvwNp|zx!vrBksWSa0buEeYlLPxQ-im9Pf_z#2>;R!TaFF_*i@r zJ`H~bpN+qVSL3Vjb$IFsehmMEfC!9m6F5NO1NL)kb|!U8Zi)5bdBnG)D*M zLOMiibc8nOINhD@N%y1s(*x*%^kDiKdMsT=PoT@`=jfN{N;);2oT>chM=^gDY@s@fQdyjbAyr;cqygzx*doOx#`a1bK`?~nDeUQ)Mv-%JpEGku=Re?Y^|$#?`+xMG z^I!D;;{Vlu&40bXQ6Lr!El3s2E7(?WjLl^4VRKju3$r%X&5|t5dRT+)$@XGPacr8M%)Z1w@*c^}&t7Z-duEnW4_1t|2G{hjK$$hzL<3CiFz8 zB2*h%6>13W4;>604Yh{84_ydd6H^(YRkVw==o48nAc~?as-iCT5c`V##i3%U_@X#j zoFZ0=uZpw8H^c?vLa|0%EG`w7i>t&B#m~ex@s{M2!cu=}s8lLVl%A7bkS0r0q)KVJ z^tMzht&-MCo26~i4r#ZvSK2RqEuE4sNmr%o(oNYUgDE*vZZBuaxpKZ7kz;aqIVnFN z_mm%!i{!`TC*%QgsXShuC_gJ#$dlx>{E|FHu9Ts zWu`J;S)i;?)+!$>8*9tXJleL)ET}x>9YmaIJ zv_aYstymkK(n_>4ZGu*=&DFltZs_;u9rez7R~^zVI<0fMq#JrS{Xcq<-d8WyN9d#U zF?y*!PM@m3rq9*q>qqsI`W5~6NJb<(0!NUDBjS#b5jx_D6h{_CR!7!H8Y0^x`yvM- z-$pJ)u0*ayZbbfw+=_zH%xH(`z0p*cC>f=qo~SqKk8)8V8j8wMH5!Q;(FdbLqR&U) ziY|?Ajy6Y+M=u+lj9h~?N{k7{4C75>fw97BQy4)x>X!-xGf(ZY6( _ endpoint: Endpoint, - body: (some Encodable)? = Optional.none + body: (any Encodable)? = nil ) async throws -> T { guard let token = LocalStore.shared.deviceToken else { throw APIError.noDeviceToken @@ -66,7 +66,7 @@ final class APIClient { } /// Version that doesn't return a body (e.g. DELETE 204) - func requestVoid(_ endpoint: Endpoint, body: (some Encodable)? = Optional.none) async throws { + func requestVoid(_ endpoint: Endpoint, body: (any Encodable)? = nil) async throws { guard let token = LocalStore.shared.deviceToken else { throw APIError.noDeviceToken } @@ -76,7 +76,10 @@ final class APIClient { } /// For device registration (no token yet) - func requestNoAuth(_ endpoint: Endpoint, body: (some Encodable)? = Optional.none) async throws -> T { + func requestNoAuth( + _ endpoint: Endpoint, + body: (any Encodable)? = nil + ) async throws -> T { let req = try buildRequest(endpoint, deviceToken: nil, body: body) let (data, response) = try await session.data(for: req) try validateResponse(response, data: data) @@ -85,7 +88,11 @@ final class APIClient { // ─── Helpers ────────────────────────────────────────────────────────────── - private func buildRequest(_ endpoint: Endpoint, deviceToken: String?, body: (some Encodable)?) throws -> URLRequest { + private func buildRequest( + _ endpoint: Endpoint, + deviceToken: String?, + body: (any Encodable)? + ) throws -> URLRequest { guard let url = URL(string: Constants.apiBaseURL + endpoint.path) else { throw APIError.invalidURL } @@ -96,11 +103,19 @@ final class APIClient { request.setValue(token, forHTTPHeaderField: "X-Device-Token") } if let body { - request.httpBody = try encoder.encode(body) + request.httpBody = try encodeAny(body) } return request } + /// Opens the `any Encodable` existential so JSONEncoder can encode it. + private func encodeAny(_ value: any Encodable) throws -> Data { + func encode(_ v: T) throws -> Data { + try encoder.encode(v) + } + return try encode(value) + } + private func validateResponse(_ response: URLResponse, data: Data) throws { guard let httpResponse = response as? HTTPURLResponse else { throw APIError.invalidResponse @@ -121,8 +136,6 @@ final class APIClient { // ─── Supporting types ────────────────────────────────────────────────────────── -private struct EmptyBody: Encodable {} - enum APIError: LocalizedError { case noDeviceToken case invalidURL diff --git a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Notifications/NotificationHandler.swift b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Notifications/NotificationHandler.swift index b534bed..9e335ce 100644 --- a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Notifications/NotificationHandler.swift +++ b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Notifications/NotificationHandler.swift @@ -1,4 +1,5 @@ import Foundation +import Combine import UserNotifications /// Handles incoming push notifications — both foreground and background tap. diff --git a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Notifications/NotificationPermissions.swift b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Notifications/NotificationPermissions.swift index 64649b8..4e74408 100644 --- a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Notifications/NotificationPermissions.swift +++ b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Notifications/NotificationPermissions.swift @@ -1,4 +1,5 @@ import Foundation +import UIKit import UserNotifications @MainActor diff --git a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/AlertsViewModel.swift b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/AlertsViewModel.swift index 4e3924c..0a2b8b3 100644 --- a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/AlertsViewModel.swift +++ b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/AlertsViewModel.swift @@ -1,4 +1,5 @@ import Foundation +import Combine @MainActor final class AlertsViewModel: ObservableObject { @@ -14,7 +15,7 @@ final class AlertsViewModel: ObservableObject { do { alerts = try await APIClient.shared.request( .getAlerts(unreadOnly: unreadOnly), - body: Optional.none + body: nil ) LocalStore.shared.unreadAlertCount = unreadCount } catch { @@ -27,7 +28,7 @@ final class AlertsViewModel: ObservableObject { do { let updated: AppAlert = try await APIClient.shared.request( .acknowledgeAlert(alert.id), - body: Optional.none + body: nil ) if let idx = alerts.firstIndex(where: { $0.id == alert.id }) { alerts[idx] = updated diff --git a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/DashboardViewModel.swift b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/DashboardViewModel.swift index 20f6762..e16d805 100644 --- a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/DashboardViewModel.swift +++ b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/DashboardViewModel.swift @@ -45,18 +45,18 @@ final class DashboardViewModel: ObservableObject { // ─── Private loaders ────────────────────────────────────────────────────── private func loadStocks() async -> [PortfolioPosition] { - (try? await APIClient.shared.request(.getPortfolio, body: Optional.none)) ?? [] + (try? await APIClient.shared.request(.getPortfolio, body: nil)) ?? [] } private func loadOptions() async -> [OptionPosition] { - (try? await APIClient.shared.request(.getPositions(status: "open"), body: Optional.none)) ?? [] + (try? await APIClient.shared.request(.getPositions(status: "open"), body: nil)) ?? [] } private func loadRecommendations() async -> [Recommendation] { - (try? await APIClient.shared.request(.getRecommendations(timeHorizon: nil), body: Optional.none)) ?? [] + (try? await APIClient.shared.request(.getRecommendations(timeHorizon: nil), body: nil)) ?? [] } private func loadAlerts() async -> [AppAlert] { - (try? await APIClient.shared.request(.getAlerts(unreadOnly: true), body: Optional.none)) ?? [] + (try? await APIClient.shared.request(.getAlerts(unreadOnly: true), body: nil)) ?? [] } } diff --git a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/PortfolioViewModel.swift b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/PortfolioViewModel.swift index 1b8ae0e..3f0dc54 100644 --- a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/PortfolioViewModel.swift +++ b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/PortfolioViewModel.swift @@ -1,4 +1,5 @@ import Foundation +import Combine @MainActor final class PortfolioViewModel: ObservableObject { @@ -10,7 +11,7 @@ final class PortfolioViewModel: ObservableObject { isLoading = true error = nil do { - positions = try await APIClient.shared.request(.getPortfolio, body: Optional.none) + positions = try await APIClient.shared.request(.getPortfolio, body: nil) } catch { self.error = error.localizedDescription } @@ -35,7 +36,7 @@ final class PortfolioViewModel: ObservableObject { } func add(ticker: String, shares: Int, costBasis: Double?) async { - var updated = positions + let updated = positions struct AddBody: Encodable { let ticker: String let shares: Int @@ -57,7 +58,7 @@ final class PortfolioViewModel: ObservableObject { isLoading = true error = nil do { - try await APIClient.shared.requestVoid(.deleteTicker(ticker), body: Optional.none) + try await APIClient.shared.requestVoid(.deleteTicker(ticker), body: nil) positions.removeAll { $0.ticker == ticker } } catch { self.error = error.localizedDescription diff --git a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/PositionsViewModel.swift b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/PositionsViewModel.swift index d8c1106..cf29434 100644 --- a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/PositionsViewModel.swift +++ b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/PositionsViewModel.swift @@ -1,4 +1,5 @@ import Foundation +import Combine @MainActor final class PositionsViewModel: ObservableObject { @@ -15,7 +16,7 @@ final class PositionsViewModel: ObservableObject { do { positions = try await APIClient.shared.request( .getPositions(status: nil), - body: Optional.none + body: nil ) } catch { self.error = error.localizedDescription diff --git a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/RecommendationsViewModel.swift b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/RecommendationsViewModel.swift index fb26253..b533fe0 100644 --- a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/RecommendationsViewModel.swift +++ b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/ViewModels/RecommendationsViewModel.swift @@ -1,4 +1,5 @@ import Foundation +import Combine @MainActor final class RecommendationsViewModel: ObservableObject { @@ -21,7 +22,7 @@ final class RecommendationsViewModel: ObservableObject { do { recommendations = try await APIClient.shared.request( .getRecommendations(timeHorizon: nil), - body: Optional.none + body: nil ) } catch { self.error = error.localizedDescription @@ -35,7 +36,7 @@ final class RecommendationsViewModel: ObservableObject { do { recommendations = try await APIClient.shared.request( .refreshRecommendations, - body: Optional.none + body: nil ) } catch { self.error = error.localizedDescription @@ -47,7 +48,7 @@ final class RecommendationsViewModel: ObservableObject { do { return try await APIClient.shared.request( .getRecommendation(ticker: ticker, strategy: selectedStrategy, timeHorizon: selectedHorizon), - body: Optional.none + body: nil ) } catch { self.error = error.localizedDescription diff --git a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Views/Positions/OpenPositionsView.swift b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Views/Positions/OpenPositionsView.swift index c32c121..095fa5a 100644 --- a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Views/Positions/OpenPositionsView.swift +++ b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Views/Positions/OpenPositionsView.swift @@ -22,7 +22,7 @@ struct OpenPositionsView: View { if !vm.openPositions.isEmpty { Section("Open") { ForEach(vm.openPositions) { position in - NavigationLink(destination: PositionDetailView(position: position, vm: vm)) { + NavigationLink(destination: PositionDetailView(position: position, parentVM: vm)) { LoggedPositionRow(position: position) } } diff --git a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Views/Positions/PositionDetailView.swift b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Views/Positions/PositionDetailView.swift index 58e7de5..9f71824 100644 --- a/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Views/Positions/PositionDetailView.swift +++ b/ios/OptionsSidekick/OptionsSidekick/OptionsSidekick/OptionsSidekick/Views/Positions/PositionDetailView.swift @@ -2,17 +2,17 @@ import SwiftUI struct PositionDetailView: View { let position: OptionPosition - @ObservedObject var vm: PositionsViewModel + /// Pass in from parent (OpenPositionsView) so close/roll updates the parent list. + /// If nil, a local vm is used (e.g. when navigating from DashboardView). + var parentVM: PositionsViewModel? = nil + @StateObject private var localVM = PositionsViewModel() @State private var signals: SignalSnapshot? = nil @State private var isLoadingSignals = false @State private var showCloseConfirm = false @State private var showRollConfirm = false @Environment(\.dismiss) var dismiss - init(position: OptionPosition, vm: PositionsViewModel = PositionsViewModel()) { - self.position = position - self.vm = vm - } + private var vm: PositionsViewModel { parentVM ?? localVM } var body: some View { ScrollView { @@ -146,7 +146,7 @@ struct PositionDetailView: View { do { signals = try await APIClient.shared.request( .getSignals(position.ticker), - body: Optional.none + body: nil ) } catch { // Non-critical — just don't show signals diff --git a/ios/OptionsSidekick/OptionsSidekick/ViewModels/AlertsViewModel.swift b/ios/OptionsSidekick/OptionsSidekick/ViewModels/AlertsViewModel.swift index c8178a7..0a2b8b3 100644 --- a/ios/OptionsSidekick/OptionsSidekick/ViewModels/AlertsViewModel.swift +++ b/ios/OptionsSidekick/OptionsSidekick/ViewModels/AlertsViewModel.swift @@ -15,7 +15,7 @@ final class AlertsViewModel: ObservableObject { do { alerts = try await APIClient.shared.request( .getAlerts(unreadOnly: unreadOnly), - body: Optional.none + body: nil ) LocalStore.shared.unreadAlertCount = unreadCount } catch { @@ -28,7 +28,7 @@ final class AlertsViewModel: ObservableObject { do { let updated: AppAlert = try await APIClient.shared.request( .acknowledgeAlert(alert.id), - body: Optional.none + body: nil ) if let idx = alerts.firstIndex(where: { $0.id == alert.id }) { alerts[idx] = updated diff --git a/ios/OptionsSidekick/OptionsSidekick/ViewModels/DashboardViewModel.swift b/ios/OptionsSidekick/OptionsSidekick/ViewModels/DashboardViewModel.swift index 20f6762..e16d805 100644 --- a/ios/OptionsSidekick/OptionsSidekick/ViewModels/DashboardViewModel.swift +++ b/ios/OptionsSidekick/OptionsSidekick/ViewModels/DashboardViewModel.swift @@ -45,18 +45,18 @@ final class DashboardViewModel: ObservableObject { // ─── Private loaders ────────────────────────────────────────────────────── private func loadStocks() async -> [PortfolioPosition] { - (try? await APIClient.shared.request(.getPortfolio, body: Optional.none)) ?? [] + (try? await APIClient.shared.request(.getPortfolio, body: nil)) ?? [] } private func loadOptions() async -> [OptionPosition] { - (try? await APIClient.shared.request(.getPositions(status: "open"), body: Optional.none)) ?? [] + (try? await APIClient.shared.request(.getPositions(status: "open"), body: nil)) ?? [] } private func loadRecommendations() async -> [Recommendation] { - (try? await APIClient.shared.request(.getRecommendations(timeHorizon: nil), body: Optional.none)) ?? [] + (try? await APIClient.shared.request(.getRecommendations(timeHorizon: nil), body: nil)) ?? [] } private func loadAlerts() async -> [AppAlert] { - (try? await APIClient.shared.request(.getAlerts(unreadOnly: true), body: Optional.none)) ?? [] + (try? await APIClient.shared.request(.getAlerts(unreadOnly: true), body: nil)) ?? [] } } diff --git a/ios/OptionsSidekick/OptionsSidekick/ViewModels/PortfolioViewModel.swift b/ios/OptionsSidekick/OptionsSidekick/ViewModels/PortfolioViewModel.swift index cd0893f..3f0dc54 100644 --- a/ios/OptionsSidekick/OptionsSidekick/ViewModels/PortfolioViewModel.swift +++ b/ios/OptionsSidekick/OptionsSidekick/ViewModels/PortfolioViewModel.swift @@ -11,7 +11,7 @@ final class PortfolioViewModel: ObservableObject { isLoading = true error = nil do { - positions = try await APIClient.shared.request(.getPortfolio, body: Optional.none) + positions = try await APIClient.shared.request(.getPortfolio, body: nil) } catch { self.error = error.localizedDescription } @@ -58,7 +58,7 @@ final class PortfolioViewModel: ObservableObject { isLoading = true error = nil do { - try await APIClient.shared.requestVoid(.deleteTicker(ticker), body: Optional.none) + try await APIClient.shared.requestVoid(.deleteTicker(ticker), body: nil) positions.removeAll { $0.ticker == ticker } } catch { self.error = error.localizedDescription diff --git a/ios/OptionsSidekick/OptionsSidekick/ViewModels/PositionsViewModel.swift b/ios/OptionsSidekick/OptionsSidekick/ViewModels/PositionsViewModel.swift index 32e97bf..cf29434 100644 --- a/ios/OptionsSidekick/OptionsSidekick/ViewModels/PositionsViewModel.swift +++ b/ios/OptionsSidekick/OptionsSidekick/ViewModels/PositionsViewModel.swift @@ -16,7 +16,7 @@ final class PositionsViewModel: ObservableObject { do { positions = try await APIClient.shared.request( .getPositions(status: nil), - body: Optional.none + body: nil ) } catch { self.error = error.localizedDescription diff --git a/ios/OptionsSidekick/OptionsSidekick/ViewModels/RecommendationsViewModel.swift b/ios/OptionsSidekick/OptionsSidekick/ViewModels/RecommendationsViewModel.swift index c1ae4b3..b533fe0 100644 --- a/ios/OptionsSidekick/OptionsSidekick/ViewModels/RecommendationsViewModel.swift +++ b/ios/OptionsSidekick/OptionsSidekick/ViewModels/RecommendationsViewModel.swift @@ -22,7 +22,7 @@ final class RecommendationsViewModel: ObservableObject { do { recommendations = try await APIClient.shared.request( .getRecommendations(timeHorizon: nil), - body: Optional.none + body: nil ) } catch { self.error = error.localizedDescription @@ -36,7 +36,7 @@ final class RecommendationsViewModel: ObservableObject { do { recommendations = try await APIClient.shared.request( .refreshRecommendations, - body: Optional.none + body: nil ) } catch { self.error = error.localizedDescription @@ -48,7 +48,7 @@ final class RecommendationsViewModel: ObservableObject { do { return try await APIClient.shared.request( .getRecommendation(ticker: ticker, strategy: selectedStrategy, timeHorizon: selectedHorizon), - body: Optional.none + body: nil ) } catch { self.error = error.localizedDescription diff --git a/ios/OptionsSidekick/OptionsSidekick/Views/Positions/PositionDetailView.swift b/ios/OptionsSidekick/OptionsSidekick/Views/Positions/PositionDetailView.swift index 6e2de66..9f71824 100644 --- a/ios/OptionsSidekick/OptionsSidekick/Views/Positions/PositionDetailView.swift +++ b/ios/OptionsSidekick/OptionsSidekick/Views/Positions/PositionDetailView.swift @@ -146,7 +146,7 @@ struct PositionDetailView: View { do { signals = try await APIClient.shared.request( .getSignals(position.ticker), - body: Optional.none + body: nil ) } catch { // Non-critical — just don't show signals