├── .gitignore ├── .swift-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Messages.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── MessagesApp.xcscheme ├── Messages.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── MessagesApp ├── .swiftlint.yml ├── Application │ └── AppDelegate.swift ├── Helpers │ └── Sequence_UniqueElements.swift ├── Models │ └── Message.swift ├── Resources │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ └── quotes.json ├── Services │ ├── DemoMessagesService.swift │ └── MessagesService.swift ├── Supporting Files │ └── Info.plist └── UI │ ├── Controllers │ ├── MessageSectionController.swift │ ├── MessagesCollectionViewController.swift │ ├── MessagesViewController.swift │ └── OutgoingMessageSectionController.swift │ ├── Helpers │ ├── UICollectionViewCell_ComputeHeight.swift │ ├── UIScrollView_ReversedContentOffset.swift │ └── UIViewController_Embed.swift │ ├── Layout │ └── MessagesCollectionViewLayout.swift │ ├── Storyboards │ └── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── ViewModels │ ├── MessageViewModel.swift │ └── OutgoingMessageViewModel.swift │ └── Views │ ├── MessageCell.swift │ ├── MessagesInputAccessoryView.swift │ └── MessagesView.swift ├── Misc └── sending_test_message.gif ├── Podfile ├── Podfile.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ################################ 2 | # OS X 3 | 4 | *.DS_Store 5 | *.swp 6 | .Trashes 7 | 8 | ################################ 9 | # Xcode 10 | 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspective 18 | !default.perspective 19 | *.perspectivev3 20 | !default.perspectivev3 21 | *.xcuserstate 22 | project.xcworkspace/ 23 | xcuserdata/ 24 | build/ 25 | dist/ 26 | DerivedData/ 27 | *.moved-aside 28 | *.xccheckout 29 | 30 | ################################ 31 | # AppCode 32 | 33 | .idea 34 | 35 | ################################ 36 | # Backup files 37 | 38 | *~ 39 | *~.nib 40 | *~.xib 41 | \#*# 42 | .#* 43 | 44 | ################################ 45 | # CocoaPods 46 | 47 | Pods/ 48 | !Podfile.lock 49 | 50 | ################################ 51 | # Fastlane 52 | 53 | fastlane/report.xml 54 | fastlane/test_output 55 | fastlane/screenshots 56 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.1 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'synx', '~> 0.2' 4 | gem 'cocoapods', '~> 1.2' 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.0) 5 | activesupport (4.2.10) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | atomos (0.1.3) 11 | claide (1.0.2) 12 | clamp (0.6.5) 13 | cocoapods (1.5.3) 14 | activesupport (>= 4.0.2, < 5) 15 | claide (>= 1.0.2, < 2.0) 16 | cocoapods-core (= 1.5.3) 17 | cocoapods-deintegrate (>= 1.0.2, < 2.0) 18 | cocoapods-downloader (>= 1.2.0, < 2.0) 19 | cocoapods-plugins (>= 1.0.0, < 2.0) 20 | cocoapods-search (>= 1.0.0, < 2.0) 21 | cocoapods-stats (>= 1.0.0, < 2.0) 22 | cocoapods-trunk (>= 1.3.0, < 2.0) 23 | cocoapods-try (>= 1.1.0, < 2.0) 24 | colored2 (~> 3.1) 25 | escape (~> 0.0.4) 26 | fourflusher (~> 2.0.1) 27 | gh_inspector (~> 1.0) 28 | molinillo (~> 0.6.5) 29 | nap (~> 1.0) 30 | ruby-macho (~> 1.1) 31 | xcodeproj (>= 1.5.7, < 2.0) 32 | cocoapods-core (1.5.3) 33 | activesupport (>= 4.0.2, < 6) 34 | fuzzy_match (~> 2.0.4) 35 | nap (~> 1.0) 36 | cocoapods-deintegrate (1.0.2) 37 | cocoapods-downloader (1.2.1) 38 | cocoapods-plugins (1.0.0) 39 | nap 40 | cocoapods-search (1.0.0) 41 | cocoapods-stats (1.0.0) 42 | cocoapods-trunk (1.3.0) 43 | nap (>= 0.8, < 2.0) 44 | netrc (~> 0.11) 45 | cocoapods-try (1.1.0) 46 | colored2 (3.1.2) 47 | colorize (0.8.1) 48 | concurrent-ruby (1.0.5) 49 | escape (0.0.4) 50 | fourflusher (2.0.1) 51 | fuzzy_match (2.0.4) 52 | gh_inspector (1.1.3) 53 | i18n (0.9.5) 54 | concurrent-ruby (~> 1.0) 55 | minitest (5.11.3) 56 | molinillo (0.6.6) 57 | nanaimo (0.2.6) 58 | nap (1.1.0) 59 | netrc (0.11.0) 60 | ruby-macho (1.2.0) 61 | synx (0.2.1) 62 | clamp (~> 0.6) 63 | colorize (~> 0.7) 64 | xcodeproj (~> 1.0) 65 | thread_safe (0.3.6) 66 | tzinfo (1.2.5) 67 | thread_safe (~> 0.1) 68 | xcodeproj (1.5.9) 69 | CFPropertyList (>= 2.3.3, < 4.0) 70 | atomos (~> 0.1.2) 71 | claide (>= 1.0.2, < 2.0) 72 | colored2 (~> 3.1) 73 | nanaimo (~> 0.2.5) 74 | 75 | PLATFORMS 76 | ruby 77 | 78 | DEPENDENCIES 79 | cocoapods (~> 1.2) 80 | synx (~> 0.2) 81 | 82 | BUNDLED WITH 83 | 1.16.3 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dariusz Rybicki Darrarski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Messages.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 310395971EAEA65B0096390A /* UICollectionViewCell_ComputeHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310395961EAEA65B0096390A /* UICollectionViewCell_ComputeHeight.swift */; }; 11 | 3103959B1EAEB44B0096390A /* OutgoingMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3103959A1EAEB44B0096390A /* OutgoingMessageViewModel.swift */; }; 12 | 3103959D1EAEB5820096390A /* OutgoingMessageSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3103959C1EAEB5820096390A /* OutgoingMessageSectionController.swift */; }; 13 | 3128630C1EAE180800C6B202 /* UIScrollView_ReversedContentOffset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3128630B1EAE180800C6B202 /* UIScrollView_ReversedContentOffset.swift */; }; 14 | 3154AEEF1EA8FBD00058C26C /* MessagesCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3154AEEE1EA8FBD00058C26C /* MessagesCollectionViewController.swift */; }; 15 | 3154AEF11EA906090058C26C /* MessagesInputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3154AEF01EA906090058C26C /* MessagesInputAccessoryView.swift */; }; 16 | 3159A2F81EAA2E3000300CC1 /* MessageSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3159A2F71EAA2E3000300CC1 /* MessageSectionController.swift */; }; 17 | 3159A2FA1EAA44B100300CC1 /* MessagesCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3159A2F91EAA44B100300CC1 /* MessagesCollectionViewLayout.swift */; }; 18 | 3159A2FF1EAA630C00300CC1 /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3159A2FE1EAA630C00300CC1 /* MessageViewModel.swift */; }; 19 | 3159A30B1EAA69F600300CC1 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3159A30A1EAA69F600300CC1 /* Message.swift */; }; 20 | 3159A30E1EAA93CC00300CC1 /* MessagesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3159A30D1EAA93CC00300CC1 /* MessagesService.swift */; }; 21 | 3159A3101EAA95C100300CC1 /* DemoMessagesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3159A30F1EAA95C100300CC1 /* DemoMessagesService.swift */; }; 22 | 31722AA31EB153C2000F23D9 /* UIViewController_Embed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31722AA21EB153C2000F23D9 /* UIViewController_Embed.swift */; }; 23 | 31722AA61EB17288000F23D9 /* Sequence_UniqueElements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31722AA51EB17288000F23D9 /* Sequence_UniqueElements.swift */; }; 24 | 31B270231EA8144D0036354B /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B270221EA8144D0036354B /* MessagesView.swift */; }; 25 | 31B270251EA81C1A0036354B /* MessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B270241EA81C1A0036354B /* MessageCell.swift */; }; 26 | 31B270271EA8325E0036354B /* quotes.json in Resources */ = {isa = PBXBuildFile; fileRef = 31B270261EA8325E0036354B /* quotes.json */; }; 27 | 31F689F11EA7FEC500D1D175 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F689F01EA7FEC500D1D175 /* AppDelegate.swift */; }; 28 | 31F689F31EA7FEC500D1D175 /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F689F21EA7FEC500D1D175 /* MessagesViewController.swift */; }; 29 | 31F689F81EA7FEC500D1D175 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 31F689F71EA7FEC500D1D175 /* Assets.xcassets */; }; 30 | 31F689FB1EA7FEC500D1D175 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 31F689F91EA7FEC500D1D175 /* LaunchScreen.storyboard */; }; 31 | AF8F869326DA28FB8E7C3432 /* Pods_MessagesApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF86503BC9CBAFE40191144C /* Pods_MessagesApp.framework */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 310395961EAEA65B0096390A /* UICollectionViewCell_ComputeHeight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICollectionViewCell_ComputeHeight.swift; sourceTree = ""; }; 36 | 3103959A1EAEB44B0096390A /* OutgoingMessageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutgoingMessageViewModel.swift; sourceTree = ""; }; 37 | 3103959C1EAEB5820096390A /* OutgoingMessageSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutgoingMessageSectionController.swift; sourceTree = ""; }; 38 | 3128630B1EAE180800C6B202 /* UIScrollView_ReversedContentOffset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIScrollView_ReversedContentOffset.swift; sourceTree = ""; }; 39 | 3154AEEE1EA8FBD00058C26C /* MessagesCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewController.swift; sourceTree = ""; }; 40 | 3154AEF01EA906090058C26C /* MessagesInputAccessoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesInputAccessoryView.swift; sourceTree = ""; }; 41 | 3159A2F71EAA2E3000300CC1 /* MessageSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageSectionController.swift; sourceTree = ""; }; 42 | 3159A2F91EAA44B100300CC1 /* MessagesCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewLayout.swift; sourceTree = ""; }; 43 | 3159A2FE1EAA630C00300CC1 /* MessageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = ""; }; 44 | 3159A30A1EAA69F600300CC1 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; 45 | 3159A30D1EAA93CC00300CC1 /* MessagesService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesService.swift; sourceTree = ""; }; 46 | 3159A30F1EAA95C100300CC1 /* DemoMessagesService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoMessagesService.swift; sourceTree = ""; }; 47 | 31722AA21EB153C2000F23D9 /* UIViewController_Embed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController_Embed.swift; sourceTree = ""; }; 48 | 31722AA51EB17288000F23D9 /* Sequence_UniqueElements.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sequence_UniqueElements.swift; sourceTree = ""; }; 49 | 31B270221EA8144D0036354B /* MessagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = ""; }; 50 | 31B270241EA81C1A0036354B /* MessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCell.swift; sourceTree = ""; }; 51 | 31B270261EA8325E0036354B /* quotes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = quotes.json; sourceTree = ""; }; 52 | 31F689ED1EA7FEC500D1D175 /* MessagesApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MessagesApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 31F689F01EA7FEC500D1D175 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 54 | 31F689F21EA7FEC500D1D175 /* MessagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; 55 | 31F689F71EA7FEC500D1D175 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 56 | 31F689FA1EA7FEC500D1D175 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 57 | 31F689FC1EA7FEC500D1D175 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58 | B54085713B0C6F8F2DE35E31 /* Pods-MessagesApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MessagesApp.release.xcconfig"; path = "Pods/Target Support Files/Pods-MessagesApp/Pods-MessagesApp.release.xcconfig"; sourceTree = ""; }; 59 | BF86503BC9CBAFE40191144C /* Pods_MessagesApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MessagesApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | E637843FCFF248A9E711D9FC /* Pods-MessagesApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MessagesApp.debug.xcconfig"; path = "Pods/Target Support Files/Pods-MessagesApp/Pods-MessagesApp.debug.xcconfig"; sourceTree = ""; }; 61 | /* End PBXFileReference section */ 62 | 63 | /* Begin PBXFrameworksBuildPhase section */ 64 | 31F689EA1EA7FEC500D1D175 /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | AF8F869326DA28FB8E7C3432 /* Pods_MessagesApp.framework in Frameworks */, 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | /* End PBXFrameworksBuildPhase section */ 73 | 74 | /* Begin PBXGroup section */ 75 | 3128630A1EAE17F300C6B202 /* Helpers */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 310395961EAEA65B0096390A /* UICollectionViewCell_ComputeHeight.swift */, 79 | 3128630B1EAE180800C6B202 /* UIScrollView_ReversedContentOffset.swift */, 80 | 31722AA21EB153C2000F23D9 /* UIViewController_Embed.swift */, 81 | ); 82 | path = Helpers; 83 | sourceTree = ""; 84 | }; 85 | 3159A3001EAA65F500300CC1 /* Views */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 31B270241EA81C1A0036354B /* MessageCell.swift */, 89 | 3154AEF01EA906090058C26C /* MessagesInputAccessoryView.swift */, 90 | 31B270221EA8144D0036354B /* MessagesView.swift */, 91 | ); 92 | path = Views; 93 | sourceTree = ""; 94 | }; 95 | 3159A3011EAA661700300CC1 /* Storyboards */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 31F689F91EA7FEC500D1D175 /* LaunchScreen.storyboard */, 99 | ); 100 | path = Storyboards; 101 | sourceTree = ""; 102 | }; 103 | 3159A3021EAA662300300CC1 /* ViewModels */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | 3159A2FE1EAA630C00300CC1 /* MessageViewModel.swift */, 107 | 3103959A1EAEB44B0096390A /* OutgoingMessageViewModel.swift */, 108 | ); 109 | path = ViewModels; 110 | sourceTree = ""; 111 | }; 112 | 3159A3031EAA663100300CC1 /* Controllers */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | 3159A2F71EAA2E3000300CC1 /* MessageSectionController.swift */, 116 | 3154AEEE1EA8FBD00058C26C /* MessagesCollectionViewController.swift */, 117 | 31F689F21EA7FEC500D1D175 /* MessagesViewController.swift */, 118 | 3103959C1EAEB5820096390A /* OutgoingMessageSectionController.swift */, 119 | ); 120 | path = Controllers; 121 | sourceTree = ""; 122 | }; 123 | 3159A3041EAA664000300CC1 /* Layout */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 3159A2F91EAA44B100300CC1 /* MessagesCollectionViewLayout.swift */, 127 | ); 128 | path = Layout; 129 | sourceTree = ""; 130 | }; 131 | 3159A3091EAA69EE00300CC1 /* Models */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 3159A30A1EAA69F600300CC1 /* Message.swift */, 135 | ); 136 | path = Models; 137 | sourceTree = ""; 138 | }; 139 | 3159A30C1EAA93BF00300CC1 /* Services */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 3159A30F1EAA95C100300CC1 /* DemoMessagesService.swift */, 143 | 3159A30D1EAA93CC00300CC1 /* MessagesService.swift */, 144 | ); 145 | path = Services; 146 | sourceTree = ""; 147 | }; 148 | 31722AA41EB17271000F23D9 /* Helpers */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 31722AA51EB17288000F23D9 /* Sequence_UniqueElements.swift */, 152 | ); 153 | path = Helpers; 154 | sourceTree = ""; 155 | }; 156 | 31B2701D1EA800670036354B /* Supporting Files */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 31F689FC1EA7FEC500D1D175 /* Info.plist */, 160 | ); 161 | path = "Supporting Files"; 162 | sourceTree = ""; 163 | }; 164 | 31B2701E1EA800750036354B /* UI */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | 3159A3031EAA663100300CC1 /* Controllers */, 168 | 3128630A1EAE17F300C6B202 /* Helpers */, 169 | 3159A3041EAA664000300CC1 /* Layout */, 170 | 3159A3011EAA661700300CC1 /* Storyboards */, 171 | 3159A3021EAA662300300CC1 /* ViewModels */, 172 | 3159A3001EAA65F500300CC1 /* Views */, 173 | ); 174 | path = UI; 175 | sourceTree = ""; 176 | }; 177 | 31B2701F1EA800790036354B /* Resources */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 31F689F71EA7FEC500D1D175 /* Assets.xcassets */, 181 | 31B270261EA8325E0036354B /* quotes.json */, 182 | ); 183 | path = Resources; 184 | sourceTree = ""; 185 | }; 186 | 31B270201EA8007F0036354B /* Application */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | 31F689F01EA7FEC500D1D175 /* AppDelegate.swift */, 190 | ); 191 | path = Application; 192 | sourceTree = ""; 193 | }; 194 | 31F689E41EA7FEC500D1D175 = { 195 | isa = PBXGroup; 196 | children = ( 197 | DB13DAAE3E04DCC1C49001EF /* Frameworks */, 198 | 31F689EF1EA7FEC500D1D175 /* MessagesApp */, 199 | B8F1536B231500FD3EE10207 /* Pods */, 200 | 31F689EE1EA7FEC500D1D175 /* Products */, 201 | ); 202 | sourceTree = ""; 203 | }; 204 | 31F689EE1EA7FEC500D1D175 /* Products */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | 31F689ED1EA7FEC500D1D175 /* MessagesApp.app */, 208 | ); 209 | name = Products; 210 | sourceTree = ""; 211 | }; 212 | 31F689EF1EA7FEC500D1D175 /* MessagesApp */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 31B270201EA8007F0036354B /* Application */, 216 | 31722AA41EB17271000F23D9 /* Helpers */, 217 | 3159A3091EAA69EE00300CC1 /* Models */, 218 | 31B2701F1EA800790036354B /* Resources */, 219 | 3159A30C1EAA93BF00300CC1 /* Services */, 220 | 31B2701D1EA800670036354B /* Supporting Files */, 221 | 31B2701E1EA800750036354B /* UI */, 222 | ); 223 | path = MessagesApp; 224 | sourceTree = ""; 225 | }; 226 | B8F1536B231500FD3EE10207 /* Pods */ = { 227 | isa = PBXGroup; 228 | children = ( 229 | E637843FCFF248A9E711D9FC /* Pods-MessagesApp.debug.xcconfig */, 230 | B54085713B0C6F8F2DE35E31 /* Pods-MessagesApp.release.xcconfig */, 231 | ); 232 | name = Pods; 233 | sourceTree = ""; 234 | }; 235 | DB13DAAE3E04DCC1C49001EF /* Frameworks */ = { 236 | isa = PBXGroup; 237 | children = ( 238 | BF86503BC9CBAFE40191144C /* Pods_MessagesApp.framework */, 239 | ); 240 | name = Frameworks; 241 | sourceTree = ""; 242 | }; 243 | /* End PBXGroup section */ 244 | 245 | /* Begin PBXNativeTarget section */ 246 | 31F689EC1EA7FEC500D1D175 /* MessagesApp */ = { 247 | isa = PBXNativeTarget; 248 | buildConfigurationList = 31F689FF1EA7FEC500D1D175 /* Build configuration list for PBXNativeTarget "MessagesApp" */; 249 | buildPhases = ( 250 | 40E78A4F012156E5A64C2424 /* [CP] Check Pods Manifest.lock */, 251 | 31B270211EA802E80036354B /* SwiftLint */, 252 | 31F689E91EA7FEC500D1D175 /* Sources */, 253 | 31F689EA1EA7FEC500D1D175 /* Frameworks */, 254 | 31F689EB1EA7FEC500D1D175 /* Resources */, 255 | 81F56CD2101D2E4119A6190A /* [CP] Embed Pods Frameworks */, 256 | ); 257 | buildRules = ( 258 | ); 259 | dependencies = ( 260 | ); 261 | name = MessagesApp; 262 | productName = Messages; 263 | productReference = 31F689ED1EA7FEC500D1D175 /* MessagesApp.app */; 264 | productType = "com.apple.product-type.application"; 265 | }; 266 | /* End PBXNativeTarget section */ 267 | 268 | /* Begin PBXProject section */ 269 | 31F689E51EA7FEC500D1D175 /* Project object */ = { 270 | isa = PBXProject; 271 | attributes = { 272 | LastSwiftUpdateCheck = 0830; 273 | LastUpgradeCheck = 0940; 274 | ORGANIZATIONNAME = Darrarski; 275 | TargetAttributes = { 276 | 31F689EC1EA7FEC500D1D175 = { 277 | CreatedOnToolsVersion = 8.3.2; 278 | ProvisioningStyle = Manual; 279 | }; 280 | }; 281 | }; 282 | buildConfigurationList = 31F689E81EA7FEC500D1D175 /* Build configuration list for PBXProject "Messages" */; 283 | compatibilityVersion = "Xcode 3.2"; 284 | developmentRegion = English; 285 | hasScannedForEncodings = 0; 286 | knownRegions = ( 287 | en, 288 | Base, 289 | ); 290 | mainGroup = 31F689E41EA7FEC500D1D175; 291 | productRefGroup = 31F689EE1EA7FEC500D1D175 /* Products */; 292 | projectDirPath = ""; 293 | projectRoot = ""; 294 | targets = ( 295 | 31F689EC1EA7FEC500D1D175 /* MessagesApp */, 296 | ); 297 | }; 298 | /* End PBXProject section */ 299 | 300 | /* Begin PBXResourcesBuildPhase section */ 301 | 31F689EB1EA7FEC500D1D175 /* Resources */ = { 302 | isa = PBXResourcesBuildPhase; 303 | buildActionMask = 2147483647; 304 | files = ( 305 | 31F689FB1EA7FEC500D1D175 /* LaunchScreen.storyboard in Resources */, 306 | 31F689F81EA7FEC500D1D175 /* Assets.xcassets in Resources */, 307 | 31B270271EA8325E0036354B /* quotes.json in Resources */, 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | /* End PBXResourcesBuildPhase section */ 312 | 313 | /* Begin PBXShellScriptBuildPhase section */ 314 | 31B270211EA802E80036354B /* SwiftLint */ = { 315 | isa = PBXShellScriptBuildPhase; 316 | buildActionMask = 2147483647; 317 | files = ( 318 | ); 319 | inputPaths = ( 320 | ); 321 | name = SwiftLint; 322 | outputPaths = ( 323 | ); 324 | runOnlyForDeploymentPostprocessing = 0; 325 | shellPath = /bin/sh; 326 | shellScript = "cd MessagesApp/; ${PODS_ROOT}/SwiftLint/swiftlint"; 327 | }; 328 | 40E78A4F012156E5A64C2424 /* [CP] Check Pods Manifest.lock */ = { 329 | isa = PBXShellScriptBuildPhase; 330 | buildActionMask = 2147483647; 331 | files = ( 332 | ); 333 | inputPaths = ( 334 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 335 | "${PODS_ROOT}/Manifest.lock", 336 | ); 337 | name = "[CP] Check Pods Manifest.lock"; 338 | outputPaths = ( 339 | "$(DERIVED_FILE_DIR)/Pods-MessagesApp-checkManifestLockResult.txt", 340 | ); 341 | runOnlyForDeploymentPostprocessing = 0; 342 | shellPath = /bin/sh; 343 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 344 | showEnvVarsInLog = 0; 345 | }; 346 | 81F56CD2101D2E4119A6190A /* [CP] Embed Pods Frameworks */ = { 347 | isa = PBXShellScriptBuildPhase; 348 | buildActionMask = 2147483647; 349 | files = ( 350 | ); 351 | inputPaths = ( 352 | "${SRCROOT}/Pods/Target Support Files/Pods-MessagesApp/Pods-MessagesApp-frameworks.sh", 353 | "${BUILT_PRODUCTS_DIR}/IGListKit/IGListKit.framework", 354 | "${PODS_ROOT}/Reveal-SDK/RevealServer-17/iOS/RevealServer.framework", 355 | "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", 356 | ); 357 | name = "[CP] Embed Pods Frameworks"; 358 | outputPaths = ( 359 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListKit.framework", 360 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RevealServer.framework", 361 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", 362 | ); 363 | runOnlyForDeploymentPostprocessing = 0; 364 | shellPath = /bin/sh; 365 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-MessagesApp/Pods-MessagesApp-frameworks.sh\"\n"; 366 | showEnvVarsInLog = 0; 367 | }; 368 | /* End PBXShellScriptBuildPhase section */ 369 | 370 | /* Begin PBXSourcesBuildPhase section */ 371 | 31F689E91EA7FEC500D1D175 /* Sources */ = { 372 | isa = PBXSourcesBuildPhase; 373 | buildActionMask = 2147483647; 374 | files = ( 375 | 31722AA31EB153C2000F23D9 /* UIViewController_Embed.swift in Sources */, 376 | 31F689F31EA7FEC500D1D175 /* MessagesViewController.swift in Sources */, 377 | 3159A2F81EAA2E3000300CC1 /* MessageSectionController.swift in Sources */, 378 | 3159A2FF1EAA630C00300CC1 /* MessageViewModel.swift in Sources */, 379 | 3159A2FA1EAA44B100300CC1 /* MessagesCollectionViewLayout.swift in Sources */, 380 | 31B270251EA81C1A0036354B /* MessageCell.swift in Sources */, 381 | 3103959B1EAEB44B0096390A /* OutgoingMessageViewModel.swift in Sources */, 382 | 3128630C1EAE180800C6B202 /* UIScrollView_ReversedContentOffset.swift in Sources */, 383 | 31722AA61EB17288000F23D9 /* Sequence_UniqueElements.swift in Sources */, 384 | 3154AEF11EA906090058C26C /* MessagesInputAccessoryView.swift in Sources */, 385 | 3159A30E1EAA93CC00300CC1 /* MessagesService.swift in Sources */, 386 | 3154AEEF1EA8FBD00058C26C /* MessagesCollectionViewController.swift in Sources */, 387 | 3159A3101EAA95C100300CC1 /* DemoMessagesService.swift in Sources */, 388 | 3103959D1EAEB5820096390A /* OutgoingMessageSectionController.swift in Sources */, 389 | 3159A30B1EAA69F600300CC1 /* Message.swift in Sources */, 390 | 310395971EAEA65B0096390A /* UICollectionViewCell_ComputeHeight.swift in Sources */, 391 | 31F689F11EA7FEC500D1D175 /* AppDelegate.swift in Sources */, 392 | 31B270231EA8144D0036354B /* MessagesView.swift in Sources */, 393 | ); 394 | runOnlyForDeploymentPostprocessing = 0; 395 | }; 396 | /* End PBXSourcesBuildPhase section */ 397 | 398 | /* Begin PBXVariantGroup section */ 399 | 31F689F91EA7FEC500D1D175 /* LaunchScreen.storyboard */ = { 400 | isa = PBXVariantGroup; 401 | children = ( 402 | 31F689FA1EA7FEC500D1D175 /* Base */, 403 | ); 404 | name = LaunchScreen.storyboard; 405 | path = .; 406 | sourceTree = ""; 407 | }; 408 | /* End PBXVariantGroup section */ 409 | 410 | /* Begin XCBuildConfiguration section */ 411 | 31F689FD1EA7FEC500D1D175 /* Debug */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | ALWAYS_SEARCH_USER_PATHS = NO; 415 | CLANG_ANALYZER_NONNULL = YES; 416 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 417 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 418 | CLANG_CXX_LIBRARY = "libc++"; 419 | CLANG_ENABLE_MODULES = YES; 420 | CLANG_ENABLE_OBJC_ARC = YES; 421 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 422 | CLANG_WARN_BOOL_CONVERSION = YES; 423 | CLANG_WARN_COMMA = YES; 424 | CLANG_WARN_CONSTANT_CONVERSION = YES; 425 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 426 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 427 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 428 | CLANG_WARN_EMPTY_BODY = YES; 429 | CLANG_WARN_ENUM_CONVERSION = YES; 430 | CLANG_WARN_INFINITE_RECURSION = YES; 431 | CLANG_WARN_INT_CONVERSION = YES; 432 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 433 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 434 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 435 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 436 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 437 | CLANG_WARN_STRICT_PROTOTYPES = YES; 438 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 439 | CLANG_WARN_UNREACHABLE_CODE = YES; 440 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 441 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 442 | COPY_PHASE_STRIP = NO; 443 | DEBUG_INFORMATION_FORMAT = dwarf; 444 | ENABLE_STRICT_OBJC_MSGSEND = YES; 445 | ENABLE_TESTABILITY = YES; 446 | GCC_C_LANGUAGE_STANDARD = gnu99; 447 | GCC_DYNAMIC_NO_PIC = NO; 448 | GCC_NO_COMMON_BLOCKS = YES; 449 | GCC_OPTIMIZATION_LEVEL = 0; 450 | GCC_PREPROCESSOR_DEFINITIONS = ( 451 | "DEBUG=1", 452 | "$(inherited)", 453 | ); 454 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 455 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 456 | GCC_WARN_UNDECLARED_SELECTOR = YES; 457 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 458 | GCC_WARN_UNUSED_FUNCTION = YES; 459 | GCC_WARN_UNUSED_VARIABLE = YES; 460 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 461 | MTL_ENABLE_DEBUG_INFO = YES; 462 | ONLY_ACTIVE_ARCH = YES; 463 | SDKROOT = iphoneos; 464 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 465 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 466 | TARGETED_DEVICE_FAMILY = "1,2"; 467 | }; 468 | name = Debug; 469 | }; 470 | 31F689FE1EA7FEC500D1D175 /* Release */ = { 471 | isa = XCBuildConfiguration; 472 | buildSettings = { 473 | ALWAYS_SEARCH_USER_PATHS = NO; 474 | CLANG_ANALYZER_NONNULL = YES; 475 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 476 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 477 | CLANG_CXX_LIBRARY = "libc++"; 478 | CLANG_ENABLE_MODULES = YES; 479 | CLANG_ENABLE_OBJC_ARC = YES; 480 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 481 | CLANG_WARN_BOOL_CONVERSION = YES; 482 | CLANG_WARN_COMMA = YES; 483 | CLANG_WARN_CONSTANT_CONVERSION = YES; 484 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 485 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 486 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 487 | CLANG_WARN_EMPTY_BODY = YES; 488 | CLANG_WARN_ENUM_CONVERSION = YES; 489 | CLANG_WARN_INFINITE_RECURSION = YES; 490 | CLANG_WARN_INT_CONVERSION = YES; 491 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 492 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 493 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 494 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 495 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 496 | CLANG_WARN_STRICT_PROTOTYPES = YES; 497 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 498 | CLANG_WARN_UNREACHABLE_CODE = YES; 499 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 500 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 501 | COPY_PHASE_STRIP = NO; 502 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 503 | ENABLE_NS_ASSERTIONS = NO; 504 | ENABLE_STRICT_OBJC_MSGSEND = YES; 505 | GCC_C_LANGUAGE_STANDARD = gnu99; 506 | GCC_NO_COMMON_BLOCKS = YES; 507 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 508 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 509 | GCC_WARN_UNDECLARED_SELECTOR = YES; 510 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 511 | GCC_WARN_UNUSED_FUNCTION = YES; 512 | GCC_WARN_UNUSED_VARIABLE = YES; 513 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 514 | MTL_ENABLE_DEBUG_INFO = NO; 515 | SDKROOT = iphoneos; 516 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 517 | TARGETED_DEVICE_FAMILY = "1,2"; 518 | VALIDATE_PRODUCT = YES; 519 | }; 520 | name = Release; 521 | }; 522 | 31F68A001EA7FEC500D1D175 /* Debug */ = { 523 | isa = XCBuildConfiguration; 524 | baseConfigurationReference = E637843FCFF248A9E711D9FC /* Pods-MessagesApp.debug.xcconfig */; 525 | buildSettings = { 526 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 527 | DEVELOPMENT_TEAM = ""; 528 | INFOPLIST_FILE = "MessagesApp/Supporting Files/Info.plist"; 529 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 530 | PRODUCT_BUNDLE_IDENTIFIER = pl.darrarski.Messages; 531 | PRODUCT_NAME = "$(TARGET_NAME)"; 532 | PROVISIONING_PROFILE_SPECIFIER = ""; 533 | SWIFT_VERSION = 3.0; 534 | }; 535 | name = Debug; 536 | }; 537 | 31F68A011EA7FEC500D1D175 /* Release */ = { 538 | isa = XCBuildConfiguration; 539 | baseConfigurationReference = B54085713B0C6F8F2DE35E31 /* Pods-MessagesApp.release.xcconfig */; 540 | buildSettings = { 541 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 542 | DEVELOPMENT_TEAM = ""; 543 | INFOPLIST_FILE = "MessagesApp/Supporting Files/Info.plist"; 544 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 545 | PRODUCT_BUNDLE_IDENTIFIER = pl.darrarski.Messages; 546 | PRODUCT_NAME = "$(TARGET_NAME)"; 547 | PROVISIONING_PROFILE_SPECIFIER = ""; 548 | SWIFT_VERSION = 3.0; 549 | }; 550 | name = Release; 551 | }; 552 | /* End XCBuildConfiguration section */ 553 | 554 | /* Begin XCConfigurationList section */ 555 | 31F689E81EA7FEC500D1D175 /* Build configuration list for PBXProject "Messages" */ = { 556 | isa = XCConfigurationList; 557 | buildConfigurations = ( 558 | 31F689FD1EA7FEC500D1D175 /* Debug */, 559 | 31F689FE1EA7FEC500D1D175 /* Release */, 560 | ); 561 | defaultConfigurationIsVisible = 0; 562 | defaultConfigurationName = Release; 563 | }; 564 | 31F689FF1EA7FEC500D1D175 /* Build configuration list for PBXNativeTarget "MessagesApp" */ = { 565 | isa = XCConfigurationList; 566 | buildConfigurations = ( 567 | 31F68A001EA7FEC500D1D175 /* Debug */, 568 | 31F68A011EA7FEC500D1D175 /* Release */, 569 | ); 570 | defaultConfigurationIsVisible = 0; 571 | defaultConfigurationName = Release; 572 | }; 573 | /* End XCConfigurationList section */ 574 | }; 575 | rootObject = 31F689E51EA7FEC500D1D175 /* Project object */; 576 | } 577 | -------------------------------------------------------------------------------- /Messages.xcodeproj/xcshareddata/xcschemes/MessagesApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Messages.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Messages.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MessagesApp/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - empty_count 3 | - force_unwrapping 4 | - private_outlet 5 | - redundant_nil_coalesing 6 | 7 | line_length: 8 | warning: 120 9 | error: 200 10 | 11 | variable_name: 12 | excluded: 13 | - id 14 | - URL 15 | - to 16 | - rx 17 | - x 18 | - y 19 | - z 20 | -------------------------------------------------------------------------------- /MessagesApp/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func application(_ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 10 | let messagesService = DemoMessagesService() 11 | let messagesViewController = MessagesViewController(messagesService: messagesService) 12 | window = UIWindow(frame: UIScreen.main.bounds) 13 | window?.rootViewController = messagesViewController 14 | window?.makeKeyAndVisible() 15 | return true 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /MessagesApp/Helpers/Sequence_UniqueElements.swift: -------------------------------------------------------------------------------- 1 | extension Sequence { 2 | 3 | func uniqueElements(identifier: (Iterator.Element) -> T) -> [Iterator.Element] { 4 | return reduce([]) { uniqueElements, element in 5 | if uniqueElements.contains(where: { identifier($0) == identifier(element) }) { 6 | return uniqueElements 7 | } else { 8 | return uniqueElements + [element] 9 | } 10 | } 11 | 12 | } 13 | 14 | } 15 | 16 | extension Sequence where Iterator.Element: Equatable { 17 | 18 | var uniqueElements: [Iterator.Element] { 19 | return self.reduce([]) { uniqueElements, element in 20 | uniqueElements.contains(element) ? uniqueElements : uniqueElements + [element] 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /MessagesApp/Models/Message.swift: -------------------------------------------------------------------------------- 1 | struct Message { 2 | let uid: String 3 | let text: String 4 | let author: String? 5 | } 6 | -------------------------------------------------------------------------------- /MessagesApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /MessagesApp/Resources/quotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | "Whatever the mind of man can conceive and believe, it can achieve.", 4 | "Napoleon Hill" 5 | ], 6 | [ 7 | "Life isn’t about getting and having, it’s about giving and being.", 8 | "Kevin Kruse" 9 | ], 10 | [ 11 | "Strive not to be a success, but rather to be of value.", 12 | "Albert Einstein" 13 | ], 14 | [ 15 | "Two roads diverged in a wood, and I—I took the one less traveled by, And that has made all the difference.", 16 | "Robert Frost" 17 | ], 18 | [ 19 | "I attribute my success to this: I never gave or took any excuse.", 20 | "Florence Nightingale" 21 | ], 22 | [ 23 | "You miss 100% of the shots you don’t take.", 24 | "Wayne Gretzky" 25 | ], 26 | [ 27 | "I’ve missed more than 9000 shots in my career. I’ve lost almost 300 games. 26 times I’ve been trusted to take the game winning shot and missed. I’ve failed over and over and over again in my life. And that is why I succeed.", 28 | "Michael Jordan" 29 | ], 30 | [ 31 | "The most difficult thing is the decision to act, the rest is merely tenacity.", 32 | "Amelia Earhart" 33 | ], 34 | [ 35 | "Every strike brings me closer to the next home run.", 36 | "Babe Ruth" 37 | ], 38 | [ 39 | "Definiteness of purpose is the starting point of all achievement.", 40 | "W. Clement Stone" 41 | ], 42 | [ 43 | "We must balance conspicuous consumption with conscious capitalism.", 44 | "Kevin Kruse" 45 | ], 46 | [ 47 | "Life is what happens to you while you’re busy making other plans.", 48 | "John Lennon" 49 | ], 50 | [ 51 | "We become what we think about.", 52 | "Earl Nightingale" 53 | ], 54 | [ 55 | "14.Twenty years from now you will be more disappointed by the things that you didn’t do than by the ones you did do, so throw off the bowlines, sail away from safe harbor, catch the trade winds in your sails. Explore, Dream, Discover.", 56 | "Mark Twain" 57 | ], 58 | [ 59 | "15.Life is 10% what happens to me and 90% of how I react to it.", 60 | "Charles Swindoll" 61 | ], 62 | [ 63 | "The most common way people give up their power is by thinking they don’t have any.", 64 | "Alice Walker" 65 | ], 66 | [ 67 | "The mind is everything. What you think you become.", 68 | "Buddha" 69 | ], 70 | [ 71 | "The best time to plant a tree was 20 years ago. The second best time is now.", 72 | "Chinese Proverb" 73 | ], 74 | [ 75 | "An unexamined life is not worth living.", 76 | "Socrates" 77 | ], 78 | [ 79 | "Eighty percent of success is showing up.", 80 | "Woody Allen" 81 | ], 82 | [ 83 | "Your time is limited, so don’t waste it living someone else’s life.", 84 | "Steve Jobs" 85 | ], 86 | [ 87 | "Winning isn’t everything, but wanting to win is.", 88 | "Vince Lombardi" 89 | ], 90 | [ 91 | "I am not a product of my circumstances. I am a product of my decisions.", 92 | "Stephen Covey" 93 | ], 94 | [ 95 | "Every child is an artist. The problem is how to remain an artist once he grows up.", 96 | "Pablo Picasso" 97 | ], 98 | [ 99 | "You can never cross the ocean until you have the courage to lose sight of the shore.", 100 | "Christopher Columbus" 101 | ], 102 | [ 103 | "I’ve learned that people will forget what you said, people will forget what you did, but people will never forget how you made them feel.", 104 | "Maya Angelou" 105 | ], 106 | [ 107 | "Either you run the day, or the day runs you.", 108 | "Jim Rohn" 109 | ], 110 | [ 111 | "Whether you think you can or you think you can’t, you’re right.", 112 | "Henry Ford" 113 | ], 114 | [ 115 | "The two most important days in your life are the day you are born and the day you find out why.", 116 | "Mark Twain" 117 | ], 118 | [ 119 | "Whatever you can do, or dream you can, begin it. Boldness has genius, power and magic in it.", 120 | "Johann Wolfgang von Goethe" 121 | ], 122 | [ 123 | "The best revenge is massive success.", 124 | "Frank Sinatra" 125 | ], 126 | [ 127 | "People often say that motivation doesn’t last. Well, neither does bathing. That’s why we recommend it daily.", 128 | "Zig Ziglar" 129 | ], 130 | [ 131 | "Life shrinks or expands in proportion to one’s courage.", 132 | "Anais Nin" 133 | ], 134 | [ 135 | "If you hear a voice within you say “you cannot paint,” then by all means paint and that voice will be silenced.", 136 | "Vincent Van Gogh" 137 | ], 138 | [ 139 | "There is only one way to avoid criticism: do nothing, say nothing, and be nothing.", 140 | "Aristotle" 141 | ], 142 | [ 143 | "Ask and it will be given to you; search, and you will find; knock and the door will be opened for you.", 144 | "Jesus" 145 | ], 146 | [ 147 | "The only person you are destined to become is the person you decide to be.", 148 | "Ralph Waldo Emerson" 149 | ], 150 | [ 151 | "Go confidently in the direction of your dreams. Live the life you have imagined.", 152 | "Henry David Thoreau" 153 | ], 154 | [ 155 | "When I stand before God at the end of my life, I would hope that I would not have a single bit of talent left and could say, I used everything you gave me.", 156 | "Erma Bombeck" 157 | ], 158 | [ 159 | "Few things can help an individual more than to place responsibility on him, and to let him know that you trust him.", 160 | "Booker T. Washington" 161 | ], 162 | [ 163 | "Certain things catch your eye, but pursue only those that capture the heart.", 164 | " Ancient Indian Proverb" 165 | ], 166 | [ 167 | "Believe you can and you’re halfway there.", 168 | "Theodore Roosevelt" 169 | ], 170 | [ 171 | "Everything you’ve ever wanted is on the other side of fear.", 172 | "George Addair" 173 | ], 174 | [ 175 | "We can easily forgive a child who is afraid of the dark; the real tragedy of life is when men are afraid of the light.", 176 | "Plato" 177 | ], 178 | [ 179 | "Teach thy tongue to say, “I do not know,” and thous shalt progress.", 180 | "Maimonides" 181 | ], 182 | [ 183 | "Start where you are. Use what you have. Do what you can.", 184 | "Arthur Ashe" 185 | ], 186 | [ 187 | "When I was 5 years old, my mother always told me that happiness was the key to life. When I went to school, they asked me what I wanted to be when I grew up. I wrote down ‘happy’. They told me I didn’t understand the assignment, and I told them they didn’t understand life.", 188 | "John Lennon" 189 | ], 190 | [ 191 | "Fall seven times and stand up eight.", 192 | "Japanese Proverb" 193 | ], 194 | [ 195 | "When one door of happiness closes, another opens, but often we look so long at the closed door that we do not see the one that has been opened for us.", 196 | "Helen Keller" 197 | ], 198 | [ 199 | "Everything has beauty, but not everyone can see.", 200 | "Confucius" 201 | ], 202 | [ 203 | "How wonderful it is that nobody need wait a single moment before starting to improve the world.", 204 | "Anne Frank" 205 | ], 206 | [ 207 | "When I let go of what I am, I become what I might be.", 208 | "Lao Tzu" 209 | ], 210 | [ 211 | "Life is not measured by the number of breaths we take, but by the moments that take our breath away.", 212 | "Maya Angelou" 213 | ], 214 | [ 215 | "Happiness is not something readymade. It comes from your own actions.", 216 | "Dalai Lama" 217 | ], 218 | [ 219 | "If you’re offered a seat on a rocket ship, don’t ask what seat! Just get on.", 220 | "Sheryl Sandberg" 221 | ], 222 | [ 223 | "First, have a definite, clear practical ideal; a goal, an objective. Second, have the necessary means to achieve your ends; wisdom, money, materials, and methods. Third, adjust all your means to that end.", 224 | "Aristotle" 225 | ], 226 | [ 227 | "If the wind will not serve, take to the oars.", 228 | "Latin Proverb" 229 | ], 230 | [ 231 | "You can’t fall if you don’t climb. But there’s no joy in living your whole life on the ground.", 232 | "Unknown" 233 | ], 234 | [ 235 | "We must believe that we are gifted for something, and that this thing, at whatever cost, must be attained.", 236 | "Marie Curie" 237 | ], 238 | [ 239 | "Too many of us are not living our dreams because we are living our fears.", 240 | "Les Brown" 241 | ], 242 | [ 243 | "Challenges are what make life interesting and overcoming them is what makes life meaningful.", 244 | "Joshua J. Marine" 245 | ], 246 | [ 247 | "If you want to lift yourself up, lift up someone else.", 248 | "Booker T. Washington" 249 | ], 250 | [ 251 | "I have been impressed with the urgency of doing. Knowing is not enough; we must apply. Being willing is not enough; we must do.", 252 | "Leonardo da Vinci" 253 | ], 254 | [ 255 | "Limitations live only in our minds. But if we use our imaginations, our possibilities become limitless.", 256 | "Jamie Paolinetti" 257 | ], 258 | [ 259 | "You take your life in your own hands, and what happens? A terrible thing, no one to blame.", 260 | "Erica Jong" 261 | ], 262 | [ 263 | "What’s money? A man is a success if he gets up in the morning and goes to bed at night and in between does what he wants to do.", 264 | "Bob Dylan" 265 | ], 266 | [ 267 | "I didn’t fail the test. I just found 100 ways to do it wrong.", 268 | "Benjamin Franklin" 269 | ], 270 | [ 271 | "In order to succeed, your desire for success should be greater than your fear of failure.", 272 | "Bill Cosby" 273 | ], 274 | [ 275 | "A person who never made a mistake never tried anything new.", 276 | " Albert Einstein" 277 | ], 278 | [ 279 | "The person who says it cannot be done should not interrupt the person who is doing it.", 280 | "Chinese Proverb" 281 | ], 282 | [ 283 | "There are no traffic jams along the extra mile.", 284 | "Roger Staubach" 285 | ], 286 | [ 287 | "It is never too late to be what you might have been.", 288 | "George Eliot" 289 | ], 290 | [ 291 | "You become what you believe.", 292 | "Oprah Winfrey" 293 | ], 294 | [ 295 | "I would rather die of passion than of boredom.", 296 | "Vincent van Gogh" 297 | ], 298 | [ 299 | "A truly rich man is one whose children run into his arms when his hands are empty.", 300 | "Unknown" 301 | ], 302 | [ 303 | "It is not what you do for your children, but what you have taught them to do for themselves, that will make them successful human beings.", 304 | "Ann Landers" 305 | ], 306 | [ 307 | "If you want your children to turn out well, spend twice as much time with them, and half as much money.", 308 | "Abigail Van Buren" 309 | ], 310 | [ 311 | "Build your own dreams, or someone else will hire you to build theirs.", 312 | "Farrah Gray" 313 | ], 314 | [ 315 | "The battles that count aren’t the ones for gold medals. The struggles within yourself–the invisible battles inside all of us–that’s where it’s at.", 316 | "Jesse Owens" 317 | ], 318 | [ 319 | "Education costs money. But then so does ignorance.", 320 | "Sir Claus Moser" 321 | ], 322 | [ 323 | "I have learned over the years that when one’s mind is made up, this diminishes fear.", 324 | "Rosa Parks" 325 | ], 326 | [ 327 | "It does not matter how slowly you go as long as you do not stop.", 328 | "Confucius" 329 | ], 330 | [ 331 | "If you look at what you have in life, you’ll always have more. If you look at what you don’t have in life, you’ll never have enough.", 332 | "Oprah Winfrey" 333 | ], 334 | [ 335 | "Remember that not getting what you want is sometimes a wonderful stroke of luck.", 336 | "Dalai Lama" 337 | ], 338 | [ 339 | "You can’t use up creativity. The more you use, the more you have.", 340 | "Maya Angelou" 341 | ], 342 | [ 343 | "Dream big and dare to fail.", 344 | "Norman Vaughan" 345 | ], 346 | [ 347 | "Our lives begin to end the day we become silent about things that matter.", 348 | "Martin Luther King Jr." 349 | ], 350 | [ 351 | "Do what you can, where you are, with what you have.", 352 | "Teddy Roosevelt" 353 | ], 354 | [ 355 | "If you do what you’ve always done, you’ll get what you’ve always gotten.", 356 | "Tony Robbins" 357 | ], 358 | [ 359 | "Dreaming, after all, is a form of planning.", 360 | "Gloria Steinem" 361 | ], 362 | [ 363 | "It’s your place in the world; it’s your life. Go on and do all you can with it, and make it the life you want to live.", 364 | "Mae Jemison" 365 | ], 366 | [ 367 | "You may be disappointed if you fail, but you are doomed if you don’t try.", 368 | "Beverly Sills" 369 | ], 370 | [ 371 | "Remember no one can make you feel inferior without your consent.", 372 | "Eleanor Roosevelt" 373 | ], 374 | [ 375 | "Life is what we make it, always has been, always will be.", 376 | "Grandma Moses" 377 | ], 378 | [ 379 | "The question isn’t who is going to let me; it’s who is going to stop me.", 380 | "Ayn Rand" 381 | ], 382 | [ 383 | "When everything seems to be going against you, remember that the airplane takes off against the wind, not with it.", 384 | "Henry Ford" 385 | ], 386 | [ 387 | "It’s not the years in your life that count. It’s the life in your years.", 388 | "Abraham Lincoln" 389 | ], 390 | [ 391 | "Change your thoughts and you change your world.", 392 | "Norman Vincent Peale" 393 | ], 394 | [ 395 | "Either write something worth reading or do something worth writing.", 396 | "Benjamin Franklin" 397 | ], 398 | [ 399 | "Nothing is impossible, the word itself says, “I’m possible!”", 400 | "–Audrey Hepburn" 401 | ], 402 | [ 403 | "The only way to do great work is to love what you do.", 404 | "Steve Jobs" 405 | ], 406 | [ 407 | "If you can dream it, you can achieve it.", 408 | "Zig Ziglar" 409 | ] 410 | ] -------------------------------------------------------------------------------- /MessagesApp/Services/DemoMessagesService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class DemoMessagesService { 4 | 5 | var fetchDelay: TimeInterval = 3 6 | var fetchError: Error? 7 | var sendDelay: TimeInterval = 2 8 | var sendError: Error? 9 | 10 | fileprivate var messages: [Message] = { 11 | let bundle = Bundle(for: DemoMessagesService.self) 12 | guard let path = bundle.path(forResource: "quotes", ofType: "json") else { fatalError() } 13 | guard let jsonData = try? Data(contentsOf: URL(fileURLWithPath: path)) else { fatalError() } 14 | let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []) 15 | guard let jsonArray = jsonObject as? [[String]] else { fatalError() } 16 | return jsonArray.enumerated().map { (index, quote) in 17 | let uid = UUID().uuidString 18 | let text = quote[0] 19 | let author: String? = index % 2 == 0 ? quote[1] : nil 20 | return Message(uid: uid, text: text, author: author) 21 | } 22 | }() 23 | 24 | } 25 | 26 | extension DemoMessagesService: MessagesService { 27 | 28 | func fetchMessages(page: Int, perPage: Int, completion: @escaping (MessagesServiceFetchResult) -> Void) { 29 | DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + fetchDelay) { [weak self] in 30 | guard let `self` = self else { return } 31 | if let error = self.fetchError { 32 | completion(.failure(error: error)) 33 | return 34 | } 35 | let startIndex = page * perPage 36 | let endIndex = min(startIndex + perPage, self.messages.endIndex) 37 | guard perPage > 0, startIndex >= self.messages.startIndex, endIndex > startIndex else { 38 | completion(.success(messages: [])) 39 | return 40 | } 41 | completion(.success(messages: Array(self.messages[startIndex.. Void) { 46 | DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + sendDelay) { [weak self] in 47 | guard let `self` = self else { return } 48 | if let error = self.sendError { 49 | completion(.failure(error: error)) 50 | return 51 | } 52 | let message = Message(uid: UUID().uuidString, text: text, author: nil) 53 | completion(.success(message: message)) 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /MessagesApp/Services/MessagesService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol MessagesService { 4 | func fetchMessages(page: Int, perPage: Int, completion: @escaping (MessagesServiceFetchResult) -> Void) 5 | func sendMessage(_ text: String, completion: @escaping (MessagesServiceSendResult) -> Void) 6 | } 7 | 8 | enum MessagesServiceFetchResult { 9 | case success(messages: [Message]) 10 | case failure(error: Error) 11 | } 12 | 13 | enum MessagesServiceSendResult { 14 | case success(message: Message) 15 | case failure(error: Error) 16 | } 17 | -------------------------------------------------------------------------------- /MessagesApp/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Messages 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 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 | -------------------------------------------------------------------------------- /MessagesApp/UI/Controllers/MessageSectionController.swift: -------------------------------------------------------------------------------- 1 | import IGListKit 2 | 3 | class MessageSectionController: IGListSectionController { 4 | 5 | override init() { 6 | super.init() 7 | inset = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10) 8 | } 9 | 10 | fileprivate(set) var viewModel: MessageViewModel? 11 | 12 | fileprivate let cellPrototype: MessageCell = { 13 | let cell = MessageCell(frame: .zero) 14 | cell.prepareForComputingHeight() 15 | return cell 16 | }() 17 | 18 | // MARK: Cell configuration 19 | 20 | fileprivate func configureCell(_ cell: MessageCell) { 21 | guard let viewModel = viewModel else { return } 22 | cell.label.text = viewModel.text 23 | if viewModel.isOutgoing { 24 | configureOutgoingCell(cell) 25 | } else { 26 | configureIncomingCell(cell) 27 | } 28 | } 29 | 30 | private func configureIncomingCell(_ cell: MessageCell) { 31 | cell.bubblePosition = .left 32 | cell.bubbleView.backgroundColor = UIColor(red: 0.9, green: 0.9, blue: 0.92, alpha: 1) 33 | cell.label.textColor = .black 34 | cell.label.textAlignment = .left 35 | } 36 | 37 | private func configureOutgoingCell(_ cell: MessageCell) { 38 | cell.bubblePosition = .right 39 | cell.bubbleView.backgroundColor = UIColor(red: 0.01, green: 0.48, blue: 0.98, alpha: 1) 40 | cell.label.textColor = .white 41 | cell.label.textAlignment = .left 42 | } 43 | 44 | } 45 | 46 | extension MessageSectionController: IGListSectionType { 47 | 48 | func numberOfItems() -> Int { 49 | return 1 50 | } 51 | 52 | func sizeForItem(at index: Int) -> CGSize { 53 | guard let context = collectionContext else { fatalError() } 54 | let width = context.containerSize.width - inset.left - inset.right 55 | cellPrototype.prepareForReuse() 56 | configureCell(cellPrototype) 57 | let height = cellPrototype.computeHeight(forWidth: width) 58 | return CGSize(width: width, height: height) 59 | } 60 | 61 | func cellForItem(at index: Int) -> UICollectionViewCell { 62 | guard let cell = collectionContext?.dequeueReusableCell( 63 | of: MessageCell.self, 64 | for: self, 65 | at: index) as? MessageCell else { fatalError() } 66 | configureCell(cell) 67 | return cell 68 | } 69 | 70 | func didUpdate(to object: Any) { 71 | guard let viewModel = object as? MessageViewModel else { fatalError() } 72 | self.viewModel = viewModel 73 | } 74 | 75 | func didSelectItem(at index: Int) {} 76 | 77 | } 78 | -------------------------------------------------------------------------------- /MessagesApp/UI/Controllers/MessagesCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import IGListKit 3 | 4 | class MessagesCollectionViewController: UICollectionViewController { 5 | 6 | init() { 7 | super.init(collectionViewLayout: UICollectionViewFlowLayout()) 8 | } 9 | 10 | required init?(coder aDecoder: NSCoder) { 11 | fatalError("init(coder:) has not been implemented") 12 | } 13 | 14 | // MARK: View 15 | 16 | var listCollectionView: IGListCollectionView! { 17 | return collectionView as? IGListCollectionView 18 | } 19 | 20 | let refreshControl = UIRefreshControl() 21 | 22 | override func loadView() { 23 | super.loadView() 24 | collectionView = Factory.listCollectionView 25 | collectionView?.refreshControl = refreshControl 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | setupList() 31 | } 32 | 33 | // MARK: ViewModels 34 | 35 | private(set) var messages = [MessageViewModel]() 36 | private(set) var outgoingMessages = [OutgoingMessageViewModel]() 37 | 38 | func updateMessages(_ newMessages: [MessageViewModel], completion: @escaping () -> Void = {}) { 39 | messages = newMessages.uniqueElements 40 | updateList(animated: false, completion: completion) 41 | } 42 | 43 | func appendMessages(_ newMessages: [MessageViewModel], completion: @escaping () -> Void = {}) { 44 | var messages = self.messages 45 | messages.append(contentsOf: newMessages) 46 | self.messages = messages.uniqueElements 47 | updateList(animated: false, completion: completion) 48 | } 49 | 50 | func insertMessage(_ message: MessageViewModel, completion: @escaping () -> Void = {}) { 51 | var messages = self.messages 52 | messages.insert(message, at: 0) 53 | self.messages = messages.uniqueElements 54 | updateList(animated: false, completion: completion) 55 | } 56 | 57 | func insertOutgoingMessage(_ message: OutgoingMessageViewModel) { 58 | var outgoingMessages = self.outgoingMessages 59 | outgoingMessages.insert(message, at: 0) 60 | self.outgoingMessages = outgoingMessages.uniqueElements 61 | updateList(animated: true) 62 | } 63 | 64 | func removeOutgoingMessage(_ message: OutgoingMessageViewModel) { 65 | guard let index = outgoingMessages.index(where: { $0 == message }) else { return } 66 | outgoingMessages.remove(at: index) 67 | updateList(animated: true) 68 | } 69 | 70 | // MARK: List 71 | 72 | private let listUpdater = IGListAdapterUpdater() 73 | 74 | private(set) lazy var listAdapter: IGListAdapter = { 75 | return IGListAdapter(updater: self.listUpdater, viewController: self, workingRangeSize: 0) 76 | }() 77 | 78 | private func setupList() { 79 | listAdapter.collectionView = listCollectionView 80 | listAdapter.dataSource = self 81 | } 82 | 83 | private func updateList(animated: Bool, completion: @escaping () -> Void = {}) { 84 | let reversedContentOffset = listCollectionView.reversedContentOffset 85 | listAdapter.performUpdates(animated: animated) { [weak self] _ in 86 | self?.listCollectionView.setReversedContentOffset(reversedContentOffset, animated: animated) 87 | completion() 88 | } 89 | } 90 | 91 | } 92 | 93 | extension MessagesCollectionViewController: IGListAdapterDataSource { 94 | 95 | func objects(for listAdapter: IGListAdapter) -> [IGListDiffable] { 96 | var objects = [IGListDiffable]() 97 | objects.append(contentsOf: outgoingMessages as [IGListDiffable]) 98 | objects.append(contentsOf: messages as [IGListDiffable]) 99 | return objects 100 | } 101 | 102 | func listAdapter(_ listAdapter: IGListAdapter, sectionControllerFor object: Any) -> IGListSectionController { 103 | switch object { 104 | case is MessageViewModel: 105 | return MessageSectionController() 106 | case is OutgoingMessageViewModel: 107 | return OutgoingMessageSectionController() 108 | default: 109 | fatalError() 110 | } 111 | } 112 | 113 | func emptyView(for listAdapter: IGListAdapter) -> UIView? { 114 | return nil 115 | } 116 | 117 | } 118 | 119 | extension MessagesCollectionViewController { 120 | 121 | struct Factory { 122 | 123 | static var collectionViewLayout: UICollectionViewFlowLayout { 124 | return MessagesCollectionViewLayout() 125 | } 126 | 127 | static var listCollectionView: IGListCollectionView { 128 | let layout = Factory.collectionViewLayout 129 | let view = IGListCollectionView(frame: .zero, collectionViewLayout: layout) 130 | view.backgroundColor = .white 131 | view.keyboardDismissMode = .interactive 132 | view.contentInset = UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 0) 133 | return view 134 | } 135 | 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /MessagesApp/UI/Controllers/MessagesViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class MessagesViewController: UIViewController { 4 | 5 | init(messagesService: MessagesService) { 6 | self.messagesService = messagesService 7 | super.init(nibName: nil, bundle: nil) 8 | } 9 | 10 | required init?(coder aDecoder: NSCoder) { 11 | fatalError("init(coder:) has not been implemented") 12 | } 13 | 14 | // MARK: View 15 | 16 | var messagesView: MessagesView! { 17 | return view as? MessagesView 18 | } 19 | 20 | override func loadView() { 21 | view = MessagesView() 22 | } 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | embed(childViewController: collectionViewController, inView: messagesView.collectionViewContainer) 27 | setupActions() 28 | loadMoreMessages() 29 | } 30 | 31 | // MARK: Child view controllers 32 | 33 | let collectionViewController = MessagesCollectionViewController() 34 | 35 | // MARK: UI Actions 36 | 37 | private func setupActions() { 38 | messagesInputAccessoryView.sendButton.addTarget(self, 39 | action: #selector(sendButtonAction), 40 | for: .touchUpInside) 41 | collectionViewController.refreshControl.addTarget(self, 42 | action: #selector(refreshControlAction), 43 | for: .valueChanged) 44 | } 45 | 46 | func sendButtonAction() { 47 | guard let text = messagesInputAccessoryView.textView.text, !text.isEmpty else { return } 48 | sendMessage(text) 49 | messagesInputAccessoryView.textView.text = nil 50 | } 51 | 52 | func refreshControlAction() { 53 | loadMoreMessages() 54 | } 55 | 56 | // MARK: Messages 57 | 58 | private let messagesPerPage = 10 59 | 60 | private var nextMessagesPage: Int { 61 | return Int(floor(Double(collectionViewController.messages.count / messagesPerPage))) 62 | } 63 | 64 | private func loadMoreMessages() { 65 | collectionViewController.refreshControl.beginRefreshing() 66 | messagesService.fetchMessages(page: nextMessagesPage, perPage: messagesPerPage) { [weak self] result in 67 | DispatchQueue.main.async { 68 | switch result { 69 | case .success(let messages): 70 | self?.collectionViewController.appendMessages(messages.map { MessageViewModel(message: $0) }) { 71 | self?.collectionViewController.refreshControl.endRefreshing() 72 | } 73 | 74 | case .failure(let error): 75 | self?.presentError(error) 76 | } 77 | } 78 | } 79 | } 80 | 81 | private func sendMessage(_ text: String) { 82 | let outgoingMessageViewModel = OutgoingMessageViewModel(text: text) 83 | collectionViewController.insertOutgoingMessage(outgoingMessageViewModel) 84 | messagesService.sendMessage(text) { [weak self] result in 85 | DispatchQueue.main.async { 86 | self?.collectionViewController.removeOutgoingMessage(outgoingMessageViewModel) 87 | switch result { 88 | case .success(let message): 89 | let messageViewModel = MessageViewModel(message: message) 90 | self?.collectionViewController.insertMessage(messageViewModel) 91 | 92 | case .failure(let error): 93 | self?.presentError(error) 94 | } 95 | } 96 | } 97 | } 98 | 99 | // MARK: Input Accessory View 100 | 101 | override var canBecomeFirstResponder: Bool { 102 | return true 103 | } 104 | 105 | override var inputAccessoryView: UIView? { 106 | return messagesInputAccessoryView 107 | } 108 | 109 | private let messagesInputAccessoryView = MessagesInputAccessoryView() 110 | 111 | // MARK: Private 112 | 113 | private let messagesService: MessagesService 114 | 115 | private func presentError(_ error: Error) { 116 | let alertController = UIAlertController(title: "Error occured", 117 | message: error.localizedDescription, 118 | preferredStyle: .alert) 119 | alertController.addAction(UIAlertAction(title: "OK", 120 | style: .default, 121 | handler: nil)) 122 | present(alertController, animated: true) 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /MessagesApp/UI/Controllers/OutgoingMessageSectionController.swift: -------------------------------------------------------------------------------- 1 | import IGListKit 2 | 3 | class OutgoingMessageSectionController: IGListSectionController { 4 | 5 | override init() { 6 | super.init() 7 | inset = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10) 8 | } 9 | 10 | fileprivate(set) var viewModel: OutgoingMessageViewModel? 11 | 12 | fileprivate let cellPrototype: MessageCell = { 13 | let cell = MessageCell(frame: .zero) 14 | cell.prepareForComputingHeight() 15 | return cell 16 | }() 17 | 18 | // MARK: Cell configuration 19 | 20 | fileprivate func configureCell(_ cell: MessageCell) { 21 | cell.label.text = viewModel?.text 22 | cell.bubblePosition = .right 23 | cell.bubbleView.backgroundColor = UIColor(red: 0.01, green: 0.48, blue: 0.98, alpha: 0.6) 24 | cell.label.textColor = .white 25 | cell.label.textAlignment = .left 26 | cell.activityIndicator.startAnimating() 27 | } 28 | 29 | } 30 | 31 | extension OutgoingMessageSectionController: IGListSectionType { 32 | 33 | func numberOfItems() -> Int { 34 | return 1 35 | } 36 | 37 | func sizeForItem(at index: Int) -> CGSize { 38 | guard let context = collectionContext else { fatalError() } 39 | let width = context.containerSize.width - inset.left - inset.right 40 | cellPrototype.prepareForReuse() 41 | configureCell(cellPrototype) 42 | let height = cellPrototype.computeHeight(forWidth: width) 43 | return CGSize(width: width, height: height) 44 | } 45 | 46 | func cellForItem(at index: Int) -> UICollectionViewCell { 47 | guard let cell = collectionContext?.dequeueReusableCell( 48 | of: MessageCell.self, 49 | for: self, 50 | at: index) as? MessageCell else { fatalError() } 51 | configureCell(cell) 52 | return cell 53 | } 54 | 55 | func didUpdate(to object: Any) { 56 | guard let viewModel = object as? OutgoingMessageViewModel else { fatalError() } 57 | self.viewModel = viewModel 58 | } 59 | 60 | func didSelectItem(at index: Int) {} 61 | 62 | } 63 | -------------------------------------------------------------------------------- /MessagesApp/UI/Helpers/UICollectionViewCell_ComputeHeight.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UICollectionViewCell { 4 | 5 | func prepareForComputingHeight() { 6 | translatesAutoresizingMaskIntoConstraints = false 7 | contentView.translatesAutoresizingMaskIntoConstraints = false 8 | setNeedsLayout() 9 | layoutIfNeeded() 10 | prepareForReuse() 11 | } 12 | 13 | func computeHeight(forWidth width: CGFloat) -> CGFloat { 14 | bounds = { 15 | var bounds = self.bounds 16 | bounds.size.width = width 17 | return bounds 18 | }() 19 | setNeedsLayout() 20 | layoutIfNeeded() 21 | let targetSize = CGSize(width: width, height: 0) 22 | let fittingSize = systemLayoutSizeFitting( 23 | targetSize, 24 | withHorizontalFittingPriority: UILayoutPriorityDefaultHigh, 25 | verticalFittingPriority: UILayoutPriorityFittingSizeLevel 26 | ) 27 | return fittingSize.height 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /MessagesApp/UI/Helpers/UIScrollView_ReversedContentOffset.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIScrollView { 4 | 5 | var reversedContentOffset: CGPoint { 6 | get { return reversedContentOffset(for: contentOffset) } 7 | set { contentOffset = reversedContentOffset(for: newValue) } 8 | } 9 | 10 | func setReversedContentOffset(_ reversedContentOffset: CGPoint, animated: Bool) { 11 | setContentOffset(self.reversedContentOffset(for: reversedContentOffset), animated: animated) 12 | } 13 | 14 | private func reversedContentOffset(for contentOffset: CGPoint) -> CGPoint { 15 | return CGPoint( 16 | x: contentSize.width - contentOffset.x - min(visibleSize.width, contentSize.width), 17 | y: contentSize.height - contentOffset.y - min(visibleSize.height, contentSize.height) 18 | ) 19 | } 20 | 21 | private var visibleSize: CGSize { 22 | return CGSize( 23 | width: frame.width - contentInset.left - contentInset.right, 24 | height: frame.height - contentInset.top - contentInset.bottom 25 | ) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /MessagesApp/UI/Helpers/UIViewController_Embed.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SnapKit 3 | 4 | extension UIViewController { 5 | 6 | func embed(childViewController: UIViewController, 7 | inView mainView: UIView, 8 | makeConstraints: ((ConstraintMaker) -> Void) = { $0.edges.equalToSuperview() }) { 9 | addChildViewController(childViewController) 10 | mainView.addSubview(childViewController.view) 11 | childViewController.view.snp.makeConstraints(makeConstraints) 12 | childViewController.didMove(toParentViewController: self) 13 | } 14 | 15 | func unembed(childViewController: UIViewController) { 16 | childViewController.willMove(toParentViewController: nil) 17 | childViewController.view.removeFromSuperview() 18 | childViewController.removeFromParentViewController() 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /MessagesApp/UI/Layout/MessagesCollectionViewLayout.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class MessagesCollectionViewLayout: UICollectionViewFlowLayout { 4 | 5 | override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 6 | guard let attributes = super.layoutAttributesForItem(at: indexPath) else { return nil } 7 | return modifiedAttributes(attributes) 8 | } 9 | 10 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 11 | guard let attributes = super.layoutAttributesForElements(in: reversedRect(for: rect)) else { return nil } 12 | return attributes.map { modifiedAttributes($0) } 13 | } 14 | 15 | // MARK: Private 16 | 17 | private func modifiedAttributes(_ attr: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { 18 | guard let copy = attr.copy() as? UICollectionViewLayoutAttributes else { fatalError() } 19 | copy.center = reversedPoint(for: attr.center) 20 | return copy 21 | } 22 | 23 | private func reversedPoint(for point: CGPoint) -> CGPoint { 24 | return CGPoint(x: point.x, y: reversedY(for: point.y)) 25 | } 26 | 27 | private func reversedY(for y: CGFloat) -> CGFloat { 28 | return collectionViewContentSize.height - y 29 | } 30 | 31 | private func reversedRect(for rect: CGRect) -> CGRect { 32 | let size = rect.size 33 | let normalTopLeft = rect.origin 34 | let reversedBottomLeft = reversedPoint(for: normalTopLeft) 35 | let reversedTopLeft = CGPoint(x: reversedBottomLeft.x, y: reversedBottomLeft.y - size.height) 36 | let reversedRect = CGRect(origin: reversedTopLeft, size: size) 37 | return reversedRect 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /MessagesApp/UI/Storyboards/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 | -------------------------------------------------------------------------------- /MessagesApp/UI/ViewModels/MessageViewModel.swift: -------------------------------------------------------------------------------- 1 | import IGListKit 2 | 3 | class MessageViewModel { 4 | 5 | init(message: Message) { 6 | self.message = message 7 | } 8 | 9 | let message: Message 10 | 11 | var text: String { 12 | return message.text 13 | } 14 | 15 | var isOutgoing: Bool { 16 | return message.author == nil 17 | } 18 | 19 | } 20 | 21 | extension MessageViewModel: Equatable { 22 | 23 | static func == (lhs: MessageViewModel, rhs: MessageViewModel) -> Bool { 24 | return lhs.message.uid == rhs.message.uid 25 | } 26 | 27 | } 28 | 29 | extension MessageViewModel: IGListDiffable { 30 | 31 | func diffIdentifier() -> NSObjectProtocol { 32 | return message.uid as NSObjectProtocol 33 | } 34 | 35 | func isEqual(toDiffableObject object: IGListDiffable?) -> Bool { 36 | guard let other = object as? MessageViewModel else { return false } 37 | return self == other 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /MessagesApp/UI/ViewModels/OutgoingMessageViewModel.swift: -------------------------------------------------------------------------------- 1 | import IGListKit 2 | 3 | class OutgoingMessageViewModel { 4 | 5 | init(text: String) { 6 | self.text = text 7 | } 8 | 9 | let uid: String = UUID().uuidString 10 | let text: String 11 | 12 | } 13 | 14 | extension OutgoingMessageViewModel: Equatable { 15 | 16 | static func == (lhs: OutgoingMessageViewModel, rhs: OutgoingMessageViewModel) -> Bool { 17 | return lhs.uid == rhs.uid 18 | } 19 | 20 | } 21 | 22 | extension OutgoingMessageViewModel: IGListDiffable { 23 | 24 | func diffIdentifier() -> NSObjectProtocol { 25 | return uid as NSObjectProtocol 26 | } 27 | 28 | func isEqual(toDiffableObject object: IGListDiffable?) -> Bool { 29 | guard let other = object as? OutgoingMessageViewModel else { return false } 30 | return self == other 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /MessagesApp/UI/Views/MessageCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SnapKit 3 | 4 | class MessageCell: UICollectionViewCell { 5 | 6 | override init(frame: CGRect) { 7 | super.init(frame: frame) 8 | loadSubviews() 9 | setupLayout() 10 | cleanUp() 11 | } 12 | 13 | required init?(coder aDecoder: NSCoder) { 14 | fatalError("init(coder:) has not been implemented") 15 | } 16 | 17 | override func prepareForReuse() { 18 | super.prepareForReuse() 19 | cleanUp() 20 | } 21 | 22 | private func cleanUp() { 23 | bubblePosition = nil 24 | bubbleView.backgroundColor = .clear 25 | label.textColor = .darkGray 26 | label.textAlignment = .center 27 | activityIndicator.stopAnimating() 28 | } 29 | 30 | // MARK: Subviews 31 | 32 | let bubbleView = Factory.bubbleView 33 | let label = Factory.label 34 | let activityIndicator = Factory.activityIndicator 35 | 36 | private func loadSubviews() { 37 | contentView.addSubview(bubbleView) 38 | bubbleView.addSubview(label) 39 | contentView.addSubview(activityIndicator) 40 | } 41 | 42 | // MARK: Layout 43 | 44 | enum BubblePosition { 45 | case left 46 | case right 47 | } 48 | 49 | var bubblePosition: BubblePosition? { 50 | didSet { 51 | switch bubblePosition { 52 | case .some(.left): 53 | bubbleViewRightConstraint.deactivate() 54 | bubbleViewLeftConstraint.activate() 55 | activityIndicatorRightConstraint.deactivate() 56 | activityIndicatorLeftConstraint.activate() 57 | case .some(.right): 58 | bubbleViewLeftConstraint.deactivate() 59 | bubbleViewRightConstraint.activate() 60 | activityIndicatorLeftConstraint.deactivate() 61 | activityIndicatorRightConstraint.activate() 62 | case .none: 63 | bubbleViewLeftConstraint.deactivate() 64 | bubbleViewRightConstraint.deactivate() 65 | activityIndicatorLeftConstraint.deactivate() 66 | activityIndicatorRightConstraint.deactivate() 67 | } 68 | } 69 | } 70 | 71 | private func setupLayout() { 72 | bubbleView.snp.makeConstraints { 73 | $0.top.bottom.equalToSuperview() 74 | $0.width.lessThanOrEqualToSuperview().multipliedBy(0.8) 75 | $0.width.greaterThanOrEqualTo(16) 76 | $0.centerX.equalToSuperview().priority(.low) 77 | } 78 | bubbleView.snp.makeConstraints { 79 | bubbleViewLeftConstraint = $0.left.equalToSuperview().constraint 80 | } 81 | bubbleViewLeftConstraint.deactivate() 82 | bubbleView.snp.makeConstraints { 83 | bubbleViewRightConstraint = $0.right.equalToSuperview().constraint 84 | } 85 | bubbleViewRightConstraint.deactivate() 86 | label.snp.makeConstraints { 87 | $0.edges.equalTo(UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)) 88 | } 89 | activityIndicator.snp.makeConstraints { 90 | $0.centerY.equalToSuperview() 91 | } 92 | activityIndicator.snp.makeConstraints { 93 | activityIndicatorLeftConstraint = $0.left.equalTo(bubbleView.snp.right).offset(8).constraint 94 | } 95 | activityIndicatorLeftConstraint.deactivate() 96 | activityIndicator.snp.makeConstraints { 97 | activityIndicatorRightConstraint = $0.right.equalTo(bubbleView.snp.left).offset(-8).constraint 98 | } 99 | activityIndicatorRightConstraint.deactivate() 100 | } 101 | 102 | private var bubbleViewLeftConstraint: Constraint! 103 | private var bubbleViewRightConstraint: Constraint! 104 | private var activityIndicatorRightConstraint: Constraint! 105 | private var activityIndicatorLeftConstraint: Constraint! 106 | 107 | } 108 | 109 | private extension MessageCell { 110 | 111 | struct Factory { 112 | 113 | static var bubbleView: UIView { 114 | let view = UIView(frame: .zero) 115 | view.backgroundColor = .lightGray 116 | view.layer.cornerRadius = 16 117 | return view 118 | } 119 | 120 | static var label: UILabel { 121 | let label = UILabel(frame: .zero) 122 | label.numberOfLines = 0 123 | return label 124 | } 125 | 126 | static var activityIndicator: UIActivityIndicatorView { 127 | let view = UIActivityIndicatorView(activityIndicatorStyle: .gray) 128 | view.hidesWhenStopped = true 129 | return view 130 | } 131 | 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /MessagesApp/UI/Views/MessagesInputAccessoryView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SnapKit 3 | 4 | class MessagesInputAccessoryView: UIToolbar { 5 | 6 | init() { 7 | super.init(frame: .zero) 8 | layoutIfNeeded() 9 | loadSubviews() 10 | setupLayout() 11 | textView.delegate = self 12 | } 13 | 14 | required init?(coder aDecoder: NSCoder) { 15 | fatalError("init(coder:) has not been implemented") 16 | } 17 | 18 | // MARK: Subviews 19 | 20 | let textView = Factory.textView 21 | let sendButton = Factory.sendButton 22 | 23 | private func loadSubviews() { 24 | addSubview(textView) 25 | addSubview(sendButton) 26 | } 27 | 28 | // MARK: Layout 29 | 30 | override func layoutSubviews() { 31 | super.layoutSubviews() 32 | updateTextViewHeight() 33 | } 34 | 35 | private let maxTextViewHeight: CGFloat = 100 36 | 37 | private func setupLayout() { 38 | translatesAutoresizingMaskIntoConstraints = false 39 | textView.snp.makeConstraints { 40 | $0.top.left.equalTo(8) 41 | $0.bottom.equalTo(-8) 42 | } 43 | sendButton.snp.makeConstraints { 44 | $0.left.equalTo(textView.snp.right).offset(8) 45 | $0.right.bottom.equalTo(-10) 46 | } 47 | textView.setContentHuggingPriority(UILayoutPriorityDefaultLow, for: .horizontal) 48 | sendButton.setContentHuggingPriority(UILayoutPriorityDefaultHigh, for: .horizontal) 49 | } 50 | 51 | fileprivate func updateTextViewHeight() { 52 | let computedTextViewHeight = self.computedTextViewHeight 53 | if computedTextViewHeight < maxTextViewHeight { 54 | textView.snp.updateConstraints { 55 | $0.height.equalTo(computedTextViewHeight) 56 | } 57 | textView.isScrollEnabled = false 58 | } else { 59 | textView.isScrollEnabled = true 60 | } 61 | } 62 | 63 | private var computedTextViewHeight: CGFloat { 64 | let textViewWidth = textView.frame.size.width 65 | let textViewSize = textView.sizeThatFits(CGSize(width: textViewWidth, height: CGFloat.greatestFiniteMagnitude)) 66 | return textViewSize.height 67 | } 68 | 69 | } 70 | 71 | extension MessagesInputAccessoryView: UITextViewDelegate { 72 | 73 | func textViewDidChange(_ textView: UITextView) { 74 | updateTextViewHeight() 75 | } 76 | 77 | } 78 | 79 | extension MessagesInputAccessoryView { 80 | 81 | struct Factory { 82 | 83 | static var textView: UITextView { 84 | let textView = UITextView(frame: .zero) 85 | textView.layer.borderWidth = 0.5 86 | textView.layer.borderColor = UIColor.lightGray.cgColor 87 | textView.layer.cornerRadius = 5 88 | textView.font = UIFont.systemFont(ofSize: 17, weight: UIFontWeightRegular) 89 | return textView 90 | } 91 | 92 | static var sendButton: UIButton { 93 | let button = UIButton(frame: .zero) 94 | button.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: UIFontWeightMedium) 95 | button.setTitleColor(UIColor(red: 0.02, green: 0.48, blue: 0.96, alpha: 1), for: .normal) 96 | button.setTitle("Send", for: .normal) 97 | return button 98 | } 99 | 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /MessagesApp/UI/Views/MessagesView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SnapKit 3 | 4 | class MessagesView: UIView { 5 | 6 | init() { 7 | super.init(frame: .zero) 8 | backgroundColor = .white 9 | loadSubviews() 10 | setupLayout() 11 | } 12 | 13 | required init?(coder aDecoder: NSCoder) { 14 | fatalError("init(coder:) has not been implemented") 15 | } 16 | 17 | // MARK: Subviews 18 | 19 | let collectionViewContainer = UIView(frame: .zero) 20 | 21 | private func loadSubviews() { 22 | addSubview(collectionViewContainer) 23 | } 24 | 25 | // MARK: Layout 26 | 27 | private func setupLayout() { 28 | collectionViewContainer.snp.makeConstraints { $0.edges.equalToSuperview() } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Misc/sending_test_message.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrarski/Messages-iOS/2f23419f5092a07439d2344709af40f57a3cf9b0/Misc/sending_test_message.gif -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'git://github.com/CocoaPods/Specs.git' 2 | platform :ios, '10.3' 3 | use_frameworks! 4 | inhibit_all_warnings! 5 | 6 | target 'MessagesApp' do 7 | pod 'SwiftLint', '~> 0.18' 8 | pod 'SnapKit', '~> 3.2' 9 | pod 'Reveal-SDK', :configurations => ['Debug'] 10 | pod 'IGListKit', '~> 2.1' 11 | end 12 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - IGListKit (2.1.0): 3 | - IGListKit/Default (= 2.1.0) 4 | - IGListKit/Default (2.1.0): 5 | - IGListKit/Diffing 6 | - IGListKit/Diffing (2.1.0) 7 | - Reveal-SDK (17) 8 | - SnapKit (3.2.0) 9 | - SwiftLint (0.27.0) 10 | 11 | DEPENDENCIES: 12 | - IGListKit (~> 2.1) 13 | - Reveal-SDK 14 | - SnapKit (~> 3.2) 15 | - SwiftLint (~> 0.18) 16 | 17 | SPEC REPOS: 18 | https://github.com/cocoapods/specs.git: 19 | - IGListKit 20 | - Reveal-SDK 21 | - SnapKit 22 | - SwiftLint 23 | 24 | SPEC CHECKSUMS: 25 | IGListKit: b826c68ef7a4ae1626c09d4d3e1ea7a169e6c36e 26 | Reveal-SDK: a6df49f47319bd19a110c960c498af32df72c0af 27 | SnapKit: 1ca44df72cfa543218d177cb8aab029d10d86ea7 28 | SwiftLint: 3207c1faa2240bf8973b191820a116113cd11073 29 | 30 | PODFILE CHECKSUM: d9c42dae0399aed4802d1f44084225d131e09daa 31 | 32 | COCOAPODS: 1.5.3 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Messages (iOS) 2 | 3 | ![Swift v3.1](https://img.shields.io/badge/swift-v3.1-orange.svg) 4 | 5 | Messages app prototype for iOS 6 | 7 | **Notice: current state of the project is "for R&D purposes only"** 8 | 9 | ![sending test message](Misc/sending_test_message.gif) 10 | 11 | ## Description 12 | 13 | Disappointed with availabe libraries and components for replicating Messages app UI in iOS application, I decided to create my own solution. It shows how to build chat-alike user interface that allows sending and receiving messages. It's not "one size fits all", not perfect nor optimal. It's definately much simplier than some messaging-UI libraries, and because of this, easier to maintain and customize. Messages list is build using `UICollectionView` managed by [IGListKit](https://github.com/Instagram/IGListKit) library. There is no backend logic in this project - app uses mocked up service that simulates communication with server side. 14 | 15 | Special thanks to [turekj](https://github.com/turekj) for help with developing simple solution to preserve `UIScrollView` content offset when reloading data. 16 | 17 | ## What's inside 18 | 19 | - `UICollectionView` managed by [IGListKit](https://github.com/Instagram/IGListKit) 20 | - `UICollectionViewLayout` that displays cells from bottom to top 21 | - Custom `UICollectionViewCell` for representing messages on the list 22 | - Workaround for preserving content offset when adding cells to `UICollectionView` 23 | - Pagination with `UIRefreshControl` that allows loading messages history 24 | - Custom toolbar for composing new messages 25 | - Mocked up service that simulates communication with backend 26 | 27 | ## License 28 | 29 | Copyright © 2017 Dariusz Rybicki Darrarski 30 | 31 | [MIT License](LICENSE). You are allowed to use the source code commercially, but licence and copyright notice MUST be distributed along with it. 32 | --------------------------------------------------------------------------------