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:
Davide De Rosa 2022-04-20 19:53:18 +02:00
parent 96b199425f
commit d1c98006d3
6 changed files with 33 additions and 120 deletions

View File

@ -68,7 +68,6 @@
0E9AA978259F756A003FAFF1 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.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 */; };
0E9C3B6C27FB3A9C00D0F02E /* ReloadingContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9C3B6B27FB3A9C00D0F02E /* ReloadingContent.swift */; };
0E9C3B6F27FC573E00D0F02E /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E9C3B6E27FC573E00D0F02E /* CloudKit.framework */; };
0E9E5AEF27B44CF1008C95DA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E9E5AE227B44CF1008C95DA /* Localizable.strings */; };
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>"; };
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>"; };
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; };
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>"; };
@ -382,7 +380,6 @@
0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */,
0E5324A827D2AC55002565C3 /* LongContentView.swift */,
0EBC075427EBC83800208AD9 /* MailComposerView.swift */,
0E9C3B6B27FB3A9C00D0F02E /* ReloadingContent.swift */,
0ED30DCB27EA197C0057D8A3 /* RevealingSecureField.swift */,
0E2C172A27CB63F9007E8488 /* Reviewer.swift */,
0ED89C1427DE0A0C008B36D6 /* Shortcut.swift */,
@ -902,7 +899,6 @@
0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */,
0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */,
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
0E9C3B6C27FB3A9C00D0F02E /* ReloadingContent.swift in Sources */,
0E5324A627D297BB002565C3 /* InApp.swift in Sources */,
0E3B7FCD27E47B3700C66F13 /* AddHostView.swift in Sources */,
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,

View File

@ -126,7 +126,7 @@
<EnvironmentVariables>
<EnvironmentVariable
key = "APP_TYPE"
value = "2"
value = "0"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable

View File

@ -112,6 +112,10 @@ class ProductManager: NSObject, ObservableObject {
guard !ids.isEmpty else {
return
}
guard products.isEmpty else {
pp_log.debug("In-app products already available, not refreshing")
return
}
isRefreshingProducts = true
inApp.requestProducts(withIdentifiers: ids, completionHandler: { _ in
pp_log.debug("In-app products: \(self.inApp.products.map { $0.productIdentifier })")

View File

@ -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?()
}
}
}
}

View File

@ -44,6 +44,8 @@ struct DonateView: View {
@Environment(\.presentationMode) private var presentationMode
@Environment(\.scenePhase) private var scenePhase
@ObservedObject private var productManager: ProductManager
@State private var alertType: AlertType?
@ -63,6 +65,15 @@ struct DonateView: View {
.toolbar {
themeCloseItem(presentationMode: presentationMode)
}.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 {
@ -89,17 +100,10 @@ struct DonateView: View {
footer: Text(L10n.Donate.Sections.OneTime.footer)
.xxxThemeTruncation()
) {
ReloadingContent(
observing: productManager,
on: \.donations,
equality: {
Set($0.map(\.productIdentifier)) == Set($1.map(\.productIdentifier))
},
reload: {
productManager.refreshProducts()
}
) {
ForEach($0, id: \.productIdentifier, content: productRow)
if !productManager.isRefreshingProducts {
ForEach(productManager.donations, id: \.productIdentifier, content: productRow)
} else {
ProgressView()
}
}
}

View File

@ -75,6 +75,15 @@ extension PaywallView {
.disabled(purchaseState != nil)
}.navigationTitle(Unlocalized.appName)
.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 {
@ -104,19 +113,12 @@ extension PaywallView {
header: Text(L10n.Paywall.title),
footer: Text(L10n.Paywall.Sections.Products.footer)
) {
ReloadingContent(
observing: productManager,
on: \.products,
equality: {
Set($0.map(\.productIdentifier)) == Set($1.map(\.productIdentifier))
},
reload: {
productManager.refreshProducts()
}
) { _ in
if !productManager.isRefreshingProducts {
ForEach(productRowModels, id: \.product.productIdentifier, content: productRow)
restoreRow
} else {
ProgressView()
}
restoreRow
}
}