diff --git a/README.md b/README.md
index 4776c2d1..88fbe4b9 100755
--- a/README.md
+++ b/README.md
@@ -1,10 +1,12 @@
# Swift Radio
-Swift Radio is an open source radio station app with robust and professional features. This is a fully realized Radio App built entirely in Swift. **Master is now the Xcode 9.0/Swift 4 branch**. Note there is an AVPlayer branch [here](https://github.com/swiftcodex/Swift-Radio-Pro/tree/xcode8).
+Swift Radio is an open source radio station app with robust and professional features. This is a fully realized Radio App built entirely in Swift. **Master is now the Xcode 9 / Swift 4 branch**.
There are over 80 different apps accepted to the app store using this code!
-![alt text](http://matthewfecher.com/wp-content/uploads/2015/09/screen-1.jpg "Swift Radio")
+
+
+
## Video
View this [**GETTING STARTED VIDEO**](https://youtu.be/m7jiajCHFvc).
@@ -13,18 +15,22 @@ Give it a quick watch.
## Features
-- LastFM API and iTunes API Integration to automatically download Album Art
-- Parses metadata from streams (Track & Artist information)
- Ability to update Stations from server or locally. (Update stations anytime without resubmitting to app store!)
- Displays Artist, Track & Album Art on Lock Screen
- Custom views optimized for 5, 6 and 6+ for backwards compatibility
-- Compiles with Xcode 9.0 & Swift 4.0
+- Compiles with Xcode 9 & Swift 4
+- Parses JSON using Swift 4 Codable protocol
- Background audio performance
- Search Bar that can be turned on or off to search stations
- Supports local or hosted station images
- "About" screen with ability to send email & visit website
-- Uses industry standard SwiftyJSON library for easy JSON manipulation
- Pull to Refresh stations
+- Uses the AVPlayer wrapper library [FRadioPlayer](https://github.com/fethica/FRadioPlayer):
+ * Automatically download Album Art from iTunes API
+ * Parses metadata from streams (Track & Artist information)
+- Uses [Spring](https://github.com/MengTo/Spring) library:
+ * Animate UI components
+ * Download and cache images using ImageLoader class
## Important Notes
- 10.6.17 Update: The AVPlayer branch migrated to Xcode 9/Swift 4 by [@joemcmahon](https://github.com/joemcmahon).
@@ -72,7 +78,7 @@ Some of the things I've built into this Radio code for clients include: Facebook
## Setup
-The "SwiftRadio-Settings.swift" file contains some project settings to get you started. If you use LastFM, please enter your own LastFM Key.
+The "SwiftRadio-Settings.swift" file contains some project settings to get you started.
Watch this [Getting Started Video](https://youtu.be/m7jiajCHFvc) to get up & running quickly.
## Integration
@@ -95,7 +101,7 @@ Includes an example "stations.json" file. You may upload the JSON file to a serv
## Contributions
-Contributions are very welcome. Please create a separate branch (e.g. features/3dtouch). Please do not commit on master.
+Contributions are very welcome. Please check out the [dev branch](https://github.com/analogcode/Swift-Radio-Pro/tree/dev), create a separate branch (e.g. features/3dtouch). Please do not commit on master.
## FAQ
@@ -105,20 +111,8 @@ A: Nope. This is completely open source, you can do whatever you want with it. I
Q: How do I make my app support ipv6 networks?
A: For an app to be accepted by Apple to the app store as of June 1, 2016, you CAN NOT use number IP addresses. i.e. You must use something like "http://mystream.com/rock" instead of "http://44.120.33.55/" for your station stream URLs.
-Q: Isn't MPMoviePlayer going to be depreciated?
-A: Yes, eventually master should be migrated to use AVPlayer instead. If you'd like to work on it, feel free! There are currently two branches that use AVPlayer instead of MPMoviePlayer. A Swift 2/Xcode 7 version [here](https://github.com/swiftcodex/Swift-Radio-Pro/tree/avplayer). and a Swift 2.3/Xcode 8 version [here](https://github.com/swiftcodex/Swift-Radio-Pro/tree/xcode8).
-
Q: Is there an example of using this with the Spotify API?
-A: Yes, there is a branch here that uses it [here]( https://github.com/swiftcodex/Swift-Radio-Pro/tree/avplayer).
-
-Q: How do I use the iTunes API instead of LastFM?
-A: In the SwiftRadio-Settings.swift file, set the "useLastFM" key to "false". You do not need an API key to use the iTunes API. It is free.
-
-Q: The LastFM site isn't working properly? I can't create an API key.
-A: LastFM will sometimes put API signups on hold. You can check back later or try a different API.
-
-Q: It looks like your LastFM api key and secret might have been left in the code?
-A: Yes, people may use it for small amounts of testing. However, I ask that you change it before submitting to the app store. (Plus, it would be self-defeating for someone to submit it to the app store with the testing keys, as it would quickly throttle out and their album art downloads would stop working!)
+A: Yes, there is a branch here that uses it [here]( https://github.com/swiftcodex/Swift-Radio-Pro/tree/avplayer) (⚠️ **deprecated**).
Q: Is there another API to get album/track information besides LastFM, Spotify, and iTunes?
A: Rovi has a pretty sweet [music API](http://prod-doc.rovicorp.com/mashery/index.php/Data/APIs/Rovi-Music). The [Echo Nest](http://developer.echonest.com/) has all kinds of APIs that are fun to play with.
@@ -133,10 +127,10 @@ Q: Can you help me add a feature? Can you help me understand the code? Can you h
A: While I have a full-time job and other project obligations, I'd highly recommend you find a developer or mentor in your area to help. The code is well-documented and most developers should be able to help you rather quickly. While I am sometimes available for paid freelance work, see below in the readme, **I am not able to provide any free support or modifications.** Thank you for understanding!
Q: The song names aren't appearing for my station?
-A: Check with your stream provider to make sure they are sending Metadata properly. If a station sends data in a unique way, you can modify the way the app parses the metadata in the "metadataUpdated" method in the NowPlayingViewController.
+A: Check with your stream provider to make sure they are sending Metadata properly. If a station sends data in a unique way, you can modify the way the app parses the metadata, in the `RadioPlayer` class implement `FRadioPlayerDelegate` method: `radioPlayer(_ player: FRadioPlayer, metadataDidChange rawValue: String?)`.
## Single Station Branch
-There's now a branch without the StationsViewController. This is so you can use this code as a starting place for an app for just one radio station. View that [Branch Here](https://github.com/swiftcodex/Swift-Radio-Pro/tree/single-station).
+There's now a branch without the StationsViewController. This is so you can use this code as a starting place for an app for just one radio station. View that [Branch Here](https://github.com/swiftcodex/Swift-Radio-Pro/tree/single-station) (⚠️ **deprecated**).
## RadioKit SDK Example
diff --git a/SwiftRadio.xcodeproj/project.pbxproj b/SwiftRadio.xcodeproj/project.pbxproj
index faedeb53..59ee0a3d 100644
--- a/SwiftRadio.xcodeproj/project.pbxproj
+++ b/SwiftRadio.xcodeproj/project.pbxproj
@@ -8,7 +8,6 @@
/* Begin PBXBuildFile section */
2C5545BD1C1124DE00728469 /* SwiftRadioUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5545BC1C1124DE00728469 /* SwiftRadioUITests.swift */; };
- 5F22B9E01F72ABEF00CB5911 /* SwiftyJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F22B9DF1F72ABEF00CB5911 /* SwiftyJSON.swift */; };
5F22BA3C1F72AD5A00CB5911 /* SpringLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F22BA1E1F72AD3700CB5911 /* SpringLabel.swift */; };
5F22BA3D1F72AD5A00CB5911 /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F22BA1F1F72AD3900CB5911 /* BlurView.swift */; };
5F22BA3E1F72AD5A00CB5911 /* DesignableTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F22BA201F72AD3B00CB5911 /* DesignableTabBarController.swift */; };
@@ -46,7 +45,6 @@
94452E551AD7086800BFE7A5 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94452E541AD7086800BFE7A5 /* AboutViewController.swift */; };
945DB3C21AD58E3A00495EBB /* stations.json in Resources */ = {isa = PBXBuildFile; fileRef = 945DB3C11AD58E3A00495EBB /* stations.json */; };
945DB3C51AD5A6E200495EBB /* NothingFoundCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 945DB3C41AD5A6E200495EBB /* NothingFoundCell.xib */; };
- 94817DFB1B547D5700D3FA23 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94817DFA1B547D5700D3FA23 /* Player.swift */; };
949BBB401ACC9DEE005B7C26 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 949BBB3F1ACC9DEE005B7C26 /* DataManager.swift */; };
949E5EB01ACB340200AB6280 /* UIImageView+Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = 949E5EAF1ACB340200AB6280 /* UIImageView+Download.swift */; };
94AC70AE1AD05C6200652982 /* RadioStation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94AC70AD1AD05C6200652982 /* RadioStation.swift */; };
@@ -56,6 +54,9 @@
94D260981B45E8B800DE671C /* Track.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D260971B45E8B800DE671C /* Track.swift */; };
94D30EA71AD07A880024FE96 /* StationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D30EA61AD07A880024FE96 /* StationTableViewCell.swift */; };
94E9761C1B1A8F3200F52B1E /* UIImage+DropShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9761B1B1A8F3200F52B1E /* UIImage+DropShadow.swift */; };
+ CAA7C15D1FD77F3A003CABDF /* FRadioAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA7C15B1FD77F3A003CABDF /* FRadioAPI.swift */; };
+ CAA7C15E1FD77F3A003CABDF /* FRadioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA7C15C1FD77F3A003CABDF /* FRadioPlayer.swift */; };
+ CAA8FDB52000614600050F77 /* RadioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA8FDB42000614600050F77 /* RadioPlayer.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -72,7 +73,6 @@
2C5545BA1C1124DE00728469 /* SwiftRadioUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftRadioUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
2C5545BC1C1124DE00728469 /* SwiftRadioUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftRadioUITests.swift; sourceTree = ""; };
2C5545BE1C1124DE00728469 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- 5F22B9DF1F72ABEF00CB5911 /* SwiftyJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftyJSON.swift; sourceTree = ""; };
5F22BA1E1F72AD3700CB5911 /* SpringLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpringLabel.swift; sourceTree = ""; };
5F22BA1F1F72AD3900CB5911 /* BlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurView.swift; sourceTree = ""; };
5F22BA201F72AD3B00CB5911 /* DesignableTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignableTabBarController.swift; sourceTree = ""; };
@@ -115,7 +115,6 @@
94452E541AD7086800BFE7A5 /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; };
945DB3C11AD58E3A00495EBB /* stations.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = stations.json; sourceTree = ""; };
945DB3C41AD5A6E200495EBB /* NothingFoundCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NothingFoundCell.xib; sourceTree = ""; };
- 94817DFA1B547D5700D3FA23 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; };
949BBB3F1ACC9DEE005B7C26 /* DataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; };
949E5EAF1ACB340200AB6280 /* UIImageView+Download.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+Download.swift"; sourceTree = ""; };
94AC70AD1AD05C6200652982 /* RadioStation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioStation.swift; sourceTree = ""; };
@@ -126,6 +125,9 @@
94D30EA61AD07A880024FE96 /* StationTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StationTableViewCell.swift; sourceTree = ""; };
94E9761B1B1A8F3200F52B1E /* UIImage+DropShadow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+DropShadow.swift"; sourceTree = ""; };
B90086461BBE40AF00E5372C /* MediaPlayer.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaPlayer.framework; path = System/Library/Frameworks/MediaPlayer.framework; sourceTree = SDKROOT; };
+ CAA7C15B1FD77F3A003CABDF /* FRadioAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FRadioAPI.swift; sourceTree = ""; };
+ CAA7C15C1FD77F3A003CABDF /* FRadioPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FRadioPlayer.swift; sourceTree = ""; };
+ CAA8FDB42000614600050F77 /* RadioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioPlayer.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -158,7 +160,7 @@
5F22B9DE1F72A8D800CB5911 /* Libraries */ = {
isa = PBXGroup;
children = (
- 5F22B9DF1F72ABEF00CB5911 /* SwiftyJSON.swift */,
+ CAA7C15A1FD77F3A003CABDF /* FRadioPlayer */,
5F22BA1D1F72ACF100CB5911 /* Spring */,
);
path = Libraries;
@@ -223,12 +225,14 @@
9409E1181ABF6FEA00312E2B /* SwiftRadio */ = {
isa = PBXGroup;
children = (
- 94D2608E1B45D0F800DE671C /* ViewControllers */,
94D2608F1B45D11800DE671C /* Cells */,
+ 94AC70AF1AD05C7400652982 /* Networking */,
94823F861B5576F4004EC711 /* Data */,
94E976191B1A8BFB00F52B1E /* Model */,
- 94AC70AF1AD05C7400652982 /* Networking */,
+ 94D2608E1B45D0F800DE671C /* ViewControllers */,
9409E11B1ABF6FEA00312E2B /* AppDelegate.swift */,
+ CAA8FDB42000614600050F77 /* RadioPlayer.swift */,
+ 94D260901B45D20000DE671C /* SwiftRadio-Settings.swift */,
9409E1221ABF6FEA00312E2B /* Main.storyboard */,
9409E1251ABF6FEA00312E2B /* Images.xcassets */,
9409E1191ABF6FEA00312E2B /* Supporting Files */,
@@ -242,7 +246,6 @@
isa = PBXGroup;
children = (
9409E11A1ABF6FEA00312E2B /* Info.plist */,
- 94D260901B45D20000DE671C /* SwiftRadio-Settings.swift */,
5FDEE0211F72FF980064333C /* LaunchScreen.storyboard */,
);
name = "Supporting Files";
@@ -269,8 +272,8 @@
children = (
94452E541AD7086800BFE7A5 /* AboutViewController.swift */,
94D1D0A41AD6D6230022CA11 /* InfoDetailViewController.swift */,
- 9409E13F1ABF78B000312E2B /* NowPlayingViewController.swift */,
94452E4E1AD6F24700BFE7A5 /* PopUpMenuViewController.swift */,
+ 9409E13F1ABF78B000312E2B /* NowPlayingViewController.swift */,
942A3F361AE43DF80011396E /* StationsViewController.swift */,
);
name = ViewControllers;
@@ -299,12 +302,20 @@
isa = PBXGroup;
children = (
94AC70AD1AD05C6200652982 /* RadioStation.swift */,
- 94817DFA1B547D5700D3FA23 /* Player.swift */,
94D260971B45E8B800DE671C /* Track.swift */,
);
name = Model;
sourceTree = "";
};
+ CAA7C15A1FD77F3A003CABDF /* FRadioPlayer */ = {
+ isa = PBXGroup;
+ children = (
+ CAA7C15B1FD77F3A003CABDF /* FRadioAPI.swift */,
+ CAA7C15C1FD77F3A003CABDF /* FRadioPlayer.swift */,
+ );
+ path = FRadioPlayer;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -361,7 +372,7 @@
};
9409E1151ABF6FEA00312E2B = {
CreatedOnToolsVersion = 6.2;
- DevelopmentTeam = G24WJ3XCZ3;
+ DevelopmentTeam = G24WJ3XCZ3;
LastSwiftMigration = 0900;
SystemCapabilities = {
com.apple.BackgroundModes = {
@@ -430,7 +441,6 @@
5F22BA401F72AD5A00CB5911 /* SpringImageView.swift in Sources */,
5F22BA511F72AD5A00CB5911 /* TransitionZoom.swift in Sources */,
5F22BA471F72AD5A00CB5911 /* DesignableTextField.swift in Sources */,
- 5F22B9E01F72ABEF00CB5911 /* SwiftyJSON.swift in Sources */,
5F22BA4C1F72AD5A00CB5911 /* ImageLoader.swift in Sources */,
5F22BA451F72AD5A00CB5911 /* DesignableLabel.swift in Sources */,
94D260911B45D20000DE671C /* SwiftRadio-Settings.swift in Sources */,
@@ -452,7 +462,9 @@
94452E551AD7086800BFE7A5 /* AboutViewController.swift in Sources */,
5F22BA551F72AD5A00CB5911 /* AutoTextView.swift in Sources */,
94D260981B45E8B800DE671C /* Track.swift in Sources */,
+ CAA7C15E1FD77F3A003CABDF /* FRadioPlayer.swift in Sources */,
5F22BA3F1F72AD5A00CB5911 /* LoadingView.swift in Sources */,
+ CAA8FDB52000614600050F77 /* RadioPlayer.swift in Sources */,
5F22BA4F1F72AD5A00CB5911 /* TransitionManager.swift in Sources */,
5F22BA531F72AD5A00CB5911 /* SpringAnimation.swift in Sources */,
94452E4F1AD6F24700BFE7A5 /* PopUpMenuViewController.swift in Sources */,
@@ -466,7 +478,7 @@
942A3F371AE43DF80011396E /* StationsViewController.swift in Sources */,
5F22BA501F72AD5A00CB5911 /* Spring.swift in Sources */,
94AC70AE1AD05C6200652982 /* RadioStation.swift in Sources */,
- 94817DFB1B547D5700D3FA23 /* Player.swift in Sources */,
+ CAA7C15D1FD77F3A003CABDF /* FRadioAPI.swift in Sources */,
5F22BA481F72AD5A00CB5911 /* DesignableButton.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
diff --git a/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/UserInterfaceState.xcuserstate b/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/UserInterfaceState.xcuserstate
index 268f2397..baf5b2e6 100644
Binary files a/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/UserInterfaceState.xcuserstate and b/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/SwiftRadio/AboutViewController.swift b/SwiftRadio/AboutViewController.swift
index d87d9a10..81bfdc32 100755
--- a/SwiftRadio/AboutViewController.swift
+++ b/SwiftRadio/AboutViewController.swift
@@ -33,18 +33,16 @@ class AboutViewController: UIViewController {
let configuredMailComposeViewController = configureMailComposeViewController(recepients: receipients, subject: subject, messageBody: messageBody)
if canSendMail() {
- self.present(configuredMailComposeViewController, animated: true, completion: nil)
+ present(configuredMailComposeViewController, animated: true, completion: nil)
} else {
showSendMailErrorAlert()
}
}
@IBAction func websiteButtonDidTouch(_ sender: UIButton) {
-
// Use your own website here
- if let url = URL(string: "http://matthewfecher.com") {
- UIApplication.shared.openURL(url)
- }
+ guard let url = URL(string: "http://matthewfecher.com") else { return }
+ UIApplication.shared.openURL(url)
}
}
diff --git a/SwiftRadio/AppDelegate.swift b/SwiftRadio/AppDelegate.swift
index 87b5534c..e4701e1c 100755
--- a/SwiftRadio/AppDelegate.swift
+++ b/SwiftRadio/AppDelegate.swift
@@ -12,6 +12,7 @@ import UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
+ weak var stationsViewController: StationsViewController?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
@@ -21,6 +22,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Make status bar white
UINavigationBar.appearance().barStyle = .black
+ // FRadioPlayer config
+ FRadioPlayer.shared.artworkSize = 600
+
+ // Get weak ref of StationsViewController
+ if let navigationController = window?.rootViewController as? UINavigationController {
+ stationsViewController = navigationController.viewControllers.first as? StationsViewController
+ }
+
return true
}
@@ -55,7 +64,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
UIApplication.shared.endReceivingRemoteControlEvents()
}
+
+ // MARK: - Remote Controls
-
+ override func remoteControlReceived(with event: UIEvent?) {
+ super.remoteControlReceived(with: event)
+
+ guard let event = event, event.type == UIEventType.remoteControl else { return }
+
+ switch event.subtype {
+ case .remoteControlPlay:
+ FRadioPlayer.shared.play()
+ case .remoteControlPause:
+ FRadioPlayer.shared.pause()
+ case .remoteControlTogglePlayPause:
+ FRadioPlayer.shared.togglePlaying()
+ case .remoteControlNextTrack:
+ stationsViewController?.didPressNextButton()
+ case .remoteControlPreviousTrack:
+ stationsViewController?.didPressPreviousButton()
+ default:
+ break
+ }
+ }
}
diff --git a/SwiftRadio/Base.lproj/Main.storyboard b/SwiftRadio/Base.lproj/Main.storyboard
index 570c890b..b2367972 100755
--- a/SwiftRadio/Base.lproj/Main.storyboard
+++ b/SwiftRadio/Base.lproj/Main.storyboard
@@ -1,20 +1,18 @@
-
-
+
+
-
+
- AvenirNext-Medium
AvenirNext-Regular
- AvenirNext-UltraLight
@@ -23,42 +21,55 @@
-
+
-
+
-
+
-
-
+
+
-
+
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -74,40 +85,40 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
@@ -150,23 +161,23 @@
-
+
-
+
-
+
-
+
-
+
-
+
@@ -177,10 +188,10 @@
-
-
+
+
-
+
@@ -188,7 +199,7 @@
-
+
@@ -211,7 +222,7 @@
-
+
@@ -276,72 +287,135 @@
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
-
-
+
+
-
+
+
-
+
-
+
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -353,23 +427,8 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -377,102 +436,66 @@
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
-
-
-
+
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+
@@ -482,8 +505,9 @@
-
-
+
+
+
@@ -492,7 +516,7 @@
-
+
@@ -502,12 +526,12 @@
-
+
-
+
@@ -525,47 +549,58 @@
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
-
+
-
+
-
+
@@ -573,35 +608,23 @@
-
-
-
-
-
-
-
+
-
+
-
-
-
-
-
-
+
-
-
+
@@ -623,120 +646,123 @@
-
+
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
FEATURES:
+ Displays Artist, Track and Album/Station Art on lock screen.
+ Background Audio performance
-+Last FM API integration to automatically download album art
++iTunes API integration to automatically download album art
+ Loads and parses Icecast metadata (i.e. artist & track names)
+ Ability to update stations from server without resubmitting to the app store.
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
+
-
+
@@ -752,10 +778,13 @@
+
+
+
-
+
diff --git a/SwiftRadio/DataManager.swift b/SwiftRadio/DataManager.swift
index 09d083e5..7bb9edae 100755
--- a/SwiftRadio/DataManager.swift
+++ b/SwiftRadio/DataManager.swift
@@ -8,13 +8,13 @@
import UIKit
-class DataManager {
+struct DataManager {
//*****************************************************************
- // Helper class to get either local or remote JSON
+ // Helper struct to get either local or remote JSON
//*****************************************************************
- class func getStationDataWithSuccess(success: @escaping ((_ metaData: Data?) -> Void)) {
+ static func getStationDataWithSuccess(success: @escaping ((_ metaData: Data?) -> Void)) {
DispatchQueue.global(qos: .userInitiated).async {
if useLocalStations {
@@ -22,10 +22,14 @@ class DataManager {
success(data)
}
} else {
- loadDataFromURL(url: URL(string: stationDataURL)!) { data, error in
- if let urlData = data {
- success(urlData)
- }
+ guard let stationDataURL = URL(string: stationDataURL) else {
+ if kDebugLog { print("stationDataURL not a valid URL") }
+ success(nil)
+ return
+ }
+
+ loadDataFromURL(url: stationDataURL) { data, error in
+ success(data)
}
}
}
@@ -35,34 +39,18 @@ class DataManager {
// Load local JSON Data
//*****************************************************************
- class func getDataFromFileWithSuccess(success: (_ data: Data) -> Void) {
-
- if let filePath = Bundle.main.path(forResource: "stations", ofType:"json") {
- do {
- let data = try NSData(contentsOfFile:filePath,
- options: NSData.ReadingOptions.uncached) as Data
- success(data)
- } catch {
- fatalError()
- }
- } else {
- print("The local JSON file could not be found")
+ static func getDataFromFileWithSuccess(success: (_ data: Data?) -> Void) {
+ guard let filePathURL = Bundle.main.url(forResource: "stations", withExtension: "json") else {
+ if kDebugLog { print("The local JSON file could not be found") }
+ success(nil)
+ return
}
- }
-
- //*****************************************************************
- // Get LastFM/iTunes Data
- //*****************************************************************
-
- class func getTrackDataWithSuccess(queryURL: String, success: @escaping ((_ metaData: Data?) -> Void)) {
-
- loadDataFromURL(url: URL(string: queryURL)!) { data, _ in
- // Return Data
- if let urlData = data {
- success(urlData)
- } else {
- if kDebugLog { print("API TIMEOUT OR ERROR") }
- }
+
+ do {
+ let data = try Data(contentsOf: filePathURL, options: .uncached)
+ success(data)
+ } catch {
+ fatalError()
}
}
@@ -70,40 +58,39 @@ class DataManager {
// REUSABLE DATA/API CALL METHOD
//*****************************************************************
- class func loadDataFromURL(url: URL, completion:@escaping (_ data: Data?, _ error: Error?) -> Void) {
+ static func loadDataFromURL(url: URL, completion: @escaping (_ data: Data?, _ error: Error?) -> Void) {
let sessionConfig = URLSessionConfiguration.default
- sessionConfig.allowsCellularAccess = true
- sessionConfig.timeoutIntervalForRequest = 15
- sessionConfig.timeoutIntervalForResource = 30
+ sessionConfig.allowsCellularAccess = true
+ sessionConfig.timeoutIntervalForRequest = 15
+ sessionConfig.timeoutIntervalForResource = 30
sessionConfig.httpMaximumConnectionsPerHost = 1
let session = URLSession(configuration: sessionConfig)
- // Use NSURLSession to get data from an NSURL
- let loadDataTask = session.dataTask(with: url){ data, response, error in
- if let responseError = error {
- completion(nil, responseError)
-
+ // Use URLSession to get data from an NSURL
+ let loadDataTask = session.dataTask(with: url) { data, response, error in
+
+ guard error == nil else {
+ completion(nil, error!)
if kDebugLog { print("API ERROR: \(error!)") }
-
- // Stop activity Indicator
- UIApplication.shared.isNetworkActivityIndicatorVisible = false
-
- } else if let httpResponse = response as? HTTPURLResponse {
- if httpResponse.statusCode != 200 {
- let statusError = NSError(domain:"com.matthewfecher", code:httpResponse.statusCode, userInfo:[NSLocalizedDescriptionKey : "HTTP status code has unexpected value."])
-
- if kDebugLog { print("API: HTTP status code has unexpected value") }
-
- completion(nil, statusError)
-
- } else {
-
- // Success, return data
- completion(data, nil)
- }
+ return
+ }
+
+ guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else {
+ completion(nil, nil)
+ if kDebugLog { print("API: HTTP status code has unexpected value") }
+ return
+ }
+
+ guard let data = data else {
+ completion(nil, nil)
+ if kDebugLog { print("API: No data received") }
+ return
}
+
+ // Success, return data
+ completion(data, nil)
}
loadDataTask.resume()
diff --git a/SwiftRadio/Images.xcassets/AppIcon.appiconset/Contents.json b/SwiftRadio/Images.xcassets/AppIcon.appiconset/Contents.json
index 1f9166b3..288f708d 100755
--- a/SwiftRadio/Images.xcassets/AppIcon.appiconset/Contents.json
+++ b/SwiftRadio/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -96,6 +96,12 @@
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "SWIFT-RADIO.png",
+ "scale" : "1x"
}
],
"info" : {
diff --git a/SwiftRadio/Images.xcassets/AppIcon.appiconset/SWIFT-RADIO.png b/SwiftRadio/Images.xcassets/AppIcon.appiconset/SWIFT-RADIO.png
new file mode 100644
index 00000000..faaf0313
Binary files /dev/null and b/SwiftRadio/Images.xcassets/AppIcon.appiconset/SWIFT-RADIO.png differ
diff --git a/SwiftRadio/Images.xcassets/Stations/Contents.json b/SwiftRadio/Images.xcassets/Stations/Contents.json
new file mode 100644
index 00000000..da4a164c
--- /dev/null
+++ b/SwiftRadio/Images.xcassets/Stations/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/SwiftRadio/Images.xcassets/station-80s.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-80s.imageset/Contents.json
old mode 100755
new mode 100644
similarity index 100%
rename from SwiftRadio/Images.xcassets/station-80s.imageset/Contents.json
rename to SwiftRadio/Images.xcassets/Stations/station-80s.imageset/Contents.json
diff --git a/SwiftRadio/Images.xcassets/station-80s.imageset/station-80s.png b/SwiftRadio/Images.xcassets/Stations/station-80s.imageset/station-80s.png
similarity index 100%
rename from SwiftRadio/Images.xcassets/station-80s.imageset/station-80s.png
rename to SwiftRadio/Images.xcassets/Stations/station-80s.imageset/station-80s.png
diff --git a/SwiftRadio/Images.xcassets/stationImage.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/Contents.json
old mode 100755
new mode 100644
similarity index 52%
rename from SwiftRadio/Images.xcassets/stationImage.imageset/Contents.json
rename to SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/Contents.json
index 10ff3cff..1683cdd5
--- a/SwiftRadio/Images.xcassets/stationImage.imageset/Contents.json
+++ b/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/Contents.json
@@ -2,18 +2,17 @@
"images" : [
{
"idiom" : "universal",
- "scale" : "1x",
- "filename" : "stationImage.png"
+ "filename" : "station-absolutecountry.png",
+ "scale" : "1x"
},
{
"idiom" : "universal",
- "scale" : "2x",
- "filename" : "stationImage@2x.png"
+ "filename" : "station-absolutecountry@2x.png",
+ "scale" : "2x"
},
{
"idiom" : "universal",
- "scale" : "3x",
- "filename" : "stationImage@3x.png"
+ "scale" : "3x"
}
],
"info" : {
diff --git a/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry.png b/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry.png
new file mode 100644
index 00000000..c51a1daf
Binary files /dev/null and b/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry.png differ
diff --git a/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry@2x.png b/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry@2x.png
new file mode 100644
index 00000000..cb24e297
Binary files /dev/null and b/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry@2x.png differ
diff --git a/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/Contents.json
new file mode 100644
index 00000000..4eccd087
--- /dev/null
+++ b/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/Contents.json
@@ -0,0 +1,22 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "station-altvault.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "station-altvault@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault.png b/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault.png
new file mode 100644
index 00000000..5b36eba6
Binary files /dev/null and b/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault.png differ
diff --git a/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault@2x.png b/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault@2x.png
new file mode 100644
index 00000000..169abb6c
Binary files /dev/null and b/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault@2x.png differ
diff --git a/SwiftRadio/Images.xcassets/station-classicrock.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/Contents.json
similarity index 100%
rename from SwiftRadio/Images.xcassets/station-classicrock.imageset/Contents.json
rename to SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/Contents.json
diff --git a/SwiftRadio/Images.xcassets/station-classicrock.imageset/station-classicrock.png b/SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/station-classicrock.png
similarity index 100%
rename from SwiftRadio/Images.xcassets/station-classicrock.imageset/station-classicrock.png
rename to SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/station-classicrock.png
diff --git a/SwiftRadio/Images.xcassets/station-killrockstars.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/Contents.json
old mode 100755
new mode 100644
similarity index 78%
rename from SwiftRadio/Images.xcassets/station-killrockstars.imageset/Contents.json
rename to SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/Contents.json
index b54b1b72..9cbd87ca
--- a/SwiftRadio/Images.xcassets/station-killrockstars.imageset/Contents.json
+++ b/SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/Contents.json
@@ -2,8 +2,8 @@
"images" : [
{
"idiom" : "universal",
- "scale" : "1x",
- "filename" : "station-killrockstars.png"
+ "filename" : "station-killrockstars.png",
+ "scale" : "1x"
},
{
"idiom" : "universal",
diff --git a/SwiftRadio/Images.xcassets/station-killrockstars.imageset/station-killrockstars.png b/SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/station-killrockstars.png
old mode 100755
new mode 100644
similarity index 100%
rename from SwiftRadio/Images.xcassets/station-killrockstars.imageset/station-killrockstars.png
rename to SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/station-killrockstars.png
diff --git a/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/Contents.json
new file mode 100644
index 00000000..c2522a7f
--- /dev/null
+++ b/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/Contents.json
@@ -0,0 +1,22 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "station-newportfolk.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "station-newportfolk@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk.png b/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk.png
new file mode 100644
index 00000000..e13f92e6
Binary files /dev/null and b/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk.png differ
diff --git a/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk@2x.png b/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk@2x.png
new file mode 100644
index 00000000..5ec140ac
Binary files /dev/null and b/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk@2x.png differ
diff --git a/SwiftRadio/Images.xcassets/station-spaceland.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/Contents.json
old mode 100755
new mode 100644
similarity index 100%
rename from SwiftRadio/Images.xcassets/station-spaceland.imageset/Contents.json
rename to SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/Contents.json
diff --git a/SwiftRadio/Images.xcassets/station-spaceland.imageset/station-spaceland.png b/SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/station-spaceland.png
old mode 100755
new mode 100644
similarity index 100%
rename from SwiftRadio/Images.xcassets/station-spaceland.imageset/station-spaceland.png
rename to SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/station-spaceland.png
diff --git a/SwiftRadio/Images.xcassets/station-sub.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-sub.imageset/Contents.json
similarity index 100%
rename from SwiftRadio/Images.xcassets/station-sub.imageset/Contents.json
rename to SwiftRadio/Images.xcassets/Stations/station-sub.imageset/Contents.json
diff --git a/SwiftRadio/Images.xcassets/station-sub.imageset/sub.png b/SwiftRadio/Images.xcassets/Stations/station-sub.imageset/sub.png
similarity index 100%
rename from SwiftRadio/Images.xcassets/station-sub.imageset/sub.png
rename to SwiftRadio/Images.xcassets/Stations/station-sub.imageset/sub.png
diff --git a/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/Contents.json
new file mode 100644
index 00000000..2aba6f47
--- /dev/null
+++ b/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "stationImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "stationImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "stationImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/SwiftRadio/Images.xcassets/stationImage.imageset/stationImage.png b/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage.png
old mode 100755
new mode 100644
similarity index 100%
rename from SwiftRadio/Images.xcassets/stationImage.imageset/stationImage.png
rename to SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage.png
diff --git a/SwiftRadio/Images.xcassets/stationImage.imageset/stationImage@2x.png b/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@2x.png
old mode 100755
new mode 100644
similarity index 100%
rename from SwiftRadio/Images.xcassets/stationImage.imageset/stationImage@2x.png
rename to SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@2x.png
diff --git a/SwiftRadio/Images.xcassets/stationImage.imageset/stationImage@3x.png b/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@3x.png
old mode 100755
new mode 100644
similarity index 100%
rename from SwiftRadio/Images.xcassets/stationImage.imageset/stationImage@3x.png
rename to SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@3x.png
diff --git a/SwiftRadio/Images.xcassets/btn-next.imageset/Contents.json b/SwiftRadio/Images.xcassets/btn-next.imageset/Contents.json
new file mode 100644
index 00000000..4eeebe0c
--- /dev/null
+++ b/SwiftRadio/Images.xcassets/btn-next.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "btn-next.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "btn-next@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "btn-next@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next.png b/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next.png
new file mode 100644
index 00000000..125342fb
Binary files /dev/null and b/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next.png differ
diff --git a/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@2x.png b/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@2x.png
new file mode 100644
index 00000000..1239d224
Binary files /dev/null and b/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@2x.png differ
diff --git a/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@3x.png b/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@3x.png
new file mode 100644
index 00000000..67f3b305
Binary files /dev/null and b/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@3x.png differ
diff --git a/SwiftRadio/Images.xcassets/btn-previous.imageset/Contents.json b/SwiftRadio/Images.xcassets/btn-previous.imageset/Contents.json
new file mode 100644
index 00000000..99940c7a
--- /dev/null
+++ b/SwiftRadio/Images.xcassets/btn-previous.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "btn-previous.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "btn-previous@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "btn-previous@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous.png b/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous.png
new file mode 100644
index 00000000..18bac56e
Binary files /dev/null and b/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous.png differ
diff --git a/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@2x.png b/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@2x.png
new file mode 100644
index 00000000..675c22ad
Binary files /dev/null and b/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@2x.png differ
diff --git a/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@3x.png b/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@3x.png
new file mode 100644
index 00000000..cc434f30
Binary files /dev/null and b/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@3x.png differ
diff --git a/SwiftRadio/Images.xcassets/btn-stop.imageset/Contents.json b/SwiftRadio/Images.xcassets/btn-stop.imageset/Contents.json
new file mode 100644
index 00000000..2f68befe
--- /dev/null
+++ b/SwiftRadio/Images.xcassets/btn-stop.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "btn-stop.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "btn-stop@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "btn-stop@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop.png b/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop.png
new file mode 100644
index 00000000..b2e99078
Binary files /dev/null and b/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop.png differ
diff --git a/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@2x.png b/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@2x.png
new file mode 100644
index 00000000..e3372467
Binary files /dev/null and b/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@2x.png differ
diff --git a/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@3x.png b/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@3x.png
new file mode 100644
index 00000000..f408ed73
Binary files /dev/null and b/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@3x.png differ
diff --git a/SwiftRadio/Info.plist b/SwiftRadio/Info.plist
index 47bb3382..e949cea8 100755
--- a/SwiftRadio/Info.plist
+++ b/SwiftRadio/Info.plist
@@ -17,11 +17,11 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.0
+ 2.0
CFBundleSignature
????
CFBundleVersion
- 1.0.2
+ 2
LSRequiresIPhoneOS
NSAppTransportSecurity
diff --git a/SwiftRadio/InfoDetailViewController.swift b/SwiftRadio/InfoDetailViewController.swift
index 92b55633..f421c719 100755
--- a/SwiftRadio/InfoDetailViewController.swift
+++ b/SwiftRadio/InfoDetailViewController.swift
@@ -43,14 +43,14 @@ class InfoDetailViewController: UIViewController {
func setupStationText() {
// Display Station Name & Short Desc
- stationNameLabel.text = currentStation.stationName
- stationDescLabel.text = currentStation.stationDesc
+ stationNameLabel.text = currentStation.name
+ stationDescLabel.text = currentStation.desc
// Display Station Long Desc
- if currentStation.stationLongDesc == "" {
+ if currentStation.longDesc == "" {
loadDefaultText()
} else {
- stationLongDescTextView.text = currentStation.stationLongDesc
+ stationLongDescTextView.text = currentStation.longDesc
}
}
@@ -62,12 +62,12 @@ class InfoDetailViewController: UIViewController {
func setupStationLogo() {
// Display Station Image/Logo
- let imageURL = currentStation.stationImageURL
+ let imageURL = currentStation.imageURL
if imageURL.range(of: "http") != nil {
// Get station image from the web, iOS should cache the image
- if let url = URL(string: currentStation.stationImageURL) {
- downloadTask = stationImageView.loadImageWithURL(url: url) { _ in }
+ if let url = URL(string: currentStation.imageURL) {
+ stationImageView.loadImageWithURL(url: url) { _ in }
}
} else if imageURL != "" {
diff --git a/SwiftRadio/LaunchScreen.storyboard b/SwiftRadio/LaunchScreen.storyboard
index 5af9a1c0..aa39ea3a 100644
--- a/SwiftRadio/LaunchScreen.storyboard
+++ b/SwiftRadio/LaunchScreen.storyboard
@@ -1,11 +1,11 @@
-
+
-
+
@@ -23,6 +23,10 @@
+
+
+
+
diff --git a/SwiftRadio/Libraries/FRadioPlayer/FRadioAPI.swift b/SwiftRadio/Libraries/FRadioPlayer/FRadioAPI.swift
new file mode 100644
index 00000000..a0dbf5d2
--- /dev/null
+++ b/SwiftRadio/Libraries/FRadioPlayer/FRadioAPI.swift
@@ -0,0 +1,81 @@
+//
+// FRadioAPI.swift
+// FRadioPlayerDemo
+//
+// Created by Fethi El Hassasna on 2017-11-25.
+// Copyright © 2017 Fethi El Hassasna. All rights reserved.
+//
+
+import Foundation
+
+// MARK: - iTunes API
+internal struct FRadioAPI {
+
+ // MARK: - Util methods
+
+ static func getArtwork(for metadata: String, size: Int, completionHandler: @escaping (_ artworkURL: URL?) -> ()) {
+
+ guard !metadata.isEmpty, metadata != " - ", let url = getURL(with: metadata) else {
+ completionHandler(nil)
+ return
+ }
+
+ URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
+ guard error == nil, let data = data else {
+ completionHandler(nil)
+ return
+ }
+
+ let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
+
+ guard let parsedResult = json as? [String: Any],
+ let results = parsedResult[Keys.results] as? Array<[String: Any]>,
+ let result = results.first,
+ var artwork = result[Keys.artwork] as? String else {
+ completionHandler(nil)
+ return
+ }
+
+ if size != 100, size > 0 {
+ artwork = artwork.replacingOccurrences(of: "100x100", with: "\(size)x\(size)")
+ }
+
+ let artworkURL = URL(string: artwork)
+ completionHandler(artworkURL)
+ }).resume()
+ }
+
+ private static func getURL(with term: String) -> URL? {
+ var components = URLComponents()
+ components.scheme = Domain.scheme
+ components.host = Domain.host
+ components.path = Domain.path
+ components.queryItems = [URLQueryItem]()
+ components.queryItems?.append(URLQueryItem(name: Keys.term, value: term))
+ components.queryItems?.append(URLQueryItem(name: Keys.entity, value: Values.entity))
+ return components.url
+ }
+
+ // MARK: - Constants
+
+ private struct Domain {
+ static let scheme = "https"
+ static let host = "itunes.apple.com"
+ static let path = "/search"
+ }
+
+ private struct Keys {
+ // Request
+ static let term = "term"
+ static let entity = "entity"
+
+ // Response
+ static let results = "results"
+ static let artwork = "artworkUrl100"
+ }
+
+ private struct Values {
+ static let entity = "song"
+ }
+}
+
diff --git a/SwiftRadio/Libraries/FRadioPlayer/FRadioPlayer.swift b/SwiftRadio/Libraries/FRadioPlayer/FRadioPlayer.swift
new file mode 100644
index 00000000..f3529400
--- /dev/null
+++ b/SwiftRadio/Libraries/FRadioPlayer/FRadioPlayer.swift
@@ -0,0 +1,483 @@
+//
+// FRadioPlayer.swift
+// FRadioPlayerDemo
+//
+// Created by Fethi El Hassasna on 2017-11-11.
+// Copyright © 2017 Fethi El Hassasna. All rights reserved.
+//
+
+import AVFoundation
+
+// MARK: - FRadioPlayingState
+
+/**
+ `FRadioPlayingState` is the Player playing state enum
+ */
+
+@objc public enum FRadioPlaybackState: Int {
+
+ /// Player is playing
+ case playing
+
+ /// Player is paused
+ case paused
+
+ /// Player is stopped
+ case stopped
+
+ /// Return a readable description
+ public var description: String {
+ switch self {
+ case .playing: return "Player is playing"
+ case .paused: return "Player is paused"
+ case .stopped: return "Player is stopped"
+ }
+ }
+}
+
+// MARK: - FRadioPlayerState
+
+/**
+ `FRadioPlayerState` is the Player status enum
+ */
+
+@objc public enum FRadioPlayerState: Int {
+
+ /// URL not set
+ case urlNotSet
+
+ /// Player is ready to play
+ case readyToPlay
+
+ /// Player is loading
+ case loading
+
+ /// The loading has finished
+ case loadingFinished
+
+ /// Error with playing
+ case error
+
+ /// Return a readable description
+ public var description: String {
+ switch self {
+ case .urlNotSet: return "URL is not set"
+ case .readyToPlay: return "Ready to play"
+ case .loading: return "Loading"
+ case .loadingFinished: return "Loading finished"
+ case .error: return "Error"
+ }
+ }
+}
+
+// MARK: - FRadioPlayerDelegate
+
+/**
+ The `FRadioPlayerDelegate` protocol defines methods you can implement to respond to playback events associated with an `FRadioPlayer` object.
+ */
+
+@objc public protocol FRadioPlayerDelegate: class {
+ /**
+ Called when player changes state
+
+ - parameter player: FRadioPlayer
+ - parameter state: FRadioPlayerState
+ */
+ func radioPlayer(_ player: FRadioPlayer, playerStateDidChange state: FRadioPlayerState)
+
+ /**
+ Called when the player changes the playing state
+
+ - parameter player: FRadioPlayer
+ - parameter state: FRadioPlaybackState
+ */
+ func radioPlayer(_ player: FRadioPlayer, playbackStateDidChange state: FRadioPlaybackState)
+
+ /**
+ Called when player changes the current player item
+
+ - parameter player: FRadioPlayer
+ - parameter url: Radio URL
+ */
+ @objc optional func radioPlayer(_ player: FRadioPlayer, itemDidChange url: URL?)
+
+ /**
+ Called when player item changes the timed metadata value, it uses (separatedBy: " - ") to get the artist/song name, if you want more control over the raw metadata, consider using `metadataDidChange rawValue` instead
+
+ - parameter player: FRadioPlayer
+ - parameter artistName: The artist name
+ - parameter trackName: The track name
+ */
+ @objc optional func radioPlayer(_ player: FRadioPlayer, metadataDidChange artistName: String?, trackName: String?)
+
+ /**
+ Called when player item changes the timed metadata value
+
+ - parameter player: FRadioPlayer
+ - parameter rawValue: metadata raw value
+ */
+ @objc optional func radioPlayer(_ player: FRadioPlayer, metadataDidChange rawValue: String?)
+
+ /**
+ Called when the player gets the artwork for the playing song
+
+ - parameter player: FRadioPlayer
+ - parameter artworkURL: URL for the artwork from iTunes
+ */
+ @objc optional func radioPlayer(_ player: FRadioPlayer, artworkDidChange artworkURL: URL?)
+}
+
+// MARK: - FRadioPlayer
+
+/**
+ FRadioPlayer is a wrapper around AVPlayer to handle internet radio playback.
+ */
+
+open class FRadioPlayer: NSObject {
+
+ // MARK: - Properties
+
+ /// Returns the singleton `FRadioPlayer` instance.
+ open static let shared = FRadioPlayer()
+
+ /**
+ The delegate object for the `FRadioPlayer`.
+ Implement the methods declared by the `FRadioPlayerDelegate` object to respond to user interactions and the player output.
+ */
+ open weak var delegate: FRadioPlayerDelegate?
+
+ /// The player current radio URL
+ open var radioURL: URL? {
+ didSet {
+ radioURLDidChange(with: radioURL)
+ }
+ }
+
+ /// The player starts playing when the radioURL property gets set. (default == true)
+ open var isAutoPlay = true
+
+ /// Enable fetching albums artwork from the iTunes API. (default == true)
+ open var enableArtwork = true
+
+ /// Artwork image size. (default == 100 | 100x100)
+ open var artworkSize = 100
+
+ /// Read only property to get the current AVPlayer rate.
+ open var rate: Float? {
+ return player?.rate
+ }
+
+ /// Check if the player is playing
+ open var isPlaying: Bool {
+ switch playbackState {
+ case .playing:
+ return true
+ case .stopped, .paused:
+ return false
+ }
+ }
+
+ /// Player current state of type `FRadioPlayerState`
+ open private(set) var state = FRadioPlayerState.urlNotSet {
+ didSet {
+ guard oldValue != state else { return }
+ delegate?.radioPlayer(self, playerStateDidChange: state)
+ }
+ }
+
+ /// Playing state of type `FRadioPlaybackState`
+ open private(set) var playbackState = FRadioPlaybackState.stopped {
+ didSet {
+ guard oldValue != playbackState else { return }
+ delegate?.radioPlayer(self, playbackStateDidChange: playbackState)
+ }
+ }
+
+ // MARK: - Private properties
+
+ /// AVPlayer
+ private var player: AVPlayer?
+
+ /// Last player item
+ private var lastPlayerItem: AVPlayerItem?
+
+ /// Check for headphones, used to handle audio route change
+ private var headphonesConnected: Bool = false
+
+ /// Default player item
+ private var playerItem: AVPlayerItem? {
+ didSet {
+ playerItemDidChange()
+ }
+ }
+
+ // MARK: - Initialization
+
+ private override init() {
+ super.init()
+
+ // Enable bluetooth playback
+ let audioSession = AVAudioSession.sharedInstance()
+ try? audioSession.setCategory(AVAudioSessionCategoryPlayback, with: [.defaultToSpeaker, .allowBluetooth])
+
+ // Notifications
+ setupNotifications()
+
+ // Check for headphones
+ checkHeadphonesConnection(outputs: AVAudioSession.sharedInstance().currentRoute.outputs)
+ }
+
+ // MARK: - Control Methods
+
+ /**
+ Trigger the play function of the radio player
+
+ */
+ open func play() {
+ guard let player = player else { return }
+ if player.currentItem == nil, playerItem != nil {
+ player.replaceCurrentItem(with: playerItem)
+ }
+
+ player.play()
+ playbackState = .playing
+ }
+
+ /**
+ Trigger the pause function of the radio player
+
+ */
+ open func pause() {
+ guard let player = player else { return }
+ player.pause()
+ playbackState = .paused
+ }
+
+ /**
+ Trigger the stop function of the radio player
+
+ */
+ open func stop() {
+ guard let player = player else { return }
+ player.replaceCurrentItem(with: nil)
+ timedMetadataDidChange(rawValue: nil)
+ playbackState = .stopped
+ }
+
+ /**
+ Toggle isPlaying state
+
+ */
+ open func togglePlaying() {
+ isPlaying ? pause() : play()
+ }
+
+ // MARK: - Private helpers
+
+ private func radioURLDidChange(with url: URL?) {
+ resetPlayer()
+ guard let url = url else { state = .urlNotSet; return }
+
+ state = .loading
+
+ preparePlayer(with: AVAsset(url: url)) { (success, asset) in
+ guard success, let asset = asset else {
+ self.resetPlayer()
+ self.state = .error
+ return
+ }
+ self.setupPlayer(with: asset)
+ }
+ }
+
+ private func setupPlayer(with asset: AVAsset) {
+ if player == nil {
+ player = AVPlayer()
+ }
+
+ playerItem = AVPlayerItem(asset: asset)
+ }
+
+ /** Reset all player item observers and create new ones
+
+ */
+ private func playerItemDidChange() {
+
+ guard lastPlayerItem != playerItem else { return }
+
+ if let item = lastPlayerItem {
+ pause()
+
+ NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item)
+ item.removeObserver(self, forKeyPath: "status")
+ item.removeObserver(self, forKeyPath: "playbackBufferEmpty")
+ item.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp")
+ item.removeObserver(self, forKeyPath: "timedMetadata")
+ }
+
+ lastPlayerItem = playerItem
+ timedMetadataDidChange(rawValue: nil)
+
+ if let item = playerItem {
+
+ item.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
+ item.addObserver(self, forKeyPath: "playbackBufferEmpty", options: NSKeyValueObservingOptions.new, context: nil)
+ item.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: NSKeyValueObservingOptions.new, context: nil)
+ item.addObserver(self, forKeyPath: "timedMetadata", options: NSKeyValueObservingOptions.new, context: nil)
+
+ player?.replaceCurrentItem(with: item)
+ if isAutoPlay { play() }
+ }
+
+ delegate?.radioPlayer?(self, itemDidChange: radioURL)
+ }
+
+ /** Prepare the player from the passed AVAsset
+
+ */
+ private func preparePlayer(with asset: AVAsset?, completionHandler: @escaping (_ isPlayable: Bool, _ asset: AVAsset?)->()) {
+ guard let asset = asset else {
+ completionHandler(false, nil)
+ return
+ }
+
+ let requestedKey = ["playable"]
+
+ asset.loadValuesAsynchronously(forKeys: requestedKey) {
+
+ DispatchQueue.main.async {
+ var error: NSError?
+
+ let keyStatus = asset.statusOfValue(forKey: "playable", error: &error)
+ if keyStatus == AVKeyValueStatus.failed || !asset.isPlayable {
+ completionHandler(false, nil)
+ return
+ }
+
+ completionHandler(true, asset)
+ }
+ }
+ }
+
+ private func timedMetadataDidChange(rawValue: String?) {
+ let parts = rawValue?.components(separatedBy: " - ")
+ delegate?.radioPlayer?(self, metadataDidChange: parts?.first, trackName: parts?.last)
+ delegate?.radioPlayer?(self, metadataDidChange: rawValue)
+ shouldGetArtwork(for: rawValue, enableArtwork)
+ }
+
+ private func shouldGetArtwork(for rawValue: String?, _ enabled: Bool) {
+ guard enabled else { return }
+ guard let rawValue = rawValue else {
+ self.delegate?.radioPlayer?(self, artworkDidChange: nil)
+ return
+ }
+
+ FRadioAPI.getArtwork(for: rawValue, size: artworkSize, completionHandler: { [unowned self] artworlURL in
+ DispatchQueue.main.async {
+ self.delegate?.radioPlayer?(self, artworkDidChange: artworlURL)
+ }
+ })
+ }
+
+ private func resetPlayer() {
+ stop()
+ playerItem = nil
+ lastPlayerItem = nil
+ player = nil
+ }
+
+ deinit {
+ resetPlayer()
+ NotificationCenter.default.removeObserver(self)
+ }
+
+ // MARK: - Notifications
+
+ private func setupNotifications() {
+ let notificationCenter = NotificationCenter.default
+ notificationCenter.addObserver(self, selector: #selector(handleInterruption), name: .AVAudioSessionInterruption, object: nil)
+ notificationCenter.addObserver(self, selector: #selector(handleRouteChange), name: .AVAudioSessionRouteChange, object: nil)
+ }
+
+ // MARK: - Responding to Interruptions
+
+ @objc private func handleInterruption(notification: Notification) {
+ guard let userInfo = notification.userInfo,
+ let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
+ let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
+ return
+ }
+
+ switch type {
+ case .began:
+ DispatchQueue.main.async { self.pause() }
+ case .ended:
+ guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { break }
+ let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
+ DispatchQueue.main.async { options.contains(.shouldResume) ? self.play() : self.pause() }
+ }
+ }
+
+ // MARK: - Responding to Route Changes
+
+ private func checkHeadphonesConnection(outputs: [AVAudioSessionPortDescription]) {
+ for output in outputs where output.portType == AVAudioSessionPortHeadphones {
+ headphonesConnected = true
+ break
+ }
+ headphonesConnected = false
+ }
+
+ @objc private func handleRouteChange(notification: Notification) {
+ guard let userInfo = notification.userInfo,
+ let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
+ let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else { return }
+
+ switch reason {
+ case .newDeviceAvailable:
+ checkHeadphonesConnection(outputs: AVAudioSession.sharedInstance().currentRoute.outputs)
+ case .oldDeviceUnavailable:
+ guard let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription else { return }
+ checkHeadphonesConnection(outputs: previousRoute.outputs);
+ DispatchQueue.main.async { self.headphonesConnected ? () : self.pause() }
+ default: break
+ }
+ }
+
+ // MARK: - KVO
+
+ /// :nodoc:
+ override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+
+ if let item = object as? AVPlayerItem, let keyPath = keyPath, item == self.playerItem {
+
+ switch keyPath {
+
+ case "status":
+
+ if player?.status == AVPlayerStatus.readyToPlay {
+ self.state = .readyToPlay
+ } else if player?.status == AVPlayerStatus.failed {
+ self.state = .error
+ }
+
+ case "playbackBufferEmpty":
+
+ if item.isPlaybackBufferEmpty { self.state = .loading }
+
+ case "playbackLikelyToKeepUp":
+
+ self.state = item.isPlaybackLikelyToKeepUp ? .loadingFinished : .loading
+
+ case "timedMetadata":
+ let rawValue = item.timedMetadata?.first?.value as? String
+ timedMetadataDidChange(rawValue: rawValue)
+
+ default:
+ break
+ }
+ }
+ }
+}
+
diff --git a/SwiftRadio/Libraries/Spring/Misc.swift b/SwiftRadio/Libraries/Spring/Misc.swift
index b2a27cef..902e7b34 100755
--- a/SwiftRadio/Libraries/Spring/Misc.swift
+++ b/SwiftRadio/Libraries/Spring/Misc.swift
@@ -23,7 +23,7 @@
import UIKit
public extension String {
- public var length: Int { return self.characters.count }
+ public var length: Int { return self.count }
public func toURL() -> NSURL? {
return NSURL(string: self)
@@ -73,7 +73,7 @@ public extension UIColor {
let scanner = Scanner(string: hex)
var hexValue: CUnsignedLongLong = 0
if scanner.scanHexInt64(&hexValue) {
- switch (hex.characters.count) {
+ switch (hex.count) {
case 3:
red = CGFloat((hexValue & 0xF00) >> 8) / 15.0
green = CGFloat((hexValue & 0x0F0) >> 4) / 15.0
diff --git a/SwiftRadio/Libraries/SwiftyJSON.swift b/SwiftRadio/Libraries/SwiftyJSON.swift
deleted file mode 100755
index 9069015a..00000000
--- a/SwiftRadio/Libraries/SwiftyJSON.swift
+++ /dev/null
@@ -1,1453 +0,0 @@
-// SwiftyJSON.swift
-//
-// Copyright (c) 2014 - 2017 Ruoyu Fu, Pinglin Tang
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-// THE SOFTWARE.
-
-import Foundation
-
-// MARK: - Error
-// swiftlint:disable line_length
-///Error domain
-@available(*, deprecated, message: "ErrorDomain is deprecated. Use `SwiftyJSONError.errorDomain` instead.", renamed: "SwiftyJSONError.errorDomain")
-public let ErrorDomain: String = "SwiftyJSONErrorDomain"
-
-///Error code
-@available(*, deprecated, message: "ErrorUnsupportedType is deprecated. Use `SwiftyJSONError.unsupportedType` instead.", renamed: "SwiftyJSONError.unsupportedType")
-public let ErrorUnsupportedType: Int = 999
-@available(*, deprecated, message: "ErrorIndexOutOfBounds is deprecated. Use `SwiftyJSONError.indexOutOfBounds` instead.", renamed: "SwiftyJSONError.indexOutOfBounds")
-public let ErrorIndexOutOfBounds: Int = 900
-@available(*, deprecated, message: "ErrorWrongType is deprecated. Use `SwiftyJSONError.wrongType` instead.", renamed: "SwiftyJSONError.wrongType")
-public let ErrorWrongType: Int = 901
-@available(*, deprecated, message: "ErrorNotExist is deprecated. Use `SwiftyJSONError.notExist` instead.", renamed: "SwiftyJSONError.notExist")
-public let ErrorNotExist: Int = 500
-@available(*, deprecated, message: "ErrorInvalidJSON is deprecated. Use `SwiftyJSONError.invalidJSON` instead.", renamed: "SwiftyJSONError.invalidJSON")
-public let ErrorInvalidJSON: Int = 490
-
-public enum SwiftyJSONError: Int, Swift.Error {
- case unsupportedType = 999
- case indexOutOfBounds = 900
- case elementTooDeep = 902
- case wrongType = 901
- case notExist = 500
- case invalidJSON = 490
-}
-
-extension SwiftyJSONError: CustomNSError {
-
- public static var errorDomain: String { return "com.swiftyjson.SwiftyJSON" }
-
- public var errorCode: Int { return self.rawValue }
-
- public var errorUserInfo: [String : Any] {
- switch self {
- case .unsupportedType:
- return [NSLocalizedDescriptionKey: "It is an unsupported type."]
- case .indexOutOfBounds:
- return [NSLocalizedDescriptionKey: "Array Index is out of bounds."]
- case .wrongType:
- return [NSLocalizedDescriptionKey: "Couldn't merge, because the JSONs differ in type on top level."]
- case .notExist:
- return [NSLocalizedDescriptionKey: "Dictionary key does not exist."]
- case .invalidJSON:
- return [NSLocalizedDescriptionKey: "JSON is invalid."]
- case .elementTooDeep:
- return [NSLocalizedDescriptionKey: "Element too deep. Increase maxObjectDepth and make sure there is no reference loop."]
- }
- }
-}
-
-// MARK: - JSON Type
-
-/**
- JSON's type definitions.
-
- See http://www.json.org
- */
-public enum Type: Int {
- case number
- case string
- case bool
- case array
- case dictionary
- case null
- case unknown
-}
-
-// MARK: - JSON Base
-
-public struct JSON {
-
- /**
- Creates a JSON using the data.
-
- - parameter data: The NSData used to convert to json.Top level object in data is an NSArray or NSDictionary
- - parameter opt: The JSON serialization reading options. `[]` by default.
-
- - returns: The created JSON
- */
- public init(data: Data, options opt: JSONSerialization.ReadingOptions = []) throws {
- let object: Any = try JSONSerialization.jsonObject(with: data, options: opt)
- self.init(jsonObject: object)
- }
-
- /**
- Creates a JSON object
- - note: this does not parse a `String` into JSON, instead use `init(parseJSON: String)`
-
- - parameter object: the object
-
- - returns: the created JSON object
- */
- public init(_ object: Any) {
- switch object {
- case let object as Data:
- do {
- try self.init(data: object)
- } catch {
- self.init(jsonObject: NSNull())
- }
- default:
- self.init(jsonObject: object)
- }
- }
-
- /**
- Parses the JSON string into a JSON object
-
- - parameter json: the JSON string
-
- - returns: the created JSON object
- */
- public init(parseJSON jsonString: String) {
- if let data = jsonString.data(using: .utf8) {
- self.init(data)
- } else {
- self.init(NSNull())
- }
- }
-
- /**
- Creates a JSON from JSON string
-
- - parameter json: Normal json string like '{"a":"b"}'
-
- - returns: The created JSON
- */
- @available(*, deprecated, message: "Use instead `init(parseJSON: )`")
- public static func parse(_ json: String) -> JSON {
- return json.data(using: String.Encoding.utf8)
- .flatMap { try? JSON(data: $0) } ?? JSON(NSNull())
- }
-
- /**
- Creates a JSON using the object.
-
- - parameter jsonObject: The object must have the following properties: All objects are NSString/String, NSNumber/Int/Float/Double/Bool, NSArray/Array, NSDictionary/Dictionary, or NSNull; All dictionary keys are NSStrings/String; NSNumbers are not NaN or infinity.
-
- - returns: The created JSON
- */
- fileprivate init(jsonObject: Any) {
- self.object = jsonObject
- }
-
- /**
- Merges another JSON into this JSON, whereas primitive values which are not present in this JSON are getting added,
- present values getting overwritten, array values getting appended and nested JSONs getting merged the same way.
-
- - parameter other: The JSON which gets merged into this JSON
-
- - throws `ErrorWrongType` if the other JSONs differs in type on the top level.
- */
- public mutating func merge(with other: JSON) throws {
- try self.merge(with: other, typecheck: true)
- }
-
- /**
- Merges another JSON into this JSON and returns a new JSON, whereas primitive values which are not present in this JSON are getting added,
- present values getting overwritten, array values getting appended and nested JSONS getting merged the same way.
-
- - parameter other: The JSON which gets merged into this JSON
-
- - throws `ErrorWrongType` if the other JSONs differs in type on the top level.
-
- - returns: New merged JSON
- */
- public func merged(with other: JSON) throws -> JSON {
- var merged = self
- try merged.merge(with: other, typecheck: true)
- return merged
- }
-
- // Private woker function which does the actual merging
- // Typecheck is set to true for the first recursion level to prevent total override of the source JSON
- fileprivate mutating func merge(with other: JSON, typecheck: Bool) throws {
- if self.type == other.type {
- switch self.type {
- case .dictionary:
- for (key, _) in other {
- try self[key].merge(with: other[key], typecheck: false)
- }
- case .array:
- self = JSON(self.arrayValue + other.arrayValue)
- default:
- self = other
- }
- } else {
- if typecheck {
- throw SwiftyJSONError.wrongType
- } else {
- self = other
- }
- }
- }
-
- /// Private object
- fileprivate var rawArray: [Any] = []
- fileprivate var rawDictionary: [String : Any] = [:]
- fileprivate var rawString: String = ""
- fileprivate var rawNumber: NSNumber = 0
- fileprivate var rawNull: NSNull = NSNull()
- fileprivate var rawBool: Bool = false
-
- /// JSON type, fileprivate setter
- public fileprivate(set) var type: Type = .null
-
- /// Error in JSON, fileprivate setter
- public fileprivate(set) var error: SwiftyJSONError?
-
- /// Object in JSON
- public var object: Any {
- get {
- switch self.type {
- case .array:
- return self.rawArray
- case .dictionary:
- return self.rawDictionary
- case .string:
- return self.rawString
- case .number:
- return self.rawNumber
- case .bool:
- return self.rawBool
- default:
- return self.rawNull
- }
- }
- set {
- error = nil
- switch unwrap(newValue) {
- case let number as NSNumber:
- if number.isBool {
- type = .bool
- self.rawBool = number.boolValue
- } else {
- type = .number
- self.rawNumber = number
- }
- case let string as String:
- type = .string
- self.rawString = string
- case _ as NSNull:
- type = .null
- case nil:
- type = .null
- case let array as [Any]:
- type = .array
- self.rawArray = array
- case let dictionary as [String : Any]:
- type = .dictionary
- self.rawDictionary = dictionary
- default:
- type = .unknown
- error = SwiftyJSONError.unsupportedType
- }
- }
- }
-
- /// The static null JSON
- @available(*, unavailable, renamed:"null")
- public static var nullJSON: JSON { return null }
- public static var null: JSON { return JSON(NSNull()) }
-}
-
-// unwrap nested JSON
-private func unwrap(_ object: Any) -> Any {
- switch object {
- case let json as JSON:
- return unwrap(json.object)
- case let array as [Any]:
- return array.map(unwrap)
- case let dictionary as [String : Any]:
- var unwrappedDic = dictionary
- for (k, v) in dictionary {
- unwrappedDic[k] = unwrap(v)
- }
- return unwrappedDic
- default:
- return object
- }
-}
-
-public enum Index: Comparable {
- case array(Int)
- case dictionary(DictionaryIndex)
- case null
-
- static public func == (lhs: Index, rhs: Index) -> Bool {
- switch (lhs, rhs) {
- case (.array(let left), .array(let right)):
- return left == right
- case (.dictionary(let left), .dictionary(let right)):
- return left == right
- case (.null, .null): return true
- default:
- return false
- }
- }
-
- static public func < (lhs: Index, rhs: Index) -> Bool {
- switch (lhs, rhs) {
- case (.array(let left), .array(let right)):
- return left < right
- case (.dictionary(let left), .dictionary(let right)):
- return left < right
- default:
- return false
- }
- }
-}
-
-public typealias JSONIndex = Index
-public typealias JSONRawIndex = Index
-
-extension JSON: Swift.Collection {
-
- public typealias Index = JSONRawIndex
-
- public var startIndex: Index {
- switch type {
- case .array:
- return .array(rawArray.startIndex)
- case .dictionary:
- return .dictionary(rawDictionary.startIndex)
- default:
- return .null
- }
- }
-
- public var endIndex: Index {
- switch type {
- case .array:
- return .array(rawArray.endIndex)
- case .dictionary:
- return .dictionary(rawDictionary.endIndex)
- default:
- return .null
- }
- }
-
- public func index(after i: Index) -> Index {
- switch i {
- case .array(let idx):
- return .array(rawArray.index(after: idx))
- case .dictionary(let idx):
- return .dictionary(rawDictionary.index(after: idx))
- default:
- return .null
- }
- }
-
- public subscript (position: Index) -> (String, JSON) {
- switch position {
- case .array(let idx):
- return (String(idx), JSON(self.rawArray[idx]))
- case .dictionary(let idx):
- let (key, value) = self.rawDictionary[idx]
- return (key, JSON(value))
- default:
- return ("", JSON.null)
- }
- }
-}
-
-// MARK: - Subscript
-
-/**
- * To mark both String and Int can be used in subscript.
- */
-public enum JSONKey {
- case index(Int)
- case key(String)
-}
-
-public protocol JSONSubscriptType {
- var jsonKey: JSONKey { get }
-}
-
-extension Int: JSONSubscriptType {
- public var jsonKey: JSONKey {
- return JSONKey.index(self)
- }
-}
-
-extension String: JSONSubscriptType {
- public var jsonKey: JSONKey {
- return JSONKey.key(self)
- }
-}
-
-extension JSON {
-
- /// If `type` is `.array`, return json whose object is `array[index]`, otherwise return null json with error.
- fileprivate subscript(index index: Int) -> JSON {
- get {
- if self.type != .array {
- var r = JSON.null
- r.error = self.error ?? SwiftyJSONError.wrongType
- return r
- } else if self.rawArray.indices.contains(index) {
- return JSON(self.rawArray[index])
- } else {
- var r = JSON.null
- r.error = SwiftyJSONError.indexOutOfBounds
- return r
- }
- }
- set {
- if self.type == .array &&
- self.rawArray.indices.contains(index) &&
- newValue.error == nil {
- self.rawArray[index] = newValue.object
- }
- }
- }
-
- /// If `type` is `.dictionary`, return json whose object is `dictionary[key]` , otherwise return null json with error.
- fileprivate subscript(key key: String) -> JSON {
- get {
- var r = JSON.null
- if self.type == .dictionary {
- if let o = self.rawDictionary[key] {
- r = JSON(o)
- } else {
- r.error = SwiftyJSONError.notExist
- }
- } else {
- r.error = self.error ?? SwiftyJSONError.wrongType
- }
- return r
- }
- set {
- if self.type == .dictionary && newValue.error == nil {
- self.rawDictionary[key] = newValue.object
- }
- }
- }
-
- /// If `sub` is `Int`, return `subscript(index:)`; If `sub` is `String`, return `subscript(key:)`.
- fileprivate subscript(sub sub: JSONSubscriptType) -> JSON {
- get {
- switch sub.jsonKey {
- case .index(let index): return self[index: index]
- case .key(let key): return self[key: key]
- }
- }
- set {
- switch sub.jsonKey {
- case .index(let index): self[index: index] = newValue
- case .key(let key): self[key: key] = newValue
- }
- }
- }
-
- /**
- Find a json in the complex data structures by using array of Int and/or String as path.
-
- Example:
-
- ```
- let json = JSON[data]
- let path = [9,"list","person","name"]
- let name = json[path]
- ```
-
- The same as: let name = json[9]["list"]["person"]["name"]
-
- - parameter path: The target json's path.
-
- - returns: Return a json found by the path or a null json with error
- */
- public subscript(path: [JSONSubscriptType]) -> JSON {
- get {
- return path.reduce(self) { $0[sub: $1] }
- }
- set {
- switch path.count {
- case 0:
- return
- case 1:
- self[sub:path[0]].object = newValue.object
- default:
- var aPath = path; aPath.remove(at: 0)
- var nextJSON = self[sub: path[0]]
- nextJSON[aPath] = newValue
- self[sub: path[0]] = nextJSON
- }
- }
- }
-
- /**
- Find a json in the complex data structures by using array of Int and/or String as path.
-
- - parameter path: The target json's path. Example:
-
- let name = json[9,"list","person","name"]
-
- The same as: let name = json[9]["list"]["person"]["name"]
-
- - returns: Return a json found by the path or a null json with error
- */
- public subscript(path: JSONSubscriptType...) -> JSON {
- get {
- return self[path]
- }
- set {
- self[path] = newValue
- }
- }
-}
-
-// MARK: - LiteralConvertible
-
-extension JSON: Swift.ExpressibleByStringLiteral {
-
- public init(stringLiteral value: StringLiteralType) {
- self.init(value as Any)
- }
-
- public init(extendedGraphemeClusterLiteral value: StringLiteralType) {
- self.init(value as Any)
- }
-
- public init(unicodeScalarLiteral value: StringLiteralType) {
- self.init(value as Any)
- }
-}
-
-extension JSON: Swift.ExpressibleByIntegerLiteral {
-
- public init(integerLiteral value: IntegerLiteralType) {
- self.init(value as Any)
- }
-}
-
-extension JSON: Swift.ExpressibleByBooleanLiteral {
-
- public init(booleanLiteral value: BooleanLiteralType) {
- self.init(value as Any)
- }
-}
-
-extension JSON: Swift.ExpressibleByFloatLiteral {
-
- public init(floatLiteral value: FloatLiteralType) {
- self.init(value as Any)
- }
-}
-
-extension JSON: Swift.ExpressibleByDictionaryLiteral {
- public init(dictionaryLiteral elements: (String, Any)...) {
- var dictionary = [String: Any](minimumCapacity: elements.count)
- for (k, v) in elements {
- dictionary[k] = v
- }
- self.init(dictionary as Any)
- }
-}
-
-extension JSON: Swift.ExpressibleByArrayLiteral {
-
- public init(arrayLiteral elements: Any...) {
- self.init(elements as Any)
- }
-}
-
-extension JSON: Swift.ExpressibleByNilLiteral {
-
- @available(*, deprecated, message: "use JSON.null instead. Will be removed in future versions")
- public init(nilLiteral: ()) {
- self.init(NSNull() as Any)
- }
-}
-
-// MARK: - Raw
-
-extension JSON: Swift.RawRepresentable {
-
- public init?(rawValue: Any) {
- if JSON(rawValue).type == .unknown {
- return nil
- } else {
- self.init(rawValue)
- }
- }
-
- public var rawValue: Any {
- return self.object
- }
-
- public func rawData(options opt: JSONSerialization.WritingOptions = JSONSerialization.WritingOptions(rawValue: 0)) throws -> Data {
- guard JSONSerialization.isValidJSONObject(self.object) else {
- throw SwiftyJSONError.invalidJSON
- }
-
- return try JSONSerialization.data(withJSONObject: self.object, options: opt)
- }
-
- public func rawString(_ encoding: String.Encoding = .utf8, options opt: JSONSerialization.WritingOptions = .prettyPrinted) -> String? {
- do {
- return try _rawString(encoding, options: [.jsonSerialization: opt])
- } catch {
- print("Could not serialize object to JSON because:", error.localizedDescription)
- return nil
- }
- }
-
- public func rawString(_ options: [writingOptionsKeys: Any]) -> String? {
- let encoding = options[.encoding] as? String.Encoding ?? String.Encoding.utf8
- let maxObjectDepth = options[.maxObjextDepth] as? Int ?? 10
- do {
- return try _rawString(encoding, options: options, maxObjectDepth: maxObjectDepth)
- } catch {
- print("Could not serialize object to JSON because:", error.localizedDescription)
- return nil
- }
- }
-
- fileprivate func _rawString(_ encoding: String.Encoding = .utf8, options: [writingOptionsKeys: Any], maxObjectDepth: Int = 10) throws -> String? {
- if maxObjectDepth < 0 {
- throw SwiftyJSONError.invalidJSON
- }
- switch self.type {
- case .dictionary:
- do {
- if !(options[.castNilToNSNull] as? Bool ?? false) {
- let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted
- let data = try self.rawData(options: jsonOption)
- return String(data: data, encoding: encoding)
- }
-
- guard let dict = self.object as? [String: Any?] else {
- return nil
- }
- let body = try dict.keys.map { key throws -> String in
- guard let value = dict[key] else {
- return "\"\(key)\": null"
- }
- guard let unwrappedValue = value else {
- return "\"\(key)\": null"
- }
-
- let nestedValue = JSON(unwrappedValue)
- guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else {
- throw SwiftyJSONError.elementTooDeep
- }
- if nestedValue.type == .string {
- return "\"\(key)\": \"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\""
- } else {
- return "\"\(key)\": \(nestedString)"
- }
- }
-
- return "{\(body.joined(separator: ","))}"
- } catch _ {
- return nil
- }
- case .array:
- do {
- if !(options[.castNilToNSNull] as? Bool ?? false) {
- let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted
- let data = try self.rawData(options: jsonOption)
- return String(data: data, encoding: encoding)
- }
-
- guard let array = self.object as? [Any?] else {
- return nil
- }
- let body = try array.map { value throws -> String in
- guard let unwrappedValue = value else {
- return "null"
- }
-
- let nestedValue = JSON(unwrappedValue)
- guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else {
- throw SwiftyJSONError.invalidJSON
- }
- if nestedValue.type == .string {
- return "\"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\""
- } else {
- return nestedString
- }
- }
-
- return "[\(body.joined(separator: ","))]"
- } catch _ {
- return nil
- }
- case .string:
- return self.rawString
- case .number:
- return self.rawNumber.stringValue
- case .bool:
- return self.rawBool.description
- case .null:
- return "null"
- default:
- return nil
- }
- }
-}
-
-// MARK: - Printable, DebugPrintable
-
-extension JSON: Swift.CustomStringConvertible, Swift.CustomDebugStringConvertible {
-
- public var description: String {
- if let string = self.rawString(options:.prettyPrinted) {
- return string
- } else {
- return "unknown"
- }
- }
-
- public var debugDescription: String {
- return description
- }
-}
-
-// MARK: - Array
-
-extension JSON {
-
- //Optional [JSON]
- public var array: [JSON]? {
- if self.type == .array {
- return self.rawArray.map { JSON($0) }
- } else {
- return nil
- }
- }
-
- //Non-optional [JSON]
- public var arrayValue: [JSON] {
- return self.array ?? []
- }
-
- //Optional [Any]
- public var arrayObject: [Any]? {
- get {
- switch self.type {
- case .array:
- return self.rawArray
- default:
- return nil
- }
- }
- set {
- if let array = newValue {
- self.object = array as Any
- } else {
- self.object = NSNull()
- }
- }
- }
-}
-
-// MARK: - Dictionary
-
-extension JSON {
-
- //Optional [String : JSON]
- public var dictionary: [String : JSON]? {
- if self.type == .dictionary {
- var d = [String: JSON](minimumCapacity: rawDictionary.count)
- for (key, value) in rawDictionary {
- d[key] = JSON(value)
- }
- return d
- } else {
- return nil
- }
- }
-
- //Non-optional [String : JSON]
- public var dictionaryValue: [String : JSON] {
- return self.dictionary ?? [:]
- }
-
- //Optional [String : Any]
-
- public var dictionaryObject: [String : Any]? {
- get {
- switch self.type {
- case .dictionary:
- return self.rawDictionary
- default:
- return nil
- }
- }
- set {
- if let v = newValue {
- self.object = v as Any
- } else {
- self.object = NSNull()
- }
- }
- }
-}
-
-// MARK: - Bool
-
-extension JSON { // : Swift.Bool
-
- //Optional bool
- public var bool: Bool? {
- get {
- switch self.type {
- case .bool:
- return self.rawBool
- default:
- return nil
- }
- }
- set {
- if let newValue = newValue {
- self.object = newValue as Bool
- } else {
- self.object = NSNull()
- }
- }
- }
-
- //Non-optional bool
- public var boolValue: Bool {
- get {
- switch self.type {
- case .bool:
- return self.rawBool
- case .number:
- return self.rawNumber.boolValue
- case .string:
- return ["true", "y", "t", "yes", "1"].contains { self.rawString.caseInsensitiveCompare($0) == .orderedSame }
- default:
- return false
- }
- }
- set {
- self.object = newValue
- }
- }
-}
-
-// MARK: - String
-
-extension JSON {
-
- //Optional string
- public var string: String? {
- get {
- switch self.type {
- case .string:
- return self.object as? String
- default:
- return nil
- }
- }
- set {
- if let newValue = newValue {
- self.object = NSString(string:newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- //Non-optional string
- public var stringValue: String {
- get {
- switch self.type {
- case .string:
- return self.object as? String ?? ""
- case .number:
- return self.rawNumber.stringValue
- case .bool:
- return (self.object as? Bool).map { String($0) } ?? ""
- default:
- return ""
- }
- }
- set {
- self.object = NSString(string:newValue)
- }
- }
-}
-
-// MARK: - Number
-
-extension JSON {
-
- //Optional number
- public var number: NSNumber? {
- get {
- switch self.type {
- case .number:
- return self.rawNumber
- case .bool:
- return NSNumber(value: self.rawBool ? 1 : 0)
- default:
- return nil
- }
- }
- set {
- self.object = newValue ?? NSNull()
- }
- }
-
- //Non-optional number
- public var numberValue: NSNumber {
- get {
- switch self.type {
- case .string:
- let decimal = NSDecimalNumber(string: self.object as? String)
- if decimal == NSDecimalNumber.notANumber { // indicates parse error
- return NSDecimalNumber.zero
- }
- return decimal
- case .number:
- return self.object as? NSNumber ?? NSNumber(value: 0)
- case .bool:
- return NSNumber(value: self.rawBool ? 1 : 0)
- default:
- return NSNumber(value: 0.0)
- }
- }
- set {
- self.object = newValue
- }
- }
-}
-
-// MARK: - Null
-
-extension JSON {
-
- public var null: NSNull? {
- get {
- switch self.type {
- case .null:
- return self.rawNull
- default:
- return nil
- }
- }
- set {
- self.object = NSNull()
- }
- }
- public func exists() -> Bool {
- if let errorValue = error, (400...1000).contains(errorValue.errorCode) {
- return false
- }
- return true
- }
-}
-
-// MARK: - URL
-
-extension JSON {
-
- //Optional URL
- public var url: URL? {
- get {
- switch self.type {
- case .string:
- // Check for existing percent escapes first to prevent double-escaping of % character
- if self.rawString.range(of: "%[0-9A-Fa-f]{2}", options: .regularExpression, range: nil, locale: nil) != nil {
- return Foundation.URL(string: self.rawString)
- } else if let encodedString_ = self.rawString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) {
- // We have to use `Foundation.URL` otherwise it conflicts with the variable name.
- return Foundation.URL(string: encodedString_)
- } else {
- return nil
- }
- default:
- return nil
- }
- }
- set {
- self.object = newValue?.absoluteString ?? NSNull()
- }
- }
-}
-
-// MARK: - Int, Double, Float, Int8, Int16, Int32, Int64
-
-extension JSON {
-
- public var double: Double? {
- get {
- return self.number?.doubleValue
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var doubleValue: Double {
- get {
- return self.numberValue.doubleValue
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-
- public var float: Float? {
- get {
- return self.number?.floatValue
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var floatValue: Float {
- get {
- return self.numberValue.floatValue
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-
- public var int: Int? {
- get {
- return self.number?.intValue
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var intValue: Int {
- get {
- return self.numberValue.intValue
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-
- public var uInt: UInt? {
- get {
- return self.number?.uintValue
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var uIntValue: UInt {
- get {
- return self.numberValue.uintValue
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-
- public var int8: Int8? {
- get {
- return self.number?.int8Value
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: Int(newValue))
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var int8Value: Int8 {
- get {
- return self.numberValue.int8Value
- }
- set {
- self.object = NSNumber(value: Int(newValue))
- }
- }
-
- public var uInt8: UInt8? {
- get {
- return self.number?.uint8Value
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var uInt8Value: UInt8 {
- get {
- return self.numberValue.uint8Value
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-
- public var int16: Int16? {
- get {
- return self.number?.int16Value
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var int16Value: Int16 {
- get {
- return self.numberValue.int16Value
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-
- public var uInt16: UInt16? {
- get {
- return self.number?.uint16Value
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var uInt16Value: UInt16 {
- get {
- return self.numberValue.uint16Value
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-
- public var int32: Int32? {
- get {
- return self.number?.int32Value
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var int32Value: Int32 {
- get {
- return self.numberValue.int32Value
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-
- public var uInt32: UInt32? {
- get {
- return self.number?.uint32Value
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var uInt32Value: UInt32 {
- get {
- return self.numberValue.uint32Value
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-
- public var int64: Int64? {
- get {
- return self.number?.int64Value
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var int64Value: Int64 {
- get {
- return self.numberValue.int64Value
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-
- public var uInt64: UInt64? {
- get {
- return self.number?.uint64Value
- }
- set {
- if let newValue = newValue {
- self.object = NSNumber(value: newValue)
- } else {
- self.object = NSNull()
- }
- }
- }
-
- public var uInt64Value: UInt64 {
- get {
- return self.numberValue.uint64Value
- }
- set {
- self.object = NSNumber(value: newValue)
- }
- }
-}
-
-// MARK: - Comparable
-
-extension JSON : Swift.Comparable {}
-
-public func == (lhs: JSON, rhs: JSON) -> Bool {
-
- switch (lhs.type, rhs.type) {
- case (.number, .number):
- return lhs.rawNumber == rhs.rawNumber
- case (.string, .string):
- return lhs.rawString == rhs.rawString
- case (.bool, .bool):
- return lhs.rawBool == rhs.rawBool
- case (.array, .array):
- return lhs.rawArray as NSArray == rhs.rawArray as NSArray
- case (.dictionary, .dictionary):
- return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
- case (.null, .null):
- return true
- default:
- return false
- }
-}
-
-public func <= (lhs: JSON, rhs: JSON) -> Bool {
-
- switch (lhs.type, rhs.type) {
- case (.number, .number):
- return lhs.rawNumber <= rhs.rawNumber
- case (.string, .string):
- return lhs.rawString <= rhs.rawString
- case (.bool, .bool):
- return lhs.rawBool == rhs.rawBool
- case (.array, .array):
- return lhs.rawArray as NSArray == rhs.rawArray as NSArray
- case (.dictionary, .dictionary):
- return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
- case (.null, .null):
- return true
- default:
- return false
- }
-}
-
-public func >= (lhs: JSON, rhs: JSON) -> Bool {
-
- switch (lhs.type, rhs.type) {
- case (.number, .number):
- return lhs.rawNumber >= rhs.rawNumber
- case (.string, .string):
- return lhs.rawString >= rhs.rawString
- case (.bool, .bool):
- return lhs.rawBool == rhs.rawBool
- case (.array, .array):
- return lhs.rawArray as NSArray == rhs.rawArray as NSArray
- case (.dictionary, .dictionary):
- return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
- case (.null, .null):
- return true
- default:
- return false
- }
-}
-
-public func > (lhs: JSON, rhs: JSON) -> Bool {
-
- switch (lhs.type, rhs.type) {
- case (.number, .number):
- return lhs.rawNumber > rhs.rawNumber
- case (.string, .string):
- return lhs.rawString > rhs.rawString
- default:
- return false
- }
-}
-
-public func < (lhs: JSON, rhs: JSON) -> Bool {
-
- switch (lhs.type, rhs.type) {
- case (.number, .number):
- return lhs.rawNumber < rhs.rawNumber
- case (.string, .string):
- return lhs.rawString < rhs.rawString
- default:
- return false
- }
-}
-
-private let trueNumber = NSNumber(value: true)
-private let falseNumber = NSNumber(value: false)
-private let trueObjCType = String(cString: trueNumber.objCType)
-private let falseObjCType = String(cString: falseNumber.objCType)
-
-// MARK: - NSNumber: Comparable
-
-extension NSNumber {
- fileprivate var isBool: Bool {
- let objCType = String(cString: self.objCType)
- if (self.compare(trueNumber) == .orderedSame && objCType == trueObjCType) || (self.compare(falseNumber) == .orderedSame && objCType == falseObjCType) {
- return true
- } else {
- return false
- }
- }
-}
-
-func == (lhs: NSNumber, rhs: NSNumber) -> Bool {
- switch (lhs.isBool, rhs.isBool) {
- case (false, true):
- return false
- case (true, false):
- return false
- default:
- return lhs.compare(rhs) == .orderedSame
- }
-}
-
-func != (lhs: NSNumber, rhs: NSNumber) -> Bool {
- return !(lhs == rhs)
-}
-
-func < (lhs: NSNumber, rhs: NSNumber) -> Bool {
-
- switch (lhs.isBool, rhs.isBool) {
- case (false, true):
- return false
- case (true, false):
- return false
- default:
- return lhs.compare(rhs) == .orderedAscending
- }
-}
-
-func > (lhs: NSNumber, rhs: NSNumber) -> Bool {
-
- switch (lhs.isBool, rhs.isBool) {
- case (false, true):
- return false
- case (true, false):
- return false
- default:
- return lhs.compare(rhs) == ComparisonResult.orderedDescending
- }
-}
-
-func <= (lhs: NSNumber, rhs: NSNumber) -> Bool {
-
- switch (lhs.isBool, rhs.isBool) {
- case (false, true):
- return false
- case (true, false):
- return false
- default:
- return lhs.compare(rhs) != .orderedDescending
- }
-}
-
-func >= (lhs: NSNumber, rhs: NSNumber) -> Bool {
-
- switch (lhs.isBool, rhs.isBool) {
- case (false, true):
- return false
- case (true, false):
- return false
- default:
- return lhs.compare(rhs) != .orderedAscending
- }
-}
-
-public enum writingOptionsKeys {
- case jsonSerialization
- case castNilToNSNull
- case maxObjextDepth
- case encoding
-}
diff --git a/SwiftRadio/NothingFoundCell.xib b/SwiftRadio/NothingFoundCell.xib
index 5a1546fc..b7814eb8 100755
--- a/SwiftRadio/NothingFoundCell.xib
+++ b/SwiftRadio/NothingFoundCell.xib
@@ -1,8 +1,12 @@
-
-
+
+
+
+
+
-
+
+
@@ -12,17 +16,27 @@
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SwiftRadio/NowPlayingViewController.swift b/SwiftRadio/NowPlayingViewController.swift
old mode 100755
new mode 100644
index 827bba4c..0933de14
--- a/SwiftRadio/NowPlayingViewController.swift
+++ b/SwiftRadio/NowPlayingViewController.swift
@@ -10,14 +10,14 @@ import UIKit
import MediaPlayer
//*****************************************************************
-// Protocol
-// Updates the StationsViewController when the track changes
+// NowPlayingViewControllerDelegate
//*****************************************************************
protocol NowPlayingViewControllerDelegate: class {
- func songMetaDataDidUpdate(track: Track)
- func artworkDidUpdate(track: Track)
- func trackPlayingToggled(track: Track)
+ func didPressPlayingButton()
+ func didPressStopButton()
+ func didPressNextButton()
+ func didPressPreviousButton()
}
//*****************************************************************
@@ -25,29 +25,32 @@ protocol NowPlayingViewControllerDelegate: class {
//*****************************************************************
class NowPlayingViewController: UIViewController {
+
+ weak var delegate: NowPlayingViewControllerDelegate?
+ // MARK: - IB UI
+
@IBOutlet weak var albumHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var albumImageView: SpringImageView!
@IBOutlet weak var artistLabel: UILabel!
- @IBOutlet weak var pauseButton: UIButton!
- @IBOutlet weak var playButton: UIButton!
+ @IBOutlet weak var playingButton: UIButton!
@IBOutlet weak var songLabel: SpringLabel!
@IBOutlet weak var stationDescLabel: UILabel!
@IBOutlet weak var volumeParentView: UIView!
- @IBOutlet weak var slider = UISlider()
+ @IBOutlet weak var previousButton: UIButton!
+ @IBOutlet weak var nextButton: UIButton!
+
+ // MARK: - Properties
var currentStation: RadioStation!
- var downloadTask: URLSessionDownloadTask?
- var iPhone4 = false
- var justBecameActive = false
+ var currentTrack: Track!
+
var newStation = true
var nowPlayingImageView: UIImageView!
- let radioPlayer = Player.radio
- var track: Track!
- var mpVolumeSlider = UISlider()
-
- weak var delegate: NowPlayingViewControllerDelegate?
+ let radioPlayer = FRadioPlayer.shared
+ var mpVolumeSlider: UISlider?
+
//*****************************************************************
// MARK: - ViewDidLoad
//*****************************************************************
@@ -55,171 +58,166 @@ class NowPlayingViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
- // Setup handoff functionality - GH
- setupUserActivity()
+ // Create Now Playing BarItem
+ createNowPlayingAnimation()
// Set AlbumArtwork Constraints
optimizeForDeviceSize()
// Set View Title
- self.title = currentStation.stationName
-
- // Create Now Playing BarItem
- createNowPlayingAnimation()
+ self.title = currentStation.name
- // Setup MPMoviePlayerController
- // If you're building an app for a client, you may want to
- // replace the MediaPlayer player with a more robust
- // streaming library/SDK. Preferably one that supports interruptions, etc.
- // Most of the good streaming libaries are in Obj-C, however they
- // will work nicely with this Swift code. There is a branch using RadioKit if
- // you need an example of how nicely this code integrates with libraries.
- setupPlayer()
-
- // Notification for when app becomes active
- NotificationCenter.default.addObserver(self,
- selector: #selector(NowPlayingViewController.didBecomeActiveNotificationReceived),
- name: Notification.Name("UIApplicationDidBecomeActiveNotification"),
- object: nil)
-
- // Notification for MediaPlayer metadata updated
- NotificationCenter.default.addObserver(self,
- selector: #selector(NowPlayingViewController.metadataUpdated),
- name: Notification.Name.MPMoviePlayerTimedMetadataUpdated,
- object: nil)
-
- // Notification for AVAudioSession Interruption (e.g. Phone call)
- NotificationCenter.default.addObserver(self,
- selector: #selector(NowPlayingViewController.sessionInterrupted),
- name: Notification.Name.AVAudioSessionInterruption,
- object: AVAudioSession.sharedInstance())
+ // Set UI
+ albumImageView.image = currentTrack.artworkImage
+ stationDescLabel.text = currentStation.desc
+ stationDescLabel.isHidden = currentTrack.artworkLoaded
// Check for station change
- if newStation {
- track = Track()
- stationDidChange()
- } else {
- updateLabels()
- albumImageView.image = track.artworkImage
-
- if !track.isPlaying {
- pausePressed()
- } else {
- nowPlayingImageView.startAnimating()
- }
- }
+ newStation ? stationDidChange() : playerStateDidChange(radioPlayer.state, animate: false)
- // Setup slider
+ // Setup volumeSlider
setupVolumeSlider()
- }
-
- @objc func didBecomeActiveNotificationReceived() {
- // View became active
- updateLabels()
- justBecameActive = true
- updateAlbumArtwork()
- }
-
- deinit {
- // Be a good citizen
- NotificationCenter.default.removeObserver(self,
- name: Notification.Name("UIApplicationDidBecomeActiveNotification"),
- object: nil)
- NotificationCenter.default.removeObserver(self,
- name: Notification.Name.MPMoviePlayerTimedMetadataUpdated,
- object: nil)
- NotificationCenter.default.removeObserver(self,
- name: Notification.Name.AVAudioSessionInterruption,
- object: AVAudioSession.sharedInstance())
+ // Hide / Show Next/Previous buttons
+ previousButton.isHidden = hideNextPreviousButtons
+ nextButton.isHidden = hideNextPreviousButtons
}
//*****************************************************************
// MARK: - Setup
//*****************************************************************
- func setupPlayer() {
- radioPlayer.view.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
- radioPlayer.view.sizeToFit()
- radioPlayer.movieSourceType = MPMovieSourceType.streaming
- radioPlayer.isFullscreen = false
- radioPlayer.shouldAutoplay = true
- radioPlayer.prepareToPlay()
- radioPlayer.controlStyle = MPMovieControlStyle.none
- }
-
func setupVolumeSlider() {
// Note: This slider implementation uses a MPVolumeView
// The volume slider only works in devices, not the simulator.
- volumeParentView.backgroundColor = UIColor.clear
- let volumeView = MPVolumeView(frame: volumeParentView.bounds)
- for view in volumeView.subviews {
- let uiview: UIView = view as UIView
- if (uiview.description as NSString).range(of: "MPVolumeSlider").location != NSNotFound {
- mpVolumeSlider = (uiview as! UISlider)
- }
+ for subview in MPVolumeView().subviews {
+ guard let volumeSlider = subview as? UISlider else { continue }
+ mpVolumeSlider = volumeSlider
}
- let thumbImageNormal = UIImage(named: "slider-ball")
- slider?.setThumbImage(thumbImageNormal, for: .normal)
+ guard let mpVolumeSlider = mpVolumeSlider else { return }
+ volumeParentView.addSubview(mpVolumeSlider)
+
+ mpVolumeSlider.translatesAutoresizingMaskIntoConstraints = false
+ mpVolumeSlider.leftAnchor.constraint(equalTo: volumeParentView.leftAnchor).isActive = true
+ mpVolumeSlider.rightAnchor.constraint(equalTo: volumeParentView.rightAnchor).isActive = true
+ mpVolumeSlider.centerYAnchor.constraint(equalTo: volumeParentView.centerYAnchor).isActive = true
+
+ mpVolumeSlider.setThumbImage(#imageLiteral(resourceName: "slider-ball"), for: .normal)
}
func stationDidChange() {
- radioPlayer.stop()
-
- radioPlayer.contentURL = URL(string: currentStation.stationStreamURL)
- radioPlayer.prepareToPlay()
- radioPlayer.play()
-
- updateLabels(statusMessage: "Loading Station...")
-
- // songLabel animate
- songLabel.animation = "flash"
- songLabel.repeatCount = 3
- songLabel.animate()
-
- resetAlbumArtwork()
-
- track.isPlaying = true
+ radioPlayer.radioURL = URL(string: currentStation.streamURL)
+ title = currentStation.name
}
//*****************************************************************
// MARK: - Player Controls (Play/Pause/Volume)
//*****************************************************************
- @IBAction func playPressed() {
- track.isPlaying = true
- playButtonEnable(enabled: false)
- radioPlayer.play()
+ // Actions
+
+ @IBAction func playingPressed(_ sender: Any) {
+ delegate?.didPressPlayingButton()
+ }
+
+ @IBAction func stopPressed(_ sender: Any) {
+ delegate?.didPressStopButton()
+ }
+
+ @IBAction func nextPressed(_ sender: Any) {
+ delegate?.didPressNextButton()
+ }
+
+ @IBAction func previousPressed(_ sender: Any) {
+ delegate?.didPressPreviousButton()
+ }
+
+ //*****************************************************************
+ // MARK: - Load station/track
+ //*****************************************************************
+
+ func load(station: RadioStation?, track: Track?, isNewStation: Bool = true) {
+ guard let station = station else { return }
+
+ currentStation = station
+ currentTrack = track
+ newStation = isNewStation
+ }
+
+ func updateTrackMetadata(with track: Track?) {
+ guard let track = track else { return }
+
+ currentTrack.artist = track.artist
+ currentTrack.title = track.title
+
updateLabels()
+ }
+
+ // Update track with new artwork
+ func updateTrackArtwork(with track: Track?) {
+ guard let track = track else { return }
- // songLabel Animation
- songLabel.animation = "flash"
- songLabel.animate()
+ // Update track struct
+ currentTrack.artworkImage = track.artworkImage
+ currentTrack.artworkLoaded = track.artworkLoaded
- // Start NowPlaying Animation
- nowPlayingImageView.startAnimating()
+ albumImageView.image = currentTrack.artworkImage
- // Update StationsVC
- self.delegate?.trackPlayingToggled(track: self.track)
+ if track.artworkLoaded {
+ // Animate artwork
+ albumImageView.animation = "wobble"
+ albumImageView.duration = 2
+ albumImageView.animate()
+ stationDescLabel.isHidden = true
+ } else {
+ stationDescLabel.isHidden = false
+ }
+
+ // Force app to update display
+ view.setNeedsDisplay()
+ }
+
+ private func isPlayingDidChange(_ isPlaying: Bool) {
+ playingButton.isSelected = isPlaying
+ startNowPlayingAnimation(isPlaying)
}
- @IBAction func pausePressed() {
- track.isPlaying = false
+ func playbackStateDidChange(_ playbackState: FRadioPlaybackState, animate: Bool) {
- playButtonEnable()
+ let message: String?
- radioPlayer.pause()
- updateLabels(statusMessage: "Station Paused...")
- nowPlayingImageView.stopAnimating()
+ switch playbackState {
+ case .paused:
+ message = "Station Paused..."
+ case .playing:
+ message = nil
+ case .stopped:
+ message = "Station Stopped..."
+ }
- // Update StationsVC
- self.delegate?.trackPlayingToggled(track: self.track)
+ updateLabels(with: message, animate: animate)
+ isPlayingDidChange(radioPlayer.isPlaying)
}
- @IBAction func volumeChanged(_ sender:UISlider) {
- mpVolumeSlider.value = sender.value
+ func playerStateDidChange(_ state: FRadioPlayerState, animate: Bool) {
+
+ let message: String?
+
+ switch state {
+ case .loading:
+ message = "Loading Station ..."
+ case .urlNotSet:
+ message = "Station URL not valide"
+ case .readyToPlay, .loadingFinished:
+ playbackStateDidChange(radioPlayer.playbackState, animate: animate)
+ return
+ case .error:
+ message = "Error Playing"
+ }
+
+ updateLabels(with: message, animate: animate)
}
//*****************************************************************
@@ -232,7 +230,6 @@ class NowPlayingViewController: UIViewController {
let deviceHeight = self.view.bounds.height
if deviceHeight == 480 {
- iPhone4 = true
albumHeightConstraint.constant = 106
view.updateConstraints()
} else if deviceHeight == 667 {
@@ -244,40 +241,42 @@ class NowPlayingViewController: UIViewController {
}
}
- func updateLabels(statusMessage: String = "") {
-
- if statusMessage != "" {
- // There's a an interruption or pause in the audio queue
- songLabel.text = statusMessage
- artistLabel.text = currentStation.stationName
-
- } else {
+ func updateLabels(with statusMessage: String? = nil, animate: Bool = true) {
+
+ guard let statusMessage = statusMessage else {
// Radio is (hopefully) streaming properly
- if track != nil {
- songLabel.text = track.title
- artistLabel.text = track.artist
- }
+ songLabel.text = currentTrack.title
+ artistLabel.text = currentTrack.artist
+ shouldAnimateSongLabel(animate)
+ return
}
- // Hide station description when album art is displayed or on iPhone 4
- if track.artworkLoaded || iPhone4 {
- stationDescLabel.isHidden = true
- } else {
- stationDescLabel.isHidden = false
- stationDescLabel.text = currentStation.stationDesc
+ // There's a an interruption or pause in the audio queue
+
+ // Update UI only when it's not aleary updated
+ guard songLabel.text != statusMessage else { return }
+
+ songLabel.text = statusMessage
+ artistLabel.text = currentStation.name
+
+ if animate {
+ songLabel.animation = "flash"
+ songLabel.repeatCount = 3
+ songLabel.animate()
}
}
- func playButtonEnable(enabled: Bool = true) {
- if enabled {
- playButton.isEnabled = true
- pauseButton.isEnabled = false
- track.isPlaying = false
- } else {
- playButton.isEnabled = false
- pauseButton.isEnabled = true
- track.isPlaying = true
- }
+ // Animations
+
+ func shouldAnimateSongLabel(_ animate: Bool) {
+ // Animate if the Track has album metadata
+ guard animate, currentTrack.title != currentStation.name else { return }
+
+ // songLabel animation
+ songLabel.animation = "zoomIn"
+ songLabel.duration = 1.5
+ songLabel.damping = 1
+ songLabel.animate()
}
func createNowPlayingAnimation() {
@@ -292,160 +291,17 @@ class NowPlayingViewController: UIViewController {
nowPlayingImageView.animationDuration = 0.7
// Create Top BarButton
- let barButton = UIButton(type: UIButtonType.custom)
- barButton.frame = CGRect(x: 0,y: 0,width: 40,height: 40);
+ let barButton = UIButton(type: .custom)
+ barButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
barButton.addSubview(nowPlayingImageView)
nowPlayingImageView.center = barButton.center
let barItem = UIBarButtonItem(customView: barButton)
self.navigationItem.rightBarButtonItem = barItem
-
- }
-
- func startNowPlayingAnimation() {
- nowPlayingImageView.startAnimating()
- }
-
- //*****************************************************************
- // MARK: - Album Art
- //*****************************************************************
-
- func resetAlbumArtwork() {
- track.artworkLoaded = false
- track.artworkURL = currentStation.stationImageURL
- updateAlbumArtwork()
- stationDescLabel.isHidden = false
- }
-
- func updateAlbumArtwork() {
- track.artworkLoaded = false
- if track.artworkURL.range(of: "http") != nil {
-
- // Hide station description
- DispatchQueue.main.async(execute: {
- //self.albumImageView.image = nil
- self.stationDescLabel.isHidden = false
- })
-
- // Attempt to download album art from an API
- if let url = URL(string: track.artworkURL) {
-
- self.downloadTask = self.albumImageView.loadImageWithURL(url: url) { (image) in
-
- // Update track struct
- self.track.artworkImage = image
- self.track.artworkLoaded = true
-
- // Turn off network activity indicator
- UIApplication.shared.isNetworkActivityIndicatorVisible = false
-
- // Animate artwork
- self.albumImageView.animation = "wobble"
- self.albumImageView.duration = 2
- self.albumImageView.animate()
- self.stationDescLabel.isHidden = true
-
- // Update lockscreen
- self.updateLockScreen()
-
- // Call delegate function that artwork updated
- self.delegate?.artworkDidUpdate(track: self.track)
- }
- }
-
- // Hide the station description to make room for album art
- if track.artworkLoaded && !self.justBecameActive {
- self.stationDescLabel.isHidden = true
- self.justBecameActive = false
- }
-
- } else if track.artworkURL != "" {
- // Local artwork
- self.albumImageView.image = UIImage(named: track.artworkURL)
- track.artworkImage = albumImageView.image
- track.artworkLoaded = true
-
- // Call delegate function that artwork updated
- self.delegate?.artworkDidUpdate(track: self.track)
-
- } else {
- // No Station or API art found, use default art
- self.albumImageView.image = UIImage(named: "albumArt")
- track.artworkImage = albumImageView.image
- }
-
- // Force app to update display
- self.view.setNeedsDisplay()
}
-
- // Call LastFM or iTunes API to get album art url
- func queryAlbumArt() {
-
- UIApplication.shared.isNetworkActivityIndicatorVisible = true
-
- // Construct either LastFM or iTunes API call URL
- let queryURL: String
- if useLastFM {
- queryURL = String(format: "http://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key=%@&artist=%@&track=%@&format=json", apiKey, track.artist, track.title)
- } else {
- queryURL = String(format: "https://itunes.apple.com/search?term=%@+%@&entity=song", track.artist, track.title)
- }
-
- let escapedURL = queryURL.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
-
- // Query API
- DataManager.getTrackDataWithSuccess(queryURL: escapedURL!) { (data) in
-
- if kDebugLog {
- print("API SUCCESSFUL RETURN")
- print("url: \(escapedURL!)")
- }
-
- let json = try! JSON(data: data! as Data)
-
- if useLastFM {
- // Get Largest Sized LastFM Image
- if let imageArray = json["track"]["album"]["image"].array {
-
- let arrayCount = imageArray.count
- let lastImage = imageArray[arrayCount - 1]
-
- if let artURL = lastImage["#text"].string {
-
- // Check for Default Last FM Image
- if artURL.range(of: "/noimage/") != nil {
- self.resetAlbumArtwork()
-
- } else {
- // LastFM image found!
- self.track.artworkURL = artURL
- self.track.artworkLoaded = true
- self.updateAlbumArtwork()
- }
-
- } else {
- self.resetAlbumArtwork()
- }
- } else {
- self.resetAlbumArtwork()
- }
-
- } else {
- // Use iTunes API. Images are 100px by 100px
- if let artURL = json["results"][0]["artworkUrl100"].string {
-
- if kDebugLog { print("iTunes artURL: \(artURL)") }
-
- self.track.artworkURL = artURL
- self.track.artworkLoaded = true
- self.updateAlbumArtwork()
- } else {
- self.resetAlbumArtwork()
- }
- }
-
- }
+ func startNowPlayingAnimation(_ animate: Bool) {
+ animate ? nowPlayingImageView.startAnimating() : nowPlayingImageView.stopAnimating()
}
//*****************************************************************
@@ -453,11 +309,8 @@ class NowPlayingViewController: UIViewController {
//*****************************************************************
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
-
- if segue.identifier == "InfoDetail" {
- let infoController = segue.destination as! InfoDetailViewController
- infoController.currentStation = currentStation
- }
+ guard segue.identifier == "InfoDetail", let infoController = segue.destination as? InfoDetailViewController else { return }
+ infoController.currentStation = currentStation
}
@IBAction func infoButtonPressed(_ sender: UIButton) {
@@ -465,147 +318,8 @@ class NowPlayingViewController: UIViewController {
}
@IBAction func shareButtonPressed(_ sender: UIButton) {
- let songToShare = "I'm listening to \(track.title) on \(currentStation.stationName) via Swift Radio Pro"
- let activityViewController = UIActivityViewController(activityItems: [songToShare, track.artworkImage!], applicationActivities: nil)
+ let songToShare = "I'm listening to \(currentTrack.title) on \(currentStation.name) via Swift Radio Pro"
+ let activityViewController = UIActivityViewController(activityItems: [songToShare, currentTrack.artworkImage!], applicationActivities: nil)
present(activityViewController, animated: true, completion: nil)
}
-
- //*****************************************************************
- // MARK: - MPNowPlayingInfoCenter (Lock screen)
- //*****************************************************************
-
- func updateLockScreen() {
-
- // Update notification/lock screen
- let albumArtwork = MPMediaItemArtwork(image: track.artworkImage!)
-
- MPNowPlayingInfoCenter.default().nowPlayingInfo = [
- MPMediaItemPropertyArtist: track.artist,
- MPMediaItemPropertyTitle: track.title,
- MPMediaItemPropertyArtwork: albumArtwork
- ]
- }
-
- override func remoteControlReceived(with receivedEvent: UIEvent?) {
- super.remoteControlReceived(with: receivedEvent)
-
- if receivedEvent!.type == UIEventType.remoteControl {
-
- switch receivedEvent!.subtype {
- case .remoteControlPlay:
- playPressed()
- case .remoteControlPause:
- pausePressed()
- default:
- break
- }
- }
- }
-
- //*****************************************************************
- // MARK: - MetaData Updated Notification
- //*****************************************************************
-
- @objc func metadataUpdated(n: NSNotification)
- {
- if(radioPlayer.timedMetadata != nil && radioPlayer.timedMetadata.count > 0)
- {
- startNowPlayingAnimation()
-
- let firstMeta: MPTimedMetadata = radioPlayer.timedMetadata.first as! MPTimedMetadata
- let metaData = firstMeta.value as! String
-
- var stringParts = [String]()
- if metaData.range(of: " - ") != nil {
- stringParts = metaData.components(separatedBy: " - ")
- } else {
- stringParts = metaData.components(separatedBy: "-")
- }
-
- // Set artist & songvariables
- let currentSongName = track.title
- track.artist = stringParts[0]
- track.title = stringParts[0]
-
- if stringParts.count > 1 {
- track.title = stringParts[1]
- }
-
- if track.artist == "" && track.title == "" {
- track.artist = currentStation.stationDesc
- track.title = currentStation.stationName
- }
-
- DispatchQueue.main.async(execute: {
-
- if currentSongName != self.track.title {
-
- if kDebugLog {
- print("METADATA artist: \(self.track.artist) | title: \(self.track.title)")
- }
-
- // Update Labels
- self.artistLabel.text = self.track.artist
- self.songLabel.text = self.track.title
- self.updateUserActivityState(self.userActivity!)
-
- // songLabel animation
- self.songLabel.animation = "zoomIn"
- self.songLabel.duration = 1.5
- self.songLabel.damping = 1
- self.songLabel.animate()
-
- // Update Stations Screen
- self.delegate?.songMetaDataDidUpdate(track: self.track)
-
- // Query API for album art
- self.resetAlbumArtwork()
- self.queryAlbumArt()
- self.updateLockScreen()
-
- }
- })
- }
- }
-
- //*****************************************************************
- // MARK: - AVAudio Sesssion Interrupted
- //*****************************************************************
-
- // Example code on handling AVAudio interruptions (e.g. Phone calls)
- @objc func sessionInterrupted(notification: NSNotification) {
- if let typeValue = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? NSNumber{
- if let type = AVAudioSessionInterruptionType(rawValue: typeValue.uintValue){
- if type == .began {
- print("interruption: began")
- // Add your code here
- } else{
- print("interruption: ended")
- // Add your code here
- }
- }
- }
- }
-
- //*****************************************************************
- // MARK: - Handoff Functionality - GH
- //*****************************************************************
-
- func setupUserActivity() {
- let activity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb ) //"com.graemeharrison.handoff.googlesearch" //NSUserActivityTypeBrowsingWeb
- userActivity = activity
- let url = "https://www.google.com/search?q=\(self.artistLabel.text!)+\(self.songLabel.text!)"
- let urlStr = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
- let searchURL : URL = URL(string: urlStr!)!
- activity.webpageURL = searchURL
- userActivity?.becomeCurrent()
- }
-
- override func updateUserActivityState(_ activity: NSUserActivity) {
- let url = "https://www.google.com/search?q=\(self.artistLabel.text!)+\(self.songLabel.text!)"
- let urlStr = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
- let searchURL : URL = URL(string: urlStr!)!
- activity.webpageURL = searchURL
- super.updateUserActivityState(activity)
- }
}
diff --git a/SwiftRadio/Player.swift b/SwiftRadio/Player.swift
deleted file mode 100755
index 438e9794..00000000
--- a/SwiftRadio/Player.swift
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-// Player.swift
-// Swift Radio
-//
-// Created by Matthew Fecher on 7/13/15.
-// Copyright (c) 2015 MatthewFecher.com. All rights reserved.
-//
-
-import MediaPlayer
-
-//*****************************************************************
-// This is a singleton struct using Swift
-//*****************************************************************
-
-struct Player {
- static let radio = MPMoviePlayerController()
-}
\ No newline at end of file
diff --git a/SwiftRadio/PopUpMenuViewController.swift b/SwiftRadio/PopUpMenuViewController.swift
index 28289890..900764f3 100755
--- a/SwiftRadio/PopUpMenuViewController.swift
+++ b/SwiftRadio/PopUpMenuViewController.swift
@@ -32,7 +32,7 @@ class PopUpMenuViewController: UIViewController {
view.backgroundColor = UIColor.clear
// Add gesture recognizer to dismiss view when touched
- let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(PopUpMenuViewController.closeButtonPressed))
+ let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(closeButtonPressed))
backgroundView.isUserInteractionEnabled = true
backgroundView.addGestureRecognizer(gestureRecognizer)
}
@@ -47,9 +47,8 @@ class PopUpMenuViewController: UIViewController {
@IBAction func websiteButtonPressed(_ sender: UIButton) {
// Use your own website URL here
- if let url = URL(string: "https://github.com/swiftcodex/") {
- UIApplication.shared.openURL(url)
- }
+ guard let url = URL(string: "https://github.com/analogcode/") else { return }
+ UIApplication.shared.openURL(url)
}
}
diff --git a/SwiftRadio/RadioPlayer.swift b/SwiftRadio/RadioPlayer.swift
new file mode 100644
index 00000000..f527a0f9
--- /dev/null
+++ b/SwiftRadio/RadioPlayer.swift
@@ -0,0 +1,134 @@
+//
+// RadioPlayer.swift
+// SwiftRadio
+//
+// Created by Fethi El Hassasna on 2018-01-05.
+// Copyright © 2018 matthewfecher.com. All rights reserved.
+//
+
+import UIKit
+
+//*****************************************************************
+// RadioPlayerDelegate: Sends FRadioPlayer and Station/Track events
+//*****************************************************************
+
+protocol RadioPlayerDelegate: class {
+ func playerStateDidChange(_ playerState: FRadioPlayerState)
+ func playbackStateDidChange(_ playbackState: FRadioPlaybackState)
+ func trackDidUpdate(_ track: Track?)
+ func trackArtworkDidUpdate(_ track: Track?)
+}
+
+//*****************************************************************
+// RadioPlayer: App Radio Player
+//*****************************************************************
+
+class RadioPlayer {
+
+ weak var delegate: RadioPlayerDelegate?
+
+ let player = FRadioPlayer.shared
+
+ var station: RadioStation? {
+ didSet { resetTrack(with: station) }
+ }
+
+ private(set) var track: Track?
+
+ init() {
+ player.delegate = self
+ }
+
+ func resetRadioPlayer() {
+ station = nil
+ track = nil
+ player.radioURL = nil
+ }
+
+ //*****************************************************************
+ // MARK: - Track loading/updates
+ //*****************************************************************
+
+ // Update the track with an artist name and track name
+ func updateTrackMetadata(artistName: String, trackName: String) {
+ if track == nil {
+ track = Track(title: trackName, artist: artistName)
+ } else {
+ track?.title = trackName
+ track?.artist = artistName
+ }
+
+ delegate?.trackDidUpdate(track)
+ }
+
+ // Update the track artwork with a UIImage
+ func updateTrackArtwork(with image: UIImage, artworkLoaded: Bool) {
+ track?.artworkImage = image
+ track?.artworkLoaded = artworkLoaded
+ delegate?.trackArtworkDidUpdate(track)
+ }
+
+ // Reset the track metadata and artwork to use the current station infos
+ func resetTrack(with station: RadioStation?) {
+ guard let station = station else { track = nil; return }
+ updateTrackMetadata(artistName: station.desc, trackName: station.name)
+ resetArtwork(with: station)
+ }
+
+ // Reset the track Artwork to current station image
+ func resetArtwork(with station: RadioStation?) {
+ guard let station = station else { track = nil; return }
+ getStationImage(from: station) { image in
+ self.updateTrackArtwork(with: image, artworkLoaded: false)
+ }
+ }
+
+ //*****************************************************************
+ // MARK: - Private helpers
+ //*****************************************************************
+
+ private func getStationImage(from station: RadioStation, completionHandler: @escaping (_ image: UIImage) -> ()) {
+
+ if station.imageURL.range(of: "http") != nil {
+ // load current station image from network
+ ImageLoader.sharedLoader.imageForUrl(urlString: station.imageURL) { (image, stringURL) in
+ completionHandler(image ?? #imageLiteral(resourceName: "albumArt"))
+ }
+ } else {
+ // load local station image
+ let image = UIImage(named: station.imageURL) ?? #imageLiteral(resourceName: "albumArt")
+ completionHandler(image)
+ }
+ }
+}
+
+extension RadioPlayer: FRadioPlayerDelegate {
+
+ func radioPlayer(_ player: FRadioPlayer, playerStateDidChange state: FRadioPlayerState) {
+ delegate?.playerStateDidChange(state)
+ }
+
+ func radioPlayer(_ player: FRadioPlayer, playbackStateDidChange state: FRadioPlaybackState) {
+ delegate?.playbackStateDidChange(state)
+ }
+
+ func radioPlayer(_ player: FRadioPlayer, metadataDidChange artistName: String?, trackName: String?) {
+ guard
+ let artistName = artistName, !artistName.isEmpty,
+ let trackName = trackName, !trackName.isEmpty else {
+ resetTrack(with: station)
+ return
+ }
+
+ updateTrackMetadata(artistName: artistName, trackName: trackName)
+ }
+
+ func radioPlayer(_ player: FRadioPlayer, artworkDidChange artworkURL: URL?) {
+ guard let artworkURL = artworkURL else { resetArtwork(with: station); return }
+
+ ImageLoader.sharedLoader.imageForUrl(urlString: artworkURL.absoluteString) { (image, stringURL) in
+ guard let image = image else { self.resetArtwork(with: self.station); return }
+ self.updateTrackArtwork(with: image, artworkLoaded: true)
+ }
+ }
+}
diff --git a/SwiftRadio/RadioStation.swift b/SwiftRadio/RadioStation.swift
index 91340900..82430d86 100755
--- a/SwiftRadio/RadioStation.swift
+++ b/SwiftRadio/RadioStation.swift
@@ -12,44 +12,26 @@ import UIKit
// Radio Station
//*****************************************************************
-// Class inherits from NSObject so that you may easily add features
-// i.e. Saving favorite stations to CoreData, etc
-
-class RadioStation: NSObject {
-
- var stationName : String
- var stationStreamURL: String
- var stationImageURL : String
- var stationDesc : String
- var stationLongDesc : String
+struct RadioStation: Codable {
- init(name: String, streamURL: String, imageURL: String, desc: String, longDesc: String) {
- self.stationName = name
- self.stationStreamURL = streamURL
- self.stationImageURL = imageURL
- self.stationDesc = desc
- self.stationLongDesc = longDesc
- }
+ var name: String
+ var streamURL: String
+ var imageURL: String
+ var desc: String
+ var longDesc: String
- // Convenience init without longDesc
- convenience init(name: String, streamURL: String, imageURL: String, desc: String) {
- self.init(name: name, streamURL: streamURL, imageURL: imageURL, desc: desc, longDesc: "")
+ init(name: String, streamURL: String, imageURL: String, desc: String, longDesc: String = "") {
+ self.name = name
+ self.streamURL = streamURL
+ self.imageURL = imageURL
+ self.desc = desc
+ self.longDesc = longDesc
}
+}
+
+extension RadioStation: Equatable {
- //*****************************************************************
- // MARK: - JSON Parsing into object
- //*****************************************************************
-
- class func parseStation(stationJSON: JSON) -> (RadioStation) {
-
- let name = stationJSON["name"].string ?? ""
- let streamURL = stationJSON["streamURL"].string ?? ""
- let imageURL = stationJSON["imageURL"].string ?? ""
- let desc = stationJSON["desc"].string ?? ""
- let longDesc = stationJSON["longDesc"].string ?? ""
-
- let station = RadioStation(name: name, streamURL: streamURL, imageURL: imageURL, desc: desc, longDesc: longDesc)
- return station
+ static func ==(lhs: RadioStation, rhs: RadioStation) -> Bool {
+ return (lhs.name == rhs.name) && (lhs.streamURL == rhs.streamURL) && (lhs.imageURL == rhs.imageURL) && (lhs.desc == rhs.desc) && (lhs.longDesc == rhs.longDesc)
}
-
}
diff --git a/SwiftRadio/StationTableViewCell.swift b/SwiftRadio/StationTableViewCell.swift
index ed5a2c07..3f66d3c9 100755
--- a/SwiftRadio/StationTableViewCell.swift
+++ b/SwiftRadio/StationTableViewCell.swift
@@ -28,15 +28,15 @@ class StationTableViewCell: UITableViewCell {
func configureStationCell(station: RadioStation) {
// Configure the cell...
- stationNameLabel.text = station.stationName
- stationDescLabel.text = station.stationDesc
+ stationNameLabel.text = station.name
+ stationDescLabel.text = station.desc
- let imageURL = station.stationImageURL as NSString
+ let imageURL = station.imageURL as NSString
if imageURL.contains("http") {
- if let url = URL(string: station.stationImageURL) {
- downloadTask = stationImageView.loadImageWithURL(url: url) { (image) in
+ if let url = URL(string: station.imageURL) {
+ stationImageView.loadImageWithURL(url: url) { (image) in
// station image loaded
}
}
diff --git a/SwiftRadio/StationsViewController.swift b/SwiftRadio/StationsViewController.swift
index 078ac80c..6a885932 100755
--- a/SwiftRadio/StationsViewController.swift
+++ b/SwiftRadio/StationsViewController.swift
@@ -11,19 +11,40 @@ import MediaPlayer
import AVFoundation
class StationsViewController: UIViewController {
+
+ // MARK: - IB UI
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var stationNowPlayingButton: UIButton!
@IBOutlet weak var nowPlayingAnimationImageView: UIImageView!
- var stations = [RadioStation]()
- var currentStation: RadioStation?
- var currentTrack: Track?
- var refreshControl: UIRefreshControl!
- var firstTime = true
+ // MARK: - Properties
+
+ let radioPlayer = RadioPlayer()
+
+ // Weak reference to update the NowPlayingViewController
+ weak var nowPlayingViewController: NowPlayingViewController?
+
+ // MARK: - Lists
+
+ var stations = [RadioStation]() {
+ didSet {
+ guard stations != oldValue else { return }
+ stationsDidUpdate()
+ }
+ }
var searchedStations = [RadioStation]()
- var searchController : UISearchController!
+
+ // MARK: - UI
+
+ var searchController: UISearchController = {
+ return UISearchController(searchResultsController: nil)
+ }()
+
+ var refreshControl: UIRefreshControl = {
+ return UIRefreshControl()
+ }()
//*****************************************************************
// MARK: - ViewDidLoad
@@ -36,15 +57,16 @@ class StationsViewController: UIViewController {
let cellNib = UINib(nibName: "NothingFoundCell", bundle: nil)
tableView.register(cellNib, forCellReuseIdentifier: "NothingFound")
- // preferredStatusBarStyle()
+ // Setup Player
+ radioPlayer.delegate = self
// Load Data
loadStationsFromJSON()
// Setup TableView
- tableView.backgroundColor = UIColor.clear
+ tableView.backgroundColor = .clear
tableView.backgroundView = nil
- tableView.separatorStyle = UITableViewCellSeparatorStyle.none
+ tableView.separatorStyle = .none
// Setup Pull to Refresh
setupPullToRefresh()
@@ -52,49 +74,26 @@ class StationsViewController: UIViewController {
// Create NowPlaying Animation
createNowPlayingAnimation()
- // Set AVFoundation category, required for background audio
- var error: NSError?
- var success: Bool
- do {
- try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
- success = true
- } catch let error1 as NSError {
- error = error1
- success = false
- }
- if !success {
- if kDebugLog { print("Failed to set audio session category. Error: \(error!)") }
- }
-
- // Set audioSession as active
+ // Activate audioSession
do {
try AVAudioSession.sharedInstance().setActive(true)
- } catch let error2 as NSError {
- if kDebugLog { print("audioSession setActive error \(error2)") }
+ } catch {
+ if kDebugLog { print("audioSession could not be activated") }
}
// Setup Search Bar
setupSearchController()
+
+ // Setup Remote Command Center
+ setupRemoteCommandCenter()
+
+ // Setup Handoff User Activity
+ setupHandoffUserActivity()
}
override func viewWillAppear(_ animated: Bool) {
- self.title = "Swift Radio"
-
- // If a station has been selected, create "Now Playing" button to get back to current station
- if !firstTime {
- createNowPlayingBarButton()
- }
-
- // If a track is playing, display title & artist information and animation
- if currentTrack != nil && currentTrack!.isPlaying {
- let title = currentStation!.stationName + ": " + currentTrack!.title + " - " + currentTrack!.artist + "..."
- stationNowPlayingButton.setTitle(title, for: .normal)
- nowPlayingAnimationImageView.startAnimating()
- } else {
- nowPlayingAnimationImageView.stopAnimating()
- nowPlayingAnimationImageView.image = UIImage(named: "NowPlayingBars")
- }
-
+ super.viewWillAppear(animated)
+ title = "Swift Radio"
}
//*****************************************************************
@@ -102,12 +101,11 @@ class StationsViewController: UIViewController {
//*****************************************************************
func setupPullToRefresh() {
- self.refreshControl = UIRefreshControl()
- self.refreshControl.attributedTitle = NSAttributedString(string: "Pull to refresh", attributes: [NSAttributedStringKey.foregroundColor:UIColor.white])
- self.refreshControl.backgroundColor = UIColor.black
- self.refreshControl.tintColor = UIColor.white
- self.refreshControl.addTarget(self, action: #selector(StationsViewController.refresh), for: UIControlEvents.valueChanged)
- self.tableView.addSubview(refreshControl)
+ refreshControl.attributedTitle = NSAttributedString(string: "Pull to refresh", attributes: [.foregroundColor: UIColor.white])
+ refreshControl.backgroundColor = .black
+ refreshControl.tintColor = .white
+ refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
+ tableView.addSubview(refreshControl)
}
func createNowPlayingAnimation() {
@@ -116,40 +114,10 @@ class StationsViewController: UIViewController {
}
func createNowPlayingBarButton() {
- if self.navigationItem.rightBarButtonItem == nil {
- let btn = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.plain, target: self, action:#selector(self.nowPlayingBarButtonPressed))
- btn.image = UIImage(named: "btn-nowPlaying")
- self.navigationItem.rightBarButtonItem = btn
- }
- }
-
- func setupSearchController() {
- // Set the UISearchController
- searchController = UISearchController(searchResultsController: nil)
-
- if searchable {
- searchController.searchResultsUpdater = self
- searchController.dimsBackgroundDuringPresentation = false
- searchController.searchBar.sizeToFit()
-
- // Add UISearchController to the tableView
- tableView.tableHeaderView = searchController?.searchBar
- tableView.tableHeaderView?.backgroundColor = UIColor.clear
- definesPresentationContext = true
- searchController.hidesNavigationBarDuringPresentation = false
-
- // Style the UISearchController
- searchController.searchBar.barTintColor = UIColor.clear
- searchController.searchBar.tintColor = UIColor.white
-
- // Hide the UISearchController
- tableView.setContentOffset(CGPoint(x: 0.0, y: searchController.searchBar.frame.size.height), animated: false)
-
- // Set a black keyborad for UISearchController's TextField
- let searchTextField = searchController.searchBar.value(forKey: "_searchField") as! UITextField
- searchTextField.keyboardAppearance = UIKeyboardAppearance.dark
- }
-
+ guard navigationItem.rightBarButtonItem == nil else { return }
+ let btn = UIBarButtonItem(title: "", style: .plain, target: self, action:#selector(nowPlayingBarButtonPressed))
+ btn.image = UIImage(named: "btn-nowPlaying")
+ navigationItem.rightBarButtonItem = btn
}
//*****************************************************************
@@ -166,7 +134,6 @@ class StationsViewController: UIViewController {
@objc func refresh(sender: AnyObject) {
// Pull to Refresh
- stations.removeAll(keepingCapacity: false)
loadStationsFromJSON()
// Wait 2 seconds then refresh screen
@@ -188,29 +155,19 @@ class StationsViewController: UIViewController {
// Get the Radio Stations
DataManager.getStationDataWithSuccess() { (data) in
- if kDebugLog { print("Stations JSON Found") }
+ // Turn off network indicator in status bar
+ defer {
+ DispatchQueue.main.async { UIApplication.shared.isNetworkActivityIndicatorVisible = false }
+ }
- let json = try! JSON(data: data! as Data)
+ if kDebugLog { print("Stations JSON Found") }
- if let stationArray = json["station"].array {
-
- for stationJSON in stationArray {
- let station = RadioStation.parseStation(stationJSON: stationJSON)
- self.stations.append(station)
- }
-
- // stations array populated, update table on main queue
- DispatchQueue.main.async(execute: {
- self.tableView.reloadData()
- self.view.setNeedsDisplay()
- })
-
- } else {
+ guard let data = data, let jsonDictionary = try? JSONDecoder().decode([String: [RadioStation]].self, from: data), let stationsArray = jsonDictionary["station"] else {
if kDebugLog { print("JSON Station Loading Error") }
+ return
}
- // Turn off network indicator in status bar
- UIApplication.shared.isNetworkActivityIndicatorVisible = false
+ self.stations = stationsArray
}
}
@@ -219,39 +176,128 @@ class StationsViewController: UIViewController {
//*****************************************************************
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
- if segue.identifier == "NowPlaying" {
-
- self.title = ""
- firstTime = false
-
- let nowPlayingVC = segue.destination as! NowPlayingViewController
- nowPlayingVC.delegate = self
-
- if let indexPath = (sender as? NSIndexPath) {
- // User clicked on row, load/reset station
- if searchController.isActive {
- currentStation = searchedStations[indexPath.row]
- } else {
- currentStation = stations[indexPath.row]
- }
- nowPlayingVC.currentStation = currentStation
- nowPlayingVC.newStation = true
+ guard segue.identifier == "NowPlaying", let nowPlayingVC = segue.destination as? NowPlayingViewController else { return }
+
+ title = ""
+
+ let newStation: Bool
+
+ if let indexPath = (sender as? IndexPath) {
+ // User clicked on row, load/reset station
+ radioPlayer.station = searchController.isActive ? searchedStations[indexPath.row] : stations[indexPath.row]
+ newStation = true
+ } else {
+ // User clicked on Now Playing button
+ newStation = false
+ }
+
+ nowPlayingViewController = nowPlayingVC
+ nowPlayingVC.load(station: radioPlayer.station, track: radioPlayer.track, isNewStation: newStation)
+ nowPlayingVC.delegate = self
+ }
+
+ //*****************************************************************
+ // MARK: - Private helpers
+ //*****************************************************************
+
+ private func stationsDidUpdate() {
+ DispatchQueue.main.async {
+ self.tableView.reloadData()
+ guard let currentStation = self.radioPlayer.station else { return }
- } else {
- // User clicked on a now playing button
- if let currentTrack = currentTrack {
- // Return to NowPlaying controller without reloading station
- nowPlayingVC.track = currentTrack
- nowPlayingVC.currentStation = currentStation
- nowPlayingVC.newStation = false
- } else {
- // Issue with track, reload station
- nowPlayingVC.currentStation = currentStation
- nowPlayingVC.newStation = true
- }
- }
+ // Reset everything if the new stations list doesn't have the current station
+ if self.stations.index(of: currentStation) == nil { self.resetCurrentStation() }
+ }
+ }
+
+ // Reset all properties to default
+ private func resetCurrentStation() {
+ radioPlayer.resetRadioPlayer()
+ nowPlayingAnimationImageView.stopAnimating()
+ stationNowPlayingButton.setTitle("Choose a station above to begin", for: .normal)
+ stationNowPlayingButton.isEnabled = false
+ navigationItem.rightBarButtonItem = nil
+ }
+
+ // Update the now playing button title
+ private func updateNowPlayingButton(station: RadioStation?, track: Track?) {
+ guard let station = station else { resetCurrentStation(); return }
+
+ var playingTitle = station.name + ": "
+
+ if track?.title == station.name {
+ playingTitle += "Now playing ..."
+ } else if let track = track {
+ playingTitle += track.title + " - " + track.artist
+ }
+
+ stationNowPlayingButton.setTitle(playingTitle, for: .normal)
+ stationNowPlayingButton.isEnabled = true
+ createNowPlayingBarButton()
+ }
+
+ func startNowPlayingAnimation(_ animate: Bool) {
+ animate ? nowPlayingAnimationImageView.startAnimating() : nowPlayingAnimationImageView.stopAnimating()
+ }
+
+ private func getIndex(of station: RadioStation?) -> Int? {
+ guard let station = station, let index = stations.index(of: station) else { return nil }
+ return index
+ }
+
+ //*****************************************************************
+ // MARK: - Remote Command Center Controls
+ //*****************************************************************
+
+ func setupRemoteCommandCenter() {
+ // Get the shared MPRemoteCommandCenter
+ let commandCenter = MPRemoteCommandCenter.shared()
+
+ // Add handler for Play Command
+ commandCenter.playCommand.addTarget { event in
+ return .success
+ }
+
+ // Add handler for Pause Command
+ commandCenter.pauseCommand.addTarget { event in
+ return .success
+ }
+
+ // Add handler for Next Command
+ commandCenter.nextTrackCommand.addTarget { event in
+ return .success
+ }
+
+ // Add handler for Previous Command
+ commandCenter.previousTrackCommand.addTarget { event in
+ return .success
}
}
+
+ //*****************************************************************
+ // MARK: - MPNowPlayingInfoCenter (Lock screen)
+ //*****************************************************************
+
+ func updateLockScreen(with track: Track?) {
+
+ // Define Now Playing Info
+ var nowPlayingInfo = [String : Any]()
+
+ if let image = track?.artworkImage {
+ nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: image)
+ }
+
+ if let artist = track?.artist {
+ nowPlayingInfo[MPMediaItemPropertyArtist] = artist
+ }
+
+ if let title = track?.title {
+ nowPlayingInfo[MPMediaItemPropertyTitle] = title
+ }
+
+ // Set the metadata
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
+ }
}
//*****************************************************************
@@ -260,10 +306,9 @@ class StationsViewController: UIViewController {
extension StationsViewController: UITableViewDataSource {
- // MARK: - Table view data source
@objc(tableView:heightForRowAtIndexPath:)
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
- return 88.0
+ return 90.0
}
func numberOfSections(in tableView: UITableView) -> Int {
@@ -272,17 +317,10 @@ extension StationsViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- // The UISeachController is active
if searchController.isActive {
return searchedStations.count
-
- // The UISeachController is not active
} else {
- if stations.count == 0 {
- return 1
- } else {
- return stations.count
- }
+ return stations.isEmpty ? 1 : stations.count
}
}
@@ -290,35 +328,19 @@ extension StationsViewController: UITableViewDataSource {
if stations.isEmpty {
let cell = tableView.dequeueReusableCell(withIdentifier: "NothingFound", for: indexPath)
- cell.backgroundColor = UIColor.clear
- cell.selectionStyle = UITableViewCellSelectionStyle.none
+ cell.backgroundColor = .clear
+ cell.selectionStyle = .none
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "StationCell", for: indexPath) as! StationTableViewCell
// alternate background color
- if indexPath.row % 2 == 0 {
- cell.backgroundColor = UIColor.clear
- } else {
- cell.backgroundColor = UIColor.black.withAlphaComponent(0.2)
- }
+ cell.backgroundColor = (indexPath.row % 2 == 0) ? UIColor.clear : UIColor.black.withAlphaComponent(0.2)
- // Configure the cell...
- let station = stations[indexPath.row]
+ let station = searchController.isActive ? searchedStations[indexPath.row] : stations[indexPath.row]
cell.configureStationCell(station: station)
- // The UISeachController is active
- if searchController.isActive {
- let station = searchedStations[indexPath.row]
- cell.configureStationCell(station: station)
-
- // The UISeachController is not active
- } else {
- let station = stations[indexPath.row]
- cell.configureStationCell(station: station)
- }
-
return cell
}
}
@@ -333,64 +355,146 @@ extension StationsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
+ performSegue(withIdentifier: "NowPlaying", sender: indexPath)
+ }
+}
+
+//*****************************************************************
+// MARK: - UISearchControllerDelegate / Setup
+//*****************************************************************
+
+extension StationsViewController: UISearchResultsUpdating {
+
+ func setupSearchController() {
+ guard searchable else { return }
- if !stations.isEmpty {
-
- // Set Now Playing Buttons
- let title = stations[indexPath.row].stationName + " - Now Playing..."
- stationNowPlayingButton.setTitle(title, for: .normal)
- stationNowPlayingButton.isEnabled = true
-
- performSegue(withIdentifier: "NowPlaying", sender: indexPath)
- }
+ searchController.searchResultsUpdater = self
+ searchController.dimsBackgroundDuringPresentation = false
+ searchController.searchBar.sizeToFit()
+
+ // Add UISearchController to the tableView
+ tableView.tableHeaderView = searchController.searchBar
+ tableView.tableHeaderView?.backgroundColor = UIColor.clear
+ definesPresentationContext = true
+ searchController.hidesNavigationBarDuringPresentation = false
+
+ // Style the UISearchController
+ searchController.searchBar.barTintColor = UIColor.clear
+ searchController.searchBar.tintColor = UIColor.white
+
+ // Hide the UISearchController
+ tableView.setContentOffset(CGPoint(x: 0.0, y: searchController.searchBar.frame.size.height), animated: false)
+
+ // Set a black keyborad for UISearchController's TextField
+ let searchTextField = searchController.searchBar.value(forKey: "_searchField") as! UITextField
+ searchTextField.keyboardAppearance = UIKeyboardAppearance.dark
+ }
+
+ func updateSearchResults(for searchController: UISearchController) {
+ guard let searchText = searchController.searchBar.text else { return }
+
+ searchedStations.removeAll(keepingCapacity: false)
+ searchedStations = stations.filter { $0.name.range(of: searchText, options: [.caseInsensitive]) != nil }
+ self.tableView.reloadData()
}
}
//*****************************************************************
-// MARK: - NowPlayingViewControllerDelegate
+// MARK: - RadioPlayerDelegate
//*****************************************************************
-extension StationsViewController: NowPlayingViewControllerDelegate {
+extension StationsViewController: RadioPlayerDelegate {
- func artworkDidUpdate(track: Track) {
- currentTrack?.artworkURL = track.artworkURL
- currentTrack?.artworkImage = track.artworkImage
+ func playerStateDidChange(_ playerState: FRadioPlayerState) {
+ nowPlayingViewController?.playerStateDidChange(playerState, animate: true)
}
- func songMetaDataDidUpdate(track: Track) {
- currentTrack = track
- let title = currentStation!.stationName + ": " + currentTrack!.title + " - " + currentTrack!.artist + "..."
- stationNowPlayingButton.setTitle(title, for: .normal)
+ func playbackStateDidChange(_ playbackState: FRadioPlaybackState) {
+ nowPlayingViewController?.playbackStateDidChange(playbackState, animate: true)
+ startNowPlayingAnimation(radioPlayer.player.isPlaying)
}
- func trackPlayingToggled(track: Track) {
- currentTrack?.isPlaying = track.isPlaying
+ func trackDidUpdate(_ track: Track?) {
+ updateLockScreen(with: track)
+ updateNowPlayingButton(station: radioPlayer.station, track: track)
+ updateHandoffUserActivity(userActivity, station: radioPlayer.station, track: track)
+ nowPlayingViewController?.updateTrackMetadata(with: track)
+ }
+
+ func trackArtworkDidUpdate(_ track: Track?) {
+ updateLockScreen(with: track)
+ nowPlayingViewController?.updateTrackArtwork(with: track)
}
-
}
//*****************************************************************
-// MARK: - UISearchControllerDelegate
+// MARK: - Handoff Functionality - GH
//*****************************************************************
-extension StationsViewController: UISearchResultsUpdating {
-
- func updateSearchResults(for searchController: UISearchController) {
+extension StationsViewController {
- // Empty the searchedStations array
- searchedStations.removeAll(keepingCapacity: false)
+ func setupHandoffUserActivity() {
+ userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
+ userActivity?.becomeCurrent()
+ }
- // Create a Predicate
- let searchPredicate = NSPredicate(format: "SELF.stationName CONTAINS[c] %@", searchController.searchBar.text!)
+ func updateHandoffUserActivity(_ activity: NSUserActivity?, station: RadioStation?, track: Track?) {
+ guard let activity = activity else { return }
+ activity.webpageURL = (track?.title == station?.name) ? nil : getHandoffURL(from: track)
+ updateUserActivityState(activity)
+ }
- // Create an NSArray with a Predicate
- let array = (self.stations as NSArray).filtered(using: searchPredicate)
+ override func updateUserActivityState(_ activity: NSUserActivity) {
+ super.updateUserActivityState(activity)
+ }
- // Set the searchedStations with search result array
- searchedStations = array as! [RadioStation]
+ private func getHandoffURL(from track: Track?) -> URL? {
+ guard let track = track else { return nil }
+
+ var components = URLComponents()
+ components.scheme = "https"
+ components.host = "google.com"
+ components.path = "/search"
+ components.queryItems = [URLQueryItem]()
+ components.queryItems?.append(URLQueryItem(name: "q", value: "\(track.artist) \(track.title)"))
+ return components.url
+ }
+}
+
+//*****************************************************************
+// MARK: - NowPlayingViewControllerDelegate
+//*****************************************************************
+
+extension StationsViewController: NowPlayingViewControllerDelegate {
- // Reload the tableView
- self.tableView.reloadData()
+ func didPressPlayingButton() {
+ radioPlayer.player.togglePlaying()
+ }
+
+ func didPressStopButton() {
+ radioPlayer.player.stop()
+ }
+
+ func didPressNextButton() {
+ guard let index = getIndex(of: radioPlayer.station) else { return }
+ radioPlayer.station = (index + 1 == stations.count) ? stations[0] : stations[index + 1]
+ handleRemoteStationChange()
}
+ func didPressPreviousButton() {
+ guard let index = getIndex(of: radioPlayer.station) else { return }
+ radioPlayer.station = (index == 0) ? stations.last : stations[index - 1]
+ handleRemoteStationChange()
+ }
+
+ private func handleRemoteStationChange() {
+ if let nowPlayingVC = nowPlayingViewController {
+ // If nowPlayingVC is presented
+ nowPlayingVC.load(station: radioPlayer.station, track: radioPlayer.track)
+ nowPlayingVC.stationDidChange()
+ } else if let station = radioPlayer.station {
+ // If nowPlayingVC is not presented (change from remote controls)
+ radioPlayer.player.radioURL = URL(string: station.streamURL)
+ }
+ }
}
diff --git a/SwiftRadio/SwiftRadio-Settings.swift b/SwiftRadio/SwiftRadio-Settings.swift
index 7d8b1a2e..b6743752 100755
--- a/SwiftRadio/SwiftRadio-Settings.swift
+++ b/SwiftRadio/SwiftRadio-Settings.swift
@@ -33,16 +33,8 @@ let stationDataURL = "http://yoururl.com/json/stations.json"
let searchable = false
//**************************************
-// LASTFM API
+// NEXT / PREVIOUS BUTTONS
//**************************************
-// Use LastFM or iTunes API
-// set to "false" to use iTunes
-let useLastFM = true
-
-// IF YOU USE LASTFM, PLEASE USE YOUR OWN KEY
-// Visit: http://www.last.fm/api
-
-let apiKey = "9a267c245324cfa4f887366d497d3dd3"
-let apiSecret = "f1191864d7ae71e580b89238129768b8"
-
+// Set this to "false" to show the next/previous player buttons
+let hideNextPreviousButtons = true
diff --git a/SwiftRadio/Track.swift b/SwiftRadio/Track.swift
index 8065361f..37a3baec 100755
--- a/SwiftRadio/Track.swift
+++ b/SwiftRadio/Track.swift
@@ -13,10 +13,13 @@ import UIKit
//*****************************************************************
struct Track {
- var title: String = ""
- var artist: String = ""
- var artworkURL: String = ""
- var artworkImage = UIImage(named: "albumArt")
- var artworkLoaded = false
- var isPlaying: Bool = false
-}
\ No newline at end of file
+ var title: String
+ var artist: String
+ var artworkImage: UIImage?
+ var artworkLoaded = false
+
+ init(title: String, artist: String) {
+ self.title = title
+ self.artist = artist
+ }
+}
diff --git a/SwiftRadio/UIImageView+Download.swift b/SwiftRadio/UIImageView+Download.swift
index e55884c4..d584afca 100755
--- a/SwiftRadio/UIImageView+Download.swift
+++ b/SwiftRadio/UIImageView+Download.swift
@@ -10,7 +10,7 @@ import UIKit
extension UIImageView {
- func loadImageWithURL(url: URL, callback: @escaping (UIImage) -> ()) -> URLSessionDownloadTask {
+ func loadImageWithURL(url: URL, callback: @escaping (UIImage) -> ()) {
let session = URLSession.shared
let downloadTask = session.downloadTask(with: url, completionHandler: {
@@ -33,7 +33,6 @@ extension UIImageView {
})
downloadTask.resume()
- return downloadTask
}
}
diff --git a/SwiftRadio/stations.json b/SwiftRadio/stations.json
index c1ca1332..b1bef30a 100755
--- a/SwiftRadio/stations.json
+++ b/SwiftRadio/stations.json
@@ -2,18 +2,25 @@
"station":
[
{
- "name": "Sub Pop Radio",
- "streamURL": "http://djboyradio.streamguys1.com/sub-pop-records.mp3",
- "imageURL": "station-sub.png",
- "desc": "Midsized Record Label",
- "longDesc": "Sub Pop is a record label founded in 1986 by Bruce Pavitt. In 1988, Sub Pop Records LLC was formed by Bruce Pavitt and Jonathan Poneman in Seattle, Washington. Sub Pop achieved fame in the late 1980s for first signing Nirvana, Soundgarden, Mudhoney and many other bands from the Seattle alternative rock scene."
+ "name": "Absolute Country Hits",
+ "streamURL": "http://strm112.1.fm/acountry_mobile_mp3",
+ "imageURL": "station-absolutecountry.png",
+ "desc": "The Music Starts Here",
+ "longDesc": "All your favorite country hits and artists, from Johnny Cash to Taylor Swift, on 1.FM's Absolute Country, playing non-stop crooners and banjos, dance-tunes and fiddles, ballads and harmonicas. Absolute Country focuses on 5th, 6th and 7th generation Country (from the 90s on) but often delves into classic, older tunes as well."
},
{
- "name": "Sounds of the 80s",
- "streamURL": "http://tuneinads.moodmedia.com/streams/tunein_sounds_of_the_80s_with_ads.mp3",
- "imageURL": "station-80s",
- "desc": "Totally Rad",
- "longDesc": "80s Baby! The '80s is not about nostalgia; it is about a decade of people, decisions and inventions that changed our future, told from the perspective of unknowing history makers who lived these iconic moments."
+ "name": "Newport Folk Radio",
+ "streamURL": "http://rfcmedia.streamguys1.com/Newport.mp3",
+ "imageURL": "station-newportfolk.png",
+ "desc": "Are you ready to Folk?",
+ "longDesc": "Do you like Indie music and along with that wants a radio that will provide lots of folk music in their daily radio programs than wait not just tune in to Newport Folk Radio as this is the kind of radio that has got lots of types of Indie music in their daily radio programs along with popular Folk music."
+ },
+ {
+ "name": "The Alt Vault",
+ "streamURL": "http://jupiter.prostreaming.net/altmixxlow",
+ "imageURL": "station-altvault.png",
+ "desc": "Your Lifestyle... Your Music!",
+ "longDesc": "The Alt Vault live broadcasting from Kissimmee, FL. The Alt Vault broadcast various kind of 90s’s pop, rock, classic, talk, culture, dance, electronic etc. The Alt Vault streaming music and programs both in online. The Alt Vault is 24 hour 7 day live Online radio."
},
{
"name": "Classic Rock",
@@ -30,4 +37,4 @@
"longDesc": "Radio 1190 is the bomb."
}
]
-}
\ No newline at end of file
+}