🦅

【Swift】Massive View Controllerで作られた負債アプリ(個人開発)をMVCパターンでリファクタリングした。

2022/02/06に公開

MassiveViewControllerとは

マシマシViewControllerといえば分かりやすいでしょうか。
いわゆるViewControllerに機能を積み込みすぎた機能になります。

改修するきっかけについて

久々にアプリを改修したいなと思って、割と勢いだけでリリースしてしまったので、開いたら1件のViewControllerに長文で書かれており、非常にメンテナンス性が低かったのですよね。

先日MVCパターン(Swift)のサンプル記事なども投稿したこともあり、よりメンテナンス性が高いコードへ移行する体験や、実際に感じたメリットなどを記載できればより良いなということで本記事を執筆しております。
https://zenn.dev/tanukidevelop/articles/33eedf516abb1e

アプリの紹介

簡単に説明すると、オフラインでも機能する「メモ帳アプリ」のようなものです。
メモ帳を都度作成することができ、そのメモ帳に「カテゴリ名」を設定することが可能です。(デッキ?なにそれ)

アプリ起動時はカテゴリ一覧を漏れなくダブリなく取得し、トップ画面に五十音順で並べます。
今回はまず、その画面をリファクタリングを行うことにしました。

上記画像ですと、カテゴリー名「(空白)」と「great」というメモ帳が存在しているので、このような画面になっております。

また、各セルを押下する事で画面遷移を行えます。

改修前の画面に実装していた機能

  • 画面左上のUINavigationButton(ハンバーガーメニュー)を押下することで、ライブラリSideMenuを開き、その他設定などを開くことができます。

  • アプリ起動時に本画面が開かれる。アプリ内にUserdefaultで保存されているデータを読み込んで、表示します(このデータはアプリ終了後も保存され、次回起動時以降もデータを保持・参照できます)

改修前のコード

こちらはマシマシViewControllerのクラス構成になります。

  • FolderListViewController.Swift
  • FolderListViewController.xib

マシマシViewControllerなので、xibファイルに直接オブジェクトも配置しており、.swiftファイルと紐付けております。

また、メインの処理をすべてFolderListViewController.Swiftに記載しております。

冗長なコードになっておりますので、一旦記事を読み勧めても問題ありません。

FolderListViewController.Swift


import UIKit
import SafariServices
import SwiftReorder
import RxSwift
import RxCocoa
import SideMenu
import UIKit
import MessageUI


