Skip to content

Commit 0e9f5e4

Browse files
authored
Add site functionality and settings tab (#57)
1 parent 3ae7a97 commit 0e9f5e4

18 files changed

+501
-20
lines changed

Tree Tracker.xcodeproj/project.pbxproj

Lines changed: 69 additions & 12 deletions
Large diffs are not rendered by default.

Tree Tracker/Extensions/UIImage.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ extension UIImage {
2323
static var listIcon: UIImage { UIImage(systemName: "list.bullet")! }
2424
static var cameraIcon: UIImage { UIImage(systemName: "camera")! }
2525
static var map: UIImage { UIImage(systemName: "map")! }
26+
static var settingsIcon: UIImage { UIImage(systemName: "gearshape")! }
2627
}

Tree Tracker/Navigation/MainFlowViewController.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ final class MainFlowViewController: UITabBarController {
88
let liveUpload = UploadSessionViewController(viewModel: UploadSessionViewModel(navigation: self))
99
let uploadQueue = UploadListFlowViewController()
1010
let uploadHistory = NavigationViewController(rootViewController: CollectionViewController(viewModel: UploadHistoryViewModel()))
11-
let entities = NavigationViewController(rootViewController: TableViewController(viewModel: EntitiesViewModel()))
11+
let settings = SettingsNavigationController()
1212

13-
viewControllers = [liveUpload, uploadQueue, uploadHistory, entities]
13+
viewControllers = [liveUpload, uploadQueue, uploadHistory, settings]
1414

1515
liveUpload.tabBarItem = UITabBarItem(title: "Session", image: .cameraIcon, selectedImage: .cameraIcon)
1616
uploadQueue.tabBarItem = UITabBarItem(title: "Queue", image: .uploadIcon, selectedImage: .uploadIcon)
1717
uploadHistory.tabBarItem = UITabBarItem(title: "History", image: .historyIcon, selectedImage: .historyIcon)
18-
entities.tabBarItem = UITabBarItem(title: "Entities", image: .listIcon, selectedImage: .listIcon)
18+
settings.tabBarItem = UITabBarItem(title: "Settings", image: .settingsIcon, selectedImage: .settingsIcon)
1919

20+
// Open on upload queue by default
2021
selectedIndex = 1
2122
}
2223
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
import UIKit
3+
4+
/*
5+
Navigation controller for Settings - acts as a container for child view controllers
6+
*/
7+
class SettingsNavigationController: UINavigationController {
8+
9+
init() {
10+
super.init(nibName: nil, bundle: nil)
11+
12+
let top = SettingsController(style: UITableView.Style.grouped)
13+
14+
self.viewControllers = [top]
15+
16+
self.title = "Settings"
17+
}
18+
19+
// boilerplate
20+
required init?(coder aDecoder: NSCoder) {
21+
fatalError("init(coder:) has not been implemented")
22+
}
23+
24+
}

Tree Tracker/Screens/Entities/EntitiesViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ final class EntitiesViewModel: TableViewModel {
6464
}
6565
}
6666

