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
This commit is contained in:
Davide 2024-10-31 10:02:21 +01:00 committed by GitHub
parent 80dd6dc779
commit 237277d4db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 250 additions and 52 deletions

View File

@ -24,8 +24,7 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
#platform: ["iOS", "macOS", "tvOS"] platform: ["iOS", "macOS", "tvOS"]
platform: ["iOS", "macOS"]
steps: steps:
- uses: passepartoutvpn/action-prepare-xcode-build@master - uses: passepartoutvpn/action-prepare-xcode-build@master
with: with:
@ -64,8 +63,7 @@ jobs:
PILOT_GROUPS: ${{ vars.PILOT_GROUPS }} PILOT_GROUPS: ${{ vars.PILOT_GROUPS }}
PILOT_NOTIFY_EXTERNAL_TESTERS: ${{ vars.PILOT_NOTIFY_EXTERNAL_TESTERS }} PILOT_NOTIFY_EXTERNAL_TESTERS: ${{ vars.PILOT_NOTIFY_EXTERNAL_TESTERS }}
run: | run: |
#PLATFORMS=("iOS" "macOS" "tvOS") PLATFORMS=("iOS" "macOS" "tvOS")
PLATFORMS=("iOS" "macOS")
for PLATFORM in ${PLATFORMS[@]}; do for PLATFORM in ${PLATFORMS[@]}; do
bundle exec fastlane --env $PLATFORM public_beta bundle exec fastlane --env $PLATFORM public_beta
done done

View File

@ -33,19 +33,8 @@ final class AppDelegate: NSObject {
let context: AppContext = .shared let context: AppContext = .shared
// let context: AppContext = .mock(withRegistry: .shared) // let context: AppContext = .mock(withRegistry: .shared)
func configure() { func configure(with appUIConfiguring: AppUIConfiguring) {
PassepartoutConfiguration.shared.configureLogging( AppUI(appUIConfiguring)
to: BundleConfiguration.urlForAppLog, .configure(with: context)
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
} }
} }

View File

@ -30,7 +30,7 @@ import SwiftUI
extension AppDelegate: UIApplicationDelegate { extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
configure() configure(with: AppUIMain())
return true return true
} }
} }

View File

