Simplify AddProfileView with implicit animations
- Animate on ViewModel in profile name views - Animate on providers in provider selection view
This commit is contained in:
parent
36cd9cfd96
commit
e71b22c7c8
|
@ -60,7 +60,7 @@ struct AddHostView: View {
|
||||||
} else {
|
} else {
|
||||||
completeView
|
completeView
|
||||||
}
|
}
|
||||||
}
|
}.animation(.default, value: viewModel)
|
||||||
|
|
||||||
// hidden
|
// hidden
|
||||||
NavigationLink("", isActive: $isEnteringCredentials) {
|
NavigationLink("", isActive: $isEnteringCredentials) {
|
||||||
|
@ -159,18 +159,13 @@ struct AddHostView: View {
|
||||||
title: Text(L10n.AddProfile.Shared.title),
|
title: Text(L10n.AddProfile.Shared.title),
|
||||||
message: Text(L10n.AddProfile.Shared.Alerts.Overwrite.message),
|
message: Text(L10n.AddProfile.Shared.Alerts.Overwrite.message),
|
||||||
primaryButton: .destructive(Text(L10n.Global.Strings.ok)) {
|
primaryButton: .destructive(Text(L10n.Global.Strings.ok)) {
|
||||||
|
|
||||||
// XXX: delay withAnimation() to not overlap with alert dismiss animation
|
|
||||||
Task {
|
|
||||||
processProfile(replacingExisting: true)
|
processProfile(replacingExisting: true)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
secondaryButton: .cancel(Text(L10n.Global.Strings.cancel))
|
secondaryButton: .cancel(Text(L10n.Global.Strings.cancel))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processProfile(replacingExisting: Bool) {
|
private func processProfile(replacingExisting: Bool) {
|
||||||
withAnimation {
|
|
||||||
viewModel.processURL(
|
viewModel.processURL(
|
||||||
url,
|
url,
|
||||||
with: profileManager,
|
with: profileManager,
|
||||||
|
@ -178,12 +173,9 @@ struct AddHostView: View {
|
||||||
deletingURLOnSuccess: deletingURLOnSuccess
|
deletingURLOnSuccess: deletingURLOnSuccess
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func saveProfile() {
|
private func saveProfile() {
|
||||||
let result = withAnimation {
|
let result = viewModel.addProcessedProfile(to: profileManager)
|
||||||
viewModel.addProcessedProfile(to: profileManager)
|
|
||||||
}
|
|
||||||
guard result else {
|
guard result else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ import TunnelKitOpenVPN
|
||||||
import TunnelKitWireGuard
|
import TunnelKitWireGuard
|
||||||
|
|
||||||
extension AddHostView {
|
extension AddHostView {
|
||||||
struct ViewModel {
|
struct ViewModel: Equatable {
|
||||||
private var isNamePreset = false
|
private var isNamePreset = false
|
||||||
|
|
||||||
var profileName = ""
|
var profileName = ""
|
||||||
|
|
|
@ -96,24 +96,18 @@ extension AddProviderView {
|
||||||
title: Text(L10n.AddProfile.Shared.title),
|
title: Text(L10n.AddProfile.Shared.title),
|
||||||
message: Text(L10n.AddProfile.Shared.Alerts.Overwrite.message),
|
message: Text(L10n.AddProfile.Shared.Alerts.Overwrite.message),
|
||||||
primaryButton: .destructive(Text(L10n.Global.Strings.ok)) {
|
primaryButton: .destructive(Text(L10n.Global.Strings.ok)) {
|
||||||
|
|
||||||
// XXX: delay withAnimation() to not overlap with alert dismiss animation
|
|
||||||
Task {
|
|
||||||
saveProfile(replacingExisting: true)
|
saveProfile(replacingExisting: true)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
secondaryButton: .cancel(Text(L10n.Global.Strings.cancel))
|
secondaryButton: .cancel(Text(L10n.Global.Strings.cancel))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveProfile(replacingExisting: Bool) {
|
private func saveProfile(replacingExisting: Bool) {
|
||||||
let addedProfile = withAnimation {
|
let addedProfile = viewModel.addProfile(
|
||||||
viewModel.addProfile(
|
|
||||||
profile,
|
profile,
|
||||||
to: profileManager,
|
to: profileManager,
|
||||||
replacingExisting: replacingExisting
|
replacingExisting: replacingExisting
|
||||||
)
|
)
|
||||||
}
|
|
||||||
guard let addedProfile = addedProfile else {
|
guard let addedProfile = addedProfile else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,9 +41,16 @@ struct AddProviderView: View {
|
||||||
self.bindings = bindings
|
self.bindings = bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var providers: [ProviderMetadata] {
|
||||||
|
providerManager.allProviders()
|
||||||
|
.filter {
|
||||||
|
$0.supportedVPNProtocols.contains(viewModel.selectedVPNProtocol)
|
||||||
|
}.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
private var availableVPNProtocols: [VPNProtocolType] {
|
private var availableVPNProtocols: [VPNProtocolType] {
|
||||||
var protos: Set<VPNProtocolType> = []
|
var protos: Set<VPNProtocolType> = []
|
||||||
viewModel.providers.forEach {
|
providers.forEach {
|
||||||
$0.supportedVPNProtocols.forEach {
|
$0.supportedVPNProtocols.forEach {
|
||||||
protos.insert($0)
|
protos.insert($0)
|
||||||
}
|
}
|
||||||
|
@ -70,16 +77,17 @@ struct AddProviderView: View {
|
||||||
ScrollViewReader { scrollProxy in
|
ScrollViewReader { scrollProxy in
|
||||||
List {
|
List {
|
||||||
mainSection
|
mainSection
|
||||||
if !viewModel.providers.isEmpty {
|
if !providers.isEmpty {
|
||||||
providersSection
|
providersSection
|
||||||
}
|
}
|
||||||
}.onChange(of: viewModel.errorMessage) {
|
}.onChange(of: viewModel.errorMessage) {
|
||||||
onErrorMessage($0, scrollProxy)
|
onErrorMessage($0, scrollProxy)
|
||||||
}.disabled(viewModel.pendingOperation != nil)
|
}.disabled(viewModel.pendingOperation != nil)
|
||||||
|
.animation(.default, value: providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// hidden
|
// hidden
|
||||||
ForEach(viewModel.providers, id: \.navigationId, content: providerNavigationLink)
|
ForEach(providers, id: \.navigationId, content: providerNavigationLink)
|
||||||
}.themeSecondaryView()
|
}.themeSecondaryView()
|
||||||
.navigationTitle(L10n.AddProfile.Shared.title)
|
.navigationTitle(L10n.AddProfile.Shared.title)
|
||||||
.toolbar(content: toolbar)
|
.toolbar(content: toolbar)
|
||||||
|
@ -87,12 +95,6 @@ struct AddProviderView: View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
PaywallView(isPresented: $viewModel.isPaywallPresented)
|
PaywallView(isPresented: $viewModel.isPaywallPresented)
|
||||||
}.themeGlobal()
|
}.themeGlobal()
|
||||||
}.onAppear {
|
|
||||||
refreshProviders()
|
|
||||||
}.onChange(of: viewModel.newProviders) { newValue in
|
|
||||||
withAnimation {
|
|
||||||
refreshProviders(newValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +124,7 @@ struct AddProviderView: View {
|
||||||
Section(
|
Section(
|
||||||
footer: themeErrorMessage(viewModel.errorMessage)
|
footer: themeErrorMessage(viewModel.errorMessage)
|
||||||
) {
|
) {
|
||||||
ForEach(viewModel.providers, content: providerRow)
|
ForEach(providers, content: providerRow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,13 +152,6 @@ struct AddProviderView: View {
|
||||||
}.withTrailingProgress(when: isUpdatingIndex)
|
}.withTrailingProgress(when: isUpdatingIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshProviders(_ newProviders: [ProviderMetadata]? = nil) {
|
|
||||||
viewModel.providers = (newProviders ?? providerManager.allProviders())
|
|
||||||
.filter {
|
|
||||||
$0.supportedVPNProtocols.contains(viewModel.selectedVPNProtocol)
|
|
||||||
}.sorted()
|
|
||||||
}
|
|
||||||
|
|
||||||
// eligibility: select or purchase provider
|
// eligibility: select or purchase provider
|
||||||
private func presentOrPurchaseProvider(_ metadata: ProviderMetadata) {
|
private func presentOrPurchaseProvider(_ metadata: ProviderMetadata) {
|
||||||
if productManager.isEligible(forProvider: metadata.name) {
|
if productManager.isEligible(forProvider: metadata.name) {
|
||||||
|
@ -176,7 +171,7 @@ struct AddProviderView: View {
|
||||||
|
|
||||||
extension AddProviderView {
|
extension AddProviderView {
|
||||||
private func scrollToErrorMessage(_ proxy: ScrollViewProxy) {
|
private func scrollToErrorMessage(_ proxy: ScrollViewProxy) {
|
||||||
proxy.maybeScrollTo(viewModel.providers.last?.id, animated: true)
|
proxy.maybeScrollTo(providers.last?.id, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,11 +34,6 @@ extension AddProviderView {
|
||||||
case provider(ProviderName)
|
case provider(ProviderName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// local copy for animations
|
|
||||||
@Published var providers: [ProviderMetadata] = []
|
|
||||||
|
|
||||||
@Published private(set) var newProviders: [ProviderMetadata] = []
|
|
||||||
|
|
||||||
@Published var selectedVPNProtocol: VPNProtocolType = .openVPN
|
@Published var selectedVPNProtocol: VPNProtocolType = .openVPN
|
||||||
|
|
||||||
@Published var selectedProvider: ProviderMetadata?
|
@Published var selectedProvider: ProviderMetadata?
|
||||||
|
@ -107,8 +102,6 @@ extension AddProviderView {
|
||||||
try await providerManager.fetchProvidersIndexPublisher(
|
try await providerManager.fetchProvidersIndexPublisher(
|
||||||
priority: .remoteThenBundle
|
priority: .remoteThenBundle
|
||||||
).async()
|
).async()
|
||||||
|
|
||||||
newProviders = providerManager.allProviders()
|
|
||||||
} catch {
|
} catch {
|
||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
}
|
}
|
||||||
|
@ -123,7 +116,7 @@ extension AddProviderView {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AddProviderView.NameView {
|
extension AddProviderView.NameView {
|
||||||
struct ViewModel {
|
struct ViewModel: Equatable {
|
||||||
private var isNamePreset = false
|
private var isNamePreset = false
|
||||||
|
|
||||||
var profileName = ""
|
var profileName = ""
|
||||||
|
|
Loading…
Reference in New Issue