├── .gitignore ├── .travis.yml ├── LICENSE ├── Package.swift ├── README.md ├── Robin.podspec ├── Sources └── Robin │ ├── Actions │ ├── RobinActionHandler.swift │ ├── RobinActionRegistrar.swift │ └── RobinActions.swift │ ├── Center │ ├── NotificationCenterManager.swift │ ├── RobinNotificationCenter.swift │ ├── RobinNotificationCenterManager.swift │ └── UNUserNotificationCenter+RobinNotificationCenter.swift │ ├── Constants.swift │ ├── Date+Robin.swift │ ├── Delegate │ ├── NotificationDelegate.swift │ └── RobinDelegate.swift │ ├── Notification │ ├── RobinNotification+CustomStringConvertible.swift │ ├── RobinNotification+Date.swift │ ├── RobinNotification+Equatable.swift │ ├── RobinNotification.swift │ ├── RobinNotificationGroup.swift │ ├── RobinNotificationRepeats.swift │ ├── RobinNotificationResponse.swift │ ├── RobinNotificationTrigger.swift │ ├── Sound │ │ ├── RobinNotificationSound.swift │ │ ├── String+SystemNotificationSound.swift │ │ ├── SystemNotificationSound.swift │ │ └── UNNotificationSound+SystemNotificationSound.swift │ └── System │ │ ├── DeliveredSystemNotification.swift │ │ ├── RobinNotification+UNNotificationRequest.swift │ │ ├── SystemNotification.swift │ │ ├── SystemNotificationResponse.swift │ │ ├── UNCalendarNotificationTrigger+Robin.swift │ │ ├── UNNotification+DeliveredSystemNotification.swift │ │ ├── UNNotificationRequest+SystemNotification.swift │ │ └── UNNotificationResponse+SystemNotificationResponse.swift │ ├── Robin.swift │ ├── Scheduler │ ├── NotificationsScheduler.swift │ └── RobinScheduler.swift │ └── Settings │ ├── NotificationSettings.swift │ ├── RobinNotificationSettings.swift │ ├── RobinSettingsManager.swift │ ├── RobinSettingsOptions.swift │ └── System │ ├── SystemNotificationSettings.swift │ └── UNNotificationSettings+SystemNotificationSettings.swift └── Tests └── RobinTests ├── Actions └── RobinActionRegistrarTests.swift ├── Date+RobinTests.swift ├── Delegate └── RobinDelegateTests.swift ├── Manager └── RobinManagerTests.swift ├── Mocks ├── DeliveredSystemNotificationMock.swift ├── RobinNotificationCenterMock.swift ├── SystemNotificationResponseMock.swift └── SystemNotificationSettingsMock.swift ├── Notification ├── RobinNotificationGroupTests.swift ├── RobinNotificationRepeatsTests.swift ├── RobinNotificationTests.swift └── UNCalendarNotificationTrigger+RobinTests.swift ├── Scheduler └── RobinSchedulerTests.swift └── Settings ├── RobinNotificationSettingsTests.swift └── RobinSettingsTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | /*.xcodeproj 22 | 23 | # SPM 24 | /.build 25 | .swiftpm/ 26 | /Packages 27 | 28 | # Bundler 29 | .bundle 30 | 31 | Carthage 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 35 | # 36 | # Note: if you ignore the Pods directory, make sure to uncomment 37 | # `pod install` in .travis.yml 38 | # 39 | Pods/ 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | language: swift 6 | 7 | install: swift package generate-xcodeproj --xcconfig-overrides conf.xcconfig 8 | 9 | jobs: 10 | include: 11 | # macOS 12 | - name: macOS 10.14 13 | osx_image: xcode10.2 14 | xcode_project: Robin.xcodeproj 15 | xcode_scheme: Robin-Package 16 | xcode_destination: platform=macOS 17 | before_install: echo "MACOSX_DEPLOYMENT_TARGET = 10.14" >> conf.xcconfig 18 | - name: macOS 10.15 19 | osx_image: xcode11.4 20 | xcode_project: Robin.xcodeproj 21 | xcode_scheme: Robin-Package 22 | xcode_destination: platform=macOS 23 | before_install: echo "MACOSX_DEPLOYMENT_TARGET = 10.14" >> conf.xcconfig 24 | - name: macOS 11.0 25 | osx_image: xcode12.2 26 | xcode_project: Robin.xcodeproj 27 | xcode_scheme: Robin-Package 28 | xcode_destination: platform=macOS 29 | before_install: echo "MACOSX_DEPLOYMENT_TARGET = 10.14" >> conf.xcconfig 30 | # iOS 31 | - name: iOS 10.3.1 32 | osx_image: xcode11.1 33 | xcode_project: Robin.xcodeproj 34 | xcode_scheme: Robin-Package 35 | xcode_destination: platform=iOS Simulator,OS=10.3.1,name=iPhone 7 36 | before_install: echo "IPHONEOS_DEPLOYMENT_TARGET = 10.0" >> conf.xcconfig 37 | - name: iOS 11.0.1 38 | osx_image: xcode11.1 39 | xcode_project: Robin.xcodeproj 40 | xcode_scheme: Robin-Package 41 | xcode_destination: platform=iOS Simulator,OS=11.0.1,name=iPhone 8 42 | before_install: echo "IPHONEOS_DEPLOYMENT_TARGET = 10.0" >> conf.xcconfig 43 | - name: iOS 12.0 44 | osx_image: xcode11.1 45 | xcode_project: Robin.xcodeproj 46 | xcode_scheme: Robin-Package 47 | xcode_destination: platform=iOS Simulator,OS=12.0,name=iPhone 8 48 | before_install: echo "IPHONEOS_DEPLOYMENT_TARGET = 10.0" >> conf.xcconfig 49 | - name: iOS 13.1 50 | osx_image: xcode11.1 51 | xcode_project: Robin.xcodeproj 52 | xcode_scheme: Robin-Package 53 | xcode_destination: platform=iOS Simulator,OS=13.1,name=iPhone 8 54 | before_install: echo "IPHONEOS_DEPLOYMENT_TARGET = 10.0" >> conf.xcconfig 55 | - name: iOS 14.0.1 56 | osx_image: xcode12.2 57 | xcode_project: Robin.xcodeproj 58 | xcode_scheme: Robin-Package 59 | xcode_destination: platform=iOS Simulator,OS=14.0.1,name=iPhone 8 60 | before_install: echo "IPHONEOS_DEPLOYMENT_TARGET = 10.0" >> conf.xcconfig 61 | # watchOS 62 | - name: watchOS 6.0 63 | osx_image: xcode11.1 64 | before_install: echo "WATCHOS_DEPLOYMENT_TARGET = 3.0" >> conf.xcconfig 65 | script: set -o pipefail && xcodebuild -project Robin.xcodeproj -scheme Robin-Package -destination platform\=watchOS\ Simulator,OS\=6.0,name\=Apple\ Watch\ Series\ 4\ -\ 40mm build | xcpretty 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ahmed Mohamed 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Robin", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "Robin", 12 | targets: ["Robin"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | ], 17 | targets: [ 18 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 19 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 20 | .target( 21 | name: "Robin", 22 | dependencies: [ 23 | ]), 24 | .testTarget( 25 | name: "RobinTests", 26 | dependencies: ["Robin"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Platform](https://img.shields.io/cocoapods/p/Robin.svg)](http://cocoapods.org/pods/Robin) 3 | [![Version](https://img.shields.io/cocoapods/v/Robin.svg)](http://cocoapods.org/pods/Robin) 4 | [![Swift 4+](https://img.shields.io/badge/Swift-4.2%2B-orange.svg)](https://swift.org) 5 | [![CI Status](http://img.shields.io/travis/ahmdx/Robin.svg)](https://travis-ci.org/ahmdx/Robin) 6 | [![License](https://img.shields.io/github/license/ahmdx/Robin.svg)](http://cocoapods.org/pods/Robin) 7 | [![Release](https://img.shields.io/github/release/ahmdx/Robin.svg)](https://github.com/ahmdx/Robin/releases/) 8 | 9 | Robin is a multi-platform notification interface for iOS, watchOS, and macOS that handles UserNotifications behind the scenes. 10 | 11 | ```swift 12 | let notification = RobinNotification(body: "Welcome to Robin!", trigger: .interval(5)) 13 | 14 | _ = Robin.scheduler.schedule(notification: notification) 15 | ``` 16 | 17 | - [Features](#features) 18 | - [Requirements](#requirements) 19 | - [Communication](#communication) 20 | - [Installation](#installation) 21 | - [Usage](#usage) 22 | - [Robin Settings](#robin-settings) 23 | - [Robin Scheduler](#robin-scheduler) 24 | - [Schedule a notification](#schedule-a-notification) 25 | - [Retrieve a scheduled notification](#retrieve-a-scheduled-notification) 26 | - [Cancel a scheduled notification](#cancel-a-scheduled-notification) 27 | - [Schedule a notification group](#schedule-a-notification-group) 28 | - [Retrieve a scheduled notification group](#retrieve-a-scheduled-notification-group) 29 | - [Cancel a scheduled notification group](#cancel-a-scheduled-notification-group) 30 | - [Robin Actions](#robin-actions) 31 | - [Robin Delegate](#robin-delegate) 32 | - [Robin Manager](#robin-manager) 33 | - [Retrieve all delivered notifications](#retrieve-all-delivered-notifications) 34 | - [Remove a delivered notification](#remove-a-delivered-notification) 35 | - [Notes](#notes) 36 | - [Author](#author) 37 | - [License](#license) 38 | 39 | # Features 40 | 41 | - [x] Request notification permissions 42 | - [x] Query notification settings 43 | - [x] Schedule date, location, and interval notifications 44 | - [x] Handle notification response actions (including text responses) 45 | - [x] Manage delivered notifications in the notification center 46 | - [ ] Push notifications support 47 | - [x] iOS, watchOS, and macOS support 48 | - [x] High test coverage 49 | - [x] Swift Package Manager 50 | - [x] CocoaPods 51 | 52 | # Requirements 53 | 54 | - iOS 10.0+ 55 | - watchOS 3.0+ 56 | - macOS 10.14+ 57 | - Xcode 11.1+ 58 | - Swift 4.2+ 59 | 60 | # Communication 61 | 62 | - If you need help or have a question, use 'robin' tag on [Stack Overflow](http://stackoverflow.com/questions/tagged/robin). 63 | - If you found a bug or have a feature request, please open an issue. 64 | 65 | Please do not open a pull request until a contribution guide is published. 66 | 67 | # Installation 68 | 69 | Robin is available through both [Swift Package Manager](https://swift.org/package-manager/) and [CocoaPods](http://cocoapods.org). 70 | 71 | To install using SPM: 72 | 73 | ```swift 74 | .package(url: "https://github.com/ahmdx/Robin", from: "0.98.0"), 75 | ``` 76 | 77 | CocoaPods: 78 | 79 | ```ruby 80 | pod 'Robin', '~> 0.98.0' 81 | ``` 82 | 83 | And if you want to include the test suite in your project: 84 | 85 | ```ruby 86 | pod 'Robin', '~> 0.98.0', :testspecs => ['Tests'] 87 | ``` 88 | 89 | # Usage 90 | 91 | ```swift 92 | import Robin 93 | ``` 94 | 95 | Robin is divided into multiple sub-modules (discussed below): 96 | 97 | - Actions 98 | - Delegate 99 | - Manager 100 | - Scheduler 101 | - Settings 102 | 103 | Each sub-module has separate concerns which allows Robin to be modular and easily extended. Each section below, ordered by their entry point, will discuss the different sub-modules. 104 | 105 | ## Robin Settings 106 | 107 | The settings sub-module is concerned with requesting notification permissions and querying the app's available notification settings. 108 | 109 | Before using `Robin`, you need to request permission to send notifications to users. The following requests `badge`, `sound`, and `alert` permissions. For all available options, refer to [UNAuthorizationOptions](https://developer.apple.com/documentation/usernotifications/unauthorizationoptions). 110 | 111 | ```swift 112 | Robin.settings.requestAuthorization(forOptions: [.badge, .sound, .alert]) { grant, error in 113 | // Handle authorization or error. 114 | } 115 | ``` 116 | 117 | To query for the app's notification settings, you can use `Robin.settings` as follows: 118 | 119 | ```swift 120 | let alertStyle = Robin.settings.alertStyle 121 | ``` 122 | 123 | > Note: `alertStyle` is not available on watchOS. 124 | 125 | ```swift 126 | let authorizationStatus = Robin.settings.authorizationStatus 127 | ``` 128 | 129 | ```swift 130 | let enabledSettings = Robin.settings.enabledSettings 131 | ``` 132 | 133 | > Note: This returns an option set of all the enabled settings. If some settings are not included in the set, they may be disabled or not supported. If you would like to know if some specific setting is enabled, you can use `enabledSettings.contains(.sound)` for example. For more details, refer to `RobinSettingsOptions`. 134 | 135 | ```swift 136 | let showPreviews = Robin.settings.showPreviews 137 | ``` 138 | 139 | > Note: `showPreviews` is not available on watchOS. 140 | 141 | Robin automatically updates information about the app's settings when the app goes into an inactive state and becomes active again to avoid unnecessary queries. If you would like to override this behavior and update the information manually, you can use `forceRefresh()`. 142 | 143 | ```swift 144 | Robin.settings.forceRefresh() 145 | ``` 146 | 147 | > Note: Robin on watchOS does not support automatic settings refresh. 148 | 149 | ## Robin Scheduler 150 | 151 | The scheduler sub-module is concerned with scheduling and managing scheduled notifications; rescheduling, canceling, and retrieving notifications. 152 | 153 | Scheduling notifications via `Robin` is carried over by manipulating `RobinNotification` objects. To create a `RobinNotification` object, simply call its initializer. 154 | 155 | ```swift 156 | init(identifier: String = default, body: String, trigger: RobinNotificationTrigger = default) 157 | ``` 158 | 159 | Example notification, with a unique identifier, to be fired an hour from now. 160 | 161 | ```swift 162 | let notification = RobinNotification(body: "A notification", trigger: .date(.next(hours: 1), repeats: .none)) 163 | ``` 164 | 165 | > Note: `next(minutes:)`, `next(hours:)`, `next(days:)`, `next(weeks:)`, `next(months:)`, and `next(years:)` are part of a `Date` extension. Robin supports the following repeating interval `.none`, `.hour`, `.day`, `.week`, and `.month`. 166 | 167 | Example notification, with a unique identifier, to be fired 30 seconds from now. 168 | 169 | ```swift 170 | let notification = RobinNotification(body: "A notification", trigger: .interval(30, repeats: false)) 171 | ``` 172 | 173 | > Note: Repeating a notification with an interval of less than 60 seconds might cause the app to crash. 174 | 175 | Example notification, with a unique identifier, to be fired when entering a region. 176 | 177 | ```swift 178 | /// https://developer.apple.com/documentation/usernotifications/unlocationnotificationtrigger 179 | let center = CLLocationCoordinate2D(latitude: 37.335400, longitude: -122.009201) 180 | let region = CLCircularRegion(center: center, radius: 2000.0, identifier: "Headquarters") 181 | region.notifyOnEntry = true 182 | region.notifyOnExit = false 183 | 184 | let notification = RobinNotification(body: "A notification", trigger: .location(region)) 185 | ``` 186 | 187 | > Note: For the system to deliver location notifications, the app should be authorized to use location services, see [here](https://developer.apple.com/documentation/usernotifications/unlocationnotificationtrigger). *Robin does not check whether the required permissions are granted before scheduling a location notification. Location notifications are not available on either macOS or watchOS.* 188 | 189 | The following table summarizes all `RobinNotification` properties. 190 | 191 | | Property | Type | Description | 192 | | --- | --- | --- | 193 | | badge | `NSNumber?` | The number the notification should display on the app icon. | 194 | | body | `String!` | The body string of the notification. | 195 | | categoryIdentifier | `String?` | The identifier of the notification's category. | 196 | | delivered | `Bool` | The delivery status of the notification. `read-only` | 197 | | identifier[1] | `String!` | A string assigned to the notification for later access. | 198 | | scheduled | `Bool` | The status of the notification. `read-only` | 199 | | sound | `RobinNotificationSound` | The sound name of the notification. `not available on watchOS`| 200 | | threadIdentifier | `String?` | The identifier used to visually group notifications together. | 201 | | title | `String?` | The title string of the notification. | 202 | | trigger | `enum` | The trigger that causes the notification to fire. One of `date`, `interval`, or `location`. | 203 | | userInfo[2] | `[AnyHashable : Any]!` | A dictionary that holds additional information. | 204 | 205 | [1] `identifier` is read-only after `RobinNotification` is initialized. 206 | 207 | [2] To add and remove keys in `userInfo`, use `setUserInfo(value: Any, forKey: AnyHashable)` and `removeUserInfoValue(forKey: AnyHashable)` respectively. 208 | 209 | ### Schedule a notification 210 | 211 | After creating a `RobinNotification` object, it can be scheduled using `schedule(notification: RobinNotification)`. 212 | 213 | ```swift 214 | let scheduledNotification = Robin.scheduler.schedule(notification: notification) 215 | ``` 216 | 217 | Now `scheduledNotification` is a valid `RobinNotification` object if it is successfully scheduled or `nil` otherwise. 218 | 219 | ### Retrieve a scheduled notification 220 | 221 | Simply retrieve a scheduled notification by calling `notification(withIdentifier: String) -> RobinNotification?`. 222 | 223 | ```swift 224 | let scheduledNotification = Robin.scheduler.notification(withIdentifier: "identifier") 225 | ``` 226 | 227 | ### Cancel a scheduled notification 228 | 229 | To cancel a notification, either call `cancel(notification: RobinNotification)` or `cancel(withIdentifier: String)` 230 | 231 | ```swift 232 | Robin.scheduler.cancel(notification: scheduledNotification) 233 | ``` 234 | 235 | ```swift 236 | Robin.scheduler.cancel(withIdentifier: scheduledNotification.identifier) 237 | ``` 238 | 239 | `Robin` allows you to cancel all scheduled notifications by calling `cancelAll()` 240 | 241 | ```swift 242 | Robin.scheduler.cancelAll() 243 | ``` 244 | 245 | ### Schedule a notification group 246 | 247 | Robin utilizes the `threadIdentifier` property to manage multiple notifications as a group under the same identifier. To group multiple notifications under the same identifier, you can either set `threadIdentifier` of the notification to the same string or schedule a notification group by calling `schedule(group: RobinNotificationGroup)`: 248 | 249 | ```swift 250 | let notifications = ... // an array of `RobinNotification` 251 | let group = RobinNotificationGroup(notifications: notifications) 252 | // or 253 | let group = RobinNotificationGroup(notifications: notifications, identifier: "group_identifier") 254 | 255 | let scheduledGroup = Robin.scheduler.schedule(group: group) 256 | ``` 257 | 258 | Robin will automatically set `threadIdentifier` of each `RobinNotification` in the array to the group's identifier. *If you omit `identifier` when initializing the group, Robin will create a unique identifier.* 259 | 260 | ### Retrieve a scheduled notification group 261 | 262 | Simply retrieve a scheduled notification group by calling `group(withIdentifier: String) -> RobinNotificationGroup?`. 263 | 264 | ```swift 265 | let scheduledGroup = Robin.scheduler.group(withIdentifier: "group_identifier") 266 | ``` 267 | 268 | *Robin will return a group if there exists at least one notification with `threadIdentifier` the same as the group's identifier. The returned group might contain less notifications than initially scheduled since some of them might have already been delivered by the system.* 269 | 270 | ### Cancel a scheduled notification group 271 | 272 | To cancel a notification group, either call `cancel(group: RobinNotificationGroup)` or `cancel(groupWithIdentifier: String)` 273 | 274 | ```swift 275 | Robin.scheduler.cancel(group: group) 276 | ``` 277 | 278 | ```swift 279 | Robin.scheduler.cancel(groupWithIdentifer: group.identifer) 280 | ``` 281 | 282 | ## Robin Actions 283 | 284 | Notifications can be scheduled with actions the users can interact with. When a user responds to a notification action, the system then informs the app about the notification and its actions' identifiers to process the user's response. Learn more about setting notification actions and categories [here](https://developer.apple.com/documentation/usernotifications/declaring_your_actionable_notification_types). 285 | 286 | The actions sub-module is concerned with registering and de-registering action handlers that are invoked when a notification response is delivered by the system. *This sub-module requires the delegate sub-module to function properly.* 287 | 288 | If your app uses many different notification actions, it may become cumbersome to handle each action. Robin actions allows you to separate the handling of each notification action type into its own isolated space. To do so, your app will need to create an object for each action type and have it conform to the `RobinActionHandler` protocol: 289 | 290 | ```swift 291 | struct ExampleActionHandler: RobinActionHandler { 292 | func handle(response: RobinNotificationResponse) { 293 | /// Handle the response here. 294 | } 295 | } 296 | ``` 297 | 298 | > Note: `RobinNotificationResponse` contains two properties, the delivered `notification` and an optional `userText?`. `userText` contains the text the user typed when responding to a `UNTextInputNotificationAction`. 299 | 300 | and register the action handler for an action identifier: 301 | 302 | ```swift 303 | Robin.actions.register(actionHandler: ExampleActionHandler.self, forIdentifier: "") 304 | ``` 305 | 306 | If you would like to deregister any registered action handler, simply call: 307 | 308 | ```swift 309 | Robin.actions.deregister(actionHandlerForIdentifier: "") 310 | ``` 311 | 312 | If you want to schedule a notification with actions, set its `categoryIdentifier` property: 313 | 314 | ```swift 315 | notification.categoryIdentifier = "" 316 | ``` 317 | 318 | > Note: You need to set the category identifier in the notification center first as shown [here](https://developer.apple.com/documentation/usernotifications/declaring_your_actionable_notification_types). 319 | 320 | Example from Apple's [documentation](https://developer.apple.com/documentation/usernotifications/declaring_your_actionable_notification_types): 321 | 322 | ```swift 323 | // Define the custom actions. 324 | let acceptAction = UNNotificationAction(identifier: "ACCEPT_ACTION", 325 | title: "Accept", 326 | options: UNNotificationActionOptions(rawValue: 0)) 327 | let declineAction = UNNotificationAction(identifier: "DECLINE_ACTION", 328 | title: "Decline", 329 | options: UNNotificationActionOptions(rawValue: 0)) 330 | // Define the notification type 331 | let meetingInviteCategory = 332 | UNNotificationCategory(identifier: "MEETING_INVITATION", 333 | actions: [acceptAction, declineAction], 334 | intentIdentifiers: [], 335 | hiddenPreviewsBodyPlaceholder: "", 336 | options: .customDismissAction) 337 | // Register the notification type. 338 | let notificationCenter = UNUserNotificationCenter.current() 339 | notificationCenter.setNotificationCategories([meetingInviteCategory]) 340 | 341 | // Register action handlers early in the app's lifecycle. 342 | Robin.actions.register(actionHandler: AcceptActionHandler.self, forIdentifier: "ACCEPT_ACTION") 343 | Robin.actions.register(actionHandler: DeclineActionHandler.self, forIdentifier: "DECLINE_ACTION") 344 | 345 | // Schedule a notification later on. 346 | let notification = RobinNotification(body: "A notification", trigger: .date(.next(hours: 1), repeats: .none)) 347 | notification.categoryIdentifier = "MEETING_INVITATION" 348 | 349 | _ = Robin.scheduler.schedule(notification: notification) 350 | ``` 351 | 352 | Robin uses its delegate sub-module to determine which handler to invoke when the system delivers the notification response to the app. You will need to register the action handlers early in the app's lifecycle to not miss handling the responses. 353 | 354 | ## Robin Delegate 355 | 356 | The delegate sub-module is concerned with handling the notification center's delegate methods. Currently, it only supports processing received notification responses. 357 | 358 | To correctly invoke registered action handlers, your `UNNotificationCenterDelegate` object will need to inform Robin about the response: 359 | 360 | ```swift 361 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 362 | Robin.delegate.didReceiveResponse(response, withCompletionHandler: completionHandler) 363 | } 364 | ``` 365 | 366 | > Note: If you don't pass the `completionHandler`, you will need to call it yourself. 367 | 368 | When a notification response is received, Robin will invoke the correct action handler if it is already registered. 369 | 370 | ## Robin Manager 371 | 372 | The manager sub-module is concerned with managing delivered notifications in the notification center. 373 | ### Retrieve all delivered notifications 374 | 375 | To retrieve all delivered notifications that are displayed in the notification center, call `allDelivered(completionHandler: @escaping ([RobinNotification]) -> Void)`. 376 | 377 | ```swift 378 | Robin.manager.allDelivered { deliveredNotifications in 379 | // Access delivered notifications 380 | } 381 | ``` 382 | 383 | ### Remove a delivered notification 384 | 385 | To remove a delivered notification from the notification center, either call `removeDelivered(notification: RobinNotification)` or `removeDelivered(withIdentifier identifier: String)`. 386 | 387 | ```swift 388 | Robin.manager.removeDelivered(notification: deliveredNotification) 389 | ``` 390 | 391 | ```swift 392 | Robin.manager.removeDelivered(withIdentifier: deliveredNotification.identifier) 393 | ``` 394 | 395 | `Robin` allows you to remove all delivered notifications by calling `removeAllDelivered()` 396 | 397 | ```swift 398 | Robin.manager.removeAllDelivered() 399 | ``` 400 | 401 | # Notes 402 | 403 | `Robin` is preset to allow 60 notifications to be scheduled by the system. The remaining four slots are kept for the app-defined notifications. These free slots are currently not handled by `Robin`; if you use `Robin` to utilize these slots, the notifications will be discarded. 404 | 405 | # Author 406 | 407 | Ahmed Mohamed, dev@ahmd.pro 408 | 409 | # License 410 | 411 | Robin is available under the MIT license. See the [LICENSE](https://github.com/ahmdx/Robin/blob/master/LICENSE) file for more info. 412 | -------------------------------------------------------------------------------- /Robin.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint Robin.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'Robin' 11 | s.version = '0.98.0' 12 | s.summary = 'A feature-packed, multi-platform notification scheduler written in Swift.' 13 | s.swift_version = '4.2' 14 | 15 | # This description is used to generate tags and improve search results. 16 | # * Think: What does it do? Why did you write it? What is the focus? 17 | # * Try to keep it short, snappy and to the point. 18 | # * Write the description between the DESC delimiters below. 19 | # * Finally, don't worry about the indent, CocoaPods strips it! 20 | 21 | s.description = <<-DESC 22 | Robin is a notifications scheduler that provides an interface to schedule notifications using the UserNotifications framework. 23 | DESC 24 | 25 | s.homepage = 'https://github.com/ahmdx/Robin' 26 | s.license = { :type => 'MIT', :file => 'LICENSE' } 27 | s.author = { 'Ahmed Mohamed' => 'dev@ahmd.pro' } 28 | s.source = { :git => 'https://github.com/ahmdx/Robin.git', :tag => s.version.to_s } 29 | 30 | s.ios.deployment_target = '10.0' 31 | s.watchos.deployment_target = '3.0' 32 | s.osx.deployment_target = '10.14' 33 | 34 | s.source_files = 'Sources/**/*' 35 | 36 | s.test_spec 'Tests' do |test_spec| 37 | test_spec.ios.deployment_target = '10.0' 38 | test_spec.osx.deployment_target = '10.14' 39 | 40 | test_spec.source_files = 'Tests/**/*' 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /Sources/Robin/Actions/RobinActionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | /// A protocol that represents objects used to handle notification actions. 24 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 25 | public protocol RobinActionHandler { 26 | init() 27 | 28 | /// Provides the logic to handle a notification response action. 29 | /// 30 | /// - Parameter response: The notification action response. 31 | func handle(response: RobinNotificationResponse) 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Robin/Actions/RobinActionRegistrar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | /// An object that registers and deregisters `RobinActionHandler`s used to handle notification actions. 24 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 25 | internal class RobinActionRegistrar: RobinActions { 26 | fileprivate(set) internal var registeredActions: [String : RobinActionHandler.Type] = [:] 27 | 28 | func register(actionHandler: RobinActionHandler.Type, forIdentifier identifier: String) { 29 | self.registeredActions[identifier] = actionHandler 30 | } 31 | 32 | func deregister(actionHandlerForIdentifier identifier: String) { 33 | self.registeredActions[identifier] = nil 34 | } 35 | 36 | /// Returns the registered `RobinActionHandler` type for the specified identifier. 37 | /// 38 | /// - Parameter identifier: The identifier of the user notification action. 39 | /// - Returns: The `RobinActionHandler` type if it exists. 40 | func action(forIdentifier identifier: String) -> RobinActionHandler.Type? { 41 | return self.registeredActions[identifier] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Robin/Actions/RobinActions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | /// A protocol that represents objects that handle `RobinActionHandler` registration and de-registration. 24 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 25 | public protocol RobinActions { 26 | /// Registers a `RobinActionHandler` type to be used to handle a user notification action having the specified identifier. 27 | /// 28 | /// - Parameters: 29 | /// - action: The `RobinActionHandler` type to register. 30 | /// - identifier: The identifier of the user notification action. 31 | func register(actionHandler: RobinActionHandler.Type, forIdentifier identifier: String) 32 | 33 | /// Deregisters the `RobinActionHandler` type registered for the specified user notification action identifier. 34 | /// 35 | /// - Parameter identifier: The identifier of the user notification action. 36 | func deregister(actionHandlerForIdentifier identifier: String) 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Robin/Center/NotificationCenterManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | /// An object that manages notifications delivered by the system. 26 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 27 | internal class NotificationCenterManager: RobinNotificationCenterManager { 28 | fileprivate let center: RobinNotificationCenter 29 | 30 | init(center: RobinNotificationCenter = UNUserNotificationCenter.current()) { 31 | self.center = center 32 | } 33 | 34 | func allDelivered(completionHandler: @escaping ([RobinNotification]) -> Void) { 35 | center.getDelivered { systemNotifications in 36 | completionHandler(systemNotifications.compactMap { $0.robinNotification() }) 37 | } 38 | } 39 | 40 | func removeDelivered(notification: RobinNotification) { 41 | center.removeDeliveredNotifications(withIdentifiers: [notification.identifier]) 42 | } 43 | 44 | func removeDelivered(withIdentifier identifier: String) { 45 | center.removeDeliveredNotifications(withIdentifiers: [identifier]) 46 | } 47 | 48 | func removeAllDelivered() { 49 | center.removeAllDeliveredNotifications() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Robin/Center/RobinNotificationCenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | protocol RobinNotificationCenter { 27 | func requestAuthorization(options: UNAuthorizationOptions, completionHandler: @escaping (Bool, Error?) -> Void) 28 | /// Returns the app's notifications settings. 29 | /// 30 | /// - note: 31 | /// Normally, the block would take `UNNotificationSettings` as its parameter, however, since it cannot be instantiated for testing, `SystemNotificationSettings` 32 | /// was used for that purpose. 33 | /// 34 | /// - Parameter completionHandler: A block that executes with the app's notifications settings. 35 | func getSettings(completionHandler: @escaping (SystemNotificationSettings) -> Void) 36 | 37 | func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)?) 38 | 39 | func getPendingNotificationRequests(completionHandler: @escaping ([UNNotificationRequest]) -> Void) 40 | 41 | func removePendingNotificationRequests(withIdentifiers identifiers: [String]) 42 | func removeAllPendingNotificationRequests() 43 | 44 | /// Returns a list of the delivered notifications that are displayed in the notification center. 45 | /// 46 | /// - note: 47 | /// Normally, the block would take `UNNotification` as its parameter, however, since it cannot be instantiated for testing, `DeliveredSystemNotification` 48 | /// was used for that purpose. 49 | /// 50 | /// - Parameter completionHandler: A block that executes with an array of the notifications. The array is empty if no notifications are currently displayed. 51 | func getDelivered(completionHandler: @escaping ([DeliveredSystemNotification]) -> Void) 52 | func removeDeliveredNotifications(withIdentifiers identifiers: [String]) 53 | func removeAllDeliveredNotifications() 54 | } 55 | 56 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 57 | extension RobinNotificationCenter { 58 | func requestAuthorization(options: UNAuthorizationOptions = [], completionHandler: @escaping (Bool, Error?) -> Void) { 59 | requestAuthorization(options: options, completionHandler: completionHandler) 60 | } 61 | 62 | func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)? = nil) { 63 | add(request, withCompletionHandler: completionHandler) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Robin/Center/RobinNotificationCenterManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | /// A protocol that represents objects that manage notifications delivered by the system. 24 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 25 | public protocol RobinNotificationCenterManager { 26 | /// Returns the list of delivered notifications that are displayed in the notification center. 27 | /// 28 | /// - Parameter completionHandler: A block that executes with an array of the delivered notifications, if any. If no notifications are displayed, the array is empty. 29 | func allDelivered(completionHandler: @escaping ([RobinNotification]) -> Void) 30 | 31 | /// Removes the passed notification from the notification center if it was delivered. 32 | /// 33 | /// - Parameter identifier: The notification to remove. 34 | func removeDelivered(notification: RobinNotification) 35 | 36 | /// Removes the delivered notification from the notification center having the passed identifier. 37 | /// 38 | /// - Parameter identifier: The identifier of the notification to remove. 39 | func removeDelivered(withIdentifier identifier: String) 40 | 41 | /// Removes all delivered notifications from the notification center. 42 | func removeAllDelivered() 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Robin/Center/UNUserNotificationCenter+RobinNotificationCenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | extension UNUserNotificationCenter: RobinNotificationCenter { 27 | func getSettings(completionHandler: @escaping (SystemNotificationSettings) -> Void) { 28 | self.getNotificationSettings(completionHandler: completionHandler) 29 | } 30 | 31 | func getDelivered(completionHandler: @escaping ([DeliveredSystemNotification]) -> Void) { 32 | self.getDeliveredNotifications(completionHandler: completionHandler) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Robin/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | public struct Constants { 24 | /// The maximum allowed notifications to be scheduled at a time by iOS. 25 | ///- Important: Do not change this value. Changing this value to be over 26 | /// 64 will cause some notifications to be discarded by iOS. 27 | internal static let maximumAllowedSystemNotifications = 64 28 | /// The maximum number of allowed notifications to be scheduled. Four slots 29 | /// are reserved if you would like to schedule notifications without them being dropped 30 | /// due to unavailable notification slots. 31 | ///> Feel free to change this value. 32 | /// 33 | ///- attention: 34 | /// iOS by default allows a maximum of 64 notifications to be scheduled at a time. 35 | /// 36 | ///- seealso: `Constants.maximumAllowedSystemNotifications` 37 | public static var maximumAllowedNotifications = 60 38 | 39 | public struct NotificationKeys { 40 | /// Holds the date of the notification; stored in the `userInfo` property. 41 | public static let date = "RobinNotificationDateKey" 42 | } 43 | 44 | public struct NotificationValues { 45 | /// Used to represent iOS default notification sound name. 46 | public static let defaultSoundName = "RobinNotificationDefaultSound" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/Robin/Date+Robin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | public extension Date { 26 | /// Adds or subtracts a number of minutes to the current data. 27 | /// 28 | /// - Parameter minutes: The number of minutes to add/subtract. 29 | /// - Returns: The current date after the minutes addition/subtraction. 30 | static func next(minutes: Int) -> Date { 31 | return Date().next(minutes: minutes) 32 | } 33 | 34 | /// Adds or subtracts a number of minutes to a date. 35 | /// 36 | /// - Parameter minutes: The number of minutes to add/subtract. 37 | /// - Returns: The date after the minutes addition/subtraction. 38 | func next(minutes: Int) -> Date { 39 | return Calendar.current.date(byAdding: .minute, value: minutes, to: self)! 40 | } 41 | 42 | /// Adds or subtracts a number of hours to the current date. 43 | /// 44 | /// - Parameter hours: The number of hours to add/subtract. 45 | /// - Returns: The current date after the hours addition/subtraction. 46 | static func next(hours: Int) -> Date { 47 | return Date().next(hours: hours) 48 | } 49 | 50 | /// Adds or subtracts a number of hours to a date. 51 | /// 52 | /// - Parameter hours: The number of hours to add/subtract. 53 | /// - Returns: The date after the hours addition/subtraction. 54 | func next(hours: Int) -> Date { 55 | return Calendar.current.date(byAdding: .hour, value: hours, to: self)! 56 | } 57 | 58 | /// Adds or subtracts a number of days to the current date. 59 | /// 60 | /// - Parameter days: The number of days to add/subtract. 61 | /// - Returns: The current date after the days addition/subtraction. 62 | static func next(days: Int) -> Date { 63 | return Date().next(days: days) 64 | } 65 | 66 | /// Adds or subtracts a number of days to a date. 67 | /// 68 | /// - Parameter days: The number of days to add/subtract. 69 | /// - Returns: The date after the days addition/subtraction. 70 | func next(days: Int) -> Date { 71 | return Calendar.current.date(byAdding: .day, value: days, to: self)! 72 | } 73 | 74 | /// Adds or subtracts a number of weeks to the current date. 75 | /// 76 | /// - Parameter days: The number of weeks to add/subtract. 77 | /// - Returns: The current date after the weeks addition/subtraction. 78 | static func next(weeks: Int) -> Date { 79 | return Date().next(weeks: weeks) 80 | } 81 | 82 | /// Adds or subtracts a number of weeks to a date. 83 | /// 84 | /// - Parameter days: The number of weeks to add/subtract. 85 | /// - Returns: The date after the weeks addition/subtraction. 86 | func next(weeks: Int) -> Date { 87 | return Calendar.current.date(byAdding: .weekOfMonth, value: weeks, to: self)! 88 | } 89 | 90 | /// Adds or subtracts a number of months to the current date. 91 | /// 92 | /// - Parameter days: The number of months to add/subtract. 93 | /// - Returns: The current date after the months addition/subtraction. 94 | static func next(months: Int) -> Date { 95 | return Date().next(months: months) 96 | } 97 | 98 | /// Adds or subtracts a number of months to a date. 99 | /// 100 | /// - Parameter days: The number of months to add/subtract. 101 | /// - Returns: The date after the months addition/subtraction. 102 | func next(months: Int) -> Date { 103 | return Calendar.current.date(byAdding: .month, value: months, to: self)! 104 | } 105 | 106 | /// Adds or subtracts a number of years to the current date. 107 | /// 108 | /// - Parameter days: The number of years to add/subtract. 109 | /// - Returns: The current date after the years addition/subtraction. 110 | static func next(years: Int) -> Date { 111 | return Date().next(years: years) 112 | } 113 | 114 | /// Adds or subtracts a number of years to a date. 115 | /// 116 | /// - Parameter days: The number of years to add/subtract. 117 | /// - Returns: The date after the years addition/subtraction. 118 | func next(years: Int) -> Date { 119 | return Calendar.current.date(byAdding: .year, value: years, to: self)! 120 | } 121 | 122 | /// Removes the seconds component from the date. 123 | /// 124 | /// - Returns: The date after removing the seconds component. 125 | func truncateSeconds() -> Date { 126 | let calendar = Calendar.current 127 | let components = (calendar as NSCalendar).components([.year, .month, .day, .hour, .minute], from: self) 128 | return calendar.date(from: components)! 129 | } 130 | 131 | /// Creates a date object with the given time and offset. The offset is used to align the time with the GMT. 132 | /// 133 | /// - Parameters: 134 | /// - time: The required time of the form HHMM. 135 | /// - offset: The offset in minutes. 136 | /// - Returns: Date with the specified time and offset. 137 | static func date(withTime time: Int, offset: Int) -> Date { 138 | let calendar = Calendar.current 139 | var components = (calendar as NSCalendar).components([.year, .month, .day, .hour, .minute], from: Date()) 140 | components.minute = (time % 100) + offset % 60 141 | components.hour = (time / 100) + (offset / 60) 142 | var date = calendar.date(from: components)! 143 | if date < Date() { 144 | date = date.next(days: 1) 145 | } 146 | 147 | return date 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Sources/Robin/Delegate/NotificationDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | /// An object that handles the system's notification center delegation. 26 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 27 | internal class NotificationDelegate: RobinDelegate { 28 | fileprivate let registrar: RobinActionRegistrar 29 | 30 | init(registrar: RobinActionRegistrar) { 31 | self.registrar = registrar 32 | } 33 | 34 | func didReceiveResponse(_ response: SystemNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 35 | defer { 36 | completionHandler() 37 | } 38 | 39 | guard let actionType = self.registrar.action(forIdentifier: response.actionIdentifier) else { 40 | return 41 | } 42 | 43 | var robinNotificationResponse: RobinNotificationResponse 44 | if let response = response as? SystemNotificationTextResponse { 45 | robinNotificationResponse = response.robinNotificationTextResponse() 46 | } else { 47 | robinNotificationResponse = response.robinNotificationResponse() 48 | } 49 | 50 | let action = actionType.init() 51 | action.handle(response: robinNotificationResponse) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Robin/Delegate/RobinDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | /// A protocol that represents objects responsible for handling the system's notification center delegation. 26 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 27 | public protocol RobinDelegate { 28 | /// Asks Robin to process the response provided by the system's notification center. Robin examines the response for the `actionIdentifier` 29 | /// property and looks for its action handler in the registrar. If an action handler is found, it will be invoked to handle the response, 30 | /// as an instance of `RobinNotificationResponse`, and Robin will invoke the `completionHandler` afterwards. 31 | /// 32 | /// - attention: 33 | /// If you don't pass the completion handler provided by the system's notification center delegate, you will need to invoke it yourself. 34 | /// 35 | /// - Parameters: 36 | /// - response: The system response. 37 | /// - completionHandler: A callback to be called when Robin finishes processing the response. 38 | func didReceiveResponse(_ response: SystemNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/RobinNotification+CustomStringConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | extension RobinNotification: CustomStringConvertible { 27 | public var description: String { 28 | var result = "" 29 | result += "RobinNotification: \(self.identifier!)\n" 30 | if let title = self.title { 31 | result += "\tTitle: \(title)\n" 32 | } 33 | if let threadIdentifier = threadIdentifier { 34 | result += "\tThread identifier: \(threadIdentifier)\n" 35 | } 36 | result += "\tBody: \(self.body!)\n" 37 | if let trigger = self.trigger, 38 | case let RobinNotificationTrigger.date(date, repeats) = trigger { 39 | result += "\tFires at: \(date)\n" 40 | result += "\tRepeats every: \(repeats.rawValue)\n" 41 | } 42 | 43 | #if !os(macOS) && !os(watchOS) 44 | if let trigger = self.trigger, 45 | case let RobinNotificationTrigger.location(region, repeats) = trigger { 46 | result += "\tFires around: \(region)\n" 47 | result += "\tRepeating: \(repeats)\n" 48 | } 49 | #endif 50 | 51 | result += "\tUser info: \(self.userInfo)\n" 52 | if let badge = self.badge { 53 | result += "\tBadge: \(badge)\n" 54 | } 55 | #if !os(watchOS) 56 | result += "\tSound name: \(self.sound)\n" 57 | #endif 58 | result += "\tScheduled: \(self.scheduled)\n" 59 | result += "\tDelivered: \(self.delivered)" 60 | if let deliveryDate = deliveryDate { 61 | result += "\n\tDelivered on: \(deliveryDate)" 62 | } 63 | 64 | return result 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/RobinNotification+Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | extension RobinNotification { 27 | /// Since repeating a `UNCalendarNotificationTrigger` nullifies some of the 28 | /// date components, the original date needs to be stored. Robin stores this date 29 | /// in the notification's `userInfo` property using `Constants.NotificationKeys.date`. 30 | /// This original date is used to fill those nullified components. 31 | /// 32 | /// - Parameters: 33 | /// - dateComponents: The `UNCalendarNotificationTrigger` date components. 34 | /// - repeats: The repeat interval of the trigger. 35 | /// - originalDate: The original date stored to fill the nullified components. Uses current date if passed as `nil`. 36 | /// - Returns: The filled date using the original date. 37 | internal static func date(fromDateComponents dateComponents: DateComponents, repeats: RobinNotificationRepeats, originalDate: Date?) -> Date { 38 | let calendar: Calendar = Calendar.current 39 | var components: DateComponents = dateComponents 40 | 41 | let date = originalDate ?? Date() 42 | 43 | switch repeats { 44 | case .none: 45 | return calendar.date(from: components)! 46 | case .month: 47 | let comps = calendar.dateComponents([.year, .month], from: date) 48 | components.year = comps.year 49 | components.month = comps.month 50 | 51 | return calendar.date(from: components)! 52 | case .week: fallthrough 53 | case .day: 54 | let comps = calendar.dateComponents([.year, .month, .day], from: date) 55 | components.year = comps.year 56 | components.month = comps.month 57 | components.day = comps.day 58 | 59 | return calendar.date(from: components)! 60 | case .hour: 61 | let comps = calendar.dateComponents([.year, .month, .day, .hour], from: date) 62 | components.year = comps.year 63 | components.month = comps.month 64 | components.day = comps.day 65 | components.hour = comps.hour 66 | 67 | return calendar.date(from: components)! 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/RobinNotification+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | extension RobinNotification: Equatable { 27 | public static func ==(lhs: RobinNotification, rhs: RobinNotification) -> Bool { 28 | return lhs.identifier == rhs.identifier 29 | } 30 | 31 | public static func <(lhs: RobinNotification, rhs: RobinNotification) -> Bool { 32 | guard let lhsTrigger = lhs.trigger, 33 | let rhsTrigger = rhs.trigger, 34 | case let RobinNotificationTrigger.date(lhsDate, _) = lhsTrigger, 35 | case let RobinNotificationTrigger.date(rhsDate, _) = rhsTrigger else { 36 | return lhs.identifier < rhs.identifier 37 | } 38 | 39 | return lhsDate.compare(rhsDate) == ComparisonResult.orderedAscending 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/RobinNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// An object that represents notifications scheduled/delivered by the system. 26 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 27 | public class RobinNotification { 28 | /// A string assigned to the notification for later access. 29 | fileprivate(set) public var identifier: String! 30 | 31 | /// The body string of the notification. 32 | public var body: String! 33 | 34 | /// The trigger that causes the notification to fire. 35 | public var trigger: RobinNotificationTrigger! { 36 | didSet { 37 | if let trigger = trigger, 38 | case let RobinNotificationTrigger.date(date, repeats) = trigger { 39 | let truncatedDate = date.truncateSeconds() 40 | 41 | self.trigger = .date(truncatedDate, repeats: repeats) 42 | self.userInfo[Constants.NotificationKeys.date] = truncatedDate 43 | } 44 | } 45 | } 46 | 47 | /// A dictionary that holds additional information. 48 | internal(set) public var userInfo: [AnyHashable : Any] = [:] 49 | 50 | /// The title string of the notification. 51 | public var title: String? = nil 52 | 53 | /// The number the notification should display on the app icon. 54 | public var badge: NSNumber? = nil 55 | 56 | #if !os(watchOS) 57 | /// The sound name of the notification. 58 | public var sound: RobinNotificationSound = RobinNotificationSound() 59 | #endif 60 | 61 | /// The status of the notification. 62 | internal(set) public var scheduled: Bool = false 63 | 64 | /// The delivery status of the notification. 65 | internal(set) public var delivered: Bool = false 66 | 67 | /// The delivery date of the notification. 68 | internal(set) public var deliveryDate: Date? 69 | 70 | /// The identifier used to visually group notifications together. 71 | public var threadIdentifier: String? 72 | 73 | /// The identifier of the notification's category. 74 | public var categoryIdentifier: String? 75 | 76 | public convenience init(identifier: String = UUID().uuidString, 77 | body: String, 78 | trigger: RobinNotificationTrigger = .date(.next(hours: 1), repeats: .none)) { 79 | self.init(identifier: identifier, body: body) 80 | 81 | if case RobinNotificationTrigger.date(let date, let repeats) = trigger { 82 | let truncatedDate = date.truncateSeconds() 83 | 84 | self.trigger = .date(truncatedDate, repeats: repeats) 85 | self.userInfo[Constants.NotificationKeys.date] = truncatedDate 86 | } else { 87 | self.trigger = trigger 88 | } 89 | } 90 | 91 | internal init(identifier: String, body: String) { 92 | self.identifier = identifier 93 | self.body = body 94 | } 95 | 96 | /// Creates a `RobinNotification` from the passed `SystemNotification`. For the details of the creation process, have a look at the system notifications extensions that implement the `SystemNotification` protocol. 97 | /// 98 | /// - Parameter notification: The system notification to create the `RobinNotification` from. 99 | /// - Returns: The `RobinNotification` if the creation succeeded, nil otherwise. 100 | public static func notification(withSystemNotification notification: SystemNotification) -> RobinNotification? { 101 | return notification.robinNotification() 102 | } 103 | 104 | /// Adds a value to the specified key in the `userInfo` property. 105 | /// 106 | /// - note: 107 | /// The value is omitted if the key is equal to one of `Constants.NotificationKeys`. 108 | /// 109 | /// - Parameters: 110 | /// - value: The value to set. 111 | /// - key: The key to set the value of. 112 | public func setUserInfo(value: Any, forKey key: AnyHashable) { 113 | if let keyString = key as? String { 114 | if (keyString == Constants.NotificationKeys.date) { 115 | return 116 | } 117 | } 118 | self.userInfo[key] = value; 119 | } 120 | 121 | /// Removes the value of the specified key. 122 | /// 123 | /// - note: 124 | /// The value is not removed if the key is equal to one of `Constants.NotificationKeys`. 125 | /// 126 | /// - Parameter key: The key to remove the value of. 127 | public func removeUserInfoValue(forKey key: AnyHashable) { 128 | if let keyString = key as? String { 129 | if (keyString == Constants.NotificationKeys.date) { 130 | return 131 | } 132 | } 133 | self.userInfo.removeValue(forKey: key) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/RobinNotificationGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// An object that holds multiple `RobinNotification` instances to be scheduled together under a single group identifier. 26 | /// 27 | /// - note: 28 | /// Notification scheduled under the same group identifier are also grouped visually when displayed to the user. 29 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 30 | public class RobinNotificationGroup { 31 | public let notifications: [RobinNotification] 32 | public let identifier: String 33 | 34 | public init(notifications: [RobinNotification], identifier: String = UUID().uuidString) { 35 | self.notifications = notifications 36 | self.identifier = identifier 37 | 38 | self.notifications.forEach { $0.threadIdentifier = identifier } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/RobinNotificationRepeats.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// An enum that represents the repeat interval of the notification. 26 | /// 27 | /// - .none: The notification should never repeat. 28 | /// - .hour: The notification should repeat every hour. 29 | /// - .day: The notification should repeat every day. 30 | /// - .week: The notification should repeat every week. 31 | /// - .month: The notification should repeat every month. 32 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 33 | public enum RobinNotificationRepeats: String { 34 | case none = "None" 35 | case hour = "Hour" 36 | case day = "Day" 37 | case week = "Week" 38 | case month = "Month" 39 | 40 | internal static func from(dateComponents components: DateComponents) -> RobinNotificationRepeats { 41 | if self.doesRepeatNone(dateComponents: components) { 42 | return .none 43 | } 44 | 45 | if doesRepeatMonth(dateComponents: components) { 46 | return .month 47 | } 48 | 49 | if doesRepeatWeek(dateComponents: components) { 50 | return .week 51 | } 52 | 53 | if doesRepeatDay(dateComponents: components) { 54 | return .day 55 | } 56 | 57 | if doesRepeatHour(dateComponents: components) { 58 | return .hour 59 | } 60 | 61 | return .none 62 | } 63 | 64 | fileprivate static func doesRepeatNone(dateComponents components: DateComponents) -> Bool { 65 | return components.year != nil && components.month != nil && components.day != nil && components.hour != nil && components.minute != nil 66 | } 67 | 68 | fileprivate static func doesRepeatMonth(dateComponents components: DateComponents) -> Bool { 69 | return components.day != nil && components.hour != nil && components.minute != nil 70 | } 71 | 72 | fileprivate static func doesRepeatWeek(dateComponents components: DateComponents) -> Bool { 73 | return components.weekday != nil && components.hour != nil && components.minute != nil && components.second != nil 74 | } 75 | 76 | fileprivate static func doesRepeatDay(dateComponents components: DateComponents) -> Bool { 77 | return components.hour != nil && components.minute != nil && components.second != nil 78 | } 79 | 80 | fileprivate static func doesRepeatHour(dateComponents components: DateComponents) -> Bool { 81 | return components.minute != nil && components.second != nil 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/RobinNotificationResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | /// An object that represents the user's response to a system notification. 24 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 25 | public struct RobinNotificationResponse { 26 | /// The delivered notification the user responded to. 27 | public let notification: RobinNotification 28 | /// The text the user responded to the notification with. 29 | public let userText: String? 30 | 31 | internal init(notification: RobinNotification, userText: String? = nil) { 32 | self.notification = notification 33 | self.userText = userText 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/RobinNotificationTrigger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | #if !os(macOS) 25 | import CoreLocation 26 | #endif 27 | 28 | /// An enum that represents the type of trigger that causes a notification to fire. 29 | /// 30 | /// * .date: The notification should be fired on a specified time with an option to repeat. 31 | /// * .interval: The notification should be fired after a specified time interval with an option to repeat. 32 | /// * .location: The notification should be fired when entering or leaving a specified region with an option to repeat. (Not available on macOS or watchOS). 33 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 34 | public enum RobinNotificationTrigger { 35 | case date(_ date: Date, repeats: RobinNotificationRepeats) 36 | case interval(_ interval: TimeInterval, repeats: Bool) 37 | 38 | #if !os(macOS) && !os(watchOS) 39 | case location(_ region: CLRegion, repeats: Bool) 40 | #endif 41 | } 42 | 43 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 44 | extension RobinNotificationTrigger: Equatable { 45 | public static func ==(lhs: RobinNotificationTrigger, rhs: RobinNotificationTrigger) -> Bool { 46 | if case let .date(lhsDate, lhsRepeats) = lhs, 47 | case let .date(rhsDate, rhsRepeats) = rhs { 48 | return lhsDate == rhsDate && lhsRepeats == rhsRepeats 49 | } 50 | 51 | if case let .interval(lhsInterval, lhsRepeats) = lhs, 52 | case let .interval(rhsInterval, rhsRepeats) = rhs { 53 | return lhsInterval == rhsInterval && lhsRepeats == rhsRepeats 54 | } 55 | 56 | #if !os(macOS) && !os(watchOS) 57 | if case let .location(lhsRegion, lhsRepeats) = lhs, 58 | case let .location(rhsRegion, rhsRepeats) = rhs { 59 | return lhsRegion == rhsRegion && lhsRepeats == rhsRepeats 60 | } 61 | #endif 62 | 63 | return false 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/Sound/RobinNotificationSound.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | @available(iOS 10.0, macOS 10.14, *) 24 | public class RobinNotificationSound { 25 | internal var name: String? 26 | internal var sound: SystemNotificationSound? 27 | 28 | public init(named name: String = Constants.NotificationValues.defaultSoundName) { 29 | self.name = name 30 | } 31 | 32 | internal init(sound: SystemNotificationSound) { 33 | self.sound = sound 34 | } 35 | 36 | public func isValid() -> Bool { 37 | return self.name != nil || self.sound != nil 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/Sound/String+SystemNotificationSound.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | @available(iOS 10.0, macOS 10.14, *) 24 | extension String: SystemNotificationSound {} 25 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/Sound/SystemNotificationSound.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | @available(iOS 10.0, macOS 10.14, *) 24 | protocol SystemNotificationSound {} 25 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/Sound/UNNotificationSound+SystemNotificationSound.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import UserNotifications 25 | 26 | @available(iOS 10.0, macOS 10.14, *) 27 | extension UNNotificationSound: SystemNotificationSound {} 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/System/DeliveredSystemNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | /// A protocol that represents notifications delivered by the system. 26 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 27 | public protocol DeliveredSystemNotification { 28 | var date: Date { get } 29 | var request: UNNotificationRequest { get } 30 | 31 | /// Creates a `RobinNotification` from the passed `DeliveredSystemNotification`. 32 | /// 33 | /// - Parameter notification: The system notification to create the `RobinNotification` from. 34 | /// - Returns: The `RobinNotification` if the creation succeeded, nil otherwise. 35 | func robinNotification() -> RobinNotification 36 | } 37 | 38 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 39 | public extension DeliveredSystemNotification { 40 | func robinNotification() -> RobinNotification { 41 | let notification = self.request.robinNotification() 42 | notification.deliveryDate = self.date 43 | notification.delivered = true 44 | 45 | return notification 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/System/RobinNotification+UNNotificationRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | internal extension RobinNotification { 27 | /// Transforms an instance of `RobinNotification` to an instance of `UNNotificationRequest`. 28 | /// 29 | /// - Returns: A `UNNotificationRequest` from the the `RobinNotification` instance. 30 | func notificationRequest() -> UNNotificationRequest { 31 | let content = UNMutableNotificationContent() 32 | 33 | if let title = self.title { 34 | content.title = title 35 | } 36 | 37 | content.body = self.body 38 | 39 | var sound: UNNotificationSound = UNNotificationSound.default 40 | #if !os(watchOS) 41 | if let name = self.sound.name { 42 | if name != Constants.NotificationValues.defaultSoundName { 43 | sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: name)) 44 | } 45 | } else { 46 | if let notificationSound = self.sound.sound as? UNNotificationSound { 47 | sound = notificationSound 48 | } 49 | } 50 | #endif 51 | content.sound = sound 52 | 53 | content.userInfo = self.userInfo 54 | 55 | content.badge = self.badge 56 | 57 | if let threadIdentifier = self.threadIdentifier { 58 | content.threadIdentifier = threadIdentifier 59 | } 60 | 61 | if let categoryIdentifier = self.categoryIdentifier { 62 | content.categoryIdentifier = categoryIdentifier 63 | } 64 | 65 | var notificationTrigger: UNNotificationTrigger! 66 | 67 | if let trigger = trigger, 68 | case let RobinNotificationTrigger.date(date, repeats) = trigger { 69 | notificationTrigger = UNCalendarNotificationTrigger(date: date, repeats: repeats) 70 | } 71 | 72 | if let trigger = trigger, 73 | case let RobinNotificationTrigger.interval(interval, repeats) = trigger { 74 | notificationTrigger = UNTimeIntervalNotificationTrigger(timeInterval: interval, repeats: repeats) 75 | } 76 | 77 | #if !os(macOS) && !os(watchOS) 78 | if let trigger = trigger, 79 | case let RobinNotificationTrigger.location(region, repeats) = trigger { 80 | notificationTrigger = UNLocationNotificationTrigger(region: region, repeats: repeats) 81 | } 82 | #endif 83 | 84 | let request = UNNotificationRequest(identifier: self.identifier, content: content, trigger: notificationTrigger) 85 | 86 | return request 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/System/SystemNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | /// A protocol that represents notifications to be delivered by the system. 26 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 27 | public protocol SystemNotification { 28 | var identifier: String { get } 29 | var content: UNNotificationContent { get } 30 | var trigger: UNNotificationTrigger? { get } 31 | 32 | /// Creates a `RobinNotification` from the passed `SystemNotification`. 33 | /// 34 | /// - Parameter notification: The system notification to create the `RobinNotification` from. 35 | /// - Returns: The `RobinNotification` if the creation succeeded, nil otherwise. 36 | func robinNotification() -> RobinNotification 37 | } 38 | 39 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 40 | public extension SystemNotification { 41 | func robinNotification() -> RobinNotification { 42 | let content = self.content 43 | 44 | let notification = RobinNotification(identifier: self.identifier, body: content.body) 45 | 46 | let userInfo = content.userInfo 47 | for (key, value) in userInfo { 48 | notification.userInfo[key] = value 49 | } 50 | 51 | if content.title.trimmingCharacters(in: .whitespaces).count > 0 { 52 | notification.title = content.title 53 | } 54 | 55 | if let trigger = self.trigger as? UNCalendarNotificationTrigger { 56 | var date: Date? 57 | if let originalDate = notification.userInfo[Constants.NotificationKeys.date] as? Date { 58 | date = originalDate 59 | } 60 | let repeats = RobinNotificationRepeats.from(dateComponents: trigger.dateComponents) 61 | let notificationDate = RobinNotification.date(fromDateComponents: trigger.dateComponents, repeats: repeats, originalDate: date) 62 | 63 | notification.trigger = .date(notificationDate, repeats: repeats) 64 | } 65 | 66 | if let trigger = self.trigger as? UNTimeIntervalNotificationTrigger { 67 | notification.trigger = .interval(trigger.timeInterval, repeats: trigger.repeats) 68 | } 69 | 70 | #if !os(macOS) && !os(watchOS) 71 | if let trigger = self.trigger as? UNLocationNotificationTrigger { 72 | notification.trigger = .location(trigger.region, repeats: trigger.repeats) 73 | } 74 | #endif 75 | 76 | notification.badge = content.badge 77 | notification.threadIdentifier = content.threadIdentifier 78 | notification.categoryIdentifier = content.categoryIdentifier 79 | 80 | #if !os(watchOS) 81 | if let sound = content.sound { 82 | if sound != UNNotificationSound.default { 83 | notification.sound = RobinNotificationSound(sound: sound) 84 | } 85 | } 86 | #endif 87 | 88 | notification.scheduled = true 89 | 90 | return notification 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/System/SystemNotificationResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | public protocol SystemNotificationResponse { 27 | var robinNotification: RobinNotification { get } 28 | var actionIdentifier: String { get } 29 | 30 | func robinNotificationResponse() -> RobinNotificationResponse 31 | } 32 | 33 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 34 | public extension SystemNotificationResponse { 35 | func robinNotificationResponse() -> RobinNotificationResponse { 36 | return RobinNotificationResponse(notification: self.robinNotification) 37 | } 38 | } 39 | 40 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 41 | public protocol SystemNotificationTextResponse: SystemNotificationResponse { 42 | var userText: String { get } 43 | 44 | func robinNotificationTextResponse() -> RobinNotificationResponse 45 | } 46 | 47 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 48 | public extension SystemNotificationTextResponse { 49 | func robinNotificationTextResponse() -> RobinNotificationResponse { 50 | return RobinNotificationResponse(notification: self.robinNotification, userText: self.userText) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/System/UNCalendarNotificationTrigger+Robin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | internal extension UNCalendarNotificationTrigger { 27 | /// Initializes an instance of `UNCalendarNotificationTrigger` given a starting date and a repeat interval. 28 | /// 29 | /// - Parameters: 30 | /// - date: The starting date of the trigger. 31 | /// - repeats: The repeat interval of the trigger. 32 | convenience init(date: Date, repeats: RobinNotificationRepeats) { 33 | var dateComponents: DateComponents = DateComponents() 34 | let shouldRepeat: Bool = repeats != .none 35 | let calendar: Calendar = Calendar.current 36 | 37 | switch repeats { 38 | case .none: 39 | dateComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date) 40 | dateComponents.second = 0 41 | case .month: 42 | dateComponents = calendar.dateComponents([.day, .hour, .minute], from: date) 43 | dateComponents.second = 0 44 | case .week: 45 | dateComponents.weekday = calendar.component(.weekday, from: date) 46 | fallthrough 47 | case .day: 48 | dateComponents.hour = calendar.component(.hour, from: date) 49 | fallthrough 50 | case .hour: 51 | dateComponents.minute = calendar.component(.minute, from: date) 52 | dateComponents.second = 0 53 | } 54 | 55 | self.init(dateMatching: dateComponents, repeats: shouldRepeat) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/System/UNNotification+DeliveredSystemNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | extension UNNotification: DeliveredSystemNotification {} 27 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/System/UNNotificationRequest+SystemNotification.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | extension UNNotificationRequest: SystemNotification {} 27 | -------------------------------------------------------------------------------- /Sources/Robin/Notification/System/UNNotificationResponse+SystemNotificationResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | extension UNNotificationResponse: SystemNotificationResponse { 27 | public var robinNotification: RobinNotification { 28 | return self.notification.robinNotification() 29 | } 30 | } 31 | 32 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 33 | extension UNTextInputNotificationResponse: SystemNotificationTextResponse {} 34 | -------------------------------------------------------------------------------- /Sources/Robin/Robin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 24 | public class Robin { 25 | static var registrar = RobinActionRegistrar() 26 | public static let actions: RobinActions = registrar 27 | 28 | static var robinDelegate: RobinDelegate! 29 | public static var delegate: RobinDelegate { 30 | if robinDelegate == nil { 31 | robinDelegate = NotificationDelegate(registrar: registrar) 32 | } 33 | 34 | return robinDelegate 35 | } 36 | 37 | static var notificationCenterManager: RobinNotificationCenterManager! 38 | public static var manager: RobinNotificationCenterManager { 39 | if notificationCenterManager == nil { 40 | self.notificationCenterManager = NotificationCenterManager() 41 | } 42 | 43 | return self.notificationCenterManager 44 | } 45 | 46 | static var notificationsScheduler: RobinScheduler! 47 | public static var scheduler: RobinScheduler { 48 | if notificationsScheduler == nil { 49 | self.notificationsScheduler = NotificationsScheduler() 50 | } 51 | 52 | return self.notificationsScheduler 53 | } 54 | 55 | static var notificationsSettings: RobinSettingsManager! 56 | public static var settings: RobinSettingsManager { 57 | if notificationsSettings == nil { 58 | self.notificationsSettings = NotificationSettings() 59 | } 60 | 61 | return self.notificationsSettings 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/Robin/Scheduler/NotificationsScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | internal class NotificationsScheduler: RobinScheduler { 27 | fileprivate let center: RobinNotificationCenter 28 | 29 | init(center: RobinNotificationCenter = UNUserNotificationCenter.current()) { 30 | self.center = center 31 | } 32 | 33 | func schedule(notification: RobinNotification) -> RobinNotification? { 34 | if notification.scheduled == true { 35 | return notification 36 | } 37 | 38 | if (!containsFreeSlots()) { 39 | return nil 40 | } 41 | 42 | let request = notification.notificationRequest() 43 | 44 | center.add(request) 45 | 46 | notification.scheduled = true 47 | 48 | return notification 49 | } 50 | 51 | func schedule(group: RobinNotificationGroup) -> RobinNotificationGroup? { 52 | let notifications = group.notifications 53 | 54 | if (self.freeSlotsCount() < notifications.count) { 55 | return nil 56 | } 57 | 58 | notifications.forEach { _ = schedule(notification: $0) } 59 | 60 | return group 61 | } 62 | 63 | func reschedule(notification: RobinNotification) -> RobinNotification? { 64 | self.cancel(notification: notification) 65 | 66 | return self.schedule(notification: notification) 67 | } 68 | 69 | func cancel(notification: RobinNotification) { 70 | if notification.scheduled == false { 71 | return 72 | } 73 | 74 | center.removePendingNotificationRequests(withIdentifiers: [notification.identifier]) 75 | notification.scheduled = false 76 | } 77 | 78 | func cancel(group: RobinNotificationGroup) { 79 | cancel(groupWithIdentifier: group.identifier) 80 | } 81 | 82 | func cancel(withIdentifier identifier: String) { 83 | center.removePendingNotificationRequests(withIdentifiers: [identifier]) 84 | } 85 | 86 | func cancel(groupWithIdentifier identifier: String) { 87 | let notifications = self.scheduled() 88 | 89 | let group = notifications.filter { $0.threadIdentifier == identifier } 90 | group.forEach { self.cancel(notification: $0) } 91 | } 92 | 93 | func cancelAll() { 94 | center.removeAllPendingNotificationRequests() 95 | } 96 | 97 | func notification(withIdentifier identifier: String) -> RobinNotification? { 98 | let notifications = self.scheduled() 99 | 100 | return notifications.first { $0.identifier == identifier } 101 | } 102 | 103 | func group(withIdentifier identifier: String) -> RobinNotificationGroup? { 104 | let notifications = self.scheduled() 105 | .filter { $0.threadIdentifier == identifier } 106 | 107 | guard notifications.count > 0 else { 108 | return nil 109 | } 110 | 111 | return RobinNotificationGroup(notifications: notifications, identifier: identifier) 112 | } 113 | 114 | func scheduled() -> [RobinNotification] { 115 | let semaphore = DispatchSemaphore(value: 0) 116 | var notifications: [RobinNotification] = [] 117 | 118 | center.getPendingNotificationRequests { requests in 119 | notifications = requests.map { $0.robinNotification() } 120 | semaphore.signal() 121 | } 122 | 123 | _ = semaphore.wait(timeout: DispatchTime.distantFuture) 124 | 125 | return notifications 126 | } 127 | 128 | func scheduledCount() -> Int { 129 | return self.scheduled().count 130 | } 131 | 132 | // MARK:- Testing 133 | 134 | func printScheduled() { 135 | let notifications = self.scheduled() 136 | 137 | guard notifications.count > 0 else { 138 | print("There are no scheduled system notifications.") 139 | return 140 | } 141 | 142 | notifications.forEach { print($0) } 143 | } 144 | 145 | fileprivate func freeSlotsCount() -> Int { 146 | return min(Constants.maximumAllowedNotifications, Constants.maximumAllowedSystemNotifications) - self.scheduledCount() 147 | } 148 | 149 | fileprivate func containsFreeSlots() -> Bool { 150 | return freeSlotsCount() > 0 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/Robin/Scheduler/RobinScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 24 | public protocol RobinScheduler: class { 25 | /// Schedules the passed notification if and only if there is an available notification slot and it is not already scheduled. The number of available slots is governed by `Robin.maximumAllowedNotifications` and `Constants.maximumAllowedSystemNotifications`. 26 | /// 27 | /// - attention: 28 | /// The system will discard notifications having the exact same attribute values (i.e if two notifications have the same attributes, it will only schedule one of them). 29 | /// If you want to schedule multiple notifications under the same identifier, see `schedule(group: RobinNotificationGroup) -> RobinNotificationGroup?`. 30 | /// 31 | /// - Parameter notification: The notification to schedule. 32 | /// - Returns: The scheduled `RobinNotification` if it was successfully scheduled, nil otherwise. 33 | func schedule(notification: RobinNotification) -> RobinNotification? 34 | 35 | /// Schedules the passed notification group if and only if there are available notification slots and the notifications are not already scheduled. The number of available slots is governed by `Robin.maximumAllowedNotifications` and `Constants.maximumAllowedSystemNotifications`. 36 | /// 37 | /// - Parameter group: The notification group to schedule. 38 | /// - Returns: The scheduled `RobinNotificationGroup` if it was successfully scheduled, nil otherwise. 39 | func schedule(group: RobinNotificationGroup) -> RobinNotificationGroup? 40 | 41 | /// Reschedules the passed `RobinNotification` whether it is already scheduled or not. This simply cancels the `RobinNotification` and schedules it again. 42 | /// 43 | /// - Parameter notification: The notification to reschedule. 44 | /// - Returns: The rescheduled `RobinNotification` if it was successfully rescheduled, nil otherwise. 45 | func reschedule(notification: RobinNotification) -> RobinNotification? 46 | 47 | /// Cancels the passed notification if it is scheduled. 48 | /// 49 | /// - Parameter notification: The notification to cancel. 50 | func cancel(notification: RobinNotification) 51 | 52 | /// Cancels the passed notification group if it is scheduled. 53 | /// 54 | /// - Parameter group: The notification group to cancel. 55 | func cancel(group: RobinNotificationGroup) 56 | 57 | /// Cancels the scheduled notification having the passed identifier. 58 | /// 59 | /// - attention: 60 | /// If you hold a reference to the notification having this same identifier, use `cancel(notification: RobinNotification)` instead. 61 | /// 62 | /// - Parameter identifier: The identifier to match against scheduled notifications to cancel. 63 | func cancel(withIdentifier identifier: String) 64 | 65 | /// Cancels the notification group having the passed identifier. 66 | /// 67 | /// - attention: 68 | /// If you hold a reference to the notification group having this same identifier, use `cancel(group: RobinNotificationGroup)` instead. 69 | /// 70 | /// - Parameter identifier: The identifier to match against scheduled notifications to cancel. 71 | func cancel(groupWithIdentifier identifier: String) 72 | 73 | /// Cancels all scheduled system notifications. 74 | func cancelAll() 75 | 76 | /// Returns a `RobinNotification` instance from a scheduled system notification that has an identifier matching the passed identifier. 77 | /// 78 | /// - Parameter identifier: The identifier to match against a scheduled system notification. 79 | /// - Returns: The `RobinNotification` created from a system notification if it exists. 80 | func notification(withIdentifier identifier: String) -> RobinNotification? 81 | 82 | /// Returns a `RobinNotificationGroup` instance from the scheduled system notifications that have an `threadIdentifier` matching the passed identifier. 83 | /// 84 | /// - Parameter identifier: The identifier to match against the scheduled system notifications. 85 | /// - Returns: The `RobinNotificationGroup` created with the system notifications, if any exists. 86 | func group(withIdentifier identifier: String) -> RobinNotificationGroup? 87 | 88 | /// Returns a list of all scheduled notifications. 89 | /// 90 | /// - Returns: The list of the scheduled notifications. 91 | func scheduled() -> [RobinNotification] 92 | 93 | /// Returns the count of the scheduled notifications. 94 | /// 95 | /// - Returns: The count of the scheduled notifications. 96 | func scheduledCount() -> Int 97 | 98 | // MARK:- Testing 99 | 100 | /// Use this method for development and testing. 101 | ///> Prints all scheduled system notifications. 102 | ///> You can freely modify it without worrying about affecting any functionality. 103 | func printScheduled() 104 | } 105 | -------------------------------------------------------------------------------- /Sources/Robin/Settings/NotificationSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | #if os(macOS) 25 | import AppKit 26 | #elseif os(iOS) 27 | import UIKit 28 | #endif 29 | 30 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 31 | internal class NotificationSettings: RobinSettingsManager { 32 | fileprivate let center: RobinNotificationCenter 33 | fileprivate var observer: NSObjectProtocol? = nil 34 | fileprivate var settings: RobinNotificationSettings 35 | 36 | var enabledSettings: RobinSettingsOptions { 37 | return self.settings.enabledSettings 38 | } 39 | 40 | var authorizationStatus: UNAuthorizationStatus { 41 | return self.settings.authorizationStatus 42 | } 43 | 44 | #if !os(watchOS) 45 | var alertStyle: UNAlertStyle { 46 | return self.settings.alertStyle 47 | } 48 | 49 | 50 | @available(iOS 11.0, *) 51 | var showPreviews: UNShowPreviewsSetting { 52 | return self.settings.showPreviews 53 | } 54 | #endif 55 | 56 | deinit { 57 | if let observer = observer { 58 | NotificationCenter.default.removeObserver(observer) 59 | } 60 | } 61 | 62 | init(center: RobinNotificationCenter = UNUserNotificationCenter.current()) { 63 | self.center = center 64 | #if os(watchOS) 65 | self.settings = RobinNotificationSettings(authorizationStatus: .notDetermined, enabledSettings: []) 66 | #else 67 | self.settings = RobinNotificationSettings(alertStyle: .none, authorizationStatus: .notDetermined, enabledSettings: []) 68 | #endif 69 | 70 | let notificationCenter = NotificationCenter.default 71 | let completionHandler: ((Notification) -> Void) = { [unowned self] notification in 72 | self.setSettings() 73 | } 74 | 75 | #if os(macOS) 76 | self.observer = notificationCenter.addObserver(forName: NSApplication.willBecomeActiveNotification, object: nil, queue: .main, using: completionHandler) 77 | #elseif os(iOS) 78 | self.observer = notificationCenter.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main, using: completionHandler) 79 | #endif 80 | 81 | setSettings() 82 | } 83 | 84 | fileprivate func setSettings() { 85 | let semaphore = DispatchSemaphore(value: 0) 86 | center.getSettings { settings in 87 | self.settings = settings.robinNotificationSettings() 88 | 89 | semaphore.signal() 90 | } 91 | semaphore.wait() 92 | } 93 | 94 | func requestAuthorization(forOptions options: UNAuthorizationOptions, completionHandler: @escaping (Bool, Error?) -> Void) { 95 | let authorizationOptions: UNAuthorizationOptions = UNAuthorizationOptions(rawValue: options.rawValue) 96 | 97 | center.requestAuthorization(options: authorizationOptions, completionHandler: completionHandler) 98 | } 99 | 100 | func forceRefresh() { 101 | setSettings() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Robin/Settings/RobinNotificationSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | /// A struct that holds information about the app's notifications settings. 26 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 27 | public struct RobinNotificationSettings { 28 | public let authorizationStatus: UNAuthorizationStatus 29 | public let enabledSettings: RobinSettingsOptions 30 | 31 | internal var _showPreviews: Any? = nil { 32 | // Makes sure the variable is set to a non-nil value exactly once. 33 | didSet { 34 | _showPreviews = oldValue ?? _showPreviews 35 | } 36 | } 37 | 38 | #if !os(watchOS) 39 | public let alertStyle: UNAlertStyle 40 | 41 | @available(iOS 11.0, *) 42 | public var showPreviews: UNShowPreviewsSetting { 43 | return _showPreviews as? UNShowPreviewsSetting ?? .never 44 | } 45 | #endif 46 | 47 | #if os(watchOS) 48 | internal init(authorizationStatus: UNAuthorizationStatus, 49 | enabledSettings: RobinSettingsOptions) { 50 | self.authorizationStatus = authorizationStatus 51 | self.enabledSettings = enabledSettings 52 | } 53 | #else 54 | internal init(alertStyle: UNAlertStyle, 55 | authorizationStatus: UNAuthorizationStatus, 56 | enabledSettings: RobinSettingsOptions) { 57 | self.alertStyle = alertStyle 58 | self.authorizationStatus = authorizationStatus 59 | self.enabledSettings = enabledSettings 60 | } 61 | #endif 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Robin/Settings/RobinSettingsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | public protocol RobinSettingsManager { 27 | /// The authorization status of the app's notifications. 28 | var authorizationStatus: UNAuthorizationStatus { get } 29 | /// The enabled settings of the app's notifications. 30 | var enabledSettings: RobinSettingsOptions { get } 31 | 32 | #if !os(watchOS) 33 | /// The alert style of the app's notifications. 34 | var alertStyle: UNAlertStyle { get } 35 | 36 | /// The show previews status of the app's notifications. 37 | @available(iOS 11.0, *) 38 | var showPreviews: UNShowPreviewsSetting { get } 39 | #endif 40 | 41 | /// Requests and registers your preferred options for notifying the user. 42 | /// 43 | /// - Parameter options: The notification options that your app requires. 44 | func requestAuthorization(forOptions options: UNAuthorizationOptions, completionHandler: (@escaping (Bool, Error?) -> Void)) 45 | 46 | /// Robin automatically updates information about the app's settings when the app goes into an inactive state and 47 | /// becomes active again. `forceRefresh()` will cause Robin to update its information immediately without waiting for 48 | /// the app's state to change. 49 | func forceRefresh() 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Robin/Settings/RobinSettingsOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 24 | public struct RobinSettingsOptions: OptionSet, RawRepresentable { 25 | public let rawValue: UInt 26 | 27 | public init(rawValue: UInt) { 28 | self.rawValue = rawValue 29 | } 30 | 31 | #if !os(watchOS) 32 | public static let badge = RobinSettingsOptions(rawValue: 1 << 0) 33 | public static let lockScreen = RobinSettingsOptions(rawValue: 1 << 4) 34 | #endif 35 | 36 | public static let sound = RobinSettingsOptions(rawValue: 1 << 1) 37 | public static let alert = RobinSettingsOptions(rawValue: 1 << 2) 38 | public static let notificationCenter = RobinSettingsOptions(rawValue: 1 << 3) 39 | 40 | @available(iOS 12.0, *) 41 | public static let criticalAlert = RobinSettingsOptions(rawValue: 1 << 5) 42 | 43 | #if !os(macOS) 44 | #if !os(watchOS) 45 | public static let carPlay = RobinSettingsOptions(rawValue: 1 << 6) 46 | #endif 47 | 48 | @available(iOS 13.0, *) 49 | public static let announcement = RobinSettingsOptions(rawValue: 1 << 7) 50 | #endif 51 | } 52 | -------------------------------------------------------------------------------- /Sources/Robin/Settings/System/SystemNotificationSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | internal protocol SystemNotificationSettings { 27 | var authorizationStatus: UNAuthorizationStatus { get } 28 | 29 | #if !os(watchOS) 30 | var alertStyle: UNAlertStyle { get } 31 | 32 | @available(iOS 11.0, *) 33 | var showPreviewsSetting: UNShowPreviewsSetting { get } 34 | 35 | var badgeSetting: UNNotificationSetting { get } 36 | var lockScreenSetting: UNNotificationSetting { get } 37 | #endif 38 | 39 | var soundSetting: UNNotificationSetting { get } 40 | var alertSetting: UNNotificationSetting { get } 41 | var notificationCenterSetting: UNNotificationSetting { get } 42 | 43 | @available(iOS 12.0, watchOS 5.0, *) 44 | var criticalAlertSetting: UNNotificationSetting { get } 45 | 46 | #if !os(macOS) 47 | #if !os(watchOS) 48 | var carPlaySetting: UNNotificationSetting { get } 49 | #endif 50 | 51 | @available(iOS 13.0, watchOS 6.0, *) 52 | var announcementSetting: UNNotificationSetting { get } 53 | #endif 54 | 55 | func getEnabledSettings() -> RobinSettingsOptions 56 | func robinNotificationSettings() -> RobinNotificationSettings 57 | } 58 | 59 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 60 | internal extension SystemNotificationSettings { 61 | func getEnabledSettings() -> RobinSettingsOptions { 62 | var enabledSettings: RobinSettingsOptions = [] 63 | 64 | #if !os(watchOS) 65 | if self.badgeSetting == .enabled { 66 | enabledSettings.insert(.badge) 67 | } 68 | 69 | if self.lockScreenSetting == .enabled { 70 | enabledSettings.insert(.lockScreen) 71 | } 72 | #endif 73 | 74 | if self.soundSetting == .enabled { 75 | enabledSettings.insert(.sound) 76 | } 77 | 78 | if self.alertSetting == .enabled { 79 | enabledSettings.insert(.alert) 80 | } 81 | 82 | if self.notificationCenterSetting == .enabled { 83 | enabledSettings.insert(.notificationCenter) 84 | } 85 | 86 | if #available(iOS 12.0, watchOS 5.0, *) { 87 | if self.criticalAlertSetting == .enabled { 88 | enabledSettings.insert(.criticalAlert) 89 | } 90 | } 91 | 92 | #if !os(macOS) 93 | #if !os(watchOS) 94 | if self.carPlaySetting == .enabled { 95 | enabledSettings.insert(.carPlay) 96 | } 97 | #endif 98 | 99 | if #available(iOS 13.0, watchOS 6.0, *) { 100 | if self.announcementSetting == .enabled { 101 | enabledSettings.insert(.announcement) 102 | } 103 | } 104 | #endif 105 | 106 | return enabledSettings 107 | } 108 | 109 | func robinNotificationSettings() -> RobinNotificationSettings { 110 | #if os(watchOS) 111 | return RobinNotificationSettings(authorizationStatus: self.authorizationStatus, 112 | enabledSettings: self.getEnabledSettings()) 113 | #else 114 | var settings = RobinNotificationSettings(alertStyle: self.alertStyle, 115 | authorizationStatus: self.authorizationStatus, 116 | enabledSettings: self.getEnabledSettings()) 117 | if #available(iOS 11.0, *) { 118 | settings._showPreviews = self.showPreviewsSetting 119 | } 120 | 121 | return settings 122 | #endif 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/Robin/Settings/System/UNNotificationSettings+SystemNotificationSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UserNotifications 24 | 25 | @available(iOS 10.0, watchOS 3.0, macOS 10.14, *) 26 | extension UNNotificationSettings: SystemNotificationSettings {} 27 | -------------------------------------------------------------------------------- /Tests/RobinTests/Actions/RobinActionRegistrarTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import XCTest 25 | @testable import Robin 26 | 27 | @available(iOS 10.0, macOS 10.14, *) 28 | class RobinActionRegistrarTests: XCTestCase { 29 | struct ActionOne: RobinActionHandler { 30 | func handle(response: RobinNotificationResponse) { 31 | } 32 | } 33 | 34 | struct ActionTwo: RobinActionHandler { 35 | func handle(response: RobinNotificationResponse) { 36 | } 37 | } 38 | 39 | /// Tests whether registering action handlers succeeds. 40 | func testRegisterActionHandlers() { 41 | let registrar = RobinActionRegistrar() 42 | 43 | registrar.register(actionHandler: ActionOne.self, forIdentifier: "ACTION_ONE") 44 | registrar.register(actionHandler: ActionTwo.self, forIdentifier: "ACTION_TWO") 45 | 46 | XCTAssertEqual(registrar.registeredActions.count, 2) 47 | XCTAssert(registrar.action(forIdentifier: "ACTION_ONE") is ActionOne.Type) 48 | XCTAssert(registrar.action(forIdentifier: "ACTION_TWO") is ActionTwo.Type) 49 | } 50 | 51 | /// Tests whether de-registering action handlers succeeds. 52 | func testDeregisterActionHandlers() { 53 | let registrar = RobinActionRegistrar() 54 | 55 | registrar.register(actionHandler: ActionOne.self, forIdentifier: "ACTION_ONE") 56 | registrar.register(actionHandler: ActionTwo.self, forIdentifier: "ACTION_TWO") 57 | 58 | XCTAssertEqual(registrar.registeredActions.count, 2) 59 | XCTAssert(registrar.action(forIdentifier: "ACTION_ONE") is ActionOne.Type) 60 | XCTAssert(registrar.action(forIdentifier: "ACTION_TWO") is ActionTwo.Type) 61 | 62 | registrar.deregister(actionHandlerForIdentifier: "ACTION_ONE") 63 | XCTAssertEqual(registrar.registeredActions.count, 1) 64 | XCTAssertNil(registrar.action(forIdentifier: "ACTION_ONE")) 65 | XCTAssert(registrar.action(forIdentifier: "ACTION_TWO") is ActionTwo.Type) 66 | } 67 | } 68 | #endif 69 | -------------------------------------------------------------------------------- /Tests/RobinTests/Date+RobinTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import XCTest 25 | @testable import Robin 26 | 27 | class Date_Robin: XCTestCase { 28 | /// Tests whether `Date.next(minutes:)` succeeds. 29 | func testDateStaticNextMinutes() { 30 | let minutes: Int = 12 31 | 32 | XCTAssertEqual(Date.next(minutes: minutes).truncateSeconds(), Date().next(minutes: minutes).truncateSeconds()) 33 | } 34 | 35 | /// Tests whether `Date().next(minutes:)` succeeds. 36 | func testDateNextMinutes() { 37 | let minutes: Int = 12 38 | let date: Date = Date().truncateSeconds() 39 | 40 | let dateAfterMinutes: Date = Calendar.current.date(byAdding: .minute, value: minutes, to: date)!.truncateSeconds() 41 | 42 | let testDate: Date = date.next(minutes: minutes) 43 | 44 | XCTAssertEqual(dateAfterMinutes, testDate) 45 | } 46 | 47 | /// Tests whether `Date.next(hours:)` succeeds. 48 | func testDateStaticNextHours() { 49 | let hours: Int = 12 50 | 51 | XCTAssertEqual(Date.next(hours: hours).truncateSeconds(), Date().next(hours: hours).truncateSeconds()) 52 | } 53 | 54 | /// Tests whether `Date().next(hours:)` succeeds. 55 | func testDateNextHours() { 56 | let hours: Int = 12 57 | let date: Date = Date().truncateSeconds() 58 | let dateAfterHours: Date = Calendar.current.date(byAdding: .hour, value: hours, to: date)!.truncateSeconds() 59 | 60 | let testDate: Date = date.next(hours: hours) 61 | 62 | XCTAssertEqual(dateAfterHours, testDate) 63 | } 64 | 65 | /// Tests whether `Date.next(days:)` succeeds. 66 | func testDateStaticNextDays() { 67 | let days: Int = 12 68 | 69 | XCTAssertEqual(Date.next(days: days).truncateSeconds(), Date().next(days: days).truncateSeconds()) 70 | } 71 | 72 | /// Tests whether `Date().next(days:)` succeeds. 73 | func testDateNextDays() { 74 | let days: Int = 12 75 | let date: Date = Date().truncateSeconds() 76 | 77 | let dateAfterDays: Date = Calendar.current.date(byAdding: .day, value: days, to: date)!.truncateSeconds() 78 | 79 | let testDate: Date = date.next(days: days) 80 | 81 | XCTAssertEqual(dateAfterDays, testDate) 82 | } 83 | 84 | /// Tests whether `Date.next(weeks:)` succeeds. 85 | func testDateStaticNextWeeks() { 86 | let weeks: Int = 12 87 | 88 | XCTAssertEqual(Date.next(weeks: weeks).truncateSeconds(), Date().next(weeks: weeks).truncateSeconds()) 89 | } 90 | 91 | /// Tests whether `Date().next(weeks:)` succeeds. 92 | func testDateNextWeeks() { 93 | let weeks: Int = 12 94 | let date: Date = Date().truncateSeconds() 95 | 96 | let dateAfterWeeks: Date = Calendar.current.date(byAdding: .weekOfMonth, value: weeks, to: date)!.truncateSeconds() 97 | 98 | let testDate: Date = date.next(weeks: weeks) 99 | 100 | XCTAssertEqual(dateAfterWeeks, testDate) 101 | } 102 | 103 | /// Tests whether `Date.next(months:)` succeeds. 104 | func testDateStaticNextMonths() { 105 | let months: Int = 12 106 | 107 | XCTAssertEqual(Date.next(months: months).truncateSeconds(), Date().next(months: months).truncateSeconds()) 108 | } 109 | 110 | /// Tests whether `Date().next(months:)` succeeds. 111 | func testDateNextMonths() { 112 | let months: Int = 12 113 | let date: Date = Date().truncateSeconds() 114 | 115 | let dateAfterMonths: Date = Calendar.current.date(byAdding: .month, value: months, to: date)!.truncateSeconds() 116 | 117 | let testDate: Date = date.next(months: months) 118 | 119 | XCTAssertEqual(dateAfterMonths, testDate) 120 | } 121 | 122 | /// Tests whether `Date.next(years:)` succeeds. 123 | func testDateStaticNextYears() { 124 | let years: Int = 12 125 | 126 | XCTAssertEqual(Date.next(years: years).truncateSeconds(), Date().next(years: years).truncateSeconds()) 127 | } 128 | 129 | /// Tests whether `Date().next(years:)` succeeds. 130 | func testDateNextYears() { 131 | let years: Int = 12 132 | let date: Date = Date().truncateSeconds() 133 | 134 | let dateAfterYears: Date = Calendar.current.date(byAdding: .year, value: years, to: date)!.truncateSeconds() 135 | 136 | let testDate: Date = date.next(years: years) 137 | 138 | XCTAssertEqual(dateAfterYears, testDate) 139 | } 140 | 141 | /// Tests whether `Date().truncateSeconds()` succeeds. 142 | func testDateTruncateSeconds() { 143 | let date: Date = Date() 144 | 145 | let calendar = Calendar.current 146 | let components = (calendar as NSCalendar).components([.year, .month, .day, .hour, .minute], from: date) 147 | let dateWithoutSeconds: Date = calendar.date(from: components)! 148 | 149 | let testDate: Date = date.truncateSeconds() 150 | 151 | XCTAssertEqual(dateWithoutSeconds, testDate) 152 | } 153 | 154 | /// Tests whether `Date().dateWithTime()` succeeds. 155 | func testDateWithTime() { 156 | let time: Int = 0000 157 | let offset: Int = 60 158 | 159 | let calendar = Calendar.current 160 | var components = (calendar as NSCalendar).components([.year, .month, .day, .hour, .minute], from: Date()) 161 | components.minute = (time % 100) + offset % 60 162 | components.hour = (time / 100) + (offset / 60) 163 | var dateWithTime = calendar.date(from: components)! 164 | if dateWithTime < Date() { 165 | dateWithTime = dateWithTime.next(minutes: 60*24) 166 | } 167 | 168 | let testDate: Date = Date.date(withTime: 0000, offset: 60) 169 | 170 | XCTAssertEqual(dateWithTime, testDate) 171 | } 172 | } 173 | #endif 174 | -------------------------------------------------------------------------------- /Tests/RobinTests/Delegate/RobinDelegateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import XCTest 25 | @testable import Robin 26 | 27 | @available(iOS 10.0, macOS 10.14, *) 28 | class RobinDelegateTests: XCTestCase { 29 | static var action: ((RobinNotificationResponse) -> Void)? 30 | 31 | struct ActionOne: RobinActionHandler { 32 | func handle(response: RobinNotificationResponse) { 33 | action?(response) 34 | } 35 | } 36 | 37 | /// Tests whether calling the `didReceive` method with a registered action handler succeeds. 38 | func testDidReceiveResponse() { 39 | let actionExpectation = XCTestExpectation() 40 | RobinDelegateTests.action = { response in 41 | XCTAssertNotNil(response) 42 | XCTAssertNotNil(response.notification) 43 | 44 | actionExpectation.fulfill() 45 | } 46 | 47 | let handlerExpectation = XCTestExpectation() 48 | let completionHandler = { 49 | handlerExpectation.fulfill() 50 | } 51 | 52 | let identifier = "ACTION_ONE" 53 | Robin.actions.register(actionHandler: ActionOne.self, forIdentifier: identifier) 54 | 55 | let notification = RobinNotification(body: "A notification") 56 | let response = SystemNotificationResponseMock(robinNotification: notification, actionIdentifier: identifier) 57 | 58 | Robin.delegate.didReceiveResponse(response, withCompletionHandler: completionHandler) 59 | 60 | wait(for: [actionExpectation, handlerExpectation], timeout: 5.0) 61 | } 62 | 63 | /// Tests whether calling the `didReceive` method with a user-provided text and a registered action handler succeeds. 64 | func testDidReceiveTextResponse() { 65 | let userText = "A text" 66 | 67 | let actionExpectation = XCTestExpectation() 68 | RobinDelegateTests.action = { response in 69 | XCTAssertEqual(response.userText, userText) 70 | XCTAssertNotNil(response) 71 | XCTAssertNotNil(response.notification) 72 | 73 | actionExpectation.fulfill() 74 | } 75 | 76 | let handlerExpectation = XCTestExpectation() 77 | let completionHandler = { 78 | handlerExpectation.fulfill() 79 | } 80 | 81 | let identifier = "ACTION_ONE" 82 | Robin.actions.register(actionHandler: ActionOne.self, forIdentifier: identifier) 83 | 84 | let notification = RobinNotification(body: "A notification") 85 | let response = SystemNotificationTextResponseMock(robinNotification: notification, actionIdentifier: identifier, userText: userText) 86 | 87 | Robin.delegate.didReceiveResponse(response, withCompletionHandler: completionHandler) 88 | 89 | wait(for: [actionExpectation, handlerExpectation], timeout: 5.0) 90 | } 91 | 92 | /// Tests whether calling the `didReceive` method with a non-registered action handler succeeds. 93 | func testDidReceiveResponseNoRegisteredActionHandler() { 94 | let expectation = XCTestExpectation() 95 | let completionHandler = { 96 | expectation.fulfill() 97 | } 98 | 99 | let identifier = "ACTION_ONE" 100 | let notification = RobinNotification(body: "A notification") 101 | let response = SystemNotificationResponseMock(robinNotification: notification, actionIdentifier: identifier) 102 | 103 | Robin.delegate.didReceiveResponse(response, withCompletionHandler: completionHandler) 104 | 105 | wait(for: [expectation], timeout: 5.0) 106 | } 107 | } 108 | #endif 109 | -------------------------------------------------------------------------------- /Tests/RobinTests/Manager/RobinManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import XCTest 25 | @testable import Robin 26 | 27 | @available(iOS 10.0, macOS 10.14, *) 28 | class RobinManagerTests: XCTestCase { 29 | override class func setUp() { 30 | let center = RobinNotificationCenterMock() 31 | let scheduler = NotificationsScheduler(center: center) 32 | let manager = NotificationCenterManager(center: center) 33 | 34 | Robin.notificationsScheduler = scheduler 35 | Robin.notificationCenterManager = manager 36 | } 37 | 38 | override func setUp() { 39 | super.setUp() 40 | // Put setup code here. This method is called before the invocation of each test method in the class. 41 | Robin.scheduler.cancelAll() 42 | Robin.manager.removeAllDelivered() 43 | } 44 | 45 | override func tearDown() { 46 | // Put teardown code here. This method is called after the invocation of each test method in the class. 47 | Robin.scheduler.cancelAll() 48 | Robin.manager.removeAllDelivered() 49 | super.tearDown() 50 | } 51 | 52 | /// Tests whether retrieving all delivered system notifications succeeds. 53 | func testGetDeliveredNotification() { 54 | let count: Int = 1 55 | 56 | let trigger: RobinNotificationTrigger = .date(Date.next(days: 1).truncateSeconds(), repeats: .none) 57 | let notification = RobinNotification(body: "This is a test notification", trigger: trigger) 58 | 59 | _ = Robin.scheduler.schedule(notification: notification) 60 | 61 | let expectation = XCTestExpectation() 62 | 63 | Robin.manager.allDelivered { notifications in 64 | XCTAssertEqual(count, notifications.count) 65 | 66 | let retrievedNotification = notifications[0] 67 | 68 | XCTAssertEqual(retrievedNotification.title, notification.title) 69 | XCTAssertEqual(retrievedNotification.identifier, notification.identifier) 70 | XCTAssertEqual(retrievedNotification.body, notification.body) 71 | XCTAssertNotNil(retrievedNotification.deliveryDate) 72 | XCTAssertEqual(retrievedNotification.trigger, notification.trigger) 73 | 74 | XCTAssertEqual(retrievedNotification.userInfo.count, notification.userInfo.count) 75 | XCTAssertEqual(retrievedNotification.badge, notification.badge) 76 | XCTAssertTrue(notification.sound.isValid()) 77 | XCTAssertEqual(retrievedNotification.scheduled, notification.scheduled) 78 | XCTAssertTrue(retrievedNotification.scheduled) 79 | XCTAssertTrue(retrievedNotification.delivered) 80 | 81 | expectation.fulfill() 82 | } 83 | 84 | wait(for: [expectation], timeout: 5.0) 85 | } 86 | 87 | /// Tests whether removing a delivered notification succeeds. 88 | func testRemoveDeliveredNotification() { 89 | let notification = RobinNotification(body: "This is a test notification") 90 | let anotherNotification = RobinNotification(body: "This is another test notification") 91 | 92 | _ = Robin.scheduler.schedule(notification: notification) 93 | _ = Robin.scheduler.schedule(notification: anotherNotification) 94 | 95 | let deliveryExpectation = XCTestExpectation() 96 | Robin.manager.allDelivered { notifications in 97 | XCTAssertEqual(2, notifications.count) 98 | deliveryExpectation.fulfill() 99 | } 100 | 101 | Robin.manager.removeDelivered(notification: notification) 102 | 103 | let removalExpectation = XCTestExpectation() 104 | Robin.manager.allDelivered { notifications in 105 | XCTAssertEqual(1, notifications.count) 106 | removalExpectation.fulfill() 107 | } 108 | 109 | wait(for: [deliveryExpectation, removalExpectation], timeout: 5.0) 110 | } 111 | 112 | /// Tests whether removing a delivered system notification by identifier succeeds. 113 | func testRemoveDeliveredNotificationByIdentifier() { 114 | let notification = RobinNotification(body: "This is a test notification") 115 | let anotherNotification = RobinNotification(body: "This is another test notification") 116 | 117 | _ = Robin.scheduler.schedule(notification: notification) 118 | _ = Robin.scheduler.schedule(notification: anotherNotification) 119 | 120 | let deliveryExpectation = XCTestExpectation() 121 | Robin.manager.allDelivered { notifications in 122 | XCTAssertEqual(2, notifications.count) 123 | deliveryExpectation.fulfill() 124 | } 125 | 126 | Robin.manager.removeDelivered(withIdentifier: notification.identifier) 127 | 128 | let removalExpectation = XCTestExpectation() 129 | Robin.manager.allDelivered { notifications in 130 | XCTAssertEqual(1, notifications.count) 131 | removalExpectation.fulfill() 132 | } 133 | 134 | wait(for: [deliveryExpectation, removalExpectation], timeout: 5.0) 135 | } 136 | 137 | /// Tests whether removing all delivered system notifications succeeds. 138 | func testRemoveAllDeliveredNotifications() { 139 | let notification = RobinNotification(body: "This is a test notification") 140 | let anotherNotification = RobinNotification(body: "This is another test notification") 141 | 142 | _ = Robin.scheduler.schedule(notification: notification) 143 | _ = Robin.scheduler.schedule(notification: anotherNotification) 144 | 145 | let deliveryExpectation = XCTestExpectation() 146 | Robin.manager.allDelivered { notifications in 147 | XCTAssertEqual(2, notifications.count) 148 | deliveryExpectation.fulfill() 149 | } 150 | 151 | Robin.manager.removeAllDelivered() 152 | 153 | let removalExpectation = XCTestExpectation() 154 | Robin.manager.allDelivered { notifications in 155 | XCTAssertEqual(0, notifications.count) 156 | removalExpectation.fulfill() 157 | } 158 | 159 | wait(for: [deliveryExpectation, removalExpectation], timeout: 5.0) 160 | } 161 | } 162 | #endif 163 | -------------------------------------------------------------------------------- /Tests/RobinTests/Mocks/DeliveredSystemNotificationMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import UserNotifications 25 | @testable import Robin 26 | 27 | @available(iOS 10.0, macOS 10.14, *) 28 | class DeliveredSystemNotificationMock: DeliveredSystemNotification { 29 | let request: UNNotificationRequest 30 | let date: Date 31 | 32 | init(request: UNNotificationRequest) { 33 | self.request = request 34 | self.date = Date() 35 | } 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Tests/RobinTests/Mocks/RobinNotificationCenterMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import UserNotifications 25 | @testable import Robin 26 | 27 | @available(iOS 10.0, macOS 10.14, *) 28 | class RobinNotificationCenterMock: RobinNotificationCenter { 29 | fileprivate var requests: [String : UNNotificationRequest] = [:] 30 | fileprivate var deliveredNotifications: [String : DeliveredSystemNotification] = [:] 31 | var settings = SystemNotificationSettingsMock() 32 | 33 | func requestAuthorization(options: UNAuthorizationOptions, completionHandler: @escaping (Bool, Error?) -> Void) { 34 | completionHandler(true, nil) 35 | } 36 | 37 | func getSettings(completionHandler: @escaping (SystemNotificationSettings) -> Void) { 38 | completionHandler(settings) 39 | } 40 | 41 | func add(_ request: UNNotificationRequest, withCompletionHandler completionHandler: ((Error?) -> Void)?) { 42 | requests[request.identifier] = request 43 | deliveredNotifications[request.identifier] = DeliveredSystemNotificationMock(request: request) 44 | 45 | completionHandler?(nil) 46 | } 47 | 48 | func getPendingNotificationRequests(completionHandler: @escaping ([UNNotificationRequest]) -> Void) { 49 | completionHandler(requests.values.map({ $0 })) 50 | } 51 | 52 | func removePendingNotificationRequests(withIdentifiers identifiers: [String]) { 53 | requests.removeValue(forKey: identifiers[0]) 54 | } 55 | 56 | func removeAllPendingNotificationRequests() { 57 | requests = [:] 58 | } 59 | 60 | func getDelivered(completionHandler: @escaping ([DeliveredSystemNotification]) -> Void) { 61 | completionHandler(deliveredNotifications.values.map({ $0 })) 62 | } 63 | 64 | func removeDeliveredNotifications(withIdentifiers identifiers: [String]) { 65 | deliveredNotifications.removeValue(forKey: identifiers[0]) 66 | } 67 | 68 | func removeAllDeliveredNotifications() { 69 | deliveredNotifications = [:] 70 | } 71 | } 72 | #endif 73 | -------------------------------------------------------------------------------- /Tests/RobinTests/Mocks/SystemNotificationResponseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | @testable import Robin 25 | @testable import UserNotifications 26 | 27 | @available(iOS 10.0, macOS 10.14, *) 28 | struct SystemNotificationResponseMock: SystemNotificationResponse { 29 | var robinNotification: RobinNotification 30 | var actionIdentifier: String 31 | } 32 | 33 | @available(iOS 10.0, macOS 10.14, *) 34 | struct SystemNotificationTextResponseMock: SystemNotificationTextResponse { 35 | var robinNotification: RobinNotification 36 | var actionIdentifier: String 37 | var userText: String 38 | } 39 | #endif 40 | -------------------------------------------------------------------------------- /Tests/RobinTests/Mocks/SystemNotificationSettingsMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | @testable import Robin 25 | import UserNotifications 26 | 27 | @available(iOS 10.0, macOS 10.14, *) 28 | class SystemNotificationSettingsMock: SystemNotificationSettings { 29 | var alertStyle: UNAlertStyle = .none 30 | 31 | var authorizationStatus: UNAuthorizationStatus = .notDetermined 32 | 33 | var badgeSetting: UNNotificationSetting = .disabled 34 | 35 | var _showPreviewsSetting: Any? = nil 36 | @available(iOS 11.0, *) 37 | var showPreviewsSetting: UNShowPreviewsSetting { 38 | return _showPreviewsSetting as? UNShowPreviewsSetting ?? .never 39 | } 40 | 41 | var soundSetting: UNNotificationSetting = .disabled 42 | 43 | var alertSetting: UNNotificationSetting = .disabled 44 | 45 | var notificationCenterSetting: UNNotificationSetting = .disabled 46 | 47 | var lockScreenSetting: UNNotificationSetting = .disabled 48 | 49 | var criticalAlertSetting: UNNotificationSetting = .disabled 50 | 51 | var announcementSetting: UNNotificationSetting = .disabled 52 | 53 | var carPlaySetting: UNNotificationSetting = .disabled 54 | } 55 | #endif 56 | -------------------------------------------------------------------------------- /Tests/RobinTests/Notification/RobinNotificationGroupTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import XCTest 25 | @testable import Robin 26 | 27 | @available(iOS 10.0, macOS 10.14, *) 28 | class RobinNotificationGroupTests: XCTestCase { 29 | /// Tests whether the initialization of `RobinNotificationGroup` succeeds. 30 | func testNotificationGroupInitialization() { 31 | let notifications = [RobinNotification(body: "#1"), RobinNotification(body: "#2"), RobinNotification(body: "#3"), RobinNotification(body: "#4")] 32 | let group = RobinNotificationGroup(notifications: notifications) 33 | 34 | XCTAssertNotNil(group.identifier) 35 | 36 | XCTAssertNotNil(group.notifications) 37 | XCTAssertEqual(group.notifications.count, notifications.count) 38 | XCTAssertEqual(group.notifications, notifications) 39 | 40 | XCTAssertEqual(group.notifications[0].threadIdentifier, group.identifier) 41 | XCTAssertEqual(group.notifications[1].threadIdentifier, group.identifier) 42 | XCTAssertEqual(group.notifications[2].threadIdentifier, group.identifier) 43 | XCTAssertEqual(group.notifications[3].threadIdentifier, group.identifier) 44 | } 45 | 46 | /// Tests whether the initialization of `RobinNotificationGroup` with a custom identifier succeeds. 47 | func testNotificationGroupInitializationWithIdentifier() { 48 | let notifications = [RobinNotification(body: "#1"), RobinNotification(body: "#2"), RobinNotification(body: "#3"), RobinNotification(body: "#4")] 49 | let identifier = "Group" 50 | let group = RobinNotificationGroup(notifications: notifications, identifier: identifier) 51 | 52 | XCTAssertNotNil(group.identifier) 53 | XCTAssertEqual(group.identifier, identifier) 54 | 55 | XCTAssertNotNil(group.notifications) 56 | XCTAssertEqual(group.notifications.count, notifications.count) 57 | XCTAssertEqual(group.notifications, notifications) 58 | 59 | XCTAssertEqual(group.notifications[0].threadIdentifier, identifier) 60 | XCTAssertEqual(group.notifications[1].threadIdentifier, identifier) 61 | XCTAssertEqual(group.notifications[2].threadIdentifier, identifier) 62 | XCTAssertEqual(group.notifications[3].threadIdentifier, identifier) 63 | } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /Tests/RobinTests/Notification/RobinNotificationRepeatsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import XCTest 25 | @testable import Robin 26 | 27 | @available(iOS 10.0, macOS 10.14, *) 28 | class RobinNotificationRepeatsTests: XCTestCase { 29 | func testNoneFromDateComponents() { 30 | let calendar = Calendar.current 31 | 32 | var dateComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: Date()) 33 | dateComponents.second = 0 34 | 35 | XCTAssertEqual(RobinNotificationRepeats.from(dateComponents: dateComponents), .none) 36 | } 37 | 38 | func testHourFromDateComponents() { 39 | let calendar = Calendar.current 40 | 41 | var dateComponents = calendar.dateComponents([.minute], from: Date()) 42 | dateComponents.second = 0 43 | 44 | XCTAssertEqual(RobinNotificationRepeats.from(dateComponents: dateComponents), .hour) 45 | } 46 | 47 | func testDayFromDateComponents() { 48 | let calendar = Calendar.current 49 | 50 | var dateComponents = calendar.dateComponents([.hour, .minute], from: Date()) 51 | dateComponents.second = 0 52 | 53 | XCTAssertEqual(RobinNotificationRepeats.from(dateComponents: dateComponents), .day) 54 | } 55 | 56 | func testWeekFromDateComponents() { 57 | let calendar = Calendar.current 58 | 59 | var dateComponents = calendar.dateComponents([.weekday, .hour, .minute], from: Date()) 60 | dateComponents.second = 0 61 | 62 | XCTAssertEqual(RobinNotificationRepeats.from(dateComponents: dateComponents), .week) 63 | } 64 | 65 | func testMonthFromDateComponents() { 66 | let calendar = Calendar.current 67 | 68 | var dateComponents = calendar.dateComponents([.day, .hour, .minute], from: Date()) 69 | dateComponents.second = 0 70 | 71 | XCTAssertEqual(RobinNotificationRepeats.from(dateComponents: dateComponents), .month) 72 | } 73 | } 74 | #endif 75 | -------------------------------------------------------------------------------- /Tests/RobinTests/Notification/RobinNotificationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import XCTest 25 | @testable import Robin 26 | #if !os(macOS) 27 | import CoreLocation 28 | #endif 29 | 30 | @available(iOS 10.0, macOS 10.14, *) 31 | class RobinNotificationTests: XCTestCase { 32 | 33 | override func setUp() { 34 | super.setUp() 35 | // Put setup code here. This method is called before the invocation of each test method in the class. 36 | } 37 | 38 | override func tearDown() { 39 | // Put teardown code here. This method is called after the invocation of each test method in the class. 40 | super.tearDown() 41 | } 42 | 43 | /// Tests whether the initialization of a date `RobinNotification` succeeds. 44 | func testDateNotificationInitialization() { 45 | let body: String = "This is a test notification" 46 | let notification: RobinNotification = RobinNotification(body: body) 47 | 48 | // Tests body property 49 | XCTAssertEqual(body, notification.body) 50 | 51 | // Tests trigger property 52 | XCTAssertEqual(notification.trigger, .date(Date.next(hours: 1).truncateSeconds(), repeats: .none)) 53 | let date: Date = Date().truncateSeconds() 54 | notification.trigger = .date(date, repeats: .month) 55 | XCTAssertEqual(notification.trigger, .date(date.truncateSeconds(), repeats: .month)) 56 | 57 | // Tests title property 58 | XCTAssertNil(notification.title) 59 | let title: String = "Title" 60 | notification.title = title 61 | XCTAssertEqual(title, notification.title) 62 | 63 | // Tests identifier property 64 | XCTAssertNotNil(notification.identifier) 65 | 66 | // Tests badge property 67 | XCTAssertNil(notification.badge) 68 | let badge: NSNumber = 5 69 | notification.badge = badge 70 | XCTAssertEqual(badge, notification.badge) 71 | 72 | // Tests userInfo property 73 | let userInfo: [AnyHashable : Any] = [Constants.NotificationKeys.date: date] 74 | XCTAssertEqual(userInfo[Constants.NotificationKeys.date] as! Date, notification.userInfo[Constants.NotificationKeys.date] as! Date) 75 | 76 | // Tests sound property 77 | XCTAssertTrue(notification.sound.isValid()) 78 | let sound: String = "SoundName" 79 | notification.sound = RobinNotificationSound(named: sound) 80 | XCTAssertTrue(notification.sound.isValid()) 81 | 82 | // Tests scheduled property 83 | XCTAssertFalse(notification.scheduled) 84 | } 85 | 86 | /// Tests whether the initialization of an interval `RobinNotification` succeeds. 87 | func testIntervalNotificationInitialization() { 88 | let body: String = "This is a test notification" 89 | let notification: RobinNotification = RobinNotification(body: body, trigger: .interval(30, repeats: false)) 90 | 91 | // Tests body property 92 | XCTAssertEqual(body, notification.body) 93 | 94 | // Tests trigger property 95 | XCTAssertEqual(notification.trigger, .interval(30, repeats: false)) 96 | notification.trigger = .interval(60, repeats: true) 97 | XCTAssertEqual(notification.trigger, .interval(60, repeats: true)) 98 | 99 | // Tests title property 100 | XCTAssertNil(notification.title) 101 | let title: String = "Title" 102 | notification.title = title 103 | XCTAssertEqual(title, notification.title) 104 | 105 | // Tests identifier property 106 | XCTAssertNotNil(notification.identifier) 107 | 108 | // Tests badge property 109 | XCTAssertNil(notification.badge) 110 | let badge: NSNumber = 5 111 | notification.badge = badge 112 | XCTAssertEqual(badge, notification.badge) 113 | 114 | // Tests sound property 115 | XCTAssertTrue(notification.sound.isValid()) 116 | let sound: String = "SoundName" 117 | notification.sound = RobinNotificationSound(named: sound) 118 | XCTAssertTrue(notification.sound.isValid()) 119 | 120 | // Tests scheduled property 121 | XCTAssertFalse(notification.scheduled) 122 | } 123 | 124 | #if !os(macOS) 125 | /// Tests whether the initialization of a location `RobinNotification` succeeds. 126 | func testLocationNotificationInitialization() { 127 | let body: String = "This is a test notification" 128 | 129 | /// https://developer.apple.com/documentation/usernotifications/unlocationnotificationtrigger 130 | let center = CLLocationCoordinate2D(latitude: 37.335400, longitude: -122.009201) 131 | let region = CLCircularRegion(center: center, radius: 2000.0, identifier: "Headquarters") 132 | region.notifyOnEntry = true 133 | 134 | let notification: RobinNotification = RobinNotification(body: body, trigger: .location(region, repeats: false)) 135 | 136 | // Tests body property 137 | XCTAssertEqual(body, notification.body) 138 | 139 | // Tests trigger property 140 | XCTAssertEqual(notification.trigger, .location(region, repeats: false)) 141 | notification.trigger = .location(region, repeats: true) 142 | XCTAssertEqual(notification.trigger, .location(region, repeats: true)) 143 | 144 | // Tests title property 145 | XCTAssertNil(notification.title) 146 | let title: String = "Title" 147 | notification.title = title 148 | XCTAssertEqual(title, notification.title) 149 | 150 | // Tests identifier property 151 | XCTAssertNotNil(notification.identifier) 152 | 153 | // Tests badge property 154 | XCTAssertNil(notification.badge) 155 | let badge: NSNumber = 5 156 | notification.badge = badge 157 | XCTAssertEqual(badge, notification.badge) 158 | 159 | // Tests sound property 160 | XCTAssertTrue(notification.sound.isValid()) 161 | let sound: String = "SoundName" 162 | notification.sound = RobinNotificationSound(named: sound) 163 | XCTAssertTrue(notification.sound.isValid()) 164 | 165 | // Tests scheduled property 166 | XCTAssertFalse(notification.scheduled) 167 | } 168 | #endif 169 | 170 | /// Tests whether the initialization of `RobinNotification` with a custom identifier succeeds. 171 | func testNotificationInitializationWithIdentifier() { 172 | let body: String = "This is a test notification" 173 | let identifier: String = "Identifier" 174 | let notification: RobinNotification = RobinNotification(identifier: identifier, body: body) 175 | 176 | XCTAssertEqual(identifier, notification.identifier) 177 | XCTAssertEqual(body, notification.body) 178 | } 179 | 180 | /// Tests whether setting a `RobinNotification` userInfo key succeeds. 181 | func testNotificationUserInfoSet() { 182 | let notification: RobinNotification = RobinNotification(body: "Notification") 183 | 184 | let key: String = "Key" 185 | let value: String = "Value" 186 | notification.setUserInfo(value: value, forKey: key) 187 | 188 | XCTAssertEqual(value, notification.userInfo[key] as! String) 189 | } 190 | 191 | /// Tests whether setting a `RobinNotification` userInfo key succeeds. 192 | func testNotificationUserInfoRemove() { 193 | let notification: RobinNotification = RobinNotification(body: "Notification") 194 | 195 | let key: String = "Key" 196 | let value: String = "Value" 197 | notification.setUserInfo(value: value, forKey: key) 198 | 199 | notification.removeUserInfoValue(forKey: key) 200 | 201 | XCTAssertNil(notification.userInfo[key]) 202 | } 203 | 204 | /// Tests whether initialized RobinNotifications have different identifiers. 205 | func testNotificationNonEquality() { 206 | let firstNotification: RobinNotification = RobinNotification(body: "First Notification") 207 | let secondNotification: RobinNotification = RobinNotification(body: "Second Notification") 208 | 209 | let notEqual: Bool = firstNotification == secondNotification 210 | 211 | XCTAssertFalse(notEqual) 212 | } 213 | 214 | /// Tests whether testing for notification date precedence succeeds. 215 | func testNotificationDatePrecedence() { 216 | let firstNotification: RobinNotification = RobinNotification(body: "First Notification", trigger: .date(.next(minutes: 10), repeats: .none)) 217 | let secondNotification: RobinNotification = RobinNotification(body: "Second Notification", trigger: .date(.next(hours: 1), repeats: .none)) 218 | 219 | let precedes: Bool = firstNotification < secondNotification 220 | 221 | XCTAssertTrue(precedes) 222 | 223 | firstNotification.trigger = .date(.next(days: 1), repeats: .none) 224 | 225 | let doesNotPrecede: Bool = firstNotification < secondNotification 226 | 227 | XCTAssertFalse(doesNotPrecede) 228 | } 229 | } 230 | #endif 231 | -------------------------------------------------------------------------------- /Tests/RobinTests/Notification/UNCalendarNotificationTrigger+RobinTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import XCTest 25 | @testable import Robin 26 | import UserNotifications 27 | 28 | @available(iOS 10.0, macOS 10.14, *) 29 | class UNCalendarNotificationTrigger_RobinTests: XCTestCase { 30 | func testTriggerHourRepeats() { 31 | let date = Date().truncateSeconds() 32 | let trigger = UNCalendarNotificationTrigger(date: date, repeats: .hour) 33 | 34 | XCTAssertEqual(trigger.nextTriggerDate(), date.next(hours: 1)) 35 | } 36 | 37 | func testTriggerDayRepeats() { 38 | let date = Date().truncateSeconds() 39 | let trigger = UNCalendarNotificationTrigger(date: date, repeats: .day) 40 | 41 | XCTAssertEqual(trigger.nextTriggerDate(), date.next(days: 1)) 42 | } 43 | 44 | func testTriggerWeekRepeats() { 45 | let date = Date().truncateSeconds() 46 | let trigger = UNCalendarNotificationTrigger(date: date, repeats: .week) 47 | 48 | XCTAssertEqual(trigger.nextTriggerDate(), date.next(weeks: 1)) 49 | } 50 | 51 | func testTriggerMonthRepeats() { 52 | let date = Date().truncateSeconds() 53 | let trigger = UNCalendarNotificationTrigger(date: date, repeats: .month) 54 | 55 | XCTAssertEqual(trigger.nextTriggerDate(), date.next(months: 1)) 56 | } 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /Tests/RobinTests/Scheduler/RobinSchedulerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import XCTest 25 | @testable import Robin 26 | #if !os(macOS) 27 | import CoreLocation 28 | #endif 29 | 30 | @available(iOS 10.0, macOS 10.14, *) 31 | class RobinSchedulerTests: XCTestCase { 32 | override class func setUp() { 33 | let center = RobinNotificationCenterMock() 34 | let scheduler = NotificationsScheduler(center: center) 35 | 36 | Robin.notificationsScheduler = scheduler 37 | } 38 | 39 | override func setUp() { 40 | super.setUp() 41 | // Put setup code here. This method is called before the invocation of each test method in the class. 42 | Robin.scheduler.cancelAll() 43 | Robin.manager.removeAllDelivered() 44 | } 45 | 46 | override func tearDown() { 47 | // Put teardown code here. This method is called after the invocation of each test method in the class. 48 | Robin.scheduler.cancelAll() 49 | Robin.manager.removeAllDelivered() 50 | super.tearDown() 51 | } 52 | 53 | /// Tests whether scheduling a date `RobinNotification` succeeds. 54 | func testDateNotificationSchedule() { 55 | let notification = RobinNotification(body: "This is a test notification") 56 | 57 | let scheduledNotification = Robin.scheduler.schedule(notification: notification) 58 | 59 | XCTAssertNotNil(scheduledNotification) 60 | XCTAssertTrue(notification.scheduled) 61 | XCTAssertTrue(scheduledNotification!.scheduled) 62 | XCTAssertEqual(1, Robin.scheduler.scheduledCount()) 63 | } 64 | 65 | /// Tests whether scheduling multiple `RobinNotification`s succeeds. 66 | func testNotificationMultipleSchedule() { 67 | let count: Int = 15 68 | for i in 0 ..< count { 69 | let notification = RobinNotification(body: "This is a test notification #\(i + 1)") 70 | 71 | _ = Robin.scheduler.schedule(notification: notification) 72 | } 73 | 74 | XCTAssertEqual(count, Robin.scheduler.scheduledCount()) 75 | } 76 | 77 | /// Tests whether scheduling a `RobinNotification` beyond the allowed maximum succeeds. 78 | func testNotificationScheduleOverAllowed() { 79 | let count: Int = Constants.maximumAllowedNotifications 80 | for i in 0 ..< count { 81 | let notification = RobinNotification(body: "This is a test notification #\(i + 1)") 82 | 83 | _ = Robin.scheduler.schedule(notification: notification) 84 | } 85 | 86 | let notification = RobinNotification(body: "This is an overflow notification") 87 | 88 | let overflowNotification = Robin.scheduler.schedule(notification: notification) 89 | 90 | XCTAssertNil(overflowNotification) 91 | XCTAssertFalse(notification.scheduled) 92 | XCTAssertEqual(count, Robin.scheduler.scheduledCount()) 93 | } 94 | 95 | /// Tests whether rescheduling a `RobinNotification` beyond the allowed maximum succeeds. 96 | func testNotificationReschedule() { 97 | let date: Date = Date.next(days: 1).truncateSeconds() 98 | let notification = RobinNotification(body: "This is a test notification") 99 | 100 | _ = Robin.scheduler.schedule(notification: notification) 101 | 102 | notification.trigger = .date(date, repeats: .none) 103 | 104 | _ = Robin.scheduler.reschedule(notification: notification) 105 | 106 | let rescheduledNotification = Robin.scheduler.notification(withIdentifier: notification.identifier) 107 | 108 | XCTAssertNotNil(rescheduledNotification) 109 | XCTAssertTrue(rescheduledNotification!.scheduled) 110 | XCTAssertEqual(rescheduledNotification!.trigger, .date(date, repeats: .none)) 111 | XCTAssertEqual(1, Robin.scheduler.scheduledCount()) 112 | } 113 | 114 | /// Tests whether canceling a scheduled system notification succeeds. 115 | func testNotificationCancel() { 116 | let notification = RobinNotification(body: "This is a test notification") 117 | 118 | let scheduledNotification = Robin.scheduler.schedule(notification: notification) 119 | 120 | Robin.scheduler.cancel(notification: scheduledNotification!) 121 | 122 | XCTAssertNotNil(scheduledNotification) 123 | XCTAssertFalse(notification.scheduled) 124 | XCTAssertFalse(scheduledNotification!.scheduled) 125 | XCTAssertEqual(0, Robin.scheduler.scheduledCount()) 126 | } 127 | 128 | /// Tests whether canceling a scheduled system notification by identifier succeeds. 129 | func testNotificationIdentifierCancel() { 130 | let notification = RobinNotification(body: "This is a test notification") 131 | 132 | _ = Robin.scheduler.schedule(notification: notification) 133 | 134 | Robin.scheduler.cancel(withIdentifier: notification.identifier) 135 | 136 | XCTAssertTrue(notification.scheduled) 137 | XCTAssertEqual(0, Robin.scheduler.scheduledCount()) 138 | } 139 | 140 | /// Tests whether canceling multiple scheduled system notifications by identifier succeeds. 141 | func testNotificationMultipleCancel() { 142 | let count: Int = 15 143 | let identifier: String = "IDENTIFIER" 144 | for i in 0 ..< count { 145 | let notification = RobinNotification(identifier: identifier, body: "This is a test notification #\(i + 1)") 146 | 147 | _ = Robin.scheduler.schedule(notification: notification) 148 | } 149 | 150 | Robin.scheduler.cancel(withIdentifier: identifier) 151 | 152 | XCTAssertEqual(0, Robin.scheduler.scheduledCount()) 153 | } 154 | 155 | /// Tests whether canceling all scheduled system notifications succeeds. 156 | func testCancelAll() { 157 | let count: Int = 15 158 | for i in 0 ..< count { 159 | let notification = RobinNotification(body: "This is a test notification #\(i + 1)") 160 | 161 | _ = Robin.scheduler.schedule(notification: notification) 162 | } 163 | 164 | Robin.scheduler.cancelAll() 165 | 166 | XCTAssertEqual(0, Robin.scheduler.scheduledCount()) 167 | } 168 | 169 | /// Tests whether retrieving a scheduled system date notification by identifier succeeds. 170 | func testDateNotificationWithIdentifier() { 171 | let notification = RobinNotification(body: "This is a test notification", trigger: .date(Date.next(hours: 1).truncateSeconds(), repeats: .week)) 172 | notification.title = "This is a test title" 173 | notification.badge = 1 174 | notification.sound = RobinNotificationSound(named: "TestSound") 175 | notification.setUserInfo(value: "Value", forKey: "Key") 176 | notification.threadIdentifier = "thread" 177 | notification.categoryIdentifier = "category" 178 | 179 | _ = Robin.scheduler.schedule(notification: notification) 180 | 181 | let retrievedNotification = Robin.scheduler.notification(withIdentifier: notification.identifier) 182 | 183 | XCTAssertEqual(retrievedNotification?.title, notification.title) 184 | XCTAssertEqual(retrievedNotification?.identifier, notification.identifier) 185 | XCTAssertEqual(retrievedNotification?.body, notification.body) 186 | XCTAssertEqual(retrievedNotification?.trigger, .date(Date.next(hours: 1).truncateSeconds(), repeats: .week)) 187 | XCTAssertEqual(retrievedNotification?.userInfo.count, notification.userInfo.count) 188 | XCTAssertEqual(retrievedNotification?.badge, notification.badge) 189 | XCTAssertTrue(notification.sound.isValid()) 190 | XCTAssertEqual(retrievedNotification?.scheduled, notification.scheduled) 191 | XCTAssertTrue(retrievedNotification!.scheduled) 192 | XCTAssertEqual(retrievedNotification?.threadIdentifier, notification.threadIdentifier) 193 | XCTAssertEqual(retrievedNotification?.categoryIdentifier, notification.categoryIdentifier) 194 | XCTAssertEqual(1, Robin.scheduler.scheduledCount()) 195 | } 196 | 197 | /// Tests whether scheduling a date notification with repeating `.none` succeeds. 198 | func testDateNotificationScheduleWithNoneRepeats() { 199 | let notification = RobinNotification(body: "This is a test notification", trigger: .date(Date.next(hours: 1).truncateSeconds(), repeats: .none)) 200 | 201 | _ = Robin.scheduler.schedule(notification: notification) 202 | 203 | let retrievedNotification = Robin.scheduler.notification(withIdentifier: notification.identifier) 204 | 205 | XCTAssertEqual(retrievedNotification?.trigger, .date(Date.next(hours: 1).truncateSeconds(), repeats: .none)) 206 | } 207 | 208 | /// Tests whether scheduling a date notification with repeating `.hour` succeeds. 209 | func testDateNotificationScheduleWithHourRepeats() { 210 | let notification = RobinNotification(body: "This is a test notification", trigger: .date(Date.next(hours: 1).truncateSeconds(), repeats: .hour)) 211 | 212 | _ = Robin.scheduler.schedule(notification: notification) 213 | 214 | let retrievedNotification = Robin.scheduler.notification(withIdentifier: notification.identifier) 215 | 216 | XCTAssertEqual(retrievedNotification?.trigger, .date(Date.next(hours: 1).truncateSeconds(), repeats: .hour)) 217 | } 218 | 219 | /// Tests whether scheduling a date notification with repeating `.day` succeeds. 220 | func testDateNotificationScheduleWithDayRepeats() { 221 | let notification = RobinNotification(body: "This is a test notification", trigger: .date(Date.next(hours: 1).truncateSeconds(), repeats: .day)) 222 | 223 | _ = Robin.scheduler.schedule(notification: notification) 224 | 225 | let retrievedNotification = Robin.scheduler.notification(withIdentifier: notification.identifier) 226 | 227 | XCTAssertEqual(retrievedNotification?.trigger, .date(Date.next(hours: 1).truncateSeconds(), repeats: .day)) 228 | } 229 | 230 | /// Tests whether scheduling a date notification with repeating `.week` succeeds. 231 | func testDateNotificationScheduleWithWeekRepeats() { 232 | let notification = RobinNotification(body: "This is a test notification", trigger: .date(Date.next(hours: 1).truncateSeconds(), repeats: .week)) 233 | 234 | _ = Robin.scheduler.schedule(notification: notification) 235 | 236 | let retrievedNotification = Robin.scheduler.notification(withIdentifier: notification.identifier) 237 | 238 | XCTAssertEqual(retrievedNotification?.trigger, .date(Date.next(hours: 1).truncateSeconds(), repeats: .week)) 239 | } 240 | 241 | /// Tests whether scheduling a date notification with repeating `.month` succeeds. 242 | func testDateNotificationScheduleWithMonthRepeats() { 243 | let notification = RobinNotification(body: "This is a test notification", trigger: .date(Date.next(hours: 1).truncateSeconds(), repeats: .month)) 244 | 245 | _ = Robin.scheduler.schedule(notification: notification) 246 | 247 | let retrievedNotification = Robin.scheduler.notification(withIdentifier: notification.identifier) 248 | 249 | XCTAssertEqual(retrievedNotification?.trigger, .date(Date.next(hours: 1).truncateSeconds(), repeats: .month)) 250 | } 251 | 252 | /// Tests whether scheduling an interval `RobinNotification` succeeds. 253 | func testIntervalNotificationSchedule() { 254 | let notification = RobinNotification(body: "This is a test notification", trigger: .interval(30, repeats: false)) 255 | 256 | let scheduledNotification = Robin.scheduler.schedule(notification: notification) 257 | 258 | XCTAssertNotNil(scheduledNotification) 259 | XCTAssertTrue(notification.scheduled) 260 | XCTAssertTrue(scheduledNotification!.scheduled) 261 | XCTAssertEqual(1, Robin.scheduler.scheduledCount()) 262 | } 263 | 264 | /// Tests whether retrieving a scheduled system interval notification by identifier succeeds. 265 | func testIntervalNotificationWithIdentifier() { 266 | let notification = RobinNotification(body: "This is a test notification", trigger: .interval(30, repeats: false)) 267 | notification.title = "This is a test title" 268 | notification.badge = 1 269 | notification.sound = RobinNotificationSound(named: "TestSound") 270 | notification.setUserInfo(value: "Value", forKey: "Key") 271 | notification.threadIdentifier = "thread" 272 | notification.categoryIdentifier = "category" 273 | 274 | _ = Robin.scheduler.schedule(notification: notification) 275 | 276 | let retrievedNotification = Robin.scheduler.notification(withIdentifier: notification.identifier) 277 | 278 | XCTAssertEqual(retrievedNotification?.title, notification.title) 279 | XCTAssertEqual(retrievedNotification?.identifier, notification.identifier) 280 | XCTAssertEqual(retrievedNotification?.body, notification.body) 281 | XCTAssertEqual(retrievedNotification?.trigger, .interval(30, repeats: false)) 282 | XCTAssertEqual(retrievedNotification?.userInfo.count, notification.userInfo.count) 283 | XCTAssertEqual(retrievedNotification?.badge, notification.badge) 284 | XCTAssertTrue(notification.sound.isValid()) 285 | XCTAssertEqual(retrievedNotification?.scheduled, notification.scheduled) 286 | XCTAssertTrue(retrievedNotification!.scheduled) 287 | XCTAssertEqual(retrievedNotification?.threadIdentifier, notification.threadIdentifier) 288 | XCTAssertEqual(retrievedNotification?.categoryIdentifier, notification.categoryIdentifier) 289 | XCTAssertEqual(1, Robin.scheduler.scheduledCount()) 290 | } 291 | 292 | #if !os(macOS) 293 | /// Tests whether scheduling a location `RobinNotification` succeeds. 294 | func testLocationNotificationSchedule() { 295 | let body: String = "This is a test notification" 296 | 297 | /// https://developer.apple.com/documentation/usernotifications/unlocationnotificationtrigger 298 | let center = CLLocationCoordinate2D(latitude: 37.335400, longitude: -122.009201) 299 | let region = CLCircularRegion(center: center, radius: 2000.0, identifier: "Headquarters") 300 | region.notifyOnEntry = true 301 | 302 | let notification: RobinNotification = RobinNotification(body: body, trigger: .location(region, repeats: false)) 303 | 304 | let scheduledNotification = Robin.scheduler.schedule(notification: notification) 305 | 306 | XCTAssertNotNil(scheduledNotification) 307 | XCTAssertTrue(notification.scheduled) 308 | XCTAssertTrue(scheduledNotification!.scheduled) 309 | XCTAssertEqual(1, Robin.scheduler.scheduledCount()) 310 | } 311 | 312 | /// Tests whether retrieving a scheduled system location notification by identifier succeeds. 313 | func testLocationNotificationWithIdentifier() { 314 | /// https://developer.apple.com/documentation/usernotifications/unlocationnotificationtrigger 315 | let center = CLLocationCoordinate2D(latitude: 37.335400, longitude: -122.009201) 316 | let region = CLCircularRegion(center: center, radius: 2000.0, identifier: "Headquarters") 317 | region.notifyOnEntry = true 318 | 319 | let notification = RobinNotification(body: "This is a test notification", trigger: .location(region, repeats: true)) 320 | notification.title = "This is a test title" 321 | notification.badge = 1 322 | notification.sound = RobinNotificationSound(named: "TestSound") 323 | notification.setUserInfo(value: "Value", forKey: "Key") 324 | notification.threadIdentifier = "thread" 325 | notification.categoryIdentifier = "category" 326 | 327 | _ = Robin.scheduler.schedule(notification: notification) 328 | 329 | let retrievedNotification = Robin.scheduler.notification(withIdentifier: notification.identifier) 330 | 331 | XCTAssertEqual(retrievedNotification?.title, notification.title) 332 | XCTAssertEqual(retrievedNotification?.identifier, notification.identifier) 333 | XCTAssertEqual(retrievedNotification?.body, notification.body) 334 | XCTAssertEqual(retrievedNotification?.trigger, .location(region, repeats: true)) 335 | XCTAssertEqual(retrievedNotification?.userInfo.count, notification.userInfo.count) 336 | XCTAssertEqual(retrievedNotification?.badge, notification.badge) 337 | XCTAssertTrue(notification.sound.isValid()) 338 | XCTAssertEqual(retrievedNotification?.scheduled, notification.scheduled) 339 | XCTAssertTrue(retrievedNotification!.scheduled) 340 | XCTAssertEqual(retrievedNotification?.threadIdentifier, notification.threadIdentifier) 341 | XCTAssertEqual(retrievedNotification?.categoryIdentifier, notification.categoryIdentifier) 342 | XCTAssertEqual(1, Robin.scheduler.scheduledCount()) 343 | } 344 | #endif 345 | 346 | // MARK:- Notification Group 347 | 348 | /// Tests whether scheduling a notification group succeeds. 349 | func testNotificationGroupSchedule() { 350 | let group = RobinNotificationGroup(notifications: [RobinNotification(body: "#1"), RobinNotification(body: "#2"), RobinNotification(body: "#3"), RobinNotification(body: "#4")]) 351 | 352 | let scheduledGroup = Robin.scheduler.schedule(group: group) 353 | 354 | XCTAssertNotNil(scheduledGroup) 355 | XCTAssertEqual(Robin.scheduler.scheduledCount(), 4) 356 | } 357 | 358 | /// Tests whether scheduling a `RobinNotificationGroup` beyond the allowed maximum succeeds. 359 | func testNotificationGroupScheduleOverAllowed() { 360 | let count: Int = Constants.maximumAllowedNotifications 361 | for i in 0 ..< count { 362 | let notification = RobinNotification(body: "This is a test notification #\(i + 1)") 363 | 364 | _ = Robin.scheduler.schedule(notification: notification) 365 | } 366 | 367 | let group = RobinNotificationGroup(notifications: [RobinNotification(body: "#1"), RobinNotification(body: "#2"), RobinNotification(body: "#3"), RobinNotification(body: "#4")]) 368 | 369 | let scheduledGroup = Robin.scheduler.schedule(group: group) 370 | 371 | XCTAssertNil(scheduledGroup) 372 | XCTAssertEqual(Robin.scheduler.scheduledCount(), Constants.maximumAllowedNotifications) 373 | } 374 | 375 | /// Tests whether canceling a notification group succeeds. 376 | func testNotificationGroupCancel() { 377 | let group = RobinNotificationGroup(notifications: [RobinNotification(body: "#1"), RobinNotification(body: "#2"), RobinNotification(body: "#3"), RobinNotification(body: "#4")]) 378 | 379 | let scheduledGroup = Robin.scheduler.schedule(group: group) 380 | 381 | XCTAssertEqual(Robin.scheduler.scheduledCount(), 4) 382 | 383 | if let scheduledGroup = scheduledGroup { 384 | Robin.scheduler.cancel(group: scheduledGroup) 385 | } 386 | 387 | XCTAssertEqual(Robin.scheduler.scheduledCount(), 0) 388 | } 389 | 390 | /// Tests whether canceling a notification group by identifier succeeds. 391 | func testNotificationGroupCancelWithIdentifier() { 392 | let identifier = "Group" 393 | 394 | let group = RobinNotificationGroup(notifications: [RobinNotification(body: "#1"), RobinNotification(body: "#2"), RobinNotification(body: "#3"), RobinNotification(body: "#4")], identifier: identifier) 395 | 396 | _ = Robin.scheduler.schedule(group: group) 397 | 398 | XCTAssertEqual(Robin.scheduler.scheduledCount(), 4) 399 | 400 | Robin.scheduler.cancel(groupWithIdentifier: identifier) 401 | 402 | XCTAssertEqual(Robin.scheduler.scheduledCount(), 0) 403 | } 404 | 405 | /// Tests whether retrieving a notification group by identifier succeeds. 406 | func testNotificationGroupWithIdentifier() { 407 | let identifier = "Group" 408 | 409 | let group = RobinNotificationGroup(notifications: [RobinNotification(body: "#1"), RobinNotification(body: "#2"), RobinNotification(body: "#3"), RobinNotification(body: "#4")], identifier: identifier) 410 | 411 | _ = Robin.scheduler.schedule(group: group) 412 | 413 | XCTAssertEqual(Robin.scheduler.scheduledCount(), 4) 414 | 415 | let scheduledGroup = Robin.scheduler.group(withIdentifier: identifier) 416 | 417 | XCTAssertEqual(scheduledGroup?.identifier, identifier) 418 | XCTAssertEqual(scheduledGroup?.notifications.count, 4) 419 | } 420 | 421 | /// Tests whether retrieving a notification group by identifier succeeds. 422 | func testNotificationGroupWithIdentifierNonExistent() { 423 | let identifier = "Group" 424 | 425 | let group = RobinNotificationGroup(notifications: [RobinNotification(body: "#1"), RobinNotification(body: "#2"), RobinNotification(body: "#3"), RobinNotification(body: "#4")], identifier: identifier) 426 | 427 | _ = Robin.scheduler.schedule(group: group) 428 | 429 | XCTAssertEqual(Robin.scheduler.scheduledCount(), 4) 430 | 431 | let scheduledGroup = Robin.scheduler.group(withIdentifier: "Another group") 432 | 433 | XCTAssertNil(scheduledGroup) 434 | } 435 | } 436 | #endif 437 | -------------------------------------------------------------------------------- /Tests/RobinTests/Settings/RobinNotificationSettingsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import XCTest 25 | @testable import Robin 26 | import UserNotifications 27 | 28 | @available(iOS 10.0, macOS 10.14, *) 29 | class RobinNotificationSettingsTests: XCTestCase { 30 | func testInit() { 31 | var notificationSettings = RobinNotificationSettings(alertStyle: .alert, 32 | authorizationStatus: .denied, 33 | enabledSettings: [.badge, .alert, .sound]) 34 | 35 | XCTAssertEqual(notificationSettings.alertStyle, .alert) 36 | XCTAssertEqual(notificationSettings.authorizationStatus, .denied) 37 | XCTAssertEqual(notificationSettings.enabledSettings, [.badge, .alert, .sound]) 38 | 39 | if #available(iOS 11.0, *) { 40 | notificationSettings._showPreviews = UNShowPreviewsSetting.always 41 | notificationSettings._showPreviews = UNShowPreviewsSetting.never 42 | 43 | XCTAssertEqual(notificationSettings.showPreviews, .always) 44 | } 45 | } 46 | 47 | func testInvalidShowPreviewsSetting() { 48 | var notificationSettings = RobinNotificationSettings(alertStyle: .alert, 49 | authorizationStatus: .denied, 50 | enabledSettings: [.badge, .alert, .sound]) 51 | 52 | XCTAssertEqual(notificationSettings.alertStyle, .alert) 53 | XCTAssertEqual(notificationSettings.authorizationStatus, .denied) 54 | XCTAssertEqual(notificationSettings.enabledSettings, [.badge, .alert, .sound]) 55 | 56 | if #available(iOS 11.0, *) { 57 | notificationSettings._showPreviews = "" 58 | 59 | XCTAssertEqual(notificationSettings.showPreviews, .never) 60 | } 61 | } 62 | } 63 | #endif 64 | -------------------------------------------------------------------------------- /Tests/RobinTests/Settings/RobinSettingsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Ahmed Mohamed 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if !os(watchOS) 24 | import Foundation 25 | import XCTest 26 | @testable import Robin 27 | import UserNotifications 28 | #if !os(macOS) 29 | import UIKit 30 | #else 31 | import AppKit 32 | #endif 33 | 34 | @available(iOS 10.0, macOS 10.14, *) 35 | class RobinSettingsTests: XCTestCase { 36 | fileprivate static let center = RobinNotificationCenterMock() 37 | 38 | override class func setUp() { 39 | Robin.notificationsSettings = NotificationSettings(center: center) 40 | } 41 | 42 | override func tearDown() { 43 | // Reset notification settings. 44 | RobinSettingsTests.center.settings = SystemNotificationSettingsMock() 45 | Robin.settings.forceRefresh() 46 | } 47 | 48 | /// Tests whether requesting authorization succeeds. 49 | func testRequestAuthorization() { 50 | let expectation = XCTestExpectation() 51 | 52 | Robin.settings.requestAuthorization(forOptions: [.alert]) { grant, error in 53 | XCTAssertTrue(grant) 54 | XCTAssertNil(error) 55 | 56 | expectation.fulfill() 57 | } 58 | 59 | wait(for: [expectation], timeout: 5.0) 60 | } 61 | 62 | /// Tests whether retrieving notification settings succeeds. 63 | func testGetNotificationsSettings() { 64 | let settings = Robin.settings 65 | 66 | XCTAssertEqual(settings.alertStyle, .none) 67 | XCTAssertEqual(settings.authorizationStatus, .notDetermined) 68 | 69 | if #available(iOS 11.0, *) { 70 | XCTAssertEqual(settings.showPreviews, .never) 71 | } 72 | 73 | XCTAssertEqual(settings.enabledSettings, []) 74 | } 75 | 76 | func testUpdateSettingsWithForceRefresh() { 77 | let settings = Robin.settings 78 | 79 | XCTAssertEqual(settings.alertStyle, .none) 80 | XCTAssertEqual(settings.authorizationStatus, .notDetermined) 81 | 82 | if #available(iOS 11.0, *) { 83 | XCTAssertEqual(settings.showPreviews, .never) 84 | } 85 | 86 | XCTAssertEqual(settings.enabledSettings, []) 87 | 88 | let centerSettings = RobinSettingsTests.center.settings 89 | 90 | centerSettings.alertStyle = .alert 91 | centerSettings.authorizationStatus = .authorized 92 | 93 | if #available(iOS 11.0, *) { 94 | centerSettings._showPreviewsSetting = UNShowPreviewsSetting.whenAuthenticated 95 | } 96 | 97 | centerSettings.badgeSetting = .enabled 98 | centerSettings.soundSetting = .enabled 99 | centerSettings.alertSetting = .enabled 100 | centerSettings.notificationCenterSetting = .enabled 101 | centerSettings.lockScreenSetting = .enabled 102 | centerSettings.criticalAlertSetting = .enabled 103 | centerSettings.announcementSetting = .enabled 104 | centerSettings.carPlaySetting = .enabled 105 | 106 | settings.forceRefresh() 107 | 108 | XCTAssertEqual(settings.alertStyle, .alert) 109 | XCTAssertEqual(settings.authorizationStatus, .authorized) 110 | 111 | // Available on both iOS 10.0+ and macOS 10.14+ 112 | let baseEnabledSettings: RobinSettingsOptions = [.badge, .sound, .alert, .notificationCenter, .lockScreen] 113 | 114 | if #available(iOS 11.0, *) { 115 | XCTAssertEqual(settings.showPreviews, .whenAuthenticated) 116 | } 117 | 118 | XCTAssertTrue(settings.enabledSettings.contains(baseEnabledSettings)) 119 | 120 | // iOS 12.0+ and macOS 10.14+ 121 | if #available(iOS 12.0, *) { 122 | XCTAssertTrue(settings.enabledSettings.contains(.criticalAlert)) 123 | } 124 | 125 | #if !os(macOS) 126 | // iOS 10.0+ only 127 | XCTAssertTrue(settings.enabledSettings.contains(.carPlay)) 128 | 129 | if #available(iOS 13.0, *) { 130 | XCTAssertTrue(settings.enabledSettings.contains(.announcement)) 131 | } 132 | #endif 133 | } 134 | 135 | func testUpdateSettingsWithNotifications() { 136 | let settings = Robin.settings 137 | 138 | XCTAssertEqual(settings.alertStyle, .none) 139 | XCTAssertEqual(settings.authorizationStatus, .notDetermined) 140 | 141 | if #available(iOS 11.0, *) { 142 | XCTAssertEqual(settings.showPreviews, .never) 143 | } 144 | 145 | XCTAssertEqual(settings.enabledSettings, []) 146 | 147 | let centerSettings = RobinSettingsTests.center.settings 148 | 149 | centerSettings.alertStyle = .alert 150 | centerSettings.authorizationStatus = .authorized 151 | 152 | if #available(iOS 11.0, *) { 153 | centerSettings._showPreviewsSetting = UNShowPreviewsSetting.whenAuthenticated 154 | } 155 | 156 | centerSettings.badgeSetting = .enabled 157 | centerSettings.soundSetting = .enabled 158 | centerSettings.alertSetting = .enabled 159 | centerSettings.notificationCenterSetting = .enabled 160 | centerSettings.lockScreenSetting = .enabled 161 | centerSettings.criticalAlertSetting = .enabled 162 | centerSettings.announcementSetting = .enabled 163 | centerSettings.carPlaySetting = .enabled 164 | 165 | let notificationCenter = NotificationCenter.default 166 | 167 | // Send application lifecycle notification 168 | #if !os(macOS) 169 | notificationCenter.post(name: UIApplication.willEnterForegroundNotification, object: nil) 170 | #else 171 | notificationCenter.post(name: NSApplication.willBecomeActiveNotification, object: nil) 172 | #endif 173 | 174 | XCTAssertEqual(settings.alertStyle, .alert) 175 | XCTAssertEqual(settings.authorizationStatus, .authorized) 176 | 177 | // Available on both iOS 10.0+ and macOS 10.14+ 178 | let baseEnabledSettings: RobinSettingsOptions = [.badge, .sound, .alert, .notificationCenter, .lockScreen] 179 | 180 | if #available(iOS 11.0, *) { 181 | XCTAssertEqual(settings.showPreviews, .whenAuthenticated) 182 | } 183 | 184 | XCTAssertTrue(settings.enabledSettings.contains(baseEnabledSettings)) 185 | 186 | // iOS 12.0+ and macOS 10.14+ 187 | if #available(iOS 12.0, *) { 188 | XCTAssertTrue(settings.enabledSettings.contains(.criticalAlert)) 189 | } 190 | 191 | #if !os(macOS) 192 | // iOS 10.0+ only 193 | XCTAssertTrue(settings.enabledSettings.contains(.carPlay)) 194 | 195 | if #available(iOS 13.0, *) { 196 | XCTAssertTrue(settings.enabledSettings.contains(.announcement)) 197 | } 198 | #endif 199 | } 200 | } 201 | #endif 202 | --------------------------------------------------------------------------------