[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"?>
|
||||
<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="latitude" 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.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import Persistence
|
||||
|
||||
protocol LocationsListViewModeling: AnyObject {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var coordinator: LocationsListCoordination? { get set }
|
||||
|
||||
var viewStatusPublisher: Published<LocationsListViewStatus>.Publisher { get }
|
||||
var numberOfSectionsInData: Int { get }
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
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.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Core
|
||||
import UIKit
|
||||
|
||||
@ -13,7 +14,25 @@ class LocationsListViewController: BaseViewController {
|
||||
|
||||
// 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
|
||||
|
||||
@ -32,7 +51,71 @@ class LocationsListViewController: BaseViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
setupBar()
|
||||
setupView()
|
||||
bindViewModel()
|
||||
|
||||
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",
|
||||
style: .plain,
|
||||
target: self,
|
||||
@ -41,13 +124,44 @@ class LocationsListViewController: BaseViewController {
|
||||
title = "Locations"
|
||||
}
|
||||
|
||||
func setupView() {
|
||||
view.addSubview(table)
|
||||
view.addSubview(error)
|
||||
view.addSubview(loading)
|
||||
|
||||
error.onRetry = {
|
||||
self.viewModel.loadLocations()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
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),
|
||||
])
|
||||
}
|
||||
|
||||
private extension LocationsListViewController {
|
||||
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
|
||||
|
||||
// MARK: Functions
|
||||
if viewStatus == .loaded {
|
||||
self.table.reloadData()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
@objc func addLocationPressed() {
|
||||
viewModel.openAddLocation()
|
||||
|
@ -6,15 +6,33 @@
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Combine
|
||||
import Core
|
||||
import Dependency
|
||||
import Foundation
|
||||
import Persistence
|
||||
|
||||
class LocationsListViewModel: ObservableObject {
|
||||
|
||||
// MARK: Dependencies
|
||||
|
||||
@Dependency(\.persistence) private var persistence
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
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
|
||||
|
||||
init(coordinator: LocationsListCoordination) {
|
||||
@ -27,10 +45,55 @@ class LocationsListViewModel: ObservableObject {
|
||||
|
||||
extension LocationsListViewModel: LocationsListViewModeling {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var viewStatusPublisher: Published<LocationsListViewStatus>.Publisher { $viewStatus }
|
||||
var numberOfSectionsInData: Int { fetchedResultsController.sections?.count ?? 0 }
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func 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 */; };
|
||||
02031EC629E5FEE4003C108C /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031EC529E5FEE4003C108C /* BaseViewController.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 */; };
|
||||
46C3B7CB29E5CD3200F8F57C /* LocationsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7CA29E5CD3200F8F57C /* LocationsListViewModel.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -174,6 +182,16 @@
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
02031EE629E68D7A003C108C /* View Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
02031EE729E68D9B003C108C /* LoadingSpinnerView.swift */,
|
||||
02031EE929E6B495003C108C /* ErrorMessageView.swift */,
|
||||
4656CBC729E6F2E400600EE6 /* LocationViewCell.swift */,
|
||||
);
|
||||
path = "View Components";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0276C96029E5F5DC000B62AF /* Protocols */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -201,6 +219,14 @@
|
||||
path = Coordination;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4656CBC029E6D31800600EE6 /* Use Cases */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4656CBC129E6D33C00600EE6 /* LoadRemoteLocationsUseCase.swift */,
|
||||
);
|
||||
path = "Use Cases";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
46C3B7C429E5BEE900F8F57C /* Coordinators */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -304,6 +330,8 @@
|
||||
46C3B7C429E5BEE900F8F57C /* Coordinators */,
|
||||
46C3B7C929E5CB8F00F8F57C /* Screens */,
|
||||
02031EC429E5FEB1003C108C /* View Controllers */,
|
||||
02031EE629E68D7A003C108C /* View Components */,
|
||||
4656CBC029E6D31800600EE6 /* Use Cases */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
@ -514,13 +542,17 @@
|
||||
02031EC629E5FEE4003C108C /* BaseViewController.swift in Sources */,
|
||||
46EB331B29E1CE04001D5EAF /* AppDelegate.swift in Sources */,
|
||||
02031EBF29E5F949003C108C /* LocationsAddViewModeling.swift in Sources */,
|
||||
4656CBC829E6F2E400600EE6 /* LocationViewCell.swift in Sources */,
|
||||
46C3B7DE29E5ED2E00F8F57C /* LocationsAddCoordinator.swift in Sources */,
|
||||
02031EEA29E6B495003C108C /* ErrorMessageView.swift in Sources */,
|
||||
46C3B7DC29E5ED2300F8F57C /* LocationsAddCoordination.swift in Sources */,
|
||||
46C3B7D829E5E55000F8F57C /* LocationsListCoordination.swift in Sources */,
|
||||
46C3B7D629E5E50500F8F57C /* LocationsListViewModeling.swift in Sources */,
|
||||
4656CBC229E6D33C00600EE6 /* LoadRemoteLocationsUseCase.swift in Sources */,
|
||||
46C3B7CF29E5D00E00F8F57C /* LocationsAddViewModel.swift in Sources */,
|
||||
02031EC929E60B29003C108C /* DependencyService+Keys.swift in Sources */,
|
||||
46C3B7D129E5D06D00F8F57C /* LocationsAddViewController.swift in Sources */,
|
||||
02031EE829E68D9B003C108C /* LoadingSpinnerView.swift in Sources */,
|
||||
46C3B7CB29E5CD3200F8F57C /* LocationsListViewModel.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
Loading…
x
Reference in New Issue
Block a user