├── .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 | ![Made with love by PGS](https://cloud.githubusercontent.com/assets/16896355/25438562/3c14f0f2-2a9a-11e7-82f1-53f49a48393e.png) 2 | # MessagesView 3 | 4 | [![Swift 3.0](https://img.shields.io/badge/Swift-3.0-orange.svg?style=flat)](https://swift.org/) 5 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](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 | ![Messages View](https://cloud.githubusercontent.com/assets/16896355/25438510/0b81e86e-2a9a-11e7-9981-df9030cda73d.png) 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 | ![embedding carthage framework](https://cloud.githubusercontent.com/assets/16896355/24654187/f16728c6-1938-11e7-806b-5ea14b4c7284.gif) 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 | ![embedding source project](https://cloud.githubusercontent.com/assets/16896355/24654176/e46736a2-1938-11e7-9425-c856cc9de166.gif) 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 | ![styling input field background and text field rounding](https://cloud.githubusercontent.com/assets/16896355/24654216/10f95768-1939-11e7-9163-79acc0753d62.gif) 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 | ![creating IBOutlet for messagesView](https://cloud.githubusercontent.com/assets/16896355/24657607/2cff52f6-1947-11e7-8840-a6c2bb3d44fb.gif) 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 | --------------------------------------------------------------------------------