Compare commits

..

76 Commits

Author SHA1 Message Date
Jørgen Henrichsen 5c8c83f914 Merge pull request #54 from jorgenhenrichsen/swift5
Swift 5
2019-04-18 17:18:00 +02:00
Jørgen Henrichsen ef2d9f5a90 Update README. Update podspec.
Bump version to 0.8.0.
2019-04-18 17:06:57 +02:00
Jørgen Henrichsen 08ebb473f2 Add a custom case to the TimeEventFrequency enum.
Will fix #52.
2019-04-18 16:53:07 +02:00
Jørgen Henrichsen d32a041159 Updated to recommended project settings. 2019-04-18 16:53:07 +02:00
Jørgen Henrichsen 1057a7fca6 Add new handler for non-frozen enums. 2019-04-18 16:53:07 +02:00
Jørgen Henrichsen cee2be50e2 Removed the AudioPlayerDelegate.
Events must be used instead.
2019-04-18 16:53:07 +02:00
Jørgen Henrichsen 8161c3cf02 Update to Swift 5. 2019-04-18 16:53:07 +02:00
Jørgen Henrichsen bb0f301383 Update dependencies. 2019-04-18 16:53:07 +02:00
Jørgen Henrichsen 9da1347d12 Update issue templates
Added issue templates based on Githubs default templates
2019-04-18 12:45:07 +02:00
Jørgen Henrichsen f8026d0915 Update podspec. Update README.
Bump versions to 0.7.2.
2019-04-16 09:15:15 +02:00
Jørgen Henrichsen 9d6534f2c9 Merge pull request #49 from dcvz/master
Add `buferredPosition` property
2019-04-16 09:14:10 +02:00
David Chavez c55c25686c Add buferredPosition property 2019-04-14 23:55:44 +02:00
Jørgen Henrichsen 852578c914 Merge pull request #47 from jorgenhenrichsen/queue-access
Queue access
2019-04-07 21:47:09 +02:00
Jørgen Henrichsen 0ae0427556 Update README. Update podspec.
Bump versions to 0.7.1.
2019-04-07 21:40:38 +02:00
Jørgen Henrichsen 9170be5e36 More information available in the QueuedAudioPlayer.
Can get the currentIndex and all items in the queue.
2019-04-07 20:58:27 +02:00
Jørgen Henrichsen 3930096357 Merge pull request #44 from jorgenhenrichsen/v0.7.0
V0.7.0
2019-03-10 12:22:24 +01:00
Jørgen Henrichsen 9e8ebdfca1 Update README. Update podspec.
Bump versions to 0.7.0.
2019-03-10 10:53:14 +01:00
Jørgen Henrichsen 31f4745bae Update README.
Updated description of events.
2019-03-10 10:47:46 +01:00
Jørgen Henrichsen 4a8af1bce3 Removed unneeded mark. 2019-03-10 10:39:32 +01:00
Jørgen Henrichsen 8e6b72613d Set volume of player in tests to 0. 2019-03-10 10:36:30 +01:00
Jørgen Henrichsen ae760c4cac Put events in seperate struct.
Makes it easier to discover which events are available.
2019-03-10 09:37:00 +01:00
Jørgen Henrichsen b3856e0a7a Tests for AudioPlayer events. 2019-03-10 01:02:24 +01:00
Jørgen Henrichsen 757e5f476c Use direct closure handlers for events. 2019-03-10 01:02:04 +01:00
Jørgen Henrichsen 1f5eae407d Make event calls async.
- Events will be emitted on the utility class.
- Use a semaphore to avoid race conditions in the Event class.
- Update example to dispatch ui updates to the main queue.
2019-03-09 21:30:50 +01:00
Jørgen Henrichsen 29660371c9 Updated tests to use events. 2019-03-09 21:30:02 +01:00
Jørgen Henrichsen c2530a0193 Update README.
Add details about the event handlers.
2019-03-09 20:31:27 +01:00
Jørgen Henrichsen 6804cf5163 Add events as an alternative to delegate. Updated example app.
This allows for selective listening to events, and no need to implement empty uneeded delegate methods.
Added a deprecation message to the delegate class, will most likely remove AudioPlayerDelegate in a future update.
2019-03-09 20:18:07 +01:00
Jørgen Henrichsen 6ea2e082b0 Call func audioPlayer(itemPlaybackEndedWithReason:) directly, instead of calling the AVWrapperDelegate implementation method.
Also remove the corresponding delegate method in the AVWrapper, and add a `AVWrapperItemDidPlayToEndTime` method instead.
2019-03-03 20:04:27 +01:00
Jørgen Henrichsen b6b4599ef5 Update nowPlayingInfo elapsed time immediately when seeking.
Causes less jumpy slider when seeking from the lockscreen.
Will rewind to the currentTime if the seeking fails.
2019-03-03 19:46:10 +01:00
Jørgen Henrichsen 3ccf9ed20e Update README
Add a missing `shared` call for the AudioSessionController.
2019-03-03 16:26:36 +01:00
Jørgen Henrichsen d6eb187788 Update README
Add more descriptions on setting the nowPlayingInfo
2019-03-03 16:25:46 +01:00
Jørgen Henrichsen 1febb782d8 Add tests for NowPlayingInfoController. 2019-03-03 16:10:30 +01:00
Jørgen Henrichsen c4c6f42ac0 Added clear() method. Use NowPlayingInfoCenter protocol.
`NowPlayingInfoCenter` makes the info center mockable, so the controller can be easily tested.
2019-03-03 16:09:29 +01:00
Jørgen Henrichsen d8b4466629 Add test for NowPlayingInfo artwork. 2019-03-01 12:21:00 +01:00
Jørgen Henrichsen cb9dec49b2 Test updating of NowPlayingInfo.
* Create protocol NowPlayingInfoControllerProtocol to make it more testable (so it can be mocked).
2019-02-28 23:06:03 +01:00
Jørgen Henrichsen 7e46a91e73 Don't reset info dict on set(keyValues:). Actually apply new values to infocenter.
The set(keyValues:) function did not actually apply the new values to the nowPlayingInfo, but does now.
2019-02-27 22:35:14 +01:00
Jørgen Henrichsen 8ca4a873a5 Remove reloadNowPlayingInfo() and replace it with two seperate functions for meta values and playbackvalues.
- loadNowPlayingMetaValues() loads the meta data from the AudioItem into the NowPlayingInfoController.
- updateNowPlayingPlaybackValues() updates the playback related values in the NowPlayingInfoController.
- Earlier, reloadNowPlayingInfo would have no effect if `automaticallyUpdateNowPlayingInfo`  was set to false. This is no longer the case, and both playbackValues and meta data can be forced to update without the player loading it automatically.
- Playback values are no longer updated each second in the NowPlayingInfoController. It should be sufficient to keep the rate, currentTime and duration in sync when they change.
2019-02-27 17:03:17 +01:00
Jørgen Henrichsen 3f54de70bc Merge pull request #42 from jorgenhenrichsen/bitrise
Bitrise
2019-02-24 14:21:28 +01:00
Jørgen Henrichsen cae549a401 Merge branch 'bitrise' of github.com:jorgenhenrichsen/SwiftAudio into bitrise 2019-02-24 14:07:41 +01:00
Jørgen Henrichsen 85fecef45e Deleted travis.yml. 2019-02-24 14:06:38 +01:00
Jørgen Henrichsen e2375f7a82 Update README
Use Bitrise for build status
2019-02-24 13:49:39 +01:00
Jørgen Henrichsen 3c8402503b Update podspec.
Bump version to 0.6.2
2019-02-24 13:19:08 +01:00
Jørgen Henrichsen 4c46137498 Update README
Bump version to 0.6.2
2019-02-24 13:18:20 +01:00
Jørgen Henrichsen a4e023d75f Merge pull request #41 from jorgenhenrichsen/initial-time
Add possiblity to start AudioItem from certain time.
2019-02-24 13:17:26 +01:00
Jørgen Henrichsen fe549a522a Update README
Add info about `InitialTiming`
2019-02-24 12:30:43 +01:00
Jørgen Henrichsen 6cc0638a70 Add possiblity to start AudioItem from certain time. 2019-02-24 11:58:03 +01:00
Jørgen Henrichsen 34c1f493ae Update podspec. Update Readme.
Bump versions to 0.6.1.
2019-01-31 22:08:16 +01:00
Jørgen Henrichsen 30750a0c81 Merge pull request #40 from jorgenhenrichsen/fix/seekto-imprecision
Increase timescale in seekto(time:) method.
2019-01-31 21:59:45 +01:00
Jørgen Henrichsen e57cf9d2e5 Increase timescale in seekto(time:) method.
Stops truncating of the seeked to time.
Also updated seekTo test to actually test the seeked value.
2019-01-31 18:31:34 +01:00
Jørgen Henrichsen 0eeedc6467 Merge pull request #38 from dcvz/patch-1
Get duration directly from asset
2019-01-31 18:26:51 +01:00
David Chavez e96cbf6337 Get duration directly from asset
It is more reliable because the current item directly will return NaN depending on what's loaded.
2019-01-30 20:39:19 +01:00
Jørgen Henrichsen debc1c519f Merge pull request #36 from jorgenhenrichsen/swift4.2
- Update to Swift 4.2
- Use Xcode 10.1 in CI
- Fix some compiler warnings in the project
- Removes custom `AudioSessionCategory`-enum as `AudioSession.Category` is introduced with Swift 4.2
2018-12-25 18:11:20 +01:00
Jørgen Henrichsen 09bec023a9 Update project version to 0.6.0. 2018-12-25 18:01:40 +01:00
Jørgen Henrichsen 44e022389c Merge branch 'swift4.2' of github.com:jorgenhenrichsen/SwiftAudio into swift4.2 2018-12-25 17:39:52 +01:00
Jørgen Henrichsen 05ca97b8eb Deleted AudioSessionCategory file. 2018-12-25 17:39:32 +01:00
Jørgen Henrichsen f6ff2b4cc0 Update travis
Test on iOS 11.4
2018-12-25 11:39:45 +01:00
Jørgen Henrichsen 9e3f5c0291 Use longer source for duration testing. 2018-12-23 14:36:39 +01:00
Jørgen Henrichsen 8ba40ca45f Updated project settings. 2018-12-23 14:30:23 +01:00
Jørgen Henrichsen 9919bde9fe Fixed compiler warnings. 2018-12-23 14:28:44 +01:00
Jørgen Henrichsen 3ba62d8657 Update travis config
Update to Xcode 10.1 and iOS 12.1
2018-12-23 12:52:27 +01:00
Jørgen Henrichsen 0f283b171f Update swift-version 2018-12-23 12:43:48 +01:00
Jørgen Henrichsen f6b5e30e85 Converted SwiftAudio and example to Swift 4.2 2018-12-23 12:38:39 +01:00
Jørgen Henrichsen 04296fa681 Update podspec. Update readme.
Bumpe versions to 0.5.0.
2018-11-18 23:42:40 +01:00
Jørgen Henrichsen 252ed947d2 Merge pull request #34 from jorgenhenrichsen/optional-time-pitch-algo
Optional time pitch algo
2018-11-18 23:35:11 +01:00
Jørgen Henrichsen b5fdb5c54e Merge branch 'master' into optional-time-pitch-algo 2018-11-18 22:59:00 +01:00
Jørgen Henrichsen 1b4c0b0d3b Update Readme. Update podspec.
Verision 0.4.4
2018-11-16 11:54:49 +01:00
Jørgen Henrichsen e0aa2a09a9 Merge pull request #35 from Alex601t/issue26
Fixes #26
2018-11-16 11:52:44 +01:00
Alexander ab0eb4f8eb Merge pull request #1 from jorgenhenrichsen/Alex601t-issue26
Observe loadedTimeRanges for AVPlayerItem
Use items duration.seconds if available.
2018-11-16 10:58:49 +03:00
Jørgen Henrichsen 99e7c65bbc Use items duration.seconds if available. 2018-11-15 20:35:07 +01:00
Jørgen Henrichsen 9072259631 Observe loadedTimeRanges for AVPlayerItem 2018-11-14 16:38:55 +01:00
Alexander eb9af1007a Issue 26:
Handling player current item duration from loaded time ranges
2018-11-14 16:34:58 +03:00
Jørgen Henrichsen 69d3a9c0c0 Test TimePitchAlgorithms in the AudioPlayer. 2018-11-14 10:06:14 +01:00
Jørgen Henrichsen 43821c68a9 Made DefaultAudioItem not conform to TimePitching.
New DefaultAudioItemTimePitching can be used instead.
2018-11-14 10:05:20 +01:00
Jørgen Henrichsen 610ff4c7f3 Made getter for wrapper internal.
Makes testing easier.
2018-11-14 10:04:12 +01:00
Jørgen Henrichsen 1fc533214f Update Readme.
- Remove timePitch algorithm from DefaultAudioItem inits
- Add audioTimePitchAlgorithm in the list of configurations
2018-11-09 13:50:53 +01:00
Jørgen Henrichsen 71f22c3e25 Time pitch algorithm is no longer required from AudioItems.
New protocol TimePitcher can be conformed to if AudioItems need to give their own Time Pitch Algorithm.
2018-11-09 13:45:48 +01:00
108 changed files with 2595 additions and 1781 deletions
+32
View File
@@ -0,0 +1,32 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
If needed, include code examples for the problem or steps to reproduce.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 0.7.0]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6, Simulator iPhone6 ...]
- OS: [e.g. iOS8.1]
- Version [e.g. 0.7.0]
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution or feature you would like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+1 -1
View File
@@ -1 +1 @@
4.0
4.2
-17
View File
@@ -1,17 +0,0 @@
# references:
# * http://www.objc.io/issue-6/travis-ci.html
# * https://github.com/supermarin/xcpretty#usage
osx_image: xcode9.4
language: swift
cache: cocoapods
podfile: Example/Podfile
before_install:
- gem install cocoapods # Since Travis is not always on latest version
- pod install --project-directory=Example --repo-update
script:
- set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/SwiftAudio.xcworkspace -scheme SwiftAudio-Example -sdk iphonesimulator11.4 -destination "OS=11.4,name=iPhone X" | xcpretty
- pod lib lint
after_success:
- bash <(curl -s https://codecov.io/bash) -J 'SwiftAudio'
+2 -2
View File
@@ -7,8 +7,8 @@ target 'SwiftAudio_Example' do
target 'SwiftAudio_Tests' do
inherit! :search_paths
pod 'Quick', '~> 1.3.0'
pod 'Nimble' , '~> 7.3.0'
pod 'Quick', '~> 2.0.0'
pod 'Nimble' , '~> 8.0.0'
end
end
+9 -9
View File
@@ -1,11 +1,11 @@
PODS:
- Nimble (7.3.1)
- Quick (1.3.2)
- SwiftAudio (0.3.3)
- Nimble (8.0.1)
- Quick (2.0.0)
- SwiftAudio (0.7.2)
DEPENDENCIES:
- Nimble (~> 7.3.0)
- Quick (~> 1.3.0)
- Nimble (~> 8.0.0)
- Quick (~> 2.0.0)
- SwiftAudio (from `../`)
SPEC REPOS:
@@ -18,10 +18,10 @@ EXTERNAL SOURCES:
:path: "../"
SPEC CHECKSUMS:
Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae
Quick: 2623cb30d7a7f41ca62f684f679586558f483d46
SwiftAudio: 2e712c3e04cf172d05639d7bb1516db7afd195da
Nimble: 45f786ae66faa9a709624227fae502db55a8bdd0
Quick: ce1276c7c27ba2da3cb2fd0cde053c3648b3b22d
SwiftAudio: a7bb22e98fd48fd908f7ffca992166e0057ce8ea
PODFILE CHECKSUM: 8a75946cbc65d8d98176f80a88d8363a28d118ce
PODFILE CHECKSUM: 08ea4075437f8ff3c4f84ed70827a8132461373f
COCOAPODS: 1.5.3
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "SwiftAudio",
"version": "0.3.3",
"version": "0.7.2",
"summary": "Easy audio streaming for iOS",
"description": "SwiftAudio is an audio player written in Swift, making it simpler to work with audio playback from streams and files.",
"homepage": "https://github.com/jorgenhenrichsen/SwiftAudio",
@@ -13,7 +13,7 @@
},
"source": {
"git": "https://github.com/jorgenhenrichsen/SwiftAudio.git",
"tag": "0.3.3"
"tag": "0.7.2"
},
"platforms": {
"ios": "10.0"
+9 -9
View File
@@ -1,11 +1,11 @@
PODS:
- Nimble (7.3.1)
- Quick (1.3.2)
- SwiftAudio (0.3.3)
- Nimble (8.0.1)
- Quick (2.0.0)
- SwiftAudio (0.7.2)
DEPENDENCIES:
- Nimble (~> 7.3.0)
- Quick (~> 1.3.0)
- Nimble (~> 8.0.0)
- Quick (~> 2.0.0)
- SwiftAudio (from `../`)
SPEC REPOS:
@@ -18,10 +18,10 @@ EXTERNAL SOURCES:
:path: "../"
SPEC CHECKSUMS:
Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae
Quick: 2623cb30d7a7f41ca62f684f679586558f483d46
SwiftAudio: 2e712c3e04cf172d05639d7bb1516db7afd195da
Nimble: 45f786ae66faa9a709624227fae502db55a8bdd0
Quick: ce1276c7c27ba2da3cb2fd0cde053c3648b3b22d
SwiftAudio: a7bb22e98fd48fd908f7ffca992166e0057ce8ea
PODFILE CHECKSUM: 8a75946cbc65d8d98176f80a88d8363a28d118ce
PODFILE CHECKSUM: 08ea4075437f8ff3c4f84ed70827a8132461373f
COCOAPODS: 1.5.3
+7 -22
View File
@@ -4,6 +4,7 @@
[![CocoaPods](https://img.shields.io/cocoapods/v/Nimble.svg)](https://cocoapods.org/pods/Nimble)
[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![Platforms](https://img.shields.io/cocoapods/p/Nimble.svg)](https://cocoapods.org/pods/Nimble)
[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
Use Nimble to express the expected outcomes of Swift
or Objective-C expressions. Inspired by
@@ -306,8 +307,7 @@ In Nimble, it's easy to make expectations on values that are updated
asynchronously. Just use `toEventually` or `toEventuallyNot`:
```swift
// Swift 3.0 and later
// Swift
DispatchQueue.main.async {
ocean.add("dolphins")
ocean.add("whales")
@@ -316,17 +316,6 @@ expect(ocean).toEventually(contain("dolphins", "whales"))
```
```swift
// Swift 2.3 and earlier
dispatch_async(dispatch_get_main_queue()) {
ocean.add("dolphins")
ocean.add("whales")
}
expect(ocean).toEventually(contain("dolphins", "whales"))
```
```objc
// Objective-C
@@ -857,11 +846,7 @@ Notes:
## Swift Error Handling
If you're using Swift 2.0 or newer, you can use the `throwError` matcher to check if an error is thrown.
Note:
The following code sample references the `Swift.Error` protocol.
This is `Swift.ErrorProtocol` in versions of Swift prior to version 3.0.
You can use the `throwError` matcher to check if an error is thrown.
```swift
// Swift
@@ -1277,7 +1262,7 @@ public func equal<T: Equatable>(expectedValue: T?) -> Predicate<T> {
// Predicate { actual in ... }
//
// But shown with types here for clarity.
return Predicate { (actual: Expression<T>) throws -> PredicateResult in
return Predicate { (actualExpression: Expression<T>) throws -> PredicateResult in
let msg = ExpectationMessage.expectedActualValueTo("equal <\(expectedValue)>")
if let actualValue = try actualExpression.evaluate() {
return PredicateResult(
@@ -1673,11 +1658,11 @@ backported.
The deprecating plan is a 3 major versions removal. Which is as follows:
1. Introduce new `Predicate` API, deprecation warning for old matcher APIs.
(Nimble `v7.x.x`)
(Nimble `v7.x.x` and `v8.x.x`)
2. Introduce warnings on migration-path features (`.predicate`,
`Predicate`-constructors with similar arguments to old API). (Nimble
`v8.x.x`)
3. Remove old API. (Nimble `v9.x.x`)
`v9.x.x`)
3. Remove old API. (Nimble `v10.x.x`)
# Installing Nimble
@@ -13,5 +13,6 @@ public protocol AssertionHandler {
///
/// @see AssertionHandler
public var NimbleAssertionHandler: AssertionHandler = { () -> AssertionHandler in
// swiftlint:disable:previous identifier_name
return isXCTestAvailable() ? NimbleXCTestHandler() : NimbleXCTestUnavailableHandler()
}()
@@ -37,21 +37,48 @@ public class AssertionRecorder: AssertionHandler {
}
}
extension NMBExceptionCapture {
internal func tryBlockThrows(_ unsafeBlock: () throws -> Void) throws {
var catchedError: Error?
tryBlock {
do {
try unsafeBlock()
} catch {
catchedError = error
}
}
if let error = catchedError {
throw error
}
}
}
/// Allows you to temporarily replace the current Nimble assertion handler with
/// the one provided for the scope of the closure.
///
/// Once the closure finishes, then the original Nimble assertion handler is restored.
///
/// @see AssertionHandler
public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler, closure: () throws -> Void) {
public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler,
file: FileString = #file,
line: UInt = #line,
closure: () throws -> Void) {
let environment = NimbleEnvironment.activeInstance
let oldRecorder = environment.assertionHandler
let capturer = NMBExceptionCapture(handler: nil, finally: ({
environment.assertionHandler = oldRecorder
}))
environment.assertionHandler = tempAssertionHandler
capturer.tryBlock {
try! closure()
do {
try capturer.tryBlockThrows {
try closure()
}
} catch {
let failureMessage = FailureMessage()
failureMessage.stringValue = "unexpected error thrown: <\(error)>"
let location = SourceLocation(file: file, line: line)
tempAssertionHandler.assert(false, message: failureMessage, location: location)
}
}
@@ -1,6 +1,6 @@
import Foundation
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
#if canImport(Darwin) && !SWIFT_PACKAGE
private func from(objcPredicate: NMBPredicate) -> Predicate<NSObject> {
return Predicate { actualExpression in
@@ -15,6 +15,7 @@ internal struct ObjCMatcherWrapper: Matcher {
func matches(_ actualExpression: Expression<NSObject>, failureMessage: FailureMessage) -> Bool {
return matcher.matches(
// swiftlint:disable:next force_try
({ try! actualExpression.evaluate() }),
failureMessage: failureMessage,
location: actualExpression.location)
@@ -22,6 +23,7 @@ internal struct ObjCMatcherWrapper: Matcher {
func doesNotMatch(_ actualExpression: Expression<NSObject>, failureMessage: FailureMessage) -> Bool {
return matcher.doesNotMatch(
// swiftlint:disable:next force_try
({ try! actualExpression.evaluate() }),
failureMessage: failureMessage,
location: actualExpression.location)
@@ -30,11 +32,13 @@ internal struct ObjCMatcherWrapper: Matcher {
// Equivalent to Expectation, but for Nimble's Objective-C interface
public class NMBExpectation: NSObject {
// swiftlint:disable identifier_name
internal let _actualBlock: () -> NSObject?
internal var _negative: Bool
internal let _file: FileString
internal let _line: UInt
internal var _timeout: TimeInterval = 1.0
// swiftlint:enable identifier_name
@objc public init(actualBlock: @escaping () -> NSObject?, negative: Bool, file: FileString, line: UInt) {
self._actualBlock = actualBlock
@@ -1,6 +1,6 @@
import Foundation
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
// swiftlint:disable line_length
public typealias MatcherBlock = (_ actualExpression: Expression<NSObject>, _ failureMessage: FailureMessage) throws -> Bool
@@ -8,8 +8,10 @@ public typealias FullMatcherBlock = (_ actualExpression: Expression<NSObject>, _
// swiftlint:enable line_length
public class NMBObjCMatcher: NSObject, NMBMatcher {
// swiftlint:disable identifier_name
let _match: MatcherBlock
let _doesNotMatch: MatcherBlock
// swiftlint:enable identifier_name
let canMatchNil: Bool
public init(canMatchNil: Bool, matcher: @escaping MatcherBlock, notMatcher: @escaping MatcherBlock) {
@@ -3,7 +3,7 @@ import Foundation
/// "Global" state of Nimble is stored here. Only DSL functions should access / be aware of this
/// class' existence
internal class NimbleEnvironment {
internal class NimbleEnvironment: NSObject {
static var activeInstance: NimbleEnvironment {
get {
let env = Thread.current.threadDictionary["NimbleEnvironment"]
@@ -20,6 +20,7 @@ internal class NimbleEnvironment {
}
}
// swiftlint:disable:next todo
// TODO: eventually migrate the global to this environment value
var assertionHandler: AssertionHandler {
get { return NimbleAssertionHandler }
@@ -29,17 +30,14 @@ internal class NimbleEnvironment {
var suppressTVOSAssertionWarning: Bool = false
var awaiter: Awaiter
init() {
let timeoutQueue: DispatchQueue
if #available(OSX 10.10, *) {
timeoutQueue = DispatchQueue.global(qos: .userInitiated)
} else {
timeoutQueue = DispatchQueue.global(priority: .high)
}
override init() {
let timeoutQueue = DispatchQueue.global(qos: .userInitiated)
awaiter = Awaiter(
waitLock: AssertionWaitLock(),
asyncQueue: .main,
timeoutQueue: timeoutQueue)
timeoutQueue: timeoutQueue
)
super.init()
}
}
@@ -64,7 +64,7 @@ class NimbleXCTestUnavailableHandler: AssertionHandler {
#endif
func isXCTestAvailable() -> Bool {
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
// XCTest is weakly linked and so may not be present
return NSClassFromString("XCTestCase") != nil
#else
@@ -77,15 +77,14 @@ public func recordFailure(_ message: String, location: SourceLocation) {
XCTFail("\(message)", file: location.file, line: location.line)
#else
if let testCase = CurrentTestCaseTracker.sharedInstance.currentTestCase {
#if swift(>=4)
let line = Int(location.line)
#else
let line = location.line
#endif
testCase.recordFailure(withDescription: message, inFile: location.file, atLine: line, expected: true)
} else {
let msg = "Attempted to report a test failure to XCTest while no test case was running. " +
"The failure was:\n\"\(message)\"\nIt occurred at: \(location.file):\(location.line)"
let msg = """
Attempted to report a test failure to XCTest while no test case was running. The failure was:
\"\(message)\"
It occurred at: \(location.file):\(location.line)
"""
NSException(name: .internalInconsistencyException, reason: msg, userInfo: nil).raise()
}
#endif
+10 -4
View File
@@ -14,7 +14,7 @@ internal class NMBWait: NSObject {
// About these kind of lines, `@objc` attributes are only required for Objective-C
// support, so that should be conditional on Darwin platforms and normal Xcode builds
// (non-SwiftPM builds).
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
#if canImport(Darwin) && !SWIFT_PACKAGE
@objc
internal class func until(
timeout: TimeInterval,
@@ -87,13 +87,19 @@ internal class NMBWait: NSObject {
}
}
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
#if canImport(Darwin) && !SWIFT_PACKAGE
@objc(untilFile:line:action:)
internal class func until(_ file: FileString = #file, line: UInt = #line, action: @escaping (() -> Void) -> Void) {
internal class func until(
_ file: FileString = #file,
line: UInt = #line,
action: @escaping (@escaping () -> Void) -> Void) {
until(timeout: 1, file: file, line: line, action: action)
}
#else
internal class func until(_ file: FileString = #file, line: UInt = #line, action: @escaping (() -> Void) -> Void) {
internal class func until(
_ file: FileString = #file,
line: UInt = #line,
action: @escaping (@escaping () -> Void) -> Void) {
until(timeout: 1, file: file, line: line, action: action)
}
#endif
+11 -7
View File
@@ -43,12 +43,13 @@ internal func nimblePrecondition(
line: UInt = #line) {
let result = expr()
if !result {
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
let e = NSException(
#if canImport(Darwin)
let exception = NSException(
name: NSExceptionName(name()),
reason: message(),
userInfo: nil)
e.raise()
userInfo: nil
)
exception.raise()
#else
preconditionFailure("\(name()) - \(message())", file: file, line: line)
#endif
@@ -56,9 +57,12 @@ internal func nimblePrecondition(
}
internal func internalError(_ msg: String, file: FileString = #file, line: UInt = #line) -> Never {
// swiftlint:disable line_length
fatalError(
"Nimble Bug Found: \(msg) at \(file):\(line).\n" +
"Please file a bug to Nimble: https://github.com/Quick/Nimble/issues with the " +
"code snippet that caused this error."
"""
Nimble Bug Found: \(msg) at \(file):\(line).
Please file a bug to Nimble: https://github.com/Quick/Nimble/issues with the code snippet that caused this error.
"""
)
// swiftlint:enable line_length
}
+16 -13
View File
@@ -75,6 +75,7 @@ public indirect enum ExpectationMessage {
}
internal func visitLeafs(_ f: (ExpectationMessage) -> ExpectationMessage) -> ExpectationMessage {
// swiftlint:disable:previous identifier_name
switch self {
case .fail, .expectedTo, .expectedActualValueTo, .expectedCustomValueTo:
return f(self)
@@ -90,6 +91,7 @@ public indirect enum ExpectationMessage {
/// Replaces a primary expectation with one returned by f. Preserves all composite expectations
/// that were built upon it (aka - all appended(message:) and appended(details:).
public func replacedExpectation(_ f: @escaping (ExpectationMessage) -> ExpectationMessage) -> ExpectationMessage {
// swiftlint:disable:previous identifier_name
func walk(_ msg: ExpectationMessage) -> ExpectationMessage {
switch msg {
case .fail, .expectedTo, .expectedActualValueTo, .expectedCustomValueTo:
@@ -124,6 +126,7 @@ public indirect enum ExpectationMessage {
return visitLeafs(walk)
}
// swiftlint:disable:next todo
// TODO: test & verify correct behavior
internal func prepended(message: String) -> ExpectationMessage {
return .prepends(message, self)
@@ -183,32 +186,32 @@ public indirect enum ExpectationMessage {
extension FailureMessage {
internal func toExpectationMessage() -> ExpectationMessage {
let defaultMsg = FailureMessage()
if expected != defaultMsg.expected || _stringValueOverride != nil {
let defaultMessage = FailureMessage()
if expected != defaultMessage.expected || _stringValueOverride != nil {
return .fail(stringValue)
}
var msg: ExpectationMessage = .fail(userDescription ?? "")
var message: ExpectationMessage = .fail(userDescription ?? "")
if actualValue != "" && actualValue != nil {
msg = .expectedCustomValueTo(postfixMessage, actualValue ?? "")
} else if postfixMessage != defaultMsg.postfixMessage {
message = .expectedCustomValueTo(postfixMessage, actualValue ?? "")
} else if postfixMessage != defaultMessage.postfixMessage {
if actualValue == nil {
msg = .expectedTo(postfixMessage)
message = .expectedTo(postfixMessage)
} else {
msg = .expectedActualValueTo(postfixMessage)
message = .expectedActualValueTo(postfixMessage)
}
}
if postfixActual != defaultMsg.postfixActual {
msg = .appends(msg, postfixActual)
if postfixActual != defaultMessage.postfixActual {
message = .appends(message, postfixActual)
}
if let m = extendedMessage {
msg = .details(msg, m)
if let extended = extendedMessage {
message = .details(message, extended)
}
return msg
return message
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
public class NMBExpectationMessage: NSObject {
private let msg: ExpectationMessage
+2
View File
@@ -24,8 +24,10 @@ internal func memoizedClosure<T>(_ closure: @escaping () throws -> T) -> (Bool)
/// This provides a common consumable API for matchers to utilize to allow
/// Nimble to change internals to how the captured closure is managed.
public struct Expression<T> {
// swiftlint:disable identifier_name
internal let _expression: (Bool) throws -> T?
internal let _withoutCaching: Bool
// swiftlint:enable identifier_name
public let location: SourceLocation
public let isClosure: Bool
+1
View File
@@ -28,6 +28,7 @@ public class FailureMessage: NSObject {
}
}
// swiftlint:disable:next identifier_name
internal var _stringValueOverride: String?
internal var hasOverriddenStringValue: Bool {
return _stringValueOverride != nil
+2 -1
View File
@@ -63,7 +63,7 @@ private func createPredicate<S>(_ elementMatcher: Predicate<S.Iterator.Element>)
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func allPassMatcher(_ matcher: NMBMatcher) -> NMBPredicate {
return NMBPredicate { actualExpression in
@@ -103,6 +103,7 @@ extension NMBObjCMatcher {
} else {
let failureMessage = FailureMessage()
let result = matcher.matches(
// swiftlint:disable:next force_try
({ try! expr.evaluate() }),
failureMessage: failureMessage,
location: expr.location
+10 -4
View File
@@ -23,14 +23,18 @@ private func async<T>(style: ExpectationStyle, predicate: Predicate<T>, timeout:
}
switch result {
case .completed: return lastPredicateResult!
case .timedOut: return PredicateResult(status: .fail, message: lastPredicateResult!.message)
case .timedOut:
let message = lastPredicateResult?.message ?? .fail("timed out before returning a value")
return PredicateResult(status: .fail, message: message)
case let .errorThrown(error):
return PredicateResult(status: .fail, message: .fail("unexpected error thrown: <\(error)>"))
case let .raisedException(exception):
return PredicateResult(status: .fail, message: .fail("unexpected exception raised: \(exception)"))
case .blockedRunLoop:
// swiftlint:disable:next line_length
return PredicateResult(status: .fail, message: lastPredicateResult!.message.appended(message: " (timed out, but main thread was unresponsive)."))
let message = lastPredicateResult?.message.appended(message: " (timed out, but main run loop was unresponsive).") ??
.fail("main run loop was unresponsive")
return PredicateResult(status: .fail, message: message)
case .incomplete:
internalError("Reached .incomplete state for \(fnName)(...).")
}
@@ -38,8 +42,10 @@ private func async<T>(style: ExpectationStyle, predicate: Predicate<T>, timeout:
}
private let toEventuallyRequiresClosureError = FailureMessage(
// swiftlint:disable:next line_length
stringValue: "expect(...).toEventually(...) requires an explicit closure (eg - expect { ... }.toEventually(...) )\nSwift 1.2 @autoclosure behavior has changed in an incompatible way for Nimble to function"
stringValue: """
expect(...).toEventually(...) requires an explicit closure (eg - expect { ... }.toEventually(...) )
Swift 1.2 @autoclosure behavior has changed in an incompatible way for Nimble to function
"""
)
extension Expectation {
+1 -1
View File
@@ -29,7 +29,7 @@ public func beAKindOf<T>(_ expectedType: T.Type) -> Predicate<Any> {
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
/// A Nimble matcher that succeeds when the actual value is an instance of the given class.
/// @see beAnInstanceOf if you want to match against the exact class
@@ -33,7 +33,7 @@ public func beAnInstanceOf(_ expectedClass: AnyClass) -> Predicate<NSObject> {
} else {
actualString = "<nil>"
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
let matches = instance != nil && instance!.isMember(of: expectedClass)
#else
let matches = instance != nil && type(of: instance!) == expectedClass
@@ -45,7 +45,7 @@ public func beAnInstanceOf(_ expectedClass: AnyClass) -> Predicate<NSObject> {
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func beAnInstanceOfMatcher(_ expected: AnyClass) -> NMBMatcher {
return NMBPredicate { actualExpression in
+8 -1
View File
@@ -1,5 +1,6 @@
import Foundation
// swiftlint:disable:next identifier_name
public let DefaultDelta = 0.0001
internal func isCloseTo(_ actualValue: NMBDoubleConvertible?,
@@ -34,10 +35,12 @@ public func beCloseTo(_ expectedValue: NMBDoubleConvertible, within delta: Doubl
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
public class NMBObjCBeCloseToMatcher: NSObject, NMBMatcher {
// swiftlint:disable identifier_name
var _expected: NSNumber
var _delta: CDouble
// swiftlint:enable identifier_name
init(expected: NSNumber, within: CDouble) {
_expected = expected
_delta = within
@@ -110,14 +113,17 @@ public func beCloseTo(_ expectedValues: [Double], within delta: Double = Default
infix operator : ComparisonPrecedence
// swiftlint:disable:next identifier_name
public func (lhs: Expectation<[Double]>, rhs: [Double]) {
lhs.to(beCloseTo(rhs))
}
// swiftlint:disable:next identifier_name
public func (lhs: Expectation<NMBDoubleConvertible>, rhs: NMBDoubleConvertible) {
lhs.to(beCloseTo(rhs))
}
// swiftlint:disable:next identifier_name
public func (lhs: Expectation<NMBDoubleConvertible>, rhs: (expected: NMBDoubleConvertible, delta: Double)) {
lhs.to(beCloseTo(rhs.expected, within: rhs.delta))
}
@@ -133,6 +139,7 @@ precedencegroup PlusMinusOperatorPrecedence {
}
infix operator ± : PlusMinusOperatorPrecedence
// swiftlint:disable:next identifier_name
public func ±(lhs: NMBDoubleConvertible, rhs: Double) -> (expected: NMBDoubleConvertible, delta: Double) {
return (expected: lhs, delta: rhs)
}
+25 -4
View File
@@ -4,15 +4,36 @@ import Foundation
/// means the are no items in that collection. For strings, it is an empty string.
public func beEmpty<S: Sequence>() -> Predicate<S> {
return Predicate.simple("be empty") { actualExpression in
let actualSeq = try actualExpression.evaluate()
if actualSeq == nil {
guard let actual = try actualExpression.evaluate() else {
return .fail
}
var generator = actualSeq!.makeIterator()
var generator = actual.makeIterator()
return PredicateStatus(bool: generator.next() == nil)
}
}
/// A Nimble matcher that succeeds when a value is "empty". For collections, this
/// means the are no items in that collection. For strings, it is an empty string.
public func beEmpty<S: SetAlgebra>() -> Predicate<S> {
return Predicate.simple("be empty") { actualExpression in
guard let actual = try actualExpression.evaluate() else {
return .fail
}
return PredicateStatus(bool: actual.isEmpty)
}
}
/// A Nimble matcher that succeeds when a value is "empty". For collections, this
/// means the are no items in that collection. For strings, it is an empty string.
public func beEmpty<S: Sequence & SetAlgebra>() -> Predicate<S> {
return Predicate.simple("be empty") { actualExpression in
guard let actual = try actualExpression.evaluate() else {
return .fail
}
return PredicateStatus(bool: actual.isEmpty)
}
}
/// A Nimble matcher that succeeds when a value is "empty". For collections, this
/// means the are no items in that collection. For strings, it is an empty string.
public func beEmpty() -> Predicate<String> {
@@ -61,7 +82,7 @@ public func beEmpty() -> Predicate<NMBCollection> {
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func beEmptyMatcher() -> NMBPredicate {
return NMBPredicate { actualExpression in
@@ -30,12 +30,12 @@ public func > (lhs: Expectation<NMBComparable>, rhs: NMBComparable?) {
lhs.to(beGreaterThan(rhs))
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func beGreaterThanMatcher(_ expected: NMBComparable?) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
@objc public class func beGreaterThanMatcher(_ expected: NMBComparable?) -> NMBMatcher {
return NMBPredicate { actualExpression in
let expr = actualExpression.cast { $0 as? NMBComparable }
return try beGreaterThan(expected).matches(expr, failureMessage: failureMessage)
return try beGreaterThan(expected).satisfies(expr).toObjectiveC()
}
}
}
@@ -32,12 +32,12 @@ public func >=<T: NMBComparable>(lhs: Expectation<T>, rhs: T) {
lhs.to(beGreaterThanOrEqualTo(rhs))
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func beGreaterThanOrEqualToMatcher(_ expected: NMBComparable?) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
@objc public class func beGreaterThanOrEqualToMatcher(_ expected: NMBComparable?) -> NMBMatcher {
return NMBPredicate { actualExpression in
let expr = actualExpression.cast { $0 as? NMBComparable }
return try beGreaterThanOrEqualTo(expected).matches(expr, failureMessage: failureMessage)
return try beGreaterThanOrEqualTo(expected).satisfies(expr).toObjectiveC()
}
}
}
@@ -4,14 +4,14 @@ import Foundation
/// as the expected instance.
public func beIdenticalTo(_ expected: Any?) -> Predicate<Any> {
return Predicate.define { actualExpression in
#if os(Linux)
#if os(Linux) && !swift(>=4.1.50)
let actual = try actualExpression.evaluate() as? AnyObject
#else
let actual = try actualExpression.evaluate() as AnyObject?
#endif
let bool: Bool
#if os(Linux)
#if os(Linux) && !swift(>=4.1.50)
bool = actual === (expected as? AnyObject) && actual !== nil
#else
bool = actual === (expected as AnyObject?) && actual !== nil
@@ -41,12 +41,12 @@ public func be(_ expected: Any?) -> Predicate<Any> {
return beIdenticalTo(expected)
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func beIdenticalToMatcher(_ expected: NSObject?) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
@objc public class func beIdenticalToMatcher(_ expected: NSObject?) -> NMBMatcher {
return NMBPredicate { actualExpression in
let aExpr = actualExpression.cast { $0 as Any? }
return try beIdenticalTo(expected).matches(aExpr, failureMessage: failureMessage)
return try beIdenticalTo(expected).satisfies(aExpr).toObjectiveC()
}
}
}
@@ -29,12 +29,12 @@ public func < (lhs: Expectation<NMBComparable>, rhs: NMBComparable?) {
lhs.to(beLessThan(rhs))
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func beLessThanMatcher(_ expected: NMBComparable?) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
@objc public class func beLessThanMatcher(_ expected: NMBComparable?) -> NMBMatcher {
return NMBPredicate { actualExpression in
let expr = actualExpression.cast { $0 as? NMBComparable }
return try beLessThan(expected).matches(expr, failureMessage: failureMessage)
return try beLessThan(expected).satisfies(expr).toObjectiveC()
}
}
}
@@ -29,12 +29,12 @@ public func <=<T: NMBComparable>(lhs: Expectation<T>, rhs: T) {
lhs.to(beLessThanOrEqualTo(rhs))
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func beLessThanOrEqualToMatcher(_ expected: NMBComparable?) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
@objc public class func beLessThanOrEqualToMatcher(_ expected: NMBComparable?) -> NMBMatcher {
return NMBPredicate { actualExpression in
let expr = actualExpression.cast { $0 as? NMBComparable }
return try beLessThanOrEqualTo(expected).matches(expr, failureMessage: failureMessage)
return try beLessThanOrEqualTo(expected).satisfies(expr).toObjectiveC()
}
}
}
+17 -30
View File
@@ -100,14 +100,6 @@ public func beTruthy<T: ExpressibleByBooleanLiteral & Equatable>() -> Predicate<
return Predicate.simpleNilable("be truthy") { actualExpression in
let actualValue = try actualExpression.evaluate()
if let actualValue = actualValue {
// FIXME: This is a workaround to SR-2290.
// See:
// - https://bugs.swift.org/browse/SR-2290
// - https://github.com/norio-nomura/Nimble/pull/5#issuecomment-237835873
if let number = actualValue as? NSNumber {
return PredicateStatus(bool: number.boolValue == true)
}
return PredicateStatus(bool: actualValue == (true as T))
}
return PredicateStatus(bool: actualValue != nil)
@@ -120,47 +112,42 @@ public func beFalsy<T: ExpressibleByBooleanLiteral & Equatable>() -> Predicate<T
return Predicate.simpleNilable("be falsy") { actualExpression in
let actualValue = try actualExpression.evaluate()
if let actualValue = actualValue {
// FIXME: This is a workaround to SR-2290.
// See:
// - https://bugs.swift.org/browse/SR-2290
// - https://github.com/norio-nomura/Nimble/pull/5#issuecomment-237835873
if let number = actualValue as? NSNumber {
return PredicateStatus(bool: number.boolValue == false)
}
return PredicateStatus(bool: actualValue == (false as T))
}
return PredicateStatus(bool: actualValue == nil)
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func beTruthyMatcher() -> NMBObjCMatcher {
return NMBObjCMatcher { actualExpression, failureMessage in
@objc public class func beTruthyMatcher() -> NMBMatcher {
return NMBPredicate { actualExpression in
let expr = actualExpression.cast { ($0 as? NSNumber)?.boolValue ?? false }
return try beTruthy().matches(expr, failureMessage: failureMessage)
return try beTruthy().satisfies(expr).toObjectiveC()
}
}
@objc public class func beFalsyMatcher() -> NMBObjCMatcher {
return NMBObjCMatcher { actualExpression, failureMessage in
@objc public class func beFalsyMatcher() -> NMBMatcher {
return NMBPredicate { actualExpression in
let expr = actualExpression.cast { ($0 as? NSNumber)?.boolValue ?? false }
return try beFalsy().matches(expr, failureMessage: failureMessage)
return try beFalsy().satisfies(expr).toObjectiveC()
}
}
@objc public class func beTrueMatcher() -> NMBObjCMatcher {
return NMBObjCMatcher { actualExpression, failureMessage in
@objc public class func beTrueMatcher() -> NMBMatcher {
return NMBPredicate { actualExpression in
let expr = actualExpression.cast { ($0 as? NSNumber)?.boolValue ?? false }
return try beTrue().matches(expr, failureMessage: failureMessage)
return try beTrue().satisfies(expr).toObjectiveC()
}
}
@objc public class func beFalseMatcher() -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
let expr = actualExpression.cast { ($0 as? NSNumber)?.boolValue ?? false }
return try beFalse().matches(expr, failureMessage: failureMessage)
@objc public class func beFalseMatcher() -> NMBMatcher {
return NMBPredicate { actualExpression in
let expr = actualExpression.cast { value -> Bool? in
guard let value = value else { return nil }
return (value as? NSNumber)?.boolValue ?? false
}
return try beFalse().satisfies(expr).toObjectiveC()
}
}
}
+4 -4
View File
@@ -8,11 +8,11 @@ public func beNil<T>() -> Predicate<T> {
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func beNilMatcher() -> NMBObjCMatcher {
return NMBObjCMatcher { actualExpression, failureMessage in
return try beNil().matches(actualExpression, failureMessage: failureMessage)
@objc public class func beNilMatcher() -> NMBMatcher {
return NMBPredicate { actualExpression in
return try beNil().satisfies(actualExpression).toObjectiveC()
}
}
}
+7 -5
View File
@@ -8,10 +8,12 @@ public func beVoid() -> Predicate<()> {
}
}
public func == (lhs: Expectation<()>, rhs: ()) {
lhs.to(beVoid())
}
extension Expectation where T == () {
public static func == (lhs: Expectation<()>, rhs: ()) {
lhs.to(beVoid())
}
public func != (lhs: Expectation<()>, rhs: ()) {
lhs.toNot(beVoid())
public static func != (lhs: Expectation<()>, rhs: ()) {
lhs.toNot(beVoid())
}
}
+8 -8
View File
@@ -35,24 +35,24 @@ public func beginWith(_ startingElement: Any) -> Predicate<NMBOrderedCollection>
public func beginWith(_ startingSubstring: String) -> Predicate<String> {
return Predicate.simple("begin with <\(startingSubstring)>") { actualExpression in
if let actual = try actualExpression.evaluate() {
let range = actual.range(of: startingSubstring)
return PredicateStatus(bool: range != nil && range!.lowerBound == actual.startIndex)
return PredicateStatus(bool: actual.hasPrefix(startingSubstring))
}
return .fail
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func beginWithMatcher(_ expected: Any) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
@objc public class func beginWithMatcher(_ expected: Any) -> NMBMatcher {
return NMBPredicate { actualExpression in
let actual = try actualExpression.evaluate()
if (actual as? String) != nil {
if actual is String {
let expr = actualExpression.cast { $0 as? String }
return try beginWith(expected as! String).matches(expr, failureMessage: failureMessage)
// swiftlint:disable:next force_cast
return try beginWith(expected as! String).satisfies(expr).toObjectiveC()
} else {
let expr = actualExpression.cast { $0 as? NMBOrderedCollection }
return try beginWith(expected).matches(expr, failureMessage: failureMessage)
return try beginWith(expected).satisfies(expr).toObjectiveC()
}
}
}
+67 -18
View File
@@ -1,16 +1,17 @@
import Foundation
/// A Nimble matcher that succeeds when the actual sequence contains the expected value.
/// A Nimble matcher that succeeds when the actual sequence contains the expected values.
public func contain<S: Sequence, T: Equatable>(_ items: T...) -> Predicate<S>
where S.Iterator.Element == T {
where S.Element == T {
return contain(items)
}
/// A Nimble matcher that succeeds when the actual sequence contains the expected values.
public func contain<S: Sequence, T: Equatable>(_ items: [T]) -> Predicate<S>
where S.Iterator.Element == T {
where S.Element == T {
return Predicate.simple("contain <\(arrayAsString(items))>") { actualExpression in
if let actual = try actualExpression.evaluate() {
let matches = items.all {
let matches = items.allSatisfy {
return actual.contains($0)
}
return PredicateStatus(bool: matches)
@@ -19,6 +20,46 @@ public func contain<S: Sequence, T: Equatable>(_ items: [T]) -> Predicate<S>
}
}
/// A Nimble matcher that succeeds when the actual set contains the expected values.
public func contain<S: SetAlgebra, T: Equatable>(_ items: T...) -> Predicate<S>
where S.Element == T {
return contain(items)
}
/// A Nimble matcher that succeeds when the actual set contains the expected values.
public func contain<S: SetAlgebra, T: Equatable>(_ items: [T]) -> Predicate<S>
where S.Element == T {
return Predicate.simple("contain <\(arrayAsString(items))>") { actualExpression in
if let actual = try actualExpression.evaluate() {
let matches = items.allSatisfy {
return actual.contains($0)
}
return PredicateStatus(bool: matches)
}
return .fail
}
}
/// A Nimble matcher that succeeds when the actual set contains the expected values.
public func contain<S: Sequence & SetAlgebra, T: Equatable>(_ items: T...) -> Predicate<S>
where S.Element == T {
return contain(items)
}
/// A Nimble matcher that succeeds when the actual set contains the expected values.
public func contain<S: Sequence & SetAlgebra, T: Equatable>(_ items: [T]) -> Predicate<S>
where S.Element == T {
return Predicate.simple("contain <\(arrayAsString(items))>") { actualExpression in
if let actual = try actualExpression.evaluate() {
let matches = items.allSatisfy {
return actual.contains($0)
}
return PredicateStatus(bool: matches)
}
return .fail
}
}
/// A Nimble matcher that succeeds when the actual string contains the expected substring.
public func contain(_ substrings: String...) -> Predicate<String> {
return contain(substrings)
@@ -27,7 +68,7 @@ public func contain(_ substrings: String...) -> Predicate<String> {
public func contain(_ substrings: [String]) -> Predicate<String> {
return Predicate.simple("contain <\(arrayAsString(substrings))>") { actualExpression in
if let actual = try actualExpression.evaluate() {
let matches = substrings.all {
let matches = substrings.allSatisfy {
let range = actual.range(of: $0)
return range != nil && !range!.isEmpty
}
@@ -45,7 +86,7 @@ public func contain(_ substrings: NSString...) -> Predicate<NSString> {
public func contain(_ substrings: [NSString]) -> Predicate<NSString> {
return Predicate.simple("contain <\(arrayAsString(substrings))>") { actualExpression in
if let actual = try actualExpression.evaluate() {
let matches = substrings.all { actual.range(of: $0.description).length != 0 }
let matches = substrings.allSatisfy { actual.range(of: $0.description).length != 0 }
return PredicateStatus(bool: matches)
}
return .fail
@@ -60,17 +101,17 @@ public func contain(_ items: Any?...) -> Predicate<NMBContainer> {
public func contain(_ items: [Any?]) -> Predicate<NMBContainer> {
return Predicate.simple("contain <\(arrayAsString(items))>") { actualExpression in
guard let actual = try actualExpression.evaluate() else { return .fail }
let matches = items.all { item in
let matches = items.allSatisfy { item in
return item.map { actual.contains($0) } ?? false
}
return PredicateStatus(bool: matches)
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func containMatcher(_ expected: [NSObject]) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
@objc public class func containMatcher(_ expected: [NSObject]) -> NMBMatcher {
return NMBPredicate { actualExpression in
let location = actualExpression.location
let actualValue = try actualExpression.evaluate()
if let value = actualValue as? NMBContainer {
@@ -78,17 +119,25 @@ extension NMBObjCMatcher {
// A straightforward cast on the array causes this to crash, so we have to cast the individual items
let expectedOptionals: [Any?] = expected.map({ $0 as Any? })
return try contain(expectedOptionals).matches(expr, failureMessage: failureMessage)
return try contain(expectedOptionals).satisfies(expr).toObjectiveC()
} else if let value = actualValue as? NSString {
let expr = Expression(expression: ({ value as String }), location: location)
return try contain(expected as! [String]).matches(expr, failureMessage: failureMessage)
} else if actualValue != nil {
// swiftlint:disable:next line_length
failureMessage.postfixMessage = "contain <\(arrayAsString(expected))> (only works for NSArrays, NSSets, NSHashTables, and NSStrings)"
} else {
failureMessage.postfixMessage = "contain <\(arrayAsString(expected))>"
// swiftlint:disable:next force_cast
return try contain(expected as! [String]).satisfies(expr).toObjectiveC()
}
return false
let message: ExpectationMessage
if actualValue != nil {
message = ExpectationMessage.expectedActualValueTo(
// swiftlint:disable:next line_length
"contain <\(arrayAsString(expected))> (only works for NSArrays, NSSets, NSHashTables, and NSStrings)"
)
} else {
message = ExpectationMessage
.expectedActualValueTo("contain <\(arrayAsString(expected))>")
.appendedBeNilHint()
}
return NMBPredicateResult(status: .fail, message: message.toObjectiveC())
}
}
}
@@ -24,20 +24,22 @@ public func containElementSatisfying<S: Sequence, T>(_ predicate: @escaping ((T)
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func containElementSatisfyingMatcher(_ predicate: @escaping ((NSObject) -> Bool)) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
@objc public class func containElementSatisfyingMatcher(_ predicate: @escaping ((NSObject) -> Bool)) -> NMBMatcher {
return NMBPredicate { actualExpression in
let value = try actualExpression.evaluate()
guard let enumeration = value as? NSFastEnumeration else {
// swiftlint:disable:next line_length
failureMessage.postfixMessage = "containElementSatisfying must be provided an NSFastEnumeration object"
failureMessage.actualValue = nil
failureMessage.expected = ""
failureMessage.to = ""
return false
let message = ExpectationMessage.fail(
"containElementSatisfying must be provided an NSFastEnumeration object"
)
return NMBPredicateResult(status: .fail, message: message.toObjectiveC())
}
let message = ExpectationMessage
.expectedTo("find object in collection that satisfies predicate")
.toObjectiveC()
var iterator = NSFastEnumerationIterator(enumeration)
while let item = iterator.next() {
guard let object = item as? NSObject else {
@@ -45,13 +47,11 @@ public func containElementSatisfying<S: Sequence, T>(_ predicate: @escaping ((T)
}
if predicate(object) {
return true
return NMBPredicateResult(status: .matches, message: message)
}
}
failureMessage.actualValue = nil
failureMessage.postfixMessage = "find object in collection that satisfies predicate"
return false
return NMBPredicateResult(status: .doesNotMatch, message: message)
}
}
}
@@ -0,0 +1,16 @@
/// A Nimble matcher that succeeds when the actual sequence contain the same elements in the same order to the exepected sequence.
public func elementsEqual<S: Sequence>(_ expectedValue: S?) -> Predicate<S> where S.Element: Equatable {
// A matcher abstraction for https://developer.apple.com/documentation/swift/sequence/2949668-elementsequal
return Predicate.define("elementsEqual <\(stringify(expectedValue))>") { (actualExpression, msg) in
let actualValue = try actualExpression.evaluate()
switch (expectedValue, actualValue) {
case (nil, _?):
return PredicateResult(status: .fail, message: msg.appendedBeNilHint())
case (nil, nil), (_, nil):
return PredicateResult(status: .fail, message: msg)
case (let expected?, let actual?):
let matches = expected.elementsEqual(actual)
return PredicateResult(bool: matches, message: msg)
}
}
}
+7 -6
View File
@@ -50,17 +50,18 @@ public func endWith(_ endingSubstring: String) -> Predicate<String> {
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func endWithMatcher(_ expected: Any) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
@objc public class func endWithMatcher(_ expected: Any) -> NMBMatcher {
return NMBPredicate { actualExpression in
let actual = try actualExpression.evaluate()
if (actual as? String) != nil {
if actual is String {
let expr = actualExpression.cast { $0 as? String }
return try endWith(expected as! String).matches(expr, failureMessage: failureMessage)
// swiftlint:disable:next force_cast
return try endWith(expected as! String).satisfies(expr).toObjectiveC()
} else {
let expr = actualExpression.cast { $0 as? NMBOrderedCollection }
return try endWith(expected).matches(expr, failureMessage: failureMessage)
return try endWith(expected).satisfies(expr).toObjectiveC()
}
}
}
+46 -118
View File
@@ -7,103 +7,30 @@ import Foundation
public func equal<T: Equatable>(_ expectedValue: T?) -> Predicate<T> {
return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in
let actualValue = try actualExpression.evaluate()
let matches = actualValue == expectedValue && expectedValue != nil
if expectedValue == nil || actualValue == nil {
if expectedValue == nil && actualValue != nil {
return PredicateResult(
status: .fail,
message: msg.appendedBeNilHint()
)
}
switch (expectedValue, actualValue) {
case (nil, _?):
return PredicateResult(status: .fail, message: msg.appendedBeNilHint())
case (nil, nil), (_, nil):
return PredicateResult(status: .fail, message: msg)
case (let expected?, let actual?):
let matches = expected == actual
return PredicateResult(bool: matches, message: msg)
}
return PredicateResult(status: PredicateStatus(bool: matches), message: msg)
}
}
/// A Nimble matcher that succeeds when the actual value is equal to the expected value.
/// Values can support equal by supporting the Equatable protocol.
///
/// @see beCloseTo if you want to match imprecise types (eg - floats, doubles).
public func equal<T, C: Equatable>(_ expectedValue: [T: C]?) -> Predicate<[T: C]> {
return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in
let actualValue = try actualExpression.evaluate()
if expectedValue == nil || actualValue == nil {
if expectedValue == nil && actualValue != nil {
return PredicateResult(
status: .fail,
message: msg.appendedBeNilHint()
)
}
return PredicateResult(status: .fail, message: msg)
}
return PredicateResult(
status: PredicateStatus(bool: expectedValue! == actualValue!),
message: msg
)
}
}
/// A Nimble matcher that succeeds when the actual collection is equal to the expected collection.
/// Items must implement the Equatable protocol.
public func equal<T: Equatable>(_ expectedValue: [T]?) -> Predicate<[T]> {
return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in
let actualValue = try actualExpression.evaluate()
if expectedValue == nil || actualValue == nil {
if expectedValue == nil && actualValue != nil {
return PredicateResult(
status: .fail,
message: msg.appendedBeNilHint()
)
}
return PredicateResult(
status: .fail,
message: msg
)
}
return PredicateResult(
bool: expectedValue! == actualValue!,
message: msg
)
}
}
/// A Nimble matcher allowing comparison of collection with optional type
public func equal<T: Equatable>(_ expectedValue: [T?]) -> Predicate<[T?]> {
return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in
if let actualValue = try actualExpression.evaluate() {
let doesNotMatch = PredicateResult(
status: .doesNotMatch,
message: msg
)
if expectedValue.count != actualValue.count {
return doesNotMatch
}
for (index, item) in actualValue.enumerated() {
let otherItem = expectedValue[index]
if item == nil && otherItem == nil {
continue
} else if item == nil && otherItem != nil {
return doesNotMatch
} else if item != nil && otherItem == nil {
return doesNotMatch
} else if item! != otherItem! {
return doesNotMatch
}
}
return PredicateResult(
status: .matches,
message: msg
)
} else {
guard let actualValue = try actualExpression.evaluate() else {
return PredicateResult(
status: .fail,
message: msg.appendedBeNilHint()
)
}
let matches = expectedValue == actualValue
return PredicateResult(bool: matches, message: msg)
}
}
@@ -128,44 +55,45 @@ private func equal<T>(_ expectedValue: Set<T>?, stringify: @escaping (Set<T>?) -
var errorMessage: ExpectationMessage =
.expectedActualValueTo("equal <\(stringify(expectedValue))>")
if let expectedValue = expectedValue {
if let actualValue = try actualExpression.evaluate() {
errorMessage = .expectedCustomValueTo(
"equal <\(stringify(expectedValue))>",
"<\(stringify(actualValue))>"
)
if expectedValue == actualValue {
return PredicateResult(
status: .matches,
message: errorMessage
)
}
let missing = expectedValue.subtracting(actualValue)
if missing.count > 0 {
errorMessage = errorMessage.appended(message: ", missing <\(stringify(missing))>")
}
let extra = actualValue.subtracting(expectedValue)
if extra.count > 0 {
errorMessage = errorMessage.appended(message: ", extra <\(stringify(extra))>")
}
return PredicateResult(
status: .doesNotMatch,
message: errorMessage
)
}
return PredicateResult(
status: .fail,
message: errorMessage.appendedBeNilHint()
)
} else {
guard let expectedValue = expectedValue else {
return PredicateResult(
status: .fail,
message: errorMessage.appendedBeNilHint()
)
}
guard let actualValue = try actualExpression.evaluate() else {
return PredicateResult(
status: .fail,
message: errorMessage.appendedBeNilHint()
)
}
errorMessage = .expectedCustomValueTo(
"equal <\(stringify(expectedValue))>",
"<\(stringify(actualValue))>"
)
if expectedValue == actualValue {
return PredicateResult(
status: .matches,
message: errorMessage
)
}
let missing = expectedValue.subtracting(actualValue)
if missing.count > 0 {
errorMessage = errorMessage.appended(message: ", missing <\(stringify(missing))>")
}
let extra = actualValue.subtracting(expectedValue)
if extra.count > 0 {
errorMessage = errorMessage.appended(message: ", extra <\(stringify(extra))>")
}
return PredicateResult(
status: .doesNotMatch,
message: errorMessage
)
}
}
@@ -209,7 +137,7 @@ public func !=<T, C: Equatable>(lhs: Expectation<[T: C]>, rhs: [T: C]?) {
lhs.toNot(equal(rhs))
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func equalMatcher(_ expected: NSObject) -> NMBMatcher {
return NMBPredicate { actualExpression in
+18 -9
View File
@@ -7,7 +7,7 @@ import Foundation
/// A Nimble matcher that succeeds when the actual Collection's count equals
/// the expected value
public func haveCount<T: Collection>(_ expectedValue: T.IndexDistance) -> Predicate<T> {
public func haveCount<T: Collection>(_ expectedValue: Int) -> Predicate<T> {
return Predicate.define { actualExpression in
if let actualValue = try actualExpression.evaluate() {
let message = ExpectationMessage
@@ -45,20 +45,29 @@ public func haveCount(_ expectedValue: Int) -> Predicate<NMBCollection> {
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func haveCountMatcher(_ expected: NSNumber) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
@objc public class func haveCountMatcher(_ expected: NSNumber) -> NMBMatcher {
return NMBPredicate { actualExpression in
let location = actualExpression.location
let actualValue = try actualExpression.evaluate()
if let value = actualValue as? NMBCollection {
let expr = Expression(expression: ({ value as NMBCollection}), location: location)
return try haveCount(expected.intValue).matches(expr, failureMessage: failureMessage)
} else if let actualValue = actualValue {
failureMessage.postfixMessage = "get type of NSArray, NSSet, NSDictionary, or NSHashTable"
failureMessage.actualValue = "\(String(describing: type(of: actualValue)))"
return try haveCount(expected.intValue).satisfies(expr).toObjectiveC()
}
return false
let message: ExpectationMessage
if let actualValue = actualValue {
message = ExpectationMessage.expectedCustomValueTo(
"get type of NSArray, NSSet, NSDictionary, or NSHashTable",
"\(String(describing: type(of: actualValue)))"
)
} else {
message = ExpectationMessage
.expectedActualValueTo("have a collection with count \(stringify(expected.intValue))")
.appendedBeNilHint()
}
return NMBPredicateResult(status: .fail, message: message.toObjectiveC())
}
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ public func match(_ expectedValue: String?) -> Predicate<String> {
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func matchMatcher(_ expected: NSString) -> NMBMatcher {
@@ -1,6 +1,6 @@
import Foundation
// `CGFloat` is in Foundation (swift-corelibs-foundation) on Linux.
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
import CoreGraphics
#endif
@@ -28,7 +28,7 @@ extension Matcher {
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
/// Objective-C interface to the Swift variant of Matcher.
@objc public protocol NMBMatcher {
func matches(_ actualBlock: @escaping () -> NSObject?, failureMessage: FailureMessage, location: SourceLocation) -> Bool
@@ -41,7 +41,8 @@ public protocol NMBContainer {
func contains(_ anObject: Any) -> Bool
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
// swiftlint:disable:next todo
// FIXME: NSHashTable can not conform to NMBContainer since swift-DEVELOPMENT-SNAPSHOT-2016-04-25-a
//extension NSHashTable : NMBContainer {} // Corelibs Foundation does not include this class yet
#endif
@@ -54,7 +55,7 @@ public protocol NMBCollection {
var count: Int { get }
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NSHashTable: NMBCollection {} // Corelibs Foundation does not include these classes yet
extension NSMapTable: NMBCollection {}
#endif
@@ -131,7 +132,7 @@ extension NSDate: TestOutputStringConvertible {
/// beGreaterThan(), beGreaterThanOrEqualTo(), and equal() matchers.
///
/// Types that conform to Swift's Comparable protocol will work implicitly too
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
@objc public protocol NMBComparable {
func NMB_compare(_ otherObject: NMBComparable!) -> ComparisonResult
}
@@ -144,11 +145,13 @@ public protocol NMBComparable {
extension NSNumber: NMBComparable {
public func NMB_compare(_ otherObject: NMBComparable!) -> ComparisonResult {
// swiftlint:disable:next force_cast
return compare(otherObject as! NSNumber)
}
}
extension NSString: NMBComparable {
public func NMB_compare(_ otherObject: NMBComparable!) -> ComparisonResult {
// swiftlint:disable:next force_cast
return compare(otherObject as! String)
}
}
@@ -1,36 +1,9 @@
import Foundation
// A workaround to SR-6419.
extension NotificationCenter {
#if !(os(macOS) || os(iOS) || os(tvOS) || os(watchOS))
#if swift(>=4.0)
#if swift(>=4.0.2)
#else
func addObserver(forName name: Notification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol {
return addObserver(forName: name, object: obj, queue: queue, usingBlock: block)
}
#endif
#elseif swift(>=3.2)
#if swift(>=3.2.2)
#else
// swiftlint:disable:next line_length
func addObserver(forName name: Notification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol {
return addObserver(forName: name, object: obj, queue: queue, usingBlock: block)
}
#endif
#else
// swiftlint:disable:next line_length
func addObserver(forName name: Notification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol {
return addObserver(forName: name, object: obj, queue: queue, usingBlock: block)
}
#endif
#endif
}
internal class NotificationCollector {
private(set) var observedNotifications: [Notification]
private let notificationCenter: NotificationCenter
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
private var token: AnyObject?
#else
private var token: NSObjectProtocol?
@@ -43,14 +16,14 @@ internal class NotificationCollector {
func startObserving() {
// swiftlint:disable:next line_length
self.token = self.notificationCenter.addObserver(forName: nil, object: nil, queue: nil, using: { [weak self] n in
self.token = self.notificationCenter.addObserver(forName: nil, object: nil, queue: nil) { [weak self] notification in
// linux-swift gets confused by .append(n)
self?.observedNotifications.append(n)
})
self?.observedNotifications.append(notification)
}
}
deinit {
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
if let token = self.token {
self.notificationCenter.removeObserver(token)
}
+3 -2
View File
@@ -218,6 +218,7 @@ extension Predicate: Matcher {
extension Predicate {
// Someday, make this public? Needs documentation
internal func after(f: @escaping (Expression<T>, PredicateResult) throws -> PredicateResult) -> Predicate<T> {
// swiftlint:disable:previous identifier_name
return Predicate { actual -> PredicateResult in
let result = try self.satisfies(actual)
return try f(actual, result)
@@ -241,7 +242,7 @@ extension Predicate {
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
public typealias PredicateBlock = (_ actualExpression: Expression<NSObject>) throws -> NMBPredicateResult
public class NMBPredicate: NSObject {
@@ -311,7 +312,7 @@ final public class NMBPredicateStatus: NSObject {
public static let doesNotMatch: NMBPredicateStatus = NMBPredicateStatus(status: 1)
public static let fail: NMBPredicateStatus = NMBPredicateStatus(status: 2)
public override var hashValue: Int { return self.status.hashValue }
public override var hash: Int { return self.status.hashValue }
public override func isEqual(_ object: Any?) -> Bool {
guard let otherPredicate = object as? NMBPredicateStatus else {
@@ -1,7 +1,7 @@
import Foundation
// This matcher requires the Objective-C, and being built by Xcode rather than the Swift Package Manager
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
#if canImport(Darwin) && !SWIFT_PACKAGE
/// A Nimble matcher that succeeds when the actual expression raises an
/// exception with the specified name, reason, and/or userInfo.
@@ -23,8 +23,12 @@ public func raiseException(
exception = e
}), finally: nil)
capture.tryBlock {
_ = try! actualExpression.evaluate()
do {
try capture.tryBlockThrows {
_ = try actualExpression.evaluate()
}
} catch {
return PredicateResult(status: .fail, message: .fail("unexpected error thrown: <\(error)>"))
}
let failureMessage = FailureMessage()
@@ -118,10 +122,12 @@ internal func exceptionMatchesNonNilFieldsOrClosure(
}
public class NMBObjCRaiseExceptionMatcher: NSObject, NMBMatcher {
// swiftlint:disable identifier_name
internal var _name: String?
internal var _reason: String?
internal var _userInfo: NSDictionary?
internal var _block: ((NSException) -> Void)?
// swiftlint:enable identifier_name
internal init(name: String?, reason: String?, userInfo: NSDictionary?, block: ((NSException) -> Void)?) {
_name = name
@@ -39,7 +39,7 @@ public func && <T>(left: Predicate<T>, right: Predicate<T>) -> Predicate<T> {
return satisfyAllOf(left, right)
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func satisfyAllOfMatcher(_ matchers: [NMBMatcher]) -> NMBPredicate {
return NMBPredicate { actualExpression in
@@ -60,8 +60,12 @@ extension NMBObjCMatcher {
return predicate.satisfies({ try expression.evaluate() }, location: actualExpression.location).toSwift()
} else {
let failureMessage = FailureMessage()
// swiftlint:disable:next line_length
let success = matcher.matches({ try! expression.evaluate() }, failureMessage: failureMessage, location: actualExpression.location)
let success = matcher.matches(
// swiftlint:disable:next force_try
{ try! expression.evaluate() },
failureMessage: failureMessage,
location: actualExpression.location
)
return PredicateResult(bool: success, message: failureMessage.toExpectationMessage())
}
}
@@ -47,7 +47,7 @@ public func || <T>(left: MatcherFunc<T>, right: MatcherFunc<T>) -> Predicate<T>
return satisfyAnyOf(left, right)
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
extension NMBObjCMatcher {
@objc public class func satisfyAnyOfMatcher(_ matchers: [NMBMatcher]) -> NMBPredicate {
return NMBPredicate { actualExpression in
@@ -68,8 +68,12 @@ extension NMBObjCMatcher {
return predicate.satisfies({ try expression.evaluate() }, location: actualExpression.location).toSwift()
} else {
let failureMessage = FailureMessage()
// swiftlint:disable:next line_length
let success = matcher.matches({ try! expression.evaluate() }, failureMessage: failureMessage, location: actualExpression.location)
let success = matcher.matches(
// swiftlint:disable:next force_try
{ try! expression.evaluate() },
failureMessage: failureMessage,
location: actualExpression.location
)
return PredicateResult(bool: success, message: failureMessage.toExpectationMessage())
}
}
@@ -2,7 +2,7 @@ import Foundation
public func throwAssertion() -> Predicate<Void> {
return Predicate { actualExpression in
#if arch(x86_64) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
#if arch(x86_64) && canImport(Darwin) && !SWIFT_PACKAGE
let message = ExpectationMessage.expectedTo("throw an assertion")
var actualError: Error?
@@ -44,9 +44,8 @@ public func throwAssertion() -> Predicate<Void> {
" conditional statement")
#else
fatalError("The throwAssertion Nimble matcher can only run on x86_64 platforms with " +
"Objective-C (e.g. Mac, iPhone 5s or later simulators). You can silence this error " +
"by placing the test case inside an #if arch(x86_64) or (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) conditional statement")
// swiftlint:disable:previous line_length
"Objective-C (e.g. macOS, iPhone 5s or later simulators). You can silence this error " +
"by placing the test case inside an #if arch(x86_64) or canImport(Darwin) conditional statement")
#endif
}
}
+13 -19
View File
@@ -2,7 +2,7 @@ import CoreFoundation
import Dispatch
import Foundation
#if !(os(macOS) || os(iOS) || os(tvOS) || os(watchOS))
#if canImport(CDispatch)
import CDispatch
#endif
@@ -32,7 +32,7 @@ internal class AssertionWaitLock: WaitLock {
func acquireWaitingLock(_ fnName: String, file: FileString, line: UInt) {
let info = WaitingInfo(name: fnName, file: file, lineNumber: line)
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
let isMainThread = Thread.isMainThread
#else
let isMainThread = _CFIsMainThread()
@@ -45,10 +45,15 @@ internal class AssertionWaitLock: WaitLock {
nimblePrecondition(
currentWaiter == nil,
"InvalidNimbleAPIUsage",
"Nested async expectations are not allowed to avoid creating flaky tests.\n\n" +
"The call to\n\t\(info)\n" +
"triggered this exception because\n\t\(currentWaiter!)\n" +
"is currently managing the main run loop."
"""
Nested async expectations are not allowed to avoid creating flaky tests.
The call to
\t\(info)
triggered this exception because
\t\(currentWaiter!)
is currently managing the main run loop.
"""
)
currentWaiter = info
}
@@ -180,25 +185,18 @@ internal class AwaitPromiseBuilder<T> {
// checked.
//
// In addition, stopping the run loop is used to halt code executed on the main run loop.
#if swift(>=4.0)
trigger.timeoutSource.schedule(
deadline: DispatchTime.now() + timeoutInterval,
repeating: .never,
leeway: timeoutLeeway
)
#else
trigger.timeoutSource.scheduleOneshot(
deadline: DispatchTime.now() + timeoutInterval,
leeway: timeoutLeeway
)
#endif
trigger.timeoutSource.setEventHandler {
guard self.promise.asyncResult.isIncomplete() else { return }
let timedOutSem = DispatchSemaphore(value: 0)
let semTimedOutOrBlocked = DispatchSemaphore(value: 0)
semTimedOutOrBlocked.signal()
let runLoop = CFRunLoopGetMain()
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
let runLoopMode = CFRunLoopMode.defaultMode.rawValue
#else
let runLoopMode = kCFRunLoopDefaultMode
@@ -263,7 +261,7 @@ internal class AwaitPromiseBuilder<T> {
self.trigger.timeoutSource.resume()
while self.promise.asyncResult.isIncomplete() {
// Stopping the run loop does not work unless we run only 1 mode
#if swift(>=4.2)
#if (swift(>=4.2) && canImport(Darwin)) || compiler(>=5.0)
_ = RunLoop.current.run(mode: .default, before: .distantFuture)
#else
_ = RunLoop.current.run(mode: .defaultRunLoopMode, before: .distantFuture)
@@ -333,11 +331,7 @@ internal class Awaiter {
let asyncSource = createTimerSource(asyncQueue)
let trigger = AwaitTrigger(timeoutSource: timeoutSource, actionSource: asyncSource) {
let interval = DispatchTimeInterval.nanoseconds(Int(pollInterval * TimeInterval(NSEC_PER_SEC)))
#if swift(>=4.0)
asyncSource.schedule(deadline: .now(), repeating: interval, leeway: pollLeeway)
#else
asyncSource.scheduleRepeating(deadline: .now(), interval: interval, leeway: pollLeeway)
#endif
asyncSource.setEventHandler {
do {
if let result = try closure() {
+4 -2
View File
@@ -1,12 +1,14 @@
import Foundation
#if !swift(>=4.2)
extension Sequence {
internal func all(_ fn: (Iterator.Element) -> Bool) -> Bool {
internal func allSatisfy(_ predicate: (Element) throws -> Bool) rethrows -> Bool {
for item in self {
if !fn(item) {
if try !predicate(item) {
return false
}
}
return true
}
}
#endif
+3 -2
View File
@@ -2,7 +2,7 @@ import Foundation
internal func identityAsString(_ value: Any?) -> String {
let anyObject: AnyObject?
#if os(Linux)
#if os(Linux) && !swift(>=4.1.50)
anyObject = value as? AnyObject
#else
anyObject = value as AnyObject?
@@ -122,6 +122,7 @@ extension String: TestOutputStringConvertible {
extension Data: TestOutputStringConvertible {
public var testDescription: String {
#if os(Linux)
// swiftlint:disable:next todo
// FIXME: Swift on Linux triggers a segfault when calling NSData's hash() (last checked on 03-11-16)
return "Data<length=\(count)>"
#else
@@ -158,7 +159,7 @@ public func stringify<T>(_ value: T?) -> String {
return String(describing: value)
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
@objc public class NMBStringer: NSObject {
@objc public class func stringify(_ obj: Any?) -> String {
return Nimble.stringify(obj)
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0930"
LastUpgradeVersion = "1010"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
+3 -1
View File
@@ -4,6 +4,7 @@
[![CocoaPods](https://img.shields.io/cocoapods/v/Quick.svg)](https://cocoapods.org/pods/Quick)
[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
[![Platforms](https://img.shields.io/cocoapods/p/Quick.svg)](https://cocoapods.org/pods/Quick)
[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
Quick is a behavior-driven development framework for Swift and Objective-C.
Inspired by [RSpec](https://github.com/rspec/rspec), [Specta](https://github.com/specta/specta), and [Ginkgo](https://github.com/onsi/ginkgo).
@@ -44,7 +45,8 @@ Certain versions of Quick and Nimble only support certain versions of Swift. Dep
|Swift version |Quick version |Nimble version |
|:--------------------|:---------------|:--------------|
|Swift 3 |v1.0.0 or later |v5.0.0 or later|
|Swift 4.2 |v1.3.2 or later |v7.3.2 or later|
|Swift 3 / Swift 4 |v1.0.0 or later |v5.0.0 or later|
|Swift 2.2 / Swift 2.3|v0.9.3 |v4.1.0 |
## Documentation
+1 -1
View File
@@ -4,7 +4,7 @@
open class Behavior<Context> {
open static var name: String { return String(describing: self) }
public static var name: String { return String(describing: self) }
/**
override this method in your behavior to define a set of reusable examples.
+3 -9
View File
@@ -1,14 +1,8 @@
import Foundation
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
// does not work as expected.
#if swift(>=3.2)
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objcMembers
public class _CallsiteBase: NSObject {}
#else
public class _CallsiteBase: NSObject {}
#endif
#if canImport(Darwin) && !SWIFT_PACKAGE
@objcMembers
public class _CallsiteBase: NSObject {}
#else
public class _CallsiteBase: NSObject {}
#endif
@@ -72,7 +72,7 @@ final public class Configuration: NSObject {
provided with metadata on the example that the closure is being run
prior to.
*/
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
@objc(beforeEachWithMetadata:)
public func beforeEach(_ closure: @escaping BeforeExampleWithMetadataClosure) {
exampleHooks.appendBefore(closure)
@@ -109,7 +109,7 @@ final public class Configuration: NSObject {
is provided with metadata on the example that the closure is being
run after.
*/
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
@objc(afterEachWithMetadata:)
public func afterEach(_ closure: @escaping AfterExampleWithMetadataClosure) {
exampleHooks.appendAfter(closure)
+3 -3
View File
@@ -56,7 +56,7 @@ extension World {
currentExampleGroup.hooks.appendBefore(closure)
}
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
#if canImport(Darwin) && !SWIFT_PACKAGE
@objc(beforeEachWithMetadata:)
internal func beforeEach(closure: @escaping BeforeExampleWithMetadataClosure) {
currentExampleGroup.hooks.appendBefore(closure)
@@ -74,7 +74,7 @@ extension World {
currentExampleGroup.hooks.appendAfter(closure)
}
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
#if canImport(Darwin) && !SWIFT_PACKAGE
@objc(afterEachWithMetadata:)
internal func afterEach(closure: @escaping AfterExampleWithMetadataClosure) {
currentExampleGroup.hooks.appendAfter(closure)
@@ -172,7 +172,7 @@ extension World {
self.itBehavesLike(behavior, context: context, flags: pendingFlags, file: file, line: line)
}
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
#if canImport(Darwin) && !SWIFT_PACKAGE
@objc(itWithDescription:flags:file:line:closure:)
internal func objc_it(_ description: String, flags: FilterFlags, file: String, line: UInt, closure: @escaping () -> Void) {
it(description, flags: flags, file: file, line: line, closure: closure)
+1 -1
View File
@@ -1,7 +1,7 @@
import Foundation
internal func raiseError(_ message: String) -> Never {
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
NSException(name: .internalInconsistencyException, reason: message, userInfo: nil).raise()
#endif
+3 -9
View File
@@ -3,15 +3,9 @@ import Foundation
private var numberOfExamplesRun = 0
private var numberOfIncludedExamples = 0
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
// does not work as expected.
#if swift(>=3.2)
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objcMembers
public class _ExampleBase: NSObject {}
#else
public class _ExampleBase: NSObject {}
#endif
#if canImport(Darwin) && !SWIFT_PACKAGE
@objcMembers
public class _ExampleBase: NSObject {}
#else
public class _ExampleBase: NSObject {}
#endif
+3 -9
View File
@@ -1,14 +1,8 @@
import Foundation
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
// does not work as expected.
#if swift(>=3.2)
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objcMembers
public class _ExampleMetadataBase: NSObject {}
#else
public class _ExampleMetadataBase: NSObject {}
#endif
#if canImport(Darwin) && !SWIFT_PACKAGE
@objcMembers
public class _ExampleMetadataBase: NSObject {}
#else
public class _ExampleMetadataBase: NSObject {}
#endif
+3 -9
View File
@@ -1,14 +1,8 @@
import Foundation
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
// does not work as expected.
#if swift(>=3.2)
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objcMembers
public class _FilterBase: NSObject {}
#else
public class _FilterBase: NSObject {}
#endif
#if canImport(Darwin) && !SWIFT_PACKAGE
@objcMembers
public class _FilterBase: NSObject {}
#else
public class _FilterBase: NSObject {}
#endif
@@ -1,4 +1,4 @@
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
#if canImport(Darwin)
import Foundation
@@ -1,4 +1,4 @@
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
#if canImport(Darwin)
import Foundation
extension NSString {
@@ -21,7 +21,7 @@ extension NSString {
return invalidCharacters
}()
/// This API is not meant to be used outside Quick, so will be unavaialbe in
/// This API is not meant to be used outside Quick, so will be unavailable in
/// a next major version.
@objc(qck_c99ExtendedIdentifier)
public var c99ExtendedIdentifier: String {
@@ -1,4 +1,4 @@
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
#if canImport(Darwin)
import Foundation
/**
+1 -1
View File
@@ -1,4 +1,4 @@
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
#if canImport(Darwin)
import XCTest
+5 -11
View File
@@ -12,15 +12,9 @@ public typealias SharedExampleContext = () -> [String: Any]
*/
public typealias SharedExampleClosure = (@escaping SharedExampleContext) -> Void
// `#if swift(>=3.2) && (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE`
// does not work as expected.
#if swift(>=3.2)
#if (os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) && !SWIFT_PACKAGE
@objcMembers
internal class _WorldBase: NSObject {}
#else
internal class _WorldBase: NSObject {}
#endif
#if canImport(Darwin) && !SWIFT_PACKAGE
@objcMembers
internal class _WorldBase: NSObject {}
#else
internal class _WorldBase: NSObject {}
#endif
@@ -57,7 +51,7 @@ final internal class World: _WorldBase {
within this test suite. This is only true within the context of Quick
functional tests.
*/
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
// Convention of generating Objective-C selector has been changed on Swift 3
@objc(isRunningAdditionalSuites)
internal var isRunningAdditionalSuites = false
@@ -158,7 +152,7 @@ final internal class World: _WorldBase {
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
#if canImport(Darwin)
@objc(examplesForSpecClass:)
internal func objc_examples(_ specClass: AnyClass) -> [Example] {
return examples(specClass)
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>7.3.1</string>
<string>8.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.3.2</string>
<string>2.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
+1 -1
View File
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>0.3.3</string>
<string>0.7.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
+32 -12
View File
@@ -24,6 +24,10 @@
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */; };
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */; };
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */; };
074B0D67222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */; };
074B0D6B222C247B001A45A9 /* NowPlayingInfoCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */; };
074B0D6D222C24DE001A45A9 /* NowPlayingInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */; };
076DFC5F22345EAF00A8D163 /* AudioPlayerEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */; };
07732651205EACA300C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
07732653205EB1B500C4D1CD /* nasa_throttle_up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */; };
07732654205ECA8B00C4D1CD /* WAV-MP3.wav in Resources */ = {isa = PBXBuildFile; fileRef = 07732650205EACA300C4D1CD /* WAV-MP3.wav */; };
@@ -32,6 +36,7 @@
07756B69218A4E870023935E /* AudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07756B68218A4E870023935E /* AudioSession.swift */; };
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */; };
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */; };
07EB8EE2222869B2000197DE /* NowPlayingInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */; };
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; };
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; };
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
@@ -65,12 +70,17 @@
074A6482205C155E0083D868 /* AVPlayerTimeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerTimeObserverTests.swift; sourceTree = "<group>"; };
074A6484205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemNotificationObserverTests.swift; sourceTree = "<group>"; };
074A6486205E59B60083D868 /* AVPlayerWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerWrapperTests.swift; sourceTree = "<group>"; };
074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoControllerTests.swift; sourceTree = "<group>"; };
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoCenter.swift; sourceTree = "<group>"; };
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoController.swift; sourceTree = "<group>"; };
076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerEventTests.swift; sourceTree = "<group>"; };
07732650205EACA300C4D1CD /* WAV-MP3.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = "WAV-MP3.wav"; sourceTree = "<group>"; };
07732652205EB1B500C4D1CD /* nasa_throttle_up.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = nasa_throttle_up.mp3; sourceTree = "<group>"; };
0775575820668B020002C6A1 /* QueueManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueManagerTests.swift; sourceTree = "<group>"; };
07756B68218A4E870023935E /* AudioSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSession.swift; sourceTree = "<group>"; };
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerItemObserverTests.swift; sourceTree = "<group>"; };
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueuedAudioPlayerTests.swift; sourceTree = "<group>"; };
07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfoTests.swift; sourceTree = "<group>"; };
521F3AEC1228A2FA2637355F /* Pods-SwiftAudio_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftAudio_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftAudio_Tests/Pods-SwiftAudio_Tests.debug.xcconfig"; sourceTree = "<group>"; };
607FACD01AFB9204008FA782 /* SwiftAudio_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftAudio_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -128,6 +138,8 @@
isa = PBXGroup;
children = (
07756B68218A4E870023935E /* AudioSession.swift */,
074B0D6A222C247B001A45A9 /* NowPlayingInfoCenter.swift */,
074B0D6C222C24DE001A45A9 /* NowPlayingInfoController.swift */,
);
path = Mocks;
sourceTree = "<group>";
@@ -193,6 +205,9 @@
078C908D210D25F700555E80 /* AVPlayerItemObserverTests.swift */,
0708ED6B2116DA4B00EB29BD /* AudioSessionControllerTests.swift */,
07DBB1E0212C17E600BB4278 /* QueuedAudioPlayerTests.swift */,
07EB8EE022286980000197DE /* NowPlayingInfoTests.swift */,
074B0D66222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift */,
076DFC5E22345EAF00A8D163 /* AudioPlayerEventTests.swift */,
0708ED712116E91300EB29BD /* Source */,
607FACE91AFB9204008FA782 /* Supporting Files */,
);
@@ -286,13 +301,13 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastUpgradeCheck = 0830;
LastUpgradeCheck = 1010;
ORGANIZATIONNAME = CocoaPods;
TargetAttributes = {
607FACCF1AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = HPNZWPB9JK;
LastSwiftMigration = 0900;
LastSwiftMigration = 1020;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 1;
@@ -302,14 +317,14 @@
607FACE41AFB9204008FA782 = {
CreatedOnToolsVersion = 6.3.1;
DevelopmentTeam = HPNZWPB9JK;
LastSwiftMigration = 0900;
LastSwiftMigration = 1020;
TestTargetID = 607FACCF1AFB9204008FA782;
};
};
};
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "SwiftAudio" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
@@ -452,16 +467,21 @@
buildActionMask = 2147483647;
files = (
07756B69218A4E870023935E /* AudioSession.swift in Sources */,
074B0D67222C1EC7001A45A9 /* NowPlayingInfoControllerTests.swift in Sources */,
0708ED702116E89900EB29BD /* Source.swift in Sources */,
0708ED742116EE0100EB29BD /* AudioPlayerTests.swift in Sources */,
0775575920668B020002C6A1 /* QueueManagerTests.swift in Sources */,
074A6483205C155E0083D868 /* AVPlayerTimeObserverTests.swift in Sources */,
078C908F210D263200555E80 /* AVPlayerItemObserverTests.swift in Sources */,
0708ED6C2116DA4C00EB29BD /* AudioSessionControllerTests.swift in Sources */,
074B0D6B222C247B001A45A9 /* NowPlayingInfoCenter.swift in Sources */,
07DBB1E1212C17E600BB4278 /* QueuedAudioPlayerTests.swift in Sources */,
076DFC5F22345EAF00A8D163 /* AudioPlayerEventTests.swift in Sources */,
074B0D6D222C24DE001A45A9 /* NowPlayingInfoController.swift in Sources */,
074A6485205C29920083D868 /* AVPlayerItemNotificationObserverTests.swift in Sources */,
607FACEC1AFB9204008FA782 /* AVPlayerObserverTests.swift in Sources */,
074A6487205E59B60083D868 /* AVPlayerWrapperTests.swift in Sources */,
07EB8EE2222869B2000197DE /* NowPlayingInfoTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -507,12 +527,14 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@@ -560,12 +582,14 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@@ -606,8 +630,7 @@
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
@@ -624,8 +647,7 @@
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
@@ -647,8 +669,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
};
name = Debug;
@@ -666,8 +687,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftAudio_Example.app/SwiftAudio_Example";
};
name = Release;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
LastUpgradeVersion = "1010"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
+1 -1
View File
@@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
application.beginReceivingRemoteControlEvents()
+4 -4
View File
@@ -17,10 +17,10 @@ class AudioController {
let audioSessionController = AudioSessionController.shared
let sources: [AudioItem] = [
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency, artwork: #imageLiteral(resourceName: "22AMI"))
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/67b51d90ffddd6bb3f095059997021b589845f81?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "33 \"GOD\"", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/081447adc23dad4f79ba4f1082615d1c56edf5e1?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "8 (circle)", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/6f9999d909b017eabef97234dd7a206355720d9d?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "715 - CRΣΣKS", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI")),
DefaultAudioItem(audioUrl: "https://p.scdn.co/mp3-preview/bf9bdd403c67fdbe06a582e7b292487c8cfd1f7e?cid=d8a5ed958d274c2e8ee717e6a4b0971d", artist: "Bon Iver", title: "____45_____", albumTitle: "22, A Million", sourceType: .stream, artwork: #imageLiteral(resourceName: "22AMI"))
]
init() {
+1 -1
View File
@@ -46,7 +46,7 @@ extension QueueViewController: UITableViewDataSource, UITableViewDelegate {
case 0:
return 1
case 1:
return controller.player.nextItems.count ?? 0
return controller.player.nextItems.count
default:
return 0
}
+44 -45
View File
@@ -27,14 +27,17 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
controller.player.delegate = self
controller.player.event.stateChange.addListener(self, handleAudioPlayerStateChange)
controller.player.event.secondElapse.addListener(self, handleAudioPlayerSecondElapsed)
controller.player.event.seek.addListener(self, handleAudioPlayerDidSeek)
controller.player.event.updateDuration.addListener(self, handleAudioPlayerUpdateDuration)
}
@IBAction func togglePlay(_ sender: Any) {
if (!controller.audioSessionController.audioSessionIsActive) {
try? controller.audioSessionController.activateSession()
}
try? controller.player.togglePlaying()
controller.player.togglePlaying()
}
@IBAction func previous(_ sender: Any) {
@@ -50,7 +53,7 @@ class ViewController: UIViewController {
}
@IBAction func scrubbing(_ sender: UISlider) {
try? controller.player.seek(to: Double(slider.value))
controller.player.seek(to: Double(slider.value))
}
@IBAction func scrubbingValueChanged(_ sender: UISlider) {
@@ -58,63 +61,59 @@ class ViewController: UIViewController {
elapsedTimeLabel.text = value.secondsToString()
remainingTimeLabel.text = (controller.player.duration - value).secondsToString()
}
}
extension ViewController: AudioPlayerDelegate {
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason) {
func updateTimeValues() {
self.slider.maximumValue = Float(self.controller.player.duration)
self.slider.setValue(Float(self.controller.player.currentTime), animated: true)
self.elapsedTimeLabel.text = self.controller.player.currentTime.secondsToString()
self.remainingTimeLabel.text = (self.controller.player.duration - self.controller.player.currentTime).secondsToString()
}
func updateMetaData() {
if let item = controller.player.currentItem {
titleLabel.text = item.getTitle()
artistLabel.text = item.getArtist()
item.getArtwork({ (image) in
self.imageView.image = image
})
}
}
func audioPlayer(playerDidChangeState state: AVPlayerWrapperState) {
func setPlayButtonState(forAudioPlayerState state: AudioPlayerState) {
playButton.setTitle(state == .playing ? "Pause" : "Play", for: .normal)
switch state {
case .ready:
if let item = controller.player.currentItem {
titleLabel.text = item.getTitle()
artistLabel.text = item.getArtist()
item.getArtwork({ (image) in
self.imageView.image = image
})
}
// MARK: - AudioPlayer Event Handlers
func handleAudioPlayerStateChange(data: AudioPlayer.StateChangeEventData) {
DispatchQueue.main.async {
self.setPlayButtonState(forAudioPlayerState: data)
switch data {
case .ready:
self.updateMetaData()
self.updateTimeValues()
case .loading, .playing, .paused, .idle:
self.updateTimeValues()
}
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
case .loading, .playing, .paused, .idle:
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
}
}
func audioPlayer(secondsElapsed seconds: Double) {
func handleAudioPlayerSecondElapsed(data: AudioPlayer.SecondElapseEventData) {
if !isScrubbing {
slider.setValue(Float(seconds), animated: false)
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
DispatchQueue.main.async {
self.updateTimeValues()
}
}
}
func audioPlayer(failedWithError error: Error?) {
}
func audioPlayer(seekTo seconds: Int, didFinish: Bool) {
func handleAudioPlayerDidSeek(data: AudioPlayer.SeekEventData) {
isScrubbing = false
}
func audioPlayer(didUpdateDuration duration: Double) {
slider.maximumValue = Float(controller.player.duration)
slider.setValue(Float(controller.player.currentTime), animated: true)
elapsedTimeLabel.text = controller.player.currentTime.secondsToString()
remainingTimeLabel.text = (controller.player.duration - controller.player.currentTime).secondsToString()
func handleAudioPlayerUpdateDuration(data: AudioPlayer.UpdateDurationEventData) {
DispatchQueue.main.async {
self.updateTimeValues()
}
}
}
+5 -4
View File
@@ -7,8 +7,8 @@ import AVFoundation
class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
var status: AVPlayerStatus?
var timeControlStatus: AVPlayerTimeControlStatus?
var status: AVPlayer.Status?
var timeControlStatus: AVPlayer.TimeControlStatus?
override func spec() {
@@ -19,6 +19,7 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
beforeEach {
player = AVPlayer()
player.volume = 0.0
observer = AVPlayerObserver(player: player)
observer.delegate = self
}
@@ -58,11 +59,11 @@ class AVPlayerObserverTests: QuickSpec, AVPlayerObserverDelegate {
}
}
func player(statusDidChange status: AVPlayerStatus) {
func player(statusDidChange status: AVPlayer.Status) {
self.status = status
}
func player(didChangeTimeControlStatus status: AVPlayerTimeControlStatus) {
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
self.timeControlStatus = status
}
+25 -9
View File
@@ -30,10 +30,6 @@ class AVPlayerWrapperTests: QuickSpec {
beforeEach {
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: false)
}
it("should be loading", closure: {
expect(wrapper.state).to(equal(AVPlayerWrapperState.loading))
})
it("should eventually be ready", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.ready))
@@ -128,6 +124,17 @@ class AVPlayerWrapperTests: QuickSpec {
expect(wrapper.state).to(equal(AVPlayerWrapperState.idle))
})
})
context("when loading source with initial time", closure: {
let initialTime: TimeInterval = 4.0
beforeEach {
wrapper.load(from: LongSource.url, playWhenReady: true, initialTime: initialTime)
}
it("should eventually be playing", closure: {
expect(wrapper.state).toEventually(equal(AVPlayerWrapperState.playing))
})
})
})
describe("its duration", {
@@ -137,7 +144,7 @@ class AVPlayerWrapperTests: QuickSpec {
context("when loading source", {
beforeEach {
wrapper.load(from: URL(fileURLWithPath: Source.path), playWhenReady: false)
wrapper.load(from: URL(fileURLWithPath: LongSource.path), playWhenReady: false)
}
it("should eventually not be 0", closure: {
expect(wrapper.duration).toEventuallyNot(equal(0))
@@ -151,18 +158,27 @@ class AVPlayerWrapperTests: QuickSpec {
})
context("when seeking to a time", {
var passed = false
let holder = AVPlayerWrapperDelegateHolder()
let seekTime: TimeInterval = 0.5
beforeEach {
wrapper.delegate = holder
holder.seekCompletion = { passed = true }
wrapper.load(from: Source.url, playWhenReady: false)
wrapper.seek(to: seekTime)
}
it("should eventually be equal to the seeked time", closure: {
expect(passed).toEventually(beTrue())
expect(wrapper.currentTime).toEventually(equal(seekTime))
})
})
context("when playing from initial time", closure: {
let initialTime: TimeInterval = 4.0
beforeEach {
wrapper.load(from: LongSource.url, playWhenReady: false, initialTime: initialTime)
}
it("should eventuallt be equal to the initial time", closure: {
expect(wrapper.currentTime).toEventually(equal(initialTime))
})
})
})
@@ -219,7 +235,7 @@ class AVPlayerWrapperTests: QuickSpec {
}
class AVPlayerWrapperDelegateHolder: AVPlayerWrapperDelegate {
func AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
func AVWrapperItemDidPlayToEndTime() {
}
+81
View File
@@ -0,0 +1,81 @@
import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
class AudioPlayerEventTests: QuickSpec {
class EventListener {
var handleEvent: ((Void)) -> Void = { _ in
}
}
override func spec() {
describe("An event") {
var event: AudioPlayer.Event<(Void)>!
beforeEach {
event = AudioPlayer.Event()
}
describe("its invokers", {
context("when adding a listener", {
var listener: EventListener!
beforeEach {
listener = EventListener()
event.addListener(listener, listener!.handleEvent)
}
it("should have one element", closure: {
expect(event.invokers.count).toEventuallyNot(equal(0))
})
context("then that listener is deinitialized and an an event is emitted", {
beforeEach {
listener = nil
event.emit(data: ())
}
it("should remove the invoker", closure: {
expect(event.invokers.count).toEventually(equal(0))
})
})
})
context("when adding multiple listeners", {
var listeners: [EventListener]!
beforeEach {
listeners = [0..<15].map {_ in
let listener = EventListener()
event.addListener(listener, listener.handleEvent)
return listener
}
}
it("should have several listeners", closure: {
expect(event.invokers.count).toEventually(equal(listeners.count))
})
context("then removing one", {
beforeEach {
event.removeListener(listeners[listeners.count / 2])
}
it("should have one less invoker", closure: {
expect(event.invokers.count).toEventually(equal(listeners.count - 1))
})
})
})
})
}
}
}
+65 -46
View File
@@ -1,5 +1,6 @@
import Quick
import Nimble
import AVFoundation
@testable import SwiftAudio
@@ -43,14 +44,12 @@ class AudioPlayerTests: QuickSpec {
})
context("when playing an item", {
var holder: AudioPlayerDelegateHolder!
var listener: AudioPlayerEventListener!
beforeEach {
holder = AudioPlayerDelegateHolder()
audioPlayer.delegate = holder
holder.stateUpdate = { state in
print(state.rawValue)
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
listener.stateUpdate = { state in
if state == .ready {
try? audioPlayer.play()
audioPlayer.play()
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
@@ -62,13 +61,12 @@ class AudioPlayerTests: QuickSpec {
})
context("when pausing an item", {
var holder: AudioPlayerDelegateHolder!
var listener: AudioPlayerEventListener!
beforeEach {
holder = AudioPlayerDelegateHolder()
audioPlayer.delegate = holder
holder.stateUpdate = { (state) in
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
listener.stateUpdate = { (state) in
if state == .playing {
try? audioPlayer.pause()
audioPlayer.pause()
}
}
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: true)
@@ -80,11 +78,10 @@ class AudioPlayerTests: QuickSpec {
})
context("when stopping an item", {
var holder: AudioPlayerDelegateHolder!
var listener: AudioPlayerEventListener!
beforeEach {
holder = AudioPlayerDelegateHolder()
audioPlayer.delegate = holder
holder.stateUpdate = { (state) in
listener = AudioPlayerEventListener(audioPlayer: audioPlayer)
listener.stateUpdate = { (state) in
if state == .playing {
audioPlayer.stop()
}
@@ -105,18 +102,26 @@ class AudioPlayerTests: QuickSpec {
})
context("when seeking to a time", {
var passed = false
let holder = AudioPlayerDelegateHolder()
let seekTime: TimeInterval = 0.5
let seekTime: TimeInterval = 1.0
beforeEach {
audioPlayer.delegate = holder
holder.seekCompletion = { passed = true }
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
audioPlayer.seek(to: seekTime)
}
it("should eventually be equal to the seeked time", closure: {
expect(passed).toEventually(beTrue())
expect(audioPlayer.currentTime).toEventually(equal(seekTime))
})
})
context("when playing an item with an initial time", {
var item: DefaultAudioItemInitialTime!
beforeEach {
item = DefaultAudioItemInitialTime(audioUrl: LongSource.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, initialTime: 4.0)
try? audioPlayer.load(item: item, playWhenReady: false)
}
it("should eventaully be equal to the initial time", closure: {
expect(audioPlayer.currentTime).toEventually(equal(item.getInitialTime()))
})
})
})
@@ -151,19 +156,42 @@ class AudioPlayerTests: QuickSpec {
expect(audioPlayer.currentItem).toNot(beNil())
})
})
context("when setting the timePitchAlgorithm", {
beforeEach {
audioPlayer.audioTimePitchAlgorithm = .timeDomain
}
context("then loading an item", {
beforeEach {
try? audioPlayer.load(item: Source.getAudioItem(), playWhenReady: false)
}
it("should have the applied timePitchAlgorithm", closure: {
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.timeDomain))
})
})
context("then loading a timepitching item", {
beforeEach {
let item = DefaultAudioItemTimePitching(audioUrl: Source.path, artist: nil, title: nil, albumTitle: nil, sourceType: .file, artwork: nil, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm.spectral)
try? audioPlayer.load(item: item, playWhenReady: false)
}
it("should have the applied timePitchAlgorithm", closure: {
expect(audioPlayer.wrapper.currentItem?.audioTimePitchAlgorithm).to(equal(AVAudioTimePitchAlgorithm.spectral))
})
})
})
})
}
}
}
class AudioPlayerDelegateHolder: AudioPlayerDelegate {
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason) {
}
class AudioPlayerEventListener {
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
var state: AudioPlayerState? {
didSet {
if let state = state {
@@ -172,29 +200,20 @@ class AudioPlayerDelegateHolder: AudioPlayerDelegate {
}
}
func audioPlayer(playerDidChangeState state: AudioPlayerState) {
var stateUpdate: ((_ state: AudioPlayerState) -> Void)?
var seekCompletion: (() -> Void)?
init(audioPlayer: AudioPlayer) {
audioPlayer.event.stateChange.addListener(self, handleDidUpdateState)
audioPlayer.event.seek.addListener(self, handleSeek)
}
func handleDidUpdateState(state: AudioPlayerState) {
self.state = state
}
func audioPlayer(secondsElapsed seconds: Double) {
}
func audioPlayer(failedWithError error: Error?) {
}
var seekCompletion: (() -> Void)?
func audioPlayer(seekTo seconds: Int, didFinish: Bool) {
func handleSeek(data: AudioPlayer.SeekEventData) {
seekCompletion?()
}
func audioPlayer(didUpdateDuration duration: Double) {
if let state = self.state {
self.stateUpdate?(state)
}
}
}
@@ -55,7 +55,7 @@ class AudioSessionControllerTests: QuickSpec {
context("when a interruption arrives", {
var delegate: AudioSessionControllerDelegateImplementation!
beforeEach {
let notification = Notification(name: .AVAudioSessionInterruption, object: nil, userInfo: [
let notification = Notification(name: AVAudioSession.interruptionNotification, object: nil, userInfo: [
AVAudioSessionInterruptionTypeKey: UInt(0)
])
delegate = AudioSessionControllerDelegateImplementation()
@@ -92,9 +92,9 @@ class AudioSessionControllerTests: QuickSpec {
class AudioSessionControllerDelegateImplementation: AudioSessionControllerDelegate {
var interruptionType: AVAudioSessionInterruptionType? = nil
var interruptionType: AVAudioSession.InterruptionType? = nil
func handleInterruption(type: AVAudioSessionInterruptionType) {
func handleInterruption(type: AVAudioSession.InterruptionType) {
self.interruptionType = type
}
}
+22 -10
View File
@@ -14,31 +14,43 @@ import AVFoundation
class NonFailingAudioSession: AudioSession {
var category: AVAudioSession.Category = AVAudioSession.Category.playback
var mode: AVAudioSession.Mode = AVAudioSession.Mode.default
var categoryOptions: AVAudioSession.CategoryOptions = []
var availableCategories: [AVAudioSession.Category] = []
var isOtherAudioPlaying: Bool = false
var availableCategories: [String] = []
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws {}
func setCategory(_ category: String) throws {}
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws {}
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws {}
func setActive(_ active: Bool) throws {}
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) throws {}
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws {}
}
class FailingAudioSession: AudioSession {
var category: AVAudioSession.Category = AVAudioSession.Category.playback
var mode: AVAudioSession.Mode = AVAudioSession.Mode.default
var categoryOptions: AVAudioSession.CategoryOptions = AVAudioSession.CategoryOptions.allowBluetooth
var availableCategories: [AVAudioSession.Category] = []
var isOtherAudioPlaying: Bool = false
var availableCategories: [String] = []
func setCategory(_ category: String) throws {
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws {
throw AVError(AVError.unknown)
}
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws {
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws {
throw AVError(AVError.unknown)
}
@@ -46,7 +58,7 @@ class FailingAudioSession: AudioSession {
throw AVError(AVError.unknown)
}
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) throws {
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws {
throw AVError(AVError.unknown)
}
@@ -0,0 +1,18 @@
//
// NowPlayingInfoCenter.swift
// SwiftAudio_Tests
//
// Created by Jørgen Henrichsen on 03/03/2019.
// Copyright © 2019 CocoaPods. All rights reserved.
//
import Foundation
import AVFoundation
@testable import SwiftAudio
class NowPlayingInfoCenter_Mock: NowPlayingInfoCenter {
var nowPlayingInfo: [String : Any]? = nil
}
@@ -0,0 +1,66 @@
//
// NowPlayingInfoController.swift
// SwiftAudio_Tests
//
// Created by Jørgen Henrichsen on 03/03/2019.
// Copyright © 2019 CocoaPods. All rights reserved.
//
import Foundation
import MediaPlayer
@testable import SwiftAudio
class NowPlayingInfoController_Mock: NowPlayingInfoControllerProtocol {
var info: [String: Any] = [:]
required public init() {
}
required public init(infoCenter: NowPlayingInfoCenter) {
}
public func set(keyValues: [NowPlayingInfoKeyValue]) {
keyValues.forEach { (keyValue) in
info[keyValue.getKey()] = keyValue.getValue()
}
}
public func set(keyValue: NowPlayingInfoKeyValue) {
info[keyValue.getKey()] = keyValue.getValue()
}
func getTitle() -> String? {
return info[MediaItemProperty.title(nil).getKey()] as? String
}
func getArtist() -> String? {
return info[MediaItemProperty.artist(nil).getKey()] as? String
}
func getAlbumTitle() -> String? {
return info[MediaItemProperty.albumTitle(nil).getKey()] as? String
}
func getRate() -> Double? {
return info[NowPlayingInfoProperty.playbackRate(nil).getKey()] as? Double
}
func getDuration() -> Double? {
return info[MediaItemProperty.duration(nil).getKey()] as? Double
}
func getCurrentTime() -> Double? {
return info[NowPlayingInfoProperty.elapsedPlaybackTime(nil).getKey()] as? Double
}
func getArtwork() -> MPMediaItemArtwork? {
return info[MediaItemProperty.artwork(nil).getKey()] as? MPMediaItemArtwork
}
func clear() {
info = [:]
}
}
@@ -0,0 +1,72 @@
import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
class NowPlayingInfoControllerTests: QuickSpec {
override func spec() {
describe("An NowPlayingInfoController") {
var nowPlayingController: NowPlayingInfoController!
beforeEach {
nowPlayingController = NowPlayingInfoController(infoCenter: NowPlayingInfoCenter_Mock())
}
describe("its info dictionary") {
context("when setting a value") {
beforeEach {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
}
it("should not be empty") {
expect(nowPlayingController.info.count).toNot(equal(0))
}
context("then calling clear()") {
beforeEach {
nowPlayingController.clear()
}
it("should be empty", closure: {
expect(nowPlayingController.info.count).to(equal(0))
})
}
}
}
describe("its info center") {
context("when setting a value") {
beforeEach {
nowPlayingController.set(keyValue: MediaItemProperty.title("Some title"))
}
it("should not be nil") {
expect(nowPlayingController.infoCenter.nowPlayingInfo).toNot(beNil())
}
it("should not be empty") {
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).toNot(equal(0))
}
context("then calling clear()") {
beforeEach {
nowPlayingController.clear()
}
it("should be empty", closure: {
expect(nowPlayingController.infoCenter.nowPlayingInfo?.count).to(equal(0))
})
}
}
}
}
}
}
+73
View File
@@ -0,0 +1,73 @@
import Quick
import Nimble
import MediaPlayer
@testable import SwiftAudio
/// Tests that the AudioPlayer is automatically updating the values it should update in the NowPlayingInfoController.
class NowPlayingInfoTests: QuickSpec {
override func spec() {
describe("An AudioPlayer") {
var audioPlayer: AudioPlayer!
var nowPlayingController: NowPlayingInfoController_Mock!
beforeEach {
nowPlayingController = NowPlayingInfoController_Mock()
audioPlayer = AudioPlayer(nowPlayingInfoController: nowPlayingController)
audioPlayer.automaticallyUpdateNowPlayingInfo = true
audioPlayer.volume = 0
}
describe("its NowPlayingInfoController", {
context("when loading an AudioItem", {
var item: AudioItem!
beforeEach {
item = Source.getAudioItem()
try? audioPlayer.load(item: item, playWhenReady: false)
}
it("should eventually be updated with meta data", closure: {
expect(nowPlayingController.getTitle()).toEventuallyNot(beNil())
expect(nowPlayingController.getTitle()).toEventually(equal(item.getTitle()!))
expect(nowPlayingController.getArtist()).toEventuallyNot(beNil())
expect(nowPlayingController.getArtist()).toEventually(equal(item.getArtist()!))
expect(nowPlayingController.getAlbumTitle()).toEventuallyNot(beNil())
expect(nowPlayingController.getAlbumTitle()).toEventually(equal(item.getAlbumTitle()!))
expect(nowPlayingController.getArtwork()).toEventuallyNot(beNil())
})
})
context("when playing an AudioItem", {
var item: AudioItem!
beforeEach {
item = LongSource.getAudioItem()
try? audioPlayer.load(item: item, playWhenReady: true)
}
it("should eventually be updated with playback values", closure: {
expect(nowPlayingController.getRate()).toEventuallyNot(beNil())
expect(nowPlayingController.getDuration()).toEventuallyNot(beNil())
expect(nowPlayingController.getCurrentTime()).toEventuallyNot(beNil())
})
})
})
}
}
}
+2 -2
View File
@@ -83,7 +83,7 @@ class QueueManagerTests: QuickSpec {
context("then replacing the item", closure: {
beforeEach {
try? manager.replaceCurrentItem(with: 1)
manager.replaceCurrentItem(with: 1)
}
it("should have replaced the current item", closure: {
expect(manager.current).to(equal(1))
@@ -217,7 +217,7 @@ class QueueManagerTests: QuickSpec {
var removed: Int?
var initialCurrentIndex: Int!
beforeEach {
try? manager.jump(to: 3)
let _ = try? manager.jump(to: 3)
initialCurrentIndex = manager.currentIndex
removed = try? manager.removeItem(at: initialCurrentIndex - 1)
}
+12 -3
View File
@@ -14,15 +14,24 @@ struct Source {
static let url: URL = URL(fileURLWithPath: Source.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: Source.path, sourceType: .file, pitchAlgorithmType: .lowQualityZeroLatency)
return DefaultAudioItem(audioUrl: Source.path, artist: "Artist", title: "Title", albumTitle: "AlbumTitle", sourceType: .file, artwork: UIImage())
}
}
struct ShortSource {
static let path: String = Bundle.main.path(forResource: "ShortTestSound", ofType: "m4a")!
static let url: URL = URL(fileURLWithPath: Source.path)
static let url: URL = URL(fileURLWithPath: ShortSource.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file, pitchAlgorithmType: .lowQualityZeroLatency)
return DefaultAudioItem(audioUrl: ShortSource.path, sourceType: .file)
}
}
struct LongSource {
static let path: String = Bundle.main.path(forResource: "WAV-MP3", ofType: "wav")!
static let url: URL = URL(fileURLWithPath: LongSource.path)
static func getAudioItem() -> AudioItem {
return DefaultAudioItem(audioUrl: LongSource.path, sourceType: .file)
}
}
+48 -11
View File
@@ -1,6 +1,6 @@
# SwiftAudio
[![Build Status](https://travis-ci.org/jorgenhenrichsen/SwiftAudio.svg?branch=master)](https://travis-ci.org/jorgenhenrichsen/SwiftAudio)
[![Build Status](https://app.bitrise.io/app/3d3ac2ba8d817235/status.svg?token=PHIPu3oMde5GdQEOZ1Ilww&branch=master)](https://app.bitrise.io/app/3d3ac2ba8d817235)
[![Version](https://img.shields.io/cocoapods/v/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
[![codecov](https://codecov.io/gh/jorgenhenrichsen/SwiftAudio/branch/master/graph/badge.svg)](https://codecov.io/gh/jorgenhenrichsen/SwiftAudio)
[![License](https://img.shields.io/cocoapods/l/SwiftAudio.svg?style=flat)](http://cocoapods.org/pods/SwiftAudio)
@@ -23,13 +23,13 @@ SwiftAudio is available through [CocoaPods](http://cocoapods.org). To install
it, simply add the following line to your Podfile:
```ruby
pod 'SwiftAudio', '~> 0.4.3'
pod 'SwiftAudio', '~> 0.8.0'
```
### Carthage
SwiftAudio supports [Carthage](https://github.com/Carthage/Carthage). Add this to your Cartfile:
```ruby
github "jorgenhenrichsen/SwiftAudio" ~> 0.4.3
github "jorgenhenrichsen/SwiftAudio" ~> 0.8.0
```
Then follow the rest of Carthage instructions on [adding a framework](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application).
@@ -39,17 +39,33 @@ Then follow the rest of Carthage instructions on [adding a framework](https://gi
To get started playing some audio:
```swift
let player = AudioPlayer()
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency)
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
player.load(item: audioItem, playWhenReady: true) // Load the item and start playing when the player is ready.
```
Implement `AudioPlayerDelegate` to get notified about useful events and updates to the state of the `AudioPlayer`.
To listen for events in the `AudioPlayer`, subscribe to events found in the `event` property of the `AudioPlayer`.
To subscribe to an event:
```swift
class MyCustomViewController: UIViewController {
let audioPlayer = AudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
audioPlayer.event.stateChange.addListener(self, handleAudioPlayerStateChange)
}
func handleAudioPlayerStateChange(state: AudioPlayerState) {
// Handle the event
}
}
```
#### QueuedAudioPlayer
The `QueuedAudioPlayer` is asubclass of `AudioPlayer` that maintains a queue of audio tracks.
The `QueuedAudioPlayer` is a subclass of `AudioPlayer` that maintains a queue of audio tracks.
```swift
let player = QueuedAudioPlayer()
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream, pitchAlgorithmType: .lowQualityZeroLatency)
let audioItem = DefaultAudioItem(audioUrl: "someUrl", sourceType: .stream)
player.add(item: audioItem, playWhenReady: true) // Since this is the first item, we can supply playWhenReady: true to immedietaly start playing when the item is loaded.
```
@@ -77,15 +93,16 @@ Current options for configuring the `AudioPlayer`:
- `volume`
- `isMuted`
- `rate`
- `audioTimePitchAlgorithm`: This value decides the `AVAudioTimePitchAlgorithm` used for each `AudioItem`. Implement `TimePitching` in your `AudioItem`-subclass to override individually for each `AudioItem`.
### Audio Session
Remember to activate an audio session with an appropriate category for your app. This can be done with `AudioSessionController`:
```swift
try? AudioSessionController.set(category: .playback)
try? AudioSessionController.shared.set(category: .playback)
//...
// You should wait with activating the session until you actually start playback of audio.
// This is to avoid interrupting other audio without the need to do it.
try? AudioSessionController.activateSession()
try? AudioSessionController.shared.activateSession()
```
**Important**: If you want audio to continue playing when the app is inactive, remember to activate background audio:
@@ -98,8 +115,25 @@ If you are storing progress for playback time on items when the app quits, it ca
To disable interruption notifcations set `isObservingForInterruptions` to `false`.
### Now Playing Info
The `AudioPlayer` will automatically update the `MPNowPlayingInfoCenter` with artist, title, album, artwork and time if the passed in `AudioItem` supports this. This functionality can be turned off by setting `automaticallyUpdateNowPlayingInfo` to `false`.
If you need to set additional properties for some items, access the player's `NowPlayingInfoController` and call `set(keyValue:)`. Available properties can be found in `NowPlayingInfoProperty`.
The `AudioPlayer` can automatically update `nowPlayingInfo` for you. This requires `automaticallyUpdateNowPlayingInfo` to be true (default), and that the `AudioItem` that is passed in return values for the getters. The `AudioPlayer` will update: artist, title, album, artwork, elapsed time, duration and rate.
Additional properties for items can be set by accessing the setter of the `nowPlayingInforController`:
```swift
let player = AudioPlayer()
player.load(item: someItem)
player.nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.isLiveStream(true))
```
The set(keyValue:) and set(keyValues:) accept both `MediaItemProperty` and `NowPlayingInfoProperty`.
The info can be forced to reload/update from the `AudioPlayer`.
```swift
audioPlayer.loadNowPlayingMetaValues()
audioPlayer.updateNowPlayingPlaybackValues()
```
The current info can be cleared with:
```swift
audioPlayer.nowPlayingInfoController.clear()
```
### Remote Commands
**First** go to App Settings -> Capabilites -> Background Modes -> Check 'Remote notifications'
@@ -125,6 +159,9 @@ player.remoteCommandController.handlePlayCommand = { (event) in
```
All available overrides can be found by looking at `RemoteCommandController`.
### Start playback from a certain point in time
Make your `AudioItem`-subclass conform to `InitialTiming` to be able to start playback from a certain time.
## Author
Jørgen Henrichsen
+1 -1
View File
@@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = 'SwiftAudio'
s.version = '0.4.3'
s.version = '0.8.0'
s.summary = 'Easy audio streaming for iOS'
# This description is used to generate tags and improve search results.
@@ -36,6 +36,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
True if the last call to load(from:playWhenReady) had playWhenReady=true.
*/
fileprivate var _playWhenReady: Bool = true
fileprivate var _initialTime: TimeInterval?
fileprivate var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle {
didSet {
@@ -85,12 +86,23 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
var duration: TimeInterval {
if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
if let seconds = currentItem?.asset.duration.seconds, !seconds.isNaN {
return seconds
}
return 0
else if let seconds = currentItem?.duration.seconds, !seconds.isNaN {
return seconds
}
else if let seconds = currentItem?.loadedTimeRanges.first?.timeRangeValue.duration.seconds,
!seconds.isNaN {
return seconds
}
return 0.0
}
var bufferedPosition: TimeInterval {
return currentItem?.loadedTimeRanges.last?.timeRangeValue.end.seconds ?? 0
}
weak var delegate: AVPlayerWrapperDelegate? = nil
var bufferDuration: TimeInterval = 0
@@ -130,6 +142,8 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
pause()
case .paused:
play()
@unknown default:
fatalError("Unknown AVPlayer.timeControlStatus")
}
}
@@ -139,7 +153,13 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
}
func seek(to seconds: TimeInterval) {
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, 1)) { (finished) in
avPlayer.seek(to: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000)) { (finished) in
if let _ = self._initialTime {
self._initialTime = nil
if self._playWhenReady {
self.play()
}
}
self.delegate?.AVWrapper(seekTo: Int(seconds), didFinish: finished)
}
}
@@ -147,7 +167,6 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
func load(from url: URL, playWhenReady: Bool) {
reset(soft: true)
_playWhenReady = playWhenReady
_state = .loading
// Set item
let currentAsset = AVURLAsset(url: url)
@@ -162,6 +181,12 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerItemObserver.startObserving(item: currentItem)
}
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?) {
_initialTime = initialTime
self.pause()
self.load(from: url, playWhenReady: playWhenReady)
}
// MARK: - Util
private func reset(soft: Bool) {
@@ -180,7 +205,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
// MARK: - AVPlayerObserverDelegate
func player(didChangeTimeControlStatus status: AVPlayerTimeControlStatus) {
func player(didChangeTimeControlStatus status: AVPlayer.TimeControlStatus) {
switch status {
case .paused:
if currentItem == nil {
@@ -193,17 +218,24 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
self._state = .loading
case .playing:
self._state = .playing
@unknown default:
break
}
}
func player(statusDidChange status: AVPlayerStatus) {
func player(statusDidChange status: AVPlayer.Status) {
switch status {
case .readyToPlay:
self._state = .ready
if _playWhenReady {
if let initialTime = _initialTime {
self.seek(to: initialTime)
}
else if _playWhenReady {
self.play()
}
break
case .failed:
@@ -212,6 +244,8 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate {
case .unknown:
break
@unknown default:
break
}
}
@@ -236,7 +270,7 @@ extension AVPlayerWrapper: AVPlayerItemNotificationObserverDelegate {
// MARK: - AVPlayerItemNotificationObserverDelegate
func itemDidPlayToEndTime() {
delegate?.AVWrapper(itemPlaybackDoneWithReason: .playedUntilEnd)
delegate?.AVWrapperItemDidPlayToEndTime()
}
}
@@ -11,10 +11,10 @@ import Foundation
protocol AVPlayerWrapperDelegate: class {
func AVWrapper(didChangeState state: AVPlayerWrapperState)
func AVWrapper(itemPlaybackDoneWithReason: PlaybackEndedReason)
func AVWrapper(secondsElapsed seconds: Double)
func AVWrapper(failedWithError error: Error?)
func AVWrapper(seekTo seconds: Int, didFinish: Bool)
func AVWrapper(didUpdateDuration duration: Double)
func AVWrapperItemDidPlayToEndTime()
}
+6 -2
View File
@@ -19,10 +19,12 @@ protocol AVPlayerWrapperProtocol {
var duration: TimeInterval { get }
var bufferedPosition: TimeInterval { get }
var reasonForWaitingToPlay: AVPlayer.WaitingReason? { get }
var rate: Float { get set }
var rate: Float { get set }
var delegate: AVPlayerWrapperDelegate? { get set }
@@ -35,7 +37,7 @@ protocol AVPlayerWrapperProtocol {
var isMuted: Bool { get set }
var automaticallyWaitsToMinimizeStalling: Bool { get set }
func play()
@@ -49,4 +51,6 @@ protocol AVPlayerWrapperProtocol {
func load(from url: URL, playWhenReady: Bool)
func load(from url: URL, playWhenReady: Bool, initialTime: TimeInterval?)
}
+55 -11
View File
@@ -20,14 +20,24 @@ public protocol AudioItem {
func getTitle() -> String?
func getAlbumTitle() -> String?
func getSourceType() -> SourceType
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
func getArtwork(_ handler: @escaping (UIImage?) -> Void)
}
public struct DefaultAudioItem: AudioItem {
/// Make your `AudioItem`-subclass conform to this protocol to control which AVAudioTimePitchAlgorithm is used for each item.
public protocol TimePitching {
func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm
}
/// Make your `AudioItem`-subclass conform to this protocol to control enable the ability to start an item at a specific time of playback.
public protocol InitialTiming {
func getInitialTime() -> TimeInterval
}
public class DefaultAudioItem: AudioItem {
public var audioUrl: String
public var artist: String?
@@ -38,17 +48,14 @@ public struct DefaultAudioItem: AudioItem {
public var sourceType: SourceType
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
public var artwork: UIImage?
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, pitchAlgorithmType: AVAudioTimePitchAlgorithm, artwork: UIImage? = nil) {
public init(audioUrl: String, artist: String? = nil, title: String? = nil, albumTitle: String? = nil, sourceType: SourceType, artwork: UIImage? = nil) {
self.audioUrl = audioUrl
self.artist = artist
self.title = title
self.albumTitle = albumTitle
self.sourceType = sourceType
self.pitchAlgorithmType = pitchAlgorithmType
self.artwork = artwork
}
@@ -71,13 +78,50 @@ public struct DefaultAudioItem: AudioItem {
public func getSourceType() -> SourceType {
return sourceType
}
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
return pitchAlgorithmType
}
public func getArtwork(_ handler: @escaping (UIImage?) -> Void) {
handler(artwork)
}
}
/// An AudioItem that also conforms to the `TimePitching`-protocol
public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {
public var pitchAlgorithmType: AVAudioTimePitchAlgorithm
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.pitchAlgorithmType = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm) {
self.pitchAlgorithmType = audioTimePitchAlgorithm
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getPitchAlgorithmType() -> AVAudioTimePitchAlgorithm {
return pitchAlgorithmType
}
}
/// An AudioItem that also conforms to the `InitialTiming`-protocol
public class DefaultAudioItemInitialTime: DefaultAudioItem, InitialTiming {
public var initialTime: TimeInterval
public override init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?) {
self.initialTime = 0.0
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public init(audioUrl: String, artist: String?, title: String?, albumTitle: String?, sourceType: SourceType, artwork: UIImage?, initialTime: TimeInterval) {
self.initialTime = initialTime
super.init(audioUrl: audioUrl, artist: artist, title: title, albumTitle: albumTitle, sourceType: sourceType, artwork: artwork)
}
public func getInitialTime() -> TimeInterval {
return initialTime
}
}
+114 -67
View File
@@ -10,29 +10,18 @@ import MediaPlayer
public typealias AudioPlayerState = AVPlayerWrapperState
public protocol AudioPlayerDelegate: class {
func audioPlayer(playerDidChangeState state: AudioPlayerState)
func audioPlayer(itemPlaybackEndedWithReason reason: PlaybackEndedReason)
func audioPlayer(secondsElapsed seconds: Double)
func audioPlayer(failedWithError error: Error?)
func audioPlayer(seekTo seconds: Int, didFinish: Bool)
func audioPlayer(didUpdateDuration duration: Double)
}
public class AudioPlayer: AVPlayerWrapperDelegate {
private var wrapper: AVPlayerWrapperProtocol
private var _wrapper: AVPlayerWrapperProtocol
public let nowPlayingInfoController: NowPlayingInfoController
/// The wrapper around the underlying AVPlayer
var wrapper: AVPlayerWrapperProtocol {
return _wrapper
}
public let nowPlayingInfoController: NowPlayingInfoControllerProtocol
public let remoteCommandController: RemoteCommandController
public weak var delegate: AudioPlayerDelegate?
public let event = EventHolder()
var _currentItem: AudioItem?
public var currentItem: AudioItem? {
@@ -44,6 +33,12 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
*/
public var automaticallyUpdateNowPlayingInfo: Bool = true
/**
Controls the time pitch algorithm applied to each item loaded into the player.
If the loaded `AudioItem` conforms to `TimePitcher`-protocol this will be overriden.
*/
public var audioTimePitchAlgorithm: AVAudioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.lowQualityZeroLatency
/**
Default remote commands to use for each playing item
*/
@@ -66,6 +61,13 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
return wrapper.duration
}
/**
The bufferedPosition of the current AudioItem.
*/
public var bufferedPosition: Double {
return wrapper.bufferedPosition
}
/**
The current state of the underlying `AudioPlayer`.
*/
@@ -84,7 +86,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
*/
public var bufferDuration: TimeInterval {
get { return wrapper.bufferDuration }
set { wrapper.bufferDuration = newValue }
set { _wrapper.bufferDuration = newValue }
}
/**
@@ -92,7 +94,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
*/
public var timeEventFrequency: TimeEventFrequency {
get { return wrapper.timeEventFrequency }
set { wrapper.timeEventFrequency = newValue }
set { _wrapper.timeEventFrequency = newValue }
}
/**
@@ -100,22 +102,22 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
*/
public var automaticallyWaitsToMinimizeStalling: Bool {
get { return wrapper.automaticallyWaitsToMinimizeStalling }
set { wrapper.automaticallyWaitsToMinimizeStalling = newValue }
set { _wrapper.automaticallyWaitsToMinimizeStalling = newValue }
}
public var volume: Float {
get { return wrapper.volume }
set { wrapper.volume = newValue }
set { _wrapper.volume = newValue }
}
public var isMuted: Bool {
get { return wrapper.isMuted }
set { wrapper.isMuted = newValue }
set { _wrapper.isMuted = newValue }
}
public var rate: Float {
get { return wrapper.rate }
set { wrapper.rate = newValue }
set { _wrapper.rate = newValue }
}
// MARK: - Init
@@ -126,13 +128,13 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- parameter infoCenter: The InfoCenter to update. Default is `MPNowPlayingInfoCenter.default()`.
*/
public init(avPlayer: AVPlayer = AVPlayer(),
nowPlayingInfoController: NowPlayingInfoController = NowPlayingInfoController(),
nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(),
remoteCommandController: RemoteCommandController = RemoteCommandController()) {
self.wrapper = AVPlayerWrapper(avPlayer: avPlayer)
self._wrapper = AVPlayerWrapper(avPlayer: avPlayer)
self.nowPlayingInfoController = nowPlayingInfoController
self.remoteCommandController = remoteCommandController
self.wrapper.delegate = self
self._wrapper.delegate = self
self.remoteCommandController.audioPlayer = self
}
@@ -145,24 +147,35 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
- parameter playWhenReady: Immediately start playback when the item is ready. Default is `true`. If you disable this you have to call play() or togglePlay() when the `state` switches to `ready`.
*/
public func load(item: AudioItem, playWhenReady: Bool = true) throws {
print("Loading: \(item)")
let url: URL
switch item.getSourceType() {
case .stream:
if let url = URL(string: item.getSourceUrl()) {
wrapper.load(from: url, playWhenReady: playWhenReady)
if let itemUrl = URL(string: item.getSourceUrl()) {
url = itemUrl
}
else {
throw APError.LoadError.invalidSourceUrl(item.getSourceUrl())
}
case .file:
wrapper.load(from: URL(fileURLWithPath: item.getSourceUrl()), playWhenReady: playWhenReady)
url = URL(fileURLWithPath: item.getSourceUrl())
}
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
wrapper.load(from: url,
playWhenReady: playWhenReady,
initialTime: (item as? InitialTiming)?.getInitialTime())
if let item = item as? TimePitching {
wrapper.currentItem?.audioTimePitchAlgorithm = item.getPitchAlgorithmType()
}
else {
wrapper.currentItem?.audioTimePitchAlgorithm = audioTimePitchAlgorithm
}
self._currentItem = item
self.updateMetaValues(item: item)
setArtwork(forItem: item)
if (automaticallyUpdateNowPlayingInfo) {
self.loadNowPlayingMetaValues()
}
enableRemoteCommands(forItem: item)
}
@@ -191,15 +204,18 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
Stop playback, resetting the player.
*/
public func stop() {
AVWrapper(itemPlaybackDoneWithReason: .playerStopped)
self.reset()
self.wrapper.stop()
self.event.playbackEnd.emit(data: .playerStopped)
}
/**
Seek to a specific time in the item.
*/
public func seek(to seconds: TimeInterval) {
if automaticallyUpdateNowPlayingInfo {
self.updateNowPlayingCurrentTime(seconds)
}
self.wrapper.seek(to: seconds)
}
@@ -220,26 +236,54 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
// MARK: - NowPlayingInfo
/// Reload all NowPlayingInfo for the playing item.
public func reloadNowPlayingInfo() {
/**
Loads NowPlayingInfo-meta values with the values found in the current `AudioItem`. Use this if a change to the `AudioItem` is made and you want to update the `NowPlayingInfoController`s values.
Reloads:
- Artist
- Title
- Album title
- Album artwork
*/
public func loadNowPlayingMetaValues() {
guard let item = currentItem else { return }
updateMetaValues(item: item)
setArtwork(forItem: item)
updatePlaybackValues()
}
func updateMetaValues(item: AudioItem) {
guard automaticallyUpdateNowPlayingInfo else { return }
nowPlayingInfoController.set(keyValues: [
MediaItemProperty.artist(item.getArtist()),
MediaItemProperty.title(item.getTitle()),
MediaItemProperty.albumTitle(item.getAlbumTitle()),
])
])
loadArtwork(forItem: item)
}
func setArtwork(forItem item: AudioItem) {
guard automaticallyUpdateNowPlayingInfo else { return }
/**
Resyncs the playbackvalues of the currently playing `AudioItem`.
Will resync:
- Current time
- Duration
- Playback rate
*/
public func updateNowPlayingPlaybackValues() {
updateNowPlayingDuration(duration)
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingRate(rate)
}
private func updateNowPlayingDuration(_ duration: Double) {
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(duration))
}
private func updateNowPlayingRate(_ rate: Float) {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(rate)))
}
private func updateNowPlayingCurrentTime(_ currentTime: Double) {
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(currentTime))
}
private func loadArtwork(forItem item: AudioItem) {
item.getArtwork { (image) in
if let image = image {
let artwork = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { (size) -> UIImage in
@@ -250,13 +294,6 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
}
}
func updatePlaybackValues() {
guard automaticallyUpdateNowPlayingInfo else { return }
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.elapsedPlaybackTime(wrapper.currentTime))
nowPlayingInfoController.set(keyValue: MediaItemProperty.duration(wrapper.duration))
nowPlayingInfoController.set(keyValue: NowPlayingInfoProperty.playbackRate(Double(wrapper.rate)))
}
// MARK: - Private
func reset() {
@@ -267,31 +304,41 @@ public class AudioPlayer: AVPlayerWrapperDelegate {
func AVWrapper(didChangeState state: AVPlayerWrapperState) {
switch state {
case .playing, .paused: updatePlaybackValues()
case .ready:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingPlaybackValues()
}
case .playing, .paused:
if (automaticallyUpdateNowPlayingInfo) {
updateNowPlayingCurrentTime(currentTime)
updateNowPlayingRate(rate)
}
default: break
}
self.delegate?.audioPlayer(playerDidChangeState: state)
}
func AVWrapper(itemPlaybackDoneWithReason reason: PlaybackEndedReason) {
self.delegate?.audioPlayer(itemPlaybackEndedWithReason: reason)
self.event.stateChange.emit(data: state)
}
func AVWrapper(secondsElapsed seconds: Double) {
self.delegate?.audioPlayer(secondsElapsed: seconds)
self.event.secondElapse.emit(data: seconds)
}
func AVWrapper(failedWithError error: Error?) {
self.delegate?.audioPlayer(failedWithError: error)
self.event.fail.emit(data: error)
}
func AVWrapper(seekTo seconds: Int, didFinish: Bool) {
self.updatePlaybackValues()
self.delegate?.audioPlayer(seekTo: seconds, didFinish: didFinish)
if !didFinish && automaticallyUpdateNowPlayingInfo {
updateNowPlayingCurrentTime(currentTime)
}
self.event.seek.emit(data: (seconds, didFinish))
}
func AVWrapper(didUpdateDuration duration: Double) {
self.delegate?.audioPlayer(didUpdateDuration: duration)
self.event.updateDuration.emit(data: duration)
}
func AVWrapperItemDidPlayToEndTime() {
self.event.playbackEnd.emit(data: .playedUntilEnd)
}
}
@@ -13,16 +13,21 @@ protocol AudioSession {
var isOtherAudioPlaying: Bool { get }
var availableCategories: [String] { get }
var category: AVAudioSession.Category { get }
var mode: AVAudioSession.Mode { get }
func setCategory(_ category: String) throws
var categoryOptions: AVAudioSession.CategoryOptions { get }
func setCategory(_ category: String, mode: String, options: AVAudioSessionCategoryOptions) throws
var availableCategories: [AVAudioSession.Category] { get }
func setActive(_ active: Bool) throws
@available(iOS 10.0, *)
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions) throws
func setActive(_ active: Bool, with options: AVAudioSessionSetActiveOptions) throws
@available(iOS 11.0, *)
func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, policy: AVAudioSession.RouteSharingPolicy, options: AVAudioSession.CategoryOptions) throws
func setActive(_ active: Bool, options: AVAudioSession.SetActiveOptions) throws
}
@@ -1,53 +0,0 @@
//
// AudioSessionCategory.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 02/11/2018.
//
import Foundation
import AVFoundation
/**
An enum wrapper around the AVAudioSessionCategories.
For detailed info about the categories, see: [AudioSession Programming Guide](https://developer.apple.com/library/content/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html#//apple_ref/doc/uid/TP40007875-CH10)
*/
public enum AudioSessionCategory {
case ambient
case soloAmbient
case playback
case record
case playAndRecord
case multiRoute
func getValue() -> String {
switch self {
case .ambient:
return AVAudioSessionCategoryAmbient
case .soloAmbient:
return AVAudioSessionCategorySoloAmbient
case .playback:
return AVAudioSessionCategoryPlayback
case .record:
return AVAudioSessionCategoryRecord
case .playAndRecord:
return AVAudioSessionCategoryPlayAndRecord
case .multiRoute:
return AVAudioSessionCategoryMultiRoute
}
}
}
@@ -10,7 +10,7 @@ import AVFoundation
public protocol AudioSessionControllerDelegate: class {
func handleInterruption(type: AVAudioSessionInterruptionType)
func handleInterruption(type: AVAudioSession.InterruptionType)
}
@@ -72,7 +72,7 @@ public class AudioSessionController {
public func activateSession() throws {
do {
try audioSession.setActive(true)
try audioSession.setActive(true, options: [])
audioSessionIsActive = true
}
catch let error { throw error }
@@ -80,17 +80,14 @@ public class AudioSessionController {
public func deactivateSession() throws {
do {
try audioSession.setActive(false)
try audioSession.setActive(false, options: [])
audioSessionIsActive = false
}
catch let error { throw error }
}
/**
Set the audiosession.
*/
public func set(category: AudioSessionCategory) throws {
try audioSession.setCategory(category.getValue())
public func set(category: AVAudioSession.Category) throws {
try audioSession.setCategory(category, mode: audioSession.mode, options: audioSession.categoryOptions)
}
// MARK: - Interruptions
@@ -98,20 +95,20 @@ public class AudioSessionController {
private func registerForInterruptionNotification() {
notificationCenter.addObserver(self,
selector: #selector(handleInterruption),
name: .AVAudioSessionInterruption,
name: AVAudioSession.interruptionNotification,
object: nil)
_isObservingForInterruptions = true
}
private func unregisterForInterruptionNotification() {
notificationCenter.removeObserver(self, name: .AVAudioSessionInterruption, object: nil)
notificationCenter.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
_isObservingForInterruptions = false
}
@objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
+121
View File
@@ -0,0 +1,121 @@
//
// Event.swift
// SwiftAudio
//
// Created by Jørgen Henrichsen on 09/03/2019.
//
import Foundation
extension AudioPlayer {
public typealias StateChangeEventData = (AudioPlayerState)
public typealias PlaybackEndEventData = (PlaybackEndedReason)
public typealias SecondElapseEventData = (TimeInterval)
public typealias FailEventData = (Error?)
public typealias SeekEventData = (seconds: Int, didFinish: Bool)
public typealias UpdateDurationEventData = (Double)
public struct EventHolder {
/**
Emitted when the `AudioPlayer`s state is changed
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let stateChange: AudioPlayer.Event<StateChangeEventData> = AudioPlayer.Event()
/**
Emitted when the playback of the player, for some reason, has stopped.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let playbackEnd: AudioPlayer.Event<PlaybackEndEventData> = AudioPlayer.Event()
/**
Emitted when a second is elapsed in the `AudioPlayer`.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let secondElapse: AudioPlayer.Event<SecondElapseEventData> = AudioPlayer.Event()
/**
Emitted when the player encounters an error.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let fail: AudioPlayer.Event<FailEventData> = AudioPlayer.Event()
/**
Emitted when the player is done attempting to seek.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let seek: AudioPlayer.Event<SeekEventData> = AudioPlayer.Event()
/**
Emitted when the player updates its duration.
- Important: Remember to dispatch to the main queue if any UI is updated in the event handler.
*/
public let updateDuration: AudioPlayer.Event<UpdateDurationEventData> = AudioPlayer.Event()
}
public typealias EventClosure<EventData> = (EventData) -> Void
class Invoker<EventData> {
// Signals false if the listener object is nil
let invoke: (EventData) -> Bool
weak var listener: AnyObject?
init<Listener: AnyObject>(listener: Listener, closure: @escaping EventClosure<EventData>) {
self.listener = listener
self.invoke = { [weak listener] (data: EventData) in
guard let _ = listener else {
return false
}
closure(data)
return true
}
}
}
public class Event<EventData> {
private let eventQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.utility)
private let actionQueue: DispatchQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated)
private let invokersSemaphore: DispatchSemaphore = DispatchSemaphore(value: 1)
var invokers: [Invoker<EventData>] = []
public func addListener<Listener: AnyObject>(_ listener: Listener, _ closure: @escaping EventClosure<EventData>) {
actionQueue.async {
self.invokersSemaphore.wait()
self.invokers.append(Invoker(listener: listener, closure: closure))
self.invokersSemaphore.signal()
}
}
public func removeListener(_ listener: AnyObject) {
actionQueue.async {
self.invokersSemaphore.wait()
self.invokers = self.invokers.filter({ (invoker) -> Bool in
if let listenerToCheck = invoker.listener {
return listenerToCheck !== listener
}
return true
})
self.invokersSemaphore.signal()
}
}
func emit(data: EventData) {
eventQueue.async {
self.invokersSemaphore.wait()
self.invokers = self.invokers.filter({ (invoker) -> Bool in
return invoker.invoke(data)
})
self.invokersSemaphore.signal()
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More