From 237277d4db8b18b2f89568e822e407fdaddb507f Mon Sep 17 00:00:00 2001 From: Davide Date: Thu, 31 Oct 2024 10:02:21 +0100 Subject: [PATCH] Do some refactoring in AppUI targets (#789) - Refactor AppUI initialization in all platforms (sort of template method pattern) - Make AppMenu specific to macOS by wrapping it into a folder for consistency - Add SizeClassProviding for repeated checks on hsClass/vsClass Fixes #659 --- .github/workflows/release.yml | 6 +- Passepartout/App/AppDelegate.swift | 17 +---- Passepartout/App/Platforms/App+iOS.swift | 2 +- Passepartout/App/Platforms/App+macOS.swift | 2 +- Passepartout/App/Platforms/App+tvOS.swift | 2 +- .../xcshareddata/xcschemes/AppUIMain.xcscheme | 67 +++++++++++++++++++ .../xcshareddata/xcschemes/AppUITV.xcscheme | 67 +++++++++++++++++++ .../Library/Sources/AppUI/AppUI.swift | 33 +++++++-- .../Sources/AppUI/Theme/Theme+UI.swift | 8 +-- .../Library/Sources/AppUIMain/AppUIMain.swift | 12 ++-- .../AppUIMain/Views/App/AppCoordinator.swift | 9 +-- .../AppUIMain/Views/App/AppToolbar.swift | 9 +-- .../AppMenu/{ => macOS}/AppMenu+Model.swift | 0 .../Views/AppMenu/{ => macOS}/AppMenu.swift | 0 .../AppMenu/{ => macOS}/AppMenuImage.swift | 0 .../Views/AppMenu/{ => macOS}/AppWindow.swift | 0 .../Library/Sources/AppUITV/AppUITV.swift | 8 ++- .../AppUITV/Views/AppCoordinator.swift | 2 +- .../CommonLibrary/Domain/Constants.swift | 2 + .../CommonLibrary/Resources/Constants.json | 1 + .../Sources/CommonLibrary/Shared.swift | 2 +- .../UtilsLibrary/Views/LongContentView.swift | 15 ++++- .../Views/SizeClassProviding.swift | 38 +++++++++++ 23 files changed, 250 insertions(+), 52 deletions(-) create mode 100644 Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/AppUIMain.xcscheme create mode 100644 Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/AppUITV.xcscheme rename Passepartout/Library/Sources/AppUIMain/Views/AppMenu/{ => macOS}/AppMenu+Model.swift (100%) rename Passepartout/Library/Sources/AppUIMain/Views/AppMenu/{ => macOS}/AppMenu.swift (100%) rename Passepartout/Library/Sources/AppUIMain/Views/AppMenu/{ => macOS}/AppMenuImage.swift (100%) rename Passepartout/Library/Sources/AppUIMain/Views/AppMenu/{ => macOS}/AppWindow.swift (100%) create mode 100644 Passepartout/Library/Sources/UtilsLibrary/Views/SizeClassProviding.swift diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6563a4d9..ee1e1a06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,8 +24,7 @@ jobs: strategy: fail-fast: true matrix: - #platform: ["iOS", "macOS", "tvOS"] - platform: ["iOS", "macOS"] + platform: ["iOS", "macOS", "tvOS"] steps: - uses: passepartoutvpn/action-prepare-xcode-build@master with: @@ -64,8 +63,7 @@ jobs: PILOT_GROUPS: ${{ vars.PILOT_GROUPS }} PILOT_NOTIFY_EXTERNAL_TESTERS: ${{ vars.PILOT_NOTIFY_EXTERNAL_TESTERS }} run: | - #PLATFORMS=("iOS" "macOS" "tvOS") - PLATFORMS=("iOS" "macOS") + PLATFORMS=("iOS" "macOS" "tvOS") for PLATFORM in ${PLATFORMS[@]}; do bundle exec fastlane --env $PLATFORM public_beta done diff --git a/Passepartout/App/AppDelegate.swift b/Passepartout/App/AppDelegate.swift index f51762c7..ed8e1ec0 100644 --- a/Passepartout/App/AppDelegate.swift +++ b/Passepartout/App/AppDelegate.swift @@ -33,19 +33,8 @@ final class AppDelegate: NSObject { let context: AppContext = .shared // let context: AppContext = .mock(withRegistry: .shared) - func configure() { - PassepartoutConfiguration.shared.configureLogging( - to: BundleConfiguration.urlForAppLog, - parameters: Constants.shared.log, - logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key) - ) - AppUI.configure(with: context) - -#if os(macOS) - // keep this for login item because scenePhase is not triggered - Task { - try await context.tunnel.prepare(purge: true) - } -#endif + func configure(with appUIConfiguring: AppUIConfiguring) { + AppUI(appUIConfiguring) + .configure(with: context) } } diff --git a/Passepartout/App/Platforms/App+iOS.swift b/Passepartout/App/Platforms/App+iOS.swift index 182f81a3..12ff2149 100644 --- a/Passepartout/App/Platforms/App+iOS.swift +++ b/Passepartout/App/Platforms/App+iOS.swift @@ -30,7 +30,7 @@ import SwiftUI extension AppDelegate: UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - configure() + configure(with: AppUIMain()) return true } } diff --git a/Passepartout/App/Platforms/App+macOS.swift b/Passepartout/App/Platforms/App+macOS.swift index e482493d..fcd267f0 100644 --- a/Passepartout/App/Platforms/App+macOS.swift +++ b/Passepartout/App/Platforms/App+macOS.swift @@ -32,8 +32,8 @@ import SwiftUI extension AppDelegate: NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { + configure(with: AppUIMain()) hideIfLoginItem() - configure() } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { diff --git a/Passepartout/App/Platforms/App+tvOS.swift b/Passepartout/App/Platforms/App+tvOS.swift index 158b396d..f3eccb92 100644 --- a/Passepartout/App/Platforms/App+tvOS.swift +++ b/Passepartout/App/Platforms/App+tvOS.swift @@ -30,7 +30,7 @@ import SwiftUI extension AppDelegate: UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - configure() + configure(with: AppUITV()) return true } } diff --git a/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/AppUIMain.xcscheme b/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/AppUIMain.xcscheme new file mode 100644 index 00000000..8e8bf209 --- /dev/null +++ b/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/AppUIMain.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/AppUITV.xcscheme b/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/AppUITV.xcscheme new file mode 100644 index 00000000..02c14f13 --- /dev/null +++ b/Passepartout/Library/.swiftpm/xcode/xcshareddata/xcschemes/AppUITV.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Passepartout/Library/Sources/AppUI/AppUI.swift b/Passepartout/Library/Sources/AppUI/AppUI.swift index 1f9ff8ac..cd87279b 100644 --- a/Passepartout/Library/Sources/AppUI/AppUI.swift +++ b/Passepartout/Library/Sources/AppUI/AppUI.swift @@ -23,24 +23,43 @@ // along with Passepartout. If not, see . // +import CommonLibrary import Foundation import PassepartoutKit public protocol AppUIConfiguring { - static func configure(with context: AppContext) + func configure(with context: AppContext) } -public enum AppUI { - public static func configure(with context: AppContext) { - assertMissingModuleImplementations() +public final class AppUI: AppUIConfiguring { + private let appUIConfiguring: AppUIConfiguring? + + public init(_ appUIConfiguring: AppUIConfiguring?) { + self.appUIConfiguring = appUIConfiguring + } + + public func configure(with context: AppContext) { + PassepartoutConfiguration.shared.configureLogging( + to: BundleConfiguration.urlForAppLog, + parameters: Constants.shared.log, + logsPrivateData: UserDefaults.appGroup.bool(forKey: AppPreference.logsPrivateData.key) + ) + + assertMissingImplementations() + appUIConfiguring?.configure(with: context) + Task { - try? await context.providerManager.fetchIndex(from: API.shared) + try await context.providerManager.fetchIndex(from: API.shared) +#if os(macOS) + // keep this for login item because scenePhase is not triggered + try await context.tunnel.prepare(purge: true) +#endif } } } -extension AppUI { - public static func assertMissingModuleImplementations() { +private extension AppUI { + func assertMissingImplementations() { ModuleType.allCases.forEach { moduleType in let builder = moduleType.newModule() guard builder is ModuleTypeProviding else { diff --git a/Passepartout/Library/Sources/AppUI/Theme/Theme+UI.swift b/Passepartout/Library/Sources/AppUI/Theme/Theme+UI.swift index 2b64bc22..6d8f4421 100644 --- a/Passepartout/Library/Sources/AppUI/Theme/Theme+UI.swift +++ b/Passepartout/Library/Sources/AppUI/Theme/Theme+UI.swift @@ -106,16 +106,16 @@ struct ThemeItemModalModifier: ViewModifier where Modal: View, T: Iden } } -struct ThemeBooleanPopoverModifier: ViewModifier where Popover: View { +struct ThemeBooleanPopoverModifier: ViewModifier, SizeClassProviding where Popover: View { @EnvironmentObject private var theme: Theme @Environment(\.horizontalSizeClass) - private var hsClass + var hsClass @Environment(\.verticalSizeClass) - private var vsClass + var vsClass @Binding var isPresented: Bool @@ -124,7 +124,7 @@ struct ThemeBooleanPopoverModifier: ViewModifier where Popover: View { let popover: Popover func body(content: Content) -> some View { - if hsClass == .regular && vsClass == .regular { + if isBigDevice { content .popover(isPresented: $isPresented) { popover diff --git a/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift b/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift index 5b511919..aa88c711 100644 --- a/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift +++ b/Passepartout/Library/Sources/AppUIMain/AppUIMain.swift @@ -26,15 +26,17 @@ @_exported import AppUI import Foundation -public enum AppUIMain: AppUIConfiguring { - public static func configure(with context: AppContext) { - assertMissingModuleImplementations() - AppUI.configure(with: context) +public final class AppUIMain: AppUIConfiguring { + public init() { + } + + public func configure(with context: AppContext) { + assertMissingImplementations() } } private extension AppUIMain { - static func assertMissingModuleImplementations() { + func assertMissingImplementations() { let providerModuleTypes: Set = [ .openVPN ] diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift index f0e86f7b..e71cd303 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/AppCoordinator.swift @@ -27,14 +27,15 @@ import AppLibrary import CommonLibrary import PassepartoutKit import SwiftUI +import UtilsLibrary -public struct AppCoordinator: View { +public struct AppCoordinator: View, SizeClassProviding { @Environment(\.horizontalSizeClass) - private var hsClass + public var hsClass @Environment(\.verticalSizeClass) - private var vsClass + public var vsClass @AppStorage(AppPreference.profilesLayout.key) private var layout: ProfilesLayout = .list @@ -59,7 +60,7 @@ public struct AppCoordinator: View { } public var body: some View { - if hsClass == .regular && vsClass == .regular { + if isBigDevice { AppModalCoordinator( layout: $layout, profileManager: profileManager, diff --git a/Passepartout/Library/Sources/AppUIMain/Views/App/AppToolbar.swift b/Passepartout/Library/Sources/AppUIMain/Views/App/AppToolbar.swift index c0563164..f460e82e 100644 --- a/Passepartout/Library/Sources/AppUIMain/Views/App/AppToolbar.swift +++ b/Passepartout/Library/Sources/AppUIMain/Views/App/AppToolbar.swift @@ -26,14 +26,15 @@ import AppLibrary import PassepartoutKit import SwiftUI +import UtilsLibrary -struct AppToolbar: ToolbarContent { +struct AppToolbar: ToolbarContent, SizeClassProviding { @Environment(\.horizontalSizeClass) - private var hsClass + var hsClass @Environment(\.verticalSizeClass) - private var vsClass + var vsClass let profileManager: ProfileManager @@ -50,7 +51,7 @@ struct AppToolbar: ToolbarContent { let onNewProfile: (Profile) -> Void var body: some ToolbarContent { - if hsClass == .regular && vsClass == .regular { + if isBigDevice { ToolbarItemGroup { addProfileMenu aboutButton diff --git a/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/AppMenu+Model.swift b/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu+Model.swift similarity index 100% rename from Passepartout/Library/Sources/AppUIMain/Views/AppMenu/AppMenu+Model.swift rename to Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu+Model.swift diff --git a/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/AppMenu.swift b/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu.swift similarity index 100% rename from Passepartout/Library/Sources/AppUIMain/Views/AppMenu/AppMenu.swift rename to Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenu.swift diff --git a/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/AppMenuImage.swift b/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenuImage.swift similarity index 100% rename from Passepartout/Library/Sources/AppUIMain/Views/AppMenu/AppMenuImage.swift rename to Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppMenuImage.swift diff --git a/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/AppWindow.swift b/Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppWindow.swift similarity index 100% rename from Passepartout/Library/Sources/AppUIMain/Views/AppMenu/AppWindow.swift rename to Passepartout/Library/Sources/AppUIMain/Views/AppMenu/macOS/AppWindow.swift diff --git a/Passepartout/Library/Sources/AppUITV/AppUITV.swift b/Passepartout/Library/Sources/AppUITV/AppUITV.swift index dde25466..752ad072 100644 --- a/Passepartout/Library/Sources/AppUITV/AppUITV.swift +++ b/Passepartout/Library/Sources/AppUITV/AppUITV.swift @@ -26,8 +26,10 @@ @_exported import AppUI import Foundation -public enum AppUITV: AppUIConfiguring { - public static func configure(with context: AppContext) { - AppUI.configure(with: context) +public final class AppUITV: AppUIConfiguring { + public init() { + } + + public func configure(with context: AppContext) { } } diff --git a/Passepartout/Library/Sources/AppUITV/Views/AppCoordinator.swift b/Passepartout/Library/Sources/AppUITV/Views/AppCoordinator.swift index 1388f628..87a463b6 100644 --- a/Passepartout/Library/Sources/AppUITV/Views/AppCoordinator.swift +++ b/Passepartout/Library/Sources/AppUITV/Views/AppCoordinator.swift @@ -27,7 +27,7 @@ import AppLibrary import PassepartoutKit import SwiftUI -// FIXME: ###, UI for Apple TV +// FIXME: #788, UI for Apple TV public struct AppCoordinator: View { private let profileManager: ProfileManager diff --git a/Passepartout/Library/Sources/CommonLibrary/Domain/Constants.swift b/Passepartout/Library/Sources/CommonLibrary/Domain/Constants.swift index 090e8325..95767760 100644 --- a/Passepartout/Library/Sources/CommonLibrary/Domain/Constants.swift +++ b/Passepartout/Library/Sources/CommonLibrary/Domain/Constants.swift @@ -100,6 +100,8 @@ public struct Constants: Decodable, Sendable { } public struct API: Decodable, Sendable { + public let bundlePath: String + public let timeoutInterval: TimeInterval } diff --git a/Passepartout/Library/Sources/CommonLibrary/Resources/Constants.json b/Passepartout/Library/Sources/CommonLibrary/Resources/Constants.json index 3def3bdc..ea9b7e65 100644 --- a/Passepartout/Library/Sources/CommonLibrary/Resources/Constants.json +++ b/Passepartout/Library/Sources/CommonLibrary/Resources/Constants.json @@ -26,6 +26,7 @@ "refreshInterval": 3.0 }, "api": { + "bundlePath": "API", "timeoutInterval": 5.0 }, "log": { diff --git a/Passepartout/Library/Sources/CommonLibrary/Shared.swift b/Passepartout/Library/Sources/CommonLibrary/Shared.swift index 2fd9667d..d0201006 100644 --- a/Passepartout/Library/Sources/CommonLibrary/Shared.swift +++ b/Passepartout/Library/Sources/CommonLibrary/Shared.swift @@ -72,7 +72,7 @@ extension API { ] public static let bundled: APIMapper = { - guard let url = Bundle.module.url(forResource: "API", withExtension: nil) else { + guard let url = Bundle.module.url(forResource: Constants.shared.api.bundlePath, withExtension: nil) else { fatalError("Unable to find bundled API") } let ws = API.V5.DefaultWebServices( diff --git a/Passepartout/Library/Sources/UtilsLibrary/Views/LongContentView.swift b/Passepartout/Library/Sources/UtilsLibrary/Views/LongContentView.swift index a4290479..11215c27 100644 --- a/Passepartout/Library/Sources/UtilsLibrary/Views/LongContentView.swift +++ b/Passepartout/Library/Sources/UtilsLibrary/Views/LongContentView.swift @@ -35,7 +35,7 @@ public struct LongContentView: View { public var copySystemImage: String? public var body: some View { - TextEditor(text: $content) + contentView .toolbar { Button { copyToPasteboard(content) @@ -43,7 +43,18 @@ public struct LongContentView: View { Image(systemName: copySystemImage ?? "doc.on.doc") } } - // TODO: #659, add padding as inset, let content extend beyond safe areas + } + + @ViewBuilder + private var contentView: some View { + if #available(iOS 17, macOS 14, *) { + TextEditor(text: $content) +// .contentMargins(8) +// .scrollContentBackground(.hidden) + .scrollClipDisabled() + } else { + TextEditor(text: $content) + } } } diff --git a/Passepartout/Library/Sources/UtilsLibrary/Views/SizeClassProviding.swift b/Passepartout/Library/Sources/UtilsLibrary/Views/SizeClassProviding.swift new file mode 100644 index 00000000..48467f3c --- /dev/null +++ b/Passepartout/Library/Sources/UtilsLibrary/Views/SizeClassProviding.swift @@ -0,0 +1,38 @@ +// +// SizeClassProviding.swift +// Passepartout +// +// Created by Davide De Rosa on 10/31/24. +// Copyright (c) 2024 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 . +// + +import SwiftUI + +public protocol SizeClassProviding { + var hsClass: UserInterfaceSizeClass? { get } + + var vsClass: UserInterfaceSizeClass? { get } +} + +extension SizeClassProviding { + public var isBigDevice: Bool { + hsClass == .regular && vsClass == .regular + } +}