Rewrite ProfileView as a view of currentProfile

Do not load profile in View, instead:

- Load active profile on app launch

- Load selected profile on organizer selection
This commit is contained in:
Davide De Rosa 2022-04-22 15:58:03 +02:00
parent 8838e9d130
commit 2432f0d97a
7 changed files with 92 additions and 95 deletions

View File

@ -132,7 +132,13 @@ class AppContext {
profileManager.availabilityFilter = { profileManager.availabilityFilter = {
self.isEligibleProfile(withHeader: $0) self.isEligibleProfile(withHeader: $0)
} }
profileManager.activeProfileId = appManager.activeProfileId if let activeProfileId = appManager.activeProfileId {
do {
try profileManager.loadActiveProfile(withId: activeProfileId)
} catch {
pp_log.warning("Unable to load active profile: \(error)")
}
}
providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager
vpnManager.rateLimitMilliseconds = Constants.RateLimit.vpnManager vpnManager.rateLimitMilliseconds = Constants.RateLimit.vpnManager
vpnManager.isOnDemandRulesSupported = { vpnManager.isOnDemandRulesSupported = {

View File

@ -31,7 +31,7 @@ struct MainView: View {
OrganizerView() OrganizerView()
.themePrimaryView() .themePrimaryView()
ProfileView(header: nil) ProfileView()
}.themeGlobal() }.themeGlobal()
} }
} }

View File

