| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import MapKit | ||
| import SwiftUI | ||
|
|
||
| class TreeAnnotation: NSObject { | ||
| let coordinate: CLLocationCoordinate2D | ||
| let tree: Tree | ||
| let isVisited: Bool | ||
|
|
||
| init?(tree: Tree, isVisited: Bool) { | ||
| guard let coordinate = tree.coordinate else { return nil } | ||
| self.coordinate = coordinate | ||
| self.tree = tree | ||
| self.isVisited = isVisited | ||
| } | ||
| } | ||
|
|
||
| extension TreeAnnotation: MKAnnotation { | ||
| var title: String? { "Tree #\(tree.id)" } | ||
| var subtitle: String? { tree.commonName } | ||
| } | ||
|
|
||
| extension TreeAnnotation: Annotation { | ||
| var identifier: Int { tree.id } | ||
| var tintColor: UIColor { isVisited ? color.withAlphaComponent(0.5) : color } | ||
|
|
||
| private var color: UIColor { UIColor(Color.accentColor) } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| class VisitableTreeViewModel: TreeViewModel { | ||
| let isVisited: Bool | ||
| var visitedButtonImageName: String { isVisited ? "checkmark.circle.fill" : "checkmark.circle" } | ||
| var visitedButtonText: String { isVisited ? "Visited" : "Mark as visited" } | ||
|
|
||
| init(tree: Tree, isVisited: Bool) { | ||
| self.isVisited = isVisited | ||
| super.init(tree: tree) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import SwiftUI | ||
|
|
||
| struct AttributeRow: View { | ||
| let name: String | ||
| let value: String? | ||
|
|
||
| var body: some View { | ||
| if let value = value { | ||
| VStack(alignment: .leading) { | ||
| Text(name) | ||
| .foregroundColor(.mutedText) | ||
| .font(.caption) | ||
| Text(value) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct AttributeRow_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| AttributeRow(name: "Height", value: "20 meters") | ||
| .autosizedPreview() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import SwiftUI | ||
|
|
||
| struct DimensionGroup: View { | ||
| let dimensions: [Dimension] | ||
|
|
||
| var body: some View { | ||
| VStack(alignment: .leading, spacing: 16) { | ||
| ForEach(dimensions, id: \.name) { dimension in | ||
| if let value = dimension.value { | ||
| DimensionView(name: dimension.name, value: value) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct DimensionGroup_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| DimensionGroup(dimensions: [ | ||
| Dimension(name: "Height", value: "205 ft"), | ||
| Dimension(name: "Missing value", value: nil), | ||
| Dimension(name: "Spread", value: "80 ft"), | ||
| ]) | ||
| .autosizedPreview() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import SwiftUI | ||
|
|
||
| struct DimensionView: View { | ||
| let name: String | ||
| let value: String | ||
|
|
||
| var body: some View { | ||
| HStack(spacing: 16) { | ||
| Image(name) | ||
| .resizable() | ||
| .aspectRatio(contentMode: .fit) | ||
| .frame(height: 28) | ||
| .padding(10) | ||
| .foregroundColor(.white) | ||
| .background(Color.accentColor) | ||
| .cornerRadius(10) | ||
| VStack(alignment: .leading) { | ||
| Text(name) | ||
| .font(.caption) | ||
| .bold() | ||
| .foregroundColor(.mutedText) | ||
| Text(value) | ||
| .font(.title2) | ||
| .bold() | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct DimensionView_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| DimensionView(name: "Height", value: "85 ft") | ||
| .autosizedPreview() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import SwiftUI | ||
|
|
||
| struct NameView: View { | ||
| let url: URL? | ||
| let title: String | ||
| let subtitle: String | ||
|
|
||
| @State private var showingSafari = false | ||
|
|
||
| var body: some View { | ||
| if let url = url { | ||
| SafariButton(url: url) { | ||
| content | ||
| } | ||
| } else { | ||
| content | ||
| } | ||
| } | ||
|
|
||
| private var content: some View { | ||
| VStack(alignment: .leading) { | ||
| Text(title) | ||
| .font(.title) | ||
| Text(subtitle) | ||
| .font(.title3) | ||
| .foregroundColor(.mutedText) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct TreeNameView_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| NameView(url: URL.example, title: "American Elm", subtitle: "Ulmus americana") | ||
| .autosizedPreview() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import SwiftUI | ||
|
|
||
| struct SafariButton<Content: View>: View { | ||
| let content: Content | ||
| let url: URL | ||
|
|
||
| @State private var showingSafari = false | ||
|
|
||
| init(url: URL, @ViewBuilder content: () -> Content) { | ||
| self.url = url | ||
| self.content = content() | ||
| } | ||
|
|
||
| var body: some View { | ||
| Button(action: { showingSafari = true }) { | ||
| HStack { | ||
| content | ||
| Spacer() | ||
| Image(systemName: "arrow.up.forward.app") | ||
| .foregroundColor(.gray) | ||
| } | ||
| } | ||
| .sheet(isPresented: $showingSafari) { | ||
| SafariView(url: url) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct SafariButton_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| SafariButton(url: URL.example) { | ||
| Text("Show me the Safari") | ||
| } | ||
| .autosizedPreview() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import SwiftUI | ||
|
|
||
| struct ToggleVisitedTreeButton: View { | ||
| let tree: Tree | ||
|
|
||
| @EnvironmentObject private var store: TreeStore | ||
|
|
||
| private var viewModel: VisitableTreeViewModel { | ||
| VisitableTreeViewModel(tree: tree, isVisited: store.isVisited(tree: tree)) | ||
| } | ||
|
|
||
| var body: some View { | ||
| Button(action: { store.toggleTreeIsVisited(tree) }) { | ||
| Image(systemName: viewModel.visitedButtonImageName) | ||
| .resizable() | ||
| .aspectRatio(contentMode: .fit) | ||
| .frame(height: 24) | ||
| Text(viewModel.visitedButtonText) | ||
| .bold() | ||
| } | ||
| .buttonStyle(SolidButton()) | ||
| } | ||
| } | ||
|
|
||
| struct ToggleVisitedTreeButton_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| ToggleVisitedTreeButton(tree: Tree.preview) | ||
| .autosizedPreview() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,100 +1,57 @@ | ||
| import SwiftUI | ||
|
|
||
| struct TreeDetailView: View { | ||
| let tree: Tree | ||
|
|
||
| @EnvironmentObject private var store: TreeStore | ||
| private var viewModel: VisitableTreeViewModel { | ||
| VisitableTreeViewModel(tree: tree, isVisited: store.isVisited(tree: tree)) | ||
| } | ||
|
|
||
| var body: some View { | ||
| ZStack { | ||
| VStack { | ||
| MapView(tree: tree) | ||
|
|
||
| List { | ||
| NameView(url: viewModel.wikipediaURL, title: tree.commonName, subtitle: tree.scientificName) | ||
| .padding(.vertical, 8) | ||
| TreeLocationRow(tree: tree) | ||
| .padding(.vertical, 8) | ||
| TreeDimensionsView(height: viewModel.height, spread: viewModel.spread, diameter: viewModel.diameter, circumference: viewModel.circumference) | ||
| .padding(.vertical, 8) | ||
| AttributeRow(name: "Notes", value: viewModel.notes) | ||
| .padding(.vertical, 8) | ||
| AttributeRow(name: "Tree fact", value: viewModel.treeFact) | ||
| .padding(.vertical, 8) | ||
| .padding(.bottom, 64) | ||
| } | ||
| } | ||
|
|
||
| VStack { | ||
| Spacer() | ||
| ToggleVisitedTreeButton(tree: tree) | ||
| } | ||
| } | ||
| .navigationBarTitle(viewModel.uniqueName, displayMode: .inline) | ||
| } | ||
| } | ||
|
|
||
| extension TreeDetailView { | ||
| struct MapView: View { | ||
| let tree: Tree | ||
|
|
||
| var body: some View { | ||
| if let coordinate = tree.coordinate { | ||
| TreeMapDetailView(coordinate: coordinate, tree: tree) | ||
| .frame(height: 200) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct TreeDetailView_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| TreeDetailView(tree: Tree.preview) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,65 +1,32 @@ | ||
| import SwiftUI | ||
|
|
||
| struct TreeDimensionsView: View { | ||
| let height: String? | ||
| let spread: String? | ||
| let diameter: String? | ||
| let circumference: String? | ||
|
|
||
| var body: some View { | ||
| HStack(spacing: 16) { | ||
| DimensionGroup(dimensions: [ | ||
| Dimension(name: "Height", value: height), | ||
| Dimension(name: "Spread", value: spread) | ||
| ]) | ||
|
|
||
| Spacer() | ||
|
|
||
| DimensionGroup(dimensions: [ | ||
| Dimension(name: "Diameter", value: diameter), | ||
| Dimension(name: "Circumference", value: circumference) | ||
| ]) | ||
| } | ||
| .padding(.trailing) | ||
| } | ||
| } | ||
|
|
||
| struct TreeDimensionsView_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| TreeDimensionsView(height: "85 ft", spread: "107 ft", diameter: "48 in", circumference: "12.8 in") | ||
| .autosizedPreview() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,21 @@ | ||
| import SwiftUI | ||
|
|
||
| struct TreeListView: View { | ||
| @EnvironmentObject private var store: TreeStore | ||
|
|
||
| var body: some View { | ||
| List(store.trees) { tree in | ||
| NavigationLink(destination: TreeDetailView(tree: tree)) { | ||
| TreeListItem(tree: tree) | ||
| } | ||
| } | ||
| .listStyle(PlainListStyle()) | ||
| } | ||
| } | ||
|
|
||
| struct TreeListView_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| TreeListView() | ||
| .environmentObject(Environment.local.store) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import MapKit | ||
| import SwiftUI | ||
|
|
||
| struct TreeLocationRow: View { | ||
| let tree: Tree | ||
|
|
||
| private var viewModel: TreeViewModel { TreeViewModel(tree: tree) } | ||
|
|
||
| var body: some View { | ||
| if let coordinate = tree.coordinate { | ||
| NavigationLink(destination: mapView(coordinate: coordinate)) { | ||
| content | ||
| } | ||
| } else { | ||
| content | ||
| } | ||
| } | ||
|
|
||
| private var content: some View { | ||
| VStack(alignment: .leading) { | ||
| if let address = viewModel.address { | ||
| Text(address) | ||
| } | ||
| if let neighborhood = viewModel.neighborhood { | ||
| Text(neighborhood) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func mapView(coordinate: CLLocationCoordinate2D) -> some View { | ||
| TreeMapDetailView(coordinate: coordinate, tree: tree) | ||
| .navigationTitle(viewModel.address ?? "") | ||
| } | ||
| } | ||
|
|
||
| struct TreeLocationRow_Previews: PreviewProvider { | ||
| static var previews: some View { | ||
| TreeLocationRow(tree: Tree.preview) | ||
| .autosizedPreview() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import MapKit | ||
| import SwiftUI | ||
|
|
||
| struct TreeMapDetailView: View { | ||
| let coordinate: CLLocationCoordinate2D | ||
| let tree: Tree | ||
|
|
||
| private let locationManager = LocationManager() | ||
|
|
||
| @State private var region = MKCoordinateRegion( | ||
| center: CLLocationCoordinate2D.northWestNeighborhood, | ||
| span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005) | ||
| ) | ||
|
|
||
| var body: some View { | ||
| Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, userTrackingMode: nil, annotationItems: [tree]) { _ in | ||
| MapPin(coordinate: coordinate, tint: .accentColor) | ||
| } | ||
| .ignoresSafeArea(.all) | ||
| .onAppear { | ||
| region.center = coordinate | ||
| locationManager.rerequestAuthorization() | ||
| } | ||
| } | ||
| } |