From 5a05302ed9b8807600ccec600e3d5fefd17f8b02 Mon Sep 17 00:00:00 2001 From: Petr Sima Date: Thu, 25 Jan 2024 13:36:36 +0100 Subject: [PATCH 1/4] refactor done but need to uncomment stuff --- .DS_Store | Bin 8196 -> 8196 bytes Ruddarr/Dependencies/Dependencies.swift | 2 +- Ruddarr/Dependencies/Router.swift | 20 ++++----- Ruddarr/Views/ContentView.swift | 55 +++++++++++++++++++----- Ruddarr/Views/InstanceView.swift | 8 ++-- Ruddarr/Views/MovieSearchView.swift | 4 +- Ruddarr/Views/MovieView.swift | 8 ++-- Ruddarr/Views/MoviesView.swift | 18 +++++--- Ruddarr/Views/SettingsView.swift | 17 +++++--- Ruddarr/Views/ShowsView.swift | 5 +-- 10 files changed, 92 insertions(+), 45 deletions(-) diff --git a/.DS_Store b/.DS_Store index 63a5f92c4382cb1f9e86e70c5dd85190afae95a7..b12aa375c344126d147e9ae79eb16e51ca18aa55 100644 GIT binary patch delta 114 zcmZp1XmQx^Ta4+L#Net4!v^P?GBFEG PEKJ(key _: KeyPath = \Key.self) -> Key.Value { + get { self[Key.self] } + set { self[Key.self] = newValue } + } +} +extension Environment where Value: EnvironmentKey, Value.Value == Value { + init() { + self.init(\.[key: \Value.self]) + } +} + +protocol EmptyInitilizable { + init() +} +protocol DefaultKey: EnvironmentKey, EmptyInitilizable { +} + +extension DefaultKey where Value == Self { + static var defaultValue: Value { .init() } +} + +@Observable final class TabRouter: EnvironmentKey { + static var defaultValue: TabRouter = .init() + + var selectedTab: Tab = .movies +} + + struct ContentView: View { + + @Environment() var tabRouter: TabRouter + @Environment() var moviesRouter: MoviesView.Router + @State private var columnVisibility: NavigationSplitViewVisibility = .detailOnly var body: some View { + @Bindable var tabRouter = tabRouter if UIDevice.current.userInterfaceIdiom == .pad { NavigationSplitView(columnVisibility: $columnVisibility) { - List(selection: dependencies.$router.selectedTab.optional) { + List(selection: $tabRouter.selectedTab.optional) { Text("Ruddarr") .font(.title) .fontWeight(.bold) @@ -14,7 +48,7 @@ struct ContentView: View { ForEach(Tab.allCases) { tab in let button = Button { - dependencies.router.selectedTab = tab + tabRouter.selectedTab = tab columnVisibility = .detailOnly } label: { tab.label @@ -30,11 +64,11 @@ struct ContentView: View { } } } detail: { - screen(for: dependencies.router.selectedTab) + screen(for: tabRouter.selectedTab) } } else { - TabView(selection: dependencies.$router.selectedTab.onSet { - if $0 == dependencies.router.selectedTab { + TabView(selection: $tabRouter.selectedTab.onSet { + if $0 == tabRouter.selectedTab { pop(tab: $0) } }) { @@ -50,9 +84,10 @@ struct ContentView: View { func pop(tab: Tab) { switch tab { case .movies: - dependencies.router.moviesPath = .init() + moviesRouter.path = .init() case .settings: - dependencies.router.settingsPath = .init() + break +// router.settingsPath = .init() default: break } @@ -63,9 +98,9 @@ struct ContentView: View { switch tab { case .movies: MoviesView( - onSettingsLinkTapped: { - dependencies.router.selectedTab = .settings - } +// onSettingsLinkTapped: { +// router.selectedTab = .settings +// } ) case .shows: ShowsView() diff --git a/Ruddarr/Views/InstanceView.swift b/Ruddarr/Views/InstanceView.swift index 1e4af007..374adc7a 100644 --- a/Ruddarr/Views/InstanceView.swift +++ b/Ruddarr/Views/InstanceView.swift @@ -233,11 +233,11 @@ extension ValidationError: LocalizedError { } #Preview { - dependencies.router.selectedTab = .settings +// dependencies.router.selectedTab = .settings - dependencies.router.settingsPath.append( - SettingsView.Path.createInstance - ) +// dependencies.router.settingsPath.append( +// SettingsView.Path.createInstance +// ) return ContentView() } diff --git a/Ruddarr/Views/MovieSearchView.swift b/Ruddarr/Views/MovieSearchView.swift index 83da934d..8a4703b5 100644 --- a/Ruddarr/Views/MovieSearchView.swift +++ b/Ruddarr/Views/MovieSearchView.swift @@ -115,8 +115,8 @@ struct MovieLookupSheet: View { } #Preview { - dependencies.router.selectedTab = .movies - dependencies.router.moviesPath.append(MoviesView.Path.search) +// dependencies.router.selectedTab = .movies +// dependencies.router.moviesPath.append(MoviesView.Path.search) return ContentView() } diff --git a/Ruddarr/Views/MovieView.swift b/Ruddarr/Views/MovieView.swift index c7a1236c..8aa6a1ec 100644 --- a/Ruddarr/Views/MovieView.swift +++ b/Ruddarr/Views/MovieView.swift @@ -11,11 +11,11 @@ struct MovieView: View { #Preview { let movies: [Movie] = PreviewData.load(name: "movies") - dependencies.router.selectedTab = .movies +// dependencies.router.selectedTab = .movies - dependencies.router.moviesPath.append( - MoviesView.Path.movie(movies[2].id) - ) +// dependencies.router.moviesPath.append( +// MoviesView.Path.movie(movies[2].id) +// ) return ContentView() } diff --git a/Ruddarr/Views/MoviesView.swift b/Ruddarr/Views/MoviesView.swift index adffd4e4..c354fd21 100644 --- a/Ruddarr/Views/MoviesView.swift +++ b/Ruddarr/Views/MoviesView.swift @@ -1,6 +1,14 @@ import SwiftUI struct MoviesView: View { + + @Observable final class Router: DefaultKey { + var path: NavigationPath = .init() + } + @Environment() var router: Router + @Environment() var tabRouter: TabRouter + @Environment() var settingsRouter: SettingsView.Router + @State private var searchQuery = "" @State private var searchPresented = false @@ -20,14 +28,13 @@ struct MoviesView: View { case movie(Movie.ID) } - var onSettingsLinkTapped: () -> Void = { } - var body: some View { + @Bindable var router = router let gridItemLayout = [ GridItem(.adaptive(minimum: 250), spacing: 15) ] - NavigationStack(path: dependencies.$router.moviesPath) { + NavigationStack(path: $router.path) { Group { if let radarrInstance { ScrollView { @@ -110,7 +117,8 @@ struct MoviesView: View { description: Text("Connect a Radarr instance under [Settings](#view).") ) .environment(\.openURL, .init { _ in - onSettingsLinkTapped() + tabRouter.selectedTab = .settings + settingsRouter.path = .init([SettingsView.Path.createInstance]) return .handled }) } @@ -123,7 +131,7 @@ struct MoviesView: View { ).environment(\.openURL, .init { _ in searchQuery = "" searchPresented = false - dependencies.router.moviesPath.append(MoviesView.Path.search) + router.path.append(MoviesView.Path.search) return .handled }) } diff --git a/Ruddarr/Views/SettingsView.swift b/Ruddarr/Views/SettingsView.swift index 7fb03d11..f0ff8ec6 100644 --- a/Ruddarr/Views/SettingsView.swift +++ b/Ruddarr/Views/SettingsView.swift @@ -3,6 +3,10 @@ import SwiftUI import Nuke struct SettingsView: View { + @Observable final class Router: DefaultKey { + var path: NavigationPath = .init() + } + @Environment() var router: Router private let log: Logger = logger("settings") @CloudStorage("instances") private var instances: [Instance] = [] @@ -14,7 +18,8 @@ struct SettingsView: View { } var body: some View { - NavigationStack(path: dependencies.$router.settingsPath) { + @Bindable var router = router + NavigationStack(path: $router.path) { List { instanceSection aboutSection @@ -234,17 +239,17 @@ struct ThridPartyLibraries: View { } #Preview { - dependencies.router.selectedTab = .settings +// dependencies.router.selectedTab = .settings return ContentView() } #Preview("Libraries") { - dependencies.router.selectedTab = .settings +// dependencies.router.selectedTab = .settings - dependencies.router.settingsPath.append( - SettingsView.Path.libraries - ) +// dependencies.router.settingsPath.append( +// SettingsView.Path.libraries +// ) return ContentView() } diff --git a/Ruddarr/Views/ShowsView.swift b/Ruddarr/Views/ShowsView.swift index 05c7a58e..91bac66e 100644 --- a/Ruddarr/Views/ShowsView.swift +++ b/Ruddarr/Views/ShowsView.swift @@ -7,7 +7,6 @@ struct ShowsView: View { } #Preview { - dependencies.router.selectedTab = .shows - - return ContentView() +// dependencies.router.selectedTab = .shows + ContentView().environment(\.[key: \TabRouter], .init()) } From 75470842d1217b955f7f24ffa51b7c57b6ae4639 Mon Sep 17 00:00:00 2001 From: Petr Sima Date: Thu, 25 Jan 2024 14:15:41 +0100 Subject: [PATCH 2/4] just wonderful progress on switchToNewInstance --- .DS_Store | Bin 8196 -> 8196 bytes Ruddarr.xcodeproj/project.pbxproj | 8 ++-- Ruddarr/Dependencies/Dependencies.swift | 1 - Ruddarr/Dependencies/Router.swift | 32 --------------- Ruddarr/Utilities/SwitchToNewInstance.swift | 27 +++++++++++++ Ruddarr/Views/ContentView.swift | 42 ++++++++++++++++++-- Ruddarr/Views/MoviesView.swift | 9 ++--- 7 files changed, 73 insertions(+), 46 deletions(-) delete mode 100644 Ruddarr/Dependencies/Router.swift create mode 100644 Ruddarr/Utilities/SwitchToNewInstance.swift diff --git a/.DS_Store b/.DS_Store index b12aa375c344126d147e9ae79eb16e51ca18aa55..bc1854b1096b6ef1a86fc7d51842c2b552098412 100644 GIT binary patch delta 110 zcmZp1XmQx^ON^PB(`Ry_xSC*YzKcszPJR*t0|STJgKz`)$$8?MlVinsSYbSK#(;^1 OiJRFaKC_XkD-Hl33?PXB delta 110 zcmZp1XmQx^ON^Q6n8f5laW%o*d>5CboctsP1_ln#+_mSOC+CT4PL37lVTJL`83iU5 OCT?bz_{>JCt~dbCIwPF` diff --git a/Ruddarr.xcodeproj/project.pbxproj b/Ruddarr.xcodeproj/project.pbxproj index 1e9a16e5..02ffc8fb 100644 --- a/Ruddarr.xcodeproj/project.pbxproj +++ b/Ruddarr.xcodeproj/project.pbxproj @@ -12,7 +12,7 @@ 79159D712B5955A800F7F997 /* DummyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79159D702B5955A800F7F997 /* DummyAPI.swift */; }; 794BD7822B5ED48E003819AB /* Binding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 794BD7812B5ED48E003819AB /* Binding.swift */; }; 795B7AD72B5AFA8C00A13DB3 /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 795B7AD62B5AFA8C00A13DB3 /* AppError.swift */; }; - 798010BA2B61719C00BBC056 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798010B92B61719C00BBC056 /* Router.swift */; }; + 796C766B2B628E6000EF3DB8 /* SwitchToNewInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796C766A2B628E6000EF3DB8 /* SwitchToNewInstance.swift */; }; BB456D202B58B7E700C29B00 /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB456D1F2B58B7E700C29B00 /* Network.swift */; }; BB456D232B58C71300C29B00 /* NoInternet.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB456D222B58C71300C29B00 /* NoInternet.swift */; }; BB456D272B58E4B900C29B00 /* system-status.json in Resources */ = {isa = PBXBuildFile; fileRef = BB456D262B58E4B900C29B00 /* system-status.json */; }; @@ -49,7 +49,7 @@ 79159D702B5955A800F7F997 /* DummyAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyAPI.swift; sourceTree = ""; }; 794BD7812B5ED48E003819AB /* Binding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Binding.swift; sourceTree = ""; }; 795B7AD62B5AFA8C00A13DB3 /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; - 798010B92B61719C00BBC056 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; + 796C766A2B628E6000EF3DB8 /* SwitchToNewInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchToNewInstance.swift; sourceTree = ""; }; BB456D1F2B58B7E700C29B00 /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; BB456D222B58C71300C29B00 /* NoInternet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoInternet.swift; sourceTree = ""; }; BB456D262B58E4B900C29B00 /* system-status.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "system-status.json"; sourceTree = ""; }; @@ -98,7 +98,6 @@ children = ( 79159D6F2B59540000F7F997 /* API */, 79159D6B2B5953D700F7F997 /* Dependencies.swift */, - 798010B92B61719C00BBC056 /* Router.swift */, ); path = Dependencies; sourceTree = ""; @@ -121,6 +120,7 @@ BBC94DE22B5F3DE600504568 /* CloudStorage.swift */, BBC94DE42B5F3E0B00504568 /* CloudStorageSync.swift */, BBC94DE82B5F63A000504568 /* Telemetry.swift */, + 796C766A2B628E6000EF3DB8 /* SwitchToNewInstance.swift */, ); path = Utilities; sourceTree = ""; @@ -321,10 +321,10 @@ BBC45B962B572A8600AB258F /* PreviewData.swift in Sources */, 795B7AD72B5AFA8C00A13DB3 /* AppError.swift in Sources */, BB456D202B58B7E700C29B00 /* Network.swift in Sources */, - 798010BA2B61719C00BBC056 /* Router.swift in Sources */, BBE1E43F2B51F61700946222 /* MovieLookup.swift in Sources */, 79159D712B5955A800F7F997 /* DummyAPI.swift in Sources */, BBF94F652B50C88300300EBA /* Instance.swift in Sources */, + 796C766B2B628E6000EF3DB8 /* SwitchToNewInstance.swift in Sources */, BBE8286A2B4F325400C1E1D9 /* ShowsView.swift in Sources */, 79159D6C2B5953D700F7F997 /* Dependencies.swift in Sources */, BBF94F632B508F8B00300EBA /* MovieView.swift in Sources */, diff --git a/Ruddarr/Dependencies/Dependencies.swift b/Ruddarr/Dependencies/Dependencies.swift index ca26c2c1..b14ddf36 100644 --- a/Ruddarr/Dependencies/Dependencies.swift +++ b/Ruddarr/Dependencies/Dependencies.swift @@ -3,7 +3,6 @@ import SwiftUI struct Dependencies { var api: API -// @Bindable var router = Router.shared } extension Dependencies { diff --git a/Ruddarr/Dependencies/Router.swift b/Ruddarr/Dependencies/Router.swift deleted file mode 100644 index c52b7c79..00000000 --- a/Ruddarr/Dependencies/Router.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import SwiftUI - -//@Observable -//final class Router { -// static let shared = Router() -// -// var selectedTab: Tab = .movies -// -// var moviesPath: NavigationPath = .init() -// var settingsPath: NavigationPath = .init() -//} -// -enum Tab: Hashable, CaseIterable, Identifiable { - var id: Self { self } - - case movies - case shows - case settings - - @ViewBuilder - var label: some View { - switch self { - case .movies: - Label("Movies", systemImage: "popcorn.fill") - case .shows: - Label("Shows", systemImage: "tv.inset.filled") - case .settings: - Label("Settings", systemImage: "gear") - } - } -} diff --git a/Ruddarr/Utilities/SwitchToNewInstance.swift b/Ruddarr/Utilities/SwitchToNewInstance.swift new file mode 100644 index 00000000..1b2cf16f --- /dev/null +++ b/Ruddarr/Utilities/SwitchToNewInstance.swift @@ -0,0 +1,27 @@ +import Foundation +import SwiftUI + +extension EnvironmentValues { + subscript(key key: Key.Type = Key.self) -> Key where Key.Value == Key { + get { self[Key.self] } + set { self[Key.self] = newValue } + } +} + +extension EnvironmentValues { + var switchToNewInstance: () -> Void { + get { + // we can make some of this syntax more tolerable but its extra work + let tabRouter = self[key: TabRouter.self] + let settingsRouter = self[key: SettingsView.Router.self] + return { + tabRouter.selectedTab = .settings + Task { @MainActor in + try await Task.sleep(until: .now + .seconds(0.1)) + assert(settingsRouter.path.isEmpty) // FUTURE: make a decision on whether its safe to switch to newInstance regardless of the fact that settings tab already had active navigation + settingsRouter.path = .init([SettingsView.Path.createInstance]) + } + } + } + } +} diff --git a/Ruddarr/Views/ContentView.swift b/Ruddarr/Views/ContentView.swift index 34b81267..1776b425 100644 --- a/Ruddarr/Views/ContentView.swift +++ b/Ruddarr/Views/ContentView.swift @@ -17,15 +17,49 @@ protocol EmptyInitilizable { } protocol DefaultKey: EnvironmentKey, EmptyInitilizable { } +fileprivate var singletonCache: [ObjectIdentifier: Any] = [:] extension DefaultKey where Value == Self { - static var defaultValue: Value { .init() } + // unfortunately this generic context doesn't support stored static properties, so we need our own external cache (to make sure defaultValue is always same instance). Not a big deal. + static var defaultValue: Self { + singletonCache[ObjectIdentifier(Self.self)] as? Self ?? { + let instance = Self() + singletonCache[ObjectIdentifier(Self.self)] = instance + return instance + }() + } } -@Observable final class TabRouter: EnvironmentKey { - static var defaultValue: TabRouter = .init() - +@Observable final class TabRouter: DefaultKey { + static var singletonCache: TabRouter? var selectedTab: Tab = .movies + + init() { + print("init") + } + deinit { + print("deinit") + } +} + +enum Tab: Hashable, CaseIterable, Identifiable { + var id: Self { self } + + case movies + case shows + case settings + + @ViewBuilder + var label: some View { + switch self { + case .movies: + Label("Movies", systemImage: "popcorn.fill") + case .shows: + Label("Shows", systemImage: "tv.inset.filled") + case .settings: + Label("Settings", systemImage: "gear") + } + } } diff --git a/Ruddarr/Views/MoviesView.swift b/Ruddarr/Views/MoviesView.swift index c354fd21..fc2cead2 100644 --- a/Ruddarr/Views/MoviesView.swift +++ b/Ruddarr/Views/MoviesView.swift @@ -3,12 +3,12 @@ import SwiftUI struct MoviesView: View { @Observable final class Router: DefaultKey { + static var singletonCache: MoviesView.Router? + var path: NavigationPath = .init() } @Environment() var router: Router - @Environment() var tabRouter: TabRouter - @Environment() var settingsRouter: SettingsView.Router - + @Environment(\.switchToNewInstance) var switchToNewInstance @State private var searchQuery = "" @State private var searchPresented = false @@ -117,8 +117,7 @@ struct MoviesView: View { description: Text("Connect a Radarr instance under [Settings](#view).") ) .environment(\.openURL, .init { _ in - tabRouter.selectedTab = .settings - settingsRouter.path = .init([SettingsView.Path.createInstance]) + switchToNewInstance() return .handled }) } From ab72cee81c4d172473500259fd34eed9e0ef7312 Mon Sep 17 00:00:00 2001 From: Petr Sima Date: Thu, 25 Jan 2024 14:22:59 +0100 Subject: [PATCH 3/4] comment --- .DS_Store | Bin 8196 -> 8196 bytes Ruddarr/Utilities/SwitchToNewInstance.swift | 1 + 2 files changed, 1 insertion(+) diff --git a/.DS_Store b/.DS_Store index bc1854b1096b6ef1a86fc7d51842c2b552098412..ba6c84049b03c6d170ebb7b908053a18420dc447 100644 GIT binary patch delta 44 ncmZp1XmQx^Ta1Z0XmX*r+GJ(%CPs&eg^8QlCBCqs3B~~cOlb}& delta 44 ncmZp1XmQx^Ta1a*XL6yq+GJ(%CdPn?g^8QlCBCqs3B~~cPDKt$ diff --git a/Ruddarr/Utilities/SwitchToNewInstance.swift b/Ruddarr/Utilities/SwitchToNewInstance.swift index 1b2cf16f..2fa29615 100644 --- a/Ruddarr/Utilities/SwitchToNewInstance.swift +++ b/Ruddarr/Utilities/SwitchToNewInstance.swift @@ -17,6 +17,7 @@ extension EnvironmentValues { return { tabRouter.selectedTab = .settings Task { @MainActor in + // delay is just for UX reasons (so user realizes where they went) try await Task.sleep(until: .now + .seconds(0.1)) assert(settingsRouter.path.isEmpty) // FUTURE: make a decision on whether its safe to switch to newInstance regardless of the fact that settings tab already had active navigation settingsRouter.path = .init([SettingsView.Path.createInstance]) From 3bc05050c9a8ddb50614711dfa3b4fdc70763d42 Mon Sep 17 00:00:00 2001 From: Petr Sima Date: Thu, 25 Jan 2024 14:36:36 +0100 Subject: [PATCH 4/4] remove needless property --- .DS_Store | Bin 8196 -> 8196 bytes Ruddarr/Views/ContentView.swift | 8 -------- 2 files changed, 8 deletions(-) diff --git a/.DS_Store b/.DS_Store index ba6c84049b03c6d170ebb7b908053a18420dc447..508eefa868a1b90159bfd0fe54c264ebaf83bdd9 100644 GIT binary patch delta 30 gcmZp1XmQx^Ta1Y{b#kG&+Gb_(8|+Z_WM%P(0HeGL9smFU delta 30 gcmZp1XmQx^Ta1Z0XmX*r+Gb_(8|+Z_WM%P(0HHz&^Z)<= diff --git a/Ruddarr/Views/ContentView.swift b/Ruddarr/Views/ContentView.swift index 1776b425..aeda609e 100644 --- a/Ruddarr/Views/ContentView.swift +++ b/Ruddarr/Views/ContentView.swift @@ -31,15 +31,7 @@ extension DefaultKey where Value == Self { } @Observable final class TabRouter: DefaultKey { - static var singletonCache: TabRouter? var selectedTab: Tab = .movies - - init() { - print("init") - } - deinit { - print("deinit") - } } enum Tab: Hashable, CaseIterable, Identifiable {