MVPパターンとは
MVPには2つの種類が存在する。
①,Passive方式(フロー同期)
絶対にPresentarを通る設計になる。(ViewはModelに直接アクセスできない)
データの流れ(Passive方式、フロー同期)
1,ユーザー操作
2,入力、タップなどのイベント
3,Presentarで受け取る(Input)
4,Modelに通知してデータをもらってくる
5,PresentarでUIの更新を指示していく
⇨コードは長くなりそうだけどデータの移動は読みやすそう?
フロー同期 Presentarの処理中に何らかのタイミングでViewを更新させていく状態管理の方法
②,SuperVising方式(フロー同期、オブザーバー同期)
1ユーザー操作
2,入力、タップなどのイベント
3、Presentarで受け取り(Input)Modelに値を渡す
4、データが変更された通知を直接Viewに渡してView自身に更新させる
オブザーバー同期:Model の変更通知を受け取り、View自身のロジックで更新していく
なぜMVPを実装するのか
よく言われるFatViewControllerにならないようにするため.
UIの変更やライフサイクルの管理などもViewControllerがしなくてはならないので、可読性が下がってしまうから。
Model
実際にデータの処理を行う。ドメインロジックを担う
View
Presentarにイベントを渡して(Input)、outputとしてUI更新の指示をもらい更新する。
Passive方式では Viewは基本的に受動的にUI更新の指示を待つだけの存在となる。
また、Modelと直接アクセスしないためModelとViewが疎の状態となっている。
Presentar
ModelとViewを仲介する役割を持つ。
プレゼンテーションロジックを担当する。プレゼンテーションロジックはユーザーの挙動によって、アプリケーションの動きを指示していくロジックのこと
感想
MVCに慣れているので違う設計は楽しい!
もっとIOS設計について学びたい!!
PokemonAPIでポケモン図鑑を作る(MVP,Passive View方式)
https://pokeapi.co/
簡単にPokemonAPIを使用してMVPのアプリを作成しました.
コピペではつかえませんが、、、
Model
protocol GetPokemonDataInput {
func fetchPokemon(completion:@escaping([PokemonModel])->Void)
}
class PokemonDataModel:GetPokemonDataInput {
func fetchPokemon(completion:@escaping([PokemonModel])->Void) {
var pokemonArray = [PokemonModel]()
let dispatchGroup = DispatchGroup()
for i in 1...386 {
dispatchGroup.enter()
let pokemonURL = "https://pokeapi.co/api/v2/pokemon-species/\(i)"
let pokemonDetailURL = "https://pokeapi.co/api/v2/pokemon/\(i)"
let keyNumber = i > 251 ? 23 : i > 151 ? 26 : 29
let keyNumber2 = i > 251 ? 31 : i > 151 ? 34 : 37
let keyNumber3 = i > 251 ? 47 : i > 151 ? 50 : 38
AF.request(pokemonURL).responseJSON { response in
switch response.result {
case .success:
do {
guard let safeData = response.data else { return }
let data = try JSONDecoder().decode(PokemonData.self,from:safeData)
let name = data.names[0].name
let id = data.id
let genera = data.genera[0].genus
let explain = data.flavor_text_entries[keyNumber].flavor_text
let explain2 = data.flavor_text_entries[keyNumber2].flavor_text
let explain3 = data.flavor_text_entries[keyNumber3].flavor_text
AF.request(pokemonDetailURL).responseJSON { response in
switch response.result {
case .success:
do {
defer { dispatchGroup.leave() }
let data = try JSONDecoder().decode(PokemonDetail.self, from: response.data!)
let height = data.height
let weight = data.weight
let imageUrl = data.sprites.front_default
let type = data.types[0].type.name
let pokemon = PokemonModel(name: name, id: id, genus: genera, explain: explain, explain2: explain2, explain3: explain3, height: height, weight: weight, urlImage: imageUrl,type:type)
pokemonArray.append(pokemon)
} catch {
print(error)
}
case .failure(let error):
print(error)
}
}
} catch {
print(error)
}
case .failure(let error):
print(error)
}
}
}
dispatchGroup.notify(queue: .main) {
completion(pokemonArray)
}
}
}
Presentar
import Foundation
protocol PokemonPresentarInput:AnyObject {
var numberOfPokemon:Int { get }
var numberOfSavePokemon:Int { get }
func didSelectTap(indexPath:IndexPath)
func viewDidLoad()
func pokemon(row:Int)->PokemonModel?
func savePokemon(row:Int)->PokemonModel?
func addPokemon(index:Int)
func searchTextInput(text:String)
func deleteFavorite(index:Int)
}
protocol PokemonPresentarOutput:AnyObject {
func gotoPokemonDetail(pokemon:PokemonModel)
func pokemonDataOutPut(pokemon:[PokemonModel])
func filterPokemonOutput(pokemon:[PokemonModel])
}
protocol PokemonFavoritePresentarOutput:AnyObject {
func deleteComplete()
}
class PokemonPresentar:PokemonPresentarInput {
//Properties
private var pokemons = [PokemonModel]()
private weak var viewOutput:PokemonPresentarOutput!
private var pokemonDataModel:GetPokemonDataInput
private var savePokemons = UserDefaultsRepository.loadFromUserDefaults()
private weak var favoriteOutput:PokemonFavoritePresentarOutput!
var numberOfPokemon: Int {
return pokemons.count
}
var numberOfSavePokemon:Int {
return savePokemons.count
}
//Mark initialize
init(viewOutput:PokemonPresentarOutput,modelInput:GetPokemonDataInput) {
self.viewOutput = viewOutput
self.pokemonDataModel = modelInput
}
init(favoriteViewOutput:PokemonFavoritePresentarOutput,modelInput:GetPokemonDataInput) {
self.favoriteOutput = favoriteViewOutput
self.pokemonDataModel = modelInput
}
//Mark inputMethod
func viewDidLoad() {
print(#function)
pokemonDataModel.fetchPokemon { [weak self] pokemons in
guard let self = self else { return }
let pokemonArray = pokemons.sorted(by: { $0.id < $1.id })
self.pokemons = pokemonArray
self.viewOutput.pokemonDataOutPut(pokemon: self.pokemons)
}
}
func addPokemon(index: Int) {
print(#function)
self.savePokemons = UserDefaultsRepository.loadFromUserDefaults()
self.savePokemons.append(pokemons[index])
UserDefaultsRepository.saveToUserDefaults(pokemon: savePokemons)
}
func pokemon(row:Int)->PokemonModel? {
print(#function)
return row >= pokemons.count ? nil:pokemons[row]
}
func savePokemon(row:Int)->PokemonModel? {
print(#function)
return row >= savePokemons.count ? nil:savePokemons[row]
}
func didSelectTap(indexPath:IndexPath) {
print(#function)
let pokemon = pokemons[indexPath.row]
viewOutput.gotoPokemonDetail(pokemon: pokemon)
}
func searchTextInput(text: String) {
print(#function)
let filterPokemonArray = self.pokemons.filter { return $0.name.contains(text)}
self.pokemons = filterPokemonArray
viewOutput.filterPokemonOutput(pokemon: self.pokemons)
}
func deleteFavorite(index: Int) {
self.savePokemons = UserDefaultsRepository.deleteFromUserDefaults(index: index, pokemons: savePokemons)
favoriteOutput.deleteComplete()
}
}
View
import UIKit
class PokemonViewController: UICollectionViewController{
//Properties
private let cellId = "cellId"
private let headerId = "headerId"
private var pokemonPresentar:PokemonPresentarInput!
private var indicatorView = UIActivityIndicatorView()
private var searchController = UISearchController()
//Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
pokemonPresentar = PokemonPresentar(viewOutput: self, modelInput: PokemonDataModel())
pokemonPresentar.viewDidLoad()
setupSeachController()
}
//Mark setupMethod
private func setupCollectionView() {
collectionView.register(PokemonCell.self, forCellWithReuseIdentifier: cellId)
collectionView.register(PokemonHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerId)
indicatorView.center = view.center
indicatorView.style = .whiteLarge
indicatorView.color = .gray
view.addSubview(indicatorView)
indicatorView.startAnimating()
}
//Initialize
init() {
super.init(collectionViewLayout: UICollectionViewFlowLayout())
}
required init?(coder: NSCoder) {
fatalError()
}
//setupMethod
private func setupSeachController() {
navigationItem.title = "Pokemon Picture Book"
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = true
}
}
//Mark collectionViewdelegate Method
extension PokemonViewController {
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return pokemonPresentar.numberOfPokemon
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! PokemonCell
guard let pokemon = pokemonPresentar.pokemon(row: indexPath.row) else { return cell}
cell.pokemon = pokemon
cell.delegate = self
return cell
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) as! PokemonHeader
header.delegate = self
return header
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(#function)
pokemonPresentar.didSelectTap(indexPath: indexPath)
}
}
//Mark collectionviewflowlayoutMethod
extension PokemonViewController :UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 60)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: view.frame.width, height: 150)
}
}
//Mark PokemonSaveDelegate
extension PokemonViewController:PokemonSaveDelegate {
func addPokemon(cell: PokemonCell) {
print(#function)
let indexTapped = collectionView.indexPath(for: cell)
guard let index = indexTapped?[1] else { return }
let vc = UIAlertController(title: "お気に入りに追加しますか", message: "", preferredStyle: .alert)
let alertAction = UIAlertAction(title: "はい", style: .default) { _ in
self.pokemonPresentar.addPokemon(index: index)
}
let cancleAction = UIAlertAction(title: "いいえ", style: .cancel) { _ in
print("canle")
}
vc.addAction(alertAction)
vc.addAction(cancleAction)
present(vc, animated: true, completion: nil)
}
}
//Mark PokemonHeaderDelegate
extension PokemonViewController:PokemonHeaderDelegate {
func allFavoritePokemon() {
print(#function)
let vc = FavoritePokemonController()
navigationController?.pushViewController(vc, animated: true)
}
}
//Mark PokemonPresarOutput
extension PokemonViewController :PokemonPresentarOutput {
func filterPokemonOutput(pokemon: [PokemonModel]) {
print(#function)
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
func pokemonDataOutPut(pokemon: [PokemonModel]) {
print(#function)
DispatchQueue.main.async {
self.indicatorView.stopAnimating()
self.collectionView.reloadData()
}
}
func gotoPokemonDetail(pokemon: PokemonModel) {
print(#function)
let vc = PokemonDetailController(pokemon: pokemon)
navigationController?.pushViewController(vc, animated: true)
}
}
//Mark searchResultUpdating
extension PokemonViewController:UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
print(#function)
guard let text = searchController.searchBar.text else { return }
if !text.isEmpty {
pokemonPresentar.searchTextInput(text: text)
} else {
pokemonPresentar.viewDidLoad()
}
}
}