Showing with 726 additions and 519 deletions.
  1. +19 −0 App/Extensions/OpenDataTree+Tree.swift
  2. +12 −0 App/Extensions/Tree+Coordinate.swift
  3. +5 −0 App/Extensions/URL+Example.swift
  4. +8 −0 App/Extensions/View+Preview.swift
  5. +4 −0 App/Models/Dimension.swift
  6. +7 −1 App/Models/Environment.swift
  7. +28 −0 App/Models/MapView.Coordinator.swift
  8. +43 −0 App/Models/OpenDataTree.swift
  9. +9 −11 App/Models/Persistence.swift
  10. +27 −73 App/Models/Tree.swift
  11. +0 −26 App/Models/TreeAnnotation.swift
  12. +1 −1 App/Models/TreeListAPIResponse.swift
  13. +55 −0 App/Models/TreeStore.swift
  14. +6 −0 App/Protocols/Annotation.swift
  15. +2 −2 App/Services/OpenDataService.swift
  16. +1 −2 App/Styles/SolidButton.swift
  17. +0 −5 App/View Models/Annotation.swift
  18. +0 −11 App/View Models/MappableHeritageTreeViewModel.swift
  19. +27 −0 App/View Models/TreeAnnotation.swift
  20. +0 −36 App/View Models/TreeListViewModel.swift
  21. +11 −22 App/View Models/TreeViewModel.swift
  22. +10 −0 App/View Models/VisitableTreeViewModel.swift
  23. +4 −1 App/Views/App.swift
  24. +24 −0 App/Views/AttributeRow.swift
  25. +26 −0 App/Views/DimensionGroup.swift
  26. +35 −0 App/Views/DimensionView.swift
  27. +7 −14 App/Views/HomeView.swift
  28. +21 −29 App/Views/MapView.swift
  29. +36 −0 App/Views/NameView.swift
  30. +36 −0 App/Views/SafariButton.swift
  31. +30 −0 App/Views/ToggleVisitedTreeButton.swift
  32. +21 −64 App/Views/TreeDetailView.swift
  33. +14 −47 App/Views/TreeDimensionsView.swift
  34. +9 −17 App/Views/TreeListItem.swift
  35. +8 −7 App/Views/TreeListView.swift
  36. +41 −0 App/Views/TreeLocationRow.swift
  37. +0 −12 App/Views/TreeMapAnnotationContent.swift
  38. +25 −0 App/Views/TreeMapDetailView.swift
  39. +0 −45 App/Views/TreeMapListView.swift
  40. +17 −17 App/Views/TreeMapView.swift
  41. +0 −43 App/Views/TreeNameView.swift
  42. +97 −33 portland-heritage-trees.xcodeproj/project.pbxproj
@@ -0,0 +1,19 @@
extension OpenDataTree {
var tree: Tree {
Tree(
address: properties.siteAddress,
circumference: properties.circumf,
commonName: properties.common,
diameter: properties.diameter,
fact: properties.treeFactLong,
height: properties.height,
id: properties.treeID,
latitude: properties.lat,
longitude: properties.lon,
neighborhood: properties.neighborhood,
notes: properties.notes,
scientificName: properties.scientific,
spread: properties.spread
)
}
}
@@ -0,0 +1,12 @@
import MapKit

