Skip to content
This repository was archived by the owner on Dec 20, 2023. It is now read-only.

Commit d9321ff

Browse files
committed
Отрефакторил экран с детальной информацией под MVP
1 parent 9fbab3a commit d9321ff

5 files changed

Lines changed: 220 additions & 116 deletions

File tree

AvitoTech-TraineeAssignment/AvitoTech-TraineeAssignment.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
8BC692F02A9FBE580096C00B /* SwipeNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC692EF2A9FBE580096C00B /* SwipeNavigationController.swift */; };
3636
8BC692F22A9FD3850096C00B /* CatalogViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC692F12A9FD3850096C00B /* CatalogViewPresenter.swift */; };
3737
8BC692F42A9FD48D0096C00B /* CatalogViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC692F32A9FD48D0096C00B /* CatalogViewProtocol.swift */; };
38+
8BC692F62A9FDDCA0096C00B /* CatalogsObjectViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC692F52A9FDDCA0096C00B /* CatalogsObjectViewPresenter.swift */; };
39+
8BC692F82A9FDDE80096C00B /* CatalogsObjectViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC692F72A9FDDE80096C00B /* CatalogsObjectViewProtocol.swift */; };
3840
/* End PBXBuildFile section */
3941

4042
/* Begin PBXFileReference section */
@@ -67,6 +69,8 @@
6769
8BC692EF2A9FBE580096C00B /* SwipeNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeNavigationController.swift; sourceTree = "<group>"; };
6870
8BC692F12A9FD3850096C00B /* CatalogViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogViewPresenter.swift; sourceTree = "<group>"; };
6971
8BC692F32A9FD48D0096C00B /* CatalogViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogViewProtocol.swift; sourceTree = "<group>"; };
72+
8BC692F52A9FDDCA0096C00B /* CatalogsObjectViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogsObjectViewPresenter.swift; sourceTree = "<group>"; };
73+
8BC692F72A9FDDE80096C00B /* CatalogsObjectViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogsObjectViewProtocol.swift; sourceTree = "<group>"; };
7074
/* End PBXFileReference section */
7175

7276
/* Begin PBXFrameworksBuildPhase section */
@@ -128,7 +132,9 @@
128132
8B57F7662A9F960400822ADB /* CatalogsObject */ = {
129133
isa = PBXGroup;
130134
children = (
135+
8BC692F72A9FDDE80096C00B /* CatalogsObjectViewProtocol.swift */,
131136
8B57F7672A9F962C00822ADB /* CatalogsObjectViewController.swift */,
137+
8BC692F52A9FDDCA0096C00B /* CatalogsObjectViewPresenter.swift */,
132138
);
133139
path = CatalogsObject;
134140
sourceTree = "<group>";
@@ -317,6 +323,7 @@
317323
8BC118B72A9CF7A000ABE6C9 /* Constants.swift in Sources */,
318324
8BC118BD2A9D048000ABE6C9 /* CatalogsObjectRequest.swift in Sources */,
319325
8BC118AA2A9CB71200ABE6C9 /* CatalogsObjectModel.swift in Sources */,
326+
8BC692F62A9FDDCA0096C00B /* CatalogsObjectViewPresenter.swift in Sources */,
320327
8B57F7682A9F962C00822ADB /* CatalogsObjectViewController.swift in Sources */,
321328
8B1C9AE02A9A83E700127D1F /* Fonts.swift in Sources */,
322329
8BC692F02A9FBE580096C00B /* SwipeNavigationController.swift in Sources */,
@@ -334,6 +341,7 @@
334341
8BC118A82A9CB4CC00ABE6C9 /* CatalogModel.swift in Sources */,
335342
8BC692EE2A9FB9990096C00B /* Images.swift in Sources */,
336343
8BC692F22A9FD3850096C00B /* CatalogViewPresenter.swift in Sources */,
344+
8BC692F82A9FDDE80096C00B /* CatalogsObjectViewProtocol.swift in Sources */,
337345
8BC118AE2A9CC04300ABE6C9 /* NetworkClient.swift in Sources */,
338346
8BC141322A9F7C72000E26F3 /* UIBlockingProgressHUD.swift in Sources */,
339347
);

AvitoTech-TraineeAssignment/AvitoTech-TraineeAssignment/Screens/Catalog/CatalogViewController.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,12 @@ extension CatalogViewController: UICollectionViewDataSource {
119119
}
120120

