Передача данных между двумя вьюконтроллерами методом Low Coupling


#1

Low Coupling (Низкая связанность) один из шаблонов GRAPS, почитать можно здесь.

Описание с хабра «Низкая связанность, отвечает за то, что бы объекты в системе знали друг о друге как можно меньше.»

В примере (приложение будет показывать список обложек альбомов и детальное их отображение) объектами будут два вьюконтроллера которые ничего не будет знать друг о друге, задача передать данные от одного к другому.

###Поехали!

Модель:

class Cover {
    
    enum CoverError: Error {
        case initializationFailed
    }
    
    let label: String
    let largeImageUrl: String
    let smallImageUrl: String
    
    init(label: String?, large: String?, small: String?) throws {
        guard label != nil && large != nil && small != nil else { throw CoverError.initializationFailed }
        self.label = label!
        self.largeImageUrl = large!
        self.smallImageUrl = small!
    }
}

Тут думаю и так всё понятно, лейбл и две ссылки на картинки (зачем я обрабатываю все ошибки, об этом в конце).

Первое что нужно сделать это вынести всю бизнес логику (всё что не вью но еще контроллер) в отдельный класс синглтон (таких классов можеть быть несколько, но в данном конкретном примере достаточно одного) что этот класс будет делать:

  • Извлекать из json файла данные и заполнять нашу модель.
  • Загружать картинки по ссылке.
  • Отдавать обложку в зависимости от индекса.
  • Отдавать массив обложек.
class DataController {
    
    enum DataError: Error {
        case invalidPath
        case invalidJSON
        case invalidImageUrl
        case invalidImageData
        case invalidCover(at: Int)
    }
    
    static let shared = DataController()
    
    var covers: () throws -> [Cover] = {
        var covers: [Cover]!
        return {
            if covers == nil {
                guard let path = Bundle.main.path(forResource: "Cover", ofType: "json") else { throw DataError.invalidPath }
                
                let data = try NSData(contentsOfFile: path, options: []) as Data
                let json = try JSONSerialization.jsonObject(with: data)
                
                guard let objects = json as? [[String: String]] else { throw DataError.invalidJSON }
                
                covers = try objects.map { try Cover(label: $0["label"], large: $0["large"], small: $0["small"] ) }
            }
            return covers
        }
    }()
    
    private var currentIndex = 0
    
    func set(index: Int) {
        currentIndex = index
    }
    
    func getCover() throws -> Cover {
        let covers = try self.covers()
        guard currentIndex < covers.count else { throw DataError.invalidCover(at: currentIndex) }
        return covers[currentIndex]
    }
    
    func getImageData(url: String, completion: @escaping (_ data: () throws -> Data) -> ()) {
        guard let url = URL(string: url) else { completion({ throw DataError.invalidImageUrl }); return }
        URLSession.shared.dataTask(with: url) { data, _, error in
            DispatchQueue.main.async {
                completion({
                    guard error == nil else { throw error! }
                    guard let data = data else { throw DataError.invalidImageData }
                    return data
                })
            }
        }.resume()
    }
}

Сториборд совершенно стандартный:

MasterTableViewController - список обложек в котором превью картинок и название альбома, в методе didSelectRowAt передаём индекс обложки:

class MasterTableViewController: UITableViewController {
    
    private var dataController: DataController {
        return DataController.shared
    }
    
    private var covers: [Cover] {
        return (try? dataController.covers()) ?? []
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return covers.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "coverTableCell", for: indexPath) as! CoverTableCell
        let cover = covers[indexPath.row]
        cell.coverLabel.text = cover.label
        cell.coverImageView.image = nil
        dataController.getImageData(url: cover.smallImageUrl) { data in
            do {
                cell.coverImageView.image = UIImage(data: try data())
            } catch {
                print(error)
            }
        }
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        dataController.set(index: indexPath.row)
    }
}

DetailViewController - извлекаем обложку (причем даже не зная её индекс) если все нормально выставляем текст лейбу и загружаем картинку:

class DetailViewController: UIViewController {
    
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var label: UILabel!
    
    private var dataController: DataController {
        return DataController.shared
    }
    
    private var cover: Cover! {
        return try? dataController.getCover()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        guard cover != nil else { return }
        
        label.text = cover.label
        dataController.getImageData(url: cover.largeImageUrl) { data in
            do {
                self.imageView.image = UIImage(data: try data())
            } catch {
                print(error)
            }
        }
    }
}

## так главный вопрос, зачем и что это дает:

1. Пример из жизни, срочно понадобилось адаптировать приложение для айпеда, тут в пору хвататься за голову, но не в нашем случае. За счет низкой связанности, головная боль что и куда передавать отходит на второй план, все что понадобится слегка адаптировать сториборд (добавить UISplitViewController)

И настроить его поведение:

class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        (window?.rootViewController as? UISplitViewController)?.delegate = self
        return true
    }
    
    // MARK: - SplitDelegate
    
    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
        return true
    }
    
    func splitViewController(_ svc: UISplitViewController, shouldHide vc: UIViewController, in orientation: UIInterfaceOrientation) -> Bool {
        return false
    }

}

2. Поддается тестированию - за счет того что бизнес логика в отдельном классе её легко протестировать и отловить ошибки, а за счет того что эти ошибки обрабатываются, достаточно проверить их отсутствие:

class LowCouplingExampleTests: XCTestCase {
    
    func testMain() {
        let dataController = DataController.shared
        let asyncExpectation = expectation(description: "Main")
        let group = DispatchGroup()
        
        do {
            let covers = try dataController.covers()
            XCTAssertFalse(covers.isEmpty)
            
            for cover in covers {
                group.enter()
                dataController.getImageData(url: cover.smallImageUrl) { data in
                    do {
                        _ = try data()
                        group.leave()
                    } catch {
                        XCTFail("\(error)")
                    }
                }
            }
        } catch {
            XCTFail("\(error)")
        }
        
        group.notify(queue: .main) { 
            asyncExpectation.fulfill()
        }
        
        waitForExpectations(timeout: 1) { _ in }
    }
    
    func testDetail() {
        let dataController = DataController.shared
        let asyncExpectation = expectation(description: "Detail")
        let group = DispatchGroup()
        
        do {
            let covers = try dataController.covers()
            XCTAssertFalse(covers.isEmpty)
            
            for (index, _) in covers.enumerated() {
                group.enter()
                
                dataController.set(index: index)
                
                let cover = try dataController.getCover()
                
                dataController.getImageData(url: cover.largeImageUrl) { data in
                    do {
                        _ = try data()
                        group.leave()
                    } catch {
                        XCTFail("\(error)")
                    }
                }
            }
        } catch {
            XCTFail("\(error)")
        }
        
        group.notify(queue: .main) {
            asyncExpectation.fulfill()
        }
        
        waitForExpectations(timeout: 2) { _ in }
    }
    
}

Скачать iPhone.
Скачать iPad.