extension Tree {
var coordinate: CLLocationCoordinate2D? {
guard
let latitude = latitude,
let longitude = longitude
else { return nil }

return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
}
@@ -0,0 +1,5 @@
import Foundation

extension URL {
static var example: Self { Self(string: "https://example.com")! }
}
@@ -0,0 +1,8 @@
import SwiftUI

extension View {
func autosizedPreview() -> some View {
previewLayout(PreviewLayout.sizeThatFits)
.padding()
}
}
@@ -0,0 +1,4 @@
struct Dimension {
let name: String
let value: String?
}
@@ -3,7 +3,7 @@ enum Environment {
}

extension Environment {
var apiSession: APIService {
var apiService: APIService {
switch self {
case .local:
return LocalSession()
@@ -12,3 +12,9 @@ extension Environment {
}
}
}

extension Environment {
var store: TreeStore {
TreeStore(apiService: apiService)
}
}
@@ -0,0 +1,28 @@
import MapKit

public extension MapView {
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView

init(_ parent: MapView) {
self.parent = parent
}

public func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
if let annotation = view.annotation as? Annotation {
parent.didSelectAnnotation?(annotation)
}
}

public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let mkAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: AnnotationViewIdentifier, for: annotation)
guard
let annotationView = mkAnnotationView as? PinAnnotationView,
let annotation = annotation as? Annotation
else { return nil }

annotationView.pinTintColor = annotation.tintColor
return annotationView
}
}
}
@@ -0,0 +1,43 @@
struct OpenDataTree: Codable {
let properties: Properties

enum CodingKeys: String, CodingKey {
case properties
}
}

extension OpenDataTree {
struct Properties: Codable {
let treeID: Int
let scientific: String
let common: String
let siteAddress: String?
let height: Int
let spread: Int?
let circumf: Float?
let diameter: Int?
let yearDesignated: Int?
let notes: String?
let lat: Double?
let lon: Double?
let neighborhood: String?
let treeFactLong: String?

enum CodingKeys: String, CodingKey {
case treeID = "TREEID"
case common = "COMMON"
case scientific = "SCIENTIFIC"
case siteAddress = "SITE_ADDRESS"
case height = "HEIGHT"
case spread = "SPREAD"
case circumf = "CIRCUMF"
case diameter = "DIAMETER"
case yearDesignated = "YEAR_Designated"
case notes = "NOTES"
case lat = "LAT"
case lon = "LON"
case neighborhood = "Neighborhood"
case treeFactLong = "Tree_fact_long"
}
}
}
@@ -1,17 +1,15 @@
import Foundation

