Skip to content

Commit

Permalink
Migrate to regex introduced in swift 5.7
Browse files Browse the repository at this point in the history
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
  • Loading branch information
timbms committed Jul 30, 2024
1 parent 111e39b commit 5a021be
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 83 deletions.
95 changes: 44 additions & 51 deletions NotificationService/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -86,7 +86,7 @@ class NotificationService: UNNotificationServiceExtension {
}
contentHandler(bestAttemptContent)
}

if isItem {
downloadAndAttachItemImage(itemURI: attachmentURLString, completion: downloadCompletionHandler)
} else {
Expand All @@ -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.
Expand All @@ -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) {
Expand All @@ -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],
Expand All @@ -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")
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
14 changes: 8 additions & 6 deletions OpenHABCore/Package.swift
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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.
Expand All @@ -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")]
)
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,21 @@ 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
self.readOnly = readOnly ?? false
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
Expand Down
8 changes: 3 additions & 5 deletions OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
6 changes: 3 additions & 3 deletions openHAB.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 5a021be

Please sign in to comment.