【Swift】Massive View Controllerで作られた負債アプリ(個人開発)をMVCパターンでリファクタリングした。
MassiveViewControllerとは
マシマシViewControllerといえば分かりやすいでしょうか。
いわゆるViewControllerに機能を積み込みすぎた機能になります。
改修するきっかけについて
久々にアプリを改修したいなと思って、割と勢いだけでリリースしてしまったので、開いたら1件のViewControllerに長文で書かれており、非常にメンテナンス性が低かったのですよね。
先日MVCパターン(Swift)のサンプル記事なども投稿したこともあり、よりメンテナンス性が高いコードへ移行する体験や、実際に感じたメリットなどを記載できればより良いなということで本記事を執筆しております。
アプリの紹介
簡単に説明すると、オフラインでも機能する「メモ帳アプリ」のようなものです。
メモ帳を都度作成することができ、そのメモ帳に「カテゴリ名」を設定することが可能です。(デッキ?なにそれ)
アプリ起動時はカテゴリ一覧を漏れなくダブリなく取得し、トップ画面に五十音順で並べます。
今回はまず、その画面をリファクタリングを行うことにしました。
上記画像ですと、カテゴリー名「(空白)」と「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