class FolderListViewController: UIViewController,SettingsDelegate,UIScrollViewDelegate, MFMailComposeViewControllerDelegate {
    var dataManager = DataManager.shared
    let disposeBag = DisposeBag()
    var menuNavigationController: SideMenuNavigationController? = nil
    var folderList = [String]()
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var bannerView: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        dataManager.loadColor()
        setThemeColor()
        dataManager.loadOriginalDataArray()
        loadSettings()
        loadNavigationController()
        loadTableView()
        UpgradeNotice.shared.fire()
        AdMobHelper.shared.setupBannerAd(adBaseView: self.bannerView, rootVC: self)
        AdMobHelper.shared.setupInterstitialAd(rootVC: self)

    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.navigationItem.title = "デッキカテゴリー 一覧"
        self.tableView.reloadData()
        self.getFolderList()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        APIRequest.getShareSheetData()
    }
    
    func getFolderList() {
        var emptyList = [String]()
        for (i,deckModel) in dataManager.deckList.enumerated() {
            emptyList.append(deckModel.folderName)
        }
        let orderedSet:NSOrderedSet = NSOrderedSet(array: emptyList)
        var folderUniqueList = orderedSet.array as! [String]
        folderUniqueList.sort()
        self.folderList = folderUniqueList
        print(folderUniqueList)
        print("並び替え終わり")

    }
    
    func loadSettings() {
        let menuViewController = SettingsViewController()
        menuViewController.delegate = self
        //サイドメニューのナビゲーションコントローラを生成
        menuNavigationController = SideMenuNavigationController(rootViewController: menuViewController)
        //設定を追加
        menuNavigationController!.settings = makeSettings()
//        //左,右のメニューとして追加
        SideMenuManager.default.leftMenuNavigationController = menuNavigationController        
        SideMenuManager.default.addScreenEdgePanGesturesToPresent(toView: self.view, forMenu: .left)
    }
    
    //サイドメニューの設定
    private func makeSettings() -> SideMenuSettings {
        var settings = SideMenuSettings()
        //動作を指定
        settings.presentationStyle = .menuSlideIn
        //メニューの陰影度
        settings.presentationStyle.onTopShadowOpacity = 10.0
        //ステータスバーの透明度
        settings.statusBarEndAlpha = 0
        return settings
    }
        
    func loadNavigationController() {
        self.navigationController?.navigationBar.tintColor = .white
        self.navigationController?.navigationBar.titleTextAttributes = [
        // 文字の色
            .foregroundColor: UIColor.white
        ]
        var createBarButtonItem: UIBarButtonItem = UIBarButtonItem(image: UIImage(named: "menu_icon.png")!, style: .plain, target: self, action: #selector(settingBarButtonTapped(_:)))

        self.navigationItem.leftBarButtonItems = [createBarButtonItem]
    }
    
    func loadTableView() {
        tableView.separatorInset = .zero
        tableView.isEditing = false
        tableView.allowsSelectionDuringEditing = true
        
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(UINib(nibName: "ArchiveTableViewCell", bundle: nil), forCellReuseIdentifier: "ArchiveCell")
        tableView.register(UINib(nibName: "DeckListTableViewCell", bundle: nil), forCellReuseIdentifier: "CustomCell")
        self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

        
        tableView.tableFooterView = UIView(frame: CGRect.zero)
    }
    
    func setThemeColor() {
        self.navigationController?.navigationBar.barTintColor = dataManager.themeColor
        self.tableView.reloadData()
        loadSettings()
    }
    
    // 選択されたサイドバーのアイテムを取得
    func tappedSettingsItem(indexpath: IndexPath) {
        switch indexpath.row {
        case SettingsMenu.openCreateDeckRecipeSite.rawValue:
            let webPage = "https://www.pokemon-card.com/deck/deck.html"
            let safariVC = SFSafariViewController(url: NSURL(string: webPage)! as URL)
            safariVC.modalPresentationStyle = .fullScreen
            present(safariVC, animated: true, completion: nil)
        case SettingsMenu.backup.rawValue:
            if MFMailComposeViewController.canSendMail()==false {
                showSimpleAlertView(title: nil, message: "メールアプリが開けませんでした。", ButtonText: "OK")
                return
            }
            var mailViewController = MFMailComposeViewController()
            mailViewController.mailComposeDelegate = self
            var backupmsg = ""
            for deckModel in dataManager.deckList {
                backupmsg += "デッキ名:" + deckModel.deckTitle + "\nデッキコード:" + deckModel.duckRecipeId + "\nメモ:" + deckModel.memo + "\n\n"
            }
            backupmsg += "\n\n【アーカイブされたデッキレシピ】\n"
            
            for deckModel in dataManager.archiveDeckList {
                backupmsg += "デッキ名:" + deckModel.deckTitle + "\nデッキコード:" + deckModel.duckRecipeId + "\nメモ:" + deckModel.memo + "\n\n"
            }
            mailViewController.setMessageBody(backupmsg, isHTML: false)
            present(mailViewController, animated: true, completion: nil)
            
        case SettingsMenu.changeThemeColor.rawValue:
            let alertView = UIAlertController(
                title: "テーマカラー:変更",
                message: "\n\n\n\n\n\n\n\n\n",
                preferredStyle: .alert)

            let pickerView = UIPickerView(frame:
                CGRect(x: 0, y: 50, width: 270, height: 162))
            pickerView.dataSource = self
            pickerView.delegate = self

            // comment this line to use white color
            pickerView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2)

            alertView.view.addSubview(pickerView)
            
            let action = UIAlertAction(title: "OK", style: UIAlertAction.Style.default) { _ in
                self.dataManager.saveColor()
                AdMobHelper.shared.setupInterstitialAd(rootVC: self)
            }

            alertView.addAction(action)
            
            present(alertView, animated: true, completion: {
                pickerView.frame.size.width = alertView.view.frame.size.width
            })
            
        case SettingsMenu.Request.rawValue:
            let webPage = "https://forms.gle/NecGPHqX9UZFALPdA"
            let safariVC = SFSafariViewController(url: NSURL(string: webPage)! as URL)
            safariVC.modalPresentationStyle = .fullScreen
            present(safariVC, animated: true, completion: nil)
        case SettingsMenu.Howtouse.rawValue:
            showSimpleAlertView(title: nil, message: "デッキレシピ表示行(以下、行)を長押しして移動させると並び替えができます。\n\n行を左にスワイプすると「アーカイブ」できます。\n\nアーカイブされたデッキ一覧画面で行を左スワイプすると「元に戻す」「(デッキレシピ)削除」できます。\n\nデッキレシピ詳細画面で右上の共有ボタンを押すとツイートすることができます。\n\n詳細画面下部の「デッキレシピをサーバーにシェアする」を押すとサーバーにアップロードされ、トップ画面の「他のプレイヤーによってシェアされたデッキレシピ」から確認可能です。\n\nデッキ編集画面に「カテゴリー」を追加しました。こちらを変更して確定(反映)することでアプリのトップ画面でのデッキカテゴリーで分類されるようになります。", ButtonText: "OK")
        case SettingsMenu.CheckUpdate.rawValue:
            let webPage = "https://apps.apple.com/jp/app/%E3%83%9D%E3%82%B1%E3%82%AB%E3%83%87%E3%83%83%E3%82%AD%E7%AE%A1%E7%90%86/id1570965372"
            let safariVC = SFSafariViewController(url: NSURL(string: webPage)! as URL)
            safariVC.modalPresentationStyle = .fullScreen
            present(safariVC, animated: true, completion: nil)
            
        default:
            break
        }
    }
    

    @objc func settingBarButtonTapped(_ sender: UIBarButtonItem) {
        present(menuNavigationController!, animated: true, completion: nil)
    }
    
    func showSimpleAlertView(title: String?,message: String, ButtonText: String) {
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let defaultAction:UIAlertAction = UIAlertAction(title: ButtonText, style: .default, handler:{ (action:UIAlertAction!) -> Void in
            alertController.dismiss(animated: true, completion: nil) // 閉じる
        })
        alertController.addAction(defaultAction)
        self.present(alertController, animated: true, completion: nil)
    }
    

}


