Skip to content

Commit

Permalink
[Fix #12] Added pagination to watchlist, alarm- and device-list. (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
joerghartmann authored Dec 19, 2024
1 parent 047bcd4 commit 7a96e6f
Show file tree
Hide file tree
Showing 11 changed files with 350 additions and 61 deletions.
14 changes: 13 additions & 1 deletion ios/AlarmApp/AlarmApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
30FD5B0F27F1ADD300F61F3F /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FD5B0E27F1ADD300F61F3F /* Credentials.swift */; };
30FD5B1127F1C1C500F61F3F /* PushNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FD5B1027F1C1C500F61F3F /* PushNotificationCenter.swift */; };
30FD5B1427F1CABA00F61F3F /* IQKeyboardManagerSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 30FD5B1327F1CABA00F61F3F /* IQKeyboardManagerSwift */; };
F113B91E2D14775600420983 /* PaginatedViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F113B91C2D14775600420983 /* PaginatedViewModel.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -161,6 +162,7 @@
30FBB9C927CD417F008302D0 /* DetailsItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailsItem.xib; sourceTree = "<group>"; };
30FD5B0E27F1ADD300F61F3F /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = "<group>"; };
30FD5B1027F1C1C500F61F3F /* PushNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationCenter.swift; sourceTree = "<group>"; };
F113B91C2D14775600420983 /* PaginatedViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginatedViewModel.swift; sourceTree = "<group>"; };
F1C9181E294300F70020DFCE /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
F1C918202943010A0020DFCE /* .swift-format */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ".swift-format"; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -216,6 +218,7 @@
30E1D0F427E8ECF600FCB4DA /* Fragments */,
30E1D0CE27E3E29C00FCB4DA /* PushGateway */,
30FBB9C427CD4115008302D0 /* Views */,
F113B91D2D14775600420983 /* Pagination */,
3056B76927CE603300ED443B /* Network */,
3056B77A27CE896F00ED443B /* Material */,
309A860B27DF659F00F69ED0 /* MaterialUI */,
Expand Down Expand Up @@ -376,6 +379,14 @@
path = Views;
sourceTree = "<group>";
};
F113B91D2D14775600420983 /* Pagination */ = {
isa = PBXGroup;
children = (
F113B91C2D14775600420983 /* PaginatedViewModel.swift */,
);
path = Pagination;
sourceTree = "<group>";
};
F1C918242943275B0020DFCE /* External */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -534,6 +545,7 @@
3008B04F296C245500A05731 /* MaterialTextfield.swift in Sources */,
30F6C36627D6B8AD00884CE8 /* SeparatorItem.swift in Sources */,
3008B04B296C17FA00A05731 /* MaterialButton.swift in Sources */,
F113B91E2D14775600420983 /* PaginatedViewModel.swift in Sources */,
3056B77C27CE899000ED443B /* MaterialLabel.swift in Sources */,
3056B77627CE696B00ED443B /* AlarmListItem.swift in Sources */,
3044433D294B81D500FE40D1 /* DecoratedLabel.swift in Sources */,
Expand Down Expand Up @@ -789,7 +801,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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class DashboardViewController: UIViewController, AlarmListReloadDelegate, EmptyA
let textfields = [self.criticalCountItem, self.majorCountItem, self.minorCountItem, self.warningCountItem]
arguments.enumerated().publisher
.flatMap { index, arg in
return alarmsApi.getAlarms(
alarmsApi.getAlarms(
pageSize: 1,
severity: [arg.rawValue],
status: [C8yAlarm.C8yStatus.active.rawValue],
Expand All @@ -90,7 +90,7 @@ class DashboardViewController: UIViewController, AlarmListReloadDelegate, EmptyA
}

/// 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

0 comments on commit 7a96e6f

Please sign in to comment.