Replace ReloadingContent with implicit animations
Infinite loop on init(), but horrible practice in general. - DonateView - PaywallView+Purchase Also show a ProgressView while rows are loading. DO NOT animate on .products value because animation won't work if products are empty and stay empty after refresh. Instead, observe .isRefreshingProducts. Lastly, to avoid annoying animation when products are actually available, do not refresh products if non-empty. They certainly do not change during the application lifecycle.
This commit is contained in:
parent
96b199425f
commit
d1c98006d3
|
@ -68,7 +68,6 @@
|
||||||
0E9AA978259F756A003FAFF1 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */; };
|
0E9AA978259F756A003FAFF1 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */; };
|
||||||
0E9C233027F47032007D5FC7 /* IntentsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9C232F27F47032007D5FC7 /* IntentsManager.swift */; };
|
0E9C233027F47032007D5FC7 /* IntentsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9C232F27F47032007D5FC7 /* IntentsManager.swift */; };
|
||||||
0E9C233327F47E95007D5FC7 /* IntentDispatcher+Activities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9C233227F47E95007D5FC7 /* IntentDispatcher+Activities.swift */; };
|
0E9C233327F47E95007D5FC7 /* IntentDispatcher+Activities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9C233227F47E95007D5FC7 /* IntentDispatcher+Activities.swift */; };
|
||||||
0E9C3B6C27FB3A9C00D0F02E /* ReloadingContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9C3B6B27FB3A9C00D0F02E /* ReloadingContent.swift */; };
|
|
||||||
0E9C3B6F27FC573E00D0F02E /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E9C3B6E27FC573E00D0F02E /* CloudKit.framework */; };
|
0E9C3B6F27FC573E00D0F02E /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E9C3B6E27FC573E00D0F02E /* CloudKit.framework */; };
|
||||||
0E9E5AEF27B44CF1008C95DA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E9E5AE227B44CF1008C95DA /* Localizable.strings */; };
|
0E9E5AEF27B44CF1008C95DA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E9E5AE227B44CF1008C95DA /* Localizable.strings */; };
|
||||||
0E9ED48127FD9BAE003B2316 /* CopySavingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9ED48027FD9BAE003B2316 /* CopySavingButton.swift */; };
|
0E9ED48127FD9BAE003B2316 /* CopySavingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9ED48027FD9BAE003B2316 /* CopySavingButton.swift */; };
|
||||||
|
@ -250,7 +249,6 @@
|
||||||
0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
||||||
0E9C232F27F47032007D5FC7 /* IntentsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentsManager.swift; sourceTree = "<group>"; };
|
0E9C232F27F47032007D5FC7 /* IntentsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentsManager.swift; sourceTree = "<group>"; };
|
||||||
0E9C233227F47E95007D5FC7 /* IntentDispatcher+Activities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentDispatcher+Activities.swift"; sourceTree = "<group>"; };
|
0E9C233227F47E95007D5FC7 /* IntentDispatcher+Activities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentDispatcher+Activities.swift"; sourceTree = "<group>"; };
|
||||||
0E9C3B6B27FB3A9C00D0F02E /* ReloadingContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadingContent.swift; sourceTree = "<group>"; };
|
|
||||||
0E9C3B6E27FC573E00D0F02E /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
|
0E9C3B6E27FC573E00D0F02E /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
|
||||||
0E9E5AE327B44CF1008C95DA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
0E9E5AE327B44CF1008C95DA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
0E9E5AE427B44CF1008C95DA /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
|
0E9E5AE427B44CF1008C95DA /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
@ -382,7 +380,6 @@
|
||||||
0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */,
|
0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */,
|
||||||
0E5324A827D2AC55002565C3 /* LongContentView.swift */,
|
0E5324A827D2AC55002565C3 /* LongContentView.swift */,
|
||||||
0EBC075427EBC83800208AD9 /* MailComposerView.swift */,
|
0EBC075427EBC83800208AD9 /* MailComposerView.swift */,
|
||||||
0E9C3B6B27FB3A9C00D0F02E /* ReloadingContent.swift */,
|
|
||||||
0ED30DCB27EA197C0057D8A3 /* RevealingSecureField.swift */,
|
0ED30DCB27EA197C0057D8A3 /* RevealingSecureField.swift */,
|
||||||
0E2C172A27CB63F9007E8488 /* Reviewer.swift */,
|
0E2C172A27CB63F9007E8488 /* Reviewer.swift */,
|
||||||
0ED89C1427DE0A0C008B36D6 /* Shortcut.swift */,
|
0ED89C1427DE0A0C008B36D6 /* Shortcut.swift */,
|
||||||
|
@ -902,7 +899,6 @@
|
||||||
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */,
|
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */,
|
||||||
0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */,
|
0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */,
|
||||||
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
|
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
|
||||||
0E9C3B6C27FB3A9C00D0F02E /* ReloadingContent.swift in Sources */,
|
|
||||||
0E5324A627D297BB002565C3 /* InApp.swift in Sources */,
|
0E5324A627D297BB002565C3 /* InApp.swift in Sources */,
|
||||||
0E3B7FCD27E47B3700C66F13 /* AddHostView.swift in Sources */,
|
0E3B7FCD27E47B3700C66F13 /* AddHostView.swift in Sources */,
|
||||||
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,
|
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,
|
||||||
|
|
|
@ -126,7 +126,7 @@
|
||||||
<EnvironmentVariables>
|
<EnvironmentVariables>
|
||||||
<EnvironmentVariable
|
<EnvironmentVariable
|
||||||
key = "APP_TYPE"
|
key = "APP_TYPE"
|
||||||
value = "2"
|
value = "0"
|
||||||
isEnabled = "YES">
|
isEnabled = "YES">
|
||||||
</EnvironmentVariable>
|
</EnvironmentVariable>
|
||||||
<EnvironmentVariable
|
<EnvironmentVariable
|
||||||
|
|
|
@ -112,6 +112,10 @@ class ProductManager: NSObject, ObservableObject {
|
||||||
guard !ids.isEmpty else {
|
guard !ids.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
guard products.isEmpty else {
|
||||||
|
pp_log.debug("In-app products already available, not refreshing")
|
||||||
|
return
|
||||||
|
}
|
||||||
isRefreshingProducts = true
|
isRefreshingProducts = true
|
||||||
inApp.requestProducts(withIdentifiers: ids, completionHandler: { _ in
|
inApp.requestProducts(withIdentifiers: ids, completionHandler: { _ in
|
||||||
pp_log.debug("In-app products: \(self.inApp.products.map { $0.productIdentifier })")
|
pp_log.debug("In-app products: \(self.inApp.products.map { $0.productIdentifier })")
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
//
|
|
||||||
// ReloadingContent.swift
|
|
||||||
// Passepartout
|
|
||||||
//
|
|
||||||
// Created by Davide De Rosa on 4/4/22.
|
|
||||||
// Copyright (c) 2022 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 SwiftUI
|
|
||||||
|
|
||||||
struct ReloadingContent<O: ObservableObject, T: Equatable, Content: View>: View {
|
|
||||||
@Environment(\.scenePhase) private var scenePhase
|
|
||||||
|
|
||||||
@ObservedObject private var object: O
|
|
||||||
|
|
||||||
private let keyPath: KeyPath<O, [T]>
|
|
||||||
|
|
||||||
private var elements: [T] {
|
|
||||||
object[keyPath: keyPath]
|
|
||||||
}
|
|
||||||
|
|
||||||
private let equality: ([T], [T]) -> Bool
|
|
||||||
|
|
||||||
private let reload: (() -> Void)?
|
|
||||||
|
|
||||||
@ViewBuilder private let content: ([T]) -> Content
|
|
||||||
|
|
||||||
@State private var localElements: [T] = []
|
|
||||||
|
|
||||||
init(
|
|
||||||
observing object: O,
|
|
||||||
on keyPath: KeyPath<O, [T]>,
|
|
||||||
equality: @escaping ([T], [T]) -> Bool = { $0 == $1 },
|
|
||||||
reload: (() -> Void)? = nil,
|
|
||||||
@ViewBuilder content: @escaping ([T]) -> Content
|
|
||||||
) {
|
|
||||||
self.object = object
|
|
||||||
self.keyPath = keyPath
|
|
||||||
self.equality = equality
|
|
||||||
self.reload = reload
|
|
||||||
self.content = content
|
|
||||||
|
|
||||||
// XXX: not sure about this, but if content() is empty .onAppear() will
|
|
||||||
// never trigger, thus never setting initial elements
|
|
||||||
//
|
|
||||||
// BEWARE: localElements will not be automatically bound to changes
|
|
||||||
// in elements (use a Binding for that), but this is actually intended
|
|
||||||
_localElements = State(initialValue: elements)
|
|
||||||
if elements.isEmpty {
|
|
||||||
reload?()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
debugChanges()
|
|
||||||
return Group {
|
|
||||||
content(localElements)
|
|
||||||
// }.onAppear {
|
|
||||||
// localElements = elements
|
|
||||||
// if localElements.isEmpty {
|
|
||||||
// reload?()
|
|
||||||
// }
|
|
||||||
}.onChange(of: elements) { newElements in
|
|
||||||
guard !equality(localElements, newElements) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
withAnimation {
|
|
||||||
localElements = newElements
|
|
||||||
}
|
|
||||||
}.onChange(of: scenePhase) {
|
|
||||||
if $0 == .active {
|
|
||||||
reload?()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -44,6 +44,8 @@ struct DonateView: View {
|
||||||
|
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
|
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
||||||
@State private var alertType: AlertType?
|
@State private var alertType: AlertType?
|
||||||
|
@ -63,6 +65,15 @@ struct DonateView: View {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
themeCloseItem(presentationMode: presentationMode)
|
themeCloseItem(presentationMode: presentationMode)
|
||||||
}.alert(item: $alertType, content: presentedAlert)
|
}.alert(item: $alertType, content: presentedAlert)
|
||||||
|
|
||||||
|
// reloading
|
||||||
|
.onAppear {
|
||||||
|
productManager.refreshProducts()
|
||||||
|
}.onChange(of: scenePhase) { newValue in
|
||||||
|
if newValue == .active {
|
||||||
|
productManager.refreshProducts()
|
||||||
|
}
|
||||||
|
}.animation(.default, value: productManager.isRefreshingProducts)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentedAlert(_ alertType: AlertType) -> Alert {
|
private func presentedAlert(_ alertType: AlertType) -> Alert {
|
||||||
|
@ -89,17 +100,10 @@ struct DonateView: View {
|
||||||
footer: Text(L10n.Donate.Sections.OneTime.footer)
|
footer: Text(L10n.Donate.Sections.OneTime.footer)
|
||||||
.xxxThemeTruncation()
|
.xxxThemeTruncation()
|
||||||
) {
|
) {
|
||||||
ReloadingContent(
|
if !productManager.isRefreshingProducts {
|
||||||
observing: productManager,
|
ForEach(productManager.donations, id: \.productIdentifier, content: productRow)
|
||||||
on: \.donations,
|
} else {
|
||||||
equality: {
|
ProgressView()
|
||||||
Set($0.map(\.productIdentifier)) == Set($1.map(\.productIdentifier))
|
|
||||||
},
|
|
||||||
reload: {
|
|
||||||
productManager.refreshProducts()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
ForEach($0, id: \.productIdentifier, content: productRow)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,15 @@ extension PaywallView {
|
||||||
.disabled(purchaseState != nil)
|
.disabled(purchaseState != nil)
|
||||||
}.navigationTitle(Unlocalized.appName)
|
}.navigationTitle(Unlocalized.appName)
|
||||||
.alert(item: $alertType, content: presentedAlert)
|
.alert(item: $alertType, content: presentedAlert)
|
||||||
|
|
||||||
|
// reloading
|
||||||
|
.onAppear {
|
||||||
|
productManager.refreshProducts()
|
||||||
|
}.onChange(of: scenePhase) { newValue in
|
||||||
|
if newValue == .active {
|
||||||
|
productManager.refreshProducts()
|
||||||
|
}
|
||||||
|
}.animation(.default, value: productManager.isRefreshingProducts)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentedAlert(_ alertType: AlertType) -> Alert {
|
private func presentedAlert(_ alertType: AlertType) -> Alert {
|
||||||
|
@ -104,19 +113,12 @@ extension PaywallView {
|
||||||
header: Text(L10n.Paywall.title),
|
header: Text(L10n.Paywall.title),
|
||||||
footer: Text(L10n.Paywall.Sections.Products.footer)
|
footer: Text(L10n.Paywall.Sections.Products.footer)
|
||||||
) {
|
) {
|
||||||
ReloadingContent(
|
if !productManager.isRefreshingProducts {
|
||||||
observing: productManager,
|
|
||||||
on: \.products,
|
|
||||||
equality: {
|
|
||||||
Set($0.map(\.productIdentifier)) == Set($1.map(\.productIdentifier))
|
|
||||||
},
|
|
||||||
reload: {
|
|
||||||
productManager.refreshProducts()
|
|
||||||
}
|
|
||||||
) { _ in
|
|
||||||
ForEach(productRowModels, id: \.product.productIdentifier, content: productRow)
|
ForEach(productRowModels, id: \.product.productIdentifier, content: productRow)
|
||||||
restoreRow
|
} else {
|
||||||
|
ProgressView()
|
||||||
}
|
}
|
||||||
|
restoreRow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue