ESPullToRefresh.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. //
  2. // ESPullToRefresh.swift
  3. //
  4. // Created by egg swift on 16/4/7.
  5. // Copyright (c) 2013-2016 ESPullToRefresh (https://github.com/eggswift/pull-to-refresh)
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in
  15. // all copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. // THE SOFTWARE.
  24. //
  25. import Foundation
  26. import UIKit
  27. private var kESRefreshHeaderKey: Void?
  28. private var kESRefreshFooterKey: Void?
  29. public extension UIScrollView {
  30. /// Pull-to-refresh associated property
  31. var header: ESRefreshHeaderView? {
  32. get { return (objc_getAssociatedObject(self, &kESRefreshHeaderKey) as? ESRefreshHeaderView) }
  33. set(newValue) { objc_setAssociatedObject(self, &kESRefreshHeaderKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) }
  34. }
  35. /// Infinitiy scroll associated property
  36. var footer: ESRefreshFooterView? {
  37. get { return (objc_getAssociatedObject(self, &kESRefreshFooterKey) as? ESRefreshFooterView) }
  38. set(newValue) { objc_setAssociatedObject(self, &kESRefreshFooterKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) }
  39. }
  40. }
  41. public extension ES where Base: UIScrollView {
  42. /// Add pull-to-refresh
  43. @discardableResult
  44. func addPullToRefresh(handler: @escaping ESRefreshHandler) -> ESRefreshHeaderView {
  45. removeRefreshHeader()
  46. let header = ESRefreshHeaderView(frame: CGRect.zero, handler: handler)
  47. let headerH = header.animator.executeIncremental
  48. header.frame = CGRect.init(x: 0.0, y: -headerH /* - contentInset.top */, width: self.base.bounds.size.width, height: headerH)
  49. self.base.addSubview(header)
  50. self.base.header = header
  51. return header
  52. }
  53. @discardableResult
  54. func addPullToRefresh(animator: ESRefreshProtocol & ESRefreshAnimatorProtocol, handler: @escaping ESRefreshHandler) -> ESRefreshHeaderView {
  55. removeRefreshHeader()
  56. let header = ESRefreshHeaderView(frame: CGRect.zero, handler: handler, animator: animator)
  57. let headerH = animator.executeIncremental
  58. header.frame = CGRect.init(x: 0.0, y: -headerH /* - contentInset.top */, width: self.base.bounds.size.width, height: headerH)
  59. self.base.addSubview(header)
  60. self.base.header = header
  61. return header
  62. }
  63. /// Add infinite-scrolling
  64. @discardableResult
  65. func addInfiniteScrolling(handler: @escaping ESRefreshHandler) -> ESRefreshFooterView {
  66. removeRefreshFooter()
  67. let footer = ESRefreshFooterView(frame: CGRect.zero, handler: handler)
  68. let footerH = footer.animator.executeIncremental
  69. footer.frame = CGRect.init(x: 0.0, y: self.base.contentSize.height + self.base.contentInset.bottom, width: self.base.bounds.size.width, height: footerH)
  70. self.base.addSubview(footer)
  71. self.base.footer = footer
  72. return footer
  73. }
  74. @discardableResult
  75. func addInfiniteScrolling(animator: ESRefreshProtocol & ESRefreshAnimatorProtocol, handler: @escaping ESRefreshHandler) -> ESRefreshFooterView {
  76. removeRefreshFooter()
  77. let footer = ESRefreshFooterView(frame: CGRect.zero, handler: handler, animator: animator)
  78. let footerH = footer.animator.executeIncremental
  79. footer.frame = CGRect.init(x: 0.0, y: self.base.contentSize.height + self.base.contentInset.bottom, width: self.base.bounds.size.width, height: footerH)
  80. self.base.footer = footer
  81. self.base.addSubview(footer)
  82. return footer
  83. }
  84. /// Remove
  85. func removeRefreshHeader() {
  86. self.base.header?.stopRefreshing()
  87. self.base.header?.removeFromSuperview()
  88. self.base.header = nil
  89. }
  90. func removeRefreshFooter() {
  91. self.base.footer?.stopRefreshing()
  92. self.base.footer?.removeFromSuperview()
  93. self.base.footer = nil
  94. }
  95. /// Manual refresh
  96. func startPullToRefresh() {
  97. DispatchQueue.main.async { [weak base] in
  98. base?.header?.startRefreshing(isAuto: false)
  99. }
  100. }
  101. /// Auto refresh if expired.
  102. func autoPullToRefresh() {
  103. if self.base.expired == true {
  104. DispatchQueue.main.async { [weak base] in
  105. base?.header?.startRefreshing(isAuto: true)
  106. }
  107. }
  108. }
  109. /// Stop pull to refresh
  110. func stopPullToRefresh(ignoreDate: Bool = false, ignoreFooter: Bool = false) {
  111. self.base.header?.stopRefreshing()
  112. if ignoreDate == false {
  113. if let key = self.base.header?.refreshIdentifier {
  114. ESRefreshDataManager.sharedManager.setDate(Date(), forKey: key)
  115. }
  116. self.base.footer?.resetNoMoreData()
  117. }
  118. self.base.footer?.isHidden = ignoreFooter
  119. }
  120. /// Footer notice method
  121. func noticeNoMoreData() {
  122. self.base.footer?.stopRefreshing()
  123. self.base.footer?.noMoreData = true
  124. }
  125. func resetNoMoreData() {
  126. self.base.footer?.noMoreData = false
  127. }
  128. func stopLoadingMore() {
  129. self.base.footer?.stopRefreshing()
  130. }
  131. }
  132. public extension UIScrollView /* Date Manager */ {
  133. /// Identifier for cache expired timeinterval and last refresh date.
  134. var refreshIdentifier: String? {
  135. get { return self.header?.refreshIdentifier }
  136. set { self.header?.refreshIdentifier = newValue }
  137. }
  138. /// If you setted refreshIdentifier and expiredTimeInterval, return nearest refresh expired or not. Default is false.
  139. var expired: Bool {
  140. get {
  141. if let key = self.header?.refreshIdentifier {
  142. return ESRefreshDataManager.sharedManager.isExpired(forKey: key)
  143. }
  144. return false
  145. }
  146. }
  147. var expiredTimeInterval: TimeInterval? {
  148. get {
  149. if let key = self.header?.refreshIdentifier {
  150. let interval = ESRefreshDataManager.sharedManager.expiredTimeInterval(forKey: key)
  151. return interval
  152. }
  153. return nil
  154. }
  155. set {
  156. if let key = self.header?.refreshIdentifier {
  157. ESRefreshDataManager.sharedManager.setExpiredTimeInterval(newValue, forKey: key)
  158. }
  159. }
  160. }
  161. /// Auto cached last refresh date when you setted refreshIdentifier.
  162. var lastRefreshDate: Date? {
  163. get {
  164. if let key = self.header?.refreshIdentifier {
  165. return ESRefreshDataManager.sharedManager.date(forKey: key)
  166. }
  167. return nil
  168. }
  169. }
  170. }
  171. open class ESRefreshHeaderView: ESRefreshComponent {
  172. fileprivate var previousOffset: CGFloat = 0.0
  173. fileprivate var scrollViewInsets: UIEdgeInsets = UIEdgeInsets.zero
  174. fileprivate var scrollViewBounces: Bool = true
  175. open var lastRefreshTimestamp: TimeInterval?
  176. open var refreshIdentifier: String?
  177. public convenience init(frame: CGRect, handler: @escaping ESRefreshHandler) {
  178. self.init(frame: frame)
  179. self.handler = handler
  180. self.animator = ESRefreshHeaderAnimator.init()
  181. }
  182. open override func didMoveToSuperview() {
  183. super.didMoveToSuperview()
  184. DispatchQueue.main.async {
  185. [weak self] in
  186. self?.scrollViewBounces = self?.scrollView?.bounces ?? true
  187. self?.scrollViewInsets = self?.scrollView?.contentInset ?? UIEdgeInsets.zero
  188. }
  189. }
  190. open override func offsetChangeAction(object: AnyObject?, change: [NSKeyValueChangeKey : Any]?) {
  191. guard let scrollView = scrollView else {
  192. return
  193. }
  194. super.offsetChangeAction(object: object, change: change)
  195. guard self.isRefreshing == false && self.isAutoRefreshing == false else {
  196. let top = scrollViewInsets.top
  197. let offsetY = scrollView.contentOffset.y
  198. let height = self.frame.size.height
  199. var scrollingTop = (-offsetY > top) ? -offsetY : top
  200. scrollingTop = (scrollingTop > height + top) ? (height + top) : scrollingTop
  201. scrollView.contentInset.top = scrollingTop
  202. return
  203. }
  204. // Check needs re-set animator's progress or not.
  205. var isRecordingProgress = false
  206. defer {
  207. if isRecordingProgress == true {
  208. let percent = -(previousOffset + scrollViewInsets.top) / self.animator.trigger
  209. self.animator.refresh(view: self, progressDidChange: percent)
  210. }
  211. }
  212. let offsets = previousOffset + scrollViewInsets.top
  213. if offsets < -self.animator.trigger {
  214. // Reached critical
  215. if isRefreshing == false && isAutoRefreshing == false {
  216. if scrollView.isDragging == false {
  217. // Start to refresh...
  218. self.startRefreshing(isAuto: false)
  219. self.animator.refresh(view: self, stateDidChange: .refreshing)
  220. } else {
  221. // Release to refresh! Please drop down hard...
  222. self.animator.refresh(view: self, stateDidChange: .releaseToRefresh)
  223. isRecordingProgress = true
  224. }
  225. }
  226. } else if offsets < 0 {
  227. // Pull to refresh!
  228. if isRefreshing == false && isAutoRefreshing == false {
  229. self.animator.refresh(view: self, stateDidChange: .pullToRefresh)
  230. isRecordingProgress = true
  231. }
  232. } else {
  233. // Normal state
  234. }
  235. previousOffset = scrollView.contentOffset.y
  236. }
  237. open override func start() {
  238. guard let scrollView = scrollView else {
  239. return
  240. }
  241. // ignore observer
  242. self.ignoreObserver(true)
  243. // stop scroll view bounces for animation
  244. scrollView.bounces = false
  245. // call super start
  246. super.start()
  247. self.animator.refreshAnimationBegin(view: self)
  248. // 缓存scrollview当前的contentInset, 并根据animator的executeIncremental属性计算刷新时所需要的contentInset,它将在接下来的动画中应用。
  249. // Tips: 这里将self.scrollViewInsets.top更新,也可以将scrollViewInsets整个更新,因为left、right、bottom属性都没有用到,如果接下来的迭代需要使用这三个属性的话,这里可能需要额外的处理。
  250. var insets = scrollView.contentInset
  251. self.scrollViewInsets.top = insets.top
  252. insets.top += animator.executeIncremental
  253. // We need to restore previous offset because we will animate scroll view insets and regular scroll view animating is not applied then.
  254. scrollView.contentInset = insets
  255. scrollView.contentOffset.y = previousOffset
  256. previousOffset -= animator.executeIncremental
  257. UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveLinear, animations: {
  258. scrollView.contentOffset.y = -insets.top
  259. }, completion: { (finished) in
  260. self.handler?()
  261. // un-ignore observer
  262. self.ignoreObserver(false)
  263. scrollView.bounces = self.scrollViewBounces
  264. })
  265. }
  266. open override func stop() {
  267. guard let scrollView = scrollView else {
  268. return
  269. }
  270. // ignore observer
  271. self.ignoreObserver(true)
  272. self.animator.refreshAnimationEnd(view: self)
  273. // Back state
  274. scrollView.contentInset.top = self.scrollViewInsets.top
  275. scrollView.contentOffset.y = self.previousOffset
  276. UIView.animate(withDuration: 0.2, delay: 0, options: .curveLinear, animations: {
  277. scrollView.contentOffset.y = -self.scrollViewInsets.top
  278. }, completion: { (finished) in
  279. self.animator.refresh(view: self, stateDidChange: .pullToRefresh)
  280. super.stop()
  281. scrollView.contentInset.top = self.scrollViewInsets.top
  282. self.previousOffset = scrollView.contentOffset.y
  283. // un-ignore observer
  284. self.ignoreObserver(false)
  285. })
  286. }
  287. }
  288. open class ESRefreshFooterView: ESRefreshComponent {
  289. fileprivate var scrollViewInsets: UIEdgeInsets = UIEdgeInsets.zero
  290. open var noMoreData = false {
  291. didSet {
  292. if noMoreData != oldValue {
  293. self.animator.refresh(view: self, stateDidChange: noMoreData ? .noMoreData : .pullToRefresh)
  294. }
  295. }
  296. }
  297. open override var isHidden: Bool {
  298. didSet {
  299. if isHidden == true {
  300. scrollView?.contentInset.bottom = scrollViewInsets.bottom
  301. var rect = self.frame
  302. rect.origin.y = scrollView?.contentSize.height ?? 0.0
  303. self.frame = rect
  304. } else {
  305. scrollView?.contentInset.bottom = scrollViewInsets.bottom + animator.executeIncremental
  306. var rect = self.frame
  307. rect.origin.y = scrollView?.contentSize.height ?? 0.0
  308. self.frame = rect
  309. }
  310. }
  311. }
  312. public convenience init(frame: CGRect, handler: @escaping ESRefreshHandler) {
  313. self.init(frame: frame)
  314. self.handler = handler
  315. self.animator = ESRefreshFooterAnimator.init()
  316. }
  317. /**
  318. In didMoveToSuperview, it will cache superview(UIScrollView)'s contentInset and update self's frame.
  319. It called ESRefreshComponent's didMoveToSuperview.
  320. */
  321. open override func didMoveToSuperview() {
  322. super.didMoveToSuperview()
  323. DispatchQueue.main.async {
  324. [weak self] in
  325. self?.scrollViewInsets = self?.scrollView?.contentInset ?? UIEdgeInsets.zero
  326. self?.scrollView?.contentInset.bottom = (self?.scrollViewInsets.bottom ?? 0) + (self?.bounds.size.height ?? 0)
  327. var rect = self?.frame ?? CGRect.zero
  328. rect.origin.y = self?.scrollView?.contentSize.height ?? 0.0
  329. self?.frame = rect
  330. }
  331. }
  332. open override func sizeChangeAction(object: AnyObject?, change: [NSKeyValueChangeKey : Any]?) {
  333. guard let scrollView = scrollView else { return }
  334. super.sizeChangeAction(object: object, change: change)
  335. let targetY = scrollView.contentSize.height + scrollViewInsets.bottom
  336. if self.frame.origin.y != targetY {
  337. var rect = self.frame
  338. rect.origin.y = targetY
  339. self.frame = rect
  340. }
  341. }
  342. open override func offsetChangeAction(object: AnyObject?, change: [NSKeyValueChangeKey : Any]?) {
  343. guard let scrollView = scrollView else {
  344. return
  345. }
  346. super.offsetChangeAction(object: object, change: change)
  347. guard isRefreshing == false && isAutoRefreshing == false && noMoreData == false && isHidden == false else {
  348. // 正在loading more或者内容为空时不相应变化
  349. return
  350. }
  351. if scrollView.contentSize.height <= 0.0 || scrollView.contentOffset.y + scrollView.contentInset.top <= 0.0 {
  352. self.alpha = 0.0
  353. return
  354. } else {
  355. self.alpha = 1.0
  356. }
  357. if scrollView.contentSize.height + scrollView.contentInset.top > scrollView.bounds.size.height {
  358. // 内容超过一个屏幕 计算公式,判断是不是在拖在到了底部
  359. if scrollView.contentSize.height - scrollView.contentOffset.y + scrollView.contentInset.bottom <= scrollView.bounds.size.height {
  360. self.animator.refresh(view: self, stateDidChange: .refreshing)
  361. self.startRefreshing()
  362. }
  363. } else {
  364. //内容没有超过一个屏幕,这时拖拽高度大于1/2footer的高度就表示请求上拉
  365. if scrollView.contentOffset.y + scrollView.contentInset.top >= animator.trigger / 2.0 {
  366. self.animator.refresh(view: self, stateDidChange: .refreshing)
  367. self.startRefreshing()
  368. }
  369. }
  370. }
  371. open override func start() {
  372. guard let scrollView = scrollView else {
  373. return
  374. }
  375. super.start()
  376. self.animator.refreshAnimationBegin(view: self)
  377. let x = scrollView.contentOffset.x
  378. let y = max(0.0, scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom)
  379. // Call handler
  380. UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveLinear, animations: {
  381. scrollView.contentOffset = CGPoint.init(x: x, y: y)
  382. }, completion: { (animated) in
  383. self.handler?()
  384. })
  385. }
  386. open override func stop() {
  387. guard let scrollView = scrollView else {
  388. return
  389. }
  390. self.animator.refreshAnimationEnd(view: self)
  391. // Back state
  392. UIView.animate(withDuration: 0.3, delay: 0, options: .curveLinear, animations: {
  393. }, completion: { (finished) in
  394. if self.noMoreData == false {
  395. self.animator.refresh(view: self, stateDidChange: .pullToRefresh)
  396. }
  397. super.stop()
  398. })
  399. // Stop deceleration of UIScrollView. When the button tap event is caught, you read what the [scrollView contentOffset].x is, and set the offset to this value with animation OFF.
  400. // http://stackoverflow.com/questions/2037892/stop-deceleration-of-uiscrollview
  401. if scrollView.isDecelerating {
  402. var contentOffset = scrollView.contentOffset
  403. contentOffset.y = min(contentOffset.y, scrollView.contentSize.height - scrollView.frame.size.height)
  404. if contentOffset.y < 0.0 {
  405. contentOffset.y = 0.0
  406. UIView.animate(withDuration: 0.1, animations: {
  407. scrollView.setContentOffset(contentOffset, animated: false)
  408. })
  409. } else {
  410. scrollView.setContentOffset(contentOffset, animated: false)
  411. }
  412. }
  413. }
  414. /// Change to no-more-data status.
  415. open func noticeNoMoreData() {
  416. self.noMoreData = true
  417. }
  418. /// Reset no-more-data status.
  419. open func resetNoMoreData() {
  420. self.noMoreData = false
  421. }
  422. }