@ -32,8 +32,8 @@ import SwiftUI
extension AppDelegate: NSApplicationDelegate { extension AppDelegate: NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
configure(with: AppUIMain())
hideIfLoginItem() hideIfLoginItem()
configure()
} }
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {

View File

@ -30,7 +30,7 @@ import SwiftUI
extension AppDelegate: UIApplicationDelegate { extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
configure() configure(with: AppUITV())
return true return true
} }
} }

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AppUIMain"
BuildableName = "AppUIMain"
BlueprintName = "AppUIMain"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AppUIMain"
BuildableName = "AppUIMain"
BlueprintName = "AppUIMain"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AppUITV"
BuildableName = "AppUITV"
BlueprintName = "AppUITV"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AppUITV"
BuildableName = "AppUITV"
BlueprintName = "AppUITV"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -23,24 +23,43 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
import CommonLibrary
import Foundation import Foundation
import PassepartoutKit import PassepartoutKit
public protocol AppUIConfiguring { public protocol AppUIConfiguring {
static func configure(with context: AppContext) func configure(with context: AppContext)
} }
public enum AppUI { public final class AppUI: AppUIConfiguring {
public static func configure(with context: AppContext) { private let appUIConfiguring: AppUIConfiguring?
assertMissingModuleImplementations()
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 { 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 { private extension AppUI {
public static func assertMissingModuleImplementations() { func assertMissingImplementations() {
ModuleType.allCases.forEach { moduleType in ModuleType.allCases.forEach { moduleType in
let builder = moduleType.newModule() let builder = moduleType.newModule()
guard builder is ModuleTypeProviding else { guard builder is ModuleTypeProviding else {

View File

@ -106,16 +106,16 @@ struct ThemeItemModalModifier<Modal, T>: ViewModifier where Modal: View, T: Iden
} }
} }
struct ThemeBooleanPopoverModifier<Popover>: ViewModifier where Popover: View { struct ThemeBooleanPopoverModifier<Popover>: ViewModifier, SizeClassProviding where Popover: View {
@EnvironmentObject @EnvironmentObject
private var theme: Theme private var theme: Theme
@Environment(\.horizontalSizeClass) @Environment(\.horizontalSizeClass)
private var hsClass var hsClass
@Environment(\.verticalSizeClass) @Environment(\.verticalSizeClass)
private var vsClass var vsClass
@Binding @Binding
var isPresented: Bool var isPresented: Bool
@ -124,7 +124,7 @@ struct ThemeBooleanPopoverModifier<Popover>: ViewModifier where Popover: View {
let popover: Popover let popover: Popover
func body(content: Content) -> some View { func body(content: Content) -> some View {
if hsClass == .regular && vsClass == .regular { if isBigDevice {
content content
.popover(isPresented: $isPresented) { .popover(isPresented: $isPresented) {
popover popover

View File

@ -26,15 +26,17 @@
@_exported import AppUI @_exported import AppUI
import Foundation import Foundation
public enum AppUIMain: AppUIConfiguring { public final class AppUIMain: AppUIConfiguring {
public static func configure(with context: AppContext) { public init() {
assertMissingModuleImplementations() }
AppUI.configure(with: context)
public func configure(with context: AppContext) {
assertMissingImplementations()
} }
} }
private extension AppUIMain { private extension AppUIMain {
static func assertMissingModuleImplementations() { func assertMissingImplementations() {
let providerModuleTypes: Set<ModuleType> = [ let providerModuleTypes: Set<ModuleType> = [
.openVPN .openVPN
] ]

View File

@ -27,14 +27,15 @@ import AppLibrary
import CommonLibrary import CommonLibrary
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary
public struct AppCoordinator: View { public struct AppCoordinator: View, SizeClassProviding {
@Environment(\.horizontalSizeClass) @Environment(\.horizontalSizeClass)
private var hsClass public var hsClass
@Environment(\.verticalSizeClass) @Environment(\.verticalSizeClass)
private var vsClass public var vsClass
@AppStorage(AppPreference.profilesLayout.key) @AppStorage(AppPreference.profilesLayout.key)
private var layout: ProfilesLayout = .list private var layout: ProfilesLayout = .list
@ -59,7 +60,7 @@ public struct AppCoordinator: View {
} }
public var body: some View { public var body: some View {
if hsClass == .regular && vsClass == .regular { if isBigDevice {
AppModalCoordinator( AppModalCoordinator(
layout: $layout, layout: $layout,
profileManager: profileManager, profileManager: profileManager,

View File

@ -26,14 +26,15 @@
import AppLibrary import AppLibrary
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
import UtilsLibrary
struct AppToolbar: ToolbarContent { struct AppToolbar: ToolbarContent, SizeClassProviding {
@Environment(\.horizontalSizeClass) @Environment(\.horizontalSizeClass)
private var hsClass var hsClass
@Environment(\.verticalSizeClass) @Environment(\.verticalSizeClass)
private var vsClass var vsClass
let profileManager: ProfileManager let profileManager: ProfileManager
@ -50,7 +51,7 @@ struct AppToolbar: ToolbarContent {
let onNewProfile: (Profile) -> Void let onNewProfile: (Profile) -> Void
var body: some ToolbarContent { var body: some ToolbarContent {
if hsClass == .regular && vsClass == .regular { if isBigDevice {
ToolbarItemGroup { ToolbarItemGroup {
addProfileMenu addProfileMenu
aboutButton aboutButton

View File

@ -26,8 +26,10 @@
@_exported import AppUI @_exported import AppUI
import Foundation import Foundation
public enum AppUITV: AppUIConfiguring { public final class AppUITV: AppUIConfiguring {
public static func configure(with context: AppContext) { public init() {
AppUI.configure(with: context) }
public func configure(with context: AppContext) {
} }
} }

View File

@ -27,7 +27,7 @@ import AppLibrary
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
// FIXME: ###, UI for Apple TV // FIXME: #788, UI for Apple TV
public struct AppCoordinator: View { public struct AppCoordinator: View {
private let profileManager: ProfileManager private let profileManager: ProfileManager

View File

@ -100,6 +100,8 @@ public struct Constants: Decodable, Sendable {
} }
public struct API: Decodable, Sendable { public struct API: Decodable, Sendable {
public let bundlePath: String
public let timeoutInterval: TimeInterval public let timeoutInterval: TimeInterval
} }

View File

@ -26,6 +26,7 @@
"refreshInterval": 3.0 "refreshInterval": 3.0
}, },
"api": { "api": {
"bundlePath": "API",
"timeoutInterval": 5.0 "timeoutInterval": 5.0
}, },
"log": { "log": {

View File

@ -72,7 +72,7 @@ extension API {
] ]
public static let bundled: APIMapper = { 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") fatalError("Unable to find bundled API")
} }
let ws = API.V5.DefaultWebServices( let ws = API.V5.DefaultWebServices(

View File

@ -35,7 +35,7 @@ public struct LongContentView: View {
public var copySystemImage: String? public var copySystemImage: String?
public var body: some View { public var body: some View {
TextEditor(text: $content) contentView
.toolbar { .toolbar {
Button { Button {
copyToPasteboard(content) copyToPasteboard(content)
@ -43,7 +43,18 @@ public struct LongContentView: View {
Image(systemName: copySystemImage ?? "doc.on.doc") 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)
}
} }
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
//
import SwiftUI
public protocol SizeClassProviding {
var hsClass: UserInterfaceSizeClass? { get }
var vsClass: UserInterfaceSizeClass? { get }
}
extension SizeClassProviding {
public var isBigDevice: Bool {
hsClass == .regular && vsClass == .regular
}
}