Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix #12] Added pagination to watchlist, alarm- and device-list. #13

Merged
merged 10 commits into from
Dec 19, 2024
Merged
2 changes: 1 addition & 1 deletion ios/AlarmApp/AlarmApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@
};
30FBB9A227CCEEEC008302D0 /* XCRemoteSwiftPackageReference "cumulocity-clients-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SoftwareAG/cumulocity-clients-swift";
repositoryURL = "https://github.com/Cumulocity-IoT/cumulocity-clients-swift";
requirement = {
branch = main;
kind = branch;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"originHash" : "894cebe1937caf51d64a44da982a158d9b8254b0fed0cc3b475be2e020af0837",
"pins" : [
{
"identity" : "cumulocity-clients-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Cumulocity-IoT/cumulocity-clients-swift",
"location" : "https://github.com/cumulocity-iot/cumulocity-clients-swift",
"state" : {
"branch" : "main",
"revision" : "fd6203c62a8f7ca6b04fdde22f8c472359e1c3ae"
Expand Down Expand Up @@ -37,5 +38,5 @@
}
}
],
"version" : 2
"version" : 3
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class AlarmFilterViewController: UIViewController {

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
delegate?.fetchAlarms()
delegate?.reload()
}

override func viewDidLoad() {
Expand Down
111 changes: 87 additions & 24 deletions ios/AlarmApp/AlarmApp/Controller/AlarmListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,41 @@ import Combine
import CumulocityCoreLibrary
import UIKit

class AlarmListViewController: UITableViewController, AlarmListReloadDelegate, EmptyAlarmsDelegate {
private var data = C8yAlarmCollection()
class AlarmListViewController: UITableViewController, AlarmListReloadDelegate, EmptyAlarmsDelegate,
UITableViewDataSourcePrefetching
{
private var viewModel = PaginatedViewModel()
private var selectedAlarm: C8yAlarm?
private var cancellableSet = Set<AnyCancellable>()
private var resolvedDeviceId: String?
let filter = AlarmFilter()

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.title = %"alarms_title"
self.tableView.prefetchDataSource = self
UITableViewController.prepareForAlarms(with: self.tableView, delegate: self)
AlarmFilterTableHeader.register(for: self.tableView)

// Refresh control
self.tableView.refreshControl = UIRefreshControl()
self.tableView.refreshControl?.addTarget(self, action: #selector(onPullToRefresh), for: .valueChanged)
self.fetchAlarms()
self.reload()
}

@objc
private func onPullToRefresh() {
self.fetchAlarms()
self.reload()
}

func fetchAlarms() {
func reload() {
// filter is modified so we remove everything cached and load again
self.resolvedDeviceId = nil
self.viewModel = PaginatedViewModel()
fetchDeviceNameAndAlarms()
}

private func fetchDeviceNameAndAlarms() {
// we want the table view header to resize correctly
self.tableView.reloadData()
if let deviceName = filter.deviceName {
Expand All @@ -53,44 +64,67 @@ class AlarmListViewController: UITableViewController, AlarmListReloadDelegate, E
receiveCompletion: { completion in
let error = try? completion.error()
if error != nil {
self.data = C8yAlarmCollection()
self.tableView.reloadData()
self.tableView.endRefreshing()
}
},
receiveValue: { collection in
if collection.managedObjects?.count ?? 0 > 0 {
self.fetchAlarms(byFilter: self.filter, byDeviceId: collection.managedObjects?[0].id)
self.resolvedDeviceId = collection.managedObjects?[0].id
self.fetchNextAlarms()
} else {
// could not find any device
self.data = C8yAlarmCollection()
self.tableView.reloadData()
self.tableView.endRefreshing()
}
}
)
.store(in: &self.cancellableSet)
} else {
self.fetchAlarms(byFilter: self.filter, byDeviceId: nil)
self.fetchNextAlarms()
}
}

private func fetchNextAlarms() {
fetchAlarms(byFilter: self.filter, byDeviceId: self.resolvedDeviceId)
}

private func fetchAlarms(byFilter filter: AlarmFilter, byDeviceId deviceId: String?) {
guard viewModel.shouldLoadMorePages() else {
return
}
let alarmsApi = Cumulocity.Core.shared.alarms.alarmsApi
let publisher = alarmsApi.getAlarmsByFilter(filter: filter, source: deviceId)
let publisher = alarmsApi.getAlarmsByFilter(filter: filter, page: self.viewModel.nextPage(), source: deviceId)
publisher.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in
self.tableView.reloadData()
self.tableView.endRefreshing()
},
receiveValue: { collection in
self.data = collection
let currentPage = collection.statistics?.currentPage ?? 1
self.viewModel.pageStatistics = collection.statistics
self.viewModel.appendAlarms(toPage: currentPage, newAlarms: collection.alarms ?? [])
if currentPage > 1 {
let indexPathsToReload = self.viewModel.calculateIndexPathsToReload(
from: collection.alarms ?? []
)
self.onFetchAlarmsCompleted(with: indexPathsToReload)
} else {
self.onFetchAlarmsCompleted(with: .none)
}
}
)
.store(in: &self.cancellableSet)
}

private func onFetchAlarmsCompleted(with newIndexPathsToReload: [IndexPath]?) {
guard let newIndexPathsToReload = newIndexPathsToReload else {
self.tableView.reloadData()
return
}
let indexPathsToReload = visibleIndexPathsToReload(intersecting: newIndexPathsToReload)
tableView.reloadRows(at: indexPathsToReload, with: .automatic)
}

// MARK: - Actions

@IBAction func onFilterTapped(_ sender: Any) {
Expand All @@ -110,7 +144,7 @@ class AlarmListViewController: UITableViewController, AlarmListReloadDelegate, E
_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
) -> UISwipeActionsConfiguration? {
let alarm = data.alarms?[indexPath.item]
let alarm = self.viewModel.alarm(at: indexPath.item)
var actions: [UIContextualAction] = []
let allAlarmStatus = [C8yAlarm.C8yStatus.active, C8yAlarm.C8yStatus.cleared, C8yAlarm.C8yStatus.acknowledged]

Expand All @@ -119,7 +153,7 @@ class AlarmListViewController: UITableViewController, AlarmListReloadDelegate, E
style: .destructive,
title: status.verb()
) { [weak self] _, _, completionHandler in
self?.changeAlarmStatus(for: alarm, toStatus: status)
self?.changeAlarmStatus(for: alarm, toStatus: status, indexPath: indexPath)
completionHandler(true)
}
action.backgroundColor = status.tint()
Expand All @@ -128,7 +162,7 @@ class AlarmListViewController: UITableViewController, AlarmListReloadDelegate, E
return UISwipeActionsConfiguration(actions: actions)
}

private func changeAlarmStatus(for alarm: C8yAlarm?, toStatus status: C8yAlarm.C8yStatus) {
private func changeAlarmStatus(for alarm: C8yAlarm?, toStatus status: C8yAlarm.C8yStatus, indexPath: IndexPath) {
if let id = alarm?.id {
var alarm = C8yAlarm()
alarm.status = status
Expand All @@ -139,7 +173,9 @@ class AlarmListViewController: UITableViewController, AlarmListReloadDelegate, E
receiveCompletion: { _ in
},
receiveValue: { _ in
self.fetchAlarms()
// updating a specific alarm could lead to issues with the overall number of elements
// e.g. Filter shows only active => you set one alarm from active to clear, list has les elements!
self.reload()
}
)
.store(in: &self.cancellableSet)
Expand All @@ -148,38 +184,56 @@ class AlarmListViewController: UITableViewController, AlarmListReloadDelegate, E

// MARK: - Table view data source

override func numberOfSections(in tableView: UITableView) -> Int {
let hasAlarms = viewModel.currentCount > 0
tableView.backgroundView?.isHidden = hasAlarms
return hasAlarms ? 1 : 0
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let alarmCount = data.alarms?.count ?? 0
tableView.backgroundView?.isHidden = alarmCount > 0
return alarmCount
self.viewModel.totalCount
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(
withIdentifier: AlarmListItem.identifier,
for: indexPath
) as? AlarmListItem {
cell.bind(with: data.alarms?[indexPath.item])
if self.viewModel.isLoadingCell(for: indexPath) {
cell.bind(with: .none)
} else {
cell.bind(with: self.viewModel.alarm(at: indexPath.item))
}
return cell
}
fatalError("Could not create AlarmListItem")
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
self.selectedAlarm = data.alarms?[indexPath.item]
self.selectedAlarm = self.viewModel.alarm(at: indexPath.item)
performSegue(withIdentifier: UIStoryboardSegue.toAlarmDetails, sender: self)
}

override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: AlarmFilterTableHeader.identifier) as? AlarmFilterTableHeader else {
guard
let headerView = tableView.dequeueReusableHeaderFooterView(
withIdentifier: AlarmFilterTableHeader.identifier
) as? AlarmFilterTableHeader
else {
fatalError("Could not create AlarmFilterTableHeader")
}
headerView.alarmFilter = filter
headerView.setBackgroundConfiguration(with: .background)
return headerView
}

func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
if indexPaths.contains(where: self.viewModel.isLoadingCell) {
fetchNextAlarms()
}
}

// MARK: - Navigation

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
Expand All @@ -191,6 +245,15 @@ class AlarmListViewController: UITableViewController, AlarmListReloadDelegate, E
}
}

extension AlarmListViewController {
/// alculates the cells of the table view that need to reload when a new page is received
fileprivate func visibleIndexPathsToReload(intersecting indexPaths: [IndexPath]) -> [IndexPath] {
let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? []
let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths)
return Array(indexPathsIntersection)
}
}

protocol AlarmListReloadDelegate: AnyObject {
func fetchAlarms()
func reload()
}
73 changes: 22 additions & 51 deletions ios/AlarmApp/AlarmApp/Controller/DashboardViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,62 +64,33 @@ class DashboardViewController: UIViewController, AlarmListReloadDelegate, EmptyA

private func fetchAlarmCount() {
let alarmsApi = Cumulocity.Core.shared.alarms.alarmsApi
alarmsApi.getNumberOfAlarms(
severity: [C8yAlarm.C8ySeverity.critical.rawValue],
status: [C8yAlarm.C8yStatus.active.rawValue]
)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in
},
receiveValue: { value in
self.criticalCountItem.countLabel.text = String(value)
}
)
.store(in: &self.cancellableSet)
alarmsApi.getNumberOfAlarms(
severity: [C8yAlarm.C8ySeverity.major.rawValue],
status: [C8yAlarm.C8yStatus.active.rawValue]
)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in
},
receiveValue: { value in
self.majorCountItem.countLabel.text = String(value)
}
)
.store(in: &self.cancellableSet)
alarmsApi.getNumberOfAlarms(
severity: [C8yAlarm.C8ySeverity.minor.rawValue],
status: [C8yAlarm.C8yStatus.active.rawValue]
)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in
},
receiveValue: { value in
self.minorCountItem.countLabel.text = String(value)
let arguments = [
C8yAlarm.C8ySeverity.critical, C8yAlarm.C8ySeverity.major, C8yAlarm.C8ySeverity.minor,
C8yAlarm.C8ySeverity.warning,
]
let textfields = [self.criticalCountItem, self.majorCountItem, self.minorCountItem, self.warningCountItem]
arguments.enumerated().publisher
.flatMap { index, arg in
alarmsApi.getAlarms(
pageSize: 1,
severity: [arg.rawValue],
status: [C8yAlarm.C8yStatus.active.rawValue],
withTotalElements: true
)
.map { $0.statistics?.totalElements ?? 0 }
.receive(on: DispatchQueue.main)
.catch { _ in Just(0) }
.map { (index, $0) }
.eraseToAnyPublisher()
}
)
.store(in: &self.cancellableSet)
alarmsApi.getNumberOfAlarms(
severity: [C8yAlarm.C8ySeverity.warning.rawValue],
status: [C8yAlarm.C8yStatus.active.rawValue]
)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in
},
receiveValue: { value in
self.warningCountItem.countLabel.text = String(value)
.sink { index, result in
textfields[index]?.countLabel.text = String(result ?? 0)
}
)
.store(in: &self.cancellableSet)
.store(in: &self.cancellableSet)
}

/// update tag list for receiving push notifications
func fetchAlarms() {
func reload() {
SubscribedAlarmFilter.shared.resolvedDeviceId = nil
if let deviceName = SubscribedAlarmFilter.shared.deviceName {
let managedObjectsApi = Cumulocity.Core.shared.inventory.managedObjectsApi
Expand Down
Loading