67-
private func sync() {
67+
func sync() {
6868
fetchAndReplaceAllSitesFromRemote()
6969
fetchAndReplaceAllSpeciesFromRemote()
7070
fetchAndReplaceAllSupervisorsFromRemote()
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import Foundation
2+
import UIKit
3+
4+
/*
5+
Controller for sheet view used to supply and save a new site
6+
*/
7+
class AddSiteController: UIViewController, UITextFieldDelegate {
8+
9+
private let api = CurrentEnvironment.api
10+
private var model: EntitiesViewModel
11+
12+
init(entitiesViewModel: EntitiesViewModel) {
13+
self.model = entitiesViewModel
14+
super.init(nibName: nil, bundle: nil)
15+
}
16+
17+
required init?(coder: NSCoder) {
18+
fatalError("init(coder:) has not been implemented")
19+
}
20+
21+
override func loadView() {
22+
view = UIView()
23+
view.backgroundColor = .systemBackground
24+
25+
// text field
26+
view.addSubview(stackView)
27+
stackView.addArrangedSubview(textField)
28+
textField.placeholder = "Enter site name"
29+
30+
// save button
31+
let buttonModel = ButtonModel(title: ButtonModel.Title.text("Save"), action: {self.doSave()}, isEnabled: true)
32+
actionButton.set(model: buttonModel)
33+
stackView.addArrangedSubview(actionButton)
34+
35+
// layout
36+
NSLayoutConstraint.activate([
37+
textField.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
38+
39+
actionButton.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -10.0),
40+
actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
41+
actionButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5)
42+
])
43+
}
44+
45+
// MARK: - UI controls
46+
private let stackView: UIStackView = {
47+
let stackView = UIStackView()
48+
stackView.translatesAutoresizingMaskIntoConstraints = false
49+
stackView.axis = .vertical
50+
stackView.alignment = .center
51+
stackView.spacing = 16.0
52+
stackView.setContentCompressionResistancePriority(.required, for: .horizontal)
53+
stackView.setContentCompressionResistancePriority(.required, for: .vertical)
54+
55+
return stackView
56+
}()
57+
58+
private let actionButton: TappableButton = {
59+
let button = RoundedTappableButton(type: .system)
60+
button.translatesAutoresizingMaskIntoConstraints = false
61+
button.titleLabel?.font = .systemFont(ofSize: 18.0)
62+
return button
63+
}()
64+
65+
private let textField: TextField = {
66+
let textField = TextField()
67+
textField.textColor = .label
68+
textField.borderStyle = .roundedRect
69+
textField.translatesAutoresizingMaskIntoConstraints = false
70+
textField.heightAnchor.constraint(equalToConstant: 48.0).isActive = true
71+
return textField
72+
}()
73+
74+
// MARK: - Delegate
75+
private func doSave() -> Void? {
76+
if(textField.hasText) {
77+
// set action button to spinner / working
78+
actionButton.set(title: .loading)
79+
80+
// save new site via API
81+
api.addSite(name: textField.text!, completion: { result in
82+
// trigger refresh of EntitiesViewModel which will also resync local database to cloud table
83+
self.model.sync()
84+
85+
// dismiss the view
86+
self.dismiss(animated: true)
87+
})
88+
}
89+
// just ignore the tap if there is no text in the text box - tap outside sheet to dismiss
90+
return ()
91+
}
92+
93+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Foundation
2+
import UIKit
3+
import Combine
4+
5+
/*
6+
Top level Settings controller
7+
*/
8+
class SettingsController: UITableViewController {
9+
10+
private var entityTypes = ["Sites", "Supervisors", "Species"]
11+
12+
override func viewDidLoad() {
13+
super.viewDidLoad()
14+
15+
self.title = "Settings"
16+
self.tableView.register(SimpleTableViewCell.self, forCellReuseIdentifier: "basicStyle")
17+
}
18+
19+
// MARK: - Datasource
20+
override func numberOfSections(in tableView: UITableView) -> Int {
21+
return 1
22+
}
23+
24+
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
25+
return "Entities"
26+
}
27+
28+
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
29+
return entityTypes.count
30+
}
31+
32+
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
33+
let cell = tableView.dequeueReusableCell(withIdentifier: "basicStyle", for: indexPath)
34+
cell.accessoryType = .disclosureIndicator
35+
cell.textLabel?.text = entityTypes[indexPath.item]
36+
return cell
37+
}
38+
39+
// MARK: - Delegate
40+
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
41+
switch entityTypes[indexPath.item] {
42+
case "Sites":
43+
self.navigationController?.pushViewController(SitesController(), animated: true)
44+
case "Supervisors":
45+
self.navigationController?.pushViewController(SupervisorsController(), animated: true)
46+
case "Species":
47+
self.navigationController?.pushViewController(SpeciesController(), animated: true)
48+
default:
49+
break
50+
}
51+
}
52+
53+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import Foundation
2+
import UIKit
3+
import Combine
4+
5+
/*
6+
Controller for sites list
7+
*/
8+
class SitesController: UITableViewController {
9+
10+
private let database = CurrentEnvironment.database
11+
private var entitiesModel: EntitiesViewModel = EntitiesViewModel()
12+
13+
private var sites: [Site] = []
14+
private var cancellable: AnyCancellable!
15+
16+
override func viewDidLoad() {
17+
super.viewDidLoad()
18+
19+
self.title = "Sites"
20+
21+
self.tableView.register(SimpleTableViewCell.self, forCellReuseIdentifier: "basicStyle")
22+
23+
// nav bar controls
24+
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addTapped))
25+
navigationItem.rightBarButtonItems?.append(UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refreshTapped)))
26+
27+
// here we are creating a Combine subscription to a @Published attribute of the entity view model which is handling data access
28+
// the closure will be invoked on any change to the data property, which is itself refreshed via the onAppear method called
29+
// in this controllers viewWillAppear() handler
30+
cancellable = entitiesModel.$data.sink() { [weak self] data in
31+
// refresh local sites array from database
32+
self?.database.fetchAll(Site.self, completion: { [weak self] sites in
33+
self?.sites = sites.sorted(by: \.name, order: .ascending)
34+
// reload table view
35+
self?.tableView.reloadData()
36+
})
37+
38+
}
39+
}
40+
41+
// MARK: - navigation item delegates
42+
@objc func addTapped() {
43+
let addSiteController = AddSiteController(entitiesViewModel: entitiesModel)
44+
if let sheet = addSiteController.sheetPresentationController {
45+
sheet.detents = [ .medium() ]
46+
}
47+
present(addSiteController, animated: true)
48+
}
49+
50+
@objc func refreshTapped() {
51+
entitiesModel.sync()
52+
}
53+
54+
// MARK: - Delegate
55+
56+
override func viewWillAppear(_ animated: Bool) {
57+
entitiesModel.onAppear()
58+
}
59+
60+
// MARK: - Datasource
61+
override func numberOfSections(in tableView: UITableView) -> Int {
62+
return 1
63+
}
64+
65+
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
66+
return sites.count
67+
}
68+
69+
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
70+
let cell = tableView.dequeueReusableCell(withIdentifier: "basicStyle", for: indexPath)
71+
72+
cell.textLabel?.text = sites[indexPath.item].name
73+
cell.isUserInteractionEnabled = false
74+
75+
return cell
76+
}
77+
78+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Foundation
2+
import UIKit
3+
import Combine
4+
5+
/*
6+
Controller for species list
7+
*/
8+
class SpeciesController: UITableViewController {
9+
10+
private let database = CurrentEnvironment.database
11+
private var entitiesModel: EntitiesViewModel = EntitiesViewModel()
12+
13+
private var species: [Species] = []
14+
private var cancellable: AnyCancellable!
15+
16+
override func viewDidLoad() {
17+
super.viewDidLoad()
18+
19+
self.title = "Species"
20+
21+
self.tableView.register(SimpleTableViewCell.self, forCellReuseIdentifier: "basicStyle")
22+
23+
// nav bar controls
24+
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refreshTapped))
25+
26+
// here we are creating a Combine subscription to a @Published attribute of the entity view model which is handling data access
27+
// the closure will be invoked on any change to the data property, which is itself refreshed via the onAppear method called
28+
// in this controllers viewWillAppear() handler
29+
cancellable = entitiesModel.$data.sink() { [weak self] data in
30+
// refresh local sites array from database
31+
self?.database.fetchAll(Species.self, completion: { [weak self] species in
32+
self?.species = species.sorted(by: \.name, order: .ascending)
33+
// reload table view
34+
self?.tableView.reloadData()
35+
})
36+
37+
}
38+
}
39+
40+
// MARK: - navigation item delegates
41+
@objc func refreshTapped() {
42+
entitiesModel.sync()
43+
}
44+
45+
// MARK: - Delegate
46+
override func viewWillAppear(_ animated: Bool) {
47+
entitiesModel.onAppear()
48+
}
49+
50+
// MARK: - Datasource
51+
override func numberOfSections(in tableView: UITableView) -> Int {
52+
return 1
53+
}
54+
55+
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
56+
return species.count
57+
}
58+
59+
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
60+
let cell = tableView.dequeueReusableCell(withIdentifier: "basicStyle", for: indexPath)
61+
62+
cell.textLabel?.text = species[indexPath.item].name
63+
cell.isUserInteractionEnabled = false
64+
65+
return cell
66+
}
67+
68+
}

0 commit comments

Comments
 (0)