├── Walkie-Talkie ├── Resources │ ├── cat.mp4 │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── micOn.imageset │ │ │ ├── micOn.png │ │ │ └── Contents.json │ │ ├── micOff.imageset │ │ │ ├── micOff.png │ │ │ └── Contents.json │ │ ├── soundOn.imageset │ │ │ ├── soundOn.png │ │ │ └── Contents.json │ │ ├── cameraOn.imageset │ │ │ ├── cameraOn.png │ │ │ └── Contents.json │ │ ├── hangup.imageset │ │ │ ├── telephone.png │ │ │ └── Contents.json │ │ ├── soundOff.imageset │ │ │ ├── soundOff.png │ │ │ └── Contents.json │ │ ├── cameraOff.imageset │ │ │ ├── cameraOff.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ └── Base.lproj │ │ └── LaunchScreen.storyboard ├── Modules │ ├── VideoView │ │ ├── VideoView.swift │ │ └── VideoVM.swift │ ├── PIP │ │ ├── PIPVideoVM.swift │ │ ├── PIPVideoVC.swift │ │ └── PIPVideoVC.xib │ ├── Contacts │ │ ├── Cell │ │ │ ├── ContactCell.swift │ │ │ ├── ContactCellVM.swift │ │ │ └── СontactCell.xib │ │ ├── ContactsVM.swift │ │ ├── ContactsVC.swift │ │ └── ContactsVC.xib │ ├── Call │ │ ├── CallVM.swift │ │ ├── CallVC.swift │ │ ├── WebRTCView.swift │ │ └── CallVC.xib │ ├── JoinView │ │ ├── JoinView.swift │ │ ├── JoinView.xib │ │ └── JoinVM.swift │ ├── Main │ │ ├── Animations │ │ │ ├── MainVC+UINavigationDelegate.swift │ │ │ ├── BaseAnimator.swift │ │ │ ├── UIView+Extension.swift │ │ │ ├── MainToCallAnimator.swift │ │ │ └── CallToMainAnimator.swift │ │ ├── MainVM.swift │ │ ├── MainVC.swift │ │ └── MainVC.xib │ ├── Source │ │ ├── SourceVM.swift │ │ ├── SourceVC.swift │ │ └── SourceVC.xib │ └── Settings │ │ ├── SettingsView.swift │ │ ├── SettingsVM.swift │ │ └── SettingsView.xib ├── Helpers │ ├── Shared │ │ ├── Bond │ │ │ ├── ReactiveClasses │ │ │ │ ├── BackableVM.swift │ │ │ │ ├── BaseServiceError.swift │ │ │ │ ├── XibLoadedVC.swift │ │ │ │ ├── BondViewModel.swift │ │ │ │ ├── SelectableTableCell.swift │ │ │ │ ├── BondTableCell.swift │ │ │ │ ├── BondTableView.swift │ │ │ │ ├── BondCollectionCell.swift │ │ │ │ ├── BondVC.swift │ │ │ │ └── BondView.swift │ │ │ ├── UIPageViewController+Bond.swift │ │ │ ├── UITableView+Bond.swift │ │ │ ├── UIButton+ThrottledTap.swift │ │ │ ├── UITextField+Bond.swift │ │ │ ├── UIDocumentPickerViewController+Bond.swift │ │ │ ├── UISearchBar+Bond.swift │ │ │ └── UIScrollView+Bond.swift │ │ ├── UIViewController+HideKeyboard.swift │ │ ├── UIStoryboard+Extensions.swift │ │ ├── UIImagePickerController+Bond.swift │ │ ├── RegisterNibsExtension.swift │ │ └── UIApplication+Extension.swift │ └── Encodable+Dictionary.swift ├── WebRTC │ ├── Extensions │ │ ├── Codables │ │ │ ├── SdpType.swift │ │ │ ├── IceCandidate.swift │ │ │ ├── SessionDescription.swift │ │ │ └── RTCStates.swift │ │ ├── WebRTCClientDelegate.swift │ │ └── Config.swift │ ├── WebRTCService+RTCDataChannelDelegate.swift │ ├── VideoClient.swift │ ├── WebRTCService+RTCPeerConnectionDelegate.swift │ ├── AudioClient.swift │ └── WebRTCClient.swift ├── Base │ ├── AppDelegate.swift │ ├── SceneDelegate.swift │ └── Router │ │ └── Router.swift ├── GoogleService-Info.plist ├── Info.plist └── Signaling │ └── SignalingClient.swift ├── Walkie-Talkie.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── Walkie-Talkie.xcscheme ├── Walkie-Talkie.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Podfile ├── .gitignore ├── LICENSE ├── README.md └── Podfile.lock /Walkie-Talkie/Resources/cat.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxatma/Walkie-Talkie/HEAD/Walkie-Talkie/Resources/cat.mp4 -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/micOn.imageset/micOn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxatma/Walkie-Talkie/HEAD/Walkie-Talkie/Resources/Assets.xcassets/micOn.imageset/micOn.png -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/micOff.imageset/micOff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxatma/Walkie-Talkie/HEAD/Walkie-Talkie/Resources/Assets.xcassets/micOff.imageset/micOff.png -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/soundOn.imageset/soundOn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxatma/Walkie-Talkie/HEAD/Walkie-Talkie/Resources/Assets.xcassets/soundOn.imageset/soundOn.png -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/cameraOn.imageset/cameraOn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxatma/Walkie-Talkie/HEAD/Walkie-Talkie/Resources/Assets.xcassets/cameraOn.imageset/cameraOn.png -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/hangup.imageset/telephone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxatma/Walkie-Talkie/HEAD/Walkie-Talkie/Resources/Assets.xcassets/hangup.imageset/telephone.png -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/soundOff.imageset/soundOff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxatma/Walkie-Talkie/HEAD/Walkie-Talkie/Resources/Assets.xcassets/soundOff.imageset/soundOff.png -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/cameraOff.imageset/cameraOff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Maxatma/Walkie-Talkie/HEAD/Walkie-Talkie/Resources/Assets.xcassets/cameraOff.imageset/cameraOff.png -------------------------------------------------------------------------------- /Walkie-Talkie.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Walkie-Talkie.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Walkie-Talkie.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Walkie-Talkie.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/micOff.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "micOff.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/micOn.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "micOn.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/cameraOn.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "cameraOn.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/soundOff.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "soundOff.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/soundOn.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "soundOn.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/cameraOff.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "cameraOff.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/hangup.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "telephone.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/VideoView/VideoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyVideoView.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/6/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | final class VideoView: RTCEAGLVideoView { 13 | var vm: VideoVM! { 14 | didSet { 15 | advise() 16 | } 17 | } 18 | 19 | func advise() { 20 | vm.startRender(view: self) 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | 2 | use_frameworks! 3 | inhibit_all_warnings! 4 | platform :ios, '13.2' 5 | 6 | def ui 7 | pod 'PIPKit' 8 | pod 'IQKeyboardManagerSwift' 9 | pod 'IHKeyboardAvoiding' 10 | pod 'SnapKit' 11 | end 12 | 13 | def main 14 | pod 'GoogleWebRTC' 15 | pod 'Bond', '6.10.2' 16 | pod 'ReactiveKit' 17 | pod 'SwiftyUserDefaults' 18 | end 19 | 20 | def firebase 21 | pod 'Firebase/Firestore' 22 | end 23 | 24 | 25 | target 'Walkie-Talkie' do 26 | main 27 | ui 28 | firebase 29 | end 30 | 31 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/ReactiveClasses/BackableVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackableVM.swift 3 | // 4 | // 5 | // Created by Alexandr on 7/22/17. 6 | // Copyright © 2017 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import ReactiveKit 10 | 11 | public protocol Backable: BondVMProtocol { 12 | var back: SafePublishSubject<()> { get } 13 | } 14 | 15 | class BackableVM: BondViewModel, Backable { 16 | let back = SafePublishSubject<()>() 17 | 18 | public override init() { 19 | super.init() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/Extensions/Codables/SdpType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SdpType.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/3/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | enum SdpType: String, Codable { 13 | case offer, prAnswer, answer 14 | 15 | var rtcSdpType: RTCSdpType { 16 | switch self { 17 | case .offer: return .offer 18 | case .answer: return .answer 19 | case .prAnswer: return .prAnswer 20 | } 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/UIPageViewController+Bond.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIPageViewController+Bond.swift 3 | // Login 4 | // 5 | // Created by Oleksandr Zaporozhchenko on 2/19/19. 6 | // Copyright © 2019 Oleksandr Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | extension ReactiveExtensions where Base: UIPageViewController { 13 | public var delegate: ProtocolProxy { 14 | return base.reactive.protocolProxy(for: UIPageViewControllerDelegate.self, selector: NSSelectorFromString("setDelegate:")) 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/UITableView+Bond.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Bond.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 1/1/19. 6 | // Copyright © 2018 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | extension UITableView { 13 | var selectedRow: SafeSignal { 14 | return reactive.delegate.signal(for: #selector(UITableViewDelegate.tableView(_:didSelectRowAt:))) { (subject: SafePublishSubject, _: UITableView, indexPath: NSIndexPath) in 15 | subject.next(indexPath.row) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/Extensions/WebRTCClientDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/2/20. 6 | // Copyright © 2020 Stas Seldin. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | protocol WebRTCClientDelegate: class { 13 | func webRTCClient(_ client: WebRTCClient, didDiscoverLocalCandidate candidate: RTCIceCandidate) 14 | func webRTCClient(_ client: WebRTCClient, didChangeConnectionState state: RTCIceConnectionState) 15 | func webRTCClient(_ client: WebRTCClient, didReceiveData data: Data) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/PIP/PIPVideoVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MeVideoVM.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/5/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | final class PIPVideoVM: BondViewModel { 13 | var webRTCClient: WebRTCClient! 14 | var videoVM: VideoVM! 15 | 16 | init(webRTCClient: WebRTCClient, videoSource: VideoSource) { 17 | super.init() 18 | self.webRTCClient = webRTCClient 19 | videoVM = VideoVM(webRTCClient: webRTCClient, videoSource: videoSource) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Walkie-Talkie/Base/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/3/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Firebase 11 | 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | func application(_ application: UIApplication, 17 | didFinishLaunchingWithOptions launchOptions: 18 | [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 19 | FirebaseApp.configure() 20 | return true 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/Extensions/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Config.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/27/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | final class Config { 13 | static let shared = Config() 14 | 15 | var iceServers = ["stun:stun.l.google.com:19302", 16 | "stun:stun1.l.google.com:19302", 17 | "stun:stun2.l.google.com:19302", 18 | "stun:stun3.l.google.com:19302", 19 | "stun:stun4.l.google.com:19302"] 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Encodable+Dictionary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Encodable+Dictionary.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/7/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension Encodable { 13 | func asDictionary() -> [String: Any] { 14 | let data = try! JSONEncoder().encode(self) 15 | guard let dictionary = try! JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [String: Any] else { 16 | return [String: Any]() 17 | } 18 | return dictionary 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/UIButton+ThrottledTap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bond+TapExtensions.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 1/1/19. 6 | // Copyright © 2018 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import ReactiveKit 10 | import UIKit 11 | 12 | public extension ReactiveExtensions where Base: UIButton { 13 | var throttledTap: SafeSignal<()> { 14 | return tap.throttle(seconds: 0.5) 15 | } 16 | } 17 | 18 | public extension ReactiveExtensions where Base: UIBarButtonItem { 19 | var throttledTap: SafeSignal<()> { 20 | return tap.throttle(seconds: 0.5) 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/ReactiveClasses/BaseServiceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseServiceError.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/5/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public enum BaseServiceError: Error { 13 | case unknown 14 | case serverBaseURLisNotConfigurated 15 | case unAuthorized 16 | case cantCreateURLComponents 17 | case cantCreateURLFromComponents 18 | case noFirebaseToken 19 | case errorFromServer(message: String) 20 | case parsingError(message: String) 21 | case dataBaseError 22 | } 23 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/ReactiveClasses/XibLoadedVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XibLoadedVC.swift 3 | // Login 4 | // 5 | // Created by Oleksandr Zaporozhchenko on 2/11/19. 6 | // Copyright © 2019 Oleksandr Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class XibLoadedVC: UIViewController { 12 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 13 | 14 | required init() { 15 | print("init \(type(of: self))") 16 | let bundle = Bundle(for: type(of: self)) 17 | super.init(nibName: String(describing: type(of: self)), bundle: bundle) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/UIViewController+HideKeyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+HideKeyboard.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 6/2/17. 6 | // Copyright © 2017 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | extension UIViewController { 13 | func hideKeyboardWhenTappedAround() { 14 | let tap = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard)) 15 | tap.cancelsTouchesInView = false 16 | view.addGestureRecognizer(tap) 17 | } 18 | 19 | @objc func dismissKeyboard() { 20 | view.endEditing(true) 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/WebRTCService+RTCDataChannelDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebRTCService+RTCDataChannelDelegate.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/28/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | extension WebRTCClient: RTCDataChannelDelegate { 13 | 14 | func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) { 15 | debugPrint("dataChannel did change state: \(dataChannel.readyState)") 16 | } 17 | 18 | func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) { 19 | delegate?.webRTCClient(self, didReceiveData: buffer.data) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Contacts/Cell/ContactCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactCell.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 15.05.2020. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import Bond 10 | 11 | 12 | final class СontactCell: SelectableTableCell { 13 | private var vm: СontactCellVM { 14 | return viewModel as! СontactCellVM 15 | } 16 | 17 | @IBOutlet var info: UILabel! 18 | 19 | override func awakeFromNib() { 20 | super.awakeFromNib() 21 | selectionStyle = .none 22 | } 23 | 24 | override func advise() { 25 | super.advise() 26 | vm.info.bind(to: info.reactive.text).dispose(in: bag) 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Contacts/Cell/ContactCellVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactCellVM.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 15.05.2020. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | import SwiftyUserDefaults 12 | 13 | 14 | final class СontactCellVM: BondViewModel, SelectableProtocol { 15 | let doSelect = SafePublishSubject() 16 | let info = Observable("") 17 | 18 | init(model: String) { 19 | super.init() 20 | info.next(model) 21 | doSelect 22 | .observeNext { 23 | Defaults[\.selesctedId] = model 24 | Router.shared.pop() } 25 | .dispose(in: bag) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/Extensions/Codables/IceCandidate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IceCandidate.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/25/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | struct IceCandidate: Codable { 13 | let sdp: String 14 | let sdpMLineIndex: Int32 15 | let sdpMid: String? 16 | 17 | init(from iceCandidate: RTCIceCandidate) { 18 | self.sdpMLineIndex = iceCandidate.sdpMLineIndex 19 | self.sdpMid = iceCandidate.sdpMid 20 | self.sdp = iceCandidate.sdp 21 | } 22 | 23 | var rtcIceCandidate: RTCIceCandidate { 24 | return RTCIceCandidate(sdp: self.sdp, sdpMLineIndex: self.sdpMLineIndex, sdpMid: self.sdpMid) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/UIStoryboard+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStoryboard+Add.swift 3 | // GPSWIFT 4 | // 5 | // Created by Alexander Zaporozhchenko on 11/27/16. 6 | // Copyright © 2016 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | extension UIStoryboard { 13 | func instantiate() -> T { 14 | let name = String(describing: T.self) 15 | return instantiateViewController(withIdentifier: name) as! T 16 | } 17 | 18 | func instantiate(_ identifier:String) -> T { 19 | return instantiateViewController(withIdentifier: identifier) as! T 20 | } 21 | 22 | func instantiate(_ controller:UIViewController, viewModel:Any) { 23 | return 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Contacts/ContactsVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactsVM.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 14.05.2020. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | import SwiftyUserDefaults 12 | 13 | 14 | final class ContactsVM: BondViewModel { 15 | var items = MutableObservableArray<СontactCellVM>() 16 | 17 | override init() { 18 | super.init() 19 | let ids = Defaults[\.roomIds] 20 | items.replace(with: ids.map { СontactCellVM(model: $0)}) 21 | } 22 | } 23 | 24 | 25 | extension DefaultsKeys { 26 | var roomIds: DefaultsKey<[String]> { .init("roomIds", defaultValue: []) } 27 | var selesctedId: DefaultsKey { .init("selesctedId") } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/ReactiveClasses/BondViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BondViewModel.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 1/1/19. 6 | // Copyright © 2017 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | 13 | public protocol BondVMProtocol: class {} 14 | 15 | public class BondViewModel: NSObject, BondVMProtocol { 16 | let errors = SafePublishSubject() 17 | 18 | public override init() { 19 | super.init() 20 | 21 | print("init \(type(of: self))") 22 | 23 | errors 24 | .observeNext { value in 25 | print("BondViewModel error ", value) 26 | // Router.shared.showAlert(message: String(describing: value)) 27 | } 28 | .dispose(in: bag) 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Call/CallVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CallVM.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/25/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import ReactiveKit 10 | import Bond 11 | import WebRTC 12 | 13 | 14 | final class CallVM: BondViewModel { 15 | var webRTCClient: WebRTCClient! 16 | var settingsVM: SettingsVM! 17 | var videoVM: VideoVM! 18 | var meVideoVM: PIPVideoVM! 19 | 20 | init(webRTCClient: WebRTCClient) { 21 | super.init() 22 | settingsVM = SettingsVM(webRTCClient: webRTCClient) 23 | videoVM = VideoVM(webRTCClient: webRTCClient, videoSource: .remote) 24 | meVideoVM = PIPVideoVM(webRTCClient: webRTCClient, videoSource: VideoSource(localVideoSource: webRTCClient.localVideoSource)) 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/UITextField+Bond.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITextField+Bond.swift 3 | // Login 4 | // 5 | // Created by Oleksandr Zaporozhchenko on 3/19/19. 6 | // Copyright © 2019 Oleksandr Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | 13 | extension ReactiveExtensions where Base: UITextField { 14 | public var delegate: ProtocolProxy { 15 | return base.reactive.protocolProxy(for: UITextFieldDelegate.self, 16 | selector: NSSelectorFromString("setDelegate:")) 17 | } 18 | } 19 | 20 | 21 | extension UITextField { 22 | var textShouldBeginEditing: SafeSignal { 23 | return reactive.delegate.signal(for: #selector(UITextFieldDelegate.textFieldShouldBeginEditing(_:))) { (subject: SafePublishSubject, _: UITextField) in 24 | subject.next(false) 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.swp 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | CocoaPods 31 | Pods/ 32 | 33 | # fastlane 34 | # 35 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 36 | # screenshots whenever they are needed. 37 | # For more information about the recommended setup visit: 38 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 39 | 40 | fastlane/report.xml 41 | fastlane/screenshots -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/VideoClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoClient.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/7/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | final class VideoClient { 13 | private let webRTCService: WebRTCClient! 14 | 15 | init(webRTCService: WebRTCClient) { 16 | self.webRTCService = webRTCService 17 | } 18 | 19 | //MARK: - Public 20 | 21 | public func offVideo() { 22 | setVideoEnabled(false) 23 | } 24 | 25 | public func onVideo() { 26 | setVideoEnabled(true) 27 | } 28 | 29 | //MARK: - Private 30 | 31 | private func setVideoEnabled(_ isEnabled: Bool) { 32 | let audioTracks = webRTCService.peerConnection.transceivers.compactMap { return $0.sender.track as? RTCVideoTrack } 33 | audioTracks.forEach { $0.isEnabled = isEnabled } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/PIP/PIPVideoVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PIPVideoVC.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/5/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PIPKit 11 | 12 | 13 | final class PIPVideoVC: BondVC, PIPUsable { 14 | var vm: PIPVideoVM { 15 | return viewModel as! PIPVideoVM 16 | } 17 | 18 | @IBOutlet var video: VideoView! 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | navigationController?.navigationBar.isHidden = true 23 | advise() 24 | hideKeyboardWhenTappedAround() 25 | } 26 | 27 | override func advise() { 28 | super.advise() 29 | video.vm = vm.videoVM 30 | } 31 | 32 | var initialPosition: PIPPosition { 33 | return .topRight 34 | } 35 | 36 | var pipSize: CGSize { 37 | return .init(width: 100, height: 100) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/Extensions/Codables/SessionDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionDescription.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/25/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | struct SessionDescription: Codable { 13 | let sdp: String 14 | let type: SdpType 15 | 16 | init(from rtcSessionDescription: RTCSessionDescription) { 17 | self.sdp = rtcSessionDescription.sdp 18 | 19 | switch rtcSessionDescription.type { 20 | case .offer: self.type = .offer 21 | case .prAnswer: self.type = .prAnswer 22 | case .answer: self.type = .answer 23 | @unknown default: 24 | fatalError("Unknown RTCSessionDescription type: \(rtcSessionDescription.type.rawValue)") 25 | } 26 | } 27 | 28 | var rtcSessionDescription: RTCSessionDescription { 29 | return RTCSessionDescription(type: self.type.rtcSdpType, sdp: self.sdp) 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/JoinView/JoinView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JoinView.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/6/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import Bond 10 | 11 | 12 | final class JoinView: BondView { 13 | var vm: JoinVM { 14 | return viewModel as! JoinVM 15 | } 16 | @IBOutlet var create: UIButton! 17 | @IBOutlet var join: UIButton! 18 | @IBOutlet var roomID: UITextField! 19 | 20 | override func advise() { 21 | super.advise() 22 | 23 | create.reactive.tap.bind(to: vm.create).dispose(in: bag) 24 | join.reactive.tap.bind(to: vm.join).dispose(in: bag) 25 | 26 | roomID.reactive.text.ignoreNils().bind(to: vm.roomID).dispose(in: bag) 27 | vm.roomID.bind(to: roomID.reactive.text).dispose(in: bag) 28 | 29 | vm.isButtonsEnabled.bind(to: create.reactive.isEnabled).dispose(in: bag) 30 | vm.isButtonsEnabled.bind(to: join.reactive.isEnabled).dispose(in: bag) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/UIDocumentPickerViewController+Bond.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDocumentPickerViewController+Bond.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 1/1/19. 6 | // Copyright © 2018 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | //extension ReactiveExtensions where Base: UIDocumentPickerViewController { 13 | // public var delegate: ProtocolProxy { 14 | // return base.reactive.protocolProxy(for: UIDocumentPickerDelegate.self, selector: NSSelectorFromString("setDelegate:")) 15 | // } 16 | //} 17 | 18 | //extension UIDocumentPickerViewController { 19 | // var pickedAtURL: SafeSignal<[URL]> { 20 | // if #available(iOS 11.0, *) { 21 | // return reactive.delegate.signal(for: #selector(UIDocumentPickerDelegate.documentPicker(_:didPickDocumentsAt:))) { (subject: SafePublishSubject<[URL]>, _: UIDocumentPickerViewController, urls: [URL]) in 22 | // subject.next(urls) 23 | // } 24 | // } else { 25 | // // Fallback on earlier versions 26 | // } 27 | // } 28 | //} 29 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/UISearchBar+Bond.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UISearchBar+Bond.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 1/1/19. 6 | // Copyright © 2018 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | extension ReactiveExtensions where Base: UISearchBar { 13 | public var delegate: ProtocolProxy { 14 | return base.reactive.protocolProxy(for: UISearchBarDelegate.self, 15 | selector: NSSelectorFromString("setDelegate:")) 16 | } 17 | } 18 | 19 | extension UISearchBar { 20 | var textChanged: SafeSignal { 21 | return reactive.delegate.signal(for: #selector(UISearchBarDelegate.searchBar(_:textDidChange:))) { (subject: SafePublishSubject, _: UISearchBar, searchText: NSString) in 22 | subject.next(searchText as String) 23 | } 24 | } 25 | 26 | var textBeginEdited: SafeSignal<()> { 27 | return reactive.delegate.signal(for: #selector(UISearchBarDelegate.searchBarTextDidBeginEditing(_:))) { (subject: SafePublishSubject<()>, _: UISearchBar) in 28 | subject.next(()) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/ReactiveClasses/SelectableTableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectableTableCell.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 1/1/19. 6 | // Copyright © 2017 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | public protocol SelectableProtocol: BondVMProtocol { 13 | var doSelect: SafePublishSubject<()> { get } 14 | } 15 | 16 | class SelectableTableCell: BondTableCell { 17 | let doSelect = SafePublishSubject<()>() 18 | 19 | var selectableVM: SelectableProtocol { 20 | return viewModel as! SelectableProtocol 21 | } 22 | 23 | override func awakeFromNib() { 24 | super.awakeFromNib() 25 | let tap = UITapGestureRecognizer(target: self, action: #selector(gestureAction)) 26 | self.addGestureRecognizer(tap) 27 | } 28 | 29 | @objc func gestureAction() { 30 | doSelect.next() 31 | } 32 | 33 | override func advise() { 34 | super.advise() 35 | doSelect 36 | .bind(to: selectableVM.doSelect) 37 | .dispose(in: bag) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zaporozhchenko Oleksandr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/ReactiveClasses/BondTableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BondTableCell.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 1/1/19. 6 | // Copyright © 2017 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | class BondTableCell: UITableViewCell { 13 | 14 | override func prepareForReuse() { 15 | bag.dispose() 16 | advise() 17 | } 18 | 19 | private var model: BondVMProtocol? 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | super.init(coder: aDecoder) 23 | } 24 | 25 | var viewModel: BondVMProtocol? { 26 | get { 27 | return model 28 | } 29 | set(newViewModel) { 30 | if model !== newViewModel { 31 | unadvise() 32 | model = newViewModel 33 | if model != nil { 34 | advise() 35 | } 36 | } 37 | } 38 | } 39 | 40 | deinit { 41 | viewModel = nil 42 | } 43 | 44 | // called to bind needed for cell 45 | func advise() {} 46 | 47 | // called to dispose binds needed for cell 48 | func unadvise() { 49 | bag.dispose() 50 | let views = subviews.compactMap { $0 as? BondView } 51 | for view in views { 52 | view.viewModel = nil 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Main/Animations/MainVC+UINavigationDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainVC+UINavigationDelegate.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/9/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | extension MainVC: UINavigationControllerDelegate { 13 | 14 | func navigationController(_ navigationController: UINavigationController, 15 | animationControllerFor operation: UINavigationController.Operation, 16 | from fromVC: UIViewController, 17 | to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { 18 | 19 | switch operation { 20 | case .push: 21 | guard fromVC is MainVC && toVC is CallVC else { 22 | return nil 23 | } 24 | return MainToCallAnimator() 25 | case .pop: 26 | guard fromVC is CallVC && toVC is MainVC else { 27 | return nil 28 | } 29 | return CallToMainAnimator() 30 | default: 31 | return nil 32 | } 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/ReactiveClasses/BondTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BondTableView.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 1/1/19. 6 | // Copyright © 2018 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | 11 | class BondTableView: UITableView { 12 | private var _viewModel: BondVMProtocol? 13 | 14 | required init?(coder aDecoder: NSCoder) { 15 | super.init(coder: aDecoder) 16 | } 17 | 18 | override init(frame: CGRect, style: UITableView.Style) { 19 | super.init(frame: frame, style: style) 20 | } 21 | 22 | var viewModel: BondVMProtocol? { 23 | get { 24 | return _viewModel 25 | } 26 | set(newViewModel) { 27 | if _viewModel !== newViewModel { 28 | unadvise() 29 | _viewModel = newViewModel 30 | if _viewModel != nil { 31 | advise() 32 | } 33 | } 34 | } 35 | } 36 | 37 | deinit { 38 | viewModel = nil 39 | } 40 | 41 | // called to dispose binds needed for view 42 | 43 | func unadvise() { 44 | bag.dispose() 45 | let views = subviews.compactMap { $0 as? BondView } 46 | for view in views { 47 | view.viewModel = nil 48 | } 49 | } 50 | 51 | // called to bind needed for view 52 | 53 | func advise() {} 54 | } 55 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Main/Animations/BaseAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseAnimator.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/9/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | class BaseAnimator : NSObject, UIViewControllerAnimatedTransitioning { 13 | 14 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 15 | return 1 16 | } 17 | 18 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 19 | 20 | guard 21 | let fromVC = transitionContext.viewController(forKey: .from) as? FromVC, 22 | let toVC = transitionContext.viewController(forKey: .to) as? ToVC 23 | else { 24 | transitionContext.completeTransition(true) 25 | return 26 | } 27 | 28 | self.fromVC = fromVC 29 | self.toVC = toVC 30 | 31 | containerView = transitionContext.containerView 32 | } 33 | 34 | var fromVC: FromVC! 35 | var toVC: ToVC! 36 | var containerView: UIView! 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/UIImagePickerController+Bond.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImagePickerController+Bond.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 16.05.2020. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | 13 | extension ReactiveExtensions where Base: UIImagePickerController { 14 | public var delegate: ProtocolProxy { 15 | return base.reactive.protocolProxy(for: UIImagePickerControllerDelegate.self, selector: NSSelectorFromString("setDelegate:")) 16 | } 17 | } 18 | 19 | extension UIImagePickerController { 20 | var pickedWithInfo: SafeSignal<[UIImagePickerController.InfoKey : Any]> { 21 | let delegateSelector = #selector(UIImagePickerControllerDelegate.imagePickerController(_:didFinishPickingMediaWithInfo:)) 22 | return reactive 23 | .delegate 24 | .signal(for: delegateSelector) { ( 25 | subject: SafePublishSubject<[UIImagePickerController.InfoKey : Any]>, 26 | picker: UIImagePickerController, 27 | info: [UIImagePickerController.InfoKey : Any] 28 | ) in 29 | 30 | subject.next(info) 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/UIScrollView+Bond.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+Bond.swift 3 | // 4 | // 5 | // Created by Alexandr on 1/1/19. 6 | // Copyright © 2017 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | extension ReactiveExtensions where Base: UIScrollView { 13 | public var delegate: ProtocolProxy { 14 | return base.reactive.protocolProxy(for: UIScrollViewDelegate.self, selector: NSSelectorFromString("setDelegate:")) 15 | } 16 | } 17 | 18 | extension UIScrollView { 19 | var scrolledY: SafeSignal { 20 | return reactive.delegate.signal(for: #selector(UIScrollViewDelegate.scrollViewDidScroll(_:))) { (subject: SafePublishSubject, scrollview: UIScrollView) in 21 | subject.next(Float(scrollview.contentOffset.y)) 22 | } 23 | } 24 | 25 | var currentPage: SafeSignal { 26 | return reactive.delegate.signal(for: #selector(UIScrollViewDelegate.scrollViewDidEndDecelerating(_:))) { 27 | (subject: SafePublishSubject, scrollview: UIScrollView) in 28 | let rounded = round(scrollview.contentOffset.x / scrollview.frame.size.width) 29 | let page = Int(rounded) 30 | subject.next(page) 31 | } 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Main/MainVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainVM.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/7/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import ReactiveKit 10 | 11 | 12 | final class MainVM: BondViewModel { 13 | var webRTCClient: WebRTCClient! 14 | var joinVM: JoinVM! 15 | var videoVM: VideoVM! 16 | 17 | let selectSource = SafePublishSubject() 18 | let selectID = SafePublishSubject() 19 | 20 | override init() { 21 | super.init() 22 | webRTCClient = WebRTCClient(iceServers: Config.shared.iceServers) 23 | joinVM = JoinVM(webRTCClient: webRTCClient) 24 | let startingVideoSource = VideoSource.localFile(name: "cat.mp4") 25 | videoVM = VideoVM(webRTCClient: webRTCClient, videoSource: startingVideoSource) 26 | 27 | selectSource 28 | .observeNext { [weak self] _ in 29 | guard let me = self else { return } 30 | Router.shared.showSource(webRTCClient: me.webRTCClient) 31 | } 32 | .dispose(in: bag) 33 | 34 | selectID 35 | .observeNext { _ in 36 | Router.shared.showContacts() 37 | } 38 | .dispose(in: bag) 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Main/Animations/UIView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extension.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/9/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | extension UIView { 13 | func add(_ subviews: UIView...) { 14 | subviews.forEach(addSubview) 15 | } 16 | 17 | func add(_ subviews: [UIView]) { 18 | subviews.forEach(addSubview) 19 | } 20 | 21 | 22 | func remove(_ subviews: UIView...) { 23 | subviews.forEach { $0.removeFromSuperview() } 24 | } 25 | 26 | func remove(_ subviews: [UIView]) { 27 | subviews.forEach { $0.removeFromSuperview() } 28 | } 29 | 30 | 31 | func frameOfViewInWindowsCoordinateSystem() -> CGRect { 32 | if let superview = superview { 33 | return superview.convert(frame, to: nil) 34 | } 35 | print("! view is not in hierarchy: \n \(self)\n") 36 | return frame 37 | } 38 | } 39 | 40 | extension UIView { 41 | func createAndOverlapWithSnapshot(afterScreenUpdates: Bool) -> UIView? { 42 | let view = snapshotView(afterScreenUpdates: afterScreenUpdates) 43 | view?.frame = frameOfViewInWindowsCoordinateSystem() 44 | return view 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Walkie-Talkie/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 874494726329-1gtrr812ttnbgkc7c89dsh6q60snrn1u.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.874494726329-1gtrr812ttnbgkc7c89dsh6q60snrn1u 9 | API_KEY 10 | AIzaSyAh7VO2JTnexj0of7mXGHQ6bt0Kvcm-SVs 11 | GCM_SENDER_ID 12 | 874494726329 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | com.maxatma.walkie-talkie 17 | PROJECT_ID 18 | walkie-talkie-b01d7 19 | STORAGE_BUCKET 20 | walkie-talkie-b01d7.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:874494726329:ios:ea5afd4cdd5f55249d4b68 33 | DATABASE_URL 34 | https://walkie-talkie-b01d7.firebaseio.com 35 | 36 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Main/MainVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainVC.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/7/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import IHKeyboardAvoiding 11 | import SwiftyUserDefaults 12 | 13 | 14 | final class MainVC: BondVC { 15 | var vm: MainVM { 16 | return viewModel as! MainVM 17 | } 18 | 19 | @IBOutlet var myVideo: WebRTCView! 20 | @IBOutlet var join: JoinView! 21 | @IBOutlet var source: UIButton! 22 | @IBOutlet var selectID: UIButton! 23 | 24 | override func viewDidLoad() { 25 | navigationController?.delegate = self 26 | super.viewDidLoad() 27 | navigationController?.navigationBar.isHidden = true 28 | hideKeyboardWhenTappedAround() 29 | advise() 30 | KeyboardAvoiding.avoidingView = join 31 | } 32 | 33 | override func viewWillAppear(_ animated: Bool) { 34 | super.viewWillAppear(animated) 35 | vm.joinVM.roomID.send(Defaults[\.selesctedId] ?? "") 36 | } 37 | 38 | override func advise() { 39 | super.advise() 40 | myVideo.videoView.vm = vm.videoVM 41 | join.viewModel = vm.joinVM 42 | source.reactive.tap.bind(to: vm.selectSource).dispose(in: bag) 43 | selectID.reactive.tap.bind(to: vm.selectID).dispose(in: bag) 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Source/SourceVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceVM.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 15.05.2020. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import ReactiveKit 10 | import Bond 11 | import WebRTC 12 | 13 | 14 | final class SourceVM: BondViewModel { 15 | let camera = SafePublishSubject() 16 | let library = SafePublishSubject() 17 | let selectDefault = SafePublishSubject() 18 | var webRTCClient: WebRTCClient! 19 | let selectInLibrary = SafePublishSubject() 20 | 21 | init(webRTCClient: WebRTCClient) { 22 | super.init() 23 | self.webRTCClient = webRTCClient 24 | 25 | camera 26 | .observeNext { _ in 27 | webRTCClient.change(localVideoSource: .camera) 28 | Router.shared.pop() 29 | } 30 | .dispose(in: bag) 31 | 32 | selectInLibrary 33 | .observeNext { name in 34 | webRTCClient.change(localVideoSource: .file(name: name)) 35 | Router.shared.pop() 36 | } 37 | .dispose(in: bag) 38 | 39 | 40 | selectDefault 41 | .observeNext { _ in 42 | webRTCClient.change(localVideoSource: .file(name: "cat.mp4")) 43 | Router.shared.pop() 44 | } 45 | .dispose(in: bag) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/5/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import Bond 10 | 11 | 12 | final class SettingsView: BondView { 13 | var vm: SettingsVM { 14 | return viewModel as! SettingsVM 15 | } 16 | @IBOutlet var microphone: OnOffButton! 17 | @IBOutlet var sound: OnOffButton! 18 | @IBOutlet var video: OnOffButton! 19 | @IBOutlet var hangup: UIButton! 20 | 21 | override func advise() { 22 | super.advise() 23 | microphone.isOn.bind(to: vm.microphone).dispose(in: bag) 24 | sound.isOn.bind(to: vm.sound).dispose(in: bag) 25 | video.isOn.bind(to: vm.video).dispose(in: bag) 26 | hangup.reactive.tap.bind(to: vm.hangup).dispose(in: bag) 27 | } 28 | } 29 | 30 | 31 | @IBDesignable 32 | final class OnOffButton: BondButton { 33 | @IBInspectable var on: UIImage! 34 | @IBInspectable var off: UIImage! 35 | 36 | let isOn = Observable(true) 37 | 38 | override func awakeFromNib() { 39 | super.awakeFromNib() 40 | reactive.tap.map { [unowned self] _ in !self.isOn.value }.bind(to: isOn).dispose(in: bag) 41 | isOn.map { [unowned self] value in value ? self.on : self.off }.bind(to: reactive.image).dispose(in: bag) 42 | } 43 | 44 | override func advise() { 45 | super.advise() 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/VideoView/VideoVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyVideoVM.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/6/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | final class VideoVM: BondViewModel { 13 | var webRTCClient: WebRTCClient! 14 | var videoSource: VideoSource! 15 | 16 | init(webRTCClient: WebRTCClient, videoSource: VideoSource) { 17 | super.init() 18 | self.webRTCClient = webRTCClient 19 | self.videoSource = videoSource 20 | } 21 | 22 | func startRender(view: RTCVideoRenderer) { 23 | switch videoSource! { 24 | case .remote: 25 | webRTCClient.renderRemoteVideo(to: view) 26 | case .localCamera: 27 | webRTCClient.startCaptureLocalCameraVideo(renderer: view) 28 | case let .localFile(name): 29 | webRTCClient.startCaptureLocalVideoFile(name: name, renderer: view) 30 | } 31 | } 32 | } 33 | 34 | 35 | enum VideoSource { 36 | case remote 37 | case localCamera 38 | case localFile(name: String) 39 | 40 | init(localVideoSource: WebRTCClient.LocalVideoSource) { 41 | switch localVideoSource { 42 | case .camera: 43 | self = .localCamera 44 | case let .file(name): 45 | self = .localFile(name: name) 46 | 47 | default: 48 | self = .localCamera 49 | } 50 | 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/ReactiveClasses/BondCollectionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BondCollectionCell.swift 3 | // Login 4 | // 5 | // Created by Oleksandr Zaporozhchenko on 2/4/19. 6 | // Copyright © 2019 Oleksandr Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import ReactiveKit 11 | 12 | class BondCollectionCell: UICollectionViewCell { 13 | let onReuseBag = DisposeBag() 14 | 15 | override func prepareForReuse() { 16 | onReuseBag.dispose() 17 | } 18 | 19 | private var model: BondVMProtocol? 20 | 21 | required override init(frame: CGRect) { 22 | super.init(frame: frame) 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | super.init(coder: aDecoder) 27 | } 28 | 29 | var viewModel: BondVMProtocol? { 30 | get { 31 | return model 32 | } 33 | set(newViewModel) { 34 | if model !== newViewModel { 35 | unadvise() 36 | model = newViewModel 37 | if model != nil { 38 | advise() 39 | } 40 | } 41 | } 42 | } 43 | 44 | deinit { 45 | viewModel = nil 46 | } 47 | 48 | // called to bind needed for cell 49 | func advise() {} 50 | 51 | // called to dispose binds needed for cell 52 | func unadvise() { 53 | bag.dispose() 54 | let views = subviews.compactMap { $0 as? BondView } 55 | for view in views { 56 | view.viewModel = nil 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Contacts/ContactsVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContactsVC.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 14.05.2020. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | final class ContactsVC: BondVC { 13 | var vm: ContactsVM { 14 | return viewModel as! ContactsVM 15 | } 16 | 17 | @IBOutlet var table: UITableView! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | navigationController?.navigationBar.isHidden = true 22 | advise() 23 | configureTable() 24 | } 25 | 26 | override func viewWillAppear(_ animated: Bool) { 27 | super.viewWillAppear(animated) 28 | navigationController?.navigationBar.isHidden = false 29 | } 30 | 31 | override func viewWillDisappear(_ animated: Bool) { 32 | super.viewWillDisappear(animated) 33 | navigationController?.navigationBar.isHidden = true 34 | } 35 | 36 | override func advise() { 37 | super.advise() 38 | } 39 | 40 | private func configureTable() { 41 | table.registerNibsFor(classes: [СontactCell.self]) 42 | 43 | vm.items 44 | .bind(to: table) { vms, indexPath, tableView in 45 | let cell = tableView.dequeueReusableCell(withIdentifier: "СontactCell", for: indexPath) as! СontactCell 46 | cell.viewModel = vms[indexPath.row] 47 | return cell 48 | } 49 | .dispose(in: bag) 50 | 51 | table.tableFooterView = UIView() 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/RegisterNibsExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegisterNibsExtension.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 1/1/19. 6 | // Copyright 2017 . All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | extension UITableView { 13 | 14 | func scrollToTop() { 15 | self.setContentOffset(CGPoint(x: 0, y: 0), animated: true) 16 | } 17 | 18 | func scrollToBot() { 19 | 20 | let lastSection = self.numberOfSections - 1 21 | guard lastSection >= 0 else { return } 22 | 23 | let lastRow = self.numberOfRows(inSection: lastSection) - 1 24 | guard lastRow >= 0 else { return } 25 | 26 | let path = IndexPath(row: lastRow, section: lastSection) 27 | 28 | DispatchQueue.main.async(execute: { () -> Void in 29 | self.scrollToRow(at: path as IndexPath, at: .top, animated: false) 30 | }) 31 | } 32 | 33 | func registerNibsFor(classes: [AnyClass]) { 34 | classes.forEach { (cellClass) in 35 | let cellId = String(describing: cellClass) 36 | let bundle = Bundle(for: cellClass) 37 | let nib = UINib(nibName: cellId, bundle: bundle) 38 | self.register(nib, forCellReuseIdentifier: cellId) 39 | } 40 | } 41 | } 42 | 43 | extension UICollectionView { 44 | 45 | func registerNibsFor(classes: [AnyClass]) { 46 | classes.forEach { (cellClass) in 47 | let cellId = String(describing: cellClass) 48 | let bundle = Bundle(for: cellClass) 49 | let nib = UINib(nibName: cellId, bundle: bundle) 50 | self.register(nib, forCellWithReuseIdentifier: cellId) 51 | } 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Settings/SettingsVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsVM.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/5/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import ReactiveKit 10 | import Bond 11 | import WebRTC 12 | 13 | 14 | final class SettingsVM: BondViewModel { 15 | var webRTCClient: WebRTCClient! 16 | 17 | let microphone = SafePublishSubject() 18 | let sound = SafePublishSubject() 19 | let video = SafePublishSubject() 20 | let hangup = SafePublishSubject() 21 | 22 | init(webRTCClient: WebRTCClient) { 23 | super.init() 24 | self.webRTCClient = webRTCClient 25 | let audioClient = AudioClient(webRTCService: webRTCClient) 26 | let videoClient = VideoClient(webRTCService: webRTCClient) 27 | 28 | microphone.observeNext { isOn in 29 | print("microphone on: ", isOn) 30 | isOn ? audioClient.unmuteAudio() : audioClient.muteAudio() 31 | } 32 | .dispose(in: bag) 33 | 34 | sound.observeNext { isOn in 35 | print("sound on: ", isOn) 36 | isOn ? audioClient.speakerOn() : audioClient.speakerOff() 37 | } 38 | .dispose(in: bag) 39 | 40 | video.observeNext { isOn in 41 | print("video on: ", isOn) 42 | isOn ? videoClient.onVideo() : videoClient.offVideo() 43 | } 44 | .dispose(in: bag) 45 | 46 | 47 | hangup.observeNext { _ in 48 | print("hang up call") 49 | webRTCClient.hangup() 50 | Router.shared.pop() 51 | webRTCClient.startCall() 52 | } 53 | .dispose(in: bag) 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Walkie-Talkie 2 | 3 | Video Calls Application that implements WebRTC power for video calls. 4 | 5 | 6 |

