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 = {
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
vpnManager.rateLimitMilliseconds = Constants.RateLimit.vpnManager
vpnManager.isOnDemandRulesSupported = {

View File

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

View File

@ -41,7 +41,7 @@ extension OrganizerView {
@State private var isFirstLaunch = true
@State private var selectedProfileId: UUID?
@State private var isPresentingProfile = false
init(alertType: Binding<AlertType?>) {
appManager = .shared
@ -53,7 +53,11 @@ extension OrganizerView {
var body: some View {
debugChanges()
return Group {
return ZStack {
NavigationLink("", isActive: $isPresentingProfile) {
ProfileView()
}.onAppear(perform: presentActiveProfile)
mainView
if profileManager.headers.isEmpty {
emptyView
@ -66,14 +70,14 @@ extension OrganizerView {
// from AddProfileView
.onReceive(profileManager.didCreateProfile) {
selectedProfileId = $0.id
presentProfile(withId: $0.id)
}
}
private var mainView: some View {
List {
Section {
ForEach(sortedHeaders, content: navigationLink(forHeader:))
ForEach(sortedHeaders, content: profileButton(forHeader:))
.onDelete(perform: removeProfiles)
}
}.animation(.default, value: profileManager.headers)
@ -86,27 +90,14 @@ extension OrganizerView {
}
}
private func navigationLink(forHeader header: Profile.Header) -> some View {
NavigationLink(tag: header.id, selection: $selectedProfileId) {
ProfileView(header: header)
private func profileButton(forHeader header: Profile.Header) -> some View {
Button {
presentProfile(withId: header.id)
} label: {
ProfileHeaderRow(
header: header,
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()
}
private func preselectIfActiveProfile(_ id: UUID) {
// 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 {
private func presentActiveProfile() {
guard isFirstLaunch, profileManager.hasActiveProfile else {
return
}
isFirstLaunch = false
selectedProfileId = id
isPresentingProfile = true
}
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() {
@ -149,18 +139,16 @@ extension OrganizerView.ProfilesList {
}
// clear selection before removal to avoid triggering a bogus navigation push
if let selectedProfileId = selectedProfileId, toDelete.contains(selectedProfileId) {
self.selectedProfileId = nil
if toDelete.contains(profileManager.currentProfile.value.id) {
isPresentingProfile = false
}
profileManager.removeProfiles(withIds: toDelete)
}
private func dismissSelectionIfDeleted(headers: [Profile.Header]) {
if let selectedProfileId = selectedProfileId,
!profileManager.isExistingProfile(withId: selectedProfileId) {
self.selectedProfileId = nil
if isPresentingProfile, !profileManager.isCurrentProfileExisting() {
isPresentingProfile = false
}
}
}

View File

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

View File

@ -47,21 +47,18 @@ struct ProfileView: View {
@ObservedObject private var profileManager: ProfileManager
private let header: Profile.Header
private var isLoading: Bool {
profileManager.isLoadingCurrentProfile
}
private var isExisting: Bool {
profileManager.isExistingProfile(withId: header.id)
profileManager.isCurrentProfileExisting()
}
@State private var modalType: ModalType?
@State private var isLoaded = false
init(header: Profile.Header?) {
let profileManager: ProfileManager = .shared
self.profileManager = profileManager
self.header = header ?? profileManager.activeHeader ?? Profile.placeholder.header
init() {
profileManager = .shared
}
var body: some View {
@ -84,22 +81,21 @@ struct ProfileView: View {
).disabled(!isExisting)
}
}.sheet(item: $modalType, content: presentedModal)
.onAppear(perform: loadProfileIfNeeded)
.navigationTitle(title)
.themeSecondaryView()
}
private var title: String {
isExisting ? header.name : ""
profileManager.currentProfile.name
}
private var mainView: some View {
List {
VPNSection(
currentProfile: profileManager.currentProfile,
isLoaded: isLoaded
isLoading: isLoading
)
if isLoaded {
if !isLoading {
ProviderSection(currentProfile: profileManager.currentProfile)
ConfigurationSection(
currentProfile: profileManager.currentProfile,
@ -156,37 +152,6 @@ struct ProfileView: View {
}.themeGlobal()
}
}
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() {
modalType = .paywallTrustedNetworks

View File

@ -47,7 +47,7 @@ public class ProfileManager: ObservableObject {
public var availabilityFilter: ((Profile.Header) -> Bool)?
public var activeProfileId: UUID? {
private var activeProfileId: UUID? {
willSet {
willUpdateActiveId.send(newValue)
}
@ -58,6 +58,8 @@ public class ProfileManager: ObservableObject {
// MARK: Observables
@Published public private(set) var isLoadingCurrentProfile = false
public let currentProfile: ObservableProfile
public let willUpdateActiveId = PassthroughSubject<UUID?, Never>()
@ -85,6 +87,15 @@ public class ProfileManager: ObservableObject {
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
@ -104,6 +115,10 @@ extension ProfileManager {
public var headers: [Profile.Header] {
availableHeaders
}
public var hasActiveProfile: Bool {
activeHeader != nil
}
public var activeHeader: Profile.Header? {
availableHeaders.first {
@ -232,7 +247,16 @@ extension ProfileManager {
// MARK: Observation
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) {
pp_log.info("Committing changes of former current profile \(currentProfile.value.logDescription)")
saveCurrentProfile()
@ -240,14 +264,28 @@ extension ProfileManager {
do {
let result = try loadProfile(withId: id)
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 {
currentProfile.value = .placeholder
isLoadingCurrentProfile = false
throw error
}
}
public func isCurrentProfileExisting() -> Bool {
isExistingProfile(withId: currentProfile.value.id)
}
public func isCurrentProfileActive() -> Bool {
currentProfile.value.id == activeProfileId
}

View File

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