diff --git a/BuildTools/.swiftlint.yml b/BuildTools/.swiftlint.yml index c6622e780..6de1dff5c 100644 --- a/BuildTools/.swiftlint.yml +++ b/BuildTools/.swiftlint.yml @@ -29,6 +29,8 @@ excluded: - ../fastlane - ../OpenHABCore/.build - .build + - ../OpenHABCore/Sources/OpenHABCore/GeneratedSources/* + - ../OpenHABCore/swift-openapi-generator nesting: type_level: 2 @@ -93,6 +95,7 @@ custom_rules: file_name: suffix_pattern: "Extension?|\\+.*" + excluded: "UICollectionViewCellRegistrationExtension.swift" opening_brace: allow_multiline_func: true diff --git a/OpenHABCore/Package.swift b/OpenHABCore/Package.swift index 8363eda5b..6ef11ec1f 100644 --- a/OpenHABCore/Package.swift +++ b/OpenHABCore/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "OpenHABCore", - platforms: [.iOS(.v12), .watchOS(.v6)], + platforms: [.iOS(.v16), .watchOS(.v8)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( @@ -15,8 +15,9 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(name: "Alamofire", url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0"), - .package(name: "Kingfisher", url: "https://github.com/onevcat/Kingfisher.git", from: "7.0.0") + .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0"), + .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.0.0"), + .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.1.0")) ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -25,7 +26,8 @@ let package = Package( name: "OpenHABCore", dependencies: [ .product(name: "Alamofire", package: "Alamofire", condition: .when(platforms: [.iOS, .watchOS])), - .product(name: "Kingfisher", package: "Kingfisher", condition: .when(platforms: [.iOS, .watchOS])) + .product(name: "Kingfisher", package: "Kingfisher", condition: .when(platforms: [.iOS, .watchOS])), + .product(name: "Collections", package: "swift-collections") ] ), .testTarget( diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABSitemapPage.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABSitemapPage.swift index bc16248ad..2c18aedd8 100644 --- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABSitemapPage.swift +++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABSitemapPage.swift @@ -9,18 +9,36 @@ // // SPDX-License-Identifier: EPL-2.0 +import Collections import Foundation import os.log -public class OpenHABSitemapPage: NSObject { +public class OpenHABSitemapPage: NSObject, ObservableObject { public var sendCommand: ((_ item: OpenHABItem, _ command: String?) -> Void)? - public var widgets: [OpenHABWidget] = [] + public var widgets = OrderedDictionary() public var pageId = "" public var title = "" public var link = "" public var leaf = false public var icon = "" + public init(pageId: String, title: String, link: String, leaf: Bool, widgets: OrderedDictionary, icon: String) { + super.init() + self.pageId = pageId + self.title = title + self.link = link + self.leaf = leaf + + self.widgets = widgets + + for (_, widget) in self.widgets { + widget.sendCommand = { [weak self] item, command in + self?.sendCommand(item, commandToSend: command) + } + } + self.icon = icon + } + public init(pageId: String, title: String, link: String, leaf: Bool, widgets: [OpenHABWidget], icon: String) { super.init() self.pageId = pageId @@ -29,8 +47,12 @@ public class OpenHABSitemapPage: NSObject { self.leaf = leaf var tempWidgets = [OpenHABWidget]() tempWidgets.flatten(widgets) - self.widgets = tempWidgets - for widget in self.widgets { + + self.widgets = OrderedDictionary( + uniqueKeysWithValues: tempWidgets.map { ($0.id, $0) } + ) + + for (_, widget) in self.widgets { widget.sendCommand = { [weak self] item, command in self?.sendCommand(item, commandToSend: command) } @@ -47,7 +69,7 @@ public class OpenHABSitemapPage: NSObject { } public extension OpenHABSitemapPage { - func filter(_ isIncluded: (OpenHABWidget) throws -> Bool) rethrows -> OpenHABSitemapPage { + func filter(_ isIncluded: (String, OpenHABWidget) throws -> Bool) rethrows -> OpenHABSitemapPage { let filteredOpenHABSitemapPage = try OpenHABSitemapPage( pageId: pageId, title: title, diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABUiTile.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABUiTile.swift index ffac41eee..1312e7a9e 100644 --- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABUiTile.swift +++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABUiTile.swift @@ -11,7 +11,7 @@ import Foundation -public class OpenHABUiTile: Decodable { +public class OpenHABUiTile: Decodable, Hashable { public var name = "" public var url = "" public var imageUrl = "" @@ -21,6 +21,14 @@ public class OpenHABUiTile: Decodable { self.url = url self.imageUrl = imageUrl } + + public static func == (lhs: OpenHABUiTile, rhs: OpenHABUiTile) -> Bool { + lhs.name == rhs.name + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + } } public extension OpenHABUiTile { diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift index 44773b735..3ffa72bdc 100644 --- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift +++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift @@ -48,7 +48,7 @@ protocol Widget: AnyObject { func flatten(_: [ChildWidget]) } -public class OpenHABWidget: NSObject, MKAnnotation, Identifiable { +public class OpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableObject { public enum WidgetType: String { case chart = "Chart" case colorpicker = "Colorpicker" @@ -86,7 +86,7 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable { public var labelcolor = "" public var valuecolor = "" public var service = "" - public var state = "" + @Published public var state = "" public var text = "" public var legend: Bool? public var encoding = "" diff --git a/OpenHABCore/Sources/OpenHABCore/Util/NetworkConnection.swift b/OpenHABCore/Sources/OpenHABCore/Util/NetworkConnection.swift index 0f54a4e57..b0a07d4f0 100644 --- a/OpenHABCore/Sources/OpenHABCore/Util/NetworkConnection.swift +++ b/OpenHABCore/Sources/OpenHABCore/Util/NetworkConnection.swift @@ -110,6 +110,7 @@ public class NetworkConnection { public static func uiTiles(openHABRootUrl: String, completionHandler: @escaping (DataResponse) -> Void) { if let url = Endpoint.uiTiles(openHABRootUrl: openHABRootUrl).url { + os_log("URL for Endpoint %{PUBLIC}@", log: .default, type: .info, url.debugDescription) load(from: url, completionHandler: completionHandler) } } @@ -154,7 +155,6 @@ public class NetworkConnection { commandRequest.setValue("text/plain", forHTTPHeaderField: "Content-type") - os_log("Timeout %{PUBLIC}g", log: .default, type: .info, commandRequest.timeoutInterval) let link = item.link os_log("OpenHABViewController posting %{PUBLIC}@ command to %{PUBLIC}@", log: .default, type: .info, command ?? "", link) os_log("%{PUBLIC}@", log: .default, type: .info, commandRequest.debugDescription) diff --git a/openHAB.xcodeproj/project.pbxproj b/openHAB.xcodeproj/project.pbxproj index e25ee3001..0bc0848ff 100644 --- a/openHAB.xcodeproj/project.pbxproj +++ b/openHAB.xcodeproj/project.pbxproj @@ -84,6 +84,7 @@ DA07764A234683BC0086C685 /* SwitchRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA077649234683BC0086C685 /* SwitchRow.swift */; }; DA0776F0234788010086C685 /* UserData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0776EF234788010086C685 /* UserData.swift */; }; DA0F37D023D4ACC7007EAB48 /* SliderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0F37CF23D4ACC7007EAB48 /* SliderRow.swift */; }; + DA11B2482C8125E8004D96C9 /* FrameCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA11B2472C8125E8004D96C9 /* FrameCellView.swift */; }; DA15BFBD23C6726400BD8ADA /* ObservableOpenHABDataObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA15BFBC23C6726400BD8ADA /* ObservableOpenHABDataObject.swift */; }; DA19E25B22FD801D002F8F2F /* OpenHABGeneralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA19E25A22FD801D002F8F2F /* OpenHABGeneralTests.swift */; }; DA21EAE22339621C001AB415 /* Throttler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA21EAE12339621C001AB415 /* Throttler.swift */; }; @@ -110,6 +111,7 @@ DAA42BA821DC97E000244B2A /* NotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA42BA721DC97DF00244B2A /* NotificationTableViewCell.swift */; }; DAA42BAA21DC983B00244B2A /* VideoUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA42BA921DC983B00244B2A /* VideoUITableViewCell.swift */; }; DAA42BAC21DC984A00244B2A /* WebUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA42BAB21DC984A00244B2A /* WebUITableViewCell.swift */; }; + DABA07542B0F9E7A00708281 /* UICollectionViewCellRegistrationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABA07532B0F9E7A00708281 /* UICollectionViewCellRegistrationExtension.swift */; }; DAC65FC7236EDF3900F4501E /* SpinnerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC65FC6236EDF3900F4501E /* SpinnerViewController.swift */; }; DAC6608D236F771600F4501E /* PreferencesSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC6608C236F771600F4501E /* PreferencesSwiftUIView.swift */; }; DAC6608F236F80BA00F4501E /* PreferencesRowUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC6608E236F80BA00F4501E /* PreferencesRowUIView.swift */; }; @@ -144,7 +146,7 @@ DAF4F6C0222734D300C24876 /* NewImageUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF4F6BF222734D200C24876 /* NewImageUITableViewCell.swift */; }; DF05EF121D00696200DD646D /* DrawerUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05EF111D00696200DD646D /* DrawerUITableViewCell.swift */; }; DF05FF231896BD2D00FF2F9B /* SelectionUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05FF221896BD2D00FF2F9B /* SelectionUITableViewCell.swift */; }; - DF06F1F618FE7A160011E7B9 /* OpenHABSelectionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF06F1F518FE7A160011E7B9 /* OpenHABSelectionTableViewController.swift */; }; + DF06F1F618FE7A160011E7B9 /* OpenHABSelectionCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF06F1F518FE7A160011E7B9 /* OpenHABSelectionCollectionViewController.swift */; }; DF06F1FC18FEC2020011E7B9 /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF06F1FB18FEC2020011E7B9 /* ColorPickerViewController.swift */; }; DF1B302D1CF5C667009C921C /* OpenHABNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF1B302C1CF5C667009C921C /* OpenHABNotification.swift */; }; DF4A022C1CF315BA006C3456 /* OpenHABDrawerTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF4A022B1CF315BA006C3456 /* OpenHABDrawerTableViewController.swift */; }; @@ -338,6 +340,7 @@ DA077649234683BC0086C685 /* SwitchRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchRow.swift; sourceTree = ""; }; DA0776EF234788010086C685 /* UserData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserData.swift; sourceTree = ""; }; DA0F37CF23D4ACC7007EAB48 /* SliderRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderRow.swift; sourceTree = ""; }; + DA11B2472C8125E8004D96C9 /* FrameCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameCellView.swift; sourceTree = ""; }; DA15BFBC23C6726400BD8ADA /* ObservableOpenHABDataObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableOpenHABDataObject.swift; sourceTree = ""; }; DA19E25A22FD801D002F8F2F /* OpenHABGeneralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABGeneralTests.swift; sourceTree = ""; }; DA1C2E4B230DC28F00FACFB0 /* Appfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Appfile; sourceTree = ""; }; @@ -400,6 +403,7 @@ DAA42BA721DC97DF00244B2A /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = ""; }; DAA42BA921DC983B00244B2A /* VideoUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoUITableViewCell.swift; sourceTree = ""; }; DAA42BAB21DC984A00244B2A /* WebUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebUITableViewCell.swift; sourceTree = ""; }; + DABA07532B0F9E7A00708281 /* UICollectionViewCellRegistrationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewCellRegistrationExtension.swift; sourceTree = ""; }; DAC65FC6236EDF3900F4501E /* SpinnerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerViewController.swift; sourceTree = ""; }; DAC6608B236F6F4200F4501E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DAC6608C236F771600F4501E /* PreferencesSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesSwiftUIView.swift; sourceTree = ""; }; @@ -437,7 +441,7 @@ DAF4F6BF222734D200C24876 /* NewImageUITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewImageUITableViewCell.swift; sourceTree = ""; }; DF05EF111D00696200DD646D /* DrawerUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawerUITableViewCell.swift; sourceTree = ""; }; DF05FF221896BD2D00FF2F9B /* SelectionUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionUITableViewCell.swift; sourceTree = ""; }; - DF06F1F518FE7A160011E7B9 /* OpenHABSelectionTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenHABSelectionTableViewController.swift; sourceTree = ""; }; + DF06F1F518FE7A160011E7B9 /* OpenHABSelectionCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenHABSelectionCollectionViewController.swift; sourceTree = ""; }; DF06F1FB18FEC2020011E7B9 /* ColorPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = ""; }; DF1B302C1CF5C667009C921C /* OpenHABNotification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenHABNotification.swift; sourceTree = ""; }; DF4A022B1CF315BA006C3456 /* OpenHABDrawerTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenHABDrawerTableViewController.swift; sourceTree = ""; }; @@ -853,7 +857,7 @@ DFDEE3FC18831099008B26AC /* OpenHABSettingsViewController.swift */, DAEAA89A21E2611000267EA3 /* OpenHABNotificationsViewController.swift */, DFDF452E1932032B00A6E581 /* OpenHABLegalViewController.swift */, - DF06F1F518FE7A160011E7B9 /* OpenHABSelectionTableViewController.swift */, + DF06F1F518FE7A160011E7B9 /* OpenHABSelectionCollectionViewController.swift */, A07EF79F2230C0A20040919F /* OpenHABClientCertificatesViewController.swift */, DF4B84101886DA9900F34902 /* Widgets */, DF4A02291CF3157B006C3456 /* Drawer */, @@ -895,6 +899,7 @@ DAEAA89E21E6B16600267EA3 /* UITableView.swift */, DA7E1E47222EB00B002AEFD8 /* PlayerView.swift */, DA21EAE12339621C001AB415 /* Throttler.swift */, + DA11B2472C8125E8004D96C9 /* FrameCellView.swift */, ); name = Widgets; sourceTree = ""; @@ -1004,6 +1009,7 @@ 938BF9C524EFCC0700E6B52F /* UILabel+Localization.swift */, 938BF9D224EFD0B700E6B52F /* UIViewController+Localization.swift */, 935B484525342B8E00E44CF0 /* URL+Static.swift */, + DABA07532B0F9E7A00708281 /* UICollectionViewCellRegistrationExtension.swift */, ); name = Util; sourceTree = ""; @@ -1475,6 +1481,7 @@ DF4B84041885A53700F34902 /* OpenHABDataObject.swift in Sources */, DAC65FC7236EDF3900F4501E /* SpinnerViewController.swift in Sources */, DF4A02421CF34096006C3456 /* OpenHABDrawerItem.swift in Sources */, + DA11B2482C8125E8004D96C9 /* FrameCellView.swift in Sources */, DA50C7BF2B0A65300009F716 /* SliderWithSwitchSupportUITableViewCell.swift in Sources */, DF4A022C1CF315BA006C3456 /* OpenHABDrawerTableViewController.swift in Sources */, DFDF452F1932032B00A6E581 /* OpenHABLegalViewController.swift in Sources */, @@ -1483,13 +1490,14 @@ DAEAA89B21E2611000267EA3 /* OpenHABNotificationsViewController.swift in Sources */, DF1B302D1CF5C667009C921C /* OpenHABNotification.swift in Sources */, 938BF9D324EFD0B700E6B52F /* UIViewController+Localization.swift in Sources */, - DF06F1F618FE7A160011E7B9 /* OpenHABSelectionTableViewController.swift in Sources */, + DF06F1F618FE7A160011E7B9 /* OpenHABSelectionCollectionViewController.swift in Sources */, DAA42BA821DC97E000244B2A /* NotificationTableViewCell.swift in Sources */, DAF0A28F2C56F1EE00A14A6A /* ColorPickerCell.swift in Sources */, 6595667E28E0BE8E00E8A53B /* MulticastDelegate.swift in Sources */, DFDEE3FD18831099008B26AC /* OpenHABSettingsViewController.swift in Sources */, 938EDCE122C4FEB800661CA1 /* ScaleAspectFitImageView.swift in Sources */, DAEAA89F21E6B16600267EA3 /* UITableView.swift in Sources */, + DABA07542B0F9E7A00708281 /* UICollectionViewCellRegistrationExtension.swift in Sources */, DFB2624418830A3600D3244D /* OpenHABSitemapViewController.swift in Sources */, 653B54C2285E714900298ECD /* OpenHABViewController.swift in Sources */, DFA16EC118898A8400EDB0BB /* SegmentedUITableViewCell.swift in Sources */, @@ -1794,6 +1802,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = openHABUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1840,6 +1849,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = openHABUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1903,7 +1913,7 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; VERSIONING_SYSTEM = "apple-generic"; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Debug; }; @@ -1950,7 +1960,7 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; VERSIONING_SYSTEM = "apple-generic"; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Release; }; @@ -1975,6 +1985,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = openHABTestsSwift/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2018,6 +2029,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; INFOPLIST_FILE = openHABTestsSwift/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2176,6 +2188,7 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "openHAB/openHAB-Prefix.pch"; INFOPLIST_FILE = "openHAB/openHAB-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2221,6 +2234,7 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "openHAB/openHAB-Prefix.pch"; INFOPLIST_FILE = "openHAB/openHAB-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/openHAB.xcodeproj/xcshareddata/xcschemes/openHAB.xcscheme b/openHAB.xcodeproj/xcshareddata/xcschemes/openHAB.xcscheme index fe4eb7c51..e6061377e 100644 --- a/openHAB.xcodeproj/xcshareddata/xcschemes/openHAB.xcscheme +++ b/openHAB.xcodeproj/xcshareddata/xcschemes/openHAB.xcscheme @@ -60,6 +60,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + enableAddressSanitizer = "YES" + enableASanStackUseAfterReturn = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -83,6 +85,13 @@ isEnabled = "YES"> + + + + - + @@ -1513,35 +1513,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/openHAB/OpenHABDrawerTableViewController.swift b/openHAB/OpenHABDrawerTableViewController.swift index d2f12070a..9b5c72754 100644 --- a/openHAB/OpenHABDrawerTableViewController.swift +++ b/openHAB/OpenHABDrawerTableViewController.swift @@ -34,7 +34,7 @@ func deriveSitemaps(_ response: Data?) -> [OpenHABSitemap] { return sitemaps } -struct UiTile: Decodable { +struct UiTile: Decodable, Hashable { var name: String var url: String var imageUrl: String @@ -45,9 +45,9 @@ class OpenHABDrawerTableViewController: UITableViewController { var sitemaps: [OpenHABSitemap] = [] var uiTiles: [OpenHABUiTile] = [] + var drawerItems: [OpenHABDrawerItem] = [] var openHABUsername = "" var openHABPassword = "" - var drawerItems: [OpenHABDrawerItem] = [] weak var delegate: ModalHandler? // App wide data access @@ -55,6 +55,8 @@ class OpenHABDrawerTableViewController: UITableViewController { AppDelegate.appDelegate.appData } + var dataSource: DataSource! + init() { super.init(nibName: nil, bundle: nil) } @@ -66,17 +68,26 @@ class OpenHABDrawerTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() tableView.tableFooterView = UIView() - drawerItems = [] - sitemaps = [] loadSettings() - setStandardDrawerItems() + configureDataSource() + getData() os_log("OpenHABDrawerTableViewController did load", log: .viewCycle, type: .info) } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - os_log("OpenHABDrawerTableViewController viewWillAppear", log: .viewCycle, type: .info) + func updateUI(animatingDifferences: Bool = false) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems([Item.main], toSection: .main) + snapshot.appendSections([.tiles]) + snapshot.appendItems(uiTiles.map { Item.tiles($0) }, toSection: .tiles) + snapshot.appendSections([.sitemaps]) + snapshot.appendItems(sitemaps.map { Item.sitemaps($0) }, toSection: .sitemaps) + snapshot.appendSections([.system]) + snapshot.appendItems(getStandardDrawerItems().map { Item.system($0) }, toSection: .system) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + } + func getData() { NetworkConnection.sitemaps(openHABRootUrl: appData?.openHABRootUrl ?? "") { response in switch response.result { case let .success(data): @@ -93,22 +104,15 @@ class OpenHABDrawerTableViewController: UITableViewController { case .label: self.sitemaps.sort { $0.label < $1.label } case .name: self.sitemaps.sort { $0.name < $1.name } } - - self.drawerItems.removeAll() - self.setStandardDrawerItems() - self.tableView.reloadData() + self.updateUI() case let .failure(error): os_log("%{PUBLIC}@", log: .default, type: .error, error.localizedDescription) - self.drawerItems.removeAll() - self.setStandardDrawerItems() - self.tableView.reloadData() } } NetworkConnection.uiTiles(openHABRootUrl: appData?.openHABRootUrl ?? "") { response in switch response.result { case .success: - UIApplication.shared.isNetworkActivityIndicatorVisible = false os_log("ui tiles response", log: .viewCycle, type: .info) guard let responseData = response.data else { os_log("Error: did not receive data", log: OSLog.remoteAccess, type: .info) @@ -116,72 +120,56 @@ class OpenHABDrawerTableViewController: UITableViewController { } do { self.uiTiles = try JSONDecoder().decode([OpenHABUiTile].self, from: responseData) - self.tableView.reloadData() + self.updateUI() } catch { os_log("Error: did not receive data %{PUBLIC}@", log: OSLog.remoteAccess, type: .info, error.localizedDescription) } case let .failure(error): - UIApplication.shared.isNetworkActivityIndicatorVisible = false os_log("%{PUBLIC}@", log: .default, type: .error, error.localizedDescription) } } } +} - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - tableView.reloadData() - os_log("RightDrawerViewController viewDidAppear", log: .viewCycle, type: .info) - os_log("Sitemap count: %d", log: .viewCycle, type: .info, Int(sitemaps.count)) - os_log("Menu items count: %d", log: .viewCycle, type: .info, Int(drawerItems.count)) - } +extension OpenHABDrawerTableViewController { + typealias SectionType = Section - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) // Call the super class implementation. - os_log("RightDrawerViewController viewDidDisappear", log: .viewCycle, type: .info) - } - - // MARK: - Table view data source + enum Section: Int, CaseIterable, CustomStringConvertible { + var description: String { + switch self { + case .main: "Main" + case .tiles: "Tiles" + case .sitemaps: "Sitemaps" + case .system: "System" + } + } - override func numberOfSections(in tableView: UITableView) -> Int { - 4 + case main = 0 + case tiles + case sitemaps + case system } - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch section { - case 0: - 1 - case 1: - uiTiles.count - case 2: - sitemaps.count - case 3: - drawerItems.count - default: - 0 - } + enum Item: Hashable { + case main + case tiles(OpenHABUiTile) + case sitemaps(OpenHABSitemap) + case system(OpenHABDrawerItem) } - override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - switch section { - case 0: - "Main" - case 1: - "Tiles" - case 2: - "Sitemaps" - case 3: - "System" - default: - "Unknown" + class DataSource: UITableViewDiffableDataSource { + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + let sectionKind = Section(rawValue: section) + return sectionKind?.description } } - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + func cell(tableView: UITableView, indexPath: IndexPath, item: Item) -> UITableViewCell? { let cell = (tableView.dequeueReusableCell(withIdentifier: OpenHABDrawerTableViewController.tableViewCellIdentifier) as? DrawerUITableViewCell)! cell.customImageView.subviews.forEach { $0.removeFromSuperview() } cell.accessoryView = nil - switch indexPath.section { - case 0: + switch item { + case .main: cell.customTextLabel?.text = "Home" cell.customImageView.image = UIImage(named: "openHABIcon") if let currentView = appData?.currentView { @@ -190,10 +178,9 @@ class OpenHABDrawerTableViewController: UITableViewController { cell.accessoryView = UIImageView(image: UIImage(named: "arrow.triangle.2.circlepath")) } } - case 1: - let imageView = UIImageView(frame: cell.customImageView.bounds) - let tile = uiTiles[indexPath.row] + case let .tiles(tile): cell.customTextLabel?.text = tile.name + let imageView = UIImageView(frame: cell.customImageView.bounds) let passedURL = tile.imageUrl // Dependent on $OPENHAB_CONF/services/runtime.cfg // Can either be an absolute URL, a path (sometimes malformed) or the content to be displayed (for imageURL) @@ -216,24 +203,18 @@ class OpenHABDrawerTableViewController: UITableViewController { imageView.image = UIImage(named: "openHABIcon") } cell.customImageView.image = imageView.image - case 2: - if !sitemaps.isEmpty { - let siteMapIndex = indexPath.row - let imageView = UIImageView(frame: cell.customImageView.bounds) - - cell.customTextLabel?.text = sitemaps[siteMapIndex].label - if !sitemaps[siteMapIndex].icon.isEmpty { - if let iconURL = Endpoint.iconForDrawer(rootUrl: appData?.openHABRootUrl ?? "", icon: sitemaps[siteMapIndex].icon).url { - imageView.kf.setImage(with: iconURL, placeholder: UIImage(named: "openHABIcon")) - } - } else { - imageView.image = UIImage(named: "openHABIcon") + case let .sitemaps(sitemap): + let imageView = UIImageView(frame: cell.customImageView.bounds) + cell.customTextLabel?.text = sitemap.label + if !sitemap.icon.isEmpty { + if let iconURL = Endpoint.iconForDrawer(rootUrl: appData?.openHABRootUrl ?? "", icon: sitemap.icon).url { + imageView.kf.setImage(with: iconURL, placeholder: UIImage(named: "openHABIcon")) } - cell.customImageView.image = imageView.image + } else { + imageView.image = UIImage(named: "openHABIcon") } - case 3: - // Then menu items - let drawerItem = drawerItems[indexPath.row] + cell.customImageView.image = imageView.image + case let .system(drawerItem): cell.customTextLabel?.text = drawerItem.localizedString switch drawerItem { case .notifications: @@ -241,8 +222,6 @@ class OpenHABDrawerTableViewController: UITableViewController { case .settings: cell.customImageView.image = UIImage(systemSymbol: .gear) } - default: - break } cell.separatorInset = UIEdgeInsets(top: 0, left: 60, bottom: 0, right: 0) @@ -252,6 +231,16 @@ class OpenHABDrawerTableViewController: UITableViewController { return cell } + func configureDataSource() { + dataSource = DataSource(tableView: tableView) { [unowned self] (tableView, indexPath, item) -> UITableViewCell? in + cell(tableView: tableView, indexPath: indexPath, item: item) + } + } +} + +extension OpenHABDrawerTableViewController { + // MARK: - Table view delegate + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 44 } @@ -261,14 +250,16 @@ class OpenHABDrawerTableViewController: UITableViewController { os_log("Clicked on drawer section %d row %d", log: .viewCycle, type: .info, indexPath.section, indexPath.row) tableView.deselectRow(at: indexPath, animated: false) - // First sitemaps - switch indexPath.section { - case 0: + + guard let menuItem = dataSource.itemIdentifier(for: indexPath) else { return } + + switch menuItem { + case .main: dismiss(animated: true) { self.delegate?.modalDismissed(to: .webview) } - case 1: - let passedURL = uiTiles[indexPath.row].url + case let .tiles(tile): + let passedURL = tile.url // Dependent on $OPENHAB_CONF/services/runtime.cfg // Can either be an absolute URL, a path (sometimes malformed) if !passedURL.isEmpty { @@ -280,20 +271,15 @@ class OpenHABDrawerTableViewController: UITableViewController { openURL(url: builtURL.url) } } - case 2: - if !sitemaps.isEmpty { - let sitemap = sitemaps[indexPath.row] - Preferences.defaultSitemap = sitemap.name - appData?.sitemapViewController?.pageUrl = "" - dismiss(animated: true) { - os_log("self delegate %d", log: .viewCycle, type: .info, self.delegate != nil) - self.delegate?.modalDismissed(to: .sitemap) - } + case let .sitemaps(sitemap): + Preferences.defaultSitemap = sitemap.name + appData?.sitemapViewController?.pageUrl = "" + dismiss(animated: true) { + os_log("self delegate %d", log: .viewCycle, type: .info, self.delegate != nil) + self.delegate?.modalDismissed(to: .sitemap) } - case 3: - // Then menu items - let drawerItem = drawerItems[indexPath.row] + case let .system(drawerItem): switch drawerItem { case .settings: dismiss(animated: true) { @@ -304,12 +290,11 @@ class OpenHABDrawerTableViewController: UITableViewController { self.delegate?.modalDismissed(to: .notifications) } } - default: - break } } - private func setStandardDrawerItems() { + private func getStandardDrawerItems() -> [OpenHABDrawerItem] { + var drawerItems: [OpenHABDrawerItem] = [] // check if we are using my.openHAB, add notifications menu item then // Actually this should better test whether the host of the remoteUrl is on openhab.org if Preferences.remoteUrl.contains("openhab.org"), !Preferences.demomode { @@ -317,9 +302,10 @@ class OpenHABDrawerTableViewController: UITableViewController { } // Settings always go last drawerItems.append(.settings) + return drawerItems } - func loadSettings() { + private func loadSettings() { openHABUsername = Preferences.username openHABPassword = Preferences.password } diff --git a/openHAB/OpenHABSelectionCollectionViewController.swift b/openHAB/OpenHABSelectionCollectionViewController.swift new file mode 100644 index 000000000..5b5fbb4ba --- /dev/null +++ b/openHAB/OpenHABSelectionCollectionViewController.swift @@ -0,0 +1,85 @@ +// Copyright (c) 2010-2024 Contributors to the openHAB project +// +// See the NOTICE file(s) distributed with this work for additional +// information. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0 +// +// SPDX-License-Identifier: EPL-2.0 + +import OpenHABCore +import os.log +import UIKit + +// swiftlint:disable:next type_name +public protocol OpenHABSelectionTableViewControllerDelegate: NSObjectProtocol { + func didSelectWidgetMapping(_ selectedMapping: Int, widget: OpenHABWidget) +} + +class OpenHABSelectionCollectionViewController: UICollectionViewController { + private let cellReuseIdentifier = "SelectionCell" + + private lazy var dataSource = makeDataSource() + + var mappings: [OpenHABWidgetMapping] = [] + weak var delegate: OpenHABSelectionTableViewControllerDelegate? + var selectionWidget: OpenHABWidget? + + override func viewDidLoad() { + super.viewDidLoad() + + os_log("I have %d mappings", log: .viewCycle, type: .info, mappings.count) + + collectionView.dataSource = dataSource + update(with: mappings) + } + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + os_log("Selected mapping %d", log: .viewCycle, type: .info, indexPath.row) + guard let selectionWidget else { fatalError("Not known selectionItem") } + delegate?.didSelectWidgetMapping(indexPath.row, widget: selectionWidget) + navigationController?.popViewController(animated: true) + } +} + +private extension OpenHABSelectionCollectionViewController { + typealias Cell = UICollectionViewListCell + typealias CellRegistration = UICollectionView.CellRegistration + + enum Section: String, CaseIterable { + case main + } + + func makeCellRegistration() -> CellRegistration { + CellRegistration { cell, _, mapping in + + var content = cell.defaultContentConfiguration() + content.text = mapping.label + + cell.contentConfiguration = content + + if self.selectionWidget?.item?.state == mapping.command { + os_log("This item is selected", log: .viewCycle, type: .info) + cell.accessories = [.checkmark()] + } else { + cell.accessories = [] + } + } + } + + func makeDataSource() -> UICollectionViewDiffableDataSource { + UICollectionViewDiffableDataSource( + collectionView: collectionView, + cellProvider: makeCellRegistration().cellProvider + ) + } + + func update(with list: [OpenHABWidgetMapping], animate: Bool = true) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections(Section.allCases) + snapshot.appendItems(list, toSection: .main) + dataSource.apply(snapshot, animatingDifferences: animate) + } +} diff --git a/openHAB/OpenHABSitemapViewController.swift b/openHAB/OpenHABSitemapViewController.swift index c0a329d60..4cb1de847 100644 --- a/openHAB/OpenHABSitemapViewController.swift +++ b/openHAB/OpenHABSitemapViewController.swift @@ -25,6 +25,10 @@ enum Action { typealias Async = (UIViewController, I, @escaping (O) -> Void) -> Void } +enum ViewControllerSection { + case main +} + struct OpenHABImageProcessor: ImageProcessor { // `identifier` should be the same for processors with the same properties/functionality // It will be used when storing and retrieving the image to/from cache. @@ -59,7 +63,6 @@ struct OpenHABImageProcessor: ImageProcessor { class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCellTouchEventDelegate { var pageUrl = "" - private var selectedWidgetRow: Int = 0 private var currentPageOperation: Alamofire.Request? private var commandOperation: Alamofire.Request? private var iconType: IconType = .png @@ -108,7 +111,10 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel search.isActive && !searchBarIsEmpty } - @IBOutlet private var widgetTableView: UITableView! + var dataSource: UITableViewDiffableDataSource! + var currentSnapshot: NSDiffableDataSourceSnapshot! + + @IBOutlet private var tableView: UITableView! // Here goes everything about view loading, appearing, disappearing, entering background and becoming active override func viewDidLoad() { @@ -117,16 +123,17 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel pageNetworkStatus = nil sitemaps = [] - widgetTableView.tableFooterView = UIView() + // tableView.tableFooterView = UIView() registerTableViewCells() + dataSource = makeDataSource() configureTableView() refreshControl = UIRefreshControl() refreshControl?.addTarget(self, action: #selector(OpenHABSitemapViewController.handleRefresh(_:)), for: .valueChanged) if let refreshControl { - widgetTableView.refreshControl = refreshControl + tableView.refreshControl = refreshControl } search.searchResultsUpdater = self @@ -136,7 +143,7 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel #if DEBUG // setup accessibilityIdentifiers for UITest - widgetTableView.accessibilityIdentifier = "OpenHABSitemapViewControllerWidgetTableView" + tableView.accessibilityIdentifier = "OpenHABSitemapViewControllerWidgetTableView" #endif } @@ -169,8 +176,8 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel // Set self as root view controller appData?.sitemapViewController = self if currentPage != nil { - currentPage?.widgets = [] - widgetTableView.reloadData() + currentPage?.widgets = [:] + updateUI() } os_log("OpenHABSitemapViewController pageUrl is empty, this is first launch", log: .viewCycle, type: .info) OpenHABTracker.shared.multicastDelegate.add(self) @@ -241,8 +248,7 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - - widgetTableView.reloadData() + updateUI() } /// Implementation of GenericUITableViewCellTouchEventDelegate @@ -254,28 +260,28 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel func touchUp() { isUserInteracting = false if isWaitingToReload { - widgetTableView.reloadData() + updateUI() refreshControl?.endRefreshing() } isWaitingToReload = false } func configureTableView() { - widgetTableView.dataSource = self - widgetTableView.delegate = self + tableView.dataSource = dataSource + tableView.delegate = self } func registerTableViewCells() { - widgetTableView.register(cellType: MapViewTableViewCell.self) - widgetTableView.register(cellType: NewImageUITableViewCell.self) - widgetTableView.register(cellType: VideoUITableViewCell.self) + tableView.register(cellType: MapViewTableViewCell.self) + tableView.register(cellType: NewImageUITableViewCell.self) + tableView.register(cellType: VideoUITableViewCell.self) } @objc func handleRefresh(_ refreshControl: UIRefreshControl?) { loadPage(false) - widgetTableView.reloadData() - widgetTableView.layoutIfNeeded() + updateUI() + tableView.layoutIfNeeded() } func restart() { @@ -288,14 +294,10 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel } } - func relevantWidget(indexPath: IndexPath) -> OpenHABWidget? { - relevantPage?.widgets[safe: indexPath.row] - } - private func updateWidgetTableView() { UIView.performWithoutAnimation { - widgetTableView.beginUpdates() - widgetTableView.endUpdates() + tableView.beginUpdates() + tableView.endUpdates() } } @@ -332,11 +334,10 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel if !NetworkConnection.atmosphereTrackingId.isEmpty { os_log("Found X-Atmosphere-tracking-id: %{PUBLIC}@", log: .remoteAccess, type: .info, NetworkConnection.atmosphereTrackingId) } - var openHABSitemapPage: OpenHABSitemapPage? do { // Self-executing closure // Inspired by https://www.swiftbysundell.com/posts/inline-types-and-functions-in-swift - openHABSitemapPage = try { + currentPage = try { let sitemapPageCodingData = try data.decoded(as: OpenHABSitemapPage.CodingData.self) return sitemapPageCodingData.openHABSitemapPage }() @@ -344,7 +345,6 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel os_log("Should not throw %{PUBLIC}@", log: OSLog.remoteAccess, type: .error, error.localizedDescription) } - currentPage = openHABSitemapPage if isFiltering { filterContentForSearchText(search.searchBar.text) } @@ -354,7 +354,7 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel } // isUserInteracting fixes https://github.com/openhab/openhab-ios/issues/646 where reloading while the user is interacting can have unintended consequences if !isUserInteracting { - widgetTableView.reloadData() + updateUI() refreshControl?.endRefreshing() } else { isWaitingToReload = true @@ -418,7 +418,7 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel self.showSideMenu() default: break } - self.widgetTableView.reloadData() + self.updateUI() case let .failure(error): os_log("%{PUBLIC}@ %d", log: .default, type: .error, error.localizedDescription, response.response?.statusCode ?? 0) DispatchQueue.main.async { @@ -486,13 +486,13 @@ class OpenHABSitemapViewController: OpenHABViewController, GenericUITableViewCel func filterContentForSearchText(_ searchText: String?, scope: String = "All") { guard let searchText else { return } - filteredPage = currentPage?.filter { - $0.label.lowercased().contains(searchText.lowercased()) && $0.type != .frame + filteredPage = currentPage?.filter { (_, widget) in + widget.label.lowercased().contains(searchText.lowercased()) && widget.type != .frame } filteredPage?.sendCommand = { [weak self] item, command in self?.sendCommand(item, commandToSend: command) } - widgetTableView.reloadData() + updateUI() } func sendCommand(_ item: OpenHABItem?, commandToSend command: String?) { @@ -540,10 +540,9 @@ extension OpenHABSitemapViewController: OpenHABTrackerDelegate { extension OpenHABSitemapViewController: OpenHABSelectionTableViewControllerDelegate { // send command on selected selection widget mapping - func didSelectWidgetMapping(_ selectedMappingIndex: Int) { - let selectedWidget: OpenHABWidget? = relevantPage?.widgets[selectedWidgetRow] - let selectedMapping: OpenHABWidgetMapping? = selectedWidget?.mappingsOrItemOptions[selectedMappingIndex] - sendCommand(selectedWidget?.item, commandToSend: selectedMapping?.command) + func didSelectWidgetMapping(_ selectedMappingIndex: Int, widget: OpenHABWidget) { + let selectedMapping: OpenHABWidgetMapping? = widget.mappingsOrItemOptions[selectedMappingIndex] + sendCommand(widget.item, commandToSend: selectedMapping?.command) } } @@ -559,45 +558,172 @@ extension OpenHABSitemapViewController: UISearchResultsUpdating { extension OpenHABSitemapViewController: ColorPickerCellDelegate { func didPressColorButton(_ cell: ColorPickerCell?) { - let colorPickerViewController = storyboard?.instantiateViewController(withIdentifier: "ColorPickerViewController") as? ColorPickerViewController - if let cell { - let widget = relevantPage?.widgets[widgetTableView.indexPath(for: cell)?.row ?? 0] - colorPickerViewController?.title = widget?.labelText - colorPickerViewController?.widget = widget - } - if let colorPickerViewController { + if let colorPickerViewController = storyboard?.instantiateViewController(withIdentifier: "ColorPickerViewController") as? ColorPickerViewController, + let cell, + let indexPath = tableView.indexPath(for: cell), + let widget = dataSource.itemIdentifier(for: indexPath) { +// let widgetId = dataSource.itemIdentifier(for: indexPath), +// let widget = relevantPage?.widgets[widgetId] { + colorPickerViewController.title = widget.labelText + colorPickerViewController.widget = widget + navigationController?.pushViewController(colorPickerViewController, animated: true) } } } -// MARK: - UITableViewDelegate, UITableViewDataSource +extension OpenHABSitemapViewController { + private func makeDataSource() -> UITableViewDiffableDataSource { + // Code from cellForItemAt transplanted into cell provider closure + + UITableViewDiffableDataSource(tableView: tableView) { [unowned self] (tableView: UITableView, indexPath: IndexPath, widget: OpenHABWidget) -> UITableViewCell? in + + let cell: UITableViewCell +// guard let widget = self?.relevantPage?.widgets[widgetId] else { return nil } + switch widget.type { + case .frame: + cell = tableView.dequeueReusableCell(for: indexPath, cellType: FrameUITableViewCell.self) + case .switchWidget: + // Reflecting the discussion held in https://github.com/openhab/openhab-core/issues/952 + if !widget.mappings.isEmpty { + cell = tableView.dequeueReusableCell(for: indexPath, cellType: SegmentedUITableViewCell.self) + } else if widget.item?.isOfTypeOrGroupType(.switchItem) ?? false { + cell = tableView.dequeueReusableCell(for: indexPath, cellType: SwitchUITableViewCell.self) + } else if widget.item?.isOfTypeOrGroupType(.rollershutter) ?? false { + cell = tableView.dequeueReusableCell(for: indexPath, cellType: RollershutterCell.self) + } else if !widget.mappingsOrItemOptions.isEmpty { + cell = tableView.dequeueReusableCell(for: indexPath, cellType: SegmentedUITableViewCell.self) + } else { + cell = tableView.dequeueReusableCell(for: indexPath, cellType: SwitchUITableViewCell.self) + } + case .setpoint: + cell = tableView.dequeueReusableCell(for: indexPath, cellType: SetpointCell.self) + case .slider: + if widget.switchSupport { + cell = tableView.dequeueReusableCell(for: indexPath) as SliderWithSwitchSupportUITableViewCell + } else { + cell = tableView.dequeueReusableCell(for: indexPath) as SliderUITableViewCell + } + case .selection: + cell = tableView.dequeueReusableCell(for: indexPath, cellType: SelectionUITableViewCell.self) + case .colorpicker: + cell = tableView.dequeueReusableCell(for: indexPath, cellType: ColorPickerCell.self) + (cell as? ColorPickerCell)?.delegate = self + case .image, .chart: + cell = tableView.dequeueReusableCell(for: indexPath, cellType: NewImageUITableViewCell.self) + (cell as? NewImageUITableViewCell)?.didLoad = { [weak self] in + self?.updateWidgetTableView() + } + case .video: + cell = tableView.dequeueReusableCell(for: indexPath, cellType: VideoUITableViewCell.self) + (cell as? VideoUITableViewCell)?.didLoad = { [weak self] in + self?.updateWidgetTableView() + } + case .webview: + cell = tableView.dequeueReusableCell(for: indexPath, cellType: WebUITableViewCell.self) + case .mapview: + cell = tableView.dequeueReusableCell(for: indexPath, cellType: MapViewTableViewCell.self) + case .group, .text: + cell = tableView.dequeueReusableCell(for: indexPath, cellType: GenericUITableViewCell.self) + default: + cell = tableView.dequeueReusableCell(for: indexPath, cellType: GenericUITableViewCell.self) + } -extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if currentPage != nil { - if isFiltering { - return filteredPage?.widgets.count ?? 0 + var iconColor = widget.iconColor + if iconColor.isEmpty, traitCollection.userInterfaceStyle == .dark { + iconColor = "white" } - return currentPage?.widgets.count ?? 0 - } else { - return 0 + // No icon is needed for image, video, frame and web widgets + if !((cell is NewImageUITableViewCell) || (cell is VideoUITableViewCell) || (cell is FrameUITableViewCell) || (cell is WebUITableViewCell)) { + if let urlc = Endpoint.icon( + rootUrl: openHABRootUrl ?? "", + version: appData?.openHABVersion ?? 2, + icon: widget.icon, + state: widget.iconState(), + iconType: iconType ?? .svg, + iconColor: iconColor + ).url { + var imageRequest = URLRequest(url: urlc) + imageRequest.timeoutInterval = 10.0 + + let reportOnResults: ((Swift.Result) -> Void)? = { result in + switch result { + case let .success(value): + os_log("Task done for: %{PUBLIC}@", log: .viewCycle, type: .info, value.source.url?.absoluteString ?? "") + case let .failure(error): + os_log("Job failed: %{PUBLIC}@", log: .viewCycle, type: .info, error.localizedDescription) + } + } + + cell.imageView?.kf.setImage( + with: KF.ImageResource(downloadURL: urlc, cacheKey: urlc.path + (urlc.query ?? "")), + placeholder: UIImage(named: "blankicon.png"), + options: [.processor(OpenHABImageProcessor())], + completionHandler: reportOnResults + ) + } + } + + if cell is FrameUITableViewCell { + cell.backgroundColor = .ohSystemGroupedBackground + } else { + cell.backgroundColor = .ohSecondarySystemGroupedBackground + } + + if let cell = cell as? GenericUITableViewCell { + cell.widget = widget + cell.displayWidget() + cell.touchEventDelegate = self + } + + // Check if this is not the last row in the widgets list + // TODO: Switch to separator layout guide https://developer.apple.com/videos/play/wwdc2020/10026/ + if indexPath.row < (relevantPage?.widgets.count ?? 1) - 1 { + let nextRow = indexPath.index(after: indexPath.row) + let nextIndexPath = IndexPath(row: nextRow, section: indexPath.section) + let nextWidget = dataSource.itemIdentifier(for: nextIndexPath) + if let type = nextWidget?.type, type.isAny(of: .frame, .image, .video, .webview, .chart) { + cell.separatorInset = UIEdgeInsets.zero + } else if !(widget.type == .frame) { + cell.separatorInset = UIEdgeInsets(top: 0, left: 60, bottom: 0, right: 0) + } + } + + return cell } } + /// - Tag: WiFiUpdate + func updateUI(animated: Bool = false) { + currentSnapshot = NSDiffableDataSourceSnapshot() // Changed + currentSnapshot.appendSections([.main]) + if let relevantPage { + currentSnapshot.appendItems(Array(relevantPage.widgets.values), toSection: .main) +// currentSnapshot.appendItems(relevantPage.widgets.map(\.value.widgetId), toSection: .main) + } + dataSource.apply(currentSnapshot, animatingDifferences: animated) + } +} + +// MARK: - UITableViewDelegate, UITableViewDataSource + +extension OpenHABSitemapViewController: UITableViewDelegate { // }, UITableViewDataSource { func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { 44.0 } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let widget: OpenHABWidget? = relevantPage?.widgets[indexPath.row] - switch widget?.type { + guard let widget = dataSource.itemIdentifier(for: indexPath) else { return 44.0 } + +// guard let widgetId = dataSource.itemIdentifier(for: indexPath), let widget = relevantPage?.widgets[widgetId] else { return 44.0 } + + switch widget.type { case .frame: - return widget?.label.count ?? 0 > 0 ? 35.0 : 0 + return !widget.label.isEmpty ? 35.0 : 0 case .image, .chart, .video: return UITableView.automaticDimension case .webview, .mapview: - if let height = widget?.height { + if let height = widget.height { // calculate webview/mapview height and return it. Limited to UIScreen.main.bounds.height let heightValue = height * 44 os_log("Webview/Mapview height would be %g", log: .viewCycle, type: .info, heightValue) @@ -610,120 +736,6 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour } } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let widget: OpenHABWidget? = relevantWidget(indexPath: indexPath) - - let cell: UITableViewCell - - switch widget?.type { - case .frame: - cell = tableView.dequeueReusableCell(for: indexPath) as FrameUITableViewCell - case .switchWidget: - // Reflecting the discussion held in https://github.com/openhab/openhab-core/issues/952 - if !(widget?.mappings ?? []).isEmpty { - cell = tableView.dequeueReusableCell(for: indexPath) as SegmentedUITableViewCell - } else if widget?.item?.isOfTypeOrGroupType(.switchItem) ?? false { - cell = tableView.dequeueReusableCell(for: indexPath) as SwitchUITableViewCell - } else if widget?.item?.isOfTypeOrGroupType(.rollershutter) ?? false { - cell = tableView.dequeueReusableCell(for: indexPath) as RollershutterCell - } else if !(widget?.mappingsOrItemOptions ?? []).isEmpty { - cell = tableView.dequeueReusableCell(for: indexPath) as SegmentedUITableViewCell - } else { - cell = tableView.dequeueReusableCell(for: indexPath) as SwitchUITableViewCell - } - case .setpoint: - cell = tableView.dequeueReusableCell(for: indexPath) as SetpointCell - case .slider: - if let switchSupport = widget?.switchSupport, switchSupport { - cell = tableView.dequeueReusableCell(for: indexPath) as SliderWithSwitchSupportUITableViewCell - } else { - cell = tableView.dequeueReusableCell(for: indexPath) as SliderUITableViewCell - } - case .selection: - cell = tableView.dequeueReusableCell(for: indexPath) as SelectionUITableViewCell - case .colorpicker: - cell = tableView.dequeueReusableCell(for: indexPath) as ColorPickerCell - (cell as? ColorPickerCell)?.delegate = self - case .image, .chart: - cell = tableView.dequeueReusableCell(for: indexPath) as NewImageUITableViewCell - (cell as? NewImageUITableViewCell)?.didLoad = { [weak self] in - self?.updateWidgetTableView() - } - case .video: - cell = tableView.dequeueReusableCell(for: indexPath) as VideoUITableViewCell - (cell as? VideoUITableViewCell)?.didLoad = { [weak self] in - self?.updateWidgetTableView() - } - case .webview: - cell = tableView.dequeueReusableCell(for: indexPath) as WebUITableViewCell - case .mapview: - cell = tableView.dequeueReusableCell(for: indexPath) as MapViewTableViewCell - case .group, .text: - cell = tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell - default: - cell = tableView.dequeueReusableCell(for: indexPath) as GenericUITableViewCell - } - - var iconColor = widget?.iconColor - if iconColor == nil || iconColor!.isEmpty, traitCollection.userInterfaceStyle == .dark { - iconColor = "white" - } - // No icon is needed for image, video, frame and web widgets - if widget?.icon != nil, !((cell is NewImageUITableViewCell) || (cell is VideoUITableViewCell) || (cell is FrameUITableViewCell) || (cell is WebUITableViewCell)) { - if let urlc = Endpoint.icon( - rootUrl: openHABRootUrl, - version: appData?.openHABVersion ?? 2, - icon: widget?.icon, - state: widget?.iconState() ?? "", - iconType: iconType, - iconColor: iconColor! - ).url { - var imageRequest = URLRequest(url: urlc) - imageRequest.timeoutInterval = 10.0 - - let reportOnResults: ((Swift.Result) -> Void)? = { result in - switch result { - case let .success(value): - os_log("Task done for: %{PUBLIC}@", log: .viewCycle, type: .info, value.source.url?.absoluteString ?? "") - case let .failure(error): - os_log("Job failed: %{PUBLIC}@", log: .viewCycle, type: .info, error.localizedDescription) - } - } - - cell.imageView?.kf.setImage( - with: KF.ImageResource(downloadURL: urlc, cacheKey: urlc.path + (urlc.query ?? "")), - placeholder: UIImage(named: "blankicon.png"), - options: [.processor(OpenHABImageProcessor())], - completionHandler: reportOnResults - ) - } - } - - if cell is FrameUITableViewCell { - cell.backgroundColor = .ohSystemGroupedBackground - } else { - cell.backgroundColor = .ohSecondarySystemGroupedBackground - } - - if let cell = cell as? GenericUITableViewCell { - cell.widget = widget - cell.displayWidget() - cell.touchEventDelegate = self - } - - // Check if this is not the last row in the widgets list - if indexPath.row < (relevantPage?.widgets.count ?? 1) - 1 { - let nextWidget: OpenHABWidget? = relevantPage?.widgets[indexPath.row + 1] - if let type = nextWidget?.type, type.isAny(of: .frame, .image, .video, .webview, .chart) { - cell.separatorInset = UIEdgeInsets.zero - } else if !(widget?.type == .frame) { - cell.separatorInset = UIEdgeInsets(top: 0, left: 60, bottom: 0, right: 0) - } - } - - return cell - } - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { // Prevent the cell from inheriting the Table View's margin settings cell.preservesSuperviewLayoutMargins = false @@ -735,31 +747,34 @@ extension OpenHABSitemapViewController: UITableViewDelegate, UITableViewDataSour } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let widget: OpenHABWidget? = relevantWidget(indexPath: indexPath) - if widget?.linkedPage != nil { - if let link = widget?.linkedPage?.link { + guard let widget = dataSource.itemIdentifier(for: indexPath) else { return } + + // guard let widgetId = dataSource.itemIdentifier(for: indexPath), let widget = relevantPage?.widgets[widgetId] else { return } + + if widget.linkedPage != nil { + if let link = widget.linkedPage?.link { os_log("Selected %{PUBLIC}@", log: .viewCycle, type: .info, link) } - selectedWidgetRow = indexPath.row let newViewController = (storyboard?.instantiateViewController(withIdentifier: "OpenHABPageViewController") as? OpenHABSitemapViewController)! - newViewController.title = widget?.linkedPage?.title.components(separatedBy: "[")[0] - newViewController.pageUrl = widget?.linkedPage?.link ?? "" + newViewController.title = widget.linkedPage?.title.components(separatedBy: "[")[0] + newViewController.pageUrl = widget.linkedPage?.link ?? "" newViewController.openHABRootUrl = openHABRootUrl navigationController?.pushViewController(newViewController, animated: true) - } else if widget?.type == .selection { + } else if widget.type == .selection { os_log("Selected selection widget", log: .viewCycle, type: .info) - selectedWidgetRow = indexPath.row - let selectionViewController = (storyboard?.instantiateViewController(withIdentifier: "OpenHABSelectionTableViewController") as? OpenHABSelectionTableViewController)! - let selectedWidget: OpenHABWidget? = relevantWidget(indexPath: indexPath) - selectionViewController.title = selectedWidget?.labelText - selectionViewController.mappings = selectedWidget?.mappingsOrItemOptions ?? [] + let layout = UICollectionViewCompositionalLayout.list( + using: UICollectionLayoutListConfiguration(appearance: .insetGrouped) + ) + let selectionViewController = OpenHABSelectionCollectionViewController(collectionViewLayout: layout) + selectionViewController.title = widget.labelText + selectionViewController.mappings = widget.mappingsOrItemOptions selectionViewController.delegate = self - selectionViewController.selectionItem = selectedWidget?.item - navigationController?.pushViewController(selectionViewController, animated: true) + selectionViewController.selectionWidget = widget + show(selectionViewController, sender: self) } - if let index = widgetTableView.indexPathForSelectedRow { - widgetTableView.deselectRow(at: index, animated: false) + if let index = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: index, animated: false) } } diff --git a/openHAB/UICollectionViewCellRegistrationExtension.swift b/openHAB/UICollectionViewCellRegistrationExtension.swift new file mode 100644 index 000000000..4ca91b93a --- /dev/null +++ b/openHAB/UICollectionViewCellRegistrationExtension.swift @@ -0,0 +1,25 @@ +// Copyright (c) 2010-2024 Contributors to the openHAB project +// +// See the NOTICE file(s) distributed with this work for additional +// information. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0 +// +// SPDX-License-Identifier: EPL-2.0 + +import Foundation +import UIKit + +extension UICollectionView.CellRegistration { + var cellProvider: (UICollectionView, IndexPath, Item) -> Cell { + { collectionView, indexPath, product in + collectionView.dequeueConfiguredReusableCell( + using: self, + for: indexPath, + item: product + ) + } + } +} diff --git a/openHABWatch Extension/Views/Rows/ImageRow.swift b/openHABWatch Extension/Views/Rows/ImageRow.swift index feb886a3d..8c0ee46a7 100644 --- a/openHABWatch Extension/Views/Rows/ImageRow.swift +++ b/openHABWatch Extension/Views/Rows/ImageRow.swift @@ -26,7 +26,7 @@ struct ImageRow: View { os_log("Failure loading icon: %{PUBLIC}s", log: .notifications, type: .debug, kingfisherError.localizedDescription) } .placeholder { - Image(systemSymbol: .arrow2CirclepathCircle) + Image(systemSymbol: .arrowTriangle2CirclepathCircle) .font(.callout) .opacity(0.3) }