Explorar el Código

ios 移除大的框架减少体积

fancy hace 5 años
padre
commit
d64df8d19e
Se han modificado 100 ficheros con 8854 adiciones y 339 borrados
  1. 29 86
      o2ios/O2Platform.xcodeproj/project.pbxproj
  2. 6 1
      o2ios/O2Platform.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  3. 21 4
      o2ios/O2Platform/App/Cms-内容管理/c/CMSCategoryListViewController.swift
  4. 53 7
      o2ios/O2Platform/App/Cms-内容管理/c/CMSCreateDocViewController.swift
  5. 2 0
      o2ios/O2Platform/App/Cms-内容管理/m/CMSCategory.swift
  6. 170 117
      o2ios/O2Platform/App/Cms-内容管理/m/CMSData.swift
  7. 10 5
      o2ios/O2Platform/App/Login-绑定登录/c/LoginViewController.swift
  8. 13 1
      o2ios/O2Platform/App/Login-绑定登录/c/OOGuidePageController.swift
  9. 12 19
      o2ios/O2Platform/App/Login-绑定登录/login.storyboard
  10. 3 2
      o2ios/O2Platform/App/NewAttance-考勤打卡/c/OOAttanceCheckInController.swift
  11. 25 27
      o2ios/O2Platform/App/NewAttance-考勤打卡/c/OOAttanceSettingController.swift
  12. 1 1
      o2ios/O2Platform/App/NewAttance-考勤打卡/c/OONewAttanceNavController.swift
  13. 42 24
      o2ios/O2Platform/App/NewAttance-考勤打卡/v/OOAttanceHeaderView.swift
  14. 1 1
      o2ios/O2Platform/App/NewAttance-考勤打卡/v/OOAttandanceSettingDataView.swift
  15. 34 39
      o2ios/O2Platform/App/Work-工作/c/TaskCreateViewController.swift
  16. 71 2
      o2ios/O2Platform/App/Work-工作/c/category/ZoneMenuViewController.swift
  17. 68 0
      o2ios/O2Platform/App/Work-工作/m/CreateProcessBean.swift
  18. 15 0
      o2ios/O2Platform/App/Work-工作/m/TaskCreateData.swift
  19. 7 1
      o2ios/O2Platform/App/Work-工作/v/NewMainItemTableViewCell.swift
  20. 1 1
      o2ios/O2Platform/App/contacts/c/ContactPersonInfoV2ViewController.swift
  21. 1 1
      o2ios/O2Platform/AppDelegate.swift
  22. 244 0
      o2ios/O2Platform/Framework/AZPopMenu/AZPopMenu.swift
  23. 367 0
      o2ios/O2Platform/Framework/DatePickerDialogSwift/LWDatePickerDialog.swift
  24. 35 0
      o2ios/O2Platform/Framework/Haneke/CGSize+Swift.swift
  25. 311 0
      o2ios/O2Platform/Framework/Haneke/Cache.swift
  26. 229 0
      o2ios/O2Platform/Framework/Haneke/CryptoSwiftMD5.swift
  27. 129 0
      o2ios/O2Platform/Framework/Haneke/Data.swift
  28. 237 0
      o2ios/O2Platform/Framework/Haneke/DiskCache.swift
  29. 92 0
      o2ios/O2Platform/Framework/Haneke/DiskFetcher.swift
  30. 89 0
      o2ios/O2Platform/Framework/Haneke/Fetch.swift
  31. 41 0
      o2ios/O2Platform/Framework/Haneke/Fetcher.swift
  32. 93 0
      o2ios/O2Platform/Framework/Haneke/Format.swift
  33. 19 0
      o2ios/O2Platform/Framework/Haneke/Haneke.h
  34. 55 0
      o2ios/O2Platform/Framework/Haneke/Haneke.swift
  35. 38 0
      o2ios/O2Platform/Framework/Haneke/Log.swift
  36. 65 0
      o2ios/O2Platform/Framework/Haneke/NSFileManager+Haneke.swift
  37. 22 0
      o2ios/O2Platform/Framework/Haneke/NSHTTPURLResponse+Haneke.swift
  38. 22 0
      o2ios/O2Platform/Framework/Haneke/NSURLResponse+Haneke.swift
  39. 105 0
      o2ios/O2Platform/Framework/Haneke/NetworkFetcher.swift
  40. 49 0
      o2ios/O2Platform/Framework/Haneke/String+Haneke.swift
  41. 234 0
      o2ios/O2Platform/Framework/Haneke/UIButton+Haneke.swift
  42. 81 0
      o2ios/O2Platform/Framework/Haneke/UIImage+Haneke.swift
  43. 139 0
      o2ios/O2Platform/Framework/Haneke/UIImageView+Haneke.swift
  44. 48 0
      o2ios/O2Platform/Framework/Haneke/UIView+Haneke.swift
  45. 54 0
      o2ios/O2Platform/Framework/ImageRow/ImageCheckRow.swift
  46. 62 0
      o2ios/O2Platform/Framework/ImageRow/ImagePickerController.swift
  47. 238 0
      o2ios/O2Platform/Framework/ImageRow/ImageRow.swift
  48. 81 0
      o2ios/O2Platform/Framework/InputView/InputView.h
  49. 168 0
      o2ios/O2Platform/Framework/InputView/InputView.m
  50. 57 0
      o2ios/O2Platform/Framework/InputView/InputView.xib
  51. 32 0
      o2ios/O2Platform/Framework/InputView/UIViewExt.h
  52. 189 0
      o2ios/O2Platform/Framework/InputView/UIViewExt.m
  53. 52 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCCEmoticon.swift
  54. 49 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCCEmoticonGroup.swift
  55. 47 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCCEmoticonLarge.swift
  56. 62 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCDraft.swift
  57. 170 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCMessage.swift
  58. 25 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCRemind.swift
  59. 26 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Protocols/JCMessageContentViewType.swift
  60. 81 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Protocols/JCMessageOptions.swift
  61. 45 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Protocols/JCMessageType.swift
  62. 15 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Protocols/JCUserType.swift
  63. 132 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/FileCell.swift
  64. 80 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/GroupAvatorCell.swift
  65. 85 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/ImageFileCell.swift
  66. 29 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/ImageFileHeader.swift
  67. 494 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatView.swift
  68. 364 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewCell.swift
  69. 46 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewData.swift
  70. 324 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewLayout.swift
  71. 40 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewLayoutAttributes.swift
  72. 38 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewLayoutAttributesInfo.swift
  73. 380 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewUpdate.swift
  74. 232 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCConversationCell.swift
  75. 78 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCGroupMemberCell.swift
  76. 215 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCGroupSettingCell.swift
  77. 74 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCMessageAvatarView.swift
  78. 32 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCMessageCardView.swift
  79. 114 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCMessageTipsView.swift
  80. 56 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCNetworkTipsCell.swift
  81. 101 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCSelectMemberCell.swift
  82. 90 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCSingleSettingCell.swift
  83. 58 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCUpdateMemberCell.swift
  84. 83 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JACMessageImageContentView.swift
  85. 27 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCBusinessCardContent.swift
  86. 125 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCBusinessCardContentView.swift
  87. 28 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageFileContent.swift
  88. 127 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageFileContentView.swift
  89. 34 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageImageContent.swift
  90. 26 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageLocationContent.swift
  91. 70 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageLocationContentView.swift
  92. 39 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageNoticeContent.swift
  93. 41 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageNoticeContentView.swift
  94. 42 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageTextContent.swift
  95. 49 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageTextContentView.swift
  96. 93 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageTimeLineContent.swift
  97. 39 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageTimeLineContentView.swift
  98. 36 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageVideoContent.swift
  99. 115 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageVideoContentView.swift
  100. 36 0
      o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageVoiceContent.swift

+ 29 - 86
o2ios/O2Platform.xcodeproj/project.pbxproj

@@ -54,12 +54,8 @@
 		B108F400229E34D400778050 /* LBXPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B108F3F9229E34D300778050 /* LBXPermissions.swift */; };
 		B108F401229E34D400778050 /* LBXScanWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B108F3FA229E34D300778050 /* LBXScanWrapper.swift */; };
 		B108F402229E34D400778050 /* LBXScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B108F3FB229E34D300778050 /* LBXScanViewController.swift */; };
-		B10A2F52233DBA7C0011CE3D /* jpush-extension-ios-1.1.2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B10A2F4D233DBA7B0011CE3D /* jpush-extension-ios-1.1.2.a */; };
-		B10A2F53233DBA7C0011CE3D /* jpush-ios-3.2.4.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B10A2F4E233DBA7B0011CE3D /* jpush-ios-3.2.4.a */; };
-		B10A2F54233DBA7C0011CE3D /* jcore-ios-2.1.2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B10A2F51233DBA7C0011CE3D /* jcore-ios-2.1.2.a */; };
 		B10A2F56233DDA990011CE3D /* AdSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B10A2F55233DDA990011CE3D /* AdSupport.framework */; };
 		B10A2F58233DDAE10011CE3D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B10A2F57233DDAE10011CE3D /* Security.framework */; };
-		B10A2F5A233E05550011CE3D /* JMessage.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B10A2F59233E05550011CE3D /* JMessage.framework */; };
 		B1298E50236692AB006E9236 /* CloudFileListBaseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1298E4F236692AB006E9236 /* CloudFileListBaseController.swift */; };
 		B1298E7C23669BA2006E9236 /* CloudFileTypeListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1298E7B23669BA2006E9236 /* CloudFileTypeListController.swift */; };
 		B1298E7E2366AE4C006E9236 /* CloudFileImageCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1298E7D2366AE4C006E9236 /* CloudFileImageCollectionController.swift */; };
@@ -202,6 +198,7 @@
 		B1B7470621523ED70041948D /* SignatureViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B746CF21523ED60041948D /* SignatureViewCell.swift */; };
 		B1B7470721523ED70041948D /* SignatureViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B1B7470521523ED60041948D /* SignatureViewCell.xib */; };
 		B1B7470921523F010041948D /* O2UISignatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1B7470821523F010041948D /* O2UISignatureView.swift */; };
+		B1BA42B824133AC40081CED8 /* TaskCreateData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1BA42B724133AC40081CED8 /* TaskCreateData.swift */; };
 		B1BC8CDA216B3D5F00AF571F /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B1BC8CD9216B3D5E00AF571F /* libc++.tbd */; };
 		B1C19025211437E200935829 /* OOCalendarLeftMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C19024211437E200935829 /* OOCalendarLeftMenuController.swift */; };
 		B1C1905D2114410D00935829 /* CalendarTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C1905C2114410D00935829 /* CalendarTableViewCell.swift */; };
@@ -339,7 +336,6 @@
 		E428AF5D20AC1CD100D964B9 /* OOAttandanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E428AF5C20AC1CD100D964B9 /* OOAttandanceViewModel.swift */; };
 		E428AF6020AD4DCE00D964B9 /* OOAttanceCheckInController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E428AF5E20AD4DCE00D964B9 /* OOAttanceCheckInController.swift */; };
 		E428AF6120AD4DCE00D964B9 /* OOAttanceCheckInController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E428AF5F20AD4DCE00D964B9 /* OOAttanceCheckInController.xib */; };
-		E428AF6320ADA06600D964B9 /* mapapi.bundle in Resources */ = {isa = PBXBuildFile; fileRef = E428AF6220ADA06600D964B9 /* mapapi.bundle */; };
 		E428AF6720AEA5E300D964B9 /* OOAttandanceSettingDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E428AF6620AEA5E300D964B9 /* OOAttandanceSettingDataView.swift */; };
 		E428AF6920AEA5EE00D964B9 /* OOAttandanceSettingDataView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E428AF6820AEA5EE00D964B9 /* OOAttandanceSettingDataView.xib */; };
 		E42E41C61E6E570E00C5B5C7 /* MainPublishTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E42E41C51E6E570E00C5B5C7 /* MainPublishTableViewCell.swift */; };
@@ -951,13 +947,6 @@
 		E4D23114209C42E700837868 /* OOSelectPersonTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E4D23112209C42E700837868 /* OOSelectPersonTableViewCell.xib */; };
 		E4D23118209EF74D00837868 /* libcrypto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D23116209EF74C00837868 /* libcrypto.a */; };
 		E4D23119209EF74D00837868 /* libssl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D23117209EF74C00837868 /* libssl.a */; };
-		E4D23122209F3CD200837868 /* BaiduMapAPI_Utils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D2311B209F3CCE00837868 /* BaiduMapAPI_Utils.framework */; };
-		E4D23123209F3CD200837868 /* BaiduMapAPI_Cloud.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D2311C209F3CCE00837868 /* BaiduMapAPI_Cloud.framework */; };
-		E4D23124209F3CD200837868 /* BaiduMapAPI_Search.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D2311D209F3CCF00837868 /* BaiduMapAPI_Search.framework */; };
-		E4D23125209F3CD200837868 /* BaiduMapAPI_Location.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D2311E209F3CCF00837868 /* BaiduMapAPI_Location.framework */; };
-		E4D23126209F3CD200837868 /* BaiduMapAPI_Map.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D2311F209F3CD100837868 /* BaiduMapAPI_Map.framework */; };
-		E4D23127209F3CD200837868 /* BaiduMapAPI_Radar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D23120209F3CD200837868 /* BaiduMapAPI_Radar.framework */; };
-		E4D23128209F3CD200837868 /* BaiduMapAPI_Base.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4D23121209F3CD200837868 /* BaiduMapAPI_Base.framework */; };
 		E4D2312B20A29E2700837868 /* OOAppMainCollectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E4D2312920A29E2600837868 /* OOAppMainCollectionHeaderView.xib */; };
 		E4D2312E20A29E7500837868 /* OOAppMainCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D2312D20A29E7500837868 /* OOAppMainCollectionHeaderView.swift */; };
 		E4D2313020A29E9F00837868 /* OOAppMainCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4D2312F20A29E9F00837868 /* OOAppMainCollectionReusableView.swift */; };
@@ -1367,14 +1356,8 @@
 		B108F3F9229E34D300778050 /* LBXPermissions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LBXPermissions.swift; sourceTree = "<group>"; };
 		B108F3FA229E34D300778050 /* LBXScanWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LBXScanWrapper.swift; sourceTree = "<group>"; };
 		B108F3FB229E34D300778050 /* LBXScanViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LBXScanViewController.swift; sourceTree = "<group>"; };
-		B10A2F4D233DBA7B0011CE3D /* jpush-extension-ios-1.1.2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "jpush-extension-ios-1.1.2.a"; sourceTree = "<group>"; };
-		B10A2F4E233DBA7B0011CE3D /* jpush-ios-3.2.4.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "jpush-ios-3.2.4.a"; sourceTree = "<group>"; };
-		B10A2F4F233DBA7B0011CE3D /* JPushNotificationExtensionService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JPushNotificationExtensionService.h; sourceTree = "<group>"; };
-		B10A2F50233DBA7B0011CE3D /* JPUSHService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JPUSHService.h; sourceTree = "<group>"; };
-		B10A2F51233DBA7C0011CE3D /* jcore-ios-2.1.2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "jcore-ios-2.1.2.a"; sourceTree = "<group>"; };
 		B10A2F55233DDA990011CE3D /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; };
 		B10A2F57233DDAE10011CE3D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
-		B10A2F59233E05550011CE3D /* JMessage.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = JMessage.framework; sourceTree = "<group>"; };
 		B1298E4F236692AB006E9236 /* CloudFileListBaseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudFileListBaseController.swift; sourceTree = "<group>"; };
 		B1298E7B23669BA2006E9236 /* CloudFileTypeListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudFileTypeListController.swift; sourceTree = "<group>"; };
 		B1298E7D2366AE4C006E9236 /* CloudFileImageCollectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudFileImageCollectionController.swift; sourceTree = "<group>"; };
@@ -1514,6 +1497,7 @@
 		B1B746CF21523ED60041948D /* SignatureViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignatureViewCell.swift; sourceTree = "<group>"; };
 		B1B7470521523ED60041948D /* SignatureViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SignatureViewCell.xib; sourceTree = "<group>"; };
 		B1B7470821523F010041948D /* O2UISignatureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = O2UISignatureView.swift; sourceTree = "<group>"; };
+		B1BA42B724133AC40081CED8 /* TaskCreateData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskCreateData.swift; sourceTree = "<group>"; };
 		B1BC8CD9216B3D5E00AF571F /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
 		B1C19024211437E200935829 /* OOCalendarLeftMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OOCalendarLeftMenuController.swift; sourceTree = "<group>"; };
 		B1C1905C2114410D00935829 /* CalendarTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarTableViewCell.swift; sourceTree = "<group>"; };
@@ -1658,7 +1642,6 @@
 		E428AF5C20AC1CD100D964B9 /* OOAttandanceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OOAttandanceViewModel.swift; sourceTree = "<group>"; };
 		E428AF5E20AD4DCE00D964B9 /* OOAttanceCheckInController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OOAttanceCheckInController.swift; sourceTree = "<group>"; };
 		E428AF5F20AD4DCE00D964B9 /* OOAttanceCheckInController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OOAttanceCheckInController.xib; sourceTree = "<group>"; };
-		E428AF6220ADA06600D964B9 /* mapapi.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = mapapi.bundle; path = BaiduMapAPI_Map.framework/Resources/mapapi.bundle; sourceTree = "<group>"; };
 		E428AF6620AEA5E300D964B9 /* OOAttandanceSettingDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OOAttandanceSettingDataView.swift; sourceTree = "<group>"; };
 		E428AF6820AEA5EE00D964B9 /* OOAttandanceSettingDataView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OOAttandanceSettingDataView.xib; sourceTree = "<group>"; };
 		E42E41C51E6E570E00C5B5C7 /* MainPublishTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainPublishTableViewCell.swift; sourceTree = "<group>"; };
@@ -2328,13 +2311,6 @@
 		E4D23112209C42E700837868 /* OOSelectPersonTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OOSelectPersonTableViewCell.xib; sourceTree = "<group>"; };
 		E4D23116209EF74C00837868 /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libcrypto.a; sourceTree = "<group>"; };
 		E4D23117209EF74C00837868 /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libssl.a; sourceTree = "<group>"; };
-		E4D2311B209F3CCE00837868 /* BaiduMapAPI_Utils.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = BaiduMapAPI_Utils.framework; sourceTree = "<group>"; };
-		E4D2311C209F3CCE00837868 /* BaiduMapAPI_Cloud.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = BaiduMapAPI_Cloud.framework; sourceTree = "<group>"; };
-		E4D2311D209F3CCF00837868 /* BaiduMapAPI_Search.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = BaiduMapAPI_Search.framework; sourceTree = "<group>"; };
-		E4D2311E209F3CCF00837868 /* BaiduMapAPI_Location.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = BaiduMapAPI_Location.framework; sourceTree = "<group>"; };
-		E4D2311F209F3CD100837868 /* BaiduMapAPI_Map.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = BaiduMapAPI_Map.framework; sourceTree = "<group>"; };
-		E4D23120209F3CD200837868 /* BaiduMapAPI_Radar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = BaiduMapAPI_Radar.framework; sourceTree = "<group>"; };
-		E4D23121209F3CD200837868 /* BaiduMapAPI_Base.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = BaiduMapAPI_Base.framework; sourceTree = "<group>"; };
 		E4D2312920A29E2600837868 /* OOAppMainCollectionHeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OOAppMainCollectionHeaderView.xib; sourceTree = "<group>"; };
 		E4D2312D20A29E7500837868 /* OOAppMainCollectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OOAppMainCollectionHeaderView.swift; sourceTree = "<group>"; };
 		E4D2312F20A29E9F00837868 /* OOAppMainCollectionReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OOAppMainCollectionReusableView.swift; sourceTree = "<group>"; };
@@ -2371,11 +2347,8 @@
 				B130E64A223B774D00B68354 /* shared_preferences.framework in Frameworks */,
 				E40958C920805211000FECC3 /* AVFoundation.framework in Frameworks */,
 				B130E644223B774400B68354 /* Flutter.framework in Frameworks */,
-				E4D23122209F3CD200837868 /* BaiduMapAPI_Utils.framework in Frameworks */,
-				B10A2F5A233E05550011CE3D /* JMessage.framework in Frameworks */,
 				E4375B1C207CABF00065A880 /* MobileCoreServices.framework in Frameworks */,
 				E4375B1E207CAC020065A880 /* ImageIO.framework in Frameworks */,
-				B10A2F54233DBA7C0011CE3D /* jcore-ios-2.1.2.a in Frameworks */,
 				E40958C7208051DE000FECC3 /* CoreTelephony.framework in Frameworks */,
 				E4418C5F1DDC1AC80066348D /* CoreGraphics.framework in Frameworks */,
 				E4375B12207CABAB0065A880 /* SystemConfiguration.framework in Frameworks */,
@@ -2389,22 +2362,14 @@
 				E4375B0E207CAB980065A880 /* CoreData.framework in Frameworks */,
 				E4375B0C207CAB860065A880 /* libz.tbd in Frameworks */,
 				E4375B08207CAB770065A880 /* libsqlite3.0.tbd in Frameworks */,
-				E4D23128209F3CD200837868 /* BaiduMapAPI_Base.framework in Frameworks */,
 				E4375B06207CAB6D0065A880 /* libresolv.tbd in Frameworks */,
 				E4418C691DDC1AF70066348D /* Foundation.framework in Frameworks */,
-				E4D23127209F3CD200837868 /* BaiduMapAPI_Radar.framework in Frameworks */,
-				E4D23126209F3CD200837868 /* BaiduMapAPI_Map.framework in Frameworks */,
 				E4418C671DDC1AEE0066348D /* UIKit.framework in Frameworks */,
-				E4D23124209F3CD200837868 /* BaiduMapAPI_Search.framework in Frameworks */,
 				E4418C651DDC1AE70066348D /* Accelerate.framework in Frameworks */,
 				E4418C631DDC1AD60066348D /* CoreImage.framework in Frameworks */,
 				E4418C611DDC1ACF0066348D /* QuartzCore.framework in Frameworks */,
 				E4D23118209EF74D00837868 /* libcrypto.a in Frameworks */,
 				37D63E73FDC49F690C780FD1 /* Pods_O2Platform.framework in Frameworks */,
-				E4D23125209F3CD200837868 /* BaiduMapAPI_Location.framework in Frameworks */,
-				B10A2F53233DBA7C0011CE3D /* jpush-ios-3.2.4.a in Frameworks */,
-				E4D23123209F3CD200837868 /* BaiduMapAPI_Cloud.framework in Frameworks */,
-				B10A2F52233DBA7C0011CE3D /* jpush-extension-ios-1.1.2.a in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -3860,10 +3825,8 @@
 				B165CC9D2241DB9F00373B66 /* DatePickerDialogSwift */,
 				B19BCE442228272500BD454C /* O2OA_Flutter_SDK */,
 				B1AE4E2221A3DA0F00183FCD /* O2OA_Auth_SDK.framework */,
-				E4D2311A209F3CA800837868 /* BaiduMap */,
 				E4D23115209EF74C00837868 /* thirdlibs */,
 				E4C24A5120844F2600E426B0 /* JMessage */,
-				E4C24A332081B05E00E426B0 /* JPUSH */,
 				E40502C120722208009A8D30 /* ImageRow */,
 				E4CB277F1E793AE6004A7ACB /* ZonePickerView */,
 				09E02E6B1F16319600579887 /* Haneke */,
@@ -4172,6 +4135,7 @@
 				E4B8884F1D9D48F1002E1A46 /* Module.swift */,
 				E4B888501D9D48F1002E1A46 /* TodoTask.swift */,
 				B1908E1322685E8F00D75632 /* O2WebViewModels.swift */,
+				B1BA42B724133AC40081CED8 /* TaskCreateData.swift */,
 			);
 			path = m;
 			sourceTree = "<group>";
@@ -4234,19 +4198,6 @@
 			path = Base;
 			sourceTree = "<group>";
 		};
-		E4C24A332081B05E00E426B0 /* JPUSH */ = {
-			isa = PBXGroup;
-			children = (
-				B10A2F59233E05550011CE3D /* JMessage.framework */,
-				B10A2F51233DBA7C0011CE3D /* jcore-ios-2.1.2.a */,
-				B10A2F4D233DBA7B0011CE3D /* jpush-extension-ios-1.1.2.a */,
-				B10A2F4E233DBA7B0011CE3D /* jpush-ios-3.2.4.a */,
-				B10A2F4F233DBA7B0011CE3D /* JPushNotificationExtensionService.h */,
-				B10A2F50233DBA7B0011CE3D /* JPUSHService.h */,
-			);
-			path = JPUSH;
-			sourceTree = "<group>";
-		};
 		E4C24A5120844F2600E426B0 /* JMessage */ = {
 			isa = PBXGroup;
 			children = (
@@ -4965,21 +4916,6 @@
 			path = thirdlibs;
 			sourceTree = "<group>";
 		};
-		E4D2311A209F3CA800837868 /* BaiduMap */ = {
-			isa = PBXGroup;
-			children = (
-				E428AF6220ADA06600D964B9 /* mapapi.bundle */,
-				E4D23121209F3CD200837868 /* BaiduMapAPI_Base.framework */,
-				E4D2311C209F3CCE00837868 /* BaiduMapAPI_Cloud.framework */,
-				E4D2311E209F3CCF00837868 /* BaiduMapAPI_Location.framework */,
-				E4D2311F209F3CD100837868 /* BaiduMapAPI_Map.framework */,
-				E4D23120209F3CD200837868 /* BaiduMapAPI_Radar.framework */,
-				E4D2311D209F3CCF00837868 /* BaiduMapAPI_Search.framework */,
-				E4D2311B209F3CCE00837868 /* BaiduMapAPI_Utils.framework */,
-			);
-			path = BaiduMap;
-			sourceTree = "<group>";
-		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -4995,6 +4931,7 @@
 				E4C24A322081AFD500E426B0 /* Embed App Extensions */,
 				B1AE4E2821A3DCC200183FCD /* Embed Frameworks */,
 				B144D7B222B76E71000AAD8F /* Run Script */,
+				1B32B645931F2C29F159EFD7 /* [CP] Copy Pods Resources */,
 			);
 			buildRules = (
 			);
@@ -5472,7 +5409,6 @@
 				E45755A51E0BA72E00EC44F4 /* qrcode_scan_light_green@2x.png in Resources */,
 				E46E6CB21DD41F5D00AB7561 /* ZSSunorderedlist@2x.png in Resources */,
 				E457559F1E0BA72E00EC44F4 /* qrcode_scan_btn_myqrcode_down@2x.png in Resources */,
-				E428AF6320ADA06600D964B9 /* mapapi.bundle in Resources */,
 				E46E6C731DD41F5D00AB7561 /* ZSSforcejustify.png in Resources */,
 				E4B888D91D9D48F1002E1A46 /* 004.jpg in Resources */,
 				E4F45447208EC359002FBC32 /* OOPersonsViewController.xib in Resources */,
@@ -5636,13 +5572,31 @@
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXShellScriptBuildPhase section */
+		1B32B645931F2C29F159EFD7 /* [CP] Copy Pods Resources */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-O2Platform/Pods-O2Platform-resources.sh",
+				"${PODS_ROOT}/BaiduMapKit/BaiduMapKit/BaiduMapAPI_Map.framework/mapapi.bundle",
+			);
+			name = "[CP] Copy Pods Resources";
+			outputPaths = (
+				"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/mapapi.bundle",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-O2Platform/Pods-O2Platform-resources.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
 		50A3AB6E115C21464BEE9779 /* [CP] Embed Pods Frameworks */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
 			);
 			inputPaths = (
-				"${SRCROOT}/Pods/Target Support Files/Pods-O2Platform/Pods-O2Platform-frameworks.sh",
+				"${PODS_ROOT}/Target Support Files/Pods-O2Platform/Pods-O2Platform-frameworks.sh",
 				"${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework",
 				"${BUILT_PRODUCTS_DIR}/AlamofireImage/AlamofireImage.framework",
 				"${BUILT_PRODUCTS_DIR}/AlamofireNetworkActivityIndicator/AlamofireNetworkActivityIndicator.framework",
@@ -5727,7 +5681,7 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 			shellPath = /bin/sh;
-			shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-O2Platform/Pods-O2Platform-frameworks.sh\"\n";
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-O2Platform/Pods-O2Platform-frameworks.sh\"\n";
 			showEnvVarsInLog = 0;
 		};
 		6CB43AA0AD5C9B1268F7531A /* [CP] Check Pods Manifest.lock */ = {
@@ -5937,6 +5891,7 @@
 				E41512631ED80AF3006531B0 /* ZoneNavigationBarManager.swift in Sources */,
 				E4B888771D9D48F1002E1A46 /* Group.swift in Sources */,
 				E4B888911D9D48F1002E1A46 /* UIViewExt.m in Sources */,
+				B1BA42B824133AC40081CED8 /* TaskCreateData.swift in Sources */,
 				09E02E871F16319600579887 /* DiskCache.swift in Sources */,
 				B1DE856623603408003C36E2 /* DispatchQueue+Extension.swift in Sources */,
 				E4B69772207630240062F6E8 /* String+Extenstion.swift in Sources */,
@@ -6664,14 +6619,12 @@
 				CODE_SIGN_IDENTITY = "iPhone Developer";
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				CODE_SIGN_STYLE = Manual;
-				CURRENT_PROJECT_VERSION = 64;
+				CURRENT_PROJECT_VERSION = 66;
 				DEVELOPMENT_TEAM = NTDRU2P6T4;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/O2Platform/framework",
-					"$(PROJECT_DIR)/O2Platform/framework/JPUSH",
-					"$(PROJECT_DIR)/O2Platform/framework/BaiduMap",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_SDK_Dependents/Moya",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_SDK_Dependents/Alamofire",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_SDK_Dependents/Result",
@@ -6682,7 +6635,6 @@
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_Flutter_SDK",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_Flutter_SDK/plugins",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_Flutter_SDK/engine",
-					"$(PROJECT_DIR)/O2Platform/Framework/JPUSH",
 				);
 				GCC_PREPROCESSOR_DEFINITIONS = (
 					"$(inherited)",
@@ -6695,14 +6647,11 @@
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)",
-					"$(PROJECT_DIR)/O2Platform/framework/JPUSH",
 					"$(PROJECT_DIR)/O2Platform/framework/thirdlibs",
-					"$(PROJECT_DIR)/O2Platform/framework/MGFaceppSDK",
 					"$(PROJECT_DIR)/O2Platform/framework/MegviiLicMgr-iOS-SDK",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_Flutter_SDK",
-					"$(PROJECT_DIR)/O2Platform/Framework/JPUSH",
 				);
-				MARKETING_VERSION = 5.0.4;
+				MARKETING_VERSION = 5.0.6;
 				OTHER_LDFLAGS = (
 					"$(inherited)",
 					"-ObjC",
@@ -6784,14 +6733,12 @@
 				CODE_SIGN_IDENTITY = "iPhone Distribution";
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
 				CODE_SIGN_STYLE = Manual;
-				CURRENT_PROJECT_VERSION = 64;
+				CURRENT_PROJECT_VERSION = 66;
 				DEVELOPMENT_TEAM = NTDRU2P6T4;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)/O2Platform/framework",
-					"$(PROJECT_DIR)/O2Platform/framework/JPUSH",
-					"$(PROJECT_DIR)/O2Platform/framework/BaiduMap",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_SDK_Dependents/Moya",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_SDK_Dependents/Alamofire",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_SDK_Dependents/Result",
@@ -6802,7 +6749,6 @@
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_Flutter_SDK",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_Flutter_SDK/plugins",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_Flutter_SDK/engine",
-					"$(PROJECT_DIR)/O2Platform/Framework/JPUSH",
 				);
 				GCC_PREPROCESSOR_DEFINITIONS = (
 					"$(inherited)",
@@ -6815,14 +6761,11 @@
 				LIBRARY_SEARCH_PATHS = (
 					"$(inherited)",
 					"$(PROJECT_DIR)",
-					"$(PROJECT_DIR)/O2Platform/framework/JPUSH",
 					"$(PROJECT_DIR)/O2Platform/framework/thirdlibs",
-					"$(PROJECT_DIR)/O2Platform/framework/MGFaceppSDK",
 					"$(PROJECT_DIR)/O2Platform/framework/MegviiLicMgr-iOS-SDK",
 					"$(PROJECT_DIR)/O2Platform/framework/O2OA_Flutter_SDK",
-					"$(PROJECT_DIR)/O2Platform/Framework/JPUSH",
 				);
-				MARKETING_VERSION = 5.0.4;
+				MARKETING_VERSION = 5.0.6;
 				OTHER_LDFLAGS = (
 					"$(inherited)",
 					"-ObjC",

+ 6 - 1
o2ios/O2Platform.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@@ -1,5 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
-<dict/>
+<dict>
+	<key>BuildSystemType</key>
+	<string>Original</string>
+	<key>PreviewsEnabled</key>
+	<false/>
+</dict>
 </plist>

+ 21 - 4
o2ios/O2Platform/App/Cms-内容管理/c/CMSCategoryListViewController.swift

@@ -110,6 +110,11 @@ class CMSCategoryListViewController: UIViewController {
             destVC.fromCreateDocVC = true
         }else if segue.identifier == "createDocument" {
             let createVC = segue.destination as! CMSCreateDocViewController
+            if let configJson = self.cmsData?.config, !configJson.isEmpty {
+                if let config = try? CMSAppConfig(configJson) {
+                    createVC.config = config
+                }
+            }
             createVC.category = self.selectedCategory
         }
     }
@@ -256,11 +261,23 @@ class CMSCategoryListViewController: UIViewController {
             switch response.result {
             case .success(let val):
                 DDLogDebug(JSON(val).description)
-                let res = Mapper<CMSCategory>().map(JSONObject: val)
-                if let docList = res?.data, docList.count > 0 {
-                    self.performSegue(withIdentifier: "showDetailContentSegue", sender: docList[0])
-                }else {
+                var needLatest = false
+                if let configJson = self.cmsData?.config, !configJson.isEmpty {
+                    if let config = try? CMSAppConfig(configJson) {
+                        if let latest = config.latest, latest == false {
+                            needLatest = true
+                        }
+                    }
+                }
+                if needLatest {
                     self.gotoNewDocController()
+                }else {
+                    let res = Mapper<CMSCategory>().map(JSONObject: val)
+                    if let docList = res?.data, docList.count > 0 {
+                        self.performSegue(withIdentifier: "showDetailContentSegue", sender: docList[0])
+                    }else {
+                        self.gotoNewDocController()
+                    }
                 }
             case .failure(let err):
                 DDLogError(err.localizedDescription)

+ 53 - 7
o2ios/O2Platform/App/Cms-内容管理/c/CMSCreateDocViewController.swift

@@ -19,6 +19,7 @@ import CocoaLumberjack
 class CMSCreateDocViewController: FormViewController {
 
     var category: CMSWrapOutCategoryList?
+    var config: CMSAppConfig?
     
     var  identityList:[IdentityV2] = []
     
@@ -47,6 +48,10 @@ class CMSCreateDocViewController: FormViewController {
             
         }
         title = self.category?.categoryName
+        
+        if let ignoreTitle = self.config?.ignoreTitle, ignoreTitle == true {
+            self.showLoading()
+        }
         loadDepartAndIdentity()
     }
     
@@ -62,7 +67,12 @@ class CMSCreateDocViewController: FormViewController {
                     self.identityList = identities
                 }
                 DispatchQueue.main.async {
-                    self.showInputUI()
+                    if let ignoreTitle = self.config?.ignoreTitle, ignoreTitle == true, self.identityList.count == 1 {
+                       self.createDocument("", self.identityList[0].distinguishedName!)
+                    }else {
+                        self.hideLoading()
+                        self.showInputUI()
+                    }
                 }
             case .failure(let err):
                 DDLogError(err.localizedDescription)
@@ -78,6 +88,13 @@ class CMSCreateDocViewController: FormViewController {
     func showInputUI(){
         form +++ Section("创建文档")
             <<< TextRow("title") {row in
+                row.hidden = Condition.function(["title"], { form in
+                    if let ignoreTitle = self.config?.ignoreTitle, ignoreTitle == true {
+                        return true
+                    }else {
+                        return false
+                    }
+                })
                 row.title = "文档标题"
                 row.placeholder = "请输入文档标题"
                 }.cellSetup({ (cell, row) in
@@ -94,16 +111,22 @@ class CMSCreateDocViewController: FormViewController {
                 }.cellSetup({ (cell, row) in
                     //cell.height = 50
                 })
-            
+        
             +++ Section()
             <<< ButtonRow("createButton") { (row:ButtonRow) in
                 row.title = "创建"
                 }.onCellSelection({ (cell, row) in
                     let titleRow:TextRow = self.form.rowBy(tag:"title")!
                     let identityRow:ActionSheetRow<IdentityV2> = self.form.rowBy(tag:"selectedIdentity")!
-                    guard let title = titleRow.value else{
-                        self.showError(title: "请输入标题")
-                        return
+                    var title = ""
+                    if let ignoreTitle = self.config?.ignoreTitle, ignoreTitle == true {
+                        title = ""
+                    }else {
+                        guard let ctitle = titleRow.value else{
+                            self.showError(title: "请输入标题")
+                            return
+                        }
+                        title = ctitle
                     }
                     guard let id = identityRow.value  else {
                         self.showError(title: "请选择身份")
@@ -111,6 +134,7 @@ class CMSCreateDocViewController: FormViewController {
                     }
                     self.createDocument(title, id.distinguishedName!)
                 })
+        
     }
     
     func createDocument(_ title: String, _ identity: String) {
@@ -159,11 +183,33 @@ class CMSCreateDocViewController: FormViewController {
     
     func createProcess(_ title:String,identity:String, processId: String){
         DDLogDebug("title = \(title),identity = \(identity)")
-        let bean = CreateProcessBean()
+        let bean = CreateProcessCmsBean()
         bean.title = title
         bean.identity = identity
+        let data = CmsDocData()
+        let categoryId = self.category!.id!
+        let appId = self.category!.appId!
+        data.title = title
+        data.creatorIdentity = identity
+        data.isNewDocument = true
+        data.appId = appId
+        data.categoryId = categoryId
+        data.docStatus = "draft"
+        data.createTime = Date().toString("yyyy-MM-dd HH:mm:ss")
+        data.categoryName = self.category!.categoryName
+        data.categoryAlias = self.category!.categoryAlias
+        let d = CreateProcessCmsData()
+        d.cmsDocument = data
+        bean.data = d
+        
+        
+        
         let createURL = AppDelegate.o2Collect.generateURLWithAppContextKey(WorkContext.workContextKey, query: WorkContext.workCreateQuery, parameter: ["##id##":processId as AnyObject])
-        self.showLoading(title: "创建中,请稍候...")
+        if let ignoreTitle = self.config?.ignoreTitle, ignoreTitle == true, self.identityList.count == 1 {
+           
+        }else {
+            self.showLoading(title: "创建中,请稍候...")
+        }
         Alamofire.request(createURL!,method:.post, parameters: bean.toJSON(), encoding: JSONEncoding.default, headers: nil).responseJSON { response in
             debugPrint(response.result)
             switch response.result {

+ 2 - 0
o2ios/O2Platform/App/Cms-内容管理/m/CMSCategory.swift

@@ -6,6 +6,8 @@ import Foundation
 import ObjectMapper
 
 
+
+
 class CMSCategory : NSObject, NSCoding, Mappable{
 
 	var count : Int?

+ 170 - 117
o2ios/O2Platform/App/Cms-内容管理/m/CMSData.swift

@@ -2,137 +2,190 @@
 //	CMSData.swift
 //	Model file generated using JSONExport: https://github.com/Ahmed-Ali/JSONExport
 
-import Foundation 
+import Foundation
 import ObjectMapper
 
+// MARK: - Helper functions for creating encoders and decoders
+func newJSONDecoder() -> JSONDecoder {
+   let decoder = JSONDecoder()
+   if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
+       decoder.dateDecodingStrategy = .iso8601
+   }
+   return decoder
+}
+
+func newJSONEncoder() -> JSONEncoder {
+   let encoder = JSONEncoder()
+   if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
+       encoder.dateEncodingStrategy = .iso8601
+   }
+   return encoder
+}
+
+//cms 栏目的配置文件 存在CMSData栏目对象中的config这个字读啊
+struct CMSAppConfig: Codable {
+
+    let ignoreTitle: Bool? //是否要填写文档标题
+    let latest: Bool? //是否忽略草稿
+
+    init(_ json: String, using encoding: String.Encoding = .utf8) throws {
+        guard let data = json.data(using: encoding) else {
+            throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
+        }
+        try self.init(data: data)
+    }
+    init(data: Data) throws {
+        self = try newJSONDecoder().decode(CMSAppConfig.self, from: data)
+    }
+    init(fromURL url: URL) throws {
+        try self.init(data: try Data(contentsOf: url))
+    }
+
+
+    func jsonData() throws -> Data {
+        return try newJSONEncoder().encode(self)
+    }
+
+    func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
+        return String(data: try self.jsonData(), encoding: encoding)
+    }
 
-class CMSData : NSObject, NSCoding, Mappable{
-
-	var appAlias : String?
-	var appIcon : String?
-	var appInfoSeq : String?
-	var appName : String?
-	var categoryList : [AnyObject]?
-	var createTime : String?
-	var creatorCompany : String?
-	var creatorDepartment : String?
-	var creatorIdentity : String?
-	var creatorPerson : String?
-	var descriptionField : String?
-	var distributeFactor : Int?
-	var id : String?
-	var sequence : String?
-	var updateTime : String?
-	var wrapOutCategoryList : [CMSWrapOutCategoryList]?
-
-
-	class func newInstance(map: Map) -> Mappable?{
-		return CMSData()
-	}
-	required init?(map: Map){}
-	private override init(){}
-
-	func mapping(map: Map)
-	{
-		appAlias <- map["appAlias"]
-		appIcon <- map["appIcon"]
-		appInfoSeq <- map["appInfoSeq"]
-		appName <- map["appName"]
-		categoryList <- map["categoryList"]
-		createTime <- map["createTime"]
-		creatorCompany <- map["creatorCompany"]
-		creatorDepartment <- map["creatorDepartment"]
-		creatorIdentity <- map["creatorIdentity"]
-		creatorPerson <- map["creatorPerson"]
-		descriptionField <- map["description"]
-		distributeFactor <- map["distributeFactor"]
-		id <- map["id"]
-		sequence <- map["sequence"]
-		updateTime <- map["updateTime"]
-		wrapOutCategoryList <- map["wrapOutCategoryList"]
-		
-	}
+   
+}
+
+class CMSData: NSObject, NSCoding, Mappable {
+
+    var appAlias: String?
+    var appIcon: String?
+    var appInfoSeq: String?
+    var appName: String?
+    var categoryList: [AnyObject]?
+    var createTime: String?
+    var creatorCompany: String?
+    var creatorDepartment: String?
+    var creatorIdentity: String?
+    var creatorPerson: String?
+    var descriptionField: String?
+    var distributeFactor: Int?
+    var id: String?
+    var sequence: String?
+    var updateTime: String?
+    var config: String? //配置参数用的 json字符串
+    var wrapOutCategoryList: [CMSWrapOutCategoryList]?
+
+
+    class func newInstance(map: Map) -> Mappable? {
+        return CMSData()
+    }
+    required init?(map: Map) { }
+    private override init() { }
+
+    func mapping(map: Map)
+    {
+        appAlias <- map["appAlias"]
+        appIcon <- map["appIcon"]
+        appInfoSeq <- map["appInfoSeq"]
+        appName <- map["appName"]
+        categoryList <- map["categoryList"]
+        createTime <- map["createTime"]
+        creatorCompany <- map["creatorCompany"]
+        creatorDepartment <- map["creatorDepartment"]
+        creatorIdentity <- map["creatorIdentity"]
+        creatorPerson <- map["creatorPerson"]
+        descriptionField <- map["description"]
+        distributeFactor <- map["distributeFactor"]
+        id <- map["id"]
+        sequence <- map["sequence"]
+        updateTime <- map["updateTime"]
+        config <- map["config"]
+        wrapOutCategoryList <- map["wrapOutCategoryList"]
+
+    }
 
     /**
     * NSCoding required initializer.
     * Fills the data from the passed decoder
     */
     @objc required init(coder aDecoder: NSCoder)
-	{
-         appAlias = aDecoder.decodeObject(forKey: "appAlias") as? String
-         appIcon = aDecoder.decodeObject(forKey: "appIcon") as? String
-         appInfoSeq = aDecoder.decodeObject(forKey: "appInfoSeq") as? String
-         appName = aDecoder.decodeObject(forKey: "appName") as? String
-         categoryList = aDecoder.decodeObject(forKey: "categoryList") as? [AnyObject]
-         createTime = aDecoder.decodeObject(forKey: "createTime") as? String
-         creatorCompany = aDecoder.decodeObject(forKey: "creatorCompany") as? String
-         creatorDepartment = aDecoder.decodeObject(forKey: "creatorDepartment") as? String
-         creatorIdentity = aDecoder.decodeObject(forKey: "creatorIdentity") as? String
-         creatorPerson = aDecoder.decodeObject(forKey: "creatorPerson") as? String
-         descriptionField = aDecoder.decodeObject(forKey: "description") as? String
-         distributeFactor = aDecoder.decodeObject(forKey: "distributeFactor") as? Int
-         id = aDecoder.decodeObject(forKey: "id") as? String
-         sequence = aDecoder.decodeObject(forKey: "sequence") as? String
-         updateTime = aDecoder.decodeObject(forKey: "updateTime") as? String
-         wrapOutCategoryList = aDecoder.decodeObject(forKey: "wrapOutCategoryList") as? [CMSWrapOutCategoryList]
-
-	}
+    {
+        appAlias = aDecoder.decodeObject(forKey: "appAlias") as? String
+        appIcon = aDecoder.decodeObject(forKey: "appIcon") as? String
+        appInfoSeq = aDecoder.decodeObject(forKey: "appInfoSeq") as? String
+        appName = aDecoder.decodeObject(forKey: "appName") as? String
+        categoryList = aDecoder.decodeObject(forKey: "categoryList") as? [AnyObject]
+        createTime = aDecoder.decodeObject(forKey: "createTime") as? String
+        creatorCompany = aDecoder.decodeObject(forKey: "creatorCompany") as? String
+        creatorDepartment = aDecoder.decodeObject(forKey: "creatorDepartment") as? String
+        creatorIdentity = aDecoder.decodeObject(forKey: "creatorIdentity") as? String
+        creatorPerson = aDecoder.decodeObject(forKey: "creatorPerson") as? String
+        descriptionField = aDecoder.decodeObject(forKey: "description") as? String
+        distributeFactor = aDecoder.decodeObject(forKey: "distributeFactor") as? Int
+        id = aDecoder.decodeObject(forKey: "id") as? String
+        sequence = aDecoder.decodeObject(forKey: "sequence") as? String
+        updateTime = aDecoder.decodeObject(forKey: "updateTime") as? String
+        config = aDecoder.decodeObject(forKey: "config") as? String
+        wrapOutCategoryList = aDecoder.decodeObject(forKey: "wrapOutCategoryList") as? [CMSWrapOutCategoryList]
+
+    }
 
     /**
     * NSCoding required method.
     * Encodes mode properties into the decoder
     */
     @objc func encode(with aCoder: NSCoder)
-	{
-		if appAlias != nil{
-			aCoder.encode(appAlias, forKey: "appAlias")
-		}
-		if appIcon != nil{
-			aCoder.encode(appIcon, forKey: "appIcon")
-		}
-		if appInfoSeq != nil{
-			aCoder.encode(appInfoSeq, forKey: "appInfoSeq")
-		}
-		if appName != nil{
-			aCoder.encode(appName, forKey: "appName")
-		}
-		if categoryList != nil{
-			aCoder.encode(categoryList, forKey: "categoryList")
-		}
-		if createTime != nil{
-			aCoder.encode(createTime, forKey: "createTime")
-		}
-		if creatorCompany != nil{
-			aCoder.encode(creatorCompany, forKey: "creatorCompany")
-		}
-		if creatorDepartment != nil{
-			aCoder.encode(creatorDepartment, forKey: "creatorDepartment")
-		}
-		if creatorIdentity != nil{
-			aCoder.encode(creatorIdentity, forKey: "creatorIdentity")
-		}
-		if creatorPerson != nil{
-			aCoder.encode(creatorPerson, forKey: "creatorPerson")
-		}
-		if descriptionField != nil{
-			aCoder.encode(descriptionField, forKey: "description")
-		}
-		if distributeFactor != nil{
-			aCoder.encode(distributeFactor, forKey: "distributeFactor")
-		}
-		if id != nil{
-			aCoder.encode(id, forKey: "id")
-		}
-		if sequence != nil{
-			aCoder.encode(sequence, forKey: "sequence")
-		}
-		if updateTime != nil{
-			aCoder.encode(updateTime, forKey: "updateTime")
-		}
-		if wrapOutCategoryList != nil{
-			aCoder.encode(wrapOutCategoryList, forKey: "wrapOutCategoryList")
-		}
-
-	}
+    {
+        if appAlias != nil {
+            aCoder.encode(appAlias, forKey: "appAlias")
+        }
+        if appIcon != nil {
+            aCoder.encode(appIcon, forKey: "appIcon")
+        }
+        if appInfoSeq != nil {
+            aCoder.encode(appInfoSeq, forKey: "appInfoSeq")
+        }
+        if appName != nil {
+            aCoder.encode(appName, forKey: "appName")
+        }
+        if categoryList != nil {
+            aCoder.encode(categoryList, forKey: "categoryList")
+        }
+        if createTime != nil {
+            aCoder.encode(createTime, forKey: "createTime")
+        }
+        if creatorCompany != nil {
+            aCoder.encode(creatorCompany, forKey: "creatorCompany")
+        }
+        if creatorDepartment != nil {
+            aCoder.encode(creatorDepartment, forKey: "creatorDepartment")
+        }
+        if creatorIdentity != nil {
+            aCoder.encode(creatorIdentity, forKey: "creatorIdentity")
+        }
+        if creatorPerson != nil {
+            aCoder.encode(creatorPerson, forKey: "creatorPerson")
+        }
+        if descriptionField != nil {
+            aCoder.encode(descriptionField, forKey: "description")
+        }
+        if distributeFactor != nil {
+            aCoder.encode(distributeFactor, forKey: "distributeFactor")
+        }
+        if id != nil {
+            aCoder.encode(id, forKey: "id")
+        }
+        if sequence != nil {
+            aCoder.encode(sequence, forKey: "sequence")
+        }
+        if updateTime != nil {
+            aCoder.encode(updateTime, forKey: "updateTime")
+        }
+        if config != nil {
+            aCoder.encode(config, forKey: "config")
+        }
+        if wrapOutCategoryList != nil {
+            aCoder.encode(wrapOutCategoryList, forKey: "wrapOutCategoryList")
+        }
+
+    }
 
 }

+ 10 - 5
o2ios/O2Platform/App/Login-绑定登录/c/LoginViewController.swift

@@ -21,6 +21,7 @@ class LoginViewController: UIViewController {
     
     @IBOutlet weak var iconImageView: UIImageView!
     @IBOutlet weak var startImage: UIImageView!
+    var showView = 0
     
     var viewModel:OOLoginViewModel = {
         return OOLoginViewModel()
@@ -40,6 +41,8 @@ class LoginViewController: UIViewController {
             iconImageView.isHidden = false
         }
         self.startImage.image = UIImage(named: "startImage")
+        
+        
     }
     
     override func viewWillAppear(_ animated: Bool) {
@@ -49,16 +52,18 @@ class LoginViewController: UIViewController {
     
     override func viewDidAppear(_ animated: Bool) {
         super.viewDidAppear(animated)
-        
+        self.showView += 1
         if AppConfigSettings.shared.isFirstTime == true {
-            O2Logger.info("启动开始 isFirstTime is true")
+            DDLogDebug("启动开始 isFirstTime is true")
             AppConfigSettings.shared.isFirstTime = false
             let pVC = OOGuidePageController(nibName: "OOGuidePageController", bundle: nil)
             //let navVC = ZLNavigationController(rootViewController: pVC)
             self.presentVC(pVC)
         }else{
-            O2Logger.info("启动开始 isFirstTime is false")
-            self.startFlowForPromise()
+            if self.showView == 1 {
+                DDLogDebug("启动开始 isFirstTime is false")
+                self.startFlowForPromise()
+            }
         }
     }
 
@@ -74,7 +79,7 @@ class LoginViewController: UIViewController {
                 let centerContext = o2Server?["centerContext"] as? String
                 let centerPort = o2Server?["centerPort"] as? Int
                 let httpProtocol = o2Server?["httpProtocol"] as? String
-                O2Logger.debug("连接服务器:\(String(describing: name)) , host:\(String(describing: centerHost)) , context:\(String(describing: centerContext)), port:\(centerPort ?? 0), portocal:\(String(describing: httpProtocol)) ")
+                DDLogDebug("连接服务器:\(String(describing: name)) , host:\(String(describing: centerHost)) , context:\(String(describing: centerContext)), port:\(centerPort ?? 0), portocal:\(String(describing: httpProtocol)) ")
                 if name == nil || centerHost == nil || centerContext == nil {
                     self.showError(title:  "服务器配置信息异常!")
                     return

+ 13 - 1
o2ios/O2Platform/App/Login-绑定登录/c/OOGuidePageController.swift

@@ -7,6 +7,7 @@
 //
 
 import UIKit
+import CocoaLumberjack
 
 class OOGuidePageController: UIViewController,UIScrollViewDelegate {
     
@@ -95,7 +96,18 @@ class OOGuidePageController: UIViewController,UIScrollViewDelegate {
     }
     
     @IBAction func startAppAction(_ sender: Any) {
-        self.dismiss(animated: true, completion: nil)
+        var login: LoginViewController?
+        if self.presentingViewController is LoginViewController {
+            DDLogDebug(" presenting is LoginViewController。")
+            login = self.presentingViewController as? LoginViewController
+        }
+        self.dismiss(animated: true, completion: {
+           DDLogDebug("关闭引导。。。。。。。。。。。。。。")
+            if let lo = login {
+                DDLogDebug(" 开始继续  LoginViewController。")
+                lo.startFlowForPromise()
+            }
+        })
     }
     
 

+ 12 - 19
o2ios/O2Platform/App/Login-绑定登录/login.storyboard

@@ -1,11 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="2FW-oB-Z7W">
-    <device id="retina4_7" orientation="portrait">
-        <adaptation id="fullscreen"/>
-    </device>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="2FW-oB-Z7W">
+    <device id="retina4_7" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <scenes>
@@ -29,7 +27,7 @@
                                 </connections>
                             </imageView>
                             <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="1bI-e6-ANg">
-                                <rect key="frame" x="147.5" y="90" width="80" height="80"/>
+                                <rect key="frame" x="147.5" y="70" width="80" height="80"/>
                                 <constraints>
                                     <constraint firstAttribute="height" constant="80" id="2zO-EG-G0T"/>
                                     <constraint firstAttribute="width" constant="80" id="uey-R9-gFc"/>
@@ -91,7 +89,6 @@
                                         <constraints>
                                             <constraint firstAttribute="height" constant="50" id="41I-dW-fmr"/>
                                         </constraints>
-                                        <nil key="textColor"/>
                                         <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                         <textInputTraits key="textInputTraits"/>
                                         <userDefinedRuntimeAttributes>
@@ -110,7 +107,6 @@
                                         <constraints>
                                             <constraint firstAttribute="height" constant="50" id="xvt-De-9D4"/>
                                         </constraints>
-                                        <nil key="textColor"/>
                                         <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                         <textInputTraits key="textInputTraits"/>
                                         <userDefinedRuntimeAttributes>
@@ -192,7 +188,7 @@
                                 <inset key="separatorInset" minX="15" minY="0.0" maxX="15" maxY="0.0"/>
                                 <prototypes>
                                     <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="OONodeUnitTableViewCell" id="sh0-GF-gvO" customClass="OONodeUnitTableViewCell" customModule="O2Platform" customModuleProvider="target">
-                                        <rect key="frame" x="0.0" y="28" width="375" height="44"/>
+                                        <rect key="frame" x="0.0" y="28" width="375" height="43.5"/>
                                         <autoresizingMask key="autoresizingMask"/>
                                         <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="sh0-GF-gvO" id="EcZ-57-FOC">
                                             <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@@ -272,21 +268,20 @@
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <subviews>
                             <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="pic_o2_moren1" highlightedImage="pic_o2_moren1" translatesAutoresizingMaskIntoConstraints="NO" id="wxO-Qg-4fw">
-                                <rect key="frame" x="140" y="80" width="95" height="95"/>
+                                <rect key="frame" x="140" y="60" width="95" height="95"/>
                                 <constraints>
                                     <constraint firstAttribute="width" constant="95" id="AFL-pU-hnM"/>
                                     <constraint firstAttribute="height" constant="95" id="ef6-XN-IYs"/>
                                 </constraints>
                             </imageView>
                             <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="VlL-pa-xtW">
-                                <rect key="frame" x="16" y="195" width="343" height="180"/>
+                                <rect key="frame" x="16" y="175" width="343" height="180"/>
                                 <subviews>
                                     <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="请输入用户名" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="lEi-tc-UYF" customClass="OOUITextField" customModule="O2Platform" customModuleProvider="target">
                                         <rect key="frame" x="0.0" y="0.0" width="343" height="50"/>
                                         <constraints>
                                             <constraint firstAttribute="height" constant="50" id="PAM-WB-VG7"/>
                                         </constraints>
-                                        <nil key="textColor"/>
                                         <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                         <textInputTraits key="textInputTraits"/>
                                         <userDefinedRuntimeAttributes>
@@ -305,7 +300,6 @@
                                         <constraints>
                                             <constraint firstAttribute="height" constant="50" id="YG6-dA-cc6"/>
                                         </constraints>
-                                        <nil key="textColor"/>
                                         <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                         <textInputTraits key="textInputTraits"/>
                                         <userDefinedRuntimeAttributes>
@@ -330,7 +324,6 @@
                                         <constraints>
                                             <constraint firstAttribute="height" constant="50" id="jfy-Ns-KfD"/>
                                         </constraints>
-                                        <nil key="textColor"/>
                                         <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                         <textInputTraits key="textInputTraits" textContentType="password"/>
                                         <userDefinedRuntimeAttributes>
@@ -380,7 +373,7 @@
                                 <nil key="highlightedColor"/>
                             </label>
                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sdH-Rd-Uu5">
-                                <rect key="frame" x="243" y="383" width="100" height="35"/>
+                                <rect key="frame" x="243" y="363" width="100" height="35"/>
                                 <constraints>
                                     <constraint firstAttribute="height" constant="35" id="McM-ZF-FyV"/>
                                     <constraint firstAttribute="width" constant="100" id="kFw-P8-2nn"/>
@@ -393,7 +386,7 @@
                                 </connections>
                             </button>
                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="VU6-Hu-1I4">
-                                <rect key="frame" x="32" y="383" width="100" height="35"/>
+                                <rect key="frame" x="32" y="363" width="100" height="35"/>
                                 <constraints>
                                     <constraint firstAttribute="height" constant="35" id="DyV-tz-NyM"/>
                                     <constraint firstAttribute="width" constant="100" id="xav-tr-QlL"/>
@@ -455,14 +448,14 @@
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <subviews>
                             <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="pic_o2_moren1" translatesAutoresizingMaskIntoConstraints="NO" id="8xt-gj-zyd">
-                                <rect key="frame" x="127.5" y="140" width="120" height="120"/>
+                                <rect key="frame" x="127.5" y="120" width="120" height="120"/>
                                 <constraints>
                                     <constraint firstAttribute="width" constant="120" id="7sa-Bj-Y2n"/>
                                     <constraint firstAttribute="height" constant="120" id="bqd-au-JQf"/>
                                 </constraints>
                             </imageView>
                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Q9Q-4z-Rfe" customClass="OOBaseUIButton" customModule="O2Platform" customModuleProvider="target">
-                                <rect key="frame" x="26" y="290" width="323" height="40"/>
+                                <rect key="frame" x="26" y="270" width="323" height="40"/>
                                 <color key="backgroundColor" red="0.98431372549999996" green="0.2784313725" blue="0.2784313725" alpha="1" colorSpace="calibratedRGB"/>
                                 <constraints>
                                     <constraint firstAttribute="height" constant="40" id="m5T-Za-xxO"/>
@@ -481,7 +474,7 @@
                                 </connections>
                             </button>
                             <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="kLj-hW-IW3">
-                                <rect key="frame" x="257" y="350" width="92" height="30"/>
+                                <rect key="frame" x="257" y="330" width="92" height="30"/>
                                 <state key="normal" title="其它方式登录"/>
                                 <connections>
                                     <segue destination="0kn-3s-0PH" kind="unwind" identifier="goBack2Login" unwindAction="unwindFromBioAuthLogin:" id="Yuv-L0-JEi"/>

+ 3 - 2
o2ios/O2Platform/App/NewAttance-考勤打卡/c/OOAttanceCheckInController.swift

@@ -8,6 +8,7 @@
 
 import UIKit
 import O2OA_Auth_SDK
+import CocoaLumberjack
 
 
 class OOAttanceCheckInController: UITableViewController {
@@ -92,7 +93,7 @@ class OOAttanceCheckInController: UITableViewController {
     }
     
     @objc private func locationReceive(_ notification:Notification){
-        if  let result = notification.object as?  BMKReverseGeoCodeResult {
+        if  let result = notification.object as?  BMKReverseGeoCodeSearchResult {
             checkinForm.recordAddress = result.address
             checkinForm.desc = result.sematicDescription
             checkinForm.longitude = String(result.location.longitude)
@@ -109,7 +110,7 @@ class OOAttanceCheckInController: UITableViewController {
             // button enable
             myButton.isEnabled = true
             headerView.addSubview(promptView)
-            O2Logger.debug("checkForm set completed")
+            DDLogDebug("checkForm set completed")
         }else{
             myButton.isEnabled = false
             promptView.removeSubviews()

+ 25 - 27
o2ios/O2Platform/App/NewAttance-考勤打卡/c/OOAttanceSettingController.swift

@@ -14,7 +14,7 @@ class OOAttanceSettingController: UIViewController {
     
     var mapView:BMKMapView!
     
-    var locService:BMKLocationService!
+    var locService:BMKLocationManager!
 
     var searchAddress:BMKGeoCodeSearch!
     
@@ -145,9 +145,9 @@ class OOAttanceSettingController: UIViewController {
         mapView.delegate = self
         //mapView.showsUserLocation = true
         //locService.desiredAccuracy = 100
-        locService = BMKLocationService()
+        locService = BMKLocationManager()
         locService.delegate = self
-        locService.startUserLocationService()
+        locService.startUpdatingLocation()
         searchAddress = BMKGeoCodeSearch()
         searchAddress.delegate = self
     }
@@ -175,7 +175,7 @@ class OOAttanceSettingController: UIViewController {
     deinit {
         mapView.delegate = nil
         locService.delegate = nil
-        locService.stopUserLocationService()
+        locService.stopUpdatingLocation()
         searchAddress.delegate = nil
     }
     
@@ -201,19 +201,19 @@ extension OOAttanceSettingController:BMKMapViewDelegate {
         }
        
         //反向查询具体地址名称
-        let re = BMKReverseGeoCodeOption()
-        re.reverseGeoPoint = coordinate
+        let re = BMKReverseGeoCodeSearchOption()
+        re.location = coordinate
         let flag = searchAddress.reverseGeoCode(re)
-        O2Logger.debug("searchAddress \(flag)")
+        DDLogDebug("searchAddress \(flag)")
     }
     
     
     func mapView(_ mapView: BMKMapView!, onClickedMapPoi mapPoi: BMKMapPoi!) {
-        let re = BMKReverseGeoCodeOption()
+        let re = BMKReverseGeoCodeSearchOption()
         let coordinate = mapPoi.pt
-        re.reverseGeoPoint = coordinate
+        re.location = coordinate
         let flag = searchAddress.reverseGeoCode(re)
-        O2Logger.debug("searchAddress \(flag)")
+        DDLogDebug("searchAddress \(flag)")
         
     }
     
@@ -222,43 +222,41 @@ extension OOAttanceSettingController:BMKMapViewDelegate {
     }
 }
 
-extension OOAttanceSettingController:BMKLocationServiceDelegate {
+extension OOAttanceSettingController:BMKLocationManagerDelegate {
     
     func willStartLocatingUser() {
-        O2Logger.debug("willStartLocatingUser")
+        DDLogDebug("willStartLocatingUser")
         MBProgressHUD_JChat.showMessage(message:"正在定位中,请稍候", toView: self.mapView)
     }
     
     func didUpdate(_ userLocation: BMKUserLocation!) {
-        O2Logger.debug("当前位置,\(userLocation.location.coordinate.latitude),\(userLocation.location.coordinate.longitude)")
+        DDLogDebug("当前位置,\(userLocation.location.coordinate.latitude),\(userLocation.location.coordinate.longitude)")
         mapView.updateLocationData(userLocation)
         mapView.centerCoordinate = userLocation.location.coordinate
         //定位完成停止定位
-        locService.stopUserLocationService()
+        locService.stopUpdatingLocation()
     }
     
     func didStopLocatingUser() {
-        O2Logger.debug("didStopLocatingUser")
+        
         MBProgressHUD_JChat.hide(forView: self.mapView, animated: true)
     }
 }
 
 extension OOAttanceSettingController:BMKGeoCodeSearchDelegate {
     
-    func onGetReverseGeoCodeResult(_ searcher: BMKGeoCodeSearch!, result: BMKReverseGeoCodeResult!, errorCode error: BMKSearchErrorCode) {
+    func onGetReverseGeoCodeResult(_ searcher: BMKGeoCodeSearch!, result: BMKReverseGeoCodeSearchResult!, errorCode error: BMKSearchErrorCode) {
         dataView.workPlaceNameTextField.text = result.address
         dataView.workAliasNameTextField.text = result.sematicDescription
-        for item in result.poiList {
-            let m = item as! BMKPoiInfo
-            print(m.name)            ///<POI名称
-            print(m.uid)
-            print(m.address)      ///<POI地址
-            print(m.city)        ///<POI所在城市
-            print(m.phone)        ///<POI电话号码
-            print(m.postcode)        ///<POI邮编
-            print(m.epoitype) ///<POI类型,0:普通点 1:公交站 2:公交线路 3:地铁站 4:地铁线路
-            print(m.pt.longitude,m.pt.latitude)    ///<POI坐标
-        }
+//        for item in result.poiList {
+//            let m = item as! BMKPoiInfo
+//            print(m.name)            ///<POI名称
+//            print(m.uid)
+//            print(m.address)      ///<POI地址
+//            print(m.city)        ///<POI所在城市
+//            print(m.phone)        ///<POI电话号码
+//            print(m.pt.longitude,m.pt.latitude)    ///<POI坐标
+//        }
         //设置settingBean
         settingBean.placeName = result.address
         settingBean.placeAlias = result.sematicDescription

+ 1 - 1
o2ios/O2Platform/App/NewAttance-考勤打卡/c/OONewAttanceNavController.swift

@@ -20,7 +20,7 @@ class OONewAttanceNavController: ZLNavigationController {
     }
     
     @objc func closeWindow() {
-        O2Logger.debug("======closeWindow")
+        
     }
 
 }

+ 42 - 24
o2ios/O2Platform/App/NewAttance-考勤打卡/v/OOAttanceHeaderView.swift

@@ -7,6 +7,7 @@
 //
 
 import UIKit
+import CocoaLumberjack
 
 class OOAttanceHeaderView: UIView {
     
@@ -14,7 +15,7 @@ class OOAttanceHeaderView: UIView {
     
     var userLocation:BMKUserLocation!
     
-    var locService:BMKLocationService!
+    var locService: BMKLocationManager!
     
     var searchAddress:BMKGeoCodeSearch!
     
@@ -52,10 +53,21 @@ class OOAttanceHeaderView: UIView {
         self.addSubview(mapView)
         
         
-        locService = BMKLocationService()
-        locService.desiredAccuracy = kCLLocationAccuracyBestForNavigation
+        locService =  BMKLocationManager()
+        locService.desiredAccuracy = kCLLocationAccuracyBest
+        //设置返回位置的坐标系类型
+        locService.coordinateType = .BMK09LL
+        //设置距离过滤参数
+        locService.distanceFilter = kCLDistanceFilterNone;
+        //设置预期精度参数
+        locService.desiredAccuracy = kCLLocationAccuracyBest;
+        //设置应用位置类型
+        locService.activityType = .automotiveNavigation
+        //设置是否自动停止位置更新
+        locService.pausesLocationUpdatesAutomatically = false
+        
         locService.delegate = self
-        locService.startUserLocationService()
+        locService.startUpdatingLocation()
       
         searchAddress = BMKGeoCodeSearch()
         searchAddress.delegate = self
@@ -95,7 +107,7 @@ class OOAttanceHeaderView: UIView {
         let annotation = BMKPointAnnotation()
         let longitude  = Double((workPlace.longitude)!)
         let latitude  = Double((workPlace.latitude)!)
-        O2Logger.debug("placeAlias=\(workPlace.placeAlias ?? ""),longitude=\(longitude),latitude=\(latitude)")
+        DDLogDebug("placeAlias=\(workPlace.placeAlias ?? ""),longitude=\(longitude),latitude=\(latitude)")
         annotation.coordinate = CLLocationCoordinate2DMake(latitude!,longitude!);
         annotation.title = workPlace.placeAlias ?? ""
         annotation.subtitle = workPlace.placeName ?? ""
@@ -103,7 +115,7 @@ class OOAttanceHeaderView: UIView {
     }
     
     func stopBMKMapViewService() {
-        locService.stopUserLocationService()
+        locService.stopUpdatingLocation()
         locService.delegate = nil
         searchAddress.delegate = nil
         mapView.delegate = nil
@@ -128,32 +140,38 @@ extension OOAttanceHeaderView:BMKMapViewDelegate {
     }
 
     func mapViewDidFinishLoading(_ mapView: BMKMapView!) {
-        O2Logger.debug("mapViewDidFinishLoading")
+        DDLogDebug("mapViewDidFinishLoading")
     }
     
     func mapViewDidFinishRendering(_ mapView: BMKMapView!) {
-        O2Logger.debug("mapViewDidFinishRendering")
+        DDLogDebug("mapViewDidFinishRendering")
     }
 }
 
-extension OOAttanceHeaderView:BMKLocationServiceDelegate {
-    
-    
-    func didUpdate(_ userLocation: BMKUserLocation!) {
-        O2Logger.debug("当前位置,\(userLocation.location.coordinate.latitude),\(userLocation.location.coordinate.longitude)")
-        mapView.updateLocationData(userLocation)
-        mapView.centerCoordinate = userLocation.location.coordinate
-        //搜索到指定的地点
-        let re = BMKReverseGeoCodeOption()
-        re.reverseGeoPoint = userLocation.location.coordinate
-        let _ = searchAddress.reverseGeoCode(re)
-
+extension OOAttanceHeaderView: BMKLocationManagerDelegate {
+    
+    func bmkLocationManager(_ manager: BMKLocationManager, didUpdate location: BMKLocation?, orError error: Error?) {
+        if let loc = location?.location {
+            DDLogDebug("当前位置,\(loc.coordinate.latitude),\(loc.coordinate.longitude)")
+            let user = BMKUserLocation()
+            user.location = loc
+            mapView.updateLocationData(user)
+            mapView.centerCoordinate = CLLocationCoordinate2D(latitude: loc.coordinate.latitude, longitude: loc.coordinate.longitude)
+            //搜索到指定的地点
+            let re = BMKReverseGeoCodeSearchOption()
+            re.location = CLLocationCoordinate2D(latitude: loc.coordinate.latitude, longitude: loc.coordinate.longitude)
+            let _ = searchAddress.reverseGeoCode(re)
+        }else {
+            DDLogError("没有获取到定位信息!!!!!")
+        }
     }
+    
+   
 }
 
 extension OOAttanceHeaderView:BMKGeoCodeSearchDelegate {
     
-    func onGetReverseGeoCodeResult(_ searcher: BMKGeoCodeSearch?, result: BMKReverseGeoCodeResult?, errorCode error: BMKSearchErrorCode) {
+    func onGetReverseGeoCodeResult(_ searcher: BMKGeoCodeSearch?, result: BMKReverseGeoCodeSearchResult?, errorCode error: BMKSearchErrorCode) {
         //发送定位的实时位置及名称信息
         if let location = result?.location, calcErrorRange(location) == true {
             NotificationCenter.post(customeNotification: .location, object: result)
@@ -162,11 +180,11 @@ extension OOAttanceHeaderView:BMKGeoCodeSearchDelegate {
         }
     }
     
-    func onGetGeoCodeResult(_ searcher: BMKGeoCodeSearch!, result: BMKGeoCodeResult!, errorCode error: BMKSearchErrorCode) {
+    func onGetGeoCodeResult(_ searcher: BMKGeoCodeSearch!, result: BMKGeoCodeSearchResult!, errorCode error: BMKSearchErrorCode) {
         if Int(error.rawValue) == 0 {
-            O2Logger.debug("result \(String(describing: result.address))")
+            DDLogDebug("result \(String(describing: result))")
         }else{
-            O2Logger.debug("result error  errorCode = \(Int(error.rawValue))")
+            DDLogDebug("result error  errorCode = \(Int(error.rawValue))")
         }
         
     }

+ 1 - 1
o2ios/O2Platform/App/NewAttance-考勤打卡/v/OOAttandanceSettingDataView.swift

@@ -55,7 +55,7 @@ class OOAttandanceSettingDataView: UIView {
     }
     
     @objc private func submitClicked(_ sender:Any?){
-        O2Logger.debug("submitClicked")
+        DDLogDebug("submitClicked")
         superview?.endEditing(true)
         let someValues = (workPlaceNameTextField.text!,workAliasNameTextField.text!,checkErrorRangeTextField.text!)
         NotificationCenter.post(customeNotification: .newWorkPlace, object: someValues)

+ 34 - 39
o2ios/O2Platform/App/Work-工作/c/TaskCreateViewController.swift

@@ -65,40 +65,39 @@ class TaskCreateViewController: FormViewController {
             
         }
         title = process?.name
-        loadDepartAndIdentity()
-       // showInputUI()
+        showInputUI()
         
         
     }
     
-    func loadDepartAndIdentity(){
-        let url = AppDelegate.o2Collect.generateURLWithAppContextKey(TaskContext.taskContextKey, query: TaskContext.todoCreateAvaiableIdentityByIdQuery, parameter: ["##processId##":process?.id as AnyObject])
-        Alamofire.request(url!).responseArray(keyPath:"data") { (response:DataResponse<[IdentityV2]>) in
-            switch response.result {
-            case .success(let identitys):
-                self.identitys = identitys
-                DispatchQueue.main.async {
-                    self.showInputUI()
-                }
-                
-            case .failure(let err):
-                DDLogError(err.localizedDescription)
-                DispatchQueue.main.async {
-                    self.showError(title: "读取身份列表失败")
-                }
-            }
-        }
-     
-    }
+//    func loadDepartAndIdentity(){
+//        let url = AppDelegate.o2Collect.generateURLWithAppContextKey(TaskContext.taskContextKey, query: TaskContext.todoCreateAvaiableIdentityByIdQuery, parameter: ["##processId##":process?.id as AnyObject])
+//        Alamofire.request(url!).responseArray(keyPath:"data") { (response:DataResponse<[IdentityV2]>) in
+//            switch response.result {
+//            case .success(let identitys):
+//                self.identitys = identitys
+//                DispatchQueue.main.async {
+//                    self.showInputUI()
+//                }
+//
+//            case .failure(let err):
+//                DDLogError(err.localizedDescription)
+//                DispatchQueue.main.async {
+//                    self.showError(title: "读取身份列表失败")
+//                }
+//            }
+//        }
+//    }
     
     func showInputUI(){
         form +++ Section("创建流程")
-        <<< TextRow("title") {row in
-            row.title = "标题"
-            row.placeholder = "请输入标题"
-        }.cellSetup({ (cell, row) in
-            //cell.height = 50
-        })
+            //不需要标题
+//        <<< TextRow("title") {row in
+//            row.title = "标题"
+//            row.placeholder = "请输入标题"
+//        }.cellSetup({ (cell, row) in
+//            //cell.height = 50
+//        })
             
         <<< ActionSheetRow<IdentityV2>("selectedIdentity") {
                 $0.title = "用户身份"
@@ -126,24 +125,23 @@ class TaskCreateViewController: FormViewController {
             <<< ButtonRow("createButton") { (row:ButtonRow) in
                 row.title = "创建"
             }.onCellSelection({ (cell, row) in
-                let titleRow:TextRow = self.form.rowBy(tag:"title")!
+//                let titleRow:TextRow = self.form.rowBy(tag:"title")!
                 let identityRow:ActionSheetRow<IdentityV2> = self.form.rowBy(tag:"selectedIdentity")!
-                guard let title = titleRow.value else{
-                    self.showError(title: "请输入标题")
-                    return
-                }
+//                guard let title = titleRow.value else{
+//                    self.showError(title: "请输入标题")
+//                    return
+//                }
                 guard let id = identityRow.value  else {
                     self.showError(title: "请选择身份")
                     return
                 }
-                self.createProcess(title, identity: id.distinguishedName!)
+                self.createProcess(identity: id.distinguishedName!)
             })
     }
     
-    func createProcess(_ title:String,identity:String){
-        DDLogDebug("title = \(title),identity = \(identity)")
+    func createProcess(identity:String){
         let bean = CreateProcessBean()
-        bean.title = title
+        bean.title = ""//不需要标题
         bean.identity = identity
         let createURL = AppDelegate.o2Collect.generateURLWithAppContextKey(WorkContext.workContextKey, query: WorkContext.workCreateQuery, parameter: ["##id##":(process?.id)! as AnyObject])
         self.showLoading(title: "创建中,请稍候...")
@@ -162,15 +160,12 @@ class TaskCreateViewController: FormViewController {
                     DispatchQueue.main.async {
                         self.hideLoading()
                     }
-                    
-                    //ProgressHUD.showSuccess("创建成功")
                 } else {
                     self.showError(title: "创建失败")
                 }
             case .failure(let err):
                 DDLogError(err.localizedDescription)
                 self.showError(title: "创建失败")
-
             }
            
         }

+ 71 - 2
o2ios/O2Platform/App/Work-工作/c/category/ZoneMenuViewController.swift

@@ -78,7 +78,7 @@ class ZoneMenuViewController: UIViewController {
     
     @objc private func receiveSubNotification(_ notification:NSNotification){
         let obj = notification.object
-        self.performSegue(withIdentifier: "showStartFlowSegue", sender: obj)
+        loadDepartAndIdentity(process: obj as? AppProcess)
     }
     
     
@@ -99,10 +99,79 @@ class ZoneMenuViewController: UIViewController {
         })
     }
     
+    //获取身份列表
+    func loadDepartAndIdentity(process: AppProcess?){
+        if process == nil {
+            self.showError(title: "流程信息获取失败!")
+            return
+        }
+        let url = AppDelegate.o2Collect.generateURLWithAppContextKey(TaskContext.taskContextKey, query: TaskContext.todoCreateAvaiableIdentityByIdQuery, parameter: ["##processId##": process!.id as AnyObject])
+        Alamofire.request(url!).responseArray(keyPath:"data") { (response:DataResponse<[IdentityV2]>) in
+            switch response.result {
+            case .success(let identitys):
+                if identitys.count > 1 { // 多身份需要去选择身份
+                    let data = TaskCreateData(process: process, identitys: identitys)
+                    self.gotoChooseIdentity(data: data)
+                }else if identitys.count == 1 {
+                    self.createProcess(processId: process!.id!, identity: identitys[0].distinguishedName!)
+                }else {
+                    DispatchQueue.main.async {
+                        self.showError(title: "当前用户没有身份,无法创建工作!")
+                    }
+                }
+            case .failure(let err):
+                DDLogError(err.localizedDescription)
+                DispatchQueue.main.async {
+                    self.showError(title: "读取身份列表失败")
+                }
+            }
+        }
+    }
+    
+    //创建流程
+    private func createProcess(processId: String, identity:String){
+        let bean = CreateProcessBean()
+        bean.title = ""
+        bean.identity = identity
+        let createURL = AppDelegate.o2Collect.generateURLWithAppContextKey(WorkContext.workContextKey, query: WorkContext.workCreateQuery, parameter: ["##id##":processId as AnyObject])
+        self.showLoading(title: "创建中,请稍候...")
+        Alamofire.request(createURL!,method:.post, parameters: bean.toJSON(), encoding: JSONEncoding.default, headers: nil).responseJSON { response in
+            debugPrint(response.result)
+            switch response.result {
+            case .success(let val):
+                let taskList = JSON(val)["data"][0]
+                DDLogDebug(taskList.description)
+                if let tasks = Mapper<TodoTask>().mapArray(JSONString:taskList["taskList"].debugDescription) , tasks.count > 0 {
+                    let taskStoryboard = UIStoryboard(name: "task", bundle: Bundle.main)
+                    let todoTaskDetailVC = taskStoryboard.instantiateViewController(withIdentifier: "todoTaskDetailVC") as! TodoTaskDetailViewController
+                    todoTaskDetailVC.todoTask = tasks[0]
+                    todoTaskDetailVC.backFlag = 1
+                    self.navigationController?.pushViewController(todoTaskDetailVC, animated: true)
+                    DispatchQueue.main.async {
+                        self.hideLoading()
+                    }
+                } else {
+                    self.showError(title: "创建失败")
+                }
+            case .failure(let err):
+                DDLogError(err.localizedDescription)
+                self.showError(title: "创建失败")
+            }
+        }
+    }
+    //进入身份选择页面 创建流程
+    private func gotoChooseIdentity(data: TaskCreateData) {
+        self.performSegue(withIdentifier: "showStartFlowSegue", sender: data)
+    }
+    
     override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
         if segue.identifier == "showStartFlowSegue" {
             let destVc = segue.destination as! TaskCreateViewController
-            destVc.process = sender as? AppProcess
+            if sender is TaskCreateData {
+                let data = sender as? TaskCreateData
+                destVc.process = data?.process
+                destVc.identitys = data?.identitys
+            }
         }
     }
 

+ 68 - 0
o2ios/O2Platform/App/Work-工作/m/CreateProcessBean.swift

@@ -25,3 +25,71 @@ class CreateProcessBean:Mappable {
         identity <- map["identity"]
     }
 }
+
+class CmsDocData:Mappable {
+    var isNewDocument:Bool? = true
+    var title:String?
+    var creatorIdentity:String?
+    var appId:String?
+    var categoryId:String?
+    var docStatus:String? = "draft"
+    var createTime:String?
+    var categoryName:String?
+    var categoryAlias:String?
+    
+    init(){
+        
+    }
+    
+    required init?(map: Map) {
+        
+    }
+    
+    func mapping(map: Map) {
+        title <- map["title"]
+        isNewDocument <- map["isNewDocument"]
+        creatorIdentity <- map["creatorIdentity"]
+        appId <- map["appId"]
+        categoryId <- map["categoryId"]
+        docStatus <- map["docStatus"]
+        createTime <- map["createTime"]
+        categoryName <- map["categoryName"]
+        categoryAlias <- map["categoryAlias"]
+    }
+}
+
+class CreateProcessCmsData:Mappable {
+    var cmsDocument:CmsDocData?
+    
+    init(){
+        
+    }
+    
+    required init?(map: Map) {
+        
+    }
+    
+    func mapping(map: Map) {
+        cmsDocument <- map["cmsDocument"]
+    }
+}
+
+class CreateProcessCmsBean:Mappable {
+    var title:String?
+    var identity:String?
+    var data:CreateProcessCmsData?
+    
+    init(){
+        
+    }
+    
+    required init?(map: Map) {
+        
+    }
+    
+    func mapping(map: Map) {
+        title <- map["title"]
+        identity <- map["identity"]
+        data <- map["data"]
+    }
+}

+ 15 - 0
o2ios/O2Platform/App/Work-工作/m/TaskCreateData.swift

@@ -0,0 +1,15 @@
+//
+//  TaskCreateData.swift
+//  O2Platform
+//
+//  Created by FancyLou on 2020/3/7.
+//  Copyright © 2020 zoneland. All rights reserved.
+//
+
+import Foundation
+
+
+struct TaskCreateData {
+    var process: AppProcess?
+    var identitys:[IdentityV2] = []
+}

+ 7 - 1
o2ios/O2Platform/App/Work-工作/v/NewMainItemTableViewCell.swift

@@ -26,7 +26,13 @@ class NewMainItemTableViewCell: UITableViewCell {
             }else if(model.isKind(of: TodoTask.self)){
                 let m = model as! TodoTask
                 self.categoryNameLabel.text = "【\(m.applicationName!)】"
-                self.titleLabel.text = m.title
+                var title = ""
+                if  m.title == nil || m.title?.isEmpty == true {
+                    title = "无标题"
+                }else {
+                    title = m.title!
+                }
+                self.titleLabel.text = title
                 self.timeLabel.text = m.updateTime?.split(separator:" ").first?.description
                 
             }

+ 1 - 1
o2ios/O2Platform/App/contacts/c/ContactPersonInfoV2ViewController.swift

@@ -112,7 +112,7 @@ class ContactPersonInfoV2ViewController: UITableViewController {
                 NotificationCenter.default.post(name: NSNotification.Name(rawValue: kUpdateConversation), object: nil, userInfo: nil)
                 self.navigationController?.pushViewController(vc, animated: true)
             }else{
-                O2Logger.error(error.debugDescription)
+                DDLogError(error.debugDescription)
                 MBProgressHUD_JChat.show(text: "创建会话失败,请重试", view: self.view)
             }
         }

+ 1 - 1
o2ios/O2Platform/AppDelegate.swift

@@ -106,7 +106,7 @@ class AppDelegate: FlutterAppDelegate, JPUSHRegisterDelegate, UNUserNotification
         _setupJMessage()
         
         _mapManager = BMKMapManager()
-        BMKMapManager.setCoordinateTypeUsedInBaiduMapSDK(BMK_COORDTYPE_BD09LL)
+        BMKMapManager.setCoordinateTypeUsedInBaiduMapSDK(.COORDTYPE_BD09LL)
         _mapManager?.start(BAIDU_MAP_KEY, generalDelegate: nil)
         
         

+ 244 - 0
o2ios/O2Platform/Framework/AZPopMenu/AZPopMenu.swift

@@ -0,0 +1,244 @@
+//
+//  AZPopMenu.swift
+//
+//  Created by Aaron Zhu on 15/6/4.
+//  Copyright (c) 2015年 Aaron Zhu All rights reserved.
+
+/******************************************
+    *作用:
+        创建一个pop菜单。
+    *使用方法: 
+        AZPopMenu.show
+    *方法声明:
+        class func show(superView:UIView, startPoint: CGPoint, items: [String], colors: [UIColor], selected: (itemSelected: Int) -> Void)
+    *方法参数:
+        superView:  父View,请使用ViewController.view,方便计算坐标
+        startPoint: pop菜单上方的箭头位置,使用superView的坐标
+        items:      要显示的菜单项
+        colors:     菜单项前显示的色块
+        selected:   选中菜单项后调用的闭包
+******************************************/
+
+
+import UIKit
+import CoreGraphics
+
+let AZ_SELL_WIDTH : CGFloat = 120        //Cell宽度
+let AZ_SELL_HEIGHT: CGFloat = 40         //Cell高度
+let AZ_ARROW_WIDTH: CGFloat = 10         //Table上方箭头的宽度
+let AZ_ARROW_HEIGHT: CGFloat = 10        //Table上方箭头的高度
+let AZ_ARROW_FROM_EDGE: CGFloat = 35     //Table上方箭头距离Table边界的最小距离
+let AZ_TABLE_WIDTH = AZ_SELL_WIDTH    //Table的宽度(同Cell宽度)
+let AZ_TABLE_FROM_EDGE: CGFloat = 10     //Table距离手机边界的最小距离
+
+let AZ_SCREEN_WIDTH = UIScreen.main.bounds.size.width
+let AZ_SCREEN_HEIGHT = UIScreen.main.bounds.size.height
+
+
+class SelectCell : UITableViewCell{
+    
+    var colorView :UIView!
+    var nameLabel :UILabel!
+    
+    func setUpItem(_ item: String, withColor: UIColor){
+        //构造cell上的内容
+        colorView = UIView(frame: CGRect(x: 18,y: 13,width: 14,height: 14))
+        colorView.layer.cornerRadius = 2
+        colorView.layer.masksToBounds = true
+        self.addSubview(colorView)
+        nameLabel = UILabel(frame: CGRect(x: 42, y: 15, width: 100, height: 10))
+        nameLabel.backgroundColor = UIColor.clear
+        nameLabel.textColor = UIColor(red:192/255, green: 193/255, blue: 195/255, alpha: 1.0)
+        self.addSubview(nameLabel)
+        self.backgroundColor = UIColor(red: 57/255, green: 60/255, blue: 66/255, alpha: 1.0)
+        
+        nameLabel.text = item
+        colorView.backgroundColor = withColor
+        
+    }
+}
+
+class ArrowView: UIView{
+    var arrowPoint: CGPoint
+    init(frame: CGRect, arrowPoint: CGPoint) {
+        //转换箭头的坐标 相对super坐标 -> 本view坐标
+        self.arrowPoint = arrowPoint
+        self.arrowPoint.y = 0
+        self.arrowPoint.x = arrowPoint.x - frame.origin.x
+        super.init(frame: frame)
+    }
+
+    required init(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    //画顶部的三角形
+    override func draw(_ rect: CGRect) {
+        super.draw(rect)
+        let  context = UIGraphicsGetCurrentContext()
+        context!.setFillColor(red: 57/255, green: 60/255, blue: 66/255, alpha: 1.0) //设置填充颜色
+        let sPoints: [CGPoint] = [arrowPoint,
+            CGPoint(x: arrowPoint.x-AZ_ARROW_WIDTH/2, y: arrowPoint.y+AZ_ARROW_HEIGHT),
+            CGPoint(x: arrowPoint.x+AZ_ARROW_WIDTH/2, y: arrowPoint.y+AZ_ARROW_HEIGHT)]
+        context?.addLines(between: sPoints)
+        //CGContextAddLines(context!, sPoints, 3)
+        context!.closePath()
+        context!.drawPath(using: .fillStroke)
+    }
+    
+}
+
+class AZPopMenu: UIView,UITableViewDelegate,UITableViewDataSource,UIGestureRecognizerDelegate{
+
+    var superView: UIView!  //父View,使用ViewController.view,方便计算坐标。
+    var startPoint: CGPoint = CGPoint(x: 0,y: 0) //箭头位置
+    var items: [String] = []                     //cell中使用字符串
+    var colors: [UIColor] = []                   //cell中使用颜色框
+    var selected: ((_ itemSelected: Int) -> Void)? //点击选项后调用的闭包
+    
+                                    //第一层,self, 全屏透明view,用于接收tabGuesture手势来关闭自己。
+    var popViewArrow: ArrowView!    //第二层,小View,用来画三角箭头
+    var popTable: UITableView!      //第三层,TableView,显示内容和接收点击事件
+    var tabGuesture: UITapGestureRecognizer!
+    
+    class func show(_ superView:UIView, startPoint: CGPoint, items: [String], colors: [UIColor], selected: @escaping (_ itemSelected: Int) -> Void){
+        let p = AZPopMenu()
+        p.showPopMenu(superView, startPoint: startPoint, items: items, colors: colors, selected: selected)
+    }
+    
+    init(){
+        super.init(frame: CGRect(x: 0, y: 0, width: AZ_SCREEN_WIDTH, height: AZ_SCREEN_HEIGHT))
+        self.backgroundColor = UIColor(white: 0.0, alpha: 0.2)
+    }
+
+    required init(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    func showPopMenu (_ superView:UIView, startPoint: CGPoint, items: [String], colors: [UIColor], selected: @escaping (_ itemSelected: Int) -> Void) {
+        if items.isEmpty {
+            print("error: items为空。函数结束。")
+            return
+        }
+        if items.count != colors.count {
+            print("error: items和colors项目数量不同。函数结束。")
+            return
+        }
+
+        self.superView = superView
+        self.startPoint = startPoint
+        self.items = items
+        self.colors = colors
+        self.selected = selected
+        
+        //调整箭头位置, 距离屏幕两边35个点以上
+        if self.startPoint.x < AZ_ARROW_FROM_EDGE {
+            self.startPoint.x = AZ_ARROW_FROM_EDGE
+        }else if self.startPoint.x > (AZ_SCREEN_WIDTH-AZ_ARROW_FROM_EDGE){
+            self.startPoint.x = AZ_SCREEN_WIDTH-AZ_ARROW_FROM_EDGE
+        }
+        //调整popmenu的frame  默认箭头置中, table距离屏幕两边10个点以上。
+        let width = AZ_TABLE_WIDTH
+        let height = AZ_ARROW_HEIGHT + AZ_SELL_HEIGHT * CGFloat(items.count) - 1.0  //隐藏掉最后一个像素的白线
+        var tableX = self.startPoint.x - AZ_SELL_WIDTH/2
+        if tableX < AZ_TABLE_FROM_EDGE {
+            tableX = AZ_TABLE_FROM_EDGE
+        }else if tableX > (AZ_SCREEN_WIDTH-AZ_SELL_WIDTH-AZ_TABLE_FROM_EDGE){
+            tableX = AZ_SCREEN_WIDTH-AZ_SELL_WIDTH-AZ_TABLE_FROM_EDGE
+        }
+
+        popViewArrow = ArrowView(frame: CGRect(x: tableX, y: startPoint.y, width: width, height: height), arrowPoint: self.startPoint)
+        popViewArrow.backgroundColor = UIColor.clear
+        //popViewArrow.backgroundColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.3)
+        
+        //放置table
+        popTable = UITableView(frame: CGRect(x: 0, y: AZ_ARROW_HEIGHT, width: width, height: height-AZ_ARROW_HEIGHT), style: UITableView.Style.plain)
+        popTable.delegate = self
+        popTable.dataSource = self
+        
+        popTable.layer.cornerRadius = 5
+        popTable.layer.masksToBounds = true
+        popTable.isScrollEnabled = false //禁止滚动
+        popTable.separatorStyle = UITableViewCell.SeparatorStyle.none //去掉cell分割线
+        
+        popViewArrow.addSubview(popTable)
+        self.addSubview(popViewArrow)
+        superView.addSubview(self)
+        
+        //UITapGestureRecognizer! 用于关闭popmenu
+        tabGuesture = UITapGestureRecognizer(target: self, action: #selector(AZPopMenu.tabAction(_:)))
+        tabGuesture.numberOfTapsRequired = 1
+        tabGuesture.delegate = self
+        self.addGestureRecognizer(tabGuesture)
+        
+        //动画打开效果
+        animationShowPopMenu()
+    
+    }
+    
+    func animationShowPopMenu(){
+        //改变popTable
+        var done = false
+        //popTable.alpha = 0.0
+        let toRect = popTable.frame
+        popTable.frame =  CGRect(x: startPoint.x-popViewArrow.frame.origin.x, y: 0, width: 1, height: 1)
+        UIView.animate(withDuration: 0.2, animations: { () -> Void in
+            self.popTable.frame = toRect
+            //self.popTable.alpha = 1.0
+            }, completion: { (b: Bool) -> Void in
+            done = true
+        }) 
+        while !done{
+            RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.01))
+        }
+    }
+    
+    
+    @objc func tabAction(_ sender: UITapGestureRecognizer){
+        //关闭本身
+        self.removeGestureRecognizer(tabGuesture)
+        self.removeFromSuperview()
+        self.selected?(-1)
+    }
+   
+
+    //UITableViewDelegate/UITableViewDataSource
+    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
+        return 0.0
+    }
+    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
+        return 0.0
+    }
+    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        return AZ_SELL_HEIGHT
+    }
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return items.count
+    }
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) ->UITableViewCell{
+        let cell = SelectCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "Cell")
+        //cell.setUpView(indexPath.row)
+        cell.setUpItem(items[(indexPath as NSIndexPath).row], withColor: colors[(indexPath as NSIndexPath).row])
+        return cell;
+    }
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){
+        tableView.deselectRow(at: indexPath, animated: true)
+        //关闭本身
+        self.removeGestureRecognizer(tabGuesture)
+        self.removeFromSuperview()
+        self.selected?((indexPath as NSIndexPath).row)
+    }
+    //UIGestureRecognizerDelegate 用于过滤tableview
+    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool{
+        if touch.view is AZPopMenu {
+            return true
+        }else{
+            return false
+        }
+    }
+
+
+}
+
+
+

+ 367 - 0
o2ios/O2Platform/Framework/DatePickerDialogSwift/LWDatePickerDialog.swift

@@ -0,0 +1,367 @@
+//
+//  LWDatePickerDialog.swift
+//  DatePickerDialogSwift
+//
+//  Created by 刘振兴 on 2018/1/17.
+//
+
+import UIKit
+
+private extension Selector {
+    //按钮点击
+    static let buttonTapped = #selector(LWDatePickerDialog.buttonTapped)
+    //设备方向转换
+    static let deviceOrientationDidChange = #selector(LWDatePickerDialog.deviceOrientationDidChange)
+}
+
+
+struct LWDialogStyle {
+    //title
+    static let titleColor = UIColor(hex: "#FFFFFF")
+    static let titleTextFont = UIFont(name: "PingFangSC-Regular", size: 18)
+    static var titleViewBackColor: UIColor {
+        get {
+            return O2ThemeManager.color(for: "Base.base_color")!
+        }
+    }
+    
+    //DatePicker unSelected TextColor Font
+    static let dpUnSelTextColor = UIColor(hex: "#999999")
+    static let dpUnSelTextFont = UIFont(name: "PingFangSC-Regular", size: 18)
+    
+    static var dpSelTextColor: UIColor {
+        get {
+            return O2ThemeManager.color(for: "Base.base_color")!
+        }
+    }
+    static let dpSelTextFont = UIFont(name: "PingFangSC-Regular", size: 23)
+    
+    //button
+    static let okButtonTextColor = UIColor(hex: "#FFFFFF")
+    static let okButtonFont = UIFont(name: "PingFangSC-Regular", size: 16)
+    static var okButtonBackColor: UIColor {
+        get {
+            return O2ThemeManager.color(for: "Base.base_color")!
+        }
+    }
+    
+    static let cancelButtonTextColor = UIColor(hex: "#FFFFFF")
+    static let cancelButtonFont = UIFont(name: "PingFangSC-Regular", size: 16)
+    static let cancelButtonBackColor = UIColor(hex: "#CCCCCC")
+    
+    // MARK: - Constants
+    static let defaultWidth:CGFloat = 300
+    
+    static let defaultTitleContainerHeight:CGFloat = 50
+    static let defaultTitleHeight:CGFloat = 35
+    
+    static let defaultDatePickerHeight:CGFloat = 230
+    
+    static let defaultButtonContainerHeight:CGFloat = 50
+    static let defaultButtonHeight: CGFloat = 35
+    
+    static let defaultButtonSpacerHeight: CGFloat = 1
+    
+    static let cornerRadius: CGFloat = 15
+    static let doneButtonTag: Int     = 1
+    
+}
+
+
+open class LWDatePickerDialog: UIView {
+    //回调类型定义
+    public typealias DatePickerCallback = ( Date? ) -> Void
+
+    // MARK: - Views
+    private var dialogView: UIView!
+    //title view
+    private var titleContainerView:UIView!
+    private var titleLabel: UILabel!
+    
+    //picker view
+    open var datePicker: UIDatePicker!
+    
+    //button view
+    private var buttonContainerView:UIView!
+    private var cancelButton: UIButton!
+    private var doneButton: UIButton!
+    
+    // MARK: - Variables
+    private var defaultDate: Date?
+    private var datePickerMode: UIDatePicker.Mode?
+    private var callback: DatePickerCallback?
+    
+    var showCancelButton: Bool = false
+    var locale: Locale?
+
+    private var textColor: UIColor!
+    private var buttonColor: UIColor!
+    private var font: UIFont!
+    
+    // MARK: - Dialog initialization
+    public init(textColor: UIColor = UIColor.black,
+                buttonColor: UIColor = UIColor.blue,
+                font: UIFont = .boldSystemFont(ofSize: 15),
+                locale: Locale? = nil,
+                showCancelButton: Bool = true) {
+        let size = UIScreen.main.bounds.size
+        super.init(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height))
+        self.textColor = textColor
+        self.buttonColor = buttonColor
+        self.font = font
+        self.showCancelButton = showCancelButton
+        self.locale = locale
+        setupView()
+    }
+    
+    required public init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+    }
+    
+    func setupView() {
+        self.dialogView = createContainerView()
+        
+        self.dialogView!.layer.shouldRasterize = true
+        self.dialogView!.layer.rasterizationScale = UIScreen.main.scale
+        
+        self.layer.shouldRasterize = true
+        self.layer.rasterizationScale = UIScreen.main.scale
+        
+        self.dialogView!.layer.opacity = 0.5
+        self.dialogView!.layer.transform = CATransform3DMakeScale(1.3, 1.3, 1)
+        
+        self.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0)
+        
+        self.addSubview(self.dialogView!)
+    }
+    
+    /// Handle device orientation changes
+    @objc func deviceOrientationDidChange(_ notification: Notification) {
+        self.frame = UIScreen.main.bounds
+        let dialogSize = CGSize(width: LWDialogStyle.defaultWidth,height: LWDialogStyle.defaultTitleContainerHeight + LWDialogStyle.defaultDatePickerHeight + LWDialogStyle.defaultButtonContainerHeight)
+        dialogView.frame = CGRect(x: (UIScreen.main.bounds.size.width - dialogSize.width) / 2,
+                                  y: (UIScreen.main.bounds.size.height - dialogSize.height) / 2,
+                                  width: dialogSize.width,
+                                  height: dialogSize.height)
+    }
+    
+    /// Create the dialog view, and animate opening the dialog
+    open func show(_ title: String,
+                   doneButtonTitle: String = "Done",
+                   cancelButtonTitle: String = "Cancel",
+                   defaultDate: Date = Date(),
+                   minimumDate: Date? = nil, maximumDate: Date? = nil,
+                   datePickerMode: UIDatePicker.Mode = .dateAndTime,
+                   callback: @escaping DatePickerCallback) {
+        self.titleLabel.text = title
+        self.doneButton.setTitle(doneButtonTitle, for: .normal)
+        if showCancelButton {
+            self.cancelButton.setTitle(cancelButtonTitle, for: .normal)
+        }
+        self.datePickerMode = datePickerMode
+        self.callback = callback
+        self.defaultDate = defaultDate
+        self.datePicker.datePickerMode = self.datePickerMode ?? UIDatePicker.Mode.date
+        self.datePicker.date = self.defaultDate ?? Date()
+        self.datePicker.maximumDate = maximumDate
+        self.datePicker.minimumDate = minimumDate
+        if let locale = self.locale {
+            self.datePicker.locale = locale
+        }
+        /* Add dialog to main window */
+        guard let appDelegate = UIApplication.shared.delegate else { fatalError() }
+        guard let window = appDelegate.window else { fatalError() }
+        window?.addSubview(self)
+        window?.bringSubviewToFront(self)
+        window?.endEditing(true)
+        
+        NotificationCenter.default.addObserver(self,
+                                               selector: .deviceOrientationDidChange,
+                                               name: UIDevice.orientationDidChangeNotification,
+                                               object: nil)
+        
+        /* Anim */
+        UIView.animate(
+            withDuration: 0.2,
+            delay: 0,
+            options: .curveEaseInOut,
+            animations: {
+                self.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.4)
+                self.dialogView!.layer.opacity = 1
+                self.dialogView!.layer.transform = CATransform3DMakeScale(1, 1, 1)
+        }
+        )
+    }
+    
+    /// Dialog close animation then cleaning and removing the view from the parent
+    private func close() {
+        let currentTransform = self.dialogView.layer.transform
+        
+        let startRotation = (self.value(forKeyPath: "layer.transform.rotation.z") as? NSNumber) as? Double ?? 0.0
+        let rotation = CATransform3DMakeRotation((CGFloat)(-startRotation + .pi * 270 / 180), 0, 0, 0)
+        
+        self.dialogView.layer.transform = CATransform3DConcat(rotation, CATransform3DMakeScale(1, 1, 1))
+        self.dialogView.layer.opacity = 1
+        
+        UIView.animate(
+            withDuration: 0.2,
+            delay: 0,
+            options: [],
+            animations: {
+                self.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0)
+                let transform = CATransform3DConcat(currentTransform, CATransform3DMakeScale(0.6, 0.6, 1))
+                self.dialogView.layer.transform = transform
+                self.dialogView.layer.opacity = 0
+        }) { (_) in
+            for v in self.subviews {
+                v.removeFromSuperview()
+            }
+            
+            self.removeFromSuperview()
+            self.setupView()
+        }
+    }
+    
+    /// Creates the container view here: create the dialog, then add the custom content and buttons
+    private func createContainerView() -> UIView {
+        let screenSize = UIScreen.main.bounds.size
+        //title + datePicker + button height
+        let dialogSize = CGSize(width: LWDialogStyle.defaultWidth, height: LWDialogStyle.defaultTitleContainerHeight + LWDialogStyle.defaultDatePickerHeight + LWDialogStyle.defaultButtonContainerHeight)
+        
+        // For the black background
+        self.frame = CGRect(x: 0, y: 0, width: screenSize.width, height: screenSize.height)
+        
+        // This is the dialog's container; we attach the custom content and the buttons to this one
+        let container = UIView(frame: CGRect(x: (screenSize.width - dialogSize.width) / 2,
+                                             y: (screenSize.height - dialogSize.height) / 2,
+                                             width: dialogSize.width,
+                                             height: dialogSize.height))
+        
+        // First, we style the dialog to match the iOS8 UIAlertView >>>
+        let gradient: CAGradientLayer = CAGradientLayer(layer: self.layer)
+        gradient.frame = container.bounds
+        gradient.colors = [UIColor(red: 218/255, green: 218/255, blue: 218/255, alpha: 1).cgColor,
+                           UIColor(red: 233/255, green: 233/255, blue: 233/255, alpha: 1).cgColor,
+                           UIColor(red: 218/255, green: 218/255, blue: 218/255, alpha: 1).cgColor]
+        
+        let cornerRadius = LWDialogStyle.cornerRadius
+        gradient.cornerRadius = cornerRadius
+        
+        container.layer.insertSublayer(gradient, at: 0)
+        container.layer.cornerRadius = cornerRadius
+        container.layer.masksToBounds = true
+        container.layer.borderColor = UIColor(red: 198/255, green: 198/255, blue: 198/255, alpha: 1).cgColor
+        container.layer.borderWidth = 1
+        container.layer.shadowRadius = cornerRadius + 5
+        container.layer.shadowOpacity = 0.1
+        container.layer.shadowOffset = CGSize(width: 0 - (cornerRadius + 5) / 2, height: 0 - (cornerRadius + 5) / 2)
+        container.layer.shadowColor = UIColor.black.cgColor
+        container.layer.shadowPath = UIBezierPath(roundedRect: container.bounds,
+                                                  cornerRadius: container.layer.cornerRadius).cgPath
+        
+        // There is a line above the button
+//        let yPosition = container.bounds.size.height - kDefaultButtonHeight - kDefaultButtonSpacerHeight
+//        let lineView = UIView(frame: CGRect(x: 0,
+//                                            y: yPosition,
+//                                            width: container.bounds.size.width,
+//                                            height: kDefaultButtonSpacerHeight))
+//        lineView.backgroundColor = UIColor(red: 198/255, green: 198/255, blue: 198/255, alpha: 1)
+//        container.addSubview(lineView)
+        
+        //Title
+        self.titleContainerView = UIView(frame: CGRect(x: 0, y: 0, width: LWDialogStyle.defaultWidth, height: LWDialogStyle.defaultTitleContainerHeight))
+        self.titleContainerView.backgroundColor = LWDialogStyle.titleViewBackColor
+        self.titleLabel = UILabel(frame: CGRect(x: 20, y: (LWDialogStyle.defaultTitleContainerHeight-LWDialogStyle.defaultTitleHeight)/2, width: LWDialogStyle.defaultWidth - 50, height: LWDialogStyle.defaultTitleHeight))
+        self.titleLabel.textAlignment = .left
+        self.titleLabel.textColor = LWDialogStyle.titleColor
+        self.titleLabel.font = LWDialogStyle.titleTextFont
+        self.titleContainerView.addSubview(self.titleLabel)
+        container.addSubview(self.titleContainerView)
+        //DatePicker
+        self.datePicker = configuredDatePicker()
+        container.addSubview(self.datePicker)
+        
+        // Add the buttons
+        self.buttonContainerView = UIView(frame: CGRect(x: 0, y: LWDialogStyle.defaultTitleContainerHeight + LWDialogStyle.defaultDatePickerHeight, width: LWDialogStyle.defaultWidth, height: LWDialogStyle.defaultButtonContainerHeight))
+        self.backgroundColor = UIColor.white
+        addButtonsToView(container: buttonContainerView)
+        container.addSubview(self.buttonContainerView)
+        
+        return container
+    }
+    
+    fileprivate func configuredDatePicker() -> UIDatePicker {
+        let datePicker = UIDatePicker(frame: CGRect(x: 0, y: LWDialogStyle.defaultTitleContainerHeight, width: 0, height: 0))
+        datePicker.setValue(LWDialogStyle.dpSelTextColor, forKeyPath: "textColor")
+        datePicker.autoresizingMask = .flexibleRightMargin
+        datePicker.frame.size.width = LWDialogStyle.defaultWidth
+        datePicker.frame.size.height = LWDialogStyle.defaultDatePickerHeight
+        return datePicker
+    }
+    
+    /// Add buttons to container
+    private func addButtonsToView(container: UIView) {
+        var buttonWidth = (container.bounds.size.width - 20*2) / 2
+        
+        var leftButtonFrame = CGRect(
+            x: 10,
+            y: (container.bounds.size.height - LWDialogStyle.defaultButtonHeight)/2,
+            width: buttonWidth,
+            height: LWDialogStyle.defaultButtonHeight
+        )
+        var rightButtonFrame = CGRect(
+            x: 10 + buttonWidth + 10 * 2,
+            y: (container.bounds.size.height - LWDialogStyle.defaultButtonHeight)/2,
+            width: buttonWidth,
+            height: LWDialogStyle.defaultButtonHeight
+        )
+        if showCancelButton == false {
+            buttonWidth = container.bounds.size.width
+            leftButtonFrame = CGRect()
+            rightButtonFrame = CGRect(
+                x: (LWDialogStyle.defaultWidth - buttonWidth) / 2,
+                y: (container.bounds.size.height - LWDialogStyle.defaultButtonHeight)/2,
+                width: buttonWidth,
+                height: LWDialogStyle.defaultButtonHeight
+            )
+        }
+        let interfaceLayoutDirection = UIApplication.shared.userInterfaceLayoutDirection
+        let isLeftToRightDirection = interfaceLayoutDirection == .leftToRight
+        
+        if showCancelButton {
+            self.cancelButton = UIButton(type: .custom) as UIButton
+            self.cancelButton.frame = isLeftToRightDirection ? leftButtonFrame : rightButtonFrame
+            self.cancelButton.setTitleColor(LWDialogStyle.cancelButtonTextColor, for: .normal)
+            self.cancelButton.setTitleColor(LWDialogStyle.cancelButtonTextColor, for: .highlighted)
+            self.cancelButton.backgroundColor = LWDialogStyle.cancelButtonBackColor
+            self.cancelButton.titleLabel!.font = LWDialogStyle.cancelButtonFont
+            self.cancelButton.layer.cornerRadius = LWDialogStyle.cornerRadius
+            self.cancelButton.addTarget(self, action: .buttonTapped, for: .touchUpInside)
+            container.addSubview(self.cancelButton)
+        }
+        self.doneButton = UIButton(type: .custom) as UIButton
+        self.doneButton.frame = isLeftToRightDirection ? rightButtonFrame : leftButtonFrame
+        self.doneButton.tag = LWDialogStyle.doneButtonTag
+        self.doneButton.backgroundColor = LWDialogStyle.okButtonBackColor
+        self.doneButton.setTitleColor(LWDialogStyle.okButtonTextColor, for: .normal)
+        self.doneButton.setTitleColor(LWDialogStyle.okButtonTextColor, for: .highlighted)
+        self.doneButton.titleLabel!.font = LWDialogStyle.okButtonFont
+        self.doneButton.layer.cornerRadius = LWDialogStyle.cornerRadius
+        self.doneButton.addTarget(self, action: .buttonTapped, for: .touchUpInside)
+        container.addSubview(self.doneButton)
+    }
+    
+    @objc func buttonTapped(sender: UIButton!) {
+        if sender.tag == LWDialogStyle.doneButtonTag {
+            self.callback?(self.datePicker.date)
+        } else {
+            self.callback?(nil)
+        }
+        close()
+    }
+    
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+    }
+
+}

+ 35 - 0
o2ios/O2Platform/Framework/Haneke/CGSize+Swift.swift

@@ -0,0 +1,35 @@
+//
+//  CGSize+Swift.swift
+//  Haneke
+//
+//  Created by Oriol Blanc Gimeno on 09/09/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+extension CGSize {
+
+    func hnk_aspectFillSize(_ size: CGSize) -> CGSize {
+        let scaleWidth = size.width / self.width
+        let scaleHeight = size.height / self.height
+        let scale = max(scaleWidth, scaleHeight)
+
+        let resultSize = CGSize(width: self.width * scale, height: self.height * scale)
+        return CGSize(width: ceil(resultSize.width), height: ceil(resultSize.height))
+    }
+
+    func hnk_aspectFitSize(_ size: CGSize) -> CGSize {
+        let targetAspect = size.width / size.height
+        let sourceAspect = self.width / self.height
+        var resultSize = size
+
+        if (targetAspect > sourceAspect) {
+            resultSize.width = size.height * sourceAspect
+        }
+        else {
+            resultSize.height = size.width / sourceAspect
+        }
+        return CGSize(width: ceil(resultSize.width), height: ceil(resultSize.height))
+    }
+}

+ 311 - 0
o2ios/O2Platform/Framework/Haneke/Cache.swift

@@ -0,0 +1,311 @@
+//
+//  Cache.swift
+//  Haneke
+//
+//  Created by Luis Ascorbe on 23/07/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+// Used to add T to NSCache
+class ObjectWrapper : NSObject {
+    let hnk_value: Any
+    
+    init(value: Any) {
+        self.hnk_value = value
+    }
+}
+
+extension HanekeGlobals {
+    
+    // It'd be better to define this in the Cache class but Swift doesn't allow statics in a generic type
+    public struct Cache {
+        
+        public static let OriginalFormatName = "original"
+
+        public enum ErrorCode : Int {
+            case objectNotFound = -100
+            case formatNotFound = -101
+        }
+        
+    }
+    
+}
+
+open class Cache<T: DataConvertible> where T.Result == T, T : DataRepresentable {
+    
+    let name: String
+    
+    var memoryWarningObserver : NSObjectProtocol!
+    
+    public init(name: String) {
+        self.name = name
+        
+        let notifications = NotificationCenter.default
+        // Using block-based observer to avoid subclassing NSObject
+        memoryWarningObserver = notifications.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification,
+            object: nil,
+            queue: OperationQueue.main,
+            using: { [unowned self] (notification : Notification!) -> Void in
+                self.onMemoryWarning()
+            }
+        )
+        
+        let originalFormat = Format<T>(name: HanekeGlobals.Cache.OriginalFormatName)
+        self.addFormat(originalFormat)
+    }
+    
+    deinit {
+        let notifications = NotificationCenter.default
+        notifications.removeObserver(memoryWarningObserver, name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
+    }
+    
+    open func set(value: T, key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, success succeed: ((T) -> ())? = nil) {
+        if let (format, memoryCache, diskCache) = self.formats[formatName] {
+            self.format(value: value, format: format) { formattedValue in
+                let wrapper = ObjectWrapper(value: formattedValue)
+                memoryCache.setObject(wrapper, forKey: key as AnyObject)
+                // Value data is sent as @autoclosure to be executed in the disk cache queue.
+                diskCache.setData(self.dataFromValue(formattedValue, format: format), key: key)
+                succeed?(formattedValue)
+            }
+        } else {
+            assertionFailure("Can't set value before adding format")
+        }
+    }
+    
+    @discardableResult open func fetch(key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
+        let fetch = Cache.buildFetch(failure: fail, success: succeed)
+        if let (format, memoryCache, diskCache) = self.formats[formatName] {
+            if let wrapper = memoryCache.object(forKey: key as AnyObject) as? ObjectWrapper, let result = wrapper.hnk_value as? T {
+                fetch.succeed(result)
+                diskCache.updateAccessDate(self.dataFromValue(result, format: format), key: key)
+                return fetch
+            }
+
+            self.fetchFromDiskCache(diskCache, key: key, memoryCache: memoryCache, failure: { error in
+                fetch.fail(error)
+            }) { value in
+                fetch.succeed(value)
+            }
+
+        } else {
+            let localizedFormat = NSLocalizedString("Format %@ not found", comment: "Error description")
+            let description = String(format:localizedFormat, formatName)
+            let error = errorWithCode(HanekeGlobals.Cache.ErrorCode.formatNotFound.rawValue, description: description)
+            fetch.fail(error)
+        }
+        return fetch
+    }
+    
+    @discardableResult open func fetch(fetcher : Fetcher<T>, formatName: String = HanekeGlobals.Cache.OriginalFormatName, failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
+        let key = fetcher.key
+        let fetch = Cache.buildFetch(failure: fail, success: succeed)
+        self.fetch(key: key, formatName: formatName, failure: { error in
+            if (error as NSError?)?.code == HanekeGlobals.Cache.ErrorCode.formatNotFound.rawValue {
+                fetch.fail(error)
+            }
+            
+            if let (format, _, _) = self.formats[formatName] {
+                self.fetchAndSet(fetcher, format: format, failure: { error in
+                    fetch.fail(error)
+                }) {value in
+                    fetch.succeed(value)
+                }
+            }
+            
+            // Unreachable code. Formats can't be removed from Cache.
+        }) { value in
+            fetch.succeed(value)
+        }
+        return fetch
+    }
+
+    open func remove(key: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName) {
+        if let (_, memoryCache, diskCache) = self.formats[formatName] {
+            memoryCache.removeObject(forKey: key as AnyObject)
+            diskCache.removeData(with: key)
+        }
+    }
+    
+    open func removeAll(_ completion: (() -> ())? = nil) {
+        let group = DispatchGroup()
+        for (_, (_, memoryCache, diskCache)) in self.formats {
+            memoryCache.removeAllObjects()
+            group.enter()
+            diskCache.removeAllData {
+                group.leave()
+            }
+        }
+        DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
+            let timeout = DispatchTime.now() + Double(Int64(60 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)
+            if group.wait(timeout: timeout) != .success {
+                Log.error(message: "removeAll timed out waiting for disk caches")
+            }
+            let path = self.cachePath
+            do {
+                try FileManager.default.removeItem(atPath: path)
+            } catch {
+                Log.error(message: "Failed to remove path \(path)", error: error)
+            }
+            if let completion = completion {
+                DispatchQueue.main.async {
+                    completion()
+                }
+            }
+        }
+    }
+
+    // MARK: Size
+
+    open var size: UInt64 {
+        var size: UInt64 = 0
+        for (_, (_, _, diskCache)) in self.formats {
+            diskCache.cacheQueue.sync { size += diskCache.size }
+        }
+        return size
+    }
+
+    // MARK: Notifications
+    
+    func onMemoryWarning() {
+        for (_, (_, memoryCache, _)) in self.formats {
+            memoryCache.removeAllObjects()
+        }
+    }
+    
+    // MARK: Formats
+
+    public var formats : [String : (Format<T>, NSCache<AnyObject, AnyObject>, DiskCache)] = [:]
+    
+    open func addFormat(_ format : Format<T>) {
+        let name = format.name
+        let formatPath = self.formatPath(withFormatName: name)
+        let memoryCache = NSCache<AnyObject, AnyObject>()
+        let diskCache = DiskCache(path: formatPath, capacity : format.diskCapacity)
+        self.formats[name] = (format, memoryCache, diskCache)
+    }
+    
+    // MARK: Internal
+    
+    lazy var cachePath: String = {
+        let basePath = DiskCache.basePath()
+        let cachePath = (basePath as NSString).appendingPathComponent(self.name)
+        return cachePath
+    }()
+    
+    func formatPath(withFormatName formatName: String) -> String {
+        let formatPath = (self.cachePath as NSString).appendingPathComponent(formatName)
+        do {
+            try FileManager.default.createDirectory(atPath: formatPath, withIntermediateDirectories: true, attributes: nil)
+        } catch {
+            Log.error(message: "Failed to create directory \(formatPath)", error: error)
+        }
+        return formatPath
+    }
+    
+    // MARK: Private
+    
+    func dataFromValue(_ value : T, format : Format<T>) -> Data? {
+        if let data = format.convertToData?(value) {
+            return data as Data
+        }
+        return value.asData()
+    }
+    
+    fileprivate func fetchFromDiskCache(_ diskCache : DiskCache, key: String, memoryCache : NSCache<AnyObject, AnyObject>, failure fail : ((Error?) -> ())?, success succeed : @escaping (T) -> ()) {
+        diskCache.fetchData(key: key, failure: { error in
+            if let block = fail {
+                if (error as NSError?)?.code == NSFileReadNoSuchFileError {
+                    let localizedFormat = NSLocalizedString("Object not found for key %@", comment: "Error description")
+                    let description = String(format:localizedFormat, key)
+                    let error = errorWithCode(HanekeGlobals.Cache.ErrorCode.objectNotFound.rawValue, description: description)
+                    block(error)
+                } else {
+                    block(error)
+                }
+            }
+        }) { data in
+            DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async(execute: {
+                let value = T.convertFromData(data)
+                if let value = value {
+                    let descompressedValue = self.decompressedImageIfNeeded(value)
+                    DispatchQueue.main.async(execute: {
+                        succeed(descompressedValue)
+                        let wrapper = ObjectWrapper(value: descompressedValue)
+                        memoryCache.setObject(wrapper, forKey: key as AnyObject)
+                    })
+                }
+            })
+        }
+    }
+    
+    fileprivate func fetchAndSet(_ fetcher : Fetcher<T>, format : Format<T>, failure fail : ((Error?) -> ())?, success succeed : @escaping (T) -> ()) {
+        fetcher.fetch(failure: { error in
+            let _ = fail?(error)
+        }) { value in
+            self.set(value: value, key: fetcher.key, formatName: format.name, success: succeed)
+        }
+    }
+    
+    fileprivate func format(value : T, format : Format<T>, success succeed : @escaping (T) -> ()) {
+        // HACK: Ideally Cache shouldn't treat images differently but I can't think of any other way of doing this that doesn't complicate the API for other types.
+        if format.isIdentity && !(value is UIImage) {
+            succeed(value)
+        } else {
+            DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
+                var formatted = format.apply(value)
+                
+                if let formattedImage = formatted as? UIImage {
+                    let originalImage = value as? UIImage
+                    if formattedImage === originalImage {
+                        formatted = self.decompressedImageIfNeeded(formatted)
+                    }
+                }
+                
+                DispatchQueue.main.async {
+                    succeed(formatted)
+                }
+            }
+        }
+    }
+    
+    fileprivate func decompressedImageIfNeeded(_ value : T) -> T {
+        if let image = value as? UIImage {
+            let decompressedImage = image.hnk_decompressedImage() as? T
+            return decompressedImage!
+        }
+        return value
+    }
+    
+    fileprivate class func buildFetch(failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
+        let fetch = Fetch<T>()
+        if let succeed = succeed {
+            fetch.onSuccess(succeed)
+        }
+        if let fail = fail {
+            fetch.onFailure(fail)
+        }
+        return fetch
+    }
+    
+    // MARK: Convenience fetch
+    // Ideally we would put each of these in the respective fetcher file as a Cache extension. Unfortunately, this fails to link when using the framework in a project as of Xcode 6.1.
+    
+    open func fetch(key: String, value getValue : @autoclosure @escaping () -> T.Result, formatName: String = HanekeGlobals.Cache.OriginalFormatName, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
+        let fetcher = SimpleFetcher<T>(key: key, value: getValue())
+        return self.fetch(fetcher: fetcher, formatName: formatName, success: succeed)
+    }
+    
+    open func fetch(path: String, formatName: String = HanekeGlobals.Cache.OriginalFormatName,  failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
+        let fetcher = DiskFetcher<T>(path: path)
+        return self.fetch(fetcher: fetcher, formatName: formatName, failure: fail, success: succeed)
+    }
+    
+    open func fetch(URL : Foundation.URL, formatName: String = HanekeGlobals.Cache.OriginalFormatName,  failure fail : Fetch<T>.Failer? = nil, success succeed : Fetch<T>.Succeeder? = nil) -> Fetch<T> {
+        let fetcher = NetworkFetcher<T>(URL: URL)
+        return self.fetch(fetcher: fetcher, formatName: formatName, failure: fail, success: succeed)
+    }
+    
+}

+ 229 - 0
o2ios/O2Platform/Framework/Haneke/CryptoSwiftMD5.swift

@@ -0,0 +1,229 @@
+//
+//  CryptoSwiftMD5.Swif
+//
+// To date, adding CommonCrypto to a Swift framework is problematic. See:
+// http://stackoverflow.com/questions/25248598/importing-commoncrypto-in-a-swift-framework
+// We're using a subset of CryptoSwift as a (temporary?) alternative.
+// The following is an altered source version that only includes MD5. The original software can be found at:
+// https://github.com/krzyzanowskim/CryptoSwift
+// This is the original copyright notice:
+
+/*
+Copyright (C) 2014 Marcin Krzyżanowski <marcin.krzyzanowski@gmail.com>
+This software is provided 'as-is', without any express or implied warranty.
+
+In no event will the authors be held liable for any damages arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
+
+- The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
+- Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
+- This notice may not be removed or altered from any source or binary distribution.
+*/
+
+import Foundation
+
+/** array of bytes, little-endian representation */
+func arrayOfBytes<T>(value:T, length:Int? = nil) -> [UInt8] {
+    let totalBytes = length ?? MemoryLayout<T>.size
+    
+    let valuePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
+    valuePointer.pointee = value
+    
+    let bytesPointer = UnsafeMutablePointer<UInt8>(OpaquePointer(valuePointer))
+    var bytes = Array<UInt8>(repeating: 0, count: totalBytes)
+    for j in 0..<min(MemoryLayout<T>.size,totalBytes) {
+        bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee
+    }
+    
+    valuePointer.deinitialize(count: 1)
+    valuePointer.deallocate()
+    
+    return bytes
+}
+
+extension Int {
+    /** Array of bytes with optional padding (little-endian) */
+    public func bytes(totalBytes: Int = MemoryLayout<Int>.size) -> [UInt8] {
+        return arrayOfBytes(value: self, length: totalBytes)
+    }
+    
+}
+
+extension NSMutableData {
+    
+    /** Convenient way to append bytes */
+    internal func appendBytes(arrayOfBytes: [UInt8]) {
+        self.append(arrayOfBytes, length: arrayOfBytes.count)
+    }
+    
+}
+
+struct BytesSequence: Sequence {
+    let chunkSize: Int
+    let data: [UInt8]
+    
+    func makeIterator() -> AnyIterator<ArraySlice<UInt8>> {
+        var offset:Int = 0
+        return AnyIterator {
+            let end = Swift.min(self.chunkSize, self.data.count - offset)
+            let result = self.data[offset..<offset + end]
+            offset += result.count
+            return !result.isEmpty ? result : nil
+        }
+    }
+}
+
+class HashBase {
+    
+    static let size:Int = 16 // 128 / 8
+    let message: [UInt8]
+    
+    init (_ message: [UInt8]) {
+        self.message = message
+    }
+    
+    /** Common part for hash calculation. Prepare header data. */
+    func prepare(_ len:Int) -> [UInt8] {
+        var tmpMessage = message
+        
+        // Step 1. Append Padding Bits
+        tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message
+        
+        // append "0" bit until message length in bits ≡ 448 (mod 512)
+        var msgLength = tmpMessage.count
+        var counter = 0
+        
+        while msgLength % len != (len - 8) {
+            counter += 1
+            msgLength += 1
+        }
+        
+        tmpMessage += Array<UInt8>(repeating: 0, count: counter)
+        return tmpMessage
+    }
+}
+
+func rotateLeft(v: UInt32, n: UInt32) -> UInt32 {
+    return ((v << n) & 0xFFFFFFFF) | (v >> (32 - n))
+}
+
+func sliceToUInt32Array(_ slice: ArraySlice<UInt8>) -> [UInt32] {
+    var result = [UInt32]()
+    result.reserveCapacity(16)
+    for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt32>.size) {
+        let val1:UInt32 = (UInt32(slice[idx.advanced(by: 3)]) << 24)
+        let val2:UInt32 = (UInt32(slice[idx.advanced(by: 2)]) << 16)
+        let val3:UInt32 = (UInt32(slice[idx.advanced(by: 1)]) << 8)
+        let val4:UInt32 = UInt32(slice[idx])
+        let val:UInt32 = val1 | val2 | val3 | val4
+        result.append(val)
+    }
+    return result
+}
+
+class MD5 : HashBase {
+    
+    
+    /** specifies the per-round shift amounts */
+    private let s: [UInt32] = [7, 12, 17, 22,  7, 12, 17, 22,  7, 12, 17, 22,  7, 12, 17, 22,
+                                    5,  9, 14, 20,  5,  9, 14, 20,  5,  9, 14, 20,  5,  9, 14, 20,
+                                    4, 11, 16, 23,  4, 11, 16, 23,  4, 11, 16, 23,  4, 11, 16, 23,
+                                    6, 10, 15, 21,  6, 10, 15, 21,  6, 10, 15, 21,  6, 10, 15, 21]
+    
+    /** binary integer part of the sines of integers (Radians) */
+    private let k: [UInt32] = [0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,
+                                    0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,
+                                    0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,
+                                    0x6b901122,0xfd987193,0xa679438e,0x49b40821,
+                                    0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,
+                                    0xd62f105d,0x2441453,0xd8a1e681,0xe7d3fbc8,
+                                    0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,
+                                    0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a,
+                                    0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,
+                                    0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,
+                                    0x289b7ec6,0xeaa127fa,0xd4ef3085,0x4881d05,
+                                    0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,
+                                    0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,
+                                    0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,
+                                    0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,
+                                    0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391]
+    
+    private let h: [UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
+    
+    func calculate() -> [UInt8] {
+        var tmpMessage = prepare(64)
+        tmpMessage.reserveCapacity(tmpMessage.count + 4)
+        
+        // initialize hh with hash values
+        var hh = h
+        
+        // Step 2. Append Length a 64-bit representation of lengthInBits
+        let lengthInBits = (message.count * 8)
+        let lengthBytes = lengthInBits.bytes(totalBytes: 64 / 8)
+        tmpMessage += lengthBytes.reversed()
+        
+        // Process the message in successive 512-bit chunks:
+        let chunkSizeBytes = 512 / 8 // 64
+        for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) {
+            // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15
+            var M = sliceToUInt32Array(chunk)
+            assert(M.count == 16, "Invalid array")
+            
+            // Initialize hash value for this chunk:
+            var A:UInt32 = hh[0]
+            var B:UInt32 = hh[1]
+            var C:UInt32 = hh[2]
+            var D:UInt32 = hh[3]
+            
+            var dTemp:UInt32 = 0
+            
+            // Main loop
+            for j in 0..<k.count {
+                var g = 0
+                var F:UInt32 = 0
+                
+                switch (j) {
+                case 0...15:
+                    F = (B & C) | ((~B) & D)
+                    g = j
+                    break
+                case 16...31:
+                    F = (D & B) | (~D & C)
+                    g = (5 * j + 1) % 16
+                    break
+                case 32...47:
+                    F = B ^ C ^ D
+                    g = (3 * j + 5) % 16
+                    break
+                case 48...63:
+                    F = C ^ (B | (~D))
+                    g = (7 * j) % 16
+                    break
+                default:
+                    break
+                }
+                dTemp = D
+                D = C
+                C = B
+                B = B &+ rotateLeft(v: A &+ F &+ k[j] &+ M[g], n: s[j])
+                A = dTemp
+            }
+            
+            hh[0] = hh[0] &+ A
+            hh[1] = hh[1] &+ B
+            hh[2] = hh[2] &+ C
+            hh[3] = hh[3] &+ D
+        }
+        
+        var result = [UInt8]()
+        result.reserveCapacity(hh.count / 4)
+        
+        hh.forEach {
+            let itemLE = $0.littleEndian
+            result += [UInt8(itemLE & 0xff), UInt8((itemLE >> 8) & 0xff), UInt8((itemLE >> 16) & 0xff), UInt8((itemLE >> 24) & 0xff)]
+        }
+        
+        return result
+    }
+}

+ 129 - 0
o2ios/O2Platform/Framework/Haneke/Data.swift

@@ -0,0 +1,129 @@
+//
+//  Data.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 9/19/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+// See: http://stackoverflow.com/questions/25922152/not-identical-to-self
+public protocol DataConvertible {
+    associatedtype Result
+    
+    static func convertFromData(_ data:Data) -> Result?
+}
+
+public protocol DataRepresentable {
+    
+    func asData() -> Data!
+}
+
+private let imageSync = NSLock()
+
+extension UIImage : DataConvertible, DataRepresentable {
+    
+    public typealias Result = UIImage
+
+    // HACK: UIImage data initializer is no longer thread safe. See: https://github.com/AFNetworking/AFNetworking/issues/2572#issuecomment-115854482
+    static func safeImageWithData(_ data:Data) -> Result? {
+        imageSync.lock()
+        let image = UIImage(data:data, scale: scale)
+        imageSync.unlock()
+        return image
+    }
+    
+    public class func convertFromData(_ data: Data) -> Result? {
+        let image = UIImage.safeImageWithData(data)
+        return image
+    }
+    
+    public func asData() -> Data! {
+        return self.hnk_data() as Data?
+    }
+    
+    fileprivate static let scale = UIScreen.main.scale
+    
+}
+
+extension String : DataConvertible, DataRepresentable {
+    
+    public typealias Result = String
+    
+    public static func convertFromData(_ data: Data) -> Result? {
+        let string = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
+        return string as Result?
+    }
+    
+    public func asData() -> Data! {
+        return self.data(using: String.Encoding.utf8)
+    }
+    
+}
+
+extension Data : DataConvertible, DataRepresentable {
+    
+    public typealias Result = Data
+    
+    public static func convertFromData(_ data: Data) -> Result? {
+        return data
+    }
+    
+    public func asData() -> Data! {
+        return self
+    }
+    
+}
+
+public enum JSONV : DataConvertible, DataRepresentable {
+    public typealias Result = JSONV
+    
+    case Dictionary([String:AnyObject])
+    case Array([AnyObject])
+    
+    public static func convertFromData(_ data: Data) -> Result? {
+        do {
+            let object : Any = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions())
+            switch (object) {
+            case let dictionary as [String:AnyObject]:
+                return JSONV.Dictionary(dictionary)
+            case let array as [AnyObject]:
+                return JSONV.Array(array)
+            default:
+                return nil
+            }
+        } catch {
+            Log.error(message: "Invalid JSON data", error: error)
+            return nil
+        }
+    }
+    
+    public func asData() -> Data! {
+        switch (self) {
+        case .Dictionary(let dictionary):
+            return try? JSONSerialization.data(withJSONObject: dictionary, options: JSONSerialization.WritingOptions())
+        case .Array(let array):
+            return try? JSONSerialization.data(withJSONObject: array, options: JSONSerialization.WritingOptions())
+        }
+    }
+    
+    public var array : [AnyObject]! {
+        switch (self) {
+        case .Dictionary(_):
+            return nil
+        case .Array(let array):
+            return array
+        }
+    }
+    
+    public var dictionary : [String:AnyObject]! {
+        switch (self) {
+        case .Dictionary(let dictionary):
+            return dictionary
+        case .Array(_):
+            return nil
+        }
+    }
+    
+}

+ 237 - 0
o2ios/O2Platform/Framework/Haneke/DiskCache.swift

@@ -0,0 +1,237 @@
+//
+//  DiskCache.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 8/10/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import Foundation
+
+open class DiskCache {
+    
+    open class func basePath() -> String {
+        let cachesPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]
+        let hanekePathComponent = HanekeGlobals.Domain
+        let basePath = (cachesPath as NSString).appendingPathComponent(hanekePathComponent)
+        // TODO: Do not recaculate basePath value
+        return basePath
+    }
+    
+    public let path: String
+
+    open var size : UInt64 = 0
+
+    open var capacity : UInt64 = 0 {
+        didSet {
+            self.cacheQueue.async(execute: {
+                self.controlCapacity()
+            })
+        }
+    }
+
+    open lazy var cacheQueue : DispatchQueue = {
+        let queueName = HanekeGlobals.Domain + "." + (self.path as NSString).lastPathComponent
+        let cacheQueue = DispatchQueue(label: queueName, attributes: [])
+        return cacheQueue
+    }()
+    
+    public init(path: String, capacity: UInt64 = UINT64_MAX) {
+        self.path = path
+        self.capacity = capacity
+        self.cacheQueue.async(execute: {
+            self.calculateSize()
+            self.controlCapacity()
+        })
+    }
+    
+    open func setData( _ getData: @autoclosure @escaping () -> Data?, key: String) {
+        cacheQueue.async(execute: {
+            if let data = getData() {
+                self.setDataSync(data, key: key)
+            } else {
+                Log.error(message: "Failed to get data for key \(key)")
+            }
+        })
+    }
+    
+    open func fetchData(key: String, failure fail: ((Error?) -> ())? = nil, success succeed: @escaping (Data) -> ()) {
+        cacheQueue.async {
+            let path = self.path(forKey: key)
+            do {
+                let data = try Data(contentsOf: URL(fileURLWithPath: path), options: Data.ReadingOptions())
+                DispatchQueue.main.async {
+                    succeed(data)
+                }
+                self.updateDiskAccessDate(atPath: path)
+            } catch {
+                if let block = fail {
+                    DispatchQueue.main.async {
+                        block(error)
+                    }
+                }
+            }
+        }
+    }
+
+    open func removeData(with key: String) {
+        cacheQueue.async(execute: {
+            let path = self.path(forKey: key)
+            self.removeFile(atPath: path)
+        })
+    }
+    
+    open func removeAllData(_ completion: (() -> ())? = nil) {
+        let fileManager = FileManager.default
+        let cachePath = self.path
+        cacheQueue.async(execute: {
+            do {
+                let contents = try fileManager.contentsOfDirectory(atPath: cachePath)
+                for pathComponent in contents {
+                    let path = (cachePath as NSString).appendingPathComponent(pathComponent)
+                    do {
+                        try fileManager.removeItem(atPath: path)
+                    } catch {
+                        Log.error(message: "Failed to remove path \(path)", error: error)
+                    }
+                }
+                self.calculateSize()
+            } catch {
+                Log.error(message: "Failed to list directory", error: error)
+            }
+            if let completion = completion {
+                DispatchQueue.main.async {
+                    completion()
+                }
+            }
+        })
+    }
+
+    open func updateAccessDate( _ getData: @autoclosure @escaping () -> Data?, key: String) {
+        cacheQueue.async(execute: {
+            let path = self.path(forKey: key)
+            let fileManager = FileManager.default
+            if (!(fileManager.fileExists(atPath: path) && self.updateDiskAccessDate(atPath: path))){
+                if let data = getData() {
+                    self.setDataSync(data, key: key)
+                } else {
+                    Log.error(message: "Failed to get data for key \(key)")
+                }
+            }
+        })
+    }
+
+    open func path(forKey key: String) -> String {
+        let escapedFilename = key.escapedFilename()
+        let filename = escapedFilename.count < Int(NAME_MAX) ? escapedFilename : key.MD5Filename()
+        let keyPath = (self.path as NSString).appendingPathComponent(filename)
+        return keyPath
+    }
+    
+    // MARK: Private
+    
+    fileprivate func calculateSize() {
+        let fileManager = FileManager.default
+        size = 0
+        let cachePath = self.path
+        do {
+            let contents = try fileManager.contentsOfDirectory(atPath: cachePath)
+            for pathComponent in contents {
+                let path = (cachePath as NSString).appendingPathComponent(pathComponent)
+                do {
+                    let attributes: [FileAttributeKey: Any] = try fileManager.attributesOfItem(atPath: path)
+                    if let fileSize = attributes[FileAttributeKey.size] as? UInt64 {
+                        size += fileSize
+                    }
+                } catch {
+                    Log.error(message: "Failed to list directory", error: error)
+                }
+            }
+            
+        } catch {
+            Log.error(message: "Failed to list directory", error: error)
+        }
+    }
+    
+    fileprivate func controlCapacity() {
+        if self.size <= self.capacity { return }
+        
+        let fileManager = FileManager.default
+        let cachePath = self.path
+        fileManager.enumerateContentsOfDirectory(atPath: cachePath, orderedByProperty: URLResourceKey.contentModificationDateKey.rawValue, ascending: true) { (URL : URL, _, stop : inout Bool) -> Void in
+            
+            self.removeFile(atPath: URL.path)
+
+            stop = self.size <= self.capacity
+        }
+    }
+    
+    fileprivate func setDataSync(_ data: Data, key: String) {
+        let path = self.path(forKey: key)
+        let fileManager = FileManager.default
+        let previousAttributes : [FileAttributeKey: Any]? = try? fileManager.attributesOfItem(atPath: path)
+        
+        do {
+            try data.write(to: URL(fileURLWithPath: path), options: Data.WritingOptions.atomicWrite)
+        } catch {
+            Log.error(message: "Failed to write key \(key)", error: error)
+        }
+        
+        if let attributes = previousAttributes {
+            if let fileSize = attributes[FileAttributeKey.size] as? UInt64 {
+                substract(size: fileSize)
+            }
+        }
+        self.size += UInt64(data.count)
+        self.controlCapacity()
+    }
+    
+    @discardableResult fileprivate func updateDiskAccessDate(atPath path: String) -> Bool {
+        let fileManager = FileManager.default
+        let now = Date()
+        do {
+            try fileManager.setAttributes([FileAttributeKey.modificationDate : now], ofItemAtPath: path)
+            return true
+        } catch {
+            Log.error(message: "Failed to update access date", error: error)
+            return false
+        }
+    }
+    
+    fileprivate func removeFile(atPath path: String) {
+        let fileManager = FileManager.default
+        do {
+            let attributes: [FileAttributeKey: Any] = try fileManager.attributesOfItem(atPath: path)
+            do {
+                try fileManager.removeItem(atPath: path)
+                if let fileSize = attributes[FileAttributeKey.size] as? UInt64 {
+                    substract(size: fileSize)
+                }
+            } catch {
+                Log.error(message: "Failed to remove file", error: error)
+            }
+        } catch {
+            if isNoSuchFileError(error) {
+                Log.debug(message: "File not found", error: error)
+            } else {
+                Log.error(message: "Failed to remove file", error: error)
+            }
+        }
+    }
+
+    fileprivate func substract(size : UInt64) {
+        if (self.size >= size) {
+            self.size -= size
+        } else {
+            Log.error(message: "Disk cache size (\(self.size)) is smaller than size to substract (\(size))")
+            self.size = 0
+        }
+    }
+}
+
+private func isNoSuchFileError(_ error : Error?) -> Bool {
+    if let error = error {
+        return NSCocoaErrorDomain == (error as NSError).domain && (error as NSError).code == NSFileReadNoSuchFileError
+    }
+    return false
+}

+ 92 - 0
o2ios/O2Platform/Framework/Haneke/DiskFetcher.swift

@@ -0,0 +1,92 @@
+//
+//  DiskFetcher.swift
+//  Haneke
+//
+//  Created by Joan Romano on 9/16/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import Foundation
+
+extension HanekeGlobals {
+
+    // It'd be better to define this in the DiskFetcher class but Swift doesn't allow to declare an enum in a generic type
+    public struct DiskFetcher {
+        
+        public enum ErrorCode : Int {
+            case invalidData = -500
+        }
+        
+    }
+    
+}
+
+open class DiskFetcher<T : DataConvertible> : Fetcher<T> {
+    
+    let path: String
+    var cancelled = false
+    
+    public init(path: String) {
+        self.path = path
+        let key = path
+        super.init(key: key)
+    }
+    
+    // MARK: Fetcher
+    
+    
+    open override func fetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) {
+        self.cancelled = false
+        DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async(execute: { [weak self] in
+            if let strongSelf = self {
+                strongSelf.privateFetch(failure: fail, success: succeed)
+            }
+        })
+    }
+    
+    open override func cancelFetch() {
+        self.cancelled = true
+    }
+    
+    // MARK: Private
+    
+    fileprivate func privateFetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) {
+        if self.cancelled {
+            return
+        }
+        
+        let data : Data
+        do {
+            data = try Data(contentsOf: URL(fileURLWithPath: self.path), options: Data.ReadingOptions())
+        } catch {
+            DispatchQueue.main.async {
+                if self.cancelled {
+                    return
+                }
+                fail(error)
+            }
+            return
+        }
+        
+        if self.cancelled {
+            return
+        }
+        
+        guard let value : T.Result = T.convertFromData(data) else {
+            let localizedFormat = NSLocalizedString("Failed to convert value from data at path %@", comment: "Error description")
+            let description = String(format:localizedFormat, self.path)
+            let error = errorWithCode(HanekeGlobals.DiskFetcher.ErrorCode.invalidData.rawValue, description: description)
+            DispatchQueue.main.async {
+                fail(error)
+            }
+            return
+        }
+        
+        DispatchQueue.main.async(execute: {
+            if self.cancelled {
+                return
+            }
+            succeed(value)
+        })
+    }
+}

+ 89 - 0
o2ios/O2Platform/Framework/Haneke/Fetch.swift

@@ -0,0 +1,89 @@
+//
+//  Fetch.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 9/28/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import Foundation
+
+enum FetchState<T> {
+    case pending
+    // Using Wrapper as a workaround for error 'unimplemented IR generation feature non-fixed multi-payload enum layout'
+    // See: http://swiftradar.tumblr.com/post/88314603360/swift-fails-to-compile-enum-with-two-data-cases
+    // See: http://owensd.io/2014/08/06/fixed-enum-layout.html
+    case success(Wrapper<T>)
+    case failure(Error?)
+}
+
+open class Fetch<T> {
+    
+    public typealias Succeeder = (T) -> ()
+    
+    public typealias Failer = (Error?) -> ()
+    
+    fileprivate var onSuccess : Succeeder?
+    
+    fileprivate var onFailure : Failer?
+    
+    fileprivate var state : FetchState<T> = FetchState.pending
+    
+    public init() {}
+    
+    @discardableResult open func onSuccess(_ onSuccess: @escaping Succeeder) -> Self {
+        self.onSuccess = onSuccess
+        switch self.state {
+        case FetchState.success(let wrapper):
+            onSuccess(wrapper.value)
+        default:
+            break
+        }
+        return self
+    }
+    
+    @discardableResult open func onFailure(_ onFailure: @escaping Failer) -> Self {
+        self.onFailure = onFailure
+        switch self.state {
+        case FetchState.failure(let error):
+            onFailure(error)
+        default:
+            break
+        }
+        return self
+    }
+    
+    func succeed(_ value: T) {
+        self.state = FetchState.success(Wrapper(value))
+        self.onSuccess?(value)
+    }
+    
+    func fail(_ error: Error? = nil) {
+        self.state = FetchState.failure(error)
+        self.onFailure?(error)
+    }
+    
+    var hasFailed : Bool {
+        switch self.state {
+        case FetchState.failure(_):
+            return true
+        default:
+            return false
+            }
+    }
+    
+    var hasSucceeded : Bool {
+        switch self.state {
+        case FetchState.success(_):
+            return true
+        default:
+            return false
+        }
+    }
+    
+}
+
+open class Wrapper<T> {
+    public let value: T
+    public init(_ value: T) { self.value = value }
+}

+ 41 - 0
o2ios/O2Platform/Framework/Haneke/Fetcher.swift

@@ -0,0 +1,41 @@
+//
+//  Fetcher.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 9/9/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+// See: http://stackoverflow.com/questions/25915306/generic-closure-in-protocol
+open class Fetcher<T : DataConvertible> {
+
+    public let key: String
+    
+    public init(key: String) {
+        self.key = key
+    }
+    
+    open func fetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) {}
+    
+    open func cancelFetch() {}
+}
+
+class SimpleFetcher<T : DataConvertible> : Fetcher<T> {
+    
+    let getValue : () -> T.Result
+    
+    init(key: String, value getValue : @autoclosure @escaping () -> T.Result) {
+        self.getValue = getValue
+        super.init(key: key)
+    }
+    
+    override func fetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) {
+        let value = getValue()
+        succeed(value)
+    }
+    
+    override func cancelFetch() {}
+    
+}

+ 93 - 0
o2ios/O2Platform/Framework/Haneke/Format.swift

@@ -0,0 +1,93 @@
+//
+//  Format.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 8/27/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+public struct Format<T> {
+    
+    public let name: String
+    
+    public let diskCapacity : UInt64
+    
+    public var transform : ((T) -> (T))?
+    
+    public var convertToData : ((T) -> Data)?
+
+    public init(name: String, diskCapacity : UInt64 = UINT64_MAX, transform: ((T) -> (T))? = nil) {
+        self.name = name
+        self.diskCapacity = diskCapacity
+        self.transform = transform
+    }
+    
+    public func apply(_ value : T) -> T {
+        var transformed = value
+        if let transform = self.transform {
+            transformed = transform(value)
+        }
+        return transformed
+    }
+    
+    var isIdentity : Bool {
+        return self.transform == nil
+    }
+
+}
+
+public struct ImageResizer {
+    
+    public enum ScaleMode: String {
+        case Fill = "fill", AspectFit = "aspectfit", AspectFill = "aspectfill", None = "none"
+    }
+    
+    public typealias T = UIImage
+    
+    public let allowUpscaling : Bool
+    
+    public let size : CGSize
+    
+    public let scaleMode: ScaleMode
+    
+    public let compressionQuality : Float
+    
+    public init(size: CGSize = CGSize.zero, scaleMode: ScaleMode = .None, allowUpscaling: Bool = true, compressionQuality: Float = 1.0) {
+        self.size = size
+        self.scaleMode = scaleMode
+        self.allowUpscaling = allowUpscaling
+        self.compressionQuality = compressionQuality
+    }
+    
+    public func resizeImage(_ image: UIImage) -> UIImage {
+        var resizeToSize: CGSize
+        switch self.scaleMode {
+        case .Fill:
+            resizeToSize = self.size
+        case .AspectFit:
+            resizeToSize = image.size.hnk_aspectFitSize(self.size)
+        case .AspectFill:
+            resizeToSize = image.size.hnk_aspectFillSize(self.size)
+        case .None:
+            return image
+        }
+        assert(self.size.width > 0 && self.size.height > 0, "Expected non-zero size. Use ScaleMode.None to avoid resizing.")
+        
+        // If does not allow to scale up the image
+        if (!self.allowUpscaling) {
+            if (resizeToSize.width > image.size.width || resizeToSize.height > image.size.height) {
+                return image
+            }
+        }
+        
+        // Avoid unnecessary computations
+        if (resizeToSize.width == image.size.width && resizeToSize.height == image.size.height) {
+            return image
+        }
+        
+        let resizedImage = image.hnk_imageByScaling(toSize: resizeToSize)
+        return resizedImage
+    }
+}

+ 19 - 0
o2ios/O2Platform/Framework/Haneke/Haneke.h

@@ -0,0 +1,19 @@
+//
+//  Haneke.h
+//  Haneke
+//
+//  Created by Luis Ascorbe on 23/07/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+//! Project version number for Haneke.
+FOUNDATION_EXPORT double HanekeVersionNumber;
+
+//! Project version string for Haneke.
+FOUNDATION_EXPORT const unsigned char HanekeVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import <Haneke/PublicHeader.h>
+
+

+ 55 - 0
o2ios/O2Platform/Framework/Haneke/Haneke.swift

@@ -0,0 +1,55 @@
+//
+//  Haneke.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 9/9/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+public struct HanekeGlobals {
+    
+    public static let Domain = "io.haneke"
+    
+}
+
+public struct Shared {
+    
+    public static var imageCache : Cache<UIImage> {
+        struct Static {
+            static let name = "shared-images"
+            static let cache = Cache<UIImage>(name: name)
+        }
+        return Static.cache
+    }
+    
+    public static var dataCache : Cache<Data> {
+        struct Static {
+            static let name = "shared-data"
+            static let cache = Cache<Data>(name: name)
+        }
+        return Static.cache
+    }
+    
+    public static var stringCache : Cache<String> {
+        struct Static {
+            static let name = "shared-strings"
+            static let cache = Cache<String>(name: name)
+        }
+        return Static.cache
+    }
+    
+    public static var JSONCache : Cache<JSONV> {
+        struct Static {
+            static let name = "shared-json"
+            static let cache = Cache<JSONV>(name: name)
+        }
+        return Static.cache
+    }
+}
+
+func errorWithCode(_ code: Int, description: String) -> Error {
+    let userInfo = [NSLocalizedDescriptionKey: description]
+    return NSError(domain: HanekeGlobals.Domain, code: code, userInfo: userInfo) as Error
+}

+ 38 - 0
o2ios/O2Platform/Framework/Haneke/Log.swift

@@ -0,0 +1,38 @@
+//
+//  Log.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 11/10/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import Foundation
+
+struct Log {
+    
+    fileprivate static let Tag = "[HANEKE]"
+    
+    fileprivate enum Level : String {
+        case Debug = "[DEBUG]"
+        case Error = "[ERROR]"
+    }
+    
+    fileprivate static func log(_ level: Level, _ message: @autoclosure () -> String, _ error: Error? = nil) {
+        if let error = error {
+            print("\(Tag)\(level.rawValue) \(message()) with error \(error)")
+        } else {
+            print("\(Tag)\(level.rawValue) \(message())")
+        }
+    }
+    
+    static func debug(message: @autoclosure () -> String, error: Error? = nil) {
+        #if DEBUG
+        log(.Debug, message(), error)
+        #endif
+    }
+    
+    static func error(message: @autoclosure () -> String, error: Error? = nil) {
+        log(.Error, message(), error)
+    }
+    
+}

+ 65 - 0
o2ios/O2Platform/Framework/Haneke/NSFileManager+Haneke.swift

@@ -0,0 +1,65 @@
+//
+//  NSFileManager+Haneke.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 8/26/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import Foundation
+
+extension FileManager {
+
+    func enumerateContentsOfDirectory(atPath path: String, orderedByProperty property: String, ascending: Bool, usingBlock block: (URL, Int, inout Bool) -> Void ) {
+
+        let directoryURL = URL(fileURLWithPath: path)
+        do {
+            let contents = try self.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: [URLResourceKey(rawValue: property)], options: FileManager.DirectoryEnumerationOptions())
+            let sortedContents = contents.sorted(by: {(URL1: URL, URL2: URL) -> Bool in
+                
+                // Maybe there's a better way to do this. See: http://stackoverflow.com/questions/25502914/comparing-anyobject-in-swift
+                
+                var value1 : AnyObject?
+                do {
+                    try (URL1 as NSURL).getResourceValue(&value1, forKey: URLResourceKey(rawValue: property))
+                } catch {
+                    return true
+                }
+                var value2 : AnyObject?
+                do {
+                    try (URL2 as NSURL).getResourceValue(&value2, forKey: URLResourceKey(rawValue: property))
+                } catch {
+                    return false
+                }
+                
+                if let string1 = value1 as? String, let string2 = value2 as? String {
+                    return ascending ? string1 < string2 : string2 < string1
+                }
+                
+                if let date1 = value1 as? Date, let date2 = value2 as? Date {
+                    return ascending ? date1 < date2 : date2 < date1
+                }
+                
+                if let number1 = value1 as? NSNumber, let number2 = value2 as? NSNumber {
+                    return ascending ? number1 < number2 : number2 < number1
+                }
+                
+                return false
+            })
+            
+            for (i, v) in sortedContents.enumerated() {
+                var stop : Bool = false
+                block(v, i, &stop)
+                if stop { break }
+            }
+
+        } catch {
+            Log.error(message: "Failed to list directory", error: error)
+        }
+    }
+
+}
+
+func < (lhs: NSNumber, rhs: NSNumber) -> Bool {
+    return lhs.compare(rhs) == ComparisonResult.orderedAscending
+}

+ 22 - 0
o2ios/O2Platform/Framework/Haneke/NSHTTPURLResponse+Haneke.swift

@@ -0,0 +1,22 @@
+//
+//  NSHTTPURLResponse+Haneke.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 1/2/16.
+//  Copyright © 2016 Haneke. All rights reserved.
+//
+
+import Foundation
+
+extension HTTPURLResponse {
+
+    func hnk_isValidStatusCode() -> Bool {
+        switch self.statusCode {
+        case 200...201:
+            return true
+        default:
+            return false
+        }
+    }
+
+}

+ 22 - 0
o2ios/O2Platform/Framework/Haneke/NSURLResponse+Haneke.swift

@@ -0,0 +1,22 @@
+//
+//  NSHTTPURLResponse+Haneke.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 9/12/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import Foundation
+
+extension URLResponse {
+    
+    func hnk_validateLength(ofData data: Data) -> Bool {
+        let expectedContentLength = self.expectedContentLength
+        if (expectedContentLength > -1) {
+            let dataLength = data.count
+            return Int64(dataLength) >= expectedContentLength
+        }
+        return true
+    }
+    
+}

+ 105 - 0
o2ios/O2Platform/Framework/Haneke/NetworkFetcher.swift

@@ -0,0 +1,105 @@
+//
+//  NetworkFetcher.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 9/12/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+extension HanekeGlobals {
+    
+    // It'd be better to define this in the NetworkFetcher class but Swift doesn't allow to declare an enum in a generic type
+    public struct NetworkFetcher {
+
+        public enum ErrorCode : Int {
+            case invalidData = -400
+            case missingData = -401
+            case invalidStatusCode = -402
+        }
+        
+    }
+    
+}
+
+open class NetworkFetcher<T : DataConvertible> : Fetcher<T> {
+    
+    let URL : Foundation.URL
+    
+    public init(URL : Foundation.URL) {
+        self.URL = URL
+
+        let key =  URL.absoluteString
+        super.init(key: key)
+    }
+    
+    open var session : URLSession { return URLSession.shared }
+    
+    var task : URLSessionDataTask? = nil
+    
+    var cancelled = false
+    
+    // MARK: Fetcher
+    
+    open override func fetch(failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) {
+        self.cancelled = false
+        self.task = self.session.dataTask(with: self.URL) {[weak self] (data, response, error) -> Void in
+            if let strongSelf = self {
+                strongSelf.onReceive(data: data, response: response, error: error, failure: fail, success: succeed)
+            }
+        }
+        self.task?.resume()
+    }
+    
+    open override func cancelFetch() {
+        self.task?.cancel()
+        self.cancelled = true
+    }
+    
+    // MARK: Private
+    
+    fileprivate func onReceive(data: Data!, response: URLResponse!, error: Error!, failure fail: @escaping ((Error?) -> ()), success succeed: @escaping (T.Result) -> ()) {
+
+        if cancelled { return }
+        
+        let URL = self.URL
+        
+        if let error = error {
+            if ((error as NSError).domain == NSURLErrorDomain && (error as NSError).code == NSURLErrorCancelled) { return }
+            
+            Log.debug(message: "Request \(URL.absoluteString) failed", error: error)
+            DispatchQueue.main.async(execute: { fail(error) })
+            return
+        }
+        
+        if let httpResponse = response as? HTTPURLResponse , !httpResponse.hnk_isValidStatusCode() {
+            let description = HTTPURLResponse.localizedString(forStatusCode: httpResponse.statusCode)
+            self.failWithCode(.invalidStatusCode, localizedDescription: description, failure: fail)
+            return
+        }
+
+        if !response.hnk_validateLength(ofData: data) {
+            let localizedFormat = NSLocalizedString("Request expected %ld bytes and received %ld bytes", comment: "Error description")
+            let description = String(format:localizedFormat, response.expectedContentLength, data.count)
+            self.failWithCode(.missingData, localizedDescription: description, failure: fail)
+            return
+        }
+        
+        guard let value = T.convertFromData(data) else {
+            let localizedFormat = NSLocalizedString("Failed to convert value from data at URL %@", comment: "Error description")
+            let description = String(format:localizedFormat, URL.absoluteString)
+            self.failWithCode(.invalidData, localizedDescription: description, failure: fail)
+            return
+        }
+
+        DispatchQueue.main.async { succeed(value) }
+
+    }
+    
+    fileprivate func failWithCode(_ code: HanekeGlobals.NetworkFetcher.ErrorCode, localizedDescription: String, failure fail: @escaping ((Error?) -> ())) {
+        let error = errorWithCode(code.rawValue, description: localizedDescription)
+        Log.debug(message: localizedDescription, error: error)
+        DispatchQueue.main.async { fail(error) }
+    }
+}

+ 49 - 0
o2ios/O2Platform/Framework/Haneke/String+Haneke.swift

@@ -0,0 +1,49 @@
+//
+//  String+Haneke.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 8/30/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import Foundation
+
+extension String {
+    
+    func escapedFilename() -> String {
+        return [ "\0":"%00", ":":"%3A", "/":"%2F" ]
+            .reduce(self.components(separatedBy: "%").joined(separator: "%25")) {
+                str, m in str.components(separatedBy: m.0).joined(separator: m.1)
+        }
+    }
+    
+    func MD5String() -> String {
+        guard let data = self.data(using: String.Encoding.utf8) else {
+            return self
+        }
+
+        let MD5Calculator = MD5(Array(data))
+        let MD5Data = MD5Calculator.calculate()
+        let resultBytes = UnsafeMutablePointer<CUnsignedChar>(mutating: MD5Data)
+        let resultEnumerator = UnsafeBufferPointer<CUnsignedChar>(start: resultBytes, count: MD5Data.count)
+        let MD5String = NSMutableString()
+        for c in resultEnumerator {
+            MD5String.appendFormat("%02x", c)
+        }
+        return MD5String as String
+    }
+    
+    func MD5Filename() -> String {
+        let MD5String = self.MD5String()
+
+        // NSString.pathExtension alone could return a query string, which can lead to very long filenames.
+        let pathExtension = URL(string: self)?.pathExtension ?? (self as NSString).pathExtension
+
+        if pathExtension.count > 0 {
+            return (MD5String as NSString).appendingPathExtension(pathExtension) ?? MD5String
+        } else {
+            return MD5String
+        }
+    }
+
+}

+ 234 - 0
o2ios/O2Platform/Framework/Haneke/UIButton+Haneke.swift

@@ -0,0 +1,234 @@
+//
+//  UIButton+Haneke.swift
+//  Haneke
+//
+//  Created by Joan Romano on 10/1/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+public extension UIButton {
+    
+    public var hnk_imageFormat : Format<UIImage> {
+        let bounds = self.bounds
+        assert(bounds.size.width > 0 && bounds.size.height > 0, "[\(Mirror(reflecting: self).description) \(#function)]: UIButton size is zero. Set its frame, call sizeToFit or force layout first. You can also set a custom format with a defined size if you don't want to force layout.")
+            let contentRect = self.contentRect(forBounds: bounds)
+            let imageInsets = self.imageEdgeInsets
+        let scaleMode = self.contentHorizontalAlignment != UIControl.ContentHorizontalAlignment.fill || self.contentVerticalAlignment != UIControl.ContentVerticalAlignment.fill ? ImageResizer.ScaleMode.AspectFit : ImageResizer.ScaleMode.Fill
+            let imageSize = CGSize(width: contentRect.width - imageInsets.left - imageInsets.right, height: contentRect.height - imageInsets.top - imageInsets.bottom)
+            
+            return HanekeGlobals.UIKit.formatWithSize(imageSize, scaleMode: scaleMode, allowUpscaling: scaleMode == ImageResizer.ScaleMode.AspectFit ? false : true)
+    }
+    
+    public func hnk_setImageFromURL(_ URL: Foundation.URL, state: UIControl.State = .normal, placeholder: UIImage? = nil, format: Format<UIImage>? = nil, failure fail: ((Error?) -> ())? = nil, success succeed: ((UIImage) -> ())? = nil) {
+        let fetcher = NetworkFetcher<UIImage>(URL: URL)
+        self.hnk_setImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, failure: fail, success: succeed)
+    }
+    
+    public func hnk_setImage(_ image: UIImage, key: String, state: UIControl.State = .normal, placeholder: UIImage? = nil, format: Format<UIImage>? = nil, success succeed: ((UIImage) -> ())? = nil) {
+        let fetcher = SimpleFetcher<UIImage>(key: key, value: image)
+        self.hnk_setImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, success: succeed)
+    }
+    
+    public func hnk_setImageFromFile(_ path: String, state: UIControl.State = .normal, placeholder: UIImage? = nil, format: Format<UIImage>? = nil, failure fail: ((Error?) -> ())? = nil, success succeed: ((UIImage) -> ())? = nil) {
+        let fetcher = DiskFetcher<UIImage>(path: path)
+        self.hnk_setImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, failure: fail, success: succeed)
+    }
+    
+    public func hnk_setImageFromFetcher(_ fetcher: Fetcher<UIImage>, state: UIControl.State = .normal, placeholder: UIImage? = nil, format: Format<UIImage>? = nil, failure fail: ((Error?) -> ())? = nil, success succeed: ((UIImage) -> ())? = nil){
+        self.hnk_cancelSetImage()
+        self.hnk_imageFetcher = fetcher
+        
+        let didSetImage = self.hnk_fetchImageForFetcher(fetcher, state: state, format : format, failure: fail, success: succeed)
+        
+        if didSetImage { return }
+        
+        if let placeholder = placeholder {
+            self.setImage(placeholder, for: state)
+        }
+    }
+    
+    public func hnk_cancelSetImage() {
+        if let fetcher = self.hnk_imageFetcher {
+            fetcher.cancelFetch()
+            self.hnk_imageFetcher = nil
+        }
+    }
+    
+    // MARK: Internal Image
+    
+    // See: http://stackoverflow.com/questions/25907421/associating-swift-things-with-nsobject-instances
+    var hnk_imageFetcher : Fetcher<UIImage>! {
+        get {
+            let wrapper = objc_getAssociatedObject(self, &HanekeGlobals.UIKit.SetImageFetcherKey) as? ObjectWrapper
+            let fetcher = wrapper?.hnk_value as? Fetcher<UIImage>
+            return fetcher
+        }
+        set (fetcher) {
+            var wrapper : ObjectWrapper?
+            if let fetcher = fetcher {
+                wrapper = ObjectWrapper(value: fetcher)
+            }
+            objc_setAssociatedObject(self, &HanekeGlobals.UIKit.SetImageFetcherKey, wrapper, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+    }
+    
+    func hnk_fetchImageForFetcher(_ fetcher : Fetcher<UIImage>, state : UIControl.State = .normal, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())?, success succeed : ((UIImage) -> ())?) -> Bool {
+        let format = format ?? self.hnk_imageFormat
+        let cache = Shared.imageCache
+        if cache.formats[format.name] == nil {
+            cache.addFormat(format)
+        }
+        var animated = false
+        let fetch = cache.fetch(fetcher: fetcher, formatName: format.name, failure: {[weak self] error in
+            if let strongSelf = self {
+                if strongSelf.hnk_shouldCancelImageForKey(fetcher.key) { return }
+                
+                strongSelf.hnk_imageFetcher = nil
+                
+                fail?(error)
+            }
+            }) { [weak self] image in
+                if let strongSelf = self {
+                    if strongSelf.hnk_shouldCancelImageForKey(fetcher.key) { return }
+                    
+                    strongSelf.hnk_setImage(image, state: state, animated: animated, success: succeed)
+                }
+        }
+        animated = true
+        return fetch.hasSucceeded
+    }
+    
+    
+    func hnk_setImage(_ image : UIImage, state : UIControl.State, animated : Bool, success succeed : ((UIImage) -> ())?) {
+        self.hnk_imageFetcher = nil
+        
+        if let succeed = succeed {
+            succeed(image)
+        } else if animated {
+            UIView.transition(with: self, duration: HanekeGlobals.UIKit.SetImageAnimationDuration, options: .transitionCrossDissolve, animations: {
+                self.setImage(image, for: state)
+                }, completion: nil)
+        } else {
+            self.setImage(image, for: state)
+        }
+    }
+    
+    func hnk_shouldCancelImageForKey(_ key:String) -> Bool {
+        if self.hnk_imageFetcher?.key == key { return false }
+        
+        Log.debug(message: "Cancelled set image for \((key as NSString).lastPathComponent)")
+        return true
+    }
+    
+    // MARK: Background image
+        
+    public var hnk_backgroundImageFormat : Format<UIImage> {
+        let bounds = self.bounds
+        assert(bounds.size.width > 0 && bounds.size.height > 0, "[\(Mirror(reflecting: self).description) \(#function)]: UIButton size is zero. Set its frame, call sizeToFit or force layout first. You can also set a custom format with a defined size if you don't want to force layout.")
+            let imageSize = self.backgroundRect(forBounds: bounds).size
+            
+            return HanekeGlobals.UIKit.formatWithSize(imageSize, scaleMode: .Fill)
+    }
+    
+    public func hnk_setBackgroundImageFromURL(_ URL : Foundation.URL, state : UIControl.State = .normal, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())? = nil, success succeed : ((UIImage) -> ())? = nil) {
+        let fetcher = NetworkFetcher<UIImage>(URL: URL)
+        self.hnk_setBackgroundImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, failure: fail, success: succeed)
+    }
+    
+    public func hnk_setBackgroundImage(_ image : UIImage, key: String, state : UIControl.State = .normal, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, success succeed : ((UIImage) -> ())? = nil) {
+        let fetcher = SimpleFetcher<UIImage>(key: key, value: image)
+        self.hnk_setBackgroundImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, success: succeed)
+    }
+    
+    public func hnk_setBackgroundImageFromFile(_ path: String, state : UIControl.State = .normal, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())? = nil, success succeed : ((UIImage) -> ())? = nil) {
+        let fetcher = DiskFetcher<UIImage>(path: path)
+        self.hnk_setBackgroundImageFromFetcher(fetcher, state: state, placeholder: placeholder, format: format, failure: fail, success: succeed)
+    }
+    
+    public func hnk_setBackgroundImageFromFetcher(_ fetcher : Fetcher<UIImage>, state : UIControl.State = .normal, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())? = nil, success succeed : ((UIImage) -> ())? = nil) {
+        self.hnk_cancelSetBackgroundImage()
+        self.hnk_backgroundImageFetcher = fetcher
+        
+        let didSetImage = self.hnk_fetchBackgroundImageForFetcher(fetcher, state: state, format : format, failure: fail, success: succeed)
+     
+        if didSetImage { return }
+        
+        if let placeholder = placeholder {
+            self.setBackgroundImage(placeholder, for: state)
+        }
+    }
+    
+    public func hnk_cancelSetBackgroundImage() {
+        if let fetcher = self.hnk_backgroundImageFetcher {
+            fetcher.cancelFetch()
+            self.hnk_backgroundImageFetcher = nil
+        }
+    }
+    
+    // MARK: Internal Background image
+    
+    // See: http://stackoverflow.com/questions/25907421/associating-swift-things-with-nsobject-instances
+    var hnk_backgroundImageFetcher : Fetcher<UIImage>! {
+        get {
+            let wrapper = objc_getAssociatedObject(self, &HanekeGlobals.UIKit.SetBackgroundImageFetcherKey) as? ObjectWrapper
+            let fetcher = wrapper?.hnk_value as? Fetcher<UIImage>
+            return fetcher
+        }
+        set (fetcher) {
+            var wrapper : ObjectWrapper?
+            if let fetcher = fetcher {
+                wrapper = ObjectWrapper(value: fetcher)
+            }
+            objc_setAssociatedObject(self, &HanekeGlobals.UIKit.SetBackgroundImageFetcherKey, wrapper, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+    }
+    
+    func hnk_fetchBackgroundImageForFetcher(_ fetcher: Fetcher<UIImage>, state: UIControl.State = .normal, format: Format<UIImage>? = nil, failure fail: ((Error?) -> ())?, success succeed : ((UIImage) -> ())?) -> Bool {
+        let format = format ?? self.hnk_backgroundImageFormat
+        let cache = Shared.imageCache
+        if cache.formats[format.name] == nil {
+            cache.addFormat(format)
+        }
+        var animated = false
+        let fetch = cache.fetch(fetcher: fetcher, formatName: format.name, failure: {[weak self] error in
+            if let strongSelf = self {
+                if strongSelf.hnk_shouldCancelBackgroundImageForKey(fetcher.key) { return }
+                
+                strongSelf.hnk_backgroundImageFetcher = nil
+                
+                fail?(error)
+            }
+            }) { [weak self] image in
+                if let strongSelf = self {
+                    if strongSelf.hnk_shouldCancelBackgroundImageForKey(fetcher.key) { return }
+                    
+                    strongSelf.hnk_setBackgroundImage(image, state: state, animated: animated, success: succeed)
+                }
+        }
+        animated = true
+        return fetch.hasSucceeded
+    }
+    
+    func hnk_setBackgroundImage(_ image: UIImage, state: UIControl.State, animated: Bool, success succeed: ((UIImage) -> ())?) {
+        self.hnk_backgroundImageFetcher = nil
+        
+        if let succeed = succeed {
+            succeed(image)
+        } else if animated {
+            UIView.transition(with: self, duration: HanekeGlobals.UIKit.SetImageAnimationDuration, options: .transitionCrossDissolve, animations: {
+                self.setBackgroundImage(image, for: state)
+                }, completion: nil)
+        } else {
+            self.setBackgroundImage(image, for: state)
+        }
+    }
+    
+    func hnk_shouldCancelBackgroundImageForKey(_ key: String) -> Bool {
+        if self.hnk_backgroundImageFetcher?.key == key { return false }
+        
+        Log.debug(message: "Cancelled set background image for \((key as NSString).lastPathComponent)")
+        return true
+    }
+}

+ 81 - 0
o2ios/O2Platform/Framework/Haneke/UIImage+Haneke.swift

@@ -0,0 +1,81 @@
+//
+//  UIImage+Haneke.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 8/10/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+extension UIImage {
+
+    func hnk_imageByScaling(toSize size: CGSize) -> UIImage {
+        UIGraphicsBeginImageContextWithOptions(size, !hnk_hasAlpha(), 0.0)
+        draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
+        let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return resizedImage!
+    }
+
+    func hnk_hasAlpha() -> Bool {
+        guard let alphaInfo = self.cgImage?.alphaInfo else { return false }
+        switch alphaInfo {
+        case .first, .last, .premultipliedFirst, .premultipliedLast, .alphaOnly:
+            return true
+        case .none, .noneSkipFirst, .noneSkipLast:
+            return false
+        }
+    }
+    
+    func hnk_data(compressionQuality: Float = 1.0) -> Data! {
+        let hasAlpha = self.hnk_hasAlpha()
+        let data = hasAlpha ? self.pngData() : self.jpegData(compressionQuality: CGFloat(compressionQuality))
+        return data
+    }
+    
+    func hnk_decompressedImage() -> UIImage! {
+        let originalImageRef = self.cgImage
+        let originalBitmapInfo = originalImageRef?.bitmapInfo
+        guard let alphaInfo = originalImageRef?.alphaInfo else { return UIImage() }
+        
+        // See: http://stackoverflow.com/questions/23723564/which-cgimagealphainfo-should-we-use
+        var bitmapInfo = originalBitmapInfo
+        switch alphaInfo {
+        case .none:
+            let rawBitmapInfoWithoutAlpha = (bitmapInfo?.rawValue)! & ~CGBitmapInfo.alphaInfoMask.rawValue
+            let rawBitmapInfo = rawBitmapInfoWithoutAlpha | CGImageAlphaInfo.noneSkipFirst.rawValue
+            bitmapInfo = CGBitmapInfo(rawValue: rawBitmapInfo)
+        case .premultipliedFirst, .premultipliedLast, .noneSkipFirst, .noneSkipLast:
+            break
+        case .alphaOnly, .last, .first: // Unsupported
+            return self
+        }
+        
+        let colorSpace = CGColorSpaceCreateDeviceRGB()
+        let pixelSize = CGSize(width: self.size.width * self.scale, height: self.size.height * self.scale)
+        guard let context = CGContext(data: nil, width: Int(ceil(pixelSize.width)), height: Int(ceil(pixelSize.height)), bitsPerComponent: (originalImageRef?.bitsPerComponent)!, bytesPerRow: 0, space: colorSpace, bitmapInfo: (bitmapInfo?.rawValue)!) else {
+            return self
+        }
+
+        let imageRect = CGRect(x: 0, y: 0, width: pixelSize.width, height: pixelSize.height)
+        UIGraphicsPushContext(context)
+        
+        // Flip coordinate system. See: http://stackoverflow.com/questions/506622/cgcontextdrawimage-draws-image-upside-down-when-passed-uiimage-cgimage
+        context.translateBy(x: 0, y: pixelSize.height)
+        context.scaleBy(x: 1.0, y: -1.0)
+        
+        // UIImage and drawInRect takes into account image orientation, unlike CGContextDrawImage.
+        self.draw(in: imageRect)
+        UIGraphicsPopContext()
+        
+        guard let decompressedImageRef = context.makeImage() else {
+            return self
+        }
+        
+        let scale = UIScreen.main.scale
+        let image = UIImage(cgImage: decompressedImageRef, scale:scale, orientation:UIImage.Orientation.up)
+        return image
+    }
+
+}

+ 139 - 0
o2ios/O2Platform/Framework/Haneke/UIImageView+Haneke.swift

@@ -0,0 +1,139 @@
+//
+//  UIImageView+Haneke.swift
+//  Haneke
+//
+//  Created by Hermes Pique on 9/17/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+public extension UIImageView {
+    
+    public var hnk_format : Format<UIImage> {
+        let viewSize = self.bounds.size
+            assert(viewSize.width > 0 && viewSize.height > 0, "[\(Mirror(reflecting: self).description) \(#function)]: UImageView size is zero. Set its frame, call sizeToFit or force layout first.")
+            let scaleMode = self.hnk_scaleMode
+            return HanekeGlobals.UIKit.formatWithSize(viewSize, scaleMode: scaleMode)
+    }
+    
+    public func hnk_setImageFromURL(_ URL: Foundation.URL, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())? = nil, success succeed : ((UIImage) -> ())? = nil) {
+        let fetcher = NetworkFetcher<UIImage>(URL: URL)
+        self.hnk_setImage(fromFetcher: fetcher, placeholder: placeholder, format: format, failure: fail, success: succeed)
+    }
+    
+    public func hnk_setImage( _ image: @autoclosure @escaping () -> UIImage, key: String, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, success succeed : ((UIImage) -> ())? = nil) {
+        let fetcher = SimpleFetcher<UIImage>(key: key, value: image())
+        self.hnk_setImage(fromFetcher: fetcher, placeholder: placeholder, format: format, success: succeed)
+    }
+    
+    public func hnk_setImageFromFile(_ path: String, placeholder : UIImage? = nil, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())? = nil, success succeed : ((UIImage) -> ())? = nil) {
+        let fetcher = DiskFetcher<UIImage>(path: path)
+        self.hnk_setImage(fromFetcher: fetcher, placeholder: placeholder, format: format, failure: fail, success: succeed)
+    }
+    
+    public func hnk_setImage(fromFetcher fetcher : Fetcher<UIImage>,
+        placeholder : UIImage? = nil,
+        format : Format<UIImage>? = nil,
+        failure fail : ((Error?) -> ())? = nil,
+        success succeed : ((UIImage) -> ())? = nil) {
+
+        self.hnk_cancelSetImage()
+        
+        self.hnk_fetcher = fetcher
+        
+        let didSetImage = self.hnk_fetchImageForFetcher(fetcher, format: format, failure: fail, success: succeed)
+        
+        if didSetImage { return }
+     
+        if let placeholder = placeholder {
+            self.image = placeholder
+        }
+    }
+    
+    public func hnk_cancelSetImage() {
+        if let fetcher = self.hnk_fetcher {
+            fetcher.cancelFetch()
+            self.hnk_fetcher = nil
+        }
+    }
+    
+    // MARK: Internal
+    
+    // See: http://stackoverflow.com/questions/25907421/associating-swift-things-with-nsobject-instances
+    var hnk_fetcher : Fetcher<UIImage>! {
+        get {
+            let wrapper = objc_getAssociatedObject(self, &HanekeGlobals.UIKit.SetImageFetcherKey) as? ObjectWrapper
+            let fetcher = wrapper?.hnk_value as? Fetcher<UIImage>
+            return fetcher
+        }
+        set (fetcher) {
+            var wrapper : ObjectWrapper?
+            if let fetcher = fetcher {
+                wrapper = ObjectWrapper(value: fetcher)
+            }
+            objc_setAssociatedObject(self, &HanekeGlobals.UIKit.SetImageFetcherKey, wrapper, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+    }
+    
+    public var hnk_scaleMode : ImageResizer.ScaleMode {
+        switch (self.contentMode) {
+        case .scaleToFill:
+            return .Fill
+        case .scaleAspectFit:
+            return .AspectFit
+        case .scaleAspectFill:
+            return .AspectFill
+        case .redraw, .center, .top, .bottom, .left, .right, .topLeft, .topRight, .bottomLeft, .bottomRight:
+            return .None
+            }
+    }
+
+    func hnk_fetchImageForFetcher(_ fetcher : Fetcher<UIImage>, format : Format<UIImage>? = nil, failure fail : ((Error?) -> ())?, success succeed : ((UIImage) -> ())?) -> Bool {
+        let cache = Shared.imageCache
+        let format = format ?? self.hnk_format
+        if cache.formats[format.name] == nil {
+            cache.addFormat(format)
+        }
+        var animated = false
+        let fetch = cache.fetch(fetcher: fetcher, formatName: format.name, failure: {[weak self] error in
+            if let strongSelf = self {
+                if strongSelf.hnk_shouldCancel(forKey: fetcher.key) { return }
+                
+                strongSelf.hnk_fetcher = nil
+                
+                fail?(error)
+            }
+        }) { [weak self] image in
+            if let strongSelf = self {
+                if strongSelf.hnk_shouldCancel(forKey: fetcher.key) { return }
+                
+                strongSelf.hnk_setImage(image, animated: animated, success: succeed)
+            }
+        }
+        animated = true
+        return fetch.hasSucceeded
+    }
+    
+    func hnk_setImage(_ image : UIImage, animated : Bool, success succeed : ((UIImage) -> ())?) {
+        self.hnk_fetcher = nil
+        
+        if let succeed = succeed {
+            succeed(image)
+        } else if animated {
+            UIView.transition(with: self, duration: HanekeGlobals.UIKit.SetImageAnimationDuration, options: .transitionCrossDissolve, animations: {
+                self.image = image
+            }, completion: nil)
+        } else {
+            self.image = image
+        }
+    }
+    
+    func hnk_shouldCancel(forKey key:String) -> Bool {
+        if self.hnk_fetcher?.key == key { return false }
+        
+        //Log.debug(message: "Cancelled set image for \((key as NSString).lastPathComponent)")
+        return true
+    }
+    
+}

+ 48 - 0
o2ios/O2Platform/Framework/Haneke/UIView+Haneke.swift

@@ -0,0 +1,48 @@
+//
+//  UIView+Haneke.swift
+//  Haneke
+//
+//  Created by Joan Romano on 15/10/14.
+//  Copyright (c) 2014 Haneke. All rights reserved.
+//
+
+import UIKit
+
+public extension HanekeGlobals {
+    
+    public struct UIKit {
+        
+        static func formatWithSize(_ size : CGSize, scaleMode : ImageResizer.ScaleMode, allowUpscaling: Bool = true) -> Format<UIImage> {
+            let name = "auto-\(size.width)x\(size.height)-\(scaleMode.rawValue)"
+            let cache = Shared.imageCache
+            if let (format,_,_) = cache.formats[name] {
+                return format
+            }
+            
+            var format = Format<UIImage>(name: name,
+                diskCapacity: HanekeGlobals.UIKit.DefaultFormat.DiskCapacity) {
+                    let resizer = ImageResizer(size:size,
+                        scaleMode: scaleMode,
+                        allowUpscaling: allowUpscaling,
+                        compressionQuality: HanekeGlobals.UIKit.DefaultFormat.CompressionQuality)
+                    return resizer.resizeImage($0)
+            }
+            format.convertToData = {(image : UIImage) -> Data in
+                image.hnk_data(compressionQuality: HanekeGlobals.UIKit.DefaultFormat.CompressionQuality) as Data
+            }
+            return format
+        }
+        
+        public struct DefaultFormat {
+            
+            public static let DiskCapacity : UInt64 = 50 * 1024 * 1024
+            public static let CompressionQuality : Float = 0.75
+            
+        }
+        
+        static var SetImageAnimationDuration = 0.1
+        static var SetImageFetcherKey = 0
+        static var SetBackgroundImageFetcherKey = 1
+    }
+    
+}

+ 54 - 0
o2ios/O2Platform/Framework/ImageRow/ImageCheckRow.swift

@@ -0,0 +1,54 @@
+//
+//  ImageCheckRow.swift
+//  O2Platform
+//
+//  Created by 刘振兴 on 2016/10/2.
+//  Copyright © 2016年 zoneland. All rights reserved.
+//
+
+import Foundation
+import Eureka
+
+public final class ImageCheckRow<T: Equatable>: Row<ImageCheckCell<T>>, SelectableRowType, RowType {
+    public var selectableValue: T?
+    required public init(tag: String?) {
+        super.init(tag: tag)
+        displayValueFor = nil
+    }
+}
+
+public class ImageCheckCell<T: Equatable> : Cell<T>, CellType {
+    
+    required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+    }
+    
+    required public init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    lazy public var trueImage: UIImage = {
+        return UIImage(named: "selected")!
+    }()
+    
+    lazy public var falseImage: UIImage = {
+        return UIImage(named: "unselected")!
+    }()
+    
+    public override func update() {
+        super.update()
+        accessoryType = .none
+        imageView?.image = row.value != nil ? trueImage : falseImage
+    }
+    
+    public override func setup() {
+        super.setup()
+    }
+    
+    public override func didSelect() {
+        row.reload()
+        row.select()
+        row.deselect()
+    }
+    
+}

+ 62 - 0
o2ios/O2Platform/Framework/ImageRow/ImagePickerController.swift

@@ -0,0 +1,62 @@
+//  ImagePickerController.swift
+//  Eureka ( https://github.com/xmartlabs/Eureka )
+//
+//  Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com )
+//
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import Foundation
+import Eureka
+
+/// Selector Controller used to pick an image
+open class ImagePickerController : UIImagePickerController, TypedRowControllerType, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
+    
+    /// The row that pushed or presented this controller
+    public var row: RowOf<UIImage>!
+    
+    /// A closure to be called when the controller disappears.
+    public var onDismissCallback : ((UIViewController) -> ())?
+    
+    open override func viewDidLoad() {
+        super.viewDidLoad()
+        self.navigationBar.isTranslucent = false
+        self.navigationBar.barTintColor = base_color
+        self.navigationBar.tintColor = base_color
+        self.navigationBar.titleTextAttributes = [
+            NSAttributedString.Key.font:navbar_text_font,
+            NSAttributedString.Key.foregroundColor:navbar_tint_color
+        ]
+        delegate = self
+    }
+    
+    open func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
+        (row as? ImageRow)?.imageURL = info[UIImagePickerController.InfoKey.referenceURL] as? URL
+        if let result = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
+            let newImage = result.fixOrientation()
+            row.value = newImage
+        }
+        onDismissCallback?(self)
+    }
+    
+    open func imagePickerControllerDidCancel(_ picker: UIImagePickerController){
+        onDismissCallback?(self)
+    }
+}
+

+ 238 - 0
o2ios/O2Platform/Framework/ImageRow/ImageRow.swift

@@ -0,0 +1,238 @@
+//  ImageRow.swift
+//  Eureka ( https://github.com/xmartlabs/Eureka )
+//
+//  Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com )
+//
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import Foundation
+import Eureka
+
+public struct ImageRowSourceTypes : OptionSet {
+    
+    public let rawValue: Int
+    public var imagePickerControllerSourceTypeRawValue: Int { return self.rawValue >> 1 }
+    
+    public init(rawValue: Int) { self.rawValue = rawValue }
+    init(_ sourceType: UIImagePickerController.SourceType) { self.init(rawValue: 1 << sourceType.rawValue) }
+    
+    public static let PhotoLibrary  = ImageRowSourceTypes(.photoLibrary)
+    public static let Camera  = ImageRowSourceTypes(.camera)
+    public static let SavedPhotosAlbum = ImageRowSourceTypes(.savedPhotosAlbum)
+    public static let All: ImageRowSourceTypes = [Camera, PhotoLibrary, SavedPhotosAlbum]
+    
+}
+
+extension ImageRowSourceTypes {
+    
+    // MARK: Helpers
+    
+    var localizedString: String {
+        switch self {
+        case ImageRowSourceTypes.Camera:
+            return "拍照"
+        case ImageRowSourceTypes.PhotoLibrary:
+            return "图库"
+        case ImageRowSourceTypes.SavedPhotosAlbum:
+            return "相册"
+        default:
+            return ""
+        }
+    }
+}
+
+public enum ImageClearAction {
+    case no
+    case yes(style: UIAlertAction.Style)
+}
+
+//MARK: Row
+open class _ImageRow<Cell: CellType>: OptionsRow<Cell>, PresenterRowType where Cell: BaseCell, Cell.Value == UIImage {
+    
+    public typealias PresenterRow = ImagePickerController
+    
+    /// Defines how the view controller will be presented, pushed, etc.
+    open var presentationMode: PresentationMode<PresenterRow>?
+    
+    /// Will be called before the presentation occurs.
+    open var onPresentCallback: ((FormViewController, PresenterRow) -> Void)?
+    
+    
+    open var sourceTypes: ImageRowSourceTypes
+    open internal(set) var imageURL: URL?
+    open var clearAction = ImageClearAction.yes(style: .destructive)
+    
+    private var _sourceType: UIImagePickerController.SourceType = .camera
+    
+    public required init(tag: String?) {
+        sourceTypes = .All
+        super.init(tag: tag)
+        presentationMode = .presentModally(controllerProvider: ControllerProvider.callback { return ImagePickerController() }, onDismiss: { [weak self] vc in
+            self?.select()
+            vc.dismiss(animated: true)
+        })
+        self.displayValueFor = nil
+        
+    }
+    
+    // copy over the existing logic from the SelectorRow
+    func displayImagePickerController(_ sourceType: UIImagePickerController.SourceType) {
+        if let presentationMode = presentationMode, !isDisabled {
+            if let controller = presentationMode.makeController(){
+                controller.row = self
+                controller.sourceType = sourceType
+                onPresentCallback?(cell.formViewController()!, controller)
+                presentationMode.present(controller, row: self, presentingController: cell.formViewController()!)
+            }
+            else{
+                _sourceType = sourceType
+                presentationMode.present(nil, row: self, presentingController: cell.formViewController()!)
+            }
+        }
+    }
+    
+    /// Extends `didSelect` method
+    /// Selecting the Image Row cell will open a popup to choose where to source the photo from,
+    /// based on the `sourceTypes` configured and the available sources.
+    open override func customDidSelect() {
+        guard !isDisabled else {
+            super.customDidSelect()
+            return
+        }
+        deselect()
+        
+        var availableSources: ImageRowSourceTypes = []
+        
+        if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
+            let _ = availableSources.insert(.PhotoLibrary)
+        }
+        if UIImagePickerController.isSourceTypeAvailable(.camera) {
+            let _ = availableSources.insert(.Camera)
+        }
+        if UIImagePickerController.isSourceTypeAvailable(.savedPhotosAlbum) {
+            let _ = availableSources.insert(.SavedPhotosAlbum)
+        }
+        
+        sourceTypes.formIntersection(availableSources)
+        
+        if sourceTypes.isEmpty {
+            super.customDidSelect()
+            guard let presentationMode = presentationMode else { return }
+            if let controller = presentationMode.makeController() {
+                controller.row = self
+                controller.title = selectorTitle ?? controller.title
+                onPresentCallback?(cell.formViewController()!, controller)
+                presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!)
+            } else {
+                presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!)
+            }
+            return
+        }
+        
+        // Now that we know the number of sources aren't empty, let the user select the source
+        let sourceActionSheet = UIAlertController(title: nil, message: selectorTitle, preferredStyle: .actionSheet)
+        guard let tableView = cell.formViewController()?.tableView  else { fatalError() }
+        if let popView = sourceActionSheet.popoverPresentationController {
+            popView.sourceView = tableView
+            popView.sourceRect = tableView.convert(cell.accessoryView?.frame ?? cell.contentView.frame, from: cell)
+        }
+        createOptionsForAlertController(sourceActionSheet)
+        if case .yes(let style) = clearAction, value != nil {
+            let clearPhotoOption = UIAlertAction(title: NSLocalizedString("Clear Photo", comment: ""), style: style, handler: { [weak self] _ in
+                self?.value = nil
+                self?.imageURL = nil
+                self?.updateCell()
+            })
+            sourceActionSheet.addAction(clearPhotoOption)
+        }
+        if sourceActionSheet.actions.count == 1 {
+            if let imagePickerSourceType = UIImagePickerController.SourceType(rawValue: sourceTypes.imagePickerControllerSourceTypeRawValue) {
+                displayImagePickerController(imagePickerSourceType)
+            }
+        } else {
+            let cancelOption = UIAlertAction(title: NSLocalizedString("取消", comment: ""), style: .cancel, handler:nil)
+            sourceActionSheet.addAction(cancelOption)
+            if let presentingViewController = cell.formViewController() {
+                presentingViewController.present(sourceActionSheet, animated: true)
+            }
+        }
+    }
+    
+    /**
+     Prepares the pushed row setting its title and completion callback.
+     */
+    open override func prepare(for segue: UIStoryboardSegue) {
+        super.prepare(for: segue)
+        guard let rowVC = segue.destination as? PresenterRow else { return }
+        rowVC.title = selectorTitle ?? rowVC.title
+        rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback
+        onPresentCallback?(cell.formViewController()!, rowVC)
+        rowVC.row = self
+        rowVC.sourceType = _sourceType
+    }
+    
+    open override func customUpdateCell() {
+        super.customUpdateCell()
+        
+        cell.accessoryType = .none
+        cell.editingAccessoryView = .none
+        
+        if let image = self.value {
+            let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
+            imageView.contentMode = .scaleAspectFill
+            imageView.image = image
+            imageView.clipsToBounds = true
+            
+            cell.accessoryView = imageView
+            cell.editingAccessoryView = imageView
+        } else {
+            cell.accessoryView = nil
+            cell.editingAccessoryView = nil
+        }
+    }
+    
+    
+}
+
+extension _ImageRow {
+    
+    //MARK: Helpers
+    
+    func createOptionForAlertController(_ alertController: UIAlertController, sourceType: ImageRowSourceTypes) {
+        guard let pickerSourceType = UIImagePickerController.SourceType(rawValue: sourceType.imagePickerControllerSourceTypeRawValue), sourceTypes.contains(sourceType) else { return }
+        let option = UIAlertAction(title: NSLocalizedString(sourceType.localizedString, comment: ""), style: .default, handler: { [weak self] _ in
+            self?.displayImagePickerController(pickerSourceType)
+        })
+        alertController.addAction(option)
+    }
+    
+    func createOptionsForAlertController(_ alertController: UIAlertController) {
+        createOptionForAlertController(alertController, sourceType: .Camera)
+        createOptionForAlertController(alertController, sourceType: .PhotoLibrary)
+        createOptionForAlertController(alertController, sourceType: .SavedPhotosAlbum)
+    }
+}
+
+/// A selector row where the user can pick an image
+public final class ImageRow : _ImageRow<PushSelectorCell<UIImage>>, RowType {
+    public required init(tag: String?) {
+        super.init(tag: tag)
+    }
+}

+ 81 - 0
o2ios/O2Platform/Framework/InputView/InputView.h

@@ -0,0 +1,81 @@
+//
+//  InputView.h
+//  TableViewDemo
+//
+//  Created by BenGang on 14-7-21.
+//  Copyright (c) 2014年 BenGang. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import "UIViewExt.h"
+@class InputView;
+
+@protocol InputViewDelegate <NSObject>
+
+@optional
+- (void)keyboardWillShow:(InputView *)inputView keyboardHeight:(CGFloat)keyboardHeight animationDuration:(NSTimeInterval)duration animationCurve:(UIViewAnimationCurve)animationCurve;
+
+- (void)keyboardWillHide:(InputView *)inputView keyboardHeight:(CGFloat)keyboardHeight animationDuration:(NSTimeInterval)duration animationCurve:(UIViewAnimationCurve)animationCurve;
+
+- (void)recordButtonDidClick:(UIButton *)button;
+
+- (void)addButtonDidClick:(UIButton *)button;
+
+- (void)publishButtonDidClick:(UIButton *)button;
+
+- (void)textViewHeightDidChange:(CGFloat)height;
+
+@end
+
+@interface InputView : UIView <UITextViewDelegate>
+@property (retain, nonatomic) IBOutlet UIImageView *images;
+
+@property (weak, nonatomic) IBOutlet UIButton *recordButton;
+@property (weak, nonatomic) IBOutlet UIButton *addButton;
+@property (weak, nonatomic) IBOutlet UIButton *publishButton;
+@property (weak, nonatomic) IBOutlet UITextView *inputTextView;
+@property (weak, nonatomic) id<InputViewDelegate> delegate;
+
+
+- (IBAction)recordButtonClick:(id)sender;
+- (IBAction)addButtonClick:(id)sender;
+- (IBAction)publishButtonClick:(id)sender;
+- (void)resetInputView;
+
+@end
+/*
+ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
+ {
+ [self.inputView.inputTextView resignFirstResponder];
+ self.backView.hidden = YES;
+ 
+ [UIView animateWithDuration:0.1 animations:^{
+ self.inputView.bottom = self.view.height;
+ }];
+ }
+ 
+ #pragma mark InputViewDelegate
+ 
+ - (void)keyboardWillShow:(InputView *)inputView keyboardHeight:(CGFloat)keyboardHeight animationDuration:(NSTimeInterval)duration
+ {
+ self.backView.hidden = YES;
+ self.keyboardHeight = keyboardHeight;
+ [UIView animateWithDuration:duration animations:^{
+ self.inputView.bottom = self.view.height - keyboardHeight;
+ }];
+ }
+ 
+ - (void)keyboardWillHide:(InputView *)inputView keyboardHeight:(CGFloat)keyboardHeight animationDuration:(NSTimeInterval)duration
+ {
+ self.keyboardHeight = 0;
+ self.backView.hidden = YES;
+ [UIView animateWithDuration:duration animations:^{
+ self.inputView.bottom = self.view.height;
+ }];
+ if ([self.inputView.inputTextView.text isEqualToString: @""]) {
+ self.inputView.inputTextView.textColor = ColorWithRGB(70, 70, 70);
+ self.currentSelectedReply.forUserId = [NSString stringWithFormat:@"%d",self.currentPost.userId];
+ louzhu = YES;
+ }
+ }
+*/

+ 168 - 0
o2ios/O2Platform/Framework/InputView/InputView.m

@@ -0,0 +1,168 @@
+//
+//  InputView.m
+//  TableViewDemo
+//
+//  Created by BenGang on 14-7-21.
+//  Copyright (c) 2014年 BenGang. All rights reserved.
+//
+
+#import "InputView.h"
+#define RGB(r, g, b) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:1]
+#define ORINGIN_X(view) view.frame.origin.x
+#define ORINGIN_Y(view) view.frame.origin.y
+#define SCREEN_WIDTH    [[UIScreen mainScreen] bounds].size.width
+#define VIEW_HEIGHT(view)  view.frame.size.height
+
+@implementation InputView
+
+- (id)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    if (self) {
+        // Initialization code
+    }
+    [_inputTextView setFont:[UIFont fontWithName:@"BauhausITC" size:17.0]];
+    return self;
+}
+
+- (void)awakeFromNib
+{
+    self.inputTextView.layer.borderColor = [ RGB(200, 200, 200) CGColor];
+    self.images.backgroundColor =  RGB(245, 245, 245);
+    [_inputTextView setFrame:CGRectMake(ORINGIN_X(_inputTextView), ORINGIN_Y(_inputTextView), SCREEN_WIDTH-80, VIEW_HEIGHT(_inputTextView))];
+  //  [self.inputTextView setRight:SCREEN_WIDTH-60];
+//    [self.inputTextView setLeft:0];
+//    [self.inputTextView setRight:(SCREEN_WIDTH - VIEW_WIDTH(_publishButton))];
+    self.inputTextView.layer.borderWidth = 1.0;
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
+}
+
+- (void)keyboardWillShowNotification:(NSNotification *)notification
+{
+    /*
+    NSDictionary *userInfo = [notification userInfo];
+    CGRect keyboardFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
+    NSValue *animationDuration = [userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey];
+    NSTimeInterval timeInterval = 0;
+    [animationDuration getValue:&timeInterval];
+    
+    if ([self.delegate respondsToSelector:@selector(keyboardWillShow:keyboardHeight:animationDuration:)]) {
+        [self.delegate keyboardWillShow:self keyboardHeight:keyboardFrame.size.height animationDuration:timeInterval];
+    }
+    */
+    CGRect keyboardEndFrameWindow;
+    [[notification.userInfo valueForKey:UIKeyboardFrameEndUserInfoKey] getValue: &keyboardEndFrameWindow];
+    
+    double keyboardTransitionDuration;
+    [[notification.userInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] getValue:&keyboardTransitionDuration];
+    
+    UIViewAnimationCurve keyboardTransitionAnimationCurve;
+    [[notification.userInfo valueForKey:UIKeyboardAnimationCurveUserInfoKey] getValue:&keyboardTransitionAnimationCurve];
+    
+    
+  //  CGRect keyboardEndFrameView = [self convertRect:keyboardEndFrameWindow fromView:nil];
+    // 参数 :速度  高度,时间
+    if ([self.delegate respondsToSelector:@selector(keyboardWillShow:keyboardHeight:animationDuration:animationCurve:)]) {
+       
+        [self.delegate keyboardWillShow:self keyboardHeight:keyboardEndFrameWindow.size.height animationDuration:keyboardTransitionDuration animationCurve:keyboardTransitionAnimationCurve];
+    }
+    
+}
+
+- (void)keyboardWillHideNotification:(NSNotification *)notification
+{
+    /*
+    NSDictionary *userInfo = [notification userInfo];
+    CGRect keyboardFrame = [[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
+    NSValue *animationDuration = [userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey];
+    NSTimeInterval timeInterval = 0;
+    [animationDuration getValue:&timeInterval];
+    if ([self.delegate respondsToSelector:@selector(keyboardWillHide:keyboardHeight:animationDuration:)]) {
+        [self.delegate keyboardWillHide:self keyboardHeight:keyboardFrame.size.height animationDuration:timeInterval];
+    }
+     */
+    CGRect keyboardEndFrameWindow;
+    [[notification.userInfo valueForKey:UIKeyboardFrameEndUserInfoKey] getValue: &keyboardEndFrameWindow];
+    
+    double keyboardTransitionDuration;// 获取键盘的速度
+    [[notification.userInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] getValue:&keyboardTransitionDuration];
+    
+    UIViewAnimationCurve keyboardTransitionAnimationCurve;
+    [[notification.userInfo valueForKey:UIKeyboardAnimationCurveUserInfoKey] getValue:&keyboardTransitionAnimationCurve];
+    if ([self.delegate respondsToSelector:@selector(keyboardWillHide:keyboardHeight:animationDuration:animationCurve:)]) {
+        
+        [self.delegate keyboardWillHide:self keyboardHeight:keyboardEndFrameWindow.size.height animationDuration:keyboardTransitionDuration animationCurve:keyboardTransitionAnimationCurve];
+    }
+
+}
+
+- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
+{
+    NSMutableString *times = [[NSMutableString alloc]initWithFormat:@"%@", textView.text];
+    //字符串查找,可以判断字符串中是否有
+    if ([times hasPrefix:@"@"]) {
+        textView.text = @"";
+       // textView.textColor =  RGB(70, 70, 70);
+    }
+    return YES;
+}
+
+- (void)textViewDidChange:(UITextView *)textView
+{
+    //计算文本的高度
+    CGSize constraintSize = CGSizeMake(textView.frame.size.width-16, 60);
+    CGRect sizeFrame = CGRectZero;
+    
+    NSDictionary *attributes = @{NSFontAttributeName:textView.font};
+    NSInteger options = NSStringDrawingUsesFontLeading | NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin;
+    sizeFrame = [textView.text boundingRectWithSize:constraintSize options:options attributes:attributes context:NULL];
+ 
+    sizeFrame.size.height += textView.font.lineHeight;
+    textView.height = sizeFrame.size.height;
+    //重新调整textView的高度
+    if ([self.delegate respondsToSelector:@selector(textViewHeightDidChange:)]) {
+        [self.delegate textViewHeightDidChange:textView.size.height];
+    }
+    
+}
+
+- (IBAction)recordButtonClick:(id)sender {
+    if ([self.delegate respondsToSelector:@selector(recordButtonDidClick:)]) {
+        [self.delegate recordButtonDidClick:sender];
+    }
+}
+
+- (IBAction)addButtonClick:(id)sender {
+    if ([self.delegate respondsToSelector:@selector(addButtonDidClick:)]) {
+        [self.delegate addButtonDidClick:sender];
+    }
+}
+
+- (IBAction)publishButtonClick:(id)sender {
+    if ([self.delegate respondsToSelector:@selector(publishButtonDidClick:)]) {
+        [self.delegate publishButtonDidClick:sender];
+    }
+}
+
+- (void)resetInputView
+{
+    self.height = 44;
+    self.inputTextView.height = 30;
+    [self setNeedsLayout];
+    
+}
+- (void)layoutSubviews
+{
+    [super layoutSubviews];
+    self.addButton.top = self.height/2 - self.addButton.height/2;
+    self.recordButton.top = self.height/2 - self.recordButton.height/2;
+    self.publishButton.top = self.height/2 - self.publishButton.height/2;
+}
+
+- (void)dealloc
+{
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+@end

+ 57 - 0
o2ios/O2Platform/Framework/InputView/InputView.xib

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="7702" systemVersion="14B25" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7701"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="InputView">
+            <rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
+            <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
+            <subviews>
+                <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" id="xsx-WU-iBk">
+                    <rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
+                    <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                    <color key="backgroundColor" white="0.0" alpha="0.050000000000000003" colorSpace="calibratedWhite"/>
+                </imageView>
+                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" id="PlG-fj-caa">
+                    <rect key="frame" x="257" y="2" width="59" height="40"/>
+                    <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
+                    <fontDescription key="fontDescription" type="system" pointSize="15"/>
+                    <state key="normal" title="评论">
+                        <color key="titleColor" white="0.0" alpha="0.53000000000000003" colorSpace="calibratedWhite"/>
+                        <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
+                    </state>
+                    <connections>
+                        <action selector="publishButtonClick:" destination="iN0-l3-epB" eventType="touchUpInside" id="fa3-wR-j4b"/>
+                    </connections>
+                </button>
+                <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" id="6dF-fX-XH8">
+                    <rect key="frame" x="7" y="7" width="246" height="30"/>
+                    <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+                    <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
+                    <fontDescription key="fontDescription" type="system" pointSize="14"/>
+                    <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
+                    <connections>
+                        <outlet property="delegate" destination="iN0-l3-epB" id="gPZ-81-sal"/>
+                    </connections>
+                </textView>
+            </subviews>
+            <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+            <nil key="simulatedStatusBarMetrics"/>
+            <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
+            <connections>
+                <outlet property="images" destination="xsx-WU-iBk" id="zn7-Lz-YKJ"/>
+                <outlet property="inputTextView" destination="6dF-fX-XH8" id="2wb-Dv-Q0d"/>
+                <outlet property="publishButton" destination="PlG-fj-caa" id="F7B-ww-39p"/>
+            </connections>
+        </view>
+    </objects>
+    <simulatedMetricsContainer key="defaultSimulatedMetrics">
+        <simulatedStatusBarMetrics key="statusBar"/>
+        <simulatedOrientationMetrics key="orientation"/>
+        <simulatedScreenMetrics key="destination" type="retina4"/>
+    </simulatedMetricsContainer>
+</document>

+ 32 - 0
o2ios/O2Platform/Framework/InputView/UIViewExt.h

@@ -0,0 +1,32 @@
+/*
+ Erica Sadun, http://ericasadun.com
+ iPhone Developer's Cookbook, 3.0 Edition
+ BSD License, Use at your own risk
+ */
+
+#import <UIKit/UIKit.h>
+
+CGPoint CGRectGetCenter(CGRect rect);
+CGRect  CGRectMoveToCenter(CGRect rect, CGPoint center);
+
+@interface UIView (ViewFrameGeometry)
+@property CGPoint origin;
+@property CGSize size;
+
+@property (readonly) CGPoint bottomLeft;
+@property (readonly) CGPoint bottomRight;
+@property (readonly) CGPoint topRight;
+
+@property CGFloat height;
+@property CGFloat width;
+
+@property CGFloat top;
+@property CGFloat left;
+
+@property CGFloat bottom;
+@property CGFloat right;
+
+- (void) moveBy: (CGPoint) delta;
+- (void) scaleBy: (CGFloat) scaleFactor;
+- (void) fitInSize: (CGSize) aSize;
+@end

+ 189 - 0
o2ios/O2Platform/Framework/InputView/UIViewExt.m

@@ -0,0 +1,189 @@
+/*
+ Erica Sadun, http://ericasadun.com
+ iPhone Developer's Cookbook, 3.0 Edition
+ BSD License, Use at your own risk
+ */
+
+#import "UIViewExt.h"
+
+CGPoint CGRectGetCenter(CGRect rect)
+{
+    CGPoint pt;
+    pt.x = CGRectGetMidX(rect);
+    pt.y = CGRectGetMidY(rect);
+    return pt;
+}
+
+CGRect CGRectMoveToCenter(CGRect rect, CGPoint center)
+{
+    CGRect newrect = CGRectZero;
+    newrect.origin.x = center.x-CGRectGetMidX(rect);
+    newrect.origin.y = center.y-CGRectGetMidY(rect);
+    newrect.size = rect.size;
+    return newrect;
+}
+
+@implementation UIView (ViewGeometry)
+
+// Retrieve and set the origin
+- (CGPoint) origin
+{
+	return self.frame.origin;
+}
+
+- (void) setOrigin: (CGPoint) aPoint
+{
+	CGRect newframe = self.frame;
+	newframe.origin = aPoint;
+	self.frame = newframe;
+}
+
+// Retrieve and set the size
+- (CGSize) size
+{
+	return self.frame.size;
+}
+
+- (void) setSize: (CGSize) aSize
+{
+	CGRect newframe = self.frame;
+	newframe.size = aSize;
+	self.frame = newframe;
+}
+
+// Query other frame locations
+- (CGPoint) bottomRight
+{
+	CGFloat x = self.frame.origin.x + self.frame.size.width;
+	CGFloat y = self.frame.origin.y + self.frame.size.height;
+	return CGPointMake(x, y);
+}
+
+- (CGPoint) bottomLeft
+{
+	CGFloat x = self.frame.origin.x;
+	CGFloat y = self.frame.origin.y + self.frame.size.height;
+	return CGPointMake(x, y);
+}
+
+- (CGPoint) topRight
+{
+	CGFloat x = self.frame.origin.x + self.frame.size.width;
+	CGFloat y = self.frame.origin.y;
+	return CGPointMake(x, y);
+}
+
+// Retrieve and set height, width, top, bottom, left, right
+- (CGFloat) height
+{
+	return self.frame.size.height;
+}
+
+- (void) setHeight: (CGFloat) newheight
+{
+	CGRect newframe = self.frame;
+	newframe.size.height = newheight;
+	self.frame = newframe;
+}
+
+- (CGFloat) width
+{
+	return self.frame.size.width;
+}
+
+- (void) setWidth: (CGFloat) newwidth
+{
+	CGRect newframe = self.frame;
+	newframe.size.width = newwidth;
+	self.frame = newframe;
+}
+
+- (CGFloat) top
+{
+	return self.frame.origin.y;
+}
+
+- (void) setTop: (CGFloat) newtop
+{
+	CGRect newframe = self.frame;
+	newframe.origin.y = newtop;
+	self.frame = newframe;
+}
+
+- (CGFloat) left
+{
+	return self.frame.origin.x;
+}
+
+- (void) setLeft: (CGFloat) newleft
+{
+	CGRect newframe = self.frame;
+	newframe.origin.x = newleft;
+	self.frame = newframe;
+}
+
+- (CGFloat) bottom
+{
+	return self.frame.origin.y + self.frame.size.height;
+}
+
+- (void) setBottom: (CGFloat) newbottom
+{
+	CGRect newframe = self.frame;
+	newframe.origin.y = newbottom - self.frame.size.height;
+	self.frame = newframe;
+}
+
+- (CGFloat) right
+{
+	return self.frame.origin.x + self.frame.size.width;
+}
+
+- (void) setRight: (CGFloat) newright
+{
+	CGFloat delta = newright - (self.frame.origin.x + self.frame.size.width);
+	CGRect newframe = self.frame;
+	newframe.origin.x += delta ;
+	self.frame = newframe;
+}
+
+// Move via offset
+- (void) moveBy: (CGPoint) delta
+{
+	CGPoint newcenter = self.center;
+	newcenter.x += delta.x;
+	newcenter.y += delta.y;
+	self.center = newcenter;
+}
+
+// Scaling
+- (void) scaleBy: (CGFloat) scaleFactor
+{
+	CGRect newframe = self.frame;
+	newframe.size.width *= scaleFactor;
+	newframe.size.height *= scaleFactor;
+	self.frame = newframe;
+}
+
+// Ensure that both dimensions fit within the given size by scaling down
+- (void) fitInSize: (CGSize) aSize
+{
+	CGFloat scale;
+	CGRect newframe = self.frame;
+	
+	if (newframe.size.height && (newframe.size.height > aSize.height))
+	{
+		scale = aSize.height / newframe.size.height;
+		newframe.size.width *= scale;
+		newframe.size.height *= scale;
+	}
+	
+	if (newframe.size.width && (newframe.size.width >= aSize.width))
+	{
+		scale = aSize.width / newframe.size.width;
+		newframe.size.width *= scale;
+		newframe.size.height *= scale;
+	}
+	self.frame = newframe;	
+}
+@end

+ 52 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCCEmoticon.swift

@@ -0,0 +1,52 @@
+//
+//  JCCEmotiocon.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCCEmoticon: JCEmoticon {
+
+    public required init?(object: NSDictionary) {
+        guard let id = object["id"] as? String, let title = object["title"] as? String, let type = object["type"] as? Int else {
+            return nil
+        }
+        
+        self.id = id
+        self.title = title
+        
+        super.init()
+        
+        self.image = object["image"] as? String
+        self.preview = object["preview"] as? String
+        
+        if type == 1 {
+            self.contents = object["contents"]
+        }
+    }
+    
+    public static func emoticons(with objects: NSArray, at directory: String) -> [JCCEmoticon] {
+        return objects.flatMap {
+            guard let dic = $0 as? NSDictionary else {
+                return nil
+            }
+            guard let e = self.init(object: dic) else {
+                return nil
+            }
+            if let name = e.preview {
+                e.contents = UIImage(contentsOfFile: "\(directory)/\(name)")
+            }
+            return e
+        }
+    }
+    
+    var id: String
+    var title: String
+    
+    var image: String?
+    var preview: String?
+    
+}

+ 49 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCCEmoticonGroup.swift

@@ -0,0 +1,49 @@
+//
+//  JCCEmoticonGroup.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class JCCEmoticonGroup: JCEmoticonGroup {
+    init?(contentsOfFile: String) {
+        guard let dic = NSDictionary(contentsOfFile: contentsOfFile), let arr = dic["emoticons"] as? NSArray else {
+            return nil
+        }
+        let directory = URL(fileURLWithPath: contentsOfFile).deletingLastPathComponent().path
+        
+        super.init()
+        
+        type = JCEmoticonType(rawValue: dic["type"] as? Int ?? 0) ?? .small
+        rows = dic["rows"] as? Int ?? 3
+        columns = dic["columns"] as? Int ?? 7
+        rowsInLandscape = dic["rowsInLandscape"] as? Int ?? 2
+        columnsInLandscape = dic["columnsInLandscape"] as? Int ?? 13
+        
+        if let img = dic["image"] as? String {
+            thumbnail = UIImage(contentsOfFile: "\(directory)/\(img)")
+        }
+        
+        if type.isSmall {
+            emoticons = JCCEmoticon.emoticons(with: arr, at: directory)
+        } else {
+            emoticons = JCCEmoticonLarge.emoticons(with: arr, at: directory)
+        }
+    }
+    
+    convenience init?(identifier : String) {
+        let bundle = Bundle.main
+        guard let path = bundle.path(forResource: "emoticons.bundle/\(identifier)/Info", ofType: "plist") else {
+            return nil
+        }
+        self.init(contentsOfFile: path)
+    }
+    
+    open var rows: Int = 3
+    open var columns: Int = 7
+    open var rowsInLandscape: Int = 3
+    open var columnsInLandscape: Int = 13
+}

+ 47 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCCEmoticonLarge.swift

@@ -0,0 +1,47 @@
+//
+//  JCCEmoticonLarge.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCCEmoticonLarge: JCCEmoticon {
+    
+    open override func draw(in rect: CGRect, in ctx: CGContext) {
+        guard let image = contents as? UIImage else {
+            return
+        }
+        
+        var nframe1 = rect.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0))
+        var nframe2 = rect.inset(by: UIEdgeInsets(top: nframe1.height, left: 0, bottom: 0, right: 0))
+        
+        // 图标
+        let scale = min(min(nframe1.width / image.size.width, nframe1.height / image.size.height), 1)
+        let imageSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
+        
+        nframe1.origin.x = nframe1.minX + (nframe1.width - imageSize.width) / 2
+        nframe1.origin.y = nframe1.minY + (nframe1.height - imageSize.height) / 2
+        nframe1.size.width = imageSize.width
+        nframe1.size.height = imageSize.height
+        
+        image.draw(in: nframe1)
+        
+        // 标题
+        let cfg = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12),
+                   NSAttributedString.Key.foregroundColor: UIColor.gray]
+        let name = title as NSString
+        
+        let titleSize = name.size(withAttributes: cfg)
+        
+        nframe2.origin.x = nframe2.minX + (nframe2.width - titleSize.width) / 2
+        nframe2.origin.y = nframe2.minY + (nframe2.height - titleSize.height) / 2
+        nframe2.size.width = titleSize.width
+        nframe2.size.height = titleSize.height
+        
+        name.draw(in: nframe2, withAttributes: cfg)
+    }
+    
+}

+ 62 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCDraft.swift

@@ -0,0 +1,62 @@
+//
+//  JCDraft.swift
+//  JChat
+//
+//  Created by deng on 2017/6/2.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class JCDraft: NSObject {
+    
+    static var draftCache: Dictionary<String, String> = Dictionary()
+    
+    static func update(text: String?, conversation: JMSGConversation) {
+        let id = JCDraft.getDraftId(conversation)
+        if text == nil || (text?.isEmpty)! {
+            UserDefaults.standard.removeObject(forKey: id)
+            draftCache.removeValue(forKey: id)
+            return
+        }
+        UserDefaults.standard.set(text!, forKey: id)
+        draftCache[id] = text!
+    }
+    
+    static func getDraft(_ conversation: JMSGConversation) -> String? {
+        let id = JCDraft.getDraftId(conversation)
+        if let cache = draftCache[id] {
+            return cache
+        }
+        let draft = UserDefaults.standard.object(forKey: id) as? String
+        if draft != nil {
+            draftCache[id] = draft
+        } else {
+            draftCache[id] = ""
+        }
+        return draft
+    }
+    
+    static func getDraftId(_ conversation: JMSGConversation) -> String {
+        var id = ""
+        let me = JMSGUser.myInfo()
+        if me.username.isEmpty {
+            return ""
+        }
+        if conversation.ex.isGroup {
+            guard let group = conversation.target as? JMSGGroup else {
+                return ""
+            }
+            id = "\(me.username)\(me.appKey!)\(group.gid)"
+        } else {
+            guard let user = conversation.target as? JMSGUser else {
+                return ""
+            }
+            guard let appkey = user.appKey else {
+                return ""
+            }
+            id = "\(me.username)\(me.appKey!)\(user.username)\(appkey)"
+        }
+        return id
+    }
+}

+ 170 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCMessage.swift

@@ -0,0 +1,170 @@
+//
+//  JCMessage.swift
+//  JChat
+//
+//  Created by deng on 10/04/2017.
+//  Copyright © 2017 HXHG. All rights reserved.
+//
+
+import UIKit
+import JMessage
+
+open class JCMessage: NSObject, JCMessageType {
+
+    init(content: JCMessageContentType) {
+        self.content = content
+        self.options = JCMessageOptions(with: content)
+        super.init()
+    }
+    
+    public let identifier: UUID = .init()
+    open var msgId = ""
+    open var name: String = "UserName"
+    open var date: Date = .init()
+    open var sender: JMSGUser?
+    open var senderAvator: UIImage?
+    open var receiver: JMSGUser?
+    open var content: JCMessageContentType
+    public let options: JCMessageOptions
+    open var updateSizeIfNeeded: Bool = false
+    open var unreadCount: Int = 0
+    open var targetType: MessageTargetType = .single
+}
+
+extension JMSGMessage {
+    typealias Callback = (JMSGMessage, Data) -> Void
+
+    func parseMessage(_ delegate: JCMessageDelegate, _ updateMediaMessage: Callback? = nil) -> JCMessage {
+
+        var msg: JCMessage!
+        let currentUser = JMSGUser.myInfo()
+        let isCurrent = fromUser.isEqual(to: currentUser)
+        let state = self.ex.state
+        let isGroup = targetType == .group
+
+        switch(contentType) {
+        case .text:
+            if ex.isBusinessCard {
+                let businessCardContent = JCBusinessCardContent()
+                businessCardContent.delegate = delegate
+                businessCardContent.appKey = ex.businessCardAppKey
+                businessCardContent.userName = ex.businessCardName
+                msg = JCMessage(content: businessCardContent)
+            } else {
+                let content = self.content as! JMSGTextContent
+                msg = JCMessage(content: JCMessageTextContent(text: content.text))
+            }
+        case .image:
+            let content = self.content as! JMSGImageContent
+            let imageContent = JCMessageImageContent()
+            imageContent.imageSize = content.imageSize
+            if ex.isLargeEmoticon {
+                imageContent.imageSize = CGSize(width: content.imageSize.width / 3, height: content.imageSize.height / 3)
+            }
+            if state == .sending {
+                content.uploadHandler = {  (percent:Float, msgId:(String?)) -> Void in
+                    imageContent.upload?(percent)
+                }
+            }
+            imageContent.delegate = delegate
+            msg = JCMessage(content: imageContent)
+
+            if let path = content.thumbImageLocalPath {
+                let image = UIImage(contentsOfFile: path)
+                imageContent.image = image
+                msg.content = imageContent
+            } else {
+                content.thumbImageData({ (data, id, error) in
+                    if let data = data {
+                        if let updateMediaMessage = updateMediaMessage {
+                            updateMediaMessage(self, data)
+                        }
+                    }
+                })
+            }
+        case .eventNotification:
+            let content = self.content as! JMSGEventContent
+            let noticeContent = JCMessageNoticeContent(text: content.showEventNotification())
+            msg = JCMessage(content: noticeContent)
+            msg.options.showsTips = false
+        case .voice:
+            let content = self.content as! JMSGVoiceContent
+            let voiceContent = JCMessageVoiceContent()
+            voiceContent.duration = TimeInterval(content.duration.intValue)
+            voiceContent.delegate = delegate
+            msg = JCMessage(content: voiceContent)
+            content.voiceData({ (data, id, error) in
+                if let data = data {
+                    voiceContent.data = data
+                }
+            })
+        case .file:
+            let content = self.content as! JMSGFileContent
+            if ex.isShortVideo {
+                let videoContent = JCMessageVideoContent()
+                videoContent.delegate = delegate
+                msg = JCMessage(content: videoContent)
+                if let path = content.originMediaLocalPath {
+                    let url = URL(fileURLWithPath: path)
+                    videoContent.data = try! Data(contentsOf: url)
+                } else {
+                    content.fileData({ (data, id, error) in
+                        if let data = data {
+                            if let updateMediaMessage = updateMediaMessage {
+                                updateMediaMessage(self, data)
+                            }
+                        }
+                    })
+                }
+                videoContent.fileContent = content
+            } else {
+                let fileContent = JCMessageFileContent()
+                fileContent.delegate = delegate
+                fileContent.fileName = content.fileName
+                fileContent.fileType = ex.fileType
+                fileContent.fileSize = ex.fileSize
+                if let path = content.originMediaLocalPath {
+                    let url = URL(fileURLWithPath: path)
+                    fileContent.data = try! Data(contentsOf: url)
+                }
+                msg = JCMessage(content: fileContent)
+            }
+        case .location:
+            let content = self.content as! JMSGLocationContent
+            let locationContent = JCMessageLocationContent()
+            locationContent.address = content.address
+            locationContent.lat = content.latitude.doubleValue
+            locationContent.lon = content.longitude.doubleValue
+            locationContent.delegate = delegate
+            msg = JCMessage(content: locationContent)
+        case .prompt:
+            let content = self.content as! JMSGPromptContent
+            let noticeContent = JCMessageNoticeContent(text: content.promptText)
+            msg = JCMessage(content: noticeContent)
+            msg.options.showsTips = false
+        default:
+            msg = JCMessage(content: JCMessageNoticeContent.unsupport)
+        }
+        if msg.options.alignment != .center {
+            msg.options.alignment = isCurrent ? .right : .left
+            if isGroup {
+                msg.options.showsCard = !isCurrent
+            }
+        }
+        if isGroup {
+            msg.targetType = .group
+        } else {
+            msg.targetType = .single
+        }
+        msg.msgId = self.msgId
+        msg.options.state = state
+        if isCurrent {
+            msg.senderAvator = UIImage.getMyAvator()
+        }
+        msg.sender = fromUser
+        msg.name = fromUser.displayName()
+        msg.unreadCount = getUnreadCount()
+        return msg
+    }
+}
+

+ 25 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Model/JCRemind.swift

@@ -0,0 +1,25 @@
+//
+//  JCRemind.swift
+//  JChat
+//
+//  Created by deng on 2017/7/19.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+class JCRemind: NSObject {
+    
+    var user: JMSGUser?
+    var startIndex: Int
+    var endIndex: Int
+    var length: Int
+    var isAtAll: Bool
+    
+    init(_ user: JMSGUser?, _ startIndex: Int, _ endIndex: Int, _ length: Int, _ isAtAll: Bool) {
+        self.user = user
+        self.startIndex = startIndex
+        self.endIndex = endIndex
+        self.length = length
+        self.isAtAll = isAtAll
+    }
+}

+ 26 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Protocols/JCMessageContentViewType.swift

@@ -0,0 +1,26 @@
+//
+//  JCMessageContentViewType.swift
+//  JChat
+//
+//  Created by deng on 10/04/2017.
+//  Copyright © 2017 HXHG. All rights reserved.
+//
+
+import UIKit
+
+@objc public protocol JCMessageContentType: class  {
+    
+    weak var delegate: JCMessageDelegate? { get }
+    var layoutMargins: UIEdgeInsets { get }
+    
+    func sizeThatFits(_ size: CGSize) -> CGSize
+    
+    static var viewType: JCMessageContentViewType.Type { get }
+}
+
+@objc public protocol JCMessageContentViewType: class {
+    
+    init()
+    func apply(_ message: JCMessageType)
+}
+

+ 81 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Protocols/JCMessageOptions.swift

@@ -0,0 +1,81 @@
+//
+//  JCMessageOptions.swift
+//  JChat
+//
+//  Created by deng on 2017/3/8.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+import JMessage
+
+/// 消息类型
+@objc public enum JCMessageStyle: Int {
+    case notice
+    case bubble
+}
+
+/// 消息对齐方式
+@objc public enum JCMessageAlignment: Int {
+    case left
+    case right
+    case center
+}
+
+@objc public enum JCMessageState: Int {
+    case sending
+    case sendError
+    case sendSucceed
+    case downloadFailed
+}
+
+/// 消息选项
+@objc open class JCMessageOptions: NSObject {
+    
+    public override init() {
+        super.init()
+    }
+    
+    public convenience init(with content: JCMessageContentType) {
+        self.init()
+        
+        switch content {
+        case is JCMessageNoticeContent:
+            self.style = .notice
+            self.alignment = .center
+            self.showsCard = false
+            self.showsAvatar = false
+            self.showsBubble = true
+            self.isUserInteractionEnabled = false
+            
+        case is JCMessageTimeLineContent:
+            self.style = .notice
+            self.alignment = .center
+            self.showsCard = false
+            self.showsAvatar = false
+            self.showsBubble = false
+            self.isUserInteractionEnabled = false
+            
+//        case is JCMessageImageContent:
+//            self.showsTips = false
+            
+        default:
+            break
+        }
+    }
+    
+    open var style: JCMessageStyle = .bubble
+    open var alignment: JCMessageAlignment = .left
+    
+    open var isUserInteractionEnabled: Bool = true
+    
+    open var showsCard: Bool = false
+    open var showsAvatar: Bool =  true
+    open var showsBubble: Bool = true
+    open var showsTips: Bool = true
+    open var state: JCMessageState = .sendSucceed
+    
+    internal func fix(with content: JCMessageContentType)  {
+    }
+}
+

+ 45 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Protocols/JCMessageType.swift

@@ -0,0 +1,45 @@
+//
+//  JCMessageType.swift
+//  JChat
+//
+//  Created by deng on 10/04/2017.
+//  Copyright © 2017 HXHG. All rights reserved.
+//
+
+import Foundation
+import JMessage
+
+@objc public enum MessageTargetType: Int {
+    case single = 0
+    case group
+}
+
+@objc public protocol JCMessageType: class {
+    
+    var name: String { get }
+    var identifier: UUID { get }
+    var msgId: String { get }
+    var date: Date { get }
+    var sender: JMSGUser? { get }
+    var senderAvator: UIImage? { get }
+    var receiver: JMSGUser? { get }
+    var content: JCMessageContentType { get }
+    var options: JCMessageOptions { get }
+    var updateSizeIfNeeded: Bool { get }
+    var unreadCount: Int { get }
+    var targetType: MessageTargetType { get }
+}
+
+@objc public protocol JCMessageDelegate: NSObjectProtocol {
+    @objc optional func message(message: JCMessageType, videoData data: Data?)
+    @objc optional func message(message: JCMessageType, voiceData data: Data?, duration: Double)
+    @objc optional func message(message: JCMessageType, fileData data: Data?, fileName: String?, fileType: String?)
+    @objc optional func message(message: JCMessageType, location address: String?, lat: Double, lon: Double)
+    @objc optional func message(message: JCMessageType, image: UIImage?)
+    // user 对象是为了提高效率,如果 user 已经加载出来了,就直接使用,不需要重新去获取一次
+    @objc optional func message(message: JCMessageType, user: JMSGUser?, businessCardName: String, businessCardAppKey: String)
+    @objc optional func clickTips(message: JCMessageType)
+    @objc optional func tapAvatarView(message: JCMessageType)
+    @objc optional func longTapAvatarView(message: JCMessageType)
+    @objc optional func tapUnreadTips(message: JCMessageType)
+}

+ 15 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/Protocols/JCUserType.swift

@@ -0,0 +1,15 @@
+//
+//  JCUserType.swift
+//  JChat
+//
+//  Created by deng on 10/04/2017.
+//  Copyright © 2017 HXHG. All rights reserved.
+//
+
+import UIKit
+
+@objc public protocol JCUserType: class {
+    var identifier: String { get }
+    var name: String? { get }
+    var portrait: UIImage? { get }
+}

+ 132 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/FileCell.swift

@@ -0,0 +1,132 @@
+//
+//  FileCell.swift
+//  JChat
+//
+//  Created by 邓永豪 on 2017/8/28.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class File {
+    var fileIcon: UIImage
+    var fileName: String
+    var fileSize: String
+    var summary: String
+    
+    init(_ fileIcon: UIImage, _ fileName: String, _ fileSize: String, _ summary: String) {
+        self.fileName = fileName
+        self.fileIcon = fileIcon
+        self.fileSize = fileSize
+        self.summary = summary
+    }
+}
+
+class FileCell: JCTableViewCell {
+    
+    var isEditMode: Bool {
+        get {
+            return !selectView.isHidden
+        }
+        set {
+            selectView.isHidden = !newValue
+            contentView.removeConstraint(selectImageWidthConstraint)
+            if newValue {
+                selectImageWidthConstraint = _JCLayoutConstraintMake(selectView, .width, .equal, nil, .notAnAttribute, 21)
+            } else {
+                selectImageWidthConstraint = _JCLayoutConstraintMake(selectView, .width, .equal, nil, .notAnAttribute, 0)
+            }
+            contentView.addConstraint(selectImageWidthConstraint)
+        }
+    }
+    
+    var isSelectImage: Bool {
+        get {
+            return isSelect
+        }
+        set {
+            if newValue {
+                selectView.image = UIImage.loadImage("com_icon_file_select")
+            } else {
+                selectView.image = UIImage.loadImage("com_icon_file_unselect")
+            }
+            isSelect = newValue
+        }
+    }
+
+    required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _init()
+    }
+    
+    override func awakeFromNib() {
+        super.awakeFromNib()
+        _init()
+    }
+    
+    func bindData(_ file: File) {
+        fileIcon.image = file.fileIcon
+        fileName.text = file.fileName
+        fileSize.text = file.fileSize
+        summary.text = file.summary
+    }
+    
+    private var selectImageWidthConstraint: NSLayoutConstraint!
+    private lazy var selectView: UIImageView = UIImageView()
+    private var isSelect = false
+    private lazy var fileIcon: UIImageView = UIImageView()
+    private lazy var fileName: UILabel = {
+        let fileName = UILabel()
+        fileName.textColor = UIColor(netHex: 0x2C2C2C)
+        fileName.font = UIFont.systemFont(ofSize: 15)
+        return fileName
+    }()
+    private lazy var fileSize: UILabel = {
+        let fileSize = UILabel()
+        fileSize.textColor = UIColor(netHex: 0x2C2C2C)
+        fileSize.font = UIFont.systemFont(ofSize: 12)
+        return fileSize
+    }()
+    private lazy var summary: UILabel = {
+        let summary = UILabel()
+        summary.textColor = UIColor(netHex: 0x999999)
+        summary.font = UIFont.systemFont(ofSize: 12)
+        return summary
+    }()
+    
+    private func _init() {
+        selectView.image = UIImage.loadImage("com_icon_file_unselect")
+        
+        contentView.addSubview(fileIcon)
+        contentView.addSubview(fileName)
+        contentView.addSubview(fileSize)
+        contentView.addSubview(summary)
+        contentView.addSubview(selectView)
+        
+        selectImageWidthConstraint = _JCLayoutConstraintMake(selectView, .width, .equal, nil, .notAnAttribute, 0)
+        contentView.addConstraint(_JCLayoutConstraintMake(selectView, .left, .equal, contentView, .left, 17.5))
+        contentView.addConstraint(_JCLayoutConstraintMake(selectView, .centerY, .equal, contentView, .centerY))
+        contentView.addConstraint(selectImageWidthConstraint)
+        contentView.addConstraint(_JCLayoutConstraintMake(selectView, .height, .equal, nil, .notAnAttribute, 21))
+        
+        contentView.addConstraint(_JCLayoutConstraintMake(fileIcon, .left, .equal, selectView, .right, 17.5))
+        contentView.addConstraint(_JCLayoutConstraintMake(fileIcon, .centerY, .equal, contentView, .centerY))
+        contentView.addConstraint(_JCLayoutConstraintMake(fileIcon, .width, .equal, nil, .notAnAttribute, 50))
+        contentView.addConstraint(_JCLayoutConstraintMake(fileIcon, .height, .equal, nil, .notAnAttribute, 50))
+        
+        contentView.addConstraint(_JCLayoutConstraintMake(fileName, .left, .equal, fileIcon, .right, 12.5))
+        contentView.addConstraint(_JCLayoutConstraintMake(fileName, .top, .equal, contentView, .top, 14.5))
+        contentView.addConstraint(_JCLayoutConstraintMake(fileName, .right, .equal, contentView, .right, -17.5))
+        contentView.addConstraint(_JCLayoutConstraintMake(fileName, .height, .equal, nil, .notAnAttribute, 21))
+        
+        contentView.addConstraint(_JCLayoutConstraintMake(fileSize, .right, .equal, fileName, .right))
+        contentView.addConstraint(_JCLayoutConstraintMake(fileSize, .left, .equal, fileName, .left))
+        contentView.addConstraint(_JCLayoutConstraintMake(fileSize, .top, .equal, fileName, .bottom))
+        contentView.addConstraint(_JCLayoutConstraintMake(fileSize, .height, .equal, nil, .notAnAttribute, 16.5))
+        
+        contentView.addConstraint(_JCLayoutConstraintMake(summary, .left, .equal, fileSize, .left))
+        contentView.addConstraint(_JCLayoutConstraintMake(summary, .right, .equal, fileSize, .right))
+        contentView.addConstraint(_JCLayoutConstraintMake(summary, .top, .equal, fileSize, .bottom))
+        contentView.addConstraint(_JCLayoutConstraintMake(summary, .height, .equal, fileSize, .height))
+    }
+}

+ 80 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/GroupAvatorCell.swift

@@ -0,0 +1,80 @@
+//
+//  GroupAvatorCell.swift
+//  JChat
+//
+//  Created by 邓永豪 on 2017/9/19.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class GroupAvatorCell: JCTableViewCell {
+
+    var title: String {
+        get {
+            return self.titleLabel.text!
+        }
+        set {
+            return self.titleLabel.text  = newValue
+        }
+    }
+
+    var avator: UIImage? {
+        get {
+            return avatorView.image
+        }
+        set {
+            avatorView.image = newValue
+        }
+    }
+
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        _init()
+    }
+
+    required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _init()
+    }
+
+    override func awakeFromNib() {
+        super.awakeFromNib()
+        _init()
+    }
+
+    private lazy var titleLabel: UILabel = UILabel()
+    private lazy var avatorView: UIImageView =  UIImageView()
+
+    func bindData(_ group: JMSGGroup) {
+        group.thumbAvatarData { (data, id , error) in
+            if let data = data {
+                let image = UIImage(data: data)
+                self.avatorView.image = image
+            }
+        }
+    }
+
+    //MARK: - private func
+    private func _init() {
+        titleLabel.textAlignment = .left
+        titleLabel.font = UIFont.systemFont(ofSize: 16)
+
+        avatorView.contentMode = .scaleAspectFill
+        avatorView.image = UIImage.loadImage("com_icon_group_36")
+        avatorView.clipsToBounds = true
+
+        contentView.addSubview(avatorView)
+        contentView.addSubview(titleLabel)
+
+        addConstraint(_JCLayoutConstraintMake(titleLabel, .left, .equal, contentView, .left, 15))
+        addConstraint(_JCLayoutConstraintMake(titleLabel, .right, .equal, contentView, .centerX))
+        addConstraint(_JCLayoutConstraintMake(titleLabel, .centerY, .equal, contentView, .centerY))
+        addConstraint(_JCLayoutConstraintMake(titleLabel, .height, .equal, nil, .notAnAttribute, 22.5))
+
+        addConstraint(_JCLayoutConstraintMake(avatorView, .right, .equal, contentView, .right))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .centerY, .equal, contentView, .centerY))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .height, .equal, nil, .notAnAttribute, 36))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .width, .equal, nil, .notAnAttribute, 36))
+    }
+}

+ 85 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/ImageFileCell.swift

@@ -0,0 +1,85 @@
+//
+//  ImageFileCell.swift
+//  JChat
+//
+//  Created by 邓永豪 on 2017/8/28.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class ImageFileCell: UICollectionViewCell {
+    
+    var isEditMode: Bool {
+        get {
+            return !selectView.isHidden
+        }
+        set {
+            selectView.isHidden = !newValue
+        }
+    }
+    
+    var isSelectImage: Bool {
+        get {
+            return isSelect
+        }
+        set {
+            if newValue {
+                selectView.image = UIImage.loadImage("com_icon_file_select")
+            } else {
+                selectView.image = UIImage.loadImage("com_icon_file_unselect")
+            }
+            isSelect = newValue
+        }
+    }
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        _init()
+    }
+    
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+    
+    func bindDate(_ message: JMSGMessage) {
+        if message.contentType == .image {
+            let content = message.content as! JMSGImageContent
+            content.largeImageData(progress: nil, completionHandler: { (data, msgId, error) in
+                if msgId == message.msgId {
+                    if let data = data {
+                        let image = UIImage(data: data)
+                        self.imageView.image = image
+                    }
+                }
+            })
+        }
+        guard let content = message.content as? JMSGFileContent else {
+            return
+        }
+        content.fileData { (data, msgId, error) in
+            if msgId == message.msgId {
+                if let data = data {
+                    let image = UIImage(data: data)
+                    self.imageView.image = image
+                }
+            }
+        }
+    }
+    
+    lazy var imageView: UIImageView = UIImageView()
+    private var isSelect = false
+    private lazy var selectView: UIImageView = UIImageView()
+
+    private func _init(){
+        imageView.frame = CGRect(x: 0, y: 0, width: self.width, height: self.height)
+        imageView.contentMode = .scaleAspectFit
+        imageView.clipsToBounds = true
+        addSubview(imageView)
+        
+        selectView.isHidden = true
+        selectView.frame = CGRect(x: self.width - 31, y: 5, width: 21, height: 21)
+        selectView.image = UIImage.loadImage("com_icon_file_unselect")
+        addSubview(selectView)
+    }
+}

+ 29 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/ImageFileHeader.swift

@@ -0,0 +1,29 @@
+//
+//  SHomeHeader.swift
+//
+//  Created by wangjie on 16/5/4.
+//  Copyright © 2016年 wangjie. All rights reserved.
+//
+
+import UIKit
+
+class ImageFileHeader: UICollectionReusableView {
+
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        _init()
+    }
+
+    required init?(coder aDecoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    var titleLabel: UILabel!
+
+    private func _init(){
+        titleLabel = UILabel(frame: CGRect(x: 16.5, y: 0, width: self.width, height: self.height))
+        titleLabel.textColor = UIColor(netHex: 0x808080)
+        titleLabel.font = UIFont.systemFont(ofSize: 13)
+        addSubview(titleLabel)
+    }
+}

+ 494 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatView.swift

@@ -0,0 +1,494 @@
+//
+//  JCChatView.swift
+//  JChat
+//
+//  Created by deng on 2017/2/28.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+public protocol JCChatViewDataSource: class {
+    
+    func numberOfItems(in chatView: JCChatView)
+    
+    func chatView(_ chatView: JCChatView, itemAtIndexPath: IndexPath)
+    
+}
+
+var isWait = false
+
+@objc public protocol JCChatViewDelegate: NSObjectProtocol {
+    
+    @objc optional func chatView(_ chatView: JCChatView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool
+    @objc optional func chatView(_ chatView: JCChatView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool
+    @objc optional func chatView(_ chatView: JCChatView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?)
+    @objc optional func refershChatView(chatView: JCChatView)
+    @objc optional func tapImageMessage(image: UIImage?, indexPath: IndexPath)
+    
+    @objc optional func deleteMessage(message: JCMessageType)
+    @objc optional func copyMessage(message: JCMessageType)
+    @objc optional func forwardMessage(message: JCMessageType)
+    @objc optional func withdrawMessage(message: JCMessageType)
+
+    @objc optional func indexPathsForVisibleItems(chatView: JCChatView, items: [IndexPath])
+}
+
+
+@objc open class JCChatView: UIView {
+    
+    public init(frame: CGRect, chatViewLayout: JCChatViewLayout) {
+        _chatViewData = JCChatViewData()
+        _chatViewLayout = chatViewLayout
+        let containerViewFrame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
+        _chatContainerView = JCChatContainerView(frame: containerViewFrame, collectionViewLayout: chatViewLayout)
+        super.init(frame: frame)
+        _commonInit()
+    }
+    
+    public required init?(coder aDecoder: NSCoder) {
+        // decode layout
+        guard let chatViewLayout = JCChatViewLayout(coder: aDecoder) else {
+            return nil
+        }
+        // decode container view
+        guard let chatContainerView = JCChatContainerView(coder: aDecoder) else {
+            return nil
+        }
+        // init data
+        _chatViewData = JCChatViewData()
+        // init to layout & container view
+        _chatViewLayout = chatViewLayout
+        _chatContainerView = chatContainerView
+        // init super
+        super.init(coder: aDecoder)
+        // init other data
+        _commonInit()
+    }
+    
+    open weak var delegate: JCChatViewDelegate?
+    open weak var dataSource: JCChatViewDataSource?
+    open weak var messageDelegate: JCMessageDelegate?
+    
+    func insert(_ newMessage: JCMessageType, at index: Int) {
+        _batchBegin()
+        _batchItems.append(.insert(newMessage, at: index))
+        _batchCommit()
+    }
+    func insert(contentsOf newMessages: Array<JCMessageType>, at index: Int) {
+        _batchBegin()
+        _batchItems.append(contentsOf: newMessages.map({ .insert($0, at: index) }))
+        _batchCommit(true)
+    }
+    
+    func update(_ newMessage: JCMessageType, at index: Int) {
+        _batchBegin()
+        _batchItems.append(.update(newMessage, at: index))
+        _batchCommit()
+    }
+    
+    func removeAll() {
+        _batchBegin()
+        for index in 0..<_chatViewData.count {
+            _batchItems.append(.remove(at: index))
+        }
+        _batchCommit()
+    }
+    
+    func remove(at index: Int) {
+        _batchBegin()
+        _batchItems.append(.remove(at: index))
+        _batchCommit()
+    }
+    func remove(contentOf indexs: Array<Int>) {
+        _batchBegin()
+        _batchItems.append(contentsOf: indexs.map({ .remove(at: $0) }))
+        _batchCommit()
+    }
+    
+    func move(at index1: Int, to index2: Int) {
+        _batchBegin()
+        _batchItems.append(.move(at: index1, to: index2))
+        _batchCommit()
+    }
+    
+    func append(_ newMessage: JCMessageType) {
+        insert(newMessage, at: _chatViewData.count)
+    }
+    func append(contentsOf newMessages: Array<JCMessageType>) {
+        insert(contentsOf: newMessages, at: _chatViewData.count)
+    }
+    
+    fileprivate func _batchBegin() {
+        _chatContainerView.messageDelegate = self.messageDelegate
+        objc_sync_enter(_batchItems)
+        _batchRequiredCount = max(_batchRequiredCount + 1, 1)
+        objc_sync_exit(_batchItems)
+    }
+    fileprivate func _batchCommit(_ isInsert: Bool = false) {
+        objc_sync_enter(_batchItems)
+        _batchRequiredCount = max(_batchRequiredCount - 1, 0)
+        guard _batchRequiredCount == 0 else {
+            objc_sync_exit(_batchItems)
+            return
+        }
+        let oldData = _chatViewData
+        let newData = JCChatViewData()
+        let updateItems = _batchItems
+        _batchItems.removeAll()
+        objc_sync_exit(_batchItems)
+        
+        _ = _chatContainerView.numberOfItems(inSection: 0)
+        let update = JCChatViewUpdate(newData: newData, oldData: oldData, updateItems: updateItems)
+        // exec
+        _chatViewData = newData
+        _chatContainerView.performBatchUpdates(with: update, isInsert, completion: nil)
+    }
+    
+    fileprivate lazy var _batchItems: Array<JCChatViewUpdateChangeItem> = []
+    fileprivate lazy var _batchRequiredCount: Int = 0
+    
+    private func _commonInit() {
+        
+        backgroundColor = UIColor(netHex: 0xe8edf3)
+        let header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(_onPullToFresh))
+        header?.stateLabel.isHidden = true
+        _chatContainerView.mj_header = header
+        _chatContainerView.allowsSelection = false
+        _chatContainerView.allowsMultipleSelection = false
+        _chatContainerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+        _chatContainerView.keyboardDismissMode = .onDrag
+        _chatContainerView.backgroundColor = UIColor(netHex: 0xE8EDF3)
+        _chatContainerView.dataSource = self
+        _chatContainerView.delegate = self
+        
+        addSubview(_chatContainerView)
+        #if READ_VERSION
+        _chatContainerView.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
+        #endif
+    }
+    
+    fileprivate var _chatViewData: JCChatViewData
+    
+    fileprivate var _chatViewLayout: JCChatViewLayout
+    fileprivate var _chatContainerView: JCChatContainerView
+    
+    fileprivate lazy var _chatContainerRegistedTypes: Set<String> = []
+    
+    @objc func _onPullToFresh() {
+        delegate?.refershChatView?(chatView: self)
+    }
+    func stopRefresh() {
+        _chatContainerView.mj_header.endRefreshing()
+    }
+    
+    func scrollToLast(animated: Bool) {
+        let count = _chatContainerView.numberOfItems(inSection: 0)
+        if count > 0 {
+            _chatContainerView.scrollToItem(at: IndexPath(row: count - 1, section: 0), at: .bottom, animated: animated)
+        }
+    }
+
+    deinit {
+        #if READ_VERSION
+        _chatContainerView.removeObserver(self, forKeyPath: "contentOffset")
+        #endif
+    }
+}
+
+internal class JCChatContainerView: UICollectionView {
+    
+    weak var messageDelegate: JCMessageDelegate?
+    
+    var currentUpdate: JCChatViewUpdate? {
+        return _currentUpdate
+    }
+    
+    func performBatchUpdates(with update: JCChatViewUpdate, _ isInsert: Bool = false, completion:((Bool) -> Void)?) {
+        
+        // read changes
+        guard let changes = update.updateChanges else {
+            return
+        }
+        _currentUpdate = update
+        
+        // TODO: 不是最优
+        if update.updateItems.count > 0 {
+            for item in update.updateItems {
+                switch item {
+                case .update:
+                    self.performBatchUpdates({ 
+                        self.reloadItems(at: [IndexPath(row: item.at, section: 0)])
+                    }, completion: nil)
+                    return
+                default:
+                    break
+                }
+            }
+        }
+        
+        
+        var oldContent = self.contentSize
+//        self.contentSize = CGSize(width: self.contentSize.width, height: 3725)
+//        self.setContentOffset(CGPoint(x: 0, y: 3725 - oldContent.height), animated: false)
+//        self.layoutIfNeeded()
+//        self.setContentOffset(CGPoint(x: 0, y: 250232320), animated: false)
+//        self.layoutIfNeeded()
+        // commit changes
+        
+        UIView.animate(withDuration: 0) {
+            if isInsert {
+                self.isHidden = true
+            }
+            self.performBatchUpdates({
+                // apply move
+                changes.filter({ $0.isMove }).forEach({
+                    self.moveItem(at: .init(item: max($0.from, 0), section: 0),
+                                  to: .init(item: max($0.to, 0), section: 0))
+                })
+//                print(oldContent)
+                // apply insert/remove/update
+                self.insertItems(at: changes.filter({ $0.isInsert }).map({ .init(item: max($0.to, 0), section: 0) }))
+                self.reloadItems(at: changes.filter({ $0.isUpdate }).map({ .init(item: max($0.from, 0), section: 0) }))
+                self.deleteItems(at: changes.filter({ $0.isRemove }).map({ .init(item: max($0.from, 0), section: 0) }))
+                
+            }, completion: { finished in
+                if isInsert {
+                    UIView.animate(withDuration: 0, animations: {
+                        if self.contentSize.height > oldContent.height && oldContent.height != 0 {
+                            self.setContentOffset(CGPoint(x: 0, y: self.contentSize.height - oldContent.height), animated: false)
+                            self.layoutIfNeeded()
+                            oldContent = self.contentSize
+                        }
+                    })
+                    self.isHidden = false
+                }
+                
+                completion?(finished)
+            })
+        }
+        
+        _currentUpdate = nil
+    }
+    
+    private var _currentUpdate: JCChatViewUpdate?
+}
+
+
+
+extension JCChatView: UICollectionViewDataSource, JCChatViewLayoutDelegate {
+    
+    open var isRoll: Bool {
+        return _chatContainerView.isDragging || _chatContainerView.isDecelerating
+    }
+    
+    open dynamic var indexPathsForVisibleItems: [IndexPath] {
+        return _chatContainerView.indexPathsForVisibleItems
+    }
+    
+    open dynamic var contentSize: CGSize {
+        set { return _chatContainerView.contentSize = newValue }
+        get { return _chatContainerView.contentSize }
+    }
+    open dynamic var contentOffset: CGPoint {
+        set { return _chatContainerView.contentOffset = newValue }
+        get { return _chatContainerView.contentOffset }
+    }
+    open dynamic var contentInset: UIEdgeInsets {
+        set { return _chatContainerView.contentInset = newValue }
+        get { return _chatContainerView.contentInset }
+    }
+    open dynamic var scrollIndicatorInsets: UIEdgeInsets {
+        set { return _chatContainerView.scrollIndicatorInsets = newValue }
+        get { return _chatContainerView.scrollIndicatorInsets }
+    }
+    
+    open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+        return _chatViewData.count
+    }
+    
+    open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+
+        let message = _chatViewData[indexPath.item]
+        
+//        let options = (message.options.showsCard.hashValue << 0) | (message.options.showsAvatar.hashValue << 1)
+        let alignment = message.options.alignment.rawValue
+        let identifier = NSStringFromClass(type(of: message.content)) + ".\(alignment)"
+        
+        if !_chatContainerRegistedTypes.contains(identifier) {
+            _chatContainerRegistedTypes.insert(identifier)
+            _chatContainerView.register(JCChatViewCell.self, forCellWithReuseIdentifier: identifier)
+        }
+        let cell = _chatContainerView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! JCChatViewCell
+        cell.delegate = messageDelegate
+        cell.updateView()
+        return cell
+    }
+    
+    open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, itemAt indexPath: IndexPath) -> JCMessageType {
+        return _chatViewData[indexPath.item]
+    }
+    
+    open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
+        guard let collectionViewLayout = collectionViewLayout as? JCChatViewLayout else {
+            return .zero
+        }
+        guard let layoutAttributesInfo = collectionViewLayout.layoutAttributesInfoForItem(at: indexPath) else {
+            return .zero
+        }
+        let size = layoutAttributesInfo.layoutedBoxRect(with: .all).size
+        return .init(width: collectionView.frame.width, height: size.height)
+    }
+    open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAvatarOf style: JCMessageStyle) -> CGSize {
+        // 78 * 78
+        return .init(width: 40, height: 40)
+    }
+    open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemCardOf style: JCMessageStyle) -> CGSize {
+        return .init(width: 0, height: 18)
+    }
+    
+    public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemTipsOf style: JCMessageStyle) -> CGSize {
+        return .init(width: 100, height: 21)
+    }
+    
+    open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemOf style: JCMessageStyle) -> UIEdgeInsets {
+        switch style {
+        case .bubble:
+            // bubble content edg, 2x
+            // +----12--+-+---+
+            // |        | |   |
+            // 16       4 40  16
+            // |        | |   |
+            // +----12--+-+---+
+            return .init(top: 6, left: 8, bottom: 6, right: 2 + 20 + 8)
+            
+        case .notice:
+            // default edg
+            // +----10----+
+            // 20         20
+            // +----10----+
+            return .init(top: 10, left: 20, bottom: 10, right: 20)
+            
+//        default:
+//            // default edg
+//            // +----10----+
+//            // 10         10
+//            // +----10----+
+//            return .init(top: 10, left: 10, bottom: 10, right: 10)
+        }
+    }
+    open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemCardOf style: JCMessageStyle) -> UIEdgeInsets {
+        return .init(top: 0, left: 8, bottom: 2, right: 8)
+    }
+    open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemAvatarOf style: JCMessageStyle) -> UIEdgeInsets {
+        return .init(top: 0, left: 2, bottom: 2, right: 2)
+    }
+    open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemBubbleOf style: JCMessageStyle) -> UIEdgeInsets {
+//        return .init(top: -2, left: 0, bottom: -2, right: 0)
+        return .init(top: 0, left: 8, bottom: 0, right: 0)
+    }
+    open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemContentOf style: JCMessageStyle) -> UIEdgeInsets {
+        switch style {
+        case .bubble:
+            // bubble image edg, scale: 2x, radius: 15
+            // /--------16-------\
+            // |  +-----04-----+ |
+            // 20 04          04 20
+            // |  +-----04-----+ |
+            // \--------16-------/
+//            return .init(top: 8 + 2, left: 10 + 2, bottom: 8 + 2, right: 10 + 2)
+            return .init(top: 2, left: 5 + 2, bottom: 2, right: 2)
+            
+        case .notice:
+            // notice edg
+            // /------4-------\
+            // 10             10
+            // \------4-------/
+            return .init(top: 4, left: 10, bottom: 4, right: 10)
+            
+        }
+    }
+    
+    open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
+        return true
+    }
+    
+    open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
+        let message = _chatViewData[indexPath.item]
+        if message.content is JCMessageNoticeContent || message.content is JCMessageTimeLineContent  {
+            return false
+        }
+        if let _ = message.content as? JCMessageTextContent {
+            if action == #selector(copyMessage(_:)) {
+                return true
+            }
+        }
+        if action == #selector(deleteMessage(_:)) {
+            return true
+        }
+        
+        if action == #selector(forwardMessage(_:)) {
+            return true
+        }
+        
+        if action == #selector(withdrawMessage(_:)) {
+            if let sender = message.sender {
+                if sender.isEqual(to: JMSGUser.myInfo()) {
+                    return true
+                }
+            }
+            return false
+        }
+ 
+        return false
+    }
+    
+    @objc func copyMessage(_ sender: Any) {}
+    @objc func deleteMessage(_ sender: Any) {}
+    @objc func forwardMessage(_ sender: Any) {}
+    @objc func withdrawMessage(_ sender: Any) {}
+    
+    open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
+        let message = _chatViewData[indexPath.item]
+        if action == #selector(copyMessage(_:)) {
+            if let content = message.content as? JCMessageTextContent {
+                let pas = UIPasteboard.general
+                pas.string = content.text.string
+            }
+        }
+        if action == #selector(deleteMessage(_:)) {
+            remove(at: indexPath.item)
+            delegate?.deleteMessage?(message: message)
+        }
+//        if action == #selector(paste(_:)) {
+//            move(at: indexPath.item, to: _chatViewData.count - 1)
+//        }
+        if action == #selector(forwardMessage(_:)) {
+            delegate?.forwardMessage?(message: message)
+        }
+        
+        if action == #selector(withdrawMessage(_:)) {
+            delegate?.withdrawMessage?(message: message)
+        }
+    }
+}
+
+extension JCChatView {
+    override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+        if keyPath == "contentOffset" {
+            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
+                if !isWait {
+                    isWait = true
+                    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.05) {
+                        self.delegate?.indexPathsForVisibleItems?(chatView: self, items: self._chatContainerView.indexPathsForVisibleItems)
+                        isWait = false
+                    }
+                }
+            }
+        }
+    }
+
+}
+
+extension JCChatView: SAIInputBarScrollViewType {
+}

+ 364 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewCell.swift

@@ -0,0 +1,364 @@
+//
+//  JCChatViewCell.swift
+//  JChat
+//
+//  Created by deng on 2017/2/28.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+import CocoaLumberjack
+
+open class JCChatViewCell: UICollectionViewCell, UIGestureRecognizerDelegate {
+    
+    weak var delegate: JCMessageDelegate?
+    
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+    
+    deinit {
+        guard let observer = _menuNotifyObserver else{
+            return
+        }
+        NotificationCenter.default.removeObserver(observer)
+    }
+    
+    func updateView() {
+        guard let message = _layoutAttributes?.message else {
+            return
+        }
+        _tipsView?.apply(message)
+        let tipsView = _tipsView as? JCMessageTipsView
+        if tipsView != nil {
+            tipsView?.delegate = self.delegate
+        }
+        
+        let avatarView = _avatarView as? JCMessageAvatarView
+        if avatarView != nil {
+            avatarView?.delegate = self.delegate
+        }
+        _contentView?.apply(message)
+    }
+    
+    
+    open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
+        
+        super.apply(layoutAttributes)
+        guard let _ = layoutAttributes as? JCChatViewLayoutAttributes else {
+            return
+        }
+        _updateViews()
+        _updateViewLayouts()
+        _updateViewValues()
+    }
+
+    open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
+        guard let rect = _layoutAttributes?.info?.layoutedBoxRect(with: .all) else {
+            return false
+        }
+        return rect.contains(point)
+    }
+    
+    open class var cardViewClass: JCMessageContentViewType.Type {
+        return JCMessageCardView.self
+    }
+    
+    open class var tipsViewClass: JCMessageContentViewType.Type {
+        return JCMessageTipsView.self
+    }
+    
+    open class var avatarViewClass: JCMessageContentViewType.Type {
+        return JCMessageAvatarView.self
+    }
+    
+    
+    private lazy var send_nor = UIImage.loadImage("chat_bubble_send_nor")!.resizableImage(withCapInsets: UIEdgeInsets(top: 25, left: 25, bottom: 25, right: 25))
+    private lazy var send_press = UIImage.loadImage("chat_bubble_send_press")!.resizableImage(withCapInsets: UIEdgeInsets(top: 25, left: 25, bottom: 25, right: 25))
+    
+    private lazy var recive_nor = UIImage.loadImage("chat_bubble_recive_nor")!.resizableImage(withCapInsets: UIEdgeInsets(top: 25, left: 25, bottom: 25, right: 25))
+    private lazy var recive_press = UIImage.loadImage("chat_bubble_recive_press")!.resizableImage(withCapInsets: UIEdgeInsets(top: 25, left: 25, bottom: 25, right: 25))
+    
+    private func _updateViews() {
+
+        guard let message = _layoutAttributes?.message else {
+            return
+        }
+        let options = message.options
+
+        if options.showsBubble {
+            if _bubbleView == nil {
+                _bubbleView = UIImageView()
+            }
+            if let view = _bubbleView, view.superview == nil {
+                insertSubview(view, belowSubview: contentView)
+            }
+        } else {
+            if let view = _bubbleView {
+                view.removeFromSuperview()
+            }
+            _bubbleView = nil
+        }
+        
+        if options.showsCard {
+            if _cardView == nil {
+                _cardView = type(of: self).cardViewClass._init()
+            }
+            if let view = _cardView as? UIView, view.superview == nil {
+                contentView.addSubview(view)
+            }
+        } else {
+            if let view = _cardView as? UIView {
+                view.removeFromSuperview()
+            }
+            _cardView = nil
+        }
+        
+        if options.showsTips {
+            if _tipsView == nil {
+                _tipsView = type(of: self).tipsViewClass._init()
+            }
+            if let view = _tipsView as? UIView, view.superview == nil {
+                contentView.addSubview(view)
+            }
+        } else {
+            if let view = _tipsView as? UIView {
+                view.removeFromSuperview()
+            }
+            _tipsView = nil
+        }
+        
+        
+        if options.showsAvatar {
+            if _avatarView == nil {
+                _avatarView = type(of: self).avatarViewClass._init()
+            }
+            if let view = _avatarView as? UIView, view.superview == nil {
+                contentView.addSubview(view)
+            }
+        } else {
+            if let view = _avatarView as? UIView {
+                view.removeFromSuperview()
+            }
+            _avatarView = nil
+        }
+        
+        if _contentView == nil {
+            // create
+            _contentView = type(of: message.content).viewType._init()
+            // move
+            if let view = _contentView as? UIView, view.superview == nil {
+                contentView.addSubview(view)
+            }
+        }
+    }
+    private func _updateViewLayouts() {
+        // prepare
+        guard let layoutInfo = _layoutAttributes?.info else {
+            return
+        }
+        // update bubble view layout
+        if let view = _bubbleView {
+            view.frame = layoutInfo.layoutedRect(with: .bubble)
+        }
+        // update visit card view layout
+        if let view = _cardView as? UIView {
+            view.frame = layoutInfo.layoutedRect(with: .card)
+        }
+        if let view = _tipsView as? UIView {
+            let frame = layoutInfo.layoutedRect(with: .tips)
+            view.frame = frame
+        }
+        // update avatar view layout
+        if let view = _avatarView as? UIView {
+            view.frame = layoutInfo.layoutedRect(with: .avatar)
+        }
+        
+        // update content view layout
+        if let view = _contentView as? UIView {
+            view.frame = layoutInfo.layoutedRect(with: .content)
+        }
+    }
+    private func _updateViewValues() {
+        guard let message = _layoutAttributes?.message else {
+            return
+        }
+        let options = message.options
+        
+        _cardView?.apply(message)
+        _tipsView?.apply(message)
+        _avatarView?.apply(message)
+        let avatarView = _avatarView as? JCMessageAvatarView
+        if avatarView != nil {
+            avatarView?.delegate = self.delegate
+            let user = message.sender?.username
+            DDLogDebug("更新头像,发送者头像:\(user ?? "")")
+            let urlstr = AppDelegate.o2Collect.generateURLWithAppContextKey(ContactContext.contactsContextKeyV2, query: ContactContext.personIconByNameQueryV2, parameter: ["##name##":user as AnyObject])
+            let url = URL(string: urlstr!)
+            avatarView?.hnk_setImageFromURL(url!)
+        }
+        _contentView?.apply(message)
+        
+        if let view = _bubbleView {
+            switch options.alignment {
+            case .left:
+                view.image = recive_nor
+                view.highlightedImage = recive_press
+                
+            case .right:
+                view.image = send_nor
+                view.highlightedImage = send_press
+                
+            case .center:
+                break
+            }
+        }
+    }
+    
+    open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+        guard gestureRecognizer == _menuGesture else {
+            return super.gestureRecognizerShouldBegin(gestureRecognizer)
+        }
+        guard let rect = _layoutAttributes?.info?.layoutedBoxRect(with: .content) else {
+            return false
+        }
+        guard rect.contains(gestureRecognizer.location(in: contentView)) else {
+            return false
+        }
+        return true
+    }
+    
+    func copyMessage(_ sender: Any) {}
+    func deleteMessage(_ sender: Any) {}
+    func forwardMessage(_ sender: Any) {}
+    func withdrawMessage(_ sender: Any) {}
+
+    private dynamic func _handleMenuGesture(_ sender: UILongPressGestureRecognizer) {
+        guard sender.state == .began else {
+            return
+        }
+        guard let view = _contentView as? UIView,
+            let content = _layoutAttributes?.message?.content,
+            let info = _layoutAttributes?.info else {
+                return
+        }
+        
+        let rect = info.layoutedRect(with: .content).inset(by: -content.layoutMargins)
+        let menuController = UIMenuController.shared
+        
+        // set responder
+        NSClassFromString("UICalloutBar")?.setValue(self, forKeyPath: "sharedCalloutBar.responderTarget")
+        
+        
+        menuController.menuItems = [
+            UIMenuItem(title: "复制", action: #selector(copyMessage(_:))),
+            UIMenuItem(title: "转发", action: #selector(forwardMessage(_:))),
+            UIMenuItem(title: "撤回", action: #selector(withdrawMessage(_:))),
+            UIMenuItem(title: "删除", action: #selector(deleteMessage(_:)))
+        ]
+    
+        // set menu display position
+        menuController.setTargetRect(convert(rect, to: view), in: view)
+        menuController.setMenuVisible(true, animated: true)
+        
+        // really show?
+        guard menuController.isMenuVisible else {
+            return
+        }
+        
+        // set selected
+        self.isHighlighted = true
+        self._menuNotifyObserver = NotificationCenter.default.addObserver(forName: UIMenuController.willHideMenuNotification, object: nil, queue: nil) { [weak self] notification in
+            // is release?
+            guard let observer = self?._menuNotifyObserver else {
+                return
+            }
+            NotificationCenter.default.removeObserver(observer)
+            // cancel select
+            self?.isHighlighted = false
+            self?._menuNotifyObserver = nil
+        }
+    }
+    
+    open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
+        // menu bar only process
+        guard sender is UIMenuController else {
+            // other is default process
+            return super.canPerformAction(action, withSender: sender)
+        }
+        // check collectionView and attributes
+        guard let view = _collectionView, let indexPath = _layoutAttributes?.indexPath else {
+            return false
+        }
+        // forward to collectionView
+        guard let result = view.delegate?.collectionView?(view, canPerformAction: action, forItemAt: indexPath, withSender: sender) else {
+            return false
+        }
+        return result
+    }
+    
+    open override func perform(_ action: Selector!, with sender: Any!) -> Unmanaged<AnyObject>! {
+        // menu bar only process
+        guard sender is UIMenuController else {
+            // other is default process
+            return super.perform(action, with: sender)
+        }
+        // check collectionView and attributes
+        guard let view = _collectionView, let indexPath = _layoutAttributes?.indexPath else {
+            return nil
+        }
+        // forward to collectionView
+        view.delegate?.collectionView?(view, performAction: action, forItemAt: indexPath, withSender: sender)
+        
+        return nil
+    }
+    
+    private func _commonInit() {
+    }
+    
+    fileprivate var _bubbleView: UIImageView?
+    
+    fileprivate var _cardView: JCMessageContentViewType?
+    fileprivate var _avatarView: JCMessageContentViewType?
+    fileprivate var _contentView: JCMessageContentViewType?
+    fileprivate var _tipsView: JCMessageContentViewType?
+    
+    fileprivate var _menuNotifyObserver: Any?
+    fileprivate var _menuGesture: UILongPressGestureRecognizer? {
+        return value(forKeyPath: "_menuGesture") as? UILongPressGestureRecognizer
+    }
+    
+    @NSManaged fileprivate var _collectionView: UICollectionView?
+    @NSManaged fileprivate var _layoutAttributes: JCChatViewLayoutAttributes?
+}
+
+
+fileprivate extension JCMessageContentViewType {
+    // 如果是NSObject对象, 直接使用self.init()会导致无法释放内存
+    // 解决方案是转为显式类型再调用cls.init()
+    fileprivate static func _init() -> JCMessageContentViewType {
+        guard let cls = self as? NSObject.Type else {
+            return self.init()
+        }
+        guard let ob = cls.init() as? JCMessageContentViewType else {
+            return self.init()
+        }
+        return ob
+    }
+}
+
+fileprivate extension UICollectionReusableView {
+    @NSManaged fileprivate func _setLayoutAttributes(_ layoutAttributes: UICollectionViewLayoutAttributes)
+}
+
+
+fileprivate prefix func -(edg: UIEdgeInsets) -> UIEdgeInsets {
+    // 取反
+    return .init(top: -edg.top, left: -edg.left, bottom: -edg.bottom, right: edg.right)
+}

+ 46 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewData.swift

@@ -0,0 +1,46 @@
+//
+//  JCChatViewData.swift
+//  JChat
+//
+//  Created by deng on 10/04/2017.
+//  Copyright © 2017 HXHG. All rights reserved.
+//
+
+import UIKit
+
+internal class JCChatViewData: NSObject, NSCopying {
+    
+    internal override init() {
+        self.elements = []
+        super.init()
+    }
+    internal init(elements: [JCMessageType]) {
+        self.elements = elements
+        super.init()
+    }
+    
+    internal var count: Int {
+        return elements.count
+    }
+    
+    func copy(with zone: NSZone? = nil) -> Any {
+        return JCChatViewData(elements: self.elements)
+    }
+    
+    
+    internal subscript(index: Int) -> JCMessageType {
+        return elements[index]
+    }
+    
+    
+    internal func subarray(with subrange: Range<Int>) -> Array<JCMessageType> {
+        return Array(elements[subrange])
+    }
+    
+    internal func replaceSubrange(_ subrange: Range<Int>, with collection: Array<JCMessageType>)  {
+        elements.replaceSubrange(subrange, with: collection)
+    }
+    
+    
+    internal var elements: [JCMessageType]
+}

+ 324 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewLayout.swift

@@ -0,0 +1,324 @@
+//
+//  JCChatViewLayout.swift
+//  JChat
+//
+//  Created by deng on 2017/2/28.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+@objc open class JCChatViewLayout: UICollectionViewFlowLayout {
+    
+    public override init() {
+        super.init()
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+    
+    internal weak var _chatView: JCChatView?
+    
+    open override class var layoutAttributesClass: AnyClass {
+        return JCChatViewLayoutAttributes.self
+    }
+    
+    open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
+        let attributes = super.layoutAttributesForItem(at: indexPath)
+        if let attributes = attributes as? JCChatViewLayoutAttributes, attributes.info == nil {
+            attributes.info = layoutAttributesInfoForItem(at: indexPath)
+        }
+        return attributes
+    }
+    open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
+        let arr = super.layoutAttributesForElements(in: rect)
+        arr?.forEach({
+            guard let attributes = $0 as? JCChatViewLayoutAttributes, attributes.info == nil else {
+                return
+            }
+            attributes.info = layoutAttributesInfoForItem(at: attributes.indexPath)
+        })
+        return arr
+    }
+    
+    open func layoutAttributesInfoForItem(at indexPath: IndexPath) -> JCChatViewLayoutAttributesInfo? {
+        guard let collectionView = collectionView, let _ = collectionView.delegate as? JCChatViewLayoutDelegate, let message = _message(at: indexPath) else {
+            return nil
+        }
+        let size = CGSize(width: collectionView.frame.width, height: .greatestFiniteMagnitude)
+        if let info = _allLayoutAttributesInfo[message.identifier] {
+            // 这不是合理的做法
+            if !message.updateSizeIfNeeded {
+                return info
+            }
+        }
+        let options = message.options
+        
+        var allRect: CGRect = .zero
+        var allBoxRect: CGRect = .zero
+        
+        var cardRect: CGRect = .zero
+        var cardBoxRect: CGRect = .zero
+        
+        var avatarRect: CGRect = .zero
+        var avatarBoxRect: CGRect = .zero
+        
+        var bubbleRect: CGRect = .zero
+        var bubbleBoxRect: CGRect = .zero
+        
+        var contentRect: CGRect = .zero
+        var contentBoxRect: CGRect = .zero
+        
+        var tipsRect: CGRect = .zero
+        var tipsBoxRect: CGRect = .zero
+        
+        // 计算的时候以左对齐为基准
+        
+        // +---------------------------------------+ r0
+        // |+---------------------------------+ r1 |
+        // ||+---+ <NAME>                     |    |
+        // ||| A | +---------------------\ r4 |    |
+        // ||+---+ |+---------------+ r5 |    |    |
+        // ||      ||    CONTENT    |    |    |    |
+        // ||      |+---------------+    |    |    |
+        // ||      \---------------------/    |    |  +---+ r6
+        // |+---------------------------------+  <-|- | ! |
+        // +---------------------------------------+  +---+
+        
+        let edg0 = _inset(with: options.style, for: .all)
+        var r0 = CGRect(x: 0, y: 0, width: size.width, height: .greatestFiniteMagnitude)
+        var r1 = r0.inset(by: edg0)
+        
+        var x1 = r1.minX
+        var y1 = r1.minY
+        var x2 = r1.maxX
+        var y2 = r1.maxY
+        
+        if options.showsAvatar {
+            let edg = _inset(with: options.style, for: .avatar)
+            let size = _size(with: options.style, for: .avatar)
+            
+            let box = CGRect(x: x1, y: y1, width: edg.left + size.width + edg.right, height: edg.top + size.height + edg.bottom)
+            let rect = box.inset(by: edg)
+            
+            avatarRect = rect
+            avatarBoxRect = box
+            
+            x1 = box.maxX
+        }
+        
+        if options.showsCard {
+            let edg = _inset(with: options.style, for: .card)
+            let size = _size(with: options.style, for: .card)
+            
+            let box = CGRect(x: x1, y: y1, width: x2 - x1, height: edg.top + size.height + edg.bottom)
+            let rect = box.inset(by: edg)
+            
+            cardRect = rect
+            cardBoxRect = box
+            
+            y1 = box.maxY
+        }
+
+        if options.showsBubble {
+            let edg = _inset(with: options.style, for: .bubble)
+            
+            let box = CGRect(x: x1, y: y1, width: x2 - x1, height: y2 - y1)
+            let rect = box.inset(by: edg)
+            
+            bubbleRect = rect
+            bubbleBoxRect = box
+            
+            x1 = rect.minX
+            x2 = rect.maxX
+            y1 = rect.minY
+            y2 = rect.maxY
+        }
+
+        if true {
+            let edg0 = _inset(with: options.style, for: .content)
+            let edg1 = message.content.layoutMargins
+            //
+            let edg = UIEdgeInsets(top: edg0.top + edg1.top, left: edg0.left + edg1.left, bottom: edg0.bottom + edg1.bottom, right: edg0.right + edg1.right)
+            
+            var box = CGRect(x: x1, y: y1, width: x2 - x1, height: y2 - y1)
+            var rect = box.inset(by: edg)
+            
+            // calc content size
+            let size = message.content.sizeThatFits(rect.size)
+            
+            // restore offset
+            box.size.width = edg.left + size.width + edg.right
+            box.size.height = edg.top + size.height + edg.bottom
+            rect.size.width = size.width
+            rect.size.height = size.height
+            
+            contentRect = rect
+            contentBoxRect = box
+            
+            x1 = box.maxX
+            y1 = box.maxY
+        }
+
+        if options.showsBubble {
+            let edg = _inset(with: options.style, for: .bubble)
+            
+            bubbleRect.size.width = contentBoxRect.width
+            bubbleRect.size.height = contentBoxRect.height
+            
+            bubbleBoxRect.size.width = edg.left + contentBoxRect.width + edg.right
+            bubbleBoxRect.size.height = edg.top + contentBoxRect.height + edg.bottom
+        }
+        
+        if options.showsTips {
+            let edg = _inset(with: options.style, for: .tips)
+            let size = _size(with: options.style, for: .tips)
+            
+            let box = CGRect(x: x1 + 3, y: y1 - size.height - edg0.bottom, width: edg.left + size.width + edg.right, height: edg.top + size.height + edg.bottom)
+            let rect = box.inset(by: edg)
+            
+            tipsRect = rect
+            tipsBoxRect = box
+            
+            x1 = box.maxX
+        }
+        
+        // adjust
+        r1.size.width = x1 - r1.minX
+        r1.size.height = y1 - r1.minY
+        r0.size.width = x1
+        r0.size.height = y1 + edg0.bottom
+        
+        allRect = r1
+        allBoxRect = r0
+        
+        // algin
+        switch options.alignment {
+        case .right:
+            // to right
+            allRect.origin.x = size.width - allRect.maxX
+            allBoxRect.origin.x = size.width - allBoxRect.maxX
+            
+            cardRect.origin.x = size.width - cardRect.maxX
+            cardBoxRect.origin.x = size.width - cardBoxRect.maxX
+            
+            avatarRect.origin.x = size.width - avatarRect.maxX
+            avatarBoxRect.origin.x = size.width - avatarBoxRect.maxX
+            
+            bubbleRect.origin.x = size.width - bubbleRect.maxX
+            bubbleBoxRect.origin.x = size.width - bubbleBoxRect.maxX
+            
+            contentRect.origin.x = size.width - contentRect.maxX
+            contentBoxRect.origin.x = size.width - contentBoxRect.maxX
+            
+            tipsRect.origin.x = size.width - tipsRect.maxX
+            tipsBoxRect.origin.x = size.width - tipsBoxRect.maxX
+            
+        case .center:
+            allRect.origin.x = (size.width - allRect.width) / 2
+            allBoxRect.origin.x = (size.width - allBoxRect.width) / 2
+            
+            bubbleRect.origin.x = (size.width - bubbleRect.width) / 2
+            bubbleBoxRect.origin.x = (size.width - bubbleBoxRect.width) / 2
+            
+            contentRect.origin.x = (size.width - contentRect.width) / 2
+            contentBoxRect.origin.x = (size.width - contentBoxRect.width) / 2
+            
+        case .left:
+            break
+        }
+        // save
+        let rects: [JCChatViewLayoutItem: CGRect] = [
+            .all: allRect,
+            .card: cardRect,
+            .avatar: avatarRect,
+            .bubble: bubbleRect,
+            .content: contentRect,
+            .tips: tipsRect
+        ]
+        let boxRects: [JCChatViewLayoutItem: CGRect] = [
+            .all: allBoxRect,
+            .card: cardBoxRect,
+            .avatar: avatarBoxRect,
+            .bubble: bubbleBoxRect,
+            .content: contentBoxRect,
+            .tips: tipsBoxRect
+        ]
+        let info = JCChatViewLayoutAttributesInfo(message: message, size: size, rects: rects, boxRects: boxRects)
+        _allLayoutAttributesInfo[message.identifier] = info
+        return info
+    }
+    
+    private func _size(with style: JCMessageStyle, for item: JCChatViewLayoutItem) -> CGSize {
+        let key = "\(style.rawValue)-\(item.rawValue)"
+        if let size = _cachedAllLayoutSize[key] {
+            return size // hit cache
+        }
+        var size: CGSize?
+        if let collectionView = collectionView, let delegate = collectionView.delegate as? JCChatViewLayoutDelegate {
+            switch item {
+            case .all: size = .zero
+            case .card: size = delegate.collectionView?(collectionView, layout: self, sizeForItemCardOf: style)
+            case .avatar: size = delegate.collectionView?(collectionView, layout: self, sizeForItemAvatarOf: style)
+            case .bubble: size = .zero
+            case .content: size = .zero
+            case .tips: size = delegate.collectionView?(collectionView, layout: self, sizeForItemTipsOf: style)
+            }
+        }
+        _cachedAllLayoutSize[key] = size ?? .zero
+        return size ?? .zero
+    }
+    private func _inset(with style: JCMessageStyle, for item: JCChatViewLayoutItem) -> UIEdgeInsets {
+        let key = "\(style.rawValue)-\(item.rawValue)"
+        if let edg = _cachedAllLayoutInset[key] {
+            return edg // hit cache
+        }
+        var edg: UIEdgeInsets?
+        if let collectionView = collectionView, let delegate = collectionView.delegate as? JCChatViewLayoutDelegate {
+            switch item {
+            case .all: edg = delegate.collectionView?(collectionView, layout: self, insetForItemOf: style)
+            case .card: edg = delegate.collectionView?(collectionView, layout: self, insetForItemCardOf: style)
+            case .tips: edg = delegate.collectionView?(collectionView, layout: self, insetForItemTipsOf: style)
+            case .avatar: edg = delegate.collectionView?(collectionView, layout: self, insetForItemAvatarOf: style)
+            case .bubble: edg = delegate.collectionView?(collectionView, layout: self, insetForItemBubbleOf: style)
+            case .content: edg = delegate.collectionView?(collectionView, layout: self, insetForItemContentOf: style)
+            }
+        }
+        _cachedAllLayoutInset[key] = edg ?? .zero
+        return edg ?? .zero
+    }
+    private func _message(at indexPath: IndexPath) -> JCMessageType? {
+        guard let collectionView = collectionView, let delegate = collectionView.delegate as? JCChatViewLayoutDelegate else {
+            return nil
+        }
+        return delegate.collectionView(collectionView, layout: self, itemAt: indexPath)
+    }
+    
+    private func _commonInit() {
+        minimumLineSpacing = 0
+        minimumInteritemSpacing = 0
+    }
+    
+    private lazy var _cachedAllLayoutSize: [String: CGSize] = [:]
+    private lazy var _cachedAllLayoutInset: [String: UIEdgeInsets] = [:]
+    
+    private lazy var _allLayoutAttributesInfo: [UUID: JCChatViewLayoutAttributesInfo] = [:]
+}
+
+@objc public protocol JCChatViewLayoutDelegate: UICollectionViewDelegateFlowLayout {
+    
+    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, itemAt indexPath: IndexPath) -> JCMessageType
+    
+    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemCardOf style: JCMessageStyle) -> CGSize
+    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemTipsOf style: JCMessageStyle) -> CGSize
+    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAvatarOf style: JCMessageStyle) -> CGSize
+    
+    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemOf style: JCMessageStyle) -> UIEdgeInsets
+    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemCardOf style: JCMessageStyle) -> UIEdgeInsets
+    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemTipsOf style: JCMessageStyle) -> UIEdgeInsets
+    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemAvatarOf style: JCMessageStyle) -> UIEdgeInsets
+    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemBubbleOf style: JCMessageStyle) -> UIEdgeInsets
+    @objc optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForItemContentOf style: JCMessageStyle) -> UIEdgeInsets
+}

+ 40 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewLayoutAttributes.swift

@@ -0,0 +1,40 @@
+//
+//  JCChatViewLayoutAttributes.swift
+//  JChat
+//
+//  Created by deng on 2017/3/1.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+@objc public enum JCChatViewLayoutItem: Int {
+    case all
+    case card
+    case avatar
+    case bubble
+    case content
+    case tips
+}
+
+@objc open class JCChatViewLayoutAttributes: UICollectionViewLayoutAttributes {
+    
+    public override init() {
+        super.init()
+    }
+    
+    open override func copy(with zone: NSZone? = nil) -> Any {
+        let new = super.copy(with: zone)
+        if let new = new as? JCChatViewLayoutAttributes {
+            new.info = info
+        }
+        return new
+    }
+    
+    open var message: JCMessageType? {
+        return info?.message
+    }
+    
+    open var info: JCChatViewLayoutAttributesInfo?
+}
+

+ 38 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewLayoutAttributesInfo.swift

@@ -0,0 +1,38 @@
+//
+//  JCChatViewLayoutAttributesInfo.swift
+//  JChat
+//
+//  Created by deng on 10/04/2017.
+//  Copyright © 2017 HXHG. All rights reserved.
+//
+
+import UIKit
+
+@objc open class JCChatViewLayoutAttributesInfo: NSObject {
+    
+    public init(message: JCMessageType, size: CGSize, rects: [JCChatViewLayoutItem: CGRect], boxRects: [JCChatViewLayoutItem: CGRect]) {
+        _message = message
+        _cacheSize = size
+        _allLayoutedRects = rects
+        _allLayoutedBoxRects = boxRects
+        super.init()
+    }
+    
+    open var message: JCMessageType {
+        return _message
+    }
+    
+    open func layoutedRect(with item: JCChatViewLayoutItem) -> CGRect {
+        return _allLayoutedRects[item] ?? .zero
+    }
+    open func layoutedBoxRect(with item: JCChatViewLayoutItem) -> CGRect {
+        return _allLayoutedBoxRects[item] ?? .zero
+    }
+    
+    private var _message: JCMessageType
+    private var _cacheSize: CGSize
+    
+    private var _allLayoutedRects: [JCChatViewLayoutItem: CGRect]
+    private var _allLayoutedBoxRects: [JCChatViewLayoutItem: CGRect]
+    
+}

+ 380 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCChatViewUpdate.swift

@@ -0,0 +1,380 @@
+//
+//  JCChatViewUpdate.swift
+//  JChat
+//
+//  Created by deng on 10/04/2017.
+//  Copyright © 2017 HXHG. All rights reserved.
+//
+
+import UIKit
+
+internal enum JCChatViewUpdateChangeItem {
+    case insert(JCMessageType, at: Int)
+    case update(JCMessageType, at: Int)
+    case remove(at: Int)
+    case move(at: Int,  to: Int)
+    
+    var at: Int {
+        switch self {
+        case .insert(_, let at): return at
+        case .update(_, let at): return at
+        case .remove(let at): return at
+        case .move(let at, _): return at
+        }
+    }
+}
+
+internal enum JCChatViewUpdateChange: CustomStringConvertible {
+    
+    case move(from: Int, to: Int)
+    case update(from: Int, to: Int)
+    case insert(from: Int, to: Int)
+    case remove(from: Int, to: Int)
+    
+    var from: Int {
+        switch self {
+        case .move(let from, _): return from
+        case .insert(let from, _): return from
+        case .update(let from, _): return from
+        case .remove(let from, _): return from
+        }
+    }
+    var to: Int {
+        switch self {
+        case .move(_, let to): return to
+        case .insert(_, let to): return to
+        case .update(_, let to): return to
+        case .remove(_, let to): return to
+        }
+    }
+    
+    var isMove: Bool {
+        switch self {
+        case .move: return true
+        default: return false
+        }
+    }
+    var isUpdate: Bool {
+        switch self {
+        case .update: return true
+        default: return false
+        }
+    }
+    var isRemove: Bool {
+        switch self {
+        case .remove: return true
+        default: return false
+        }
+    }
+    var isInsert: Bool {
+        switch self {
+        case .insert: return true
+        default: return false
+        }
+    }
+    
+    
+    var description: String {
+        let from = self.from >= 0 ? "\(self.from)" : "N"
+        let to = self.to >= 0 ? "\(self.to)" : "N"
+        
+        switch self {
+        case .move: return "M\(from)/\(to)"
+        case .insert: return "A\(from)/\(to)"
+        case .update: return "R\(from)/\(to)"
+        case .remove: return "D\(from)/\(to)"
+        }
+    }
+    
+    func offset(_ offset: Int) -> JCChatViewUpdateChange {
+        let from = self.from + offset + max(min(self.from, 0), -1) * offset
+        let to = self.to + offset + max(min(self.to, 0), -1) * offset
+        // convert
+        switch self {
+        case .move: return .move(from: from, to: to)
+        case .insert: return .insert(from: from, to: to)
+        case .update: return .update(from: from, to: to)
+        case .remove: return .remove(from: from, to: to)
+        }
+    }
+}
+
+internal class JCChatViewUpdate: NSObject {
+    
+    
+    internal init(newData: JCChatViewData, oldData: JCChatViewData, updateItems: Array<JCChatViewUpdateChangeItem>) {
+        self.newData = newData
+        self.oldData = oldData
+        self.updateItems = updateItems
+        super.init()
+        self.updateChanges = _computeItemUpdates(newData, oldData, updateItems)
+    }
+    
+    // MARK: compute
+    internal func _computeItemUpdates(_ newData: JCChatViewData, _ oldData: JCChatViewData, _ updateItems: Array<JCChatViewUpdateChangeItem>) -> Array<JCChatViewUpdateChange> {
+        guard !updateItems.isEmpty else {
+            return []
+        }
+        var allInserts: Array<(Int, JCMessageType)> = []
+        var allUpdates: Array<(Int, JCMessageType)> = []
+        var allRemoves: Array<(Int)> = []
+        var allMoves: Array<(Int, Int)> = []
+        
+        // get max & min
+        let (first, last) = updateItems.reduce((.max, .min)) { result, item -> (Int, Int) in
+            
+            switch item {
+            case .move(let from, let to):
+                // ignore for source equ dest
+                guard abs(from - to) >= 1 else {
+                    return result
+                }
+                // move message
+                allMoves.append((from, to))
+                // splite to insert & remove
+                if let message = _element(at: from) {
+                    allRemoves.append((from))
+                    allInserts.append((to + 1, message))
+                }
+                // from + 1: the selected row will change
+                return (min(min(from, to + 1), result.0), max(max(from + 1, to + 1), result.1))
+                
+            case .remove(let index):
+                // remove message
+                allRemoves.append((index))
+                return (min(index, result.0), max(index + 1, result.1))
+                
+            case .update(let message, let index):
+                // update message
+                allUpdates.append((index, message))
+                return (min(index, result.0), max(index + 1, result.1))
+                
+            case .insert(let message, let index):
+                // insert message
+                allInserts.append((index, message))
+                return (min(index, result.0), max(index, result.1))
+            }
+        }
+        // is empty
+        guard first != .max && last != .min else {
+            return []
+        }
+
+        // sort
+//        allInserts.sort { $0.0 < $1.0 }
+//        allUpdates.sort { $0.0 < $1.0 }
+//        allRemoves.sort { $0 < $1 }
+//        allMoves.sort { $0.0 < $1.0 }
+
+        let count = oldData.count
+        let begin = first - 1 // prev
+        let end = last + 1 // next
+        
+        var ii = allInserts.startIndex
+        var iu = allUpdates.startIndex
+        var ir = allRemoves.startIndex
+//        var im = allMoves.startIndex
+        
+        // priority: insert > remove > update > move
+        
+        var items: Array<JCMessageType> = []
+        
+        // processing
+        (first ... last).forEach { index in
+            // do you need to insert the operation?
+            while ii < allInserts.endIndex && allInserts[ii].0 == index {
+                items.append(allInserts[ii].1)
+                ii += 1
+            }
+            // do you need to do this?
+            guard index < last && index < count else {
+                return
+            }
+            // do you need to remove the operation?
+            while ir < allRemoves.endIndex && allRemoves[ir] == index {
+                // adjust previous tl-message & next tl-message, if needed
+                if let content = _element(at: index - 1)?.content as? JCMessageTimeLineContent {
+                    content.after = nil
+                }
+                if let content = _element(at: index + 1)?.content as? JCMessageTimeLineContent {
+                    content.before = nil
+                }
+                // move to next operator(prevent repeat operation)
+                while ir < allRemoves.endIndex && allRemoves[ir] == index {
+                    ir += 1
+                }
+                // can't update or copy
+                return
+            }
+            // do you need to update the operation?
+            while iu < allUpdates.endIndex && allUpdates[iu].0 == index {
+                let message = allUpdates[iu].1
+                // updating
+                items.append(message)
+                // adjust previous tl-message & next tl-message, if needed
+                if let content = _element(at: index - 1)?.content as? JCMessageTimeLineContent {
+                    content.after = message
+                }
+                if let content = _element(at: index + 1)?.content as? JCMessageTimeLineContent {
+                    content.before = message
+                }
+                // move to next operator(prevent repeat operation)
+                while iu < allUpdates.endIndex && allUpdates[iu].0 == index {
+                    iu += 1
+                }
+                // can't copy
+                return
+            }
+            // copy
+            items.append(oldData[index])
+        }
+        // convert messages and replace specify message
+        let newItems = items as [JCMessageType]
+        let convertedItems = _convert(messages: newItems, first: _element(at: begin), last: _element(at: end - 1))
+        let selectedRange = max(begin, 0) ..< min(end, count)
+        let selectedItems = oldData.subarray(with: selectedRange)
+        
+        // compute index paths
+        let start = selectedRange.lowerBound
+        // lcs
+        let diff = _diff(selectedItems, convertedItems).map { $0.offset(start) }
+        // ::
+        // replace
+        newData.elements = oldData.elements
+        newData.replaceSubrange(selectedRange, with: convertedItems)
+        
+        return diff
+    }
+    
+    // MARK: convert message
+    
+    private func _convert(messages elements: [JCMessageType], first: JCMessageType?, last: JCMessageType?) -> [JCMessageType] {
+        // merge
+        let elements = [first].flatMap({ $0 }) + elements + [last].flatMap({ $0 })
+        // processing
+        return (0 ..< elements.count).reduce(NSMutableArray(capacity: elements.count * 2)) { result, index in
+            let current = elements[index]
+            result.add(current)
+            // continue
+            return result
+        } as! [JCMessageType]
+    }
+
+    internal func _element(at index: Int) -> JCMessageType? {
+        guard index >= 0 && index < oldData.count else {
+            return nil
+        }
+        return oldData[index]
+    }
+    
+
+    // MARK: compare
+    private func _equal<T: JCMessageType>(_ lhs: T, _ rhs: T) -> Bool {
+        return lhs.identifier == rhs.identifier && lhs.options.state == rhs.options.state
+    }
+    
+    private func _diff<T: JCMessageType>(_ src: Array<T>, _ dest: Array<T>) -> Array<JCChatViewUpdateChange> {
+        
+        let len1 = src.count
+        let len2 = dest.count
+        
+        var c = [[Int]](repeating: [Int](repeating: 0, count: len2 + 1), count: len1 + 1)
+        
+        // lcs + 动态规划
+        for i in 1 ..< len1 + 1 {
+            for j in 1 ..< len2 + 1 {
+                if _equal(src[i - 1], (dest[j - 1])) {
+                    c[i][j] = c[i - 1][j - 1] + 1
+                } else {
+                    c[i][j] = max(c[i - 1][j], c[i][j - 1])
+                }
+            }
+        }
+        
+        var i = len1
+        var j = len2
+        
+        var rms: Array<(from: Int, to: Int)> = []
+        var adds: Array<(from: Int, to: Int)> = []
+        
+        // create the optimal path
+        repeat {
+            guard i != 0 else {
+                // the remaining is add
+                while j > 0 {
+                    adds.append((from: i - 1, to: j - 1))
+                    j -= 1
+                }
+                break
+            }
+            guard j != 0 else {
+                // the remaining is remove
+                while i > 0 {
+                    rms.append((from: i - 1, to: j - 1))
+                    i -= 1
+                }
+                break
+            }
+            guard !_equal(src[i - 1], (dest[j - 1])) else {
+                // no change, ignore
+                i -= 1
+                j -= 1
+                continue
+            }
+            // check the weight
+            if c[i - 1][j] > c[i][j - 1] {
+                // is remove
+                rms.append((from: i - 1, to: j - 1))
+                i -= 1
+            } else {
+                // is add
+                adds.append((from: i - 1, to: j - 1))
+                j -= 1
+            }
+        } while i > 0 || j > 0
+        
+        var results: Array<JCChatViewUpdateChange> = []
+        results.reserveCapacity(rms.count + adds.count)
+        
+        // move(f,t): f = remove(f), t = insert(t), new move(f,t): f = remove(f), t = insert(f)
+        // update(f,t): f = remove(f), t = insert(t), new update(f,t): f = remove(f), t = insert(f)
+        
+        // automatic merge delete and update items
+        results.append(contentsOf: rms.map({ item in
+            let from = item.from
+            let delElement = src[from]
+            // can't merge to move item?
+            if let addIndex = adds.index(where: { _equal(dest[$0.to], delElement) }) {
+                let addItem = adds.remove(at: addIndex)
+                return .move(from: from, to: addItem.to)
+            }
+            // can't merge to update item?
+            if let addIndex = adds.index(where: { $0.to == from }) {
+                let addItem = adds[addIndex]
+                let addElement = dest[addItem.to]
+                // the same type is allowed to merge
+                if type(of: delElement.content) == type(of: addElement.content) {
+                    adds.remove(at: addIndex)
+                    return .update(from: from, to: addItem.to)
+                }
+            }
+            return .remove(from: item.from, to: -1)
+        }))
+        // automatic merge insert items
+        results.append(contentsOf: adds.map({ item in
+            return .insert(from: -1, to: item.to)
+        }))
+        
+        // sort
+        return results.sorted { $0.from < $1.from }
+    }
+    
+    // MARK: property
+    internal let newData: JCChatViewData
+    internal let oldData: JCChatViewData
+    
+    internal let updateItems: Array<JCChatViewUpdateChangeItem>
+    internal var updateChanges: Array<JCChatViewUpdateChange>?
+    
+    internal static var minimuxTimeInterval: TimeInterval = 60
+}

+ 232 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCConversationCell.swift

@@ -0,0 +1,232 @@
+//
+//  JCConversationCell.swift
+//  JChat
+//
+//  Created by deng on 2017/3/22.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+import JMessage
+import CocoaLumberjack
+
+class JCConversationCell: JCTableViewCell {
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        _init()
+    }
+    required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _init()
+    }
+    
+    override func awakeFromNib() {
+        super.awakeFromNib()
+        _init()
+    }
+
+    private lazy var avatorView: UIImageView = {
+        let avatorView = UIImageView()
+        avatorView.contentMode = .scaleToFill
+        return avatorView
+    }()
+    private lazy var statueView: UIImageView = UIImageView()
+    private lazy var titleLabel: UILabel = {
+        let titleLabel = UILabel()
+        titleLabel.font = UIFont.systemFont(ofSize: 16)
+        return titleLabel
+    }()
+    private lazy var msgLabel: UILabel = {
+        let msgLabel = UILabel()
+        msgLabel.textColor = UIColor(netHex: 0x808080)
+        msgLabel.font = UIFont.systemFont(ofSize: 14)
+        return msgLabel
+    }()
+    private lazy var dateLabel: UILabel = {
+        let dateLabel = UILabel()
+        dateLabel.textAlignment = .right
+        dateLabel.font = UIFont.systemFont(ofSize: 12)
+        dateLabel.textColor = UIColor(netHex: 0xB3B3B3)
+        return dateLabel
+    }()
+    private lazy var redPoin: UILabel = {
+        let redPoin = UILabel(frame: CGRect(x: 65 - 17, y: 4.5, width: 20, height: 20))
+        redPoin.textAlignment = .center
+        redPoin.font = UIFont.systemFont(ofSize: 11)
+        redPoin.textColor = .white
+        redPoin.layer.backgroundColor = UIColor(netHex: 0xEB424C).cgColor
+        redPoin.textAlignment = .center
+        return redPoin
+    }()
+    
+    //MARK: - public func
+    open func bindConversation(_ conversation: JMSGConversation) {
+        statueView.isHidden = true
+        let isGroup = conversation.ex.isGroup
+        if conversation.unreadCount != nil && (conversation.unreadCount?.intValue)! > 0 {
+            redPoin.isHidden = false
+            var text = ""
+            if (conversation.unreadCount?.intValue)! > 99 {
+                text = "99+"
+                redPoin.layer.cornerRadius = 9.0
+                redPoin.layer.masksToBounds = true
+                redPoin.frame = CGRect(x: 65 - 28, y: 4.5, width: 33, height: 18)
+            } else {
+                redPoin.layer.cornerRadius = 10.0
+                redPoin.layer.masksToBounds = true
+                redPoin.frame = CGRect(x: 65 - 15, y: 4.5, width: 20, height: 20)
+                text = "\(conversation.unreadCount!)"
+            }
+            redPoin.text = text
+            
+            var isNoDisturb = false
+            if isGroup {
+                if let group = conversation.target as? JMSGGroup {
+                    isNoDisturb = group.isNoDisturb
+                }
+            } else {
+                if let user = conversation.target as? JMSGUser {
+                    isNoDisturb = user.isNoDisturb
+                }
+            }
+            
+            if isNoDisturb {
+                redPoin.layer.cornerRadius = 4.0
+                redPoin.layer.masksToBounds = true
+                redPoin.text = ""
+                redPoin.frame = CGRect(x: 65 - 5, y: 4.5, width: 8, height: 8)
+            }
+        } else {
+            redPoin.isHidden = true
+        }
+        
+        if let latestMessage = conversation.latestMessage {
+            let time = latestMessage.timestamp.intValue / 1000
+            let date = Date(timeIntervalSince1970: TimeInterval(time))
+            dateLabel.text = date.conversationDate()
+        } else {
+            dateLabel.text = ""
+        }
+        
+        msgLabel.text = conversation.latestMessageContentText()
+        if isGroup {
+            if let latestMessage = conversation.latestMessage {
+                let fromUser = latestMessage.fromUser
+                if !fromUser.isEqual(to: JMSGUser.myInfo()) &&
+                    latestMessage.contentType != .eventNotification &&
+                    latestMessage.contentType != .prompt {
+                    msgLabel.text = "\(fromUser.displayName()):\(msgLabel.text!)"
+                }
+                if conversation.unreadCount != nil &&
+                    conversation.unreadCount!.intValue > 0 &&
+                    latestMessage.contentType != .prompt {
+                    if latestMessage.isAtAll() {
+                        msgLabel.attributedText = getAttributString(attributString: "[@所有人]", string: msgLabel.text!)
+                    } else if latestMessage.isAtMe() {
+                        msgLabel.attributedText = getAttributString(attributString: "[有人@我]", string: msgLabel.text!)
+                    }
+                }
+            }
+        }
+        
+        if let draft = JCDraft.getDraft(conversation) {
+            if !draft.isEmpty {
+                msgLabel.attributedText = getAttributString(attributString: "[草稿]", string: draft)
+            }
+        }
+
+        if !isGroup {
+            let user = conversation.target as? JMSGUser
+            titleLabel.text = user?.displayName() ?? ""
+            // 处理头像
+            DDLogDebug("更新头像,发送者头像:\(user?.username ?? "")")
+            let urlstr = AppDelegate.o2Collect.generateURLWithAppContextKey(ContactContext.contactsContextKeyV2, query: ContactContext.personIconByNameQueryV2, parameter: ["##name##":user?.username as AnyObject])
+            let url = URL(string: urlstr!)
+            let bound = self.avatorView.bounds
+            if bound.width <= 0 || bound.height <= 0 {
+                self.avatorView.bounds = CGRect(x: 15, y: 7.5, width: 50, height: 50)
+            }
+            self.avatorView.hnk_setImageFromURL(url!)
+
+//            user?.thumbAvatarData { (data, username, error) in
+//                guard let imageData = data else {
+//                    self.avatorView.image = self.userDefaultIcon
+//                    return
+//                }
+//                let image = UIImage(data: imageData)
+//                self.avatorView.image = image
+//            }
+        } else {
+            if let group = conversation.target as? JMSGGroup {
+                titleLabel.text = group.displayName()
+                if group.isShieldMessage {
+                    statueView.isHidden = false
+                }
+                group.thumbAvatarData({ (data, _, error) in
+                    if let data = data {
+                        self.avatorView.image = UIImage(data: data)
+                    } else {
+                        self.avatorView.image = self.groupDefaultIcon
+                    }
+                })
+            }
+        }
+
+        if conversation.ex.isSticky {
+            backgroundColor = UIColor(netHex: 0xF5F6F8)
+        } else {
+            backgroundColor = .white
+        }
+    }
+    
+    func getAttributString(attributString: String, string: String) -> NSMutableAttributedString {
+        let attr = NSMutableAttributedString(string: "")
+        var attrSearchString: NSAttributedString!
+        attrSearchString = NSAttributedString(string: attributString, attributes: [ NSAttributedString.Key.foregroundColor : UIColor(netHex: 0xEB424C), NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 14.0)])
+        attr.append(attrSearchString)
+        attr.append(NSAttributedString(string: string))
+        return attr
+    }
+    
+    private lazy var groupDefaultIcon = UIImage.loadImage("com_icon_group_50")
+    private lazy var userDefaultIcon = UIImage.loadImage("com_icon_user_50")
+    
+    //MARK: - private func
+    private func _init() {
+        avatorView.image = userDefaultIcon
+        statueView.image = UIImage.loadImage("com_icon_shield")
+        
+        contentView.addSubview(avatorView)
+        contentView.addSubview(statueView)
+        contentView.addSubview(titleLabel)
+        contentView.addSubview(msgLabel)
+        contentView.addSubview(dateLabel)
+        contentView.addSubview(redPoin)
+        
+        addConstraint(_JCLayoutConstraintMake(avatorView, .left, .equal, contentView, .left, 15))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .top, .equal, contentView, .top, 7.5))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .width, .equal, nil, .notAnAttribute, 50))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .height, .equal, nil, .notAnAttribute, 50))
+        
+        addConstraint(_JCLayoutConstraintMake(titleLabel, .left, .equal, avatorView, .right, 10.5))
+        addConstraint(_JCLayoutConstraintMake(titleLabel, .top, .equal, contentView, .top, 10.5))
+        addConstraint(_JCLayoutConstraintMake(titleLabel, .right, .equal, dateLabel, .left, -3))
+        addConstraint(_JCLayoutConstraintMake(titleLabel, .height, .equal, nil, .notAnAttribute, 22.5))
+        
+        addConstraint(_JCLayoutConstraintMake(msgLabel, .left, .equal, titleLabel, .left))
+        addConstraint(_JCLayoutConstraintMake(msgLabel, .top, .equal, titleLabel, .bottom, 1.5))
+        addConstraint(_JCLayoutConstraintMake(msgLabel, .right, .equal, statueView, .left, -5))
+        addConstraint(_JCLayoutConstraintMake(msgLabel, .height, .equal, nil, .notAnAttribute, 20))
+        
+        addConstraint(_JCLayoutConstraintMake(dateLabel, .top, .equal, contentView, .top, 16))
+        addConstraint(_JCLayoutConstraintMake(dateLabel, .right, .equal, contentView, .right, -15))
+        addConstraint(_JCLayoutConstraintMake(dateLabel, .height, .equal, nil, .notAnAttribute, 16.5))
+        addConstraint(_JCLayoutConstraintMake(dateLabel, .width, .equal, nil, .notAnAttribute, 100))
+        
+        addConstraint(_JCLayoutConstraintMake(statueView, .top, .equal, dateLabel, .bottom, 7))
+        addConstraint(_JCLayoutConstraintMake(statueView, .right, .equal, contentView, .right, -16))
+        addConstraint(_JCLayoutConstraintMake(statueView, .height, .equal, nil, .notAnAttribute, 12))
+        addConstraint(_JCLayoutConstraintMake(statueView, .width, .equal, nil, .notAnAttribute, 12))
+    }
+}

+ 78 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCGroupMemberCell.swift

@@ -0,0 +1,78 @@
+//
+//  JCGroupMemberCell.swift
+//  JChat
+//
+//  Created by deng on 2017/5/10.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+import JMessage
+
+class JCGroupMemberCell: UICollectionViewCell {
+    
+    var avator: UIImage? {
+        get {
+            return avatorView.image
+        }
+        set {
+            nickname.text = ""
+            avatorView.image = newValue
+        }
+    }
+    
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _init()
+    }
+    
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _init()
+    }
+    
+    private var avatorView: UIImageView = UIImageView()
+    private var nickname: UILabel = UILabel()
+    private lazy var userDefaultIcon = UIImage.loadImage("com_icon_user_50")
+    
+    private func _init() {
+        
+        nickname.font = UIFont.systemFont(ofSize: 12)
+        nickname.textAlignment = .center
+        
+        addSubview(avatorView)
+        addSubview(nickname)
+        
+        addConstraint(_JCLayoutConstraintMake(avatorView, .centerY, .equal, contentView, .centerY, -10))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .width, .equal, nil, .notAnAttribute, 50))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .height, .equal, nil, .notAnAttribute, 50))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .centerX, .equal, contentView, .centerX))
+        
+        addConstraint(_JCLayoutConstraintMake(nickname, .centerX, .equal, contentView, .centerX))
+        addConstraint(_JCLayoutConstraintMake(nickname, .width, .equal, nil, .notAnAttribute, 50))
+        addConstraint(_JCLayoutConstraintMake(nickname, .height, .equal, nil, .notAnAttribute, 15))
+        addConstraint(_JCLayoutConstraintMake(nickname, .top, .equal, avatorView, .bottom, 5))
+        
+    }
+    
+    func bindDate(user: JMSGUser) {
+        nickname.text = user.displayName()
+        let urlstr = AppDelegate.o2Collect.generateURLWithAppContextKey(ContactContext.contactsContextKeyV2, query: ContactContext.personIconByNameQueryV2, parameter: ["##name##":user.username as AnyObject])
+        let url = URL(string: urlstr!)
+        let bound = self.avatorView.bounds
+        if bound.width <= 0 || bound.height <= 0 {
+            self.avatorView.bounds = CGRect(x: 0, y: 0, width: 50, height: 50)
+        }
+        self.avatorView.hnk_setImageFromURL(url!)
+        
+//        user.thumbAvatarData { (data, id, error) in
+//            if let data = data {
+//                let image = UIImage(data: data)
+//                self.avatorView.image = image
+//            } else {
+//                self.avatorView.image = self.userDefaultIcon
+//            }
+//        }
+    }
+    
+}

+ 215 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCGroupSettingCell.swift

@@ -0,0 +1,215 @@
+//
+//  JCGroupSettingCell.swift
+//  JChat
+//
+//  Created by deng on 2017/4/27.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+import JMessage
+
+@objc public protocol JCGroupSettingCellDelegate: NSObjectProtocol {
+    @objc optional func clickMoreButton(clickButton button: UIButton)
+    @objc optional func clickAddCell(cell: JCGroupSettingCell)
+    @objc optional func clickRemoveCell(cell: JCGroupSettingCell)
+    @objc optional func didSelectCell(cell: JCGroupSettingCell, indexPath: IndexPath)
+}
+
+public class JCGroupSettingCell: UITableViewCell {
+    
+    weak var delegate: JCGroupSettingCellDelegate?
+    
+    var group: JMSGGroup!
+    
+    convenience init(style: UITableViewCell.CellStyle, reuseIdentifier: String?, group: JMSGGroup) {
+        self.init(style: style, reuseIdentifier: reuseIdentifier)
+        self.group = group
+        _init()
+    }
+    
+    override public func awakeFromNib() {
+        super.awakeFromNib()
+        _init()
+    }
+
+    private lazy var moreButton: UIButton = UIButton()
+    fileprivate var count = 0
+    fileprivate var sectionCount = 0
+    fileprivate lazy var users: [JMSGUser] = []
+    fileprivate var isMyGroup = false
+    fileprivate var currentUserCount = 0
+
+    private lazy var flowLayout: UICollectionViewFlowLayout = {
+        let flowLayout = UICollectionViewFlowLayout()
+        flowLayout.scrollDirection = .vertical
+        flowLayout.minimumInteritemSpacing = 0
+        flowLayout.minimumLineSpacing = 0
+        return flowLayout
+    }()
+    private lazy var collectionView: UICollectionView = {
+        let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.flowLayout)
+        collectionView.delegate = self
+        collectionView.dataSource = self
+        collectionView.register(JCGroupMemberCell.self, forCellWithReuseIdentifier: "JCGroupMemberCell")
+        collectionView.isScrollEnabled = false
+        collectionView.backgroundColor = UIColor.clear
+        return collectionView
+    }()
+    
+    func bindData(_ group: JMSGGroup) {
+        self.group = group
+        _getData()
+        self.collectionView.reloadData()
+    }
+    
+    private func _init() {
+        
+        _getData()
+
+        addSubview(collectionView)
+
+        let showMore = isMyGroup ? count > 13 : count > 14
+        if showMore {
+            moreButton.addTarget(self, action: #selector(_clickMore), for: .touchUpInside)
+            moreButton.setTitleColor(UIColor(netHex: 0x999999), for: .normal)
+            moreButton.titleLabel?.font = UIFont.systemFont(ofSize: 15)
+            moreButton.setTitle("查看更多 >", for: .normal)
+            self.addSubview(moreButton)
+            
+            addConstraint(_JCLayoutConstraintMake(moreButton, .centerX, .equal, self, .centerX))
+            addConstraint(_JCLayoutConstraintMake(moreButton, .width, .equal, self, .width))
+            addConstraint(_JCLayoutConstraintMake(moreButton, .height, .equal, nil, .notAnAttribute, 26))
+            addConstraint(_JCLayoutConstraintMake(moreButton, .bottom, .equal, self, .bottom, -14))
+        }
+        
+        addConstraint(_JCLayoutConstraintMake(collectionView, .left, .equal, self, .left, 15))
+        addConstraint(_JCLayoutConstraintMake(collectionView, .right, .equal, self, .right, -15))
+        addConstraint(_JCLayoutConstraintMake(collectionView, .top, .equal, self, .top))
+        if isMyGroup {
+            if count > 8 {
+                addConstraint(_JCLayoutConstraintMake(collectionView, .height, .equal, nil, .notAnAttribute, 260))
+            } else if count > 3 {
+                addConstraint(_JCLayoutConstraintMake(collectionView, .height, .equal, nil, .notAnAttribute, 200))
+            } else {
+                addConstraint(_JCLayoutConstraintMake(collectionView, .height, .equal, nil, .notAnAttribute, 100))
+            }
+        } else {
+            if count > 9 {
+                addConstraint(_JCLayoutConstraintMake(collectionView, .height, .equal, nil, .notAnAttribute, 260))
+            } else if count > 4 {
+                addConstraint(_JCLayoutConstraintMake(collectionView, .height, .equal, nil, .notAnAttribute, 200))
+            } else {
+                addConstraint(_JCLayoutConstraintMake(collectionView, .height, .equal, nil, .notAnAttribute, 100))
+            }
+        }
+    }
+    
+    @objc func _clickMore() {
+        delegate?.clickMoreButton?(clickButton: moreButton)
+    }
+    
+    fileprivate func _getData() {
+        users = group.memberArray()
+        
+        currentUserCount = users.count
+        
+        let user = JMSGUser.myInfo()
+//        && group.ownerAppKey == user.appKey!  这里group.ownerAppKey == "" 目测sdk bug
+        if group.owner == user.username  {
+            isMyGroup = true
+        }
+        
+        count = users.count
+        
+        if isMyGroup {
+            if count > 13 {
+                currentUserCount = 13
+            }
+            if count > 8 {
+                sectionCount = 3
+            } else if count > 3 {
+                sectionCount = 2
+            } else {
+                sectionCount = 1
+            }
+        } else {
+            if count > 14 {
+                currentUserCount = 14
+            }
+            if count > 9 {
+                sectionCount = 3
+            } else if count > 4 {
+                sectionCount = 2
+            } else {
+                sectionCount = 1
+            }
+        }
+        
+    }
+
+}
+
+extension JCGroupSettingCell: UICollectionViewDelegate, UICollectionViewDataSource {
+    public func numberOfSections(in collectionView: UICollectionView) -> Int {
+        return sectionCount
+    }
+    
+    public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+        if isMyGroup {
+            if section == 0 {
+                return count >= 3 ? 5 : count + 2
+            }
+            if section == 1 {
+                return  count >= 8 ? 5 : (count - 3)
+            }
+            return count >= 13 ? 5 : count - 8
+        }
+        if section == 0 {
+            return count >= 4 ? 5 : count + 1
+        }
+        if section == 1 {
+            return  count >= 9 ? 5 : (count - 4)
+        }
+        return count >= 14 ? 5 : count - 9
+    }
+    
+    func collectionView(_ collectionView: UICollectionView,
+                        layout collectionViewLayout: UICollectionViewLayout,
+                        sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
+        return CGSize(width:Int(collectionView.frame.size.width / 5), height: Int(collectionView.frame.size.height / CGFloat(sectionCount)))
+    }
+
+    public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+        return collectionView.dequeueReusableCell(withReuseIdentifier: "JCGroupMemberCell", for: indexPath)
+    }
+    
+    public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
+        guard let cell = cell as? JCGroupMemberCell else {
+            return
+        }
+        let index = indexPath.section * 5 + indexPath.row
+        if index == currentUserCount {
+            cell.avator = UIImage.loadImage("com_icon_single_add")
+            return
+        }
+        if index == currentUserCount + 1 {
+            cell.avator = UIImage.loadImage("com_icon_remove")
+            return
+        }
+        cell.bindDate(user: users[index])
+    }
+    
+    public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
+        let index = indexPath.section * 5 + indexPath.row
+        if index == currentUserCount {
+            delegate?.clickAddCell?(cell: self)
+            return
+        }
+        if index == currentUserCount + 1 {
+            delegate?.clickRemoveCell?(cell: self)
+            return
+        }
+        delegate?.didSelectCell?(cell: self, indexPath: indexPath)
+    }
+}

+ 74 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCMessageAvatarView.swift

@@ -0,0 +1,74 @@
+//
+//  JCMessageAvatarView.swift
+//  JChat
+//
+//  Created by deng on 10/04/2017.
+//  Copyright © 2017 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageAvatarView: UIImageView, JCMessageContentViewType {
+    
+    weak var delegate: JCMessageDelegate?
+    
+    public override init(image: UIImage?) {
+        super.init(image: image)
+        _commonInit()
+    }
+    public override init(image: UIImage?, highlightedImage: UIImage?) {
+        super.init(image: image, highlightedImage: highlightedImage)
+        _commonInit()
+    }
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+    
+    open func apply(_ message: JCMessageType) {
+        self.message = message
+        if message.senderAvator != nil {
+            image = message.senderAvator
+            return
+        }
+        weak var weakSelf = self
+        message.sender?.thumbAvatarData({ (data, id, error) in
+            if let data = data {
+                weakSelf?.image = UIImage(data: data)
+            } else {
+                self.image = self.userDefaultIcon
+            }
+        })
+    }
+    
+    private var message: JCMessageType!
+    private lazy var userDefaultIcon = UIImage.loadImage("com_icon_user_36")
+    
+    private func _commonInit() {
+        image = userDefaultIcon
+        isUserInteractionEnabled = true
+        layer.masksToBounds = true
+
+        let tapGR = UITapGestureRecognizer(target: self, action: #selector(_tapHandler))
+        self.addGestureRecognizer(tapGR)
+
+        let longTapGesture = UILongPressGestureRecognizer(target: self, action: #selector(_longTap(_:)))
+        longTapGesture.minimumPressDuration = 0.4
+        addGestureRecognizer(longTapGesture)
+    }
+    
+    @objc func _tapHandler(sender:UITapGestureRecognizer) {
+        delegate?.tapAvatarView?(message: message)
+    }
+
+    @objc func _longTap(_ gestureRecognizer: UILongPressGestureRecognizer)  {
+        if gestureRecognizer.state == .began {
+            delegate?.longTapAvatarView?(message: message)
+        }
+    }
+
+}

+ 32 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCMessageCardView.swift

@@ -0,0 +1,32 @@
+//
+//  JCMessageCardView.swift
+//  JChat
+//
+//  Created by deng on 10/04/2017.
+//  Copyright © 2017 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageCardView: UILabel, JCMessageContentViewType {
+    
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+    
+    open func apply(_ message: JCMessageType) {
+        let isRight = message.options.alignment == .right
+        text = message.name
+        textAlignment = isRight ? .right : .left
+    }
+    
+    private func _commonInit() {
+        font = UIFont.systemFont(ofSize: 14)
+        textColor = UIColor(netHex: 0xB3B3B3)
+    }
+}

+ 114 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCMessageTipsView.swift

@@ -0,0 +1,114 @@
+//
+//  JCMessageTipsView.swift
+//  JChat
+//
+//  Created by deng on 2017/4/26.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageTipsView: UIView, JCMessageContentViewType {
+    
+    weak var delegate: JCMessageDelegate?
+    
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+    
+    open func apply(_ message: JCMessageType) {
+        self.message = message
+        switch message.options.state {
+        case .sending:
+            errorInfoView.isHidden = true
+            activityView.startAnimating()
+        case .sendSucceed:
+            errorInfoView.isHidden = true
+            activityView.stopAnimating()
+        case .sendError:
+            activityView.stopAnimating()
+            errorInfoView.isHidden = false
+        default:
+            activityView.stopAnimating()
+        }
+        if message.content is JCMessageImageContent {
+            activityView.stopAnimating()
+            activityView.isHidden = true
+        }
+
+        #if READ_VERSION
+        if activityView.isHidden && errorInfoView.isHidden && message.options.alignment == .right {
+            unreadCountTips.isHidden = false
+            if message.unreadCount > 0 {
+                unreadCountTips.isEnabled = true
+                unreadCountTips.setTitleColor(UIColor(netHex: 0x2DD0CF), for: .normal)
+                if message.targetType == .single {
+                    unreadCountTips.isEnabled = false
+                    unreadCountTips.setTitle("未读", for: .normal)
+                } else {
+                    unreadCountTips.setTitle("\(message.unreadCount)人未读", for: .normal)
+                }
+            } else {
+                unreadCountTips.isEnabled = false
+                unreadCountTips.setTitleColor(UIColor(netHex: 0x999999), for: .normal)
+                if message.targetType == .single {
+                    unreadCountTips.setTitle("已读", for: .normal)
+                } else {
+                    unreadCountTips.setTitle("全部已读", for: .normal)
+                }
+            }
+        } else {
+            unreadCountTips.isHidden = true
+        }
+        #endif
+    }
+    
+    private lazy var activityView: UIActivityIndicatorView = {
+        let activityView = UIActivityIndicatorView(frame: CGRect(x: 100 - 15, y: 5, width: 10, height: 10))
+        activityView.style = .gray
+        activityView.isUserInteractionEnabled = false
+        return activityView
+    }()
+    private lazy var errorInfoView: UIImageView = {
+        let image = UIImage.loadImage("com_icon_send_error")
+        let errorInfoView = UIImageView(frame: CGRect(x: 100 - 21, y: 0, width: 21, height: 21))
+        errorInfoView.isUserInteractionEnabled = true
+        errorInfoView.image = image
+        errorInfoView.isHidden = true
+        return errorInfoView
+    }()
+    private lazy var unreadCountTips: UIButton = {
+        let unreadCountTips = UIButton(frame: CGRect(x: 0, y: 0, width: 95, height: 21))
+        unreadCountTips.addTarget(self, action: #selector(_clickUnreadCount), for: .touchUpInside)
+        unreadCountTips.setTitle("未读", for: .normal)
+        unreadCountTips.titleLabel?.font = UIFont.systemFont(ofSize: 12)
+        unreadCountTips.setTitleColor(UIColor(netHex: 0x2DD0CF), for: .normal)
+        unreadCountTips.isHidden = true
+        unreadCountTips.contentHorizontalAlignment = .right
+        return unreadCountTips
+    }()
+    private var message: JCMessageType!
+    
+    private func _commonInit() {
+        addSubview(activityView)
+        let tapGR = UITapGestureRecognizer(target: self, action: #selector(_tapHandler))
+        errorInfoView.addGestureRecognizer(tapGR)
+        addSubview(errorInfoView)
+        #if READ_VERSION
+        addSubview(unreadCountTips)
+        #endif
+    }
+    
+    func _clickUnreadCount() {
+        delegate?.tapUnreadTips?(message: message)
+    }
+    
+    func _tapHandler(sender: UITapGestureRecognizer) {
+        delegate?.clickTips?(message: message)
+    }
+}

+ 56 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCNetworkTipsCell.swift

@@ -0,0 +1,56 @@
+//
+//  JCNetworkTipsCell.swift
+//  JChat
+//
+//  Created by deng on 2017/6/12.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class JCNetworkTipsCell: UITableViewCell {
+
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        _init()
+    }
+    required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _init()
+    }
+    
+    override func awakeFromNib() {
+        super.awakeFromNib()
+        _init()
+    }
+    
+    private lazy var statueView: UIImageView = {
+        let statueView = UIImageView()
+        statueView.image = UIImage.loadImage("com_icon_send_error")
+        return statueView
+    }()
+    private lazy var tipsLabel: UILabel = {
+        let tipsLabel = UILabel()
+        tipsLabel.text = "当前网络不可用,请检查您的网络设置"
+        tipsLabel.font = UIFont.systemFont(ofSize: 14)
+        return tipsLabel
+    }()
+    
+    //MARK: - private func
+    private func _init() {
+        backgroundColor = UIColor(netHex: 0xFFDFE0)
+
+        contentView.addSubview(statueView)
+        contentView.addSubview(tipsLabel)
+        
+        addConstraint(_JCLayoutConstraintMake(tipsLabel, .left, .equal, statueView, .right, 11.5))
+        addConstraint(_JCLayoutConstraintMake(tipsLabel, .centerY, .equal, contentView, .centerY))
+        addConstraint(_JCLayoutConstraintMake(tipsLabel, .right, .equal, contentView, .right))
+        addConstraint(_JCLayoutConstraintMake(tipsLabel, .height, .equal, contentView, .height))
+
+        addConstraint(_JCLayoutConstraintMake(statueView, .centerY, .equal, contentView, .centerY))
+        addConstraint(_JCLayoutConstraintMake(statueView, .left, .equal, contentView, .left, 15))
+        addConstraint(_JCLayoutConstraintMake(statueView, .height, .equal, nil, .notAnAttribute, 21))
+        addConstraint(_JCLayoutConstraintMake(statueView, .width, .equal, nil, .notAnAttribute, 21))
+    }
+}

+ 101 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCSelectMemberCell.swift

@@ -0,0 +1,101 @@
+//
+//  JCSelectMemberCell.swift
+//  JChat
+//
+//  Created by deng on 2017/5/11.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+import JMessage
+
+class JCSelectMemberCell: UITableViewCell {
+
+    var icon: UIImage? {
+        get {
+            return avatorView.image
+        }
+        set {
+            avatorView.image = newValue
+        }
+    }
+    
+    var selectIcon: UIImage? {
+        get {
+            return selectIconView.image
+        }
+        set {
+            selectIconView.image = newValue
+        }
+    }
+    
+    var title: String? {
+        get {
+            return usernameLabel.text
+        }
+        set {
+            usernameLabel.text = newValue
+        }
+    }
+    
+    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+        super.init(style: style, reuseIdentifier: reuseIdentifier)
+        _init()
+    }
+    required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _init()
+    }
+    
+    override func awakeFromNib() {
+        super.awakeFromNib()
+        _init()
+    }
+    
+    private lazy var avatorView: UIImageView = UIImageView()
+    private lazy var usernameLabel: UILabel = UILabel()
+    private lazy var selectIconView: UIImageView = UIImageView()
+    
+    public func bindDate(_ user : JMSGUser) {
+        title = user.displayName()
+        icon = UIImage.loadImage("com_icon_user_36")
+        user.thumbAvatarData({ (data, name, error) in
+            if data != nil {
+                let image = UIImage(data: data!)
+                self.icon = image
+            }
+        })
+    }
+    
+    //MARK: - private func
+    private func _init() {
+        
+        let image = UIImage.loadImage("com_icon_unselect")
+        
+        selectIconView.image = image
+        
+        usernameLabel.textColor = UIColor(netHex: 0x2c2c2c)
+        usernameLabel.font = UIFont.systemFont(ofSize: 14)
+        
+        contentView.addSubview(selectIconView)
+        contentView.addSubview(avatorView)
+        contentView.addSubview(usernameLabel)
+        
+        addConstraint(_JCLayoutConstraintMake(selectIconView, .left, .equal, contentView, .left, 15))
+        addConstraint(_JCLayoutConstraintMake(selectIconView, .centerY, .equal, contentView, .centerY))
+        addConstraint(_JCLayoutConstraintMake(selectIconView, .width, .equal, nil, .notAnAttribute, 20))
+        addConstraint(_JCLayoutConstraintMake(selectIconView, .height, .equal, nil, .notAnAttribute, 20))
+        
+        addConstraint(_JCLayoutConstraintMake(avatorView, .left, .equal, selectIconView, .right, 15))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .top, .equal, contentView, .top, 9.5))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .width, .equal, nil, .notAnAttribute, 36))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .height, .equal, nil, .notAnAttribute, 36))
+        
+        addConstraint(_JCLayoutConstraintMake(usernameLabel, .left, .equal, avatorView, .right, 11))
+        addConstraint(_JCLayoutConstraintMake(usernameLabel, .top, .equal, contentView, .top, 19.5))
+        addConstraint(_JCLayoutConstraintMake(usernameLabel, .right, .equal, contentView, .right, -15))
+        addConstraint(_JCLayoutConstraintMake(usernameLabel, .height, .equal, nil, .notAnAttribute, 16))
+        
+    }
+
+}

+ 90 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCSingleSettingCell.swift

@@ -0,0 +1,90 @@
+//
+//  JCSingleSettingCell.swift
+//  JChat
+//
+//  Created by deng on 2017/4/27.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+@objc public protocol JCSingleSettingCellDelegate: NSObjectProtocol {
+    @objc optional func singleSettingCell(clickAddButton button: UIButton)
+    @objc optional func singleSettingCell(clickAvatorButton button: UIButton)
+}
+
+class JCSingleSettingCell: UITableViewCell {
+
+    weak var delegate: JCSingleSettingCellDelegate?
+
+    required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _init()
+    }
+    
+    override func awakeFromNib() {
+        super.awakeFromNib()
+        _init()
+    }
+    
+    private lazy var avatorButton: UIButton = {
+        let avatorButton = UIButton()
+        avatorButton.setBackgroundImage(UIImage.loadImage("com_icon_user_50"), for: .normal)
+        avatorButton.addTarget(self, action: #selector(_clickAvator), for: .touchUpInside)
+        return avatorButton
+    }()
+    private lazy var addButton: UIButton = {
+        let addButton = UIButton()
+        addButton.setBackgroundImage(UIImage.loadImage("com_icon_single_add"), for: .normal)
+        addButton.setBackgroundImage(UIImage.loadImage("com_icon_single_add_per"), for: .highlighted)
+        addButton.addTarget(self, action: #selector(_clickAdd), for: .touchUpInside)
+        return addButton
+    }()
+    private lazy var nickname: UILabel = {
+        let nickname = UILabel()
+        nickname.font = UIFont.systemFont(ofSize: 12)
+        nickname.textAlignment = .center
+        nickname.textColor = UIColor(netHex: 0x2C2C2C)
+        return nickname
+    }()
+    
+    func bindData(_ user: JMSGUser) {
+        nickname.text = user.displayName()
+        user.thumbAvatarData { (data, id, error) in
+            if data != nil {
+                let image = UIImage(data: data!)
+                self.avatorButton.setBackgroundImage(image, for: .normal)
+            }
+        }
+    }
+    
+    private func _init() {
+        contentView.addSubview(avatorButton)
+        //contentView.addSubview(addButton)
+        contentView.addSubview(nickname)
+        
+        addConstraint(_JCLayoutConstraintMake(avatorButton, .left, .equal, contentView, .left, 20))
+        addConstraint(_JCLayoutConstraintMake(avatorButton, .width, .equal, nil, .notAnAttribute, 50))
+        addConstraint(_JCLayoutConstraintMake(avatorButton, .height, .equal, nil, .notAnAttribute, 50))
+        addConstraint(_JCLayoutConstraintMake(avatorButton, .top, .equal, contentView, .top, 16.5))
+        
+        addConstraint(_JCLayoutConstraintMake(nickname, .left, .equal, avatorButton, .left))
+        addConstraint(_JCLayoutConstraintMake(nickname, .width, .equal, avatorButton, .width))
+        addConstraint(_JCLayoutConstraintMake(nickname, .height, .equal, nil, .notAnAttribute, 16.5))
+        addConstraint(_JCLayoutConstraintMake(nickname, .top, .equal, avatorButton, .bottom, 3))
+        
+//        addConstraint(_JCLayoutConstraintMake(addButton, .left, .equal, avatorButton, .right, 20))
+//        addConstraint(_JCLayoutConstraintMake(addButton, .width, .equal, nil, .notAnAttribute, 50))
+//        addConstraint(_JCLayoutConstraintMake(addButton, .height, .equal, nil, .notAnAttribute, 50))
+//        addConstraint(_JCLayoutConstraintMake(addButton, .top, .equal, contentView, .top, 16.5))
+    }
+    
+    @objc func _clickAvator() {
+        delegate?.singleSettingCell?(clickAvatorButton: avatorButton)
+    }
+    
+    @objc func _clickAdd() {
+        delegate?.singleSettingCell?(clickAddButton: addButton)
+    }
+
+}

+ 58 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/JCUpdateMemberCell.swift

@@ -0,0 +1,58 @@
+//
+//  JCUpdateMemberCell.swift
+//  JChat
+//
+//  Created by deng on 2017/5/11.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+import JMessage
+
+class JCUpdateMemberCell: UICollectionViewCell {
+    var avator: UIImage? {
+        get {
+            return avatorView.image
+        }
+        set {
+            avatorView.image = newValue
+        }
+    }
+    
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _init()
+    }
+    
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _init()
+    }
+    
+    private var avatorView: UIImageView = UIImageView()
+    private lazy var defaultUserIcon = UIImage.loadImage("com_icon_user_36")
+    
+    private func _init() {
+        
+        avatorView.image = defaultUserIcon
+
+        addSubview(avatorView)
+        
+        addConstraint(_JCLayoutConstraintMake(avatorView, .centerY, .equal, contentView, .centerY))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .width, .equal, nil, .notAnAttribute, 36))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .height, .equal, nil, .notAnAttribute, 36))
+        addConstraint(_JCLayoutConstraintMake(avatorView, .centerX, .equal, contentView, .centerX))
+        
+    }
+    
+    func bindDate(user: JMSGUser) {
+        user.thumbAvatarData { (data, id, error) in
+            if let data = data {
+                let image = UIImage(data: data)
+                self.avatorView.image = image
+            } else {
+                self.avatorView.image = self.defaultUserIcon
+            }
+        }
+    }
+}

+ 83 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JACMessageImageContentView.swift

@@ -0,0 +1,83 @@
+//
+//  JCMessageImageContentView.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageImageContentView: UIImageView, JCMessageContentViewType {
+    
+    public override init(image: UIImage?) {
+        super.init(image: image)
+        _commonInit()
+    }
+    public override init(image: UIImage?, highlightedImage: UIImage?) {
+        super.init(image: image, highlightedImage: highlightedImage)
+        _commonInit()
+    }
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+
+    open func apply(_ message: JCMessageType) {
+        _message = message
+        weak var weakSelf = self
+        guard let content = message.content as? JCMessageImageContent else {
+            return
+        } 
+        image = content.image
+        _delegate = content.delegate
+        percentLabel.frame = CGRect(x: 0, y: 0, width: self.width, height: self.height)
+        if message.options.state == .sending {
+            percentLabel.backgroundColor = UIColor.black.withAlphaComponent(0.3)
+            percentLabel.isHidden = false
+            percentLabel.textColor = .white
+            content.upload = {  (percent:Float) -> Void in
+                DispatchQueue.main.async {
+                    let p = Int(percent * 100)
+                    weakSelf?.percentLabel.text = "\(p)%"
+                    if percent == 1.0 {
+                        weakSelf?.percentLabel.isHidden = true
+                        weakSelf?.percentLabel.text = ""
+                    }
+                }
+            }
+        } else {
+            percentLabel.textColor = .clear
+            percentLabel.backgroundColor = .clear
+        }
+    }
+    
+    private weak var _delegate: JCMessageDelegate?
+    private var _message: JCMessageType!
+    
+    private lazy var percentLabel: UILabel = {
+        var percentLabel = UILabel(frame: CGRect(x: 20, y: 40, width: 50, height: 20))
+        percentLabel.isUserInteractionEnabled = false
+        percentLabel.textAlignment = .center
+        percentLabel.textColor = .white
+        percentLabel.font = UIFont.systemFont(ofSize: 17)
+        return percentLabel
+    }()
+    
+    private func _commonInit() {
+        let tapGR = UITapGestureRecognizer(target: self, action: #selector(_tapHandler))
+        addGestureRecognizer(tapGR)
+        isUserInteractionEnabled = true
+        layer.cornerRadius = 2
+        layer.masksToBounds = true
+        addSubview(percentLabel)
+    }
+    
+    @objc func _tapHandler(sender:UITapGestureRecognizer) {
+        _delegate?.message?(message: _message, image: image)
+    }
+}

+ 27 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCBusinessCardContent.swift

@@ -0,0 +1,27 @@
+//
+//  JCBusinessCardContent.swift
+//  JChat
+//
+//  Created by 邓永豪 on 2017/8/31.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class JCBusinessCardContent: NSObject, JCMessageContentType {
+
+    public weak var delegate: JCMessageDelegate?
+    open var layoutMargins: UIEdgeInsets = .zero
+    
+    open class var viewType: JCMessageContentViewType.Type {
+        return JCBusinessCardContentView.self
+    }
+    
+    open var userName: String?
+    open var appKey: String?
+    
+    open func sizeThatFits(_ size: CGSize) -> CGSize {
+        return .init(width: 200, height: 87)
+    }
+
+}

+ 125 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCBusinessCardContentView.swift

@@ -0,0 +1,125 @@
+//
+//  JCBusinessCardContentView.swift
+//  JChat
+//
+//  Created by 邓永豪 on 2017/8/31.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class JCBusinessCardContentView: UIView, JCMessageContentViewType {
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+    
+    open func apply(_ message: JCMessageType) {
+        guard let content = message.content as? JCBusinessCardContent else {
+            return
+        }
+        
+        _message = message
+        _delegate = content.delegate
+        _userName = content.userName
+        _appKey = content.appKey
+        
+        userNameLabel.text = "用户名:\(String(describing: _userName!))"
+        
+        if let userName = _userName {
+            JMSGUser.userInfoArray(withUsernameArray: [userName], completionHandler: { (result, error) in
+                let users = result as? [JMSGUser]
+                guard let user = users?.first else {
+                    return
+                }
+                self._user = user
+
+                if user.nickname != nil && !user.nickname!.isEmpty {
+                    self.nickNameLabel.text = user.nickname
+                    self.nickNameLabel.frame = CGRect(x: 62, y: 11.5, width: 126, height: 22.5)
+                } else {
+                    self.userNameLabel.text = ""
+                    self.nickNameLabel.text = user.username
+                    self.nickNameLabel.frame = CGRect(x: 62, y: 22.5, width: 126, height: 22.5)
+                }
+
+                user.thumbAvatarData({ (data, msgId, error) in
+                    if let data = data {
+                        self.imageView.image = UIImage(data: data)
+                    } else {
+                        self.imageView.image = UIImage.loadImage("com_icon_user_40")
+                    }
+                })
+            })
+        }
+    }
+    
+    
+    private weak var _delegate: JCMessageDelegate?
+    
+    private var _userName: String?
+    private var _appKey: String?
+    private var _nickname: String?
+    private var _message: JCMessageType!
+    private var _user: JMSGUser?
+    
+    private lazy var imageView: UIImageView = {
+        let imageView = UIImageView(frame: CGRect(x: 12, y: 13.5, width: 40, height: 40))
+        imageView.image = UIImage.loadImage("com_icon_user_40")
+        return imageView
+    }()
+    private lazy var line: UILabel = {
+        let line = UILabel()
+        line.frame = CGRect(x: 10, y: 66, width: 180, height: 1)
+        line.layer.backgroundColor = UIColor(netHex: 0xE8E8E8).cgColor
+        return line
+    }()
+    private lazy var userNameLabel: UILabel = {
+        let userNameLabel = UILabel()
+        userNameLabel.frame = CGRect(x: 62, y: 37, width: 126, height: 20)
+        userNameLabel.font = UIFont.systemFont(ofSize: 14)
+        userNameLabel.textColor = UIColor(netHex: 0x999999)
+        return userNameLabel
+    }()
+    private lazy var nickNameLabel: UILabel = {
+        let nickNameLabel = UILabel()
+        nickNameLabel.frame = CGRect(x: 62, y: 11.5, width: 126, height: 22.5)
+        nickNameLabel.font = UIFont.systemFont(ofSize: 16)
+        nickNameLabel.textColor = .black
+        return nickNameLabel
+    }()
+    private lazy var tipsLabel: UILabel = {
+        let tipsLabel = UILabel()
+        tipsLabel.frame = CGRect(x: 12, y: 69.5, width: 100, height: 14)
+        tipsLabel.font = UIFont.systemFont(ofSize: 10)
+        tipsLabel.textColor = UIColor(netHex: 0x989898)
+        tipsLabel.text = "个人名片"
+        return tipsLabel
+    }()
+    
+    private func _commonInit() {
+        _tapGesture()
+
+        addSubview(imageView)
+        addSubview(nickNameLabel)
+        addSubview(userNameLabel)
+        addSubview(tipsLabel)
+        addSubview(line)
+    }
+    
+    func _tapGesture() {
+        let tap = UITapGestureRecognizer(target: self, action: #selector(_clickCell))
+        tap.numberOfTapsRequired = 1
+        addGestureRecognizer(tap)
+    }
+    
+    @objc func _clickCell() {
+        _delegate?.message?(message: _message, user: _user, businessCardName: _userName!, businessCardAppKey: _appKey!)
+    }
+
+}

+ 28 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageFileContent.swift

@@ -0,0 +1,28 @@
+//
+//  JCMessageFileContent.swift
+//  JChat
+//
+//  Created by deng on 2017/7/20.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class JCMessageFileContent: NSObject, JCMessageContentType {
+
+    public weak var delegate: JCMessageDelegate?
+    open var layoutMargins: UIEdgeInsets = .zero
+    
+    open class var viewType: JCMessageContentViewType.Type {
+        return JCMessageFileContentView.self
+    }
+    
+    open var data: Data?
+    open var fileName: String?
+    open var fileType: String?
+    open var fileSize: String?
+    
+    open func sizeThatFits(_ size: CGSize) -> CGSize {
+        return .init(width: 200, height: 95)
+    }
+}

+ 127 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageFileContentView.swift

@@ -0,0 +1,127 @@
+//
+//  JCMessageFileContentView.swift
+//  JChat
+//
+//  Created by deng on 2017/7/20.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class JCMessageFileContentView: UIView, JCMessageContentViewType {
+    
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+    
+    open func apply(_ message: JCMessageType) {
+        guard let content = message.content as? JCMessageFileContent else {
+            return
+        }
+        _message = message
+        _delegate = content.delegate
+        _fileData = content.data
+        _fileName = content.fileName
+        _fileType = content.fileType
+        _fileSize = content.fileSize
+        
+        _updateFileTypeIcon(_fileType)
+
+        fileNameLabel.text = _fileName
+        fileSizeLabel.text = _fileSize
+        if _fileData != nil {
+            fileStatusLabel.text = "己下载"
+        } else {
+            fileStatusLabel.text = "未下载"
+        }
+    }
+    
+    private func _updateFileTypeIcon(_ fileType: String?) {
+        if let type = fileType {
+            switch type.fileFormat() {
+            case .document:
+                imageView.image = UIImage.loadImage("com_icon_file_file")
+            case .video:
+                imageView.image = UIImage.loadImage("com_icon_file_video")
+            case .photo:
+                imageView.image = UIImage.loadImage("com_icon_file_photo")
+            case .voice:
+                imageView.image = UIImage.loadImage("com_icon_file_music")
+            default:
+                imageView.image = UIImage.loadImage("com_icon_file_other")
+            }
+        } else {
+            imageView.image = UIImage.loadImage("com_icon_file_other")
+        }
+    }
+    
+    private weak var _delegate: JCMessageDelegate?
+    
+    private var _fileData: Data?
+    private var _fileName: String?
+    private var _fileType: String?
+    private var _fileSize: String?
+    private var _message: JCMessageType!
+    
+    private lazy var imageView: UIImageView = {
+        let imageView = UIImageView(frame: CGRect(x: 12, y: 18, width: 40, height: 40))
+        imageView.layer.cornerRadius = 2.5
+        imageView.layer.masksToBounds = true
+        return imageView
+    }()
+    private lazy var line: UILabel = {
+        let line = UILabel()
+        line.frame = CGRect(x: 12, y: 74, width: 176, height: 1)
+        line.backgroundColor = UIColor(netHex: 0xE8E8E8)
+        line.layer.backgroundColor = UIColor(netHex: 0xE8E8E8).cgColor
+        return line
+    }()
+    private lazy var fileNameLabel: UILabel = {
+        let fileNameLabel = UILabel()
+        fileNameLabel.frame = CGRect(x: 68, y: 18, width: 120, height: 40)
+        fileNameLabel.numberOfLines = 0
+        fileNameLabel.font = UIFont.systemFont(ofSize: 16)
+        fileNameLabel.textColor = UIColor(netHex: 0x5a5a5a)
+        return fileNameLabel
+    }()
+    private lazy var fileSizeLabel: UILabel = {
+        let fileSizeLabel = UILabel()
+        fileSizeLabel.frame = CGRect(x: 12, y: 75, width: 85, height: 20)
+        fileSizeLabel.font = UIFont.systemFont(ofSize: 12)
+        fileSizeLabel.textColor = UIColor(netHex: 0x989898)
+        return fileSizeLabel
+    }()
+    private lazy var fileStatusLabel: UILabel = {
+        let fileStatusLabel = UILabel()
+        fileStatusLabel.frame = CGRect(x: 103, y: 75, width: 85, height: 20)
+        fileStatusLabel.textAlignment = .right
+        fileStatusLabel.font = UIFont.systemFont(ofSize: 12)
+        fileStatusLabel.textColor = UIColor(netHex: 0x989898)
+        return fileStatusLabel
+    }()
+    
+    private func _commonInit() {
+        _tapGesture()
+
+        addSubview(imageView)
+        addSubview(fileNameLabel)
+        addSubview(fileStatusLabel)
+        addSubview(fileSizeLabel)
+        addSubview(line)
+    }
+    
+    func _tapGesture() {
+        let tap = UITapGestureRecognizer(target: self, action: #selector(_clickCell))
+        tap.numberOfTapsRequired = 1
+        addGestureRecognizer(tap)
+    }
+    
+    @objc func _clickCell() {
+        _delegate?.message?(message: _message, fileData: _fileData, fileName: _fileName, fileType: _fileType)
+    }
+}

+ 34 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageImageContent.swift

@@ -0,0 +1,34 @@
+//
+//  JCMessageImageContent.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageImageContent: NSObject, JCMessageContentType {
+    typealias uploadHandle = (_ percent: Float) -> ()
+
+    public weak var delegate: JCMessageDelegate?
+    var upload: uploadHandle?
+    var imageSize: CGSize?
+    open var image: UIImage?
+    open var layoutMargins: UIEdgeInsets = .zero
+    
+    open class var viewType: JCMessageContentViewType.Type {
+        return JCMessageImageContentView.self
+    }
+    
+    open func sizeThatFits(_ size: CGSize) -> CGSize {
+        if image == nil {
+             image = UIImage.createImage(color: UIColor(netHex: 0xCDD0D1), size: imageSize ?? CGSize(width: 160, height: 160))
+        }
+        let size = imageSize ?? (image?.size)!
+        let scale = min(min(160, size.width) / size.width, min(160, size.height) / size.height)
+        let w = size.width * scale
+        let h = size.height * scale
+        return .init(width: w, height: h)
+    }
+}

+ 26 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageLocationContent.swift

@@ -0,0 +1,26 @@
+//
+//  JCMessageLocationContent.swift
+//  JChat
+//
+//  Created by deng on 2017/4/19.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class JCMessageLocationContent: NSObject, JCMessageContentType {
+
+    public weak var delegate: JCMessageDelegate?
+    open var layoutMargins: UIEdgeInsets = .zero
+    open class var viewType: JCMessageContentViewType.Type {
+        return JCMessageLocationContentView.self
+    }
+    
+    open var address: String?
+    open var lon: Double?
+    open var lat: Double?
+    
+    open func sizeThatFits(_ size: CGSize) -> CGSize {
+        return .init(width: 141, height: 91)
+    }
+}

+ 70 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageLocationContentView.swift

@@ -0,0 +1,70 @@
+//
+//  JCMessageLocationContentView.swift
+//  JChat
+//
+//  Created by deng on 2017/4/19.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+class JCMessageLocationContentView: UIImageView, JCMessageContentViewType {
+    public override init(image: UIImage?) {
+        super.init(image: image)
+        _commonInit()
+    }
+    public override init(image: UIImage?, highlightedImage: UIImage?) {
+        super.init(image: image, highlightedImage: highlightedImage)
+        _commonInit()
+    }
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+    
+    open func apply(_ message: JCMessageType) {
+        guard let content = message.content as? JCMessageLocationContent else {
+            return
+        }
+        _message = message
+        _delegate = content.delegate
+        _address = content.address
+        _lon = content.lon
+        _lat = content.lat
+        _addressLabel.text = content.address
+    }
+    
+    private weak var _delegate: JCMessageDelegate?
+    private var _message: JCMessageType!
+    private var _address: String?
+    private var _lon: Double?
+    private var _lat: Double?
+    private var _addressLabel = UILabel(frame: CGRect(x: 10, y: 0, width: 113, height: 40))
+    private lazy var locationImage: UIImage? = UIImage.loadImage("location_address")
+    
+    private func _commonInit() {
+        _addressLabel.font = UIFont.systemFont(ofSize: 13)
+        _addressLabel.numberOfLines = 2
+        addSubview(_addressLabel)
+        isUserInteractionEnabled = true
+        layer.cornerRadius = 2
+        layer.masksToBounds = true
+        image = locationImage
+        _tapGesture()
+    }
+    
+    func _tapGesture() {
+        
+        let tap = UITapGestureRecognizer(target: self, action: #selector(_clickCell))
+        tap.numberOfTapsRequired = 1
+        addGestureRecognizer(tap)
+    }
+    
+    func _clickCell() {
+        _delegate?.message?(message: _message, location: _address, lat: _lat ?? 0, lon: _lon ?? 0)
+    }
+}

+ 39 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageNoticeContent.swift

@@ -0,0 +1,39 @@
+//
+//  JCMessageNoticeContent.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageNoticeContent: NSObject, JCMessageContentType {
+    public weak var delegate: JCMessageDelegate?
+    
+    open var layoutMargins: UIEdgeInsets = .zero
+    
+    open class var viewType: JCMessageContentViewType.Type {
+        return JCMessageNoticeContentView.self
+    }
+    
+    public init(text: String) {
+        self.text = text
+        super.init()
+    }
+    
+    open var text: String
+    
+    open func sizeThatFits(_ size: CGSize) -> CGSize {
+        let attributes = [
+            NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12),
+            NSAttributedString.Key.foregroundColor: UIColor.white,
+        ]
+        let attr = NSMutableAttributedString(string: text, attributes: attributes)
+        let mattrSize = attr.boundingRect(with: CGSize(width: 250.0, height: Double(MAXFLOAT)), options: [.usesLineFragmentOrigin,.usesFontLeading], context: nil)
+        let size = CGSize(width: mattrSize.size.width + 11, height: mattrSize.size.height + 4)
+        return size
+    }
+
+    public static let unsupport: JCMessageNoticeContent = JCMessageNoticeContent(text: "The message does not support")
+}

+ 41 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageNoticeContentView.swift

@@ -0,0 +1,41 @@
+//
+//  JCMessageNoticeContentView.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageNoticeContentView: UILabel, JCMessageContentViewType {
+    
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+
+    
+    open func apply(_ message: JCMessageType) {
+        guard let content = message.content as? JCMessageNoticeContent else {
+            return
+        }
+        text = content.text
+    }
+    
+    private func _commonInit() {
+        layer.cornerRadius = 2.5
+        layer.borderWidth = 1.0
+        layer.borderColor = UIColor(netHex: 0xD7DCE2).cgColor
+        layer.masksToBounds = true
+        font = UIFont.systemFont(ofSize: 12)
+        backgroundColor = UIColor(netHex: 0xD7DCE2)
+        textColor = .white
+        textAlignment = .center
+        numberOfLines = 0
+    }
+}

+ 42 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageTextContent.swift

@@ -0,0 +1,42 @@
+//
+//  JCMessageTextContent.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageTextContent: NSObject, JCMessageContentType {
+    public weak var delegate: JCMessageDelegate?
+    public override init() {
+        let text = "this is a test text"
+        self.text = NSAttributedString(string: text)
+        super.init()
+    }
+    public init(text: String) {
+        self.text = NSAttributedString(string: text)
+        super.init()
+    }
+    public init(attributedText: NSAttributedString) {
+        self.text = attributedText
+        super.init()
+    }
+    
+    open class var viewType: JCMessageContentViewType.Type {
+        return JCMessageTextContentView.self
+    }
+    open var layoutMargins: UIEdgeInsets = .init(top: 9, left: 10, bottom: 9, right: 10)
+    
+    open var text: NSAttributedString
+    
+    open func sizeThatFits(_ size: CGSize) -> CGSize {
+        let mattr = NSMutableAttributedString(attributedString: text)
+        mattr.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, mattr.length))
+
+        let mattrSize = mattr.boundingRect(with: CGSize(width: 220.0, height: Double(MAXFLOAT)), options: [.usesLineFragmentOrigin,.usesFontLeading], context: nil)
+        self.text = mattr
+        return .init(width: max(mattrSize.width, 15), height: max(mattrSize.height, 15))
+    }
+}

+ 49 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageTextContentView.swift

@@ -0,0 +1,49 @@
+//
+//  JCMessageTextContentView.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageTextContentView: KILabel, JCMessageContentViewType {
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+
+    open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
+        return super.canPerformAction(action, withSender: sender)
+    }
+    
+    open func apply(_ message: JCMessageType) {
+        guard let content = message.content as? JCMessageTextContent else {
+            return
+        }
+        self.attributedText = content.text
+        self.linkDetectionTypes = KILinkTypeOption.URL
+        self.urlLinkTapHandler = { label, url, range in
+            if let Url = URL(string: url) {
+                if UIApplication.shared.canOpenURL(Url) {
+                    UIApplication.shared.openURL(Url)
+                } else {
+                    let newUrl = URL(string: "https://" + url)
+                    UIApplication.shared.openURL(newUrl!)
+                }
+            }
+        }
+    }
+    
+    private func _commonInit() {
+        self.numberOfLines = 0
+    }
+}
+
+

+ 93 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageTimeLineContent.swift

@@ -0,0 +1,93 @@
+//
+//  JCMessageTimeLineContent.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageTimeLineContent: NSObject, JCMessageContentType {
+
+    public weak var delegate: JCMessageDelegate?
+    open var layoutMargins: UIEdgeInsets = .zero
+
+    open class var viewType: JCMessageContentViewType.Type {
+        return JCMessageTimeLineContentView.self
+    }
+    
+    public init(date: Date) {
+        self.date = date
+        super.init()
+    }
+
+    internal var before: JCMessageType?
+    internal var after: JCMessageType?
+    
+    open var date: Date
+    open var text: String {
+        return JCMessageTimeLineContent.dd(after?.date ?? date)
+    }
+
+    // 这个是比较耗时的操作,所以这里设置为全局只创建一次
+    static let defaultFormat = DateFormatter.dateFormat(fromTemplate: "hh:mm", options: 0, locale: nil) ?? "hh:mm"
+    
+    static func dd(_ date: Date) -> String {
+        
+        // yy-MM-dd hh:mm
+        // MM-dd hh:mm
+        // 星期一 hh:mm - 7 * 24小时内
+        // 昨天 hh:mm - 2 * 24小时内
+        // 今天 hh:mm - 24小时内
+        
+        let s1 = TimeInterval(date.timeIntervalSince1970)
+        let s2 = TimeInterval(time(nil))
+        
+        let dz = TimeInterval(TimeZone.current.secondsFromGMT())
+        
+        let formatter = DateFormatter()
+        // 每次都创建会非常耗时
+        let format1 = JCMessageTimeLineContent.defaultFormat
+        
+        let days1 = Int64(s1 + dz) / (24 * 60 * 60)
+        let days2 = Int64(s2 + dz) / (24 * 60 * 60)
+        
+        switch days1 - days2 {
+        case +0:
+            // Today
+            formatter.dateFormat = "\(format1)"
+        case +1:
+            // Tomorrow
+            formatter.dateFormat = "'明天' \(format1)"
+        case +2 ... +7:
+            // 2 - 7 day later
+            formatter.dateFormat = "EEEE \(format1)"
+        case -1:
+            formatter.dateFormat = "'昨天' \(format1)"
+        case -2:
+            formatter.dateFormat = "'前天' \(format1)"
+        case -7 ... -2:
+            // 2 - 7 day ago
+            formatter.dateFormat = "EEEE \(format1)"
+        default:
+            // Distant
+            if date.isThisYear() {
+                formatter.dateFormat = "MM-dd \(format1)"
+            } else {
+                formatter.dateFormat = "yy-MM-dd \(format1)"
+            }
+        }
+        return formatter.string(from: date)
+    }
+    
+    open func sizeThatFits(_ size: CGSize) -> CGSize {
+        
+        let attr = NSMutableAttributedString(string: text, attributes: [
+            NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12),
+            NSAttributedString.Key.foregroundColor: UIColor.white,
+            ])
+        
+        return CGSize(width: attr.size().width + 11, height: 18)
+    }
+}

+ 39 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageTimeLineContentView.swift

@@ -0,0 +1,39 @@
+//
+//  JCMessageTimeLineContentView.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageTimeLineContentView: UILabel, JCMessageContentViewType {
+    
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+    
+    open func apply(_ message: JCMessageType) {
+        guard let content = message.content as? JCMessageTimeLineContent else {
+            return
+        }
+        text = content.text
+    }
+    
+    private func _commonInit() {
+        layer.cornerRadius = 2.5
+        layer.borderWidth = 1.0
+        layer.borderColor = UIColor(netHex: 0xD7DCE2).cgColor
+        layer.masksToBounds = true
+        font = UIFont.systemFont(ofSize: 12)
+        backgroundColor = UIColor(netHex: 0xD7DCE2)
+        textColor = .white
+        textAlignment = .center
+    }
+}

+ 36 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageVideoContent.swift

@@ -0,0 +1,36 @@
+//
+//  JCMessageVideoContent.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+import JMessage
+
+open class JCMessageVideoContent: NSObject, JCMessageContentType {
+
+    public weak var delegate: JCMessageDelegate?
+    open var layoutMargins: UIEdgeInsets = .zero
+    open class var viewType: JCMessageContentViewType.Type {
+        return JCMessageVideoContentView.self
+    }
+    open var data: Data?
+    open var image: UIImage?
+    open var fileContent: JMSGFileContent?
+    
+    open func sizeThatFits(_ size: CGSize) -> CGSize {
+        if data == nil {
+            return .init(width: 140, height: 89)
+        }
+        image = JCVideoManager.getFristImage(data: data!)
+        let size = image?.size ?? .zero
+        
+        let scale = min(min(160, size.width) / size.width, min(160, size.height) / size.height)
+        
+        let w = size.width * scale
+        let h = size.height * scale
+        return .init(width: w, height: h)
+    }
+}

+ 115 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageVideoContentView.swift

@@ -0,0 +1,115 @@
+//
+//  JCMessageVideoContentView.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+import AVKit
+import AVFoundation
+
+open class JCMessageVideoContentView: UIImageView, JCMessageContentViewType {
+    
+    public override init(image: UIImage?) {
+        super.init(image: image)
+        _commonInit()
+    }
+    public override init(image: UIImage?, highlightedImage: UIImage?) {
+        super.init(image: image, highlightedImage: highlightedImage)
+        _commonInit()
+    }
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        _commonInit()
+    }
+    public required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        _commonInit()
+    }
+    
+    open func apply(_ message: JCMessageType) {
+        guard message.content is JCMessageVideoContent else {
+            return
+        }
+        _message = message
+        let content = message.content as! JCMessageVideoContent
+        _delegate = content.delegate
+        if content.data != nil {
+            _data = content.data
+            if content.image != nil {
+                self.image = content.image
+            }else {
+                DispatchQueue.main.async {
+                    self.image = JCVideoManager.getFristImage(data: self._data!)
+                }
+            }
+        }else{
+            //self.image = UIImage.createImage(color: UIColor.init(hex: "0xCDD0D1"), size: self.size)
+            content.fileContent?.fileData({ (data, id, error) in
+                if data != nil {
+                    self._data = data
+                    DispatchQueue.main.async {
+                        self.image = JCVideoManager.getFristImage(data: self._data!)
+                    }
+                }
+            })
+        }
+//
+        _playImageView.center = CGPoint(x: self.width/2, y: self.height/2)
+        
+        
+//        guard let content = message.content as? JCMessageVideoContent else {
+//            return
+//        }
+//        _message = message
+//        _delegate = content.delegate
+//        if content.data != nil {
+//            _data = content.data
+//            if content.image != nil {
+//                self.image = content.image
+//            } else {
+//                DispatchQueue.main.async {
+//                    self.image = JCVideoManager.getFristImage(data: self._data!)
+//                }
+//            }
+//        } else {
+//            self.image = UIImage.createImage(color: UIColor(netHex: 0xCDD0D1), size: self.size)
+//            content.fileContent?.fileData({ (data, id, error) in
+//                if data != nil {
+//                    self._data = data
+//                    DispatchQueue.main.async {
+//                        self.image = JCVideoManager.getFristImage(data: self._data!)
+//                    }
+//                }
+//            })
+//        }
+//        _playImageView.center = CGPoint(x: self.width / 2, y: self.height / 2)
+    }
+
+    private weak var _delegate: JCMessageDelegate?
+    private var _data: Data?
+    private var _playImageView: UIImageView!
+    private var _message: JCMessageType!
+    
+    private func _commonInit() {
+        isUserInteractionEnabled = true
+        layer.cornerRadius = 2
+        layer.masksToBounds = true
+        _tapGesture()
+        _playImageView = UIImageView(frame: CGRect(x: 0, y: 50, width: 41, height: 41))
+        _playImageView.image = UIImage.loadImage("com_icon_play")
+        addSubview(_playImageView)
+    }
+    
+    func _tapGesture() {
+        let tap = UITapGestureRecognizer(target: self, action: #selector(_clickCell))
+        tap.numberOfTapsRequired = 1
+        addGestureRecognizer(tap)
+    }
+    
+    @objc func _clickCell() {
+        _delegate?.message?(message: _message, videoData: _data)
+    }
+}

+ 36 - 0
o2ios/O2Platform/Framework/JMessage/ChatModule/Chat/View/Message/JCMessageVoiceContent.swift

@@ -0,0 +1,36 @@
+//
+//  JCMessageVoiceContent.swift
+//  JChat
+//
+//  Created by deng on 2017/3/9.
+//  Copyright © 2017年 HXHG. All rights reserved.
+//
+
+import UIKit
+
+open class JCMessageVoiceContent: NSObject, JCMessageContentType {
+
+    public weak var delegate: JCMessageDelegate?
+    open var layoutMargins: UIEdgeInsets = .init(top: 5, left: 10, bottom: 5, right: 10)
+    open class var viewType: JCMessageContentViewType.Type {
+        return JCMessageVoiceContentView.self
+    }
+    open var data: Data?
+    open var duration: TimeInterval = 9999
+    open var attributedText: NSAttributedString?
+    
+    open func sizeThatFits(_ size: CGSize) -> CGSize {
+        // +---------------+
+        // | |||  99'59''  |
+        // +---------------+
+        let minute = Int(duration) / 60
+        let second = Int(duration) % 60
+        var string = "\(minute)'\(second)''"
+        if minute == 0 {
+            string = "\(second)''"
+        }
+        attributedText = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14)])
+        
+        return .init(width: 20 + 38 + 20, height: 26)
+    }
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio