├── .github └── workflows │ ├── carplay.yml │ └── ios.yml ├── .gitignore ├── LICENSE ├── README.md ├── SwiftRadio.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ ├── jonahss.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ └── wu.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── SwiftRadio.xcscheme ├── SwiftRadio ├── AppDelegate.swift ├── Base.lproj │ └── LaunchScreen.xib ├── CarPlay │ └── AppDelegate+CarPlay.swift ├── Cells │ ├── NothingFoundCell.xib │ └── StationTableViewCell.swift ├── Config.swift ├── Coordinators │ ├── Coordinator.swift │ └── MainCoordinator.swift ├── Data │ └── stations.json ├── Helpers │ ├── AnimationFrames.swift │ ├── Bundle+appName.swift │ ├── Handoffable.swift │ ├── ShareActivity.swift │ ├── Storyboard.swift │ ├── UIImage+Cache.swift │ ├── UIImage+DropShadow.swift │ ├── UIImageView+Cache.swift │ ├── UITableViewCell+reuseIdentifier.swift │ └── UIViewController+Email.swift ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-29.png │ │ ├── Icon-29@2x-1.png │ │ ├── Icon-29@2x.png │ │ ├── Icon-29@3x.png │ │ ├── Icon-40.png │ │ ├── Icon-40@2x-1.png │ │ ├── Icon-40@2x.png │ │ ├── Icon-40@3x.png │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-76.png │ │ ├── Icon-76@2x.png │ │ ├── Icon-83.5@2x.png │ │ └── SWIFT-RADIO.png │ ├── Contents.json │ ├── LaunchImage.launchimage │ │ ├── 4s.png │ │ ├── 5C.png │ │ ├── 6plus.png │ │ ├── Contents.json │ │ └── Portrait6.png │ ├── NowPlayingBars-0.imageset │ │ ├── Contents.json │ │ ├── NowPlayingBars-0.png │ │ ├── NowPlayingBars-0@2x.png │ │ └── NowPlayingBars-0@3x.png │ ├── NowPlayingBars-1.imageset │ │ ├── Contents.json │ │ ├── NowPlayingBars-1.png │ │ ├── NowPlayingBars-1@2x.png │ │ └── NowPlayingBars-1@3x.png │ ├── NowPlayingBars-2.imageset │ │ ├── Contents.json │ │ ├── NowPlayingBars-2.png │ │ ├── NowPlayingBars-2@2x.png │ │ └── NowPlayingBars-2@3x.png │ ├── NowPlayingBars-3.imageset │ │ ├── Contents.json │ │ ├── NowPlayingBars-3.png │ │ ├── NowPlayingBars-3@2x.png │ │ └── NowPlayingBars-3@3x.png │ ├── NowPlayingBars.imageset │ │ ├── Contents.json │ │ ├── NowPlayingBars.png │ │ ├── NowPlayingBars@2x.png │ │ └── NowPlayingBars@3x.png │ ├── Stations │ │ ├── Contents.json │ │ ├── az-rock-radio.imageset │ │ │ ├── Contents.json │ │ │ ├── az-rock-radio.png │ │ │ ├── az-rock-radio@2x.png │ │ │ └── az-rock-radio@3x.png │ │ ├── station-80s.imageset │ │ │ ├── Contents.json │ │ │ └── station-80s.png │ │ ├── station-absolutecountry.imageset │ │ │ ├── Contents.json │ │ │ ├── station-absolutecountry.png │ │ │ └── station-absolutecountry@2x.png │ │ ├── station-altvault.imageset │ │ │ ├── Contents.json │ │ │ ├── station-altvault.png │ │ │ └── station-altvault@2x.png │ │ ├── station-classicrock.imageset │ │ │ ├── Contents.json │ │ │ └── station-classicrock.png │ │ ├── station-killrockstars.imageset │ │ │ ├── Contents.json │ │ │ └── station-killrockstars.png │ │ ├── station-newportfolk.imageset │ │ │ ├── Contents.json │ │ │ ├── station-newportfolk.png │ │ │ └── station-newportfolk@2x.png │ │ ├── station-spaceland.imageset │ │ │ ├── Contents.json │ │ │ └── station-spaceland.png │ │ ├── station-sub.imageset │ │ │ ├── Contents.json │ │ │ └── sub.png │ │ ├── station-therockfm.imageset │ │ │ ├── Contents.json │ │ │ ├── station-therockfm.png │ │ │ ├── station-therockfm@2x.png │ │ │ └── station-therockfm@3x.png │ │ └── stationImage.imageset │ │ │ ├── Contents.json │ │ │ ├── stationImage.png │ │ │ ├── stationImage@2x.png │ │ │ └── stationImage@3x.png │ ├── albumArt.imageset │ │ ├── Contents.json │ │ ├── albumArt.png │ │ ├── albumArt@2x.png │ │ └── albumArt@3x.png │ ├── background.imageset │ │ ├── Contents.json │ │ ├── background.png │ │ ├── background@2x.png │ │ └── background@3x.png │ ├── btn-close.imageset │ │ ├── Contents.json │ │ ├── btn-close.png │ │ ├── btn-close@2x.png │ │ └── btn-close@3x.png │ ├── btn-next.imageset │ │ ├── Contents.json │ │ ├── btn-next.png │ │ ├── btn-next@2x.png │ │ └── btn-next@3x.png │ ├── btn-nowPlaying.imageset │ │ ├── Contents.json │ │ ├── btn-nowPlaying.png │ │ └── btn-nowPlaying@2x.png │ ├── btn-pause.imageset │ │ ├── Contents.json │ │ ├── btn-pause.png │ │ ├── btn-pause@2x.png │ │ └── btn-pause@3x.png │ ├── btn-play.imageset │ │ ├── Contents.json │ │ ├── btn-play.png │ │ ├── btn-play@2x.png │ │ └── btn-play@3x.png │ ├── btn-previous.imageset │ │ ├── Contents.json │ │ ├── btn-previous.png │ │ ├── btn-previous@2x.png │ │ └── btn-previous@3x.png │ ├── btn-stop.imageset │ │ ├── Contents.json │ │ ├── btn-stop.png │ │ ├── btn-stop@2x.png │ │ └── btn-stop@3x.png │ ├── carPlayTab.imageset │ │ ├── Contents.json │ │ ├── carPlayTab.png │ │ ├── carPlayTab@2x.png │ │ └── carPlayTab@3x.png │ ├── icon-hamburger.imageset │ │ ├── Contents.json │ │ ├── icon-hamburger.png │ │ ├── icon-hamburger@2x.png │ │ └── icon-hamburger@3x.png │ ├── icon-info.imageset │ │ ├── Contents.json │ │ ├── icon-info.png │ │ ├── icon-info@2x.png │ │ └── icon-info@3x.png │ ├── logo.imageset │ │ ├── Contents.json │ │ ├── swift-radio-1.png │ │ ├── swift-radio-2.png │ │ └── swift-radio.png │ ├── share.imageset │ │ ├── Contents.json │ │ ├── share.png │ │ ├── share@2x.png │ │ └── share@3x.png │ ├── slider-ball.imageset │ │ ├── Contents.json │ │ ├── slider-ball.png │ │ ├── slider-ball@2x.png │ │ └── slider-ball@3x.png │ ├── swift-radio-black.imageset │ │ ├── Contents.json │ │ ├── swift-radio-black.png │ │ └── swift-radio-black@2x.png │ ├── vol-max.imageset │ │ ├── Contents.json │ │ ├── vol-max.png │ │ ├── vol-max@2x.png │ │ └── vol-max@3x.png │ └── vol-min.imageset │ │ ├── Contents.json │ │ ├── vol-min.png │ │ ├── vol-min@2x.png │ │ └── vol-min@3x.png ├── Info-CarPlay.plist ├── Info.plist ├── LaunchScreen.storyboard ├── Main.storyboard ├── Model │ ├── RadioStation.swift │ └── StationsManager.swift ├── Networking │ └── DataManager.swift ├── SwiftRadio.entitlements ├── ViewControllers │ ├── AboutViewController.swift │ ├── BaseController.swift │ ├── InfoDetailViewController.swift │ ├── LoaderController.swift │ ├── NowPlayingViewController.swift │ ├── PopUpMenuViewController.swift │ └── StationsViewController.swift └── Views │ ├── LogoShareView.swift │ ├── LogoShareView.xib │ └── NowPlayingView.swift └── SwiftRadioUITests ├── Info.plist └── SwiftRadioUITests.swift /.github/workflows/carplay.yml: -------------------------------------------------------------------------------- 1 | name: CarPlay build 2 | 3 | on: 4 | push: 5 | branches: [ "master"] 6 | pull_request: 7 | branches: [ "master", "dev" ] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test The SwiftRadio-CarPlay using any available iPhone simulator 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Set Default Scheme 18 | run: | 19 | scheme_list=$(xcodebuild -list -json | tr -d "\n") 20 | default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][1]") 21 | echo $default | cat >default 22 | echo Using default scheme: $default 23 | - name: Build 24 | env: 25 | scheme: ${{ 'default' }} 26 | platform: ${{ 'iOS Simulator' }} 27 | run: | 28 | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) 29 | device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` 30 | if [ $scheme = default ]; then scheme=$(cat default); fi 31 | if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi 32 | file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` 33 | xcodebuild build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" 34 | -------------------------------------------------------------------------------- /.github/workflows/ios.yml: -------------------------------------------------------------------------------- 1 | name: iOS build 2 | 3 | on: 4 | push: 5 | branches: [ "master"] 6 | pull_request: 7 | branches: [ "master", "dev" ] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test the SwiftRadio iOS target using any available iPhone simulator 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Set Default Scheme 18 | run: | 19 | scheme_list=$(xcodebuild -list -json | tr -d "\n") 20 | default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]") 21 | echo $default | cat >default 22 | echo Using default scheme: $default 23 | - name: Build 24 | env: 25 | scheme: ${{ 'default' }} 26 | platform: ${{ 'iOS Simulator' }} 27 | run: | 28 | # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) 29 | device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` 30 | if [ $scheme = default ]; then scheme=$(cat default); fi 31 | if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi 32 | file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` 33 | xcodebuild build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" 34 | # Disable testing for now 35 | # - name: Test 36 | # env: 37 | # scheme: ${{ 'default' }} 38 | # platform: ${{ 'iOS Simulator' }} 39 | # run: | 40 | # # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) 41 | # device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` 42 | # if [ $scheme = default ]; then scheme=$(cat default); fi 43 | # if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi 44 | # file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` 45 | # xcodebuild test-without-building -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.xccheckout 3 | *.xcscmblueprint 4 | xcuserdata 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matthew Fecher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | ### -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Radio 2 | 3 | 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 14 branch**. 4 | 5 | There are over 80 different apps accepted to the app store using this code! 6 | 7 |

8 | Swift Radio 9 |