extension FolderListViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if (indexPath.row ==  self.folderList.count + 1) {
            // シェアされたデッキレシピが押下された
            let vc = ShareListViewController()
            self.navigationController?.pushViewController(vc, animated: true)
        } else if (indexPath.row ==  self.folderList.count) {
            // アーカイブされたデッキレシピが押下された時
            let vc = ArchiveListViewController()
            self.navigationController?.pushViewController(vc, animated: true)
        } else {
            let listVC = ListViewController()
            listVC.selectFolder = self.folderList[indexPath.row]
            self.navigationController?.pushViewController(listVC, animated: true)
        }
    }

}

extension FolderListViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 60

    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.folderList.count + 1 + 1
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        if (indexPath.row < self.folderList.count) {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") else {
                return UITableViewCell()
            }
            cell.textLabel?.text = folderList[indexPath.row]
            return cell
        }
        
        if (indexPath.row == self.folderList.count) {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "ArchiveCell") as? ArchiveTableViewCell  else {
                return UITableViewCell()
            }
            cell.backgroundColor = dataManager.themeColor
            cell.title?.text = "アーカイブされたデッキレシピ"
            return cell
        }
        
        if (indexPath.row == self.folderList.count + 1) {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "ArchiveCell") as? ArchiveTableViewCell  else {
                return UITableViewCell()
            }
            cell.backgroundColor = dataManager.themeColor
            cell.title?.text = "他のプレイヤーによってシェアされたデッキレシピ"
            return cell
        }
        return UITableViewCell()
    }
    
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
        return .none //表示させない。
    }
    
    func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
        return false //ずれない。
    }
}

extension FolderListViewController: UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return dataManager.colorList.count
    }
    

}

extension FolderListViewController: UIPickerViewDelegate {
    // UIPickerViewに表示する配列
    func pickerView(_ pickerView: UIPickerView,
                    titleForRow row: Int,
                    forComponent component: Int) -> String? {
        
        return String(dataManager.colorList[row])
    }
    
