UIRefreshControl in UIScrollView Doesn't disappear on endRefreshing - ios

I tried to use a UIRefreshControll in a UIScrollView but I had the problem that I needed to scroll a lot in order to get it to refresh. I needed to use both hands to make it work so I decided to write some scrolling logic to start the refreshing.
Now the problem is that sometimes the endRefreshing function doesn't make the UIRefreshControll view disappear and I end up having it there forever.
I have tried called endRefreshing in the main queue with a delay and it doesn't work. I have also tried setting isHidden to true and also removeFromSuperView. I've had no luck in making it work.
import UIKit
class RefreshableView : UIView {
#IBOutlet weak var scrollView: UIScrollView!
private let refreshControl = UIRefreshControl()
private var canRefresh = true
var refreshFunction: (() -> ())?
func setupUI() {
backgroundColor = ColorName.grayColor.color
scrollView.refreshControl = refreshControl
}
func endRefreshing() {
self.refreshControl.endRefreshing()
}
}
extension RefreshableView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < -100 {
if canRefresh && !self.refreshControl.isRefreshing {
self.canRefresh = false
self.refreshControl.beginRefreshing()
self.refreshFunction?()
}
} else if scrollView.contentOffset.y >= 0 {
self.canRefresh = true
}
}
}

The scroll view's refreshControl handles the pull-to-refresh feature automatically, so you don't need any of the scrollViewDidScroll() code.
Assuming your storyboard looks something like this, where:
the view with the Red background is an instance of RefreshableView class
it contains a scroll view (connected via #IBOutlet)
and some content in the scroll view (here I have just a single label)
Your code can be like this:
class RefreshableView : UIView {
#IBOutlet var scrollView: UIScrollView!
private let refreshControl = UIRefreshControl()
var refreshFunction: (() -> ())?
func setupUI() {
backgroundColor = .gray
refreshControl.addTarget(self, action: #selector(didPullToRefresh), for: .valueChanged)
scrollView.refreshControl = refreshControl
}
#objc func didPullToRefresh() {
print("calling refreshFunction in controller")
self.refreshFunction?()
}
func endRefreshing() {
print("end refreshing called from controller")
self.refreshControl.endRefreshing()
}
}
class RefreshTestViewController: UIViewController {
#IBOutlet var theRefreshableView: RefreshableView!
override func viewDidLoad() {
super.viewDidLoad()
theRefreshableView.setupUI()
theRefreshableView.refreshFunction = {
print("simulating refresh for 2 seconds...")
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
[weak self] in
guard let self = self else { return }
// do what you want to update the contents of theRefreshableView
// ...
// then tell theRefreshableView to endRefreshing
self.theRefreshableView.endRefreshing()
}
}
}
}

Related

Pull - to - refresh doesn't work with multiple Views

Im trying to implement one refresher to tableView and scrollView on one ViewController. When there are no data - hide table view and show scrollView. Is it possible to implement one refresher so I don't need to write two?
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var allScreenView: UIView!
var refreshControl = UIRefreshControl()
override func viewDidLoad() {
super.viewDidLoad()
self.scrollView.isScrollEnabled = true
self.scrollView.alwaysBounceVertical = true
refreshControl.attributedTitle = NSAttributedString(string: "Update")
refreshControl.addTarget(self, action: #selector(refresh), for: UIControl.Event.valueChanged)
// Adding pull to request to scrollview
scrollView.addSubview(refreshControl)
// Adding pull to reqest to tableView
tableView.addSubview(refreshControl)
}
#objc func refresh(sender:AnyObject) {
// End refreshing after 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.refreshControl.endRefreshing()
}
}
}
When I implement to both, it doesn't refresh

Swift 4 Refresh Control not smooth but always jump

