Split reusable views into extensions (#322)

Like in #321
This commit is contained in:
Davide De Rosa 2023-07-03 17:37:16 +02:00 committed by GitHub
parent d7ebcb23ba
commit bd6340ce77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 176 deletions

View File

@ -104,7 +104,6 @@
0E71ACF927C12E4800F85C4B /* CreditsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACF827C12E4800F85C4B /* CreditsView.swift */; };
0E71ACFB27C12E5300F85C4B /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFA27C12E5300F85C4B /* VersionView.swift */; };
0E71ACFD27C1321A00F85C4B /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E71ACFC27C1321A00F85C4B /* ActivityView.swift */; };
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7577D62816A3B200081CBE /* DestructiveButton.swift */; };
0E7577DF2817E22C00081CBE /* VPNToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7577DE2817E22C00081CBE /* VPNToggle.swift */; };
0E7A8C0A2A1D410500780F4B /* PersistenceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7A8C092A1D410400780F4B /* PersistenceManager.swift */; };
0E7A8C0C2A1D4A6100780F4B /* PassepartoutLibrary in Frameworks */ = {isa = PBXBuildFile; productRef = 0E7A8C0B2A1D4A6100780F4B /* PassepartoutLibrary */; };
@ -398,7 +397,6 @@
0E71ACF827C12E4800F85C4B /* CreditsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsView.swift; sourceTree = "<group>"; };
0E71ACFA27C12E5300F85C4B /* VersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionView.swift; sourceTree = "<group>"; };
0E71ACFC27C1321A00F85C4B /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
0E7577D62816A3B200081CBE /* DestructiveButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveButton.swift; sourceTree = "<group>"; };
0E7577DE2817E22C00081CBE /* VPNToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNToggle.swift; sourceTree = "<group>"; };
0E7A8C072A1D40BA00780F4B /* Picker+OpenVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Picker+OpenVPN.swift"; sourceTree = "<group>"; };
0E7A8C082A1D40BA00780F4B /* Picker+Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Picker+Network.swift"; sourceTree = "<group>"; };
@ -613,7 +611,6 @@
0EB4042D27CA136200378B1A /* AddingTextField.swift */,
0EB3412F27C7761A00483410 /* Binding+Extensions.swift */,
0E9ED48027FD9BAE003B2316 /* CopySavingButton.swift */,
0E7577D62816A3B200081CBE /* DestructiveButton.swift */,
0EDE02C127F61C79000FBE3C /* EditableTextList.swift */,
0E3A593B2A50975700B3FE40 /* ErrorHandler.swift */,
0E2C171A27CB5A3A007E8488 /* GenericCreditsView.swift */,
@ -1458,7 +1455,6 @@
0E3A593C2A50975700B3FE40 /* ErrorHandler.swift in Sources */,
0E34AC8227F892C40042F2AB /* OnDemandView+SSID.swift in Sources */,
0E3B7FCD27E47B3700C66F13 /* AddHostView+Name.swift in Sources */,
0E7577D72816A3B200081CBE /* DestructiveButton.swift in Sources */,
0EF2212D27E66EB5001D0BD7 /* AddProviderView.swift in Sources */,
0EB90CC129C25BBD00E64628 /* InteractiveConnectionView.swift in Sources */,
0E35C09A280E95BB0071FA35 /* ProviderProfileAvailability.swift in Sources */,

View File