    // UIPickerViewのRowが選択された時の挙動
    func pickerView(_ pickerView: UIPickerView,
                    didSelectRow row: Int,
                    inComponent component: Int) {
        switch row {
        case ThemeColor.themeGrass.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 60, green: 179, blue: 113, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themeFire.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 203, green: 86, blue: 70, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themeWater.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 0, green: 191, blue: 255, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themeLightning.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 255, green: 215, blue: 0, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themePsychic.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 255, green: 105, blue: 180, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themeFighting.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 210, green: 105, blue: 30, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themeDarkness.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 25, green: 25, blue: 112, alpha: 0.8)
            setThemeColor()
            break
        case ThemeColor.themeMetal.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 192, green: 192, blue: 192, alpha: 1.0)
            setThemeColor()
            break
        default:
            break
        }
    }
}

改修に向けて、改修前の問題点を洗い出す

  • 他の画面でも同様の処理があるにも関わらず、全てのViewControllerに記載してしまっている。(FolderListViewController.Swiftにも共通処理化できる内容を記載しております。)
  • Viewの処理、ビジネスロジックの処理が混在しているので、どこに何が書いているのか分からない。
  • PickerViewの処理も、UITableViewDelegate,UITableViewDatasourceもすべて記載している。

早い話が書き分けなどされていないので、FatなViewControllerになってしまっているんですよね。

MVCパターンへの改修に向けて

まずはMVCパターンに書き換えることで、Viewでの描画をView部分、ビジネスロジックの処理をModel部分にかき分けることで、スリム化を目指します。

また、他の画面で重複している処理は継承元クラスのBaseListViewController.swiftを作成し、すべての一覧表示を行う画面では上記クラスを継承することとします。

こちらのクラスに記載しないのはその画面でしか存在しない、処理だけになります。たとえば全画面は使わないけど、2画面だけ使う機能の場合は、コメントで分かりやすくして記載しております。

また、本記事のソースコードには出てきませんが、以下のようなクラス・処理が存在しております。

  • DataManager(シングルトンクラス)
    • シングルトンクラスでメモ帳のデータを管理しているのでアプリのどこからでもデータを参照できる。
  • AdMob(Google広告)を利用しているので、広告をリクエストする処理がある。

FolderListModel.swift(Model)

import Foundation


class FolderListModel: NSObject,UITableViewDataSource {
    // シングルトンクラス
    var dataManager = DataManager.shared
    
    // Modelを監視するクラス
    let notificationCenter = NotificationCenter()
    
    // カテゴリー名(文字列)を管理する配列。この配列を元に表示を行う。
    private(set) var folderNameList: [String] = [
    ] {
        didSet{
            // Modelで管理している配列に変化があった場合に呼び出されて、通知する。
            notificationCenter.post(name: .init(rawValue: "changeFolderNameList"), object: nil, userInfo: ["list" : folderNameList])
        }
    }
    
    // MARK: logic
    
    // 最新の値を取得してTableViewを更新する
    func reacquisitionListAndReloadTableView(tableView: UITableView) {
        self.getCategoryFilterdDeckList()
        tableView.reloadData()
    }
    // カテゴリーを漏れなく抜けなく取り出して、表示する
    func getCategoryFilterdDeckList() {
        var emptyList = [String]()
        for (i,deckModel) in dataManager.deckList.enumerated() {
            emptyList.append(deckModel.folderName)
        }
        let orderedSet:NSOrderedSet = NSOrderedSet(array: emptyList)
        var folderUniqueList = orderedSet.array as! [String]
        folderUniqueList.sort()
        self.folderNameList = folderUniqueList
    }
    
    
    // MARK: UITableViewDatasoruce    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // 「アーカイブされたデッキレシピ」「他のプレイヤーによってシェアされたデッキレシピ」
        return self.folderNameList.count + 1 + 1
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        if (indexPath.row < self.folderNameList.count) {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") else {
                return UITableViewCell()
            }
            let deckModel: String = self.folderNameList[indexPath.row]
            cell.textLabel?.text = deckModel
            return cell
        }
        
        if (indexPath.row == self.folderNameList.count) {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "ArchiveCell") as? ArchiveTableViewCell  else {
                return UITableViewCell()
            }
            cell.backgroundColor = dataManager.themeColor
            cell.title?.text = "アーカイブされたデッキレシピ"
            return cell
        }
        
        if (indexPath.row == self.folderNameList.count + 1) {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: "ArchiveCell") as? ArchiveTableViewCell  else {
                return UITableViewCell()
            }
            cell.backgroundColor = dataManager.themeColor
            cell.title?.text = "他のプレイヤーによってシェアされたデッキレシピ"
            return cell
        }
        return UITableViewCell()
    }
    
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
        return .none //表示させない。
    }
    
    func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool {
        return false //ずれない。
    }
}

