From 85f4e088263c9295f1425d01064909fbe53fc780 Mon Sep 17 00:00:00 2001 From: Fethi El Hassasna Date: Tue, 21 Jan 2025 23:12:12 -0500 Subject: [PATCH 1/7] Refactor NowPlayingViewController to remove storyboard dependency --- .github/workflows/carplay.yml | 0 .github/workflows/ios.yml | 0 .gitignore | 0 SwiftRadio.xcodeproj/project.pbxproj | 64 +++- .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/swiftpm/Package.resolved | 4 +- .../UserInterfaceState.xcuserstate | Bin .../UserInterfaceState.xcuserstate | Bin .../WorkspaceSettings.xcsettings | 0 .../xcschemes/SwiftRadio.xcscheme | 0 SwiftRadio/AppDelegate.swift | 15 +- SwiftRadio/CarPlay/AppDelegate+CarPlay.swift | 0 SwiftRadio/Cells/StationTableViewCell.swift | 0 SwiftRadio/Config.swift | 2 +- SwiftRadio/Coordinators/Coordinator.swift | 0 SwiftRadio/Coordinators/MainCoordinator.swift | 15 +- SwiftRadio/Data/stations.json | 82 ++-- SwiftRadio/Helpers/Bundle+appName.swift | 0 SwiftRadio/Helpers/Handoffable.swift | 0 .../Helpers/NSLayoutConstraint+with.swift | 16 + SwiftRadio/Helpers/ShareActivity.swift | 37 +- SwiftRadio/Helpers/Storyboard.swift | 0 SwiftRadio/Helpers/UIImage+Cache.swift | 0 SwiftRadio/Helpers/UIImageView+Cache.swift | 0 .../UITableViewCell+reuseIdentifier.swift | 0 .../Helpers/UIViewController+Email.swift | 0 .../AppIcon.appiconset/Icon-83.5@2x.png | Bin .../AppIcon.appiconset/SWIFT-RADIO.png | Bin .../Images.xcassets/Stations/Contents.json | 0 .../az-rock-radio.imageset/Contents.json | 0 .../az-rock-radio.imageset/az-rock-radio.png | Bin .../az-rock-radio@2x.png | Bin .../az-rock-radio@3x.png | Bin .../station-80s.imageset/Contents.json | 0 .../station-80s.imageset/station-80s.png | Bin .../Contents.json | 0 .../station-absolutecountry.png | Bin .../station-absolutecountry@2x.png | Bin .../station-altvault.imageset/Contents.json | 0 .../station-altvault.png | Bin .../station-altvault@2x.png | Bin .../Contents.json | 0 .../station-classicrock.png | Bin .../Contents.json | 0 .../station-killrockstars.png | Bin .../Contents.json | 0 .../station-newportfolk.png | Bin .../station-newportfolk@2x.png | Bin .../station-spaceland.imageset/Contents.json | 0 .../station-spaceland.png | Bin .../station-sub.imageset/Contents.json | 0 .../Stations/station-sub.imageset/sub.png | Bin .../station-therockfm.imageset/Contents.json | 0 .../station-therockfm.png | Bin .../station-therockfm@2x.png | Bin .../station-therockfm@3x.png | Bin .../stationImage.imageset/Contents.json | 0 .../stationImage.imageset/stationImage.png | Bin .../stationImage.imageset/stationImage@2x.png | Bin .../stationImage.imageset/stationImage@3x.png | Bin .../btn-next.imageset/Contents.json | 0 .../btn-next.imageset/btn-next.png | Bin .../btn-next.imageset/btn-next@2x.png | Bin .../btn-next.imageset/btn-next@3x.png | Bin .../btn-previous.imageset/Contents.json | 0 .../btn-previous.imageset/btn-previous.png | Bin .../btn-previous.imageset/btn-previous@2x.png | Bin .../btn-previous.imageset/btn-previous@3x.png | Bin .../btn-stop.imageset/Contents.json | 0 .../btn-stop.imageset/btn-stop.png | Bin .../btn-stop.imageset/btn-stop@2x.png | Bin .../btn-stop.imageset/btn-stop@3x.png | Bin .../carPlayTab.imageset/Contents.json | 0 .../carPlayTab.imageset/carPlayTab.png | Bin .../carPlayTab.imageset/carPlayTab@2x.png | Bin .../carPlayTab.imageset/carPlayTab@3x.png | Bin .../Images.xcassets/player/Contents.json | 6 + .../player/backward.imageset/Contents.json | 15 + .../player/backward.imageset/backward.pdf | Bin 0 -> 2052 bytes .../player/forward.imageset/Contents.json | 15 + .../player/forward.imageset/forward.pdf | Bin 0 -> 2057 bytes .../player/pause.imageset/Contents.json | 15 + .../player/pause.imageset/pause.pdf | Bin 0 -> 1955 bytes .../player/play.imageset/Contents.json | 15 + .../player/play.imageset/play.pdf | Bin 0 -> 1690 bytes .../player/stop.imageset/Contents.json | 15 + .../player/stop.imageset/stop.pdf | Bin 0 -> 1588 bytes .../share.imageset/Contents.json | 0 .../Images.xcassets/share.imageset/share.png | Bin .../share.imageset/share@2x.png | Bin .../share.imageset/share@3x.png | Bin SwiftRadio/Info.plist | 133 ++++--- SwiftRadio/LaunchScreen.storyboard | 0 SwiftRadio/Main.storyboard | 259 +------------ SwiftRadio/Model/RadioStation.swift | 20 +- SwiftRadio/Model/StationsManager.swift | 6 + SwiftRadio/SceneDelegate.swift | 102 +++++ SwiftRadio/SwiftRadio.entitlements | 0 .../ViewControllers/BaseController.swift | 0 .../ViewControllers/LoaderController.swift | 0 .../NowPlayingViewController.swift | 339 ++++++++--------- .../StationsViewController.swift | 4 +- SwiftRadio/Views/AlbumArtworkView.swift | 143 +++++++ SwiftRadio/Views/BottomSheetHandler.swift | 41 ++ .../Views/BottomSheetViewController.swift | 194 ++++++++++ SwiftRadio/Views/ControlsView.swift | 353 ++++++++++++++++++ SwiftRadio/Views/LogoShareView.swift | 0 SwiftRadio/Views/LogoShareView.xib | 0 SwiftRadio/Views/NowPlayingView.swift | 0 SwiftRadioUITests/Info.plist | 0 SwiftRadioUITests/SwiftRadioUITests.swift | 0 111 files changed, 1311 insertions(+), 599 deletions(-) mode change 100644 => 100755 .github/workflows/carplay.yml mode change 100644 => 100755 .github/workflows/ios.yml mode change 100644 => 100755 .gitignore mode change 100644 => 100755 SwiftRadio.xcodeproj/project.pbxproj mode change 100644 => 100755 SwiftRadio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist mode change 100644 => 100755 SwiftRadio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved mode change 100644 => 100755 SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/jonahss.xcuserdatad/UserInterfaceState.xcuserstate mode change 100644 => 100755 SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/UserInterfaceState.xcuserstate mode change 100644 => 100755 SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/WorkspaceSettings.xcsettings mode change 100644 => 100755 SwiftRadio.xcodeproj/xcshareddata/xcschemes/SwiftRadio.xcscheme mode change 100644 => 100755 SwiftRadio/CarPlay/AppDelegate+CarPlay.swift mode change 100644 => 100755 SwiftRadio/Cells/StationTableViewCell.swift mode change 100644 => 100755 SwiftRadio/Coordinators/Coordinator.swift mode change 100644 => 100755 SwiftRadio/Coordinators/MainCoordinator.swift mode change 100644 => 100755 SwiftRadio/Helpers/Bundle+appName.swift mode change 100644 => 100755 SwiftRadio/Helpers/Handoffable.swift create mode 100644 SwiftRadio/Helpers/NSLayoutConstraint+with.swift mode change 100644 => 100755 SwiftRadio/Helpers/ShareActivity.swift mode change 100644 => 100755 SwiftRadio/Helpers/Storyboard.swift mode change 100644 => 100755 SwiftRadio/Helpers/UIImage+Cache.swift mode change 100644 => 100755 SwiftRadio/Helpers/UIImageView+Cache.swift mode change 100644 => 100755 SwiftRadio/Helpers/UITableViewCell+reuseIdentifier.swift mode change 100644 => 100755 SwiftRadio/Helpers/UIViewController+Email.swift mode change 100644 => 100755 SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/AppIcon.appiconset/SWIFT-RADIO.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio@3x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-80s.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-80s.imageset/station-80s.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/station-classicrock.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/station-killrockstars.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/station-spaceland.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-sub.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-sub.imageset/sub.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm@3x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/stationImage.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@3x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-next.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-next.imageset/btn-next.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@3x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-previous.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@3x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-stop.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@3x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/carPlayTab.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab@3x.png create mode 100755 SwiftRadio/Images.xcassets/player/Contents.json create mode 100755 SwiftRadio/Images.xcassets/player/backward.imageset/Contents.json create mode 100755 SwiftRadio/Images.xcassets/player/backward.imageset/backward.pdf create mode 100755 SwiftRadio/Images.xcassets/player/forward.imageset/Contents.json create mode 100755 SwiftRadio/Images.xcassets/player/forward.imageset/forward.pdf create mode 100755 SwiftRadio/Images.xcassets/player/pause.imageset/Contents.json create mode 100755 SwiftRadio/Images.xcassets/player/pause.imageset/pause.pdf create mode 100755 SwiftRadio/Images.xcassets/player/play.imageset/Contents.json create mode 100755 SwiftRadio/Images.xcassets/player/play.imageset/play.pdf create mode 100755 SwiftRadio/Images.xcassets/player/stop.imageset/Contents.json create mode 100755 SwiftRadio/Images.xcassets/player/stop.imageset/stop.pdf mode change 100644 => 100755 SwiftRadio/Images.xcassets/share.imageset/Contents.json mode change 100644 => 100755 SwiftRadio/Images.xcassets/share.imageset/share.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/share.imageset/share@2x.png mode change 100644 => 100755 SwiftRadio/Images.xcassets/share.imageset/share@3x.png mode change 100644 => 100755 SwiftRadio/LaunchScreen.storyboard mode change 100644 => 100755 SwiftRadio/Model/StationsManager.swift create mode 100644 SwiftRadio/SceneDelegate.swift mode change 100644 => 100755 SwiftRadio/SwiftRadio.entitlements mode change 100644 => 100755 SwiftRadio/ViewControllers/BaseController.swift mode change 100644 => 100755 SwiftRadio/ViewControllers/LoaderController.swift mode change 100644 => 100755 SwiftRadio/ViewControllers/NowPlayingViewController.swift mode change 100644 => 100755 SwiftRadio/ViewControllers/StationsViewController.swift create mode 100755 SwiftRadio/Views/AlbumArtworkView.swift create mode 100644 SwiftRadio/Views/BottomSheetHandler.swift create mode 100644 SwiftRadio/Views/BottomSheetViewController.swift create mode 100755 SwiftRadio/Views/ControlsView.swift mode change 100644 => 100755 SwiftRadio/Views/LogoShareView.swift mode change 100644 => 100755 SwiftRadio/Views/LogoShareView.xib mode change 100644 => 100755 SwiftRadio/Views/NowPlayingView.swift mode change 100644 => 100755 SwiftRadioUITests/Info.plist mode change 100644 => 100755 SwiftRadioUITests/SwiftRadioUITests.swift diff --git a/.github/workflows/carplay.yml b/.github/workflows/carplay.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/SwiftRadio.xcodeproj/project.pbxproj b/SwiftRadio.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 7d661445..7a102790 --- a/SwiftRadio.xcodeproj/project.pbxproj +++ b/SwiftRadio.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -14,7 +14,6 @@ 6258DCDA22D93A5400166C65 /* LogoShareView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6258DCD922D93A5400166C65 /* LogoShareView.xib */; }; 9409E11C1ABF6FEA00312E2B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9409E11B1ABF6FEA00312E2B /* AppDelegate.swift */; }; 9409E1261ABF6FEA00312E2B /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9409E1251ABF6FEA00312E2B /* Images.xcassets */; }; - 9409E1401ABF78B000312E2B /* NowPlayingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9409E13F1ABF78B000312E2B /* NowPlayingViewController.swift */; }; 94452E4F1AD6F24700BFE7A5 /* PopUpMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94452E4E1AD6F24700BFE7A5 /* PopUpMenuViewController.swift */; }; 94452E551AD7086800BFE7A5 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94452E541AD7086800BFE7A5 /* AboutViewController.swift */; }; 945DB3C21AD58E3A00495EBB /* stations.json in Resources */ = {isa = PBXBuildFile; fileRef = 945DB3C11AD58E3A00495EBB /* stations.json */; }; @@ -25,6 +24,14 @@ 94D260911B45D20000DE671C /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D260901B45D20000DE671C /* Config.swift */; }; 94D260961B45E3FA00DE671C /* AnimationFrames.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D260951B45E3FA00DE671C /* AnimationFrames.swift */; }; 94E9761C1B1A8F3200F52B1E /* UIImage+DropShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94E9761B1B1A8F3200F52B1E /* UIImage+DropShadow.swift */; }; + CA142F672D3D8E2F0071A388 /* NSLayoutConstraint+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F662D3D8E280071A388 /* NSLayoutConstraint+with.swift */; }; + CA142F682D3D8E2F0071A388 /* NSLayoutConstraint+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F662D3D8E280071A388 /* NSLayoutConstraint+with.swift */; }; + CA142F6A2D3D9A0D0071A388 /* BottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F692D3D9A0D0071A388 /* BottomSheetViewController.swift */; }; + CA142F6B2D3D9A0D0071A388 /* BottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F692D3D9A0D0071A388 /* BottomSheetViewController.swift */; }; + CA142F6D2D3D9DFB0071A388 /* BottomSheetHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F6C2D3D9DFB0071A388 /* BottomSheetHandler.swift */; }; + CA142F6E2D3D9DFB0071A388 /* BottomSheetHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F6C2D3D9DFB0071A388 /* BottomSheetHandler.swift */; }; + CAB4E8292D3D7DC7001282E9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB4E8282D3D7DC7001282E9 /* SceneDelegate.swift */; }; + CAB4E82A2D3D7DC7001282E9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB4E8282D3D7DC7001282E9 /* SceneDelegate.swift */; }; CE0A4996291F3AD40071C0CC /* AppDelegate+CarPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0A4995291F3AD40071C0CC /* AppDelegate+CarPlay.swift */; }; CE0A4997291F3B080071C0CC /* AppDelegate+CarPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0A4995291F3AD40071C0CC /* AppDelegate+CarPlay.swift */; }; CE321FF329371140001572BD /* Bundle+appName.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE321FF229371140001572BD /* Bundle+appName.swift */; }; @@ -45,7 +52,6 @@ CE60362A2A48F70A00E15E15 /* NVActivityIndicatorViewExtended in Frameworks */ = {isa = PBXBuildFile; productRef = CE6036292A48F70A00E15E15 /* NVActivityIndicatorViewExtended */; }; CE6A3E31291F376D0058C82A /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D260901B45D20000DE671C /* Config.swift */; }; CE6A3E33291F376D0058C82A /* ShareActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53113F38230C720900462C0E /* ShareActivity.swift */; }; - CE6A3E34291F376D0058C82A /* NowPlayingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9409E13F1ABF78B000312E2B /* NowPlayingViewController.swift */; }; CE6A3E35291F376D0058C82A /* StationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE963ECB29135A6F004F299E /* StationsManager.swift */; }; CE6A3E36291F376D0058C82A /* InfoDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D1D0A41AD6D6230022CA11 /* InfoDetailViewController.swift */; }; CE6A3E37291F376D0058C82A /* LogoShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6258DCD722D93A3500166C65 /* LogoShareView.swift */; }; @@ -84,6 +90,12 @@ CED6353C293081ED002B216F /* Handoffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED6353A293081ED002B216F /* Handoffable.swift */; }; CEDABBEB291217AF00C0367F /* UIImageView+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDABBEA291217AF00C0367F /* UIImageView+Cache.swift */; }; CEDABBED29121BBA00C0367F /* UIImage+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDABBEC29121BBA00C0367F /* UIImage+Cache.swift */; }; + CEE9A2782B5345C30018FE68 /* NowPlayingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A2772B5345C30018FE68 /* NowPlayingViewController.swift */; }; + CEE9A2792B5345C30018FE68 /* NowPlayingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A2772B5345C30018FE68 /* NowPlayingViewController.swift */; }; + CEE9A27B2B535C780018FE68 /* AlbumArtworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A27A2B535C780018FE68 /* AlbumArtworkView.swift */; }; + CEE9A27C2B535C780018FE68 /* AlbumArtworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A27A2B535C780018FE68 /* AlbumArtworkView.swift */; }; + CEE9A27E2B54A5520018FE68 /* ControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A27D2B54A5520018FE68 /* ControlsView.swift */; }; + CEE9A27F2B54A5520018FE68 /* ControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A27D2B54A5520018FE68 /* ControlsView.swift */; }; CF72ACE721F7155200461EED /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CF72ACE621F714D000461EED /* Main.storyboard */; }; /* End PBXBuildFile section */ @@ -109,7 +121,6 @@ 9409E11A1ABF6FEA00312E2B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9409E11B1ABF6FEA00312E2B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 9409E1251ABF6FEA00312E2B /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - 9409E13F1ABF78B000312E2B /* NowPlayingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NowPlayingViewController.swift; sourceTree = ""; }; 94452E4E1AD6F24700BFE7A5 /* PopUpMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopUpMenuViewController.swift; sourceTree = ""; }; 94452E541AD7086800BFE7A5 /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 945DB3C11AD58E3A00495EBB /* stations.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = stations.json; sourceTree = ""; }; @@ -120,6 +131,10 @@ 94D260901B45D20000DE671C /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 94D260951B45E3FA00DE671C /* AnimationFrames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationFrames.swift; sourceTree = ""; }; 94E9761B1B1A8F3200F52B1E /* UIImage+DropShadow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+DropShadow.swift"; sourceTree = ""; }; + CA142F662D3D8E280071A388 /* NSLayoutConstraint+with.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+with.swift"; sourceTree = ""; }; + CA142F692D3D9A0D0071A388 /* BottomSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetViewController.swift; sourceTree = ""; }; + CA142F6C2D3D9DFB0071A388 /* BottomSheetHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetHandler.swift; sourceTree = ""; }; + CAB4E8282D3D7DC7001282E9 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; CE0A4994291F3A220071C0CC /* SwiftRadio.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftRadio.entitlements; sourceTree = ""; }; CE0A4995291F3AD40071C0CC /* AppDelegate+CarPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CarPlay.swift"; sourceTree = ""; }; CE321FF229371140001572BD /* Bundle+appName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+appName.swift"; sourceTree = ""; }; @@ -139,6 +154,9 @@ CED6353A293081ED002B216F /* Handoffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Handoffable.swift; sourceTree = ""; }; CEDABBEA291217AF00C0367F /* UIImageView+Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Cache.swift"; sourceTree = ""; }; CEDABBEC29121BBA00C0367F /* UIImage+Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Cache.swift"; sourceTree = ""; }; + CEE9A2772B5345C30018FE68 /* NowPlayingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingViewController.swift; sourceTree = ""; }; + CEE9A27A2B535C780018FE68 /* AlbumArtworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumArtworkView.swift; sourceTree = ""; }; + CEE9A27D2B54A5520018FE68 /* ControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsView.swift; sourceTree = ""; }; CF72ACE621F714D000461EED /* Main.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ @@ -219,6 +237,7 @@ CE8062F2291EF545008BD097 /* Cells */, 94D260901B45D20000DE671C /* Config.swift */, 9409E11B1ABF6FEA00312E2B /* AppDelegate.swift */, + CAB4E8282D3D7DC7001282E9 /* SceneDelegate.swift */, CF72ACE621F714D000461EED /* Main.storyboard */, 9409E1251ABF6FEA00312E2B /* Images.xcassets */, 9409E11A1ABF6FEA00312E2B /* Info.plist */, @@ -242,6 +261,10 @@ 6258DCD722D93A3500166C65 /* LogoShareView.swift */, 6258DCD922D93A5400166C65 /* LogoShareView.xib */, CE60361C2A48C4A400E15E15 /* NowPlayingView.swift */, + CEE9A27A2B535C780018FE68 /* AlbumArtworkView.swift */, + CEE9A27D2B54A5520018FE68 /* ControlsView.swift */, + CA142F692D3D9A0D0071A388 /* BottomSheetViewController.swift */, + CA142F6C2D3D9DFB0071A388 /* BottomSheetHandler.swift */, ); path = Views; sourceTree = ""; @@ -304,8 +327,8 @@ 94452E541AD7086800BFE7A5 /* AboutViewController.swift */, 94D1D0A41AD6D6230022CA11 /* InfoDetailViewController.swift */, 94452E4E1AD6F24700BFE7A5 /* PopUpMenuViewController.swift */, - 9409E13F1ABF78B000312E2B /* NowPlayingViewController.swift */, CE6036122A47A87A00E15E15 /* StationsViewController.swift */, + CEE9A2772B5345C30018FE68 /* NowPlayingViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -313,6 +336,7 @@ CE8062F7291EF5D9008BD097 /* Helpers */ = { isa = PBXGroup; children = ( + CA142F662D3D8E280071A388 /* NSLayoutConstraint+with.swift */, CE6ECCFF292F215F008B3C16 /* Storyboard.swift */, CEDABBEA291217AF00C0367F /* UIImageView+Cache.swift */, CEDABBEC29121BBA00C0367F /* UIImage+Cache.swift */, @@ -500,8 +524,10 @@ CE6036192A48BA2500E15E15 /* UITableViewCell+reuseIdentifier.swift in Sources */, CE6036162A47B88600E15E15 /* StationTableViewCell.swift in Sources */, CE9EE8DE293BB41300F62041 /* BaseController.swift in Sources */, + CA142F6D2D3D9DFB0071A388 /* BottomSheetHandler.swift in Sources */, + CEE9A2782B5345C30018FE68 /* NowPlayingViewController.swift in Sources */, 53113F39230C720900462C0E /* ShareActivity.swift in Sources */, - 9409E1401ABF78B000312E2B /* NowPlayingViewController.swift in Sources */, + CA142F682D3D8E2F0071A388 /* NSLayoutConstraint+with.swift in Sources */, CE963ECC29135A6F004F299E /* StationsManager.swift in Sources */, CE0A4997291F3B080071C0CC /* AppDelegate+CarPlay.swift in Sources */, CE321FF329371140001572BD /* Bundle+appName.swift in Sources */, @@ -514,14 +540,18 @@ 94452E551AD7086800BFE7A5 /* AboutViewController.swift in Sources */, CED6353B293081ED002B216F /* Handoffable.swift in Sources */, 94452E4F1AD6F24700BFE7A5 /* PopUpMenuViewController.swift in Sources */, + CEE9A27B2B535C780018FE68 /* AlbumArtworkView.swift in Sources */, CE6ECCFD292F1445008B3C16 /* MainCoordinator.swift in Sources */, 9409E11C1ABF6FEA00312E2B /* AppDelegate.swift in Sources */, 94D260961B45E3FA00DE671C /* AnimationFrames.swift in Sources */, + CAB4E82A2D3D7DC7001282E9 /* SceneDelegate.swift in Sources */, CE6ECD03292F358C008B3C16 /* UIViewController+Email.swift in Sources */, CEDABBED29121BBA00C0367F /* UIImage+Cache.swift in Sources */, CE6036252A48F68D00E15E15 /* NowPlayingView.swift in Sources */, 94AC70AE1AD05C6200652982 /* RadioStation.swift in Sources */, + CA142F6A2D3D9A0D0071A388 /* BottomSheetViewController.swift in Sources */, CE9EE8E1293C048A00F62041 /* LoaderController.swift in Sources */, + CEE9A27E2B54A5520018FE68 /* ControlsView.swift in Sources */, CEDABBEB291217AF00C0367F /* UIImageView+Cache.swift in Sources */, CE6036132A47A87A00E15E15 /* StationsViewController.swift in Sources */, ); @@ -535,8 +565,10 @@ CE60361A2A48BA2500E15E15 /* UITableViewCell+reuseIdentifier.swift in Sources */, CE6036172A47B88600E15E15 /* StationTableViewCell.swift in Sources */, CE9EE8DF293BB41300F62041 /* BaseController.swift in Sources */, + CA142F6E2D3D9DFB0071A388 /* BottomSheetHandler.swift in Sources */, + CEE9A2792B5345C30018FE68 /* NowPlayingViewController.swift in Sources */, CE6A3E33291F376D0058C82A /* ShareActivity.swift in Sources */, - CE6A3E34291F376D0058C82A /* NowPlayingViewController.swift in Sources */, + CA142F672D3D8E2F0071A388 /* NSLayoutConstraint+with.swift in Sources */, CE6A3E35291F376D0058C82A /* StationsManager.swift in Sources */, CE0A4996291F3AD40071C0CC /* AppDelegate+CarPlay.swift in Sources */, CE321FF429371140001572BD /* Bundle+appName.swift in Sources */, @@ -549,14 +581,18 @@ CE6A3E3A291F376D0058C82A /* AboutViewController.swift in Sources */, CED6353C293081ED002B216F /* Handoffable.swift in Sources */, CE6A3E3B291F376D0058C82A /* PopUpMenuViewController.swift in Sources */, + CEE9A27C2B535C780018FE68 /* AlbumArtworkView.swift in Sources */, CE6ECCFE292F1448008B3C16 /* MainCoordinator.swift in Sources */, CE6A3E3C291F376D0058C82A /* AppDelegate.swift in Sources */, CE6A3E3D291F376D0058C82A /* AnimationFrames.swift in Sources */, + CAB4E8292D3D7DC7001282E9 /* SceneDelegate.swift in Sources */, CE6ECD04292F358C008B3C16 /* UIViewController+Email.swift in Sources */, CE6A3E3E291F376D0058C82A /* UIImage+Cache.swift in Sources */, CE6036242A48F68D00E15E15 /* NowPlayingView.swift in Sources */, CE6A3E40291F376D0058C82A /* RadioStation.swift in Sources */, + CA142F6B2D3D9A0D0071A388 /* BottomSheetViewController.swift in Sources */, CE9EE8E2293C048A00F62041 /* LoaderController.swift in Sources */, + CEE9A27F2B54A5520018FE68 /* ControlsView.swift in Sources */, CE6A3E41291F376D0058C82A /* UIImageView+Cache.swift in Sources */, CE6036142A47A87A00E15E15 /* StationsViewController.swift in Sources */, ); @@ -579,7 +615,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = SwiftRadioUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -599,7 +635,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = SwiftRadioUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -733,7 +769,7 @@ "$(PROJECT_DIR)/SwiftRadio", ); INFOPLIST_FILE = SwiftRadio/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -763,7 +799,7 @@ "$(PROJECT_DIR)/SwiftRadio", ); INFOPLIST_FILE = SwiftRadio/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -796,7 +832,7 @@ "$(PROJECT_DIR)/SwiftRadio", ); INFOPLIST_FILE = "SwiftRadio/Info-CarPlay.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -831,7 +867,7 @@ "$(PROJECT_DIR)/SwiftRadio", ); INFOPLIST_FILE = "SwiftRadio/Info-CarPlay.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -903,7 +939,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/fethica/FRadioPlayer.git"; requirement = { - branch = v0.2.0; + branch = v0.2.1; kind = branch; }; }; diff --git a/SwiftRadio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SwiftRadio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist old mode 100644 new mode 100755 diff --git a/SwiftRadio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftRadio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved old mode 100644 new mode 100755 index 4645fed8..bd3d64ce --- a/SwiftRadio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftRadio.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/fethica/FRadioPlayer.git", "state" : { - "branch" : "v0.2.0", - "revision" : "c3f472ed8cb59b442312362bca363863962c88b3" + "branch" : "v0.2.1", + "revision" : "a14ce692f28ece480b5eeb9e208aa98493ee4922" } }, { diff --git a/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/jonahss.xcuserdatad/UserInterfaceState.xcuserstate b/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/jonahss.xcuserdatad/UserInterfaceState.xcuserstate old mode 100644 new mode 100755 diff --git a/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/UserInterfaceState.xcuserstate b/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/UserInterfaceState.xcuserstate old mode 100644 new mode 100755 diff --git a/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/WorkspaceSettings.xcsettings b/SwiftRadio.xcodeproj/project.xcworkspace/xcuserdata/wu.xcuserdatad/WorkspaceSettings.xcsettings old mode 100644 new mode 100755 diff --git a/SwiftRadio.xcodeproj/xcshareddata/xcschemes/SwiftRadio.xcscheme b/SwiftRadio.xcodeproj/xcshareddata/xcschemes/SwiftRadio.xcscheme old mode 100644 new mode 100755 diff --git a/SwiftRadio/AppDelegate.swift b/SwiftRadio/AppDelegate.swift index 6d0f576f..b93d3685 100755 --- a/SwiftRadio/AppDelegate.swift +++ b/SwiftRadio/AppDelegate.swift @@ -10,7 +10,7 @@ import UIKit import MediaPlayer import FRadioPlayer -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? @@ -86,6 +86,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, + didDiscardSceneSessions sceneSessions: Set) { + } + // MARK: - Remote Controls private func setupRemoteCommandCenter() { @@ -135,4 +147,3 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } } - diff --git a/SwiftRadio/CarPlay/AppDelegate+CarPlay.swift b/SwiftRadio/CarPlay/AppDelegate+CarPlay.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Cells/StationTableViewCell.swift b/SwiftRadio/Cells/StationTableViewCell.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Config.swift b/SwiftRadio/Config.swift index b48eed40..0d061709 100755 --- a/SwiftRadio/Config.swift +++ b/SwiftRadio/Config.swift @@ -21,7 +21,7 @@ struct Config { static let searchable = false // Set this to "false" to show the next/previous player buttons - static let hideNextPreviousButtons = true + static let hideNextPreviousButtons = false // Contact infos static let website = "https://github.com/analogcode/Swift-Radio-Pro" diff --git a/SwiftRadio/Coordinators/Coordinator.swift b/SwiftRadio/Coordinators/Coordinator.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Coordinators/MainCoordinator.swift b/SwiftRadio/Coordinators/MainCoordinator.swift old mode 100644 new mode 100755 index 3b957572..32b557f3 --- a/SwiftRadio/Coordinators/MainCoordinator.swift +++ b/SwiftRadio/Coordinators/MainCoordinator.swift @@ -66,7 +66,7 @@ extension MainCoordinator: LoaderControllerDelegate { extension MainCoordinator: StationsViewControllerDelegate { func pushNowPlayingController(_ stationsViewController: StationsViewController, newStation: Bool) { - let nowPlayingController = Storyboard.viewController as NowPlayingViewController + let nowPlayingController = NowPlayingViewController() nowPlayingController.delegate = self nowPlayingController.isNewStation = newStation navigationController.pushViewController(nowPlayingController, animated: true) @@ -83,21 +83,14 @@ extension MainCoordinator: StationsViewControllerDelegate { extension MainCoordinator: NowPlayingViewControllerDelegate { - func didTapInfoButton(_ nowPlayingViewController: NowPlayingViewController, station: RadioStation) { - let infoController = Storyboard.viewController as InfoDetailViewController - infoController.currentStation = station - navigationController.pushViewController(infoController, animated: true) + func didSelectBottomSheetOption(_ option: BottomSheetViewController.Option, from controller: NowPlayingViewController) { + guard let station = StationsManager.shared.currentStation else { return } + BottomSheetHandler.handle(option, station: station, from: controller) } func didTapCompanyButton(_ nowPlayingViewController: NowPlayingViewController) { openAbout(in: nowPlayingViewController) } - - func didTapShareButton(_ nowPlayingViewController: NowPlayingViewController, station: RadioStation, artworkURL: URL?) { - ShareActivity.activityController(station: station, artworkURL: artworkURL, sourceView: nowPlayingViewController.view) { [weak nowPlayingViewController] controller in - nowPlayingViewController?.present(controller, animated: true, completion: nil) - } - } } // MARK: - PopUpMenuViewControllerDelegate diff --git a/SwiftRadio/Data/stations.json b/SwiftRadio/Data/stations.json index dc7252bc..07456348 100755 --- a/SwiftRadio/Data/stations.json +++ b/SwiftRadio/Data/stations.json @@ -1,40 +1,52 @@ { "station": [ - { - "name": "Absolute Country Hits", - "streamURL": "http://strm112.1.fm/acountry_mobile_mp3", - "imageURL": "station-absolutecountry.png", - "desc": "The Music Starts Here", - "longDesc": "All your favorite country hits and artists, from Johnny Cash to Taylor Swift, on 1.FM's Absolute Country, playing non-stop crooners and banjos, dance-tunes and fiddles, ballads and harmonicas. Absolute Country focuses on 5th, 6th and 7th generation Country (from the 90s on) but often delves into classic, older tunes as well." - }, - { - "name": "AZ Rock Radio", - "streamURL": "http://cassini.shoutca.st:9300/stream", - "imageURL": "az-rock-radio", - "desc": "We Know Music from A to Z", - "longDesc": "Web Radio Station from Puerto Rico. Download our App (Apple & Android) or go to: www.azrockradio.com" - }, - { - "name": "The Rock FM", - "streamURL": "http://tunein-icecast.mediaworks.nz/rock_128kbps", - "imageURL": "https://fethica.com/assets/swift-radio/station-therockfm@3x.png", - "desc": "Rock Music", - "longDesc": "NZ's number one Rock music station." - }, - { - "name": "Classic Rock", - "streamURL": "http://rfcmedia.streamguys1.com/classicrock.mp3", - "imageURL": "station-classicrock", - "desc": "Classic Rock Hits", - "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." - }, - { - "name": "Radio 1190", - "streamURL": "http://104.250.149.122:8082/stream", - "imageURL": "", - "desc": "KVCU - Boulder, CO", - "longDesc": "Radio 1190 is the bomb." - } + { + "name": "Absolute Country Hits", + "website": "https://www.1.fm/station/acountry", + "streamURL": "http://strm112.1.fm/acountry_mobile_mp3", + "imageURL": "station-absolutecountry.png", + "desc": "The Music Starts Here", + "longDesc": "All your favorite country hits and artists, from Johnny Cash to Taylor Swift, on 1.FM's Absolute Country, playing non-stop crooners and banjos, dance-tunes and fiddles, ballads and harmonicas. Absolute Country focuses on 5th, 6th and 7th generation Country (from the 90s on) but often delves into classic, older tunes as well." + }, + { + "name": "AZ Rock Radio", + "website": "https://www.azrockradio.com", + "streamURL": "http://cassini.shoutca.st:9300/stream", + "imageURL": "az-rock-radio", + "desc": "We Know Music from A to Z", + "longDesc": "Web Radio Station from Puerto Rico. Download our App (Apple & Android) or go to: www.azrockradio.com" + }, + { + "name": "The Rock FM", + "website": "https://www.therock.net.nz", + "streamURL": "https://20593.live.streamtheworld.com/CKGEFMAAC.aac", + "imageURL": "https://fethica.com/assets/swift-radio/949-the-rock-radio.png", + "desc": "Rock Music", + "longDesc": "NZ's number one Rock music station." + }, + { + "name": "Classic Rock", + "streamURL": "https://rfcm.streamguys1.com/classicrock-mp3", + "imageURL": "station-classicrock", + "desc": "Classic Rock Hits", + "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." + }, + { + "name": "Radio 1190", + "website": "https://radio1190.org", + "streamURL": "http://kvcu.streamguys1.com/live", + "imageURL": "", + "desc": "KVCU - Boulder, CO", + "longDesc": "Radio 1190 is the bomb." + }, + { + "name": "MP3 file sample", + "website": "https://samplelib.com", + "streamURL": "https://freetestdata.com/wp-content/uploads/2021/09/Free_Test_Data_1OMB_MP3.mp3", + "imageURL": "", + "desc": "From: https://samplelib.com", + "longDesc": "https://samplelib.com" + } ] } diff --git a/SwiftRadio/Helpers/Bundle+appName.swift b/SwiftRadio/Helpers/Bundle+appName.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Helpers/Handoffable.swift b/SwiftRadio/Helpers/Handoffable.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Helpers/NSLayoutConstraint+with.swift b/SwiftRadio/Helpers/NSLayoutConstraint+with.swift new file mode 100644 index 00000000..372a7498 --- /dev/null +++ b/SwiftRadio/Helpers/NSLayoutConstraint+with.swift @@ -0,0 +1,16 @@ +// +// NSLayoutConstraint+with.swift +// SwiftRadio +// +// Created by Fethi El Hassasna on 1/19/25. +// Copyright © 2025 matthewfecher.com. All rights reserved. +// + +import UIKit + +extension NSLayoutConstraint { + func with(_ config: (NSLayoutConstraint) -> Void) -> NSLayoutConstraint { + config(self) + return self + } +} diff --git a/SwiftRadio/Helpers/ShareActivity.swift b/SwiftRadio/Helpers/ShareActivity.swift old mode 100644 new mode 100755 index 6730c60d..42eed931 --- a/SwiftRadio/Helpers/ShareActivity.swift +++ b/SwiftRadio/Helpers/ShareActivity.swift @@ -10,31 +10,22 @@ import UIKit struct ShareActivity { - static func activityController(station: RadioStation, artworkURL: URL?, sourceView: UIView, _ completion: @escaping (UIActivityViewController) -> Void) { - - getImage(station: station, artworkURL: artworkURL) { image in - let shareImage = generateImage(from: image, station: station) - - let activityViewController = UIActivityViewController(activityItems: [station.shoutout, shareImage], applicationActivities: nil) - activityViewController.popoverPresentationController?.sourceRect = CGRect(x: sourceView.center.x, y: sourceView.center.y, width: 0, height: 0) - activityViewController.popoverPresentationController?.sourceView = sourceView - activityViewController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0) - - activityViewController.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems:[Any]?, error: Error?) in - if completed { - // do something on completion if you want - } + static func activityController(image: UIImage?, station: RadioStation, sourceView: UIView, _ completion: @escaping (UIActivityViewController) -> Void) { + let shareImage = generateImage(from: image, station: station) + + let activityViewController = UIActivityViewController(activityItems: [station.shoutout, shareImage], applicationActivities: nil) + activityViewController.popoverPresentationController?.sourceRect = CGRect(x: sourceView.center.x, y: sourceView.center.y, width: 0, height: 0) + activityViewController.popoverPresentationController?.sourceView = sourceView + activityViewController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0) + + activityViewController.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems:[Any]?, error: Error?) in + if completed { + // do something on completion if you want } - - completion(activityViewController) } - } - - private static func getImage(station: RadioStation, artworkURL: URL?, _ completion: @escaping (UIImage?) -> Void) { - if let artworkURL = artworkURL { - UIImage.image(from: artworkURL) { completion($0) } - } else { - station.getImage { completion($0) } + + DispatchQueue.main.async { + completion(activityViewController) } } diff --git a/SwiftRadio/Helpers/Storyboard.swift b/SwiftRadio/Helpers/Storyboard.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Helpers/UIImage+Cache.swift b/SwiftRadio/Helpers/UIImage+Cache.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Helpers/UIImageView+Cache.swift b/SwiftRadio/Helpers/UIImageView+Cache.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Helpers/UITableViewCell+reuseIdentifier.swift b/SwiftRadio/Helpers/UITableViewCell+reuseIdentifier.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Helpers/UIViewController+Email.swift b/SwiftRadio/Helpers/UIViewController+Email.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/SwiftRadio/Images.xcassets/AppIcon.appiconset/Icon-83.5@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/AppIcon.appiconset/SWIFT-RADIO.png b/SwiftRadio/Images.xcassets/AppIcon.appiconset/SWIFT-RADIO.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/Contents.json b/SwiftRadio/Images.xcassets/Stations/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio.png b/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio@2x.png b/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio@3x.png b/SwiftRadio/Images.xcassets/Stations/az-rock-radio.imageset/az-rock-radio@3x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-80s.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-80s.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-80s.imageset/station-80s.png b/SwiftRadio/Images.xcassets/Stations/station-80s.imageset/station-80s.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry.png b/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry@2x.png b/SwiftRadio/Images.xcassets/Stations/station-absolutecountry.imageset/station-absolutecountry@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault.png b/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault@2x.png b/SwiftRadio/Images.xcassets/Stations/station-altvault.imageset/station-altvault@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/station-classicrock.png b/SwiftRadio/Images.xcassets/Stations/station-classicrock.imageset/station-classicrock.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/station-killrockstars.png b/SwiftRadio/Images.xcassets/Stations/station-killrockstars.imageset/station-killrockstars.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk.png b/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk@2x.png b/SwiftRadio/Images.xcassets/Stations/station-newportfolk.imageset/station-newportfolk@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/station-spaceland.png b/SwiftRadio/Images.xcassets/Stations/station-spaceland.imageset/station-spaceland.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-sub.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-sub.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-sub.imageset/sub.png b/SwiftRadio/Images.xcassets/Stations/station-sub.imageset/sub.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm.png b/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm@2x.png b/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm@3x.png b/SwiftRadio/Images.xcassets/Stations/station-therockfm.imageset/station-therockfm@3x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/Contents.json b/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage.png b/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@2x.png b/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@3x.png b/SwiftRadio/Images.xcassets/Stations/stationImage.imageset/stationImage@3x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-next.imageset/Contents.json b/SwiftRadio/Images.xcassets/btn-next.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next.png b/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@2x.png b/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@3x.png b/SwiftRadio/Images.xcassets/btn-next.imageset/btn-next@3x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-previous.imageset/Contents.json b/SwiftRadio/Images.xcassets/btn-previous.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous.png b/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@2x.png b/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@3x.png b/SwiftRadio/Images.xcassets/btn-previous.imageset/btn-previous@3x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-stop.imageset/Contents.json b/SwiftRadio/Images.xcassets/btn-stop.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop.png b/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@2x.png b/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@3x.png b/SwiftRadio/Images.xcassets/btn-stop.imageset/btn-stop@3x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/carPlayTab.imageset/Contents.json b/SwiftRadio/Images.xcassets/carPlayTab.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab.png b/SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab@2x.png b/SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab@3x.png b/SwiftRadio/Images.xcassets/carPlayTab.imageset/carPlayTab@3x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/player/Contents.json b/SwiftRadio/Images.xcassets/player/Contents.json new file mode 100755 index 00000000..73c00596 --- /dev/null +++ b/SwiftRadio/Images.xcassets/player/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SwiftRadio/Images.xcassets/player/backward.imageset/Contents.json b/SwiftRadio/Images.xcassets/player/backward.imageset/Contents.json new file mode 100755 index 00000000..7cfdb5b7 --- /dev/null +++ b/SwiftRadio/Images.xcassets/player/backward.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "backward.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/SwiftRadio/Images.xcassets/player/backward.imageset/backward.pdf b/SwiftRadio/Images.xcassets/player/backward.imageset/backward.pdf new file mode 100755 index 0000000000000000000000000000000000000000..20fc4ec14651986d748e2755445e1155aba00c17 GIT binary patch literal 2052 zcmZXVO>Yx15Qgvm6~0s|2ON*bpAtfXwjczEl5#^FmhDo4nq9~)sPO9%Z>DDI4&Pw^(Kb@6=rld@`SZh{==$4i zCd`rjyRN^RzDp#yriEghc3ra-tIO`srtQ|RU&@P{`dd4SUt)Cy@@^EMas-N@mzSsI z?ew?uQ@dr$vaGy}Z7+=SA!v;3cq@W3-W#;c?nP1>0)}c`QputTRE|mr(O6lCQ30&; zC|k0dSD{v@UC>r$XA3k6Y=WT4cJ`0BR9Mnr)N;NI?*(OGTueqXEGks zIybiDoZZc=R8|6%pHBPMithrg3R(x_sVQEENP=wzdS#N1A@_5J7?exN5CRpoiJnp~ z$SNHmT1(wyXd?BDU~V5%1Z7ATAoCK03eLJjkem%O@`)-2XPuQyi5UR&E;I0?j?Qlw zy-p}8ijQb4P)0xr2j_g0kd!mgS)`0WD{H-_VStziD(FaxMr*vGg@MeSj;=ypInZFt zenOUYPN8$wBw4w$3axPzx(bYHKQno}I7uMmU}DaOtD&tKKvUDWXx89Vt1O|XvR&R2 ztkza|r0)9!dHiPyy;H_H8hnDQ>{KgN|l7f zv^9Xo5+VtQKwI%hLW~Nu!9*ijB&0#3AU@U_R;&`SqD95ZV^Oh+U8%@t^u2yWKP|hX?tY2BI7m^NaTjqN$tN;%VKD!|z?&jWXXY>)GAK&`(`Y5%Yzy!0Tp5 zOu8A-g|XC-5noGhH@B3k%oVKV66a$0O}227|45|rMCVlHDHk}FfL!=RGd1_a-LdJz zuKOmm4B}8>{x3ak2qy{856~vckL(M|r{EC+r>PZ>5M3sZkWb+;3V{u$U9-9Gb|T-? zZ!W1F=i{*Lp2+jf?UO@OR_kGyWG;B$;ML9MUq{*R@8M=Q?aP6PTAZC-eR%aBK_#tG literal 0 HcmV?d00001 diff --git a/SwiftRadio/Images.xcassets/player/forward.imageset/Contents.json b/SwiftRadio/Images.xcassets/player/forward.imageset/Contents.json new file mode 100755 index 00000000..c52f624b --- /dev/null +++ b/SwiftRadio/Images.xcassets/player/forward.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "forward.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/SwiftRadio/Images.xcassets/player/forward.imageset/forward.pdf b/SwiftRadio/Images.xcassets/player/forward.imageset/forward.pdf new file mode 100755 index 0000000000000000000000000000000000000000..c109eb94a14c36ced9260098d993906523b2c1e4 GIT binary patch literal 2057 zcmZvdU2oeq6o&8lE6z=T_M$WozXcQp)+8;4ZCK{)uIPoPsHq2A+AMX*_Urdjwn#du z5XAChp3j#&yt=#j@P=g&f=SW%$De}e?OWR1KFuHR=XUN--+1}xT2eA-?UbT-?@oWB zA0CpK&=>YU_rv4-l?>n(3&k`a`}QC(iLAVHqjq<8^J_xU+3z3a4a4)=-#x~Z40=<*kIinp=V>oI{ z1!)=BI&Y{G1sWxdazR;w!)$G|WZQ61RDu|@R$8(ttg8|)bT6Ej+Jwe>D%3e?m}At0 zILf20NEOQ&!Dn8XlZVR;bCD%V?(}#1*om(K^*U!wwCIpmA!3&tgkgysy{q7onMrvo z83QY8Q?k)F*ja?-F6fv#=}a(AP0hS? ztXS*hv#bo#N2lPu@))HLAt>~$#BQY15p}`3q=~Y8G#at@i7IOvXHpsL8*3Dmvcas; z*kM%qQ&=Z-lufi)lB~}wHF9%aC1qJ^4u&-qmMknxEHWz8N);Fs2^Glbm5v_PIZD>1 zq?1un5lSriT!@HMZ+wH(yb6of$Izh7Swd`ZHXNE-LG6-Zuz;0$R9}qF!8Y{eO zk6?rWyCB>eYYhpNyXmLh15%|_!IsuI*W)+X@{9Z@kjfKgPUR^TIOTw)@auMN_v7QG z>C>_QA`}I2DnI?l4d9F;;Pb<{TGB&9Mge(VAa)8_JVWUuU811Sn`bB#!}-|m_We<$ z%lgL~wByw@9{ShxYWMK^)RfJ39A`=eFAd(@@BZtN{c;(%$9XA-qjBQu>h{+U{{!Hl Bt`7hJ literal 0 HcmV?d00001 diff --git a/SwiftRadio/Images.xcassets/player/pause.imageset/Contents.json b/SwiftRadio/Images.xcassets/player/pause.imageset/Contents.json new file mode 100755 index 00000000..7c819ca8 --- /dev/null +++ b/SwiftRadio/Images.xcassets/player/pause.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "pause.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/SwiftRadio/Images.xcassets/player/pause.imageset/pause.pdf b/SwiftRadio/Images.xcassets/player/pause.imageset/pause.pdf new file mode 100755 index 0000000000000000000000000000000000000000..32450ccf51617e7bfdde6094f08bbac97c547a79 GIT binary patch literal 1955 zcma)7$!^;)5WVv&<`N(|gyJg7KwzM878Gq!*Xb?jL6sTDg)Oy~Ql$O*4rPf$$3Bz? zn|hq(%_BKHo8R1BO70kwAfWmFg8^J!L3aDv-9L0?=l5T#|Eo$6ys!cXOs8mhG_qSs zmO2=ow46?Zzps}eAyWsLKl*yrJ%dJY5vzWk76AL_;v{lWdjK^Yo3Yea!Hk?EFc+y_}`n<~CxxMfyNtV?aR z;f2(NGNdg*kt6C5?Wh`zNNCiSvXDB&hp_(k4BQFbFr~Oqlia#QT_w1aP6!PoZ-T|$ z#+cDElE;t{j0#g0((RO%LF4^J3B7J;WMkJ04Q}6WrIIE#L+lz!8-7>UP1nNXw|%qC zzW({?A)A*gKLDTna$R0Gukc7mCLX8cN+;7X*21^V%dYY*gx0?p0?wMc^EKuSU3moO z+8-xF#(S4yJGl-+*)k`;bBN@?O{MYLQ-Ri2aq)47{c4}#wy1sZy|>@53#ykx!(94 z3(x5L8%Sh^_m5q<>-ut})0&-~-G09N2ll&` A7XSbN literal 0 HcmV?d00001 diff --git a/SwiftRadio/Images.xcassets/player/play.imageset/Contents.json b/SwiftRadio/Images.xcassets/player/play.imageset/Contents.json new file mode 100755 index 00000000..a005e057 --- /dev/null +++ b/SwiftRadio/Images.xcassets/player/play.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "play.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/SwiftRadio/Images.xcassets/player/play.imageset/play.pdf b/SwiftRadio/Images.xcassets/player/play.imageset/play.pdf new file mode 100755 index 0000000000000000000000000000000000000000..14c10895c04e22a24abe80568f728118a73150b9 GIT binary patch literal 1690 zcma)7OK;mS48H4E@KT^XRMz_e6am)sfngh#rMne7c#fK8XzVR^hi<=qloLy8y8-hh z^e2)Z`I%yl78h5ioLWLSFzCL1BLHV-P+dL^*Ed5wg#DWsf0`N?l`K9mo1y2cr@G|X z)9HVC&So(_w5v!+#53mip850`0i1bu=>9hmEDCgEI3lBtbYJ?Pyf#4LS)7*j*R7oq4LZHQ@ z628PFb>2B=FlXVZsq{)J4n;8TQ5b&$}nM%^D_cib4x>%C!uA_qc09 z4{_ry4+-a8JA@WF<1(7SMZH4|^Z_bXR)q4he79L)u@HM$!o)N09x;}`>YzuY$b=uT z$P`=b6CtkGL+kT;s2{rZ^U$YVxFZZK+3&h>*&3~-280g}$AKLW+6m$kTYLs_E^)+O zK;~s2mzem4mtsCa!>-;ugdK_h9&ayDj^n=Dh7&m6tWNf#RLia#AT~S-T-|Jb2dsYF S@%3&P+u_PMa&&b0@#+skAY-Ng literal 0 HcmV?d00001 diff --git a/SwiftRadio/Images.xcassets/player/stop.imageset/Contents.json b/SwiftRadio/Images.xcassets/player/stop.imageset/Contents.json new file mode 100755 index 00000000..ba414e10 --- /dev/null +++ b/SwiftRadio/Images.xcassets/player/stop.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "stop.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/SwiftRadio/Images.xcassets/player/stop.imageset/stop.pdf b/SwiftRadio/Images.xcassets/player/stop.imageset/stop.pdf new file mode 100755 index 0000000000000000000000000000000000000000..0c7d884d0fa423cb283b9706401b89076d9e55ab GIT binary patch literal 1588 zcma)6OK;mS48H4E@DiXs)VAI>Py|?$v>3KwnX_B5gXgGehQ!`%cgXhZN7du=tEChf z1U>;PiKiaG1NLbwF@U}gHuR7rK%ULysd(N8;_lGS-#A=u67A3pI{=5k&!_OH-3;%l zs_t5N{5H&g{PoXI1M#}pnGyJ8w)^6)KEY#nzMfooYC&?5a?{o?O=()NyC@&SE$XT> z6=t++!h!3eK@4zsdT6ptEELw*hy50*f~_D2muFGGpfA1P-y$-Jq|PGQ0$ZOD7vEU; zqUef4y*qb3HRg#VpvmxB?Jp=PxdepAgYUVQ0AKoW@FBi}Xu{JANWFHXjN`@G<6}{t z;PM!ut||5h(-3=m@0Zw)tF}IxYq;8PuZO0@d0ls43+@{nZ}xu;vhTNV(R6(|lq*GM Iv(@MOf1l(~FaQ7m literal 0 HcmV?d00001 diff --git a/SwiftRadio/Images.xcassets/share.imageset/Contents.json b/SwiftRadio/Images.xcassets/share.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/share.imageset/share.png b/SwiftRadio/Images.xcassets/share.imageset/share.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/share.imageset/share@2x.png b/SwiftRadio/Images.xcassets/share.imageset/share@2x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Images.xcassets/share.imageset/share@3x.png b/SwiftRadio/Images.xcassets/share.imageset/share@3x.png old mode 100644 new mode 100755 diff --git a/SwiftRadio/Info.plist b/SwiftRadio/Info.plist index 21a6bd2d..4e8ac0a1 100755 --- a/SwiftRadio/Info.plist +++ b/SwiftRadio/Info.plist @@ -2,63 +2,80 @@ - CFBundleDevelopmentRegion - en - CFBundleDisplayName - Swift Radio - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSRequiresIPhoneOS - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSPhotoLibraryAddUsageDescription - This app would like to save the image associated with the current track and station to your photo library. - NSUserActivityTypes - - NSUserActivityTypeBrowsingWeb - - UIBackgroundModes - - audio - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UIStatusBarStyle - UIStatusBarStyleLightContent - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Dark + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Swift Radio + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSPhotoLibraryAddUsageDescription + This app would like to save the image associated with the current track and station to your photo library. + NSUserActivityTypes + + NSUserActivityTypeBrowsingWeb + + UIBackgroundModes + + audio + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Dark + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + diff --git a/SwiftRadio/LaunchScreen.storyboard b/SwiftRadio/LaunchScreen.storyboard old mode 100644 new mode 100755 diff --git a/SwiftRadio/Main.storyboard b/SwiftRadio/Main.storyboard index 21875fe4..0c634a97 100755 --- a/SwiftRadio/Main.storyboard +++ b/SwiftRadio/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -150,253 +150,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -630,17 +383,9 @@ - - - - - - - - diff --git a/SwiftRadio/Model/RadioStation.swift b/SwiftRadio/Model/RadioStation.swift index a940573c..f58ce765 100755 --- a/SwiftRadio/Model/RadioStation.swift +++ b/SwiftRadio/Model/RadioStation.swift @@ -14,12 +14,13 @@ import FRadioPlayer struct RadioStation: Codable { var name: String + var website: String? var streamURL: String var imageURL: String var desc: String var longDesc: String - init(name: String, streamURL: String, imageURL: String, desc: String, longDesc: String = "") { + init(name: String, website: String? = nil, streamURL: String, imageURL: String, desc: String, longDesc: String = "") { self.name = name self.streamURL = streamURL self.imageURL = imageURL @@ -29,6 +30,16 @@ struct RadioStation: Codable { } extension RadioStation { + var hasValidWebsite: Bool { + guard let websiteString = website, + !websiteString.isEmpty, + let url = URL(string: websiteString), + url.scheme?.hasPrefix("http") == true else { + return false + } + return true + } + var shoutout: String { "I'm listening to \(name) via \(Bundle.main.appName) app" } @@ -66,4 +77,11 @@ extension RadioStation { var artistName: String { FRadioPlayer.shared.currentMetadata?.artistName ?? desc } + + var musicSearchURL: URL? { + guard let encodedSongName = FRadioPlayer.shared.currentMetadata?.trackName?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let encodedArtistName = FRadioPlayer.shared.currentMetadata?.artistName?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return nil } + let musicSearchURLString = "https://music.apple.com/search?term=\(encodedSongName)+\(encodedArtistName)".replacingOccurrences(of: "%2B", with: "%20") + return URL(string: musicSearchURLString) + } } diff --git a/SwiftRadio/Model/StationsManager.swift b/SwiftRadio/Model/StationsManager.swift old mode 100644 new mode 100755 index e0706fb6..d881bf7a --- a/SwiftRadio/Model/StationsManager.swift +++ b/SwiftRadio/Model/StationsManager.swift @@ -172,6 +172,12 @@ extension StationsManager { nowPlayingInfo[MPMediaItemPropertyTitle] = trackName } + if player.duration != 0 { + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentTime + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = player.duration + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate + } + // Set the metadata MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } diff --git a/SwiftRadio/SceneDelegate.swift b/SwiftRadio/SceneDelegate.swift new file mode 100644 index 00000000..79d99da6 --- /dev/null +++ b/SwiftRadio/SceneDelegate.swift @@ -0,0 +1,102 @@ +import UIKit +import MediaPlayer +import FRadioPlayer +import AVFAudio + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + var coordinator: MainCoordinator? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + // FRadioPlayer config + FRadioPlayer.shared.isAutoPlay = true + FRadioPlayer.shared.enableArtwork = true + FRadioPlayer.shared.artworkAPI = iTunesAPI(artworkSize: 600) + + // AudioSession & RemotePlay + activateAudioSession() + setupRemoteCommandCenter() + UIApplication.shared.beginReceivingRemoteControlEvents() + + // Make status bar white + UINavigationBar.appearance().barStyle = .black + UINavigationBar.appearance().tintColor = .white + UINavigationBar.appearance().prefersLargeTitles = true + + // Start the coordinator + coordinator = MainCoordinator(navigationController: UINavigationController()) + + window = UIWindow(windowScene: windowScene) + window?.rootViewController = coordinator?.navigationController + window?.makeKeyAndVisible() + + coordinator?.start() + } + + func sceneDidDisconnect(_ scene: UIScene) { + } + + func sceneDidBecomeActive(_ scene: UIScene) { + } + + func sceneWillResignActive(_ scene: UIScene) { + } + + func sceneWillEnterForeground(_ scene: UIScene) { + } + + func sceneDidEnterBackground(_ scene: UIScene) { + } + + // MARK: - Remote Controls + + private func setupRemoteCommandCenter() { + // Get the shared MPRemoteCommandCenter + let commandCenter = MPRemoteCommandCenter.shared() + + // Add handler for Play Command + commandCenter.playCommand.addTarget { event in + FRadioPlayer.shared.play() + return .success + } + + // Add handler for Pause Command + commandCenter.pauseCommand.addTarget { event in + FRadioPlayer.shared.pause() + return .success + } + + // Add handler for Toggle Command + commandCenter.togglePlayPauseCommand.addTarget { event in + FRadioPlayer.shared.togglePlaying() + return .success + } + + // Add handler for Next Command + commandCenter.nextTrackCommand.addTarget { event in + StationsManager.shared.setNext() + return .success + } + + // Add handler for Previous Command + commandCenter.previousTrackCommand.addTarget { event in + StationsManager.shared.setPrevious() + return .success + } + } + + // MARK: - Activate Audio Session + + private func activateAudioSession() { + do { + try AVAudioSession.sharedInstance().setActive(true) + } catch let error { + if Config.debugLog { + print("audioSession could not be activated: \(error.localizedDescription)") + } + } + } +} diff --git a/SwiftRadio/SwiftRadio.entitlements b/SwiftRadio/SwiftRadio.entitlements old mode 100644 new mode 100755 diff --git a/SwiftRadio/ViewControllers/BaseController.swift b/SwiftRadio/ViewControllers/BaseController.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/ViewControllers/LoaderController.swift b/SwiftRadio/ViewControllers/LoaderController.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/ViewControllers/NowPlayingViewController.swift b/SwiftRadio/ViewControllers/NowPlayingViewController.swift old mode 100644 new mode 100755 index c750e0d0..7ba6a56d --- a/SwiftRadio/ViewControllers/NowPlayingViewController.swift +++ b/SwiftRadio/ViewControllers/NowPlayingViewController.swift @@ -1,51 +1,48 @@ // -// NowPlayingViewController.swift -// Swift Radio +// NowPlayingViewControllerWIP.swift +// SwiftRadio // -// Created by Matthew Fecher on 7/22/15. -// Copyright (c) 2015 MatthewFecher.com. All rights reserved. +// Created by Fethi El Hassasna on 2024-01-13. +// Copyright © 2024 matthewfecher.com. All rights reserved. // import UIKit import MediaPlayer import AVKit -import Spring import FRadioPlayer +import NVActivityIndicatorView protocol NowPlayingViewControllerDelegate: AnyObject { + func didSelectBottomSheetOption(_ option: BottomSheetViewController.Option, from controller: NowPlayingViewController) func didTapCompanyButton(_ nowPlayingViewController: NowPlayingViewController) - func didTapInfoButton(_ nowPlayingViewController: NowPlayingViewController, station: RadioStation) - func didTapShareButton(_ nowPlayingViewController: NowPlayingViewController, station: RadioStation, artworkURL: URL?) } -class NowPlayingViewController: UIViewController { +class NowPlayingViewController: BaseController { + // MARK: - Delegate weak var delegate: NowPlayingViewControllerDelegate? - // MARK: - IB UI + // MARK: - UI + private let animationView: NVActivityIndicatorView = { + let activityIndicatorView = NVActivityIndicatorView(frame: .zero, type: .audioEqualizer, color: .white, padding: nil) + NSLayoutConstraint.activate([ + activityIndicatorView.widthAnchor.constraint(equalToConstant: 30), + activityIndicatorView.widthAnchor.constraint(equalToConstant: 30), + activityIndicatorView.heightAnchor.constraint(equalToConstant: 20) + ]) + return activityIndicatorView + }() - @IBOutlet weak var albumHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var albumImageView: SpringImageView! - @IBOutlet weak var artistLabel: UILabel! - @IBOutlet weak var playingButton: UIButton! - @IBOutlet weak var songLabel: SpringLabel! - @IBOutlet weak var stationDescLabel: UILabel! - @IBOutlet weak var volumeParentView: UIView! - @IBOutlet weak var previousButton: UIButton! - @IBOutlet weak var nextButton: UIButton! - @IBOutlet weak var airPlayView: UIView! + private let albumArtworkView = AlbumArtworkView() + private let controlsView = ControlsView() // MARK: - Properties - private let player = FRadioPlayer.shared private let manager = StationsManager.shared var isNewStation = true - var nowPlayingImageView: UIImageView! - - var mpVolumeSlider: UISlider? - - // MARK: - ViewDidLoad + + // MARK: - Lifecycle Methods override func viewDidLoad() { super.viewDidLoad() @@ -55,19 +52,9 @@ class NowPlayingViewController: UIViewController { player.addObserver(self) manager.addObserver(self) - // Create Now Playing BarItem - createNowPlayingAnimation() - - // Set AlbumArtwork Constraints - optimizeForDeviceSize() - - // Set View Title - self.title = manager.currentStation?.name - - // Set UI + title = manager.currentStation?.name - stationDescLabel.text = manager.currentStation?.desc - stationDescLabel.isHidden = player.currentMetadata != nil + // Set UI - Reset UI // Check for station change if isNewStation { @@ -77,102 +64,37 @@ class NowPlayingViewController: UIViewController { playerStateDidChange(player.state, animate: false) } - // Setup volumeSlider - setupVolumeSlider() - - // Setup AirPlayButton - setupAirPlayButton() - - // Hide / Show Next/Previous buttons - previousButton.isHidden = Config.hideNextPreviousButtons - nextButton.isHidden = Config.hideNextPreviousButtons + setupViews() isPlayingDidChange(player.isPlaying) + controlsView.setLive(player.duration == 0) } - // MARK: - Setup - - func setupVolumeSlider() { - // Note: This slider implementation uses a MPVolumeView - // The volume slider only works in devices, not the simulator. - for subview in MPVolumeView().subviews { - guard let volumeSlider = subview as? UISlider else { continue } - mpVolumeSlider = volumeSlider - } - - guard let mpVolumeSlider = mpVolumeSlider else { return } - - volumeParentView.addSubview(mpVolumeSlider) - - mpVolumeSlider.translatesAutoresizingMaskIntoConstraints = false - mpVolumeSlider.leftAnchor.constraint(equalTo: volumeParentView.leftAnchor).isActive = true - mpVolumeSlider.rightAnchor.constraint(equalTo: volumeParentView.rightAnchor).isActive = true - mpVolumeSlider.centerYAnchor.constraint(equalTo: volumeParentView.centerYAnchor).isActive = true - - mpVolumeSlider.setThumbImage(#imageLiteral(resourceName: "slider-ball"), for: .normal) - } - - func setupAirPlayButton() { - let airPlayButton = AVRoutePickerView(frame: airPlayView.bounds) - airPlayButton.activeTintColor = .white - airPlayButton.tintColor = .gray - airPlayView.backgroundColor = .clear - airPlayView.addSubview(airPlayButton) + private func isPlayingDidChange(_ isPlaying: Bool) { + controlsView.setPlaying(isPlaying) + controlsView.setStop(isPlaying) + isPlaying ? animationView.startAnimating() : animationView.stopAnimating() } func stationDidChange() { - albumImageView.image = nil + albumArtworkView.setImage(nil) manager.currentStation?.getImage { [weak self] image in - self?.albumImageView.image = image + self?.albumArtworkView.setImage(image) } - stationDescLabel.text = manager.currentStation?.desc - stationDescLabel.isHidden = player.currentArtworkURL != nil + title = manager.currentStation?.name updateLabels() + controlsView.setLive(player.duration == 0) } - // MARK: - Player Controls (Play/Pause/Volume) - - @IBAction func playingPressed(_ sender: Any) { - player.togglePlaying() - } - - @IBAction func stopPressed(_ sender: Any) { - player.stop() - } - - @IBAction func nextPressed(_ sender: Any) { - manager.setNext() - } - - @IBAction func previousPressed(_ sender: Any) { - manager.setPrevious() - } - - // Update track with new artwork - func updateTrackArtwork() { - guard let artworkURL = player.currentArtworkURL else { - manager.currentStation?.getImage { [weak self] image in - self?.albumImageView.image = image - self?.stationDescLabel.isHidden = false - } + func updateLabels(with statusMessage: String? = nil, animate: Bool = true) { + guard let statusMessage = statusMessage else { + controlsView.updateLabels(with: .track(song: manager.currentStation?.trackName, + artist: manager.currentStation?.artistName)) return } - albumImageView.load(url: artworkURL) { [weak self] in - self?.albumImageView.animation = "wobble" - self?.albumImageView.duration = 2 - self?.albumImageView.animate() - self?.stationDescLabel.isHidden = true - - // Force app to update display - self?.view.setNeedsDisplay() - } - } - - private func isPlayingDidChange(_ isPlaying: Bool) { - playingButton.isSelected = isPlaying - startNowPlayingAnimation(isPlaying) + controlsView.updateLabels(with: .status(message: statusMessage, name: manager.currentStation?.name)) } func playbackStateDidChange(_ playbackState: FRadioPlayer.PlaybackState, animate: Bool) { @@ -181,11 +103,11 @@ class NowPlayingViewController: UIViewController { switch playbackState { case .paused: - message = "Station Paused..." + message = "Paused..." case .playing: message = nil case .stopped: - message = "Station Stopped..." + message = "Stopped..." } updateLabels(with: message, animate: animate) @@ -198,9 +120,9 @@ class NowPlayingViewController: UIViewController { switch state { case .loading: - message = "Loading Station ..." + message = "Loading ..." case .urlNotSet: - message = "Station URL not valide" + message = "URL not valide" case .readyToPlay, .loadingFinished: playbackStateDidChange(player.playbackState, animate: animate) return @@ -211,99 +133,103 @@ class NowPlayingViewController: UIViewController { updateLabels(with: message, animate: animate) } - // MARK: - UI Helper Methods + // MARK: - Setup Methods - func optimizeForDeviceSize() { + override func setupViews() { + super.setupViews() - // Adjust album size to fit iPhone 4s, 6s & 6s+ - let deviceHeight = self.view.bounds.height + let mainStackView = UIStackView(arrangedSubviews: [albumArtworkView, controlsView]) + mainStackView.axis = .vertical + mainStackView.spacing = 20 + mainStackView.distribution = .fillEqually + mainStackView.translatesAutoresizingMaskIntoConstraints = false - if deviceHeight == 480 { - albumHeightConstraint.constant = 106 - view.updateConstraints() - } else if deviceHeight == 667 { - albumHeightConstraint.constant = 230 - view.updateConstraints() - } else if deviceHeight > 667 { - albumHeightConstraint.constant = 260 - view.updateConstraints() + controlsView.playingAction = { [unowned self] in + player.togglePlaying() } - } - - func updateLabels(with statusMessage: String? = nil, animate: Bool = true) { - - guard let statusMessage = statusMessage else { - // Radio is (hopefully) streaming properly - songLabel.text = manager.currentStation?.trackName - artistLabel.text = manager.currentStation?.artistName - shouldAnimateSongLabel(animate) - return + + controlsView.stopAction = { [unowned self] in + player.stop() } - // There's a an interruption or pause in the audio queue + controlsView.nextAction = { [unowned self] in + manager.setNext() + } - // Update UI only when it's not aleary updated - guard songLabel.text != statusMessage else { return } + controlsView.previousAction = { [unowned self] in + manager.setPrevious() + } - songLabel.text = statusMessage - artistLabel.text = manager.currentStation?.name - - if animate { - songLabel.animation = "flash" - songLabel.repeatCount = 2 - songLabel.animate() + controlsView.logoAction = { [unowned self] in + delegate?.didTapCompanyButton(self) } - } - - // Animations - - func shouldAnimateSongLabel(_ animate: Bool) { - // Animate if the Track has album metadata - guard animate, player.currentMetadata != nil else { return } - // songLabel animation - songLabel.animation = "zoomIn" - songLabel.duration = 1.5 - songLabel.damping = 1 - songLabel.animate() - } - - func createNowPlayingAnimation() { - // Setup ImageView - nowPlayingImageView = UIImageView(image: UIImage(named: "NowPlayingBars-3")) - nowPlayingImageView.autoresizingMask = [] - nowPlayingImageView.contentMode = UIView.ContentMode.center + controlsView.moreAction = { [unowned self] in + handleMoreMenu() + } + + controlsView.timeAction = { [unowned self] slider, event in + handleTimeSlider(slider: slider, event: event) + } - // Create Animation - nowPlayingImageView.animationImages = AnimationFrames.createFrames() - nowPlayingImageView.animationDuration = 0.7 + view.addSubview(mainStackView) - // Create Top BarButton - let barButton = UIButton(type: .custom) - barButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40) - barButton.addSubview(nowPlayingImageView) - nowPlayingImageView.center = barButton.center + NSLayoutConstraint.activate([ + mainStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), + mainStackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20), + mainStackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20), + mainStackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) - let barItem = UIBarButtonItem(customView: barButton) - self.navigationItem.rightBarButtonItem = barItem + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: animationView) } - func startNowPlayingAnimation(_ animate: Bool) { - animate ? nowPlayingImageView.startAnimating() : nowPlayingImageView.stopAnimating() + func updateTrackArtwork() { + getTrackArtwork { [weak self] image, isAnimated in + DispatchQueue.main.async { + self?.albumArtworkView.setImage(image) + if isAnimated { self?.albumArtworkView.animate() } + } + } } - @IBAction func infoButtonPressed(_ sender: UIButton) { - guard let station = manager.currentStation else { return } - delegate?.didTapInfoButton(self, station: station) + private func getTrackArtwork(completion: @escaping (UIImage?, Bool) -> Void) { + guard let artworkURL = player.currentArtworkURL else { + manager.currentStation?.getImage { image in + completion(image, false) + } + return + } + + UIImage.image(from: artworkURL) { image in + completion(image, true) + } } - @IBAction func shareButtonPressed(_ sender: UIButton) { + func handleMoreMenu() { guard let station = manager.currentStation else { return } - delegate?.didTapShareButton(self, station: station, artworkURL: player.currentArtworkURL) + let bottomSheet = BottomSheetViewController(station: station) + bottomSheet.delegate = self + present(bottomSheet, animated: true) } - @IBAction func handleCompanyButton(_ sender: Any) { - delegate?.didTapCompanyButton(self) + private func handleTimeSlider(slider: UISlider, event: UIControl.Event) { + + guard player.duration != 0 else { return } + + let seekTime = TimeInterval(slider.value) * player.duration + + switch event { + case .valueChanged: + controlsView.setCurrentTime(seekTime) + controlsView.setTotalTime(player.duration - seekTime) + case .touchUpInside: + player.seek(to: seekTime) { [weak self] in + self?.controlsView.isSliderSliding = false + } + default: + break + } } } @@ -324,6 +250,19 @@ extension NowPlayingViewController: FRadioPlayerObserver { func radioPlayer(_ player: FRadioPlayer, artworkDidChange artworkURL: URL?) { updateTrackArtwork() } + + func radioPlayer(_ player: FRadioPlayer, durationDidChange duration: TimeInterval) { + controlsView.setLive(player.duration == 0) + } + + func radioPlayer(_ player: FRadioPlayer, playTimeDidChange currentTime: TimeInterval, duration: TimeInterval) { + guard !controlsView.isSliderSliding, player.duration != 0 else { return } + + // Update timer labels + controlsView.setCurrentTime(currentTime) + controlsView.setTotalTime(duration - currentTime) + controlsView.setTimeSilder(value: Float(currentTime / duration)) + } } extension NowPlayingViewController: StationsManagerObserver { @@ -332,3 +271,19 @@ extension NowPlayingViewController: StationsManagerObserver { stationDidChange() } } + +extension NowPlayingViewController: BottomSheetViewControllerDelegate { + + func bottomSheet(_ controller: BottomSheetViewController, didSelect option: BottomSheetViewController.Option) { + if case .share = option { + getTrackArtwork { [weak self] image, _ in + guard let self = self else { return } + self.delegate?.didSelectBottomSheetOption(.share(image), from: self) + } + } else if case .openInMusic = option { + delegate?.didSelectBottomSheetOption(.openInMusic(manager.currentStation?.musicSearchURL), from: self) + } else { + delegate?.didSelectBottomSheetOption(option, from: self) + } + } +} diff --git a/SwiftRadio/ViewControllers/StationsViewController.swift b/SwiftRadio/ViewControllers/StationsViewController.swift old mode 100644 new mode 100755 index 1fb35b24..d53898c9 --- a/SwiftRadio/ViewControllers/StationsViewController.swift +++ b/SwiftRadio/ViewControllers/StationsViewController.swift @@ -52,9 +52,7 @@ class StationsViewController: BaseController, Handoffable { return tableView }() - private let nowPlayingView: NowPlayingView = { - return NowPlayingView() - }() + private let nowPlayingView = NowPlayingView() override func loadView() { super.loadView() diff --git a/SwiftRadio/Views/AlbumArtworkView.swift b/SwiftRadio/Views/AlbumArtworkView.swift new file mode 100755 index 00000000..ffcb2ae7 --- /dev/null +++ b/SwiftRadio/Views/AlbumArtworkView.swift @@ -0,0 +1,143 @@ +// +// AlbumArtworkView.swift +// SwiftRadio +// +// Created by Fethi El Hassasna on 2024-01-13. +// Copyright 2024 matthewfecher.com. All rights reserved. +// + +import UIKit +import Spring + +class AlbumArtworkView: UIView { + + // TODO: Add desc label + + // Corner radius + private let cornerRadius: CGFloat = 4 + + // Add background image view + private let backgroundImageView: UIImageView = { + let view = UIImageView() + view.contentMode = .scaleAspectFill + view.clipsToBounds = true + return view + }() + + // Add blur effect view + private let blurEffectView: UIVisualEffectView = { + let blurEffect = UIBlurEffect(style: .prominent) + let view = UIVisualEffectView(effect: blurEffect) + return view + }() + + private let imageView: SpringImageView = { + let view = SpringImageView() + // Keep aspectFit but adjust constraints + view.contentMode = .scaleAspectFit + view.clipsToBounds = true + return view + }() + + // Keep track of aspect ratio constraint + private var aspectRatioConstraint: NSLayoutConstraint? + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setImage(_ image: UIImage?) { + imageView.image = image + backgroundImageView.image = image + updateAspectRatio(for: image) + } + + func animate() { + imageView.animation = "wobble" + imageView.duration = 2 + imageView.animate() + } + + private func updateAspectRatio(for image: UIImage?) { + // Remove existing aspect ratio constraint if any + aspectRatioConstraint?.isActive = false + + guard let image = image else { return } + + // Create new aspect ratio constraint based on image dimensions + let aspect = image.size.width / image.size.height + aspectRatioConstraint = imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: aspect) + aspectRatioConstraint?.isActive = true + } + + private func setupViews() { + // Configure corner radius for all views + layer.cornerRadius = cornerRadius + layer.masksToBounds = true + backgroundImageView.layer.cornerRadius = cornerRadius + blurEffectView.layer.cornerRadius = cornerRadius + imageView.layer.cornerRadius = cornerRadius + + // Use containerView to constrain the image + let containerView = UIView() + containerView.translatesAutoresizingMaskIntoConstraints = false + + containerView.addSubview(imageView) + + let stackView = UIStackView(arrangedSubviews: [containerView]) + stackView.axis = .vertical + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + + // Add background views first + addSubview(backgroundImageView) + addSubview(blurEffectView) + addSubview(stackView) + + imageView.translatesAutoresizingMaskIntoConstraints = false + backgroundImageView.translatesAutoresizingMaskIntoConstraints = false + blurEffectView.translatesAutoresizingMaskIntoConstraints = false + containerView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + // Background image view constraints + backgroundImageView.topAnchor.constraint(equalTo: topAnchor), + backgroundImageView.leadingAnchor.constraint(equalTo: leadingAnchor), + backgroundImageView.trailingAnchor.constraint(equalTo: trailingAnchor), + backgroundImageView.bottomAnchor.constraint(equalTo: bottomAnchor), + + // Blur effect view constraints + blurEffectView.topAnchor.constraint(equalTo: topAnchor), + blurEffectView.leadingAnchor.constraint(equalTo: leadingAnchor), + blurEffectView.trailingAnchor.constraint(equalTo: trailingAnchor), + blurEffectView.bottomAnchor.constraint(equalTo: bottomAnchor), + + // Stack view constraints + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor), + + // Container view constraints + containerView.widthAnchor.constraint(equalTo: containerView.heightAnchor), + + // Center imageView in containerView + imageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + + // Make imageView fill containerView while maintaining aspect ratio + imageView.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor), + imageView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor), + imageView.topAnchor.constraint(greaterThanOrEqualTo: containerView.topAnchor), + imageView.bottomAnchor.constraint(lessThanOrEqualTo: containerView.bottomAnchor), + + // Make imageView width equal to containerView width (priority optional) + imageView.widthAnchor.constraint(equalTo: containerView.widthAnchor).with { $0.priority = .defaultHigh } + ]) + } +} diff --git a/SwiftRadio/Views/BottomSheetHandler.swift b/SwiftRadio/Views/BottomSheetHandler.swift new file mode 100644 index 00000000..2a51a616 --- /dev/null +++ b/SwiftRadio/Views/BottomSheetHandler.swift @@ -0,0 +1,41 @@ +// +// BottomSheetHandler.swift +// SwiftRadio +// +// Created by Fethi El Hassasna on 2024-01-14. +// Copyright © 2024 matthewfecher.com. All rights reserved. +// + +import UIKit +import FRadioPlayer + +class BottomSheetHandler { + static func handle(_ option: BottomSheetViewController.Option, + station: RadioStation, + from viewController: UIViewController) { + switch option { + case .info: + let infoController = Storyboard.viewController as InfoDetailViewController + infoController.currentStation = station + viewController.navigationController?.pushViewController(infoController, animated: true) + + case .share(let image): + ShareActivity.activityController(image: image, + station: station, + sourceView: viewController.view) { controller in + viewController.present(controller, animated: true) + } + + case .website: + if let website = station.website, let websiteURL = URL(string: website) { + UIApplication.shared.open(websiteURL) + } + + case .openInMusic(let url): + if let url { + UIApplication.shared.open(url) + } + } + } +} + diff --git a/SwiftRadio/Views/BottomSheetViewController.swift b/SwiftRadio/Views/BottomSheetViewController.swift new file mode 100644 index 00000000..620e418e --- /dev/null +++ b/SwiftRadio/Views/BottomSheetViewController.swift @@ -0,0 +1,194 @@ +// +// BottomSheetViewController.swift +// SwiftRadio +// +// Created by Fethi El Hassasna on 2024-01-14. +// Copyright 2024 matthewfecher.com. All rights reserved. +// + +import UIKit +import FRadioPlayer + +protocol BottomSheetViewControllerDelegate: AnyObject { + func bottomSheet(_ controller: BottomSheetViewController, didSelect option: BottomSheetViewController.Option) +} + +class BottomSheetViewController: UIViewController { + + enum Section: Int, CaseIterable { + case stationInfo + case music + case share + + var title: String? { + return nil + } + + } + + enum Option { + case info + case share(UIImage?) + case website + case openInMusic(URL?) + + var title: String { + switch self { + case .info: return "About Station" + case .share: return "Share Now Playing" + case .website: return "Station Website" + case .openInMusic: return "Play in Music App" + } + } + + var image: UIImage? { + switch self { + case .info: return UIImage(systemName: "info.circle") + case .share: return UIImage(systemName: "square.and.arrow.up") + case .website: return UIImage(systemName: "safari") + case .openInMusic: return UIImage(systemName: "music.note") + } + } + } + + weak var delegate: BottomSheetViewControllerDelegate? + private let station: RadioStation + private let player = FRadioPlayer.shared + + private lazy var tableView: UITableView = { + let table = UITableView(frame: .zero, style: .insetGrouped) + table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") + table.delegate = self + table.dataSource = self + table.translatesAutoresizingMaskIntoConstraints = false + return table + }() + + init(station: RadioStation) { + self.station = station + super.init(nibName: nil, bundle: nil) + + if let sheet = sheetPresentationController { + sheet.prefersGrabberVisible = true + sheet.delegate = self + sheet.prefersScrollingExpandsWhenScrolledToEdge = false + sheet.detents = [.medium()] + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func getOptions(for section: Section) -> [Option] { + switch section { + case .stationInfo: + var options: [Option] = [.info] + if station.hasValidWebsite { + options.append(.website) + } + return options + case .music: + return [.openInMusic(nil)] + case .share: + return [.share(nil)] + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + setupViews() + + player.addObserver(self) + } + + private func setupViews() { + view.addSubview(tableView) + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if let sheet = sheetPresentationController { + let contentHeight = tableView.contentSize.height + view.safeAreaInsets.top + view.safeAreaInsets.bottom + sheet.detents = [.custom { _ in contentHeight }] + sheet.animateChanges { + sheet.selectedDetentIdentifier = sheet.detents.first?.identifier + } + } + } +} + +extension BottomSheetViewController: UITableViewDataSource, UITableViewDelegate { + + func numberOfSections(in tableView: UITableView) -> Int { + return Section.allCases.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let section = Section(rawValue: section)! + return getOptions(for: section).count + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + let section = Section(rawValue: section)! + return section.title + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + let section = Section(rawValue: indexPath.section)! + let option = getOptions(for: section)[indexPath.row] + + var config = cell.defaultContentConfiguration() + config.text = option.title + config.image = option.image + + // Disable OpenInMusic cell if no metadata + if case .openInMusic = option { + let hasMetadata = player.currentArtworkURL != nil + cell.isUserInteractionEnabled = hasMetadata + // Update text color + config.textProperties.color = hasMetadata ? .label : .systemGray3 + config.imageProperties.tintColor = hasMetadata ? .label : .systemGray3 + } + + cell.contentConfiguration = config + cell.tintColor = .label + + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let section = Section(rawValue: indexPath.section)! + let option = getOptions(for: section)[indexPath.row] + delegate?.bottomSheet(self, didSelect: option) + dismiss(animated: true) + } +} + +extension BottomSheetViewController: UISheetPresentationControllerDelegate { + func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) { + // Handle detent changes if needed + } +} + +extension BottomSheetViewController: FRadioPlayerObserver { + + func radioPlayer(_ player: FRadioPlayer, artworkDidChange artworkURL: URL?) { + // Reload the music section to update cell state + if let musicSection = Section.allCases.firstIndex(of: .music) { + tableView.reloadSections(IndexSet(integer: musicSection), with: .none) + } + } +} diff --git a/SwiftRadio/Views/ControlsView.swift b/SwiftRadio/Views/ControlsView.swift new file mode 100755 index 00000000..47306183 --- /dev/null +++ b/SwiftRadio/Views/ControlsView.swift @@ -0,0 +1,353 @@ +// +// ControlsView.swift +// SwiftRadio +// +// Created by Fethi El Hassasna on 2024-01-14. +// Copyright © 2024 matthewfecher.com. All rights reserved. +// + +import UIKit +import Spring +import AVKit + +enum SongInfoType { + case track(song: String?, artist: String?) + case status(message: String, name: String?) +} + +class ControlsView: UIView { + + var timeAction: ((UISlider, UIControl.Event) -> Void)? + + var playingAction: (() -> Void)? + var stopAction: (() -> Void)? + var nextAction: (() -> Void)? + var previousAction: (() -> Void)? + + var logoAction: (() -> Void)? + var moreAction: (() -> Void)? + + var isSliderSliding = false + + private let songLabel: SpringLabel = { + let label = SpringLabel() + label.font = UIFont.preferredFont(forTextStyle: .headline) + label.textAlignment = .center + label.setContentCompressionResistancePriority(.required, for: .vertical) + return label + }() + + private let artistLabel: SpringLabel = { + let label = SpringLabel() + label.font = UIFont.preferredFont(forTextStyle: .subheadline) + label.textAlignment = .center + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.alpha = 0.8 + return label + }() + + private let timeSlider: UISlider = { + let slider = UISlider() + slider.value = 0.0 + slider.setThumbImage(UIImage(), for: .normal) + slider.translatesAutoresizingMaskIntoConstraints = false + slider.value = 0.0 + slider.minimumTrackTintColor = .white + return slider + }() + + private let currentTimeLabel: UILabel = { + let label = UILabel() + label.text = "00:00" + label.font = UIFont.systemFont(ofSize: 10, weight: .medium) + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + label.textAlignment = .left + label.alpha = 0.8 + return label + }() + + private let totalTimeLabel: UILabel = { + let label = UILabel() + label.text = "00:00" + label.font = UIFont.systemFont(ofSize: 10, weight: .medium) + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + label.textAlignment = .right + label.alpha = 0.8 + return label + }() + + private let liveLabel: UILabel = { + let label = UILabel() + label.text = "Live".uppercased() + label.font = UIFont.systemFont(ofSize: 12, weight: .bold) + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.setContentCompressionResistancePriority(.required, for: .horizontal) + label.textAlignment = .center + label.alpha = 0.8 + return label + }() + + private let playPauseButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(named: "play"), for: .normal) + button.setImage(UIImage(named: "pause"), for: .selected) + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([button.widthAnchor.constraint(equalToConstant: 70), button.heightAnchor.constraint(equalToConstant: 70)]) + return button + }() + + private let stopButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(named: "stop"), for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([button.widthAnchor.constraint(equalToConstant: 55), button.heightAnchor.constraint(equalToConstant: 55)]) + return button + }() + + private let nextButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(named: "forward"), for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([button.widthAnchor.constraint(equalToConstant: 44), button.heightAnchor.constraint(equalToConstant: 22)]) + return button + }() + + private let previousButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(named: "backward"), for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([button.widthAnchor.constraint(equalToConstant: 44), button.heightAnchor.constraint(equalToConstant: 22)]) + return button + }() + + private let logoButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(named: "logo"), for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([button.heightAnchor.constraint(equalToConstant: 36)]) + return button + }() + + private let airPlayButton: AVRoutePickerView = { + let button = AVRoutePickerView() + button.activeTintColor = .white + button.tintColor = .white.withAlphaComponent(0.85) + return button + }() + + private let moreButton: UIButton = { + let button = UIButton() + button.setImage(UIImage(systemName: "list.dash"), for: .normal) + button.tintColor = .white.withAlphaComponent(0.85) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setPlaying(_ isPlaying: Bool) { + playPauseButton.isSelected = isPlaying + } + + func setStop(_ isActive: Bool) { + stopButton.isEnabled = isActive + } + + func setLive(_ isLive: Bool) { + timeSlider.isEnabled = !isLive + currentTimeLabel.isHidden = isLive + totalTimeLabel.isHidden = isLive + liveLabel.isHidden = !isLive + } + + func setCurrentTime(_ secounds: TimeInterval) { + currentTimeLabel.text = formatSecondsToString(secounds) + } + + func setTotalTime(_ secounds: TimeInterval) { + totalTimeLabel.text = "-" + formatSecondsToString(secounds) + } + + func setTimeSilder(value: Float) { + timeSlider.value = value + } + + func updateLabels(with type: SongInfoType, animate: Bool = true) { + switch type { + case .track(song: let song, artist: let artist): + songLabel.text = song + artistLabel.text = artist + shouldAnimateSong(animate) + case .status(message: let message, let name): + guard songLabel.text != message else { break } + songLabel.text = message + artistLabel.text = name + shouldAnimateStatus(animate) + } + } + + // TODO: Combine the 2 animate func + + private func shouldAnimateSong(_ animate: Bool) { + guard animate else { return } + songLabel.animation = "zoomIn" + songLabel.duration = 1.5 + songLabel.damping = 1 + songLabel.animate() + } + + private func shouldAnimateStatus(_ animate: Bool) { + guard animate else { return } + songLabel.animation = "flash" + songLabel.repeatCount = 2 + songLabel.animate() + } + + private func formatSecondsToString(_ secounds: TimeInterval) -> String { + guard secounds != 0 else { return "00:00" } + let min = Int(secounds / 60) + let sec = Int(secounds.truncatingRemainder(dividingBy: 60)) + return String(format: "%02d:%02d", min, sec) + } + + private func setupViews() { + + let mainStackView = UIStackView(arrangedSubviews: [songLabel, artistLabel, timeSliderStackView, buttonsStackView, menuStackView]) + mainStackView.axis = .vertical + mainStackView.spacing = 8 + mainStackView.alignment = .fill + + // Add custom spacing after artistLabel + mainStackView.setCustomSpacing(16, after: artistLabel) + + mainStackView.translatesAutoresizingMaskIntoConstraints = false + + addSubview(mainStackView) + + NSLayoutConstraint.activate([ + mainStackView.topAnchor.constraint(equalTo: topAnchor), + mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + + // MARK: - StackViews + + private var timeSliderStackView: UIStackView { + let spacer1 = UIView() + spacer1.setContentHuggingPriority(.defaultLow, for: .horizontal) + let spacer2 = UIView() + spacer2.setContentHuggingPriority(.defaultLow, for: .horizontal) + + let timeLabelsStackView = UIStackView(arrangedSubviews: [currentTimeLabel, spacer1, liveLabel, spacer2, totalTimeLabel]) + timeLabelsStackView.axis = .horizontal + timeLabelsStackView.distribution = .fillEqually + timeLabelsStackView.alignment = .fill + + let vStackView = UIStackView(arrangedSubviews: [timeSlider, timeLabelsStackView]) + vStackView.axis = .vertical + vStackView.distribution = .fill + vStackView.alignment = .fill + vStackView.spacing = 4 + + return vStackView + } + + private var buttonsStackView: UIStackView { + let playStackView = UIStackView(arrangedSubviews: [playPauseButton, stopButton]) + playStackView.axis = .horizontal + playStackView.spacing = 10 + playStackView.alignment = .center + + let hStackView = UIStackView(arrangedSubviews: [previousButton, playStackView, nextButton]) + hStackView.axis = .horizontal + hStackView.spacing = 20 + hStackView.alignment = .center + + // Actions + playPauseButton.addTarget(self, action: #selector(playingPressed), for: .touchUpInside) + stopButton.addTarget(self, action: #selector(stopPressed), for: .touchUpInside) + nextButton.addTarget(self, action: #selector(nextPressed), for: .touchUpInside) + previousButton.addTarget(self, action: #selector(previousPressed), for: .touchUpInside) + + nextButton.isHidden = Config.hideNextPreviousButtons + previousButton.isHidden = Config.hideNextPreviousButtons + + let vStackView = UIStackView(arrangedSubviews: [hStackView]) + vStackView.axis = .vertical + vStackView.distribution = .fill + vStackView.alignment = .center + + return vStackView + } + + private var menuStackView: UIStackView { + let spacer1 = UIView() + spacer1.setContentHuggingPriority(.defaultLow, for: .horizontal) + let spacer2 = UIView() + spacer2.setContentHuggingPriority(.defaultLow, for: .horizontal) + + let stackView = UIStackView(arrangedSubviews: [logoButton, spacer1, airPlayButton, spacer2, moreButton]) + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.alignment = .fill + + // Actions + logoButton.addTarget(self, action: #selector(logoPressed), for: .touchUpInside) + moreButton.addTarget(self, action: #selector(morePressed), for: .touchUpInside) + timeSlider.addTarget(self, action: #selector(timeSliderTouchBegan(sender:)), for: .touchDown) + timeSlider.addTarget(self, action: #selector(timeSliderValueChanged(sender:)), for: .valueChanged) + timeSlider.addTarget(self, action: #selector(timeSliderTouchEnded(sender:)), for: [.touchUpInside, .touchCancel, .touchUpOutside]) + + + return stackView + } + + // MARK: - Actions + + @objc private func playingPressed(_ sender: Any) { + playingAction?() + } + + @objc private func stopPressed(_ sender: Any) { + stopAction?() + } + + @objc private func nextPressed(_ sender: Any) { + nextAction?() + } + + @objc private func previousPressed(_ sender: Any) { + previousAction?() + } + + @objc private func logoPressed(_ sender: Any) { + logoAction?() + } + + @objc private func morePressed(_ sender: Any) { + moreAction?() + } + + @objc private func timeSliderTouchBegan(sender: UISlider) { + isSliderSliding = true + timeAction?(sender, .touchDown) + } + + @objc private func timeSliderValueChanged(sender: UISlider) { + timeAction?(sender, .valueChanged) + } + + @objc private func timeSliderTouchEnded(sender: UISlider) { + timeAction?(sender, .touchUpInside) + } +} diff --git a/SwiftRadio/Views/LogoShareView.swift b/SwiftRadio/Views/LogoShareView.swift old mode 100644 new mode 100755 diff --git a/SwiftRadio/Views/LogoShareView.xib b/SwiftRadio/Views/LogoShareView.xib old mode 100644 new mode 100755 diff --git a/SwiftRadio/Views/NowPlayingView.swift b/SwiftRadio/Views/NowPlayingView.swift old mode 100644 new mode 100755 diff --git a/SwiftRadioUITests/Info.plist b/SwiftRadioUITests/Info.plist old mode 100644 new mode 100755 diff --git a/SwiftRadioUITests/SwiftRadioUITests.swift b/SwiftRadioUITests/SwiftRadioUITests.swift old mode 100644 new mode 100755 From bc985d18b097d737789bb75f8a7cb51d611ef4a6 Mon Sep 17 00:00:00 2001 From: Fethi El Hassasna Date: Wed, 22 Jan 2025 01:22:53 -0500 Subject: [PATCH 2/7] Update CarPlay to iOS 14 Signed-off-by: Fethi El Hassasna --- SwiftRadio.xcodeproj/project.pbxproj | 14 +- SwiftRadio/AppDelegate.swift | 9 - SwiftRadio/CarPlay/AppDelegate+CarPlay.swift | 117 ------------ SwiftRadio/CarPlay/CarPlaySceneDelegate.swift | 94 ++++++++++ SwiftRadio/Info-CarPlay.plist | 169 +++++++++++------- SwiftRadio/SwiftRadio.entitlements | 2 + 6 files changed, 211 insertions(+), 194 deletions(-) delete mode 100755 SwiftRadio/CarPlay/AppDelegate+CarPlay.swift create mode 100644 SwiftRadio/CarPlay/CarPlaySceneDelegate.swift diff --git a/SwiftRadio.xcodeproj/project.pbxproj b/SwiftRadio.xcodeproj/project.pbxproj index 7a102790..02dfdcf8 100755 --- a/SwiftRadio.xcodeproj/project.pbxproj +++ b/SwiftRadio.xcodeproj/project.pbxproj @@ -30,10 +30,10 @@ CA142F6B2D3D9A0D0071A388 /* BottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F692D3D9A0D0071A388 /* BottomSheetViewController.swift */; }; CA142F6D2D3D9DFB0071A388 /* BottomSheetHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F6C2D3D9DFB0071A388 /* BottomSheetHandler.swift */; }; CA142F6E2D3D9DFB0071A388 /* BottomSheetHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F6C2D3D9DFB0071A388 /* BottomSheetHandler.swift */; }; + CA58087D2D40BAC000F8082B /* CarPlaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA58087B2D40BAC000F8082B /* CarPlaySceneDelegate.swift */; }; + CA58087E2D40C11300F8082B /* Info-CarPlay.plist in Resources */ = {isa = PBXBuildFile; fileRef = CEA82F492921F260009E9FA0 /* Info-CarPlay.plist */; }; CAB4E8292D3D7DC7001282E9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB4E8282D3D7DC7001282E9 /* SceneDelegate.swift */; }; CAB4E82A2D3D7DC7001282E9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB4E8282D3D7DC7001282E9 /* SceneDelegate.swift */; }; - CE0A4996291F3AD40071C0CC /* AppDelegate+CarPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0A4995291F3AD40071C0CC /* AppDelegate+CarPlay.swift */; }; - CE0A4997291F3B080071C0CC /* AppDelegate+CarPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0A4995291F3AD40071C0CC /* AppDelegate+CarPlay.swift */; }; CE321FF329371140001572BD /* Bundle+appName.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE321FF229371140001572BD /* Bundle+appName.swift */; }; CE321FF429371140001572BD /* Bundle+appName.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE321FF229371140001572BD /* Bundle+appName.swift */; }; CE37D7B4290F47E000B0933B /* Spring in Frameworks */ = {isa = PBXBuildFile; productRef = CE37D7B3290F47E000B0933B /* Spring */; }; @@ -85,7 +85,6 @@ CE9EE8DF293BB41300F62041 /* BaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9EE8DD293BB41300F62041 /* BaseController.swift */; }; CE9EE8E1293C048A00F62041 /* LoaderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9EE8E0293C048A00F62041 /* LoaderController.swift */; }; CE9EE8E2293C048A00F62041 /* LoaderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9EE8E0293C048A00F62041 /* LoaderController.swift */; }; - CEA82F4A2921F260009E9FA0 /* Info-CarPlay.plist in Resources */ = {isa = PBXBuildFile; fileRef = CEA82F492921F260009E9FA0 /* Info-CarPlay.plist */; }; CED6353B293081ED002B216F /* Handoffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED6353A293081ED002B216F /* Handoffable.swift */; }; CED6353C293081ED002B216F /* Handoffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED6353A293081ED002B216F /* Handoffable.swift */; }; CEDABBEB291217AF00C0367F /* UIImageView+Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDABBEA291217AF00C0367F /* UIImageView+Cache.swift */; }; @@ -134,9 +133,9 @@ CA142F662D3D8E280071A388 /* NSLayoutConstraint+with.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+with.swift"; sourceTree = ""; }; CA142F692D3D9A0D0071A388 /* BottomSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetViewController.swift; sourceTree = ""; }; CA142F6C2D3D9DFB0071A388 /* BottomSheetHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetHandler.swift; sourceTree = ""; }; + CA58087B2D40BAC000F8082B /* CarPlaySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlaySceneDelegate.swift; sourceTree = ""; }; CAB4E8282D3D7DC7001282E9 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; CE0A4994291F3A220071C0CC /* SwiftRadio.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftRadio.entitlements; sourceTree = ""; }; - CE0A4995291F3AD40071C0CC /* AppDelegate+CarPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+CarPlay.swift"; sourceTree = ""; }; CE321FF229371140001572BD /* Bundle+appName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+appName.swift"; sourceTree = ""; }; CE6036122A47A87A00E15E15 /* StationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationsViewController.swift; sourceTree = ""; }; CE6036152A47B88600E15E15 /* StationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StationTableViewCell.swift; sourceTree = ""; }; @@ -250,7 +249,7 @@ CE0A4993291F39660071C0CC /* CarPlay */ = { isa = PBXGroup; children = ( - CE0A4995291F3AD40071C0CC /* AppDelegate+CarPlay.swift */, + CA58087B2D40BAC000F8082B /* CarPlaySceneDelegate.swift */, ); path = CarPlay; sourceTree = ""; @@ -482,7 +481,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - CEA82F4A2921F260009E9FA0 /* Info-CarPlay.plist in Resources */, 945DB3C21AD58E3A00495EBB /* stations.json in Resources */, 945DB3C51AD5A6E200495EBB /* NothingFoundCell.xib in Resources */, 6258DCDA22D93A5400166C65 /* LogoShareView.xib in Resources */, @@ -496,6 +494,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + CA58087E2D40C11300F8082B /* Info-CarPlay.plist in Resources */, CE6A3E46291F376D0058C82A /* stations.json in Resources */, CE6A3E47291F376D0058C82A /* NothingFoundCell.xib in Resources */, CE6A3E48291F376D0058C82A /* LogoShareView.xib in Resources */, @@ -529,7 +528,6 @@ 53113F39230C720900462C0E /* ShareActivity.swift in Sources */, CA142F682D3D8E2F0071A388 /* NSLayoutConstraint+with.swift in Sources */, CE963ECC29135A6F004F299E /* StationsManager.swift in Sources */, - CE0A4997291F3B080071C0CC /* AppDelegate+CarPlay.swift in Sources */, CE321FF329371140001572BD /* Bundle+appName.swift in Sources */, CE6ECCFA292F13DF008B3C16 /* Coordinator.swift in Sources */, 94D1D0A51AD6D6230022CA11 /* InfoDetailViewController.swift in Sources */, @@ -570,7 +568,6 @@ CE6A3E33291F376D0058C82A /* ShareActivity.swift in Sources */, CA142F672D3D8E2F0071A388 /* NSLayoutConstraint+with.swift in Sources */, CE6A3E35291F376D0058C82A /* StationsManager.swift in Sources */, - CE0A4996291F3AD40071C0CC /* AppDelegate+CarPlay.swift in Sources */, CE321FF429371140001572BD /* Bundle+appName.swift in Sources */, CE6ECCFB292F13E6008B3C16 /* Coordinator.swift in Sources */, CE6A3E36291F376D0058C82A /* InfoDetailViewController.swift in Sources */, @@ -583,6 +580,7 @@ CE6A3E3B291F376D0058C82A /* PopUpMenuViewController.swift in Sources */, CEE9A27C2B535C780018FE68 /* AlbumArtworkView.swift in Sources */, CE6ECCFE292F1448008B3C16 /* MainCoordinator.swift in Sources */, + CA58087D2D40BAC000F8082B /* CarPlaySceneDelegate.swift in Sources */, CE6A3E3C291F376D0058C82A /* AppDelegate.swift in Sources */, CE6A3E3D291F376D0058C82A /* AnimationFrames.swift in Sources */, CAB4E8292D3D7DC7001282E9 /* SceneDelegate.swift in Sources */, diff --git a/SwiftRadio/AppDelegate.swift b/SwiftRadio/AppDelegate.swift index b93d3685..1cc555fa 100755 --- a/SwiftRadio/AppDelegate.swift +++ b/SwiftRadio/AppDelegate.swift @@ -16,9 +16,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var coordinator: MainCoordinator? - // CarPlay - var playableContentManager: MPPlayableContentManager? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // FRadioPlayer config @@ -36,12 +33,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UINavigationBar.appearance().tintColor = .white UINavigationBar.appearance().prefersLargeTitles = true - // `CarPlay` is defined only in SwiftRadio-CarPlay target: - // Build Settings > Swift Compiler - Custom Flags - #if CarPlay - setupCarPlay() - #endif - // Start the coordinator coordinator = MainCoordinator(navigationController: UINavigationController()) diff --git a/SwiftRadio/CarPlay/AppDelegate+CarPlay.swift b/SwiftRadio/CarPlay/AppDelegate+CarPlay.swift deleted file mode 100755 index 4bba565e..00000000 --- a/SwiftRadio/CarPlay/AppDelegate+CarPlay.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// AppDelegate+CarPlay.swift -// SwiftRadio -// -// Created by Fethi El Hassasna on 2019-02-02. -// Copyright © 2019 matthewfecher.com. All rights reserved. -// - -import Foundation -import MediaPlayer - -// MARK: - CarPlay Setup - -extension AppDelegate { - - func setupCarPlay() { - playableContentManager = MPPlayableContentManager.shared() - - playableContentManager?.delegate = self - playableContentManager?.dataSource = self - - StationsManager.shared.addObserver(self) - } -} - -// MARK: - MPPlayableContentDelegate - -extension AppDelegate: MPPlayableContentDelegate { - - func playableContentManager(_ contentManager: MPPlayableContentManager, initiatePlaybackOfContentItemAt indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) { - - DispatchQueue.main.async { - if indexPath.count == 2 { - let station = StationsManager.shared.stations[indexPath[1]] - StationsManager.shared.set(station: station) - MPPlayableContentManager.shared().nowPlayingIdentifiers = [station.name] - } - completionHandler(nil) - } - } - - func beginLoadingChildItems(at indexPath: IndexPath, completionHandler: @escaping (Error?) -> Void) { - StationsManager.shared.fetch { result in - guard case .failure(let error) = result else { - completionHandler(nil) - return - } - - completionHandler(error) - } - } -} - -// MARK: - MPPlayableContentDataSource - -extension AppDelegate: MPPlayableContentDataSource { - - func numberOfChildItems(at indexPath: IndexPath) -> Int { - if indexPath.indices.count == 0 { - return 1 - } - - return StationsManager.shared.stations.count - } - - func contentItem(at indexPath: IndexPath) -> MPContentItem? { - - if indexPath.count == 1 { - // Tab section - let item = MPContentItem(identifier: "Stations") - item.title = "Stations" - item.isContainer = true - item.isPlayable = false - item.artwork = MPMediaItemArtwork(boundsSize: #imageLiteral(resourceName: "carPlayTab").size, requestHandler: { _ -> UIImage in - return #imageLiteral(resourceName: "carPlayTab") - }) - return item - } else if indexPath.count == 2, indexPath.item < StationsManager.shared.stations.count { - - // Stations section - let station = StationsManager.shared.stations[indexPath.item] - - let item = MPContentItem(identifier: "\(station.name)") - item.title = station.name - item.subtitle = station.desc - item.isPlayable = true - item.isStreamingContent = true - station.getImage { image in - item.artwork = MPMediaItemArtwork(boundsSize: image.size) { _ -> UIImage in - return image - } - } - - return item - } else { - return nil - } - } -} - -// MARK: - StationsManagerObserver - -extension AppDelegate: StationsManagerObserver { - - func stationsManager(_ manager: StationsManager, stationsDidUpdate stations: [RadioStation]) { - playableContentManager?.reloadData() - } - - func stationsManager(_ manager: StationsManager, stationDidChange station: RadioStation?) { - guard let station = station else { - playableContentManager?.nowPlayingIdentifiers = [] - return - } - - playableContentManager?.nowPlayingIdentifiers = [station.name] - } -} diff --git a/SwiftRadio/CarPlay/CarPlaySceneDelegate.swift b/SwiftRadio/CarPlay/CarPlaySceneDelegate.swift new file mode 100644 index 00000000..c826e3a8 --- /dev/null +++ b/SwiftRadio/CarPlay/CarPlaySceneDelegate.swift @@ -0,0 +1,94 @@ +// +// CarPlaySceneDelegate.swift +// SwiftRadio +// + +import CarPlay +import FRadioPlayer + +class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate { + + private var interfaceController: CPInterfaceController? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let templateApplicationScene = scene as? CPTemplateApplicationScene else { return } + + // Set up the CarPlay window + templateApplicationScene.delegate = self + } + + func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, + didConnect interfaceController: CPInterfaceController) { + print("CarPlay connected") + self.interfaceController = interfaceController + + // Create a simple list template + let listTemplate = CPListTemplate(title: "Radio Stations", sections: []) + + // Set as root template immediately + interfaceController.setRootTemplate(listTemplate, animated: false) + + // Then fetch and update stations + StationsManager.shared.fetch { [weak self] _ in + self?.updateStationsList(listTemplate) + } + + // Subscribe to updates + StationsManager.shared.addObserver(self) + } + + private func updateStationsList(_ template: CPListTemplate) { + let stations = StationsManager.shared.stations + print("Setting up stations list with \(stations.count) stations") + + let items = stations.map { station -> CPListItem in + // Create list item with image + let item = CPListItem(text: station.name, detailText: station.desc) + + // Add station image if available + station.getImage { image in + item.setImage(image) + } + + // Handle selection + item.handler = { _, completion in + print("Selected station: \(station.name)") + StationsManager.shared.set(station: station) + FRadioPlayer.shared.play() + completion() + } + + return item + } + + let section = CPListSection(items: items) + template.updateSections([section]) + } + + func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, + didDisconnectInterfaceController interfaceController: CPInterfaceController) { + print("CarPlay disconnected") + self.interfaceController = nil + StationsManager.shared.removeObserver(self) + } +} + +// MARK: - StationsManagerObserver + +extension CarPlaySceneDelegate: StationsManagerObserver { + + func stationsManager(_ manager: StationsManager, stationsDidUpdate stations: [RadioStation]) { + print("Stations updated: \(stations.count) stations") + if let listTemplate = interfaceController?.rootTemplate as? CPListTemplate { + DispatchQueue.main.async { + self.updateStationsList(listTemplate) + } + } + } + + func stationsManager(_ manager: StationsManager, stationDidChange station: RadioStation?) { + if let station { + print("Station changed to: \(station.name)") + } + } +} diff --git a/SwiftRadio/Info-CarPlay.plist b/SwiftRadio/Info-CarPlay.plist index 69bd7353..8743139c 100755 --- a/SwiftRadio/Info-CarPlay.plist +++ b/SwiftRadio/Info-CarPlay.plist @@ -2,65 +2,114 @@ - CFBundleDevelopmentRegion - en - CFBundleDisplayName - Swift Radio CP - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSRequiresIPhoneOS - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSPhotoLibraryAddUsageDescription - This app would like to save the image associated with the current track and station to your photo library. - NSUserActivityTypes - - NSUserActivityTypeBrowsingWeb - - UIBackgroundModes - - audio - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UIStatusBarStyle - UIStatusBarStyleLightContent - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Dark - UIBrowsableContentSupportsSectionedBrowsing - + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Swift Radio CP + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSPhotoLibraryAddUsageDescription + This app would like to save the image associated with the current track and station to your photo library. + NSUserActivityTypes + + NSUserActivityTypeBrowsingWeb + + UIBackgroundModes + + audio + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Dark + UIBrowsableContentSupportsSectionedBrowsing + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + CPTemplateApplicationSceneSessionRoleApplication + + + UISceneClassName + CPTemplateApplicationScene + UISceneConfigurationName + CarPlay Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).CarPlaySceneDelegate + + + + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedExternalAccessoryProtocols + + com.apple.carplay + + + NSCarPlayAudioUsageDescription + This app needs to play audio through CarPlay to stream radio stations. + diff --git a/SwiftRadio/SwiftRadio.entitlements b/SwiftRadio/SwiftRadio.entitlements index 8d6e9120..b161a3b0 100755 --- a/SwiftRadio/SwiftRadio.entitlements +++ b/SwiftRadio/SwiftRadio.entitlements @@ -4,5 +4,7 @@ com.apple.developer.playable-content + com.apple.developer.carplay-audio + From 0e20129a541f1146be8248e22b7cd718dc891899 Mon Sep 17 00:00:00 2001 From: Fethi El Hassasna Date: Thu, 23 Jan 2025 21:13:40 -0500 Subject: [PATCH 3/7] Clean up AppDelegate and remove duplicate code Signed-off-by: Fethi El Hassasna --- SwiftRadio.xcodeproj/project.pbxproj | 4 +- SwiftRadio/AppDelegate.swift | 124 +++------------------------ SwiftRadio/SceneDelegate.swift | 53 ++++++++---- 3 files changed, 49 insertions(+), 132 deletions(-) diff --git a/SwiftRadio.xcodeproj/project.pbxproj b/SwiftRadio.xcodeproj/project.pbxproj index 02dfdcf8..fbf4f80e 100755 --- a/SwiftRadio.xcodeproj/project.pbxproj +++ b/SwiftRadio.xcodeproj/project.pbxproj @@ -841,7 +841,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = swiftradiocarplay; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = swiftradiocarplay2; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -876,7 +876,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = swiftradiocarplay; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = swiftradiocarplay2; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/SwiftRadio/AppDelegate.swift b/SwiftRadio/AppDelegate.swift index 1cc555fa..3984c484 100755 --- a/SwiftRadio/AppDelegate.swift +++ b/SwiftRadio/AppDelegate.swift @@ -7,134 +7,32 @@ // import UIKit -import MediaPlayer -import FRadioPlayer @main class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - var coordinator: MainCoordinator? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - // FRadioPlayer config - FRadioPlayer.shared.isAutoPlay = true - FRadioPlayer.shared.enableArtwork = true - FRadioPlayer.shared.artworkAPI = iTunesAPI(artworkSize: 600) - - // AudioSession & RemotePlay - activateAudioSession() - setupRemoteCommandCenter() - UIApplication.shared.beginReceivingRemoteControlEvents() - - // Make status bar white - UINavigationBar.appearance().barStyle = .black - UINavigationBar.appearance().tintColor = .white - UINavigationBar.appearance().prefersLargeTitles = true - - // Start the coordinator - coordinator = MainCoordinator(navigationController: UINavigationController()) - - window = UIWindow(frame: UIScreen.main.bounds) - window?.rootViewController = coordinator?.navigationController - window?.makeKeyAndVisible() - - coordinator?.start() - + // Override point for customization after application launch. return true } - - func applicationWillResignActive(_ application: UIApplication) { - // 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. - // 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. - - } - - func applicationDidEnterBackground(_ application: UIApplication) { - // 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. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - - - } - - func applicationWillEnterForeground(_ application: UIApplication) { - // 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. - - } - - func applicationDidBecomeActive(_ application: UIApplication) { - // 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. - - - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - // Saves changes in the application's managed object context before the application terminates. - - UIApplication.shared.endReceivingRemoteControlEvents() - - } - + // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + #if CarPlay + if connectingSceneSession.role == .carTemplateApplication { + let config = UISceneConfiguration(name: "CarPlay Configuration", sessionRole: connectingSceneSession.role) + config.delegateClass = CarPlaySceneDelegate.self + return config + } + #endif + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { } - - // MARK: - Remote Controls - - private func setupRemoteCommandCenter() { - // Get the shared MPRemoteCommandCenter - let commandCenter = MPRemoteCommandCenter.shared() - - // Add handler for Play Command - commandCenter.playCommand.addTarget { event in - FRadioPlayer.shared.play() - return .success - } - - // Add handler for Pause Command - commandCenter.pauseCommand.addTarget { event in - FRadioPlayer.shared.pause() - return .success - } - - // Add handler for Toggle Command - commandCenter.togglePlayPauseCommand.addTarget { event in - FRadioPlayer.shared.togglePlaying() - return .success - } - - // Add handler for Next Command - commandCenter.nextTrackCommand.addTarget { event in - StationsManager.shared.setNext() - return .success - } - - // Add handler for Previous Command - commandCenter.previousTrackCommand.addTarget { event in - StationsManager.shared.setPrevious() - return .success - } - } - - // MARK: - Activate Audio Session - - private func activateAudioSession() { - do { - try AVAudioSession.sharedInstance().setActive(true) - } catch let error { - if Config.debugLog { - print("audioSession could not be activated: \(error.localizedDescription)") - } - } - } } diff --git a/SwiftRadio/SceneDelegate.swift b/SwiftRadio/SceneDelegate.swift index 79d99da6..83fe04e3 100644 --- a/SwiftRadio/SceneDelegate.swift +++ b/SwiftRadio/SceneDelegate.swift @@ -8,25 +8,44 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var coordinator: MainCoordinator? + private let player = FRadioPlayer.shared + private let manager = StationsManager.shared + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } // FRadioPlayer config - FRadioPlayer.shared.isAutoPlay = true - FRadioPlayer.shared.enableArtwork = true - FRadioPlayer.shared.artworkAPI = iTunesAPI(artworkSize: 600) + setupFRadioPlayer() // AudioSession & RemotePlay + setupAudioSessionAndRemoteControls() + + // UI Setup + setupUIAppearance() + + // Start the coordinator + setupCoordinator(windowScene: windowScene) + } + + private func setupFRadioPlayer() { + player.isAutoPlay = true + player.enableArtwork = true + player.artworkAPI = iTunesAPI(artworkSize: 600) + } + + private func setupAudioSessionAndRemoteControls() { activateAudioSession() setupRemoteCommandCenter() UIApplication.shared.beginReceivingRemoteControlEvents() - - // Make status bar white + } + + private func setupUIAppearance() { UINavigationBar.appearance().barStyle = .black UINavigationBar.appearance().tintColor = .white UINavigationBar.appearance().prefersLargeTitles = true - - // Start the coordinator + } + + private func setupCoordinator(windowScene: UIWindowScene) { coordinator = MainCoordinator(navigationController: UINavigationController()) window = UIWindow(windowScene: windowScene) @@ -58,32 +77,32 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let commandCenter = MPRemoteCommandCenter.shared() // Add handler for Play Command - commandCenter.playCommand.addTarget { event in - FRadioPlayer.shared.play() + commandCenter.playCommand.addTarget { [weak self] _ in + self?.player.play() return .success } // Add handler for Pause Command - commandCenter.pauseCommand.addTarget { event in - FRadioPlayer.shared.pause() + commandCenter.pauseCommand.addTarget { [weak self] _ in + self?.player.pause() return .success } // Add handler for Toggle Command - commandCenter.togglePlayPauseCommand.addTarget { event in - FRadioPlayer.shared.togglePlaying() + commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in + self?.player.togglePlaying() return .success } // Add handler for Next Command - commandCenter.nextTrackCommand.addTarget { event in - StationsManager.shared.setNext() + commandCenter.nextTrackCommand.addTarget { [weak self] _ in + self?.manager.setNext() return .success } // Add handler for Previous Command - commandCenter.previousTrackCommand.addTarget { event in - StationsManager.shared.setPrevious() + commandCenter.previousTrackCommand.addTarget { [weak self] _ in + self?.manager.setPrevious() return .success } } From 012ce396b3ff857b1dfe582d4850bbec1ed2cdef Mon Sep 17 00:00:00 2001 From: Fethi El Hassasna Date: Sat, 25 Jan 2025 15:08:12 -0500 Subject: [PATCH 4/7] Fix a background playback issue Signed-off-by: Fethi El Hassasna --- SwiftRadio/AppDelegate.swift | 14 +++++++------- SwiftRadio/SceneDelegate.swift | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/SwiftRadio/AppDelegate.swift b/SwiftRadio/AppDelegate.swift index 3984c484..c8d60886 100755 --- a/SwiftRadio/AppDelegate.swift +++ b/SwiftRadio/AppDelegate.swift @@ -15,24 +15,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. return true } - + // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions) -> UISceneConfiguration { - #if CarPlay + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { +#if CarPlay if connectingSceneSession.role == .carTemplateApplication { let config = UISceneConfiguration(name: "CarPlay Configuration", sessionRole: connectingSceneSession.role) config.delegateClass = CarPlaySceneDelegate.self return config } - #endif +#endif return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - + func application(_ application: UIApplication, - didDiscardSceneSessions sceneSessions: Set) { + didDiscardSceneSessions sceneSessions: Set) { } } diff --git a/SwiftRadio/SceneDelegate.swift b/SwiftRadio/SceneDelegate.swift index 83fe04e3..ff64a16b 100644 --- a/SwiftRadio/SceneDelegate.swift +++ b/SwiftRadio/SceneDelegate.swift @@ -34,7 +34,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } private func setupAudioSessionAndRemoteControls() { - activateAudioSession() setupRemoteCommandCenter() UIApplication.shared.beginReceivingRemoteControlEvents() } @@ -59,15 +58,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneDidBecomeActive(_ scene: UIScene) { + activateAudioSession() } func sceneWillResignActive(_ scene: UIScene) { } func sceneWillEnterForeground(_ scene: UIScene) { + activateAudioSession() } func sceneDidEnterBackground(_ scene: UIScene) { + activateAudioSession() } // MARK: - Remote Controls @@ -107,8 +109,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } - // MARK: - Activate Audio Session - private func activateAudioSession() { do { try AVAudioSession.sharedInstance().setActive(true) From 5fa5db198b5c0b2a4aa6d44856f961c352cc45a2 Mon Sep 17 00:00:00 2001 From: Fethi El Hassasna Date: Sat, 25 Jan 2025 16:08:14 -0500 Subject: [PATCH 5/7] Refactor and centralize audio setup in AudioSetupService - Create a shared AudioSetupService to handle all audio-related setup code. - Fixes background audio playback and ensures consistent behavior between CarPlay and main app launches. - Removes code duplication between SceneDelegate and CarPlaySceneDelegate. Signed-off-by: Fethi El Hassasna --- SwiftRadio.xcodeproj/project.pbxproj | 6 ++ SwiftRadio/AudioSetupService.swift | 79 +++++++++++++++++ SwiftRadio/CarPlay/CarPlaySceneDelegate.swift | 19 ++-- SwiftRadio/SceneDelegate.swift | 86 +++---------------- 4 files changed, 112 insertions(+), 78 deletions(-) create mode 100644 SwiftRadio/AudioSetupService.swift diff --git a/SwiftRadio.xcodeproj/project.pbxproj b/SwiftRadio.xcodeproj/project.pbxproj index fbf4f80e..fbf1640e 100755 --- a/SwiftRadio.xcodeproj/project.pbxproj +++ b/SwiftRadio.xcodeproj/project.pbxproj @@ -30,6 +30,8 @@ CA142F6B2D3D9A0D0071A388 /* BottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F692D3D9A0D0071A388 /* BottomSheetViewController.swift */; }; CA142F6D2D3D9DFB0071A388 /* BottomSheetHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F6C2D3D9DFB0071A388 /* BottomSheetHandler.swift */; }; CA142F6E2D3D9DFB0071A388 /* BottomSheetHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA142F6C2D3D9DFB0071A388 /* BottomSheetHandler.swift */; }; + CA2511B52D458033006D1A99 /* AudioSetupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2511B42D458033006D1A99 /* AudioSetupService.swift */; }; + CA2511B62D458033006D1A99 /* AudioSetupService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2511B42D458033006D1A99 /* AudioSetupService.swift */; }; CA58087D2D40BAC000F8082B /* CarPlaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA58087B2D40BAC000F8082B /* CarPlaySceneDelegate.swift */; }; CA58087E2D40C11300F8082B /* Info-CarPlay.plist in Resources */ = {isa = PBXBuildFile; fileRef = CEA82F492921F260009E9FA0 /* Info-CarPlay.plist */; }; CAB4E8292D3D7DC7001282E9 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB4E8282D3D7DC7001282E9 /* SceneDelegate.swift */; }; @@ -133,6 +135,7 @@ CA142F662D3D8E280071A388 /* NSLayoutConstraint+with.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+with.swift"; sourceTree = ""; }; CA142F692D3D9A0D0071A388 /* BottomSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetViewController.swift; sourceTree = ""; }; CA142F6C2D3D9DFB0071A388 /* BottomSheetHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetHandler.swift; sourceTree = ""; }; + CA2511B42D458033006D1A99 /* AudioSetupService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSetupService.swift; sourceTree = ""; }; CA58087B2D40BAC000F8082B /* CarPlaySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlaySceneDelegate.swift; sourceTree = ""; }; CAB4E8282D3D7DC7001282E9 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; CE0A4994291F3A220071C0CC /* SwiftRadio.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SwiftRadio.entitlements; sourceTree = ""; }; @@ -237,6 +240,7 @@ 94D260901B45D20000DE671C /* Config.swift */, 9409E11B1ABF6FEA00312E2B /* AppDelegate.swift */, CAB4E8282D3D7DC7001282E9 /* SceneDelegate.swift */, + CA2511B42D458033006D1A99 /* AudioSetupService.swift */, CF72ACE621F714D000461EED /* Main.storyboard */, 9409E1251ABF6FEA00312E2B /* Images.xcassets */, 9409E11A1ABF6FEA00312E2B /* Info.plist */, @@ -541,6 +545,7 @@ CEE9A27B2B535C780018FE68 /* AlbumArtworkView.swift in Sources */, CE6ECCFD292F1445008B3C16 /* MainCoordinator.swift in Sources */, 9409E11C1ABF6FEA00312E2B /* AppDelegate.swift in Sources */, + CA2511B52D458033006D1A99 /* AudioSetupService.swift in Sources */, 94D260961B45E3FA00DE671C /* AnimationFrames.swift in Sources */, CAB4E82A2D3D7DC7001282E9 /* SceneDelegate.swift in Sources */, CE6ECD03292F358C008B3C16 /* UIViewController+Email.swift in Sources */, @@ -564,6 +569,7 @@ CE6036172A47B88600E15E15 /* StationTableViewCell.swift in Sources */, CE9EE8DF293BB41300F62041 /* BaseController.swift in Sources */, CA142F6E2D3D9DFB0071A388 /* BottomSheetHandler.swift in Sources */, + CA2511B62D458033006D1A99 /* AudioSetupService.swift in Sources */, CEE9A2792B5345C30018FE68 /* NowPlayingViewController.swift in Sources */, CE6A3E33291F376D0058C82A /* ShareActivity.swift in Sources */, CA142F672D3D8E2F0071A388 /* NSLayoutConstraint+with.swift in Sources */, diff --git a/SwiftRadio/AudioSetupService.swift b/SwiftRadio/AudioSetupService.swift new file mode 100644 index 00000000..69f6d9e5 --- /dev/null +++ b/SwiftRadio/AudioSetupService.swift @@ -0,0 +1,79 @@ +// +// AudioSetupService.swift +// Swift Radio +// +// Created by Fethi El Hassasna on 1/25/25. +// Copyright (c) 2015 MatthewFecher.com. All rights reserved. +// + +import AVFAudio +import MediaPlayer +import FRadioPlayer + +class AudioSetupService { + static let shared = AudioSetupService() + + private let player = FRadioPlayer.shared + private let manager = StationsManager.shared + + private init() {} + + func setupAudioSession() { + do { + let session = AVAudioSession.sharedInstance() + try session.setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowBluetooth]) + try session.setActive(true) + } catch { + if Config.debugLog { + print("Failed to configure audio session: \(error.localizedDescription)") + } + } + } + + func setupFRadioPlayer() { + player.isAutoPlay = true + player.enableArtwork = true + player.artworkAPI = iTunesAPI(artworkSize: 600) + } + + func setupRemoteCommandCenter() { + let commandCenter = MPRemoteCommandCenter.shared() + + commandCenter.playCommand.addTarget { [weak self] _ in + self?.player.play() + return .success + } + + commandCenter.pauseCommand.addTarget { [weak self] _ in + self?.player.pause() + return .success + } + + commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in + self?.player.togglePlaying() + return .success + } + + commandCenter.nextTrackCommand.addTarget { [weak self] _ in + self?.manager.setNext() + return .success + } + + commandCenter.previousTrackCommand.addTarget { [weak self] _ in + self?.manager.setPrevious() + return .success + } + + UIApplication.shared.beginReceivingRemoteControlEvents() + } + + func activateAudioSession() { + do { + try AVAudioSession.sharedInstance().setActive(true) + } catch let error { + if Config.debugLog { + print("audioSession could not be activated: \(error.localizedDescription)") + } + } + } +} diff --git a/SwiftRadio/CarPlay/CarPlaySceneDelegate.swift b/SwiftRadio/CarPlay/CarPlaySceneDelegate.swift index c826e3a8..56781e75 100644 --- a/SwiftRadio/CarPlay/CarPlaySceneDelegate.swift +++ b/SwiftRadio/CarPlay/CarPlaySceneDelegate.swift @@ -1,6 +1,10 @@ // // CarPlaySceneDelegate.swift -// SwiftRadio +// Swift Radio +// +// Created by Fethi El Hassasna on 1/25/25. +// Copyright (c) 2015 MatthewFecher.com. All rights reserved. +// // import CarPlay @@ -9,10 +13,10 @@ import FRadioPlayer class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate { private var interfaceController: CPInterfaceController? + private let audioService = AudioSetupService.shared func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let templateApplicationScene = scene as? CPTemplateApplicationScene else { return } - // Set up the CarPlay window templateApplicationScene.delegate = self } @@ -26,11 +30,16 @@ class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate { let listTemplate = CPListTemplate(title: "Radio Stations", sections: []) // Set as root template immediately - interfaceController.setRootTemplate(listTemplate, animated: false) + interfaceController + .setRootTemplate(listTemplate, animated: false, completion: nil) // Then fetch and update stations - StationsManager.shared.fetch { [weak self] _ in - self?.updateStationsList(listTemplate) + if StationsManager.shared.stations.isEmpty { + StationsManager.shared.fetch { [weak self] _ in + self?.updateStationsList(listTemplate) + } + } else { + updateStationsList(listTemplate) } // Subscribe to updates diff --git a/SwiftRadio/SceneDelegate.swift b/SwiftRadio/SceneDelegate.swift index ff64a16b..cec292fb 100644 --- a/SwiftRadio/SceneDelegate.swift +++ b/SwiftRadio/SceneDelegate.swift @@ -1,25 +1,23 @@ +// +// SceneDelegate.swift +// Swift Radio +// +// Created by Fethi El Hassasna on 1/25/25. +// Copyright (c) 2015 MatthewFecher.com. All rights reserved. +// + import UIKit -import MediaPlayer -import FRadioPlayer -import AVFAudio class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - var coordinator: MainCoordinator? + private var coordinator: MainCoordinator? - private let player = FRadioPlayer.shared - private let manager = StationsManager.shared + private let audioService = AudioSetupService.shared func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } - // FRadioPlayer config - setupFRadioPlayer() - - // AudioSession & RemotePlay - setupAudioSessionAndRemoteControls() - // UI Setup setupUIAppearance() @@ -27,17 +25,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { setupCoordinator(windowScene: windowScene) } - private func setupFRadioPlayer() { - player.isAutoPlay = true - player.enableArtwork = true - player.artworkAPI = iTunesAPI(artworkSize: 600) - } - - private func setupAudioSessionAndRemoteControls() { - setupRemoteCommandCenter() - UIApplication.shared.beginReceivingRemoteControlEvents() - } - private func setupUIAppearance() { UINavigationBar.appearance().barStyle = .black UINavigationBar.appearance().tintColor = .white @@ -58,64 +45,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneDidBecomeActive(_ scene: UIScene) { - activateAudioSession() + audioService.activateAudioSession() } func sceneWillResignActive(_ scene: UIScene) { } func sceneWillEnterForeground(_ scene: UIScene) { - activateAudioSession() + audioService.activateAudioSession() } func sceneDidEnterBackground(_ scene: UIScene) { - activateAudioSession() - } - - // MARK: - Remote Controls - - private func setupRemoteCommandCenter() { - // Get the shared MPRemoteCommandCenter - let commandCenter = MPRemoteCommandCenter.shared() - - // Add handler for Play Command - commandCenter.playCommand.addTarget { [weak self] _ in - self?.player.play() - return .success - } - - // Add handler for Pause Command - commandCenter.pauseCommand.addTarget { [weak self] _ in - self?.player.pause() - return .success - } - - // Add handler for Toggle Command - commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in - self?.player.togglePlaying() - return .success - } - - // Add handler for Next Command - commandCenter.nextTrackCommand.addTarget { [weak self] _ in - self?.manager.setNext() - return .success - } - - // Add handler for Previous Command - commandCenter.previousTrackCommand.addTarget { [weak self] _ in - self?.manager.setPrevious() - return .success - } - } - - private func activateAudioSession() { - do { - try AVAudioSession.sharedInstance().setActive(true) - } catch let error { - if Config.debugLog { - print("audioSession could not be activated: \(error.localizedDescription)") - } - } + audioService.activateAudioSession() } } From 890dd32502613988b4afec63b253a82dd74564fc Mon Sep 17 00:00:00 2001 From: Fethi El Hassasna Date: Sat, 25 Jan 2025 16:09:18 -0500 Subject: [PATCH 6/7] Sync app UI with CarPlay Signed-off-by: Fethi El Hassasna --- SwiftRadio/ViewControllers/StationsViewController.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SwiftRadio/ViewControllers/StationsViewController.swift b/SwiftRadio/ViewControllers/StationsViewController.swift index d53898c9..04409aa7 100755 --- a/SwiftRadio/ViewControllers/StationsViewController.swift +++ b/SwiftRadio/ViewControllers/StationsViewController.swift @@ -80,6 +80,11 @@ class StationsViewController: BaseController, Handoffable { nowPlayingView.tapHandler = { [weak self] in self?.nowPlayingBarButtonPressed() } + + // Set defaults station if the app started from CarPlay + updateNowPlayingButton(station: manager.currentStation) + updateHandoffUserActivity(userActivity, station: manager.currentStation) + startNowPlayingAnimation(player.isPlaying) } override func viewWillAppear(_ animated: Bool) { From 45029fb1ec48c7528b4c338073a340dac9744afa Mon Sep 17 00:00:00 2001 From: Fethi El Hassasna Date: Sat, 25 Jan 2025 21:36:15 -0500 Subject: [PATCH 7/7] Add audioService to AppDelegate Signed-off-by: Fethi El Hassasna --- SwiftRadio/AppDelegate.swift | 7 + .../Views/BottomSheetViewController.swift | 194 ------------------ 2 files changed, 7 insertions(+), 194 deletions(-) delete mode 100644 SwiftRadio/Views/BottomSheetViewController.swift diff --git a/SwiftRadio/AppDelegate.swift b/SwiftRadio/AppDelegate.swift index c8d60886..b8a335cd 100755 --- a/SwiftRadio/AppDelegate.swift +++ b/SwiftRadio/AppDelegate.swift @@ -11,8 +11,15 @@ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { + private let audioService = AudioSetupService.shared + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. + // Setup all audio-related configurations at app launch + audioService.setupFRadioPlayer() + audioService.setupAudioSession() + audioService.setupRemoteCommandCenter() + return true } diff --git a/SwiftRadio/Views/BottomSheetViewController.swift b/SwiftRadio/Views/BottomSheetViewController.swift deleted file mode 100644 index 620e418e..00000000 --- a/SwiftRadio/Views/BottomSheetViewController.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// BottomSheetViewController.swift -// SwiftRadio -// -// Created by Fethi El Hassasna on 2024-01-14. -// Copyright 2024 matthewfecher.com. All rights reserved. -// - -import UIKit -import FRadioPlayer - -protocol BottomSheetViewControllerDelegate: AnyObject { - func bottomSheet(_ controller: BottomSheetViewController, didSelect option: BottomSheetViewController.Option) -} - -class BottomSheetViewController: UIViewController { - - enum Section: Int, CaseIterable { - case stationInfo - case music - case share - - var title: String? { - return nil - } - - } - - enum Option { - case info - case share(UIImage?) - case website - case openInMusic(URL?) - - var title: String { - switch self { - case .info: return "About Station" - case .share: return "Share Now Playing" - case .website: return "Station Website" - case .openInMusic: return "Play in Music App" - } - } - - var image: UIImage? { - switch self { - case .info: return UIImage(systemName: "info.circle") - case .share: return UIImage(systemName: "square.and.arrow.up") - case .website: return UIImage(systemName: "safari") - case .openInMusic: return UIImage(systemName: "music.note") - } - } - } - - weak var delegate: BottomSheetViewControllerDelegate? - private let station: RadioStation - private let player = FRadioPlayer.shared - - private lazy var tableView: UITableView = { - let table = UITableView(frame: .zero, style: .insetGrouped) - table.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") - table.delegate = self - table.dataSource = self - table.translatesAutoresizingMaskIntoConstraints = false - return table - }() - - init(station: RadioStation) { - self.station = station - super.init(nibName: nil, bundle: nil) - - if let sheet = sheetPresentationController { - sheet.prefersGrabberVisible = true - sheet.delegate = self - sheet.prefersScrollingExpandsWhenScrolledToEdge = false - sheet.detents = [.medium()] - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func getOptions(for section: Section) -> [Option] { - switch section { - case .stationInfo: - var options: [Option] = [.info] - if station.hasValidWebsite { - options.append(.website) - } - return options - case .music: - return [.openInMusic(nil)] - case .share: - return [.share(nil)] - } - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .systemBackground - setupViews() - - player.addObserver(self) - } - - private func setupViews() { - view.addSubview(tableView) - - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - if let sheet = sheetPresentationController { - let contentHeight = tableView.contentSize.height + view.safeAreaInsets.top + view.safeAreaInsets.bottom - sheet.detents = [.custom { _ in contentHeight }] - sheet.animateChanges { - sheet.selectedDetentIdentifier = sheet.detents.first?.identifier - } - } - } -} - -extension BottomSheetViewController: UITableViewDataSource, UITableViewDelegate { - - func numberOfSections(in tableView: UITableView) -> Int { - return Section.allCases.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let section = Section(rawValue: section)! - return getOptions(for: section).count - } - - func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - let section = Section(rawValue: section)! - return section.title - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) - let section = Section(rawValue: indexPath.section)! - let option = getOptions(for: section)[indexPath.row] - - var config = cell.defaultContentConfiguration() - config.text = option.title - config.image = option.image - - // Disable OpenInMusic cell if no metadata - if case .openInMusic = option { - let hasMetadata = player.currentArtworkURL != nil - cell.isUserInteractionEnabled = hasMetadata - // Update text color - config.textProperties.color = hasMetadata ? .label : .systemGray3 - config.imageProperties.tintColor = hasMetadata ? .label : .systemGray3 - } - - cell.contentConfiguration = config - cell.tintColor = .label - - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - let section = Section(rawValue: indexPath.section)! - let option = getOptions(for: section)[indexPath.row] - delegate?.bottomSheet(self, didSelect: option) - dismiss(animated: true) - } -} - -extension BottomSheetViewController: UISheetPresentationControllerDelegate { - func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) { - // Handle detent changes if needed - } -} - -extension BottomSheetViewController: FRadioPlayerObserver { - - func radioPlayer(_ player: FRadioPlayer, artworkDidChange artworkURL: URL?) { - // Reload the music section to update cell state - if let musicSection = Section.allCases.firstIndex(of: .music) { - tableView.reloadSections(IndexSet(integer: musicSection), with: .none) - } - } -}