@ -51,15 +51,19 @@ struct AddingTextField<Field: View, ActionLabel: View>: View {
}
}
}
}
private func doAdd() {
// MARK: -
private extension AddingTextField {
func doAdd() {
withAnimation {
onAdd?()
isAdding = true
}
}
private func doCommit() {
func doCommit() {
withAnimation {
onCommit?()
isAdding = false

View File

@ -51,12 +51,20 @@ struct CopySavingButton<T: Equatable, Label: View>: View {
}
}
}
}
private var canSave: Bool {
// MARK: -
private extension CopySavingButton {
var canSave: Bool {
isLoaded && (saveAnyway || copy != original)
}
}
private func loadFromOriginal(once: Bool) {
// MARK: -
private extension CopySavingButton {
func loadFromOriginal(once: Bool) {
guard !once || !isLoaded else {
return
}
@ -64,7 +72,7 @@ struct CopySavingButton<T: Equatable, Label: View>: View {
isLoaded = true
}
private func saveToOriginal() {
func saveToOriginal() {
if copy != original {
original = copy
}

View File

@ -1,36 +0,0 @@
//
// DestructiveButton.swift
// Passepartout
//
// Created by Davide De Rosa on 4/25/22.
// Copyright (c) 2023 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 DestructiveButton<Label: View>: View {
let action: () -> Void
let label: () -> Label
var body: some View {
Button(role: .destructive, action: action, label: label)
}
}

View File

@ -64,14 +64,6 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
private let addedUUID = UUID()
private var addedText: Binding<String> {
.init {
editedTextStrings[addedUUID] ?? ""
} set: {
editedTextStrings[addedUUID] = $0
}
}
var body: some View {
debugChanges()
return Group {
@ -90,8 +82,20 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
}
}.onChange(of: elements, perform: remapElements)
}
}
private func existingRow(_ element: IdentifiableString) -> some View {
// MARK: -
private extension EditableTextList {
var addedText: Binding<String> {
.init {
editedTextStrings[addedUUID] ?? ""
} set: {
editedTextStrings[addedUUID] = $0
}
}
func existingRow(_ element: IdentifiableString) -> some View {
let editedText = binding(toEditedElement: element)
return textField(.init(isNewElement: false, text: editedText, onEditingChanged: {
@ -104,7 +108,7 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
}))
}
private var newRow: some View {
var newRow: some View {
AddingTextField(
onAdd: {
addedText.wrappedValue = ""
@ -120,10 +124,8 @@ struct EditableTextList<Field: View, ActionLabel: View>: View {
}
}
// MARK: View model
extension EditableTextList {
private func remapElements(_ newElements: [String]) {
private extension EditableTextList {
func remapElements(_ newElements: [String]) {
var oldIdentifiableElements = identifiableElements
var newIdentifiableElements: [IdentifiableString] = []
@ -148,7 +150,20 @@ extension EditableTextList {
}
}
private func addElement() {
func binding(toEditedElement element: IdentifiableString) -> Binding<String> {
// print(">>> <-> \(element)")
.init {
editedTextStrings[element.id] ?? element.string
} set: {
editedTextStrings[element.id] = $0
}
}
}
// MARK: -
private extension EditableTextList {
func addElement() {
guard allowsDuplicates || !identifiableElements.contains(where: {
$0.string == addedText.wrappedValue
}) else {
@ -159,16 +174,7 @@ extension EditableTextList {
commit()
}
private func binding(toEditedElement element: IdentifiableString) -> Binding<String> {
// print(">>> <-> \(element)")
.init {
editedTextStrings[element.id] ?? element.string
} set: {
editedTextStrings[element.id] = $0
}
}
private func replaceElement(at id: UUID, with editedText: Binding<String>) {
func replaceElement(at id: UUID, with editedText: Binding<String>) {
// print(">>> \(identifiableElements[id].string) -> \(editedText.wrappedValue)")
guard let i = identifiableElements.firstIndex(where: {
$0.id == id
@ -188,21 +194,21 @@ extension EditableTextList {
commit()
}
private func onDelete(offsets: IndexSet) {
func onDelete(offsets: IndexSet) {
var mapped = mapping(identifiableElements)
mapped.remove(atOffsets: offsets)
identifiableElements = mapped
commit()
}
private func onMove(indexSet: IndexSet, to offset: Int) {
func onMove(indexSet: IndexSet, to offset: Int) {
var mapped = mapping(identifiableElements)
mapped.move(fromOffsets: indexSet, toOffset: offset)
identifiableElements = mapped
commit()
}
private func commit() {
func commit() {
// print(">>> identifiableElements = \(identifiableElements.map { "\($0.string) (\($0.id))" })")
elements = identifiableElements.map(\.string)
}

View File

@ -26,31 +26,6 @@
import SwiftUI
struct GenericCreditsView: View {
struct License {
let name: String
let licenseName: String
let licenseURL: URL
init(_ name: String, _ licenseName: String, _ licenseURL: URL) {
self.name = name
self.licenseName = licenseName
self.licenseURL = licenseURL
}
}
struct Notice {
let name: String
let noticeString: String
init(_ name: String, _ noticeString: String) {
self.name = name
self.noticeString = noticeString
}
}
var licensesHeader: String? = "Licenses"
var noticesHeader: String? = "Notices"
@ -78,26 +53,80 @@ struct GenericCreditsView: View {
}
}
}
}
private var sortedLicenses: [License] {
extension GenericCreditsView {
struct License {
let name: String
let licenseName: String
let licenseURL: URL
init(_ name: String, _ licenseName: String, _ licenseURL: URL) {
self.name = name
self.licenseName = licenseName
self.licenseURL = licenseURL
}
}
struct Notice {
let name: String
let noticeString: String
init(_ name: String, _ noticeString: String) {
self.name = name
self.noticeString = noticeString
}
}
}
private extension GenericCreditsView {
struct LicenseView: View {
let url: URL
@Binding var content: String?
var body: some View {
ZStack {
content.map { unwrapped in
ScrollView {
Text(unwrapped)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
}
if content == nil {
ProgressView()
}
}.onAppear(perform: loadURL)
}
}
}
// MARK: -
private extension GenericCreditsView {
var sortedLicenses: [License] {
licenses.sorted {
$0.name.lowercased() < $1.name.lowercased()
}
}
private var sortedNotices: [Notice] {
var sortedNotices: [Notice] {
notices.sorted {
$0.name.lowercased() < $1.name.lowercased()
}
}
private var sortedLanguages: [String] {
var sortedLanguages: [String] {
translations.keys.sorted {
$0.localizedAsCountryCode < $1.localizedAsCountryCode
}
}
private var licensesSection: some View {
var licensesSection: some View {
Section(
header: licensesHeader.map(Text.init)
) {
@ -118,7 +147,7 @@ struct GenericCreditsView: View {
}
}
private var noticesSection: some View {
var noticesSection: some View {
Section(
header: noticesHeader.map(Text.init)
) {
@ -128,7 +157,7 @@ struct GenericCreditsView: View {
}
}
private var translationsSection: some View {
var translationsSection: some View {
Section(
header: translationsHeader.map(Text.init)
) {
@ -145,7 +174,7 @@ struct GenericCreditsView: View {
}
}
private func noticeView(_ content: Notice) -> some View {
func noticeView(_ content: Notice) -> some View {
VStack {
Text(content.noticeString)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
@ -155,46 +184,27 @@ struct GenericCreditsView: View {
}
}
extension GenericCreditsView {
struct LicenseView: View {
let url: URL
@Binding var content: String?
var body: some View {
ZStack {
content.map { unwrapped in
ScrollView {
Text(unwrapped)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
}
if content == nil {
ProgressView()
}
}.onAppear(perform: loadURL)
}
private func loadURL() {
guard content == nil else {
return
}
Task { @MainActor in
withAnimation {
do {
content = try String(contentsOf: url)
} catch {
content = AppError(error).localizedDescription
}
}
}
}
}
}
private extension String {
var localizedAsCountryCode: String {
Locale.current.localizedString(forLanguageCode: self)?.capitalized ?? self
}
}
// MARK: -
private extension GenericCreditsView.LicenseView {
func loadURL() {
guard content == nil else {
return
}
Task { @MainActor in
withAnimation {
do {
content = try String(contentsOf: url)
} catch {
content = AppError(error).localizedDescription
}
}
}
}
}

View File

@ -66,8 +66,31 @@ struct LockableView<Content: View, LockedContent: View>: View {
}
}.onChange(of: scenePhase, perform: onScenePhase)
}
}
private func onScenePhase(_ scenePhase: ScenePhase) {
// MARK: -
private final class Lock: ObservableObject {
enum State {
case none
case covered
case locked
}
static let shared = Lock()
@Published var state: State = .locked
private init() {
}
}
// MARK: -
private extension LockableView {
func onScenePhase(_ scenePhase: ScenePhase) {
switch scenePhase {
case .active:
unlockIfNeeded()
@ -114,20 +137,3 @@ struct LockableView<Content: View, LockedContent: View>: View {
}
}
}
private final class Lock: ObservableObject {
enum State {
case none
case covered
case locked
}
static let shared = Lock()
@Published var state: State = .locked
private init() {
}
}

View File

@ -27,18 +27,6 @@ import MessageUI
import SwiftUI
struct MailComposerView: UIViewControllerRepresentable {
final class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
@Binding private var isPresented: Bool
init(_ view: MailComposerView) {
_isPresented = view._isPresented
}
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
isPresented = false
}
}
struct Attachment {
let data: Data
@ -80,3 +68,17 @@ struct MailComposerView: UIViewControllerRepresentable {
Coordinator(self)
}
}
extension MailComposerView {
final class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
@Binding private var isPresented: Bool
init(_ view: MailComposerView) {
_isPresented = view._isPresented
}
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
isPresented = false
}
}
}

View File

@ -49,8 +49,12 @@ struct StyledPicker<T: Hashable, Label: View, Style: ListStyle>: View {
}
}
}
}
private func pickerView() -> some View {
// MARK: -
private extension StyledPicker {
func pickerView() -> some View {
List {
Section {
ForEach(values, id: \.self) { value in

View File

@ -157,7 +157,7 @@ private extension OrganizerView.ProfileContextMenu {
}
var deleteButton: some View {
DestructiveButton {
Button(role: .destructive) {
withAnimation {
profileManager.removeProfiles(withIds: [header.id])
}

View File

@ -229,7 +229,7 @@ private extension ProfileView.MainMenu {
}
var deleteProfileButton: some View {
DestructiveButton {
Button(role: .destructive) {
alertType = .deleteProfile
isAlertPresented = true
} label: {