Model部分にUITableViewDataSourceの処理を担当するように変更しました。
セルの表示数や内容を表示する際に、セルで表示する内容(配列)を保持しているModel部分で処理するのが適任だと考えました。
少なくともControllerはModelとViewの橋渡しに徹底するべきなので、こちらの設計で問題ないと思います。

ModelにUITableViewDataSourceを処理させる方法などは、Controller部分に記載しております。

FolderListView.swift (View部分)

import Foundation
import UIKit

class FolderListView: UIView {
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var bannerView: UIView!
    
    override init(frame: CGRect){
        super.init(frame: frame)
        loadNib()
    }
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }
    
    func loadNib(){
        let view = Bundle.main.loadNibNamed("FolderListView", owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
    }
    
}

FolderListView.xibは新規作成でViewだけを作成し、作成後にFolderListView.swiftと紐付けてください(作成時にあらかじめ紐付けることはできません)

画像を参考に進めてください。

FolderListViewController.swift (Controller部分)

import UIKit
import SafariServices
import SwiftReorder
import RxSwift
import RxCocoa
import SideMenu
import UIKit
import MessageUI


class FolderListViewController: BaseListViewController,UIScrollViewDelegate {
    var myModel: FolderListModel? {
        // セットされるたびにdidSetが動作する
        didSet {
            // ViewとModelとを結合し、Modelの監視を開始する
            registerModel()
        }
    }
    
    // MARK: Lifecycle
    
    override func loadView() {
        super.loadView()
        self.view = FolderListView()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        prepareViewController()

        self.myModel = FolderListModel()
        
        // インスタンス生成直後なのでクラッシュの可能性が低く、gurad letを利用しない。
        self.myModel!.getCategoryFilterdDeckList()

        // サイドメニュー
        settingSideMenu()
        settingNavigationController()
        
        // アップデートを確認する。
        UpgradeNotice.shared.fire()

        let folderListView = self.view as! FolderListView
        guard let myModel = self.myModel else { return }
        settingTableView(tableView: folderListView.tableView, delegate: self, datasource: myModel)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.navigationItem.title = "デッキカテゴリー 一覧"
        
        let folderListView = self.view as! FolderListView
        guard let model = myModel else { return }

        model.reacquisitionListAndReloadTableView(tableView: folderListView.tableView)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        APIRequest.getShareSheetData()
    }
    
    // サブメニューを表示する処理
    @objc override func settingBarButtonTapped(_ sender: UIBarButtonItem) {
        present(menuNavigationController!, animated: true, completion: nil)
    }
    
    // 継承元クラス setThemeColor:で色変更の処理を行い、追加で継承先の本クラスでテーブルビューを更新する処理を走らせる。
    override func setThemeColor() {
        super.setThemeColor()
        let folderListView = self.view as! FolderListView
        self.reloadTableView(tableView: folderListView.tableView)
    }
    
    
    private func registerModel() {
          guard let model = myModel else { return }
          
          // 配列が変化したらnotificationCenterで通知を受け取る。
          model.notificationCenter.addObserver(forName: .init(rawValue: "changeFolderNameList"),
                                               object: nil,
                                               queue: nil,
                                               using: {
              [unowned self] notification in
              let folderListView = self.view as! FolderListView
              folderListView.tableView.reloadData()
          })
      }
}


