Crowd Cast is designed to be a modern video calling and management platform. With Crowdcast, there is no need to individually share video call links with people anymore. Providing an independant video calling platform, Crowd Cast allows you to create and join video call channels seamlessly. Crowds are groups of people that can be created where each member can access each video call channel within the Crowd. The individual channels you create can be shared and join using generated short deeplinks that provide a swift experience to quickly get started.
Crowd Cast is a side project aimed at showcasing my programming style and some of the technologies that I have worked with over time with Swift in iOS.
The app includes the following programming practices (Examples attached):
- MVVM
- Protocol Oriented Programming
protocol CCSetsNavbar : CCOpensSettings {}
extension CCSetsNavbar {
/// Sets up navigation bar
/// - Parameters:
/// - navigationBar: View Controller's navigation bar
/// - navigationItem: View Controller's navigation controller
/// - title: View Controller's title
/// - largeTitles: should prefer large titles
/// - profileAction: profile button action
func setupNavBar(navigationBar: UINavigationBar?,navigationItem: UINavigationItem, title: String?, largeTitles: Bool, profileAction: Selector?){
navigationItem.title = title
navigationBar?.prefersLargeTitles = largeTitles
let leftButton = getLogoButton()
let profileButton = getProfileButton(action: profileAction)
profileButton.action = profileAction
navigationItem.leftBarButtonItem = leftButton
navigationItem.setRightBarButtonItems([profileButton], animated: true)
}
}
- Generic Programming
func request<T: Codable>(endpoint: Endpoint, result: @escaping (Result<T, CCError>)->()){
var components = URLComponents()
components.scheme = endpoint.scheme
components.host = endpoint.host
components.path = endpoint.path
guard let url = components.url else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = endpoint.httpMethod.rawValue
let session = URLSession(configuration: .default)
let dataTask = session.dataTask(with: urlRequest) { data, response, error in
guard error == nil, response != nil, let data = data else { return }
let responseObject = try! JSONDecoder().decode(T.self, from: data)
result(.success(responseObject))
}
dataTask.resume()
}
- Multithreading
Defining custom Dispatch Queue protocol for ease of use:
protocol CCDispatchQueue {}
extension CCDispatchQueue {
/// Dispatch Priority Item
/// - Parameters:
/// - type: Async/Sync
/// - code: Code Block to run
/// - Returns: nil
func dispatchPriorityItem(_ type: DispatchQueue.Attributes, code: @escaping ()->()){
let queue = DispatchQueue(label: "com.CrowdCast.HighPriority", qos: .utility, attributes: type)
queue.async(execute: DispatchWorkItem(block: code))
}
}
Implementation:
extension CCChannelsVM : CCChannelsService, CCDispatchQueue, CCGetIndexPaths {
func fetchFreshData() {
let dg = DispatchGroup()
var fetchedCounts = (0, 0)
var newMyChannels = 0
var newJoinedChannels = 0
dg.enter()
dispatchPriorityItem(.concurrent, code: {
self.getUserChannels(type: .owned) { (result) in
switch result {
case .success(let inputData):
self.myChannels.clearData()
self.myChannels.updateData(input: inputData)
fetchedCounts = (inputData.data.count, fetchedCounts.1)
newMyChannels = inputData.data.count
dg.leave()
case .failure(let error):
prints(error)
dg.leave()
}
}
})
dg.enter()
dispatchPriorityItem(.concurrent, code: {
self.getUserChannels(type: .joined) { (result) in
switch result {
case .success(let inputData):
self.joinedChannels.clearData()
self.joinedChannels.updateData(input: inputData)
fetchedCounts = (fetchedCounts.0, inputData.data.count)
newJoinedChannels = inputData.data.count
dg.leave()
case .failure(let error):
prints(error)
dg.leave()
}
}
})
dg.notify(queue: .global()) { [weak self] in
self?.publishChannelUpdates(action: CCChannelsVC.refresh ? .refresh : .insert, newCreatedChannels: newMyChannels, newJoinedChannels: newJoinedChannels)
CCChannelsVC.refresh = false
}
}
- Swift Combine
class CCChannelsVM {
.
.
.
let channelsPublisher = PassthroughSubject<(dataAction, [IndexPath]), Never>()
.
.
.
self?.publishChannelUpdates(action: CCChannelsVC.refresh ? .refresh : .insert, newCreatedChannels: newMyChannels, newJoinedChannels: newJoinedChannels)
.
.
.
}
extension CCChannelsVC {
func bindVM(){
viewModel?.channelsPublisher.sink(receiveValue: { [weak self] (indexPathsInput) in
switch indexPathsInput.0 {
case .insert:
self?.insertRows(at: indexPathsInput.1)
case .remove:
self?.removeRows(at: indexPathsInput.1)
case .refresh:
self?.refreshRows()
}}).store(in: &combineCancellable)
}
.
.
.
}
- Swift Extensions
class CCCameraView : UIView {
var captureSession = AVCaptureSession()
var videoPreviewLayer : AVCaptureVideoPreviewLayer?
var capturePhotoOutput = AVCapturePhotoOutput()
func setupCameraView() {
initUI(.front)
}
func initUI(_ position: AVCaptureDevice.Position) {
guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position) else { return }
do {
let input = try AVCaptureDeviceInput(device: captureDevice)
if captureSession.canAddInput(input) { captureSession.addInput(input) }
DispatchQueue.main.async { [weak self] in
self?.videoPreviewLayer = AVCaptureVideoPreviewLayer(session: self!.captureSession)
self?.videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
self?.videoPreviewLayer?.frame = self!.frame
self?.layer.addSublayer(self!.videoPreviewLayer!)
self?.captureSession.commitConfiguration()
}
} catch {
print("Error")
}
}
}
- Swift Enums
enum CardHeaderAction {
case newChannel, newGroup, joinChannel, joinGroup, viewAll, pinnedChannels
}
Frameworks used:
- Firestore
//MARK: CHANNELS
extension CCQueryEngine {
.
.
.
func userChannel(id: String?) -> DocumentReference {
let db = Firestore.firestore()
let env = Constants.environment
return db.document("\(env)\(CCQueryPath.channelsData.rawValue)/\(id ?? "")")
}
.
.
.
}
func fetchData<T: Codable>(query: Query, completion: @escaping (Result<[T], Error>) -> ()){
query.getDocuments { (documents, error) in
guard error == nil, let data = documents else {
completion(.failure(CCError.firebaseFailure))
return
}
do {
let output = try data.documents.compactMap({ try $0.data(as: T.self) })
completion(.success(output))
} catch {
completion(.failure(CCError.networkEngineFailure))
}
}
}
}
- Twilio Video SDK
extension CCCallScreenVM : CCTwilioService {
///Joins the Twilio Video Channel
func joinChannel(result: ((Result<Room, CCError>)->())?){
guard let channelID = channelData?.id else { result?(.failure(.twilioCredentialsError)); return }
refreshAccessToken { [weak self](tokenResult) in
switch tokenResult {
case .success(let token):
let connectOptions = ConnectOptions(token: token.token ?? "") { (connectOptionsBuilder) in
connectOptionsBuilder.roomName = channelID
if let audioTrack = self?.localAudioTrack {
connectOptionsBuilder.audioTracks = [ audioTrack ]
}
if let dataTrack = self?.localDataTrack {
connectOptionsBuilder.dataTracks = [ dataTrack ]
}
if let videoTrack = self?.localVideoTrack {
connectOptionsBuilder.videoTracks = [ videoTrack ]
}
}
self?.room = TwilioVideoSDK.connect(options: connectOptions, delegate: self)
case .failure(let error):
result?(.failure(error))
}
}
}
}
- Firebase Deeplinking
protocol CCDynamicLinkEngine {}
extension CCDynamicLinkEngine {
func generateShareLink<T: CCContainsID>(input: T?, completion: @escaping (Result<String?, CCError>)->()){
let lp = linkProperties()
lp.addControlParam("id", withValue: input?.id ?? "")
lp.addControlParam("isGroup", withValue: input is CCChannel ? "false" : "true")
universalObject().getShortUrl(with: lp) { (string, error) in
guard error == nil else { completion(.failure(.branchLinkError)); return }
completion(.success(string))
}
}
}
- Bulletin Board
class CCBulletinManager {
var manager: BLTNItemManager?
func setItem(item: BLTNItem) {
manager = BLTNItemManager(rootItem: item)
manager?.backgroundViewStyle = .dimmed
}
static func joinChannel() -> BLTNPageItem {
let page = CCBLTNPageItem(title: "Join Channel")
page.alternativeButtonTitle = "Join via Dynamic Link"
page.alternativeHandler = { item in
page.next = enterCode()
item.manager?.displayNextItem()
}
return page
}
.
.
.
}
Muhammad Usman Nazir