I have a UIViewController, inside which there is a tableView. I added a RefreshControl. But when I pull, it always jumps for some times, which is not smooth and continuous at all.
I'm using Swift 4 and Xcode 10.1.
class ItemsController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextViewDelegate, ItemCellDelegate {
......
lazy var refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action:
#selector(handleRefresh(_:)),
for: UIControl.Event.valueChanged)
refreshControl.tintColor = UIColor.lightGray
return refreshControl
}()
......
override func viewDidLoad() {
// Refresh
self.tableView.refreshControl = self.refreshControl
}
......
#objc func handleRefresh(_ refreshControl: UIRefreshControl) {
loadData()
self.tableView.reloadData()
refreshControl.endRefreshing()
}
......
}
I also faced that kind of problem. You can use this approach below:
Change your handleRefresh method like this:
#objc func handleRefresh(_ refreshControl: UIRefreshControl) {
if !tableView.isDragging {
refresh() // refresh is another method for your reloading jobs
}
}
Add refresh method.
func refresh() {
self.refreshControl.endRefreshing()
self.tableView.reloadData()
}
Lastly you need to implement scrollViewDidEndDragging. If you've pulled down far enough, the refreshControl will be refreshing, so call refresh. Otherwise, you either haven't pulled down far enough, you were dragging on a different part of the table, or you dragged so quickly that the refreshControl ValueChanged event has yet to fire, and it will refresh the table there.
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if refreshControl.isRefreshing == true {
refresh()
}
}
Is loadData() asynchronous? because if so it should have a completion handler and
tableView.reloadData()
refreshControl.endRefreshing()
should be done (on the main thread) from there. Otherwise what is happening is the user pulls down and you instantly update the tableView and stop the refresh control, so thats why you have no animation.
try
#objc func handleRefresh(_ refreshControl: UIRefreshControl) {
loadData()
self.tableView.reloadData()
DispatchQueue.main.async {
//DispatchQueue.main.asyncAfter(deadline: .now + 0.2) { // or this one with
//short delay
refreshControl.endRefreshing()
}
}
I just solved this problem by doing this:
#objc func handleRefresh(_ refreshControl: UIRefreshControl) {
refreshControl.endRefreshing()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
loadData()
}
Pretty simple. Now the animation is much better.
The tableView.reloadData(), which is inside of loadData(), is getting the animation less smooth. So I make it do after the user releases from dragging.

Disable / Enable a button in Xcode

Hi i'm new with Swift programming.
What im trying to do is Disable my button (signIn) in viewDidLoad and only enable when the textfields have text in them. Here's what i've achieved so far. (not much though!)
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
signIn.isEnabled = false
// Do any additional setup after loading the view, typically from a nib.
}
#IBOutlet weak var emailtxt: UITextField!
#IBOutlet weak var passwordtxt: UITextField!
#IBOutlet weak var signIn: UIButton!
I need help to create a function in signIn that keeps button disabled until text fields (emailtxt & passwordtxt) have text in them and then proceed.
Glad if anyone can sort me.
Thanks in advance!
First add these for all of your textFields in viewDidLoad():
emailtxt.addTarget(self, action: #selector(textFieldDidChange(_:)),
for: UIControlEvents.editingChanged)
passwordtxt.addTarget(self, action: #selector(textFieldDidChange(_:)),
for: UIControlEvents.editingChanged)
Then use this:
#objc func textFieldDidChange(_ textField: UITextField) {
self.buttonIsEnabled()
}
func buttonIsEnabled() {
var buttonIsEnabled = true
defer {
self.signIn.isEnabled = buttonIsEnabled
}
guard let emailtxt = self.emailtxt.text, !emailtxt.isEmpty else {
addButtonIsEnabled = false
return
}
guard let passwordtxt = self. passwordtxt.text, ! passwordtxt.isEmpty else {
addButtonIsEnabled = false
return
}
}
I use this way in my codes and it works well.
Even you can add more methods for additional checking to buttonIsEnabled, like:
self.checkEmailIsValid(for: emailtxt)
Of course you should handle this method before:
func checkEmailIsValid(for: String) {
//...
}
Set ViewController as delegate for emailtxt and passwordtxt like this,
override func viewDidLoad() {
super.viewDidLoad()
signIn.isEnabled = false
emailtxt.delegate = self
passwordtxt.delegate = self
// Do any additional setup after loading the view, typically from a nib.
}
Conform your ViewController to UITextFieldDelegate and enable/disable as the text input is finished,
extension ViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
if emailtxt.text?.isEmpty == false && passwordtxt.text?.isEmpty == false {
signIn.isEnabled = true
} else {
signIn.isEnabled = false
}
}
}
Here is the fix for your code you shared.
import UIKit
extension UIViewController {
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
#objc func dismissKeyboard() {
view.endEditing(true)
}
}
extension SignInVC: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
if emailtxt.text?.isEmpty == false && passwordtxt.text?.isEmpty == false {
signIn.isEnabled = true
} else {
signIn.isEnabled = false
}
}
}
class SignInVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
signIn.isEnabled = false
emailtxt.delegate = self
passwordtxt.delegate = self
self.hideKeyboardWhenTappedAround()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBOutlet weak var emailtxt: UITextField!
#IBOutlet weak var passwordtxt: UITextField!
#IBOutlet weak var signIn: UIButton!
}
What I would do is create an IBAction from one of your text fields, and set the event to Editing Changed:
The code should look like this:
#IBAction func textFieldEditingDidChange(_ sender: UITextField) {
}
You can then connect that same outlet to both of your text fields by dragging from the outlet to your remaining field. If you've connected both correctly, clicking on the circle to the left of your IBAction should show two text fields:
The action will now be fired every time text changes in either of your fields.
Then, at the top of the file, I'd create a computed property that returns false unless there is something in both fields:
var shouldEnableButton: Bool {
guard let text1 = textField1.text, let text2 = textField2.text else {
return false
}
return text1.isEmpty && text2.isEmpty ? false : true
}
Finally, we add shouldEnableButton to our IBAction:
#IBAction func textFieldEditingDidChange(_ sender: UITextField) {
button.isEnabled = shouldEnableButton
}
Important
When you connect your second text field to the outlet, it will incorrectly assign Editing Did End as its event:
Delete this event and click and drag from Editing Changed to your IBAction:
Use SwiftValidator
https://github.com/SwiftValidatorCommunity/SwiftValidator
by this, you will set validation of email & password like below
import SwiftValidator
let validator = Validator()
validator.registerField(emailTextField, errorLabel: emailErrorLabel, rules: [RequiredRule(), EmailRule(message: "Invalid email")])
// MARK: - ValidationDelegate
extension ViewController: ValidationDelegate {
func validationSuccessful() {
self.loginUser()
}
func validationFailed(_ errors:[(Validatable ,ValidationError)]) {
for (field, error) in errors {
//Handle as per need - show extra label - shake view etc
/*
if let field = field as? UITextField {
Utilities.shakeTheView(shakeView: field)
}
error.errorLabel?.text = error.errorMessage
error.errorLabel?.isHidden = false
*/
}
}
}

