Armony helps musicians connect with each other. Users can create profiles showing their musical skills and find other musicians to work with. It's perfect for musicians, songwriters, composers, sound engineers, music teachers, producers, and more. You can find it on the App Store or visit Armony for more info.
- Project Tech Stack
- Getting Started
- Secrets
- Structure
- SwiftUI Integration
- Coordinator Pattern with SwiftUI
- Deep Linking
- GitHub Actions Scripts
- CI Scripts
- Networking
- Language: Swift
- UI Frameworks: UIKit & SwiftUI
- Minimum iOS Version: 15.0
- Design Pattern: MVVM-C
- Package Manager: Swift Package Manager
- Main Libraries: Alamofire, AlamofireImage and UIScrollView-InfiniteScroll
- Style Guide: Raywenderlich
- Powered by ❤️
Follow these steps to get started with the project:
- Xcode
- Access to Firebase and Facebook Developer accounts (to generate required secret keys)
Clone the repository:
git clone git@github.com:studiogo-armony/armony-ios.git
cd armony-ios
This project uses sensitive keys for integrations.
GoogleService-Info.plist
: For Release buildsGoogleService-Info-Debug.plist
: For Debug builds
FACEBOOK_APP_ID
FACEBOOK_CLIENT_TOKEN
MIXPANEL_TOKEN
ADJUST_TOKEN
Store secrets in configuration files specific to your environment:
- Debug Configuration (
${PROJECT_DIR}/Armony/Resources/Configs/DebugConfiguration.xcconfig
) - Release Configuration (
${PROJECT_DIR}/Armony/Resources/Configs/ReleaseConfiguration.xcconfig
)
⊢ Common
⊢ UI
⊢ Analytics
⊢ Deeplink
⊢ Protocols
⊢ UI
⊢ Banner, Card, Avatar etc
⊢ Service
⊢ Firebase
⊢ RemoteNotification
⊢ Socket
⊢ Authentication
⊢ Scenes
⊢ Adverts
⊢ API
⊢ Tasks // Tasks that retrieve any related response coming from API.
⊢ Models // Entity for API
⊢ UI
⊢ Presentation
⊢ View
⊢ ABCPresentation.swift //Presentation/ViewModel object of ABC
⊢ ABCView.swift
⊢ ABCView.xib
⊢ AdvertsCoordinator.swift
⊢ AdvertsViewModel.swift
⊢ AdvertsViewController.swift
⊢ Home.storyboard
This app uses both UIKit and SwiftUI. Here's how we use SwiftUI:
- Regions Screen: A screen for selecting regions, built with SwiftUI components
- AdvertListing Screen: Shows ads in a grid layout
- Info View: A simple view to show information with an icon
We use these components to connect UIKit and SwiftUI:
EmptyStateSwiftUIView
: Shows empty statesSwiftUICardView
: Shows cardsNotchViewSwiftUI
: Shows the notch viewPolicyTextView
: Shows text content
- Uses MVVM pattern with SwiftUI's @ObservedObject
- Works with our app's theme system
- Combines UIKit and SwiftUI views
- Needs iOS 16+ for some features
- New screens use SwiftUI
- Old UIKit components work with SwiftUI
We use MVVM-C pattern to handle navigation in both UIKit and SwiftUI screens.
public typealias Navigator = UINavigationController
public protocol Coordinator {
associatedtype Controller: ViewController
var navigator
8000
: Navigator? { get }
func createViewController() -> Controller
func createNavigatorWithRootViewController() -> (navigator: Navigator, view: Controller)
}
protocol SwiftUICoordinator: Coordinator {
associatedtype Content: View
}
final class MyCoordinator: SwiftUICoordinator {
var navigator: Navigator?
typealias Content = MySwiftUIView
typealias Controller = UIHostingController<MySwiftUIView>
init(navigator: Navigator? = nil) {
self.navigator = navigator
}
func start() {
let viewModel = MySwiftUIViewModel()
viewModel.coordinator = self
let view = MySwiftUIView(viewModel: viewModel)
let hosting = createHostingViewController(rootView: view)
hosting.title = "TITLE"
navigator?.pushViewController(hosting, animated: true)
}
}
With this setup, SwiftUI views:
- Work with UIKit navigation
- Use the same navigation system
- Support deep linking
- Match the app's style
Our app uses deep links to open specific screens directly. It's built with a custom URL system.
public extension Deeplink {
static let account: Deeplink = "/account"
static let advert: Deeplink = "/advert"
static let chats: Deeplink = "/chats"
static let liveChat: Deeplink = "/live-chat"
// ... and more
}
- Controls all deep link navigation
- Handles URL patterns
- Checks if user is logged in
- Works with coordinators
- Can pass data through URLs
public protocol URLNavigatable {
var isAuthenticationRequired: Bool { get }
static var instance: URLNavigatable { get }
static func register(navigator: URLNavigation)
}
Here's how to add deep linking to a coordinator:
extension MyCoordinator: URLNavigatable {
var isAuthenticationRequired: Bool { true }
static var instance: URLNavigatable {
return MyCoordinator()
}
static func register(navigator: URLNavigation) {
navigator.register(coordinator: instance, pattern: .myScreen) { result in
if let id = result.value(forKey: "id") as? String {
MyCoordinator(navigator: result.navigator, id: id).start()
}
}
}
Our project uses GitHub Actions for automated workflows. Here are the main workflows:
This workflow automates the creation of release branches with version increments:
# Triggered manually with version type choice
- Automatically increments version (major, minor, or patch)
- Creates a new release branch from development
- Updates the marketing version in Xcode project
This workflow handles merging release branches to the main branch:
# Can be triggered manually or automatically
- Merges the latest release branch to main
- Supports manual selection of release branch
- Uses no-fast-forward merge strategy
This workflow keeps the development branch in sync with main:
# Triggers:
- Automatically after successful release merge
- Manually when needed
- Ensures development branch stays updated with main
Our project includes several CI scripts in the ci_scripts
directory that handle different stages of the build process. These scripts are designed to work with both Xcode Cloud and other CI environments.
ci_pre_xcodebuild.sh
runs before the build process starts:
# Main responsibilities:
- Validates required environment variables
- Sets up configuration files for different environments (Debug/Release)
- Configures third-party service integrations
- Updates necessary plists and configuration files
ci_post_clone.sh
runs after repository cloning:
# Main responsibilities:
- Creates necessary directory structure
- Sets up configuration files
- Initializes required plists
- Prepares the environment for building
ci_post_xcodebuild.sh
runs after the build process:
# Main responsibilities:
- Handles debug symbol uploads
- Processes build artifacts
- Performs necessary post-build tasks
Our project uses a layered networking architecture that provides type-safe API requests. Here's how it works:
The core service class that handles all network operations:
class RestService: Service {
func execute<R: APIResponse>(task: HTTPTask, type: R.Type) async throws -> R
}
RestObjectResponse<T>
: For single object responsesRestArrayResponse<T>
: For array responses- Built-in error handling with
RestErrorResponse
Each API endpoint is represented by a task that implements HTTPTask
:
struct GetMyNetworkTask: HTTPTask {
var method: HTTPMethod = .get
var path: String = "/my-network"
var urlQueryItems: [URLQueryItem]?
init(userID: String) {
urlQueryItems = [
URLQueryItem(name: "userID", value: userID)
]
}
}
// Create the task
let task = GetMyNetworkTask(userID: "123")
// Execute the request
Task {
do {
let response = try await restService.execute(
task: task,
type: RestArrayResponse<MyNetworkModel>.self
)
// Handle response
} catch {
// Handle error
}
}