├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------