From 5a021be7a7317c5979029f569b32a0dcc9d362a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=BCller-Seydlitz?= Date: Tue, 30 Jul 2024 23:57:15 +0200 Subject: [PATCH] Migrate to regex introduced in swift 5.7 Migrate swift-tools-version to 5.9 Uplift target for OpenHABCore to iOS 16, watchOS 9 - Consistently upgrade target of watch app to watchOS 9 --- NotificationService/NotificationService.swift | 95 +++++++++---------- OpenHABCore/Package.swift | 14 +-- .../Model/OpenHABStateDescription.swift | 18 ++-- .../OpenHABCore/Model/OpenHABWidget.swift | 8 +- .../OpenHABCoreGeneralTests.swift | 3 +- openHAB.xcodeproj/project.pbxproj | 6 +- .../Model/ObservableOpenHABWidget.swift | 10 +- 7 files changed, 71 insertions(+), 83 deletions(-) diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index 038aacdc..b63d6a0a 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -18,23 +18,23 @@ import UserNotifications class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? - + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) if let bestAttemptContent { var notificationActions: [UNNotificationAction] = [] let userInfo = bestAttemptContent.userInfo - + os_log("didReceive userInfo %{PUBLIC}@", log: .default, type: .info, userInfo) - + if let title = userInfo["title"] as? String { bestAttemptContent.title = title } if let message = userInfo["message"] as? String { bestAttemptContent.body = message } - + // Check if the user has defined custom actions in the payload if let actionsArray = parseActions(userInfo), let category = parseCategory(userInfo) { for actionDict in actionsArray { @@ -56,12 +56,12 @@ class NotificationService: UNNotificationServiceExtension { if !notificationActions.isEmpty { os_log("didReceive registering %{PUBLIC}@ for category %{PUBLIC}@", log: .default, type: .info, notificationActions, category) let notificationCategory = - UNNotificationCategory( - identifier: category, - actions: notificationActions, - intentIdentifiers: [], - options: .customDismissAction - ) + UNNotificationCategory( + identifier: category, + actions: notificationActions, + intentIdentifiers: [], + options: .customDismissAction + ) UNUserNotificationCenter.current().getNotificationCategories { existingCategories in var updatedCategories = existingCategories os_log("handleNotification adding category %{PUBLIC}@", log: .default, type: .info, category) @@ -70,13 +70,13 @@ class NotificationService: UNNotificationServiceExtension { } } } - + // check if there is an attachment to put on the notification // this should be last as we need to wait for media // TODO: we should support relative paths and try the user's openHAB (local,remote) for content if let attachmentURLString = userInfo["media-attachment-url"] as? String { let isItem = attachmentURLString.starts(with: "item:") - + let downloadCompletionHandler: @Sendable (UNNotificationAttachment?) -> Void = { attachment in if let attachment { os_log("handleNotification attaching %{PUBLIC}@", log: .default, type: .info, attachmentURLString) @@ -86,7 +86,7 @@ class NotificationService: UNNotificationServiceExtension { } contentHandler(bestAttemptContent) } - + if isItem { downloadAndAttachItemImage(itemURI: attachmentURLString, completion: downloadCompletionHandler) } else { @@ -97,7 +97,7 @@ class NotificationService: UNNotificationServiceExtension { } } } - + override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. @@ -106,7 +106,7 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(bestAttemptContent) } } - + private func parseActions(_ userInfo: [AnyHashable: Any]) -> [[String: String]]? { // Extract actions and convert it from JSON string to an array of dictionaries if let actionsString = userInfo["actions"] as? String, let actionsData = actionsString.data(using: .utf8) { @@ -120,7 +120,7 @@ class NotificationService: UNNotificationServiceExtension { } return nil } - + private func parseCategory(_ userInfo: [AnyHashable: Any]) -> String? { // Extract category from aps dictionary if let aps = userInfo["aps"] as? [String: Any], @@ -129,10 +129,10 @@ class NotificationService: UNNotificationServiceExtension { } return nil } - + private func downloadAndAttachMedia(url: String, completion: @escaping (UNNotificationAttachment?) -> Void) { let client = HTTPClient(username: Preferences.username, password: Preferences.username, alwaysSendBasicAuth: Preferences.alwaysSendCreds) - + let downloadCompletionHandler: @Sendable (URL?, URLResponse?, Error?) -> Void = { (localURL, response, error) in guard let localURL else { os_log("Error downloading media %{PUBLIC}@", log: .default, type: .error, error?.localizedDescription ?? "Unknown error") @@ -147,16 +147,16 @@ class NotificationService: UNNotificationServiceExtension { client.downloadFile(url: url, completionHandler: downloadCompletionHandler) } } - + func downloadAndAttachItemImage(itemURI: String, completion: @escaping (UNNotificationAttachment?) -> Void) { guard let itemURI = URL(string: itemURI), let scheme = itemURI.scheme else { os_log("Could not find scheme %{PUBLIC}@", log: .default, type: .info) completion(nil) return } - + let itemName = String(itemURI.absoluteString.dropFirst(scheme.count + 1)) - + let client = HTTPClient(username: Preferences.username, password: Preferences.password, alwaysSendBasicAuth: Preferences.alwaysSendCreds) client.getItem(baseURLs: [Preferences.localUrl, Preferences.remoteUrl], itemName: itemName) { item, error in guard let item else { @@ -165,50 +165,43 @@ class NotificationService: UNNotificationServiceExtension { return } if let state = item.state { - do { - // Extract MIME type and base64 string - let pattern = "^data:(.*?);base64,(.*)$" - let regex = try NSRegularExpression(pattern: pattern, options: []) - if let match = regex.firstMatch(in: state, options: [], range: NSRange(location: 0, length: state.utf16.count)) { - let mimeTypeRange = Range(match.range(at: 1), in: state) - let base64Range = Range(match.range(at: 2), in: state) - if let mimeTypeRange, let base64Range { - let mimeType = String(state[mimeTypeRange]) - let base64String = String(state[base64Range]) - if let imageData = Data(base64Encoded: base64String) { - // Create a temporary file URL - let tempDirectory = FileManager.default.temporaryDirectory - let tempFileURL = tempDirectory.appendingPathComponent(UUID().uuidString) - do { - try imageData.write(to: tempFileURL) - os_log("Image saved to temporary file: %{PUBLIC}@", log: .default, type: .info, tempFileURL.absoluteString) - self.attachFile(localURL: tempFileURL, mimeType: mimeType, completion: completion) - return - } catch { - os_log("Failed to write image data to file: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription) - } - } else { - os_log("Failed to decode base64 string to Data", log: .default, type: .error) - } + // Extract MIME type and base64 string + let pattern = /^data:(.*?);base64,(.*)$/ + if let firstMatch = state.firstMatch(of: pattern) { + let mimeType = String(firstMatch.1) + let base64String = String(firstMatch.2) + if let imageData = Data(base64Encoded: base64String) { + // Create a temporary file URL + let tempDirectory = FileManager.default.temporaryDirectory + let tempFileURL = tempDirectory.appendingPathComponent(UUID().uuidString) + do { + try imageData.write(to: tempFileURL) + os_log("Image saved to temporary file: %{PUBLIC}@", log: .default, type: .info, tempFileURL.absoluteString) + self.attachFile(localURL: tempFileURL, mimeType: mimeType, completion: completion) + return + } catch { + os_log("Failed to write image data to file: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription) } + } else { + os_log("Failed to decode base64 string to Data", log: .default, type: .error) } - } catch { - os_log("Failed to parse data: %{PUBLIC}@", log: .default, type: .error, error.localizedDescription) + } else { + os_log("Failed to parse data: %{PUBLIC}@", log: .default, type: .error, error?.localizedDescription ?? "") } } completion(nil) } } - + func attachFile(localURL: URL, mimeType: String?, completion: @escaping (UNNotificationAttachment?) -> Void) { do { let fileManager = FileManager.default let tempDirectory = NSTemporaryDirectory() let tempFile = URL(fileURLWithPath: tempDirectory).appendingPathComponent(UUID().uuidString) - + try fileManager.moveItem(at: localURL, to: tempFile) let attachment: UNNotificationAttachment? - + if let mimeType, let utType = UTType(mimeType: mimeType), utType.conforms(to: .data) { diff --git a/OpenHABCore/Package.swift b/OpenHABCore/Package.swift index 8363eda5..110808da 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(.v9)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( @@ -15,8 +15,8 @@ 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") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -26,14 +26,16 @@ let package = Package( dependencies: [ .product(name: "Alamofire", package: "Alamofire", condition: .when(platforms: [.iOS, .watchOS])), .product(name: "Kingfisher", package: "Kingfisher", condition: .when(platforms: [.iOS, .watchOS])) - ] + ], + swiftSettings: [.enableUpcomingFeature("BareSlashRegexLiterals")] ), .testTarget( name: "OpenHABCoreTests", dependencies: ["OpenHABCore"], resources: [ .process("Resources") - ] + ], + swiftSettings: [.enableUpcomingFeature("BareSlashRegexLiterals")] ) ] ) diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABStateDescription.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABStateDescription.swift index 03511972..aa5851f5 100644 --- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABStateDescription.swift +++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABStateDescription.swift @@ -21,7 +21,7 @@ public class OpenHABStateDescription { public var numberPattern: String? - init(minimum: Double?, maximum: Double?, step: Double?, readOnly: Bool?, options: [OpenHABOptions]?, pattern: String?) { + init(minimum: Double?, maximum: Double?, step: Double?, readOnly: Bool?, options: [OpenHABOptions]?, pattern tobeSearched: String?) { self.minimum = minimum ?? 0.0 self.maximum = maximum ?? 100.0 self.step = step ?? 1.0 @@ -29,17 +29,13 @@ public class OpenHABStateDescription { self.options = options ?? [] // Remove transformation instructions (e.g. for 'MAP(foo.map):%s' keep only '%s') - - let regexPattern = #"^[A-Z]+(\(.*\))?:(.*)$"# - let regex = try? NSRegularExpression(pattern: regexPattern, options: .caseInsensitive) - if let pattern { - let nsrange = NSRange(pattern.startIndex ..< pattern.endIndex, in: pattern) - if let match = regex?.firstMatch(in: pattern, options: [], range: nsrange) { - if let range = Range(match.range(at: 2), in: pattern) { - numberPattern = String(pattern[range]) - } + + let regexPattern = /^[A-Z]+(\(.*\))?:(.*)$/.ignoresCase() + if let tobeSearched { + if let firstMatch = tobeSearched.firstMatch(of: regexPattern){ + numberPattern = String(firstMatch.2) } else { - numberPattern = pattern + numberPattern = tobeSearched } } else { numberPattern = nil diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift index 44773b73..b881bd56 100644 --- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift +++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift @@ -107,11 +107,9 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable { // Text between square brackets public var labelValue: String? { - // Swift 5 raw strings - let regex = try? NSRegularExpression(pattern: #"\[(.*?)\]"#, options: [.dotMatchesLineSeparators]) - guard let match = regex?.firstMatch(in: label, options: [], range: NSRange(location: 0, length: (label as NSString).length)) else { return nil } - guard let range = Range(match.range(at: 1), in: label) else { return nil } - return String(label[range]) + let pattern = /\[(.*?)\]/.dotMatchesNewlines() + guard let firstMatch = label.firstMatch(of: pattern) else { return nil } + return String(firstMatch.1) } public var coordinate: CLLocationCoordinate2D { diff --git a/OpenHABCore/Tests/OpenHABCoreTests/OpenHABCoreGeneralTests.swift b/OpenHABCore/Tests/OpenHABCoreTests/OpenHABCoreGeneralTests.swift index 773c1443..1ed604ae 100644 --- a/OpenHABCore/Tests/OpenHABCoreTests/OpenHABCoreGeneralTests.swift +++ b/OpenHABCore/Tests/OpenHABCoreTests/OpenHABCoreGeneralTests.swift @@ -28,7 +28,8 @@ final class OpenHABCoreGeneralTests: XCTestCase { XCTAssertEqual(urlc, URL(string: "http://192.169.2.1/icon/switch?state=OFF&format=SVG"), "Check endpoint creation") } - func testLabelVale() { + @available(iOS 16.0, *) + func testLabelValue() { let widget = OpenHABWidget() widget.label = "llldl [llsl]" XCTAssertEqual(widget.labelValue, "llsl") diff --git a/openHAB.xcodeproj/project.pbxproj b/openHAB.xcodeproj/project.pbxproj index 4cb5a094..e7754ea1 100644 --- a/openHAB.xcodeproj/project.pbxproj +++ b/openHAB.xcodeproj/project.pbxproj @@ -1821,7 +1821,7 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = openHAB; - WATCHOS_DEPLOYMENT_TARGET = 6.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Debug; }; @@ -1909,7 +1909,7 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; VERSIONING_SYSTEM = "apple-generic"; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -1956,7 +1956,7 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; VERSIONING_SYSTEM = "apple-generic"; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; diff --git a/openHABWatch Extension/openHABWatch Extension/Model/ObservableOpenHABWidget.swift b/openHABWatch Extension/openHABWatch Extension/Model/ObservableOpenHABWidget.swift index 9241caa9..8e0c798b 100644 --- a/openHABWatch Extension/openHABWatch Extension/Model/ObservableOpenHABWidget.swift +++ b/openHABWatch Extension/openHABWatch Extension/Model/ObservableOpenHABWidget.swift @@ -83,12 +83,10 @@ class ObservableOpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableO } // Text between square brackets - var labelValue: String? { - // Swift 5 raw strings - let regex = try? NSRegularExpression(pattern: #"\[(.*?)\]"#, options: []) - guard let match = regex?.firstMatch(in: label, options: [], range: NSRange(location: 0, length: (label as NSString).length)) else { return nil } - guard let range = Range(match.range(at: 1), in: label) else { return nil } - return String(label[range]) + public var labelValue: String? { + let pattern = /\[(.*?)\]/.dotMatchesNewlines() + guard let firstMatch = label.firstMatch(of: pattern) else { return nil } + return String(firstMatch.1) } var coordinate: CLLocationCoordinate2D {