LoginSignup
0
1

More than 1 year has passed since last update.

MVPパターン

Posted at

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()
        }
    }
}

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1