Skip to content

iUsmanN/CrowdCast_iOS

Repository files navigation

----------------------

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.


Some Screens

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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published