7 | 8 | 9 |

10 | 11 | 12 | ## Technologies used: 13 | 14 | - Language: Swift 5 15 | - Main Architectural pattern: MVVM + Router 16 | - Dependency manager: CocoaPods 17 | - Network: 18 | WebRTC: GoogleWebRTC 19 | Signaling: Firebase/Firestore 20 | - UI: Xib, Autolayout, UIStackView 21 | - Reactive Programming: ReactiveKit + Bond 22 | - Object JSON Mapping: Codable 23 | 24 | 25 | ## Functionality description: 26 | 27 | - Create room by entering ID and pressing "create" 28 | - Provide you ID to callee and make him enter ID and press "join" 29 | - Choose source for video stream - camera or default videofile 30 | - Choose previous room IDs you have used 31 | - Your video goes in picture-in-picture 32 | - Tap to hide all but caller video 33 | - Press settings buttons for mic/sound/video on/off 34 | - Enjoy video call 35 | 36 | 37 | You can use WebRTCClient and SignalingClient classes to make your own video call project. To do that create Firebase project and put your GoogleService-Info.plist file into it. 38 | Don't forget to call FirebaseApp.configure() in AppDelegate 39 | 40 | ## Other platforms: 41 | 42 | Android version https://github.com/paulzin/webrtc-demo 43 | 44 | Web Code: https://github.com/webrtc/FirebaseRTC/blob/master/public/app.js 45 | 46 | Web sample: https://walkie-talkie-b01d7.web.app 47 | 48 | 49 | ## Contacts 50 | 51 | Oleksandr Zaporozhchenko 52 | [[github]](https://github.com/Maxatma) [[gmail]](mailto:maxatma.ids@gmail.com) [[fb]](https://www.facebook.com/profile.php?id=100008291260780) [[in]](https://www.linkedin.com/in/maxatma/) 53 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/UIApplication+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 6/26/17. 6 | // Copyright © 2017 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | extension UIApplication { 13 | 14 | public static func getVisibleViewControllerFrom(vc: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? { 15 | if let nc = vc as? UINavigationController { 16 | return UIApplication.getVisibleViewControllerFrom(vc: nc.visibleViewController) 17 | } 18 | else if let tc = vc as? UITabBarController { 19 | return UIApplication.getVisibleViewControllerFrom(vc: tc.selectedViewController) 20 | } 21 | else { 22 | if let pvc = vc?.presentedViewController { 23 | return UIApplication.getVisibleViewControllerFrom(vc:pvc) 24 | } else { 25 | return vc 26 | } 27 | } 28 | } 29 | 30 | class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? { 31 | if let nav = base as? UINavigationController { 32 | return topViewController(base: nav.visibleViewController) 33 | } 34 | if let tab = base as? UITabBarController { 35 | if let selected = tab.selectedViewController { 36 | return topViewController(base: selected) 37 | } 38 | } 39 | if let presented = base?.presentedViewController { 40 | return topViewController(base: presented) 41 | } 42 | return base 43 | } 44 | 45 | class func topView()->UIView? { 46 | var view = topViewController()?.view 47 | 48 | while view != nil && view?.superview != nil { 49 | view = view!.superview 50 | } 51 | return view 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/ReactiveClasses/BondVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BondVC.swift 3 | // Login 4 | // 5 | // Created by Oleksandr Zaporozhchenko on 2/20/19. 6 | // Copyright © 2019 Oleksandr Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | class BondVC: XibLoadedVC, BondViewProtocol { 13 | var _viewModel: BondVMProtocol? 14 | 15 | var viewModel: BondVMProtocol? { 16 | get { 17 | return _viewModel 18 | } 19 | set(newViewModel) { 20 | if _viewModel !== newViewModel { 21 | if _viewModel != nil { 22 | unadvise() 23 | } 24 | _viewModel = newViewModel 25 | if _viewModel != nil && self.isViewLoaded { 26 | advise() 27 | } 28 | } 29 | } 30 | } 31 | 32 | 33 | deinit { 34 | viewModel = nil 35 | } 36 | 37 | // called to dispose binds needed for view 38 | 39 | func unadvise() { 40 | bag.dispose() 41 | } 42 | 43 | // called to bind needed for view 44 | 45 | func advise() {} 46 | } 47 | 48 | import UIKit 49 | 50 | class BondPageVC: UIPageViewController, BondViewProtocol { 51 | var _viewModel: BondVMProtocol? 52 | 53 | var viewModel: BondVMProtocol? { 54 | get { 55 | return _viewModel 56 | } 57 | set(newViewModel) { 58 | if _viewModel !== newViewModel { 59 | unadvise() 60 | _viewModel = newViewModel 61 | if _viewModel != nil { 62 | advise() 63 | } 64 | } 65 | } 66 | } 67 | 68 | 69 | deinit { 70 | viewModel = nil 71 | } 72 | 73 | // called to dispose binds needed for view 74 | 75 | func unadvise() { 76 | bag.dispose() 77 | } 78 | 79 | 80 | // called to bind needed for view 81 | 82 | func advise() {} 83 | 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Call/CallVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/3/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | import PIPKit 11 | 12 | 13 | final class CallVC: BondVC { 14 | var vm: CallVM { 15 | return viewModel as! CallVM 16 | } 17 | 18 | @IBOutlet var caller: WebRTCView! 19 | @IBOutlet var settings: SettingsView! 20 | 21 | let pipVC = PIPVideoVC() 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | navigationController?.navigationBar.isHidden = true 26 | advise() 27 | PIPKit.show(with: pipVC) 28 | hideViewsWhenTappedAround() 29 | } 30 | 31 | override func viewDidDisappear(_ animated: Bool) { 32 | super.viewDidDisappear(animated) 33 | PIPKit.dismiss(animated: false) 34 | } 35 | 36 | override func advise() { 37 | super.advise() 38 | caller.videoView.vm = vm.videoVM 39 | settings.viewModel = vm.settingsVM 40 | pipVC.viewModel = vm.meVideoVM 41 | } 42 | 43 | @objc override func hideViews() { 44 | settings.alpha = settings.alpha == 0 ? 1 : 0 45 | pipVC.view.alpha = pipVC.view.alpha == 0 ? 1 : 0 46 | } 47 | } 48 | 49 | extension UIViewController { 50 | 51 | func hideViewsWhenTappedAround() { 52 | let tap = UITapGestureRecognizer(target: self, action: #selector(Self.hideViews)) 53 | tap.cancelsTouchesInView = false 54 | tap.numberOfTapsRequired = 1 55 | tap.delegate = self 56 | view.addGestureRecognizer(tap) 57 | } 58 | 59 | @objc func hideViews() { 60 | 61 | } 62 | } 63 | 64 | 65 | extension UIViewController: UIGestureRecognizerDelegate { 66 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 67 | let isControllTapped = touch.view is UIControl 68 | return !isControllTapped 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Walkie-Talkie/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/WebRTCService+RTCPeerConnectionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebRTCService+RTCPeerConnectionDelegate.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/28/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | extension WebRTCClient: RTCPeerConnectionDelegate { 13 | 14 | func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) { 15 | debugPrint("peer connection new signaling state: \(stateChanged)") 16 | } 17 | 18 | func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { 19 | debugPrint("peer connection did add stream") 20 | } 21 | 22 | func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) { 23 | debugPrint("peer connection did remove stream") 24 | } 25 | 26 | func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { 27 | debugPrint("peer connection should negotiate") 28 | } 29 | 30 | func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) { 31 | debugPrint("peer connection new connection state: \(newState)") 32 | delegate?.webRTCClient(self, didChangeConnectionState: newState) 33 | } 34 | 35 | func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) { 36 | debugPrint("peer connection new gathering state: \(newState)") 37 | } 38 | 39 | func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { 40 | delegate?.webRTCClient(self, didDiscoverLocalCandidate: candidate) 41 | } 42 | 43 | func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { 44 | debugPrint("peer connection did remove candidate(s)") 45 | } 46 | 47 | func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { 48 | debugPrint("peer connection did open data channel") 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Source/SourceVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceVC.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 15.05.2020. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | final class SourceVC: BondVC { 13 | var vm: SourceVM { 14 | return viewModel as! SourceVM 15 | } 16 | 17 | @IBOutlet var defaultSource: UIButton! 18 | @IBOutlet var library: UIButton! 19 | @IBOutlet var camera: UIButton! 20 | 21 | private let picker = UIImagePickerController() 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | navigationController?.navigationBar.isHidden = true 26 | configurePicker() 27 | // Can't figure out how to pass video not from main bundle to webrtc library 28 | library.isHidden = true 29 | advise() 30 | } 31 | 32 | private func configurePicker() { 33 | picker.sourceType = .savedPhotosAlbum 34 | picker.mediaTypes = UIImagePickerController.availableMediaTypes(for: .savedPhotosAlbum)! 35 | picker.mediaTypes = ["public.movie"] 36 | picker.allowsEditing = false 37 | } 38 | 39 | override func advise() { 40 | super.advise() 41 | 42 | picker 43 | .pickedWithInfo 44 | .map { ($0[.mediaURL] as! URL).path } 45 | .bind(to: vm.selectInLibrary) 46 | .dispose(in: bag) 47 | 48 | picker 49 | .pickedWithInfo 50 | .observeNext { info in 51 | print("info ", info) 52 | } 53 | .dispose(in: bag) 54 | 55 | vm.library 56 | .observeNext { [weak self] _ in 57 | guard let me = self else { return } 58 | Router.shared.present(vc: me.picker) 59 | } 60 | .dispose(in: bag) 61 | 62 | defaultSource.reactive.tap.bind(to: vm.selectDefault).dispose(in: bag) 63 | library.reactive.tap.bind(to: vm.library).dispose(in: bag) 64 | camera.reactive.tap.bind(to: vm.camera).dispose(in: bag) 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/AudioClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebRTCService+Audio Control.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/28/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | final class AudioClient { 13 | let audioQueue = DispatchQueue(label: "audio") 14 | private let webRTCService: WebRTCClient! 15 | 16 | init(webRTCService: WebRTCClient) { 17 | self.webRTCService = webRTCService 18 | } 19 | 20 | //MARK: - Public 21 | 22 | public func muteAudio() { 23 | setAudioEnabled(false) 24 | } 25 | 26 | public func unmuteAudio() { 27 | setAudioEnabled(true) 28 | } 29 | 30 | public func speakerOn() { 31 | setSpeaker(isOn: true) 32 | } 33 | 34 | public func speakerOff() { 35 | setSpeaker(isOn: false) 36 | } 37 | 38 | //MARK: - Private 39 | 40 | private func setAudioEnabled(_ isEnabled: Bool) { 41 | let audioTracks = webRTCService.peerConnection.transceivers.compactMap { return $0.sender.track as? RTCAudioTrack } 42 | audioTracks.forEach { $0.isEnabled = isEnabled } 43 | } 44 | 45 | private func setSpeaker(isOn: Bool) { 46 | audioQueue.async { [weak self] in 47 | guard let me = self else { 48 | return 49 | } 50 | 51 | me.webRTCService.rtcAudioSession.lockForConfiguration() 52 | // 53 | do { 54 | try me.webRTCService.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue) 55 | // 56 | if isOn { 57 | try me.webRTCService.rtcAudioSession.overrideOutputAudioPort(.speaker) 58 | try me.webRTCService.rtcAudioSession.setActive(true) 59 | } else { 60 | try me.webRTCService.rtcAudioSession.overrideOutputAudioPort(.none) 61 | } 62 | } catch let error { 63 | debugPrint("Error setting AVAudioSession category: \(error)") 64 | } 65 | me.webRTCService.rtcAudioSession.unlockForConfiguration() 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/Extensions/Codables/RTCStates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RTCConnectionState.swift 3 | // WebRTC 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/25/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | extension RTCIceConnectionState: CustomStringConvertible { 13 | public var description: String { 14 | switch self { 15 | case .new: return "new" 16 | case .checking: return "checking" 17 | case .connected: return "connected" 18 | case .completed: return "completed" 19 | case .failed: return "failed" 20 | case .disconnected: return "disconnected" 21 | case .closed: return "closed" 22 | case .count: return "count" 23 | @unknown default: return "Unknown \(self.rawValue)" 24 | } 25 | } 26 | } 27 | 28 | extension RTCSignalingState: CustomStringConvertible { 29 | public var description: String { 30 | switch self { 31 | case .stable: return "stable" 32 | case .haveLocalOffer: return "haveLocalOffer" 33 | case .haveLocalPrAnswer: return "haveLocalPrAnswer" 34 | case .haveRemoteOffer: return "haveRemoteOffer" 35 | case .haveRemotePrAnswer: return "haveRemotePrAnswer" 36 | case .closed: return "closed" 37 | @unknown default: return "Unknown \(self.rawValue)" 38 | } 39 | } 40 | } 41 | 42 | extension RTCIceGatheringState: CustomStringConvertible { 43 | public var description: String { 44 | switch self { 45 | case .new: return "new" 46 | case .gathering: return "gathering" 47 | case .complete: return "complete" 48 | @unknown default: return "Unknown \(self.rawValue)" 49 | } 50 | } 51 | } 52 | 53 | extension RTCDataChannelState: CustomStringConvertible { 54 | public var description: String { 55 | switch self { 56 | case .connecting: return "connecting" 57 | case .open: return "open" 58 | case .closing: return "closing" 59 | case .closed: return "closed" 60 | @unknown default: return "Unknown \(self.rawValue)" 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Call/WebRTCView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebRTCView.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/4/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebRTC 11 | 12 | 13 | final class WebRTCView: UIView, RTCVideoViewDelegate { 14 | let videoView = VideoView(frame: .zero) 15 | var videoSize = CGSize.zero 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | videoView.delegate = self 20 | addSubview(videoView) 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | super.init(coder: coder) 25 | videoView.delegate = self 26 | addSubview(videoView) 27 | } 28 | 29 | func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) { 30 | self.videoSize = size 31 | setNeedsLayout() 32 | } 33 | 34 | override func layoutSubviews() { 35 | super.layoutSubviews() 36 | 37 | guard 38 | videoSize.width > 0 && videoSize.height > 0 39 | else { 40 | videoView.frame = bounds 41 | return 42 | } 43 | 44 | var videoFrame = AVMakeRect(aspectRatio: videoSize, insideRect: bounds) 45 | videoFrame.size.aspectFitSize(in: bounds.size) 46 | videoView.frame = videoFrame 47 | videoView.center = CGPoint(x: bounds.midX, y: bounds.midY) 48 | } 49 | } 50 | 51 | 52 | extension CGSize { 53 | 54 | mutating func aspectFitSize(in container: CGSize) { 55 | let scale = aspectFitScale(in: container) 56 | width = width * CGFloat(scale) 57 | height = height * CGFloat(scale) 58 | } 59 | 60 | func aspectFitScale(in container: CGSize) -> CGFloat { 61 | 62 | if height <= container.height && width > container.width { 63 | return container.width / width 64 | } 65 | if height > container.height && width <= container.width { 66 | return container.height / height 67 | } 68 | 69 | if height > container.height && width > container.width || 70 | height <= container.height && width <= container.width { 71 | return min(container.width / width, container.height / height) 72 | } 73 | 74 | return 1.0 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /Walkie-Talkie/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSCameraUsageDescription 24 | $(PRODUCT_NAME) Requires camera access in order to capture and transmit video 25 | NSMicrophoneUsageDescription 26 | $(PRODUCT_NAME) Requires microphone access in order to capture and transmit audio 27 | UIApplicationSceneManifest 28 | 29 | UIApplicationSupportsMultipleScenes 30 | 31 | UISceneConfigurations 32 | 33 | UIWindowSceneSessionRoleApplication 34 | 35 | 36 | UISceneConfigurationName 37 | Default Configuration 38 | UISceneDelegateClassName 39 | $(PRODUCT_MODULE_NAME).SceneDelegate 40 | 41 | 42 | 43 | 44 | UIBackgroundModes 45 | 46 | voip 47 | 48 | UILaunchStoryboardName 49 | LaunchScreen 50 | UIRequiredDeviceCapabilities 51 | 52 | armv7 53 | 54 | UISupportedInterfaceOrientations 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationLandscapeLeft 58 | UIInterfaceOrientationLandscapeRight 59 | 60 | UISupportedInterfaceOrientations~ipad 61 | 62 | UIInterfaceOrientationPortrait 63 | UIInterfaceOrientationPortraitUpsideDown 64 | UIInterfaceOrientationLandscapeLeft 65 | UIInterfaceOrientationLandscapeRight 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Walkie-Talkie/Base/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/3/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let windowScene = (scene as? UIWindowScene) else { return } 21 | Router.shared.configureAppearance() 22 | Router.shared.makeVisible(windowScene: windowScene) 23 | } 24 | 25 | func sceneDidDisconnect(_ scene: UIScene) { 26 | // Called as the scene is being released by the system. 27 | // This occurs shortly after the scene enters the background, or when its session is discarded. 28 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 29 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 30 | } 31 | 32 | func sceneDidBecomeActive(_ scene: UIScene) { 33 | // Called when the scene has moved from an inactive state to an active state. 34 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 35 | } 36 | 37 | func sceneWillResignActive(_ scene: UIScene) { 38 | // Called when the scene will move from an active state to an inactive state. 39 | // This may occur due to temporary interruptions (ex. an incoming phone call). 40 | } 41 | 42 | func sceneWillEnterForeground(_ scene: UIScene) { 43 | // Called as the scene transitions from the background to the foreground. 44 | // Use this method to undo the changes made on entering the background. 45 | } 46 | 47 | func sceneDidEnterBackground(_ scene: UIScene) { 48 | // Called as the scene transitions from the foreground to the background. 49 | // Use this method to save data, release shared resources, and store enough scene-specific state information 50 | // to restore the scene back to its current state. 51 | } 52 | 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Contacts/ContactsVC.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/PIP/PIPVideoVC.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Walkie-Talkie.xcodeproj/xcshareddata/xcschemes/Walkie-Talkie.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Main/Animations/MainToCallAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainToCallAnimator.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/9/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | final class MainToCallAnimator: BaseAnimator { 13 | 14 | override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 15 | super.animateTransition(using: transitionContext) 16 | 17 | containerView.add(toVC.view) 18 | containerView.sendSubviewToBack(toVC.view) 19 | toVC.view.alpha = 0 20 | toVC.pipVC.view.alpha = 0 21 | 22 | guard 23 | let videoView = fromVC.myVideo?.videoView, 24 | let videoViewSnapshot = videoView.snapshotView(afterScreenUpdates: false) 25 | else { 26 | transitionContext.completeTransition(true) 27 | return 28 | } 29 | 30 | 31 | videoViewSnapshot.frame = videoView.frameOfViewInWindowsCoordinateSystem() 32 | 33 | containerView 34 | .add( 35 | videoViewSnapshot 36 | ) 37 | 38 | let duration = transitionDuration(using: transitionContext) 39 | 40 | UIView 41 | .animateKeyframes(withDuration: duration, 42 | delay: 0, 43 | options: .calculationModeLinear, 44 | animations: { 45 | 46 | UIView.addKeyframe(withRelativeStartTime: 0, 47 | relativeDuration: 0.1, 48 | animations: { [unowned self] in 49 | self.fromVC.view.alpha = 0 50 | }) 51 | 52 | UIView.addKeyframe(withRelativeStartTime: 0, 53 | relativeDuration: 1, 54 | animations: { [unowned self] in 55 | videoViewSnapshot.frame = self.toVC.pipVC.view.frameOfViewInWindowsCoordinateSystem() 56 | }) 57 | 58 | UIView.addKeyframe(withRelativeStartTime: 0, 59 | relativeDuration: 1, 60 | animations: { [unowned self] in 61 | self.toVC.view.alpha = 1 62 | }) 63 | }, 64 | completion: { [unowned self] _ in 65 | self.toVC.pipVC.view.alpha = 1 66 | 67 | self.containerView 68 | .remove( 69 | videoViewSnapshot 70 | ) 71 | 72 | 73 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Contacts/Cell/СontactCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Main/Animations/CallToMainAnimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CallToMainAnimator.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/9/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | final class CallToMainAnimator: BaseAnimator { 13 | 14 | override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 15 | super.animateTransition(using: transitionContext) 16 | 17 | containerView.add(toVC.view) 18 | containerView.sendSubviewToBack(toVC.view) 19 | toVC.view.alpha = 0 20 | toVC.myVideo.alpha = 0 21 | 22 | guard 23 | let videoView = fromVC.pipVC.video, 24 | let videoViewSnapshot = videoView.snapshotView(afterScreenUpdates: false) 25 | else { 26 | transitionContext.completeTransition(true) 27 | return 28 | } 29 | 30 | 31 | videoViewSnapshot.frame = videoView.frameOfViewInWindowsCoordinateSystem() 32 | 33 | containerView 34 | .add( 35 | videoViewSnapshot 36 | ) 37 | fromVC.pipVC.view.alpha = 0 38 | 39 | let duration = transitionDuration(using: transitionContext) 40 | 41 | UIView 42 | .animateKeyframes(withDuration: duration, 43 | delay: 0, 44 | options: .calculationModeLinear, 45 | animations: { 46 | 47 | UIView.addKeyframe(withRelativeStartTime: 0, 48 | relativeDuration: 0.1, 49 | animations: { [unowned self] in 50 | self.fromVC.view.alpha = 0 51 | }) 52 | 53 | UIView.addKeyframe(withRelativeStartTime: 0, 54 | relativeDuration: 1, 55 | animations: { [unowned self] in 56 | videoViewSnapshot.frame = self.toVC.myVideo.videoView.frameOfViewInWindowsCoordinateSystem() 57 | }) 58 | 59 | UIView.addKeyframe(withRelativeStartTime: 0, 60 | relativeDuration: 1, 61 | animations: { [unowned self] in 62 | self.toVC.view.alpha = 1 63 | }) 64 | }, 65 | completion: { [unowned self] _ in 66 | self.toVC.myVideo.alpha = 1 67 | 68 | self.containerView 69 | .remove( 70 | videoViewSnapshot 71 | ) 72 | 73 | 74 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Walkie-Talkie/Helpers/Shared/Bond/ReactiveClasses/BondView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BondView.swift 3 | // 4 | // 5 | // Created by Alexander Zaporozhchenko on 1/1/17. 6 | // Copyright © 2017 Alexander Zaporozhchenko. All rights reserved. 7 | // 8 | 9 | import Bond 10 | import SnapKit 11 | 12 | 13 | class XibLoadedView: UIView { 14 | 15 | func xibSetup() { 16 | let view = self.loadFromNib() 17 | self.addSubview(view) 18 | view.snp.makeConstraints { make -> Void in 19 | make.edges.equalTo(self) 20 | } 21 | setNeedsLayout() 22 | layoutIfNeeded() 23 | } 24 | 25 | var nibname: String { 26 | return String(describing: type(of: self)) 27 | } 28 | 29 | func loadFromNib() -> UIView { 30 | let bundle = Bundle(for: type(of: self)) 31 | let nib = UINib(nibName: nibname, bundle: bundle) 32 | let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView 33 | return view 34 | } 35 | 36 | override init(frame: CGRect) { 37 | super.init(frame: frame) 38 | self.xibSetup() 39 | } 40 | 41 | required init?(coder: NSCoder) { 42 | super.init(coder: coder) 43 | self.xibSetup() 44 | } 45 | } 46 | 47 | class BondView: XibLoadedView, BondViewProtocol { 48 | var _viewModel: BondVMProtocol? 49 | 50 | var viewModel: BondVMProtocol? { 51 | get { 52 | return _viewModel 53 | } 54 | set(newViewModel) { 55 | if _viewModel !== newViewModel { 56 | unadvise() 57 | _viewModel = newViewModel 58 | if _viewModel != nil { 59 | advise() 60 | } 61 | } 62 | } 63 | } 64 | 65 | deinit { 66 | viewModel = nil 67 | } 68 | 69 | // called to dispose binds needed for view 70 | 71 | func unadvise() { 72 | bag.dispose() 73 | let views = subviews.compactMap { $0 as? BondView } 74 | for view in views { 75 | view.viewModel = nil 76 | } 77 | } 78 | 79 | // called to bind needed for view 80 | 81 | func advise() {} 82 | } 83 | 84 | 85 | protocol BondViewProtocol { 86 | var _viewModel: BondVMProtocol? { get set } 87 | var viewModel: BondVMProtocol? { get set } 88 | func advise() 89 | func unadvise() 90 | } 91 | 92 | extension BondViewProtocol where Self: UIView { 93 | var viewModel: BondVMProtocol? { 94 | get { 95 | return _viewModel 96 | } 97 | mutating set(newViewModel) { 98 | if _viewModel !== newViewModel { 99 | unadvise() 100 | _viewModel = newViewModel 101 | if _viewModel != nil { 102 | advise() 103 | } 104 | } 105 | } 106 | } 107 | 108 | func unadvise() { 109 | bag.dispose() 110 | let views = subviews.compactMap { $0 as? BondView } 111 | for view in views { 112 | view.viewModel = nil 113 | } 114 | } 115 | } 116 | 117 | class BondButton: UIButton, BondViewProtocol { 118 | var _viewModel: BondVMProtocol? 119 | 120 | var viewModel: BondVMProtocol? { 121 | get { 122 | return _viewModel 123 | } 124 | set(newViewModel) { 125 | if _viewModel !== newViewModel { 126 | unadvise() 127 | _viewModel = newViewModel 128 | if _viewModel != nil { 129 | advise() 130 | } 131 | } 132 | } 133 | } 134 | 135 | deinit { 136 | viewModel = nil 137 | } 138 | 139 | // called to dispose binds needed for view 140 | 141 | func unadvise() { 142 | bag.dispose() 143 | let views = subviews.compactMap { $0 as? BondView } 144 | for view in views { 145 | view.viewModel = nil 146 | } 147 | } 148 | 149 | // called to bind needed for view 150 | 151 | func advise() {} 152 | } 153 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Source/SourceVC.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 32 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Call/CallVC.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Walkie-Talkie/Base/Router/Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/25/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import ReactiveKit 10 | import Bond 11 | import IQKeyboardManagerSwift 12 | 13 | 14 | public final class Router: NSObject { 15 | 16 | public static let shared = Router() 17 | 18 | var window: UIWindow! 19 | private var rootNavigation: UINavigationController! 20 | 21 | //MARK: - Initialization 22 | 23 | public override init() { 24 | super.init() 25 | 26 | let rootVC = MainVC() 27 | rootVC.viewModel = MainVM() 28 | rootNavigation = UINavigationController(rootViewController: rootVC) 29 | } 30 | 31 | public func configureAppearance() { 32 | let navigationBarAppearace = UINavigationBar.appearance() 33 | navigationBarAppearace.tintColor = .white 34 | navigationBarAppearace.barTintColor = .black 35 | 36 | IQKeyboardManager.shared.enable = true 37 | IQKeyboardManager.shared.toolbarManageBehaviour = .byPosition 38 | IQKeyboardManager.shared.disabledDistanceHandlingClasses = [MainVC.self, CallVC.self] 39 | IQKeyboardManager.shared.toolbarTintColor = .black 40 | } 41 | 42 | public func makeVisible(windowScene: UIWindowScene) { 43 | window = UIWindow() 44 | window!.rootViewController = rootNavigation 45 | window!.windowScene = windowScene 46 | window!.makeKeyAndVisible() 47 | } 48 | 49 | //MARK: - Global 50 | 51 | public func showAlert(title: String? = "Something went wrong", 52 | message: String? = "Try again") { 53 | let alert = UIAlertController(title: title, 54 | message: message, 55 | preferredStyle: .alert) 56 | 57 | let secondAction = UIAlertAction(title: "Okay", 58 | style: .default, 59 | handler: nil) 60 | alert.addAction(secondAction) 61 | self.rootNavigation.viewControllers.first!.present(alert, animated: true, completion: nil) 62 | } 63 | 64 | func pop() { 65 | rootNavigation.popViewController(animated: true) 66 | } 67 | 68 | func present(vc: UIViewController) { 69 | if let topController = UIApplication.topViewController() { 70 | topController.present(vc, animated: true, completion: nil) 71 | } else { 72 | print("no topViewController ") 73 | } 74 | } 75 | 76 | func presentLandscape(vc: UIViewController) { 77 | if UIDevice.current.orientation.isLandscape, !vc.isBeingPresented { 78 | present(vc: vc) 79 | } else { 80 | vc.dismiss(animated: false) 81 | } 82 | } 83 | 84 | func poptoRoot() { 85 | rootNavigation.popToRootViewController(animated: true) 86 | } 87 | 88 | func poptoVC(vcClass: AnyClass) { 89 | 90 | let controllers = rootNavigation.viewControllers 91 | let maybeOurs = controllers.filter { $0.isKind(of: vcClass)}.first 92 | 93 | if let vc = maybeOurs { 94 | rootNavigation.popToViewController(vc, animated: true) 95 | } 96 | } 97 | 98 | func showActivity(_ items: [AnyObject]) { 99 | let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil) 100 | activityVC.excludedActivityTypes = [UIActivity.ActivityType.assignToContact, UIActivity.ActivityType.print] 101 | rootNavigation.present(activityVC, animated: true, completion: nil) 102 | } 103 | 104 | func push(_ controller: UIViewController) { 105 | rootNavigation.navigationBar.isHidden = false 106 | rootNavigation.pushViewController(controller, animated: true) 107 | } 108 | 109 | func dissmissTop() { 110 | if let topController = UIApplication.topViewController() { 111 | topController.dismiss(animated: true, completion: nil) 112 | } 113 | } 114 | 115 | //MARK: - Global Signals 116 | 117 | func popSignal() -> SafeSignal { 118 | return SafeSignal { observer in 119 | self.pop() 120 | observer.next(()) 121 | observer.completed() 122 | return SimpleDisposable() 123 | } 124 | } 125 | 126 | func dissmissTopSignal() -> SafeSignal { 127 | 128 | return SafeSignal { observer in 129 | 130 | if let topController = UIApplication.topViewController() { 131 | topController.dismiss(animated: true, completion: nil) 132 | } 133 | 134 | observer.next(()) 135 | observer.completed() 136 | return SimpleDisposable() 137 | } 138 | } 139 | } 140 | 141 | //MARK: - Routing 142 | 143 | extension Router { 144 | func showCall(vm: CallVM) { 145 | DispatchQueue.main.async { 146 | let vc = CallVC() 147 | vc.viewModel = vm 148 | self.push(vc) 149 | } 150 | } 151 | 152 | func showSource(webRTCClient: WebRTCClient) { 153 | DispatchQueue.main.async { 154 | let vm = SourceVM(webRTCClient: webRTCClient) 155 | let vc = SourceVC() 156 | vc.viewModel = vm 157 | self.push(vc) 158 | } 159 | } 160 | 161 | func showContacts() { 162 | DispatchQueue.main.async { 163 | let vm = ContactsVM() 164 | let vc = ContactsVC() 165 | vc.viewModel = vm 166 | self.push(vc) 167 | } 168 | } 169 | } 170 | 171 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/JoinView/JoinView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 31 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/JoinView/JoinVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JoinVM.swift 3 | // Walkie-Talkie 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 5/6/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import ReactiveKit 10 | import Bond 11 | import WebRTC 12 | import SwiftyUserDefaults 13 | 14 | 15 | final class JoinVM: BondViewModel { 16 | var webRTCClient: WebRTCClient! 17 | var signalingClient: SignalingClient! 18 | let isCreatingRoom = Observable(true) 19 | let state = Observable("") 20 | let create = SafePublishSubject() 21 | let join = SafePublishSubject() 22 | let roomID = Observable("") 23 | let isButtonsEnabled = Observable(false) 24 | 25 | init(webRTCClient: WebRTCClient) { 26 | super.init() 27 | self.webRTCClient = webRTCClient 28 | webRTCClient.delegate = self 29 | signalingClient = SignalingClient.shared 30 | 31 | roomID.map { $0 != nil && $0!.count > 0 }.bind(to: isButtonsEnabled).dispose(in: bag) 32 | 33 | create.map { true }.bind(to: isCreatingRoom).dispose(in: bag) 34 | join.map { false }.bind(to: isCreatingRoom).dispose(in: bag) 35 | 36 | create 37 | .observeNext { [weak self] in 38 | guard let me = self else { return } 39 | 40 | Defaults[\.roomIds].append(me.roomID.value) 41 | let callVM = CallVM(webRTCClient: webRTCClient) 42 | Router.shared.showCall(vm: callVM) 43 | 44 | print("create ") 45 | 46 | me.signalingClient.createRoom() 47 | me.webRTCClient.offer { rtcDescription in 48 | print("me.webRTCClient.offer") 49 | let descr = SessionDescription(from: rtcDescription) 50 | me.signalingClient.createOfferAndSubscribe(desc: descr, id: me.roomID.value) 51 | .observeNext { sessionDescrip in 52 | print("createOfferAndSubscribe next ") 53 | print("got answer sessionDescription ", sessionDescrip) 54 | me.signalingClient 55 | .getRemoteIceCandidates(id: me.roomID.value, name: "calleeCandidates") 56 | .observeNext { candiates in 57 | let rtcCandiates = candiates.map { 58 | $0.rtcIceCandidate 59 | } 60 | rtcCandiates.forEach { 61 | me.webRTCClient.set(remoteCandidate: $0) 62 | } 63 | } 64 | .dispose(in: me.bag) 65 | me.webRTCClient.set(remoteSdp: sessionDescrip.rtcSessionDescription) { error in 66 | if let error = error { 67 | print("createOfferAndSubscribe error set(remoteSdp: ", error) 68 | } 69 | } 70 | } 71 | .dispose(in: me.bag) 72 | } 73 | } 74 | .dispose(in: bag) 75 | 76 | join 77 | .observeNext { [weak self] in 78 | guard let me = self else { return } 79 | print("join ") 80 | 81 | me.signalingClient 82 | .getRemoteIceCandidates(id: me.roomID.value, name: "callerCandidates") 83 | .observeNext { candiates in 84 | print("candidates: ", candiates) 85 | let rtcCandiates = candiates.map { 86 | $0.rtcIceCandidate 87 | } 88 | rtcCandiates.forEach { 89 | me.webRTCClient.set(remoteCandidate: $0) 90 | } 91 | 92 | me.signalingClient 93 | .subscribeForRemoteOffer(roomId: me.roomID.value) 94 | .observeNext { descr in 95 | me.webRTCClient.set(remoteSdp: descr.rtcSessionDescription) { error in 96 | if let error = error { 97 | print("webRTCClient set description error ", error) 98 | } else { 99 | print("webRTCClient set description done! ") 100 | } 101 | } 102 | 103 | me.webRTCClient.answer { descr in 104 | print("descr is ", descr) 105 | me.signalingClient.createAnswer(desc: SessionDescription(from: descr), id: me.roomID.value) 106 | let callVM = CallVM(webRTCClient: webRTCClient) 107 | Router.shared.showCall(vm: callVM) 108 | } 109 | } 110 | .dispose(in: me.bag) 111 | } 112 | .dispose(in: me.bag) 113 | } 114 | .dispose(in: bag) 115 | } 116 | } 117 | 118 | 119 | extension JoinVM: WebRTCClientDelegate { 120 | 121 | func webRTCClient(_ client: WebRTCClient, didDiscoverLocalCandidate candidate: RTCIceCandidate) { 122 | print("WebRTCClientDelegate didDiscoverLocalCandidate ") 123 | print("isCreating ", isCreatingRoom.value) 124 | let candidate = IceCandidate(from: candidate) 125 | 126 | signalingClient.collect(iceCandidate: candidate, 127 | id: roomID.value, 128 | name: isCreatingRoom.value ? "callerCandidates" : "calleeCandidates") 129 | } 130 | 131 | func webRTCClient(_ client: WebRTCClient, didChangeConnectionState state: RTCIceConnectionState) { 132 | self.state.send(state.description) 133 | print("WebRTCClientDelegate didChangeConnectionState ", state) 134 | } 135 | 136 | func webRTCClient(_ client: WebRTCClient, didReceiveData data: Data) { 137 | print("didReceiveData ", data) 138 | } 139 | } 140 | 141 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Settings/SettingsView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 34 | 42 | 50 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Walkie-Talkie/Modules/Main/MainVC.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 39 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Walkie-Talkie/Signaling/SignalingClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignalClient.swift 3 | // WebRTC 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/25/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | import Firebase 11 | import FirebaseFirestore 12 | import ReactiveKit 13 | import Bond 14 | 15 | 16 | final class SignalingClient { 17 | static let shared = SignalingClient() 18 | 19 | //MARK: - CREATE 20 | 21 | func createOfferAndSubscribe(desc: SessionDescription?, id: String) -> SafeSignal { 22 | return createOffer(desc: desc, id: id) 23 | .flatMapLatest { [unowned self] _ in 24 | return self.subscribeForAnswer(id: id) 25 | } 26 | } 27 | 28 | 29 | func createRoom() { 30 | Firestore.firestore() 31 | .collection("rooms") 32 | .addDocument(data: [:]) 33 | } 34 | 35 | private func subscribeForAnswer(id: String) -> SafeSignal { 36 | return Signal { observer in 37 | 38 | Firestore.firestore() 39 | .collection("rooms") 40 | .document(id) 41 | .addSnapshotListener { snapshot, error in 42 | 43 | if let error = error { 44 | print("error is ", error) 45 | // observer.failed(error as! NSError) 46 | return 47 | } 48 | 49 | guard let snapshot = snapshot, snapshot.exists else { 50 | print("no snapshot ") 51 | return 52 | } 53 | 54 | guard let answer = snapshot.get("answer") as? [String: String] else { 55 | print("no answer ") 56 | return 57 | } 58 | 59 | guard let sdp = answer["sdp"] else { 60 | print("no sdp in answer" ) 61 | return 62 | } 63 | 64 | let description = SessionDescription(from: RTCSessionDescription(type: .answer, sdp: sdp)) 65 | observer.next(description) 66 | observer.completed() 67 | } 68 | 69 | return BlockDisposable { } 70 | } 71 | } 72 | 73 | private func createOffer(desc: SessionDescription?, id: String) -> SafeSignal { 74 | return Signal { observer in 75 | let offer = desc.asDictionary() 76 | 77 | Firestore.firestore() 78 | .collection("rooms") 79 | .document(id) 80 | .setData(["offer": offer], 81 | completion: { error in 82 | if let error = error { 83 | print("error is ", error) 84 | //observer.failed(error as! NSError) 85 | return 86 | } 87 | observer.next() 88 | observer.completed() 89 | 90 | }) 91 | 92 | return BlockDisposable { } 93 | } 94 | } 95 | 96 | //MARK: - JOIN 97 | 98 | func createAnswer(desc: SessionDescription?, id: String) { 99 | let answer = desc.asDictionary() 100 | 101 | Firestore.firestore() 102 | .collection("rooms") 103 | .document(id) 104 | .setData(["answer": answer]) 105 | } 106 | 107 | func subscribeForRemoteOffer(roomId: String) ->SafeSignal { 108 | return Signal { observer in 109 | 110 | Firestore.firestore() 111 | .collection("rooms") 112 | .document(roomId) 113 | .addSnapshotListener { snapshot, error in 114 | 115 | if let error = error { 116 | print("error is ", error) 117 | // observer.failed(error as! NSError) 118 | return 119 | } 120 | 121 | guard let snapshot = snapshot, snapshot.exists else { 122 | print("no snapshot ") 123 | return 124 | } 125 | 126 | guard let remoteOffer = snapshot.data()?["offer"] as? [String: Any] else { 127 | print("no offer ") 128 | return 129 | } 130 | 131 | guard let sdp = remoteOffer["sdp"] as? String else { 132 | print("no sdp in offer" ) 133 | return 134 | } 135 | 136 | let description = SessionDescription(from: RTCSessionDescription(type: .offer, sdp: sdp)) 137 | observer.next(description) 138 | observer.completed() 139 | } 140 | 141 | return BlockDisposable { } 142 | } 143 | } 144 | 145 | //MARK: - Candidates 146 | 147 | func collect(iceCandidate: IceCandidate?, id: String, name: String) { 148 | 149 | guard let iceCandidate = iceCandidate else { 150 | return 151 | } 152 | 153 | let ice = [ 154 | "candidate": iceCandidate.sdp, 155 | "sdpMLineIndex": iceCandidate.sdpMLineIndex, 156 | "sdpMid": iceCandidate.sdpMid ?? "0" 157 | ] as [String : Any] 158 | 159 | Firestore.firestore() 160 | .collection("rooms") 161 | .document(id) 162 | .collection(name) 163 | .addDocument(data: ice) 164 | } 165 | 166 | func getRemoteIceCandidates(id: String, name: String) -> SafeSignal<[IceCandidate]> { 167 | return Signal { observer in 168 | Firestore.firestore() 169 | .collection("rooms") 170 | .document(id) 171 | .collection(name) 172 | .addSnapshotListener { snapshot, error in 173 | 174 | if let error = error { 175 | print("error ", error) 176 | return 177 | } 178 | 179 | let dataChanges = snapshot!.documentChanges.filter { $0.type == .added } 180 | 181 | let ices = dataChanges 182 | .map { change -> IceCandidate in 183 | let data = change.document.data() 184 | 185 | return IceCandidate(from: 186 | RTCIceCandidate(sdp: data["candidate"] as! String, 187 | sdpMLineIndex: data["sdpMLineIndex"] as! Int32, 188 | sdpMid: data["sdpMid"] as? String)) 189 | } 190 | 191 | observer.next(ices) 192 | observer.completed() 193 | } 194 | 195 | return BlockDisposable { } 196 | } 197 | } 198 | } 199 | 200 | -------------------------------------------------------------------------------- /Walkie-Talkie/WebRTC/WebRTCClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebRTCService.swift 3 | // WebRTC 4 | // 5 | // Created by Zaporozhchenko Oleksandr on 4/25/20. 6 | // Copyright © 2020 maxatma. All rights reserved. 7 | // 8 | 9 | import WebRTC 10 | 11 | 12 | final class WebRTCClient: NSObject { 13 | 14 | private static let factory: RTCPeerConnectionFactory = { 15 | RTCInitializeSSL() 16 | let videoEncoderFactory = RTCDefaultVideoEncoderFactory() 17 | let codec = RTCVideoCodecInfo(name: "VP8") // this is coz ios 13.3.1 screen is red 18 | videoEncoderFactory.preferredCodec = codec 19 | let videoDecoderFactory = RTCDefaultVideoDecoderFactory() 20 | videoDecoderFactory.createDecoder(codec) 21 | return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory) 22 | }() 23 | 24 | weak var delegate: WebRTCClientDelegate? 25 | 26 | var peerConnection: RTCPeerConnection! 27 | let rtcAudioSession = RTCAudioSession.sharedInstance() 28 | private let mediaConstrains = [kRTCMediaConstraintsOfferToReceiveAudio: kRTCMediaConstraintsValueTrue, 29 | kRTCMediaConstraintsOfferToReceiveVideo: kRTCMediaConstraintsValueTrue] 30 | private var videoCapturer: RTCVideoCapturer? 31 | private var localVideoTrack: RTCVideoTrack? 32 | private var localAudioTrack: RTCAudioTrack? 33 | private var remoteVideoTrack: RTCVideoTrack? 34 | 35 | private let config: RTCConfiguration! 36 | private let constraints = RTCMediaConstraints(optional: ["DtlsSrtpKeyAgreement": kRTCMediaConstraintsValueTrue]) 37 | 38 | //MARK: - Initialize 39 | 40 | @available(*, unavailable) 41 | override init() { 42 | fatalError("WebRTCService init is unavailable") 43 | } 44 | 45 | convenience init(iceServers: [String]) { 46 | let config = RTCConfiguration() 47 | config.iceServers = [RTCIceServer(urlStrings: iceServers)] 48 | config.sdpSemantics = .unifiedPlan 49 | config.continualGatheringPolicy = .gatherContinually 50 | self.init(config: config) 51 | } 52 | 53 | init(config: RTCConfiguration) { 54 | self.config = config 55 | super.init() 56 | createMediaSenders() 57 | configureAudioSession() 58 | startCall() 59 | } 60 | 61 | 62 | // MARK:- Signaling 63 | 64 | func offer(completion: @escaping (_ sdp: RTCSessionDescription) -> Void) { 65 | print("WebRTCClient offer") 66 | let constrains = RTCMediaConstraints(mandatoryConstraints: mediaConstrains, optionalConstraints: nil) 67 | 68 | peerConnection.offer(for: constrains) { sdp, error in 69 | guard let sdp = sdp else { 70 | print("WebRTCService offer no sdp, error ", error) 71 | return 72 | } 73 | 74 | self.peerConnection.setLocalDescription(sdp) { error in 75 | if let error = error { 76 | print("WebRTCService setLocalDescription error ", error) 77 | return 78 | } 79 | completion(sdp) 80 | } 81 | } 82 | } 83 | 84 | func answer(completion: @escaping (_ sdp: RTCSessionDescription) -> Void) { 85 | print("WebRTCClient answer") 86 | 87 | let constrains = RTCMediaConstraints(mandatoryConstraints: mediaConstrains, optionalConstraints: nil) 88 | 89 | peerConnection.answer(for: constrains) { sdp, error in 90 | guard let sdp = sdp else { 91 | print("WebRTCService answer no sdp ") 92 | return 93 | } 94 | 95 | self.peerConnection.setLocalDescription(sdp) { error in 96 | if let error = error { 97 | print("WebRTCService setLocalDescription error ", error) 98 | return 99 | } 100 | completion(sdp) 101 | } 102 | } 103 | } 104 | 105 | func set(remoteSdp: RTCSessionDescription, completion: @escaping (Error?) -> ()) { 106 | peerConnection.setRemoteDescription(remoteSdp, completionHandler: completion) 107 | } 108 | 109 | func set(remoteCandidate: RTCIceCandidate) { 110 | peerConnection.add(remoteCandidate) 111 | } 112 | 113 | var localRenderer: RTCVideoRenderer! 114 | 115 | func change(localVideoSource: LocalVideoSource) { 116 | change(localVideoSource: localVideoSource, renderer: localRenderer) 117 | } 118 | 119 | func change(localVideoSource: LocalVideoSource, renderer: RTCVideoRenderer) { 120 | print("change local source") 121 | self.localVideoSource = localVideoSource 122 | switch localVideoSource { 123 | case .camera: 124 | startCaptureLocalCameraVideo(renderer: renderer) 125 | case let .file(name): 126 | startCaptureLocalVideoFile(name: name, renderer: renderer) 127 | } 128 | } 129 | 130 | var localVideoSource: LocalVideoSource! 131 | 132 | enum LocalVideoSource { 133 | case camera 134 | case file(name: String) 135 | } 136 | 137 | // MARK: - Media 138 | 139 | public func startCaptureLocalCameraVideo(renderer: RTCVideoRenderer) { 140 | print("startCaptureLocalCameraVideo") 141 | localRenderer = renderer 142 | stopLocalCapture() 143 | videoCapturer = RTCCameraVideoCapturer(delegate: videoSource) 144 | 145 | guard let capturer = videoCapturer as? RTCCameraVideoCapturer else { 146 | print("WebRTCService can't get capturer") 147 | return 148 | } 149 | 150 | guard 151 | let frontCamera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == .front }), 152 | // choose highest res 153 | let format = (RTCCameraVideoCapturer.supportedFormats(for: frontCamera).sorted { (f1, f2) -> Bool in 154 | let width1 = CMVideoFormatDescriptionGetDimensions(f1.formatDescription).width 155 | let width2 = CMVideoFormatDescriptionGetDimensions(f2.formatDescription).width 156 | return width1 < width2 157 | }).last, 158 | // choose highest fps 159 | let frameRateRange = (format.videoSupportedFrameRateRanges.sorted { return $0.maxFrameRate < $1.maxFrameRate }.last) 160 | else { 161 | print("WebRTCService can't get frontCamera") 162 | return 163 | } 164 | 165 | let fps = Int(frameRateRange.maxFrameRate) 166 | 167 | capturer.startCapture(with: frontCamera, 168 | format: format, 169 | fps: fps) 170 | 171 | localVideoTrack?.add(renderer) 172 | } 173 | 174 | public func startCaptureLocalVideoFile(name: String, renderer: RTCVideoRenderer) { 175 | print("startCaptureLocalVideoFile") 176 | 177 | stopLocalCapture() 178 | 179 | localRenderer = renderer 180 | videoCapturer = RTCFileVideoCapturer(delegate: videoSource) 181 | 182 | guard let capturer = videoCapturer as? RTCFileVideoCapturer else { 183 | print("WebRTCService can't get capturer") 184 | return 185 | } 186 | 187 | capturer.startCapturing(fromFileNamed: name) { error in 188 | print("startCapturing error ", error) 189 | return 190 | } 191 | 192 | localVideoTrack?.add(renderer) 193 | } 194 | 195 | private func stopLocalCapture() { 196 | if let capt = videoCapturer as? RTCCameraVideoCapturer { 197 | capt.stopCapture() 198 | } 199 | 200 | if let capt = videoCapturer as? RTCFileVideoCapturer { 201 | capt.stopCapture() 202 | } 203 | } 204 | 205 | func renderRemoteVideo(to renderer: RTCVideoRenderer) { 206 | remoteVideoTrack?.add(renderer) 207 | } 208 | 209 | func startCall() { 210 | print("WebRTCService startCall") 211 | peerConnection = Self.factory.peerConnection(with: config, constraints: constraints, delegate: nil) 212 | let streamID = "stream" 213 | peerConnection.add(localAudioTrack!, streamIds: [streamID]) 214 | peerConnection.add(localVideoTrack!, streamIds: [streamID]) 215 | peerConnection.delegate = self 216 | 217 | remoteVideoTrack = peerConnection.transceivers 218 | .first { $0.mediaType == .video }? 219 | .receiver 220 | .track as? RTCVideoTrack 221 | } 222 | 223 | func hangup() { 224 | print("WebRTCService hangup") 225 | peerConnection.close() 226 | } 227 | 228 | //MARK: - Private 229 | 230 | private func configureAudioSession() { 231 | rtcAudioSession.lockForConfiguration() 232 | do { 233 | try rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue) 234 | try rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue) 235 | } catch let error { 236 | debugPrint("WebRTCService Error changeing AVAudioSession category: \(error)") 237 | } 238 | 239 | rtcAudioSession.unlockForConfiguration() 240 | } 241 | 242 | private func createMediaSenders() { 243 | localAudioTrack = createAudioTrack() 244 | localVideoTrack = createVideoTrack() 245 | } 246 | 247 | private func createAudioTrack() -> RTCAudioTrack { 248 | let audioConstrains = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) 249 | let audioSource = WebRTCClient.factory.audioSource(with: audioConstrains) 250 | let audioTrack = WebRTCClient.factory.audioTrack(with: audioSource, trackId: "audio0") 251 | return audioTrack 252 | } 253 | 254 | private let videoSource = WebRTCClient.factory.videoSource() 255 | 256 | private func createVideoTrack() -> RTCVideoTrack { 257 | let videoTrack = WebRTCClient.factory.videoTrack(with: videoSource, trackId: "video0") 258 | return videoTrack 259 | } 260 | } 261 | 262 | extension RTCMediaConstraints { 263 | convenience init(constraints mandatory: [String : String]? = nil, optional: [String : String]? = nil) { 264 | self.init(mandatoryConstraints: mandatory, optionalConstraints: optional) 265 | } 266 | } 267 | 268 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - abseil/algorithm (0.20200225.0): 3 | - abseil/algorithm/algorithm (= 0.20200225.0) 4 | - abseil/algorithm/container (= 0.20200225.0) 5 | - abseil/algorithm/algorithm (0.20200225.0): 6 | - abseil/base/config 7 | - abseil/algorithm/container (0.20200225.0): 8 | - abseil/algorithm/algorithm 9 | - abseil/base/core_headers 10 | - abseil/meta/type_traits 11 | - abseil/base (0.20200225.0): 12 | - abseil/base/atomic_hook (= 0.20200225.0) 13 | - abseil/base/base (= 0.20200225.0) 14 | - abseil/base/base_internal (= 0.20200225.0) 15 | - abseil/base/bits (= 0.20200225.0) 16 | - abseil/base/config (= 0.20200225.0) 17 | - abseil/base/core_headers (= 0.20200225.0) 18 | - abseil/base/dynamic_annotations (= 0.20200225.0) 19 | - abseil/base/endian (= 0.20200225.0) 20 | - abseil/base/errno_saver (= 0.20200225.0) 21 | - abseil/base/exponential_biased (= 0.20200225.0) 22 | - abseil/base/log_severity (= 0.20200225.0) 23 | - abseil/base/malloc_internal (= 0.20200225.0) 24 | - abseil/base/periodic_sampler (= 0.20200225.0) 25 | - abseil/base/pretty_function (= 0.20200225.0) 26 | - abseil/base/raw_logging_internal (= 0.20200225.0) 27 | - abseil/base/spinlock_wait (= 0.20200225.0) 28 | - abseil/base/throw_delegate (= 0.20200225.0) 29 | - abseil/base/atomic_hook (0.20200225.0): 30 | - abseil/base/config 31 | - abseil/base/core_headers 32 | - abseil/base/base (0.20200225.0): 33 | - abseil/base/atomic_hook 34 | - abseil/base/base_internal 35 | - abseil/base/config 36 | - abseil/base/core_headers 37 | - abseil/base/dynamic_annotations 38 | - abseil/base/log_severity 39 | - abseil/base/raw_logging_internal 40 | - abseil/base/spinlock_wait 41 | - abseil/meta/type_traits 42 | - abseil/base/base_internal (0.20200225.0): 43 | - abseil/base/config 44 | - abseil/meta/type_traits 45 | - abseil/base/bits (0.20200225.0): 46 | - abseil/base/config 47 | - abseil/base/core_headers 48 | - abseil/base/config (0.20200225.0) 49 | - abseil/base/core_headers (0.20200225.0): 50 | - abseil/base/config 51 | - abseil/base/dynamic_annotations (0.20200225.0) 52 | - abseil/base/endian (0.20200225.0): 53 | - abseil/base/config 54 | - abseil/base/core_headers 55 | - abseil/base/errno_saver (0.20200225.0): 56 | - abseil/base/config 57 | - abseil/base/exponential_biased (0.20200225.0): 58 | - abseil/base/config 59 | - abseil/base/core_headers 60 | - abseil/base/log_severity (0.20200225.0): 61 | - abseil/base/config 62 | - abseil/base/core_headers 63 | - abseil/base/malloc_internal (0.20200225.0): 64 | - abseil/base/base 65 | - abseil/base/base_internal 66 | - abseil/base/config 67 | - abseil/base/core_headers 68 | - abseil/base/dynamic_annotations 69 | - abseil/base/raw_logging_internal 70 | - abseil/base/periodic_sampler (0.20200225.0): 71 | - abseil/base/core_headers 72 | - abseil/base/exponential_biased 73 | - abseil/base/pretty_function (0.20200225.0) 74 | - abseil/base/raw_logging_internal (0.20200225.0): 75 | - abseil/base/atomic_hook 76 | - abseil/base/config 77 | - abseil/base/core_headers 78 | - abseil/base/log_severity 79 | - abseil/base/spinlock_wait (0.20200225.0): 80 | - abseil/base/base_internal 81 | - abseil/base/core_headers 82 | - abseil/base/errno_saver 83 | - abseil/base/throw_delegate (0.20200225.0): 84 | - abseil/base/config 85 | - abseil/base/raw_logging_internal 86 | - abseil/container/compressed_tuple (0.20200225.0): 87 | - abseil/utility/utility 88 | - abseil/container/inlined_vector (0.20200225.0): 89 | - abseil/algorithm/algorithm 90 | - abseil/base/core_headers 91 | - abseil/base/throw_delegate 92 | - abseil/container/inlined_vector_internal 93 | - abseil/memory/memory 94 | - abseil/container/inlined_vector_internal (0.20200225.0): 95 | - abseil/base/core_headers 96 | - abseil/container/compressed_tuple 97 | - abseil/memory/memory 98 | - abseil/meta/type_traits 99 | - abseil/types/span 100 | - abseil/memory (0.20200225.0): 101 | - abseil/memory/memory (= 0.20200225.0) 102 | - abseil/memory/memory (0.20200225.0): 103 | - abseil/base/core_headers 104 | - abseil/meta/type_traits 105 | - abseil/meta (0.20200225.0): 106 | - abseil/meta/type_traits (= 0.20200225.0) 107 | - abseil/meta/type_traits (0.20200225.0): 108 | - abseil/base/config 109 | - abseil/numeric/int128 (0.20200225.0): 110 | - abseil/base/config 111 | - abseil/base/core_headers 112 | - abseil/strings/internal (0.20200225.0): 113 | - abseil/base/config 114 | - abseil/base/core_headers 115 | - abseil/base/endian 116 | - abseil/base/raw_logging_internal 117 | - abseil/meta/type_traits 118 | - abseil/strings/str_format (0.20200225.0): 119 | - abseil/strings/str_format_internal 120 | - abseil/strings/str_format_internal (0.20200225.0): 121 | - abseil/base/config 122 | - abseil/base/core_headers 123 | - abseil/meta/type_traits 124 | - abseil/numeric/int128 125 | - abseil/strings/strings 126 | - abseil/types/span 127 | - abseil/strings/strings (0.20200225.0): 128 | - abseil/base/base 129 | - abseil/base/bits 130 | - abseil/base/config 131 | - abseil/base/core_headers 132 | - abseil/base/endian 133 | - abseil/base/raw_logging_internal 134 | - abseil/base/throw_delegate 135 | - abseil/memory/memory 136 | - abseil/meta/type_traits 137 | - abseil/numeric/int128 138 | - abseil/strings/internal 139 | - abseil/time (0.20200225.0): 140 | - abseil/time/internal (= 0.20200225.0) 141 | - abseil/time/time (= 0.20200225.0) 142 | - abseil/time/internal (0.20200225.0): 143 | - abseil/time/internal/cctz (= 0.20200225.0) 144 | - abseil/time/internal/cctz (0.20200225.0): 145 | - abseil/time/internal/cctz/civil_time (= 0.20200225.0) 146 | - abseil/time/internal/cctz/time_zone (= 0.20200225.0) 147 | - abseil/time/internal/cctz/civil_time (0.20200225.0): 148 | - abseil/base/config 149 | - abseil/time/internal/cctz/time_zone (0.20200225.0): 150 | - abseil/base/config 151 | - abseil/time/internal/cctz/civil_time 152 | - abseil/time/time (0.20200225.0): 153 | - abseil/base/base 154 | - abseil/base/core_headers 155 | - abseil/base/raw_logging_internal 156 | - abseil/numeric/int128 157 | - abseil/strings/strings 158 | - abseil/time/internal/cctz/civil_time 159 | - abseil/time/internal/cctz/time_zone 160 | - abseil/types (0.20200225.0): 161 | - abseil/types/any (= 0.20200225.0) 162 | - abseil/types/bad_any_cast (= 0.20200225.0) 163 | - abseil/types/bad_any_cast_impl (= 0.20200225.0) 164 | - abseil/types/bad_optional_access (= 0.20200225.0) 165 | - abseil/types/bad_variant_access (= 0.20200225.0) 166 | - abseil/types/compare (= 0.20200225.0) 167 | - abseil/types/optional (= 0.20200225.0) 168 | - abseil/types/span (= 0.20200225.0) 169 | - abseil/types/variant (= 0.20200225.0) 170 | - abseil/types/any (0.20200225.0): 171 | - abseil/base/config 172 | - abseil/base/core_headers 173 | - abseil/meta/type_traits 174 | - abseil/types/bad_any_cast 175 | - abseil/utility/utility 176 | - abseil/types/bad_any_cast (0.20200225.0): 177 | - abseil/base/config 178 | - abseil/types/bad_any_cast_impl 179 | - abseil/types/bad_any_cast_impl (0.20200225.0): 180 | - abseil/base/config 181 | - abseil/base/raw_logging_internal 182 | - abseil/types/bad_optional_access (0.20200225.0): 183 | - abseil/base/config 184 | - abseil/base/raw_logging_internal 185 | - abseil/types/bad_variant_access (0.20200225.0): 186 | - abseil/base/config 187 | - abseil/base/raw_logging_internal 188 | - abseil/types/compare (0.20200225.0): 189 | - abseil/base/core_headers 190 | - abseil/meta/type_traits 191 | - abseil/types/optional (0.20200225.0): 192 | - abseil/base/base_internal 193 | - abseil/base/config 194 | - abseil/base/core_headers 195 | - abseil/memory/memory 196 | - abseil/meta/type_traits 197 | - abseil/types/bad_optional_access 198 | - abseil/utility/utility 199 | - abseil/types/span (0.20200225.0): 200 | - abseil/algorithm/algorithm 201 | - abseil/base/core_headers 202 | - abseil/base/throw_delegate 203 | - abseil/meta/type_traits 204 | - abseil/types/variant (0.20200225.0): 205 | - abseil/base/base_internal 206 | - abseil/base/config 207 | - abseil/base/core_headers 208 | - abseil/meta/type_traits 209 | - abseil/types/bad_variant_access 210 | - abseil/utility/utility 211 | - abseil/utility/utility (0.20200225.0): 212 | - abseil/base/base_internal 213 | - abseil/base/config 214 | - abseil/meta/type_traits 215 | - Bond (6.10.2): 216 | - Differ (~> 1.3) 217 | - ReactiveKit (~> 3.9) 218 | - BoringSSL-GRPC (0.0.7): 219 | - BoringSSL-GRPC/Implementation (= 0.0.7) 220 | - BoringSSL-GRPC/Interface (= 0.0.7) 221 | - BoringSSL-GRPC/Implementation (0.0.7): 222 | - BoringSSL-GRPC/Interface (= 0.0.7) 223 | - BoringSSL-GRPC/Interface (0.0.7) 224 | - Differ (1.4.5) 225 | - Firebase/CoreOnly (6.24.0): 226 | - FirebaseCore (= 6.7.0) 227 | - Firebase/Firestore (6.24.0): 228 | - Firebase/CoreOnly 229 | - FirebaseFirestore (~> 1.13.0) 230 | - FirebaseAuthInterop (1.1.0) 231 | - FirebaseCore (6.7.0): 232 | - FirebaseCoreDiagnostics (~> 1.3) 233 | - FirebaseCoreDiagnosticsInterop (~> 1.2) 234 | - GoogleUtilities/Environment (~> 6.5) 235 | - GoogleUtilities/Logger (~> 6.5) 236 | - FirebaseCoreDiagnostics (1.3.0): 237 | - FirebaseCoreDiagnosticsInterop (~> 1.2) 238 | - GoogleDataTransportCCTSupport (~> 3.1) 239 | - GoogleUtilities/Environment (~> 6.5) 240 | - GoogleUtilities/Logger (~> 6.5) 241 | - nanopb (~> 1.30905.0) 242 | - FirebaseCoreDiagnosticsInterop (1.2.0) 243 | - FirebaseFirestore (1.13.0): 244 | - abseil/algorithm (= 0.20200225.0) 245 | - abseil/base (= 0.20200225.0) 246 | - abseil/memory (= 0.20200225.0) 247 | - abseil/meta (= 0.20200225.0) 248 | - abseil/strings/strings (= 0.20200225.0) 249 | - abseil/time (= 0.20200225.0) 250 | - abseil/types (= 0.20200225.0) 251 | - FirebaseAuthInterop (~> 1.0) 252 | - FirebaseCore (~> 6.2) 253 | - "gRPC-C++ (~> 1.28.0)" 254 | - leveldb-library (~> 1.22) 255 | - nanopb (~> 1.30905.0) 256 | - GoogleDataTransport (6.1.0) 257 | - GoogleDataTransportCCTSupport (3.1.0): 258 | - GoogleDataTransport (~> 6.1) 259 | - nanopb (~> 1.30905.0) 260 | - GoogleUtilities/Environment (6.6.0): 261 | - PromisesObjC (~> 1.2) 262 | - GoogleUtilities/Logger (6.6.0): 263 | - GoogleUtilities/Environment 264 | - GoogleWebRTC (1.1.29400) 265 | - "gRPC-C++ (1.28.0)": 266 | - "gRPC-C++/Implementation (= 1.28.0)" 267 | - "gRPC-C++/Interface (= 1.28.0)" 268 | - "gRPC-C++/Implementation (1.28.0)": 269 | - abseil/container/inlined_vector (= 0.20200225.0) 270 | - abseil/memory/memory (= 0.20200225.0) 271 | - abseil/strings/str_format (= 0.20200225.0) 272 | - abseil/strings/strings (= 0.20200225.0) 273 | - abseil/types/optional (= 0.20200225.0) 274 | - "gRPC-C++/Interface (= 1.28.0)" 275 | - gRPC-Core (= 1.28.0) 276 | - "gRPC-C++/Interface (1.28.0)" 277 | - gRPC-Core (1.28.0): 278 | - gRPC-Core/Implementation (= 1.28.0) 279 | - gRPC-Core/Interface (= 1.28.0) 280 | - gRPC-Core/Implementation (1.28.0): 281 | - abseil/container/inlined_vector (= 0.20200225.0) 282 | - abseil/memory/memory (= 0.20200225.0) 283 | - abseil/strings/str_format (= 0.20200225.0) 284 | - abseil/strings/strings (= 0.20200225.0) 285 | - abseil/types/optional (= 0.20200225.0) 286 | - BoringSSL-GRPC (= 0.0.7) 287 | - gRPC-Core/Interface (= 1.28.0) 288 | - gRPC-Core/Interface (1.28.0) 289 | - IHKeyboardAvoiding (4.7) 290 | - IQKeyboardManagerSwift (6.5.5) 291 | - leveldb-library (1.22) 292 | - nanopb (1.30905.0): 293 | - nanopb/decode (= 1.30905.0) 294 | - nanopb/encode (= 1.30905.0) 295 | - nanopb/decode (1.30905.0) 296 | - nanopb/encode (1.30905.0) 297 | - PIPKit (0.2.0) 298 | - PromisesObjC (1.2.8) 299 | - ReactiveKit (3.17.1) 300 | - SnapKit (5.0.1) 301 | - SwiftyUserDefaults (5.0.0) 302 | 303 | DEPENDENCIES: 304 | - Bond (= 6.10.2) 305 | - Firebase/Firestore 306 | - GoogleWebRTC 307 | - IHKeyboardAvoiding 308 | - IQKeyboardManagerSwift 309 | - PIPKit 310 | - ReactiveKit 311 | - SnapKit 312 | - SwiftyUserDefaults 313 | 314 | SPEC REPOS: 315 | trunk: 316 | - abseil 317 | - Bond 318 | - BoringSSL-GRPC 319 | - Differ 320 | - Firebase 321 | - FirebaseAuthInterop 322 | - FirebaseCore 323 | - FirebaseCoreDiagnostics 324 | - FirebaseCoreDiagnosticsInterop 325 | - FirebaseFirestore 326 | - GoogleDataTransport 327 | - GoogleDataTransportCCTSupport 328 | - GoogleUtilities 329 | - GoogleWebRTC 330 | - "gRPC-C++" 331 | - gRPC-Core 332 | - IHKeyboardAvoiding 333 | - IQKeyboardManagerSwift 334 | - leveldb-library 335 | - nanopb 336 | - PIPKit 337 | - PromisesObjC 338 | - ReactiveKit 339 | - SnapKit 340 | - SwiftyUserDefaults 341 | 342 | SPEC CHECKSUMS: 343 | abseil: 6c8eb7892aefa08d929b39f9bb108e5367e3228f 344 | Bond: 5c9d854fb14d3268496ccaf9b7ccd8428ec1609c 345 | BoringSSL-GRPC: 8edf627ee524575e2f8d19d56f068b448eea3879 346 | Differ: 3b6bd78e2b20cc795d9a86f7641d087524e4273e 347 | Firebase: b28e55c60efd98963cd9011fe2fac5a10c2ba124 348 | FirebaseAuthInterop: a0f37ae05833af156e72028f648d313f7e7592e9 349 | FirebaseCore: e610482f64097b0e9f056cd97bc6b33dfabcbb6a 350 | FirebaseCoreDiagnostics: 4a773a47bd83bbd5a9b1ccf1ce7caa8b2d535e67 351 | FirebaseCoreDiagnosticsInterop: 296e2c5f5314500a850ad0b83e9e7c10b011a850 352 | FirebaseFirestore: 35f8f67d7b25e4743c62ea6e46c38cafa8dc32b5 353 | GoogleDataTransport: f6f8eba931df03ebd2232ff4645aa85f8f47b5ab 354 | GoogleDataTransportCCTSupport: d70a561f7d236af529fee598835caad5e25f6d3d 355 | GoogleUtilities: 39530bc0ad980530298e9c4af8549e991fd033b1 356 | GoogleWebRTC: cfb83bc346435a17fe06bb05f04ad826b858a7fb 357 | "gRPC-C++": 2ea13a2e14f0b89991a0b4b0151e7c6a56319516 358 | gRPC-Core: 325ba201411619a7302c621a1c8ee787719d4b9b 359 | IHKeyboardAvoiding: 4cae0880c3975feffa278495df894c5166e71ad0 360 | IQKeyboardManagerSwift: 0fb93310284665245591f50f7a5e38de615960b7 361 | leveldb-library: 55d93ee664b4007aac644a782d11da33fba316f7 362 | nanopb: c43f40fadfe79e8b8db116583945847910cbabc9 363 | PIPKit: 00fd62aa4967f9cef1eca1a5587d3192c23881e1 364 | PromisesObjC: c119f3cd559f50b7ae681fa59dc1acd19173b7e6 365 | ReactiveKit: f578a74d3555296ea61fab228f5245a4ed7b4b27 366 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 367 | SwiftyUserDefaults: 33fcb42bd1feb53a37d873feb62c82967db5f7f6 368 | 369 | PODFILE CHECKSUM: f999dab304ec87e92b8c73a280f742a21b73a89c 370 | 371 | COCOAPODS: 1.9.1 372 | --------------------------------------------------------------------------------