Skip to content

Commit

Permalink
Replace RefreshControl with native (#1352)
Browse files Browse the repository at this point in the history
* Replace RefreshControl with native

* Replace custom loaders

---------

Co-authored-by: Andrii Momot <[email protected]>
  • Loading branch information
AndreyMomot and Andrii Momot authored Jun 21, 2024
1 parent 4f3bc8b commit 3a6b475
Show file tree
Hide file tree
Showing 8 changed files with 50 additions and 160 deletions.
22 changes: 9 additions & 13 deletions Demo/Sources/Components/RefreshControl/RefreshControlDemo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class RefreshControlDemoView: UIView, Demoable {
}()

private lazy var refreshControl: UIRefreshControl = {
let refreshControl = RefreshControl(frame: .zero)
refreshControl.delegate = self
let refreshControl = UIRefreshControl(frame: .zero)
refreshControl.addTarget(self, action: #selector(handleRefreshBegan), for: .valueChanged)
return refreshControl
}()

Expand Down Expand Up @@ -47,6 +47,13 @@ class RefreshControlDemoView: UIView, Demoable {
tableView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}

@objc private func handleRefreshBegan() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) { [weak self] in
self?.refreshControl.endRefreshing()
self?.tableView.reloadData()
}
}
}

// MARK: - UITableViewDataSource
Expand All @@ -66,14 +73,3 @@ extension RefreshControlDemoView: UITableViewDataSource {
return cell
}
}

// MARK: - RefreshControlDelegate

extension RefreshControlDemoView: RefreshControlDelegate {
func refreshControlDidBeginRefreshing(_ refreshControl: RefreshControl) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) { [weak self] in
self?.refreshControl.endRefreshing()
self?.tableView.reloadData()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,62 +1,15 @@
import UIKit

/// Branded replacement for UIActivityIndicatorView with possibility to use delayed start.
public class LoadingIndicatorView: UIView {
/// Currently not in use. Replaced with Native UIActivityIndicatorView. Still here in case we decide to go back
public class LoadingIndicatorView: UIActivityIndicatorView {
public enum State {
case delayedStart
case started
case stopped
}
private let backgroundLayer = CAShapeLayer()
private let animatedLayer = CAShapeLayer()
private let duration: CGFloat = 2.5
private let lineWidth: CGFloat = 4
private let startAngle: CGFloat = 3 * .pi / 2

private var endAngle: CGFloat {
return startAngle + 2 * .pi
}

public private(set) var state = State.stopped

public var progress: CGFloat {
get { return animatedLayer.strokeEnd }
set {
CATransaction.begin()
CATransaction.setDisableActions(true)
animatedLayer.strokeEnd = newValue
backgroundLayer.opacity = Float(newValue)
CATransaction.commit()
}
}

public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}

public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}

public override func layoutSubviews() {
super.layoutSubviews()

let center = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
let radius = min(bounds.size.width, bounds.size.height) / 2.0 - animatedLayer.lineWidth / 2.0

let bezierPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)

backgroundLayer.path = bezierPath.cgPath
animatedLayer.path = bezierPath.cgPath
backgroundLayer.frame = bounds
animatedLayer.frame = bounds

layer.addSublayer(backgroundLayer)
layer.addSublayer(animatedLayer)
}

/// Starts the animation of the loading indicator after a short delay, unless it has already been stopped.
/// - Parameters:
/// - after: Seconds to wait until starting animation (approximately)
Expand All @@ -70,91 +23,42 @@ public class LoadingIndicatorView: UIView {
}
}

/// Starts the animation of the loading indicator.
public func startAnimating() {
animateGroup()
isHidden = false
state = .started
}

/// Stops the animation of the loading indicator.
public func stopAnimating() {
backgroundLayer.removeAllAnimations()
animatedLayer.removeAllAnimations()
isHidden = true
state = .stopped
public init() {
super.init(frame: .zero)
setup()
}
}

extension LoadingIndicatorView {
private func setup() {
backgroundColor = .clear

backgroundLayer.fillColor = UIColor.clear.cgColor
backgroundLayer.strokeColor = UIColor.loadingIndicatorBackground.cgColor
backgroundLayer.strokeStart = 0
backgroundLayer.strokeEnd = 1
backgroundLayer.lineWidth = lineWidth
backgroundLayer.lineCap = .round

animatedLayer.fillColor = UIColor.clear.cgColor
animatedLayer.strokeColor = UIColor.loadingIndicator.cgColor
animatedLayer.strokeStart = 0
animatedLayer.strokeEnd = 1
animatedLayer.lineWidth = lineWidth
animatedLayer.lineCap = .round

isHidden = true
public override init(style: UIActivityIndicatorView.Style = .large) {
super.init(frame: .zero)
setup(style: style)
}

private func animateStrokeEnd() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.beginTime = 0
animation.duration = CFTimeInterval(duration / 2.0)
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

return animation
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}

private func animateStrokeStart() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "strokeStart")
animation.beginTime = CFTimeInterval(duration / 2.0)
animation.duration = CFTimeInterval(duration / 2.0)
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)

return animation
public required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}

private func animateRotation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0
animation.toValue = .pi * 2.0
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.repeatCount = .infinity