10 | 11 | ## Video 12 | View this [**GETTING STARTED VIDEO**](https://youtu.be/m7jiajCHFvc). 13 | It's short & sweet to give you a quick overview. 14 | Give it a quick watch. 15 | 16 | ## Features 17 | 18 | - Ability to update Stations from server or locally. (Update stations anytime without resubmitting to app store!) 19 | - Displays Artist, Track & Album Art on Lock Screen 20 | - Custom views optimized for SE, 6 and 6+ for backwards compatibility 21 | - Compiles with Xcode 14 & Swift 5 22 | - Parses JSON using Swift Codable protocol 23 | - Background audio performance 24 | - Search Bar that can be turned on or off to search stations 25 | - Supports local or hosted station images 26 | - "About" screen with ability to send email & visit website 27 | - Pull to Refresh stations 28 | - Uses the AVPlayer wrapper library [FRadioPlayer](https://github.com/fethica/FRadioPlayer): 29 | * Automatically download Album Art from iTunes API 30 | * Parses metadata from streams (Track & Artist information) 31 | - Uses [Spring](https://github.com/MengTo/Spring) library: 32 | * Animate UI components 33 | * Download and cache images using ImageLoader class 34 | 35 | ## Credits 36 | - **Co-organizer & current-lead developer [Fethi El Hassasna](https://fethica.com), Twitter: [@fethica](https://twitter.com/fethica)** 37 | - **Created by [Matthew Fecher](http://matthewfecher.com) from [AudioKit Pro](https://audiokitpro.com), Twitter: [@analogMatthew](http://twitter.com/analogMatthew)** 38 | - *Contributions by others listed in Github [here](https://github.com/swiftcodex/Swift-Radio-Pro/graphs/contributors).* 39 | Thanks to everyone! We couldn't do it without you! 40 | 41 | ## Requirements 42 | 43 | - Xcode 14 44 | - Know a little bit of how to program in Swift with the iOS SDK 45 | 46 | Please note: I am unable to offer any free support or modifications. Thanks! 47 | 48 | ## Creating an App 49 | 50 | If you create an app with the code, or interesting project inspired by the code, shoot me an email. I love hearing about your projects! 51 | 52 | This is just a basic template. You may use it as a clean starting point to add other features. 53 | 54 | Some of the things I've built into this Radio code for clients include: Facebook login, Profiles, Saving Favorite Tracks, Playlists, Genres, Spotify integration, Enhanced Streaming, Tempo Analyzing, etc. There's almost unlimited things you can use this code as a starting place for. I keep this repo lightweight. That way you can customize it easily. 55 | 56 | ## Setup 57 | 58 | The "Config.swift" file contains some project configs to get you started. 59 | Watch this [Getting Started Video](https://youtu.be/m7jiajCHFvc) to get up & running quickly. 60 | 61 | ## Integration 62 | 63 | Includes full Xcode Project to jumpstart development. 64 | 65 | ## Stations 66 | 67 | Includes an example "stations.json" file. You may upload the JSON file to a server, so that you can update the stations in the app without resubmitting to the app store. The following fields are supported in the app: 68 | 69 | - **name**: The name of the station as you want it displayed (e.g. "Sub Pop Radio") 70 | 71 | - **streamURL**: The url of the actual stream 72 | 73 | - **imageURL**: Station image url. Station images in demo are 350x206. Image can be local or hosted. Leave out the "http" to use a local image (You can use either: "station-subpop" or "http://myurl.com/images/station-subpop.jpg") 74 | 75 | - **desc**: Short 2 or 3 word description of the station as you want it displayed (e.g. "Outlaw Country") 76 | 77 | - **longDesc**: Long description of the station to be used on the "info screen". This is optional. 78 | 79 | ## Contributions 80 | 81 | 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. 82 | 83 | ## FAQ 84 | 85 | Q: Do I have to pay you anything if I make an app with this code? 86 | A: Nope. This is completely open source, you can do whatever you want with it. It's usually cool to thank the project if you use the code. Go build stuff. Enjoy. 87 | 88 | Q: How do I make my app support ipv6 networks? 89 | 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. 90 | 91 | Q: Is there an example of using this with the Spotify API? 92 | A: Yes, there is a branch here that uses it [here]( https://github.com/swiftcodex/Swift-Radio-Pro/tree/avplayer) (⚠️ **deprecated**). 93 | 94 | Q: Is there another API to get album/track information besides LastFM, Spotify, and iTunes? 95 | 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. 96 | 97 | Q: I updated the album art size in the Storyboard, and now the sizing is acting funny? 98 | A: There is an albumArt constraint modified in the code. See the "optimizeForDeviceSize()" method in the NowPlayingVC. 99 | 100 | Q: My radio station isn't playing? 101 | A: Paste your stream URL into a browser to see if it will play there. The stream may be offline or have a weak connection. 102 | 103 | Q: Can you help me add a feature? Can you help me understand the code? Can you help with a problem I'm having? 104 | 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! 105 | 106 | Q: The song names aren't appearing for my station? 107 | 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?)`. 108 | 109 | ## Get Single Station Code 110 | If you'd like to support this project, co-organizer Fethi has created a well-architected single station version of this code. It's a super great bargain: The developers behind this project typically charge up to $200/hr for freelance work, but this fully working code is only $50. No extra fees. 111 | 112 | You can PayPal: [fethica@me.com](mailto:fethica@me.com) or use this link: [Paypal Me](https://www.paypal.me/fethicaEH) 113 | We will send you the code after 24 hours with setup instructions. All funds go to support the project. 114 | 115 | **Need something more advanced?** We have recent experience building iOS apps for high-profile brands. Send a friendly email to [Matthew](mailto:matthew@audiokitpro.com) or [Fethi](mailto:contact@fethica.com). 116 | 117 | 118 | ## RadioKit SDK Example 119 | 120 | - You can use this Swift code as a front-end for a more robust streaming backend. 121 | - Brian Stormont, creator of RadioKit, has created a branch with the professional [RadioKit](http://stormyprods.com/products/radiokit.php) SDK already integrated. **Plus, his branch adds rewind & fast forward stream playback.** This is an excellent learning tool for those who are interested in seeing how a streaming library integrates with Swift Radio Pro. View the [branch here](https://github.com/MostTornBrain/Swift-Radio-Pro/tree/RadioKit). 122 | -------------------------------------------------------------------------------- /SwiftRadio.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftRadio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftRadio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "fradioplayer", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/fethica/FRadioPlayer.git", 7 | "state" : { 8 | "branch" : "v0.2.0", 9 | "revision" : "c3f472ed8cb59b442312362bca363863962c88b3" 10 | } 11 | }, 12 | { 13 | "identity" : "nvactivityindicatorview", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/ninjaprox/NVActivityIndicatorView.git", 16 | "state" : { 17 | "revision" : "bcb52371f2259254bac6690f92bb474a61768c47", 18 | "version" : "5.1.1" 19 | } 20 | }, 21 | { 22 | "identity" : "spring", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/fethica/Spring.git", 25 | "state" : { 26 | "branch" : "master", 27 | "revision" : "63d6b2b4339606181617caddea3a988aa65bcadd" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/jonahss.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/jonahss.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges 6 | 7 | SnapshotAutomaticallyBeforeSignificantChanges 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SwiftRadio.xcodeproj/xcshareddata/xcschemes/SwiftRadio.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /SwiftRadio/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Swift Radio 4 | // 5 | // Created by Matthew Fecher on 7/2/15. 6 | // Copyright (c) 2015 MatthewFecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MediaPlayer 11 | import FRadioPlayer 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | var coordinator: MainCoordinator? 18 | 19 | // CarPlay 20 | var playableContentManager: MPPlayableContentManager? 21 | 22 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 23 | 24 | // FRadioPlayer config 25 | FRadioPlayer.shared.isAutoPlay = true 26 | FRadioPlayer.shared.enableArtwork = true 27 | FRadioPlayer.shared.artworkAPI = iTunesAPI(artworkSize: 600) 28 | 29 | // AudioSession & RemotePlay 30 | activateAudioSession() 31 | setupRemoteCommandCenter() 32 | UIApplication.shared.beginReceivingRemoteControlEvents() 33 | 34 | // Make status bar white 35 | UINavigationBar.appearance().barStyle = .black 36 | UINavigationBar.appearance().tintColor = .white 37 | UINavigationBar.appearance().prefersLargeTitles = true 38 | 39 | // `CarPlay` is defined only in SwiftRadio-CarPlay target: 40 | // Build Settings > Swift Compiler - Custom Flags 41 | #if CarPlay 42 | setupCarPlay() 43 | #endif 44 | 45 | // Start the coordinator 46 | coordinator = MainCoordinator(navigationController: UINavigationController()) 47 | 48 | window = UIWindow(frame: UIScreen.main.bounds) 49 | window?.rootViewController = coordinator?.navigationController 50 | window?.makeKeyAndVisible() 51 | 52 | coordinator?.start() 53 | 54 | return true 55 | } 56 | 57 | func applicationWillResignActive(_ application: UIApplication) { 58 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 59 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 60 | 61 | } 62 | 63 | func applicationDidEnterBackground(_ application: UIApplication) { 64 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 65 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 66 | 67 | 68 | } 69 | 70 | func applicationWillEnterForeground(_ application: UIApplication) { 71 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 72 | 73 | } 74 | 75 | func applicationDidBecomeActive(_ application: UIApplication) { 76 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 77 | 78 | 79 | } 80 | 81 | func applicationWillTerminate(_ application: UIApplication) { 82 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 83 | // Saves changes in the application's managed object context before the application terminates. 84 | 85 | UIApplication.shared.endReceivingRemoteControlEvents() 86 | 87 | } 88 | 89 | // MARK: - Remote Controls 90 | 91 | private func setupRemoteCommandCenter() { 92 | // Get the shared MPRemoteCommandCenter 93 | let commandCenter = MPRemoteCommandCenter.shared() 94 | 95 | // Add handler for Play Command 96 | commandCenter.playCommand.addTarget { event in 97 | FRadioPlayer.shared.play() 98 | return .success 99 | } 100 | 101 | // Add handler for Pause Command 102 | commandCenter.pauseCommand.addTarget { event in 103 | FRadioPlayer.shared.pause() 104 | return .success 105 | } 106 | 107 | // Add handler for Toggle Command 108 | commandCenter.togglePlayPauseCommand.addTarget { event in 109 | FRadioPlayer.shared.togglePlaying() 110 | return .success 111 | } 112 | 113 | // Add handler for Next Command 114 | commandCenter.nextTrackCommand.addTarget { event in 115 | StationsManager.shared.setNext() 116 | return .success 117 | } 118 | 119 | // Add handler for Previous Command 120 | commandCenter.previousTrackCommand.addTarget { event in 121 | StationsManager.shared.setPrevious() 122 | return .success 123 | } 124 | } 125 | 126 | // MARK: - Activate Audio Session 127 | 128 | private func activateAudioSession() { 129 | do { 130 | try AVAudioSession.sharedInstance().setActive(true) 131 | } catch let error { 132 | if Config.debugLog { 133 | print("audioSession could not be activated: \(error.localizedDescription)") 134 | } 135 | } 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /SwiftRadio/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /SwiftRadio/CarPlay/AppDelegate+CarPlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+CarPlay.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2019-02-02. 6 | // Copyright © 2019 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MediaPlayer 11 | 12 | // MARK: - CarPlay Setup 13 | 14 | extension AppDelegate { 15 | 16 | func setupCarPlay() { 17 | playableContentManager = MPPlayableContentManager.shared() 18 | 19 | playableContentManager?.delegate = self 20 | playableContentManager?.dataSource = self 21 | 22 | StationsManager.shared.addObserver(self) 23 | } 24 | } 25 | 26 | // MARK: - MPPlayableContentDelegate 27 | 28 | extension AppDelegate: MPPlayableContentDelegate { 29 | 30 | func playableContentManager(_ contentManager: MPPlayableContentManager, initiatePlaybackOfContentItemAt indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) { 31 | 32 | DispatchQueue.main.async { 33 | if indexPath.count == 2 { 34 | let station = StationsManager.shared.stations[indexPath[1]] 35 | StationsManager.shared.set(station: station) 36 | MPPlayableContentManager.shared().nowPlayingIdentifiers = [station.name] 37 | } 38 | completionHandler(nil) 39 | } 40 | } 41 | 42 | func beginLoadingChildItems(at indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) { 43 | StationsManager.shared.fetch { result in 44 | guard case .failure(let error) = result else { 45 | completionHandler(nil) 46 | return 47 | } 48 | 49 | completionHandler(error) 50 | } 51 | } 52 | } 53 | 54 | // MARK: - MPPlayableContentDataSource 55 | 56 | extension AppDelegate: MPPlayableContentDataSource { 57 | 58 | func numberOfChildItems(at indexPath: IndexPath) -> Int { 59 | if indexPath.indices.count == 0 { 60 | return 1 61 | } 62 | 63 | return StationsManager.shared.stations.count 64 | } 65 | 66 | func contentItem(at indexPath: IndexPath) -> MPContentItem? { 67 | 68 | if indexPath.count == 1 { 69 | // Tab section 70 | let item = MPContentItem(identifier: "Stations") 71 | item.title = "Stations" 72 | item.isContainer = true 73 | item.isPlayable = false 74 | item.artwork = MPMediaItemArtwork(boundsSize: #imageLiteral(resourceName: "carPlayTab").size, requestHandler: { _ -> UIImage in 75 | return #imageLiteral(resourceName: "carPlayTab") 76 | }) 77 | return item 78 | } else if indexPath.count == 2, indexPath.item < StationsManager.shared.stations.count { 79 | 80 | // Stations section 81 | let station = StationsManager.shared.stations[indexPath.item] 82 | 83 | let item = MPContentItem(identifier: "\(station.name)") 84 | item.title = station.name 85 | item.subtitle = station.desc 86 | item.isPlayable = true 87 | item.isStreamingContent = true 88 | station.getImage { image in 89 | item.artwork = MPMediaItemArtwork(boundsSize: image.size) { _ -> UIImage in 90 | return image 91 | } 92 | } 93 | 94 | return item 95 | } else { 96 | return nil 97 | } 98 | } 99 | } 100 | 101 | // MARK: - StationsManagerObserver 102 | 103 | extension AppDelegate: StationsManagerObserver { 104 | 105 | func stationsManager(_ manager: StationsManager, stationsDidUpdate stations: [RadioStation]) { 106 | playableContentManager?.reloadData() 107 | } 108 | 109 | func stationsManager(_ manager: StationsManager, stationDidChange station: RadioStation?) { 110 | guard let station = station else { 111 | playableContentManager?.nowPlayingIdentifiers = [] 112 | return 113 | } 114 | 115 | playableContentManager?.nowPlayingIdentifiers = [station.name] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /SwiftRadio/Cells/NothingFoundCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /SwiftRadio/Cells/StationTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StationTableViewCell.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2023-06-24. 6 | // Copyright © 2023 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import NVActivityIndicatorView 11 | 12 | class StationTableViewCell: UITableViewCell { 13 | 14 | let stationImageView: UIImageView = { 15 | let imageView = UIImageView() 16 | imageView.contentMode = .scaleAspectFill 17 | imageView.clipsToBounds = true 18 | NSLayoutConstraint.activate([ 19 | imageView.heightAnchor.constraint(equalToConstant: 75), 20 | imageView.widthAnchor.constraint(equalToConstant: 110) 21 | ]) 22 | return imageView 23 | }() 24 | 25 | let titleLabel: UILabel = { 26 | let label = UILabel() 27 | label.font = .preferredFont(forTextStyle: .title3) 28 | label.numberOfLines = 2 29 | label.translatesAutoresizingMaskIntoConstraints = false 30 | return label 31 | }() 32 | 33 | let subtitleLabel: UILabel = { 34 | let label = UILabel() 35 | label.font = .preferredFont(forTextStyle: .footnote) 36 | label.translatesAutoresizingMaskIntoConstraints = false 37 | return label 38 | }() 39 | 40 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 41 | super.init(style: style, reuseIdentifier: reuseIdentifier) 42 | setupViews() 43 | } 44 | 45 | override func prepareForReuse() { 46 | super.prepareForReuse() 47 | titleLabel.text = nil 48 | subtitleLabel.text = nil 49 | stationImageView.image = nil 50 | } 51 | 52 | required init?(coder: NSCoder) { 53 | fatalError("init(coder:) has not been implemented") 54 | } 55 | 56 | private func setupViews() { 57 | 58 | selectionStyle = .default 59 | 60 | let vStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) 61 | vStackView.spacing = 8 62 | vStackView.axis = .vertical 63 | vStackView.translatesAutoresizingMaskIntoConstraints = false 64 | 65 | let hStackView = UIStackView(arrangedSubviews: [stationImageView, vStackView]) 66 | hStackView.spacing = 8 67 | hStackView.axis = .horizontal 68 | hStackView.alignment = .center 69 | hStackView.translatesAutoresizingMaskIntoConstraints = false 70 | 71 | contentView.addSubview(hStackView) 72 | 73 | NSLayoutConstraint.activate([ 74 | hStackView.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor), 75 | hStackView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), 76 | hStackView.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor), 77 | hStackView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor) 78 | ]) 79 | } 80 | } 81 | 82 | extension StationTableViewCell { 83 | func configureStationCell(station: RadioStation) { 84 | 85 | // Configure the cell... 86 | titleLabel.text = station.name 87 | subtitleLabel.text = station.desc 88 | 89 | station.getImage { [weak self] image in 90 | self?.stationImageView.image = image 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /SwiftRadio/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRadio-Settings.swift 3 | // Swift Radio 4 | // 5 | // Created by Matthew Fecher on 7/2/15. 6 | // Copyright (c) 2015 MatthewFecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct Config { 12 | 13 | static let debugLog = true 14 | 15 | // If this is set to "true", it will use the JSON file in the app 16 | // Set it to "false" to use the JSON file at the stationDataURL 17 | static let useLocalStations = true 18 | static let stationsURL = "https://fethica.com/assets/swift-radio/stations.json" 19 | 20 | // Set this to "true" to enable the search bar 21 | static let searchable = false 22 | 23 | // Set this to "false" to show the next/previous player buttons 24 | static let hideNextPreviousButtons = true 25 | 26 | // Contact infos 27 | static let website = "https://github.com/analogcode/Swift-Radio-Pro" 28 | static let email = "contact@fethica.com" 29 | static let emailSubject = "From \(Bundle.main.appName) App" 30 | } 31 | 32 | -------------------------------------------------------------------------------- /SwiftRadio/Coordinators/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-11-23. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol Coordinator: AnyObject { 12 | var childCoordinators: [Coordinator] { get set } 13 | func start() 14 | } 15 | 16 | protocol NavigationCoordinator: Coordinator { 17 | var navigationController: UINavigationController { get } 18 | } 19 | 20 | protocol TabCoordinator: Coordinator { 21 | var tabBarController: UITabBarController { get } 22 | } 23 | -------------------------------------------------------------------------------- /SwiftRadio/Coordinators/MainCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainCoordinator.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-11-23. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MessageUI 11 | 12 | class MainCoordinator: NavigationCoordinator { 13 | var childCoordinators: [Coordinator] = [] 14 | let navigationController: UINavigationController 15 | 16 | func start() { 17 | let loaderVC = LoaderController() 18 | loaderVC.delegate = self 19 | navigationController.setViewControllers([loaderVC], animated: false) 20 | } 21 | 22 | init(navigationController: UINavigationController) { 23 | self.navigationController = navigationController 24 | } 25 | 26 | // MARK: - Shared 27 | 28 | func openWebsite() { 29 | guard let url = URL(string: Config.website) else { return } 30 | UIApplication.shared.open(url, options: [:], completionHandler: nil) 31 | } 32 | 33 | func openEmail(in viewController: UIViewController & MFMailComposeViewControllerDelegate) { 34 | let receipients = [Config.email] 35 | let subject = Config.emailSubject 36 | let messageBody = "" 37 | 38 | let configuredMailComposeViewController = viewController.configureMailComposeViewController(recepients: receipients, subject: subject, messageBody: messageBody) 39 | 40 | if viewController.canSendMail { 41 | viewController.present(configuredMailComposeViewController, animated: true, completion: nil) 42 | } else { 43 | viewController.showSendMailErrorAlert() 44 | } 45 | } 46 | 47 | func openAbout(in viewController: UIViewController) { 48 | let aboutController = Storyboard.viewController as AboutViewController 49 | aboutController.delegate = self 50 | viewController.present(aboutController, animated: true) 51 | } 52 | } 53 | 54 | // MARK: - LoaderControllerDelegate 55 | 56 | extension MainCoordinator: LoaderControllerDelegate { 57 | func didFinishLoading(_ controller: LoaderController, stations: [RadioStation]) { 58 | let stationsVC = StationsViewController() 59 | stationsVC.delegate = self 60 | navigationController.setViewControllers([stationsVC], animated: false) 61 | } 62 | } 63 | 64 | // MARK: - StationsViewControllerDelegate 65 | 66 | extension MainCoordinator: StationsViewControllerDelegate { 67 | 68 | func pushNowPlayingController(_ stationsViewController: StationsViewController, newStation: Bool) { 69 | let nowPlayingController = Storyboard.viewController as NowPlayingViewController 70 | nowPlayingController.delegate = self 71 | nowPlayingController.isNewStation = newStation 72 | navigationController.pushViewController(nowPlayingController, animated: true) 73 | } 74 | 75 | func presentPopUpMenuController(_ stationsViewController: StationsViewController) { 76 | let popUpMenuController = Storyboard.viewController as PopUpMenuViewController 77 | popUpMenuController.delegate = self 78 | navigationController.present(popUpMenuController, animated: true) 79 | } 80 | } 81 | 82 | // MARK: - NowPlayingViewControllerDelegate 83 | 84 | extension MainCoordinator: NowPlayingViewControllerDelegate { 85 | 86 | func didTapInfoButton(_ nowPlayingViewController: NowPlayingViewController, station: RadioStation) { 87 | let infoController = Storyboard.viewController as InfoDetailViewController 88 | infoController.currentStation = station 89 | navigationController.pushViewController(infoController, animated: true) 90 | } 91 | 92 | func didTapCompanyButton(_ nowPlayingViewController: NowPlayingViewController) { 93 | openAbout(in: nowPlayingViewController) 94 | } 95 | 96 | func didTapShareButton(_ nowPlayingViewController: NowPlayingViewController, station: RadioStation, artworkURL: URL?) { 97 | ShareActivity.activityController(station: station, artworkURL: artworkURL, sourceView: nowPlayingViewController.view) { [weak nowPlayingViewController] controller in 98 | nowPlayingViewController?.present(controller, animated: true, completion: nil) 99 | } 100 | } 101 | } 102 | 103 | // MARK: - PopUpMenuViewControllerDelegate 104 | 105 | extension MainCoordinator: PopUpMenuViewControllerDelegate { 106 | 107 | func didTapWebsiteButton(_ popUpMenuViewController: PopUpMenuViewController) { 108 | openWebsite() 109 | } 110 | 111 | func didTapAboutButton(_ popUpMenuViewController: PopUpMenuViewController) { 112 | openAbout(in: popUpMenuViewController) 113 | } 114 | } 115 | 116 | // MARK: - PopUpMenuViewControllerDelegate 117 | 118 | extension MainCoordinator: AboutViewControllerDelegate { 119 | func didTapEmailButton(_ aboutViewController: AboutViewController) { 120 | openEmail(in: aboutViewController) 121 | } 122 | 123 | func didTapWebsiteButton(_ aboutViewController: AboutViewController) { 124 | openWebsite() 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /SwiftRadio/Data/stations.json: -------------------------------------------------------------------------------- 1 | { 2 | "station": 3 | [ 4 | { 5 | "name": "Absolute Country Hits", 6 | "streamURL": "http://strm112.1.fm/acountry_mobile_mp3", 7 | "imageURL": "station-absolutecountry.png", 8 | "desc": "The Music Starts Here", 9 | "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." 10 | }, 11 | { 12 | "name": "AZ Rock Radio", 13 | "streamURL": "http://cassini.shoutca.st:9300/stream", 14 | "imageURL": "az-rock-radio", 15 | "desc": "We Know Music from A to Z", 16 | "longDesc": "Web Radio Station from Puerto Rico. Download our App (Apple & Android) or go to: www.azrockradio.com" 17 | }, 18 | { 19 | "name": "The Rock FM", 20 | "streamURL": "http://tunein-icecast.mediaworks.nz/rock_128kbps", 21 | "imageURL": "https://fethica.com/assets/swift-radio/station-therockfm@3x.png", 22 | "desc": "Rock Music", 23 | "longDesc": "NZ's number one Rock music station." 24 | }, 25 | { 26 | "name": "Classic Rock", 27 | "streamURL": "http://rfcmedia.streamguys1.com/classicrock.mp3", 28 | "imageURL": "station-classicrock", 29 | "desc": "Classic Rock Hits", 30 | "longDesc": "Classic rock is a radio format which developed from the album-oriented rock (AOR) format in the early 1980s. In the United States, the classic rock format features music ranging generally from the late 1960s to the late 1980s, primarily focusing on commercially successful hard rock popularized in the 1970s. The radio format became increasingly popular with the baby boomer demographic by the end of the 1990s." 31 | }, 32 | { 33 | "name": "Radio 1190", 34 | "streamURL": "http://104.250.149.122:8082/stream", 35 | "imageURL": "", 36 | "desc": "KVCU - Boulder, CO", 37 | "longDesc": "Radio 1190 is the bomb." 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /SwiftRadio/Helpers/AnimationFrames.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationFrames.swift 3 | // Swift Radio 4 | // 5 | // Created by Matthew Fecher on 7/2/15. 6 | // Copyright (c) 2015 MatthewFecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AnimationFrames { 12 | 13 | class func createFrames() -> [UIImage] { 14 | 15 | // Setup "Now Playing" Animation Bars 16 | var animationFrames = [UIImage]() 17 | for i in 0...3 { 18 | if let image = UIImage(named: "NowPlayingBars-\(i)") { 19 | animationFrames.append(image) 20 | } 21 | } 22 | 23 | for i in stride(from: 2, to: 0, by: -1) { 24 | if let image = UIImage(named: "NowPlayingBars-\(i)") { 25 | animationFrames.append(image) 26 | } 27 | } 28 | return animationFrames 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /SwiftRadio/Helpers/Bundle+appName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+appName.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-11-29. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Bundle { 12 | var appName: String { 13 | object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? 14 | object(forInfoDictionaryKey: "CFBundleName") as? String ?? 15 | "" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SwiftRadio/Helpers/Handoffable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Handoffable.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-11-24. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FRadioPlayer 11 | 12 | protocol Handoffable: UIResponder {} 13 | 14 | extension Handoffable { 15 | 16 | func setupHandoffUserActivity() { 17 | userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) 18 | userActivity?.becomeCurrent() 19 | } 20 | 21 | func updateHandoffUserActivity(_ activity: NSUserActivity?, station: RadioStation?) { 22 | guard let activity = activity else { return } 23 | 24 | defer { updateUserActivityState(activity) } 25 | 26 | guard let metadata = FRadioPlayer.shared.currentMetadata, let artistName = metadata.artistName, let trackName = metadata.trackName else { 27 | activity.webpageURL = nil 28 | return 29 | } 30 | 31 | activity.webpageURL = getHandoffURL(artistName: artistName, trackName: trackName) 32 | } 33 | 34 | private func getHandoffURL(artistName: String, trackName: String) -> URL? { 35 | var components = URLComponents() 36 | components.scheme = "https" 37 | components.host = "google.com" 38 | components.path = "/search" 39 | components.queryItems = [URLQueryItem]() 40 | components.queryItems?.append(URLQueryItem(name: "q", value: "\(artistName) \(trackName)")) 41 | return components.url 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SwiftRadio/Helpers/ShareActivity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShareActivity.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2019-08-20. 6 | // Copyright © 2019 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct ShareActivity { 12 | 13 | static func activityController(station: RadioStation, artworkURL: URL?, sourceView: UIView, _ completion: @escaping (UIActivityViewController) -> Void) { 14 | 15 | getImage(station: station, artworkURL: artworkURL) { image in 16 | let shareImage = generateImage(from: image, station: station) 17 | 18 | let activityViewController = UIActivityViewController(activityItems: [station.shoutout, shareImage], applicationActivities: nil) 19 | activityViewController.popoverPresentationController?.sourceRect = CGRect(x: sourceView.center.x, y: sourceView.center.y, width: 0, height: 0) 20 | activityViewController.popoverPresentationController?.sourceView = sourceView 21 | activityViewController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0) 22 | 23 | activityViewController.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems:[Any]?, error: Error?) in 24 | if completed { 25 | // do something on completion if you want 26 | } 27 | } 28 | 29 | completion(activityViewController) 30 | } 31 | } 32 | 33 | private static func getImage(station: RadioStation, artworkURL: URL?, _ completion: @escaping (UIImage?) -> Void) { 34 | if let artworkURL = artworkURL { 35 | UIImage.image(from: artworkURL) { completion($0) } 36 | } else { 37 | station.getImage { completion($0) } 38 | } 39 | } 40 | 41 | private static func generateImage(from image: UIImage?, station: RadioStation) -> UIImage { 42 | let logoShareView = LogoShareView.instanceFromNib() 43 | 44 | logoShareView.shareSetup(albumArt: image ?? #imageLiteral(resourceName: "albumArt"), radioShoutout: station.shoutout, trackTitle: station.trackName, trackArtist: station.artistName) 45 | 46 | let renderer = UIGraphicsImageRenderer(size: logoShareView.bounds.size) 47 | let shareImage = renderer.image { ctx in 48 | logoShareView.drawHierarchy(in: logoShareView.bounds, afterScreenUpdates: true) 49 | } 50 | 51 | return shareImage 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SwiftRadio/Helpers/Storyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Storyboard.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-11-23. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct Storyboard { 12 | 13 | static var storyboardName: String { 14 | return String(describing: T.self) 15 | } 16 | 17 | static var viewController: T { 18 | let storyboard = UIStoryboard(name: "Main", bundle: nil) 19 | 20 | guard let vc = storyboard.instantiateViewController(withIdentifier: Self.storyboardName) as? T else { 21 | fatalError("Could not get controller from Storyboard: \(Self.storyboardName)") 22 | } 23 | 24 | return vc 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SwiftRadio/Helpers/UIImage+Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Cache.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-11-01. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | 13 | static func image(from url: URL?, completion: @escaping (_ image: UIImage?) -> Void) { 14 | 15 | guard let url = url else { 16 | completion(nil) 17 | return 18 | } 19 | 20 | let cache = URLCache.shared 21 | let request = URLRequest(url: url) 22 | 23 | if let data = cache.cachedResponse(for: request)?.data, let image = UIImage(data: data) { 24 | DispatchQueue.main.async { 25 | completion(image) 26 | } 27 | } else { 28 | URLSession.shared.dataTask(with: request) { (data, response, error) in 29 | guard let data = data, let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode, let image = UIImage(data: data) else { 30 | DispatchQueue.main.async { completion(nil) } 31 | return 32 | } 33 | 34 | let cachedData = CachedURLResponse(response: httpResponse, data: data) 35 | cache.storeCachedResponse(cachedData, for: request) 36 | DispatchQueue.main.async { completion(image) } 37 | }.resume() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SwiftRadio/Helpers/UIImage+DropShadow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+DropShadow.swift 3 | // Swift Radio 4 | // 5 | // Created by Matthew Fecher on 5/30/15. 6 | // Copyright (c) 2015 MatthewFecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImageView { 12 | 13 | // APPLY DROP SHADOW 14 | func applyShadow() { 15 | layer.shadowColor = UIColor.black.cgColor 16 | layer.shadowOffset = CGSize(width: 0, height: 1) 17 | layer.shadowOpacity = 0.4 18 | layer.shadowRadius = 2 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /SwiftRadio/Helpers/UIImageView+Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+Cache.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-11-01. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | extension UIImageView { 13 | 14 | func load(url: URL, placeholder: UIImage? = nil, _ completion: (() -> Void)? = nil) { 15 | let cache = URLCache.shared 16 | let request = URLRequest(url: url) 17 | 18 | if let data = cache.cachedResponse(for: request)?.data, let image = UIImage(data: data) { 19 | DispatchQueue.main.async { 20 | self.image = image 21 | completion?() 22 | } 23 | } else { 24 | self.image = placeholder 25 | 26 | URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in 27 | guard let data = data, let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode, let image = UIImage(data: data) else { return } 28 | 29 | let cachedData = CachedURLResponse(response: httpResponse, data: data) 30 | cache.storeCachedResponse(cachedData, for: request) 31 | DispatchQueue.main.async { 32 | self?.image = image 33 | completion?() 34 | } 35 | }.resume() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftRadio/Helpers/UITableViewCell+reuseIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableViewCell+reuseIdentifier.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2023-06-25. 6 | // Copyright © 2023 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UITableViewCell { 12 | static var reuseIdentifier: String { 13 | return String(describing: self) 14 | } 15 | } 16 | 17 | extension UITableView { 18 | func register(_: T.Type) { 19 | register(T.self, forCellReuseIdentifier: T.reuseIdentifier) 20 | } 21 | 22 | func dequeueReusableCell(for indexPath: IndexPath) -> T { 23 | guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 24 | fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") 25 | } 26 | return cell 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SwiftRadio/Helpers/UIViewController+Email.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Email.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-11-24. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import MessageUI 10 | 11 | extension MFMailComposeViewControllerDelegate where Self: UIViewController { 12 | 13 | var canSendMail: Bool { 14 | MFMailComposeViewController.canSendMail() 15 | } 16 | 17 | func configureMailComposeViewController(recepients: [String], subject: String, messageBody: String) -> MFMailComposeViewController { 18 | 19 | let mailComposerVC = MFMailComposeViewController() 20 | mailComposerVC.mailComposeDelegate = self 21 | 22 | mailComposerVC.setToRecipients(recepients) 23 | mailComposerVC.setSubject(subject) 24 | mailComposerVC.setMessageBody(messageBody, isHTML: false) 25 | 26 | return mailComposerVC 27 | } 28 | 29 | func showSendMailErrorAlert() { 30 | let sendMailErrorAlert = UIAlertController(title: "Could Not Send Email", message: "Your device could not send e-mail. Please check e-mail configuration and try again.", preferredStyle: .alert) 31 | let cancelAction = UIAlertAction(title: "OK", style: .cancel, handler: nil) 32 | 33 | sendMailErrorAlert.addAction(cancelAction) 34 | present(sendMailErrorAlert, animated: true, completion: nil) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "size" : "29x29", 15 | "idiom" : "iphone", 16 | "filename" : "Icon-29@2x.png", 17 | "scale" : "2x" 18 | }, 19 | { 20 | "size" : "29x29", 21 | "idiom" : "iphone", 22 | "filename" : "Icon-29@3x.png", 23 | "scale" : "3x" 24 | }, 25 | { 26 | "size" : "40x40", 27 | "idiom" : "iphone", 28 | "filename" : "Icon-40@2x.png", 29 | "scale" : "2x" 30 | }, 31 | { 32 | "size" : "40x40", 33 | "idiom" : "iphone", 34 | "filename" : "Icon-40@3x.png", 35 | "scale" : "3x" 36 | }, 37 | { 38 | "size" : "60x60", 39 | "idiom" : "iphone", 40 | "filename" : "Icon-60@2x.png", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "size" : "60x60", 45 | "idiom" : "iphone", 46 | "filename" : "Icon-60@3x.png", 47 | "scale" : "3x" 48 | }, 49 | { 50 | "idiom" : "ipad", 51 | "size" : "20x20", 52 | "scale" : "1x" 53 | }, 54 | { 55 | "idiom" : "ipad", 56 | "size" : "20x20", 57 | "scale" : "2x" 58 | }, 59 | { 60 | "size" : "29x29", 61 | "idiom" : "ipad", 62 | "filename" : "Icon-29.png", 63 | "scale" : "1x" 64 | }, 65 | { 66 | "size" : "29x29", 67 | "idiom" : "ipad", 68 | "filename" : "Icon-29@2x-1.png", 69 | "scale" : "2x" 70 | }, 71 | { 72 | "size" : "40x40", 73 | "idiom" : "ipad", 74 | "filename" : "Icon-40.png", 75 | "scale" : "1x" 76 | }, 77 | { 78 | "size" : "40x40", 79 | "idiom" : "ipad", 80 | "filename" : "Icon-40@2x-1.png", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "size" : "76x76", 85 | "idiom" : "ipad", 86 | "filename" : "Icon-76.png", 87 | "scale" : "1x" 88 | }, 89 | { 90 | "size" : "76x76", 91 | "idiom" : "ipad", 92 | "filename" : "Icon-76@2x.png", 93 | "scale" : "2x" 94 | }, 95 | { 96 | "size" : "83.5x83.5", 97 | "idiom" : "ipad", 98 | "filename" : "Icon-83.5@2x.png", 99 | "scale" : "2x" 100 | }, 101 | { 102 | "size" : "1024x1024", 103 | "idiom" : "ios-marketing", 104 | "filename" : "SWIFT-RADIO.png", 105 | "scale" : "1x" 106 | } 107 | ], 108 | "info" : { 109 | "version" : 1, 110 | "author" : "xcode" 111 | } 112 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-29.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-29@2x-1.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-29@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-29@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-40@2x-1.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/AppIcon.appiconset/SWIFT-RADIO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/AppIcon.appiconset/SWIFT-RADIO.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/LaunchImage.launchimage/4s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/LaunchImage.launchimage/4s.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/LaunchImage.launchimage/5C.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/LaunchImage.launchimage/5C.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/LaunchImage.launchimage/6plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/LaunchImage.launchimage/6plus.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "extent" : "full-screen", 5 | "idiom" : "iphone", 6 | "subtype" : "736h", 7 | "filename" : "6plus.png", 8 | "minimum-system-version" : "8.0", 9 | "orientation" : "portrait", 10 | "scale" : "3x" 11 | }, 12 | { 13 | "extent" : "full-screen", 14 | "idiom" : "iphone", 15 | "subtype" : "667h", 16 | "filename" : "Portrait6.png", 17 | "minimum-system-version" : "8.0", 18 | "orientation" : "portrait", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "orientation" : "portrait", 23 | "idiom" : "iphone", 24 | "filename" : "4s.png", 25 | "extent" : "full-screen", 26 | "minimum-system-version" : "7.0", 27 | "scale" : "2x" 28 | }, 29 | { 30 | "extent" : "full-screen", 31 | "idiom" : "iphone", 32 | "subtype" : "retina4", 33 | "filename" : "5C.png", 34 | "minimum-system-version" : "7.0", 35 | "orientation" : "portrait", 36 | "scale" : "2x" 37 | } 38 | ], 39 | "info" : { 40 | "version" : 1, 41 | "author" : "xcode" 42 | } 43 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/LaunchImage.launchimage/Portrait6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/LaunchImage.launchimage/Portrait6.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-0.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "NowPlayingBars-0.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "NowPlayingBars-0@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "NowPlayingBars-0@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-0.imageset/NowPlayingBars-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-0.imageset/NowPlayingBars-0.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-0.imageset/NowPlayingBars-0@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-0.imageset/NowPlayingBars-0@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-0.imageset/NowPlayingBars-0@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-0.imageset/NowPlayingBars-0@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "NowPlayingBars-1.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "NowPlayingBars-1@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "NowPlayingBars-1@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-1.imageset/NowPlayingBars-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-1.imageset/NowPlayingBars-1.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-1.imageset/NowPlayingBars-1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-1.imageset/NowPlayingBars-1@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-1.imageset/NowPlayingBars-1@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-1.imageset/NowPlayingBars-1@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "NowPlayingBars-2.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "NowPlayingBars-2@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "NowPlayingBars-2@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-2.imageset/NowPlayingBars-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-2.imageset/NowPlayingBars-2.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-2.imageset/NowPlayingBars-2@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-2.imageset/NowPlayingBars-2@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-2.imageset/NowPlayingBars-2@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-2.imageset/NowPlayingBars-2@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "NowPlayingBars-3.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "NowPlayingBars-3@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "NowPlayingBars-3@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-3.imageset/NowPlayingBars-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-3.imageset/NowPlayingBars-3.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-3.imageset/NowPlayingBars-3@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-3.imageset/NowPlayingBars-3@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars-3.imageset/NowPlayingBars-3@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars-3.imageset/NowPlayingBars-3@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "NowPlayingBars.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "NowPlayingBars@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "NowPlayingBars@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars.imageset/NowPlayingBars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars.imageset/NowPlayingBars.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars.imageset/NowPlayingBars@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars.imageset/NowPlayingBars@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/NowPlayingBars.imageset/NowPlayingBars@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/NowPlayingBars.imageset/NowPlayingBars@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "az-rock-radio.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "az-rock-radio@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "az-rock-radio@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-80s.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "station-80s.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-80s.imageset/station-80s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-80s.imageset/station-80s.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "station-absolutecountry.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "station-absolutecountry@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "station-altvault.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "station-altvault@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "station-classicrock.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/station-classicrock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/station-classicrock.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "station-killrockstars.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/station-killrockstars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/station-killrockstars.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "station-newportfolk.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "station-newportfolk@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "station-spaceland.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/station-spaceland.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/station-spaceland.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-sub.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "sub.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-sub.imageset/sub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-sub.imageset/sub.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "station-therockfm.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "station-therockfm@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "station-therockfm@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/stationImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "stationImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "stationImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "stationImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/albumArt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "albumArt.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "albumArt@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "albumArt@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/albumArt.imageset/albumArt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/albumArt.imageset/albumArt.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/albumArt.imageset/albumArt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/albumArt.imageset/albumArt@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/albumArt.imageset/albumArt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/albumArt.imageset/albumArt@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "background.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "background@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "background@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/background.imageset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/background.imageset/background.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/background.imageset/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/background.imageset/background@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/background.imageset/background@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/background.imageset/background@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-close.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "btn-close.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "btn-close@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "btn-close@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-close.imageset/btn-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-close.imageset/btn-close.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-close.imageset/btn-close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-close.imageset/btn-close@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-close.imageset/btn-close@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-close.imageset/btn-close@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-next.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "btn-next.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "btn-next@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "btn-next@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-next.imageset/btn-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-nowPlaying.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "btn-nowPlaying.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "btn-nowPlaying@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-nowPlaying.imageset/btn-nowPlaying.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-nowPlaying.imageset/btn-nowPlaying.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-nowPlaying.imageset/btn-nowPlaying@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-nowPlaying.imageset/btn-nowPlaying@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-pause.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "btn-pause.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "btn-pause@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "btn-pause@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-pause.imageset/btn-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-pause.imageset/btn-pause.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-pause.imageset/btn-pause@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-pause.imageset/btn-pause@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-pause.imageset/btn-pause@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-pause.imageset/btn-pause@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-play.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "btn-play.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "btn-play@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "btn-play@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-play.imageset/btn-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-play.imageset/btn-play.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-play.imageset/btn-play@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-play.imageset/btn-play@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-play.imageset/btn-play@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-play.imageset/btn-play@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-previous.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "btn-previous.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "btn-previous@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "btn-previous@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-stop.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "btn-stop.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "btn-stop@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "btn-stop@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/carPlayTab.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "carPlayTab.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "carPlayTab@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "carPlayTab@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/icon-hamburger.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "icon-hamburger.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "icon-hamburger@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "icon-hamburger@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/icon-hamburger.imageset/icon-hamburger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/icon-hamburger.imageset/icon-hamburger.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/icon-hamburger.imageset/icon-hamburger@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/icon-hamburger.imageset/icon-hamburger@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/icon-hamburger.imageset/icon-hamburger@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/icon-hamburger.imageset/icon-hamburger@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/icon-info.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "icon-info.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "icon-info@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "icon-info@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/icon-info.imageset/icon-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/icon-info.imageset/icon-info.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/icon-info.imageset/icon-info@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/icon-info.imageset/icon-info@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/icon-info.imageset/icon-info@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/icon-info.imageset/icon-info@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "swift-radio.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "swift-radio-1.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "swift-radio-2.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/logo.imageset/swift-radio-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/logo.imageset/swift-radio-1.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/logo.imageset/swift-radio-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/logo.imageset/swift-radio-2.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/logo.imageset/swift-radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/logo.imageset/swift-radio.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/share.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "share.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "share@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "share@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/share.imageset/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/share.imageset/share.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/share.imageset/share@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/share.imageset/share@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/share.imageset/share@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/share.imageset/share@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/slider-ball.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "slider-ball.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "slider-ball@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "slider-ball@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/slider-ball.imageset/slider-ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/slider-ball.imageset/slider-ball.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/slider-ball.imageset/slider-ball@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/slider-ball.imageset/slider-ball@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/slider-ball.imageset/slider-ball@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/slider-ball.imageset/slider-ball@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/swift-radio-black.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "swift-radio-black.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "swift-radio-black@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/swift-radio-black.imageset/swift-radio-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/swift-radio-black.imageset/swift-radio-black.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/swift-radio-black.imageset/swift-radio-black@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/swift-radio-black.imageset/swift-radio-black@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/vol-max.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "vol-max.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "vol-max@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "vol-max@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/vol-max.imageset/vol-max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/vol-max.imageset/vol-max.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/vol-max.imageset/vol-max@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/vol-max.imageset/vol-max@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/vol-max.imageset/vol-max@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/vol-max.imageset/vol-max@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/vol-min.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "vol-min.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "vol-min@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "vol-min@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/vol-min.imageset/vol-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/vol-min.imageset/vol-min.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/vol-min.imageset/vol-min@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/vol-min.imageset/vol-min@2x.png -------------------------------------------------------------------------------- /SwiftRadio/Images.xcassets/vol-min.imageset/vol-min@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/8a433a44f0f8ea676378dea55ede39518b225247/SwiftRadio/Images.xcassets/vol-min.imageset/vol-min@3x.png -------------------------------------------------------------------------------- /SwiftRadio/Info-CarPlay.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Swift Radio CP 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | 32 | NSPhotoLibraryAddUsageDescription 33 | This app would like to save the image associated with the current track and station to your photo library. 34 | NSUserActivityTypes 35 | 36 | NSUserActivityTypeBrowsingWeb 37 | 38 | UIBackgroundModes 39 | 40 | audio 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UIStatusBarStyle 49 | UIStatusBarStyleLightContent 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | UIUserInterfaceStyle 62 | Dark 63 | UIBrowsableContentSupportsSectionedBrowsing 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /SwiftRadio/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Swift Radio 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | 32 | NSPhotoLibraryAddUsageDescription 33 | This app would like to save the image associated with the current track and station to your photo library. 34 | NSUserActivityTypes 35 | 36 | NSUserActivityTypeBrowsingWeb 37 | 38 | UIBackgroundModes 39 | 40 | audio 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UIStatusBarStyle 49 | UIStatusBarStyleLightContent 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | UIUserInterfaceStyle 62 | Dark 63 | 64 | 65 | -------------------------------------------------------------------------------- /SwiftRadio/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /SwiftRadio/Model/RadioStation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RadioStation.swift 3 | // Swift Radio 4 | // 5 | // Created by Matthew Fecher on 7/4/15. 6 | // Copyright (c) 2015 MatthewFecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FRadioPlayer 11 | 12 | // Radio Station 13 | 14 | struct RadioStation: Codable { 15 | 16 | var name: String 17 | var streamURL: String 18 | var imageURL: String 19 | var desc: String 20 | var longDesc: String 21 | 22 | init(name: String, streamURL: String, imageURL: String, desc: String, longDesc: String = "") { 23 | self.name = name 24 | self.streamURL = streamURL 25 | self.imageURL = imageURL 26 | self.desc = desc 27 | self.longDesc = longDesc 28 | } 29 | } 30 | 31 | extension RadioStation { 32 | var shoutout: String { 33 | "I'm listening to \(name) via \(Bundle.main.appName) app" 34 | } 35 | } 36 | 37 | extension RadioStation: Equatable { 38 | 39 | static func == (lhs: RadioStation, rhs: RadioStation) -> Bool { 40 | return (lhs.name == rhs.name) && (lhs.streamURL == rhs.streamURL) && (lhs.imageURL == rhs.imageURL) && (lhs.desc == rhs.desc) && (lhs.longDesc == rhs.longDesc) 41 | } 42 | } 43 | 44 | extension RadioStation { 45 | func getImage(completion: @escaping (_ image: UIImage) -> Void) { 46 | 47 | if imageURL.range(of: "http") != nil, let url = URL(string: imageURL) { 48 | // load current station image from network 49 | UIImage.image(from: url) { image in 50 | completion(image ?? #imageLiteral(resourceName: "stationImage")) 51 | } 52 | } else { 53 | // load local station image 54 | let image = UIImage(named: imageURL) ?? #imageLiteral(resourceName: "stationImage") 55 | completion(image) 56 | } 57 | } 58 | } 59 | 60 | extension RadioStation { 61 | 62 | var trackName: String { 63 | FRadioPlayer.shared.currentMetadata?.trackName ?? name 64 | } 65 | 66 | var artistName: String { 67 | FRadioPlayer.shared.currentMetadata?.artistName ?? desc 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SwiftRadio/Model/StationsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StationsManager.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-11-02. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FRadioPlayer 11 | import MediaPlayer 12 | 13 | protocol StationsManagerObserver: AnyObject { 14 | func stationsManager(_ manager: StationsManager, stationsDidUpdate stations: [RadioStation]) 15 | func stationsManager(_ manager: StationsManager, stationDidChange station: RadioStation?) 16 | } 17 | 18 | extension StationsManagerObserver { 19 | func stationsManager(_ manager: StationsManager, stationsDidUpdate stations: [RadioStation]) {} 20 | } 21 | 22 | class StationsManager { 23 | 24 | static let shared = StationsManager() 25 | 26 | private(set) var stations: [RadioStation] = [] { 27 | didSet { 28 | notifiyObservers { observer in 29 | observer.stationsManager(self, stationsDidUpdate: stations) 30 | } 31 | } 32 | } 33 | 34 | private(set) var currentStation: RadioStation? { 35 | didSet { 36 | notifiyObservers { observer in 37 | observer.stationsManager(self, stationDidChange: currentStation) 38 | } 39 | 40 | resetArtwork(with: currentStation) 41 | } 42 | } 43 | 44 | var searchedStations: [RadioStation] = [] 45 | 46 | private var observations = [ObjectIdentifier : Observation]() 47 | private let player = FRadioPlayer.shared 48 | 49 | private init() { 50 | self.player.addObserver(self) 51 | } 52 | 53 | func fetch(_ completion: StationsCompletion? = nil) { 54 | DataManager.getStation { [weak self] result in 55 | guard case .success(let stations) = result, self?.stations != stations else { 56 | completion?(result) 57 | return 58 | } 59 | 60 | self?.stations = stations 61 | 62 | // Reset everything if the new stations list doesn't have the current station 63 | if let currentStation = self?.currentStation, self?.stations.firstIndex(of: currentStation) == nil { 64 | self?.reset() 65 | } 66 | 67 | completion?(result) 68 | } 69 | } 70 | 71 | func set(station: RadioStation?) { 72 | guard let station = station else { 73 | reset() 74 | return 75 | } 76 | 77 | currentStation = station 78 | player.radioURL = URL(string: station.streamURL) 79 | } 80 | 81 | func setNext() { 82 | guard let index = getIndex(of: currentStation) else { return } 83 | let station = (index + 1 == stations.count) ? stations[0] : stations[index + 1] 84 | currentStation = station 85 | player.radioURL = URL(string: station.streamURL) 86 | } 87 | 88 | func setPrevious() { 89 | guard let index = getIndex(of: currentStation), let station = (index == 0) ? stations.last : stations[index - 1] else { return } 90 | currentStation = station 91 | player.radioURL = URL(string: station.streamURL) 92 | } 93 | 94 | func updateSearch(with filter: String) { 95 | searchedStations.removeAll(keepingCapacity: false) 96 | searchedStations = stations.filter { $0.name.range(of: filter, options: [.caseInsensitive]) != nil } 97 | } 98 | 99 | private func reset() { 100 | currentStation = nil 101 | player.radioURL = nil 102 | } 103 | 104 | private func getIndex(of station: RadioStation?) -> Int? { 105 | guard let station = station, let index = stations.firstIndex(of: station) else { return nil } 106 | return index 107 | } 108 | } 109 | 110 | // MARK: - StationsManager Observation 111 | 112 | extension StationsManager { 113 | 114 | private struct Observation { 115 | weak var observer: StationsManagerObserver? 116 | } 117 | 118 | func addObserver(_ observer: StationsManagerObserver) { 119 | let id = ObjectIdentifier(observer) 120 | observations[id] = Observation(observer: observer) 121 | } 122 | 123 | func removeObserver(_ observer: StationsManagerObserver) { 124 | let id = ObjectIdentifier(observer) 125 | observations.removeValue(forKey: id) 126 | } 127 | 128 | private func notifiyObservers(with action: (_ observer: StationsManagerObserver) -> Void) { 129 | for (id, observation) in observations { 130 | guard let observer = observation.observer else { 131 | observations.removeValue(forKey: id) 132 | continue 133 | } 134 | 135 | action(observer) 136 | } 137 | } 138 | } 139 | 140 | // MARK: - MPNowPlayingInfoCenter (Lock screen) 141 | 142 | extension StationsManager { 143 | 144 | private func resetArtwork(with station: RadioStation?) { 145 | 146 | guard let station = station else { 147 | updateLockScreen(with: nil) 148 | return 149 | } 150 | 151 | station.getImage { [weak self] image in 152 | self?.updateLockScreen(with: image) 153 | } 154 | } 155 | 156 | private func updateLockScreen(with artworkImage: UIImage?) { 157 | 158 | // Define Now Playing Info 159 | var nowPlayingInfo = [String : Any]() 160 | 161 | if let image = artworkImage { 162 | nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { size -> UIImage in 163 | return image 164 | }) 165 | } 166 | 167 | if let artistName = currentStation?.artistName { 168 | nowPlayingInfo[MPMediaItemPropertyArtist] = artistName 169 | } 170 | 171 | if let trackName = currentStation?.trackName { 172 | nowPlayingInfo[MPMediaItemPropertyTitle] = trackName 173 | } 174 | 175 | // Set the metadata 176 | MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo 177 | } 178 | } 179 | 180 | // MARK: - FRadioPlayerObserver 181 | 182 | extension StationsManager: FRadioPlayerObserver { 183 | 184 | func radioPlayer(_ player: FRadioPlayer, metadataDidChange metadata: FRadioPlayer.Metadata?) { 185 | resetArtwork(with: currentStation) 186 | } 187 | 188 | func radioPlayer(_ player: FRadioPlayer, artworkDidChange artworkURL: URL?) { 189 | 190 | guard let artworkURL = artworkURL else { 191 | resetArtwork(with: currentStation) 192 | return 193 | } 194 | 195 | UIImage.image(from: artworkURL) { [weak self] image in 196 | guard let image = image else { 197 | self?.resetArtwork(with: self?.currentStation) 198 | return 199 | } 200 | 201 | self?.updateLockScreen(with: image) 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /SwiftRadio/Networking/DataManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataManager.swift 3 | // Swift Radio 4 | // 5 | // Created by Matthew Fecher on 3/24/15. 6 | // Copyright (c) 2015 MatthewFecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum DataError: Error { 12 | case urlNotValid, dataNotValid, dataNotFound, fileNotFound, httpResponseNotValid 13 | } 14 | 15 | typealias StationsResult = Result<[RadioStation], Error> 16 | typealias StationsCompletion = (StationsResult) -> Void 17 | 18 | struct DataManager { 19 | 20 | // Helper struct to get either local or remote JSON 21 | 22 | static func getStation(completion: @escaping StationsCompletion) { 23 | 24 | DispatchQueue.global(qos: .userInitiated).async { 25 | 26 | if Config.useLocalStations { 27 | loadLocal() { dataResult in 28 | handle(dataResult, completion) 29 | } 30 | } else { 31 | loadHttp { dataResult in 32 | handle(dataResult, completion) 33 | } 34 | } 35 | } 36 | } 37 | 38 | private typealias DataResult = Result 39 | private typealias DataCompletion = (DataResult) -> Void 40 | 41 | private static func handle(_ dataResult: DataResult, _ completion: @escaping StationsCompletion) { 42 | DispatchQueue.main.async { 43 | switch dataResult { 44 | case .success(let data): 45 | let result = decode(data) 46 | completion(result) 47 | case .failure(let error): 48 | completion(.failure(error)) 49 | } 50 | } 51 | } 52 | 53 | private static func decode(_ data: Data?) -> Result<[RadioStation], Error> { 54 | if Config.debugLog { print("Stations JSON Found") } 55 | 56 | guard let data = data else { 57 | return .failure(DataError.dataNotFound) 58 | } 59 | 60 | let jsonDictionary: [String: [RadioStation]] 61 | 62 | do { 63 | jsonDictionary = try JSONDecoder().decode([String: [RadioStation]].self, from: data) 64 | } catch let error { 65 | return .failure(error) 66 | } 67 | 68 | guard let stations = jsonDictionary["station"] else { 69 | return .failure(DataError.dataNotValid) 70 | } 71 | 72 | return .success(stations) 73 | } 74 | 75 | // Load local JSON Data 76 | 77 | private static func loadLocal(_ completion: DataCompletion) { 78 | guard let filePathURL = Bundle.main.url(forResource: "stations", withExtension: "json") else { 79 | if Config.debugLog { print("The local JSON file could not be found") } 80 | completion(.failure(DataError.fileNotFound)) 81 | return 82 | } 83 | 84 | do { 85 | let data = try Data(contentsOf: filePathURL, options: .uncached) 86 | completion(.success(data)) 87 | } catch let error { 88 | completion(.failure(error)) 89 | } 90 | } 91 | 92 | // Load http JSON Data 93 | private static func loadHttp(_ completion: @escaping DataCompletion) { 94 | guard let url = URL(string: Config.stationsURL) else { 95 | if Config.debugLog { print("stationsURL not a valid URL") } 96 | completion(.failure(DataError.urlNotValid)) 97 | return 98 | } 99 | 100 | let config = URLSessionConfiguration.default 101 | config.requestCachePolicy = .reloadIgnoringLocalCacheData 102 | 103 | let session = URLSession(configuration: config) 104 | 105 | // Use URLSession to get data from an NSURL 106 | let loadDataTask = session.dataTask(with: url) { data, response, error in 107 | 108 | if let error = error { 109 | if Config.debugLog { print("API ERROR: \(error.localizedDescription)") } 110 | completion(.failure(error)) 111 | return 112 | } 113 | 114 | guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { 115 | if Config.debugLog { print("API: HTTP status code has unexpected value") } 116 | completion(.failure(DataError.httpResponseNotValid)) 117 | return 118 | } 119 | 120 | guard let data = data else { 121 | if Config.debugLog { print("API: No data received") } 122 | completion(.failure(DataError.dataNotFound)) 123 | return 124 | } 125 | 126 | completion(.success(data)) 127 | } 128 | 129 | loadDataTask.resume() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /SwiftRadio/SwiftRadio.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.playable-content 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftRadio/ViewControllers/AboutViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutViewController.swift 3 | // Swift Radio 4 | // 5 | // Created by Matthew Fecher on 7/9/15. 6 | // Copyright (c) 2015 MatthewFecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MessageUI 11 | 12 | protocol AboutViewControllerDelegate: AnyObject { 13 | func didTapEmailButton(_ aboutViewController: AboutViewController) 14 | func didTapWebsiteButton(_ aboutViewController: AboutViewController) 15 | } 16 | 17 | class AboutViewController: UIViewController { 18 | 19 | weak var delegate: AboutViewControllerDelegate? 20 | 21 | // MARK: - ViewDidLoad 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | } 26 | 27 | // MARK: - IBActions 28 | 29 | @IBAction func emailButtonDidTouch(_ sender: UIButton) { 30 | delegate?.didTapEmailButton(self) 31 | } 32 | 33 | @IBAction func websiteButtonDidTouch(_ sender: UIButton) { 34 | delegate?.didTapWebsiteButton(self) 35 | } 36 | 37 | } 38 | 39 | // MARK: - MFMailComposeViewController Delegate 40 | 41 | extension AboutViewController: MFMailComposeViewControllerDelegate { 42 | 43 | func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { 44 | controller.dismiss(animated: true, completion: nil) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SwiftRadio/ViewControllers/BaseController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseController.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-12-03. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseController: UIViewController { 12 | 13 | let backgroundImageView: UIImageView = { 14 | let image = UIImage(named: "background") 15 | let imageView = UIImageView(image: image) 16 | imageView.contentMode = .scaleAspectFill 17 | imageView.translatesAutoresizingMaskIntoConstraints = false 18 | return imageView 19 | }() 20 | 21 | override func loadView() { 22 | super.loadView() 23 | setupViews() 24 | } 25 | 26 | func setupViews() { 27 | view.addSubview(backgroundImageView) 28 | 29 | NSLayoutConstraint.activate([ 30 | backgroundImageView.topAnchor.constraint(equalTo: view.topAnchor), 31 | backgroundImageView.rightAnchor.constraint(equalTo: view.rightAnchor), 32 | backgroundImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 33 | backgroundImageView.leftAnchor.constraint(equalTo: view.leftAnchor) 34 | ]) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SwiftRadio/ViewControllers/InfoDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfoDetailViewController.swift 3 | // Swift Radio 4 | // 5 | // Created by Matthew Fecher on 7/9/15. 6 | // Copyright (c) 2015 MatthewFecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class InfoDetailViewController: UIViewController { 12 | 13 | @IBOutlet weak var stationImageView: UIImageView! 14 | @IBOutlet weak var stationNameLabel: UILabel! 15 | @IBOutlet weak var stationDescLabel: UILabel! 16 | @IBOutlet weak var stationLongDescTextView: UITextView! 17 | @IBOutlet weak var okayButton: UIButton! 18 | 19 | var currentStation: RadioStation! 20 | 21 | // MARK: - ViewDidLoad 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | setupStationText() 27 | setupStationLogo() 28 | } 29 | 30 | // MARK: - UI Helpers 31 | 32 | func setupStationText() { 33 | 34 | // Display Station Name & Short Desc 35 | stationNameLabel.text = currentStation.name 36 | stationDescLabel.text = currentStation.desc 37 | 38 | // Display Station Long Desc 39 | if currentStation.longDesc == "" { 40 | loadDefaultText() 41 | } else { 42 | stationLongDescTextView.text = currentStation.longDesc 43 | } 44 | } 45 | 46 | func loadDefaultText() { 47 | // Add your own default ext 48 | stationLongDescTextView.text = "You are listening to Swift Radio. This is a sweet open source project. Tell your friends, swiftly!" 49 | } 50 | 51 | func setupStationLogo() { 52 | 53 | // Display Station Image/Logo 54 | currentStation.getImage { [weak self] image in 55 | self?.stationImageView.image = image 56 | } 57 | 58 | // Apply shadow to Station Image 59 | stationImageView.applyShadow() 60 | } 61 | 62 | // MARK: - IBActions 63 | 64 | @IBAction func okayButtonPressed(_ sender: UIButton) { 65 | _ = navigationController?.popViewController(animated: true) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /SwiftRadio/ViewControllers/LoaderController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoaderController.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2022-12-03. 6 | // Copyright © 2022 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol LoaderControllerDelegate: AnyObject { 12 | func didFinishLoading(_ controller: LoaderController, stations: [RadioStation]) 13 | } 14 | 15 | class LoaderController: BaseController { 16 | 17 | weak var delegate: LoaderControllerDelegate? 18 | 19 | private let manager = StationsManager.shared 20 | 21 | private let activityIndicatorView: UIActivityIndicatorView = { 22 | let view = UIActivityIndicatorView(style: .white) 23 | view.translatesAutoresizingMaskIntoConstraints = false 24 | return view 25 | }() 26 | 27 | private let errorTitleLabel: UILabel = { 28 | let label = UILabel() 29 | label.textAlignment = .center 30 | label.font = UIFont.preferredFont(forTextStyle: .headline) 31 | label.numberOfLines = 0 32 | label.text = "Something went wrong!" 33 | return label 34 | }() 35 | 36 | private let errorMessageLabel: UILabel = { 37 | let label = UILabel() 38 | label.textAlignment = .center 39 | label.numberOfLines = 0 40 | label.font = UIFont.preferredFont(forTextStyle: .footnote) 41 | return label 42 | }() 43 | 44 | private let stackView: UIStackView = { 45 | let view = UIStackView() 46 | view.translatesAutoresizingMaskIntoConstraints = false 47 | view.axis = .vertical 48 | view.alignment = .center 49 | view.spacing = 16 50 | return view 51 | }() 52 | 53 | override func viewDidAppear(_ animated: Bool) { 54 | super.viewDidAppear(animated) 55 | fetchStations() 56 | } 57 | 58 | private func handle(_ error: Error) { 59 | stackView.isHidden = false 60 | errorMessageLabel.text = error.localizedDescription 61 | } 62 | 63 | private func fetchStations() { 64 | stackView.isHidden = true 65 | activityIndicatorView.startAnimating() 66 | 67 | manager.fetch { [weak self] result in 68 | guard let self = self else { return } 69 | 70 | self.activityIndicatorView.stopAnimating() 71 | 72 | switch result { 73 | case .success(let stations): 74 | self.delegate?.didFinishLoading(self, stations: stations) 75 | case .failure(let error): 76 | self.handle(error) 77 | } 78 | } 79 | } 80 | 81 | override func setupViews() { 82 | super.setupViews() 83 | 84 | // Logo Image 85 | let logoImage = UIImage(named: "logo") 86 | let logoImageView = UIImageView(image: logoImage) 87 | logoImageView.translatesAutoresizingMaskIntoConstraints = false 88 | 89 | view.addSubview(logoImageView) 90 | 91 | NSLayoutConstraint.activate([ 92 | logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 93 | logoImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor) 94 | ]) 95 | 96 | // Activity Indicator 97 | view.addSubview(activityIndicatorView) 98 | 99 | NSLayoutConstraint.activate([ 100 | activityIndicatorView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 101 | activityIndicatorView.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 32) 102 | ]) 103 | 104 | // Retry button 105 | let retryButton = UIButton(type: .system) 106 | retryButton.setTitle("Try again", for: .normal) 107 | retryButton.addTarget(self, action: #selector(handleRetry), for: .touchUpInside) 108 | 109 | // Stack view 110 | stackView.addArrangedSubview(errorTitleLabel) 111 | stackView.addArrangedSubview(errorMessageLabel) 112 | stackView.addArrangedSubview(retryButton) 113 | 114 | view.addSubview(stackView) 115 | 116 | NSLayoutConstraint.activate([ 117 | stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 118 | stackView.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 32), 119 | stackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.7) 120 | ]) 121 | } 122 | 123 | @objc private func handleRetry() { 124 | fetchStations() 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /SwiftRadio/ViewControllers/NowPlayingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NowPlayingViewController.swift 3 | // Swift Radio 4 | // 5 | // Created by Matthew Fecher on 7/22/15. 6 | // Copyright (c) 2015 MatthewFecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MediaPlayer 11 | import AVKit 12 | import Spring 13 | import FRadioPlayer 14 | 15 | protocol NowPlayingViewControllerDelegate: AnyObject { 16 | func didTapCompanyButton(_ nowPlayingViewController: NowPlayingViewController) 17 | func didTapInfoButton(_ nowPlayingViewController: NowPlayingViewController, station: RadioStation) 18 | func didTapShareButton(_ nowPlayingViewController: NowPlayingViewController, station: RadioStation, artworkURL: URL?) 19 | } 20 | 21 | class NowPlayingViewController: UIViewController { 22 | 23 | weak var delegate: NowPlayingViewControllerDelegate? 24 | 25 | // MARK: - IB UI 26 | 27 | @IBOutlet weak var albumHeightConstraint: NSLayoutConstraint! 28 | @IBOutlet weak var albumImageView: SpringImageView! 29 | @IBOutlet weak var artistLabel: UILabel! 30 | @IBOutlet weak var playingButton: UIButton! 31 | @IBOutlet weak var songLabel: SpringLabel! 32 | @IBOutlet weak var stationDescLabel: UILabel! 33 | @IBOutlet weak var volumeParentView: UIView! 34 | @IBOutlet weak var previousButton: UIButton! 35 | @IBOutlet weak var nextButton: UIButton! 36 | @IBOutlet weak var airPlayView: UIView! 37 | 38 | // MARK: - Properties 39 | 40 | private let player = FRadioPlayer.shared 41 | private let manager = StationsManager.shared 42 | 43 | var isNewStation = true 44 | var nowPlayingImageView: UIImageView! 45 | 46 | var mpVolumeSlider: UISlider? 47 | 48 | // MARK: - ViewDidLoad 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | 53 | navigationItem.largeTitleDisplayMode = .never 54 | 55 | player.addObserver(self) 56 | manager.addObserver(self) 57 | 58 | // Create Now Playing BarItem 59 | createNowPlayingAnimation() 60 | 61 | // Set AlbumArtwork Constraints 62 | optimizeForDeviceSize() 63 | 64 | // Set View Title 65 | self.title = manager.currentStation?.name 66 | 67 | // Set UI 68 | 69 | stationDescLabel.text = manager.currentStation?.desc 70 | stationDescLabel.isHidden = player.currentMetadata != nil 71 | 72 | // Check for station change 73 | if isNewStation { 74 | stationDidChange() 75 | } else { 76 | updateTrackArtwork() 77 | playerStateDidChange(player.state, animate: false) 78 | } 79 | 80 | // Setup volumeSlider 81 | setupVolumeSlider() 82 | 83 | // Setup AirPlayButton 84 | setupAirPlayButton() 85 | 86 | // Hide / Show Next/Previous buttons 87 | previousButton.isHidden = Config.hideNextPreviousButtons 88 | nextButton.isHidden = Config.hideNextPreviousButtons 89 | 90 | isPlayingDidChange(player.isPlaying) 91 | } 92 | 93 | // MARK: - Setup 94 | 95 | func setupVolumeSlider() { 96 | // Note: This slider implementation uses a MPVolumeView 97 | // The volume slider only works in devices, not the simulator. 98 | for subview in MPVolumeView().subviews { 99 | guard let volumeSlider = subview as? UISlider else { continue } 100 | mpVolumeSlider = volumeSlider 101 | } 102 | 103 | guard let mpVolumeSlider = mpVolumeSlider else { return } 104 | 105 | volumeParentView.addSubview(mpVolumeSlider) 106 | 107 | mpVolumeSlider.translatesAutoresizingMaskIntoConstraints = false 108 | mpVolumeSlider.leftAnchor.constraint(equalTo: volumeParentView.leftAnchor).isActive = true 109 | mpVolumeSlider.rightAnchor.constraint(equalTo: volumeParentView.rightAnchor).isActive = true 110 | mpVolumeSlider.centerYAnchor.constraint(equalTo: volumeParentView.centerYAnchor).isActive = true 111 | 112 | mpVolumeSlider.setThumbImage(#imageLiteral(resourceName: "slider-ball"), for: .normal) 113 | } 114 | 115 | func setupAirPlayButton() { 116 | let airPlayButton = AVRoutePickerView(frame: airPlayView.bounds) 117 | airPlayButton.activeTintColor = .white 118 | airPlayButton.tintColor = .gray 119 | airPlayView.backgroundColor = .clear 120 | airPlayView.addSubview(airPlayButton) 121 | } 122 | 123 | func stationDidChange() { 124 | albumImageView.image = nil 125 | manager.currentStation?.getImage { [weak self] image in 126 | self?.albumImageView.image = image 127 | } 128 | stationDescLabel.text = manager.currentStation?.desc 129 | stationDescLabel.isHidden = player.currentArtworkURL != nil 130 | title = manager.currentStation?.name 131 | updateLabels() 132 | } 133 | 134 | // MARK: - Player Controls (Play/Pause/Volume) 135 | 136 | @IBAction func playingPressed(_ sender: Any) { 137 | player.togglePlaying() 138 | } 139 | 140 | @IBAction func stopPressed(_ sender: Any) { 141 | player.stop() 142 | } 143 | 144 | @IBAction func nextPressed(_ sender: Any) { 145 | manager.setNext() 146 | } 147 | 148 | @IBAction func previousPressed(_ sender: Any) { 149 | manager.setPrevious() 150 | } 151 | 152 | // Update track with new artwork 153 | func updateTrackArtwork() { 154 | guard let artworkURL = player.currentArtworkURL else { 155 | manager.currentStation?.getImage { [weak self] image in 156 | self?.albumImageView.image = image 157 | self?.stationDescLabel.isHidden = false 158 | } 159 | return 160 | } 161 | 162 | albumImageView.load(url: artworkURL) { [weak self] in 163 | self?.albumImageView.animation = "wobble" 164 | self?.albumImageView.duration = 2 165 | self?.albumImageView.animate() 166 | self?.stationDescLabel.isHidden = true 167 | 168 | // Force app to update display 169 | self?.view.setNeedsDisplay() 170 | } 171 | } 172 | 173 | private func isPlayingDidChange(_ isPlaying: Bool) { 174 | playingButton.isSelected = isPlaying 175 | startNowPlayingAnimation(isPlaying) 176 | } 177 | 178 | func playbackStateDidChange(_ playbackState: FRadioPlayer.PlaybackState, animate: Bool) { 179 | 180 | let message: String? 181 | 182 | switch playbackState { 183 | case .paused: 184 | message = "Station Paused..." 185 | case .playing: 186 | message = nil 187 | case .stopped: 188 | message = "Station Stopped..." 189 | } 190 | 191 | updateLabels(with: message, animate: animate) 192 | isPlayingDidChange(player.isPlaying) 193 | } 194 | 195 | func playerStateDidChange(_ state: FRadioPlayer.State, animate: Bool) { 196 | 197 | let message: String? 198 | 199 | switch state { 200 | case .loading: 201 | message = "Loading Station ..." 202 | case .urlNotSet: 203 | message = "Station URL not valide" 204 | case .readyToPlay, .loadingFinished: 205 | playbackStateDidChange(player.playbackState, animate: animate) 206 | return 207 | case .error: 208 | message = "Error Playing" 209 | } 210 | 211 | updateLabels(with: message, animate: animate) 212 | } 213 | 214 | // MARK: - UI Helper Methods 215 | 216 | func optimizeForDeviceSize() { 217 | 218 | // Adjust album size to fit iPhone 4s, 6s & 6s+ 219 | let deviceHeight = self.view.bounds.height 220 | 221 | if deviceHeight == 480 { 222 | albumHeightConstraint.constant = 106 223 | view.updateConstraints() 224 | } else if deviceHeight == 667 { 225 | albumHeightConstraint.constant = 230 226 | view.updateConstraints() 227 | } else if deviceHeight > 667 { 228 | albumHeightConstraint.constant = 260 229 | view.updateConstraints() 230 | } 231 | } 232 | 233 | func updateLabels(with statusMessage: String? = nil, animate: Bool = true) { 234 | 235 | guard let statusMessage = statusMessage else { 236 | // Radio is (hopefully) streaming properly 237 | songLabel.text = manager.currentStation?.trackName 238 | artistLabel.text = manager.currentStation?.artistName 239 | shouldAnimateSongLabel(animate) 240 | return 241 | } 242 | 243 | // There's a an interruption or pause in the audio queue 244 | 245 | // Update UI only when it's not aleary updated 246 | guard songLabel.text != statusMessage else { return } 247 | 248 | songLabel.text = statusMessage 249 | artistLabel.text = manager.currentStation?.name 250 | 251 | if animate { 252 | songLabel.animation = "flash" 253 | songLabel.repeatCount = 2 254 | songLabel.animate() 255 | } 256 | } 257 | 258 | // Animations 259 | 260 | func shouldAnimateSongLabel(_ animate: Bool) { 261 | // Animate if the Track has album metadata 262 | guard animate, player.currentMetadata != nil else { return } 263 | 264 | // songLabel animation 265 | songLabel.animation = "zoomIn" 266 | songLabel.duration = 1.5 267 | songLabel.damping = 1 268 | songLabel.animate() 269 | } 270 | 271 | func createNowPlayingAnimation() { 272 | // Setup ImageView 273 | nowPlayingImageView = UIImageView(image: UIImage(named: "NowPlayingBars-3")) 274 | nowPlayingImageView.autoresizingMask = [] 275 | nowPlayingImageView.contentMode = UIView.ContentMode.center 276 | 277 | // Create Animation 278 | nowPlayingImageView.animationImages = AnimationFrames.createFrames() 279 | nowPlayingImageView.animationDuration = 0.7 280 | 281 | // Create Top BarButton 282 | let barButton = UIButton(type: .custom) 283 | barButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40) 284 | barButton.addSubview(nowPlayingImageView) 285 | nowPlayingImageView.center = barButton.center 286 | 287 | let barItem = UIBarButtonItem(customView: barButton) 288 | self.navigationItem.rightBarButtonItem = barItem 289 | } 290 | 291 | func startNowPlayingAnimation(_ animate: Bool) { 292 | animate ? nowPlayingImageView.startAnimating() : nowPlayingImageView.stopAnimating() 293 | } 294 | 295 | @IBAction func infoButtonPressed(_ sender: UIButton) { 296 | guard let station = manager.currentStation else { return } 297 | delegate?.didTapInfoButton(self, station: station) 298 | } 299 | 300 | @IBAction func shareButtonPressed(_ sender: UIButton) { 301 | guard let station = manager.currentStation else { return } 302 | delegate?.didTapShareButton(self, station: station, artworkURL: player.currentArtworkURL) 303 | } 304 | 305 | @IBAction func handleCompanyButton(_ sender: Any) { 306 | delegate?.didTapCompanyButton(self) 307 | } 308 | } 309 | 310 | extension NowPlayingViewController: FRadioPlayerObserver { 311 | 312 | func radioPlayer(_ player: FRadioPlayer, playerStateDidChange state: FRadioPlayer.State) { 313 | playerStateDidChange(state, animate: true) 314 | } 315 | 316 | func radioPlayer(_ player: FRadioPlayer, playbackStateDidChange state: FRadioPlayer.PlaybackState) { 317 | playbackStateDidChange(state, animate: true) 318 | } 319 | 320 | func radioPlayer(_ player: FRadioPlayer, metadataDidChange metadata: FRadioPlayer.Metadata?) { 321 | updateLabels() 322 | } 323 | 324 | func radioPlayer(_ player: FRadioPlayer, artworkDidChange artworkURL: URL?) { 325 | updateTrackArtwork() 326 | } 327 | } 328 | 329 | extension NowPlayingViewController: StationsManagerObserver { 330 | 331 | func stationsManager(_ manager: StationsManager, stationDidChange station: RadioStation?) { 332 | stationDidChange() 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /SwiftRadio/ViewControllers/PopUpMenuViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopUpMenuViewController.swift 3 | // Swift Radio 4 | // 5 | // Created by Matthew Fecher on 7/9/15. 6 | // Copyright (c) 2015 MatthewFecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol PopUpMenuViewControllerDelegate: AnyObject { 12 | func didTapWebsiteButton(_ popUpMenuViewController: PopUpMenuViewController) 13 | func didTapAboutButton(_ popUpMenuViewController: PopUpMenuViewController) 14 | } 15 | 16 | class PopUpMenuViewController: UIViewController { 17 | 18 | weak var delegate: PopUpMenuViewControllerDelegate? 19 | 20 | @IBOutlet weak var popupView: UIView! 21 | @IBOutlet weak var backgroundView: UIImageView! 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | super.init(coder: aDecoder) 25 | modalPresentationStyle = .custom 26 | } 27 | 28 | // MARK: - ViewDidLoad 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | 33 | // Round corners 34 | popupView.layer.cornerRadius = 10 35 | 36 | // Set background color to clear 37 | view.backgroundColor = UIColor.clear 38 | 39 | // Add gesture recognizer to dismiss view when touched 40 | let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(closeButtonPressed)) 41 | backgroundView.isUserInteractionEnabled = true 42 | backgroundView.addGestureRecognizer(gestureRecognizer) 43 | } 44 | 45 | // MARK: - IBActions 46 | 47 | @IBAction func closeButtonPressed() { 48 | dismiss(animated: true, completion: nil) 49 | } 50 | 51 | @IBAction func websiteButtonPressed(_ sender: UIButton) { 52 | delegate?.didTapWebsiteButton(self) 53 | } 54 | 55 | @IBAction func aboutButtonPressed(_ sender: Any) { 56 | delegate?.didTapAboutButton(self) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SwiftRadio/ViewControllers/StationsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StationsViewController.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2023-06-24. 6 | // Copyright © 2023 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import FRadioPlayer 11 | 12 | protocol StationsViewControllerDelegate: AnyObject { 13 | func pushNowPlayingController(_ stationsViewController: StationsViewController, newStation: Bool) 14 | func presentPopUpMenuController(_ stationsViewController: StationsViewController) 15 | } 16 | 17 | class StationsViewController: BaseController, Handoffable { 18 | 19 | // MARK: - Delegate 20 | weak var delegate: StationsViewControllerDelegate? 21 | 22 | // MARK: - Properties 23 | private let player = FRadioPlayer.shared 24 | private let manager = StationsManager.shared 25 | 26 | // MARK: - UI 27 | 28 | private lazy var refreshControl: UIRefreshControl = { 29 | let refreshControl = UIRefreshControl() 30 | refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) 31 | return refreshControl 32 | }() 33 | 34 | private let searchController: UISearchController = { 35 | let controller = UISearchController(searchResultsController: nil) 36 | controller.obscuresBackgroundDuringPresentation = false 37 | controller.hidesNavigationBarDuringPresentation = true 38 | return controller 39 | }() 40 | 41 | private lazy var tableView: UITableView = { 42 | let tableView = UITableView() 43 | tableView.backgroundColor = .clear 44 | tableView.backgroundView = nil 45 | tableView.separatorStyle = .none 46 | let cellNib = UINib(nibName: "NothingFoundCell", bundle: nil) 47 | tableView.register(cellNib, forCellReuseIdentifier: "NothingFound") 48 | tableView.register(StationTableViewCell.self) 49 | tableView.dataSource = self 50 | tableView.delegate = self 51 | tableView.translatesAutoresizingMaskIntoConstraints = false 52 | return tableView 53 | }() 54 | 55 | private let nowPlayingView: NowPlayingView = { 56 | return NowPlayingView() 57 | }() 58 | 59 | override func loadView() { 60 | super.loadView() 61 | setupViews() 62 | } 63 | 64 | override func viewDidLoad() { 65 | super.viewDidLoad() 66 | navigationController?.navigationBar.prefersLargeTitles = true 67 | 68 | // NavigationBar items 69 | navigationItem.leftBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "icon-hamburger"), style: .plain, target: self, action: #selector(handleMenuTap)) 70 | 71 | // Setup Player 72 | player.addObserver(self) 73 | manager.addObserver(self) 74 | 75 | // Setup Handoff User Activity 76 | setupHandoffUserActivity() 77 | 78 | // Setup Search Bar 79 | setupSearchController() 80 | 81 | // Now Playing View 82 | nowPlayingView.tapHandler = { [weak self] in 83 | self?.nowPlayingBarButtonPressed() 84 | } 85 | } 86 | 87 | override func viewWillAppear(_ animated: Bool) { 88 | super.viewWillAppear(animated) 89 | title = "Swift Radio" 90 | } 91 | 92 | @objc func refresh(sender: AnyObject) { 93 | // Pull to Refresh 94 | manager.fetch() 95 | 96 | // Wait 2 seconds then refresh screen 97 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in 98 | self?.refreshControl.endRefreshing() 99 | self?.view.setNeedsDisplay() 100 | } 101 | } 102 | 103 | // Reset all properties to default 104 | private func resetCurrentStation() { 105 | nowPlayingView.reset() 106 | navigationItem.rightBarButtonItem = nil 107 | } 108 | 109 | // Update the now playing button title 110 | private func updateNowPlayingButton(station: RadioStation?) { 111 | 112 | guard let station = station else { 113 | nowPlayingView.reset() 114 | return 115 | } 116 | 117 | var playingTitle: String? 118 | 119 | if player.currentMetadata != nil { 120 | playingTitle = station.trackName + " - " + station.artistName 121 | } 122 | 123 | nowPlayingView.update(with: playingTitle, subtitle: station.name) 124 | createNowPlayingBarButton() 125 | } 126 | 127 | func startNowPlayingAnimation(_ animate: Bool) { 128 | animate ? nowPlayingView.startAnimating() : nowPlayingView.stopAnimating() 129 | } 130 | 131 | private func createNowPlayingBarButton() { 132 | guard navigationItem.rightBarButtonItem == nil else { return } 133 | navigationItem.rightBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "btn-nowPlaying"), style: .plain, target: self, action: #selector(nowPlayingBarButtonPressed)) 134 | } 135 | 136 | @objc func nowPlayingBarButtonPressed() { 137 | pushNowPlayingController() 138 | } 139 | 140 | @objc func handleMenuTap() { 141 | delegate?.presentPopUpMenuController(self) 142 | } 143 | 144 | func nowPlayingPressed(_ sender: UIButton) { 145 | pushNowPlayingController() 146 | } 147 | 148 | func pushNowPlayingController(with station: RadioStation? = nil) { 149 | title = "" 150 | 151 | let newStation: Bool 152 | 153 | if let station = station { 154 | // User clicked on row, load/reset station 155 | newStation = station != manager.currentStation 156 | if newStation { 157 | manager.set(station: station) 158 | } 159 | } else { 160 | // User clicked on Now Playing button 161 | newStation = false 162 | } 163 | 164 | delegate?.pushNowPlayingController(self, newStation: newStation) 165 | } 166 | 167 | override func setupViews() { 168 | super.setupViews() 169 | 170 | let stackView = UIStackView(arrangedSubviews: [tableView, nowPlayingView]) 171 | stackView.axis = .vertical 172 | stackView.translatesAutoresizingMaskIntoConstraints = false 173 | 174 | tableView.addSubview(refreshControl) 175 | view.addSubview(stackView) 176 | 177 | NSLayoutConstraint.activate([ 178 | stackView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor), 179 | stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 180 | stackView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor), 181 | stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor) 182 | ]) 183 | } 184 | } 185 | 186 | // MARK: - TableViewDataSource 187 | 188 | extension StationsViewController: UITableViewDataSource { 189 | 190 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 191 | 90.0 192 | } 193 | 194 | func numberOfSections(in tableView: UITableView) -> Int { 195 | 1 196 | } 197 | 198 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 199 | if searchController.isActive { 200 | return manager.searchedStations.count 201 | } else { 202 | return manager.stations.isEmpty ? 1 : manager.stations.count 203 | } 204 | } 205 | 206 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 207 | 208 | if manager.stations.isEmpty { 209 | let cell = tableView.dequeueReusableCell(withIdentifier: "NothingFound", for: indexPath) 210 | cell.backgroundColor = .clear 211 | cell.selectionStyle = .none 212 | return cell 213 | } else { 214 | let cell = tableView.dequeueReusableCell(for: indexPath) as StationTableViewCell 215 | 216 | // alternate background color 217 | cell.backgroundColor = (indexPath.row % 2 == 0) ? .clear : .black.withAlphaComponent(0.2) 218 | 219 | let station = searchController.isActive ? manager.searchedStations[indexPath.row] : manager.stations[indexPath.row] 220 | cell.configureStationCell(station: station) 221 | return cell 222 | } 223 | } 224 | } 225 | 226 | // MARK: - TableViewDelegate 227 | 228 | extension StationsViewController: UITableViewDelegate { 229 | 230 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 231 | tableView.deselectRow(at: indexPath, animated: true) 232 | 233 | let station = searchController.isActive ? manager.searchedStations[indexPath.item] : manager.stations[indexPath.item] 234 | 235 | pushNowPlayingController(with: station) 236 | } 237 | } 238 | 239 | // MARK: - UISearchControllerDelegate / Setup 240 | 241 | extension StationsViewController: UISearchResultsUpdating { 242 | 243 | func setupSearchController() { 244 | guard Config.searchable else { return } 245 | 246 | searchController.searchResultsUpdater = self 247 | navigationItem.searchController = searchController 248 | navigationItem.hidesSearchBarWhenScrolling = true 249 | } 250 | 251 | func updateSearchResults(for searchController: UISearchController) { 252 | guard let filter = searchController.searchBar.text else { return } 253 | manager.updateSearch(with: filter) 254 | tableView.reloadData() 255 | } 256 | } 257 | 258 | // MARK: - FRadioPlayerObserver 259 | 260 | extension StationsViewController: FRadioPlayerObserver { 261 | 262 | func radioPlayer(_ player: FRadioPlayer, playbackStateDidChange state: FRadioPlayer.PlaybackState) { 263 | startNowPlayingAnimation(player.isPlaying) 264 | } 265 | 266 | func radioPlayer(_ player: FRadioPlayer, metadataDidChange metadata: FRadioPlayer.Metadata?) { 267 | updateNowPlayingButton(station: manager.currentStation) 268 | updateHandoffUserActivity(userActivity, station: manager.currentStation) 269 | } 270 | } 271 | 272 | extension StationsViewController: StationsManagerObserver { 273 | 274 | func stationsManager(_ manager: StationsManager, stationsDidUpdate stations: [RadioStation]) { 275 | tableView.reloadData() 276 | } 277 | 278 | func stationsManager(_ manager: StationsManager, stationDidChange station: RadioStation?) { 279 | guard let station = station else { 280 | resetCurrentStation() 281 | return 282 | } 283 | 284 | updateNowPlayingButton(station: station) 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /SwiftRadio/Views/LogoShareView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoShareView.swift 3 | // SwiftRadio 4 | // 5 | // Created by Cameron Mcleod on 2019-07-12. 6 | // Copyright © 2019 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LogoShareView: UIView { 12 | 13 | @IBOutlet weak var albumArtImageView: UIImageView! 14 | @IBOutlet weak var radioShoutoutLabel: UILabel! 15 | @IBOutlet weak var trackTitleLabel: UILabel! 16 | @IBOutlet weak var trackArtistLabel: UILabel! 17 | @IBOutlet weak var logoImageView: UIImageView! 18 | 19 | class func instanceFromNib() -> LogoShareView { 20 | return UINib(nibName: "LogoShareView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! LogoShareView 21 | } 22 | 23 | func shareSetup(albumArt : UIImage, radioShoutout: String, trackTitle: String, trackArtist: String) { 24 | self.albumArtImageView.image = albumArt 25 | self.radioShoutoutLabel.text = radioShoutout 26 | self.trackTitleLabel.text = trackTitle 27 | self.trackArtistLabel.text = trackArtist 28 | self.logoImageView.image = UIImage(named: "logo") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SwiftRadio/Views/LogoShareView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /SwiftRadio/Views/NowPlayingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NowPlayingView.swift 3 | // SwiftRadio 4 | // 5 | // Created by Fethi El Hassasna on 2023-06-25. 6 | // Copyright © 2023 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import NVActivityIndicatorView 11 | 12 | class NowPlayingView: UIView { 13 | 14 | var tapHandler: (() -> Void)? 15 | 16 | private static let resetTitle = "Choose a station above to begin..." 17 | 18 | private let animationView: NVActivityIndicatorView = { 19 | let activityIndicatorView = NVActivityIndicatorView(frame: .zero, type: .audioEqualizer, color: .white, padding: nil) 20 | NSLayoutConstraint.activate([ 21 | activityIndicatorView.widthAnchor.constraint(equalToConstant: 30), 22 | activityIndicatorView.heightAnchor.constraint(equalToConstant: 20) 23 | ]) 24 | return activityIndicatorView 25 | }() 26 | 27 | private let nowPlayingButton: UIButton = { 28 | let button = UIButton() 29 | button.isEnabled = false 30 | button.contentHorizontalAlignment = .left 31 | button.translatesAutoresizingMaskIntoConstraints = false 32 | return button 33 | }() 34 | 35 | private let titleLabel: UILabel = { 36 | let label = UILabel() 37 | label.text = NowPlayingView.resetTitle 38 | label.font = .preferredFont(forTextStyle: .callout) 39 | label.textColor = .lightText 40 | return label 41 | }() 42 | 43 | private let subtitleLabel: UILabel = { 44 | let label = UILabel() 45 | label.font = .preferredFont(forTextStyle: .caption2) 46 | label.textColor = .lightText 47 | return label 48 | }() 49 | 50 | init() { 51 | super.init(frame: .zero) 52 | setupViews() 53 | } 54 | 55 | required init?(coder aDecoder: NSCoder) { 56 | fatalError("init(coder:) has not been implemented") 57 | } 58 | 59 | private func setupViews() { 60 | backgroundColor = .black.withAlphaComponent(0.1) 61 | NSLayoutConstraint.activate([ 62 | heightAnchor.constraint(greaterThanOrEqualToConstant: 50) 63 | ]) 64 | 65 | nowPlayingButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside) 66 | 67 | let dividerView = UIView() 68 | dividerView.backgroundColor = UIColor.darkGray.withAlphaComponent(0.8) 69 | dividerView.translatesAutoresizingMaskIntoConstraints = false 70 | addSubview(dividerView) 71 | 72 | NSLayoutConstraint.activate([ 73 | dividerView.topAnchor.constraint(equalTo: topAnchor), 74 | dividerView.leadingAnchor.constraint(equalTo: leadingAnchor), 75 | dividerView.trailingAnchor.constraint(equalTo: trailingAnchor), 76 | dividerView.heightAnchor.constraint(equalToConstant: 0.5) 77 | ]) 78 | 79 | let titleStackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) 80 | titleStackView.translatesAutoresizingMaskIntoConstraints = false 81 | titleStackView.axis = .vertical 82 | titleStackView.spacing = 4 83 | 84 | let stackView = UIStackView(arrangedSubviews: [titleStackView, animationView]) 85 | stackView.translatesAutoresizingMaskIntoConstraints = false 86 | stackView.axis = .horizontal 87 | stackView.alignment = .center 88 | 89 | addSubview(stackView) 90 | 91 | NSLayoutConstraint.activate([ 92 | stackView.topAnchor.constraint(equalTo: dividerView.bottomAnchor, constant: 8), 93 | stackView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: -8), 94 | stackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor, constant: 8), 95 | stackView.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: 8) 96 | ]) 97 | 98 | addSubview(nowPlayingButton) 99 | 100 | NSLayoutConstraint.activate([ 101 | nowPlayingButton.topAnchor.constraint(equalTo: topAnchor), 102 | nowPlayingButton.rightAnchor.constraint(equalTo: rightAnchor), 103 | nowPlayingButton.bottomAnchor.constraint(equalTo: bottomAnchor), 104 | nowPlayingButton.leftAnchor.constraint(equalTo: leftAnchor) 105 | ]) 106 | 107 | bringSubviewToFront(nowPlayingButton) 108 | } 109 | 110 | func startAnimating() { 111 | animationView.startAnimating() 112 | } 113 | 114 | func stopAnimating() { 115 | animationView.stopAnimating() 116 | } 117 | 118 | func reset() { 119 | animationView.stopAnimating() 120 | titleLabel.text = NowPlayingView.resetTitle 121 | subtitleLabel.text = nil 122 | nowPlayingButton.isEnabled = false 123 | } 124 | 125 | func update(with title: String?, subtitle: String) { 126 | nowPlayingButton.isEnabled = true 127 | 128 | if let title { 129 | titleLabel.text = title 130 | subtitleLabel.text = subtitle 131 | } else { 132 | titleLabel.text = subtitle 133 | subtitleLabel.text = "Now playing ..." 134 | } 135 | } 136 | 137 | @objc private func handleTap() { 138 | tapHandler?() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /SwiftRadioUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SwiftRadioUITests/SwiftRadioUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRadioUITests.swift 3 | // SwiftRadioUITests 4 | // 5 | // Created by Jonah Stiennon on 12/3/15. 6 | // Copyright © 2015 matthewfecher.com. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class SwiftRadioUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | } 24 | --------------------------------------------------------------------------------