注意
Objective-CでのMVCですが、かなり多くの文献・書籍を確認しましたが
モデルの捉え方や挙動などで差異がある記事が多かったので、
今回は下記のMVC(モデルビューコントローラ)モデルで作成してます。
また、あえて難易度が高いであろうUITableViewを描画してみます。
今後更新がある場合は本記事でも更新したいと思います。
ご指摘など頂けるのであればコメント欄でご教示頂きたく存じます。
こういう人に向けて発信しています。
・Objective-CでMVCモデルを学びたい人
・処理の流れを知りたい人
・Objective-C中級者
アプリイメージ
ADDボタンを押下するとセルが増えて、
セルをタップするとラベルのテキストが反映されるシンプルアプリです。
Model(NSObject)
・データの保持(Viewなどで描画する内容データなども)
・データの変更をControllerに通知する。
・ビジネスロジック(処理全般)
ADDボタンを押下するとセルが増えて、
セルをタップするとラベルのテキストが反映されるシンプルアプリです。
Model(NSObject)
・データの保持(Viewなどで描画する内容データなども)
・データの変更をControllerに通知する。
・ビジネスロジック(処理全般)
View(UIView)
・ユーザーに見える画面のレイアウト・描画
・ユーザーアクションをコントローラに通知する
Controller(UIViewController)
・Viewでのユーザーアクションを通知された後にModelを変更する。
・Modelの変更を通知された後、Viewを更新する。
クラス構成
・DemoModel.h(NSObjectサブクラス)
・DemoModel.m
・DemoController.h(UIViewControllerサブクラス)
・DemoController.m
・DemoView.h(UIViewサブクラス)
・DemoView.m
・DemoView.xib
・DemoModel.h(NSObjectサブクラス)
# import <Foundation/Foundation.h>
# import <UIKit/UIKit.h> //本記述があればUITableViewDataSource指定できる
@interface DemoModel : NSObject<UITableViewDataSource>
/**
@brief 上部ラベルに挿入されるテキスト
*/
@property (nonatomic,strong) NSString *topLabelText;
/**
@brief セルに表示されるテキストが入っている配列
*/
@property (nonatomic) NSMutableArray<NSString *> *cellTextArray;
@end
・DemoController.m
# import "DemoModel.h"
@implementation DemoModel
# pragma mark - Initialize
/**
初期設定を行う
@return インスタンス
*/
- (instancetype)init{
if (self = [super init]) {
self.topLabelText = @"押下するとセルのテキストが反映";
self.cellTextArray = @[@"No0",@"No1",@"No2",@"No3",@"No4",@"No5",@"No6"].mutableCopy;
}
return self;
}
# pragma mark UITableViewDelegate
/**
@brief TableViewで描画する件数を返す
*/
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return self.cellTextArray.count;
}
/**
@brief TableViewで描画するセルの内容を返す
@return セル(UITableViewCell)
*/
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//標準で用意されているTableViewを利用する場合。
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
if (!cell) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
}
cell.textLabel.text = self.cellTextArray[indexPath.row];
return cell;
}
@end
・DemoController.h(UIViewControllerサブクラス)
# import <UIKit/UIKit.h>
# import "DemoModel.h"
@interface DemoViewController : UIViewController
@property (nonatomic) DemoModel *model;
@end
・DemoController.m
# import "DemoViewController.h"
# import "DemoView.h"
@interface DemoViewController ()<DemoViewDelegate,UITableViewDelegate>
@property (nonatomic) DemoView *demoView;
//注意:NSMutableArrayの追加・削除を検知出来ないのでコントローラ内に監視用同一インスタンスを生成する。
@property (nonatomic) NSMutableArray<NSString *> *observingArray;
@end
@implementation DemoViewController
# pragma mark over ride
/**
@brief DemoViewControllerではxibを有していないのでDemoView(UIViewを継承)を採用する。
*/
- (void)loadView{
self.demoView = [[DemoView alloc]init];
self.view = self.demoView;
//本来は外部クラスから本クラスで初期化される際に初期化されて本クラスのインスタンスにセットされる。
//今回はアプリ起動後、本画面に遷移するのでモデルも初期化する。
self.model = [[DemoModel alloc]init];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.model addObserver:self forKeyPath:@"topLabelText" options:NSKeyValueObservingOptionInitial context:@selector(textLabelUpdate)];
[self.model addObserver:self forKeyPath:@"cellTextArray" options:NSKeyValueObservingOptionInitial context:@selector(cellTextArrayUpdate)];
//NSMutableArrayで追加・削除が検知出来ないのでモデルから取り出して同一インスタンス配列を持つ
self.observingArray = [self.model mutableArrayValueForKeyPath:@"cellTextArray"];
//DemoViewの設定を行う
[self loadViewScreen];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context {
NSLog(@"%@",change);
NSLog(@"context %s",context);
[self performSelector:context withObject:nil afterDelay:0];
}
- (void)textLabelUpdate {
self.demoView.showLabel.text = self.model.topLabelText;
}
- (void)cellTextArrayUpdate {
[self.demoView.tableView reloadData];
}
/**
@brief DemoViewの設定を行う
*/
- (void)loadViewScreen{
//DemeViewのdelegateを採用する事でDemeViewでオブジェクトを操作した際の通知を本クラスで受け取る。
self.demoView.delegate = self;
/*DemoViewに存在するTableViewについて
(1)delegateはコントローラ(本クラス)で行う。画面遷移などモデルで行うのが不適と考えた為。
(2)dataSourceに関してはモデルで行う。
*/
self.demoView.tableView.delegate = self;
self.demoView.tableView.dataSource = self.model;
}
# pragma mark DemoViewDelegate
/**
@brief ビューで押下したイベントを通知で受け取る。
*/
- (void)tappedButton:(DemoView *)demoView button:(UIButton *)button{
[self.observingArray addObject:@"AddCell"];
}
# pragma mark tableViewDelegate
//押下された時に呼び出される処理
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
self.model.topLabelText = selectedCell.textLabel.text;
}
@end
・DemoView.h(UIViewサブクラス)
# import <UIKit/UIKit.h>
@class DemoView;
@protocol DemoViewDelegate<NSObject>
/**
押下時の処理
@param demoView 対象View
@param button 対象ボタン
*/
- (void)tappedButton:(DemoView *)demoView
button:(UIButton *)button;
@end
@interface DemoView : UIView
@property (weak, nonatomic) IBOutlet UILabel *showLabel;
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic, weak) id<DemoViewDelegate> delegate;
@end
・DemoView.m
# import "DemoView.h"
@implementation DemoView
# pragma mark - Initialize
/**
@brief 初期設定を行う(本クラス名と同一のxibを採用。結果配列として格納されるので、1番目のインスタンスを自身のインスタンスとする)
@return インスタンス
*/
- (instancetype)init{
NSArray *array = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
owner:self
options:nil];
self = array.firstObject;
return self;
}
# pragma mark - over ride
/**
@brief コードで位置調整する場合は本関数を用意して調整する。
*/
- (void)layoutSubviews{
}
/**
@brief ボタン押下時の処理をコントローラに通知する。
*/
- (IBAction)tappedButton:(id)sender {
if ([self.delegate respondsToSelector:@selector(tappedButton:button:)]) {
[self.delegate tappedButton:self button:sender];
}
}
@end
・DemoView.xib
どういう処理が行われているのか
□ユーザーに表示されるまで
(1)コントローラが初期化され、ビューを初期化して自身のビューとして採用されている。
(2)コントローラ内でモデルを初期化する。
(ここでモデル内のデータの初期値をセットしている)
(3)モデル内のデータ(特定プロパティ)が更新されたら、コントローラのメソッドを呼び出すように設定する。
(4)2にて更新されているのでコントローラで通知を受け取り、ビューのレイアウトを更新する。
□ユーザーアクションが行われてから反映されるまで
(1)ビューでのアクション接続メソッドが呼び出され、デリゲートによりコントローラに押下されたと通知する。
(2)コントローラにて、モデルのデータを変更する。
(3)コントローラにて、モデルの変更が行われたので、モデルの最新の値を参照してビューの更新処理が行われる。
気をつけたところ:UITableView
UITableViewDataSourceをモデル、
UITableViewDelegateをコントローラで指定しております。
下記の記事にてこのように書かれていました。
確かにTableViewを描画する処理はアプリケーションの処理に含まれる処理であると思いますが、
他の画面に遷移するのはモデルでは行うべきではないという気持ちは分かります。
2.delegateはself,dataSourceはmodel
上のコードでは、tableのdategateにselfを突っ込んで、dataSourceには作ったmodelを突っ込んでいます。
これはどうしてかと申しますと、dataSourceに関しては、すべてdataSourceメソッド内だけで完結させる処理を書きますが、delegateに関しては、例えばセルがタップされたら別の画面へ遷移といったようなことをする必要があり、modelに書くのは得策ではないので、delegateはselfを突っ込みます。
Objective-Cで注意が必要なのは、importが必要な点です。
# import <Foundation/Foundation.h>
# import <UIKit/UIKit.h>
@interface DemoModel : NSObject<UITableViewDataSource>
気をつけたところ:NSMutableArray
モデルでTableViewを描画する為だけに持っていた配列ですが、
コントローラ内で「= nil」は通知受け取れるのに、
addObjectやremoveObjectでの差分が受け取れない事象が発生しました。
こちらを参考にし、mutableArrayValueForKeyPath: メソッドで取得した NSMutableArray インスタンスでコントローラ自体に同一配列インスタンスを持ちました。
(モデルと同一の内容になり別インスタンスではありません)
モデルの配列変更する際は、
こちら(コントローラ内のインスタンス変数としての配列)を
追加・削除する事にしました。