├── .gitignore
├── Demo
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── bubble_left.imageset
│ │ ├── Contents.json
│ │ └── bubble_left.png
│ └── bubble_orange.imageset
│ │ ├── Contents.json
│ │ └── bubble_orange.png
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Info.plist
├── TestData.swift
└── ViewController.swift
├── DemoTests
├── DemoTests.swift
└── Info.plist
├── DemoUITests
├── DemoUITests.swift
└── Info.plist
├── MessagesView.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcshareddata
│ └── xcschemes
│ │ └── MessagesView.xcscheme
└── xcuserdata
│ └── pgs-dkanak.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── MessagesView
├── BubbleImage.swift
├── Info.plist
├── MessageCollectionViewCell.swift
├── MessageCollectionViewCell.xib
├── MessageEditorTextView.swift
├── MessagesCollectionView.swift
├── MessagesInputToolbar.swift
├── MessagesToolbarContentView.swift
├── MessagesToolbarContentView.xib
├── MessagesView.h
├── MessagesView.swift
├── MessagesView.xib
├── MessagesViewSettings.swift
├── UIColor+RGB.swift
└── UIImage+Flipped.swift
├── MessagesViewTests
├── Info.plist
└── MessagesViewTests.swift
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | ## Various Xcode settings and user-related
3 | *.pbxuser
4 | *.xccheckout
5 | *.xcuserdatad
6 | *.xcscheme
7 | *.xcuserstate
8 |
9 | ## OSX
10 | .DS_Store
11 |
--------------------------------------------------------------------------------
/Demo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Demo
4 | //
5 | // Created by Damian Kanak on 03/04/17.
6 | // Copyright © 2017 pgs-dkanak. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) {
23 | // 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.
24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(_ application: UIApplication) {
28 | // 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.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(_ application: UIApplication) {
33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(_ application: UIApplication) {
37 | // 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.
38 | }
39 |
40 | func applicationWillTerminate(_ application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | }
88 | ],
89 | "info" : {
90 | "version" : 1,
91 | "author" : "xcode"
92 | }
93 | }
--------------------------------------------------------------------------------
/Demo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Demo/Assets.xcassets/bubble_left.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "bubble_left.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Demo/Assets.xcassets/bubble_left.imageset/bubble_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PGSSoft/MessagesView/5945df619f4d3e038ff357cf16cd43087bf666d5/Demo/Assets.xcassets/bubble_left.imageset/bubble_left.png
--------------------------------------------------------------------------------
/Demo/Assets.xcassets/bubble_orange.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "bubble_orange.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Demo/Assets.xcassets/bubble_orange.imageset/bubble_orange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PGSSoft/MessagesView/5945df619f4d3e038ff357cf16cd43087bf666d5/Demo/Assets.xcassets/bubble_orange.imageset/bubble_orange.png
--------------------------------------------------------------------------------
/Demo/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 |
--------------------------------------------------------------------------------
/Demo/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 |
--------------------------------------------------------------------------------
/Demo/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 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/Demo/TestData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestData.swift
3 | // MessagesView
4 | //
5 | // Created by Damian Kanak on 03/04/17.
6 | // Copyright © 2017 pgs-dkanak. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class TestData {
12 | static var peerNames = ["Alice","Bob"]
13 |
14 | static var exampleMessageText = [
15 | "Welcome to MessagesView",
16 | "MessagesView is the best messaging framework in the world",
17 | "you won't imagine life without MessagesView",
18 | "vote for MessagesView!",
19 | "You can use and customize MessagesView anywhere"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Demo
4 | //
5 | // Created by Damian Kanak on 03/04/17.
6 | // Copyright © 2017 pgs-dkanak. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import MessagesView
11 |
12 | class ViewController: UIViewController {
13 |
14 | @IBOutlet weak var messagesView: MessagesView!
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 |
19 | messagesView.delegate = self
20 | messagesView.dataSource = self
21 |
22 | //addCustomMessageBubbles()
23 | }
24 |
25 | override func viewDidAppear(_ animated: Bool) {
26 | messagesView.scrollToLastMessage(animated: false)
27 | }
28 |
29 | func addCustomMessageBubbles() {
30 |
31 | let leftBubble = BubbleImage(image: UIImage(named: "bubble_left")!,
32 | resizeInsets: UIEdgeInsets(top: 4, left: 21, bottom: 14, right: 4),
33 | textInsets: UIEdgeInsets(top: 10, left: 22, bottom: 10, right: 6))
34 |
35 | messagesView.setBubbleImagesWith(left: leftBubble)
36 | }
37 | }
38 |
39 | extension ViewController: MessagesViewDelegate {
40 | func didTapLeftButton() {
41 |
42 | }
43 |
44 | func didTapRightButton() {
45 |
46 | let text = messagesView.inputText.trimmingCharacters(in: .whitespaces)
47 |
48 | guard !text.isEmpty else {
49 | return
50 | }
51 |
52 | TestData.exampleMessageText.append(text)
53 | messagesView.refresh(scrollToLastMessage: true, animateLastMessage: true)
54 | }
55 | }
56 |
57 | extension ViewController: MessagesViewDataSource {
58 | struct Peer: MessagesViewPeer {
59 | var id: String
60 | }
61 |
62 | struct Message: MessagesViewChatMessage {
63 | var text: String
64 | var sender: MessagesViewPeer
65 | var onRight: Bool
66 | }
67 |
68 | var peers: [MessagesViewPeer] {
69 | return TestData.peerNames.map{ Peer(id: $0) }
70 | }
71 |
72 | var messages: [MessagesViewChatMessage] {
73 | return TestData.exampleMessageText.enumerated().map { (index, element) in
74 | let peer = self.peers[index % peers.count]
75 | return Message(text: element, sender: peer, onRight: index != 0)
76 | }
77 | }
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/DemoTests/DemoTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoTests.swift
3 | // DemoTests
4 | //
5 | // Created by Damian Kanak on 03/04/17.
6 | // Copyright © 2017 pgs-dkanak. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Demo
11 |
12 | class DemoTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | // Use XCTAssert and related functions to verify your tests produce the correct results.
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/DemoTests/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 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/DemoUITests/DemoUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DemoUITests.swift
3 | // DemoUITests
4 | //
5 | // Created by Damian Kanak on 03/04/17.
6 | // Copyright © 2017 pgs-dkanak. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class DemoUITests: XCTestCase {
12 |
13 | override func setUp() {
14 | super.setUp()
15 |
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 |
18 | // In UI tests it is usually best to stop immediately when a failure occurs.
19 | continueAfterFailure = false
20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
21 | XCUIApplication().launch()
22 |
23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
24 | }
25 |
26 | override func tearDown() {
27 | // Put teardown code here. This method is called after the invocation of each test method in the class.
28 | super.tearDown()
29 | }
30 |
31 | func testExample() {
32 | // Use recording to get started writing UI tests.
33 | // Use XCTAssert and related functions to verify your tests produce the correct results.
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/DemoUITests/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 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/MessagesView.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 052769E61EA8F360009AB833 /* UIImage+Flipped.swift in Sources */ = {isa = PBXBuildFile; fileRef = 052769E51EA8F360009AB833 /* UIImage+Flipped.swift */; };
11 | 0553B2741E9CF92E00E76010 /* BubbleImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0553B2731E9CF92E00E76010 /* BubbleImage.swift */; };
12 | 055DA17F1E9296600091279C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 055DA17E1E9296600091279C /* AppDelegate.swift */; };
13 | 055DA1811E9296600091279C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 055DA1801E9296600091279C /* ViewController.swift */; };
14 | 055DA1841E9296600091279C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 055DA1821E9296600091279C /* Main.storyboard */; };
15 | 055DA1861E9296600091279C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 055DA1851E9296600091279C /* Assets.xcassets */; };
16 | 055DA1891E9296600091279C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 055DA1871E9296600091279C /* LaunchScreen.storyboard */; };
17 | 055DA1AA1E9296F80091279C /* MessagesView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05ECAF841E7ADF8400833D84 /* MessagesView.framework */; };
18 | 055DA1AB1E9296F80091279C /* MessagesView.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 05ECAF841E7ADF8400833D84 /* MessagesView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
19 | 055DA1B21E929CCD0091279C /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 055DA1B11E929CCD0091279C /* TestData.swift */; };
20 | 05925ADF1E82D21B00421928 /* MessagesView.swift in Headers */ = {isa = PBXBuildFile; fileRef = 05ECAFA51E7ADFEF00833D84 /* MessagesView.swift */; settings = {ATTRIBUTES = (Public, ); }; };
21 | 05925AE71E82E24A00421928 /* MessagesViewSettings.swift in Headers */ = {isa = PBXBuildFile; fileRef = 05ECAFAA1E7ADFEF00833D84 /* MessagesViewSettings.swift */; settings = {ATTRIBUTES = (Public, ); }; };
22 | 05B94A351E842FC400CAB715 /* UIColor+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B94A341E842FC400CAB715 /* UIColor+RGB.swift */; };
23 | 05ECAF8E1E7ADF8400833D84 /* MessagesView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05ECAF841E7ADF8400833D84 /* MessagesView.framework */; };
24 | 05ECAF931E7ADF8400833D84 /* MessagesViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05ECAF921E7ADF8400833D84 /* MessagesViewTests.swift */; };
25 | 05ECAF951E7ADF8400833D84 /* MessagesView.h in Headers */ = {isa = PBXBuildFile; fileRef = 05ECAF871E7ADF8400833D84 /* MessagesView.h */; settings = {ATTRIBUTES = (Public, ); }; };
26 | 05ECAFAB1E7ADFEF00833D84 /* MessageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05ECAF9E1E7ADFEF00833D84 /* MessageCollectionViewCell.swift */; };
27 | 05ECAFAC1E7ADFEF00833D84 /* MessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 05ECAF9F1E7ADFEF00833D84 /* MessageCollectionViewCell.xib */; };
28 | 05ECAFAD1E7ADFEF00833D84 /* MessageEditorTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05ECAFA01E7ADFEF00833D84 /* MessageEditorTextView.swift */; };
29 | 05ECAFAE1E7ADFEF00833D84 /* MessagesCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05ECAFA11E7ADFEF00833D84 /* MessagesCollectionView.swift */; };
30 | 05ECAFAF1E7ADFEF00833D84 /* MessagesInputToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05ECAFA21E7ADFEF00833D84 /* MessagesInputToolbar.swift */; };
31 | 05ECAFB01E7ADFEF00833D84 /* MessagesToolbarContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05ECAFA31E7ADFEF00833D84 /* MessagesToolbarContentView.swift */; };
32 | 05ECAFB11E7ADFEF00833D84 /* MessagesToolbarContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 05ECAFA41E7ADFEF00833D84 /* MessagesToolbarContentView.xib */; };
33 | 05ECAFB21E7ADFEF00833D84 /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05ECAFA51E7ADFEF00833D84 /* MessagesView.swift */; };
34 | 05ECAFB31E7ADFEF00833D84 /* MessagesView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 05ECAFA61E7ADFEF00833D84 /* MessagesView.xib */; };
35 | 05ECAFB71E7ADFEF00833D84 /* MessagesViewSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05ECAFAA1E7ADFEF00833D84 /* MessagesViewSettings.swift */; };
36 | /* End PBXBuildFile section */
37 |
38 | /* Begin PBXContainerItemProxy section */
39 | 055DA1AC1E9296F80091279C /* PBXContainerItemProxy */ = {
40 | isa = PBXContainerItemProxy;
41 | containerPortal = 05ECAF7B1E7ADF8400833D84 /* Project object */;
42 | proxyType = 1;
43 | remoteGlobalIDString = 05ECAF831E7ADF8400833D84;
44 | remoteInfo = MessagesView;
45 | };
46 | 05ECAF8F1E7ADF8400833D84 /* PBXContainerItemProxy */ = {
47 | isa = PBXContainerItemProxy;
48 | containerPortal = 05ECAF7B1E7ADF8400833D84 /* Project object */;
49 | proxyType = 1;
50 | remoteGlobalIDString = 05ECAF831E7ADF8400833D84;
51 | remoteInfo = MessagesView;
52 | };
53 | /* End PBXContainerItemProxy section */
54 |
55 | /* Begin PBXCopyFilesBuildPhase section */
56 | 055DA1AE1E9296F80091279C /* Embed Frameworks */ = {
57 | isa = PBXCopyFilesBuildPhase;
58 | buildActionMask = 2147483647;
59 | dstPath = "";
60 | dstSubfolderSpec = 10;
61 | files = (
62 | 055DA1AB1E9296F80091279C /* MessagesView.framework in Embed Frameworks */,
63 | );
64 | name = "Embed Frameworks";
65 | runOnlyForDeploymentPostprocessing = 0;
66 | };
67 | /* End PBXCopyFilesBuildPhase section */
68 |
69 | /* Begin PBXFileReference section */
70 | 052769E51EA8F360009AB833 /* UIImage+Flipped.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Flipped.swift"; sourceTree = ""; };
71 | 0553B2731E9CF92E00E76010 /* BubbleImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BubbleImage.swift; sourceTree = ""; };
72 | 055DA17C1E9296600091279C /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
73 | 055DA17E1E9296600091279C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
74 | 055DA1801E9296600091279C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
75 | 055DA1831E9296600091279C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
76 | 055DA1851E9296600091279C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
77 | 055DA1881E9296600091279C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
78 | 055DA18A1E9296600091279C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
79 | 055DA1931E9296600091279C /* DemoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoTests.swift; sourceTree = ""; };
80 | 055DA1951E9296600091279C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
81 | 055DA19E1E9296600091279C /* DemoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoUITests.swift; sourceTree = ""; };
82 | 055DA1A01E9296600091279C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
83 | 055DA1B11E929CCD0091279C /* TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; };
84 | 05B94A341E842FC400CAB715 /* UIColor+RGB.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+RGB.swift"; sourceTree = ""; };
85 | 05ECAF841E7ADF8400833D84 /* MessagesView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessagesView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
86 | 05ECAF871E7ADF8400833D84 /* MessagesView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MessagesView.h; sourceTree = ""; };
87 | 05ECAF881E7ADF8400833D84 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
88 | 05ECAF8D1E7ADF8400833D84 /* MessagesViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MessagesViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
89 | 05ECAF921E7ADF8400833D84 /* MessagesViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewTests.swift; sourceTree = ""; };
90 | 05ECAF941E7ADF8400833D84 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
91 | 05ECAF9E1E7ADFEF00833D84 /* MessageCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewCell.swift; sourceTree = ""; };
92 | 05ECAF9F1E7ADFEF00833D84 /* MessageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCollectionViewCell.xib; sourceTree = ""; };
93 | 05ECAFA01E7ADFEF00833D84 /* MessageEditorTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageEditorTextView.swift; sourceTree = ""; };
94 | 05ECAFA11E7ADFEF00833D84 /* MessagesCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionView.swift; sourceTree = ""; };
95 | 05ECAFA21E7ADFEF00833D84 /* MessagesInputToolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesInputToolbar.swift; sourceTree = ""; };
96 | 05ECAFA31E7ADFEF00833D84 /* MessagesToolbarContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesToolbarContentView.swift; sourceTree = ""; };
97 | 05ECAFA41E7ADFEF00833D84 /* MessagesToolbarContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessagesToolbarContentView.xib; sourceTree = ""; };
98 | 05ECAFA51E7ADFEF00833D84 /* MessagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = ""; };
99 | 05ECAFA61E7ADFEF00833D84 /* MessagesView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessagesView.xib; sourceTree = ""; };
100 | 05ECAFAA1E7ADFEF00833D84 /* MessagesViewSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesViewSettings.swift; sourceTree = ""; };
101 | /* End PBXFileReference section */
102 |
103 | /* Begin PBXFrameworksBuildPhase section */
104 | 055DA1791E9296600091279C /* Frameworks */ = {
105 | isa = PBXFrameworksBuildPhase;
106 | buildActionMask = 2147483647;
107 | files = (
108 | 055DA1AA1E9296F80091279C /* MessagesView.framework in Frameworks */,
109 | );
110 | runOnlyForDeploymentPostprocessing = 0;
111 | };
112 | 05ECAF801E7ADF8400833D84 /* Frameworks */ = {
113 | isa = PBXFrameworksBuildPhase;
114 | buildActionMask = 2147483647;
115 | files = (
116 | );
117 | runOnlyForDeploymentPostprocessing = 0;
118 | };
119 | 05ECAF8A1E7ADF8400833D84 /* Frameworks */ = {
120 | isa = PBXFrameworksBuildPhase;
121 | buildActionMask = 2147483647;
122 | files = (
123 | 05ECAF8E1E7ADF8400833D84 /* MessagesView.framework in Frameworks */,
124 | );
125 | runOnlyForDeploymentPostprocessing = 0;
126 | };
127 | /* End PBXFrameworksBuildPhase section */
128 |
129 | /* Begin PBXGroup section */
130 | 055DA17D1E9296600091279C /* Demo */ = {
131 | isa = PBXGroup;
132 | children = (
133 | 055DA17E1E9296600091279C /* AppDelegate.swift */,
134 | 055DA1801E9296600091279C /* ViewController.swift */,
135 | 055DA1B11E929CCD0091279C /* TestData.swift */,
136 | 055DA1821E9296600091279C /* Main.storyboard */,
137 | 055DA1851E9296600091279C /* Assets.xcassets */,
138 | 055DA1871E9296600091279C /* LaunchScreen.storyboard */,
139 | 055DA18A1E9296600091279C /* Info.plist */,
140 | );
141 | path = Demo;
142 | sourceTree = "";
143 | };
144 | 055DA1921E9296600091279C /* DemoTests */ = {
145 | isa = PBXGroup;
146 | children = (
147 | 055DA1931E9296600091279C /* DemoTests.swift */,
148 | 055DA1951E9296600091279C /* Info.plist */,
149 | );
150 | path = DemoTests;
151 | sourceTree = "";
152 | };
153 | 055DA19D1E9296600091279C /* DemoUITests */ = {
154 | isa = PBXGroup;
155 | children = (
156 | 055DA19E1E9296600091279C /* DemoUITests.swift */,
157 | 055DA1A01E9296600091279C /* Info.plist */,
158 | );
159 | path = DemoUITests;
160 | sourceTree = "";
161 | };
162 | 05ECAF7A1E7ADF8400833D84 = {
163 | isa = PBXGroup;
164 | children = (
165 | 05ECAF861E7ADF8400833D84 /* MessagesView */,
166 | 05ECAF911E7ADF8400833D84 /* MessagesViewTests */,
167 | 055DA17D1E9296600091279C /* Demo */,
168 | 055DA1921E9296600091279C /* DemoTests */,
169 | 055DA19D1E9296600091279C /* DemoUITests */,
170 | 05ECAF851E7ADF8400833D84 /* Products */,
171 | );
172 | sourceTree = "";
173 | };
174 | 05ECAF851E7ADF8400833D84 /* Products */ = {
175 | isa = PBXGroup;
176 | children = (
177 | 05ECAF841E7ADF8400833D84 /* MessagesView.framework */,
178 | 05ECAF8D1E7ADF8400833D84 /* MessagesViewTests.xctest */,
179 | 055DA17C1E9296600091279C /* Demo.app */,
180 | );
181 | name = Products;
182 | sourceTree = "";
183 | };
184 | 05ECAF861E7ADF8400833D84 /* MessagesView */ = {
185 | isa = PBXGroup;
186 | children = (
187 | 05ECAF871E7ADF8400833D84 /* MessagesView.h */,
188 | 05B94A341E842FC400CAB715 /* UIColor+RGB.swift */,
189 | 052769E51EA8F360009AB833 /* UIImage+Flipped.swift */,
190 | 05ECAF881E7ADF8400833D84 /* Info.plist */,
191 | 05ECAF9E1E7ADFEF00833D84 /* MessageCollectionViewCell.swift */,
192 | 05ECAF9F1E7ADFEF00833D84 /* MessageCollectionViewCell.xib */,
193 | 05ECAFA01E7ADFEF00833D84 /* MessageEditorTextView.swift */,
194 | 05ECAFA11E7ADFEF00833D84 /* MessagesCollectionView.swift */,
195 | 05ECAFA21E7ADFEF00833D84 /* MessagesInputToolbar.swift */,
196 | 05ECAFA31E7ADFEF00833D84 /* MessagesToolbarContentView.swift */,
197 | 05ECAFA41E7ADFEF00833D84 /* MessagesToolbarContentView.xib */,
198 | 05ECAFA51E7ADFEF00833D84 /* MessagesView.swift */,
199 | 05ECAFA61E7ADFEF00833D84 /* MessagesView.xib */,
200 | 05ECAFAA1E7ADFEF00833D84 /* MessagesViewSettings.swift */,
201 | 0553B2731E9CF92E00E76010 /* BubbleImage.swift */,
202 | );
203 | path = MessagesView;
204 | sourceTree = "";
205 | };
206 | 05ECAF911E7ADF8400833D84 /* MessagesViewTests */ = {
207 | isa = PBXGroup;
208 | children = (
209 | 05ECAF921E7ADF8400833D84 /* MessagesViewTests.swift */,
210 | 05ECAF941E7ADF8400833D84 /* Info.plist */,
211 | );
212 | path = MessagesViewTests;
213 | sourceTree = "";
214 | };
215 | /* End PBXGroup section */
216 |
217 | /* Begin PBXHeadersBuildPhase section */
218 | 05ECAF811E7ADF8400833D84 /* Headers */ = {
219 | isa = PBXHeadersBuildPhase;
220 | buildActionMask = 2147483647;
221 | files = (
222 | 05ECAF951E7ADF8400833D84 /* MessagesView.h in Headers */,
223 | 05925ADF1E82D21B00421928 /* MessagesView.swift in Headers */,
224 | 05925AE71E82E24A00421928 /* MessagesViewSettings.swift in Headers */,
225 | );
226 | runOnlyForDeploymentPostprocessing = 0;
227 | };
228 | /* End PBXHeadersBuildPhase section */
229 |
230 | /* Begin PBXNativeTarget section */
231 | 055DA17B1E9296600091279C /* Demo */ = {
232 | isa = PBXNativeTarget;
233 | buildConfigurationList = 055DA1A71E9296600091279C /* Build configuration list for PBXNativeTarget "Demo" */;
234 | buildPhases = (
235 | 055DA1781E9296600091279C /* Sources */,
236 | 055DA1791E9296600091279C /* Frameworks */,
237 | 055DA17A1E9296600091279C /* Resources */,
238 | 055DA1AE1E9296F80091279C /* Embed Frameworks */,
239 | );
240 | buildRules = (
241 | );
242 | dependencies = (
243 | 055DA1AD1E9296F80091279C /* PBXTargetDependency */,
244 | );
245 | name = Demo;
246 | productName = Demo;
247 | productReference = 055DA17C1E9296600091279C /* Demo.app */;
248 | productType = "com.apple.product-type.application";
249 | };
250 | 05ECAF831E7ADF8400833D84 /* MessagesView */ = {
251 | isa = PBXNativeTarget;
252 | buildConfigurationList = 05ECAF981E7ADF8400833D84 /* Build configuration list for PBXNativeTarget "MessagesView" */;
253 | buildPhases = (
254 | 05ECAF7F1E7ADF8400833D84 /* Sources */,
255 | 05ECAF801E7ADF8400833D84 /* Frameworks */,
256 | 05ECAF811E7ADF8400833D84 /* Headers */,
257 | 05ECAF821E7ADF8400833D84 /* Resources */,
258 | );
259 | buildRules = (
260 | );
261 | dependencies = (
262 | );
263 | name = MessagesView;
264 | productName = MessagesView;
265 | productReference = 05ECAF841E7ADF8400833D84 /* MessagesView.framework */;
266 | productType = "com.apple.product-type.framework";
267 | };
268 | 05ECAF8C1E7ADF8400833D84 /* MessagesViewTests */ = {
269 | isa = PBXNativeTarget;
270 | buildConfigurationList = 05ECAF9B1E7ADF8400833D84 /* Build configuration list for PBXNativeTarget "MessagesViewTests" */;
271 | buildPhases = (
272 | 05ECAF891E7ADF8400833D84 /* Sources */,
273 | 05ECAF8A1E7ADF8400833D84 /* Frameworks */,
274 | 05ECAF8B1E7ADF8400833D84 /* Resources */,
275 | );
276 | buildRules = (
277 | );
278 | dependencies = (
279 | 05ECAF901E7ADF8400833D84 /* PBXTargetDependency */,
280 | );
281 | name = MessagesViewTests;
282 | productName = MessagesViewTests;
283 | productReference = 05ECAF8D1E7ADF8400833D84 /* MessagesViewTests.xctest */;
284 | productType = "com.apple.product-type.bundle.unit-test";
285 | };
286 | /* End PBXNativeTarget section */
287 |
288 | /* Begin PBXProject section */
289 | 05ECAF7B1E7ADF8400833D84 /* Project object */ = {
290 | isa = PBXProject;
291 | attributes = {
292 | LastSwiftUpdateCheck = 0820;
293 | LastUpgradeCheck = 0900;
294 | ORGANIZATIONNAME = "pgs-dkanak";
295 | TargetAttributes = {
296 | 055DA17B1E9296600091279C = {
297 | CreatedOnToolsVersion = 8.2.1;
298 | DevelopmentTeam = 36CHZQXF4C;
299 | ProvisioningStyle = Automatic;
300 | };
301 | 05ECAF831E7ADF8400833D84 = {
302 | CreatedOnToolsVersion = 8.2.1;
303 | DevelopmentTeam = 36CHZQXF4C;
304 | LastSwiftMigration = 0820;
305 | ProvisioningStyle = Automatic;
306 | };
307 | 05ECAF8C1E7ADF8400833D84 = {
308 | CreatedOnToolsVersion = 8.2.1;
309 | DevelopmentTeam = 36CHZQXF4C;
310 | ProvisioningStyle = Automatic;
311 | };
312 | };
313 | };
314 | buildConfigurationList = 05ECAF7E1E7ADF8400833D84 /* Build configuration list for PBXProject "MessagesView" */;
315 | compatibilityVersion = "Xcode 3.2";
316 | developmentRegion = English;
317 | hasScannedForEncodings = 0;
318 | knownRegions = (
319 | en,
320 | Base,
321 | );
322 | mainGroup = 05ECAF7A1E7ADF8400833D84;
323 | productRefGroup = 05ECAF851E7ADF8400833D84 /* Products */;
324 | projectDirPath = "";
325 | projectRoot = "";
326 | targets = (
327 | 05ECAF831E7ADF8400833D84 /* MessagesView */,
328 | 05ECAF8C1E7ADF8400833D84 /* MessagesViewTests */,
329 | 055DA17B1E9296600091279C /* Demo */,
330 | );
331 | };
332 | /* End PBXProject section */
333 |
334 | /* Begin PBXResourcesBuildPhase section */
335 | 055DA17A1E9296600091279C /* Resources */ = {
336 | isa = PBXResourcesBuildPhase;
337 | buildActionMask = 2147483647;
338 | files = (
339 | 055DA1891E9296600091279C /* LaunchScreen.storyboard in Resources */,
340 | 055DA1861E9296600091279C /* Assets.xcassets in Resources */,
341 | 055DA1841E9296600091279C /* Main.storyboard in Resources */,
342 | );
343 | runOnlyForDeploymentPostprocessing = 0;
344 | };
345 | 05ECAF821E7ADF8400833D84 /* Resources */ = {
346 | isa = PBXResourcesBuildPhase;
347 | buildActionMask = 2147483647;
348 | files = (
349 | 05ECAFB11E7ADFEF00833D84 /* MessagesToolbarContentView.xib in Resources */,
350 | 05ECAFB31E7ADFEF00833D84 /* MessagesView.xib in Resources */,
351 | 05ECAFAC1E7ADFEF00833D84 /* MessageCollectionViewCell.xib in Resources */,
352 | );
353 | runOnlyForDeploymentPostprocessing = 0;
354 | };
355 | 05ECAF8B1E7ADF8400833D84 /* Resources */ = {
356 | isa = PBXResourcesBuildPhase;
357 | buildActionMask = 2147483647;
358 | files = (
359 | );
360 | runOnlyForDeploymentPostprocessing = 0;
361 | };
362 | /* End PBXResourcesBuildPhase section */
363 |
364 | /* Begin PBXSourcesBuildPhase section */
365 | 055DA1781E9296600091279C /* Sources */ = {
366 | isa = PBXSourcesBuildPhase;
367 | buildActionMask = 2147483647;
368 | files = (
369 | 055DA1B21E929CCD0091279C /* TestData.swift in Sources */,
370 | 055DA1811E9296600091279C /* ViewController.swift in Sources */,
371 | 055DA17F1E9296600091279C /* AppDelegate.swift in Sources */,
372 | );
373 | runOnlyForDeploymentPostprocessing = 0;
374 | };
375 | 05ECAF7F1E7ADF8400833D84 /* Sources */ = {
376 | isa = PBXSourcesBuildPhase;
377 | buildActionMask = 2147483647;
378 | files = (
379 | 05ECAFB01E7ADFEF00833D84 /* MessagesToolbarContentView.swift in Sources */,
380 | 052769E61EA8F360009AB833 /* UIImage+Flipped.swift in Sources */,
381 | 05ECAFAF1E7ADFEF00833D84 /* MessagesInputToolbar.swift in Sources */,
382 | 05ECAFB21E7ADFEF00833D84 /* MessagesView.swift in Sources */,
383 | 05B94A351E842FC400CAB715 /* UIColor+RGB.swift in Sources */,
384 | 0553B2741E9CF92E00E76010 /* BubbleImage.swift in Sources */,
385 | 05ECAFAD1E7ADFEF00833D84 /* MessageEditorTextView.swift in Sources */,
386 | 05ECAFB71E7ADFEF00833D84 /* MessagesViewSettings.swift in Sources */,
387 | 05ECAFAB1E7ADFEF00833D84 /* MessageCollectionViewCell.swift in Sources */,
388 | 05ECAFAE1E7ADFEF00833D84 /* MessagesCollectionView.swift in Sources */,
389 | );
390 | runOnlyForDeploymentPostprocessing = 0;
391 | };
392 | 05ECAF891E7ADF8400833D84 /* Sources */ = {
393 | isa = PBXSourcesBuildPhase;
394 | buildActionMask = 2147483647;
395 | files = (
396 | 05ECAF931E7ADF8400833D84 /* MessagesViewTests.swift in Sources */,
397 | );
398 | runOnlyForDeploymentPostprocessing = 0;
399 | };
400 | /* End PBXSourcesBuildPhase section */
401 |
402 | /* Begin PBXTargetDependency section */
403 | 055DA1AD1E9296F80091279C /* PBXTargetDependency */ = {
404 | isa = PBXTargetDependency;
405 | target = 05ECAF831E7ADF8400833D84 /* MessagesView */;
406 | targetProxy = 055DA1AC1E9296F80091279C /* PBXContainerItemProxy */;
407 | };
408 | 05ECAF901E7ADF8400833D84 /* PBXTargetDependency */ = {
409 | isa = PBXTargetDependency;
410 | target = 05ECAF831E7ADF8400833D84 /* MessagesView */;
411 | targetProxy = 05ECAF8F1E7ADF8400833D84 /* PBXContainerItemProxy */;
412 | };
413 | /* End PBXTargetDependency section */
414 |
415 | /* Begin PBXVariantGroup section */
416 | 055DA1821E9296600091279C /* Main.storyboard */ = {
417 | isa = PBXVariantGroup;
418 | children = (
419 | 055DA1831E9296600091279C /* Base */,
420 | );
421 | name = Main.storyboard;
422 | sourceTree = "";
423 | };
424 | 055DA1871E9296600091279C /* LaunchScreen.storyboard */ = {
425 | isa = PBXVariantGroup;
426 | children = (
427 | 055DA1881E9296600091279C /* Base */,
428 | );
429 | name = LaunchScreen.storyboard;
430 | sourceTree = "";
431 | };
432 | /* End PBXVariantGroup section */
433 |
434 | /* Begin XCBuildConfiguration section */
435 | 055DA1A11E9296600091279C /* Debug */ = {
436 | isa = XCBuildConfiguration;
437 | buildSettings = {
438 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
439 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
440 | DEVELOPMENT_TEAM = 36CHZQXF4C;
441 | INFOPLIST_FILE = Demo/Info.plist;
442 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
443 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
444 | PRODUCT_BUNDLE_IDENTIFIER = com.pgs.Demo;
445 | PRODUCT_NAME = "$(TARGET_NAME)";
446 | SWIFT_VERSION = 3.0;
447 | };
448 | name = Debug;
449 | };
450 | 055DA1A21E9296600091279C /* Release */ = {
451 | isa = XCBuildConfiguration;
452 | buildSettings = {
453 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
454 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
455 | DEVELOPMENT_TEAM = 36CHZQXF4C;
456 | INFOPLIST_FILE = Demo/Info.plist;
457 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
458 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
459 | PRODUCT_BUNDLE_IDENTIFIER = com.pgs.Demo;
460 | PRODUCT_NAME = "$(TARGET_NAME)";
461 | SWIFT_VERSION = 3.0;
462 | };
463 | name = Release;
464 | };
465 | 05ECAF961E7ADF8400833D84 /* Debug */ = {
466 | isa = XCBuildConfiguration;
467 | buildSettings = {
468 | ALWAYS_SEARCH_USER_PATHS = NO;
469 | CLANG_ANALYZER_NONNULL = YES;
470 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
471 | CLANG_CXX_LIBRARY = "libc++";
472 | CLANG_ENABLE_MODULES = YES;
473 | CLANG_ENABLE_OBJC_ARC = YES;
474 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
475 | CLANG_WARN_BOOL_CONVERSION = YES;
476 | CLANG_WARN_COMMA = YES;
477 | CLANG_WARN_CONSTANT_CONVERSION = YES;
478 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
479 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
480 | CLANG_WARN_EMPTY_BODY = YES;
481 | CLANG_WARN_ENUM_CONVERSION = YES;
482 | CLANG_WARN_INFINITE_RECURSION = YES;
483 | CLANG_WARN_INT_CONVERSION = YES;
484 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
485 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
486 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
487 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
488 | CLANG_WARN_STRICT_PROTOTYPES = YES;
489 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
490 | CLANG_WARN_UNREACHABLE_CODE = YES;
491 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
492 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
493 | COPY_PHASE_STRIP = NO;
494 | CURRENT_PROJECT_VERSION = 1;
495 | DEBUG_INFORMATION_FORMAT = dwarf;
496 | ENABLE_STRICT_OBJC_MSGSEND = YES;
497 | ENABLE_TESTABILITY = YES;
498 | GCC_C_LANGUAGE_STANDARD = gnu99;
499 | GCC_DYNAMIC_NO_PIC = NO;
500 | GCC_NO_COMMON_BLOCKS = YES;
501 | GCC_OPTIMIZATION_LEVEL = 0;
502 | GCC_PREPROCESSOR_DEFINITIONS = (
503 | "DEBUG=1",
504 | "$(inherited)",
505 | );
506 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
507 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
508 | GCC_WARN_UNDECLARED_SELECTOR = YES;
509 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
510 | GCC_WARN_UNUSED_FUNCTION = YES;
511 | GCC_WARN_UNUSED_VARIABLE = YES;
512 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
513 | MTL_ENABLE_DEBUG_INFO = YES;
514 | ONLY_ACTIVE_ARCH = YES;
515 | SDKROOT = iphoneos;
516 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
517 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
518 | TARGETED_DEVICE_FAMILY = "1,2";
519 | VERSIONING_SYSTEM = "apple-generic";
520 | VERSION_INFO_PREFIX = "";
521 | };
522 | name = Debug;
523 | };
524 | 05ECAF971E7ADF8400833D84 /* Release */ = {
525 | isa = XCBuildConfiguration;
526 | buildSettings = {
527 | ALWAYS_SEARCH_USER_PATHS = NO;
528 | CLANG_ANALYZER_NONNULL = YES;
529 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
530 | CLANG_CXX_LIBRARY = "libc++";
531 | CLANG_ENABLE_MODULES = YES;
532 | CLANG_ENABLE_OBJC_ARC = YES;
533 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
534 | CLANG_WARN_BOOL_CONVERSION = YES;
535 | CLANG_WARN_COMMA = YES;
536 | CLANG_WARN_CONSTANT_CONVERSION = YES;
537 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
538 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
539 | CLANG_WARN_EMPTY_BODY = YES;
540 | CLANG_WARN_ENUM_CONVERSION = YES;
541 | CLANG_WARN_INFINITE_RECURSION = YES;
542 | CLANG_WARN_INT_CONVERSION = YES;
543 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
544 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
545 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
546 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
547 | CLANG_WARN_STRICT_PROTOTYPES = YES;
548 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
549 | CLANG_WARN_UNREACHABLE_CODE = YES;
550 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
551 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
552 | COPY_PHASE_STRIP = NO;
553 | CURRENT_PROJECT_VERSION = 1;
554 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
555 | ENABLE_NS_ASSERTIONS = NO;
556 | ENABLE_STRICT_OBJC_MSGSEND = YES;
557 | GCC_C_LANGUAGE_STANDARD = gnu99;
558 | GCC_NO_COMMON_BLOCKS = YES;
559 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
560 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
561 | GCC_WARN_UNDECLARED_SELECTOR = YES;
562 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
563 | GCC_WARN_UNUSED_FUNCTION = YES;
564 | GCC_WARN_UNUSED_VARIABLE = YES;
565 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
566 | MTL_ENABLE_DEBUG_INFO = NO;
567 | SDKROOT = iphoneos;
568 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
569 | TARGETED_DEVICE_FAMILY = "1,2";
570 | VALIDATE_PRODUCT = YES;
571 | VERSIONING_SYSTEM = "apple-generic";
572 | VERSION_INFO_PREFIX = "";
573 | };
574 | name = Release;
575 | };
576 | 05ECAF991E7ADF8400833D84 /* Debug */ = {
577 | isa = XCBuildConfiguration;
578 | buildSettings = {
579 | CLANG_ENABLE_MODULES = YES;
580 | CODE_SIGN_IDENTITY = "";
581 | DEFINES_MODULE = YES;
582 | DEVELOPMENT_TEAM = 36CHZQXF4C;
583 | DYLIB_COMPATIBILITY_VERSION = 1;
584 | DYLIB_CURRENT_VERSION = 1;
585 | DYLIB_INSTALL_NAME_BASE = "@rpath";
586 | INFOPLIST_FILE = MessagesView/Info.plist;
587 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
588 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
589 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
590 | PRODUCT_BUNDLE_IDENTIFIER = com.pgs.MessagesView;
591 | PRODUCT_NAME = "$(TARGET_NAME)";
592 | SKIP_INSTALL = YES;
593 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
594 | SWIFT_VERSION = 3.0;
595 | };
596 | name = Debug;
597 | };
598 | 05ECAF9A1E7ADF8400833D84 /* Release */ = {
599 | isa = XCBuildConfiguration;
600 | buildSettings = {
601 | CLANG_ENABLE_MODULES = YES;
602 | CODE_SIGN_IDENTITY = "";
603 | DEFINES_MODULE = YES;
604 | DEVELOPMENT_TEAM = 36CHZQXF4C;
605 | DYLIB_COMPATIBILITY_VERSION = 1;
606 | DYLIB_CURRENT_VERSION = 1;
607 | DYLIB_INSTALL_NAME_BASE = "@rpath";
608 | INFOPLIST_FILE = MessagesView/Info.plist;
609 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
610 | IPHONEOS_DEPLOYMENT_TARGET = 9.0;
611 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
612 | PRODUCT_BUNDLE_IDENTIFIER = com.pgs.MessagesView;
613 | PRODUCT_NAME = "$(TARGET_NAME)";
614 | SKIP_INSTALL = YES;
615 | SWIFT_VERSION = 3.0;
616 | };
617 | name = Release;
618 | };
619 | 05ECAF9C1E7ADF8400833D84 /* Debug */ = {
620 | isa = XCBuildConfiguration;
621 | buildSettings = {
622 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
623 | DEVELOPMENT_TEAM = 36CHZQXF4C;
624 | INFOPLIST_FILE = MessagesViewTests/Info.plist;
625 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
626 | PRODUCT_BUNDLE_IDENTIFIER = com.pgs.MessagesViewTests;
627 | PRODUCT_NAME = "$(TARGET_NAME)";
628 | SWIFT_VERSION = 3.0;
629 | };
630 | name = Debug;
631 | };
632 | 05ECAF9D1E7ADF8400833D84 /* Release */ = {
633 | isa = XCBuildConfiguration;
634 | buildSettings = {
635 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
636 | DEVELOPMENT_TEAM = 36CHZQXF4C;
637 | INFOPLIST_FILE = MessagesViewTests/Info.plist;
638 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
639 | PRODUCT_BUNDLE_IDENTIFIER = com.pgs.MessagesViewTests;
640 | PRODUCT_NAME = "$(TARGET_NAME)";
641 | SWIFT_VERSION = 3.0;
642 | };
643 | name = Release;
644 | };
645 | /* End XCBuildConfiguration section */
646 |
647 | /* Begin XCConfigurationList section */
648 | 055DA1A71E9296600091279C /* Build configuration list for PBXNativeTarget "Demo" */ = {
649 | isa = XCConfigurationList;
650 | buildConfigurations = (
651 | 055DA1A11E9296600091279C /* Debug */,
652 | 055DA1A21E9296600091279C /* Release */,
653 | );
654 | defaultConfigurationIsVisible = 0;
655 | defaultConfigurationName = Release;
656 | };
657 | 05ECAF7E1E7ADF8400833D84 /* Build configuration list for PBXProject "MessagesView" */ = {
658 | isa = XCConfigurationList;
659 | buildConfigurations = (
660 | 05ECAF961E7ADF8400833D84 /* Debug */,
661 | 05ECAF971E7ADF8400833D84 /* Release */,
662 | );
663 | defaultConfigurationIsVisible = 0;
664 | defaultConfigurationName = Release;
665 | };
666 | 05ECAF981E7ADF8400833D84 /* Build configuration list for PBXNativeTarget "MessagesView" */ = {
667 | isa = XCConfigurationList;
668 | buildConfigurations = (
669 | 05ECAF991E7ADF8400833D84 /* Debug */,
670 | 05ECAF9A1E7ADF8400833D84 /* Release */,
671 | );
672 | defaultConfigurationIsVisible = 0;
673 | defaultConfigurationName = Release;
674 | };
675 | 05ECAF9B1E7ADF8400833D84 /* Build configuration list for PBXNativeTarget "MessagesViewTests" */ = {
676 | isa = XCConfigurationList;
677 | buildConfigurations = (
678 | 05ECAF9C1E7ADF8400833D84 /* Debug */,
679 | 05ECAF9D1E7ADF8400833D84 /* Release */,
680 | );
681 | defaultConfigurationIsVisible = 0;
682 | defaultConfigurationName = Release;
683 | };
684 | /* End XCConfigurationList section */
685 | };
686 | rootObject = 05ECAF7B1E7ADF8400833D84 /* Project object */;
687 | }
688 |
--------------------------------------------------------------------------------
/MessagesView.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/MessagesView.xcodeproj/xcshareddata/xcschemes/MessagesView.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
66 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/MessagesView.xcodeproj/xcuserdata/pgs-dkanak.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Demo.xcscheme
8 |
9 | orderHint
10 | 1
11 |
12 | MessagesView.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | 055DA17B1E9296600091279C
21 |
22 | primary
23 |
24 |
25 | 055DA18E1E9296600091279C
26 |
27 | primary
28 |
29 |
30 | 055DA1991E9296600091279C
31 |
32 | primary
33 |
34 |
35 | 05ECAF831E7ADF8400833D84
36 |
37 | primary
38 |
39 |
40 | 05ECAF8C1E7ADF8400833D84
41 |
42 | primary
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/MessagesView/BubbleImage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BubbleImage.swift
3 | // MessagesView
4 | //
5 | // Created by Damian Kanak on 11/04/17.
6 | // Copyright © 2017 pgs-dkanak. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class BubbleImage {
12 |
13 | private enum Slice {
14 | case whole
15 | case top
16 | case middle
17 | case bottom
18 | }
19 |
20 | public let image: UIImage
21 |
22 | public let resizeInsets: UIEdgeInsets
23 | public let textInsets: UIEdgeInsets
24 |
25 | public lazy var whole: UIImage? = self.cropAndResize(slice: .whole)
26 | public lazy var top: UIImage? = self.cropAndResize(slice: .top)
27 | public lazy var middle: UIImage? = self.cropAndResize(slice: .middle)
28 | public lazy var bottom: UIImage? = self.cropAndResize(slice: .bottom)
29 |
30 | public var flipped: BubbleImage {
31 |
32 | let flippedImage = image.flipped
33 | let flippedResizeInsets = insetsFlippedHorizontally(resizeInsets)
34 | let flippedTextInsets = insetsFlippedHorizontally(textInsets)
35 |
36 | return BubbleImage(image: flippedImage, resizeInsets: flippedResizeInsets, textInsets: flippedTextInsets)
37 | }
38 |
39 | required public init(image: UIImage, resizeInsets: UIEdgeInsets, textInsets: UIEdgeInsets) {
40 | self.image = image
41 | self.resizeInsets = resizeInsets
42 | self.textInsets = textInsets
43 | }
44 |
45 | public convenience init(cornerRadius: CGFloat) {
46 |
47 | let image = BubbleImage.defaultBubbleImage(cornerRadius: cornerRadius)
48 |
49 | let resizeInsets = UIEdgeInsets(top: cornerRadius,
50 | left: cornerRadius * 3,
51 | bottom: cornerRadius * 2,
52 | right: cornerRadius)
53 |
54 | let textInsets = UIEdgeInsets(top: cornerRadius,
55 | left: cornerRadius * 3,
56 | bottom: cornerRadius,
57 | right: cornerRadius)
58 |
59 | self.init(image: image, resizeInsets: resizeInsets, textInsets: textInsets)
60 | }
61 |
62 | private func insetsFlippedHorizontally(_ edgeInsets: UIEdgeInsets) -> UIEdgeInsets {
63 | return UIEdgeInsets(top: edgeInsets.top,
64 | left: edgeInsets.right,
65 | bottom: edgeInsets.bottom,
66 | right: edgeInsets.left)
67 | }
68 |
69 | private func cropAndResize(slice: Slice) -> UIImage? {
70 |
71 | let width = image.size.width * image.scale
72 | let height = image.size.height * image.scale
73 |
74 | let scaledResizeInsets = UIEdgeInsets(top: resizeInsets.top * image.scale,
75 | left: resizeInsets.left * image.scale,
76 | bottom: resizeInsets.bottom * image.scale,
77 | right: resizeInsets.right * image.scale)
78 |
79 | let middleHeight = height - scaledResizeInsets.top - scaledResizeInsets.bottom
80 |
81 | let capInsets: UIEdgeInsets
82 | let cropRect: CGRect
83 |
84 | switch slice {
85 | case .whole:
86 | capInsets = resizeInsets
87 | cropRect = CGRect(x: 0, y: 0, width: width, height: height)
88 | case .top:
89 | capInsets = UIEdgeInsets(top: resizeInsets.top, left: resizeInsets.left, bottom: 0, right: resizeInsets.right)
90 | cropRect = CGRect(x: 0, y: 0, width: width, height: scaledResizeInsets.top + middleHeight)
91 | case .middle:
92 | capInsets = UIEdgeInsets(top: 0, left: resizeInsets.left, bottom: 0, right: resizeInsets.right)
93 | cropRect = CGRect(x: 0, y: scaledResizeInsets.top, width: width, height: middleHeight)
94 | case .bottom:
95 | capInsets = UIEdgeInsets(top: 0, left: resizeInsets.left, bottom: resizeInsets.bottom, right: resizeInsets.right)
96 | cropRect = CGRect(x: 0, y: scaledResizeInsets.top, width: width, height: scaledResizeInsets.bottom + middleHeight)
97 | }
98 |
99 | guard let croppedImage = image.cgImage?.cropping(to: cropRect) else {
100 | return nil
101 | }
102 | let cropped = UIImage(cgImage: croppedImage, scale: image.scale, orientation: .up)
103 |
104 |
105 | return cropped.resizableImage(withCapInsets: capInsets, resizingMode: .stretch).withRenderingMode(.alwaysTemplate)
106 | }
107 |
108 | public static func defaultBubbleImage(cornerRadius: CGFloat) -> UIImage {
109 |
110 | let size = CGSize(width: cornerRadius * 4 + 1, height: cornerRadius * 3 + 1)
111 |
112 | UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
113 |
114 | UIColor.red.setFill()
115 | UIColor.red.setStroke()
116 |
117 | let tailSize = CGSize(width: cornerRadius * 2, height: cornerRadius * 2)
118 | let bubbleSize = CGSize(width: size.width - tailSize.width, height: size.height)
119 |
120 | let bubblePath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: tailSize.width, y: 0), size: bubbleSize), byRoundingCorners: [.topLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
121 | bubblePath.fill()
122 | bubblePath.lineWidth = 1
123 | bubblePath.stroke()
124 |
125 | let tailPath = createTailPathIn(origin: CGPoint(x: 0, y: size.height - tailSize.height), size: tailSize)
126 | tailPath.fill()
127 | tailPath.stroke()
128 |
129 | let result = UIGraphicsGetImageFromCurrentImageContext()!
130 | UIGraphicsEndImageContext()
131 |
132 | return result
133 | }
134 |
135 | private static func createTailPathIn(origin: CGPoint, size: CGSize) -> UIBezierPath {
136 | let width = size.width
137 | let height = size.height
138 |
139 | let path = UIBezierPath()
140 |
141 | path.lineWidth = 1
142 |
143 | path.move(to: CGPoint(x: 0.0, y: height))
144 | path.addQuadCurve(to: CGPoint(x: width, y: 0), controlPoint: CGPoint(x: width - 1, y: height - 1))
145 | path.addLine(to: CGPoint(x: width, y: height))
146 | path.close()
147 |
148 | path.apply(CGAffineTransform(translationX: origin.x, y: origin.y))
149 |
150 | return path
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/MessagesView/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 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/MessagesView/MessageCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageCollectionViewCell.swift
3 | // kilio
4 | //
5 | // Created by Damian Kanak on 14.06.2016.
6 | // Copyright © 2016 PGS Software. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | enum Side {
12 | case right
13 | case left
14 |
15 | var other : Side {
16 | switch self {
17 | case .left:
18 | return .right
19 | case .right:
20 | return .left
21 | }
22 | }
23 | }
24 |
25 |
26 | @IBDesignable
27 | class MessageCollectionViewCell: UICollectionViewCell {
28 | @IBInspectable var cornerRadius : CGFloat = 5.0
29 | @IBInspectable var textBackgroundColor : UIColor = UIColor.blue
30 | @IBInspectable var tailStrokeColor : UIColor = UIColor.black
31 | @IBInspectable var tailFillColor : UIColor = UIColor.blue
32 |
33 | @IBOutlet weak var textLabel: UILabel!
34 | @IBOutlet weak var messageBackgroundView: UIImageView!
35 |
36 | @IBOutlet weak var labelWidthLayoutConstraint: NSLayoutConstraint!
37 | @IBOutlet weak var labelLeadingConstraint: NSLayoutConstraint!
38 | @IBOutlet weak var labelTrailingConstraint: NSLayoutConstraint!
39 | @IBOutlet weak var labelTopConstraint: NSLayoutConstraint!
40 | @IBOutlet weak var labelBottomConstraint: NSLayoutConstraint!
41 |
42 | @IBOutlet weak var backgroundTopConstraint: NSLayoutConstraint!
43 | @IBOutlet weak var backgroundTrailingConstraint: NSLayoutConstraint!
44 | @IBOutlet weak var backgroundLeadingConstraint: NSLayoutConstraint!
45 | @IBOutlet weak var backgroundBottomConstraint: NSLayoutConstraint!
46 |
47 | private let defaultBubbleMargin: CGFloat = 8
48 | private let additionalTextLabelVerticalSpacing: CGFloat = 4
49 |
50 | static let patternCell = MessageCollectionViewCell.fromNib()
51 |
52 | var side: Side = .left
53 | var positionInGroup: MessagePositionInGroup = .whole
54 | var textInsets: UIEdgeInsets = .zero
55 | var bottomSpacing: CGFloat = 0
56 | var minimalHorizontalSpacing: CGFloat = 0
57 |
58 | var message : MessagesViewChatMessage? {
59 | didSet {
60 | textLabel.text = message?.text ?? ""
61 | side = (message?.onRight ?? false) ? .right : .left
62 | }
63 | }
64 |
65 | private var backgroundMarginConstant: CGFloat = 0.0
66 | private var labelMarginConstant: CGFloat = 0.0
67 |
68 | class func fromNib() -> MessageCollectionViewCell?
69 | {
70 | var cell: MessageCollectionViewCell?
71 | let bundle = Bundle(for: self.classForCoder())
72 | let nibViews = bundle.loadNibNamed(String(describing: self.classForCoder()), owner: nil, options: nil)
73 | for nibView in nibViews! {
74 | if let cellView = nibView as? MessageCollectionViewCell {
75 | cell = cellView
76 | }
77 | }
78 | return cell
79 | }
80 |
81 | class func calculateCellSizeFor(text: String) -> CGSize {
82 | if let cell = patternCell {
83 | cell.textLabel.text = text
84 | return cell.contentView.systemLayoutSizeFitting(cell.textLabel.frame.size)
85 | }
86 | return CGSize(width: 0, height: 0)
87 | }
88 |
89 | override func awakeFromNib() {
90 | super.awakeFromNib()
91 | messageBackgroundView.backgroundColor = self.textBackgroundColor
92 | messageBackgroundView.layer.cornerRadius = self.cornerRadius
93 | backgroundMarginConstant = self.backgroundTrailingConstraint.constant
94 | labelMarginConstant = self.labelLeadingConstraint.constant
95 |
96 | autoresizingMask = [.flexibleWidth, .flexibleHeight]
97 | }
98 |
99 | override func layoutSubviews() {
100 |
101 | adjustConstraints()
102 |
103 | super.layoutSubviews()
104 | }
105 |
106 | private func adjustConstraints() {
107 |
108 | switch side {
109 |
110 | case .left:
111 | labelLeadingConstraint.constant = textInsets.left
112 | labelTrailingConstraint.constant = textInsets.right
113 | backgroundLeadingConstraint.constant = defaultBubbleMargin
114 | backgroundTrailingConstraint.constant = minimalHorizontalSpacing
115 |
116 | case .right:
117 | labelLeadingConstraint.constant = textInsets.left
118 | labelTrailingConstraint.constant = textInsets.right
119 | backgroundLeadingConstraint.constant = minimalHorizontalSpacing
120 | backgroundTrailingConstraint.constant = defaultBubbleMargin
121 | }
122 |
123 | switch positionInGroup {
124 | case .top:
125 | labelTopConstraint.constant = textInsets.top
126 | labelBottomConstraint.constant = additionalTextLabelVerticalSpacing
127 | case .middle:
128 | labelTopConstraint.constant = additionalTextLabelVerticalSpacing
129 | labelBottomConstraint.constant = additionalTextLabelVerticalSpacing
130 | case .bottom:
131 | labelTopConstraint.constant = additionalTextLabelVerticalSpacing
132 | labelBottomConstraint.constant = textInsets.bottom
133 | case .whole:
134 | labelTopConstraint.constant = textInsets.top
135 | labelBottomConstraint.constant = textInsets.bottom
136 | }
137 |
138 | backgroundBottomConstraint.constant = bottomSpacing
139 | }
140 |
141 | func size(message: String, width: CGFloat, bubbleImage: BubbleImage, minimalHorizontalSpacing: CGFloat,
142 | messagePositionInGroup: MessagePositionInGroup) -> CGSize {
143 |
144 | var labelMargins = minimalHorizontalSpacing + defaultBubbleMargin
145 | labelMargins += bubbleImage.textInsets.left + bubbleImage.textInsets.right
146 |
147 | let rect = message.boundingRect(with: CGSize(width: width - labelMargins, height: .infinity),
148 | options: [.usesLineFragmentOrigin],
149 | attributes: [NSFontAttributeName: textLabel.font], context: nil)
150 |
151 | var resultSize = rect.integral.size
152 |
153 | resultSize.width += labelMargins
154 |
155 | switch messagePositionInGroup {
156 | case .top:
157 | resultSize.height += bubbleImage.textInsets.top + additionalTextLabelVerticalSpacing
158 | case .middle:
159 | resultSize.height += 2 * additionalTextLabelVerticalSpacing
160 | case .bottom:
161 | resultSize.height += bubbleImage.textInsets.bottom + additionalTextLabelVerticalSpacing
162 | case .whole:
163 | resultSize.height += bubbleImage.textInsets.top + bubbleImage.textInsets.bottom
164 | }
165 |
166 | return resultSize
167 | }
168 |
169 | func applySettings(settings: MessagesViewSettings) {
170 |
171 | let textColor, backgroundColor: UIColor
172 |
173 | switch side {
174 | case .left:
175 | textColor = settings.leftMessageCellTextColor
176 | backgroundColor = settings.leftMessageCellBackgroundColor
177 | case .right:
178 | textColor = settings.rightMessageCellTextColor
179 | backgroundColor = settings.rightMessageCellBackgroundColor
180 | }
181 |
182 | messageBackgroundView.backgroundColor = UIColor.clear
183 | messageBackgroundView.tintColor = backgroundColor
184 |
185 | textLabel.textColor = textColor
186 |
187 | minimalHorizontalSpacing = settings.minimalHorizontalSpacing
188 | }
189 |
190 | func slideIn() {
191 | let horizontalConstraint: NSLayoutConstraint!
192 | switch self.side {
193 | case .left:
194 | horizontalConstraint = backgroundTrailingConstraint
195 | case .right:
196 | horizontalConstraint = backgroundLeadingConstraint
197 | }
198 |
199 | let horizontal = horizontalConstraint.constant
200 | let top = backgroundTopConstraint.constant
201 | backgroundTopConstraint.constant = self.textLabel.frame.height
202 | horizontalConstraint.constant = contentView.frame.size.width * 2
203 | layoutIfNeeded()
204 |
205 | UIView.animate(withDuration: 0.25) {
206 | horizontalConstraint.constant = horizontal
207 | self.backgroundTopConstraint.constant = top
208 | self.contentView.layoutIfNeeded()
209 | }
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/MessagesView/MessageCollectionViewCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/MessagesView/MessageEditorTextView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageEditorTextView.swift
3 | // kilio
4 | //
5 | // Created by Damian Kanak on 29.06.2016.
6 | // Copyright © 2016 PGS Software. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MessageEditorTextView: UITextField {
12 |
13 | func applySettings(settings: MessagesViewSettings) {
14 |
15 | textColor = settings.textInputFieldTextColor
16 | backgroundColor = settings.textInputFieldBackgroundColor
17 | tintColor = settings.textInputTintColor
18 | layer.cornerRadius = settings.textInputFieldCornerRadius
19 | placeholder = settings.textInputFieldTextPlaceholderText
20 |
21 | keyboardType = settings.keyboardType
22 | keyboardAppearance = settings.keyboardAppearance
23 | returnKeyType = settings.returnKeyType
24 | enablesReturnKeyAutomatically = settings.enablesReturnKeyAutomatically
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/MessagesView/MessagesCollectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessagesCollectionView.swift
3 | // kilio
4 | //
5 | // Created by Malgorzata Gocal on 22.02.2017.
6 | // Copyright © 2017 PGS Software. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MessagesCollectionView: UICollectionView {
12 |
13 | /*
14 | // Only override draw() if you perform custom drawing.
15 | // An empty implementation adversely affects performance during animation.
16 | override func draw(_ rect: CGRect) {
17 | // Drawing code
18 | }
19 | */
20 |
21 | func apply(settings: MessagesViewSettings) {
22 | self.backgroundColor = settings.collectionViewBackgroundColor
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/MessagesView/MessagesInputToolbar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessagesInputToolbar.swift
3 | // kilio
4 | //
5 | // Created by Damian Kanak on 29.06.2016.
6 | // Copyright © 2016 PGS Software. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MessagesInputToolbar: UIView {
12 |
13 | let toolbarContentView = MessagesToolbarContentView.fromNib()
14 |
15 | var leftButtonAction: ()->() {
16 | get {
17 | return toolbarContentView.leftButtonAction
18 | }
19 | set {
20 | toolbarContentView.leftButtonAction = newValue
21 | }
22 | }
23 | var rightButtonAction: ()->() {
24 | get {
25 | return toolbarContentView.rightButtonAction
26 | }
27 | set {
28 | toolbarContentView.rightButtonAction = newValue
29 | }
30 | }
31 |
32 | var inputText : String {
33 | get {
34 | return toolbarContentView.inputText
35 | }
36 | set {
37 | toolbarContentView.inputText = newValue
38 | }
39 | }
40 |
41 | var settings = MessagesViewSettings() {
42 | didSet {
43 | toolbarContentView.settings = self.settings
44 | }
45 | }
46 |
47 | override func awakeFromNib() {
48 | super.awakeFromNib()
49 | addSubview(toolbarContentView)
50 | toolbarContentView.frame = self.bounds
51 | }
52 |
53 | override func resignFirstResponder() -> Bool {
54 | return toolbarContentView.resignFirstResponder()
55 | }
56 |
57 | func rightButton(show: Bool, animated: Bool) {
58 | toolbarContentView.righButton(show: show, animated: animated)
59 | }
60 |
61 | func leftButton(show: Bool, animated: Bool) {
62 | toolbarContentView.leftButton(show: show, animated: animated)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/MessagesView/MessagesToolbarContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessagesToolbarContentView.swift
3 | // kilio
4 | //
5 | // Created by Damian Kanak on 29.06.2016.
6 | // Copyright © 2016 PGS Software. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class MessagesToolbarContentView: UIView {
12 |
13 | @IBOutlet weak var topSeparatorLineView: UIView!
14 | @IBOutlet weak var topSeparatorLineViewHeightConstraint: NSLayoutConstraint!
15 |
16 | @IBOutlet weak var leftButtonContainerView: UIImageView!
17 | @IBOutlet weak var leftButtonLabel: UILabel!
18 |
19 | @IBOutlet weak var rightButtonContainerView: UIImageView!
20 | @IBOutlet weak var rightButtonLabel: UILabel!
21 |
22 | @IBOutlet weak var leftButtonContainerViewLeadingConstraint: NSLayoutConstraint!
23 | @IBOutlet weak var leftButtonContainerViewWidthConstraint: NSLayoutConstraint!
24 | @IBOutlet weak var rightButtonContainerViewTrailingConstraint: NSLayoutConstraint!
25 | @IBOutlet weak var rightButtonContainerViewWidthConstraint: NSLayoutConstraint!
26 |
27 | @IBOutlet weak var messageEditorTextView: MessageEditorTextView!
28 |
29 | private var originalLeftButtonContainerViewMargin = CGFloat(0)
30 | private var originalLeftButtonContainerViewWidth = CGFloat(0)
31 | private var originalRightButtonContainerViewMargin = CGFloat(0)
32 | private var originalRightButtonContainerViewWidth = CGFloat(0)
33 |
34 | private var leftButtonEnabled: Bool = true
35 | private var rightButtonEnabled: Bool = true
36 |
37 | var leftButtonAction: () -> () = {}
38 | var rightButtonAction: () -> () = {}
39 |
40 | private var leftTintColor: UIColor {
41 | return leftButtonEnabled ? settings.leftButtonTextColor : settings.leftButtonDisabledColor
42 | }
43 |
44 | private var rightTintColor: UIColor {
45 | return rightButtonEnabled ? settings.rightButtonTextColor : settings.rightButtonDisabledColor
46 | }
47 |
48 | override func awakeFromNib() {
49 | super.awakeFromNib()
50 | saveOriginalConstraintValues()
51 | }
52 |
53 | private func saveOriginalConstraintValues() {
54 | originalLeftButtonContainerViewMargin = leftButtonContainerViewLeadingConstraint.constant
55 | originalLeftButtonContainerViewWidth = leftButtonContainerViewWidthConstraint.constant
56 | originalRightButtonContainerViewMargin = rightButtonContainerViewTrailingConstraint.constant
57 | originalRightButtonContainerViewWidth = rightButtonContainerViewWidthConstraint.constant
58 | }
59 |
60 | @IBAction func didPressLeftButton(_ sender: AnyObject) {
61 |
62 | if leftButtonEnabled {
63 | leftButtonAction()
64 | }
65 |
66 | if settings.leftButtonHidesKeyboard {
67 | _ = resignFirstResponder()
68 | }
69 | }
70 |
71 | @IBAction func didPressRightButton(_ sender: AnyObject) {
72 |
73 | if rightButtonEnabled {
74 | rightButtonAction()
75 | }
76 |
77 | if settings.rightButtonHidesKeyboard {
78 | _ = resignFirstResponder()
79 | }
80 | }
81 |
82 | override func resignFirstResponder() -> Bool {
83 | return messageEditorTextView.resignFirstResponder()
84 | }
85 |
86 | class func fromNib() -> MessagesToolbarContentView {
87 | let bundle = Bundle(for: MessagesToolbarContentView.classForCoder())
88 | let nibViews = bundle.loadNibNamed(String(describing: MessagesToolbarContentView.self), owner: nil, options: nil)
89 | return nibViews!.first as! MessagesToolbarContentView
90 | }
91 |
92 | var settings = MessagesViewSettings() {
93 | didSet {
94 | apply(settings: settings)
95 | }
96 | }
97 | var inputText : String {
98 | get {
99 | return messageEditorTextView.text ?? ""
100 | }
101 | set {
102 | messageEditorTextView.text = newValue
103 | }
104 | }
105 |
106 | func righButton(show: Bool, animated: Bool) {
107 | let destination = calculateDestination(side: .right, show: show)
108 | move(view: self.rightButtonContainerView, animated: animated, constraint: self.rightButtonContainerViewTrailingConstraint, value: destination.margin, alpha: destination.alpha)
109 | }
110 |
111 | func leftButton(show: Bool, animated: Bool) {
112 | let destination = calculateDestination(side: .left, show: show)
113 | move(view: self.leftButtonContainerView, animated: animated, constraint: self.leftButtonContainerViewLeadingConstraint, value: destination.margin, alpha: destination.alpha)
114 | }
115 |
116 | func setLeftButton(enabled: Bool) {
117 | leftButtonEnabled = enabled
118 |
119 | leftButtonLabel.textColor = leftTintColor
120 | leftButtonContainerView.tintColor = leftTintColor
121 | }
122 |
123 | func setRightButton(enabled: Bool) {
124 | rightButtonEnabled = enabled
125 |
126 | rightButtonLabel.textColor = rightTintColor
127 | rightButtonContainerView.tintColor = rightTintColor
128 | }
129 |
130 | private func calculateDestination(side: Side, show: Bool) -> (margin: CGFloat, alpha: CGFloat) {
131 | let xMargin: CGFloat
132 | let alpha: CGFloat
133 | switch (side, show) {
134 | case (.right, true):
135 | xMargin = originalRightButtonContainerViewMargin
136 | alpha = 1.0
137 | case (.right, false):
138 | xMargin = -originalRightButtonContainerViewWidth
139 | alpha = 0.0
140 | case (.left, true):
141 | xMargin = originalLeftButtonContainerViewMargin
142 | alpha = 1.0
143 | case (.left, false):
144 | xMargin = -originalLeftButtonContainerViewWidth
145 | alpha = 0.0
146 | }
147 |
148 | return (xMargin, alpha)
149 | }
150 |
151 | private func move(view: UIView, animated: Bool, constraint: NSLayoutConstraint, value: CGFloat, alpha: CGFloat) {
152 | let performTransition = {
153 | constraint.constant = value
154 | view.alpha = alpha
155 | view.superview?.layoutIfNeeded()
156 | }
157 |
158 | switch animated {
159 | case true:
160 | UIView.animate(withDuration: settings.buttonSlideAnimationDuration, animations: {
161 | performTransition()
162 | })
163 | case false:
164 | performTransition()
165 | }
166 | }
167 |
168 | private func apply(settings: MessagesViewSettings) {
169 | messageEditorTextView.applySettings(settings: settings)
170 |
171 | backgroundColor = settings.inputToolbarBackgroundColor
172 |
173 | topSeparatorLineView.backgroundColor = settings.textInputFieldTopSeparatorLineColor
174 | topSeparatorLineView.alpha = settings.textInputFieldTopSeparatorLineAlpha
175 | topSeparatorLineViewHeightConstraint.constant = settings.textInputFieldTopSeparatorLineHeight
176 |
177 | leftButtonLabel.text = settings.leftButtonText
178 | leftButtonLabel.textColor = leftTintColor
179 | leftButtonContainerView.tintColor = leftTintColor
180 | leftButtonContainerView.backgroundColor = settings.leftButtonBackgroundColor
181 | leftButtonContainerView.image = settings.leftButtonBackgroundImage?.withRenderingMode(.alwaysTemplate)
182 | leftButtonContainerView.layer.cornerRadius = settings.leftButtonCornerRadius
183 |
184 | rightButtonLabel.text = settings.rightButtonText
185 | rightButtonLabel.textColor = rightTintColor
186 | rightButtonContainerView.tintColor = rightTintColor
187 | rightButtonContainerView.backgroundColor = settings.rightButtonBackgroundColor
188 | rightButtonContainerView.image = settings.rightButtonBackgroundImage?.withRenderingMode(.alwaysTemplate)
189 | rightButtonContainerView.layer.cornerRadius = settings.rightButtonCornerRadius
190 | }
191 | }
192 |
193 | extension MessagesToolbarContentView : UITextFieldDelegate {
194 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
195 | return true
196 | }
197 |
198 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
199 |
200 | if settings.shouldDoRightActionWithReturnKey {
201 | didPressRightButton(textField)
202 | }
203 |
204 | return true
205 | }
206 |
207 | func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
208 | return true
209 | }
210 | }
211 |
212 |
--------------------------------------------------------------------------------
/MessagesView/MessagesToolbarContentView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/MessagesView/MessagesView.h:
--------------------------------------------------------------------------------
1 | //
2 | // MessagesView.h
3 | // MessagesView
4 | //
5 | // Created by pgs-dkanak on 28/02/17.
6 | // Copyright © 2017 PGS Software. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for MessagesView.
12 | FOUNDATION_EXPORT double MessagesViewVersionNumber;
13 |
14 | //! Project version string for MessagesView.
15 | FOUNDATION_EXPORT const unsigned char MessagesViewVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/MessagesView/MessagesView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessagesView.swift
3 | // kilio
4 | //
5 | // Created by pgs-dkanak on 10/03/17.
6 | // Copyright © 2017 PGS Software. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public protocol MessagesViewDelegate {
12 | func didTapLeftButton()
13 | func didTapRightButton()
14 | }
15 |
16 | public protocol MessagesViewDataSource {
17 | var messages : [MessagesViewChatMessage] {get}
18 | var peers : [MessagesViewPeer] {get}
19 | }
20 |
21 | public protocol MessagesViewChatMessage {
22 | var text : String {get}
23 | var sender: MessagesViewPeer {get}
24 | var onRight : Bool {get}
25 | }
26 |
27 | public protocol MessagesViewPeer {
28 | var id : String {get}
29 | }
30 |
31 | enum MessagePositionInGroup {
32 | case whole
33 | case top
34 | case middle
35 | case bottom
36 | }
37 |
38 | @IBDesignable
39 | public class MessagesView: UIView {
40 |
41 | @IBOutlet weak var messagesCollectionView: MessagesCollectionView!
42 | @IBOutlet weak var messagesInputToolbar: MessagesInputToolbar!
43 |
44 | @IBOutlet weak var messageInputToolbarBottomConstraint: NSLayoutConstraint!
45 |
46 | private var toolBarBottomConstraintWithoutKeyboard: CGFloat = 0
47 | private var toolBarFrameWithoutKeyboard: CGRect = .zero
48 |
49 | //MARK:- Public properties
50 |
51 | @IBInspectable public var leftMessageCellTextColor: UIColor = .black
52 | @IBInspectable public var leftMessageCellBackgroundColor: UIColor = .antiflashWhite
53 | @IBInspectable public var rightMessageCellTextColor: UIColor = .antiflashWhite
54 | @IBInspectable public var rightMessageCellBackgroundColor: UIColor = .pumpkin
55 |
56 | @IBInspectable public var collectionViewBackgroundColor: UIColor = .white
57 |
58 | @IBInspectable public var textInputFieldTextColor: UIColor = .black
59 | @IBInspectable public var textInputFieldBackgroundColor: UIColor = .clear
60 | @IBInspectable public var textInputTintColor: UIColor = .pumpkin
61 | @IBInspectable public var textInputFieldTextPlaceholderText: String = "Write your message here"
62 | @IBInspectable public var textInputFieldCornerRadius: CGFloat = 0.0
63 | @IBInspectable public var textInputFieldFont: UIFont = .systemFont(ofSize: 10)
64 |
65 | @IBInspectable public var textInputFieldTopSeparatorLineHeight: CGFloat = 1.0
66 | @IBInspectable public var textInputFieldTopSeparatorLineColor: UIColor = .pumpkin
67 | @IBInspectable public var textInputFieldTopSeparatorLineAlpha: CGFloat = 1.0
68 |
69 | @IBInspectable public var inputToolbarBackgroundColor: UIColor = UIColor.white
70 |
71 | @IBInspectable public var leftButtonText: String = "Left"
72 | @IBInspectable public var leftButtonShow: Bool = false
73 | @IBInspectable public var leftButtonShowAnimated: Bool = false
74 | @IBInspectable public var leftButtonTextColor: UIColor = .black
75 | @IBInspectable public var leftButtonDisabledColor: UIColor = .lightGray
76 | @IBInspectable public var leftButtonBackgroundColor: UIColor = .clear
77 | @IBInspectable public var leftButtonBackgroundImage: UIImage?
78 | @IBInspectable public var leftButtonCornerRadius: CGFloat = 0.0
79 |
80 | @IBInspectable public var rightButtonText: String = "Right"
81 | @IBInspectable public var rightButtonShow: Bool = true
82 | @IBInspectable public var rightButtonShowAnimated: Bool = true
83 | @IBInspectable public var rightButtonTextColor: UIColor = .pumpkin
84 | @IBInspectable public var rightButtonDisabledColor: UIColor = .lightGray
85 | @IBInspectable public var rightButtonBackgroundColor: UIColor = .clear
86 | @IBInspectable public var rightButtonBackgroundImage: UIImage?
87 | @IBInspectable public var rightButtonCornerRadius: CGFloat = 0.0
88 |
89 | public var buttonSlideAnimationDuration: TimeInterval = 0.5
90 |
91 | public var delegate : MessagesViewDelegate?
92 | public var dataSource: MessagesViewDataSource?
93 |
94 | public var isLastMessageAnimated = false
95 |
96 | //MARK:-
97 |
98 | var bubbleImageLeft: BubbleImage = BubbleImage(cornerRadius: 8)
99 | var bubbleImageRight: BubbleImage = BubbleImage(cornerRadius: 8).flipped
100 |
101 | private var isKeyboardShown = false
102 |
103 | public func setBubbleImagesWith(left: BubbleImage, right: BubbleImage? = nil) {
104 |
105 | bubbleImageLeft = left
106 | bubbleImageRight = right ?? left.flipped
107 | }
108 |
109 | public var inputText: String {
110 | get {
111 | return messagesInputToolbar.inputText
112 | }
113 | set {
114 | messagesInputToolbar.inputText = newValue
115 | }
116 | }
117 |
118 | var view: UIView!
119 | public var settings = MessagesViewSettings() {
120 | didSet {
121 | apply(settings: settings)
122 | }
123 | }
124 |
125 | struct Key {
126 | static let messageCollectionViewCell = "MessageCollectionViewCell"
127 | static let messagesCollectionViewHeader = "MessagesCollectionViewHeader"
128 | static let messagesCollectionViewFooter = "MessagesCollectionViewFooter"
129 | }
130 |
131 | override init(frame: CGRect) {
132 | super.init(frame: frame)
133 | setup()
134 | }
135 |
136 | deinit {
137 | NotificationCenter.default.removeObserver(self)
138 | }
139 |
140 | //MARK:- Public methods
141 |
142 | required public init?(coder aDecoder: NSCoder) {
143 | super.init(coder: aDecoder)
144 | setup()
145 | }
146 |
147 | public override func awakeFromNib() {
148 | super.awakeFromNib()
149 | readSettingsFromInpectables(settings: &settings)
150 | apply(settings: settings)
151 | }
152 |
153 | public override func prepareForInterfaceBuilder() {
154 | super.prepareForInterfaceBuilder()
155 | readSettingsFromInpectables(settings: &settings)
156 | apply(settings: settings)
157 | }
158 |
159 | public func refresh(scrollToLastMessage: Bool, animateLastMessage: Bool = false) {
160 | DispatchQueue.main.async {
161 | self.isLastMessageAnimated = animateLastMessage
162 | self.messagesCollectionView.reloadData()
163 |
164 | if scrollToLastMessage {
165 | self.scrollToLastMessage(animated: true)
166 | }
167 | }
168 | }
169 |
170 | public func scrollToLastMessage(animated: Bool) {
171 | guard !self.messagesCollectionView.isDragging, self.messagesCollectionView.numberOfItems(inSection: 0) > 0 else {
172 | return
173 | }
174 |
175 | self.messagesCollectionView.scrollToItem(at: IndexPath(row: self.messagesCollectionView.numberOfItems(inSection: 0) - 1, section: 0), at: .top, animated: animated)
176 | }
177 |
178 | public func leftButton(show: Bool, animated: Bool) {
179 | messagesInputToolbar.leftButton(show: show, animated: animated)
180 | }
181 |
182 | public func rightButton(show: Bool, animated: Bool) {
183 | messagesInputToolbar.rightButton(show: show, animated: animated)
184 | }
185 |
186 | public func setLeftButton(enabled: Bool) {
187 | messagesInputToolbar.toolbarContentView.setLeftButton(enabled: enabled)
188 | }
189 |
190 | public func setRightButton(enabled: Bool) {
191 | messagesInputToolbar.toolbarContentView.setRightButton(enabled: enabled)
192 | }
193 |
194 | //MARK:-
195 |
196 | private func setup() {
197 | view = loadFromNib()
198 | addSubview(view)
199 | pinSubviewToEdges(subview: view)
200 | registerCellNib()
201 |
202 | messagesInputToolbar.leftButtonAction = { [weak self] _ in
203 | self?.delegate?.didTapLeftButton()
204 | }
205 | messagesInputToolbar.rightButtonAction = { [weak self] _ in
206 | self?.delegate?.didTapRightButton()
207 | }
208 |
209 | messagesInputToolbar.settings = settings
210 |
211 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: .UIKeyboardWillShow, object: nil)
212 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: .UIKeyboardWillHide, object: nil)
213 | NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame), name: .UIKeyboardWillChangeFrame, object: nil)
214 | }
215 |
216 | @objc private func keyboardWillShow(notification: Notification) {
217 |
218 | guard !isKeyboardShown else {
219 | return
220 | }
221 |
222 | isKeyboardShown = true
223 |
224 | toolBarBottomConstraintWithoutKeyboard = messageInputToolbarBottomConstraint.constant
225 | toolBarFrameWithoutKeyboard = convert(messagesInputToolbar.frame, to: nil)
226 |
227 | respondToKeyboardFrameChange(notification: notification)
228 | }
229 |
230 | @objc private func keyboardWillChangeFrame(notification: Notification) {
231 |
232 | guard isKeyboardShown else {
233 | return
234 | }
235 |
236 | respondToKeyboardFrameChange(notification: notification)
237 | }
238 |
239 | @objc private func keyboardWillHide(notification: Notification) {
240 |
241 | isKeyboardShown = false
242 | }
243 |
244 | private func respondToKeyboardFrameChange(notification: Notification) {
245 |
246 | guard settings.shouldAdjustToKeyboard,
247 | let userInfo = notification.userInfo,
248 | let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
249 | let animationDuration = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue else {
250 | return
251 | }
252 |
253 | let keyboardOverlap = toolBarFrameWithoutKeyboard.origin.y - keyboardFrame.origin.y
254 |
255 | let verticalAdjustment = keyboardOverlap > 0 ? keyboardOverlap + toolBarFrameWithoutKeyboard.size.height : 0
256 |
257 | let newBottomConstraint = toolBarBottomConstraintWithoutKeyboard + verticalAdjustment
258 |
259 | guard newBottomConstraint != messageInputToolbarBottomConstraint.constant else {
260 | return
261 | }
262 |
263 | messageInputToolbarBottomConstraint.constant = newBottomConstraint
264 |
265 | UIView.animate(withDuration: animationDuration) {
266 | let contentOffset = self.messagesCollectionView.contentOffset
267 |
268 | self.messagesCollectionView.contentOffset = CGPoint(x: contentOffset.x, y: contentOffset.y + verticalAdjustment)
269 | self.layoutIfNeeded()
270 | }
271 | }
272 |
273 | private func pinSubviewToEdges(subview: UIView) {
274 | subview.translatesAutoresizingMaskIntoConstraints = false
275 |
276 | subview.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
277 | subview.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
278 | subview.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
279 | subview.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
280 | }
281 |
282 | private func loadFromNib() -> UIView {
283 | let bundle = Bundle(for: MessagesView.classForCoder())
284 | guard let view = bundle.loadNibNamed("MessagesView", owner: self, options: [:])?.first as? UIView else {
285 | assertionFailure("No nib for MessagesView")
286 | return UIView()
287 | }
288 | return view
289 | }
290 |
291 | func registerCellNib() {
292 | let nib = UINib(nibName: Key.messageCollectionViewCell, bundle: Bundle(for: self.classForCoder))
293 | messagesCollectionView.register(nib, forCellWithReuseIdentifier: Key.messageCollectionViewCell)
294 | messagesCollectionView.register(UICollectionReusableView.classForCoder(), forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: Key.messagesCollectionViewHeader)
295 | messagesCollectionView.register(UICollectionReusableView.classForCoder(), forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: Key.messagesCollectionViewFooter)
296 | }
297 |
298 | @IBAction func didTapCollectionViewArea(_ sender: Any) {
299 | _ = messagesInputToolbar.resignFirstResponder()
300 | }
301 |
302 | public func refresh() {
303 | DispatchQueue.main.async {
304 | self.messagesCollectionView.reloadData()
305 | }
306 | }
307 |
308 | fileprivate func messagePositionInGroup(for index: Int) -> MessagePositionInGroup {
309 |
310 | guard let messages = dataSource?.messages else {
311 | return .whole
312 | }
313 |
314 | var isPreviousMessageOnTheSameSide = false
315 | var isNextMessageOnTheSameSide = false
316 |
317 | if 0 <= index-1 {
318 | isPreviousMessageOnTheSameSide = messages[index-1].onRight == messages[index].onRight
319 | }
320 |
321 | if index+1 < messages.count {
322 | isNextMessageOnTheSameSide = messages[index+1].onRight == messages[index].onRight
323 | }
324 |
325 | switch (isPreviousMessageOnTheSameSide, isNextMessageOnTheSameSide) {
326 | case (false, false):
327 | return .whole
328 | case (false, true):
329 | return .top
330 | case (true, false):
331 | return .bottom
332 | case (true, true):
333 | return .middle
334 | }
335 | }
336 |
337 | private func readSettingsFromInpectables(settings: inout MessagesViewSettings) {
338 |
339 | settings.leftMessageCellTextColor = leftMessageCellTextColor
340 | settings.leftMessageCellBackgroundColor = leftMessageCellBackgroundColor
341 | settings.rightMessageCellTextColor = rightMessageCellTextColor
342 | settings.rightMessageCellBackgroundColor = rightMessageCellBackgroundColor
343 |
344 | settings.collectionViewBackgroundColor = collectionViewBackgroundColor
345 |
346 | settings.textInputFieldTextColor = textInputFieldTextColor
347 | settings.textInputFieldBackgroundColor = textInputFieldBackgroundColor
348 | settings.textInputTintColor = textInputTintColor
349 | settings.textInputFieldTextPlaceholderText = textInputFieldTextPlaceholderText
350 | settings.textInputFieldCornerRadius = textInputFieldCornerRadius
351 | settings.textInputFieldFont = textInputFieldFont
352 |
353 | settings.textInputFieldTopSeparatorLineHeight = textInputFieldTopSeparatorLineHeight
354 | settings.textInputFieldTopSeparatorLineColor = textInputFieldTopSeparatorLineColor
355 | settings.textInputFieldTopSeparatorLineAlpha = textInputFieldTopSeparatorLineAlpha
356 |
357 | settings.inputToolbarBackgroundColor = inputToolbarBackgroundColor
358 |
359 | settings.leftButtonText = leftButtonText
360 | settings.leftButtonShow = leftButtonShow
361 | settings.leftButtonShowAnimated = leftButtonShowAnimated
362 | settings.leftButtonTextColor = leftButtonTextColor
363 | settings.leftButtonDisabledColor = leftButtonDisabledColor
364 | settings.leftButtonBackgroundColor = leftButtonBackgroundColor
365 | settings.leftButtonBackgroundImage = leftButtonBackgroundImage
366 | settings.leftButtonCornerRadius = leftButtonCornerRadius
367 |
368 | settings.rightButtonText = rightButtonText
369 | settings.rightButtonShow = rightButtonShow
370 | settings.rightButtonShowAnimated = rightButtonShowAnimated
371 | settings.rightButtonTextColor = rightButtonTextColor
372 | settings.rightButtonDisabledColor = rightButtonDisabledColor
373 | settings.rightButtonBackgroundColor = rightButtonBackgroundColor
374 | settings.rightButtonBackgroundImage = rightButtonBackgroundImage
375 | settings.rightButtonCornerRadius = rightButtonCornerRadius
376 |
377 | settings.buttonSlideAnimationDuration = buttonSlideAnimationDuration
378 | }
379 |
380 | private func apply(settings: MessagesViewSettings) {
381 | messagesInputToolbar.settings = settings
382 | messagesCollectionView.apply(settings: settings)
383 | leftButton(show: settings.leftButtonShow, animated: settings.leftButtonShowAnimated)
384 | rightButton(show: settings.rightButtonShow, animated: settings.rightButtonShowAnimated)
385 | }
386 | }
387 |
388 | extension MessagesView: UICollectionViewDataSource {
389 |
390 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
391 | return dataSource?.messages.count ?? 0
392 | }
393 |
394 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
395 |
396 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Key.messageCollectionViewCell, for: indexPath) as? MessageCollectionViewCell,
397 | let messages = dataSource?.messages else {
398 | return UICollectionViewCell()
399 | }
400 |
401 | cell.message = messages[indexPath.row]
402 |
403 | let bubbleImage = messages[indexPath.row].onRight ? bubbleImageRight : bubbleImageLeft
404 |
405 | let messagePosition = messagePositionInGroup(for: indexPath.row)
406 |
407 | switch messagePosition {
408 | case .whole:
409 | cell.messageBackgroundView.image = bubbleImage.whole
410 | cell.bottomSpacing = settings.groupSeparationSpacing
411 | case .top:
412 | cell.messageBackgroundView.image = bubbleImage.top
413 | cell.bottomSpacing = settings.groupInternalSpacing
414 | case .middle:
415 | cell.messageBackgroundView.image = bubbleImage.middle
416 | cell.bottomSpacing = settings.groupInternalSpacing
417 | case .bottom:
418 | cell.messageBackgroundView.image = bubbleImage.bottom
419 | cell.bottomSpacing = settings.groupSeparationSpacing
420 | }
421 |
422 | cell.positionInGroup = messagePosition
423 | cell.textInsets = bubbleImage.textInsets
424 |
425 | cell.applySettings(settings: settings)
426 |
427 | return cell
428 | }
429 |
430 | public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
431 |
432 | switch kind {
433 | case UICollectionElementKindSectionHeader:
434 | let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: Key.messagesCollectionViewHeader, for: indexPath)
435 | headerView.backgroundColor = settings.messageCollectionViewHeaderBackgroundColor
436 | return headerView
437 | case UICollectionElementKindSectionFooter:
438 | let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: Key.messagesCollectionViewFooter, for: indexPath)
439 | footerView.backgroundColor = settings.messageCollectionViewFooterBackgroundColor
440 | return footerView
441 | default:
442 | fatalError("Unexpected element kind")
443 | }
444 | }
445 |
446 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
447 | return CGSize(width: collectionView.contentSize.width, height: CGFloat(settings.messageCollectionViewHeaderHeight))
448 | }
449 |
450 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
451 | return CGSize(width: collectionView.contentSize.width, height: CGFloat(settings.messageCollectionViewFooterHeight))
452 | }
453 | }
454 |
455 | extension MessagesView: UICollectionViewDelegateFlowLayout {
456 |
457 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
458 |
459 | guard let message = dataSource?.messages[indexPath.row], let cell = MessageCollectionViewCell.fromNib() else {
460 | return .zero
461 | }
462 |
463 | let requiredWidth = collectionView.bounds.width - collectionView.contentInset.left - collectionView.contentInset.right
464 |
465 | let bubble = message.onRight ? bubbleImageRight : bubbleImageLeft
466 | let messagePosition = messagePositionInGroup(for: indexPath.row)
467 |
468 | var size = cell.size(message: message.text, width: requiredWidth, bubbleImage: bubble, minimalHorizontalSpacing: settings.minimalHorizontalSpacing, messagePositionInGroup: messagePosition)
469 | size.width = requiredWidth
470 |
471 | switch messagePosition {
472 |
473 | case .bottom, .whole:
474 | size.height += settings.groupSeparationSpacing
475 |
476 | case .top, .middle:
477 | size.height += settings.groupInternalSpacing
478 | }
479 |
480 | return size
481 | }
482 |
483 | func isLastMessage(indexPath: IndexPath)->Bool {
484 | let lastMessageIndex = (dataSource?.messages.count ?? 0) - 1
485 | return indexPath.row == lastMessageIndex
486 | }
487 |
488 | public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
489 | guard let cell = cell as? MessageCollectionViewCell else {
490 | return
491 | }
492 | if isLastMessage(indexPath: indexPath) && isLastMessageAnimated {
493 | cell.slideIn()
494 | }
495 | }
496 | }
497 |
--------------------------------------------------------------------------------
/MessagesView/MessagesView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/MessagesView/MessagesViewSettings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessagesViewSettings.swift
3 | // kilio
4 | //
5 | // Created by Damian Kanak on 14.07.2016.
6 | // Copyright © 2016 PGS Software. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | public class MessagesViewSettings {
13 | public var leftButtonActionName = ""
14 | public var rightButtonActionName = ""
15 |
16 | public var leftButtonHidesKeyboard = false
17 | public var rightButtonHidesKeyboard = false
18 | public var shouldAdjustToKeyboard = true
19 | public var shouldDoRightActionWithReturnKey = true
20 |
21 | public var keyboardType: UIKeyboardType = .default
22 | public var keyboardAppearance: UIKeyboardAppearance = .default
23 | public var returnKeyType: UIReturnKeyType = .done
24 | public var enablesReturnKeyAutomatically = false
25 |
26 | public var textInputScrollsToRecentMessage = true
27 |
28 | public var messageCollectionViewHeaderHeight = 5.0
29 | public var messageCollectionViewFooterHeight = 20.0
30 | public var messageCollectionViewHeaderBackgroundColor = UIColor.clear
31 | public var messageCollectionViewFooterBackgroundColor = UIColor.clear
32 |
33 | public var leftMessageCellTextColor: UIColor = .black
34 | public var leftMessageCellBackgroundColor: UIColor = .antiflashWhite
35 | public var rightMessageCellTextColor: UIColor = .antiflashWhite
36 | public var rightMessageCellBackgroundColor: UIColor = .pumpkin
37 |
38 | public var collectionViewBackgroundColor: UIColor = .white
39 |
40 | public var textInputFieldTextColor: UIColor = .black
41 | public var textInputFieldBackgroundColor: UIColor = .clear
42 | public var textInputTintColor: UIColor = .pumpkin
43 | public var textInputFieldTextPlaceholderText: String = "Write your message here"
44 | public var textInputFieldCornerRadius: CGFloat = 0.0
45 | public var textInputFieldFont: UIFont = .systemFont(ofSize: 10)
46 |
47 | public var textInputFieldTopSeparatorLineHeight: CGFloat = 1.0
48 | public var textInputFieldTopSeparatorLineColor: UIColor = .pumpkin
49 | public var textInputFieldTopSeparatorLineAlpha: CGFloat = 1.0
50 |
51 | public var inputToolbarBackgroundColor = UIColor.white
52 |
53 | public var leftButtonText: String = ""
54 | public var leftButtonShow: Bool = false
55 | public var leftButtonShowAnimated: Bool = false
56 | public var leftButtonTextColor: UIColor = .pumpkin
57 | public var leftButtonDisabledColor: UIColor = .antiflashWhite
58 | public var leftButtonBackgroundColor: UIColor = .clear
59 | public var leftButtonBackgroundImage: UIImage?
60 | public var leftButtonCornerRadius: CGFloat = 0.0
61 |
62 | public var rightButtonText: String = "Send"
63 | public var rightButtonShow: Bool = true
64 | public var rightButtonShowAnimated: Bool = true
65 | public var rightButtonTextColor: UIColor = .pumpkin
66 | public var rightButtonDisabledColor: UIColor = .antiflashWhite
67 | public var rightButtonBackgroundColor: UIColor = .clear
68 | public var rightButtonBackgroundImage: UIImage?
69 | public var rightButtonCornerRadius: CGFloat = 0.0
70 |
71 | public var buttonSlideAnimationDuration: TimeInterval = 0.5
72 |
73 | public var groupSeparationSpacing: CGFloat = 12
74 | public var groupInternalSpacing: CGFloat = 1
75 | public var minimalHorizontalSpacing: CGFloat = 80
76 |
77 | public static var defaultSettings: MessagesViewSettings {
78 | return MessagesViewSettings()
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/MessagesView/UIColor+RGB.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorUtils.swift
3 | // MessagesView
4 | //
5 | // Created by Damian Kanak on 23/03/17.
6 | // Copyright © 2017 pgs-dkanak. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension UIColor {
12 | var rgba : (CGFloat, CGFloat, CGFloat, CGFloat) {
13 | var red = CGFloat()
14 | var green = CGFloat()
15 | var blue = CGFloat()
16 | var alpha = CGFloat()
17 | self.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
18 | return (red, green, blue, alpha)
19 | }
20 |
21 | convenience init(red: Int, green: Int, blue: Int) {
22 | assert(red >= 0 && red <= 255, "Invalid red component")
23 | assert(green >= 0 && green <= 255, "Invalid green component")
24 | assert(blue >= 0 && blue <= 255, "Invalid blue component")
25 |
26 | self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0)
27 | }
28 |
29 | convenience init(rgb: Int) {
30 | self.init(
31 | red: (rgb >> 16) & 0xFF,
32 | green: (rgb >> 8) & 0xFF,
33 | blue: rgb & 0xFF
34 | )
35 | }
36 | }
37 |
38 | //MARK: MessagesView unique color schema
39 |
40 | extension UIColor {
41 | static let eucalyptus = UIColor(rgb: 0x42C4A3)
42 | static let pumpkin = UIColor(rgb: 0xFF7726)
43 | static let antiflashWhite = UIColor(rgb: 0xF0F4F2)
44 | static let pastelGrey = UIColor(rgb: 0xCCCCCC)
45 | }
46 |
--------------------------------------------------------------------------------
/MessagesView/UIImage+Flipped.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImageUtils.swift
3 | // MessagesView
4 | //
5 | // Created by Damian Kanak on 20/04/17.
6 | // Copyright © 2017 pgs-dkanak. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension UIImage {
12 | var flipped: UIImage {
13 |
14 | defer {
15 | UIGraphicsEndImageContext()
16 | }
17 |
18 | UIGraphicsBeginImageContextWithOptions(self.size, false, scale)
19 |
20 | guard let context = UIGraphicsGetCurrentContext() else {
21 | return UIImage()
22 | }
23 |
24 | context.translateBy(x: size.width, y: size.height)
25 | context.scaleBy(x: -1, y: -1)
26 | context.draw(self.cgImage!, in: CGRect(origin: .zero, size: size))
27 |
28 | return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/MessagesViewTests/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 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/MessagesViewTests/MessagesViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessagesViewTests.swift
3 | // MessagesViewTests
4 | //
5 | // Created by pgs-dkanak on 16/03/17.
6 | // Copyright © 2017 pgs-dkanak. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import MessagesView
11 |
12 | class MessagesViewTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | // Use XCTAssert and related functions to verify your tests produce the correct results.
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | # MessagesView
3 |
4 | [](https://swift.org/)
5 | [](https://github.com/Carthage/Carthage)
6 |
7 | View for displaying messages similarly to iOS Messages system app. This view when deployed within your application will handle incoming and outgoing messages display.
8 |
9 | While using this module you don't need to handle messages view on your own. You're getting complete solution easy to configure and customize to your needs. If you think you can have more customisation, please tell us what you think via addess below.
10 |
11 | 
12 |
13 |
14 | ## Getting started
15 |
16 | In order to start using this framework you need to:
17 | 1. Embed this framework
18 | 2. Style your view
19 | 3. Communicate with view via ViewController
20 |
21 |
22 |
23 | ### 1. Embedding framework
24 |
25 |
26 | #### 1.1 Using Carthage
27 | In Cartfile put
28 | `github "PGSSoft/MessagesView"`
29 |
30 | In project root directory say:
31 | > carthage update --no-use-binaries --platform iOS
32 |
33 | This will fetch the project and compile it to library form. When using carthage, you have two options:
34 |
35 | ### a) Standard carthage module
36 |
37 | 
38 |
39 | Using standard cathage module requires you to go to Carthage/Build folder within your project and drag it into _**Embedded Binaries**_ section int your project **General settings**.
40 | This solution no 1. is pretty standard. Unfortunately it doesn't work with storyboard as we intended and you will not be able to customize MessagesView from storyboard directly. It will be working but have to be customized from code. Considering all conditions above we recommend _Embedding into your project_.
41 |
42 | ### b) Embedding sources into your project
43 |
44 | 
45 |
46 | to embed this project as source code you need to:
47 | 1. Go to folder `Carthage/Checkouts/MessagesView` and drag `MessagesView.scodeproj` into your own project.
48 | 2. In MessagesView project find `Products/MessagesView.framework` and drag it into `Embedded Frameworks` section in your project general settings
49 |
50 | *NOTE: To be able to track changes from storyboard at design time, you need to embed framework in non-standard way as described in 1b.*
51 |
52 |
53 | ### 2. Styling your View
54 |
55 | Let's design!
56 |
57 | 1. Go to storyboard
58 | 2. Set up a `UIView` with constraints of your choice
59 | 3. Change owner class from `UIView` to `MessagesView`. Don't forget to change module name underneath too into `MessagesView`. Xcode will now recompile source code necessary to show rendered MessagesView in Storyboard. Completely rendered messages view will appear in storyboard in seconds!
60 |
61 | Now you are free to style your messages view as you wish!
62 |
63 | Example:
64 | - change messages background color
65 | - change button label caption
66 | - change button background color
67 | - change button background image
68 | 
69 | You can find full list of customizable properties in the Wiki. This will be prepared soon.
70 |
71 | ### 3. Communicating with View via ViewController
72 |
73 | Create example ViewController From example below. ViewController has to conform protocols `MessagesViewDataSource` and `MessagesViewDelegate`.
74 |
75 | In order to communicate with MessagesView, your ViewController should contain:
76 | - `MessagesViewDelegate` to take action when user taps a button
77 | - `MessagesViewDataSource` to feed the view
78 | - `IBOutlet MessagesView` to read information from view
79 |
80 | ##### Don't forget do connect MessagesView to its `MessagesViewDataSource` and `MessagesViewDelegate`!
81 |
82 | #### 3.1 MessagesViewDelegate
83 |
84 |
85 | ```swift
86 | public protocol MessagesViewDelegate {
87 | func didTapLeftButton()
88 | func didTapRightButton()
89 | }
90 | ```
91 |
92 | Your viewController need to implement actions taken after user taps left or right button
93 |
94 | #### 3.2 MessagesViewDataSource
95 |
96 | To feed view with intormation, your datasource have to provide two sets of information: *peers* and *messages*
97 |
98 | ```swift
99 | public protocol MessagesViewDataSource {
100 | var messages : [MessagesViewChatMessage] {get}
101 | var peers : [MessagesViewPeer] {get}
102 | }
103 | ```
104 |
105 | As you can see objects that carry messages have to conform to `MessagesViewChatMessage` protocol and objects that carry peers have to conform to `MessagesViewPeer`protocol. These are listed below:
106 |
107 | ```swift
108 | public protocol MessagesViewChatMessage {
109 | var text : String {get}
110 | var sender: MessagesViewPeer {get}
111 | var onRight : Bool {get}
112 | }
113 |
114 | public protocol MessagesViewPeer {
115 | var id : String {get}
116 | }
117 | ```
118 |
119 | In the demo app we created extension to ViewController class that makes it compliant to `MessagesViewDataSource` protocol. In this case the limitation was that extension cannot contain stored properties so we decided to provide information via computed variables only but you can do as you wish in your own project. You need only to have object which is `MessagesViewDataSource` compliant. This is how we dealt with it in demo app:
120 |
121 | ```swift
122 | extension ViewController: MessagesViewDataSource {
123 | struct Peer: MessagesViewPeer {
124 | var id: String
125 | }
126 |
127 | struct Message: MessagesViewChatMessage {
128 | var text: String
129 | var sender: MessagesViewPeer
130 | var onRight: Bool
131 | }
132 |
133 | var peers: [MessagesViewPeer] {
134 | return TestData.peerNames.map{ Peer(id: $0) }
135 | }
136 |
137 | var messages: [MessagesViewChatMessage] {
138 | return TestData.exampleMessageText.enumerated().map { (index, element) in
139 | let peer = self.peers[index % peers.count]
140 | return Message(text: element, sender: peer, onRight: index%2 == 0)
141 | }
142 | }
143 | }
144 | ```
145 |
146 | In this example, we have converted on the fly our stored `TestData` information into *Messages* and *Peers*. No other class in the project have to be aware of `MessagesView`. Only `ViewController` is interested.
147 |
148 | #### 3.3 Create IBOutlet messagesView
149 |
150 | Create IBOutlet in standard way
151 | 
152 |
153 | Having your ViewController know about messagesView presence enables it to use view's public API.
154 |
155 | #### 3.4 Connecting delegate and data source
156 |
157 | In our example it is when ViewController loads:
158 |
159 | ```swift
160 | override func viewDidLoad() {
161 | super.viewDidLoad()
162 | // Do any additional setup after loading the view, typically from a nib.
163 | messagesView.delegate = self
164 | messagesView.dataSource = self
165 | }
166 | ```
167 |
168 | #### 3.5 Custom behaviours
169 |
170 | Additional behaviours that may be useful to you.
171 |
172 | - hiding buttons
173 | ```swift
174 | leftButton(show: Bool, animated: Bool)
175 | rightButton(show: Bool, animated: Bool)
176 | ```
177 |
178 | This way you can show or hide button. Animated or not - as you wish.
179 |
180 | ### Stay in touch
181 | If you have any ideas how this project can be developed - do not hesitate to contact us at:
182 |
183 | dkanak@pgs-soft.com
184 |
185 | mgocal@pgs-soft.com
186 |
187 | bdudar@pgs-soft.com
188 |
189 | kszwaba@pgs-soft.com
190 |
191 | You are now fully aware of MessagesView capabilities. Good luck!
192 |
193 | ### Contributing to development
194 | When contributing to MessagesView you may need to run it from within another project. Embedding this framework as described in paragraph 1.1b will help you work on opened source of this project.
195 |
196 | Made with love in PGS ♥
197 |
198 | __Please Use develop branch and fork from there!__
199 |
200 | ### License ###
201 | MIT License
202 |
203 | Copyright (c) 2016 PGS Software SA
204 |
205 | Permission is hereby granted, free of charge, to any person obtaining a copy
206 | of this software and associated documentation files (the "Software"), to deal
207 | in the Software without restriction, including without limitation the rights
208 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
209 | copies of the Software, and to permit persons to whom the Software is
210 | furnished to do so, subject to the following conditions:
211 |
212 | The above copyright notice and this permission notice shall be included in all
213 | copies or substantial portions of the Software.
214 |
215 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
216 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
217 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
218 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
219 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
220 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
221 | SOFTWARE.
222 |
--------------------------------------------------------------------------------