├── 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 |
--------------------------------------------------------------------------------