extension FolderListViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let myModel = self.myModel else { return }
        if (indexPath.row ==  myModel.folderNameList.count + 1) {
            // シェアされたデッキレシピが押下された
            let vc = ShareListViewController()
            self.navigationController?.pushViewController(vc, animated: true)
        } else if (indexPath.row ==  myModel.folderNameList.count) {
            // アーカイブされたデッキレシピが押下された時
            let vc = ArchiveListViewController()
            self.navigationController?.pushViewController(vc, animated: true)
        } else {
            let listVC = ListViewController()
            listVC.selectFolder = myModel.folderNameList[indexPath.row]
            self.navigationController?.pushViewController(listVC, animated: true)
        }
    }

}

BaseListViewController.swift (継承元のクラス)

どうしても処理が多いのでコード量が多くなってしまいましたが、
他の部分で重複するコードをここだけ1クラスで記載しているので、
なんとかなるでしょう。

import UIKit
import SafariServices
import SwiftReorder
import RxSwift
import RxCocoa
import SideMenu
import UIKit
import MessageUI



class BaseListViewController: UIViewController, SettingsDelegate, MFMailComposeViewControllerDelegate {

    // シングルトンクラス
    var dataManager = DataManager.shared
    
    // サイドメニュー
    var menuNavigationController: SideMenuNavigationController? = nil


    // MARK: Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    

    // MARK: AdMob
    
    // AdMobにバナー広告をリクエストする処理
    func requestAdMobOfBannerAd (bannerView: UIView)  {
        AdMobHelper.shared.setupBannerAd(adBaseView: bannerView, rootVC: self)
        AdMobHelper.shared.setupInterstitialAd(rootVC: self)
    }
    
    // AdMobに全画面広告をリクエストする処理
    func requestAdMobOfInterstitialAd ()  {
        AdMobHelper.shared.setupInterstitialAd(rootVC: self)
    }
    
    // MARK: データマネージャ(シングルトン)にアクセスする処理
    func prepareDataManagerForShowData() {
        dataManager.loadColor()
        dataManager.loadOriginalDataArray()
    }
    
    // MARK: 汎用的な一覧画面で行われる処理
    
    // TableViewを更新する(テーマカラー更新後などもこちらのメソッドを呼び出す)
    func reloadTableView (tableView: UITableView)  {
        tableView.reloadData()
    }
    
    // 全画面で共通しているデザインなどの画面の準備
    func prepareViewController() {
        dataManager.loadColor()
        dataManager.loadOriginalDataArray()
        setThemeColor()
        settingNavigationController()
    }
    
    // TableViewで必要な設定を行う。Delegate,DataSourceは各ViewControllerでself,Modelを設定する
    func settingTableView(tableView: UITableView, delegate:NSObject,datasource: NSObject)  {
        tableView.separatorInset = .zero
        tableView.isEditing = false
        tableView.allowsSelectionDuringEditing = true
        
        tableView.delegate = delegate as! UITableViewDelegate
        tableView.dataSource = datasource as! UITableViewDataSource
        
        tableView.register(UINib(nibName: "ArchiveTableViewCell", bundle: nil), forCellReuseIdentifier: "ArchiveCell")
        tableView.register(UINib(nibName: "DeckListTableViewCell", bundle: nil), forCellReuseIdentifier: "CustomCell")
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        tableView.tableFooterView = UIView(frame: CGRect.zero)
    }
    
    
    // シンプルなAlertViewを表示する共通処理
    func showSimpleAlertView(title: String?,message: String, ButtonText: String) {
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let defaultAction:UIAlertAction = UIAlertAction(title: ButtonText, style: .default, handler:{ (action:UIAlertAction!) -> Void in
            alertController.dismiss(animated: true, completion: nil) // 閉じる
        })
        alertController.addAction(defaultAction)
        self.present(alertController, animated: true, completion: nil)
    }
    
