Skip to content

Commit 1c9af5c

Browse files
committed
Remove storyboard dependency and migrate to coordinator pattern
1 parent 45029fb commit 1c9af5c

18 files changed

Lines changed: 1586 additions & 559 deletions

SwiftRadio.xcodeproj/project.pbxproj

Lines changed: 37 additions & 13 deletions
Large diffs are not rendered by default.

SwiftRadio/Config.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,52 @@ struct Config {
2727
static let website = "https://github.com/analogcode/Swift-Radio-Pro"
2828
static let email = "contact@fethica.com"
2929
static let emailSubject = "From \(Bundle.main.appName) App"
30+
31+
struct Libraries {
32+
static let items: [LibraryItem] = [
33+
LibraryItem(owner: "analogcode", repo: "Swift-Radio-Pro"),
34+
LibraryItem(owner: "fethica", repo: "FRadioPlayer"),
35+
LibraryItem(owner: "MengTo", repo: "Spring"),
36+
LibraryItem(owner: "ninjaprox", repo: "NVActivityIndicatorView"),
37+
]
38+
}
39+
40+
struct Features {
41+
static let items: [FeatureItem] = [
42+
FeatureItem(title: Content.Features.swiftCodebase.0, subtitle: Content.Features.swiftCodebase.1, icon: "swift"),
43+
FeatureItem(title: Content.Features.carPlay.0, subtitle: Content.Features.carPlay.1, icon: "car.fill"),
44+
FeatureItem(title: Content.Features.customizableUI.0, subtitle: Content.Features.customizableUI.1, icon: "paintbrush"),
45+
FeatureItem(title: Content.Features.albumArt.0, subtitle: Content.Features.albumArt.1, icon: "music.note.list"),
46+
FeatureItem(title: Content.Features.lockScreen.0, subtitle: Content.Features.lockScreen.1, icon: "lock.circle"),
47+
FeatureItem(title: Content.Features.multipleStations.0, subtitle: Content.Features.multipleStations.1, icon: "radio"),
48+
FeatureItem(title: Content.Features.easySetup.0, subtitle: Content.Features.easySetup.1, icon: "checkmark.seal.fill"),
49+
]
50+
}
51+
52+
struct About {
53+
static let sections: [InfoSection] = [
54+
InfoSection(title: Content.About.Sections.features, items: [
55+
.features()
56+
]),
57+
InfoSection(title: Content.About.Sections.contact, items: [
58+
.email(address: Config.email),
59+
.link(title: Content.About.feedback.0, subtitle: Content.About.feedback.1, url: "https://fethica.com/#contact")
60+
]),
61+
InfoSection(title: Content.About.Sections.support, items: [
62+
.rateApp(appID: "YOUR_APP_ID"),
63+
.share(text: Content.About.shareText)
64+
]),
65+
InfoSection(title: Content.About.Sections.credits, items: [
66+
.libraries(),
67+
.credits(owner: "analogcode", repo: "Swift-Radio-Pro")
68+
]),
69+
InfoSection(title: Content.About.Sections.legal, items: [
70+
.link(title: Content.About.license.0, subtitle: Content.About.license.1, url: "https://raw.githubusercontent.com/analogcode/Swift-Radio-Pro/refs/heads/master/LICENSE")
71+
]),
72+
InfoSection(title: Content.About.Sections.version, items: [
73+
.version()
74+
])
75+
]
76+
}
3077
}
3178

