diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj index 30312415..f10fbb64 100644 --- a/Passepartout.xcodeproj/project.pbxproj +++ b/Passepartout.xcodeproj/project.pbxproj @@ -68,7 +68,7 @@ 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 /* ReloadingSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9C3B6B27FB3A9C00D0F02E /* ReloadingSection.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 +250,7 @@ 0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = ""; }; 0E9C232F27F47032007D5FC7 /* IntentsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentsManager.swift; sourceTree = ""; }; 0E9C233227F47E95007D5FC7 /* IntentDispatcher+Activities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IntentDispatcher+Activities.swift"; sourceTree = ""; }; - 0E9C3B6B27FB3A9C00D0F02E /* ReloadingSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadingSection.swift; sourceTree = ""; }; + 0E9C3B6B27FB3A9C00D0F02E /* ReloadingContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReloadingContent.swift; sourceTree = ""; }; 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 = ""; }; 0E9E5AE427B44CF1008C95DA /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; @@ -448,7 +448,7 @@ 0E5324A527D297BB002565C3 /* InApp.swift */, 0EF0FAF827DD212C007EB181 /* IntentActivity.swift */, 0E5324A827D2AC55002565C3 /* LongContentView.swift */, - 0E9C3B6B27FB3A9C00D0F02E /* ReloadingSection.swift */, + 0E9C3B6B27FB3A9C00D0F02E /* ReloadingContent.swift */, 0ED30DCB27EA197C0057D8A3 /* RevealingSecureField.swift */, 0E2C172A27CB63F9007E8488 /* Reviewer.swift */, 0ED89C1427DE0A0C008B36D6 /* Shortcut.swift */, @@ -944,7 +944,7 @@ 0EF2213127E674BD001D0BD7 /* AddProviderViewModel.swift in Sources */, 0E90DFE627BACC1500EF5078 /* AddHostViewModel.swift in Sources */, 0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */, - 0E9C3B6C27FB3A9C00D0F02E /* ReloadingSection.swift in Sources */, + 0E9C3B6C27FB3A9C00D0F02E /* ReloadingContent.swift in Sources */, 0E5324A627D297BB002565C3 /* InApp.swift in Sources */, 0E3B7FCD27E47B3700C66F13 /* AddHostView.swift in Sources */, 0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */, diff --git a/Passepartout/App/Shared/Reusable/ReloadingSection.swift b/Passepartout/App/Shared/Reusable/ReloadingContent.swift similarity index 55% rename from Passepartout/App/Shared/Reusable/ReloadingSection.swift rename to Passepartout/App/Shared/Reusable/ReloadingContent.swift index 50ed4f66..e9b23d02 100644 --- a/Passepartout/App/Shared/Reusable/ReloadingSection.swift +++ b/Passepartout/App/Shared/Reusable/ReloadingContent.swift @@ -1,5 +1,5 @@ // -// ReloadingSection.swift +// ReloadingContent.swift // Passepartout // // Created by Davide De Rosa on 4/4/22. @@ -25,36 +25,49 @@ import SwiftUI -struct ReloadingSection: View { +struct ReloadingContent: View { @Environment(\.scenePhase) private var scenePhase - let header: Header + private let elements: [T] - let footer: Footer + private let equality: ([T], [T]) -> Bool - let elements: [T] + private let reload: (() -> Void)? - var equality: ([T], [T]) -> Bool = { $0 == $1 } - - var isReloading = false - - var reload: (() -> Void)? - - @ViewBuilder let content: ([T]) -> Content + @ViewBuilder private let content: ([T]) -> Content @State private var localElements: [T] = [] + init( + observing elements: [T], + equality: @escaping ([T], [T]) -> Bool = { $0 == $1 }, + reload: (() -> Void)? = nil, + @ViewBuilder content: @escaping ([T]) -> Content + ) { + self.elements = elements + 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 { - Section( - header: header,//progressHeader, - footer: footer - ) { + Group { content(localElements) - }.onAppear { - localElements = elements - if localElements.isEmpty { - reload?() - } +// }.onAppear { +// localElements = elements +// if localElements.isEmpty { +// reload?() +// } }.onChange(of: elements) { newElements in guard !equality(localElements, newElements) else { return @@ -68,14 +81,4 @@ struct ReloadingSection } } } - -// private var progressHeader: some View { -// HStack { -// header -// if isReloading { -// ProgressView() -// .padding(.leading, 5) -// } -// } -// } } diff --git a/Passepartout/App/iOS/Views/DonateView.swift b/Passepartout/App/iOS/Views/DonateView.swift index d2336efa..e35e4666 100644 --- a/Passepartout/App/iOS/Views/DonateView.swift +++ b/Passepartout/App/iOS/Views/DonateView.swift @@ -82,20 +82,22 @@ struct DonateView: View { } private var productsSection: some View { - ReloadingSection( + Section( header: Text(L10n.Donate.Sections.OneTime.header), footer: Text(L10n.Donate.Sections.OneTime.footer) - .xxxThemeTruncation(), - elements: productManager.donations, - equality: { - Set($0.map(\.productIdentifier)) == Set($1.map(\.productIdentifier)) - }, - isReloading: productManager.isRefreshingProducts, - reload: { - productManager.refreshProducts() - } + .xxxThemeTruncation() ) { - ForEach($0, id: \.productIdentifier, content: productRow) + ReloadingContent( + observing: productManager.donations, + equality: { + Set($0.map(\.productIdentifier)) == Set($1.map(\.productIdentifier)) + }, + reload: { + productManager.refreshProducts() + } + ) { + ForEach($0, id: \.productIdentifier, content: productRow) + } } } diff --git a/Passepartout/App/iOS/Views/OrganizerView+Profiles.swift b/Passepartout/App/iOS/Views/OrganizerView+Profiles.swift index 5a88edbd..a108696a 100644 --- a/Passepartout/App/iOS/Views/OrganizerView+Profiles.swift +++ b/Passepartout/App/iOS/Views/OrganizerView+Profiles.swift @@ -53,22 +53,22 @@ extension OrganizerView { var body: some View { debugChanges() - return ReloadingSection( - header: Text(Unlocalized.VPN.vpn), - footer: EmptyView(), - elements: profileManager.headers, - equality: { - Set($0) == Set($1) - } - ) { - if !$0.isEmpty { - ForEach($0.sorted(), content: navigationLink(forHeader:)) - .onAppear(perform: selectActiveProfile) - } else { - AddProfileMenu( - withImportedURLs: false, - bindings: addProfileMenuBindings - ) + return Section { + ReloadingContent( + observing: profileManager.headers, + equality: { + Set($0) == Set($1) + } + ) { + if !$0.isEmpty { + ForEach($0.sorted(), content: navigationLink(forHeader:)) + .onAppear(perform: selectActiveProfile) + } else { + AddProfileMenu( + withImportedURLs: false, + bindings: addProfileMenuBindings + ) + } } }.onAppear(perform: performMigrationsIfNeeded) diff --git a/Passepartout/App/iOS/Views/Paywall/PaywallView+Purchase.swift b/Passepartout/App/iOS/Views/Paywall/PaywallView+Purchase.swift index 25a42db3..41029ca5 100644 --- a/Passepartout/App/iOS/Views/Paywall/PaywallView+Purchase.swift +++ b/Passepartout/App/iOS/Views/Paywall/PaywallView+Purchase.swift @@ -100,22 +100,23 @@ extension PaywallView { } private var productsSection: some View { - ReloadingSection( + Section( header: Text(L10n.Paywall.title), - footer: Text(L10n.Paywall.Sections.Products.footer), - elements: productManager.products, - equality: { - Set($0.map(\.productIdentifier)) == Set($1.map(\.productIdentifier)) - }, - isReloading: productManager.isRefreshingProducts, - reload: { - productManager.refreshProducts() - }, - content: { _ in + footer: Text(L10n.Paywall.Sections.Products.footer) + ) { + ReloadingContent( + observing: productManager.products, + equality: { + Set($0.map(\.productIdentifier)) == Set($1.map(\.productIdentifier)) + }, + reload: { + productManager.refreshProducts() + } + ) { _ in ForEach(productRowModels, id: \.product.productIdentifier, content: productRow) restoreRow } - ) + } } private func productRow(_ model: RowModel) -> some View {