121121
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
122+
//tODO: Перенести в презентер
122123
guard let cell = collectionView.cellForItem(at: indexPath) as? CatalogCell,
123124
let id = cell.id else { return }
124-
navigationController?.pushViewController(CatalogsObjectViewController(objectId: id), animated: true)
125+
let catalogsObjectPresenter = CatalogsObjectViewPresenter(objectId: id)
126+
let catalogsViewController = CatalogsObjectViewController(presenter: catalogsObjectPresenter)
127+
catalogsObjectPresenter.injectViewController(viewController: catalogsViewController)
128+
navigationController?.pushViewController(catalogsViewController, animated: true)
125129
}
126130
}

AvitoTech-TraineeAssignment/AvitoTech-TraineeAssignment/Screens/CatalogsObject/CatalogsObjectViewController.swift

Lines changed: 37 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@
77

88
import UIKit
99
import MapKit
10-
import CoreLocation
1110
import MessageUI
1211

13-
final class CatalogsObjectViewController: UIViewController {
14-
private var objectId: String?
15-
private var email: String?
16-
private var phoneNumber: String?
12+
final class CatalogsObjectViewController: UIViewController, CatalogsObjectViewControllerProtocol {
13+
private var presenter: CatalogsObjectViewPresenterProtocol
1714

1815
private let scrollView: UIScrollView = {
1916
let scrollView = UIScrollView()
@@ -120,12 +117,8 @@ final class CatalogsObjectViewController: UIViewController {
120117
return map
121118
}()
122119

123-
private let catalogsObjectNetworkService = CatalogsObjectNetworkService.shared
124-
private var catalogsObjectNetworkServiceObserverData: NSObjectProtocol?
125-
private var catalogsObjectNetworkServiceObserverError: NSObjectProtocol?
126-
127-
init(objectId: String) {
128-
self.objectId = objectId
120+
init(presenter: CatalogsObjectViewPresenterProtocol) {
121+
self.presenter = presenter
129122
super.init(nibName: nil, bundle: nil)
130123
}
131124

@@ -139,44 +132,47 @@ final class CatalogsObjectViewController: UIViewController {
139132
view.backgroundColor = .systemBackground
140133
configureNavigationBar()
141134

142-
catalogsObjectNetworkServiceObserverData = NotificationCenter.default
143-
.addObserver(
144-
forName: CatalogsObjectNetworkService.dataReceivedNotification,
145-
object: nil,
146-
queue: .main
147-
) { [weak self] _ in
148-
self?.configureView()
149-
}
150-
catalogsObjectNetworkServiceObserverError = NotificationCenter.default
151-
.addObserver(
152-
forName: CatalogsObjectNetworkService.errorNotification,
153-
object: nil,
154-
queue: .main
155-
) { [weak self] _ in
156-
self?.showAlert()
157-
}
158-
loadData()
135+
presenter.viewDidLoad()
159136
}
160137

161-
private func configureNavigationBar() {
162-
navigationItem.leftBarButtonItem = UIBarButtonItem(image: .backArrow, style: .plain, target: self, action: #selector(backButtonTap))
138+
func showHud() {
139+
UIBlockingProgressHUD.show()
163140
}
164141

165-
private func configureView() {
166-
guard let model = catalogsObjectNetworkService.catalogsObjectModel else { showAlert(); return }
142+
func removeHud() {
167143
UIBlockingProgressHUD.dismiss()
144+
}
145+
146+
func configureView(model: CatalogsObjectModel) {
168147
configureConstraints()
169148
configureImage(link: URL(string: model.imageURL))
170-
let adress = "г. " + model.location + ", " + model.address
171149
configureLabels(
172150
name: model.title,
173151
cost: model.price,
174152
date: model.createdDate,
175153
description: model.description,
176-
adress: adress)
177-
geocodeCity(cityName: adress)
178-
email = model.email
179-
phoneNumber = model.phoneNumber
154+
adress: presenter.adress)
155+
}
156+
157+
func showAlert(alert: UIAlertController) {
158+
present(alert, animated: true)
159+
}
160+
161+
func showMailComposer(mailComposer: MFMailComposeViewController) {
162+
present(mailComposer, animated: true, completion: nil)
163+
}
164+
165+
func showCallAction(phoneURL: URL) {
166+
UIApplication.shared.open(phoneURL, options: [:], completionHandler: nil)
167+
}
168+
169+
func setupMap(annotation: MKPointAnnotation, region: MKCoordinateRegion) {
170+
mapView.addAnnotation(annotation)
171+
mapView.setRegion(region, animated: true)
172+
}
173+
174+
private func configureNavigationBar() {
175+
navigationItem.leftBarButtonItem = UIBarButtonItem(image: .backArrow, style: .plain, target: self, action: #selector(backButtonTap))
180176
}
181177

182178
private func configureConstraints() {
@@ -246,90 +242,16 @@ final class CatalogsObjectViewController: UIViewController {
246242
adressLabel.text = adress
247243
}
248244

249-
private func showAlert() {
250-
UIBlockingProgressHUD.dismiss()
251-
let alertTitle = NSLocalizedString("alert.title", comment: "Название алерта")
252-
let alertMessage = NSLocalizedString("alert.message", comment: "Сообщение в алерте")
253-
let alertActionRetryTitle = NSLocalizedString("alert.action.again", comment: "Название действия повторения в алерте")
254-
let alertActionLeaveTitle = NSLocalizedString("alert.action.leave", comment: "Название действия выхода в алерте")
255-
256-
let actionRetry = UIAlertAction(title: alertActionRetryTitle, style: .default) { [weak self] _ in
257-
self?.loadData()
258-
}
259-
let actionLeave = UIAlertAction(title: alertActionLeaveTitle, style: .default) { [weak self] _ in
260-
self?.dismiss(animated: true)
261-
self?.backButtonTap()
262-
}
263-
264-
let alert = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
265-
alert.addAction(actionRetry)
266-
alert.addAction(actionLeave)
267-
present(alert, animated: true)
268-
}
269-
270-
private func loadData() {
271-
guard let objectId = objectId else { return }
272-
UIBlockingProgressHUD.show()
273-
catalogsObjectNetworkService.fetch(id: objectId)
245+
@objc func backButtonTap() {
246+
navigationController?.popViewController(animated: true)
274247
}
275248

276249
@objc private func callButtonTap() {
277-
guard let phoneNumber = phoneNumber else { return }
278-
if let phoneURL = URL(string: "tel://\(phoneNumber.replacingOccurrences(of: " ", with: ""))") {
279-
if UIApplication.shared.canOpenURL(phoneURL) {
280-
UIApplication.shared.open(phoneURL, options: [:], completionHandler: nil)
281-
} else {
282-
// На симуляторе этот вариант будет срабатывать
283-
print("Невозможно совершить звонок")
284-
}
285-
}
250+
presenter.viewDidTapCallButton()
286251
}
287252

288253
@objc private func mailButtonTap() {
289-
guard let email = email else { return }
290-
if MFMailComposeViewController.canSendMail() {
291-
let mailComposer = MFMailComposeViewController()
292-
mailComposer.mailComposeDelegate = self
293-
mailComposer.setToRecipients([email])
294-
present(mailComposer, animated: true, completion: nil)
295-
} else {
296-
// На симуляторе этот вариант будет срабатывать
297-
print("Почтовый клиент недоступен")
298-
}
299-
}
300-
301-
@objc private func backButtonTap() {
302-
navigationController?.popViewController(animated: true)
303-
}
304-
}
305-
306-
extension CatalogsObjectViewController {
307-
func geocodeCity(cityName: String) {
308-
guard !cityName.isEmpty else { return }
309-
let geocoder = CLGeocoder()
310-
geocoder.geocodeAddressString(cityName) { placemarks, error in
311-
if let error = error {
312-
print("Координаты не найдены: \(error.localizedDescription)")
313-
self.geocodeCity(cityName: cityName.split(separator: ",").first?.description ?? "" )
314-
return
315-
}
316-
317-
if let location = placemarks?.first?.location?.coordinate {
318-
self.setupMapWithCoordinate(coordinate: location, title: cityName)
319-
} else {
320-
print("Координаты не найдены.")
321-
}
322-
}
323-
}
324-
325-
func setupMapWithCoordinate(coordinate: CLLocationCoordinate2D, title: String) {
326-
let annotation = MKPointAnnotation()
327-
annotation.coordinate = coordinate
328-
annotation.title = title
329-
330-
mapView.addAnnotation(annotation)
331-
let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 10000, longitudinalMeters: 10000)
332-
mapView.setRegion(region, animated: true)
254+
presenter.viewDidTapMailButton()
333255
}
334256
}
335257

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//
2+
// CatalogsObjectViewPresenter.swift
3+
// AvitoTech-TraineeAssignment
4+
//
5+
// Created by Богдан Полыгалов on 31.08.2023.
6+
//
7+
8+
import UIKit
9+
import MapKit
10+
import MessageUI
11+
import CoreLocation
12+
13+
final class CatalogsObjectViewPresenter: CatalogsObjectViewPresenterProtocol {
14+
var adress: String {
15+
guard let model = catalogsObjectNetworkService.catalogsObjectModel else { return "" }
16+
return "г. " + model.location + ", " + model.address
17+
}
18+
19+
weak private var viewController: CatalogsObjectViewControllerProtocol?
20+
21+
private var objectId: String?
22+
private var email: String?
23+
private var phoneNumber: String?
24+
25+
private let catalogsObjectNetworkService = CatalogsObjectNetworkService.shared
26+
private var catalogsObjectNetworkServiceObserverData: NSObjectProtocol?
27+
private var catalogsObjectNetworkServiceObserverError: NSObjectProtocol?
28+
29+
init(objectId: String) {
30+
self.objectId = objectId
31+
}
32+
33+
func injectViewController(viewController: CatalogsObjectViewControllerProtocol) {
34+
self.viewController = viewController
35+
}
36+
37+
func viewDidLoad() {
38+
catalogsObjectNetworkServiceObserverData = NotificationCenter.default
39+
.addObserver(
40+
forName: CatalogsObjectNetworkService.dataReceivedNotification,
41+
object: nil,
42+
queue: .main
43+
) { [weak self] _ in
44+
self?.configureView()
45+
}
46+
catalogsObjectNetworkServiceObserverError = NotificationCenter.default
47+
.addObserver(
48+
forName: CatalogsObjectNetworkService.errorNotification,
49+
object: nil,
50+
queue: .main
51+
) { [weak self] _ in
52+
self?.showViewAlert()
53+
}
54+
loadData()
55+
}
56+
57+
func viewDidTapMailButton() {
58+
guard let email = email else { return }
59+
if MFMailComposeViewController.canSendMail() {
60+
let mailComposer = MFMailComposeViewController()
61+
mailComposer.mailComposeDelegate = viewController as? CatalogsObjectViewController
62+
mailComposer.setToRecipients([email])
63+
viewController?.showMailComposer(mailComposer: mailComposer)
64+
} else {
65+
// На симуляторе этот вариант будет срабатывать
66+
print("Почтовый клиент недоступен")
67+
}
68+
}
69+
70+
func viewDidTapCallButton() {
71+
guard let phoneNumber = phoneNumber else { return }
72+
if let phoneURL = URL(string: "tel://\(phoneNumber.replacingOccurrences(of: " ", with: ""))") {
73+
if UIApplication.shared.canOpenURL(phoneURL) {
74+
viewController?.showCallAction(phoneURL: phoneURL)
75+
} else {
76+
// На симуляторе этот вариант будет срабатывать
77+
print("Невозможно совершить звонок")
78+
}
79+
}
80+
}
81+
82+
private func loadData() {
83+
guard let objectId = objectId else { return }
84+
viewController?.showHud()
85+
catalogsObjectNetworkService.fetch(id: objectId)
86+
}
87+
88+
private func configureView() {
89+
guard let model = catalogsObjectNetworkService.catalogsObjectModel else { showViewAlert(); return }
90+
viewController?.removeHud()
91+
viewController?.configureView(model: model)
92+
email = model.email
93+
phoneNumber = model.phoneNumber
94+
geocodeCity(cityName: adress)
95+
}
96+
97+
private func showViewAlert() {
98+
viewController?.removeHud()
99+
let alertTitle = NSLocalizedString("alert.title", comment: "Название алерта")
100+
let alertMessage = NSLocalizedString("alert.message", comment: "Сообщение в алерте")
101+
let alertActionRetryTitle = NSLocalizedString("alert.action.again", comment: "Название действия повторения в алерте")
102+
let alertActionLeaveTitle = NSLocalizedString("alert.action.leave", comment: "Название действия выхода в алерте")
103+
let alert = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
104+
let actionRetry = UIAlertAction(title: alertActionRetryTitle, style: .default) { [weak self] _ in
105+
self?.loadData()
106+
}
107+
let actionLeave = UIAlertAction(title: alertActionLeaveTitle, style: .default) { [weak self] _ in
108+
alert.dismiss(animated: true)
109+
self?.viewController?.backButtonTap()
110+
}
111+
alert.addAction(actionRetry)
112+
alert.addAction(actionLeave)
113+
viewController?.showAlert(alert: alert)
114+
}
115+
116+
private func geocodeCity(cityName: String) {
117+
guard !cityName.isEmpty else { return }
118+
let geocoder = CLGeocoder()
119+
geocoder.geocodeAddressString(cityName) { placemarks, error in
120+
if let error = error {
121+
print("Координаты не найдены: \(error.localizedDescription)")
122+
self.geocodeCity(cityName: cityName.split(separator: ",").first?.description ?? "" )
123+
return
124+
}
125+
126+
if let location = placemarks?.first?.location?.coordinate {
127+
self.setupMapWithCoordinate(coordinate: location, title: cityName)
128+
} else {
129+
print("Координаты не найдены.")
130+
}
131+
}
132+
}
133+
134+
private func setupMapWithCoordinate(coordinate: CLLocationCoordinate2D, title: String) {
135+
let annotation = MKPointAnnotation()
136+
annotation.coordinate = coordinate
137+
annotation.title = title
138+
let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 10000, longitudinalMeters: 10000)
139+
viewController?.setupMap(annotation: annotation, region: region)
140+
}
141+
}

0 commit comments

Comments
 (0)