SwiftRadio/Content.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// Content.swift
3+
// Swift Radio
4+
//
5+
// Created by Fethi El Hassasna on 2025-01-26.
6+
// Copyright © 2025 matthewfecher.com. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
struct Content {
12+
struct About {
13+
static let title = "About Swift Radio"
14+
15+
static let headerText = """
16+
**Swift Radio** is a fully featured, open-source radio station app written entirely in **Swift**. \
17+
It provides robust, professional functionality out of the box, \
18+
complete with **Apple CarPlay** support, making it the perfect foundation \
19+
for **building** or **customizing** your own streaming radio experience.
20+
"""
21+
22+
static let footerAuthors = "Fethi El Hassasna & Matt Fecher"
23+
static let footerCopyright = "Swift Radio"
24+
25+
struct Sections {
26+
static let features = "Features"
27+
static let contact = "Contact"
28+
static let support = "Support"
29+
static let credits = "Credits"
30+
static let legal = "Legal"
31+
static let version = "Version"
32+
}
33+
34+
static let feedback = ("Feedback", "We value your input! Please take a moment to provide feedback")
35+
static let shareText = "Check out Swift Radio!"
36+
static let license = ("License", "MIT License")
37+
}
38+
39+
struct Contributors {
40+
static let title = "Contributors"
41+
}
42+
43+
struct Libraries {
44+
static let title = "Libraries"
45+
}
46+
47+
struct Features {
48+
static let title = "Features"
49+
50+
static let swiftCodebase = ("Swift Codebase", "Entirely written in Swift with a clean and modern structure.")
51+
static let carPlay = ("Apple CarPlay Support", "Lets users control their radio playback directly from their CarPlay dashboard.")
52+
static let customizableUI = ("Customizable UI", "Includes a flexible interface that you can easily personalize with your own theme and branding.")
53+
static let albumArt = ("Album Art & Metadata", "Displays track information and album covers to enhance the listening experience.")
54+
static let lockScreen = ("Lock Screen & Control Center Integration", "Shows artwork and track info on the lock screen, and provides convenient controls without opening the app.")
55+
static let multipleStations = ("Multiple Stations Setup", "Comes with a straightforward station list manager that supports multiple streaming URLs.")
56+
static let easySetup = ("Easy Project Setup", "Ready to run right out of the box, and you can adjust key settings in a single configuration file.")
57+
}
58+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// AboutCoordinator.swift
3+
// SwiftRadio
4+
//
5+
// Created by Fethi El Hassasna on 2025-01-31.
6+
// Copyright © 2025 matthewfecher.com. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import MessageUI
11+
12+
class AboutCoordinator: NSObject, NavigationCoordinator {
13+
var childCoordinators: [Coordinator] = []
14+
let navigationController: UINavigationController
15+
weak var parentCoordinator: MainCoordinator?
16+
17+
init(navigationController: UINavigationController) {
18+
self.navigationController = navigationController
19+
}
20+
21+
func start() {
22+
let aboutVC = AboutViewController()
23+
aboutVC.delegate = self
24+
navigationController.setViewControllers([aboutVC], animated: false)
25+
}
26+
}
27+
28+
// MARK: - AboutViewControllerDelegate
29+
30+
extension AboutCoordinator: AboutViewControllerDelegate {
31+
func aboutViewController(_ controller: AboutViewController, didSelectItem item: InfoItem) {
32+
switch item {
33+
case .features:
34+
let featuresVC = FeaturesViewController()
35+
navigationController.pushViewController(featuresVC, animated: true)
36+
37+
case .libraries:
38+
let librariesVC = LibrariesViewController()
39+
navigationController.pushViewController(librariesVC, animated: true)
40+
41+
case .credits(_, _, let owner, let repo, _):
42+
let contributorsVC = ContributorsViewController(owner: owner, repo: repo)
43+
navigationController.pushViewController(contributorsVC, animated: true)
44+
45+
case .link(_, _, let urlString, _):
46+
if let url = URL(string: urlString) {
47+
UIApplication.shared.open(url, options: [:], completionHandler: nil)
48+
}
49+
50+
case .email(_, _, let address, _):
51+
parentCoordinator?.openEmail(to: address, from: self)
52+
53+
case .share(_, let text, _):
54+
guard let aboutVC = navigationController.viewControllers.first as? AboutViewController else { return }
55+
parentCoordinator?.share(text, from: aboutVC)
56+
57+
case .rateApp(_, let appID, _):
58+
if let url = URL(string: "itms-apps://itunes.apple.com/app/id\(appID)") {
59+
UIApplication.shared.open(url, options: [:], completionHandler: nil)
60+
}
61+
62+
case .version:
63+
break
64+
}
65+
}
66+
}
67+
68+
// MARK: - MFMailComposeViewControllerDelegate
69+
70+
extension AboutCoordinator: MFMailComposeViewControllerDelegate {
71+
func mailComposeController(_ controller: MFMailComposeViewController,
72+
didFinishWith result: MFMailComposeResult,
73+
error: Error?) {
74+
controller.dismiss(animated: true)
75+
}
76+
}

SwiftRadio/Coordinators/MainCoordinator.swift

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,55 @@ import MessageUI
1212
class MainCoordinator: NavigationCoordinator {
1313
var childCoordinators: [Coordinator] = []
1414
let navigationController: UINavigationController
15-
15+
1616
func start() {
1717
let loaderVC = LoaderController()
1818
loaderVC.delegate = self
1919
navigationController.setViewControllers([loaderVC], animated: false)
2020
}
21-
21+
2222
init(navigationController: UINavigationController) {
2323
self.navigationController = navigationController
2424
}
25-
25+
2626
// MARK: - Shared
27-
28-
func openWebsite() {
29-
guard let url = URL(string: Config.website) else { return }
27+
28+
func openEmail(to email: String, from coordinator: AboutCoordinator) {
29+
guard let aboutVC = coordinator.navigationController.viewControllers.first as? AboutViewController else { return }
30+
guard MFMailComposeViewController.canSendMail() else {
31+
aboutVC.showSendMailErrorAlert()
32+
return
33+
}
34+
35+
let mailComposer = MFMailComposeViewController()
36+
mailComposer.mailComposeDelegate = coordinator
37+
mailComposer.setToRecipients([email])
38+
mailComposer.setSubject(Config.emailSubject)
39+
mailComposer.setMessageBody("", isHTML: false)
40+
aboutVC.present(mailComposer, animated: true)
41+
}
42+
43+
func openAbout() {
44+
let modalNav = UINavigationController()
45+
let aboutCoordinator = AboutCoordinator(navigationController: modalNav)
46+
aboutCoordinator.parentCoordinator = self
47+
aboutCoordinator.start()
48+
childCoordinators.append(aboutCoordinator)
49+
navigationController.present(modalNav, animated: true)
50+
}
51+
52+
func openWebsite(url: URL, from viewController: UIViewController) {
3053
UIApplication.shared.open(url, options: [:], completionHandler: nil)
3154
}
32-
33-
func openEmail(in viewController: UIViewController & MFMailComposeViewControllerDelegate) {
34-
let receipients = [Config.email]
35-
let subject = Config.emailSubject
36-
let messageBody = ""
37-
38-
let configuredMailComposeViewController = viewController.configureMailComposeViewController(recepients: receipients, subject: subject, messageBody: messageBody)
39-
40-
if viewController.canSendMail {
41-
viewController.present(configuredMailComposeViewController, animated: true, completion: nil)
42-
} else {
43-
viewController.showSendMailErrorAlert()
55+
56+
func share(_ text: String, from viewController: UIViewController) {
57+
let activityViewController = UIActivityViewController(activityItems: [text], applicationActivities: nil)
58+
if let popoverController = activityViewController.popoverPresentationController {
59+
popoverController.sourceView = viewController.view
60+
popoverController.sourceRect = CGRect(x: viewController.view.bounds.midX, y: viewController.view.bounds.midY, width: 0, height: 0)
61+
popoverController.permittedArrowDirections = []
4462
}
45-
}
46-
47-
func openAbout(in viewController: UIViewController) {
48-
let aboutController = Storyboard.viewController as AboutViewController
49-
aboutController.delegate = self
50-
viewController.present(aboutController, animated: true)
63+
viewController.present(activityViewController, animated: true)
5164
}
5265
}
5366

@@ -64,56 +77,43 @@ extension MainCoordinator: LoaderControllerDelegate {
6477
// MARK: - StationsViewControllerDelegate
6578

6679
extension MainCoordinator: StationsViewControllerDelegate {
67-
80+
6881
func pushNowPlayingController(_ stationsViewController: StationsViewController, newStation: Bool) {
6982
let nowPlayingController = NowPlayingViewController()
7083
nowPlayingController.delegate = self
7184
nowPlayingController.isNewStation = newStation
7285
navigationController.pushViewController(nowPlayingController, animated: true)
7386
}
74-
87+
7588
func presentPopUpMenuController(_ stationsViewController: StationsViewController) {
76-
let popUpMenuController = Storyboard.viewController as PopUpMenuViewController
77-
popUpMenuController.delegate = self
78-
navigationController.present(popUpMenuController, animated: true)
89+
openAbout()
7990
}
8091
}
8192

8293
// MARK: - NowPlayingViewControllerDelegate
8394

8495
extension MainCoordinator: NowPlayingViewControllerDelegate {
85-
96+
8697
func didSelectBottomSheetOption(_ option: BottomSheetViewController.Option, from controller: NowPlayingViewController) {
8798
guard let station = StationsManager.shared.currentStation else { return }
8899
BottomSheetHandler.handle(option, station: station, from: controller)
89100
}
90-
101+
91102
func didTapCompanyButton(_ nowPlayingViewController: NowPlayingViewController) {
92-
openAbout(in: nowPlayingViewController)
103+
openAbout()
93104
}
94105
}
95106

96107
// MARK: - PopUpMenuViewControllerDelegate
97108

98109
extension MainCoordinator: PopUpMenuViewControllerDelegate {
99-
110+
100111
func didTapWebsiteButton(_ popUpMenuViewController: PopUpMenuViewController) {
101-
openWebsite()
102-
}
103-
104-
func didTapAboutButton(_ popUpMenuViewController: PopUpMenuViewController) {
105-
openAbout(in: popUpMenuViewController)
112+
guard let url = URL(string: Config.website) else { return }
113+
openWebsite(url: url, from: popUpMenuViewController)
106114
}
107-
}
108-
109-
// MARK: - PopUpMenuViewControllerDelegate
110115

111-
extension MainCoordinator: AboutViewControllerDelegate {
112-
func didTapEmailButton(_ aboutViewController: AboutViewController) {
113-
openEmail(in: aboutViewController)
114-
}
115-
116-
func didTapWebsiteButton(_ aboutViewController: AboutViewController) {
117-
openWebsite()
116+
func didTapAboutButton(_ popUpMenuViewController: PopUpMenuViewController) {
117+
openAbout()
118118
}
119119
}

SwiftRadio/Helpers/Storyboard.swift

Lines changed: 0 additions & 26 deletions
This file was deleted.

SwiftRadio/Helpers/UIViewController+Email.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,25 @@
99
import MessageUI
1010

1111
extension MFMailComposeViewControllerDelegate where Self: UIViewController {
12-
12+
1313
var canSendMail: Bool {
1414
MFMailComposeViewController.canSendMail()
1515
}
16-
16+
1717
func configureMailComposeViewController(recepients: [String], subject: String, messageBody: String) -> MFMailComposeViewController {
18-
18+
1919
let mailComposerVC = MFMailComposeViewController()
2020
mailComposerVC.mailComposeDelegate = self
21-
21+
2222
mailComposerVC.setToRecipients(recepients)
2323
mailComposerVC.setSubject(subject)
2424
mailComposerVC.setMessageBody(messageBody, isHTML: false)
25-
25+
2626
return mailComposerVC
2727
}
28-
28+
}
29+
30+
extension UIViewController {
2931
func showSendMailErrorAlert() {
3032
let sendMailErrorAlert = UIAlertController(title: "Could Not Send Email", message: "Your device could not send e-mail. Please check e-mail configuration and try again.", preferredStyle: .alert)
3133
let cancelAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)

0 commit comments

Comments
 (0)