Compare commits
18 Commits
main
...
setup/app-
Author | SHA1 | Date | |
---|---|---|---|
cb5b4743f2 | |||
83ed85b755 | |||
b73a8e1010 | |||
b9417244a4 | |||
9b14b79ea2 | |||
5e5653bbe2 | |||
c16b40652f | |||
0d824637ae | |||
8a0c7d95e0 | |||
8deadffe2a | |||
2d068de8ed | |||
0dfbe62603 | |||
b4b99dfa87 | |||
a55ba672da | |||
229bd1b878 | |||
0fe40095c7 | |||
30bb62e795 | |||
c3fc42d724 |
@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "APICoreTests"
|
||||
BuildableName = "APICoreTests"
|
||||
BlueprintName = "APICoreTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CoreTests"
|
||||
BuildableName = "CoreTests"
|
||||
BlueprintName = "CoreTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DependencyTests"
|
||||
BuildableName = "DependencyTests"
|
||||
BlueprintName = "DependencyTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -1,66 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Libraries"
|
||||
BuildableName = "Libraries"
|
||||
BlueprintName = "Libraries"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Libraries"
|
||||
BuildableName = "Libraries"
|
||||
BlueprintName = "Libraries"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "PersistenceTests"
|
||||
BuildableName = "PersistenceTests"
|
||||
BlueprintName = "PersistenceTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "RemoteTests"
|
||||
BuildableName = "RemoteTests"
|
||||
BlueprintName = "RemoteTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -1,23 +0,0 @@
|
||||
//
|
||||
// Application.swift
|
||||
// Core
|
||||
//
|
||||
// Created by Javier Cicchelli on 13/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public protocol Application {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func canOpenURL(_ url: URL) -> Bool
|
||||
func open(
|
||||
_ url: URL,
|
||||
options: [UIApplication.OpenExternalURLOptionsKey : Any],
|
||||
completionHandler completion: ((Bool) -> Void)?
|
||||
)
|
||||
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
//
|
||||
// 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">
|
||||
<entity name="Location" representedClassName="Location" syncable="YES" codeGenerationType="class">
|
||||
<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"/>
|
||||
|
@ -1,36 +0,0 @@
|
||||
//
|
||||
// Service.swift
|
||||
// Persistence
|
||||
//
|
||||
// Created by Javier Cicchelli on 13/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
public protocol Service {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The main managed object context.
|
||||
var viewContext: NSManagedObjectContext { get }
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Create a private queue context.
|
||||
/// - Returns: A concurrent `NSManagedObjectContext` context instance ready to use.
|
||||
func makeTaskContext() -> NSManagedObjectContext
|
||||
|
||||
/// Create a child context of the view context.
|
||||
/// - Returns: A generated child `NSManagedObjectContext` context instance ready to use.
|
||||
func makeChildContext() -> NSManagedObjectContext
|
||||
|
||||
/// Save a given context,
|
||||
/// - Parameter context: A `NSManagedObjectContext` context instance to save.
|
||||
func save(context: NSManagedObjectContext)
|
||||
|
||||
/// Save a given child context as well as its respective parent context.
|
||||
/// - Parameter context: A child `NSManagedObjectContext` context instance to save.
|
||||
func save(childContext context: NSManagedObjectContext)
|
||||
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
//
|
||||
// LocationProvider.swift
|
||||
// Persistence
|
||||
//
|
||||
// Created by Javier Cicchelli on 12/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
|
||||
public class LocationProvider: NSObject {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let fetchedResultsController: NSFetchedResultsController<Location>
|
||||
|
||||
/// The publisher that emits the changes detected to the Location entities in a given object context.
|
||||
public let didChangePublisher = PassthroughSubject<[Change], Never>()
|
||||
|
||||
private var inProgressChanges: [Change] = []
|
||||
|
||||
/// The number of sections in the data.
|
||||
public var numberOfSections: Int { fetchedResultsController.sections?.count ?? 0 }
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
/// Initialise this provider with the managed object context that would be used.
|
||||
/// - Parameter managedContext: A `NSManagedObjectContext` object context instance that will be used to provide entities.
|
||||
public init(managedContext: NSManagedObjectContext) {
|
||||
self.fetchedResultsController = .init(
|
||||
fetchRequest: .allLocations(),
|
||||
managedObjectContext: managedContext,
|
||||
sectionNameKeyPath: nil,
|
||||
cacheName: nil
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
self.fetchedResultsController.delegate = self
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Perform the fetching.
|
||||
public func fetch() throws {
|
||||
try fetchedResultsController.performFetch()
|
||||
}
|
||||
|
||||
/// Retrieve the number of locations inside a given section number.
|
||||
/// - Parameter section: The section number to inquiry about.
|
||||
/// - Returns: A number of locations inside the given section number.
|
||||
public func numberOfLocationsInSection(_ section: Int) -> Int {
|
||||
guard
|
||||
let sections = fetchedResultsController.sections,
|
||||
sections.endIndex > section
|
||||
else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return sections[section].numberOfObjects
|
||||
}
|
||||
|
||||
/// Retrieve a location entity out of a given index path.
|
||||
/// - Parameter indexPath: The index path to which retrieve a location entity.
|
||||
/// - Returns: A `Location` entity positioned in the given index path.
|
||||
public func location(at indexPath: IndexPath) -> Location {
|
||||
return fetchedResultsController.object(at: indexPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
|
||||
extension LocationProvider: NSFetchedResultsControllerDelegate {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||
inProgressChanges.removeAll()
|
||||
}
|
||||
|
||||
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||
didChangePublisher.send(inProgressChanges)
|
||||
}
|
||||
|
||||
public func controller(
|
||||
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
|
||||
didChange sectionInfo: NSFetchedResultsSectionInfo,
|
||||
atSectionIndex sectionIndex: Int,
|
||||
for type: NSFetchedResultsChangeType
|
||||
) {
|
||||
if type == .insert {
|
||||
inProgressChanges.append(.section(.inserted(sectionIndex)))
|
||||
} else if type == .delete {
|
||||
inProgressChanges.append(.section(.deleted(sectionIndex)))
|
||||
}
|
||||
}
|
||||
|
||||
public func controller(
|
||||
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
|
||||
didChange anObject: Any,
|
||||
at indexPath: IndexPath?,
|
||||
for type: NSFetchedResultsChangeType,
|
||||
newIndexPath: IndexPath?
|
||||
) {
|
||||
switch type {
|
||||
case .insert:
|
||||
guard let newIndexPath else { return }
|
||||
|
||||
inProgressChanges.append(.object(.inserted(at: newIndexPath)))
|
||||
case .delete:
|
||||
guard let indexPath else { return }
|
||||
|
||||
inProgressChanges.append(.object(.deleted(from: indexPath)))
|
||||
case .move:
|
||||
guard let indexPath, let newIndexPath else { return }
|
||||
|
||||
inProgressChanges.append(.object(.moved(from: indexPath, to: newIndexPath)))
|
||||
case .update:
|
||||
guard let indexPath else { return }
|
||||
|
||||
inProgressChanges.append(.object(.updated(at: indexPath)))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Enumerations
|
||||
|
||||
public enum Change: Hashable {
|
||||
public enum SectionUpdate: Hashable {
|
||||
case inserted(Int)
|
||||
case deleted(Int)
|
||||
}
|
||||
|
||||
public enum ObjectUpdate: Hashable {
|
||||
case inserted(at: IndexPath)
|
||||
case deleted(from: IndexPath)
|
||||
case updated(at: IndexPath)
|
||||
case moved(from: IndexPath, to: IndexPath)
|
||||
}
|
||||
|
||||
case section(SectionUpdate)
|
||||
case object(ObjectUpdate)
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ public struct PersistenceService {
|
||||
else {
|
||||
fatalError("Could not load the model from the library.")
|
||||
}
|
||||
|
||||
|
||||
container = NSPersistentContainer(
|
||||
name: .Model.name,
|
||||
managedObjectModel: managedObjectModel
|
||||
@ -35,18 +35,10 @@ public struct PersistenceService {
|
||||
setContainer(inMemory)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Service
|
||||
|
||||
extension PersistenceService: Service {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
public var viewContext: NSManagedObjectContext { container.viewContext }
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
|
||||
/// Create a private queue context.
|
||||
/// - Returns: A concurrent `NSManagedObjectContext` context instance ready to use.
|
||||
public func makeTaskContext() -> NSManagedObjectContext {
|
||||
let taskContext = container.newBackgroundContext()
|
||||
|
||||
@ -56,6 +48,8 @@ extension PersistenceService: Service {
|
||||
return taskContext
|
||||
}
|
||||
|
||||
/// Create a child context of the view context.
|
||||
/// - Returns: A generated child `NSManagedObjectContext` context instance ready to use.
|
||||
public func makeChildContext() -> NSManagedObjectContext {
|
||||
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||
|
||||
@ -66,6 +60,8 @@ extension PersistenceService: Service {
|
||||
return context
|
||||
}
|
||||
|
||||
/// Save a given context,
|
||||
/// - Parameter context: A `NSManagedObjectContext` context instance to save.
|
||||
public func save(context: NSManagedObjectContext) {
|
||||
guard context.hasChanges else {
|
||||
return
|
||||
@ -79,6 +75,8 @@ extension PersistenceService: Service {
|
||||
}
|
||||
}
|
||||
|
||||
/// Save a given child context as well as its respective parent context.
|
||||
/// - Parameter context: A child `NSManagedObjectContext` context instance to save.
|
||||
public func save(childContext context: NSManagedObjectContext) {
|
||||
guard context.hasChanges else {
|
||||
return
|
||||
@ -102,7 +100,7 @@ extension PersistenceService: Service {
|
||||
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
@ -16,7 +16,7 @@ public struct Location: Equatable {
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(
|
||||
public init(
|
||||
name: String? = nil,
|
||||
latitude: Float,
|
||||
longitude: Float
|
||||
|
@ -1,19 +0,0 @@
|
||||
//
|
||||
// Service.swift
|
||||
// Remote
|
||||
//
|
||||
// Created by Javier Cicchelli on 13/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol Service {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
/// Retrieve a set of locations.
|
||||
/// - Returns: The set of locations represented as a `Location` instances.
|
||||
func getLocations() async throws -> [Location]
|
||||
|
||||
}
|
@ -21,12 +21,6 @@ public struct RemoteService {
|
||||
self.client = RemoteClient(configuration: configuration)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Service
|
||||
|
||||
extension RemoteService: Service {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
public func getLocations() async throws -> [Location] {
|
||||
@ -35,7 +29,7 @@ extension RemoteService: Service {
|
||||
for: Locations.self
|
||||
).locations
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Models
|
||||
|
@ -1,10 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>wikipedia</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict/>
|
||||
</plist>
|
||||
|
@ -38,12 +38,4 @@ class LocationsAddCoordinator: Coordinator {
|
||||
|
||||
// MARK: - LocationsAddCoordination
|
||||
|
||||
extension LocationsAddCoordinator: LocationsAddCoordination {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func closeLocationsAddScreen() {
|
||||
router.dismiss(animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
extension LocationsAddCoordinator: LocationsAddCoordination {}
|
||||
|
@ -7,15 +7,10 @@
|
||||
//
|
||||
|
||||
import Core
|
||||
import Dependency
|
||||
import UIKit
|
||||
|
||||
class LocationsListCoordinator: Coordinator {
|
||||
|
||||
// MARK: Dependencies
|
||||
|
||||
@Dependency(\.app) private var app
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var children: [Coordinator] = []
|
||||
@ -53,7 +48,7 @@ extension LocationsListCoordinator: LocationsListCoordination {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func openLocationsAddScreen() {
|
||||
func openAddLocation() {
|
||||
guard let viewController else {
|
||||
return
|
||||
}
|
||||
@ -66,12 +61,4 @@ extension LocationsListCoordinator: LocationsListCoordination {
|
||||
)
|
||||
}
|
||||
|
||||
func openWikipediaApp(with url: URL) {
|
||||
guard app.canOpenURL(url) else {
|
||||
return
|
||||
}
|
||||
|
||||
app.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,26 +6,19 @@
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Core
|
||||
import Dependency
|
||||
import Persistence
|
||||
import Remote
|
||||
import UIKit
|
||||
|
||||
// MARK: - DependencyService+Keys
|
||||
|
||||
extension DependencyService {
|
||||
var app: Core.Application {
|
||||
get { Self[ApplicationKey.self] }
|
||||
set { Self[ApplicationKey.self] = newValue }
|
||||
}
|
||||
|
||||
var persistence: Persistence.Service {
|
||||
var persistence: PersistenceService {
|
||||
get { Self[PersistenceKey.self] }
|
||||
set { Self[PersistenceKey.self] = newValue }
|
||||
}
|
||||
|
||||
var remote: Remote.Service {
|
||||
var remote: RemoteService {
|
||||
get { Self[RemoteKey.self] }
|
||||
set { Self[RemoteKey.self] = newValue }
|
||||
}
|
||||
@ -33,14 +26,10 @@ extension DependencyService {
|
||||
|
||||
// MARK: - Dependency keys
|
||||
|
||||
struct ApplicationKey: DependencyKey {
|
||||
static var currentValue: Core.Application = UIApplication.shared
|
||||
}
|
||||
|
||||
struct PersistenceKey: DependencyKey {
|
||||
static var currentValue: Persistence.Service = PersistenceService.shared
|
||||
static var currentValue: PersistenceService = .shared
|
||||
}
|
||||
|
||||
struct RemoteKey: DependencyKey {
|
||||
static var currentValue: Remote.Service = RemoteService()
|
||||
static var currentValue: RemoteService = .init()
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
//
|
||||
// Location+URLs.swift
|
||||
// Locations
|
||||
//
|
||||
// Created by Javier Cicchelli on 13/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Persistence
|
||||
|
||||
extension Location {
|
||||
|
||||
var wikipediaPlacesURL: URL? {
|
||||
var urlComponents = URLComponents()
|
||||
|
||||
urlComponents.scheme = .Scheme.wikipedia
|
||||
urlComponents.host = .Host.places
|
||||
urlComponents.queryItems = [
|
||||
.init(
|
||||
name: .Query.key,
|
||||
value: .init(format: .Query.value, latitude, longitude)
|
||||
)
|
||||
]
|
||||
|
||||
return urlComponents.url
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - String+Constants
|
||||
|
||||
private extension String {
|
||||
enum Scheme {
|
||||
static let wikipedia = "wikipedia"
|
||||
}
|
||||
|
||||
enum Host {
|
||||
static let places = "places"
|
||||
}
|
||||
|
||||
enum Query {
|
||||
static let key = "coordinates"
|
||||
static let value = "%f,%f"
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
//
|
||||
// UIApplication+Conformances.swift
|
||||
// Locations
|
||||
//
|
||||
// Created by Javier Cicchelli on 13/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Core
|
||||
import UIKit
|
||||
|
||||
extension UIApplication: Application {}
|
@ -6,10 +6,4 @@
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
protocol LocationsAddCoordination: AnyObject {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func closeLocationsAddScreen()
|
||||
|
||||
}
|
||||
protocol LocationsAddCoordination: AnyObject {}
|
||||
|
@ -6,13 +6,10 @@
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol LocationsListCoordination: AnyObject {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func openLocationsAddScreen()
|
||||
func openWikipediaApp(with url: URL)
|
||||
func openAddLocation()
|
||||
|
||||
}
|
||||
|
@ -6,20 +6,10 @@
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
protocol LocationsAddViewModeling: AnyObject {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var coordinator: LocationsAddCoordination? { get set }
|
||||
|
||||
var locationExistsPublisher: Published<Bool>.Publisher { get }
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func cleanLocation()
|
||||
func saveLocation()
|
||||
func setLocation(latitude: Float, longitude: Float)
|
||||
|
||||
}
|
||||
|
@ -6,26 +6,14 @@
|
||||
// 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 locationsDidChangePublisher: PassthroughSubject<[Change], Never> { get }
|
||||
var numberOfLocationSections: Int { get }
|
||||
var viewStatusPublisher: Published<LocationsListViewStatus>.Publisher { get }
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func loadLocations()
|
||||
func location(at indexPath: IndexPath) -> Location
|
||||
func numberOfLocations(in section: Int) -> Int
|
||||
func openLocationsAdd()
|
||||
func openWikipedia(at indexPath: IndexPath)
|
||||
|
||||
func openAddLocation()
|
||||
|
||||
}
|
||||
|
@ -6,30 +6,14 @@
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Core
|
||||
import UIKit
|
||||
import MapKit
|
||||
|
||||
class LocationsAddViewController: BaseViewController {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let viewModel: LocationsAddViewModeling
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
// MARK: Outlets
|
||||
|
||||
private lazy var map = {
|
||||
let map = MKMapView()
|
||||
|
||||
map.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
map.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapOnMap)))
|
||||
|
||||
return map
|
||||
}()
|
||||
var viewModel: LocationsAddViewModeling
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
@ -48,91 +32,7 @@ class LocationsAddViewController: BaseViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupBar()
|
||||
setupView()
|
||||
bindViewModel()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension LocationsAddViewController {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func bindViewModel() {
|
||||
viewModel
|
||||
.locationExistsPublisher
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { locationExists in
|
||||
self.navigationItem
|
||||
.rightBarButtonItems?
|
||||
.forEach { $0.isEnabled = locationExists }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func setupBar() {
|
||||
title = "Add a location"
|
||||
navigationController?.navigationBar.prefersLargeTitles = false
|
||||
navigationController?.navigationBar.backgroundColor = .systemBackground
|
||||
navigationController?.navigationBar.isTranslucent = true
|
||||
navigationController?.navigationBar.tintColor = .red
|
||||
navigationItem.rightBarButtonItems = [
|
||||
.init(
|
||||
title: "Save",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(saveButtonPressed)
|
||||
),
|
||||
.init(
|
||||
title: "Clean",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(cleanButtonPressed)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
func setupView() {
|
||||
view.addSubview(map)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
view.bottomAnchor.constraint(equalTo: map.bottomAnchor),
|
||||
view.leadingAnchor.constraint(equalTo: map.leadingAnchor),
|
||||
view.topAnchor.constraint(equalTo: map.topAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: map.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc func cleanButtonPressed() {
|
||||
map.removeAnnotations(map.annotations)
|
||||
|
||||
viewModel.cleanLocation()
|
||||
}
|
||||
|
||||
@objc func saveButtonPressed() {
|
||||
viewModel.saveLocation()
|
||||
}
|
||||
|
||||
@objc func tapOnMap(recognizer: UITapGestureRecognizer) {
|
||||
let tapOnView = recognizer.location(in: map)
|
||||
let mapCoordinates = map.convert(tapOnView, toCoordinateFrom: map)
|
||||
let annotation = MKPointAnnotation()
|
||||
|
||||
annotation.coordinate = mapCoordinates
|
||||
|
||||
map.removeAnnotations(map.annotations)
|
||||
map.addAnnotation(annotation)
|
||||
map.setCenter(mapCoordinates, animated: true)
|
||||
|
||||
viewModel.setLocation(
|
||||
latitude: Float(mapCoordinates.latitude),
|
||||
longitude: Float(mapCoordinates.longitude)
|
||||
)
|
||||
title = "Location Add"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,82 +10,19 @@ import Combine
|
||||
import Core
|
||||
|
||||
class LocationsAddViewModel: ObservableObject {
|
||||
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
weak var coordinator: LocationsAddCoordination?
|
||||
|
||||
@Published private var location: Location?
|
||||
@Published private var locationExists: Bool = false
|
||||
|
||||
private let saveLocalLocation = SaveLocalLocationUseCase()
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(coordinator: LocationsAddCoordination) {
|
||||
self.coordinator = coordinator
|
||||
|
||||
setupBindings()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - LocationsAddViewModeling
|
||||
|
||||
extension LocationsAddViewModel: LocationsAddViewModeling {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var locationExistsPublisher: Published<Bool>.Publisher { $locationExists }
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func cleanLocation() {
|
||||
location = nil
|
||||
}
|
||||
|
||||
func saveLocation() {
|
||||
guard let location else {
|
||||
return
|
||||
}
|
||||
|
||||
saveLocalLocation(
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude
|
||||
)
|
||||
|
||||
coordinator?.closeLocationsAddScreen()
|
||||
}
|
||||
|
||||
func setLocation(latitude: Float, longitude: Float) {
|
||||
if location == nil {
|
||||
location = .init(latitude: latitude, longitude: longitude)
|
||||
} else {
|
||||
location?.latitude = latitude
|
||||
location?.longitude = longitude
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension LocationsAddViewModel {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func setupBindings() {
|
||||
$location
|
||||
.map { $0 != nil }
|
||||
.assign(to: &$locationExists)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Structs
|
||||
|
||||
private extension LocationsAddViewModel {
|
||||
struct Location {
|
||||
var latitude: Float
|
||||
var longitude: Float
|
||||
}
|
||||
}
|
||||
extension LocationsAddViewModel: LocationsAddViewModeling {}
|
||||
|
@ -6,7 +6,6 @@
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Core
|
||||
import UIKit
|
||||
|
||||
@ -14,25 +13,7 @@ class LocationsListViewController: BaseViewController {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
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
|
||||
}()
|
||||
var viewModel: LocationsListViewModeling
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
@ -50,85 +31,8 @@ class LocationsListViewController: BaseViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupBar()
|
||||
setupView()
|
||||
bindViewModel()
|
||||
|
||||
viewModel.loadLocations()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
extension LocationsListViewController: UITableViewDataSource {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
viewModel.numberOfLocationSections
|
||||
}
|
||||
|
||||
func tableView(
|
||||
_ tableView: UITableView,
|
||||
numberOfRowsInSection section: Int
|
||||
) -> Int {
|
||||
viewModel.numberOfLocations(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.location(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: Functions
|
||||
|
||||
func tableView(
|
||||
_ tableView: UITableView,
|
||||
didSelectRowAt indexPath: IndexPath
|
||||
) {
|
||||
viewModel.openWikipedia(at: indexPath)
|
||||
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension LocationsListViewController {
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func setupBar() {
|
||||
navigationController?.navigationBar.prefersLargeTitles = true
|
||||
navigationController?.navigationBar.tintColor = .red
|
||||
navigationItem.rightBarButtonItem = .init(
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: "Add",
|
||||
style: .plain,
|
||||
target: self,
|
||||
@ -137,82 +41,16 @@ private extension LocationsListViewController {
|
||||
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
|
||||
|
||||
if viewStatus == .loaded {
|
||||
self.table.reloadData()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel
|
||||
.locationsDidChangePublisher
|
||||
.sink(receiveValue: { [weak self] updates in
|
||||
var movedToIndexPaths = [IndexPath]()
|
||||
|
||||
self?.table.performBatchUpdates({
|
||||
for update in updates {
|
||||
switch update {
|
||||
case let .section(sectionUpdate):
|
||||
switch sectionUpdate {
|
||||
case let .inserted(index):
|
||||
self?.table.insertSections([index], with: .automatic)
|
||||
case let .deleted(index):
|
||||
self?.table.deleteSections([index], with: .automatic)
|
||||
}
|
||||
case let .object(objectUpdate):
|
||||
switch objectUpdate {
|
||||
case let .inserted(at: indexPath):
|
||||
self?.table.insertRows(at: [indexPath], with: .automatic)
|
||||
case let .deleted(from: indexPath):
|
||||
self?.table.deleteRows(at: [indexPath], with: .automatic)
|
||||
case let .updated(at: indexPath):
|
||||
self?.table.reloadRows(at: [indexPath], with: .automatic)
|
||||
case let .moved(from: source, to: target):
|
||||
self?.table.moveRow(at: source, to: target)
|
||||
movedToIndexPaths.append(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, completion: { done in
|
||||
self?.table.reloadRows(at: movedToIndexPaths, with: .automatic)
|
||||
})
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
// MARK: Functions
|
||||
|
||||
@objc func addLocationPressed() {
|
||||
viewModel.openLocationsAdd()
|
||||
viewModel.openAddLocation()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,25 +7,13 @@
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Dependency
|
||||
import Foundation
|
||||
import Persistence
|
||||
import Core
|
||||
|
||||
class LocationsListViewModel: ObservableObject {
|
||||
|
||||
// MARK: Dependencies
|
||||
|
||||
@Dependency(\.persistence) private var persistence
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
weak var coordinator: LocationsListCoordination?
|
||||
|
||||
private lazy var locationProvider = LocationProvider(managedContext: persistence.viewContext)
|
||||
|
||||
@Published private var viewStatus: LocationsListViewStatus = .initialised
|
||||
|
||||
private let loadRemoteLocations = LoadRemoteLocationsUseCase()
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
@ -38,59 +26,11 @@ class LocationsListViewModel: ObservableObject {
|
||||
// MARK: - LocationsListViewModeling
|
||||
|
||||
extension LocationsListViewModel: LocationsListViewModeling {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var locationsDidChangePublisher: PassthroughSubject<[Persistence.Change], Never> { locationProvider.didChangePublisher }
|
||||
var numberOfLocationSections: Int { locationProvider.numberOfSections }
|
||||
var viewStatusPublisher: Published<LocationsListViewStatus>.Publisher { $viewStatus }
|
||||
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func loadLocations() {
|
||||
Task {
|
||||
do {
|
||||
viewStatus = .loading
|
||||
|
||||
try await loadRemoteLocations()
|
||||
|
||||
try locationProvider.fetch()
|
||||
|
||||
viewStatus = .loaded
|
||||
} catch {
|
||||
viewStatus = .error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func location(at indexPath: IndexPath) -> Location {
|
||||
locationProvider.location(at: indexPath)
|
||||
}
|
||||
|
||||
func numberOfLocations(in section: Int) -> Int {
|
||||
locationProvider.numberOfLocationsInSection(section)
|
||||
}
|
||||
|
||||
func openLocationsAdd() {
|
||||
coordinator?.openLocationsAddScreen()
|
||||
}
|
||||
|
||||
func openWikipedia(at indexPath: IndexPath) {
|
||||
guard let url = locationProvider.location(at: indexPath).wikipediaPlacesURL else {
|
||||
return
|
||||
}
|
||||
|
||||
coordinator?.openWikipediaApp(with: url)
|
||||
func openAddLocation() {
|
||||
coordinator?.openAddLocation()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Enumerations
|
||||
|
||||
enum LocationsListViewStatus {
|
||||
case initialised
|
||||
case loading
|
||||
case loaded
|
||||
case error
|
||||
}
|
||||
|
@ -1,77 +0,0 @@
|
||||
//
|
||||
// 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: Persistence.Service
|
||||
private let remoteService: Remote.Service
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(
|
||||
persistence: Persistence.Service,
|
||||
remoteService: Remote.Service
|
||||
) {
|
||||
self.persistence = persistence
|
||||
self.remoteService = remoteService
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
//
|
||||
// SaveLocalLocationUseCase.swift
|
||||
// Locations
|
||||
//
|
||||
// Created by Javier Cicchelli on 12/04/2023.
|
||||
// Copyright © 2023 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Dependency
|
||||
import Persistence
|
||||
|
||||
struct SaveLocalLocationUseCase {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let persistence: Persistence.Service
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(persistence: Persistence.Service) {
|
||||
self.persistence = persistence
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func callAsFunction(
|
||||
name: String? = nil,
|
||||
latitude: Float,
|
||||
longitude: Float
|
||||
) {
|
||||
let context = persistence.makeTaskContext()
|
||||
let entity = Location(context: context)
|
||||
|
||||
entity.createdAt = .now
|
||||
entity.name = name
|
||||
entity.latitude = latitude
|
||||
entity.longitude = longitude
|
||||
entity.source = .local
|
||||
|
||||
persistence.save(context: context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - LoadRemoteLocationsUseCase+Initialisers
|
||||
|
||||
extension SaveLocalLocationUseCase {
|
||||
init() {
|
||||
@Dependency(\.persistence) var persistence
|
||||
|
||||
self.init(persistence: persistence)
|
||||
}
|
||||
}
|
||||
|
@ -1,124 +0,0 @@
|
||||
//
|
||||
// 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?()
|
||||
}
|
||||
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
//
|
||||
// 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),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
@ -1,223 +0,0 @@
|
||||
//
|
||||
// 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),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
@ -456,6 +456,7 @@
|
||||
41FCAA3721C844CB001D8411 /* ReadingListEntryCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41FCAA3521C844CB001D8411 /* ReadingListEntryCollectionViewController.swift */; };
|
||||
41FCAA3821C844CB001D8411 /* ReadingListEntryCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41FCAA3521C844CB001D8411 /* ReadingListEntryCollectionViewController.swift */; };
|
||||
41FCAA3921C844CB001D8411 /* ReadingListEntryCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41FCAA3521C844CB001D8411 /* ReadingListEntryCollectionViewController.swift */; };
|
||||
46EB334829E1D204001D5EAF /* Shared in Frameworks */ = {isa = PBXBuildFile; productRef = 46EB334729E1D204001D5EAF /* Shared */; };
|
||||
533AB8AE259792A9003A43D9 /* wikipedia-language-variants.json in Resources */ = {isa = PBXBuildFile; fileRef = 533AB8AD259792A9003A43D9 /* wikipedia-language-variants.json */; };
|
||||
535F16D625CE11A300875AAD /* MWKDataStore+LanguageVariantMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535F16D525CE11A300875AAD /* MWKDataStore+LanguageVariantMigration.swift */; };
|
||||
53A575FA2602C845009835E6 /* WMFAppViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A575F92602C845009835E6 /* WMFAppViewController+Extensions.swift */; };
|
||||
@ -5775,6 +5776,7 @@
|
||||
D8D553621DF1B63200B90177 /* QuartzCore.framework in Frameworks */,
|
||||
D4E6D9121A5C65F9004916C1 /* CoreData.framework in Frameworks */,
|
||||
D499143B181D51DE00E6073C /* CoreGraphics.framework in Frameworks */,
|
||||
46EB334829E1D204001D5EAF /* Shared in Frameworks */,
|
||||
D499143D181D51DE00E6073C /* UIKit.framework in Frameworks */,
|
||||
D4991439181D51DE00E6073C /* Foundation.framework in Frameworks */,
|
||||
041EFC371996A1F800B2CB28 /* MapKit.framework in Frameworks */,
|
||||
@ -10077,6 +10079,7 @@
|
||||
);
|
||||
name = Wikipedia;
|
||||
packageProductDependencies = (
|
||||
46EB334729E1D204001D5EAF /* Shared */,
|
||||
);
|
||||
productName = "Wikipedia-iOS";
|
||||
productReference = D4991435181D51DE00E6073C /* Wikipedia.app */;
|
||||
@ -20143,6 +20146,10 @@
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
46EB334729E1D204001D5EAF /* Shared */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Shared;
|
||||
};
|
||||
67A770C7251BFE0400F94EF9 /* CocoaLumberjackSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 67A770C6251BFE0400F94EF9 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */;
|
||||
|
@ -10,12 +10,6 @@
|
||||
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 */; };
|
||||
02031F0829E75EF0003C108C /* SaveLocalLocationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031F0729E75EED003C108C /* SaveLocalLocationUseCase.swift */; };
|
||||
02031F0A29E7645F003C108C /* Location+URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02031F0929E7645F003C108C /* Location+URLs.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 */; };
|
||||
@ -24,12 +18,12 @@
|
||||
46C3B7D829E5E55000F8F57C /* LocationsListCoordination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7D729E5E55000F8F57C /* LocationsListCoordination.swift */; };
|
||||
46C3B7DC29E5ED2300F8F57C /* LocationsAddCoordination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7DB29E5ED2300F8F57C /* LocationsAddCoordination.swift */; };
|
||||
46C3B7DE29E5ED2E00F8F57C /* LocationsAddCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C3B7DD29E5ED2E00F8F57C /* LocationsAddCoordinator.swift */; };
|
||||
46DF736D29E82A1500AA6D21 /* UIApplication+Conformances.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DF736C29E82A1500AA6D21 /* UIApplication+Conformances.swift */; };
|
||||
46EB331B29E1CE04001D5EAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331A29E1CE04001D5EAF /* AppDelegate.swift */; };
|
||||
46EB331F29E1CE04001D5EAF /* LocationsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46EB331E29E1CE04001D5EAF /* LocationsListViewController.swift */; };
|
||||
46EB332729E1CE05001D5EAF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 46EB332629E1CE05001D5EAF /* Assets.xcassets */; };
|
||||
46EB332A29E1CE05001D5EAF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 46EB332829E1CE05001D5EAF /* LaunchScreen.storyboard */; };
|
||||
46EB334429E1D1EC001D5EAF /* Libraries in Frameworks */ = {isa = PBXBuildFile; productRef = 46EB334329E1D1EC001D5EAF /* Libraries */; };
|
||||
46EB334629E1D1F0001D5EAF /* Shared in Frameworks */ = {isa = PBXBuildFile; productRef = 46EB334529E1D1F0001D5EAF /* Shared */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -130,12 +124,6 @@
|
||||
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>"; };
|
||||
02031F0729E75EED003C108C /* SaveLocalLocationUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveLocalLocationUseCase.swift; sourceTree = "<group>"; };
|
||||
02031F0929E7645F003C108C /* Location+URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Location+URLs.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>"; };
|
||||
@ -144,7 +132,6 @@
|
||||
46C3B7D729E5E55000F8F57C /* LocationsListCoordination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsListCoordination.swift; sourceTree = "<group>"; };
|
||||
46C3B7DB29E5ED2300F8F57C /* LocationsAddCoordination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddCoordination.swift; sourceTree = "<group>"; };
|
||||
46C3B7DD29E5ED2E00F8F57C /* LocationsAddCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsAddCoordinator.swift; sourceTree = "<group>"; };
|
||||
46DF736C29E82A1500AA6D21 /* UIApplication+Conformances.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Conformances.swift"; sourceTree = "<group>"; };
|
||||
46EB325829E1BD5C001D5EAF /* Wikipedia.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Wikipedia.xcodeproj; path = Wikipedia/Wikipedia.xcodeproj; sourceTree = "<group>"; };
|
||||
46EB331829E1CE04001D5EAF /* Locations.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Locations.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
46EB331A29E1CE04001D5EAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@ -153,6 +140,7 @@
|
||||
46EB332929E1CE05001D5EAF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
46EB332B29E1CE05001D5EAF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
46EB333229E1CFD9001D5EAF /* Libraries */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Libraries; sourceTree = "<group>"; };
|
||||
46EB333429E1D158001D5EAF /* Shared */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Shared; sourceTree = "<group>"; };
|
||||
46EB334929E1D34B001D5EAF /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
46EB334A29E1D3C0001D5EAF /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@ -163,6 +151,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
46EB334429E1D1EC001D5EAF /* Libraries in Frameworks */,
|
||||
46EB334629E1D1F0001D5EAF /* Shared in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -181,22 +170,10 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
02031EC829E60B29003C108C /* DependencyService+Keys.swift */,
|
||||
02031F0929E7645F003C108C /* Location+URLs.swift */,
|
||||
46DF736C29E82A1500AA6D21 /* UIApplication+Conformances.swift */,
|
||||
);
|
||||
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 = (
|
||||
@ -224,15 +201,6 @@
|
||||
path = Coordination;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4656CBC029E6D31800600EE6 /* Use Cases */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4656CBC129E6D33C00600EE6 /* LoadRemoteLocationsUseCase.swift */,
|
||||
02031F0729E75EED003C108C /* SaveLocalLocationUseCase.swift */,
|
||||
);
|
||||
path = "Use Cases";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
46C3B7C429E5BEE900F8F57C /* Coordinators */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -272,6 +240,7 @@
|
||||
46EB325029E1BBD1001D5EAF = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
46EB333429E1D158001D5EAF /* Shared */,
|
||||
46EB325729E1BCAB001D5EAF /* Apps */,
|
||||
46EB334B29E1D3D2001D5EAF /* Others */,
|
||||
46EB32EE29E1CD20001D5EAF /* Products */,
|
||||
@ -335,8 +304,6 @@
|
||||
46C3B7C429E5BEE900F8F57C /* Coordinators */,
|
||||
46C3B7C929E5CB8F00F8F57C /* Screens */,
|
||||
02031EC429E5FEB1003C108C /* View Controllers */,
|
||||
02031EE629E68D7A003C108C /* View Components */,
|
||||
4656CBC029E6D31800600EE6 /* Use Cases */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
@ -385,6 +352,7 @@
|
||||
name = Locations;
|
||||
packageProductDependencies = (
|
||||
46EB334329E1D1EC001D5EAF /* Libraries */,
|
||||
46EB334529E1D1F0001D5EAF /* Shared */,
|
||||
);
|
||||
productName = Locations;
|
||||
productReference = 46EB331829E1CE04001D5EAF /* Locations.app */;
|
||||
@ -546,20 +514,13 @@
|
||||
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 */,
|
||||
02031F0A29E7645F003C108C /* Location+URLs.swift in Sources */,
|
||||
02031F0829E75EF0003C108C /* SaveLocalLocationUseCase.swift in Sources */,
|
||||
02031EEA29E6B495003C108C /* ErrorMessageView.swift in Sources */,
|
||||
46C3B7DC29E5ED2300F8F57C /* LocationsAddCoordination.swift in Sources */,
|
||||
46C3B7D829E5E55000F8F57C /* LocationsListCoordination.swift in Sources */,
|
||||
46DF736D29E82A1500AA6D21 /* UIApplication+Conformances.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;
|
||||
@ -786,6 +747,10 @@
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Libraries;
|
||||
};
|
||||
46EB334529E1D1F0001D5EAF /* Shared */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Shared;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 46EB325129E1BBD1001D5EAF /* Project object */;
|
||||
|
@ -1,77 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "46EB331729E1CE04001D5EAF"
|
||||
BuildableName = "Locations.app"
|
||||
BlueprintName = "Locations"
|
||||
ReferencedContainer = "container:DeepLinking.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "46EB331729E1CE04001D5EAF"
|
||||
BuildableName = "Locations.app"
|
||||
BlueprintName = "Locations"
|
||||
ReferencedContainer = "container:DeepLinking.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "46EB331729E1CE04001D5EAF"
|
||||
BuildableName = "Locations.app"
|
||||
BlueprintName = "Locations"
|
||||
ReferencedContainer = "container:DeepLinking.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
42
README.md
42
README.md
@ -1,44 +1,2 @@
|
||||
# Deep linking: Wikipedia
|
||||
|
||||
## App
|
||||
|
||||
The ultimate purpose of this application is to open the **Wikipedia** app in the right location when a user tap on one of the locations listed in the **Locations** app.
|
||||
|
||||
<center>
|
||||
<video controls>
|
||||
<source src="https://static.rock-n-code.com/mp4/deep-linking-app-demo.mp4" type="video/mp4">
|
||||
</video>
|
||||
</center>
|
||||
|
||||
Of course, to accomplish such goal the app therefore shows a list of locations (which are either fetched from a remote server or created by the user) and also, allows the user to add new location coordinates to this list by selecting them from a map.
|
||||
|
||||
## Features
|
||||
|
||||
In its current state, the **Locations** app does:
|
||||
|
||||
- [x] fetch locations from a remote server;
|
||||
- [x] handle `loading`, `loaded` and `error` states reactively when loading data;
|
||||
- [x] add locations manually to the list by obtaining locations from map;
|
||||
- [x] clean map of selected location if required;
|
||||
- [x] open the **Wikipedia** app when location is selected from a list;
|
||||
|
||||
While the **Wikipedia** app does:
|
||||
- [x] open a location in the right position on the map of *Places* screen from a deep link;
|
||||
|
||||
## Design
|
||||
|
||||
<object data="https://static.rock-n-code.com/pdf/deep-linking-app-design.pdf" type="application/pdf" width="100%" height="800px">
|
||||
<p>Unable to display a PDF file with some design considerations. Please <a href="https://static.rock-n-code.com/pdf/deep-linking-app-design.pdf">Download the file</a> instead.</p>
|
||||
</object>
|
||||
|
||||
## Implementation
|
||||
|
||||
This application was built as a `UIKit` application given that the [assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) explicitedly indicates the app should target the `iOS` platform (**iPhone** app only, in this particular case) and also, because it disallow the use of the `SwiftUI` framework. It is out of discussion that the `UIKit` framework has been battle-tested for over a decade now and [Apple](https://apple.com) keeps updating and adding features to it on a regular basis. The imperative nature of the framework, which is based in implementing how the `iOS` platform UI components should work, is perfectly suitable the developer who want absolute control over the UI, at a cost of maintaining platform-specific, more complex codebases.
|
||||
|
||||
With regards to the choice of framework to built this app, it also comes the question of the type of architecture pattern to use in it: for this particular case, and given the limitations of how the view controllers have been defined in the Apple platforms, [MVVM](https://en.wikipedia.org/wiki/Model–view–viewmodel)-C is the chosen architecrture as it facilitates the separation between logic and UI components while also, decoupling the navigation logic by using coordinates (and routers) from them.
|
||||
|
||||
Now that design patterns have been mentioned, in this exercise some well-known patterns are being used in some degree. For example, the *Singleton* pattern is used to initialise the `PersistenceService` service in the `Persistence` library. Both public and internal *Interfaces* that either describe an entity or how the entity should behave are used throughout this codebase, as this pattern is essential to create decoupled components that can be easily plugged as dependencies whenever needed as this forces the developer to think about (single) responsibilites and, as a consequence, these components are also easy to test in isolation as mocks, stubs and spies can be easily created out of them. Last, but definitely not least, the *Use cases* are a pattern from Android that basically execute a function based on some given input, and provides an output after that particular function is finished. This pattern is particularly useful to encapsulate in a simple way some certain logic from view models.
|
||||
|
||||
This application was built with scalability in terms of the codebase in mind, which tries to address how this codebase could grow in a controllable, organised manner. For this very reason, this application uses the [Swift Package Manager](https://www.swift.org/package-manager/) (or simply **SwiftPM**) to define the `Core`, `Dependency`, `Persistence` and `Remote` libraries of the `Library` package, that the **Locations** target should use extensively. These libraries focus on a specific purpose, and they can be self-contained, like in the case of the `Persistence` library that contains its own **CoreData** data model definitons and respective assets inside. Packages could use 3rd party dependencies if needed. This approach forces the developer to think about actual separation of concerns, as the different dependencies are grouped as independent, reusable building blocks, and to move the code into the SPM packages out of the main app target, reducing compiling time and overall weight of the application.
|
||||
|
||||
As a (indirect) consequence for the use of **SwiftPM** packages, the [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) pattern comes into question. I implemented my own simple **DI** mechanism that uses extensively the dynamic property wrapper functionality in the last versions of the [Swift](https://www.swift.org) language rather than using a 3rd party dependency for this case.
|
||||
|
28
Shared/Package.swift
Normal file
28
Shared/Package.swift
Normal file
@ -0,0 +1,28 @@
|
||||
// swift-tools-version: 5.8
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Shared",
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "Shared",
|
||||
targets: ["Shared"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "Shared",
|
||||
dependencies: []),
|
||||
.testTarget(
|
||||
name: "SharedTests",
|
||||
dependencies: ["Shared"]),
|
||||
]
|
||||
)
|
6
Shared/Sources/Shared/Shared.swift
Normal file
6
Shared/Sources/Shared/Shared.swift
Normal file
@ -0,0 +1,6 @@
|
||||
public struct Shared {
|
||||
public private(set) var text = "Hello, World!"
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
11
Shared/Tests/SharedTests/SharedTests.swift
Normal file
11
Shared/Tests/SharedTests/SharedTests.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import XCTest
|
||||
@testable import Shared
|
||||
|
||||
final class SharedTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||
// results.
|
||||
XCTAssertEqual(Shared().text, "Hello, World!")
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user