    // カラーを設定する
    func setThemeColor() {
        if #available(iOS 15.0, *) {
            let appearance = UINavigationBarAppearance()
            appearance.configureWithOpaqueBackground()
            appearance.backgroundColor = dataManager.themeColor
            navigationController?.navigationBar.standardAppearance = appearance
            appearance.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
            navigationController?.navigationBar.scrollEdgeAppearance = appearance
        } else {
            navigationController?.navigationBar.barTintColor = dataManager.themeColor
        }
    }
    
    // MARK: SideMenuが表示されているフォルダ一覧、デッキ一覧で表示する処理
    
    // FolderListViewControllerでサブメニューを表示する処理
    @objc func settingBarButtonTapped(_ sender: UIBarButtonItem) {
    }
    
    func settingSideMenu() {
        let menuViewController = SettingsViewController()
        menuViewController.delegate = self
        //サイドメニューのナビゲーションコントローラを生成
        menuNavigationController = SideMenuNavigationController(rootViewController: menuViewController)
        
        
        //設定を追加
        menuNavigationController!.settings = makeSettings()
//        //左,右のメニューとして追加
        SideMenuManager.default.leftMenuNavigationController = menuNavigationController
        
        if #available(iOS 15.0, *) {
            let appearance = UINavigationBarAppearance()
            appearance.configureWithOpaqueBackground()
            appearance.backgroundColor = dataManager.themeColor
            appearance.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
            SideMenuManager.default.leftMenuNavigationController?.navigationBar.scrollEdgeAppearance = appearance
            SideMenuManager.default.leftMenuNavigationController?.navigationBar.standardAppearance = appearance
        } else {
            navigationController?.navigationBar.barTintColor = dataManager.themeColor
        }
        SideMenuManager.default.addScreenEdgePanGesturesToPresent(toView: self.view, forMenu: .left)
    }
    
    //サイドメニューの設定
    private func makeSettings() -> SideMenuSettings {
        var settings = SideMenuSettings()
        //動作を指定
        settings.presentationStyle = .menuSlideIn
        //メニューの陰影度
        settings.presentationStyle.onTopShadowOpacity = 10.0
        //ステータスバーの透明度
        settings.statusBarEndAlpha = 0
        return settings
    }
    
    // NavigationBarItemを設定する
    func settingNavigationController() {
        self.navigationController?.navigationBar.tintColor = .white
        self.navigationController?.navigationBar.titleTextAttributes = [
        // 文字の色
            .foregroundColor: UIColor.white
        ]

        var createBarButtonItem: UIBarButtonItem =
        UIBarButtonItem(image: UIImage(systemName: "list.bullet")!, style: .plain, target: self, action: #selector(settingBarButtonTapped(_:)))
        self.navigationItem.leftBarButtonItems = [createBarButtonItem]
    }
    
    // 選択されたサイドバーのアイテムを取得
    func tappedSettingsItem(indexpath: IndexPath) {
        switch indexpath.row {
        case SettingsMenu.openCreateDeckRecipeSite.rawValue:
            let webPage = "https://www.pokemon-card.com/deck/deck.html"
            let safariVC = SFSafariViewController(url: NSURL(string: webPage)! as URL)
            safariVC.modalPresentationStyle = .fullScreen
            present(safariVC, animated: true, completion: nil)
        case SettingsMenu.backup.rawValue:
            if MFMailComposeViewController.canSendMail()==false {
                showSimpleAlertView(title: nil, message: "メールアプリが開けませんでした。", ButtonText: "OK")
                return
            }
            var mailViewController = MFMailComposeViewController()
            mailViewController.mailComposeDelegate = self
            var backupmsg = ""
            for deckModel in dataManager.deckList {
                backupmsg += "デッキ名:" + deckModel.deckTitle + "\nデッキコード:" + deckModel.duckRecipeId + "\nメモ:" + deckModel.memo + "\n\n"
            }
            backupmsg += "\n\n【アーカイブされたデッキレシピ】\n"
            
            for deckModel in dataManager.archiveDeckList {
                backupmsg += "デッキ名:" + deckModel.deckTitle + "\nデッキコード:" + deckModel.duckRecipeId + "\nメモ:" + deckModel.memo + "\n\n"
            }
            mailViewController.setMessageBody(backupmsg, isHTML: false)
            present(mailViewController, animated: true, completion: nil)
            
        case SettingsMenu.changeThemeColor.rawValue:
            let alertView = UIAlertController(
                title: "テーマカラー:変更",
                message: "\n\n\n\n\n\n\n\n\n",
                preferredStyle: .alert)

            let pickerView = UIPickerView(frame:
                CGRect(x: 0, y: 50, width: 270, height: 162))
            pickerView.dataSource = self
            pickerView.delegate = self

            // comment this line to use white color
            pickerView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2)

            alertView.view.addSubview(pickerView)
            
            let action = UIAlertAction(title: "OK", style: UIAlertAction.Style.default) { _ in
                self.dataManager.saveColor()
                AdMobHelper.shared.setupInterstitialAd(rootVC: self)
            }

            alertView.addAction(action)
            
            present(alertView, animated: true, completion: {
                pickerView.frame.size.width = alertView.view.frame.size.width
            })
            
        case SettingsMenu.Request.rawValue:
            let webPage = "https://forms.gle/NecGPHqX9UZFALPdA"
            let safariVC = SFSafariViewController(url: NSURL(string: webPage)! as URL)
            safariVC.modalPresentationStyle = .fullScreen
            present(safariVC, animated: true, completion: nil)
        case SettingsMenu.Howtouse.rawValue:
            showSimpleAlertView(title: nil, message: "デッキレシピ表示行(以下、行)を長押しして移動させると並び替えができます。\n\n行を左にスワイプすると「アーカイブ」できます。\n\nアーカイブされたデッキ一覧画面で行を左スワイプすると「元に戻す」「(デッキレシピ)削除」できます。\n\nデッキレシピ詳細画面で右上の共有ボタンを押すとツイートすることができます。\n\n詳細画面下部の「デッキレシピをサーバーにシェアする」を押すとサーバーにアップロードされ、トップ画面の「他のプレイヤーによってシェアされたデッキレシピ」から確認可能です。\n\nデッキ編集画面に「カテゴリー」を追加しました。こちらを変更して確定(反映)することでアプリのトップ画面でのデッキカテゴリーで分類されるようになります。", ButtonText: "OK")
        case SettingsMenu.CheckUpdate.rawValue:
            let webPage = "https://apps.apple.com/jp/app/%E3%83%9D%E3%82%B1%E3%82%AB%E3%83%87%E3%83%83%E3%82%AD%E7%AE%A1%E7%90%86/id1570965372"
            let safariVC = SFSafariViewController(url: NSURL(string: webPage)! as URL)
            safariVC.modalPresentationStyle = .fullScreen
            present(safariVC, animated: true, completion: nil)
            
        default:
            break
        }
    }

    
    

    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destination.
        // Pass the selected object to the new view controller.
    }
    */

}