struct Persistence {
static func isTreeVisited(_ tree: Tree) -> Bool {
let key = userDefaultsKey(for: tree)
return UserDefaults.standard.bool(forKey: key)
static var isVisitedStatuses: [Int: Bool] {
get {
UserDefaults.standard.data(forKey: "trees.visited")
.flatMap { try? JSONDecoder().decode([Int: Bool].self, from: $0) } ?? [:]
}
set {
UserDefaults.standard.set(try? JSONEncoder().encode(newValue), forKey: isVisitedStatusesKey)
}
}

static func setTree(_ tree: Tree, visited: Bool) {
let key = userDefaultsKey(for: tree)
UserDefaults.standard.set(visited, forKey: key)
}

private static func userDefaultsKey(for tree: Tree) -> String {
"tree.visited.\(tree.properties.treeID)"
}
private static let isVisitedStatusesKey = "trees.visited"
}
@@ -1,85 +1,39 @@
import Foundation

struct Tree: Codable, Identifiable {
let id = UUID()
let properties: Properties

enum CodingKeys: String, CodingKey {
case properties
}

struct Properties: Codable {
let treeID: Int
let scientific: String
let common: String
let siteAddress: String?
let height: Int
let spread: Int?
let circumf: Float?
let diameter: Int?
let yearDesignated: Int?
let notes: String?
let lat: Double?
let lon: Double?
let neighborhood: String?
let treeFactLong: String?

enum CodingKeys: String, CodingKey {
case treeID = "TREEID"
case common = "COMMON"
case scientific = "SCIENTIFIC"
case siteAddress = "SITE_ADDRESS"
case height = "HEIGHT"
case spread = "SPREAD"
case circumf = "CIRCUMF"
case diameter = "DIAMETER"
case yearDesignated = "YEAR_Designated"
case notes = "NOTES"
case lat = "LAT"
case lon = "LON"
case neighborhood = "Neighborhood"
case treeFactLong = "Tree_fact_long"
}
}
struct Tree: Identifiable {
let address: String?
let circumference: Float?
let commonName: String
let diameter: Int?
let fact: String?
let height: Int
let id: Int
let latitude: Double?
let longitude: Double?
let neighborhood: String?
let notes: String?
let scientificName: String
let spread: Int?
}

// MARK: Preview Content
extension Tree {
static var preview: Self {
Self(properties: Properties(
treeID: 1,
scientific: "Ulmus americana",
common: "American elm", siteAddress: "1111 SW 10th AVE",
height: 85,
spread: 107,
circumf: 12.8,
Self(
address: "1111 SW 10th AVE",
circumference: 12.8,
commonName: "American elm",
diameter: 48,
yearDesignated: 1973,
notes: "This tree was planted in front of the home of Martin and Rosetta Burrell in 1870 and is thus known as the 'Burrell Elm'.",
lat: 45.51672923,
lon: -122.68401382,
fact: "The Burrell Elm is the second historic tree recognized by the City of Portland.",
height: 85,
id: 1,
latitude: 45.51672923,
longitude: -122.68401382,
neighborhood: "DOWNTOWN",
treeFactLong: "The Burrell Elm is the second historic tree recognized by the City of Portland. It is an American Elm tree that was planted in 1870 at the home of Martin and Rosetta Burrell, in what was their private garden at the time. As Portland grew, the character of the neighborhood changed from private single-family homes to denser development. This elm tree is now behind the sidewalk in front of the YWCA in downtown Portland."
))
}
}

extension Tree.Properties {
init(treeID: Int, scientific: String, common: String, height: Int) {
self.treeID = treeID
self.scientific = scientific
self.common = common
self.siteAddress = nil
self.height = height
self.spread = nil
self.circumf = nil
self.diameter = nil
self.yearDesignated = nil
self.notes = nil
self.lat = nil
self.lon = nil
self.neighborhood = nil
self.treeFactLong = nil
notes: "This tree was planted in front of the home of Martin and Rosetta Burrell in 1870 and is thus known as the 'Burrell Elm'.",
scientificName: "Ulmus americana",
spread: 107
)
}
}

This file was deleted.

@@ -1,3 +1,3 @@
struct TreeListAPIResponse: Codable {
let features: [Tree]
let features: [OpenDataTree]
}
@@ -0,0 +1,55 @@
import Combine

class TreeStore: ObservableObject, OpenDataService {
@Published private(set) var treeAnnotations = [TreeAnnotation]()
let apiService: APIService

@Published private(set) var isVisitedStatuses: [Int: Bool] {
didSet { setTreeAnnotations() }
}

@Published private(set) var trees = [Tree]() {
didSet { setTreeAnnotations() }
}

private var cancellables = Set<AnyCancellable>()

init(apiService: APIService) {
self.apiService = apiService
self.isVisitedStatuses = Persistence.isVisitedStatuses
}

func getTrees() {
getTrees()
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("Handle error:", error)
case .finished:
break
}
}) { result in
self.trees = result.features
.map { $0.tree }
}
.store(in: &cancellables)
}

func isVisited(tree: Tree) -> Bool {
isVisitedStatuses[tree.id] ?? false
}

func toggleTreeIsVisited(_ tree: Tree) {
var statuses = isVisitedStatuses
statuses[tree.id] = !(statuses[tree.id] ?? false)
isVisitedStatuses = statuses
Persistence.isVisitedStatuses = isVisitedStatuses
}

private func setTreeAnnotations() {
treeAnnotations = trees.compactMap { tree in
let isVisited = isVisitedStatuses[tree.id] ?? false
return TreeAnnotation(tree: tree, isVisited: isVisited)
}
}
}
@@ -0,0 +1,6 @@
import MapKit

public protocol Annotation: MKAnnotation {
var identifier: Int { get }
var tintColor: UIColor { get }
}
@@ -1,13 +1,13 @@
import Combine

protocol OpenDataService {
var apiSession: APIService { get }
var apiService: APIService { get }
func getTrees() -> AnyPublisher<TreeListAPIResponse, APIError>
}

extension OpenDataService {
func getTrees() -> AnyPublisher<TreeListAPIResponse, APIError> {
apiSession.request(with: OpenDataEndpoint.heritageTrees)
apiService.request(with: OpenDataEndpoint.heritageTrees)
.eraseToAnyPublisher()
}
}