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