こんにちは!Popoです。
こんな方へのお勧めの記事です。
- 自作のサイドメニューを作成したい
- UITableViewの応用機能を学習したい
サイドメニューを作ろうとした場合!
ライブラリで実装するのが一番楽なんだけど、お客さんの細かな要望を対応しようとすると。。。。
対応できない事が多いな😥
こんな状況になってしまう事、多いのではないでしょうか。
ライブラリでの実装が完了している状態で、上記問題が発生するとサイドメニューの作り直しになりかねません。
スケジュールもあるので「今更作り直しはできない」となります。
「要望は諦めてもらう」といった対応になってしまいます。
私も何度かそんな経験をしましたが、それであれば!
はじめから、自作で作成するようにすればいい!
はじめから、この考えで対応をしています。
自作であればカスタマイズは自在です😊
という事で、今回は自作のサイドメニューを作成していきたいと思います。
下記の記事で使用したUITableViewとdelegateを利用して自作のサイドメニューを作成してみます。
2つの記事を利用してみましょう!
サイドメニューも色々なアプリで利用されています。
ライブラリを利用する事も有効ですが、自作できればカスタマイズが自由自在です。
アプリの動作環境
下記がアプリの動作環境になります。
項目 | バージョン |
Xcode | Version 14.3.1 (14E300c) |
Swift | Swift version 5.8.1 |
MacOS | macOS Ventura バージョン13.4(22F66) |
サイドメニューの設計
はじめにサイドメニューの設計を行ってみます。
下記の考え方でロジックを実装します。
delegateはサイドメニュー画面からメイン画面に値を受け渡すために使用します。
- ①あらかじめサイドメニューのUIViewを表示画面右端に生成しておきます。
- ②サイドメニューを表示する場合、①のUIViewを表示画面にアニメーション付きで移動させてやります。
- ③サイドメニューを非表示にする場合は、①のUIViewを再度表示画面右端にアニメーション付きで移動させてやります。
メリット、デミリットは下記になると思います。
メリット | デメリット |
---|---|
カスタマイズがしやすい | アプリ全体で共通利用する場合に一手間かかる |
アプリのソースコード全体
はじめにソースコード全体を見てみたいと思います。
UIViewController (OneViewController)
OneViewControllerは、サイドメニューを表示するメインの画面クラスです。
主な処理
- ナビゲーションバーに「MENU」ボタン設置
- サイドメニューのUIView生成とOPEN・CLOSE制御
- MENUボタンタップイベント
- delegateメソッドの定義
import UIKit
class OneViewController: UIViewController, SideMenuViewDelegate {
//サイドメニュー
var sideMenuView : SideMenuView!
//表示用Label
let oneLabel:UILabel = UILabel()
//スターテスバー高さ
var statusBarHeight: CGFloat = 0
//ナビゲーションバーの高さ
var navBarHeight:CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "Popo"
let attrs: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.font: UIFont(name: "HiraginoSans-W6",size:17)!,
.baselineOffset:1
]
// iOS15以降の場合
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = .brown
appearance.titleTextAttributes = attrs
self.navigationController?.navigationBar.scrollEdgeAppearance = appearance
//MENUボタン生成
let menuBarButtonItem: UIBarButtonItem = UIBarButtonItem(title: "MENU", style: .done, target: self, action: #selector(menuBarButtonItemTapped(_:)))
//バーボタンアイテムの追加
self.navigationItem.rightBarButtonItems = [menuBarButtonItem]
//ステータスバー高さ取得
if #available(iOS 13.0, *) {
//let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
let window = UIApplication
.shared
.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.last
self.statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
} else {
self.statusBarHeight = UIApplication.shared.statusBarFrame.height
}
// ナビゲーションバーの高さを取得
self.navBarHeight = self.navigationController?.navigationBar.frame.size.height ?? 0
if self.sideMenuView == nil
{
// サイドメニューの作成
self.createSideMenu()
}
//delegateメソッドからの表示用Label
self.oneLabel.frame = CGRect(x: SCREEN_WIDTH/2 - (SCREEN_WIDTH/3.5)/2, y: SCREEN_HEIGHT/2, width: SCREEN_WIDTH/3.5, height: 40)
self.oneLabel.textAlignment = NSTextAlignment.left
self.oneLabel.font = UIFont(name:"HiraKakuProN-W3",size:16)!
self.oneLabel.textColor = .red
self.oneLabel.backgroundColor = .clear
//cellnavLabel.backgroundColor = .yellow
self.oneLabel.text = ""
self.view.addSubview(self.oneLabel)
}
// "menu"ボタンが押された時の処理
@objc func menuBarButtonItemTapped(_ sender: UIBarButtonItem)
{
//MENUのOPEN、CLOSE制御
if self.sideMenuView.isOpen
{
self.closeMenu()
}
else
{
self.openMenu()
}
}
}
// サイドメニュー
extension OneViewController
{
// サイドメニューの作成
fileprivate func createSideMenu()
{
//サイドメニュー生成
self.sideMenuView = SideMenuView()
self.sideMenuView.frame = CGRect(x: SCREEN_WIDTH + 300, y: self.navBarHeight + self.statusBarHeight, width: 300, height: SCREEN_HEIGHT - (self.navBarHeight + self.statusBarHeight))
self.sideMenuView.delegate = self
//枠線の太さを指定
self.sideMenuView.layer.borderWidth = 1.0
//枠線の色を指定
self.sideMenuView.layer.borderColor = UIColor.lightGray.cgColor
let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.addSubview(sideMenuView)
//let keyWindow = UIApplication.shared.keyWindow
//let keyWindow = UIApplication.shared.windows.first { $0.isKeyWindow }
let keyWindow = UIApplication
.shared
.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.last
keyWindow?.insertSubview(self.sideMenuView, aboveSubview: self.view)
}
// サイドメニューOPEN
fileprivate func openMenu()
{
self.sideMenuView.isOpen = true
let rect:CGRect = CGRect(x: SCREEN_WIDTH - 300, y: self.navBarHeight + self.statusBarHeight, width: 300, height: SCREEN_HEIGHT - (self.navBarHeight + self.statusBarHeight))
// アニメーションして移動
UIView.animate(withDuration: 0.1,
animations: { () -> Void in
self.sideMenuView.frame = rect
},
completion: { (finished: Bool) -> Void in
}
)
}
//サイドメニューCLOSE
func closeMenu(completion: ((Bool) -> Swift.Void)? = nil)
{
if self.sideMenuView == nil
{
// 一度も表示されていない
return
}
//MENUはCLOSE
self.sideMenuView.isOpen = false
//MENUのサイズ
let rect:CGRect = CGRect(x: SCREEN_WIDTH + 300, y: self.navBarHeight + self.statusBarHeight, width: 300, height: SCREEN_HEIGHT - (self.navBarHeight + self.statusBarHeight))
// アニメーションして移動
UIView.animate(withDuration: 0.1,
animations:
{ () -> Void in
self.sideMenuView.frame = rect
},
completion: completion
)
}
//delegate メソッド
func selectSideMenu(_ selectedIndexName: String)
{
self.oneLabel.text = selectedIndexName
//MENUのOPEN、CLOSE制御
if self.sideMenuView.isOpen
{
self.closeMenu()
}
else
{
self.openMenu()
}
}
}
UIView (SideMenuView)
SideMenuViewは、サイドメニュー本体のクラスです。
主な処理
- protocol定義
- UITableView生成
import UIKit
// プロトコルを作る。
protocol SideMenuViewDelegate: AnyObject {
func selectSideMenu(_ selectedIndexName: String)
}
class SideMenuView: UIView {
//delegate定義
weak var delegate: SideMenuViewDelegate?
//UITableView定義
fileprivate var sideTableView: UITableView!
let SCREEN_WIDTH = UIScreen.main.bounds.width
let SCREEN_HEIGHT = UIScreen.main.bounds.height
var navigationBarHeight: CGFloat = 44
var statusHieght:CGFloat = 0
var cellCounter:Int = 0
// 表示状態
var isOpen: Bool = false
override func draw(_ rect: CGRect)
{
//UITableView
self.sideTableView = UITableView()
self.sideTableView.frame = CGRect(x: 0, y: 0, width: 300, height: SCREEN_HEIGHT)
// DataSourceの設定をする.
self.sideTableView.dataSource = self
// Delegateを設定する.
self.sideTableView.delegate = self
self.sideTableView.separatorInset = UIEdgeInsets.zero // 区切り線を左まで伸ばす
self.sideTableView.backgroundColor = RGBAlpa(238, 245, 243, 1)
self.sideTableView.separatorStyle = .singleLine
self.sideTableView.separatorColor = .gray
self.addSubview(self.sideTableView)
}
}
extension SideMenuView: UITableViewDelegate, UITableViewDataSource {
// MARK: - Table view data source
/*
セクション数を返すデータソースメソッド.
(実装必須)
*/
func numberOfSections(in tableView: UITableView) -> Int
{
return 1
}
/*
Cellの総数を返すデータソースメソッド.
(実装必須)
*/
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return 9
}
/*
Cellの高さを返すデータソースメソッド.
(実装必須)
*/
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 60
}
/*
Cellに値を設定するデータソースメソッド.
(実装必須)
*/
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let workCellName : String! = NSString(format:"MyCell%d_%d",self.cellCounter,self.cellCounter) as String
self.cellCounter = self.cellCounter + 1
self.sideTableView.register(UITableViewCell.self, forCellReuseIdentifier: workCellName)
let cell = self.sideTableView.dequeueReusableCell(withIdentifier: workCellName, for: indexPath)
//これでセルをタップ時、色は変化しなくなる
cell.selectionStyle = UITableViewCell.SelectionStyle.none
//二重に表示されるのを防ぐ
for subview in cell.contentView.subviews{
subview.removeFromSuperview()
}
let index = indexPath.row
let cellnavLabel = UILabel(frame: CGRect(x: 30, y: 5, width: cell.frame.width - 30, height: cell.frame.height - 10))
cellnavLabel.textAlignment = NSTextAlignment.left
cellnavLabel.font = UIFont(name:"HiraKakuProN-W3",size:16)!
cellnavLabel.textColor = .black
cellnavLabel.backgroundColor = .clear
//cellnavLabel.backgroundColor = .yellow
cellnavLabel.text = self.setCellLabel(index)
cell.contentView.addSubview(cellnavLabel)
return cell
}
/*
Cellが選択された際に呼び出されるデリゲートメソッド.
*/
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
sideTableView?.deselectRow(at: indexPath, animated: true)
let saveName:String = self.setCellLabel((indexPath as NSIndexPath).row) ?? ""
// デリゲートメソッドを呼ぶ(処理をデリゲートインスタンスに委譲する)
self.delegate?.selectSideMenu(saveName)
}
}
extension SideMenuView {
// ログインしている時のサイドメニュー
func setCellLabel(_ index: Int) -> String?
{
switch index
{
case 0:
// メニュー1
return "メニュー1"
case 1:
// メニュー2
return "メニュー2"
case 2:
// メニュー3
return "メニュー3"
case 3:
// メニュー4
return "メニュー4"
case 4:
// メニュー5
return "メニュー5"
case 5:
//メニュー6
return "メニュー6"
case 6:
// メニュー7
return "メニュー7"
case 7:
// メニュー8
return "メニュー8"
case 8:
// メニュー9
return "メニュー9"
default:
return nil
}
}
}
画面動作
サイドメニューの動作はこんな感じになります。
各ロジック解説
各クラスのロジックを解説していきます。
OneViewController
メインのUIViewController クラスから解説します。
viewDidLoad
- ナビゲーションバーのカスタマイズ
ナビゲーションバーのタイトル、フォント色、種類、文字サイズを設定します。
self.navigationItem.title = “Popo”
→ナビゲーションバーのタイトルを設定します。
let attrs: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.font: UIFont(name: “HiraginoSans-W6”,size:17)!,
.baselineOffset:1
]
→フォント色、種類、サイズ定義
// iOS15以降の場合
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = .brown
appearance.titleTextAttributes = attrs
self.navigationController?.navigationBar.scrollEdgeAppearance = appearance
→ナビゲーションバーの背景色、タイトルのフォント色、種類、サイズ設定
- 「MENU」ボタン生成
今回は、「UIBarButtonItem」を利用しました。
//MENUボタン生成
let menuBarButtonItem: UIBarButtonItem = UIBarButtonItem(title: “MENU”, style: .done, target: self, action: #selector(menuBarButtonItemTapped(_:)))
//バーボタンアイテムの追加
self.navigationItem.rightBarButtonItems = [menuBarButtonItem]
- ステータスバー、ナビゲーションバーの高さ取得
MENUのサイズ調整のためステータスバー、ナビゲーションバーの高さをあらかじめ取得しておきます。
//ステータスバー高さ取得
if #available(iOS 13.0, *) {
//let window = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
let window = UIApplication
.shared
.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.last
self.statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
} else {
self.statusBarHeight = UIApplication.shared.statusBarFrame.height
}
// ナビゲーションバーの高さを取得
self.navBarHeight = self.navigationController?.navigationBar.frame.size.height ?? 0
- サイドメニュー生成
if self.sideMenuView == nil
{
// サイドメニューの作成
self.createSideMenu()
}
- サイドメニューから引き継いだ文字列を表示するラベル生成
「viewDidLoad」でサイドメニューUIView生成メソッドをCALLしておきます。
//delegateメソッドからの表示用Label
self.oneLabel.frame = CGRect(x: SCREEN_WIDTH/2 – (SCREEN_WIDTH/3.5)/2, y: SCREEN_HEIGHT/2, width: SCREEN_WIDTH/3.5, height: 40)
self.oneLabel.textAlignment = NSTextAlignment.left
self.oneLabel.font = UIFont(name:”HiraKakuProN-W3″,size:16)!
self.oneLabel.textColor = .red
self.oneLabel.backgroundColor = .clear
//cellnavLabel.backgroundColor = .yellow
self.oneLabel.text = “”
self.view.addSubview(self.oneLabel)
選択したセルをわかりやすくするためにUILabelを貼り付けておきます。
// “menu”ボタンが押された時の処理
@objc func menuBarButtonItemTapped(_ sender: UIBarButtonItem)
{
//MENUのOPEN、CLOSE制御
if self.sideMenuView.isOpen
{
self.closeMenu()
}
else
{
self.openMenu()
}
}
MENUボタンのタップイベントで、サイドメニューのOPEN、CLOSEを制御します。
サイドメニューの作成
// サイドメニューの作成
fileprivate func createSideMenu()
{
//サイドメニュー生成
self.sideMenuView = SideMenuView()
self.sideMenuView.frame = CGRect(x: SCREEN_WIDTH + 300, y: self.navBarHeight + self.statusBarHeight, width: 300, height: SCREEN_HEIGHT – (self.navBarHeight + self.statusBarHeight))
self.sideMenuView.delegate = self
//枠線の太さを指定
self.sideMenuView.layer.borderWidth = 1.0
//枠線の色を指定
self.sideMenuView.layer.borderColor = UIColor.lightGray.cgColor
let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.addSubview(sideMenuView)
//let keyWindow = UIApplication.shared.keyWindow
//let keyWindow = UIApplication.shared.windows.first { $0.isKeyWindow }
let keyWindow = UIApplication
.shared
.connectedScenes
.compactMap { ($0 as? UIWindowScene)?.keyWindow }
.last
keyWindow?.insertSubview(self.sideMenuView, aboveSubview: self.view)
}
サイドメニューのUIViewを生成して、最前画面にサイドメニューを挿入します。
あらかじめ、画面の右端にサイドメニューを貼り付けて隠しておきます。
サイドメニューOPEN
画面右端に隠しておいたサイドメニューを画面全面に移動させるイメージになります。
// サイドメニューOPEN
fileprivate func openMenu()
{
self.sideMenuView.isOpen = true
let rect:CGRect = CGRect(x: SCREEN_WIDTH – 300, y: self.navBarHeight + self.statusBarHeight, width: 300, height: SCREEN_HEIGHT – (self.navBarHeight + self.statusBarHeight))
// アニメーションして移動
UIView.animate(withDuration: 0.1,
animations: { () -> Void in
self.sideMenuView.frame = rect
},
completion: { (finished: Bool) -> Void in
}
)
}
サイドメニューCLOSE
CLOSE時は逆で、表示画面に移動されたサイドメニューを右端に移動して隠します。
//サイドメニューCLOSE
func closeMenu(completion: ((Bool) -> Swift.Void)? = nil)
{
if self.sideMenuView == nil
{
// 一度も表示されていない
return
}
//MENUはCLOSE
self.sideMenuView.isOpen = false
//MENUのサイズ
let rect:CGRect = CGRect(x: SCREEN_WIDTH + 300, y: self.navBarHeight + self.statusBarHeight, width: 300, height: SCREEN_HEIGHT – (self.navBarHeight + self.statusBarHeight))
// アニメーションして移動
UIView.animate(withDuration: 0.1,
animations:
{ () -> Void in
self.sideMenuView.frame = rect
},
completion: completion
)
}
delegate メソッド
サイドメニューから戻り時の処理です。
引き継いだ文字列をUILabelに設定します。
メニュー選択時は、サイドメニューをCLOSEさせます。
//delegate メソッド
func selectSideMenu(_ selectedIndexName: String)
{
self.oneLabel.text = selectedIndexName
//MENUのOPEN、CLOSE制御
if self.sideMenuView.isOpen
{
self.closeMenu()
}
else
{
self.openMenu()
}
}
SideMenuView
サイドメニュー本体のviewクラスです。
protocolを定義
サイドメニューで選択したメニューの情報を引き継ぐため、protocolを定義します。
// プロトコルを作る。
protocol SideMenuViewDelegate: AnyObject {
func selectSideMenu(_ selectedIndexName: String)
}
UITableView生成
UITableViewを生成して、サイドメニュー画面に貼り付けます。
override func draw(_ rect: CGRect)
{
//UITableView
self.sideTableView = UITableView()
self.sideTableView.frame = CGRect(x: 0, y: 0, width: 300, height: SCREEN_HEIGHT)
// DataSourceの設定をする.
self.sideTableView.dataSource = self
// Delegateを設定する.
self.sideTableView.delegate = self
self.sideTableView.separatorInset = UIEdgeInsets.zero // 区切り線を左まで伸ばす
self.sideTableView.backgroundColor = RGBAlpa(238, 245, 243, 1)
self.sideTableView.separatorStyle = .singleLine
self.sideTableView.separatorColor = .gray
self.addSubview(self.sideTableView)
}
UITableViewDataSourceメソッド
サイドメニューはUITableViewで作成します。
- UITableViewのセッション数、セル数、セルの高さを定義します。
// MARK: – Table view data source
/*
セクション数を返すデータソースメソッド.
(実装必須)
*/
func numberOfSections(in tableView: UITableView) -> Int
{
return 1
}
/*
Cellの総数を返すデータソースメソッド.
(実装必須)
*/
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return 9
}
/*
Cellの高さを返すデータソースメソッド.
(実装必須)
*/
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 60
}
/*
- UILabelを生成して、セルに表示するメニュー名を表示させます。
/*
Cellに値を設定するデータソースメソッド.
(実装必須)
*/
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let workCellName : String! = NSString(format:”MyCell%d_%d”,self.cellCounter,self.cellCounter) as String
self.cellCounter = self.cellCounter + 1
self.sideTableView.register(UITableViewCell.self, forCellReuseIdentifier: workCellName)
let cell = self.sideTableView.dequeueReusableCell(withIdentifier: workCellName, for: indexPath)
//これでセルをタップ時、色は変化しなくなる
cell.selectionStyle = UITableViewCell.SelectionStyle.none
//二重に表示されるのを防ぐ
for subview in cell.contentView.subviews{
subview.removeFromSuperview()
}
let index = indexPath.row
let cellnavLabel = UILabel(frame: CGRect(x: 30, y: 5, width: cell.frame.width – 30, height: cell.frame.height – 10))
cellnavLabel.textAlignment = NSTextAlignment.left
cellnavLabel.font = UIFont(name:”HiraKakuProN-W3″,size:16)!
cellnavLabel.textColor = .black
cellnavLabel.backgroundColor = .clear
//cellnavLabel.backgroundColor = .yellow
cellnavLabel.text = self.setCellLabel(index)
cell.contentView.addSubview(cellnavLabel)
return cell
}
- メニュー選択時、delegateメソッドを発行します。今回はメニュー名のStringを引き継ぎます。
今回はメニュー名ですが、例えば添字やurl、遷移先の画面名等などを引き継ぎ次処理の情報とします。
/*
Cellが選択された際に呼び出されるデリゲートメソッド.
*/
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
sideTableView?.deselectRow(at: indexPath, animated: true)
let saveName:String = self.setCellLabel((indexPath as NSIndexPath).row) ?? “”
// デリゲートメソッドを呼ぶ(処理をデリゲートインスタンスに委譲する)
self.delegate?.selectSideMenu(saveName)
}
その他
- セルのindexPath.Rowの値によってメニュー名を返却してやるメソッドです。
extension SideMenuView {
// ログインしている時のサイドメニュー
func setCellLabel(_ index: Int) -> String?
{
switch index
{
case 0:
// メニュー1
return “メニュー1”
case 1:
// メニュー2
return “メニュー2”
case 2:
// メニュー3
return “メニュー3”
case 3:
// メニュー4
return “メニュー4”
case 4:
// メニュー5
return “メニュー5”
case 5:
//メニュー6
return “メニュー6”
case 6:
// メニュー7
return “メニュー7”
case 7:
// メニュー8
return “メニュー8”
case 8:
// メニュー9
return “メニュー9”
default:
return nil
}
}
}
まとめ
下記2記事の応用として、自作サイドメニューを作成してみました。
自由にカスタマイズする事ができるという点で、自作のサイドメニューは便利ですね。
メリット | デメリット |
---|---|
カスタマイズがしやすい | アプリ全体で共通利用する場合に一手間かかる |
メリット、デミリットを認識して活用していただきたいと思います。
それではまた!