return animation
/// Starts the animation of the loading indicator.
public override func startAnimating() {
super.startAnimating()
isHidden = false
state = .started
}

private func animateGroup() {
let animationGroup = CAAnimationGroup()
animationGroup.animations = [animateStrokeEnd(), animateStrokeStart(), animateRotation()]
animationGroup.duration = CFTimeInterval(duration)
animationGroup.fillMode = .both
animationGroup.isRemovedOnCompletion = false
animationGroup.repeatCount = .infinity

animatedLayer.add(animationGroup, forKey: "loading")
/// Stops the animation of the loading indicator.
public override func stopAnimating() {
super.stopAnimating()
isHidden = true
state = .stopped
}
}

// MARK: - Private extensions

private extension UIColor {
static var loadingIndicatorBackground: UIColor {
return UIColor(r: 221, g: 232, b: 250)
private func setup(style: UIActivityIndicatorView.Style = .large) {
self.style = style
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ public class RefreshControl: UIRefreshControl {
private func handleLoadingProgress() {
let pullDistance = superview.flatMap({ $0.bounds.height / 5 }) ?? defaultPullDistance
let progress = min(max(topOffset, 0.0), pullDistance) / pullDistance
loadingIndicatorView.progress = progress

if topOffset >= pullDistance {
beginRefreshing()
Expand All @@ -105,7 +104,6 @@ public class RefreshControl: UIRefreshControl {

private func startAnimatingLoadingIndicator() {
isAnimating = true
loadingIndicatorView.progress = 1
loadingIndicatorView.startAnimating()
delegate?.refreshControlDidBeginRefreshing(self)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public struct LoadingSwiftUIView: View {
}
}
} else {
SwiftUILoadingIndicator()
ProgressView()
.scaleEffect(loadingIndicatorScale)
.onAppear {
loadingIndicatorScale = initialScale
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ import UIKit
private var loadingIndicatorCenterY: NSLayoutConstraint?

private lazy var loadingIndicator: LoadingIndicatorView = {
let view = LoadingIndicatorView()
view.translatesAutoresizingMaskIntoConstraints = false
let view = LoadingIndicatorView(withAutoLayout: true)
view.transform = loadingIndicatorInitialTransform
return view
}()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ public class AdRecommendationsGridView: UIView {
}()

private lazy var refreshControl: UIRefreshControl = {
let refreshControl = RefreshControl(frame: .zero)
refreshControl.delegate = self
let refreshControl = UIRefreshControl(frame: .zero)
refreshControl.addTarget(self, action: #selector(handleRefreshBegan), for: .valueChanged)
return refreshControl
}()

Expand Down Expand Up @@ -116,6 +116,10 @@ public class AdRecommendationsGridView: UIView {
collectionView.collectionViewLayout.invalidateLayout()
}

@objc private func handleRefreshBegan() {
delegate?.adRecommendationsGridViewDidStartRefreshing(self)
}

// MARK: - Public methods

public func reloadData() {
Expand Down Expand Up @@ -252,11 +256,3 @@ extension AdRecommendationsGridView: AdRecommendationsGridViewLayoutDelegate {
return dataSource?.adRecommendationsGridView(self, heightForItemWithWidth: width, at: indexPath) ?? 0
}
}

// MARK: - RefreshControlDelegate

extension AdRecommendationsGridView: RefreshControlDelegate {
public func refreshControlDidBeginRefreshing(_ refreshControl: RefreshControl) {
delegate?.adRecommendationsGridViewDidStartRefreshing(self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ public class FavoriteFoldersListView: UIView {
}()

private lazy var refreshControl: UIRefreshControl = {
let refreshControl = RefreshControl(frame: .zero)
refreshControl.delegate = self
let refreshControl = UIRefreshControl(frame: .zero)
refreshControl.addTarget(self, action: #selector(handleRefreshBegan), for: .valueChanged)
return refreshControl
}()

Expand Down Expand Up @@ -366,6 +366,10 @@ public class FavoriteFoldersListView: UIView {
@objc private func handleXmasButtonTap() {
delegate?.favoriteFoldersListViewDidSelectXmasButton(self)
}

@objc private func handleRefreshBegan() {
delegate?.favoriteFoldersListViewDidBeginRefreshing(self)
}
}

// MARK: - UITableViewDataSource
Expand Down Expand Up @@ -460,14 +464,6 @@ extension FavoriteFoldersListView: UITableViewDelegate {
}
}

// MARK: - RefreshControlDelegate

extension FavoriteFoldersListView: RefreshControlDelegate {
public func refreshControlDidBeginRefreshing(_ refreshControl: RefreshControl) {
delegate?.favoriteFoldersListViewDidBeginRefreshing(self)
}
}

// MARK: - FavoriteFoldersFooterViewDelegate

extension FavoriteFoldersListView: FavoriteFoldersFooterViewDelegate {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwiftUI

/// Currently not in use. Replaced with Native ProgressView. Still here in case we decide to go back
public struct SwiftUILoadingIndicator: View {
private struct ProgressCircle: Shape {
var startAngle: Angle
Expand Down

0 comments on commit 3a6b475

Please sign in to comment.