// MARK: PickerView

extension BaseListViewController: UIPickerViewDataSource {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return dataManager.colorList.count
    }
    

}

extension BaseListViewController: UIPickerViewDelegate {
    // UIPickerViewに表示する配列
    func pickerView(_ pickerView: UIPickerView,
                    titleForRow row: Int,
                    forComponent component: Int) -> String? {
        
        return String(dataManager.colorList[row])
    }
    
    // UIPickerViewのRowが選択された時の挙動
    func pickerView(_ pickerView: UIPickerView,
                    didSelectRow row: Int,
                    inComponent component: Int) {
        switch row {
        case ThemeColor.themeGrass.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 60, green: 179, blue: 113, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themeFire.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 203, green: 86, blue: 70, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themeWater.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 0, green: 191, blue: 255, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themeLightning.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 255, green: 215, blue: 0, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themePsychic.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 255, green: 105, blue: 180, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themeFighting.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 210, green: 105, blue: 30, alpha: 1.0)
            setThemeColor()
            break
        case ThemeColor.themeDarkness.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 25, green: 25, blue: 112, alpha: 0.8)
            setThemeColor()
            break
        case ThemeColor.themeMetal.rawValue:
            dataManager.themeColor = UIColor.rgba(red: 192, green: 192, blue: 192, alpha: 1.0)
            setThemeColor()
            break
        default:
            break
        }
    }
}

余談

Xcode13(iOS15)対応でNavigationBarの背景色が透明になってしまう件があるのでそこの件は修正しています。

リファクタリングしてのメリット

かなり大きいです。

元々MassiveViewControllerで書くことになれていたのですが、
MVCパターンで書くことは慣れていないといえる状況です。
当初の予定ではMVCパターンを都度作ること自体に労力が割かれるのかと思いましたが、そんなことはなかったです。

しかしながら、Model-View-Controllerクラスを同じように複製し、
それぞれの責務に分けて、それぞれに処理を記載していけばそれだけでスリムなコードになるので、効率的に進めることができて、リファクタリングとは思えないほど作業が時間かからず進んでおります。

Discussion