UITapGestureRecognizer in UICollectionViewCell for UIImageView does not work

I'm trying to put a UITapGestureRecognizer on UIImageView in UICollectionViewCell but it does not work. Here is the code:
class ImageGalleryCell : UICollectionViewCell {
#IBOutlet weak var imgPhoto: UIImageView!
let uniqueTag = String.random()
func setup(){
let rec = UITapGestureRecognizer.init(target: self, action: #selector(tap))
imgPhoto.addGestureRecognizer(rec)
}
#objc func tap(){
print("tap")
}
}
What am I doing wrong?
Based on the code snippet you provide, it seems that you should manually set the image view isUserInteractionEnabled property to true -since it is false by default-, in setup:
func setup(){
let rec = UITapGestureRecognizer.init(target: self, action: #selector(tap))
// here we go:
imgPhoto.isUserInteractionEnabled = true
imgPhoto.addGestureRecognizer(rec)
}
Also, I would suggest to call setup method in the cell class itself, in the awakeFromNib() instead of calling it in the view controller - tableView(_:cellForRowAt:) (or any method in the view controller layer), simply like this:
class ImageGalleryCell : UICollectionViewCell {
#IBOutlet weak var imgPhoto: UIImageView!
//let uniqueTag = String.random()
func setup(){
let rec = UITapGestureRecognizer.init(target: self, action: #selector(tap))
// here we go:
imgPhoto.isUserInteractionEnabled = true
imgPhoto.addGestureRecognizer(rec)
}
#objc func tap(){
print("tap")
}
override func awakeFromNib() {
super.awakeFromNib()
setup()
}
}
That's because such a behavior should be setup only for one time when delivering the cell but not each time it has been shown.

Subview of UIScrollView don't resize (manually)

I have a UIScrollView with a UIView inside of it. When I try to manually resize the UIView by code, it doesn't work. I tried to set the autoResizingMask property of the UIView to .None and I implemented the touchesShouldCancelInContentView method of the UIScrollView to return true but it still doesn't work. Here's my code :
class ViewController: UIViewController {
#IBOutlet var scrollView: CustomScrollView!
#IBOutlet var field: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
var gesture = UITapGestureRecognizer(target: self, action: "gestureHandler")
gesture.numberOfTapsRequired = 1
view.addGestureRecognizer(gesture)
scrollView.contentSize = CGSizeMake(430, 140)
field.autoresizingMask = .None
}
func gestureHandler() {
field.frame.size.width += 20
var view = UIView(frame: CGRectMake(0, 0, 30, 100))
view.backgroundColor = UIColor.redColor()
field.frame.size.width -= 30
field.frame.origin.x += 30
scrollView.addSubview(view)
}
}
class CustomScrollView: UIScrollView {
override func touchesShouldCancelInContentView(view: UIView!) -> Bool {
return true
}
}
What I get :
What I want :
Thanks for your help !

Resources