diff --git a/Classes/RMRPullToRefresh.swift b/Classes/RMRPullToRefresh.swift old mode 100755 new mode 100644 index 3175e0d..194558f --- a/Classes/RMRPullToRefresh.swift +++ b/Classes/RMRPullToRefresh.swift @@ -10,7 +10,7 @@ import UIKit open class RMRPullToRefresh: NSObject { - fileprivate var сontroller: RMRPullToRefreshController? + private var сontroller: RMRPullToRefreshController? open var height : CGFloat = RMRPullToRefreshConstants.DefaultHeight { didSet { @@ -46,14 +46,6 @@ open class RMRPullToRefresh: NSObject { self.сontroller = controller } - /// Метод нужно вызывать в deinit экрана, в котором используется pull-to-refresh. - /// - /// Это временное решение для избежания краша из-за KVO-наблюдателей на scroll view - /// (при уничтожении скролла и экрана, в котором он лежит). - open func unsubscribeFromBindings() { - сontroller?.unsubscribeFromScrollViewEvents() - } - open func configureView(_ view :RMRPullToRefreshView, state:RMRPullToRefreshState, result:RMRPullToRefreshResultType) { сontroller?.configureView(view, state: state, result: result) } diff --git a/Classes/RMRPullToRefreshConstants.swift b/Classes/RMRPullToRefreshConstants.swift index 699c372..341a41c 100755 --- a/Classes/RMRPullToRefreshConstants.swift +++ b/Classes/RMRPullToRefreshConstants.swift @@ -27,14 +27,6 @@ public enum RMRPullToRefreshResultType: Int { public struct RMRPullToRefreshConstants { - struct KeyPaths { - static let ContentOffset = "contentOffset" - static let ContentSize = "contentSize" - static let ContentInset = "contentInset" - static let PanState = "pan.state" - static let Frame = "frame" - } - static let DefaultHeight = CGFloat(90.0) static let DefaultBackgroundColor = UIColor.white } diff --git a/Classes/RMRPullToRefreshController.swift b/Classes/RMRPullToRefreshController.swift old mode 100755 new mode 100644 index 4fb6359..d27b29b --- a/Classes/RMRPullToRefreshController.swift +++ b/Classes/RMRPullToRefreshController.swift @@ -7,26 +7,6 @@ // import UIKit -fileprivate func < (lhs: T?, rhs: T?) -> Bool { - switch (lhs, rhs) { - case let (l?, r?): - return l < r - case (nil, _?): - return true - default: - return false - } -} - -fileprivate func > (lhs: T?, rhs: T?) -> Bool { - switch (lhs, rhs) { - case let (l?, r?): - return l > r - default: - return rhs < lhs - } -} - open class RMRPullToRefreshController { @@ -41,7 +21,6 @@ open class RMRPullToRefreshController { var backgroundViewTopConstraint: NSLayoutConstraint? var stopped = true - var subscribing = false var actionHandler: (() -> Void)! @@ -60,6 +39,12 @@ open class RMRPullToRefreshController { open var hideWhenError: Bool = true + // MARK: - Observation + + private var contentOffsetObservation: NSKeyValueObservation? + private var contentSizeObservation: NSKeyValueObservation? + private var panStateObservation: NSKeyValueObservation? + // MARK: - Init init(scrollView: UIScrollView, position:RMRPullToRefreshPosition, actionHandler: @escaping () -> Void) { @@ -76,30 +61,36 @@ open class RMRPullToRefreshController { self.subscribeOnScrollViewEvents() } - fileprivate func configureBackgroundView(_ backgroundView: UIView) { + private func configureBackgroundView(_ backgroundView: UIView) { backgroundView.translatesAutoresizingMaskIntoConstraints = false scrollView?.addSubview(backgroundView) addBackgroundViewConstraints(backgroundView) } - fileprivate func addBackgroundViewConstraints(_ backgroundView: UIView) { - // Constraints - self.backgroundViewHeightConstraint = NSLayoutConstraint(item: backgroundView, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: 0) - backgroundView.addConstraint(self.backgroundViewHeightConstraint!) + private func addBackgroundViewConstraints(_ backgroundView: UIView) { + guard let scrollView = scrollView, let position = position else { + return + } + + let backgroundViewHeightConstraint = backgroundView.heightAnchor.constraint(equalToConstant: 0) + backgroundViewHeightConstraint.isActive = true + self.backgroundViewHeightConstraint = backgroundViewHeightConstraint - scrollView?.addConstraint(NSLayoutConstraint(item: backgroundView, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: scrollView, attribute: NSLayoutConstraint.Attribute.width, multiplier: 1, constant: 0)) + backgroundView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true - if position == .top { - scrollView?.addConstraint(NSLayoutConstraint(item: backgroundView, attribute: NSLayoutConstraint.Attribute.bottom, relatedBy: NSLayoutConstraint.Relation.equal, toItem: scrollView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: 0)) - } else if position == .bottom, let scrollView = self.scrollView { + switch position { + case .top: + backgroundView.bottomAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true + case .bottom: let constant = max(scrollView.contentSize.height, scrollView.bounds.height) - self.backgroundViewTopConstraint = NSLayoutConstraint(item: backgroundView, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: scrollView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1, constant: constant) - scrollView.addConstraint(self.backgroundViewTopConstraint!) + let backgroundViewTopConstraint = backgroundView.topAnchor.constraint( + equalTo: scrollView.bottomAnchor, constant: constant) + backgroundViewTopConstraint.isActive = true + self.backgroundViewTopConstraint = backgroundViewTopConstraint } } - fileprivate func configureHeight() { - + private func configureHeight() { if let scrollView = self.scrollView { self.originalTopInset = scrollView.contentInset.top self.originalBottomInset = scrollView.contentInset.bottom @@ -227,23 +218,23 @@ open class RMRPullToRefreshController { containerView.startLoadingAnimation(startProgress) } - @objc fileprivate func stopAllAnimations() { + @objc private func stopAllAnimations() { if shouldHideWhenStopLoading() { stopped = true } containerView.stopAllAnimations(shouldHideWhenStopLoading()) } - @objc fileprivate func forceStopAllAnimations() { + @objc private func forceStopAllAnimations() { stopped = true containerView.stopAllAnimations(true) } - @objc fileprivate func resetBackgroundViewHeightConstraint() { + @objc private func resetBackgroundViewHeightConstraint() { backgroundViewHeightConstraint?.constant = 0 } - fileprivate func scrollViewDidChangePanState(_ scrollView: UIScrollView, panState: UIGestureRecognizer.State) { + private func scrollViewDidChangePanState(_ scrollView: UIScrollView, panState: UIGestureRecognizer.State) { if panState == .ended || panState == .cancelled || panState == .failed { if state == .loading || (shouldHideWhenStopLoading() && !stopped) { @@ -274,7 +265,7 @@ open class RMRPullToRefreshController { } } - fileprivate func scrollViewDidChangeContentSize(_ scrollView: UIScrollView, contentSize: CGSize) { + private func scrollViewDidChangeContentSize(_ scrollView: UIScrollView, contentSize: CGSize) { updateContainerFrame() if position == .bottom { self.backgroundViewTopConstraint?.constant = max(scrollView.contentSize.height, scrollView.bounds.height) @@ -284,7 +275,7 @@ open class RMRPullToRefreshController { } } - fileprivate func scrollViewDidScroll(_ scrollView: UIScrollView, contentOffset: CGPoint) { + private func scrollViewDidScroll(_ scrollView: UIScrollView, contentOffset: CGPoint) { if state == .loading { if scrollView.contentOffset.y >= 0 { @@ -316,15 +307,17 @@ open class RMRPullToRefreshController { } } - fileprivate func configureBackgroundHeightConstraint(_ contentOffsetY: CGFloat, contentInset: UIEdgeInsets) { + private func configureBackgroundHeightConstraint(_ contentOffsetY: CGFloat, contentInset: UIEdgeInsets) { var constant = CGFloat(-1.0) if position == .top { constant = contentOffsetY + contentInset.top } else { constant = contentOffsetY + contentInset.bottom } - if constant > 0 && constant > backgroundViewHeightConstraint?.constant { - backgroundViewHeightConstraint?.constant = constant + if let backgroundViewHeightConstraint = backgroundViewHeightConstraint, + constant > 0, + constant > backgroundViewHeightConstraint.constant { + backgroundViewHeightConstraint.constant = constant } } @@ -393,34 +386,36 @@ open class RMRPullToRefreshController { } // MARK: - KVO - - var contentOffsetObservation: NSKeyValueObservation? - var contentSizeObservation: NSKeyValueObservation? open func subscribeOnScrollViewEvents() { - guard let scrollView = self.scrollView else { return } - - contentOffsetObservation = scrollView.observe(\.contentOffset, options: .new) { [weak self] scrollView, changes in - guard let newContentOffset = changes.newValue else { return } - self?.scrollViewDidScroll(scrollView, contentOffset: newContentOffset) + guard let scrollView = scrollView else { + return } - contentSizeObservation = scrollView.observe(\.contentSize, options: .new) { [weak self] scrollView, changes in - guard let newContentSize = changes.newValue else { return } - self?.scrollViewDidChangeContentSize(scrollView, contentSize: newContentSize) + self.contentOffsetObservation = scrollView.observe( + \.contentOffset, + options: [.new]) { [weak self] (scrollView, change) in + guard let newContentOffset = change.newValue else { return } + self?.scrollViewDidScroll(scrollView, contentOffset: newContentOffset) } - self.scrollView?.panGestureRecognizer.addTarget(self, action: #selector(onPanGesture)) - } - - @objc func onPanGesture(gesture: UIPanGestureRecognizer) { - guard let scrollView = self.scrollView else { return } - scrollViewDidChangePanState(scrollView, panState: gesture.state) + self.contentSizeObservation = scrollView.observe( + \.contentSize, + options: [.new]) { [weak self] (scrollView, change) in + guard let newContentSize = change.newValue else { return } + self?.scrollViewDidChangeContentSize(scrollView, contentSize: newContentSize) + } + + self.panStateObservation = scrollView.panGestureRecognizer.observe( + \.state, + options: [.new]) { [weak self] panGestureRecognizer, _ in + self?.scrollViewDidChangePanState(scrollView, panState: panGestureRecognizer.state) + } } open func unsubscribeFromScrollViewEvents() { - contentOffsetObservation = nil - contentSizeObservation = nil + contentOffsetObservation?.invalidate() + contentSizeObservation?.invalidate() + panStateObservation?.invalidate() } - } diff --git a/Example/Podfile b/Example/Podfile index 0b6e4a4..8e82ea7 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,8 +1,11 @@ source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' use_frameworks! -target 'RMRPullToRefreshExample' do - project 'RMRPullToRefreshExample.xcodeproj' - pod 'RMRPullToRefresh’, :path => "../" +project 'RMRPullToRefreshExample.xcodeproj' +workspace 'RMRPullToRefreshExample.xcworkspace' + +target :RMRPullToRefreshExample do + pod 'RMRPullToRefresh', :path => "../" end diff --git a/Example/Podfile.lock b/Example/Podfile.lock new file mode 100644 index 0000000..3b14f7d --- /dev/null +++ b/Example/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - RMRPullToRefresh (0.5.0) + +DEPENDENCIES: + - RMRPullToRefresh (from `../`) + +EXTERNAL SOURCES: + RMRPullToRefresh: + :path: ../ + +SPEC CHECKSUMS: + RMRPullToRefresh: 6c25f48af80d0e5d72b89ef5d6ea0dfcc21e5444 + +PODFILE CHECKSUM: 6bf08c33e827c034420f4dcfc61024a8ba7eab2f + +COCOAPODS: 1.3.1 diff --git a/Example/RMRPullToRefresh/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/RMRPullToRefresh/Assets.xcassets/AppIcon.appiconset/Contents.json index 118c98f..19882d5 100644 --- a/Example/RMRPullToRefresh/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/RMRPullToRefresh/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", @@ -29,6 +39,11 @@ "idiom" : "iphone", "size" : "60x60", "scale" : "3x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { diff --git a/Example/RMRPullToRefresh/Base.lproj/Main.storyboard b/Example/RMRPullToRefresh/Base.lproj/Main.storyboard index e3552ef..88cd43d 100644 --- a/Example/RMRPullToRefresh/Base.lproj/Main.storyboard +++ b/Example/RMRPullToRefresh/Base.lproj/Main.storyboard @@ -1,11 +1,11 @@ - + - + @@ -24,7 +24,7 @@ - + @@ -315,7 +315,7 @@ - + diff --git a/Example/RMRPullToRefresh/TableViewController.swift b/Example/RMRPullToRefresh/TableViewController.swift index 81ada46..603643a 100644 --- a/Example/RMRPullToRefresh/TableViewController.swift +++ b/Example/RMRPullToRefresh/TableViewController.swift @@ -8,9 +8,8 @@ import UIKit -class TableViewController: UITableViewController { +final class TableViewController: UITableViewController { - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { super.prepare(for: segue, sender: sender) diff --git a/Example/RMRPullToRefresh/ViewController.swift b/Example/RMRPullToRefresh/ViewController.swift index e295bef..78a9f5c 100644 --- a/Example/RMRPullToRefresh/ViewController.swift +++ b/Example/RMRPullToRefresh/ViewController.swift @@ -18,20 +18,48 @@ public enum ExampleType: Int { case redmadrobotBottom } -class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIActionSheetDelegate { +final class ViewController: UIViewController { - @IBOutlet weak var tableView: UITableView! + // MARK: - Public properties var exampleType: ExampleType = .beelineBottom - var pullToRefresh: RMRPullToRefresh? + // MARK: - Private properites - let formatter = DateFormatter() + private var pullToRefresh: RMRPullToRefresh? + private let formatter = DateFormatter() + private var items: [String] = [] + private var count = 2 + private var result = RMRPullToRefreshResultType.success - var items: [String] = [] - var count = 2 + // MARK: - IBOutlets - var result = RMRPullToRefreshResultType.success + @IBOutlet weak var tableView: UITableView! + + // MARK: - IBActions + + @IBAction func settings(_ sender: AnyObject) { + let alertController = UIAlertController(title: "Result type", message: nil, preferredStyle: .actionSheet) + + let successAction = UIAlertAction(title: "Success", style: .default) { _ in + self.result = .noUpdates + } + alertController.addAction(successAction) + + let noUpdatesAction = UIAlertAction(title: "No updates", style: .default) { _ in + self.result = .noUpdates + } + alertController.addAction(noUpdatesAction) + + let errorAction = UIAlertAction(title: "Error", style: .default) { _ in + self.result = .error + } + alertController.addAction(errorAction) + + present(alertController, animated: true, completion: nil) + } + + // MARK: - UIViewController override func viewDidLoad() { super.viewDidLoad() @@ -44,51 +72,47 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega // MARK: - Pull to Refresh - func configurePullToRefresh() { - - pullToRefresh = RMRPullToRefresh( - scrollView: tableView, - position: position()) { [weak self] in - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(5.0 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: { - if self?.result == .success { - self?.loadMore() - } - if let result = self?.result { - self?.pullToRefresh?.stopLoading(result) - } - }) + private func configurePullToRefresh() { + pullToRefresh = RMRPullToRefresh(scrollView: tableView, position: position()) { [weak self] in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5, execute: { + if self?.result == .success { + self?.loadMore() + } + if let result = self?.result { + self?.pullToRefresh?.stopLoading(result) + } + }) } - if exampleType == .perekrestokTop || exampleType == .perekrestokBottom { + switch exampleType { + case .perekrestokTop, .perekrestokBottom: perekrestok() - } else if exampleType == .beelineTop || exampleType == .beelineBottom { + case .beelineTop, .beelineBottom: beeline() - } else if exampleType == .redmadrobotTop || exampleType == .redmadrobotBottom { + case .redmadrobotTop, .redmadrobotBottom: redmadrobot() } - pullToRefresh?.setHideDelay(5.0, result: .success) - - pullToRefresh?.hideWhenError = false + //pullToRefresh?.setHideDelay(5.0, result: .success) + //pullToRefresh?.hideWhenError = false } // MARK: - Build example values - func perekrestok() { - + private func perekrestok() { if let pullToRefreshView = PerekrestokView.XIB_VIEW() { pullToRefresh?.configureView(pullToRefreshView, state: .dragging, result: .success) pullToRefresh?.configureView(pullToRefreshView, state: .loading, result: .success) } pullToRefresh?.height = 90.0 - pullToRefresh?.backgroundColor = UIColor(red: 16.0/255.0, - green: 192.0/255.0, - blue: 119.0/255.0, - alpha: 1.0) + pullToRefresh?.backgroundColor = UIColor( + red: 16.0/255.0, + green: 192.0/255.0, + blue: 119.0/255.0, + alpha: 1.0) } - func beeline() { - + private func beeline() { if let pullToRefreshView = BeelineView.XIB_VIEW() { pullToRefresh?.configureView(pullToRefreshView, state: .dragging, result: .success) pullToRefresh?.configureView(pullToRefreshView, state: .loading, result: .success) @@ -97,11 +121,11 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega pullToRefresh?.backgroundColor = UIColor.white } - func redmadrobot() { + private func redmadrobot() { pullToRefresh?.setupDefaultSettings() } - func position() -> RMRPullToRefreshPosition { + private func position() -> RMRPullToRefreshPosition { if exampleType == .perekrestokTop || exampleType == .beelineTop || exampleType == .redmadrobotTop { return .top } @@ -110,49 +134,29 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega // MARK: - Configure - func someConfiguring() { + private func someConfiguring() { formatter.dateStyle = DateFormatter.Style.long formatter.timeStyle = .medium } - // MARK: - Action - - - @IBAction func settings(_ sender: AnyObject) { - UIActionSheet(title: "Result type", delegate: self, cancelButtonTitle: nil, destructiveButtonTitle: nil, otherButtonTitles: ".Success", ".NoUpdates", ".Error").show(in: self.view) - } - - // MARK: - UIActionSheetDelegate - - func actionSheet(_ actionSheet: UIActionSheet, clickedButtonAt buttonIndex: Int) { - switch buttonIndex { - case 0: - self.result = .success - case 1: - self.result = .noUpdates - case 2: - self.result = .error - default: - break; - } - } - // MARK: - Test data - func loadData() { + private func loadData() { for _ in 0...count { items.append(formatter.string(from: Date())) } } - func loadMore() { + private func loadMore() { for _ in 0...20 { self.items.append(formatter.string(from: Date(timeIntervalSinceNow: 20))) } self.tableView.reloadData() } - - // MARK: - TableView +} + +// MARK: - UITableViewDataSource +extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) @@ -168,4 +172,3 @@ class ViewController: UIViewController, UITableViewDataSource, UITableViewDelega return 1; } } - diff --git a/Example/RMRPullToRefreshExample.xcodeproj/project.pbxproj b/Example/RMRPullToRefreshExample.xcodeproj/project.pbxproj index cc273b6..26df298 100644 --- a/Example/RMRPullToRefreshExample.xcodeproj/project.pbxproj +++ b/Example/RMRPullToRefreshExample.xcodeproj/project.pbxproj @@ -148,14 +148,13 @@ 89CB122C1C9DA07B00048E46 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1000; + LastSwiftUpdateCheck = 0720; LastUpgradeCheck = 1000; ORGANIZATIONNAME = "Merkulov Ilya"; TargetAttributes = { 89CB12331C9DA07B00048E46 = { CreatedOnToolsVersion = 7.2.1; - DevelopmentTeam = GMD7EK7S94; - LastSwiftMigration = 0800; + LastSwiftMigration = 0920; }; }; }; @@ -363,7 +362,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/RMRPullToRefresh.podspec b/RMRPullToRefresh.podspec index 6b8c56f..4d1f32e 100644 --- a/RMRPullToRefresh.podspec +++ b/RMRPullToRefresh.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "RMRPullToRefresh" - spec.version = "0.6.0" + spec.version = "0.8.0" spec.platform = :ios, "9.0" spec.license = { :type => "MIT", :file => "LICENSE" } spec.summary = "A pull to refresh control for UIScrollView (UITableView and UICollectionView)" @@ -8,5 +8,5 @@ Pod::Spec.new do |spec| spec.author = "Ilya Merkulov" spec.source = { :git => "https://git.redmadrobot.com/helper-ios/RMRPullToRefresh.git", :tag => spec.version } spec.source_files = "Classes/*.{swift}", "Classes/Default/*.{swift}" - spec.resources = ['Images/*.png'] + spec.resources = ['Images/*.png'] end \ No newline at end of file