Everything is a tunnel.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
Jeroen Leenarts
2018-05-26 09:17:36 +02:00
parent 9724073447
commit 15e62d7f02
10 changed files with 129 additions and 114 deletions
+26 -13
View File
@@ -11,19 +11,32 @@
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Profiles-->
<!--Navigation Controller-->
<scene sceneID="1sx-zW-OnW">
<objects>
<navigationController storyboardIdentifier="UINavigationController" id="JOA-JU-iCW" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="GsQ-3q-wZr">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cAJ-Ch-uyg" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-1403" y="201"/>
</scene>
<!--Tunnels-->
<scene sceneID="Tud-vM-cYZ">
<objects>
<tableViewController storyboardIdentifier="ConnectionsTableViewController" id="kTU-BV-32R" customClass="ConnectionsTableViewController" customModule="WireGuard" customModuleProvider="target" sceneMemberID="viewController">
<tableViewController storyboardIdentifier="TunnelsTableViewController" id="kTU-BV-32R" customClass="TunnelsTableViewController" customModule="WireGuard" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="AJg-r0-KJH">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailDisclosureButton" indentationWidth="10" reuseIdentifier="ProfileTableViewCell" textLabel="hzX-lc-GyT" style="IBUITableViewCellStyleDefault" id="fM3-cC-KPN" customClass="ProfileTableViewCell" customModule="WireGuard" customModuleProvider="target">
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailDisclosureButton" indentationWidth="10" reuseIdentifier="TunnelTableViewCell" textLabel="hzX-lc-GyT" style="IBUITableViewCellStyleDefault" id="fM3-cC-KPN" customClass="TunnelTableViewCell" customModule="WireGuard" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fM3-cC-KPN" id="Rv6-XK-aK2" customClass="ProfileTableViewCell" customModule="WireGuard" customModuleProvider="target">
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fM3-cC-KPN" id="Rv6-XK-aK2" customClass="TunnelTableViewCell" customModule="WireGuard" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="307" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
@@ -43,7 +56,7 @@
<outlet property="delegate" destination="kTU-BV-32R" id="b6T-ZR-cmO"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Profiles" id="j0L-5U-jDs">
<navigationItem key="navigationItem" title="Tunnels" id="j0L-5U-jDs">
<barButtonItem key="rightBarButtonItem" systemItem="add" id="h2H-H8-3Tn">
<connections>
<action selector="addProvider:" destination="kTU-BV-32R" id="xSg-ap-3Fx"/>
@@ -56,10 +69,10 @@
</objects>
<point key="canvasLocation" x="34" y="154"/>
</scene>
<!--Title-->
<!--Tunnel settings-->
<scene sceneID="xV8-BW-4R7">
<objects>
<tableViewController storyboardIdentifier="ProfileConfigurationTableViewController" id="0VM-73-EPX" customClass="ProfileConfigurationTableViewController" customModule="WireGuard" customModuleProvider="target" sceneMemberID="viewController">
<tableViewController storyboardIdentifier="TunnelConfigurationTableViewController" id="0VM-73-EPX" customClass="TunnelConfigurationTableViewController" customModule="WireGuard" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="0Uy-k2-O3i">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -235,15 +248,15 @@
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="AddPeerTableViewCell" id="RyR-s5-lBV" customClass="AddPeerTableViewCell" customModule="WireGuard" customModuleProvider="target">
<rect key="frame" x="0.0" y="396" width="375" height="44"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="AddPeerTableViewCell" rowHeight="60" id="RyR-s5-lBV" customClass="AddPeerTableViewCell" customModule="WireGuard" customModuleProvider="target">
<rect key="frame" x="0.0" y="396" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="RyR-s5-lBV" id="gPY-qW-fbd">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="59.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fVu-Aa-9dn">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="59.5"/>
<state key="normal" title="Add peer"/>
</button>
</subviews>
@@ -256,7 +269,7 @@
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="PeerTableViewCell" rowHeight="308" id="gzz-88-0IG" customClass="PeerTableViewCell" customModule="WireGuard" customModuleProvider="target">
<rect key="frame" x="0.0" y="440" width="375" height="308"/>
<rect key="frame" x="0.0" y="456" width="375" height="308"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="gzz-88-0IG" id="XA6-EM-5V9">
<rect key="frame" x="0.0" y="0.0" width="375" height="307.5"/>
@@ -393,7 +406,7 @@
<outlet property="delegate" destination="0VM-73-EPX" id="oHt-UY-XIz"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Title" id="PPu-rX-T38">
<navigationItem key="navigationItem" title="Tunnel settings" id="PPu-rX-T38">
<barButtonItem key="rightBarButtonItem" systemItem="save" id="mft-1l-bWa"/>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
+30 -28
View File
@@ -11,6 +11,8 @@ import Foundation
import CoreData
import BNRCoreDataStack
extension UINavigationController: Identifyable {}
class AppCoordinator: RootViewCoordinator {
let persistentContainer = NSPersistentContainer(name: "WireGuard")
@@ -21,16 +23,16 @@ class AppCoordinator: RootViewCoordinator {
var childCoordinators: [Coordinator] = []
var rootViewController: UIViewController {
return self.connectionsTableViewController
return self.tunnelsTableViewController
}
var connectionsTableViewController: ConnectionsTableViewController!
var tunnelsTableViewController: TunnelsTableViewController!
/// Window to manage
let window: UIWindow
let navigationController: UINavigationController = {
let navController = UINavigationController()
let navController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(type: UINavigationController.self)
return navController
}()
@@ -54,14 +56,14 @@ class AppCoordinator: RootViewCoordinator {
} else {
DispatchQueue.main.async {
//start
if let connectionsTableViewController = self?.storyboard.instantiateViewController(type: ConnectionsTableViewController.self) {
self?.connectionsTableViewController = connectionsTableViewController
self?.connectionsTableViewController.viewContext = self?.persistentContainer.viewContext
self?.connectionsTableViewController.delegate = self
self?.navigationController.viewControllers = [connectionsTableViewController]
if let tunnelsTableViewController = self?.storyboard.instantiateViewController(type: TunnelsTableViewController.self) {
self?.tunnelsTableViewController = tunnelsTableViewController
self?.tunnelsTableViewController.viewContext = self?.persistentContainer.viewContext
self?.tunnelsTableViewController.delegate = self
self?.navigationController.viewControllers = [tunnelsTableViewController]
do {
if let context = self?.persistentContainer.viewContext, try Profile.countInContext(context) == 0 {
print("No profiles ... yet")
if let context = self?.persistentContainer.viewContext, try Tunnel.countInContext(context) == 0 {
print("No tunnels ... yet")
}
} catch {
self?.showError(error)
@@ -83,45 +85,45 @@ class AppCoordinator: RootViewCoordinator {
}
}
extension AppCoordinator: ConnectionsTableViewControllerDelegate {
func addProvider(connectionsTableViewController: ConnectionsTableViewController) {
extension AppCoordinator: TunnelsTableViewControllerDelegate {
func addProvider(tunnelsTableViewController: TunnelsTableViewController) {
let addContext = persistentContainer.newBackgroundContext()
showProfileConfigurationViewController(profile: nil, context: addContext)
showTunnelConfigurationViewController(tunnel: nil, context: addContext)
}
func connect(profile: Profile, connectionsTableViewController: ConnectionsTableViewController) {
func connect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) {
// TODO implement
print("connect profile \(profile)")
print("connect tunnel \(tunnel)")
}
func configure(profile: Profile, connectionsTableViewController: ConnectionsTableViewController) {
func configure(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) {
// TODO implement
print("configure profile \(profile)")
print("configure tunnel \(tunnel)")
let editContext = persistentContainer.newBackgroundContext()
var backgroundProfile: Profile?
var backgroundTunnel: Tunnel?
editContext.performAndWait {
backgroundProfile = editContext.object(with: profile.objectID) as? Profile
backgroundTunnel = editContext.object(with: tunnel.objectID) as? Tunnel
}
showProfileConfigurationViewController(profile: backgroundProfile, context: editContext)
showTunnelConfigurationViewController(tunnel: backgroundTunnel, context: editContext)
}
func showProfileConfigurationViewController(profile: Profile?, context: NSManagedObjectContext) {
let profileConfigurationViewController = storyboard.instantiateViewController(type: ProfileConfigurationTableViewController.self)
func showTunnelConfigurationViewController(tunnel: Tunnel?, context: NSManagedObjectContext) {
let tunnelConfigurationViewController = storyboard.instantiateViewController(type: TunnelConfigurationTableViewController.self)
profileConfigurationViewController.viewContext = context
profileConfigurationViewController.delegate = self
tunnelConfigurationViewController.viewContext = context
tunnelConfigurationViewController.delegate = self
self.navigationController.pushViewController(profileConfigurationViewController, animated: true)
self.navigationController.pushViewController(tunnelConfigurationViewController, animated: true)
}
func delete(profile: Profile, connectionsTableViewController: ConnectionsTableViewController) {
func delete(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) {
// TODO implement
print("delete profile \(profile)")
print("delete tunnel \(tunnel)")
}
}
extension AppCoordinator: ProfileConfigurationTableViewControllerDelegate {
extension AppCoordinator: TunnelConfigurationTableViewControllerDelegate {
}
@@ -22,7 +22,7 @@ extension Interface {
@NSManaged public var mtu: Int32
@NSManaged public var dns: String?
@NSManaged public var table: String?
@NSManaged public var profile: Profile?
@NSManaged public var tunnel: Tunnel?
@NSManaged public var adresses: NSSet?
}
@@ -20,6 +20,6 @@ extension Peer {
@NSManaged public var allowedIPs: String?
@NSManaged public var endpoint: String?
@NSManaged public var persistentKeepalive: Int16
@NSManaged public var profile: Profile?
@NSManaged public var tunnel: Tunnel?
}
@@ -1,16 +0,0 @@
//
// Profile+CoreDataClass.swift
// WireGuard
//
// Created by Jeroen Leenarts on 23-05-18.
// Copyright © 2018 WireGuard. All rights reserved.
//
//
import Foundation
import CoreData
@objc(Profile)
public class Profile: NSManagedObject {
}
@@ -1,40 +0,0 @@
//
// Profile+CoreDataProperties.swift
// WireGuard
//
// Created by Jeroen Leenarts on 23-05-18.
// Copyright © 2018 WireGuard. All rights reserved.
//
//
import Foundation
import CoreData
extension Profile {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Profile> {
return NSFetchRequest<Profile>(entityName: "Profile")
}
@NSManaged public var title: String?
@NSManaged public var peers: NSSet?
@NSManaged public var interface: Interface?
}
// MARK: Generated accessors for peers
extension Profile {
@objc(addPeersObject:)
@NSManaged public func addToPeers(_ value: Peer)
@objc(removePeersObject:)
@NSManaged public func removeFromPeers(_ value: Peer)
@objc(addPeers:)
@NSManaged public func addToPeers(_ values: NSSet)
@objc(removePeers:)
@NSManaged public func removeFromPeers(_ values: NSSet)
}
@@ -12,7 +12,7 @@
<attribute name="privateKey" attributeType="String" syncable="YES"/>
<attribute name="table" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="adresses" toMany="YES" deletionRule="Cascade" destinationEntity="Address" inverseName="interface" inverseEntity="Address" syncable="YES"/>
<relationship name="profile" maxCount="1" deletionRule="Nullify" destinationEntity="Profile" inverseName="interface" inverseEntity="Profile" syncable="YES"/>
<relationship name="tunnel" maxCount="1" deletionRule="Nullify" destinationEntity="Tunnel" inverseName="interface" inverseEntity="Tunnel" syncable="YES"/>
</entity>
<entity name="Peer" representedClassName="Peer" syncable="YES">
<attribute name="allowedIPs" attributeType="String" syncable="YES"/>
@@ -20,17 +20,17 @@
<attribute name="persistentKeepalive" attributeType="Integer 16" minValueString="0" maxValueString="65535" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="presharedKey" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="publicKey" attributeType="String" syncable="YES"/>
<relationship name="profile" maxCount="1" deletionRule="Nullify" destinationEntity="Profile" inverseName="peers" inverseEntity="Profile" syncable="YES"/>
<relationship name="tunnel" maxCount="1" deletionRule="Nullify" destinationEntity="Tunnel" inverseName="peers" inverseEntity="Tunnel" syncable="YES"/>
</entity>
<entity name="Profile" representedClassName="Profile" syncable="YES">
<entity name="Tunnel" representedClassName="Tunnel" syncable="YES">
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="interface" maxCount="1" deletionRule="Cascade" destinationEntity="Interface" inverseName="profile" inverseEntity="Interface" syncable="YES"/>
<relationship name="peers" toMany="YES" minCount="1" deletionRule="Cascade" destinationEntity="Peer" inverseName="profile" inverseEntity="Peer" syncable="YES"/>
<relationship name="interface" maxCount="1" deletionRule="Cascade" destinationEntity="Interface" inverseName="tunnel" inverseEntity="Interface" syncable="YES"/>
<relationship name="peers" toMany="YES" minCount="1" deletionRule="Cascade" destinationEntity="Peer" inverseName="tunnel" inverseEntity="Peer" syncable="YES"/>
</entity>
<elements>
<element name="Interface" positionX="-54" positionY="-9" width="128" height="165"/>
<element name="Peer" positionX="-36" positionY="9" width="128" height="135"/>
<element name="Profile" positionX="-63" positionY="-18" width="128" height="90"/>
<element name="Address" positionX="-54" positionY="45" width="128" height="75"/>
<element name="Address" positionX="0" positionY="0" width="0" height="0"/>
<element name="Interface" positionX="0" positionY="0" width="0" height="0"/>
<element name="Peer" positionX="0" positionY="0" width="0" height="0"/>
<element name="Tunnel" positionX="0" positionY="0" width="0" height="0"/>
</elements>
</model>
@@ -1,176 +0,0 @@
//
// ConnectionsTableViewController.swift
// WireGuard
//
// Created by Jeroen Leenarts on 23-05-18.
// Copyright © 2018 WireGuard. All rights reserved.
//
import UIKit
import CoreData
import BNRCoreDataStack
protocol ConnectionsTableViewControllerDelegate: class {
func addProvider(connectionsTableViewController: ConnectionsTableViewController)
func connect(profile: Profile, connectionsTableViewController: ConnectionsTableViewController)
func configure(profile: Profile, connectionsTableViewController: ConnectionsTableViewController)
func delete(profile: Profile, connectionsTableViewController: ConnectionsTableViewController)
}
class ConnectionsTableViewController: UITableViewController {
weak var delegate: ConnectionsTableViewControllerDelegate?
var viewContext: NSManagedObjectContext!
private lazy var fetchedResultsController: FetchedResultsController<Profile> = {
let fetchRequest = NSFetchRequest<Profile>()
fetchRequest.entity = Profile.entity()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "title", ascending: true)]
let frc = FetchedResultsController<Profile>(fetchRequest: fetchRequest,
managedObjectContext: viewContext)
frc.setDelegate(self.frcDelegate)
return frc
}()
private lazy var frcDelegate: ProfileFetchedResultsControllerDelegate = { // swiftlint:disable:this weak_delegate
return ProfileFetchedResultsControllerDelegate(tableView: self.tableView)
}()
override func viewDidLoad() {
super.viewDidLoad()
do {
try fetchedResultsController.performFetch()
} catch {
print("Failed to fetch objects: \(error)")
}
}
@IBAction func addProvider(_ sender: Any) {
delegate?.addProvider(connectionsTableViewController: self)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fetchedResultsController.sections?[0].objects.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(type: ProfileTableViewCell.self, for: indexPath)
guard let sections = fetchedResultsController.sections else {
fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil")
}
let section = sections[indexPath.section]
let profile = section.objects[indexPath.row]
cell.textLabel?.text = profile.title
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let sections = fetchedResultsController.sections else {
fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil")
}
let section = sections[indexPath.section]
let profile = section.objects[indexPath.row]
delegate?.connect(profile: profile, connectionsTableViewController: self)
tableView.deselectRow(at: indexPath, animated: true)
}
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
guard let sections = fetchedResultsController.sections else {
fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil")
}
let section = sections[indexPath.section]
let profile = section.objects[indexPath.row]
delegate?.configure(profile: profile, connectionsTableViewController: self)
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
guard let sections = fetchedResultsController.sections else {
fatalError("FetchedResultsController \(fetchedResultsController) should have sections, but found nil")
}
let section = sections[indexPath.section]
let profile = section.objects[indexPath.row]
delegate?.delete(profile: profile, connectionsTableViewController: self)
}
}
}
extension ConnectionsTableViewController: Identifyable {}
class ProfileFetchedResultsControllerDelegate: NSObject, FetchedResultsControllerDelegate {
private weak var tableView: UITableView?
// MARK: - Lifecycle
init(tableView: UITableView) {
self.tableView = tableView
}
func fetchedResultsControllerDidPerformFetch(_ controller: FetchedResultsController<Profile>) {
tableView?.reloadData()
}
func fetchedResultsControllerWillChangeContent(_ controller: FetchedResultsController<Profile>) {
tableView?.beginUpdates()
}
func fetchedResultsControllerDidChangeContent(_ controller: FetchedResultsController<Profile>) {
tableView?.endUpdates()
}
func fetchedResultsController(_ controller: FetchedResultsController<Profile>, didChangeObject change: FetchedResultsObjectChange<Profile>) {
guard let tableView = tableView else { return }
switch change {
case let .insert(_, indexPath):
tableView.insertRows(at: [indexPath], with: .automatic)
case let .delete(_, indexPath):
tableView.deleteRows(at: [indexPath], with: .automatic)
case let .move(_, fromIndexPath, toIndexPath):
tableView.moveRow(at: fromIndexPath, to: toIndexPath)
case let .update(_, indexPath):
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
func fetchedResultsController(_ controller: FetchedResultsController<Profile>, didChangeSection change: FetchedResultsSectionChange<Profile>) {
guard let tableView = tableView else { return }
switch change {
case let .insert(_, index):
tableView.insertSections(IndexSet(integer: index), with: .automatic)
case let .delete(_, index):
tableView.deleteSections(IndexSet(integer: index), with: .automatic)
}
}
}
class ProfileTableViewCell: UITableViewCell {
}
extension ProfileTableViewCell: Identifyable {}