├── .gitignore ├── LICENSE ├── MessageComposerView.podspec ├── MessageComposerView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── MessageComposerView.xcscheme ├── MessageComposerView ├── Info.plist ├── MessageComposerView.h └── MessageComposerView.m └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Created by https://www.gitignore.io/api/swift 4 | 5 | ### Swift ### 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## Build generated 11 | build/ 12 | DerivedData 13 | 14 | ## Various settings 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | 25 | ## Other 26 | *.xccheckout 27 | *.moved-aside 28 | *.xcuserstate 29 | *.xcscmblueprint 30 | 31 | ## Obj-C/Swift specific 32 | *.hmap 33 | *.ipa 34 | 35 | # CocoaPods 36 | # 37 | # We recommend against adding the Pods directory to your .gitignore. However 38 | # you should judge for yourself, the pros and cons are mentioned at: 39 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 40 | # 41 | # Pods/ 42 | 43 | # Carthage 44 | # 45 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 46 | # Carthage/Checkouts 47 | 48 | Carthage/Build 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Oskar Separovic 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MessageComposerView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | name = "MessageComposerView" 3 | url = "https://github.com/oseparovic/#{name}" 4 | git_url = "#{url}.git" 5 | version = "1.3.4" 6 | 7 | s.name = name 8 | s.version = version 9 | s.summary = "A library to provide an iMessage like input box that sticks to the keyboard." 10 | s.homepage = url 11 | s.screenshots = "http://www.thegameengine.org/wp-content/uploads/2013/11/message_composer_quad_1.jpg" 12 | s.license = 'MIT' 13 | s.author = { "Oskar Separovic" => "oseparovic@gmail.com" } 14 | s.source = { :git => git_url, :tag => version } 15 | s.social_media_url = 'https://twitter.com/alexgophermix' 16 | s.requires_arc = true 17 | s.source_files = "#{name}/*.{h,m}" 18 | s.resources = "#{name}/*.{xib}" 19 | s.platform = :ios, '7.0' 20 | end 21 | -------------------------------------------------------------------------------- /MessageComposerView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 461DB77F1BDDFB390070CBCA /* MessageComposerView.h in Headers */ = {isa = PBXBuildFile; fileRef = 461DB77E1BDDFB390070CBCA /* MessageComposerView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 461DB7871BDDFB6D0070CBCA /* MessageComposerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 461DB7861BDDFB6D0070CBCA /* MessageComposerView.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 461DB77B1BDDFB390070CBCA /* MessageComposerView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageComposerView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | 461DB77E1BDDFB390070CBCA /* MessageComposerView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MessageComposerView.h; sourceTree = ""; }; 17 | 461DB7801BDDFB390070CBCA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 18 | 461DB7861BDDFB6D0070CBCA /* MessageComposerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MessageComposerView.m; sourceTree = ""; }; 19 | /* End PBXFileReference section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | 461DB7771BDDFB390070CBCA /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | ); 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXFrameworksBuildPhase section */ 30 | 31 | /* Begin PBXGroup section */ 32 | 461DB7711BDDFB390070CBCA = { 33 | isa = PBXGroup; 34 | children = ( 35 | 461DB77D1BDDFB390070CBCA /* MessageComposerView */, 36 | 461DB77C1BDDFB390070CBCA /* Products */, 37 | ); 38 | sourceTree = ""; 39 | }; 40 | 461DB77C1BDDFB390070CBCA /* Products */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | 461DB77B1BDDFB390070CBCA /* MessageComposerView.framework */, 44 | ); 45 | name = Products; 46 | sourceTree = ""; 47 | }; 48 | 461DB77D1BDDFB390070CBCA /* MessageComposerView */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 461DB77E1BDDFB390070CBCA /* MessageComposerView.h */, 52 | 461DB7861BDDFB6D0070CBCA /* MessageComposerView.m */, 53 | 461DB7801BDDFB390070CBCA /* Info.plist */, 54 | ); 55 | path = MessageComposerView; 56 | sourceTree = ""; 57 | }; 58 | /* End PBXGroup section */ 59 | 60 | /* Begin PBXHeadersBuildPhase section */ 61 | 461DB7781BDDFB390070CBCA /* Headers */ = { 62 | isa = PBXHeadersBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | 461DB77F1BDDFB390070CBCA /* MessageComposerView.h in Headers */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXHeadersBuildPhase section */ 70 | 71 | /* Begin PBXNativeTarget section */ 72 | 461DB77A1BDDFB390070CBCA /* MessageComposerView */ = { 73 | isa = PBXNativeTarget; 74 | buildConfigurationList = 461DB7831BDDFB390070CBCA /* Build configuration list for PBXNativeTarget "MessageComposerView" */; 75 | buildPhases = ( 76 | 461DB7761BDDFB390070CBCA /* Sources */, 77 | 461DB7771BDDFB390070CBCA /* Frameworks */, 78 | 461DB7781BDDFB390070CBCA /* Headers */, 79 | 461DB7791BDDFB390070CBCA /* Resources */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | name = MessageComposerView; 86 | productName = MessageComposerView; 87 | productReference = 461DB77B1BDDFB390070CBCA /* MessageComposerView.framework */; 88 | productType = "com.apple.product-type.framework"; 89 | }; 90 | /* End PBXNativeTarget section */ 91 | 92 | /* Begin PBXProject section */ 93 | 461DB7721BDDFB390070CBCA /* Project object */ = { 94 | isa = PBXProject; 95 | attributes = { 96 | LastUpgradeCheck = 0710; 97 | ORGANIZATIONNAME = oseparovic; 98 | TargetAttributes = { 99 | 461DB77A1BDDFB390070CBCA = { 100 | CreatedOnToolsVersion = 7.1; 101 | }; 102 | }; 103 | }; 104 | buildConfigurationList = 461DB7751BDDFB390070CBCA /* Build configuration list for PBXProject "MessageComposerView" */; 105 | compatibilityVersion = "Xcode 3.2"; 106 | developmentRegion = English; 107 | hasScannedForEncodings = 0; 108 | knownRegions = ( 109 | en, 110 | ); 111 | mainGroup = 461DB7711BDDFB390070CBCA; 112 | productRefGroup = 461DB77C1BDDFB390070CBCA /* Products */; 113 | projectDirPath = ""; 114 | projectRoot = ""; 115 | targets = ( 116 | 461DB77A1BDDFB390070CBCA /* MessageComposerView */, 117 | ); 118 | }; 119 | /* End PBXProject section */ 120 | 121 | /* Begin PBXResourcesBuildPhase section */ 122 | 461DB7791BDDFB390070CBCA /* Resources */ = { 123 | isa = PBXResourcesBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | ); 127 | runOnlyForDeploymentPostprocessing = 0; 128 | }; 129 | /* End PBXResourcesBuildPhase section */ 130 | 131 | /* Begin PBXSourcesBuildPhase section */ 132 | 461DB7761BDDFB390070CBCA /* Sources */ = { 133 | isa = PBXSourcesBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | 461DB7871BDDFB6D0070CBCA /* MessageComposerView.m in Sources */, 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | /* End PBXSourcesBuildPhase section */ 141 | 142 | /* Begin XCBuildConfiguration section */ 143 | 461DB7811BDDFB390070CBCA /* Debug */ = { 144 | isa = XCBuildConfiguration; 145 | buildSettings = { 146 | ALWAYS_SEARCH_USER_PATHS = NO; 147 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 148 | CLANG_CXX_LIBRARY = "libc++"; 149 | CLANG_ENABLE_MODULES = YES; 150 | CLANG_ENABLE_OBJC_ARC = YES; 151 | CLANG_WARN_BOOL_CONVERSION = YES; 152 | CLANG_WARN_CONSTANT_CONVERSION = YES; 153 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 154 | CLANG_WARN_EMPTY_BODY = YES; 155 | CLANG_WARN_ENUM_CONVERSION = YES; 156 | CLANG_WARN_INT_CONVERSION = YES; 157 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 158 | CLANG_WARN_UNREACHABLE_CODE = YES; 159 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 160 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 161 | COPY_PHASE_STRIP = NO; 162 | CURRENT_PROJECT_VERSION = 1; 163 | DEBUG_INFORMATION_FORMAT = dwarf; 164 | ENABLE_STRICT_OBJC_MSGSEND = YES; 165 | ENABLE_TESTABILITY = YES; 166 | GCC_C_LANGUAGE_STANDARD = gnu99; 167 | GCC_DYNAMIC_NO_PIC = NO; 168 | GCC_NO_COMMON_BLOCKS = YES; 169 | GCC_OPTIMIZATION_LEVEL = 0; 170 | GCC_PREPROCESSOR_DEFINITIONS = ( 171 | "DEBUG=1", 172 | "$(inherited)", 173 | ); 174 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 175 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 176 | GCC_WARN_UNDECLARED_SELECTOR = YES; 177 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 178 | GCC_WARN_UNUSED_FUNCTION = YES; 179 | GCC_WARN_UNUSED_VARIABLE = YES; 180 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 181 | MTL_ENABLE_DEBUG_INFO = YES; 182 | ONLY_ACTIVE_ARCH = YES; 183 | SDKROOT = iphoneos; 184 | TARGETED_DEVICE_FAMILY = "1,2"; 185 | VERSIONING_SYSTEM = "apple-generic"; 186 | VERSION_INFO_PREFIX = ""; 187 | }; 188 | name = Debug; 189 | }; 190 | 461DB7821BDDFB390070CBCA /* Release */ = { 191 | isa = XCBuildConfiguration; 192 | buildSettings = { 193 | ALWAYS_SEARCH_USER_PATHS = NO; 194 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 195 | CLANG_CXX_LIBRARY = "libc++"; 196 | CLANG_ENABLE_MODULES = YES; 197 | CLANG_ENABLE_OBJC_ARC = YES; 198 | CLANG_WARN_BOOL_CONVERSION = YES; 199 | CLANG_WARN_CONSTANT_CONVERSION = YES; 200 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 201 | CLANG_WARN_EMPTY_BODY = YES; 202 | CLANG_WARN_ENUM_CONVERSION = YES; 203 | CLANG_WARN_INT_CONVERSION = YES; 204 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 205 | CLANG_WARN_UNREACHABLE_CODE = YES; 206 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 207 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 208 | COPY_PHASE_STRIP = NO; 209 | CURRENT_PROJECT_VERSION = 1; 210 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 211 | ENABLE_NS_ASSERTIONS = NO; 212 | ENABLE_STRICT_OBJC_MSGSEND = YES; 213 | GCC_C_LANGUAGE_STANDARD = gnu99; 214 | GCC_NO_COMMON_BLOCKS = YES; 215 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 216 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 217 | GCC_WARN_UNDECLARED_SELECTOR = YES; 218 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 219 | GCC_WARN_UNUSED_FUNCTION = YES; 220 | GCC_WARN_UNUSED_VARIABLE = YES; 221 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 222 | MTL_ENABLE_DEBUG_INFO = NO; 223 | SDKROOT = iphoneos; 224 | TARGETED_DEVICE_FAMILY = "1,2"; 225 | VALIDATE_PRODUCT = YES; 226 | VERSIONING_SYSTEM = "apple-generic"; 227 | VERSION_INFO_PREFIX = ""; 228 | }; 229 | name = Release; 230 | }; 231 | 461DB7841BDDFB390070CBCA /* Debug */ = { 232 | isa = XCBuildConfiguration; 233 | buildSettings = { 234 | DEFINES_MODULE = YES; 235 | DYLIB_COMPATIBILITY_VERSION = 1; 236 | DYLIB_CURRENT_VERSION = 1; 237 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 238 | INFOPLIST_FILE = MessageComposerView/Info.plist; 239 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 240 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 241 | PRODUCT_BUNDLE_IDENTIFIER = oseparovic.MessageComposerView; 242 | PRODUCT_NAME = "$(TARGET_NAME)"; 243 | SKIP_INSTALL = YES; 244 | }; 245 | name = Debug; 246 | }; 247 | 461DB7851BDDFB390070CBCA /* Release */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | DEFINES_MODULE = YES; 251 | DYLIB_COMPATIBILITY_VERSION = 1; 252 | DYLIB_CURRENT_VERSION = 1; 253 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 254 | INFOPLIST_FILE = MessageComposerView/Info.plist; 255 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 256 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 257 | PRODUCT_BUNDLE_IDENTIFIER = oseparovic.MessageComposerView; 258 | PRODUCT_NAME = "$(TARGET_NAME)"; 259 | SKIP_INSTALL = YES; 260 | }; 261 | name = Release; 262 | }; 263 | /* End XCBuildConfiguration section */ 264 | 265 | /* Begin XCConfigurationList section */ 266 | 461DB7751BDDFB390070CBCA /* Build configuration list for PBXProject "MessageComposerView" */ = { 267 | isa = XCConfigurationList; 268 | buildConfigurations = ( 269 | 461DB7811BDDFB390070CBCA /* Debug */, 270 | 461DB7821BDDFB390070CBCA /* Release */, 271 | ); 272 | defaultConfigurationIsVisible = 0; 273 | defaultConfigurationName = Release; 274 | }; 275 | 461DB7831BDDFB390070CBCA /* Build configuration list for PBXNativeTarget "MessageComposerView" */ = { 276 | isa = XCConfigurationList; 277 | buildConfigurations = ( 278 | 461DB7841BDDFB390070CBCA /* Debug */, 279 | 461DB7851BDDFB390070CBCA /* Release */, 280 | ); 281 | defaultConfigurationIsVisible = 0; 282 | defaultConfigurationName = Release; 283 | }; 284 | /* End XCConfigurationList section */ 285 | }; 286 | rootObject = 461DB7721BDDFB390070CBCA /* Project object */; 287 | } 288 | -------------------------------------------------------------------------------- /MessageComposerView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MessageComposerView.xcodeproj/xcshareddata/xcschemes/MessageComposerView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /MessageComposerView/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /MessageComposerView/MessageComposerView.h: -------------------------------------------------------------------------------- 1 | // MessageComposerView.h 2 | // 3 | // Copyright (c) 2015 oseparovic. ( http://thegameengine.org ) 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | #import 24 | #import 25 | #import 26 | 27 | @protocol MessageComposerViewDelegate 28 | // delegate method executed after the user clicks the send button. Message is the message contained within the 29 | // text view when send is pressed 30 | - (void)messageComposerSendMessageClickedWithMessage:(NSString*)message; 31 | @optional 32 | // executed whenever the MessageComposerView's frame changes. Provides the frame it is changing to and the animation duration 33 | - (void)messageComposerFrameDidChange:(CGRect)frame withAnimationDuration:(CGFloat)duration __attribute__((deprecated)); 34 | // executed whenever the MessageComposerView's frame changes. Provides the frame it is changing to and the animation duration 35 | - (void)messageComposerFrameDidChange:(CGRect)frame withAnimationDuration:(CGFloat)duration andCurve:(NSInteger)curve; 36 | // executed whenever the user is typing in the text view 37 | - (void)messageComposerUserTyping; 38 | @end 39 | 40 | @interface MessageComposerView : UIView 41 | @property(nonatomic, weak) id delegate; 42 | @property(nonatomic, strong) UITextView *messageTextView; 43 | @property(nonatomic, strong) NSString *messagePlaceholder; 44 | @property(nonatomic, strong) UIButton *sendButton; 45 | @property(nonatomic) UIEdgeInsets composerBackgroundInsets; 46 | @property(nonatomic) UIEdgeInsets composerTVInsets; 47 | @property(nonatomic) NSInteger keyboardHeight; 48 | @property(nonatomic) NSInteger keyboardAnimationCurve; 49 | @property(nonatomic) CGFloat keyboardAnimationDuration; 50 | @property(nonatomic) NSInteger keyboardOffset; 51 | @property(nonatomic) NSInteger characterCap; 52 | // configuration method. 53 | - (void)setup; 54 | // layout method 55 | - (void)setupFrames; 56 | 57 | // init with screen width and default height. Offset provided is space between composer and keyboard/bottom of screen 58 | - (id)initWithKeyboardOffset:(NSInteger)offset andMaxHeight:(CGFloat)maxTVHeight; 59 | // init with provided frame and offset between composer and keyboard/bottom of screen 60 | - (id)initWithFrame:(CGRect)frame andKeyboardOffset:(NSInteger)offset; 61 | // init with provided frame and offset between composer and keyboard/bottom of screen. Also set a max height on composer. 62 | - (id)initWithFrame:(CGRect)frame andKeyboardOffset:(NSInteger)offset andMaxHeight:(CGFloat)maxTVHeight; 63 | // provide a function to scroll the textview to bottom manually in fringe cases like loading message drafts etc. 64 | - (void)scrollTextViewToBottom; 65 | // for adding accessory views to the left of the messageTextView 66 | - (void)configureWithAccessory:(UIView *)accessoryView; 67 | // keyboarding resizing function in case you want to overwrite it 68 | - (void)keyboardWillChangeFrame:(NSNotification *)notification; 69 | 70 | // returns the current keyboard height. 0 if keyboard dismissed. 71 | - (CGFloat)currentKeyboardHeight; 72 | 73 | // To avoid exposing the UITextView and attempt to prevent bad practice, startEditing and finishEditing 74 | // are available to become and resign first responder. This means you shouldn't have an excuse to 75 | // do [messageComposerView.messageTextView resignFirstResponder] etc. 76 | - (void)startEditing; 77 | - (void)finishEditing; 78 | @end 79 | 80 | -------------------------------------------------------------------------------- /MessageComposerView/MessageComposerView.m: -------------------------------------------------------------------------------- 1 | // MessageComposerView.m 2 | // 3 | // Copyright (c) 2015 oseparovic. ( http://thegameengine.org ) 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 13 | // all 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 21 | // THE SOFTWARE. 22 | 23 | #import "MessageComposerView.h" 24 | 25 | @interface MessageComposerView() 26 | - (IBAction)sendClicked:(id)sender; 27 | @property(nonatomic, strong) UIView *accessoryView; 28 | @property(nonatomic, strong) UIView *accessoryViewSubView; 29 | @property(nonatomic) CGFloat composerTVMaxHeight; 30 | @end 31 | 32 | @implementation MessageComposerView 33 | 34 | const NSInteger defaultHeight = 44; 35 | const NSInteger defaultMaxHeight = 100; 36 | 37 | - (id)init { 38 | return [self initWithKeyboardOffset:0 andMaxHeight:defaultMaxHeight]; 39 | } 40 | 41 | - (id)initWithKeyboardOffset:(NSInteger)offset andMaxHeight:(CGFloat)maxTVHeight { 42 | CGFloat frameWidth = [self currentScreenSizeInInterfaceOrientation:[self currentInterfaceOrientation]].width; 43 | CGFloat yPos = [self currentScreenSizeInInterfaceOrientation:[self currentInterfaceOrientation]].height-defaultHeight; 44 | return [self initWithFrame:CGRectMake(0, yPos, frameWidth, defaultHeight) andKeyboardOffset:offset andMaxHeight:maxTVHeight]; 45 | } 46 | 47 | - (id)initWithFrame:(CGRect)frame { 48 | return [self initWithFrame:frame andKeyboardOffset:0]; 49 | } 50 | 51 | - (id)initWithFrame:(CGRect)frame andKeyboardOffset:(NSInteger)offset { 52 | return [self initWithFrame:frame andKeyboardOffset:offset andMaxHeight:defaultMaxHeight]; 53 | } 54 | 55 | - (id)initWithFrame:(CGRect)frame andKeyboardOffset:(NSInteger)offset andMaxHeight:(CGFloat)maxTVHeight { 56 | self = [super initWithFrame:frame]; 57 | if (self) { 58 | // Insets for the entire MessageComposerView. Top inset is used as a minimum value of top padding. 59 | _composerBackgroundInsets = UIEdgeInsetsMake(10, 10, 10, 10); 60 | // Insets only for the message UITextView. Default to 0 61 | _composerTVInsets = UIEdgeInsetsMake(0, 0, 0, 10); 62 | 63 | // Default animation time for 5 <= iOS <= 7. Should be overwritten by first keyboard notification. 64 | _keyboardAnimationDuration = 0.25; 65 | _keyboardAnimationCurve = 7; 66 | _keyboardOffset = offset; 67 | _composerBackgroundInsets.top = MAX(_composerBackgroundInsets.top, frame.size.height - _composerBackgroundInsets.bottom - 34); 68 | _composerTVMaxHeight = maxTVHeight; 69 | 70 | // Default character cap if one hasn't been set 71 | if (_characterCap <= 0) { 72 | _characterCap = 400; 73 | } 74 | 75 | // alloc necessary elements 76 | self.sendButton = [[UIButton alloc] initWithFrame:CGRectZero]; 77 | [self.sendButton addTarget:self action:@selector(sendClicked:) forControlEvents:UIControlEventTouchUpInside]; 78 | self.accessoryView = [[UIView alloc] init]; 79 | 80 | // fix ridiculous jumpy scrolling bug inherant in native UITextView since 7.0 81 | // http://stackoverflow.com/a/19339716/740474 82 | NSString *reqSysVer = @"7.0"; 83 | NSString *currSysVer = [[UIDevice currentDevice] systemVersion]; 84 | BOOL osVersionSupported = ([currSysVer compare:reqSysVer options:NSNumericSearch] != NSOrderedAscending); 85 | if (osVersionSupported) { 86 | NSTextStorage* textStorage = [[NSTextStorage alloc] init]; 87 | NSLayoutManager* layoutManager = [NSLayoutManager new]; 88 | [textStorage addLayoutManager:layoutManager]; 89 | NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size]; 90 | [layoutManager addTextContainer:textContainer]; 91 | self.messageTextView = [[UITextView alloc] initWithFrame:CGRectZero textContainer:textContainer]; 92 | } else { 93 | self.messageTextView = [[UITextView alloc] initWithFrame:CGRectZero]; 94 | } 95 | 96 | self.messageTextView.delegate = self; 97 | 98 | // configure elements 99 | self.messagePlaceholder = @"Write a message"; 100 | [self setup]; 101 | 102 | // insert elements above MessageComposerView 103 | [self addSubview:self.sendButton]; 104 | [self addSubview:self.accessoryView]; 105 | [self addSubview:self.messageTextView]; 106 | } 107 | return self; 108 | } 109 | 110 | - (void)dealloc { 111 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 112 | } 113 | 114 | 115 | #pragma mark - Configuration 116 | - (void)setup { 117 | self.backgroundColor = [UIColor lightGrayColor]; 118 | self.autoresizesSubviews = YES; 119 | self.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleWidth; 120 | self.userInteractionEnabled = YES; 121 | self.multipleTouchEnabled = NO; 122 | 123 | [self.sendButton setAutoresizingMask:(UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin)]; 124 | [self.sendButton.layer setCornerRadius:5]; 125 | [self.sendButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 126 | [self.sendButton setTitleColor:[UIColor grayColor] forState:UIControlStateHighlighted]; 127 | [self.sendButton setTitleColor:[UIColor grayColor] forState:UIControlStateDisabled]; 128 | [self.sendButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateSelected]; 129 | [self.sendButton setBackgroundColor:[UIColor orangeColor]]; 130 | [self.sendButton setTitle:@"Send" forState:UIControlStateNormal]; 131 | [self.sendButton.titleLabel setFont:[UIFont boldSystemFontOfSize:14]]; 132 | 133 | [self.accessoryView setAutoresizingMask:(UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin)]; 134 | 135 | [self.messageTextView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin)]; 136 | [self.messageTextView setShowsHorizontalScrollIndicator:NO]; 137 | [self.messageTextView.layer setCornerRadius:2]; 138 | [self.messageTextView setFont:[UIFont systemFontOfSize:14]]; 139 | [self.messageTextView setTextColor:[UIColor lightGrayColor]]; 140 | [self.messageTextView setDelegate:self]; 141 | 142 | [self setupFrames]; 143 | 144 | NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; 145 | [defaultCenter addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; 146 | [defaultCenter addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil]; 147 | } 148 | 149 | - (void)setupFrames { 150 | CGRect sendButtonFrame = self.bounds; 151 | sendButtonFrame.size.width = 60; 152 | sendButtonFrame.size.height = defaultHeight - _composerBackgroundInsets.top - _composerBackgroundInsets.bottom; 153 | sendButtonFrame.origin.x = self.frame.size.width - _composerBackgroundInsets.right - sendButtonFrame.size.width; 154 | sendButtonFrame.origin.y = self.bounds.size.height - _composerBackgroundInsets.bottom - sendButtonFrame.size.height; 155 | [self.sendButton setFrame:sendButtonFrame]; 156 | 157 | CGRect accessoryFrame = self.bounds; 158 | accessoryFrame.size.width = self.accessoryViewSubView.frame.size.width; 159 | accessoryFrame.size.height = defaultHeight - _composerBackgroundInsets.top - _composerBackgroundInsets.bottom; 160 | accessoryFrame.origin.x = _composerBackgroundInsets.left; 161 | accessoryFrame.origin.y = self.bounds.size.height - _composerBackgroundInsets.bottom - accessoryFrame.size.height; 162 | [self.accessoryView setFrame:accessoryFrame]; 163 | [self.accessoryViewSubView setCenter:CGPointMake(self.accessoryView.frame.size.width/2, self.accessoryView.frame.size.height/2)]; 164 | 165 | CGRect messageTextViewFrame = self.bounds; 166 | messageTextViewFrame.origin.x = _composerTVInsets.left + _composerBackgroundInsets.left; 167 | if (accessoryFrame.size.width > 0) { 168 | messageTextViewFrame.origin.x += accessoryFrame.size.width; 169 | } 170 | messageTextViewFrame.origin.y = _composerTVInsets.top; 171 | messageTextViewFrame.size.width = sendButtonFrame.origin.x - _composerTVInsets.right - accessoryFrame.size.width - _composerTVInsets.left - _composerBackgroundInsets.left; 172 | messageTextViewFrame.size.height = messageTextViewFrame.size.height - _composerBackgroundInsets.top - _composerBackgroundInsets.bottom; 173 | [self.messageTextView setFrame:messageTextViewFrame]; 174 | } 175 | 176 | - (void)layoutSubviews { 177 | // Due to inconsistent handling of rotation when receiving UIDeviceOrientationDidChange notifications 178 | // ( see http://stackoverflow.com/q/19974246/740474 ) rotation handling and view resizing is done here. 179 | CGFloat oldHeight = self.messageTextView.frame.size.height; 180 | CGFloat newHeight = [self sizeWithText:self.messageTextView.text]; 181 | 182 | if (newHeight >= _composerTVMaxHeight) { 183 | [self scrollTextViewToBottom]; 184 | } 185 | if (oldHeight == newHeight) { 186 | // In cases where the height remains the same after the text change/rotation only change the y origin 187 | CGRect frame = self.frame; 188 | frame.origin.y = ([self currentScreenSize].height - [self currentKeyboardHeight]) - frame.size.height - _keyboardOffset; 189 | self.frame = frame; 190 | 191 | // Even though the height didn't change the origin did so notify delegates 192 | if ([self.delegate respondsToSelector:@selector(messageComposerFrameDidChange:withAnimationDuration:andCurve:)]) { 193 | [self.delegate messageComposerFrameDidChange:frame withAnimationDuration:_keyboardAnimationDuration andCurve:_keyboardAnimationCurve]; 194 | } 195 | } else { 196 | // The view is already animating as part of the rotation so we just have to make sure it 197 | // snaps to the right place and resizes the textView to wrap the text with the new width. Changing 198 | // to add an additional animation will overload the animation and make it look like someone is 199 | // shuffling a deck of cards. 200 | // Recalculate MessageComposerView container frame 201 | CGRect newContainerFrame = self.frame; 202 | newContainerFrame.size.height = newHeight + _composerBackgroundInsets.top + _composerBackgroundInsets.bottom + _composerTVInsets.top + _composerTVInsets.bottom; 203 | newContainerFrame.origin.y = ([self currentScreenSize].height - [self currentKeyboardHeight]) - newContainerFrame.size.height - _keyboardOffset; 204 | 205 | // Recalculate send button frame 206 | CGRect newSendButtonFrame = self.sendButton.frame; 207 | newSendButtonFrame.origin.y = newContainerFrame.size.height - (_composerBackgroundInsets.bottom + newSendButtonFrame.size.height); 208 | 209 | // Recalculate accessory frame 210 | CGRect newAccessoryFrame = self.accessoryView.frame; 211 | newAccessoryFrame.origin.y = newContainerFrame.size.height - (_composerBackgroundInsets.bottom + newAccessoryFrame.size.height); 212 | 213 | // Recalculate UITextView frame 214 | CGRect newTextViewFrame = self.messageTextView.frame; 215 | newTextViewFrame.size.height = newHeight; 216 | newTextViewFrame.origin.y = _composerBackgroundInsets.top + _composerTVInsets.top; 217 | 218 | self.frame = newContainerFrame; 219 | self.sendButton.frame = newSendButtonFrame; 220 | self.accessoryView.frame = newAccessoryFrame; 221 | self.messageTextView.frame = newTextViewFrame; 222 | [self scrollTextViewToBottom]; 223 | 224 | if ([self.delegate respondsToSelector:@selector(messageComposerFrameDidChange:withAnimationDuration:andCurve:)]) { 225 | [self.delegate messageComposerFrameDidChange:newContainerFrame withAnimationDuration:0 andCurve:0]; 226 | } 227 | } 228 | } 229 | 230 | 231 | #pragma mark - UITextViewDelegate 232 | - (void)textViewDidChange:(UITextView *)textView { 233 | [self layoutSubviews]; 234 | if ([textView.text isEqualToString:self.messagePlaceholder] || [textView.text length] == 0 || [self isStringOnlyWhiteSpace:textView.text]) { 235 | [self.sendButton setEnabled:NO]; 236 | } else { 237 | [self.sendButton setEnabled:YES]; 238 | self.messageTextView.textColor = [UIColor blackColor]; 239 | } 240 | 241 | if ([self.delegate respondsToSelector:@selector(messageComposerUserTyping)]) 242 | [self.delegate messageComposerUserTyping]; 243 | } 244 | 245 | - (void)textViewDidBeginEditing:(UITextView*)textView { 246 | if ([textView.text isEqualToString:self.messagePlaceholder]) { 247 | textView.text = @""; 248 | textView.textColor = [UIColor blackColor]; 249 | [self.sendButton setEnabled:NO]; 250 | } 251 | 252 | CGRect frame = self.frame; 253 | frame.origin.y = ([self currentScreenSize].height - [self currentKeyboardHeight]) - frame.size.height - _keyboardOffset; 254 | 255 | [UIView animateWithDuration:_keyboardAnimationDuration 256 | delay:0.0 257 | options:(_keyboardAnimationCurve << 16) 258 | animations:^{self.frame = frame;} 259 | completion:nil]; 260 | } 261 | 262 | - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { 263 | return textView.text.length + (text.length - range.length) <= self.characterCap; 264 | } 265 | 266 | - (void)textViewDidEndEditing:(UITextView*)textView { 267 | if ([textView.text isEqualToString:@""] || [textView.text length] == 0 || [self isStringOnlyWhiteSpace:textView.text]) { 268 | textView.text = self.messagePlaceholder; 269 | textView.textColor = [UIColor lightGrayColor]; 270 | [self.sendButton setEnabled:NO]; 271 | } 272 | 273 | CGRect frame = self.frame; 274 | frame.origin.y = [self currentScreenSize].height - self.frame.size.height - _keyboardOffset; 275 | 276 | [UIView animateWithDuration:_keyboardAnimationDuration 277 | delay:0.0 278 | options:(_keyboardAnimationCurve << 16) 279 | animations:^{self.frame = frame;} 280 | completion:nil]; 281 | } 282 | 283 | 284 | #pragma mark - Keyboard Notifications 285 | - (void)keyboardWillShow:(NSNotification*)notification { 286 | // Because keyboard animation time and cure vary by iOS version, and we don't want to build the library 287 | // on top of spammy keyboard notifications we use UIKeyboardWillShowNotification ONLY to dynamically set our 288 | // animation duration. As a UIKeyboardWillShowNotification is fired BEFORE textViewDidBeginEditing 289 | // is triggered we can use the following values for all of animations including the first. 290 | _keyboardAnimationDuration = [[notification userInfo][UIKeyboardAnimationDurationUserInfoKey] floatValue]; 291 | _keyboardAnimationCurve = [[notification userInfo][UIKeyboardAnimationCurveUserInfoKey] intValue]; 292 | } 293 | 294 | - (void)keyboardWillChangeFrame:(NSNotification *)notification { 295 | CGRect rect = [[notification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue]; 296 | CGRect converted = [self convertRect:rect fromView:nil]; 297 | self.keyboardHeight = converted.size.height; 298 | [self setNeedsLayout]; 299 | } 300 | 301 | 302 | #pragma mark - IBAction 303 | - (IBAction)sendClicked:(id)sender { 304 | if ([self.delegate respondsToSelector:@selector(messageComposerSendMessageClickedWithMessage:)]) { 305 | [self.delegate messageComposerSendMessageClickedWithMessage:self.messageTextView.text]; 306 | } 307 | 308 | [self.messageTextView setText:@""]; 309 | // Manually trigger the textViewDidChange method as setting the text when the messageTextView is not first responder the 310 | // UITextViewTextDidChangeNotification notification does not get fired. 311 | [self textViewDidChange:self.messageTextView]; 312 | } 313 | 314 | 315 | #pragma mark - Utils 316 | - (void)setMessagePlaceholder:(NSString *)messagePlaceholder { 317 | _messagePlaceholder = messagePlaceholder; 318 | [self.messageTextView setText:_messagePlaceholder]; 319 | // Manually trigger the textViewDidChange method as setting the text when the messageTextView is not first responder the 320 | // UITextViewTextDidChangeNotification notification does not get fired. 321 | [self textViewDidChange:self.messageTextView]; 322 | } 323 | - (void)configureWithAccessory:(UIView *)accessoryView { 324 | // add the accessory view (camera icons etc) to the left of the message text view and rejig the frames to accomodate. 325 | self.accessoryViewSubView = accessoryView; 326 | [self.accessoryViewSubView removeFromSuperview]; 327 | [self.accessoryView addSubview:self.accessoryViewSubView]; 328 | [self setupFrames]; 329 | } 330 | 331 | - (void)scrollTextViewToBottom { 332 | // scrollRangeToVisible:NSMakeRange is a pretty buggy function. Manually setting the content offset seems to work better 333 | CGPoint offset = CGPointMake(0, self.messageTextView.contentSize.height - self.messageTextView.frame.size.height); 334 | [self.messageTextView setContentOffset:offset animated:NO]; 335 | } 336 | 337 | - (CGFloat)currentKeyboardHeight { 338 | if ([self.messageTextView isFirstResponder]) { 339 | return self.keyboardHeight; 340 | } else { 341 | return 0; 342 | } 343 | } 344 | 345 | - (CGFloat)sizeWithText:(NSString*)text { 346 | CGFloat fixedWidth = self.messageTextView.frame.size.width; 347 | CGSize newSize = [self.messageTextView sizeThatFits:CGSizeMake(fixedWidth, CGFLOAT_MAX)]; 348 | return MIN(_composerTVMaxHeight, newSize.height); 349 | } 350 | 351 | - (void)startEditing { 352 | if ([self.messageTextView isFirstResponder] == NO) 353 | [self.messageTextView becomeFirstResponder]; 354 | } 355 | 356 | - (void)finishEditing { 357 | if ([self.messageTextView isFirstResponder]) 358 | [self.messageTextView resignFirstResponder]; 359 | } 360 | 361 | - (BOOL)isStringOnlyWhiteSpace:(NSString*)text { 362 | if ([self isStringEmpty:[text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]) { 363 | return YES; 364 | } 365 | return NO; 366 | } 367 | 368 | - (BOOL)isStringEmpty:(NSString*)inputString { 369 | //http://stackoverflow.com/a/3675518/740474 370 | //isEmpty will return true if the string equates to @"" or nil. Has to be static as 371 | //calling a method on a nil NSString will not execute the method. 372 | return (inputString == nil) 373 | || [inputString isKindOfClass:[NSNull class]] 374 | || ([inputString respondsToSelector:@selector(length)] 375 | && [(NSData *)inputString length] == 0) 376 | || ([inputString respondsToSelector:@selector(count)] 377 | && [(NSArray *)inputString count] == 0); 378 | } 379 | 380 | 381 | #pragma mark - Screen Size Computation 382 | - (CGSize)currentScreenSize { 383 | // There seem to be problems witch each implementation of getting screenSize. If one of these isn't working for your 384 | // specific case please try the others. Note that they might not be cross compatible between different iOS versions :( 385 | 386 | return [self currentScreenSizeAlt1]; 387 | // return [self currentScreenSizeAlt2]; 388 | // return [self currentScreenSizeInInterfaceOrientation:[self currentInterfaceOrientation]]; 389 | } 390 | 391 | - (CGSize)currentScreenSizeAlt1 { 392 | // http://stackoverflow.com/q/31549254/740474 393 | // PROBLEMS: self.nextResponder within the UIView is nil at the point of initialization, meaning it can't be 394 | // used (at least not that I know) to set up the initial frame. Once the view has been initialized and added 395 | // as a subview though this seems to work like a charm for various repositioning uses. 396 | return ((UIView*)self.nextResponder).frame.size; 397 | } 398 | 399 | - (CGSize)currentScreenSizeAlt2 { 400 | // http://stackoverflow.com/a/15707997/740474 401 | // PROBLEMS: nav bar height was unreliable. For example when UIAlertView height was present 402 | // we couldn't properly determine the nav bar height. Doesn't work well with rotation. 403 | UIView *rootView = [[[UIApplication sharedApplication] keyWindow] rootViewController].view; 404 | CGRect originalFrame = [[UIScreen mainScreen] bounds]; 405 | CGRect adjustedFrame = [rootView convertRect:originalFrame fromView:nil]; 406 | return adjustedFrame.size; 407 | } 408 | 409 | - (CGSize)currentScreenSizeInInterfaceOrientation:(UIInterfaceOrientation)orientation { 410 | // http://stackoverflow.com/a/7905540/740474 411 | // PROBLEMS: nav bar height was unreliable. For example when UIAlertView height was present 412 | // we couldn't properly determine the nav bar height. 413 | 414 | // get the size of the application frame (screensize - status bar height) 415 | CGSize size = [UIScreen mainScreen].applicationFrame.size; 416 | 417 | // if the orientation at this point is landscape but it hasn't fully rotated yet use landscape size instead. 418 | // handling differs between iOS 7 && 8 so need to check if size is properly configured or not. On 419 | // iOS 7 height will still be greater than width in landscape without this call but on iOS 8 420 | // it won't 421 | if (UIInterfaceOrientationIsLandscape(orientation) && size.height > size.width) { 422 | size = CGSizeMake(size.height, size.width); 423 | } 424 | 425 | // subtract the height of the navigation bar from the screen height 426 | size.height -= [self currentNavigationBarHeight]; 427 | 428 | return size; 429 | } 430 | 431 | - (UIInterfaceOrientation)currentInterfaceOrientation { 432 | // Returns the orientation of the Interface NOT the Device. The two do not happen in exact unison so 433 | // this point is important. 434 | return [UIApplication sharedApplication].statusBarOrientation; 435 | } 436 | 437 | - (CGFloat)currentNavigationBarHeight { 438 | // TODO this will fail to get the correct height when a UIAlertView is present 439 | id nav = [UIApplication sharedApplication].keyWindow.rootViewController; 440 | if ([nav isKindOfClass:[UINavigationController class]]) { 441 | UINavigationController *navc = (UINavigationController *) nav; 442 | if(navc.navigationBarHidden) { 443 | return 0; 444 | } else { 445 | return navc.navigationBar.frame.size.height; 446 | } 447 | } 448 | return 0; 449 | } 450 | 451 | @end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MessageComposerView [DEPRECATED] 2 | =================== 3 | 4 | If you find yourself needing a `UITextView` that sticks to the keyboard similar to an `inputAccessoryView` that does *not* disappear when the keyboard hides, you'll quickly find that you'll likely have to build out a fairly time consuming custom view. MessageComposerView aims to save you all that setup time and headache and provide a simple, customizable implementation. 5 | 6 | Rather that being an `inputAccessoryView`, it is a custom `UIView` that will automatically "stick" the keyboard, handling rotation, text changes and keyboard state changes. 7 | 8 | ![](http://www.thegameengine.org/wp-content/uploads/2013/11/message_composer_quad_1.jpg) 9 | 10 | Usage 11 | ----- 12 | ####Setup 13 | In your header file: 14 | 15 | 1. Import the `MessageComosposerView.h` file 16 | 2. Add the `MessageComposerViewDelegate` delegate 17 | 3. Optionally create a `MessageComposerView` property for your message composer view. 18 | 19 | Example: 20 | ``` 21 | #import "MessageComposerView.h" 22 | @interface ViewController : UIViewController 23 | @property (nonatomic, strong) MessageComposerView *messageComposerView; 24 | @end 25 | ``` 26 | In your class file, instantiate and add `MessageComposerView` to the bottom of your view controller. You can do this simply via `init` (the default height is 54) 27 | ``` 28 | self.messageComposerView = [[MessageComposerView alloc] init]; 29 | self.messageComposerView.delegate = self; 30 | [self.view addSubview:self.messageComposerView]; 31 | ``` 32 | There are several custom initializers that are also supported: 33 | 34 | * `- (id)initWithKeyboardOffset:(NSInteger)offset andMaxHeight:(CGFloat)maxTVHeight;` 35 | init with screen width and default height. Offset provided is space between composer and keyboard/bottom of screen 36 | * `- (id)initWithFrame:(CGRect)frame andKeyboardOffset:(NSInteger)offset;` 37 | init with provided frame and offset between composer and keyboard/bottom of screen 38 | * `- (id)initWithFrame:(CGRect)frame andKeyboardOffset:(NSInteger)offset andMaxHeight:(CGFloat)maxTVHeight;` 39 | init with provided frame and offset between composer and keyboard/bottom of screen. Also set a max height on composer. 40 | 41 | Message composerview supports text placeholders. If you want to customize the placeholder text, simply edit the `messagePlaceholder` property like so: 42 | 43 | ``` 44 | self.messageComposerView.messagePlaceholder = @"Type a comment..."; 45 | ``` 46 | 47 | If you want to add an accessory view (e.g. a camera button) you can do so via the `configureWithAccessory:` function. This will automatically add your passed in `UIView` to the left of the message text view. Configuration would go something like this: 48 | 49 | ``` 50 | UIButton *cameraButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 20, 20)]; 51 | [self.cameraButton setImage:[UIImage cameraButtonImage] forState:UIControlStateNormal]; 52 | [self.cameraButton addTarget:self action:@selector(cameraClicked:) forControlEvents:UIControlEventTouchUpInside]; 53 | [self.cameraButton setContentMode:UIViewContentModeCenter]; 54 | [self.messageComposerView configureWithAccessory:cameraButton]; 55 | ``` 56 | 57 | ####Delegation 58 | The `MessageComposerViewDelegate` has several delegate methods: 59 | 60 | 1. `- (void)messageComposerSendMessageClickedWithMessage:(NSString*)message;` 61 | **Required** - Triggered whenever the user presses the send button. `message` is the text within the `UITextView` at the time the button was pressed. 62 | 63 | 2. `- (void)messageComposerFrameDidChange:(CGRect)frame withAnimationDuration:(CGFloat)duration andCurve:(NSInteger)curve;` 64 | **Optional** - Triggered whenever the UITextView frame is reconfigured. `frame` is the CGRect that was applied to the MessageComposerView container. You can use this frame - namely the y pos - to determine the offset of your own views when the keyboard changes position. The duration will allow you to match the animation precisely. 65 | 66 | 3. `- (void)messageComposerUserTyping;` 67 | **Optional** - Triggered whenever the UITextView text changes. 68 | 69 | ####CocoaPods 70 | If you're using (or want to use) the CocoaPods you can get the latest stable release by doing the following: 71 | 72 | 1. Install cocoapods and set it up (if you haven't already) 73 | `sudo gem install cocoapods` 74 | `pod setup` 75 | 76 | 2. Create a textfile in your project's root directory called `Podfile` (if you haven't already) and add the following line: 77 | `pod 'MessageComposerView', :git => 'https://github.com/oseparovic/MessageComposerView.git'` 78 | 79 | 3. Run `pod install`. You should see something like the following: 80 | `Downloading dependencies` 81 | `Installing MessageComposerView (1.3.0)` 82 | `Generating Pods project` 83 | `Integrating client project` 84 | 85 | 4. Start your project via the newly created `.xcworkspace` file. Cocoapods creates a seperate xcode project file that has the included pods set up for you. It should be in your project's root directory right alongside your `.xcodeproj` file if everything went smoothly! 86 | 87 | How it works 88 | ------------ 89 | 90 | MessageComposerView tries to avoid configuration in `UIKeyboardWillShowNotification` and `UIKeyboardDidShowNotification` as I found them to be at times excessive and difficult to properly manipulate, especially when it came to rotation. 91 | Instead the `UIKeyboardWillShowNotification` notification is used solely to dynamically determine the keyboard animation duration on your device before any animations occur. 92 | 93 | The actual resizing of the views is handled via `layoutSubviews` and through the following `UITextViewDelegate` methods: 94 | 95 | * `- (void)textViewDidBeginEditing:(UITextView*)textView` 96 | * `- (void)textViewDidEndEditing:(UITextView*)textView` 97 | 98 | Contact 99 | ------- 100 | 101 | If you need to contact me you can do so via my twitter [@alexgophermix](https://twitter.com/alexgophermix) or through my website [thegameengine.org](http://www.thegameengine.org/contact/) 102 | --------------------------------------------------------------------------------