@ -41,7 +41,7 @@ extension OrganizerView {
@State private var isFirstLaunch = true @State private var isFirstLaunch = true
@State private var selectedProfileId: UUID? @State private var isPresentingProfile = false
init(alertType: Binding<AlertType?>) { init(alertType: Binding<AlertType?>) {
appManager = .shared appManager = .shared
@ -53,7 +53,11 @@ extension OrganizerView {
var body: some View { var body: some View {
debugChanges() debugChanges()
return Group { return ZStack {
NavigationLink("", isActive: $isPresentingProfile) {
ProfileView()
}.onAppear(perform: presentActiveProfile)
mainView mainView
if profileManager.headers.isEmpty { if profileManager.headers.isEmpty {
emptyView emptyView
@ -66,14 +70,14 @@ extension OrganizerView {
// from AddProfileView // from AddProfileView
.onReceive(profileManager.didCreateProfile) { .onReceive(profileManager.didCreateProfile) {
selectedProfileId = $0.id presentProfile(withId: $0.id)
} }
} }
private var mainView: some View { private var mainView: some View {
List { List {
Section { Section {
ForEach(sortedHeaders, content: navigationLink(forHeader:)) ForEach(sortedHeaders, content: profileButton(forHeader:))
.onDelete(perform: removeProfiles) .onDelete(perform: removeProfiles)
} }
}.animation(.default, value: profileManager.headers) }.animation(.default, value: profileManager.headers)
@ -86,27 +90,14 @@ extension OrganizerView {
} }
} }
private func navigationLink(forHeader header: Profile.Header) -> some View { private func profileButton(forHeader header: Profile.Header) -> some View {
NavigationLink(tag: header.id, selection: $selectedProfileId) { Button {
ProfileView(header: header) presentProfile(withId: header.id)
} label: { } label: {
ProfileHeaderRow( ProfileHeaderRow(
header: header, header: header,
isActive: profileManager.isActiveProfile(header.id) isActive: profileManager.isActiveProfile(header.id)
) )
}.onAppear {
preselectIfActiveProfile(header.id)
// XXX: iOS 14 bug, if selectedProfileId is set before its NavigationLink
// has appeared, the NavigationLink will not auto-activate once appeared
// enforce activation by clearing and resetting selectedProfileId to its
// current value
withAnimation {
if let tmp = selectedProfileId, tmp == header.id {
selectedProfileId = nil
selectedProfileId = tmp
}
}
} }
} }
} }
@ -117,22 +108,21 @@ extension OrganizerView.ProfilesList {
profileManager.headers.sorted() profileManager.headers.sorted()
} }
private func preselectIfActiveProfile(_ id: UUID) { private func presentActiveProfile() {
guard isFirstLaunch, profileManager.hasActiveProfile else {
// do not push profile if:
//
// - an alert is active, as it would break navigation
// - on iPad, as it's already shown
//
guard alertType == nil, themeIdiom != .pad, id == profileManager.activeHeader?.id else {
return
}
guard isFirstLaunch else {
return return
} }
isFirstLaunch = false isFirstLaunch = false
isPresentingProfile = true
}
selectedProfileId = id private func presentProfile(withId id: UUID) {
do {
try profileManager.loadCurrentProfile(withId: id, makeReady: true)
isPresentingProfile = true
} catch {
pp_log.error("Unable to load profile: \(error)")
}
} }
private func performMigrationsIfNeeded() { private func performMigrationsIfNeeded() {
@ -149,18 +139,16 @@ extension OrganizerView.ProfilesList {
} }
// clear selection before removal to avoid triggering a bogus navigation push // clear selection before removal to avoid triggering a bogus navigation push
if let selectedProfileId = selectedProfileId, toDelete.contains(selectedProfileId) { if toDelete.contains(profileManager.currentProfile.value.id) {
self.selectedProfileId = nil isPresentingProfile = false
} }
profileManager.removeProfiles(withIds: toDelete) profileManager.removeProfiles(withIds: toDelete)
} }
private func dismissSelectionIfDeleted(headers: [Profile.Header]) { private func dismissSelectionIfDeleted(headers: [Profile.Header]) {
if let selectedProfileId = selectedProfileId, if isPresentingProfile, !profileManager.isCurrentProfileExisting() {
!profileManager.isExistingProfile(withId: selectedProfileId) { isPresentingProfile = false
self.selectedProfileId = nil
} }
} }
} }

View File

@ -42,7 +42,7 @@ extension ProfileView {
@ObservedObject private var currentProfile: ObservableProfile @ObservedObject private var currentProfile: ObservableProfile
private let isLoaded: Bool private let isLoading: Bool
private var isActiveProfile: Bool { private var isActiveProfile: Bool {
profileManager.isCurrentProfileActive() profileManager.isCurrentProfileActive()
@ -52,7 +52,7 @@ extension ProfileView {
productManager.isEligible(forFeature: .siriShortcuts) productManager.isEligible(forFeature: .siriShortcuts)
} }
init(currentProfile: ObservableProfile, isLoaded: Bool) { init(currentProfile: ObservableProfile, isLoading: Bool) {
appManager = .shared appManager = .shared
profileManager = .shared profileManager = .shared
providerManager = .shared providerManager = .shared
@ -60,11 +60,11 @@ extension ProfileView {
currentVPNState = .shared currentVPNState = .shared
productManager = .shared productManager = .shared
self.currentProfile = currentProfile self.currentProfile = currentProfile
self.isLoaded = isLoaded self.isLoading = isLoading
} }
var body: some View { var body: some View {
if isLoaded { if !isLoading {
if isActiveProfile { if isActiveProfile {
activeView activeView
} else { } else {
@ -114,7 +114,7 @@ extension ProfileView {
profileManager.activateCurrentProfile() profileManager.activateCurrentProfile()
// IMPORTANT: save immediately to keep in sync with VPN status // IMPORTANT: save immediately to keep in sync with VPN status
appManager.activeProfileId = profileManager.activeProfileId appManager.activeProfileId = profileManager.activeHeader?.id
} }
Task { Task {
await vpnManager.disable() await vpnManager.disable()

View File

@ -47,21 +47,18 @@ struct ProfileView: View {
@ObservedObject private var profileManager: ProfileManager @ObservedObject private var profileManager: ProfileManager
private let header: Profile.Header private var isLoading: Bool {
profileManager.isLoadingCurrentProfile
}
private var isExisting: Bool { private var isExisting: Bool {
profileManager.isExistingProfile(withId: header.id) profileManager.isCurrentProfileExisting()
} }
@State private var modalType: ModalType? @State private var modalType: ModalType?
@State private var isLoaded = false init() {
profileManager = .shared
init(header: Profile.Header?) {
let profileManager: ProfileManager = .shared
self.profileManager = profileManager
self.header = header ?? profileManager.activeHeader ?? Profile.placeholder.header
} }
var body: some View { var body: some View {
@ -84,22 +81,21 @@ struct ProfileView: View {
).disabled(!isExisting) ).disabled(!isExisting)
} }
}.sheet(item: $modalType, content: presentedModal) }.sheet(item: $modalType, content: presentedModal)
.onAppear(perform: loadProfileIfNeeded)
.navigationTitle(title) .navigationTitle(title)
.themeSecondaryView() .themeSecondaryView()
} }
private var title: String { private var title: String {
isExisting ? header.name : "" profileManager.currentProfile.name
} }
private var mainView: some View { private var mainView: some View {
List { List {
VPNSection( VPNSection(
currentProfile: profileManager.currentProfile, currentProfile: profileManager.currentProfile,
isLoaded: isLoaded isLoading: isLoading
) )
if isLoaded { if !isLoading {
ProviderSection(currentProfile: profileManager.currentProfile) ProviderSection(currentProfile: profileManager.currentProfile)
ConfigurationSection( ConfigurationSection(
currentProfile: profileManager.currentProfile, currentProfile: profileManager.currentProfile,
@ -157,37 +153,6 @@ struct ProfileView: View {
} }
} }
private func loadProfileIfNeeded() {
guard !isLoaded else {
return
}
guard !header.isPlaceholder else {
pp_log.debug("ProfileView is a placeholder for WelcomeView, no active profile")
return
}
do {
let result = try profileManager.loadCurrentProfile(withId: header.id)
if result.isReady {
isLoaded = true
return
}
Task {
do {
try await profileManager.makeProfileReady(result.profile)
withAnimation {
isLoaded = true
}
} catch {
pp_log.error("Profile \(header.id) could not be made ready: \(error)")
presentationMode.wrappedValue.dismiss()
}
}
} catch {
pp_log.error("Profile \(header.id) could not be loaded: \(error)")
presentationMode.wrappedValue.dismiss()
}
}
private func presentPaywallTrustedNetworks() { private func presentPaywallTrustedNetworks() {
modalType = .paywallTrustedNetworks modalType = .paywallTrustedNetworks
} }

View File

@ -47,7 +47,7 @@ public class ProfileManager: ObservableObject {
public var availabilityFilter: ((Profile.Header) -> Bool)? public var availabilityFilter: ((Profile.Header) -> Bool)?
public var activeProfileId: UUID? { private var activeProfileId: UUID? {
willSet { willSet {
willUpdateActiveId.send(newValue) willUpdateActiveId.send(newValue)
} }
@ -58,6 +58,8 @@ public class ProfileManager: ObservableObject {
// MARK: Observables // MARK: Observables
@Published public private(set) var isLoadingCurrentProfile = false
public let currentProfile: ObservableProfile public let currentProfile: ObservableProfile
public let willUpdateActiveId = PassthroughSubject<UUID?, Never>() public let willUpdateActiveId = PassthroughSubject<UUID?, Never>()
@ -85,6 +87,15 @@ public class ProfileManager: ObservableObject {
currentProfile = ObservableProfile() currentProfile = ObservableProfile()
} }
public func loadActiveProfile(withId id: UUID) throws {
guard isExistingProfile(withId: id) else {
pp_log.warning("Active profile \(id) does not exist, ignoring")
return
}
activeProfileId = id
try loadCurrentProfile(withId: id, makeReady: true)
}
} }
// MARK: Index // MARK: Index
@ -105,6 +116,10 @@ extension ProfileManager {
availableHeaders availableHeaders
} }
public var hasActiveProfile: Bool {
activeHeader != nil
}
public var activeHeader: Profile.Header? { public var activeHeader: Profile.Header? {
availableHeaders.first { availableHeaders.first {
$0.id == activeProfileId $0.id == activeProfileId
@ -232,7 +247,16 @@ extension ProfileManager {
// MARK: Observation // MARK: Observation
extension ProfileManager { extension ProfileManager {
public func loadCurrentProfile(withId id: UUID) throws -> LoadResult { public func loadCurrentProfile(withId id: UUID, makeReady: Bool) throws {
guard !isLoadingCurrentProfile else {
pp_log.warning("Already loading another profile")
return
}
guard id != currentProfile.value.id else {
pp_log.debug("Profile \(id) is already current profile")
return
}
isLoadingCurrentProfile = true
if isExistingProfile(withId: currentProfile.value.id) { if isExistingProfile(withId: currentProfile.value.id) {
pp_log.info("Committing changes of former current profile \(currentProfile.value.logDescription)") pp_log.info("Committing changes of former current profile \(currentProfile.value.logDescription)")
saveCurrentProfile() saveCurrentProfile()
@ -240,14 +264,28 @@ extension ProfileManager {
do { do {
let result = try loadProfile(withId: id) let result = try loadProfile(withId: id)
pp_log.info("Current profile: \(result.profile.logDescription)") pp_log.info("Current profile: \(result.profile.logDescription)")
currentProfile.value = result.profile
return result if !makeReady || result.isReady {
currentProfile.value = result.profile
isLoadingCurrentProfile = false
} else {
Task {
try await makeProfileReady(result.profile)
currentProfile.value = result.profile
isLoadingCurrentProfile = false
}
}
} catch { } catch {
currentProfile.value = .placeholder currentProfile.value = .placeholder
isLoadingCurrentProfile = false
throw error throw error
} }
} }
public func isCurrentProfileExisting() -> Bool {
isExistingProfile(withId: currentProfile.value.id)
}
public func isCurrentProfileActive() -> Bool { public func isCurrentProfileActive() -> Bool {
currentProfile.value.id == activeProfileId currentProfile.value.id == activeProfileId
} }

View File

@ -30,7 +30,7 @@ public class ObservableProfile: ValueHolder, ObservableObject {
@Published public var value: Profile @Published public var value: Profile
public var name: String { public var name: String {
value.header.name !value.header.isPlaceholder ? value.header.name : ""
} }
public init() { public init() {