[Feature] Locations list (#10)
This PR contains the work done to fetch a set of locations from a remote server to then persist them into the persistence stack and finally to display these data into the locations list screen. To give further details about the work done: - [x] implemented the `LoadingSpinnerView` and `ErrorMessageView` custom views; - [x] implemented the outlets of the `LocationsListViewController` view controller; - [x] add properties and functions to the `LocationsListViewModeling` protocol to support reactive updates, load data, and table data source conformances; - [x] deactivated the Location entity code generation from the Core Data model in the `Persistence` library; - [x] add fetch requests builder functions to the `NSFetchRequest+Location` extension in the `Persistence` library; - [x] implemented the `LoadRemoteLocationUseCase` use case; - [x] implemented the loading of locations in the LocationsListViewModel view model; - [x] implemented properties and functions in the LocationsListViewModel view model to support the table data source conformance of the `LocationsListViewController` view controller; - [x] implemented the `LocationViewCell` custom cell; - [x] registered the `LocationViewCell` with the table of the `LocationsListViewController` view controller and implemented its update with real data from Location entities. Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Reviewed-on: rock-n-code/deep-linking-assignment#10
This commit is contained in:
parent
43c156a2c3
commit
c8d2c288af
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// Location+CoreDataClass.swift
|
||||||
|
// Locations
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 12/04/2023.
|
||||||
|
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(Location)
|
||||||
|
public class Location: NSManagedObject {
|
||||||
|
convenience init(context: NSManagedObjectContext) {
|
||||||
|
self.init(
|
||||||
|
entity: .entity(forEntityName: "Location", in: context)!,
|
||||||
|
insertInto: context
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// Location+CoreDataProperties.swift
|
||||||
|
// Locations
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 12/04/2023.
|
||||||
|
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
extension Location {
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<Location> {
|
||||||
|
return NSFetchRequest<Location>(entityName: "Location")
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged public var createdAt: Date
|
||||||
|
@NSManaged public var latitude: Float
|
||||||
|
@NSManaged public var longitude: Float
|
||||||
|
@NSManaged public var name: String?
|
||||||
|
@NSManaged public var source: LocationSource
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Identifiable
|
||||||
|
|
||||||
|
extension Location: Identifiable {}
|
||||||
|
|
||||||
|
// MARK: - Enumerations
|
||||||
|
|
||||||
|
@objc public enum LocationSource: Int16 {
|
||||||
|
case remote = 0
|
||||||
|
case local
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// NSFetchRequest+Location.swift
|
||||||
|
// Persistence
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 12/04/2023.
|
||||||
|
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
public extension NSFetchRequest where ResultType == Location {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
static func allLocations() -> NSFetchRequest<Location> {
|
||||||
|
let request = Location.fetchRequest()
|
||||||
|
|
||||||
|
request.sortDescriptors = [
|
||||||
|
.init(keyPath: \Location.source, ascending: true),
|
||||||
|
.init(keyPath: \Location.createdAt, ascending: true)
|
||||||
|
]
|
||||||
|
request.resultType = .managedObjectResultType
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Location" representedClassName="Location" syncable="YES" codeGenerationType="class">
|
<entity name="Location" representedClassName="Location" syncable="YES">
|
||||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="latitude" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
<attribute name="latitude" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
<attribute name="longitude" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
<attribute name="longitude" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||||
|
@ -6,14 +6,24 @@
|
|||||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import Persistence
|
||||||
|
|
||||||
protocol LocationsListViewModeling: AnyObject {
|
protocol LocationsListViewModeling: AnyObject {
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
var coordinator: LocationsListCoordination? { get set }
|
var coordinator: LocationsListCoordination? { get set }
|
||||||
|
|
||||||
|
var viewStatusPublisher: Published<LocationsListViewStatus>.Publisher { get }
|
||||||
|
var numberOfSectionsInData: Int { get }
|
||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
|
|
||||||
func openAddLocation()
|
func openAddLocation()
|
||||||
|
func loadLocations()
|
||||||
|
func numberOfDataItems(in section: Int) -> Int
|
||||||
|
func dataItem(at indexPath: IndexPath) -> Location
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
import Core
|
import Core
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@ -13,7 +14,25 @@ class LocationsListViewController: BaseViewController {
|
|||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
var viewModel: LocationsListViewModeling
|
private let viewModel: LocationsListViewModeling
|
||||||
|
|
||||||
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
// MARK: Outlets
|
||||||
|
|
||||||
|
private lazy var error = ErrorMessageView()
|
||||||
|
private lazy var loading = LoadingSpinnerView()
|
||||||
|
private lazy var table = {
|
||||||
|
let table = UITableView(frame: .zero, style: .plain)
|
||||||
|
|
||||||
|
table.dataSource = self
|
||||||
|
table.delegate = self
|
||||||
|
table.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
table.register(LocationViewCell.self, forCellReuseIdentifier: LocationViewCell.identifier)
|
||||||
|
|
||||||
|
return table
|
||||||
|
}()
|
||||||
|
|
||||||
// MARK: Initialisers
|
// MARK: Initialisers
|
||||||
|
|
||||||
@ -31,8 +50,72 @@ class LocationsListViewController: BaseViewController {
|
|||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
setupBar()
|
||||||
|
setupView()
|
||||||
|
bindViewModel()
|
||||||
|
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
viewModel.loadLocations()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDataSource
|
||||||
|
|
||||||
|
extension LocationsListViewController: UITableViewDataSource {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func numberOfSections(in tableView: UITableView) -> Int {
|
||||||
|
viewModel.numberOfSectionsInData
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(
|
||||||
|
_ tableView: UITableView,
|
||||||
|
numberOfRowsInSection section: Int
|
||||||
|
) -> Int {
|
||||||
|
viewModel.numberOfDataItems(in: section)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(
|
||||||
|
_ tableView: UITableView,
|
||||||
|
cellForRowAt indexPath: IndexPath
|
||||||
|
) -> UITableViewCell {
|
||||||
|
guard let cell = tableView.dequeueReusableCell(
|
||||||
|
withIdentifier: LocationViewCell.identifier,
|
||||||
|
for: indexPath
|
||||||
|
) as? LocationViewCell else {
|
||||||
|
return .init()
|
||||||
|
}
|
||||||
|
|
||||||
|
let entity = viewModel.dataItem(at: indexPath)
|
||||||
|
|
||||||
|
cell.update(
|
||||||
|
iconName: entity.source == .remote ? "network" : "house",
|
||||||
|
name: entity.name,
|
||||||
|
latitude: entity.latitude,
|
||||||
|
longitude: entity.longitude
|
||||||
|
)
|
||||||
|
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
|
||||||
|
extension LocationsListViewController: UITableViewDelegate {}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension LocationsListViewController {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func setupBar() {
|
||||||
|
navigationController?.navigationBar.prefersLargeTitles = true
|
||||||
|
navigationController?.navigationBar.tintColor = .red
|
||||||
|
navigationItem.rightBarButtonItem = .init(
|
||||||
title: "Add",
|
title: "Add",
|
||||||
style: .plain,
|
style: .plain,
|
||||||
target: self,
|
target: self,
|
||||||
@ -41,13 +124,44 @@ class LocationsListViewController: BaseViewController {
|
|||||||
title = "Locations"
|
title = "Locations"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
func setupView() {
|
||||||
|
view.addSubview(table)
|
||||||
|
view.addSubview(error)
|
||||||
|
view.addSubview(loading)
|
||||||
|
|
||||||
// MARK: - Helpers
|
error.onRetry = {
|
||||||
|
self.viewModel.loadLocations()
|
||||||
|
}
|
||||||
|
|
||||||
private extension LocationsListViewController {
|
NSLayoutConstraint.activate([
|
||||||
|
error.widthAnchor.constraint(equalToConstant: 300),
|
||||||
|
view.centerXAnchor.constraint(equalTo: error.centerXAnchor),
|
||||||
|
view.centerYAnchor.constraint(equalTo: error.centerYAnchor),
|
||||||
|
view.centerXAnchor.constraint(equalTo: loading.centerXAnchor),
|
||||||
|
view.centerYAnchor.constraint(equalTo: loading.centerYAnchor),
|
||||||
|
view.bottomAnchor.constraint(equalTo: table.bottomAnchor),
|
||||||
|
view.leadingAnchor.constraint(equalTo: table.leadingAnchor),
|
||||||
|
view.topAnchor.constraint(equalTo: table.topAnchor),
|
||||||
|
view.trailingAnchor.constraint(equalTo: table.trailingAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Functions
|
func bindViewModel() {
|
||||||
|
viewModel
|
||||||
|
.viewStatusPublisher
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.sink { viewStatus in
|
||||||
|
self.navigationItem.rightBarButtonItem?.isEnabled = viewStatus == .loaded
|
||||||
|
self.error.isHidden = viewStatus != .error
|
||||||
|
self.loading.isHidden = viewStatus != .loading
|
||||||
|
self.table.isHidden = viewStatus != .loaded
|
||||||
|
|
||||||
|
if viewStatus == .loaded {
|
||||||
|
self.table.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
@objc func addLocationPressed() {
|
@objc func addLocationPressed() {
|
||||||
viewModel.openAddLocation()
|
viewModel.openAddLocation()
|
||||||
|
@ -6,15 +6,33 @@
|
|||||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
import Combine
|
import Combine
|
||||||
import Core
|
import Dependency
|
||||||
|
import Foundation
|
||||||
|
import Persistence
|
||||||
|
|
||||||
class LocationsListViewModel: ObservableObject {
|
class LocationsListViewModel: ObservableObject {
|
||||||
|
|
||||||
|
// MARK: Dependencies
|
||||||
|
|
||||||
|
@Dependency(\.persistence) private var persistence
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
weak var coordinator: LocationsListCoordination?
|
weak var coordinator: LocationsListCoordination?
|
||||||
|
|
||||||
|
@Published private var viewStatus: LocationsListViewStatus = .initialised
|
||||||
|
|
||||||
|
private lazy var fetchedResultsController = NSFetchedResultsController(
|
||||||
|
fetchRequest: NSFetchRequest<Location>.allLocations(),
|
||||||
|
managedObjectContext: persistence.container.viewContext,
|
||||||
|
sectionNameKeyPath: nil,
|
||||||
|
cacheName: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
private let loadRemoteLocations = LoadRemoteLocationsUseCase()
|
||||||
|
|
||||||
// MARK: Initialisers
|
// MARK: Initialisers
|
||||||
|
|
||||||
init(coordinator: LocationsListCoordination) {
|
init(coordinator: LocationsListCoordination) {
|
||||||
@ -27,10 +45,55 @@ class LocationsListViewModel: ObservableObject {
|
|||||||
|
|
||||||
extension LocationsListViewModel: LocationsListViewModeling {
|
extension LocationsListViewModel: LocationsListViewModeling {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
var viewStatusPublisher: Published<LocationsListViewStatus>.Publisher { $viewStatus }
|
||||||
|
var numberOfSectionsInData: Int { fetchedResultsController.sections?.count ?? 0 }
|
||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
|
|
||||||
func openAddLocation() {
|
func openAddLocation() {
|
||||||
coordinator?.openAddLocation()
|
coordinator?.openAddLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadLocations() {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
viewStatus = .loading
|
||||||
|
|
||||||
|
try await loadRemoteLocations()
|
||||||
|
|
||||||
|
try fetchedResultsController.performFetch()
|
||||||
|
|
||||||
|
viewStatus = .loaded
|
||||||
|
} catch {
|
||||||
|
viewStatus = .error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberOfDataItems(in section: Int) -> Int {
|
||||||
|
guard
|
||||||
|
let sections = fetchedResultsController.sections,
|
||||||
|
sections.endIndex > section
|
||||||
|
else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections[section].numberOfObjects
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataItem(at indexPath: IndexPath) -> Location {
|
||||||
|
fetchedResultsController.object(at: indexPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Enumerations
|
||||||
|
|
||||||
|
enum LocationsListViewStatus {
|
||||||
|
case initialised
|
||||||
|
case loading
|
||||||
|
case loaded
|
||||||
|
case error
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
//
|
||||||
|
// LoadRemoteLocationsUseCase.swift
|
||||||
|
// Locations
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 12/04/2023.
|
||||||
|
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Dependency
|
||||||
|
import Persistence
|
||||||
|
import Remote
|
||||||
|
|
||||||
|
struct LoadRemoteLocationsUseCase {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private let persistence: PersistenceService
|
||||||
|
private let remoteService: RemoteService
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
init(
|
||||||
|
persistence: PersistenceService,
|
||||||
|
remoteService: RemoteService
|
||||||
|
) {
|
||||||
|
self.persistence = persistence
|
||||||
|
self.remoteService = remoteService
|
||||||
|
}
|
||||||
|
|
||||||
|
func callAsFunction() async throws {
|
||||||
|
let context = persistence.makeTaskContext()
|
||||||
|
let fetchRequest = NSFetchRequest<Persistence.Location>.allLocations()
|
||||||
|
|
||||||
|
try await context.perform {
|
||||||
|
let localLocations = try context.fetch(fetchRequest)
|
||||||
|
|
||||||
|
localLocations
|
||||||
|
.filter { $0.source == .remote }
|
||||||
|
.forEach(context.delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
let remoteLocations = try await remoteService.getLocations()
|
||||||
|
|
||||||
|
_ = remoteLocations
|
||||||
|
.map {
|
||||||
|
let entity = Persistence.Location(context: context)
|
||||||
|
|
||||||
|
entity.createdAt = .now
|
||||||
|
entity.name = $0.name
|
||||||
|
entity.latitude = $0.latitude
|
||||||
|
entity.longitude = $0.longitude
|
||||||
|
entity.source = .remote
|
||||||
|
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
persistence.save(context: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - LoadRemoteLocationsUseCase+Initialisers
|
||||||
|
|
||||||
|
extension LoadRemoteLocationsUseCase {
|
||||||
|
init() {
|
||||||
|
@Dependency(\.persistence) var persistence
|
||||||
|
@Dependency(\.remote) var remote
|
||||||
|
|
||||||
|
self.init(
|
||||||
|
persistence: persistence,
|
||||||
|
remoteService: remote
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
124
Apps/Locations/Sources/View Components/ErrorMessageView.swift
Normal file
124
Apps/Locations/Sources/View Components/ErrorMessageView.swift
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
//
|
||||||
|
// ErrorMessageView.swift
|
||||||
|
// Locations
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 12/04/2023.
|
||||||
|
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ErrorMessageView: UIView {
|
||||||
|
|
||||||
|
// MARK: Typealiases
|
||||||
|
|
||||||
|
typealias OnRetryClosure = () -> Void
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
var onRetry: OnRetryClosure?
|
||||||
|
|
||||||
|
// MARK: Outlets
|
||||||
|
|
||||||
|
private lazy var stack: UIStackView = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
|
||||||
|
stack.alignment = .center
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.distribution = .fill
|
||||||
|
stack.spacing = 32
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var title = {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
label.font = .preferredFont(forTextStyle: .largeTitle)
|
||||||
|
label.numberOfLines = 0
|
||||||
|
label.lineBreakMode = .byWordWrapping
|
||||||
|
label.text = "Some error title goes in here..."
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var message = {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
label.font = .preferredFont(forTextStyle: .body)
|
||||||
|
label.lineBreakMode = .byWordWrapping
|
||||||
|
label.numberOfLines = 0
|
||||||
|
label.text = "Some long, descriptive, explanatory error message goes in here..."
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var retry = {
|
||||||
|
let button = UIButton()
|
||||||
|
|
||||||
|
button.backgroundColor = .red
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
button.layer.borderColor = UIColor.red.cgColor
|
||||||
|
button.layer.borderWidth = 1
|
||||||
|
button.layer.cornerRadius = 5
|
||||||
|
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
|
||||||
|
button.addTarget(self, action: #selector(retryPressed), for: .touchUpInside)
|
||||||
|
button.setTitle("Try again", for: .normal)
|
||||||
|
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension ErrorMessageView {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func setupView() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
addSubview(stack)
|
||||||
|
|
||||||
|
stack.addArrangedSubview(title)
|
||||||
|
stack.addArrangedSubview(message)
|
||||||
|
stack.addArrangedSubview(retry)
|
||||||
|
stack.setCustomSpacing(160, after: message)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bottomAnchor.constraint(equalTo: stack.bottomAnchor),
|
||||||
|
leadingAnchor.constraint(equalTo: stack.leadingAnchor),
|
||||||
|
topAnchor.constraint(equalTo: stack.topAnchor),
|
||||||
|
trailingAnchor.constraint(equalTo: stack.trailingAnchor),
|
||||||
|
retry.heightAnchor.constraint(equalToConstant: 44),
|
||||||
|
retry.leadingAnchor.constraint(equalTo: stack.leadingAnchor),
|
||||||
|
retry.trailingAnchor.constraint(equalTo: stack.trailingAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func retryPressed() {
|
||||||
|
onRetry?()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
//
|
||||||
|
// LoadingSpinnerView.swift
|
||||||
|
// Locations
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 12/04/2023.
|
||||||
|
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class LoadingSpinnerView: UIView {
|
||||||
|
|
||||||
|
// MARK: Outlets
|
||||||
|
|
||||||
|
private lazy var stack = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
|
||||||
|
stack.alignment = .center
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.distribution = .fill
|
||||||
|
stack.spacing = 8
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var spinner = {
|
||||||
|
let activity = UIActivityIndicatorView(style: .large)
|
||||||
|
|
||||||
|
activity.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
activity.startAnimating()
|
||||||
|
|
||||||
|
return activity
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var title = {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
label.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
label.numberOfLines = 0
|
||||||
|
label.lineBreakMode = .byWordWrapping
|
||||||
|
label.text = "Loading..."
|
||||||
|
label.textAlignment = .center
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension LoadingSpinnerView {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func setupView() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
addSubview(stack)
|
||||||
|
|
||||||
|
stack.addArrangedSubview(spinner)
|
||||||
|
stack.addArrangedSubview(title)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
topAnchor.constraint(equalTo: stack.topAnchor),
|
||||||
|
leadingAnchor.constraint(equalTo: stack.leadingAnchor),
|
||||||
|
trailingAnchor.constraint(equalTo: stack.trailingAnchor),
|
||||||
|
bottomAnchor.constraint(equalTo: stack.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
223
Apps/Locations/Sources/View Components/LocationViewCell.swift
Normal file
223
Apps/Locations/Sources/View Components/LocationViewCell.swift
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
//
|
||||||
|
// LocationViewCell.swift
|
||||||
|
// Locations
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 12/04/2023.
|
||||||
|
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class LocationViewCell: UITableViewCell {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
static let identifier = "LocationViewCell"
|
||||||
|
|
||||||
|
// MARK: Outlets
|
||||||
|
|
||||||
|
private lazy var icon = {
|
||||||
|
let view = UIImageView()
|
||||||
|
|
||||||
|
view.contentMode = .top
|
||||||
|
view.tintColor = .red
|
||||||
|
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var latitudeTitle = {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
label.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
label.numberOfLines = 1
|
||||||
|
label.text = "• Latitude"
|
||||||
|
label.textAlignment = .natural
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var latitudeValue = {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
label.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
label.numberOfLines = 1
|
||||||
|
label.textAlignment = .natural
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var longitudeTitle = {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
label.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
label.numberOfLines = 1
|
||||||
|
label.text = "• Longitude"
|
||||||
|
label.textAlignment = .natural
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var longitudeValue = {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
label.font = .preferredFont(forTextStyle: .subheadline)
|
||||||
|
label.numberOfLines = 1
|
||||||
|
label.textAlignment = .natural
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var name = {
|
||||||
|
let label = UILabel()
|
||||||
|
|
||||||
|
label.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
label.numberOfLines = 0
|
||||||
|
label.lineBreakMode = .byWordWrapping
|
||||||
|
label.text = "Untitled"
|
||||||
|
label.textAlignment = .natural
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var stack = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
|
||||||
|
stack.alignment = .center
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.distribution = .fill
|
||||||
|
stack.spacing = 12
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var stackData = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
|
||||||
|
stack.alignment = .leading
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.distribution = .fill
|
||||||
|
stack.spacing = 8
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var stackCoordinates = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
|
||||||
|
stack.alignment = .leading
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.distribution = .fill
|
||||||
|
stack.spacing = 2
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var stackLatitude = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
|
||||||
|
stack.alignment = .leading
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.distribution = .fill
|
||||||
|
stack.spacing = 4
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var stackLongitude = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
|
||||||
|
stack.alignment = .leading
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.distribution = .fill
|
||||||
|
stack.spacing = 4
|
||||||
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
override init(
|
||||||
|
style: UITableViewCell.CellStyle,
|
||||||
|
reuseIdentifier: String?
|
||||||
|
) {
|
||||||
|
super.init(
|
||||||
|
style: style,
|
||||||
|
reuseIdentifier: reuseIdentifier
|
||||||
|
)
|
||||||
|
|
||||||
|
setupCell()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func update(
|
||||||
|
iconName: String,
|
||||||
|
name: String?,
|
||||||
|
latitude: Float,
|
||||||
|
longitude: Float
|
||||||
|
) {
|
||||||
|
self.icon.image = .init(systemName: iconName)
|
||||||
|
self.name.text = name ?? "Untitled"
|
||||||
|
self.latitudeValue.text = "\(latitude)"
|
||||||
|
self.longitudeValue.text = "\(longitude)"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension LocationViewCell {
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func setupCell() {
|
||||||
|
accessoryType = .disclosureIndicator
|
||||||
|
backgroundColor = .clear
|
||||||
|
|
||||||
|
addSubview(stack)
|
||||||
|
|
||||||
|
stack.addArrangedSubview(icon)
|
||||||
|
stack.addArrangedSubview(stackData)
|
||||||
|
|
||||||
|
stackData.addArrangedSubview(name)
|
||||||
|
stackData.addArrangedSubview(stackCoordinates)
|
||||||
|
|
||||||
|
stackCoordinates.addArrangedSubview(stackLatitude)
|
||||||
|
stackCoordinates.addArrangedSubview(stackLongitude)
|
||||||
|
|
||||||
|
stackLatitude.addArrangedSubview(latitudeTitle)
|
||||||
|
stackLatitude.addArrangedSubview(latitudeValue)
|
||||||
|
|
||||||
|
stackLongitude.addArrangedSubview(longitudeTitle)
|
||||||
|
stackLongitude.addArrangedSubview(longitudeValue)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
bottomAnchor.constraint(equalTo: stack.bottomAnchor, constant: 8),
|
||||||
|
leadingAnchor.constraint(equalTo: stack.leadingAnchor, constant: -20),
|
||||||
|
topAnchor.constraint(equalTo: stack.topAnchor, constant: -8),
|
||||||
|
trailingAnchor.constraint(equalTo: stack.trailingAnchor),
|
||||||
|
icon.bottomAnchor.constraint(equalTo: stack.bottomAnchor),
|
||||||
|
icon.topAnchor.constraint(equalTo: stack.topAnchor),
|
||||||
|
icon.widthAnchor.constraint(equalToConstant: 24),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -10,6 +10,10 @@
|
|||||||
02031EBF29E5F949003C108C /* LocationsAddViewModeling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EBE29E5F949003C108C /* LocationsAddViewModeling.swift */; };
|
02031EBF29E5F949003C108C /* LocationsAddViewModeling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EBE29E5F949003C108C /* LocationsAddViewModeling.swift */; };
|
||||||
02031EC629E5FEE4003C108C /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EC529E5FEE4003C108C /* BaseViewController.swift */; };
|
02031EC629E5FEE4003C108C /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EC529E5FEE4003C108C /* BaseViewController.swift */; };
|
||||||
02031EC929E60B29003C108C /* DependencyService+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EC829E60B29003C108C /* DependencyService+Keys.swift */; };
|
02031EC929E60B29003C108C /* DependencyService+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EC829E60B29003C108C /* DependencyService+Keys.swift */; };
|
||||||
|
02031EE829E68D9B003C108C /* LoadingSpinnerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EE729E68D9B003C108C /* LoadingSpinnerView.swift */; };
|
||||||
|
02031EEA29E6B495003C108C /* ErrorMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EE929E6B495003C108C /* ErrorMessageView.swift */; };
|
||||||
|
4656CBC229E6D33C00600EE6 /* LoadRemoteLocationsUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4656CBC129E6D33C00600EE6 /* LoadRemoteLocationsUseCase.swift */; };
|
||||||
|
4656CBC829E6F2E400600EE6 /* LocationViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4656CBC729E6F2E400600EE6 /* LocationViewCell.swift */; };
|
||||||
46C3B7C629E5BF1500F8F57C /* LocationsListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7C529E5BF1500F8F57C /* LocationsListCoordinator.swift */; };
|
46C3B7C629E5BF1500F8F57C /* LocationsListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7C529E5BF1500F8F57C /* LocationsListCoordinator.swift */; };
|
||||||
46C3B7CB29E5CD3200F8F57C /* LocationsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7CA29E5CD3200F8F57C /* LocationsListViewModel.swift */; };
|
46C3B7CB29E5CD3200F8F57C /* LocationsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7CA29E5CD3200F8F57C /* LocationsListViewModel.swift */; };
|
||||||
46C3B7CF29E5D00E00F8F57C /* LocationsAddViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7CE29E5D00E00F8F57C /* LocationsAddViewModel.swift */; };
|
46C3B7CF29E5D00E00F8F57C /* LocationsAddViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7CE29E5D00E00F8F57C /* LocationsAddViewModel.swift */; };
|
||||||
@ -124,6 +128,10 @@
|
|||||||
02031EBE29E5F949003C108C /* LocationsAddViewModeling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddViewModeling.swift; sourceTree = "<group>"; };
|
02031EBE29E5F949003C108C /* LocationsAddViewModeling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddViewModeling.swift; sourceTree = "<group>"; };
|
||||||
02031EC529E5FEE4003C108C /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = "<group>"; };
|
02031EC529E5FEE4003C108C /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = "<group>"; };
|
||||||
02031EC829E60B29003C108C /* DependencyService+Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DependencyService+Keys.swift"; sourceTree = "<group>"; };
|
02031EC829E60B29003C108C /* DependencyService+Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DependencyService+Keys.swift"; sourceTree = "<group>"; };
|
||||||
|
02031EE729E68D9B003C108C /* LoadingSpinnerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingSpinnerView.swift; sourceTree = "<group>"; };
|
||||||
|
02031EE929E6B495003C108C /* ErrorMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessageView.swift; sourceTree = "<group>"; };
|
||||||
|
4656CBC129E6D33C00600EE6 /* LoadRemoteLocationsUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadRemoteLocationsUseCase.swift; sourceTree = "<group>"; };
|
||||||
|
4656CBC729E6F2E400600EE6 /* LocationViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationViewCell.swift; sourceTree = "<group>"; };
|
||||||
46C3B7C529E5BF1500F8F57C /* LocationsListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsListCoordinator.swift; sourceTree = "<group>"; };
|
46C3B7C529E5BF1500F8F57C /* LocationsListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsListCoordinator.swift; sourceTree = "<group>"; };
|
||||||
46C3B7CA29E5CD3200F8F57C /* LocationsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsListViewModel.swift; sourceTree = "<group>"; };
|
46C3B7CA29E5CD3200F8F57C /* LocationsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsListViewModel.swift; sourceTree = "<group>"; };
|
||||||
46C3B7CE29E5D00E00F8F57C /* LocationsAddViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddViewModel.swift; sourceTree = "<group>"; };
|
46C3B7CE29E5D00E00F8F57C /* LocationsAddViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddViewModel.swift; sourceTree = "<group>"; };
|
||||||
@ -174,6 +182,16 @@
|
|||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
02031EE629E68D7A003C108C /* View Components */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02031EE729E68D9B003C108C /* LoadingSpinnerView.swift */,
|
||||||
|
02031EE929E6B495003C108C /* ErrorMessageView.swift */,
|
||||||
|
4656CBC729E6F2E400600EE6 /* LocationViewCell.swift */,
|
||||||
|
);
|
||||||
|
path = "View Components";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
0276C96029E5F5DC000B62AF /* Protocols */ = {
|
0276C96029E5F5DC000B62AF /* Protocols */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -201,6 +219,14 @@
|
|||||||
path = Coordination;
|
path = Coordination;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4656CBC029E6D31800600EE6 /* Use Cases */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4656CBC129E6D33C00600EE6 /* LoadRemoteLocationsUseCase.swift */,
|
||||||
|
);
|
||||||
|
path = "Use Cases";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
46C3B7C429E5BEE900F8F57C /* Coordinators */ = {
|
46C3B7C429E5BEE900F8F57C /* Coordinators */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -304,6 +330,8 @@
|
|||||||
46C3B7C429E5BEE900F8F57C /* Coordinators */,
|
46C3B7C429E5BEE900F8F57C /* Coordinators */,
|
||||||
46C3B7C929E5CB8F00F8F57C /* Screens */,
|
46C3B7C929E5CB8F00F8F57C /* Screens */,
|
||||||
02031EC429E5FEB1003C108C /* View Controllers */,
|
02031EC429E5FEB1003C108C /* View Controllers */,
|
||||||
|
02031EE629E68D7A003C108C /* View Components */,
|
||||||
|
4656CBC029E6D31800600EE6 /* Use Cases */,
|
||||||
);
|
);
|
||||||
path = Sources;
|
path = Sources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -514,13 +542,17 @@
|
|||||||
02031EC629E5FEE4003C108C /* BaseViewController.swift in Sources */,
|
02031EC629E5FEE4003C108C /* BaseViewController.swift in Sources */,
|
||||||
46EB331B29E1CE04001D5EAF /* AppDelegate.swift in Sources */,
|
46EB331B29E1CE04001D5EAF /* AppDelegate.swift in Sources */,
|
||||||
02031EBF29E5F949003C108C /* LocationsAddViewModeling.swift in Sources */,
|
02031EBF29E5F949003C108C /* LocationsAddViewModeling.swift in Sources */,
|
||||||
|
4656CBC829E6F2E400600EE6 /* LocationViewCell.swift in Sources */,
|
||||||
46C3B7DE29E5ED2E00F8F57C /* LocationsAddCoordinator.swift in Sources */,
|
46C3B7DE29E5ED2E00F8F57C /* LocationsAddCoordinator.swift in Sources */,
|
||||||
|
02031EEA29E6B495003C108C /* ErrorMessageView.swift in Sources */,
|
||||||
46C3B7DC29E5ED2300F8F57C /* LocationsAddCoordination.swift in Sources */,
|
46C3B7DC29E5ED2300F8F57C /* LocationsAddCoordination.swift in Sources */,
|
||||||
46C3B7D829E5E55000F8F57C /* LocationsListCoordination.swift in Sources */,
|
46C3B7D829E5E55000F8F57C /* LocationsListCoordination.swift in Sources */,
|
||||||
46C3B7D629E5E50500F8F57C /* LocationsListViewModeling.swift in Sources */,
|
46C3B7D629E5E50500F8F57C /* LocationsListViewModeling.swift in Sources */,
|
||||||
|
4656CBC229E6D33C00600EE6 /* LoadRemoteLocationsUseCase.swift in Sources */,
|
||||||
46C3B7CF29E5D00E00F8F57C /* LocationsAddViewModel.swift in Sources */,
|
46C3B7CF29E5D00E00F8F57C /* LocationsAddViewModel.swift in Sources */,
|
||||||
02031EC929E60B29003C108C /* DependencyService+Keys.swift in Sources */,
|
02031EC929E60B29003C108C /* DependencyService+Keys.swift in Sources */,
|
||||||
46C3B7D129E5D06D00F8F57C /* LocationsAddViewController.swift in Sources */,
|
46C3B7D129E5D06D00F8F57C /* LocationsAddViewController.swift in Sources */,
|
||||||
|
02031EE829E68D9B003C108C /* LoadingSpinnerView.swift in Sources */,
|
||||||
46C3B7CB29E5CD3200F8F57C /* LocationsListViewModel.swift in Sources */,
|
46C3B7CB29E5CD3200F8F57C /* LocationsListViewModel.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user