Handle load/save preferences inside ProfileEditor (#982)

Simplify preferences model by doing a bulk load/save together with
load/save Profile. ModulePreferences is now a struct rather than an
ObservableObject, because it doesn't need ad hoc observation. It's just
a binding to ProfileEditor.preferences

Fix:

- Disable CloudKit in tunnel singleton of PreferencesManager
(.sharedForTunnel)

Additionally:

- Replace MainActor in PreferencesManager with Sendable (immutable)
- Replace MainActor from ProviderPreferencesRepository with Sendable
(syncs on NSManagedObjectContext)
- Drop ModuleMetadata for good
This commit is contained in:
Davide 2024-12-08 16:05:23 +01:00 committed by GitHub
parent 14847b2de5
commit a4ebea1f95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 279 additions and 224 deletions

View File

@ -41,7 +41,7 @@
"kind" : "remoteSourceControl",
"location" : "git@github.com:passepartoutvpn/passepartoutkit-source",
"state" : {
"revision" : "d753c05f36b789fe413aeccbb543fb8c383ddc2b"
"revision" : "406712a60faf8208a15c4ffaf286b1c71df7c6d2"
}
},
{

View File

@ -65,7 +65,7 @@ let package = Package(
],
dependencies: [
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", from: "0.12.0"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "d753c05f36b789fe413aeccbb543fb8c383ddc2b"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source", revision: "406712a60faf8208a15c4ffaf286b1c71df7c6d2"),
// .package(path: "../../passepartoutkit-source"),
.package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", from: "0.9.1"),
// .package(url: "git@github.com:passepartoutvpn/passepartoutkit-source-openvpn-openssl", revision: "031863a1cd683962a7dfe68e20b91fa820a1ecce"),

View File

@ -0,0 +1,40 @@
//
// Mapper.swift
// Passepartout
//
// Created by Davide De Rosa on 12/7/24.
// Copyright (c) 2024 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
//
// This file is part of Passepartout.
//
// Passepartout is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Passepartout is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import CommonLibrary
import CoreData
import Foundation
import PassepartoutKit
struct DomainMapper {
func preferences(from entity: CDModulePreferencesV3) throws -> ModulePreferences {
ModulePreferences()
}
}
struct CoreDataMapper {
func set(_ entity: CDModulePreferencesV3, from preferences: ModulePreferences) throws {
}
}

View File

@ -30,15 +30,11 @@ import Foundation
import PassepartoutKit
extension AppData {
@MainActor
public static func cdModulePreferencesRepositoryV3(context: NSManagedObjectContext) -> ModulePreferencesRepository {
CDModulePreferencesRepositoryV3(context: context)
}
}
// MARK: - Repository
private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository {
private nonisolated let context: NSManagedObjectContext
@ -46,36 +42,49 @@ private final class CDModulePreferencesRepositoryV3: ModulePreferencesRepository
self.context = context
}
func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
let entity = try context.performAndWait {
func preferences(for moduleIds: [UUID]) throws -> [UUID: ModulePreferences] {
try context.performAndWait {
let request = CDModulePreferencesV3.fetchRequest()
request.predicate = NSPredicate(format: "uuid == %@", moduleId.uuidString)
request.predicate = NSPredicate(format: "any uuid in %@", moduleIds.map(\.uuidString))
let entities = try request.execute()
let mapper = DomainMapper()
return entities.reduce(into: [:]) {
guard let moduleId = $1.uuid else {
return
}
do {
let entity = try request.execute().first ?? CDModulePreferencesV3(context: context)
entity.uuid = moduleId
return entity
let preferences = try mapper.preferences(from: $1)
$0[moduleId] = preferences
} catch {
pp_log(.app, .error, "Unable to load preferences for module \(moduleId): \(error)")
throw error
}
}
return CDModulePreferencesProxy(context: context, entity: entity)
}
}
// MARK: - Preference
func set(_ preferences: [UUID: ModulePreferences]) throws {
try context.performAndWait {
let request = CDModulePreferencesV3.fetchRequest()
request.predicate = NSPredicate(format: "any uuid in %@", Array(preferences.keys))
private final class CDModulePreferencesProxy: ModulePreferencesProxy {
private let context: NSManagedObjectContext
private let entity: CDModulePreferencesV3
init(context: NSManagedObjectContext, entity: CDModulePreferencesV3) {
self.context = context
self.entity = entity
var entities = try request.execute()
let existingIds = entities.compactMap(\.uuid)
let newIds = Set(preferences.keys).subtracting(existingIds)
newIds.forEach {
let newEntity = CDModulePreferencesV3(context: context)
newEntity.uuid = $0
entities.append(newEntity)
}
let mapper = CoreDataMapper()
try entities.forEach {
guard let id = $0.uuid, let entityPreferences = preferences[id] else {
return
}
try mapper.set($0, from: entityPreferences)
}
func save() throws {
guard context.hasChanges else {
return
}
@ -87,3 +96,8 @@ private final class CDModulePreferencesProxy: ModulePreferencesProxy {
}
}
}
func rollback() {
context.rollback()
}
}

View File

@ -30,24 +30,20 @@ import Foundation
import PassepartoutKit
extension AppData {
@MainActor
public static func cdProviderPreferencesRepositoryV3(context: NSManagedObjectContext) -> ProviderPreferencesRepository {
CDProviderPreferencesRepositoryV3(context: context)
public static func cdProviderPreferencesRepositoryV3(context: NSManagedObjectContext, providerId: ProviderID) throws -> ProviderPreferencesRepository {
try CDProviderPreferencesRepositoryV3(context: context, providerId: providerId)
}
}
// MARK: - Repository
private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesRepository {
private nonisolated let context: NSManagedObjectContext
init(context: NSManagedObjectContext) {
self.context = context
}
private let entity: CDProviderPreferencesV3
func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
let entity = try context.performAndWait {
init(context: NSManagedObjectContext, providerId: ProviderID) throws {
self.context = context
entity = try context.performAndWait {
let request = CDProviderPreferencesV3.fetchRequest()
request.predicate = NSPredicate(format: "providerId == %@", providerId.rawValue)
do {
@ -59,20 +55,6 @@ private final class CDProviderPreferencesRepositoryV3: ProviderPreferencesReposi
throw error
}
}
return CDProviderPreferencesProxy(context: context, entity: entity)
}
}
// MARK: - Preference
private final class CDProviderPreferencesProxy: ProviderPreferencesProxy {
private let context: NSManagedObjectContext
private let entity: CDProviderPreferencesV3
init(context: NSManagedObjectContext, entity: CDProviderPreferencesV3) {
self.context = context
self.entity = entity
}
var favoriteServers: Set<String> {

View File

@ -36,7 +36,7 @@ extension AppData {
}
}
private actor CDProviderRepositoryV3: NSObject, ProviderRepository {
private final class CDProviderRepositoryV3: NSObject, ProviderRepository {
private nonisolated let context: NSManagedObjectContext
private nonisolated let providersSubject: CurrentValueSubject<[Provider], Never>

View File

@ -34,6 +34,9 @@ public struct AppCoordinator: View, AppCoordinatorConforming, SizeClassProviding
@EnvironmentObject
public var iapManager: IAPManager
@EnvironmentObject
public var preferencesManager: PreferencesManager
@Environment(\.isUITesting)
private var isUITesting
@ -117,12 +120,7 @@ extension AppCoordinator {
isImporting: $isImporting,
errorHandler: errorHandler,
flow: .init(
onEditProfile: {
guard let profile = profileManager.profile(withId: $0.id) else {
return
}
enterDetail(of: profile.editable(), initialModuleId: nil)
},
onEditProfile: onEditProfile,
onMigrateProfiles: {
modalRoute = .migrateProfiles
},
@ -168,7 +166,7 @@ extension AppCoordinator {
onMigrateProfiles: {
present(.migrateProfiles)
},
onNewProfile: enterDetail
onNewProfile: onNewProfile
)
}
@ -232,7 +230,11 @@ extension AppCoordinator {
extension AppCoordinator {
public func onInteractiveLogin(_ profile: Profile, _ onComplete: @escaping InteractiveManager.CompletionBlock) {
pp_log(.app, .info, "Present interactive login")
interactiveManager.present(with: profile, onComplete: onComplete)
interactiveManager.present(
with: profile,
preferencesManager: preferencesManager,
onComplete: onComplete
)
}
public func onProviderEntityRequired(_ profile: Profile, force: Bool) {
@ -287,10 +289,21 @@ private extension AppCoordinator {
}
}
func enterDetail(of profile: EditableProfile, initialModuleId: UUID?) {
func onNewProfile(_ profile: EditableProfile, initialModuleId: UUID?) {
editProfile(profile, initialModuleId: initialModuleId)
}
func onEditProfile(_ preview: ProfilePreview) {
guard let profile = profileManager.profile(withId: preview.id) else {
return
}
editProfile(profile.editable(), initialModuleId: nil)
}
func editProfile(_ profile: EditableProfile, initialModuleId: UUID?) {
profilePath = NavigationPath()
let isShared = profileManager.isRemotelyShared(profileWithId: profile.id)
profileEditor.editProfile(profile, isShared: isShared)
profileEditor.load(profile, isShared: isShared, preferencesManager: preferencesManager)
present(.editProfile(initialModuleId))
}
}

View File

@ -242,7 +242,6 @@ private extension OnDemandView {
module: $0,
parameters: .init(
editor: $1,
preferences: nil,
impl: nil
),
observer: MockWifi()

View File

@ -28,19 +28,12 @@ import PassepartoutKit
import SwiftUI
struct ModuleDetailView: View {
@EnvironmentObject
private var preferencesManager: PreferencesManager
let profileEditor: ProfileEditor
let moduleId: UUID?
let moduleViewFactory: any ModuleViewFactory
@StateObject
private var modulePreferences = ModulePreferences(proxy: nil)
var body: some View {
debugChanges()
return Group {
@ -50,16 +43,6 @@ struct ModuleDetailView: View {
emptyView
}
}
.onLoad {
guard let moduleId else {
return
}
do {
modulePreferences.proxy = try preferencesManager.modulePreferencesProxy(in: moduleId)
} catch {
pp_log(.app, .error, "Unable to load module preferences: \(error)")
}
}
}
}
@ -69,7 +52,6 @@ private extension ModuleDetailView {
func editorView(forModuleWithId moduleId: UUID) -> some View {
AnyView(moduleViewFactory.view(
with: profileEditor,
preferences: modulePreferences,
moduleId: moduleId
))
}

View File

@ -43,6 +43,9 @@ struct ProfileCoordinator: View {
@EnvironmentObject
private var iapManager: IAPManager
@EnvironmentObject
private var preferencesManager: PreferencesManager
let profileManager: ProfileManager
let profileEditor: ProfileEditor
@ -133,7 +136,10 @@ private extension ProfileCoordinator {
// standard: always save, warn if purchase required
func onCommitEditingStandard() async throws {
let savedProfile = try await profileEditor.save(to: profileManager)
let savedProfile = try await profileEditor.save(
to: profileManager,
preferencesManager: preferencesManager
)
do {
try iapManager.verify(savedProfile)
} catch AppError.ineligibleProfile(let requiredFeatures) {
@ -151,11 +157,15 @@ private extension ProfileCoordinator {
paywallReason = .init(requiredFeatures)
return
}
try await profileEditor.save(to: profileManager)
try await profileEditor.save(
to: profileManager,
preferencesManager: preferencesManager
)
onDismiss()
}
func onCancelEditing() {
profileEditor.discard()
onDismiss()
}
}

View File

@ -68,7 +68,7 @@ struct VPNProviderServerView<Configuration>: View where Configuration: Identifia
private var onlyShowsFavorites = false
@StateObject
private var providerPreferences = ProviderPreferences(proxy: nil)
private var providerPreferences = ProviderPreferences()
@StateObject
private var filtersViewModel = VPNFiltersView.Model()
@ -159,7 +159,7 @@ private extension VPNProviderServerView {
private extension VPNProviderServerView {
func loadInitialServers() async {
do {
providerPreferences.proxy = try preferencesManager.providerPreferencesProxy(in: providerId)
providerPreferences.repository = try preferencesManager.preferencesRepository(forProviderWithId: providerId)
} catch {
pp_log(.app, .error, "Unable to load preferences for provider \(providerId): \(error)")
}

View File

@ -34,6 +34,9 @@ public struct AppCoordinator: View, AppCoordinatorConforming {
@EnvironmentObject
public var iapManager: IAPManager
@EnvironmentObject
private var preferencesManager: PreferencesManager
private let profileManager: ProfileManager
public let tunnel: ExtendedTunnel
@ -136,7 +139,11 @@ private extension AppCoordinator {
extension AppCoordinator {
public func onInteractiveLogin(_ profile: Profile, _ onComplete: @escaping InteractiveManager.CompletionBlock) {
pp_log(.app, .info, "Present interactive login")
interactiveManager.present(with: profile, onComplete: onComplete)
interactiveManager.present(
with: profile,
preferencesManager: preferencesManager,
onComplete: onComplete
)
}
public func onProviderEntityRequired(_ profile: Profile, force: Bool) {

View File

@ -27,51 +27,66 @@ import CommonUtils
import Foundation
import PassepartoutKit
@MainActor
public final class PreferencesManager: ObservableObject {
public final class PreferencesManager: ObservableObject, Sendable {
private let modulesRepository: ModulePreferencesRepository
private let providersRepository: ProviderPreferencesRepository
private let providersFactory: @Sendable (ProviderID) throws -> ProviderPreferencesRepository
public init(
modulesRepository: ModulePreferencesRepository? = nil,
providersRepository: ProviderPreferencesRepository? = nil
providersFactory: (@Sendable (ProviderID) throws -> ProviderPreferencesRepository)? = nil
) {
self.modulesRepository = modulesRepository ?? DummyModulePreferencesRepository()
self.providersRepository = providersRepository ?? DummyProviderPreferencesRepository()
self.providersFactory = providersFactory ?? { _ in
DummyProviderPreferencesRepository()
}
}
}
public func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
try modulesRepository.modulePreferencesProxy(in: moduleId)
// MARK: - Modules
extension PreferencesManager {
public func preferences(forProfile profile: Profile) throws -> [UUID: ModulePreferences] {
try preferences(forModulesWithIds: profile.modules.map(\.id))
}
public func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
try providersRepository.providerPreferencesProxy(in: providerId)
public func preferences(forProfile editableProfile: EditableProfile) throws -> [UUID: ModulePreferences] {
try preferences(forModulesWithIds: editableProfile.modules.map(\.id))
}
public func savePreferences(_ preferences: [UUID: ModulePreferences]) throws {
try modulesRepository.set(preferences)
}
}
private extension PreferencesManager {
func preferences(forModulesWithIds moduleIds: [UUID]) throws -> [UUID: ModulePreferences] {
try modulesRepository.preferences(for: moduleIds)
}
}
// MARK: - Providers
extension PreferencesManager {
public func preferencesRepository(forProviderWithId providerId: ProviderID) throws -> ProviderPreferencesRepository {
try providersFactory(providerId)
}
}
// MARK: - Dummy
private final class DummyModulePreferencesRepository: ModulePreferencesRepository {
private final class Proxy: ModulePreferencesProxy {
func save() throws {
}
func preferences(for moduleIds: [UUID]) throws -> [UUID: ModulePreferences] {
[:]
}
func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy {
Proxy()
func set(_ preferences: [UUID: ModulePreferences]) throws {
}
}
private final class DummyProviderPreferencesRepository: ProviderPreferencesRepository {
private final class Proxy: ProviderPreferencesProxy {
var favoriteServers: Set<String> = []
func save() throws {
}
}
func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy {
Proxy()
}
}

View File

@ -27,35 +27,27 @@ import Foundation
import PassepartoutKit
@MainActor
public final class ProviderPreferences: ObservableObject {
public var proxy: ProviderPreferencesProxy? {
public final class ProviderPreferences: ObservableObject, ProviderPreferencesRepository {
public var repository: ProviderPreferencesRepository? {
didSet {
objectWillChange.send()
}
}
public init(proxy: ProviderPreferencesProxy?) {
self.proxy = proxy
public init() {
}
public var favoriteServers: Set<String> {
get {
proxy?.favoriteServers ?? []
repository?.favoriteServers ?? []
}
set {
objectWillChange.send()
proxy?.favoriteServers = newValue
repository?.favoriteServers = newValue
}
}
public func save() throws {
try proxy?.save()
try repository?.save()
}
}
@MainActor
public protocol ProviderPreferencesProxy {
var favoriteServers: Set<String> { get set }
func save() throws
}

View File

@ -35,8 +35,6 @@ public struct EditableProfile: MutableProfileType {
public var activeModulesIds: Set<UUID>
public var modulesMetadata: [UUID: ModuleMetadata]?
public var userInfo: AnyHashable?
public init(
@ -44,14 +42,12 @@ public struct EditableProfile: MutableProfileType {
name: String = "",
modules: [any ModuleBuilder] = [],
activeModulesIds: Set<UUID> = [],
modulesMetadata: [UUID: ModuleMetadata]? = nil,
userInfo: AnyHashable? = nil
) {
self.id = id
self.name = name
self.modules = modules
self.activeModulesIds = activeModulesIds
self.modulesMetadata = modulesMetadata
self.userInfo = userInfo
}
@ -71,16 +67,6 @@ public struct EditableProfile: MutableProfileType {
throw AppError.emptyProfileName
}
builder.name = trimmedName
builder.modulesMetadata = modulesMetadata?.reduce(into: [:]) {
var metadata = $1.value
if var trimmedName = metadata.name {
trimmedName = trimmedName.trimmingCharacters(in: .whitespaces)
metadata.name = !trimmedName.isEmpty ? trimmedName : nil
}
$0[$1.key] = metadata
}
builder.userInfo = userInfo
return builder
@ -105,7 +91,6 @@ extension Profile {
name: name,
modules: modulesBuilders(),
activeModulesIds: activeModulesIds,
modulesMetadata: modulesMetadata,
userInfo: userInfo
)
}

View File

@ -26,24 +26,7 @@
import Foundation
import PassepartoutKit
@MainActor
public final class ModulePreferences: ObservableObject {
public var proxy: ModulePreferencesProxy? {
didSet {
objectWillChange.send()
public struct ModulePreferences: Sendable {
public init() {
}
}
public init(proxy: ModulePreferencesProxy?) {
self.proxy = proxy
}
public func save() throws {
try proxy?.save()
}
}
@MainActor
public protocol ModulePreferencesProxy {
func save() throws
}

View File

@ -24,8 +24,10 @@
//
import Foundation
import PassepartoutKit
@MainActor
public protocol ModulePreferencesRepository {
func modulePreferencesProxy(in moduleId: UUID) throws -> ModulePreferencesProxy
public protocol ModulePreferencesRepository: Sendable {
func preferences(for moduleIds: [UUID]) throws -> [UUID: ModulePreferences]
func set(_ preferences: [UUID: ModulePreferences]) throws
}

View File

@ -26,7 +26,8 @@
import Foundation
import PassepartoutKit
@MainActor
public protocol ProviderPreferencesRepository {
func providerPreferencesProxy(in providerId: ProviderID) throws -> ProviderPreferencesProxy
var favoriteServers: Set<String> { get set }
func save() throws
}

View File

@ -28,13 +28,13 @@ import Combine
import CoreData
import Foundation
public protocol CoreDataPersistentStoreLogger {
public protocol CoreDataPersistentStoreLogger: Sendable {
func debug(_ msg: String)
func warning(_ msg: String)
}
public final class CoreDataPersistentStore {
public final class CoreDataPersistentStore: Sendable {
private let logger: CoreDataPersistentStoreLogger?
private let container: NSPersistentContainer

View File

@ -41,8 +41,9 @@ public final class InteractiveManager: ObservableObject {
public init() {
}
public func present(with profile: Profile, onComplete: CompletionBlock?) {
editor = ProfileEditor(profile: profile)
public func present(with profile: Profile, preferencesManager: PreferencesManager, onComplete: CompletionBlock?) {
editor = ProfileEditor()
editor.load(profile.editable(), isShared: false, preferencesManager: preferencesManager)
self.onComplete = onComplete
isPresented = true
}

View File

@ -37,30 +37,31 @@ public final class ProfileEditor: ObservableObject {
@Published
public var isShared: Bool
@Published
public var preferences: [UUID: ModulePreferences]
private(set) var removedModules: [UUID: any ModuleBuilder]
public convenience init() {
self.init(modules: [])
}
// for testing/previews
public init(profile: Profile) {
editableProfile = profile.editable()
isShared = false
preferences = [:]
removedModules = [:]
}
// for testing/previews
public init(modules: [any ModuleBuilder]) {
editableProfile = EditableProfile(
modules: modules,
activeModulesIds: Set(modules.map(\.id))
)
isShared = false
removedModules = [:]
}
public init(profile: Profile) {
editableProfile = profile.editable()
isShared = false
removedModules = [:]
}
public func editProfile(_ profile: EditableProfile, isShared: Bool) {
editableProfile = profile
self.isShared = isShared
preferences = [:]
removedModules = [:]
}
}
@ -198,21 +199,47 @@ extension ProfileEditor {
}
}
// MARK: - Saving
// MARK: - Load/Save
extension ProfileEditor {
public func load(
_ profile: EditableProfile,
isShared: Bool,
preferencesManager: PreferencesManager
) {
editableProfile = profile
self.isShared = isShared
do {
preferences = try preferencesManager.preferences(forProfile: profile)
} catch {
preferences = [:]
pp_log(.app, .error, "Unable to load preferences for profile \(profile.id): \(error)")
}
removedModules = [:]
}
@discardableResult
public func save(to profileManager: ProfileManager) async throws -> Profile {
public func save(
to profileManager: ProfileManager,
preferencesManager: PreferencesManager
) async throws -> Profile {
do {
let newProfile = try build()
try await profileManager.save(newProfile, isLocal: true, remotelyShared: isShared)
do {
try preferencesManager.savePreferences(preferences)
} catch {
pp_log(.App.profiles, .error, "Unable to save preferences for profile \(profile.id): \(error)")
}
return newProfile
} catch {
pp_log(.app, .fault, "Unable to save edited profile: \(error)")
throw error
}
}
public func discard() {
}
}
// MARK: - Testing

View File

@ -33,7 +33,6 @@ extension ModuleBuilder where Self: ModuleViewProviding {
NavigationStack {
moduleView(with: .init(
editor: ProfileEditor(modules: [self]),
preferences: nil,
impl: nil
))
.navigationTitle(title)

View File

@ -23,18 +23,11 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import CommonLibrary
import PassepartoutKit
import SwiftUI
extension ProfileEditor {
public func binding(forNameOf moduleId: UUID) -> Binding<String> {
Binding { [weak self] in
self?.profile.name(forModuleWithId: moduleId) ?? ""
} set: { [weak self] in
self?.profile.setName($0, forModuleWithId: moduleId)
}
}
public subscript<T>(module: T) -> Binding<T> where T: ModuleBuilder {
Binding { [weak self] in
guard let foundModule = self?.module(withId: module.id) else {
@ -48,4 +41,12 @@ extension ProfileEditor {
self?.saveModule($0, activating: false)
}
}
public func binding(forPreferencesOf moduleId: UUID) -> Binding<ModulePreferences> {
Binding { [weak self] in
self?.preferences[moduleId] ?? ModulePreferences()
} set: { [weak self] in
self?.preferences[moduleId] = $0
}
}
}

View File

@ -36,12 +36,11 @@ public final class DefaultModuleViewFactory: ModuleViewFactory {
}
@ViewBuilder
public func view(with editor: ProfileEditor, preferences: ModulePreferences, moduleId: UUID) -> some View {
public func view(with editor: ProfileEditor, moduleId: UUID) -> some View {
let result = editor.moduleViewProvider(withId: moduleId, registry: registry)
if let result {
AnyView(result.provider.moduleView(with: .init(
editor: editor,
preferences: preferences,
impl: result.impl
)))
.navigationTitle(result.title)

View File

@ -31,5 +31,5 @@ public protocol ModuleViewFactory: AnyObject {
associatedtype Content: View
@MainActor
func view(with editor: ProfileEditor, preferences: ModulePreferences, moduleId: UUID) -> Content
func view(with editor: ProfileEditor, moduleId: UUID) -> Content
}

View File

@ -37,18 +37,14 @@ public protocol ModuleViewProviding {
public struct ModuleViewParameters {
public let editor: ProfileEditor
public let preferences: ModulePreferences
public let impl: (any ModuleImplementation)?
@MainActor
public init(
editor: ProfileEditor,
preferences: ModulePreferences?,
impl: (any ModuleImplementation)?
) {
self.editor = editor
self.preferences = preferences ?? ModulePreferences(proxy: nil)
self.impl = impl
}
}

View File

@ -251,7 +251,7 @@ extension ProfileEditorTests {
}
.store(in: &subscriptions)
try await sut.save(to: manager)
try await sut.save(to: manager, preferencesManager: PreferencesManager())
await fulfillment(of: [exp])
}
}

View File

@ -39,7 +39,7 @@ import UITesting
extension AppContext {
static let shared: AppContext = {
let iapManager: IAPManager = .sharedForApp
let processor = InAppProcessor.shared(iapManager) {
let processor = InAppProcessor.sharedImplementation(with: iapManager) {
$0.localizedPreview
}
@ -124,7 +124,7 @@ extension AppContext {
migrationManager: migrationManager,
profileManager: profileManager,
providerManager: providerManager,
preferencesManager: .shared,
preferencesManager: .sharedForApp,
registry: .shared,
tunnel: tunnel,
tunnelReceiptURL: BundleConfiguration.urlForBetaReceipt

View File

@ -39,6 +39,10 @@ extension IAPManager {
)
}
extension PreferencesManager {
static let sharedForApp = PreferencesManager.sharedImplementation(withCloudKit: true)
}
// MARK: - Dependencies
private extension Dependencies.IAPManager {

View File

@ -37,6 +37,10 @@ extension IAPManager {
)
}
extension PreferencesManager {
static let sharedForTunnel = PreferencesManager.sharedImplementation(withCloudKit: false)
}
// MARK: - Dependencies
private extension Dependencies.IAPManager {

View File

@ -92,7 +92,7 @@ extension TunnelEnvironment where Self == AppGroupEnvironment {
extension InAppProcessor {
@MainActor
static func shared(_ iapManager: IAPManager, preview: @escaping (Profile) -> ProfilePreview) -> InAppProcessor {
static func sharedImplementation(with iapManager: IAPManager, preview: @escaping (Profile) -> ProfilePreview) -> InAppProcessor {
InAppProcessor(
iapManager: iapManager,
title: {
@ -132,22 +132,24 @@ extension InAppProcessor {
}
extension PreferencesManager {
static let shared: PreferencesManager = {
@MainActor
static func sharedImplementation(withCloudKit: Bool) -> PreferencesManager {
let preferencesStore = CoreDataPersistentStore(
logger: .default,
containerName: Constants.shared.containers.preferences,
baseURL: BundleConfiguration.urlForGroupDocuments,
model: AppData.cdPreferencesModel,
cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitPreferencesId),
cloudKitIdentifier: withCloudKit ? BundleConfiguration.mainString(for: .cloudKitPreferencesId) : nil,
author: nil
)
let modulePreferencesRepository = AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context)
let providerPreferencesRepository = AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context)
return PreferencesManager(
modulesRepository: modulePreferencesRepository,
providersRepository: providerPreferencesRepository
modulesRepository: AppData.cdModulePreferencesRepositoryV3(context: preferencesStore.context),
providersFactory: {
try AppData.cdProviderPreferencesRepositoryV3(context: preferencesStore.context, providerId: $0)
}
)
}()
}
}
// MARK: - Logging

View File

@ -40,7 +40,7 @@ extension AppContext {
[]
}
)
let processor = InAppProcessor.shared(iapManager) {
let processor = InAppProcessor.sharedImplementation(with: iapManager) {
$0.localizedPreview
}

View File

@ -41,10 +41,7 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
provider: self,
decoder: Registry.sharedProtocolCoder,
registry: .shared,
environment: .shared,
profileBlock: {
$0
}
environment: .shared
)
guard let fwd else {
fatalError("NEPTPForwarder nil without throwing error?")