From 3a5e3889d3c68481e53d2f72936749222b20ceb2 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 10 Nov 2024 12:00:07 +0100 Subject: [PATCH] Add more view modifiers (#838) 1. ThemeProgressViewModifier to replace content with a progress view while a condition is active 2. ThemeEmptyContentModifier to replace content with a message if an empty condition is met 3. Replace .opacity(bool ? 1.0 : 0.0) with .opaque(bool) Reuse: - 1 in PaywallView and DonateView - 2 in ProfileContainerView --- .../AppUIMain/Views/About/DonateView.swift | 63 +++++++++---------- .../Views/App/InstalledProfileView.swift | 16 ++--- .../Views/App/ProfileContainerView.swift | 19 ++---- .../AppUIMain/Views/App/ProfileRowView.swift | 2 +- .../iOS/VPNProviderServerView+iOS.swift | 2 +- .../macOS/VPNProviderServerView+macOS.swift | 2 +- .../AppUITV/Views/Profile/ProfileView.swift | 2 +- .../CommonUtils/Views/View+Extensions.swift | 4 ++ .../UILibrary/Theme/UI/Theme+Modifiers.swift | 43 ++++++++++++- .../UILibrary/Theme/UI/Theme+Views.swift | 11 ++++ .../UILibrary/Views/Paywall/PaywallView.swift | 46 +++++++------- .../UILibrary/Views/UI/FavoriteToggle.swift | 8 +-- 12 files changed, 130 insertions(+), 88 deletions(-) diff --git a/Passepartout/Library/Sources/AppUIMain/Views/About/DonateView.swift b/Passepartout/Library/Sources/AppUIMain/Views/About/DonateView.swift index f1857bcc..ba3239ad 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/About/DonateView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/About/DonateView.swift @@ -51,27 +51,19 @@ struct DonateView: View { private var errorHandler: ErrorHandler = .default() var body: some View { - Form { - if isFetchingProducts { - ProgressView() - .id(UUID()) - } else if !availableProducts.isEmpty { - donationsView - .disabled(purchasingIdentifier != nil) + donationsView + .themeProgress(if: isFetchingProducts) + .navigationTitle(title) + .alert( + title, + isPresented: $isThankYouPresented, + actions: thankYouActions, + message: thankYouMessage + ) + .task { + await fetchAvailableProducts() } - } - .themeForm() - .navigationTitle(title) - .alert( - title, - isPresented: $isThankYouPresented, - actions: thankYouActions, - message: thankYouMessage - ) - .task { - await fetchAvailableProducts() - } - .withErrorHandler(errorHandler) + .withErrorHandler(errorHandler) } } @@ -80,24 +72,27 @@ private extension DonateView { Strings.Views.Donate.title } - @ViewBuilder var donationsView: some View { + Form { #if os(macOS) - Section { - Text(Strings.Views.Donate.Sections.Main.footer) - } + Section { + Text(Strings.Views.Donate.Sections.Main.footer) + } #endif - ForEach(availableProducts, id: \.productIdentifier) { - PaywallProductView( - iapManager: iapManager, - style: .donation, - product: $0, - purchasingIdentifier: $purchasingIdentifier, - onComplete: onComplete, - onError: onError - ) + ForEach(availableProducts, id: \.productIdentifier) { + PaywallProductView( + iapManager: iapManager, + style: .donation, + product: $0, + purchasingIdentifier: $purchasingIdentifier, + onComplete: onComplete, + onError: onError + ) + } + .themeSection(footer: Strings.Views.Donate.Sections.Main.footer) } - .themeSection(footer: Strings.Views.Donate.Sections.Main.footer) + .themeForm() + .disabled(purchasingIdentifier != nil) } func thankYouActions() -> some View { diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift index 5cd648cb..08fc3872 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/InstalledProfileView.swift @@ -62,8 +62,8 @@ struct InstalledProfileView: View, Routable { } private extension InstalledProfileView { - var installedOpacity: CGFloat { - profile != nil ? 1.0 : 0.0 + var isOpaque: Bool { + profile != nil } var cardView: some View { @@ -102,7 +102,7 @@ private extension InstalledProfileView { StatusText( theme: theme, tunnel: tunnel, - opacity: installedOpacity + isOpaque: isOpaque ) } } @@ -115,7 +115,7 @@ private extension InstalledProfileView { nextProfileId: $nextProfileId, interactiveManager: interactiveManager, errorHandler: errorHandler, - opacity: installedOpacity, + isOpaque: isOpaque, flow: flow ) } @@ -160,13 +160,13 @@ private struct StatusText: View { @ObservedObject var tunnel: ExtendedTunnel - let opacity: Double + let isOpaque: Bool var body: some View { ConnectionStatusText(tunnel: tunnel) .font(.body) .foregroundStyle(tunnel.statusColor(theme)) - .opacity(opacity) + .opaque(isOpaque) } } @@ -189,7 +189,7 @@ private struct ToggleButton: View { @ObservedObject var errorHandler: ErrorHandler - let opacity: Double + let isOpaque: Bool let flow: ProfileFlow? @@ -209,7 +209,7 @@ private struct ToggleButton: View { // TODO: #584, necessary to avoid cell selection .buttonStyle(.plain) .foregroundStyle(tunnel.statusColor(theme)) - .opacity(opacity) + .opaque(isOpaque) } } diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContainerView.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContainerView.swift index fc9ea079..ca862045 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContainerView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileContainerView.swift @@ -113,20 +113,13 @@ private struct ContainerModifier: ViewModifier { func body(content: Content) -> some View { debugChanges() - return ZStack { - content - .opacity(profileManager.hasProfiles ? 1.0 : 0.0) - - if !profileManager.hasProfiles { - Text(Strings.Views.Profiles.Folders.noProfiles) - .themeEmptyMessage() + return content + .themeEmptyContent(if: !profileManager.hasProfiles, message: Strings.Views.Profiles.Folders.noProfiles) + .searchable(text: $search) + .onChange(of: search) { + profileManager.search(byName: $0) } - } - .searchable(text: $search) - .onChange(of: search) { - profileManager.search(byName: $0) - } - .themeAnimation(on: profileManager.headers, category: .profiles) + .themeAnimation(on: profileManager.headers, category: .profiles) } } diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift index e7dab326..79384b1d 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/ProfileRowView.swift @@ -79,7 +79,7 @@ struct ProfileRowView: View, Routable { private extension ProfileRowView { var markerView: some View { ThemeImage(header.id == nextProfileId ? .pending : statusImage) - .opacity(header.id == nextProfileId || header.id == tunnel.currentProfile?.id ? 1.0 : 0.0) + .opaque(header.id == nextProfileId || header.id == tunnel.currentProfile?.id) .frame(width: 24.0) } diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Provider/iOS/VPNProviderServerView+iOS.swift b/Passepartout/Library/Sources/AppUIMain/Views/Provider/iOS/VPNProviderServerView+iOS.swift index c34655da..03839d51 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/Provider/iOS/VPNProviderServerView+iOS.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/Provider/iOS/VPNProviderServerView+iOS.swift @@ -178,7 +178,7 @@ private extension VPNProviderServerView.ServersSubview { } label: { HStack { ThemeImage(.marked) - .opacity(server.id == selectedServerId ? 1.0 : 0.0) + .opaque(server.id == selectedServerId) VStack(alignment: .leading) { if let area = server.provider.area { Text(area) diff --git a/Passepartout/Library/Sources/AppUIMain/Views/Provider/macOS/VPNProviderServerView+macOS.swift b/Passepartout/Library/Sources/AppUIMain/Views/Provider/macOS/VPNProviderServerView+macOS.swift index d5243539..e793f7aa 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/Provider/macOS/VPNProviderServerView+macOS.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/Provider/macOS/VPNProviderServerView+macOS.swift @@ -68,7 +68,7 @@ extension VPNProviderServerView { return Table(servers) { TableColumn("") { server in ThemeImage(.marked) - .opacity(server.id == selectedServerId ? 1.0 : 0.0) + .opaque(server.id == selectedServerId) } .width(10.0) diff --git a/Passepartout/Library/Sources/AppUITV/Views/Profile/ProfileView.swift b/Passepartout/Library/Sources/AppUITV/Views/Profile/ProfileView.swift index 0f51b84c..e3217598 100644 --- a/Passepartout/Library/Sources/AppUITV/Views/Profile/ProfileView.swift +++ b/Passepartout/Library/Sources/AppUITV/Views/Profile/ProfileView.swift @@ -77,7 +77,7 @@ struct ProfileView: View, TunnelInstallationProviding { ZStack { listView .padding(.horizontal) - .opacity(interactiveManager.isPresented ? 0.0 : 1.0) + .opaque(!interactiveManager.isPresented) if interactiveManager.isPresented { interactiveView diff --git a/Passepartout/Library/Sources/CommonUtils/Views/View+Extensions.swift b/Passepartout/Library/Sources/CommonUtils/Views/View+Extensions.swift index 2e6a81de..fb5cc23f 100644 --- a/Passepartout/Library/Sources/CommonUtils/Views/View+Extensions.swift +++ b/Passepartout/Library/Sources/CommonUtils/Views/View+Extensions.swift @@ -38,6 +38,10 @@ extension View { self } } + + public func opaque(_ condition: Bool) -> some View { + opacity(condition ? 1.0 : 0.0) + } } extension ViewModifier { diff --git a/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift b/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift index 16923eca..0cf64676 100644 --- a/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift +++ b/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Modifiers.swift @@ -24,10 +24,10 @@ // import CommonLibrary +import CommonUtils #if canImport(LocalAuthentication) import LocalAuthentication #endif -import CommonUtils import SwiftUI // MARK: Shortcuts @@ -143,6 +143,14 @@ extension View { modifier(ThemeAnimationModifier(value: value, category: category)) } + public func themeProgress(if isProgressing: Bool) -> some View { + modifier(ThemeProgressViewModifier(isProgressing: isProgressing)) + } + + public func themeEmptyContent(if isEmpty: Bool, message: String) -> some View { + modifier(ThemeEmptyContentModifier(isEmpty: isEmpty, message: message)) + } + #if !os(tvOS) public func themeWindow(width: CGFloat, height: CGFloat) -> some View { modifier(ThemeWindowModifier(size: .init(width: width, height: height))) @@ -349,6 +357,39 @@ struct ThemeAnimationModifier: ViewModifier where T: Equatable { } } +struct ThemeProgressViewModifier: ViewModifier { + let isProgressing: Bool + + func body(content: Content) -> some View { + ZStack { + if isProgressing { + ThemeProgressView() + } + content + .opaque(!isProgressing) + } + } +} + +struct ThemeEmptyContentModifier: ViewModifier { + let isEmpty: Bool + + let message: String + + func body(content: Content) -> some View { + ZStack { + content + .opaque(!isEmpty) + + if isEmpty { + Text(message) + .themeEmptyMessage() + .opaque(isEmpty) + } + } + } +} + #if !os(tvOS) struct ThemeWindowModifier: ViewModifier { diff --git a/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Views.swift b/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Views.swift index 056404d2..115ce41b 100644 --- a/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Views.swift +++ b/Passepartout/Library/Sources/UILibrary/Theme/UI/Theme+Views.swift @@ -225,6 +225,17 @@ public struct ThemeSecureField: View { } } +public struct ThemeProgressView: View { + public init() { + } + + public var body: some View { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .id(UUID()) + } +} + #if !os(tvOS) public struct ThemeMenuImage: View { diff --git a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift index 553abbf0..6151cc22 100644 --- a/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift +++ b/Passepartout/Library/Sources/UILibrary/Views/Paywall/PaywallView.swift @@ -59,31 +59,19 @@ struct PaywallView: View { private var errorHandler: ErrorHandler = .default() var body: some View { - Form { - if isFetchingProducts { - ProgressView() - .id(UUID()) - } else if !recurringProducts.isEmpty { - Group { - productsView - subscriptionFeaturesView - restoreView - } - .disabled(purchasingIdentifier != nil) + paywallView + .themeProgress(if: isFetchingProducts) + .toolbar(content: toolbarContent) + .alert( + Strings.Global.purchase, + isPresented: $isPurchasePendingConfirmation, + actions: pendingActions, + message: pendingMessage + ) + .task(id: feature) { + await fetchAvailableProducts() } - } - .themeForm() - .toolbar(content: toolbarContent) - .alert( - Strings.Global.purchase, - isPresented: $isPurchasePendingConfirmation, - actions: pendingActions, - message: pendingMessage - ) - .task(id: feature) { - await fetchAvailableProducts() - } - .withErrorHandler(errorHandler) + .withErrorHandler(errorHandler) } } @@ -92,6 +80,16 @@ private extension PaywallView { Strings.Global.purchase } + var paywallView: some View { + Form { + productsView + subscriptionFeaturesView + restoreView + } + .themeForm() + .disabled(purchasingIdentifier != nil) + } + var subscriptionFeatures: [AppFeature] { AppFeature.allCases.sorted { $0.localizedDescription.lowercased() < $1.localizedDescription.lowercased() diff --git a/Passepartout/Library/Sources/UILibrary/Views/UI/FavoriteToggle.swift b/Passepartout/Library/Sources/UILibrary/Views/UI/FavoriteToggle.swift index 31463079..f9391eb5 100644 --- a/Passepartout/Library/Sources/UILibrary/Views/UI/FavoriteToggle.swift +++ b/Passepartout/Library/Sources/UILibrary/Views/UI/FavoriteToggle.swift @@ -48,7 +48,7 @@ public struct FavoriteToggle: View where ID: Hashable { } } label: { ThemeImage(selection.contains(value) ? .favoriteOn : .favoriteOff) - .opacity(opacity) + .opaque(opaque) } #if os(macOS) .onHover { @@ -59,11 +59,11 @@ public struct FavoriteToggle: View where ID: Hashable { } private extension FavoriteToggle { - var opacity: Double { + var opaque: Bool { #if os(macOS) - selection.contains(value) || value == hover ? 1.0 : 0.0 + selection.contains(value) || value == hover #else - 1.0 + true #endif } }