├── twiliochat
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Icon-29.png
│ │ ├── Icon-40.png
│ │ ├── Icon-76.png
│ │ ├── Icon-29@2x-1.png
│ │ ├── Icon-29@2x.png
│ │ ├── Icon-29@3x.png
│ │ ├── Icon-40@2x-1.png
│ │ ├── Icon-40@2x.png
│ │ ├── Icon-40@3x.png
│ │ ├── Icon-60@2x.png
│ │ ├── Icon-60@3x.png
│ │ ├── Icon-76@2x.png
│ │ ├── Icon-83_5@2x.png
│ │ └── Contents.json
│ ├── home-bg.imageset
│ │ ├── home-bg.png
│ │ ├── home-bg@2x.png
│ │ ├── home-bg@3x.png
│ │ └── Contents.json
│ ├── login-bg.imageset
│ │ ├── login-bg.png
│ │ ├── login-bg@2x.png
│ │ ├── login-bg@3x.png
│ │ └── Contents.json
│ ├── btn-login.imageset
│ │ ├── btn-login.png
│ │ ├── btn-login@2x.png
│ │ ├── btn-login@3x.png
│ │ └── Contents.json
│ ├── header-bg.imageset
│ │ ├── header-bg.png
│ │ ├── header-bg@2x.png
│ │ ├── header-bg@3x.png
│ │ └── Contents.json
│ ├── icon-list.imageset
│ │ ├── icon-list.png
│ │ ├── icon-list@2x.png
│ │ ├── icon-list@3x.png
│ │ └── Contents.json
│ ├── icon-user.imageset
│ │ ├── icon-user.png
│ │ ├── icon-user@2x.png
│ │ ├── icon-user@3x.png
│ │ └── Contents.json
│ ├── chat-field-bg.imageset
│ │ ├── chat-field-bg.png
│ │ ├── chat-field-bg@2x.png
│ │ ├── chat-field-bg@3x.png
│ │ └── Contents.json
│ ├── landing-logo.imageset
│ │ ├── landing-logo.png
│ │ ├── landing-logo@2x.png
│ │ ├── landing-logo@3x.png
│ │ └── Contents.json
│ ├── channel-list-bg.imageset
│ │ ├── channel-list-bg.png
│ │ ├── channel-list-bg@2x.png
│ │ ├── channel-list-bg@3x.png
│ │ └── Contents.json
│ ├── user-panel-line.imageset
│ │ ├── user-panel-line.png
│ │ ├── user-panel-line@2x.png
│ │ ├── user-panel-line@3x.png
│ │ └── Contents.json
│ ├── add-channel-button.imageset
│ │ ├── add-channel-button.png
│ │ ├── add-channel-button@2x.png
│ │ ├── add-channel-button@3x.png
│ │ └── Contents.json
│ ├── password-field-bg.imageset
│ │ ├── password-field-bg.png
│ │ ├── password-field-bg@2x.png
│ │ ├── password-field-bg@3x.png
│ │ └── Contents.json
│ └── username-field-bg.imageset
│ │ ├── username-field-bg.png
│ │ ├── username-field-bg@2x.png
│ │ ├── username-field-bg@3x.png
│ │ └── Contents.json
├── twiliochat-Bridging-Header.h
├── Keys.plist
├── ChatTableCell.swift
├── AlertDialogController.swift
├── StatusMessage.swift
├── SessionManager.swift
├── MenuTableCell.swift
├── Info.plist
├── DateTodayFormatter.swift
├── AppDelegate.swift
├── InputDialogController.swift
├── TokenRequestHandler.swift
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── LoginViewController.swift
├── ChatStatusTableCell.xib
├── ChatTableCell.xib
├── TextFieldFormHandler.swift
├── ChannelManager.swift
├── MessagingManager.swift
├── MenuViewController.swift
└── MainChatViewController.swift
├── twiliochat.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── project.pbxproj
├── twiliochat.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── xcschemes
│ └── twiliochat.xcscheme
├── twiliochatTests
├── MockAlertDialogController.swift
├── MockMessagingManager.swift
├── Info.plist
├── LoginViewControllerTests.swift
└── DateTodayFormatterTests.swift
├── Podfile
├── .travis.yml
├── .gitignore
├── Podfile.lock
├── LICENSE
└── README.md
/twiliochat/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-29.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-40.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/home-bg.imageset/home-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/home-bg.imageset/home-bg.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/login-bg.imageset/login-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/login-bg.imageset/login-bg.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/btn-login.imageset/btn-login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/btn-login.imageset/btn-login.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/header-bg.imageset/header-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/header-bg.imageset/header-bg.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/home-bg.imageset/home-bg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/home-bg.imageset/home-bg@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/home-bg.imageset/home-bg@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/home-bg.imageset/home-bg@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/icon-list.imageset/icon-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/icon-list.imageset/icon-list.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/icon-user.imageset/icon-user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/icon-user.imageset/icon-user.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-83_5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/AppIcon.appiconset/Icon-83_5@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/btn-login.imageset/btn-login@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/btn-login.imageset/btn-login@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/btn-login.imageset/btn-login@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/btn-login.imageset/btn-login@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/header-bg.imageset/header-bg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/header-bg.imageset/header-bg@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/header-bg.imageset/header-bg@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/header-bg.imageset/header-bg@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/icon-list.imageset/icon-list@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/icon-list.imageset/icon-list@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/icon-list.imageset/icon-list@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/icon-list.imageset/icon-list@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/icon-user.imageset/icon-user@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/icon-user.imageset/icon-user@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/icon-user.imageset/icon-user@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/icon-user.imageset/icon-user@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/login-bg.imageset/login-bg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/login-bg.imageset/login-bg@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/login-bg.imageset/login-bg@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/login-bg.imageset/login-bg@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/chat-field-bg.imageset/chat-field-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/chat-field-bg.imageset/chat-field-bg.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/landing-logo.imageset/landing-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/landing-logo.imageset/landing-logo.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/landing-logo.imageset/landing-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/landing-logo.imageset/landing-logo@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/landing-logo.imageset/landing-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/landing-logo.imageset/landing-logo@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/channel-list-bg.imageset/channel-list-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/channel-list-bg.imageset/channel-list-bg.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/chat-field-bg.imageset/chat-field-bg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/chat-field-bg.imageset/chat-field-bg@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/chat-field-bg.imageset/chat-field-bg@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/chat-field-bg.imageset/chat-field-bg@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/user-panel-line.imageset/user-panel-line.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/user-panel-line.imageset/user-panel-line.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/channel-list-bg.imageset/channel-list-bg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/channel-list-bg.imageset/channel-list-bg@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/channel-list-bg.imageset/channel-list-bg@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/channel-list-bg.imageset/channel-list-bg@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/user-panel-line.imageset/user-panel-line@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/user-panel-line.imageset/user-panel-line@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/user-panel-line.imageset/user-panel-line@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/user-panel-line.imageset/user-panel-line@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/add-channel-button.imageset/add-channel-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/add-channel-button.imageset/add-channel-button.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/password-field-bg.imageset/password-field-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/password-field-bg.imageset/password-field-bg.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/username-field-bg.imageset/username-field-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/username-field-bg.imageset/username-field-bg.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/password-field-bg.imageset/password-field-bg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/password-field-bg.imageset/password-field-bg@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/password-field-bg.imageset/password-field-bg@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/password-field-bg.imageset/password-field-bg@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/username-field-bg.imageset/username-field-bg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/username-field-bg.imageset/username-field-bg@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/username-field-bg.imageset/username-field-bg@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/username-field-bg.imageset/username-field-bg@3x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/add-channel-button.imageset/add-channel-button@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/add-channel-button.imageset/add-channel-button@2x.png
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/add-channel-button.imageset/add-channel-button@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/twiliochat-swift/HEAD/twiliochat/Assets.xcassets/add-channel-button.imageset/add-channel-button@3x.png
--------------------------------------------------------------------------------
/twiliochat.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/twiliochat/twiliochat-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #ifndef twiliochat_Bridging_Header_h
2 | #define twiliochat_Bridging_Header_h
3 |
4 | #import
5 | #import
6 |
7 | #endif
8 |
--------------------------------------------------------------------------------
/twiliochat/Keys.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | TokenRequestUrl
6 | http://localhost:8000/token
7 |
8 |
9 |
--------------------------------------------------------------------------------
/twiliochat.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/twiliochatTests/MockAlertDialogController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | @testable import twiliochat
3 |
4 | class MockAlertDialogController: AlertDialogController {
5 | static var showedAlertWithMessage = ""
6 |
7 | override class func showAlertWithMessage(message:String?, title:String?, presenter:(UIViewController)) {
8 | if let message = message {
9 | showedAlertWithMessage = message
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/home-bg.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "home-bg.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "home-bg@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "home-bg@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/login-bg.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "login-bg.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "login-bg@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "login-bg@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/btn-login.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "btn-login.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "btn-login@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "btn-login@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/header-bg.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "header-bg.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "header-bg@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "header-bg@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/icon-list.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon-list.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "icon-list@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "icon-list@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/icon-user.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "icon-user.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "icon-user@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "icon-user@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/landing-logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "landing-logo.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "landing-logo@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "landing-logo@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/chat-field-bg.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "chat-field-bg.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "chat-field-bg@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "chat-field-bg@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/channel-list-bg.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "channel-list-bg.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "channel-list-bg@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "channel-list-bg@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/user-panel-line.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "user-panel-line.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "user-panel-line@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "user-panel-line@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://github.com/CocoaPods/Specs'
2 |
3 | # Uncomment this line to define a global platform for your project
4 | platform :ios, '11.0'
5 | # Uncomment this line if you're using Swift
6 | use_frameworks!
7 |
8 | target 'twiliochat' do
9 | pod 'SWRevealViewController', '~> 2.3'
10 | pod 'SlackTextViewController', '1.9.6'
11 | pod 'TwilioChatClient', '~> 4.0'
12 | end
13 |
14 | target 'twiliochatTests' do
15 | pod 'SWRevealViewController', '~> 2.3'
16 | pod 'SlackTextViewController', '1.9.6'
17 | pod 'TwilioChatClient', '~> 4.0'
18 | end
19 |
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/password-field-bg.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "password-field-bg.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "password-field-bg@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "password-field-bg@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/username-field-bg.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "username-field-bg.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "username-field-bg@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "username-field-bg@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/twiliochat/Assets.xcassets/add-channel-button.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "add-channel-button.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "add-channel-button@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "add-channel-button@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 | osx_image: xcode10
3 | cache: cocoapods
4 | xcode_workspace: twiliochat.xcworkspace
5 | xcode_scheme: twiliochat
6 |
7 | before_install:
8 | - travis_wait pod repo update >/dev/null 2>&1
9 |
10 | install:
11 | - travis_wait pod install
12 |
13 | script:
14 | - xcodebuild -workspace twiliochat.xcworkspace -scheme twiliochat CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO clean build-for-testing | xcpretty
15 | - xcodebuild -workspace twiliochat.xcworkspace -scheme twiliochat -destination 'platform=iOS Simulator,name=iPhone 6' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO clean test | xcpretty
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | *.DS_Store
4 | build/
5 | *.pbxuser
6 | !default.pbxuser
7 | *.mode1v3
8 | !default.mode1v3
9 | *.mode2v3
10 | !default.mode2v3
11 | *.perspectivev3
12 | !default.perspectivev3
13 | xcuserdata
14 | *.xccheckout
15 | *.moved-aside
16 | DerivedData
17 | *.hmap
18 | *.ipa
19 | *.xcuserstate
20 | twiliochat/Keys.plist
21 |
22 | # CocoaPods
23 | #
24 | # We recommend against adding the Pods directory to your .gitignore. However
25 | # you should judge for yourself, the pros and cons are mentioned at:
26 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
27 | #
28 | Pods/
29 |
--------------------------------------------------------------------------------
/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - SlackTextViewController (1.9.6)
3 | - SWRevealViewController (2.3.0)
4 | - TwilioChatClient (4.0.0)
5 |
6 | DEPENDENCIES:
7 | - SlackTextViewController (= 1.9.6)
8 | - SWRevealViewController (~> 2.3)
9 | - TwilioChatClient (~> 4.0)
10 |
11 | SPEC REPOS:
12 | https://github.com/CocoaPods/Specs.git:
13 | - SlackTextViewController
14 | - SWRevealViewController
15 | - TwilioChatClient
16 |
17 | SPEC CHECKSUMS:
18 | SlackTextViewController: b854e62c1c156336bc4fd409c6ca79b5773e8f9d
19 | SWRevealViewController: 6d3fd97f70112fd7cef9de14df4260eacce4c63a
20 | TwilioChatClient: 72fad6bd1fcff3bb13d90a5a5c873509072d859a
21 |
22 | PODFILE CHECKSUM: 9fb530d9294dcf8e8f7f735b87bea8ccde0b39f3
23 |
24 | COCOAPODS: 1.8.4
25 |
--------------------------------------------------------------------------------
/twiliochatTests/MockMessagingManager.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | @testable import twiliochat
3 |
4 | class MockMessagingManager: MessagingManager {
5 | static let mockManager = MockMessagingManager()
6 |
7 | static var loginWithUsernameCalled = false
8 | static var registerWithUsernameCalled = false
9 | static var passwordUsed = ""
10 | static var usernameUsed = ""
11 |
12 | override class func sharedManager() -> MessagingManager {
13 | return mockManager
14 | }
15 |
16 | override func loginWithUsername(username: String,
17 | completion: @escaping (Bool, NSError?) -> Void) {
18 | MockMessagingManager.loginWithUsernameCalled = true
19 | MockMessagingManager.usernameUsed = username
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/twiliochat/ChatTableCell.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class ChatTableCell: UITableViewCell {
4 | static let TWCUserLabelTag = 200
5 | static let TWCDateLabelTag = 201
6 | static let TWCMessageLabelTag = 202
7 |
8 | var userLabel: UILabel!
9 | var messageLabel: UILabel!
10 | var dateLabel: UILabel!
11 |
12 | func setUser(user:String!, message:String!, date:String!) {
13 | userLabel.text = user
14 | messageLabel.text = message
15 | dateLabel.text = date
16 | }
17 |
18 | override func awakeFromNib() {
19 | userLabel = viewWithTag(ChatTableCell.TWCUserLabelTag) as? UILabel
20 | messageLabel = viewWithTag(ChatTableCell.TWCMessageLabelTag) as? UILabel
21 | dateLabel = viewWithTag(ChatTableCell.TWCDateLabelTag) as? UILabel
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/twiliochatTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/twiliochat/AlertDialogController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class AlertDialogController: NSObject {
4 |
5 | class func showAlertWithMessage(message:String?, title:String?, presenter:(UIViewController)) {
6 | showAlertWithMessage(message: message, title: title, presenter: presenter, completion: nil)
7 | }
8 |
9 | class func showAlertWithMessage(message:String?, title:String?,
10 | presenter:(UIViewController), completion:(() -> Void)?) {
11 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
12 |
13 | let defaultAction = UIAlertAction(title: "OK", style: .cancel) { (_) -> Void in
14 | if let block = completion {
15 | block()
16 | }
17 | }
18 |
19 | alert.addAction(defaultAction)
20 | presenter.present(alert, animated: true, completion: nil)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/twiliochat/StatusMessage.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import UIKit
3 |
4 | enum TWCMemberStatus {
5 | case Joined
6 | case Left
7 | }
8 |
9 | class StatusMessage: TCHMessage {
10 | var status: TWCMemberStatus! = nil
11 | var statusMember: TCHMember! = nil
12 |
13 | var _dateCreated: String = ""
14 | override var dateCreated: String {
15 | get {
16 | return _dateCreated
17 | }
18 | set(newDateCreated) {
19 | _dateCreated = newDateCreated
20 | }
21 | }
22 |
23 | init(statusMember: TCHMember, status: TWCMemberStatus) {
24 | super.init()
25 | let dateFormatter = DateFormatter()
26 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
27 | dateFormatter.timeZone = NSTimeZone(forSecondsFromGMT: 0) as TimeZone?
28 | dateCreated = dateFormatter.string(from: NSDate() as Date)
29 | self.statusMember = statusMember
30 | self.status = status
31 | }
32 |
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/twiliochat/SessionManager.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class SessionManager {
4 | static let UsernameKey: String = "username"
5 | static let IsLoggedInKey: String = "loggedIn"
6 | static let defaults = UserDefaults.standard
7 |
8 | class func loginWithUsername(username:String) {
9 | defaults.set(username, forKey: UsernameKey)
10 | defaults.set(true, forKey: IsLoggedInKey)
11 |
12 | defaults.synchronize()
13 | }
14 |
15 | class func logout() {
16 | defaults.set("", forKey: UsernameKey)
17 | defaults.set(false, forKey: IsLoggedInKey)
18 | defaults.synchronize()
19 | }
20 |
21 | class func isLoggedIn() -> Bool {
22 | let isLoggedIn = defaults.bool(forKey: IsLoggedInKey)
23 | if (isLoggedIn) {
24 | return true
25 | }
26 | return false
27 | }
28 |
29 | class func getUsername() -> String {
30 | if let username = defaults.object(forKey: UsernameKey) as? String {
31 | return username
32 | }
33 | return ""
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 TwilioDevEd
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 |
23 |
--------------------------------------------------------------------------------
/twiliochat/MenuTableCell.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class MenuTableCell: UITableViewCell {
4 | var label: UILabel!
5 |
6 | var channelName: String {
7 | get {
8 | return label?.text ?? String()
9 | }
10 | set(name) {
11 | label.text = name
12 | }
13 | }
14 |
15 | var selectedBackgroundColor: UIColor {
16 | return UIColor(red:0.969, green:0.902, blue:0.894, alpha:1)
17 | }
18 |
19 | var labelHighlightedTextColor: UIColor {
20 | return UIColor(red:0.22, green:0.024, blue:0.016, alpha:1)
21 | }
22 |
23 | var labelTextColor: UIColor {
24 | return UIColor(red:0.973, green:0.557, blue:0.502, alpha:1)
25 | }
26 |
27 | override func awakeFromNib() {
28 | label = viewWithTag(200) as? UILabel
29 | label.highlightedTextColor = labelHighlightedTextColor
30 | label.textColor = labelTextColor
31 | }
32 |
33 | override func setSelected(_ selected: Bool, animated: Bool) {
34 | super.setSelected(selected, animated: animated)
35 | if (selected) {
36 | contentView.backgroundColor = selectedBackgroundColor
37 | } else {
38 | contentView.backgroundColor = nil
39 | }
40 | }
41 |
42 | override func setHighlighted(_ highlighted: Bool, animated: Bool) {
43 | super.setHighlighted(highlighted, animated: animated)
44 | if (highlighted) {
45 | contentView.backgroundColor = selectedBackgroundColor
46 | } else {
47 | contentView.backgroundColor = nil
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/twiliochat/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSAppTransportSecurity
6 |
7 | NSAllowsArbitraryLoads
8 |
9 |
10 | CFBundleDevelopmentRegion
11 | en
12 | CFBundleExecutable
13 | $(EXECUTABLE_NAME)
14 | CFBundleIdentifier
15 | $(PRODUCT_BUNDLE_IDENTIFIER)
16 | CFBundleInfoDictionaryVersion
17 | 6.0
18 | CFBundleName
19 | $(PRODUCT_NAME)
20 | CFBundlePackageType
21 | APPL
22 | CFBundleShortVersionString
23 | 1.0
24 | CFBundleSignature
25 | ????
26 | CFBundleVersion
27 | 1
28 | LSRequiresIPhoneOS
29 |
30 | UILaunchStoryboardName
31 | LaunchScreen
32 | UIMainStoryboardFile
33 | Main
34 | UIRequiredDeviceCapabilities
35 |
36 | armv7
37 |
38 | UISupportedInterfaceOrientations
39 |
40 | UIInterfaceOrientationPortrait
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UISupportedInterfaceOrientations~ipad
45 |
46 | UIInterfaceOrientationPortrait
47 | UIInterfaceOrientationPortraitUpsideDown
48 | UIInterfaceOrientationLandscapeLeft
49 | UIInterfaceOrientationLandscapeRight
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/twiliochat/DateTodayFormatter.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class DateTodayFormatter {
4 | func stringFromDate(date: NSDate?) -> String? {
5 | guard let date = date else {
6 | return nil
7 | }
8 |
9 | let messageDate = roundDateToDay(date: date)
10 | let todayDate = roundDateToDay(date: NSDate())
11 |
12 | let formatter = DateFormatter()
13 |
14 | if messageDate == todayDate {
15 | formatter.dateFormat = "'Today' - hh:mma"
16 | }
17 | else {
18 | formatter.dateFormat = "MMM. dd - hh:mma"
19 | }
20 |
21 | return formatter.string(from: date as Date)
22 | }
23 |
24 | func roundDateToDay(date: NSDate) -> NSDate {
25 | let calendar = Calendar.current
26 | let flags = Set([.day, .month, .year])
27 | let components = calendar.dateComponents(flags, from: date as Date)
28 |
29 | return calendar.date(from:components)! as NSDate
30 | }
31 | }
32 |
33 | extension NSDate {
34 | class func dateWithISO8601String(dateString: String) -> NSDate? {
35 | var formattedDateString = dateString
36 |
37 | if dateString.hasSuffix("Z") {
38 | let lastIndex = dateString.indices.last!
39 | formattedDateString = dateString.substring(to: lastIndex) + "-000"
40 | }
41 | return dateFromString(str: formattedDateString, withFormat:"yyyy-MM-dd'T'HH:mm:ss.SSSZ")
42 | }
43 |
44 | class func dateFromString(str: String, withFormat dateFormat: String) -> NSDate? {
45 | let formatter = DateFormatter()
46 | formatter.dateFormat = dateFormat
47 | formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") as Locale?
48 | return formatter.date(from: str) as NSDate?
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/twiliochatTests/LoginViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import twiliochat
3 |
4 | class LoginViewControllerTests: XCTestCase {
5 | var viewController: LoginViewController!
6 |
7 | override func setUp() {
8 | super.setUp()
9 | let storyBoard = UIStoryboard(name:"Main", bundle: Bundle.main)
10 | viewController = storyBoard.instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController
11 | viewController.loadView()
12 | viewController.viewDidLoad()
13 | }
14 |
15 | override func tearDown() {
16 | super.tearDown()
17 | MockMessagingManager.loginWithUsernameCalled = false
18 | MockMessagingManager.registerWithUsernameCalled = false
19 | MockMessagingManager.usernameUsed = ""
20 | MockMessagingManager.passwordUsed = ""
21 | MockAlertDialogController.showedAlertWithMessage = ""
22 | }
23 |
24 | func testLoginUser() {
25 | viewController.usernameTextField.text = "username"
26 | viewController.MessagingClientClass = MockMessagingManager.self
27 |
28 | viewController.loginUser()
29 |
30 | XCTAssertTrue(MockMessagingManager.loginWithUsernameCalled)
31 | XCTAssertEqual(MockMessagingManager.usernameUsed, "username")
32 | }
33 |
34 | func testSignUpOrLoginEmptyFields() {
35 | viewController.MessagingClientClass = MockMessagingManager.self
36 | viewController.alertDialogControllerClass = MockAlertDialogController.self
37 | viewController.loginUser()
38 |
39 | XCTAssertEqual(MockAlertDialogController.showedAlertWithMessage, "All fields are required")
40 | XCTAssertFalse(MockMessagingManager.loginWithUsernameCalled)
41 | }
42 |
43 | func testSignUpOrLoginIsLoginIn() {
44 | viewController.usernameTextField.text = "username"
45 | viewController.MessagingClientClass = MockMessagingManager.self
46 | viewController.loginUser()
47 |
48 | XCTAssertTrue(MockMessagingManager.loginWithUsernameCalled)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/twiliochat/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @UIApplicationMain
4 | class AppDelegate: UIResponder, UIApplicationDelegate {
5 | var window: UIWindow?
6 |
7 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
8 | MessagingManager.sharedManager().presentLaunchScreen()
9 | MessagingManager.sharedManager().presentRootViewController()
10 | return true
11 | }
12 |
13 | func applicationWillResignActive(_ application: UIApplication) {
14 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
15 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
16 | }
17 |
18 | func applicationDidEnterBackground(_ application: UIApplication) {
19 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
20 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
21 | }
22 |
23 | func applicationWillEnterForeground(_ application: UIApplication) {
24 | // Called as part of the transition from the background to the inactive state here you can undo many of the changes made on entering the background.
25 | }
26 |
27 | func applicationDidBecomeActive(_ application: UIApplication) {
28 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
29 | }
30 |
31 | func applicationWillTerminate(_ application: UIApplication) {
32 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
33 | }
34 |
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/twiliochat/InputDialogController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class InputDialogController: NSObject {
4 |
5 | var saveAction: UIAlertAction!
6 |
7 | class func showWithTitle(title: String, message: String,
8 | placeholder: String, presenter: UIViewController, handler: @escaping (String) -> Void) {
9 | InputDialogController().showWithTitle(title: title, message: message,
10 | placeholder: placeholder, presenter: presenter, handler: handler)
11 | }
12 |
13 | func showWithTitle(title: String, message: String, placeholder: String,
14 | presenter: UIViewController, handler: @escaping (String) -> Void) {
15 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
16 |
17 | let defaultAction = UIAlertAction(title: "Cancel", style: .cancel) { action in
18 | self.removeTextFieldObserver()
19 | }
20 |
21 | saveAction = UIAlertAction(title: "Save", style: .default) { action in
22 | self.removeTextFieldObserver()
23 | let textFieldText = alert.textFields![0].text ?? String()
24 | handler(textFieldText)
25 | }
26 |
27 | saveAction.isEnabled = false
28 |
29 | alert.addTextField { textField in
30 | textField.placeholder = placeholder
31 | NotificationCenter.default.addObserver(self,
32 | selector: #selector(InputDialogController.handleTextFieldTextDidChangeNotification(notification:)),
33 | name: UITextField.textDidChangeNotification,
34 | object: nil)
35 | }
36 |
37 | alert.addAction(defaultAction)
38 | alert.addAction(saveAction)
39 | presenter.present(alert, animated: true, completion: nil)
40 | }
41 |
42 | @objc func handleTextFieldTextDidChangeNotification(notification: NSNotification) {
43 | let textField = notification.object as? UITextField
44 | saveAction.isEnabled = !(textField!.text?.isEmpty ?? false)
45 | }
46 |
47 | func removeTextFieldObserver() {
48 | NotificationCenter.default.removeObserver(self)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/twiliochat/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 | "size" : "29x29",
15 | "idiom" : "iphone",
16 | "filename" : "Icon-29@2x.png",
17 | "scale" : "2x"
18 | },
19 | {
20 | "size" : "29x29",
21 | "idiom" : "iphone",
22 | "filename" : "Icon-29@3x.png",
23 | "scale" : "3x"
24 | },
25 | {
26 | "size" : "40x40",
27 | "idiom" : "iphone",
28 | "filename" : "Icon-40@2x-1.png",
29 | "scale" : "2x"
30 | },
31 | {
32 | "size" : "40x40",
33 | "idiom" : "iphone",
34 | "filename" : "Icon-40@3x.png",
35 | "scale" : "3x"
36 | },
37 | {
38 | "size" : "60x60",
39 | "idiom" : "iphone",
40 | "filename" : "Icon-60@2x.png",
41 | "scale" : "2x"
42 | },
43 | {
44 | "size" : "60x60",
45 | "idiom" : "iphone",
46 | "filename" : "Icon-60@3x.png",
47 | "scale" : "3x"
48 | },
49 | {
50 | "idiom" : "ipad",
51 | "size" : "20x20",
52 | "scale" : "1x"
53 | },
54 | {
55 | "idiom" : "ipad",
56 | "size" : "20x20",
57 | "scale" : "2x"
58 | },
59 | {
60 | "size" : "29x29",
61 | "idiom" : "ipad",
62 | "filename" : "Icon-29.png",
63 | "scale" : "1x"
64 | },
65 | {
66 | "size" : "29x29",
67 | "idiom" : "ipad",
68 | "filename" : "Icon-29@2x-1.png",
69 | "scale" : "2x"
70 | },
71 | {
72 | "size" : "40x40",
73 | "idiom" : "ipad",
74 | "filename" : "Icon-40.png",
75 | "scale" : "1x"
76 | },
77 | {
78 | "size" : "40x40",
79 | "idiom" : "ipad",
80 | "filename" : "Icon-40@2x.png",
81 | "scale" : "2x"
82 | },
83 | {
84 | "size" : "76x76",
85 | "idiom" : "ipad",
86 | "filename" : "Icon-76.png",
87 | "scale" : "1x"
88 | },
89 | {
90 | "size" : "76x76",
91 | "idiom" : "ipad",
92 | "filename" : "Icon-76@2x.png",
93 | "scale" : "2x"
94 | },
95 | {
96 | "size" : "83.5x83.5",
97 | "idiom" : "ipad",
98 | "filename" : "Icon-83_5@2x.png",
99 | "scale" : "2x"
100 | }
101 | ],
102 | "info" : {
103 | "version" : 1,
104 | "author" : "xcode"
105 | }
106 | }
--------------------------------------------------------------------------------
/twiliochatTests/DateTodayFormatterTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import twiliochat
3 |
4 | class DateTodayFormatterTests: XCTestCase {
5 | var dateTodayFormatter: DateTodayFormatter!
6 |
7 | override func setUp() {
8 | super.setUp()
9 | dateTodayFormatter = DateTodayFormatter()
10 | }
11 |
12 | override func tearDown() {
13 | super.tearDown()
14 | }
15 |
16 | func testStringFromDate() {
17 | let dateFormatter = DateFormatter()
18 | dateFormatter.dateFormat = "yyyy-MM-dd"
19 | let date = dateFormatter.date(from: "1990-05-14")
20 |
21 | let dateString = dateTodayFormatter.stringFromDate(date: date! as NSDate)
22 |
23 | XCTAssertEqual(dateString, "May. 14 - 12:00AM")
24 | }
25 |
26 | func testStringFromDateToday() {
27 | let dateToday = NSDate()
28 | let dateFormatter = DateFormatter()
29 | dateFormatter.dateFormat = "yyyy-MM-dd"
30 | let dateTodayString = dateFormatter.string(from: dateToday as Date)
31 | let date = dateFormatter.date(from: dateTodayString)
32 | let dateString = dateTodayFormatter.stringFromDate(date: date! as NSDate)
33 |
34 | XCTAssertEqual(dateString, "Today - 12:00AM")
35 | }
36 |
37 | func testDateFromString() {
38 | let dateFormatter = DateFormatter()
39 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
40 | let dateString = "1990-05-14 21:04:12"
41 |
42 | let dateFromTestClass = NSDate.dateFromString(str: dateString, withFormat: "yyyy-MM-dd HH:mm:ss")
43 |
44 | XCTAssertEqual(dateFromTestClass, dateFormatter.date(from: "1990-05-14 21:04:12")! as NSDate)
45 | }
46 |
47 | func testDateWithISO8601String() {
48 | let dateString = "1990-05-14T12:05:12.003"
49 | let dateFormatter = DateFormatter()
50 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
51 | let date = dateFormatter.date(from: dateString)
52 |
53 | let dateFromTestClass = NSDate.dateWithISO8601String(dateString: dateString)
54 |
55 | XCTAssertEqual(dateFromTestClass, date as NSDate?)
56 | }
57 |
58 | func testDateWithISO8601StringWithSuffix() {
59 | let dateFormatter = DateFormatter()
60 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
61 | let date = dateFormatter.date(from: "1990-05-14T12:05:12.003-00:00")
62 |
63 | let dateFromTestClass = NSDate.dateWithISO8601String(dateString: "1990-05-14T12:05:12.003Z")
64 |
65 | XCTAssertEqual(dateFromTestClass, date as NSDate?)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/twiliochat/TokenRequestHandler.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class TokenRequestHandler {
4 |
5 | class func postDataFrom(params:[String:String]) -> String {
6 | var data = ""
7 |
8 | for (key, value) in params {
9 | if let encodedKey = key.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
10 | let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) {
11 | if !data.isEmpty {
12 | data = data + "&"
13 | }
14 | data += encodedKey + "=" + encodedValue;
15 | }
16 | }
17 |
18 | return data
19 | }
20 |
21 | class func fetchToken(params:[String:String], completion:@escaping (NSDictionary, NSError?) -> Void) {
22 | if let filePath = Bundle.main.path(forResource: "Keys", ofType:"plist"),
23 | let dictionary = NSDictionary(contentsOfFile:filePath) as? [String: AnyObject],
24 | let tokenRequestUrl = dictionary["TokenRequestUrl"] as? String {
25 |
26 | var request = URLRequest(url: URL(string: tokenRequestUrl)!)
27 | request.httpMethod = "POST"
28 | let postString = self.postDataFrom(params: params)
29 | request.httpBody = postString.data(using: .utf8)
30 | let task = URLSession.shared.dataTask(with: request) { data, response, error in
31 | guard let data = data, error == nil else {
32 | print("error=\(String(describing: error))")
33 | completion(NSDictionary(), error as NSError?)
34 | return
35 | }
36 |
37 | if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
38 | completion(NSDictionary(), NSError(domain: "TWILIO", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Incorrect return code for token request."]))
39 | return
40 | }
41 |
42 | do {
43 | let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String:Any]
44 | print("json = \(json)")
45 | completion(json as NSDictionary, error as NSError?)
46 | } catch let error as NSError {
47 | completion(NSDictionary(), error)
48 | }
49 | }
50 | task.resume()
51 | }
52 | else {
53 | let userInfo = [NSLocalizedDescriptionKey : "TokenRequestUrl Key is missing"]
54 | let error = NSError(domain: "app", code: 404, userInfo: userInfo)
55 |
56 | completion(NSDictionary(), error)
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/twiliochat/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 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Important Notice
2 |
3 | We intend to sunset the Programmable Chat API on July 25, 2022 to focus on the next generation of chat: the [Twilio Conversations API](https://www.twilio.com/docs/conversations). Find out about the [EOL process](https://www.twilio.com/changelog/programmable-chat-end-of-life). We have also prepared [this Migration Guide](https://www.twilio.com/docs/conversations/migrating-chat-conversations) to assist in the transition from Chat to Conversations.
4 |
5 | # Twilio Chat Tutorial for Swift
6 | [](https://travis-ci.org/TwilioDevEd/twiliochat-swift)
7 |
8 | Swift implementation of Twilio Chat
9 |
10 | ### Running the Application
11 |
12 | 1. Clone the repository and `cd` into it
13 | 1. Install the application's dependencies with [CocoaPods](https://cocoapods.org/)
14 |
15 | ```bash
16 | $ pod install
17 | ```
18 | 1. Open the project with `Xcode` but don't use `twiliochat.xcodeproj` file. For
19 | CocoaPods dependencies to work you must use `twiliochat.xcworkspace`.
20 | 1. [Twilio Chat](https://www.twilio.com/docs/chat) requires an
21 | [access token](https://www.twilio.com/docs/chat/identity) generated using your
22 | Twilio credentials in order to connect. First we need to setup a server that will generate this token
23 | for the mobile application to use. We have created web versions of Twilio Chat, you can use any of these
24 | applications to generate the token that this mobile app requires. Just pick your favorite flavor:
25 |
26 | * [PHP - Laravel](https://github.com/TwilioDevEd/twiliochat-laravel)
27 | * [C# - .NET MVC](https://github.com/TwilioDevEd/twiliochat-csharp)
28 | * [Java - Servlets](https://github.com/TwilioDevEd/twiliochat-servlets)
29 | * [JS - Node](https://github.com/TwilioDevEd/twiliochat-node)
30 |
31 | Look for instructions on how to setup these servers in any of the links above.
32 |
33 | 1. Once you have the server running (from the previous step), you need to edit one
34 | file in the Xcode project.
35 |
36 | ```
37 | ProjectRoot -> twiliochat -> resources -> Keys.plist
38 | ```
39 | This file contains the `TokenRequestUrl` key. The default value is `http://localhost:8000/token`. This
40 | address refers to the host machine loopback interface when running this application
41 | in the iOS simulator. You must change this value to match the address of your server running
42 | the token generation application. We are using the [PHP - Laravel](https://github.com/TwilioDevEd/twiliochat-laravel)
43 | version in this case, that's why we use port 8000.
44 |
45 | ***Note:*** In some operating systems you need to specify the address for the development server
46 | when you run the Laravel application, here's an example:
47 |
48 | ```
49 | $ php artisan serve --host=127.0.0.1
50 | ```
51 |
52 | 1. Now Twilio Chat is ready to go. Run the application on the simulator or your own device, just
53 | make sure that you have properly set up the token generation server and the `TokenRequestUrl` key.
54 | To run the application in a real device you'll need to expose your local token generation server
55 | by manually forwarding ports, or using a tool like [ngrok](https://ngrok.com/).
56 | If you decide to work with ngrok, your Keys.plist file should hold a key like this one:
57 |
58 | ```
59 | TokenRequestUrl -> http://.ngrok.io/token
60 | ```
61 | No need to specify the port in this url, as ngrok will forward the request to the specified port.
62 |
--------------------------------------------------------------------------------
/twiliochat/LoginViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class LoginViewController: UIViewController {
4 | @IBOutlet weak var loginButton: UIButton!
5 | @IBOutlet weak var usernameTextField: UITextField!
6 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
7 |
8 | // MARK: - Injectable Properties
9 |
10 | var alertDialogControllerClass = AlertDialogController.self
11 | var MessagingClientClass = MessagingManager.self
12 |
13 | // MARK: - Initialization
14 |
15 | var textFieldFormHandler: TextFieldFormHandler!
16 |
17 | override func viewDidLoad() {
18 | super.viewDidLoad()
19 | initializeTextFields()
20 | }
21 |
22 | func initializeTextFields() {
23 | let textFields: [UITextField] = [usernameTextField]
24 | textFieldFormHandler = TextFieldFormHandler(withTextFields: textFields, topContainer: view)
25 | textFieldFormHandler.delegate = self
26 | }
27 |
28 | func resetFirstResponderOnSignUpModeChange() {
29 | self.view.layoutSubviews()
30 |
31 | if let index = self.textFieldFormHandler.firstResponderIndex {
32 | if (index > 1) {
33 | textFieldFormHandler.setTextFieldAtIndexAsFirstResponder(index: 1)
34 | }
35 | else {
36 | textFieldFormHandler.resetScroll()
37 | }
38 | }
39 | }
40 |
41 | override func viewWillDisappear(_ animated: Bool) {
42 | super.viewWillDisappear(animated)
43 | textFieldFormHandler.cleanUp()
44 | }
45 |
46 | override func didReceiveMemoryWarning() {
47 | super.didReceiveMemoryWarning()
48 | }
49 |
50 | // MARK: - Actions
51 |
52 | @IBAction func loginButtonTouched(_ sender: UIButton) {
53 | loginUser()
54 | }
55 |
56 | // MARK: - Login
57 |
58 | func loginUser() {
59 | if (validUserData()) {
60 | view.isUserInteractionEnabled = false
61 | activityIndicator.startAnimating()
62 |
63 | let MessagingManager = MessagingClientClass.sharedManager()
64 | if let username = usernameTextField.text {
65 | MessagingManager.loginWithUsername(username: username, completion: handleResponse)
66 | }
67 | }
68 | }
69 |
70 | func validUserData() -> Bool {
71 | if let usernameEmpty = usernameTextField.text?.isEmpty, !usernameEmpty {
72 | return true
73 | }
74 | showError(message: "All fields are required")
75 | return false
76 | }
77 |
78 | func showError(message:String) {
79 | alertDialogControllerClass.showAlertWithMessage(message: message, title: nil, presenter: self)
80 | }
81 |
82 | func handleResponse(succeeded: Bool, error: NSError?) {
83 | DispatchQueue.main.async {
84 | self.activityIndicator.stopAnimating()
85 | if let error = error, !succeeded {
86 | self.showError(message: error.localizedDescription)
87 | }
88 | self.view.isUserInteractionEnabled = true
89 | }
90 | }
91 |
92 | // MARK: - Style
93 |
94 | override var preferredStatusBarStyle: UIStatusBarStyle {
95 | return .lightContent
96 | }
97 |
98 | override var shouldAutorotate: Bool {
99 | return true
100 | }
101 |
102 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
103 | if (UI_USER_INTERFACE_IDIOM() == .pad) {
104 | return .all
105 | }
106 | return .portrait
107 | }
108 |
109 | override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
110 | return .portrait
111 | }
112 | }
113 |
114 | // MARK: - TextFieldFormHandlerDelegate
115 | extension LoginViewController : TextFieldFormHandlerDelegate {
116 | func textFieldFormHandlerDoneEnteringData(handler: TextFieldFormHandler) {
117 | loginUser()
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/twiliochat.xcworkspace/xcshareddata/xcschemes/twiliochat.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
43 |
49 |
50 |
51 |
52 |
53 |
59 |
60 |
61 |
62 |
63 |
64 |
74 |
76 |
82 |
83 |
84 |
85 |
86 |
87 |
93 |
95 |
101 |
102 |
103 |
104 |
106 |
107 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/twiliochat/ChatStatusTableCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
--------------------------------------------------------------------------------
/twiliochat/ChatTableCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
30 |
36 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/twiliochat/TextFieldFormHandler.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @objc public protocol TextFieldFormHandlerDelegate {
4 | @objc optional func textFieldFormHandlerDoneEnteringData(handler: TextFieldFormHandler)
5 | }
6 |
7 | public class TextFieldFormHandler: NSObject {
8 | var textFields: [UITextField]!
9 | var keyboardSize: CGFloat = 0
10 | var animationOffset: CGFloat = 0
11 | var topContainer: UIView!
12 |
13 | var _lastTextField: UITextField?
14 | var lastTextField: UITextField? {
15 | get {
16 | return _lastTextField
17 | }
18 | set (newValue){
19 | if let textField = _lastTextField {
20 | setTextField(textField: textField, returnKeyType: .next)
21 | }
22 | _lastTextField = newValue
23 |
24 | if let textField = newValue {
25 | setTextField(textField: textField, returnKeyType: .done)
26 | }
27 | else if let textField = textFields.last {
28 | setTextField(textField: textField, returnKeyType: .done)
29 | }
30 | }
31 | }
32 |
33 | public weak var delegate: TextFieldFormHandlerDelegate?
34 |
35 | public var firstResponderIndex: Int? {
36 | if let firstResponder = self.firstResponder {
37 | return self.textFields.firstIndex(of: firstResponder)
38 | }
39 | return nil
40 | }
41 |
42 | public var firstResponder: UITextField? {
43 | return self.textFields.filter { textField in textField.isFirstResponder }.first
44 | }
45 |
46 | // MARK: - Initialization
47 |
48 | init(withTextFields textFields: [UITextField], topContainer: UIView) {
49 | super.init()
50 | self.textFields = textFields
51 | self.topContainer = topContainer
52 | initializeTextFields()
53 | initializeObservers()
54 | }
55 |
56 | func initializeTextFields() {
57 | for (index, textField) in self.textFields.enumerated() {
58 | textField.delegate = self
59 | setTextField(textField: textField, returnKeyType: (index == self.textFields.count - 1 ? .done : .next))
60 | }
61 | }
62 |
63 | func initializeObservers() {
64 | NotificationCenter.default.addObserver(self, selector: #selector(TextFieldFormHandler.keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
65 | let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(TextFieldFormHandler.backgroundTap(sender:)))
66 | self.topContainer.addGestureRecognizer(tapRecognizer)
67 | }
68 |
69 | // MARK: - Public Methods
70 |
71 | public func resetScroll() {
72 | if let firstResponder = self.firstResponder {
73 | setAnimationOffsetForTextField(textField: firstResponder)
74 | moveScreenUp()
75 | }
76 | }
77 |
78 | public func setTextFieldAtIndexAsFirstResponder(index:Int) {
79 | textFields[index].becomeFirstResponder()
80 | }
81 |
82 | public func cleanUp() {
83 | NotificationCenter.default.removeObserver(self)
84 | }
85 |
86 | // MARK: - Private Methods
87 |
88 | func doneEnteringData() {
89 | topContainer.endEditing(true)
90 | moveScreenDown()
91 | delegate?.textFieldFormHandlerDoneEnteringData?(handler: self)
92 | }
93 |
94 | func moveScreenUp() {
95 | shiftScreenYPosition(position: -keyboardSize - animationOffset, duration: 0.3, curve: .easeInOut)
96 | }
97 |
98 | func moveScreenDown() {
99 | shiftScreenYPosition(position: 0, duration: 0.2, curve: .easeInOut)
100 | }
101 |
102 | func shiftScreenYPosition(position: CGFloat, duration: TimeInterval, curve: UIView.AnimationCurve) {
103 | UIView.beginAnimations("moveView", context: nil)
104 | UIView.setAnimationCurve(curve)
105 | UIView.setAnimationDuration(duration)
106 |
107 | topContainer.frame.origin.y = position
108 | UIView.commitAnimations()
109 | }
110 |
111 | @objc func backgroundTap(sender: UITapGestureRecognizer) {
112 | topContainer.endEditing(true)
113 | moveScreenDown()
114 | }
115 |
116 | @objc func keyboardWillShow(notification: NSNotification) {
117 | if (keyboardSize == 0) {
118 | if let keyboardRect = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
119 | keyboardSize = min(keyboardRect.height, keyboardRect.width)
120 | }
121 | }
122 | moveScreenUp()
123 | }
124 |
125 | func setAnimationOffsetForTextField(textField: UITextField) {
126 | let screenHeight = UIScreen.main.bounds.size.height
127 | let textFieldHeight = textField.frame.size.height
128 | let textFieldY = textField.convert(CGPoint.zero, to: topContainer).y
129 |
130 | animationOffset = -screenHeight + textFieldY + textFieldHeight
131 | }
132 |
133 | func setTextField(textField: UITextField, returnKeyType type: UIReturnKeyType) {
134 | if (textField.isFirstResponder) {
135 | textField.resignFirstResponder()
136 | textField.returnKeyType = type
137 | textField.becomeFirstResponder()
138 | }
139 | else {
140 | textField.returnKeyType = type
141 | }
142 | }
143 |
144 | deinit {
145 | cleanUp()
146 | }
147 | }
148 |
149 | extension TextFieldFormHandler : UITextFieldDelegate {
150 | public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
151 | if let lastTextField = self.lastTextField, textField == lastTextField {
152 | doneEnteringData()
153 | return true
154 | }
155 | else if let lastTextField = self.textFields.last, lastTextField == textField {
156 | doneEnteringData()
157 | return true
158 | }
159 |
160 | let index = self.textFields.firstIndex(of: textField)
161 | let nextTextField = self.textFields[index! + 1]
162 | nextTextField.becomeFirstResponder()
163 | return true
164 | }
165 |
166 | public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
167 | setAnimationOffsetForTextField(textField: textField)
168 | return true
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/twiliochat/ChannelManager.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | protocol ChannelManagerDelegate {
4 | func reloadChannelDescriptorList()
5 | }
6 |
7 | class ChannelManager: NSObject {
8 | static let sharedManager = ChannelManager()
9 |
10 | static let defaultChannelUniqueName = "general"
11 | static let defaultChannelName = "General Channel"
12 |
13 | var delegate:ChannelManagerDelegate?
14 |
15 | var channelsList:TCHChannels?
16 | var channelDescriptors:NSOrderedSet?
17 | var generalChannel:TCHChannel!
18 |
19 | override init() {
20 | super.init()
21 | channelDescriptors = NSMutableOrderedSet()
22 | }
23 |
24 | // MARK: - General channel
25 |
26 | func joinGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {
27 |
28 | let uniqueName = ChannelManager.defaultChannelUniqueName
29 | if let channelsList = self.channelsList {
30 | channelsList.channel(withSidOrUniqueName: uniqueName) { result, channel in
31 | self.generalChannel = channel
32 |
33 | if self.generalChannel != nil {
34 | self.joinGeneralChatRoomWithUniqueName(name: nil, completion: completion)
35 | } else {
36 | self.createGeneralChatRoomWithCompletion { succeeded in
37 | if (succeeded) {
38 | self.joinGeneralChatRoomWithUniqueName(name: uniqueName, completion: completion)
39 | return
40 | }
41 |
42 | completion(false)
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
49 | func joinGeneralChatRoomWithUniqueName(name: String?, completion: @escaping (Bool) -> Void) {
50 | generalChannel.join { result in
51 | if ((result.isSuccessful()) && name != nil) {
52 | self.setGeneralChatRoomUniqueNameWithCompletion(completion: completion)
53 | return
54 | }
55 | completion((result.isSuccessful()))
56 | }
57 | }
58 |
59 | func createGeneralChatRoomWithCompletion(completion: @escaping (Bool) -> Void) {
60 | let channelName = ChannelManager.defaultChannelName
61 | let options = [
62 | TCHChannelOptionFriendlyName: channelName,
63 | TCHChannelOptionType: TCHChannelType.public.rawValue
64 | ] as [String : Any]
65 | channelsList!.createChannel(options: options) { result, channel in
66 | if (result.isSuccessful()) {
67 | self.generalChannel = channel
68 | }
69 | completion((result.isSuccessful()))
70 | }
71 | }
72 |
73 | func setGeneralChatRoomUniqueNameWithCompletion(completion:@escaping (Bool) -> Void) {
74 | generalChannel.setUniqueName(ChannelManager.defaultChannelUniqueName) { result in
75 | completion((result.isSuccessful()))
76 | }
77 | }
78 |
79 | // MARK: - Populate channel Descriptors
80 |
81 | func populateChannelDescriptors() {
82 |
83 | channelsList?.userChannelDescriptors { result, paginator in
84 | guard let paginator = paginator else {
85 | return
86 | }
87 |
88 | let newChannelDescriptors = NSMutableOrderedSet()
89 | newChannelDescriptors.addObjects(from: paginator.items())
90 | self.channelsList?.publicChannelDescriptors { result, paginator in
91 | guard let paginator = paginator else {
92 | return
93 | }
94 |
95 | // de-dupe channel list
96 | let channelIds = NSMutableSet()
97 | for descriptor in newChannelDescriptors {
98 | if let descriptor = descriptor as? TCHChannelDescriptor {
99 | if let sid = descriptor.sid {
100 | channelIds.add(sid)
101 | }
102 | }
103 | }
104 | for descriptor in paginator.items() {
105 | if let sid = descriptor.sid {
106 | if !channelIds.contains(sid) {
107 | channelIds.add(sid)
108 | newChannelDescriptors.add(descriptor)
109 | }
110 | }
111 | }
112 |
113 |
114 | // sort the descriptors
115 | let sortSelector = #selector(NSString.localizedCaseInsensitiveCompare(_:))
116 | let descriptor = NSSortDescriptor(key: "friendlyName", ascending: true, selector: sortSelector)
117 | newChannelDescriptors.sort(using: [descriptor])
118 |
119 | self.channelDescriptors = newChannelDescriptors
120 |
121 | if let delegate = self.delegate {
122 | delegate.reloadChannelDescriptorList()
123 | }
124 | }
125 | }
126 | }
127 |
128 |
129 | // MARK: - Create channel
130 |
131 | func createChannelWithName(name: String, completion: @escaping (Bool, TCHChannel?) -> Void) {
132 | if (name == ChannelManager.defaultChannelName) {
133 | completion(false, nil)
134 | return
135 | }
136 |
137 | let channelOptions = [
138 | TCHChannelOptionFriendlyName: name,
139 | TCHChannelOptionType: TCHChannelType.public.rawValue
140 | ] as [String : Any]
141 | UIApplication.shared.isNetworkActivityIndicatorVisible = true;
142 | self.channelsList?.createChannel(options: channelOptions) { result, channel in
143 | UIApplication.shared.isNetworkActivityIndicatorVisible = false
144 | completion((result.isSuccessful()), channel)
145 | }
146 | }
147 | }
148 |
149 | // MARK: - TwilioChatClientDelegate
150 | extension ChannelManager : TwilioChatClientDelegate {
151 | func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {
152 | DispatchQueue.main.async {
153 | self.populateChannelDescriptors()
154 | }
155 | }
156 |
157 | func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {
158 | DispatchQueue.main.async {
159 | self.delegate?.reloadChannelDescriptorList()
160 | }
161 | }
162 |
163 | func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
164 | DispatchQueue.main.async {
165 | self.populateChannelDescriptors()
166 | }
167 |
168 | }
169 |
170 | func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/twiliochat/MessagingManager.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class MessagingManager: NSObject {
4 |
5 | static let _sharedManager = MessagingManager()
6 |
7 | var client:TwilioChatClient?
8 | var delegate:ChannelManager?
9 | var connected = false
10 |
11 | var userIdentity:String {
12 | return SessionManager.getUsername()
13 | }
14 |
15 | var hasIdentity: Bool {
16 | return SessionManager.isLoggedIn()
17 | }
18 |
19 | override init() {
20 | super.init()
21 | delegate = ChannelManager.sharedManager
22 | }
23 |
24 | class func sharedManager() -> MessagingManager {
25 | return _sharedManager
26 | }
27 |
28 | func presentRootViewController() {
29 | if (!hasIdentity) {
30 | presentViewControllerByName(viewController: "LoginViewController")
31 | return
32 | }
33 |
34 | if (!connected) {
35 | connectClientWithCompletion { success, error in
36 | print("Delegate method will load views when sync is complete")
37 | if (!success || error != nil) {
38 | DispatchQueue.main.async {
39 | self.presentViewControllerByName(viewController: "LoginViewController")
40 | }
41 | }
42 | }
43 | return
44 | }
45 |
46 | presentViewControllerByName(viewController: "RevealViewController")
47 | }
48 |
49 | func presentViewControllerByName(viewController: String) {
50 | presentViewController(controller: storyBoardWithName(name: "Main").instantiateViewController(withIdentifier: viewController))
51 | }
52 |
53 | func presentLaunchScreen() {
54 | presentViewController(controller: storyBoardWithName(name: "LaunchScreen").instantiateInitialViewController()!)
55 | }
56 |
57 | func presentViewController(controller: UIViewController) {
58 | let window = UIApplication.shared.delegate!.window!!
59 | window.rootViewController = controller
60 | }
61 |
62 | func storyBoardWithName(name:String) -> UIStoryboard {
63 | return UIStoryboard(name:name, bundle: Bundle.main)
64 | }
65 |
66 | // MARK: User and session management
67 |
68 | func loginWithUsername(username: String,
69 | completion: @escaping (Bool, NSError?) -> Void) {
70 | SessionManager.loginWithUsername(username: username)
71 | connectClientWithCompletion(completion: completion)
72 | }
73 |
74 | func logout() {
75 | SessionManager.logout()
76 | DispatchQueue.global(qos: .userInitiated).async {
77 | self.client?.shutdown()
78 | self.client = nil
79 | }
80 | self.connected = false
81 | }
82 |
83 | // MARK: Twilio Client
84 |
85 | func loadGeneralChatRoomWithCompletion(completion:@escaping (Bool, NSError?) -> Void) {
86 | ChannelManager.sharedManager.joinGeneralChatRoomWithCompletion { succeeded in
87 | if succeeded {
88 | completion(succeeded, nil)
89 | }
90 | else {
91 | let error = self.errorWithDescription(description: "Could not join General channel", code: 300)
92 | completion(succeeded, error)
93 | }
94 | }
95 | }
96 |
97 | func connectClientWithCompletion(completion: @escaping (Bool, NSError?) -> Void) {
98 | if (client != nil) {
99 | logout()
100 | }
101 |
102 | requestTokenWithCompletion { succeeded, token in
103 | if let token = token, succeeded {
104 | self.initializeClientWithToken(token: token)
105 | completion(succeeded, nil)
106 | }
107 | else {
108 | let error = self.errorWithDescription(description: "Could not get access token", code:301)
109 | completion(succeeded, error)
110 | }
111 | }
112 | }
113 |
114 | func initializeClientWithToken(token: String) {
115 | DispatchQueue.main.async {
116 | UIApplication.shared.isNetworkActivityIndicatorVisible = true
117 | }
118 | TwilioChatClient.chatClient(withToken: token, properties: nil, delegate: self) { [weak self] result, chatClient in
119 | guard (result.isSuccessful()) else { return }
120 |
121 | UIApplication.shared.isNetworkActivityIndicatorVisible = true
122 | self?.connected = true
123 | self?.client = chatClient
124 | }
125 | }
126 |
127 | func requestTokenWithCompletion(completion:@escaping (Bool, String?) -> Void) {
128 | if let device = UIDevice.current.identifierForVendor?.uuidString {
129 | TokenRequestHandler.fetchToken(params: ["device": device, "identity":SessionManager.getUsername()]) {response,error in
130 | var token: String?
131 | token = response["token"] as? String
132 | completion(token != nil, token)
133 | }
134 | }
135 | }
136 |
137 | func errorWithDescription(description: String, code: Int) -> NSError {
138 | let userInfo = [NSLocalizedDescriptionKey : description]
139 | return NSError(domain: "app", code: code, userInfo: userInfo)
140 | }
141 | }
142 |
143 | // MARK: - TwilioChatClientDelegate
144 | extension MessagingManager : TwilioChatClientDelegate {
145 | func chatClient(_ client: TwilioChatClient, channelAdded channel: TCHChannel) {
146 | self.delegate?.chatClient(client, channelAdded: channel)
147 | }
148 |
149 | func chatClient(_ client: TwilioChatClient, channel: TCHChannel, updated: TCHChannelUpdate) {
150 | self.delegate?.chatClient(client, channel: channel, updated: updated)
151 | }
152 |
153 | func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
154 | self.delegate?.chatClient(client, channelDeleted: channel)
155 | }
156 |
157 | func chatClient(_ client: TwilioChatClient, synchronizationStatusUpdated status: TCHClientSynchronizationStatus) {
158 | if status == TCHClientSynchronizationStatus.completed {
159 | UIApplication.shared.isNetworkActivityIndicatorVisible = false
160 | ChannelManager.sharedManager.channelsList = client.channelsList()
161 | ChannelManager.sharedManager.populateChannelDescriptors()
162 | loadGeneralChatRoomWithCompletion { success, error in
163 | if success {
164 | self.presentRootViewController()
165 | }
166 | }
167 | }
168 | self.delegate?.chatClient(client, synchronizationStatusUpdated: status)
169 | }
170 |
171 | func chatClientTokenWillExpire(_ client: TwilioChatClient) {
172 | requestTokenWithCompletion { succeeded, token in
173 | if (succeeded) {
174 | client.updateToken(token!)
175 | }
176 | else {
177 | print("Error while trying to get new access token")
178 | }
179 | }
180 | }
181 |
182 | func chatClientTokenExpired(_ client: TwilioChatClient) {
183 | requestTokenWithCompletion { succeeded, token in
184 | if (succeeded) {
185 | client.updateToken(token!)
186 | }
187 | else {
188 | print("Error while trying to get new access token")
189 | }
190 | }
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/twiliochat/MenuViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | class MenuViewController: UIViewController {
4 | static let TWCOpenChannelSegue = "OpenChat"
5 | static let TWCRefreshControlXOffset: CGFloat = 120
6 |
7 | @IBOutlet weak var tableView: UITableView!
8 | @IBOutlet weak var usernameLabel: UILabel!
9 |
10 | var refreshControl: UIRefreshControl!
11 |
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 |
15 | let bgImage = UIImageView(image: UIImage(named:"home-bg"))
16 | bgImage.frame = self.tableView.frame
17 | tableView.backgroundView = bgImage
18 |
19 | usernameLabel.text = MessagingManager.sharedManager().userIdentity
20 |
21 | refreshControl = UIRefreshControl()
22 | tableView.addSubview(refreshControl)
23 | refreshControl.addTarget(self, action: #selector(MenuViewController.refreshChannels), for: .valueChanged)
24 | refreshControl.tintColor = UIColor.white
25 |
26 | self.refreshControl.frame.origin.x -= MenuViewController.TWCRefreshControlXOffset
27 | ChannelManager.sharedManager.delegate = self
28 | tableView.reloadData()
29 | }
30 |
31 | // MARK: - Internal methods
32 |
33 | func loadingCellForTableView(tableView: UITableView) -> UITableViewCell {
34 | return tableView.dequeueReusableCell(withIdentifier: "loadingCell")!
35 | }
36 |
37 | func channelCellForTableView(tableView: UITableView, atIndexPath indexPath: NSIndexPath) -> UITableViewCell {
38 | let menuCell = tableView.dequeueReusableCell(withIdentifier: "channelCell", for: indexPath as IndexPath) as! MenuTableCell
39 |
40 | if let channelDescriptor = ChannelManager.sharedManager.channelDescriptors![indexPath.row] as? TCHChannelDescriptor {
41 | menuCell.channelName = channelDescriptor.friendlyName ?? "[Unknown channel name]"
42 | } else {
43 | menuCell.channelName = "[Unknown channel name]"
44 | }
45 |
46 | return menuCell
47 | }
48 |
49 | @objc func refreshChannels() {
50 | refreshControl.beginRefreshing()
51 | tableView.reloadData()
52 | refreshControl.endRefreshing()
53 | }
54 |
55 | func deselectSelectedChannel() {
56 | let selectedRow = tableView.indexPathForSelectedRow
57 | if let row = selectedRow {
58 | tableView.deselectRow(at: row, animated: true)
59 | }
60 | }
61 |
62 | // MARK: - Channel
63 |
64 | func createNewChannelDialog() {
65 | InputDialogController.showWithTitle(title: "New Channel",
66 | message: "Enter a name for this channel",
67 | placeholder: "Name",
68 | presenter: self) { text in
69 | ChannelManager.sharedManager.createChannelWithName(name: text, completion: { _,_ in
70 | ChannelManager.sharedManager.populateChannelDescriptors()
71 | })
72 | }
73 | }
74 |
75 | // MARK: Logout
76 |
77 | func promptLogout() {
78 | let alert = UIAlertController(title: nil, message: "You are about to Logout", preferredStyle: .alert)
79 |
80 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
81 | let confirmAction = UIAlertAction(title: "Confirm", style: .default) { action in
82 | self.logOut()
83 | }
84 |
85 | alert.addAction(cancelAction)
86 | alert.addAction(confirmAction)
87 | present(alert, animated: true, completion: nil)
88 | }
89 |
90 | func logOut() {
91 | MessagingManager.sharedManager().logout()
92 | MessagingManager.sharedManager().presentRootViewController()
93 | }
94 |
95 | // MARK: - Actions
96 |
97 | @IBAction func logoutButtonTouched(_ sender: UIButton) {
98 | promptLogout()
99 | }
100 |
101 | @IBAction func newChannelButtonTouched(_ sender: UIButton) {
102 | createNewChannelDialog()
103 | }
104 |
105 | // MARK: - Navigation
106 |
107 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
108 | if segue.identifier == MenuViewController.TWCOpenChannelSegue {
109 | let indexPath = sender as! NSIndexPath
110 |
111 | let channelDescriptor = ChannelManager.sharedManager.channelDescriptors![indexPath.row] as! TCHChannelDescriptor
112 | let navigationController = segue.destination as! UINavigationController
113 |
114 | channelDescriptor.channel { (result, channel) in
115 | if let channel = channel {
116 | (navigationController.visibleViewController as! MainChatViewController).channel = channel
117 | }
118 | }
119 |
120 | }
121 | }
122 |
123 | // MARK: - Style
124 |
125 | override var preferredStatusBarStyle: UIStatusBarStyle {
126 | return .lightContent
127 | }
128 | }
129 |
130 | // MARK: - UITableViewDataSource
131 | extension MenuViewController : UITableViewDataSource {
132 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
133 | if let channelDescriptors = ChannelManager.sharedManager.channelDescriptors {
134 | print (channelDescriptors.count)
135 | return channelDescriptors.count
136 | }
137 | return 1
138 | }
139 |
140 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
141 | let cell: UITableViewCell
142 |
143 | if ChannelManager.sharedManager.channelDescriptors == nil {
144 | cell = loadingCellForTableView(tableView: tableView)
145 | }
146 | else {
147 | cell = channelCellForTableView(tableView: tableView, atIndexPath: indexPath as NSIndexPath)
148 | }
149 |
150 | cell.layoutIfNeeded()
151 | return cell
152 | }
153 |
154 | func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
155 | if let channel = ChannelManager.sharedManager.channelDescriptors?.object(at: indexPath.row) as? TCHChannel {
156 | return channel != ChannelManager.sharedManager.generalChannel
157 | }
158 | return false
159 | }
160 |
161 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle,
162 | forRowAt indexPath: IndexPath) {
163 | if editingStyle != .delete {
164 | return
165 | }
166 | if let channel = ChannelManager.sharedManager.channelDescriptors?.object(at: indexPath.row) as? TCHChannel {
167 | channel.destroy { result in
168 | if (result.isSuccessful()) {
169 | tableView.reloadData()
170 | }
171 | else {
172 | AlertDialogController.showAlertWithMessage(message: "You can not delete this channel", title: nil, presenter: self)
173 | }
174 | }
175 | }
176 | }
177 | }
178 |
179 | // MARK: - UITableViewDelegate
180 | extension MenuViewController : UITableViewDelegate {
181 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
182 | tableView.deselectRow(at: indexPath, animated: true)
183 | performSegue(withIdentifier: MenuViewController.TWCOpenChannelSegue, sender: indexPath)
184 | }
185 | }
186 |
187 |
188 | // MARK: - ChannelManagerDelegate
189 | extension MenuViewController : ChannelManagerDelegate {
190 | func reloadChannelDescriptorList() {
191 | tableView.reloadData()
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/twiliochat/MainChatViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SlackTextViewController
3 |
4 | class MainChatViewController: SLKTextViewController {
5 | static let TWCChatCellIdentifier = "ChatTableCell"
6 | static let TWCChatStatusCellIdentifier = "ChatStatusTableCell"
7 |
8 | static let TWCOpenGeneralChannelSegue = "OpenGeneralChat"
9 | static let TWCLabelTag = 200
10 |
11 | var _channel:TCHChannel!
12 | var channel:TCHChannel! {
13 | get {
14 | return _channel
15 | }
16 | set(channel) {
17 | _channel = channel
18 | title = _channel.friendlyName
19 | _channel.delegate = self
20 |
21 | if _channel == ChannelManager.sharedManager.generalChannel {
22 | navigationItem.rightBarButtonItem = nil
23 | }
24 |
25 | joinChannel()
26 | }
27 | }
28 |
29 | var messages:Set = Set()
30 | var sortedMessages:[TCHMessage]!
31 |
32 | @IBOutlet weak var revealButtonItem: UIBarButtonItem!
33 | @IBOutlet weak var actionButtonItem: UIBarButtonItem!
34 |
35 | override func viewDidLoad() {
36 | super.viewDidLoad()
37 |
38 | if (revealViewController() != nil) {
39 | revealButtonItem.target = revealViewController()
40 | revealButtonItem.action = #selector(SWRevealViewController.revealToggle(_:))
41 | navigationController?.navigationBar.addGestureRecognizer(revealViewController().panGestureRecognizer())
42 | revealViewController().rearViewRevealOverdraw = 0
43 | }
44 |
45 | bounces = true
46 | shakeToClearEnabled = true
47 | isKeyboardPanningEnabled = true
48 | shouldScrollToBottomAfterKeyboardShows = false
49 | isInverted = true
50 |
51 | let cellNib = UINib(nibName: MainChatViewController.TWCChatCellIdentifier, bundle: nil)
52 | tableView!.register(cellNib, forCellReuseIdentifier:MainChatViewController.TWCChatCellIdentifier)
53 |
54 | let cellStatusNib = UINib(nibName: MainChatViewController.TWCChatStatusCellIdentifier, bundle: nil)
55 | tableView!.register(cellStatusNib, forCellReuseIdentifier:MainChatViewController.TWCChatStatusCellIdentifier)
56 |
57 | textInputbar.autoHideRightButton = true
58 | textInputbar.maxCharCount = 256
59 | textInputbar.counterStyle = .split
60 | textInputbar.counterPosition = .top
61 |
62 | let font = UIFont(name:"Avenir-Light", size:14)
63 | textView.font = font
64 |
65 | rightButton.setTitleColor(UIColor(red:0.973, green:0.557, blue:0.502, alpha:1), for: .normal)
66 |
67 | if let font = UIFont(name:"Avenir-Heavy", size:17) {
68 | navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.font: font]
69 | }
70 |
71 | tableView!.allowsSelection = false
72 | tableView!.estimatedRowHeight = 70
73 | tableView!.rowHeight = UITableView.automaticDimension
74 | tableView!.separatorStyle = .none
75 |
76 | if channel == nil {
77 | channel = ChannelManager.sharedManager.generalChannel
78 | }
79 | }
80 |
81 | override func viewDidLayoutSubviews() {
82 | super.viewDidLayoutSubviews()
83 |
84 | // required for iOS 11
85 | textInputbar.bringSubviewToFront(textInputbar.textView)
86 | textInputbar.bringSubviewToFront(textInputbar.leftButton)
87 | textInputbar.bringSubviewToFront(textInputbar.rightButton)
88 |
89 | }
90 |
91 | override func viewDidAppear(_ animated: Bool) {
92 | super.viewDidAppear(animated)
93 | scrollToBottom()
94 | }
95 |
96 | override func numberOfSections(in tableView: UITableView) -> Int {
97 | return 1
98 | }
99 |
100 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: NSInteger) -> Int {
101 | return messages.count
102 | }
103 |
104 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
105 | var cell:UITableViewCell
106 |
107 | let message = sortedMessages[indexPath.row]
108 |
109 | if let statusMessage = message as? StatusMessage {
110 | cell = getStatusCellForTableView(tableView: tableView, forIndexPath:indexPath, message:statusMessage)
111 | }
112 | else {
113 | cell = getChatCellForTableView(tableView: tableView, forIndexPath:indexPath, message:message)
114 | }
115 |
116 | cell.transform = tableView.transform
117 | return cell
118 | }
119 |
120 | func getChatCellForTableView(tableView: UITableView, forIndexPath indexPath:IndexPath, message: TCHMessage) -> UITableViewCell {
121 | let cell = tableView.dequeueReusableCell(withIdentifier: MainChatViewController.TWCChatCellIdentifier, for:indexPath as IndexPath)
122 |
123 | let chatCell: ChatTableCell = cell as! ChatTableCell
124 | let date = NSDate.dateWithISO8601String(dateString: message.dateCreated ?? "")
125 | let timestamp = DateTodayFormatter().stringFromDate(date: date)
126 |
127 | chatCell.setUser(user: message.author ?? "[Unknown author]", message: message.body, date: timestamp ?? "[Unknown date]")
128 |
129 | return chatCell
130 | }
131 |
132 | func getStatusCellForTableView(tableView: UITableView, forIndexPath indexPath:IndexPath, message: StatusMessage) -> UITableViewCell {
133 | let cell = tableView.dequeueReusableCell(withIdentifier: MainChatViewController.TWCChatStatusCellIdentifier, for:indexPath as IndexPath)
134 |
135 | let label = cell.viewWithTag(MainChatViewController.TWCLabelTag) as! UILabel
136 | let memberStatus = (message.status! == .Joined) ? "joined" : "left"
137 | label.text = "User \(message.statusMember.identity ?? "[Unknown user]") has \(memberStatus)"
138 | return cell
139 | }
140 |
141 | func joinChannel() {
142 | setViewOnHold(onHold: true)
143 |
144 | if channel.status != .joined {
145 | channel.join { result in
146 | print("Channel Joined")
147 | }
148 | return
149 | }
150 |
151 | loadMessages()
152 | setViewOnHold(onHold: false)
153 | }
154 |
155 | // Disable user input and show activity indicator
156 | func setViewOnHold(onHold: Bool) {
157 | self.isTextInputbarHidden = onHold;
158 | UIApplication.shared.isNetworkActivityIndicatorVisible = onHold;
159 | }
160 |
161 | override func didPressRightButton(_ sender: Any!) {
162 | textView.refreshFirstResponder()
163 | sendMessage(inputMessage: textView.text)
164 | super.didPressRightButton(sender)
165 | }
166 |
167 | // MARK: - Chat Service
168 |
169 | func sendMessage(inputMessage: String) {
170 | let messageOptions = TCHMessageOptions().withBody(inputMessage)
171 | channel.messages?.sendMessage(with: messageOptions, completion: nil)
172 | }
173 |
174 | func addMessages(newMessages:Set) {
175 | messages = messages.union(newMessages)
176 | sortMessages()
177 | DispatchQueue.main.async {
178 | self.tableView!.reloadData()
179 | if self.messages.count > 0 {
180 | self.scrollToBottom()
181 | }
182 | }
183 | }
184 |
185 | func sortMessages() {
186 | sortedMessages = messages.sorted { (a, b) -> Bool in
187 | (a.dateCreated ?? "") > (b.dateCreated ?? "")
188 | }
189 | }
190 |
191 | func loadMessages() {
192 | messages.removeAll()
193 | if channel.synchronizationStatus == .all {
194 | channel.messages?.getLastWithCount(100) { (result, items) in
195 | self.addMessages(newMessages: Set(items!))
196 | }
197 | }
198 | }
199 |
200 | func scrollToBottom() {
201 | if messages.count > 0 {
202 | let indexPath = IndexPath(row: 0, section: 0)
203 | tableView!.scrollToRow(at: indexPath, at: .bottom, animated: true)
204 | }
205 | }
206 |
207 | func leaveChannel() {
208 | channel.leave { result in
209 | if (result.isSuccessful()) {
210 | let menuViewController = self.revealViewController().rearViewController as! MenuViewController
211 | menuViewController.deselectSelectedChannel()
212 | self.revealViewController().rearViewController.performSegue(withIdentifier: MainChatViewController.TWCOpenGeneralChannelSegue, sender: nil)
213 | }
214 | }
215 | }
216 |
217 | // MARK: - Actions
218 |
219 | @IBAction func actionButtonTouched(_ sender: UIBarButtonItem) {
220 | leaveChannel()
221 | }
222 |
223 | @IBAction func revealButtonTouched(_ sender: AnyObject) {
224 | revealViewController().revealToggle(animated: true)
225 | }
226 | }
227 |
228 | extension MainChatViewController : TCHChannelDelegate {
229 | func chatClient(_ client: TwilioChatClient, channel: TCHChannel, messageAdded message: TCHMessage) {
230 | if !messages.contains(message) {
231 | addMessages(newMessages: [message])
232 | }
233 | }
234 |
235 | func chatClient(_ client: TwilioChatClient, channel: TCHChannel, memberJoined member: TCHMember) {
236 | addMessages(newMessages: [StatusMessage(statusMember:member, status:.Joined)])
237 | }
238 |
239 | func chatClient(_ client: TwilioChatClient, channel: TCHChannel, memberLeft member: TCHMember) {
240 | addMessages(newMessages: [StatusMessage(statusMember:member, status:.Left)])
241 | }
242 |
243 | func chatClient(_ client: TwilioChatClient, channelDeleted channel: TCHChannel) {
244 | DispatchQueue.main.async {
245 | if channel == self.channel {
246 | self.revealViewController().rearViewController
247 | .performSegue(withIdentifier: MainChatViewController.TWCOpenGeneralChannelSegue, sender: nil)
248 | }
249 | }
250 | }
251 |
252 | func chatClient(_ client: TwilioChatClient,
253 | channel: TCHChannel,
254 | synchronizationStatusUpdated status: TCHChannelSynchronizationStatus) {
255 | if status == .all {
256 | loadMessages()
257 | DispatchQueue.main.async {
258 | self.tableView?.reloadData()
259 | self.setViewOnHold(onHold: false)
260 | }
261 | }
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/twiliochat.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 59A866DD1C1B5E8100525F07 /* MockAlertDialogController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A866DC1C1B5E8100525F07 /* MockAlertDialogController.swift */; };
11 | 59A866DF1C1B6C0900525F07 /* LoginViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59A866DE1C1B6C0900525F07 /* LoginViewControllerTests.swift */; };
12 | 59AE1CCE1C0C8C280008B10B /* TextFieldFormHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59AE1CCD1C0C8C280008B10B /* TextFieldFormHandler.swift */; };
13 | 59AE1CD21C0D03820008B10B /* MessagingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59AE1CD11C0D03820008B10B /* MessagingManager.swift */; };
14 | 59F208FE1C04C06800CC2392 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F208FD1C04C06800CC2392 /* AppDelegate.swift */; };
15 | 59F209031C04C06800CC2392 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 59F209011C04C06800CC2392 /* Main.storyboard */; };
16 | 59F209051C04C06800CC2392 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 59F209041C04C06800CC2392 /* Assets.xcassets */; };
17 | 59F209081C04C06800CC2392 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 59F209061C04C06800CC2392 /* LaunchScreen.storyboard */; };
18 | 59F209131C04C06800CC2392 /* DateTodayFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F209121C04C06800CC2392 /* DateTodayFormatterTests.swift */; };
19 | 59F2092F1C04C35E00CC2392 /* Keys.plist in Resources */ = {isa = PBXBuildFile; fileRef = 59F2092D1C04C35E00CC2392 /* Keys.plist */; };
20 | 59F209321C050EF700CC2392 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F209311C050EF700CC2392 /* LoginViewController.swift */; };
21 | 59F5EDB01C6E925500CE7D32 /* TokenRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F5EDAF1C6E925500CE7D32 /* TokenRequestHandler.swift */; };
22 | 59F5EDB31C720E1200CE7D32 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F5EDB21C720E1200CE7D32 /* SessionManager.swift */; };
23 | 877109151C10EF4F004C57D3 /* ChannelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877109141C10EF4F004C57D3 /* ChannelManager.swift */; };
24 | 877109171C15E55E004C57D3 /* MenuTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877109161C15E55E004C57D3 /* MenuTableCell.swift */; };
25 | 8771091A1C162FC8004C57D3 /* ChatStatusTableCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 877109181C162FC8004C57D3 /* ChatStatusTableCell.xib */; };
26 | 8771091B1C162FC8004C57D3 /* ChatTableCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 877109191C162FC8004C57D3 /* ChatTableCell.xib */; };
27 | 8771091D1C1638E9004C57D3 /* InputDialogController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8771091C1C1638E9004C57D3 /* InputDialogController.swift */; };
28 | 8771091F1C175AC2004C57D3 /* ChatTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8771091E1C175AC2004C57D3 /* ChatTableCell.swift */; };
29 | 877109211C1772C5004C57D3 /* StatusMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877109201C1772C5004C57D3 /* StatusMessage.swift */; };
30 | 877109231C1776FA004C57D3 /* DateTodayFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877109221C1776FA004C57D3 /* DateTodayFormatter.swift */; };
31 | 87F895F21C0CF3CE00754007 /* AlertDialogController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F895F11C0CF3CE00754007 /* AlertDialogController.swift */; };
32 | 87F895F61C0E044300754007 /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F895F51C0E044300754007 /* MenuViewController.swift */; };
33 | 87F895F81C0E045F00754007 /* MainChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F895F71C0E045F00754007 /* MainChatViewController.swift */; };
34 | 8A07B2CB1F224CAD009F0836 /* MockMessagingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A07B2CA1F224CAD009F0836 /* MockMessagingManager.swift */; };
35 | /* End PBXBuildFile section */
36 |
37 | /* Begin PBXContainerItemProxy section */
38 | 59F2090F1C04C06800CC2392 /* PBXContainerItemProxy */ = {
39 | isa = PBXContainerItemProxy;
40 | containerPortal = 59F208F21C04C06800CC2392 /* Project object */;
41 | proxyType = 1;
42 | remoteGlobalIDString = 59F208F91C04C06800CC2392;
43 | remoteInfo = twiliochat;
44 | };
45 | /* End PBXContainerItemProxy section */
46 |
47 | /* Begin PBXFileReference section */
48 | 59A866DC1C1B5E8100525F07 /* MockAlertDialogController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockAlertDialogController.swift; sourceTree = ""; };
49 | 59A866DE1C1B6C0900525F07 /* LoginViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewControllerTests.swift; sourceTree = ""; };
50 | 59AE1CCD1C0C8C280008B10B /* TextFieldFormHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldFormHandler.swift; sourceTree = ""; };
51 | 59AE1CD11C0D03820008B10B /* MessagingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagingManager.swift; sourceTree = ""; };
52 | 59F208FA1C04C06800CC2392 /* twiliochat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = twiliochat.app; sourceTree = BUILT_PRODUCTS_DIR; };
53 | 59F208FD1C04C06800CC2392 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
54 | 59F209021C04C06800CC2392 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
55 | 59F209041C04C06800CC2392 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
56 | 59F209071C04C06800CC2392 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
57 | 59F209091C04C06800CC2392 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
58 | 59F2090E1C04C06800CC2392 /* twiliochatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = twiliochatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
59 | 59F209121C04C06800CC2392 /* DateTodayFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTodayFormatterTests.swift; sourceTree = ""; };
60 | 59F209141C04C06800CC2392 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
61 | 59F2092D1C04C35E00CC2392 /* Keys.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Keys.plist; sourceTree = ""; };
62 | 59F209311C050EF700CC2392 /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; };
63 | 59F5EDAF1C6E925500CE7D32 /* TokenRequestHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenRequestHandler.swift; sourceTree = ""; };
64 | 59F5EDB21C720E1200CE7D32 /* SessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; };
65 | 825276F51DFB48EC0029E3CA /* twiliochat-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "twiliochat-Bridging-Header.h"; sourceTree = ""; };
66 | 877109141C10EF4F004C57D3 /* ChannelManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelManager.swift; sourceTree = ""; };
67 | 877109161C15E55E004C57D3 /* MenuTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuTableCell.swift; sourceTree = ""; };
68 | 877109181C162FC8004C57D3 /* ChatStatusTableCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ChatStatusTableCell.xib; sourceTree = ""; };
69 | 877109191C162FC8004C57D3 /* ChatTableCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ChatTableCell.xib; sourceTree = ""; };
70 | 8771091C1C1638E9004C57D3 /* InputDialogController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputDialogController.swift; sourceTree = ""; };
71 | 8771091E1C175AC2004C57D3 /* ChatTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTableCell.swift; sourceTree = ""; };
72 | 877109201C1772C5004C57D3 /* StatusMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusMessage.swift; sourceTree = ""; };
73 | 877109221C1776FA004C57D3 /* DateTodayFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateTodayFormatter.swift; sourceTree = ""; };
74 | 87F895F11C0CF3CE00754007 /* AlertDialogController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertDialogController.swift; sourceTree = ""; };
75 | 87F895F51C0E044300754007 /* MenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuViewController.swift; sourceTree = ""; };
76 | 87F895F71C0E045F00754007 /* MainChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainChatViewController.swift; sourceTree = ""; };
77 | 8A07B2CA1F224CAD009F0836 /* MockMessagingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMessagingManager.swift; sourceTree = ""; };
78 | /* End PBXFileReference section */
79 |
80 | /* Begin PBXFrameworksBuildPhase section */
81 | 59F208F71C04C06800CC2392 /* Frameworks */ = {
82 | isa = PBXFrameworksBuildPhase;
83 | buildActionMask = 2147483647;
84 | files = (
85 | );
86 | runOnlyForDeploymentPostprocessing = 0;
87 | };
88 | 59F2090B1C04C06800CC2392 /* Frameworks */ = {
89 | isa = PBXFrameworksBuildPhase;
90 | buildActionMask = 2147483647;
91 | files = (
92 | );
93 | runOnlyForDeploymentPostprocessing = 0;
94 | };
95 | /* End PBXFrameworksBuildPhase section */
96 |
97 | /* Begin PBXGroup section */
98 | 59A866D91C1B5D3700525F07 /* Mocks */ = {
99 | isa = PBXGroup;
100 | children = (
101 | 8A07B2CA1F224CAD009F0836 /* MockMessagingManager.swift */,
102 | 59A866DC1C1B5E8100525F07 /* MockAlertDialogController.swift */,
103 | );
104 | name = Mocks;
105 | sourceTree = "";
106 | };
107 | 59AE1CCF1C0C8C6D0008B10B /* util */ = {
108 | isa = PBXGroup;
109 | children = (
110 | 59AE1CCD1C0C8C280008B10B /* TextFieldFormHandler.swift */,
111 | 87F895F11C0CF3CE00754007 /* AlertDialogController.swift */,
112 | 8771091C1C1638E9004C57D3 /* InputDialogController.swift */,
113 | 877109221C1776FA004C57D3 /* DateTodayFormatter.swift */,
114 | 59F5EDAF1C6E925500CE7D32 /* TokenRequestHandler.swift */,
115 | );
116 | name = util;
117 | sourceTree = "";
118 | };
119 | 59AE1CD01C0D034D0008B10B /* messaging */ = {
120 | isa = PBXGroup;
121 | children = (
122 | 59AE1CD11C0D03820008B10B /* MessagingManager.swift */,
123 | );
124 | name = messaging;
125 | sourceTree = "";
126 | };
127 | 59F208F11C04C06800CC2392 = {
128 | isa = PBXGroup;
129 | children = (
130 | 59F208FC1C04C06800CC2392 /* twiliochat */,
131 | 59F209111C04C06800CC2392 /* twiliochatTests */,
132 | 59F208FB1C04C06800CC2392 /* Products */,
133 | 89D67A5C1B1630989E04019D /* Pods */,
134 | );
135 | sourceTree = "";
136 | };
137 | 59F208FB1C04C06800CC2392 /* Products */ = {
138 | isa = PBXGroup;
139 | children = (
140 | 59F208FA1C04C06800CC2392 /* twiliochat.app */,
141 | 59F2090E1C04C06800CC2392 /* twiliochatTests.xctest */,
142 | );
143 | name = Products;
144 | sourceTree = "";
145 | };
146 | 59F208FC1C04C06800CC2392 /* twiliochat */ = {
147 | isa = PBXGroup;
148 | children = (
149 | 59F5EDB11C720DD700CE7D32 /* session */,
150 | 877109101C10EEF0004C57D3 /* channels */,
151 | 59AE1CCF1C0C8C6D0008B10B /* util */,
152 | 59AE1CD01C0D034D0008B10B /* messaging */,
153 | 87F895F41C0E040E00754007 /* menu */,
154 | 87F895F31C0E033A00754007 /* chat */,
155 | 59F209301C050ED800CC2392 /* login */,
156 | 59F2092B1C04C33C00CC2392 /* resources */,
157 | 59F208FD1C04C06800CC2392 /* AppDelegate.swift */,
158 | 59F209011C04C06800CC2392 /* Main.storyboard */,
159 | 59F209041C04C06800CC2392 /* Assets.xcassets */,
160 | 59F209061C04C06800CC2392 /* LaunchScreen.storyboard */,
161 | 877109181C162FC8004C57D3 /* ChatStatusTableCell.xib */,
162 | 877109191C162FC8004C57D3 /* ChatTableCell.xib */,
163 | 825276F51DFB48EC0029E3CA /* twiliochat-Bridging-Header.h */,
164 | 59F209091C04C06800CC2392 /* Info.plist */,
165 | );
166 | path = twiliochat;
167 | sourceTree = "";
168 | };
169 | 59F209111C04C06800CC2392 /* twiliochatTests */ = {
170 | isa = PBXGroup;
171 | children = (
172 | 59A866D91C1B5D3700525F07 /* Mocks */,
173 | 59F209121C04C06800CC2392 /* DateTodayFormatterTests.swift */,
174 | 59F209141C04C06800CC2392 /* Info.plist */,
175 | 59A866DE1C1B6C0900525F07 /* LoginViewControllerTests.swift */,
176 | );
177 | path = twiliochatTests;
178 | sourceTree = "";
179 | };
180 | 59F2092B1C04C33C00CC2392 /* resources */ = {
181 | isa = PBXGroup;
182 | children = (
183 | 59F2092D1C04C35E00CC2392 /* Keys.plist */,
184 | );
185 | name = resources;
186 | sourceTree = "";
187 | };
188 | 59F209301C050ED800CC2392 /* login */ = {
189 | isa = PBXGroup;
190 | children = (
191 | 59F209311C050EF700CC2392 /* LoginViewController.swift */,
192 | );
193 | name = login;
194 | sourceTree = "";
195 | };
196 | 59F5EDB11C720DD700CE7D32 /* session */ = {
197 | isa = PBXGroup;
198 | children = (
199 | 59F5EDB21C720E1200CE7D32 /* SessionManager.swift */,
200 | );
201 | name = session;
202 | sourceTree = "";
203 | };
204 | 877109101C10EEF0004C57D3 /* channels */ = {
205 | isa = PBXGroup;
206 | children = (
207 | 877109141C10EF4F004C57D3 /* ChannelManager.swift */,
208 | );
209 | name = channels;
210 | sourceTree = "";
211 | };
212 | 87F895F31C0E033A00754007 /* chat */ = {
213 | isa = PBXGroup;
214 | children = (
215 | 87F895F71C0E045F00754007 /* MainChatViewController.swift */,
216 | 8771091E1C175AC2004C57D3 /* ChatTableCell.swift */,
217 | 877109201C1772C5004C57D3 /* StatusMessage.swift */,
218 | );
219 | name = chat;
220 | sourceTree = "";
221 | };
222 | 87F895F41C0E040E00754007 /* menu */ = {
223 | isa = PBXGroup;
224 | children = (
225 | 87F895F51C0E044300754007 /* MenuViewController.swift */,
226 | 877109161C15E55E004C57D3 /* MenuTableCell.swift */,
227 | );
228 | name = menu;
229 | sourceTree = "";
230 | };
231 | 89D67A5C1B1630989E04019D /* Pods */ = {
232 | isa = PBXGroup;
233 | children = (
234 | );
235 | path = Pods;
236 | sourceTree = "";
237 | };
238 | /* End PBXGroup section */
239 |
240 | /* Begin PBXNativeTarget section */
241 | 59F208F91C04C06800CC2392 /* twiliochat */ = {
242 | isa = PBXNativeTarget;
243 | buildConfigurationList = 59F209221C04C06800CC2392 /* Build configuration list for PBXNativeTarget "twiliochat" */;
244 | buildPhases = (
245 | 59F208F61C04C06800CC2392 /* Sources */,
246 | 59F208F71C04C06800CC2392 /* Frameworks */,
247 | 59F208F81C04C06800CC2392 /* Resources */,
248 | );
249 | buildRules = (
250 | );
251 | dependencies = (
252 | );
253 | name = twiliochat;
254 | productName = twiliochat;
255 | productReference = 59F208FA1C04C06800CC2392 /* twiliochat.app */;
256 | productType = "com.apple.product-type.application";
257 | };
258 | 59F2090D1C04C06800CC2392 /* twiliochatTests */ = {
259 | isa = PBXNativeTarget;
260 | buildConfigurationList = 59F209251C04C06800CC2392 /* Build configuration list for PBXNativeTarget "twiliochatTests" */;
261 | buildPhases = (
262 | 59F2090A1C04C06800CC2392 /* Sources */,
263 | 59F2090B1C04C06800CC2392 /* Frameworks */,
264 | 59F2090C1C04C06800CC2392 /* Resources */,
265 | );
266 | buildRules = (
267 | );
268 | dependencies = (
269 | 59F209101C04C06800CC2392 /* PBXTargetDependency */,
270 | );
271 | name = twiliochatTests;
272 | productName = twiliochatTests;
273 | productReference = 59F2090E1C04C06800CC2392 /* twiliochatTests.xctest */;
274 | productType = "com.apple.product-type.bundle.unit-test";
275 | };
276 | /* End PBXNativeTarget section */
277 |
278 | /* Begin PBXProject section */
279 | 59F208F21C04C06800CC2392 /* Project object */ = {
280 | isa = PBXProject;
281 | attributes = {
282 | LastSwiftUpdateCheck = 0710;
283 | LastUpgradeCheck = 1150;
284 | ORGANIZATIONNAME = Twilio;
285 | TargetAttributes = {
286 | 59F208F91C04C06800CC2392 = {
287 | CreatedOnToolsVersion = 7.1.1;
288 | LastSwiftMigration = 1150;
289 | };
290 | 59F2090D1C04C06800CC2392 = {
291 | CreatedOnToolsVersion = 7.1.1;
292 | LastSwiftMigration = 1150;
293 | TestTargetID = 59F208F91C04C06800CC2392;
294 | };
295 | };
296 | };
297 | buildConfigurationList = 59F208F51C04C06800CC2392 /* Build configuration list for PBXProject "twiliochat" */;
298 | compatibilityVersion = "Xcode 3.2";
299 | developmentRegion = English;
300 | hasScannedForEncodings = 0;
301 | knownRegions = (
302 | English,
303 | en,
304 | Base,
305 | );
306 | mainGroup = 59F208F11C04C06800CC2392;
307 | productRefGroup = 59F208FB1C04C06800CC2392 /* Products */;
308 | projectDirPath = "";
309 | projectRoot = "";
310 | targets = (
311 | 59F208F91C04C06800CC2392 /* twiliochat */,
312 | 59F2090D1C04C06800CC2392 /* twiliochatTests */,
313 | );
314 | };
315 | /* End PBXProject section */
316 |
317 | /* Begin PBXResourcesBuildPhase section */
318 | 59F208F81C04C06800CC2392 /* Resources */ = {
319 | isa = PBXResourcesBuildPhase;
320 | buildActionMask = 2147483647;
321 | files = (
322 | 8771091B1C162FC8004C57D3 /* ChatTableCell.xib in Resources */,
323 | 59F209081C04C06800CC2392 /* LaunchScreen.storyboard in Resources */,
324 | 59F209051C04C06800CC2392 /* Assets.xcassets in Resources */,
325 | 8771091A1C162FC8004C57D3 /* ChatStatusTableCell.xib in Resources */,
326 | 59F209031C04C06800CC2392 /* Main.storyboard in Resources */,
327 | 59F2092F1C04C35E00CC2392 /* Keys.plist in Resources */,
328 | );
329 | runOnlyForDeploymentPostprocessing = 0;
330 | };
331 | 59F2090C1C04C06800CC2392 /* Resources */ = {
332 | isa = PBXResourcesBuildPhase;
333 | buildActionMask = 2147483647;
334 | files = (
335 | );
336 | runOnlyForDeploymentPostprocessing = 0;
337 | };
338 | /* End PBXResourcesBuildPhase section */
339 |
340 | /* Begin PBXSourcesBuildPhase section */
341 | 59F208F61C04C06800CC2392 /* Sources */ = {
342 | isa = PBXSourcesBuildPhase;
343 | buildActionMask = 2147483647;
344 | files = (
345 | 877109151C10EF4F004C57D3 /* ChannelManager.swift in Sources */,
346 | 87F895F81C0E045F00754007 /* MainChatViewController.swift in Sources */,
347 | 877109171C15E55E004C57D3 /* MenuTableCell.swift in Sources */,
348 | 877109231C1776FA004C57D3 /* DateTodayFormatter.swift in Sources */,
349 | 877109211C1772C5004C57D3 /* StatusMessage.swift in Sources */,
350 | 8771091D1C1638E9004C57D3 /* InputDialogController.swift in Sources */,
351 | 59F209321C050EF700CC2392 /* LoginViewController.swift in Sources */,
352 | 8771091F1C175AC2004C57D3 /* ChatTableCell.swift in Sources */,
353 | 87F895F21C0CF3CE00754007 /* AlertDialogController.swift in Sources */,
354 | 59AE1CCE1C0C8C280008B10B /* TextFieldFormHandler.swift in Sources */,
355 | 59F208FE1C04C06800CC2392 /* AppDelegate.swift in Sources */,
356 | 59AE1CD21C0D03820008B10B /* MessagingManager.swift in Sources */,
357 | 59F5EDB01C6E925500CE7D32 /* TokenRequestHandler.swift in Sources */,
358 | 59F5EDB31C720E1200CE7D32 /* SessionManager.swift in Sources */,
359 | 87F895F61C0E044300754007 /* MenuViewController.swift in Sources */,
360 | );
361 | runOnlyForDeploymentPostprocessing = 0;
362 | };
363 | 59F2090A1C04C06800CC2392 /* Sources */ = {
364 | isa = PBXSourcesBuildPhase;
365 | buildActionMask = 2147483647;
366 | files = (
367 | 59A866DD1C1B5E8100525F07 /* MockAlertDialogController.swift in Sources */,
368 | 59F209131C04C06800CC2392 /* DateTodayFormatterTests.swift in Sources */,
369 | 59A866DF1C1B6C0900525F07 /* LoginViewControllerTests.swift in Sources */,
370 | 8A07B2CB1F224CAD009F0836 /* MockMessagingManager.swift in Sources */,
371 | );
372 | runOnlyForDeploymentPostprocessing = 0;
373 | };
374 | /* End PBXSourcesBuildPhase section */
375 |
376 | /* Begin PBXTargetDependency section */
377 | 59F209101C04C06800CC2392 /* PBXTargetDependency */ = {
378 | isa = PBXTargetDependency;
379 | target = 59F208F91C04C06800CC2392 /* twiliochat */;
380 | targetProxy = 59F2090F1C04C06800CC2392 /* PBXContainerItemProxy */;
381 | };
382 | /* End PBXTargetDependency section */
383 |
384 | /* Begin PBXVariantGroup section */
385 | 59F209011C04C06800CC2392 /* Main.storyboard */ = {
386 | isa = PBXVariantGroup;
387 | children = (
388 | 59F209021C04C06800CC2392 /* Base */,
389 | );
390 | name = Main.storyboard;
391 | sourceTree = "";
392 | };
393 | 59F209061C04C06800CC2392 /* LaunchScreen.storyboard */ = {
394 | isa = PBXVariantGroup;
395 | children = (
396 | 59F209071C04C06800CC2392 /* Base */,
397 | );
398 | name = LaunchScreen.storyboard;
399 | sourceTree = "";
400 | };
401 | /* End PBXVariantGroup section */
402 |
403 | /* Begin XCBuildConfiguration section */
404 | 59F209201C04C06800CC2392 /* Debug */ = {
405 | isa = XCBuildConfiguration;
406 | buildSettings = {
407 | ALWAYS_SEARCH_USER_PATHS = NO;
408 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
409 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
410 | CLANG_CXX_LIBRARY = "libc++";
411 | CLANG_ENABLE_MODULES = YES;
412 | CLANG_ENABLE_OBJC_ARC = YES;
413 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
414 | CLANG_WARN_BOOL_CONVERSION = YES;
415 | CLANG_WARN_COMMA = YES;
416 | CLANG_WARN_CONSTANT_CONVERSION = YES;
417 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
418 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
419 | CLANG_WARN_EMPTY_BODY = YES;
420 | CLANG_WARN_ENUM_CONVERSION = YES;
421 | CLANG_WARN_INFINITE_RECURSION = YES;
422 | CLANG_WARN_INT_CONVERSION = YES;
423 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
424 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
425 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
426 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
427 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
428 | CLANG_WARN_STRICT_PROTOTYPES = YES;
429 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
430 | CLANG_WARN_UNREACHABLE_CODE = YES;
431 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
432 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
433 | COPY_PHASE_STRIP = NO;
434 | DEBUG_INFORMATION_FORMAT = dwarf;
435 | ENABLE_BITCODE = NO;
436 | ENABLE_STRICT_OBJC_MSGSEND = YES;
437 | ENABLE_TESTABILITY = YES;
438 | GCC_C_LANGUAGE_STANDARD = gnu99;
439 | GCC_DYNAMIC_NO_PIC = NO;
440 | GCC_NO_COMMON_BLOCKS = YES;
441 | GCC_OPTIMIZATION_LEVEL = 0;
442 | GCC_PREPROCESSOR_DEFINITIONS = (
443 | "DEBUG=1",
444 | "$(inherited)",
445 | );
446 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
447 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
448 | GCC_WARN_UNDECLARED_SELECTOR = YES;
449 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
450 | GCC_WARN_UNUSED_FUNCTION = YES;
451 | GCC_WARN_UNUSED_VARIABLE = YES;
452 | IPHONEOS_DEPLOYMENT_TARGET = 10.1;
453 | MTL_ENABLE_DEBUG_INFO = YES;
454 | ONLY_ACTIVE_ARCH = YES;
455 | SDKROOT = iphoneos;
456 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
457 | TARGETED_DEVICE_FAMILY = "1,2";
458 | };
459 | name = Debug;
460 | };
461 | 59F209211C04C06800CC2392 /* Release */ = {
462 | isa = XCBuildConfiguration;
463 | buildSettings = {
464 | ALWAYS_SEARCH_USER_PATHS = NO;
465 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
466 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
467 | CLANG_CXX_LIBRARY = "libc++";
468 | CLANG_ENABLE_MODULES = YES;
469 | CLANG_ENABLE_OBJC_ARC = YES;
470 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
471 | CLANG_WARN_BOOL_CONVERSION = YES;
472 | CLANG_WARN_COMMA = YES;
473 | CLANG_WARN_CONSTANT_CONVERSION = YES;
474 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
475 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
476 | CLANG_WARN_EMPTY_BODY = YES;
477 | CLANG_WARN_ENUM_CONVERSION = YES;
478 | CLANG_WARN_INFINITE_RECURSION = YES;
479 | CLANG_WARN_INT_CONVERSION = YES;
480 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
481 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
482 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
483 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
484 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
485 | CLANG_WARN_STRICT_PROTOTYPES = YES;
486 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
487 | CLANG_WARN_UNREACHABLE_CODE = YES;
488 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
489 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
490 | COPY_PHASE_STRIP = NO;
491 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
492 | ENABLE_BITCODE = NO;
493 | ENABLE_NS_ASSERTIONS = NO;
494 | ENABLE_STRICT_OBJC_MSGSEND = YES;
495 | GCC_C_LANGUAGE_STANDARD = gnu99;
496 | GCC_NO_COMMON_BLOCKS = YES;
497 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
498 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
499 | GCC_WARN_UNDECLARED_SELECTOR = YES;
500 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
501 | GCC_WARN_UNUSED_FUNCTION = YES;
502 | GCC_WARN_UNUSED_VARIABLE = YES;
503 | IPHONEOS_DEPLOYMENT_TARGET = 10.1;
504 | MTL_ENABLE_DEBUG_INFO = NO;
505 | SDKROOT = iphoneos;
506 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
507 | TARGETED_DEVICE_FAMILY = "1,2";
508 | VALIDATE_PRODUCT = YES;
509 | };
510 | name = Release;
511 | };
512 | 59F209231C04C06800CC2392 /* Debug */ = {
513 | isa = XCBuildConfiguration;
514 | buildSettings = {
515 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
516 | CODE_SIGN_IDENTITY = "iPhone Developer";
517 | ENABLE_BITCODE = NO;
518 | FRAMEWORK_SEARCH_PATHS = "$(inherited)";
519 | HEADER_SEARCH_PATHS = (
520 | "$(inherited)",
521 | "\"${PODS_ROOT}/Headers/Public\"",
522 | "\"${PODS_ROOT}\"/**",
523 | );
524 | INFOPLIST_FILE = twiliochat/Info.plist;
525 | IPHONEOS_DEPLOYMENT_TARGET = 10.1;
526 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
527 | PRODUCT_BUNDLE_IDENTIFIER = twilio.twiliochat;
528 | PRODUCT_NAME = "$(TARGET_NAME)";
529 | SWIFT_OBJC_BRIDGING_HEADER = "twiliochat/twiliochat-Bridging-Header.h";
530 | SWIFT_VERSION = 5.0;
531 | };
532 | name = Debug;
533 | };
534 | 59F209241C04C06800CC2392 /* Release */ = {
535 | isa = XCBuildConfiguration;
536 | buildSettings = {
537 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
538 | CODE_SIGN_IDENTITY = "iPhone Developer";
539 | ENABLE_BITCODE = NO;
540 | FRAMEWORK_SEARCH_PATHS = "$(inherited)";
541 | HEADER_SEARCH_PATHS = (
542 | "$(inherited)",
543 | "\"${PODS_ROOT}/Headers/Public\"",
544 | "\"${PODS_ROOT}\"/**",
545 | );
546 | INFOPLIST_FILE = twiliochat/Info.plist;
547 | IPHONEOS_DEPLOYMENT_TARGET = 10.1;
548 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
549 | PRODUCT_BUNDLE_IDENTIFIER = twilio.twiliochat;
550 | PRODUCT_NAME = "$(TARGET_NAME)";
551 | SWIFT_OBJC_BRIDGING_HEADER = "twiliochat/twiliochat-Bridging-Header.h";
552 | SWIFT_VERSION = 5.0;
553 | };
554 | name = Release;
555 | };
556 | 59F209261C04C06800CC2392 /* Debug */ = {
557 | isa = XCBuildConfiguration;
558 | buildSettings = {
559 | BUNDLE_LOADER = "$(TEST_HOST)";
560 | DEVELOPMENT_TEAM = "";
561 | INFOPLIST_FILE = twiliochatTests/Info.plist;
562 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
563 | PRODUCT_BUNDLE_IDENTIFIER = twilio.twiliochatTests;
564 | PRODUCT_NAME = "$(TARGET_NAME)";
565 | SWIFT_OBJC_BRIDGING_HEADER = "";
566 | SWIFT_VERSION = 5.0;
567 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/twiliochat.app/twiliochat";
568 | };
569 | name = Debug;
570 | };
571 | 59F209271C04C06800CC2392 /* Release */ = {
572 | isa = XCBuildConfiguration;
573 | buildSettings = {
574 | BUNDLE_LOADER = "$(TEST_HOST)";
575 | DEVELOPMENT_TEAM = "";
576 | INFOPLIST_FILE = twiliochatTests/Info.plist;
577 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
578 | PRODUCT_BUNDLE_IDENTIFIER = twilio.twiliochatTests;
579 | PRODUCT_NAME = "$(TARGET_NAME)";
580 | SWIFT_OBJC_BRIDGING_HEADER = "";
581 | SWIFT_VERSION = 5.0;
582 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/twiliochat.app/twiliochat";
583 | };
584 | name = Release;
585 | };
586 | /* End XCBuildConfiguration section */
587 |
588 | /* Begin XCConfigurationList section */
589 | 59F208F51C04C06800CC2392 /* Build configuration list for PBXProject "twiliochat" */ = {
590 | isa = XCConfigurationList;
591 | buildConfigurations = (
592 | 59F209201C04C06800CC2392 /* Debug */,
593 | 59F209211C04C06800CC2392 /* Release */,
594 | );
595 | defaultConfigurationIsVisible = 0;
596 | defaultConfigurationName = Release;
597 | };
598 | 59F209221C04C06800CC2392 /* Build configuration list for PBXNativeTarget "twiliochat" */ = {
599 | isa = XCConfigurationList;
600 | buildConfigurations = (
601 | 59F209231C04C06800CC2392 /* Debug */,
602 | 59F209241C04C06800CC2392 /* Release */,
603 | );
604 | defaultConfigurationIsVisible = 0;
605 | defaultConfigurationName = Release;
606 | };
607 | 59F209251C04C06800CC2392 /* Build configuration list for PBXNativeTarget "twiliochatTests" */ = {
608 | isa = XCConfigurationList;
609 | buildConfigurations = (
610 | 59F209261C04C06800CC2392 /* Debug */,
611 | 59F209271C04C06800CC2392 /* Release */,
612 | );
613 | defaultConfigurationIsVisible = 0;
614 | defaultConfigurationName = Release;
615 | };
616 | /* End XCConfigurationList section */
617 | };
618 | rootObject = 59F208F21C04C06800CC2392 /* Project object */;
619 | }
620 |
--------------------------------------------------------------------------------
/twiliochat/Base.lproj/Main.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 |
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 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
321 |
327 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
--------------------------------------------------------------------------------