Swift - Use button to manually scroll UITextView - ios

I want to use a button to scroll down or up in UITextView,
but I ran into a problem.
Using textView.setContentOffset(CGPoint(x: 0, y: MyTextView.contentOffset.y + 100), animated: true) can make my textview scroll down, but it still scrolls when I click the button in the end of text...
like this..
e
f
g
===================
But I want is this..
a
b
c
d
e
f
g
===================
My code is
#IBAction func down(_ sender: UIButton) {
MyTextView.setContentOffset(CGPoint(x: 0, y: MyTextView.contentOffset.y - 100), animated: true)
}
#IBAction func up(_ sender: UIButton) {
MyTextView.setContentOffset(CGPoint(x: 0, y: MyTextView.contentOffset.y - 100), animated: true)
}
please help me!!!

Try this one
#IBAction func down(_ sender: UIButton) {
if (textView.contentSize.height>(textView.frame.size.height+textView.contentOffset.y+100)){
textView.setContentOffset(CGPoint(x:0,y:textView.contentOffset.y + 100), animated: true);
}
else{
textView.setContentOffset(CGPoint(x:0,y:(textView.contentSize.height - textView.frame.size.height)), animated: true)
}
}

I have tested and it works. Check this out
#IBOutlet weak var textView: UITextView!
#IBAction func up(_ sender: UIButton) {
if textView.contentOffset.y < 100{
textView.setContentOffset(CGPoint.zero, animated: true)
}else{
textView.setContentOffset(CGPoint(x: 0, y: textView.contentOffset.y-100), animated: true)
}
}
#IBAction func down(_ sender: UIButton) {
if textView.contentOffset.y > textView.contentSize.width - textView.frame.size.height{
textView.setContentOffset(CGPoint(x: 0, y: textView.contentSize.height-textView.frame.size.height), animated: true)
}else{
textView.setContentOffset(CGPoint(x: 0, y: textView.contentOffset.y+100), animated: true)
}
}

You probably want to check what the current offset is and only move the amount you need to. For example for scrolling down, try something like below. Then you should be able to adjust it and do the opposite for scrolling up.
struct Constants {
static let preferredScrollAmount: CGFloat = 100.0
}
#IBAction func downButtonTapped(_ sender: UIButton) {
// Get the current offset, content height, text view height and work out the scrollable distance for the text view
let currentOffset: CGFloat = textView.contentOffset.y
let contentHeight: CGFloat = textView.contentSize.height
let textViewHeight: CGFloat = textView.frame.size.height
let scrollableDistance = contentHeight - textViewHeight
// Check that the current offset isn't beyond the scrollable area otherwise return (no need to scroll)
guard currentOffset < scrollableDistance else { return }
// Work out how far we can move
let distanceWeCanMove = scrollableDistance - currentOffset
// Get the distance we should move (the smaller value so it doesn't go past the end)
let distanceToScroll = min(distanceWeCanMove, Constants.preferredScrollAmount)
// Do the scrolling
textView.setContentOffset(CGPoint(x: 0, y: currentOffset + distanceToScroll), animated: true)
}

Related

how to hide/view header view table in iOS swift

I have static UItableview to hold user form. I added header view in the table to show email validation files my problem is the header does not show a smooth transition between hiding/show and overlap with the first row
I want to ask how I can fix the hight of the table header view and does not make it overlap
code
#IBOutlet weak var errorView: UIView!
#IBAction func next(_ sender: Any) {
let newUserEmail = self.txtfEmail.text
if isValidEmail(newUserEmail!) {
performSegue(withIdentifier: "addInventryToNewUser", sender: self)
}
else {
cellemail.layer.borderWidth = 2.0
cellemail.layer.borderColor = UIColor.red.cgColor
let f = errorView.frame;
errorView.frame = CGRect(x: f.origin.x, y: f.origin.y, width: f.width, height: 21);
errorView.isHidden = false
lblError.text = "❌ Invalid email address."
}
}
You need to apply some animation for a smooth transition. Here's how:
#IBAction func next(_ sender: Any) {
let newUserEmail = self.txtfEmail.text
if isValidEmail(newUserEmail!) {
performSegue(withIdentifier: "addInventryToNewUser", sender: self)
}
else {
cellemail.layer.borderWidth = 2.0
cellemail.layer.borderColor = UIColor.red.cgColor
UIView.animate(withDuration: 0.5) {
self.errorView.frame.size.height = 21
}
errorView.isHidden = false
lblError.text = "❌ Invalid email address."
}
}
I have no enough reputation to add a comment, but the answer on "how to hide it after 5 seconds?" Is:
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
// your code to hide view here
}
I will combine the two answer:
#IBAction func next(_ sender: Any) {
let newUserEmail = self.txtfEmail.text
if isValidEmail(newUserEmail!) {
performSegue(withIdentifier: "addInventryToNewUser", sender: self)
}
else {
cellemail.layer.borderWidth = 2.0
cellemail.layer.borderColor = UIColor.red.cgColor
UIView.animate(withDuration: 0.5) {
self.errorView.frame.size.height = 21
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
// your code to hide view here
}
errorView.isHidden = false
lblError.text = "❌ Invalid email address."
}
}

Interacting with a ViewController that has Multiple child ViewControllers

I have a parent viewController and a child viewController. The childViewController is like a card and functions similar to Apple's stock or map app. I can expand or collapse it and interact with the buttons within it. The only problem is that I can't interact with any buttons within the parent viewController. Anyone know what's the problem.
class BaseViewController: UIViewController {
enum CardState {
case expanded
case collapsed
}
var cardViewController: CardViewController!
var visualEffectView: UIVisualEffectView!
lazy var deviceCardHeight: CGFloat = view.frame.height - (view.frame.height / 6)
lazy var cardHeight: CGFloat = deviceCardHeight
let cardHandleAreaHeight: CGFloat = 65
var cardVisible = false
var nextState: CardState {
return cardVisible ? .collapsed : .expanded
}
override func viewDidLoad() {
super.viewDidLoad()
setupCard()
}
#IBAction func expandCard(_ sender: Any) {
print("Button Pressed")
}
func setupCard() {
cardViewController = CardViewController(nibName: "CardViewController", bundle: nil)
self.addChild(cardViewController)
self.view.addSubview(cardViewController.view)
//Set up frame of cardView
cardViewController.view.frame = CGRect(x: 0, y: view.frame.height - cardHandleAreaHeight, width: view.frame.width, height: cardHeight)
cardViewController.view.clipsToBounds = true
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCardTap(recognizer:)))
cardViewController.handleArea.addGestureRecognizer(tapGestureRecognizer)
}
#objc func handleCardTap(recognizer: UITapGestureRecognizer) {
switch recognizer.state {
case .ended:
animationTransitionIfNeeded(state: nextState, duration: 0.9)
default:
break
}
}
func animationTransitionIfNeeded(state: CardState, duration: TimeInterval) {
if runningAnimations.isEmpty {
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1) {
switch state {
case .expanded:
self.cardViewController.view.frame.origin.y = self.view.frame.height - self.cardHeight
case .collapsed:
self.cardViewController.view.frame.origin.y = self.view.frame.height - self.cardHandleAreaHeight
}
}
frameAnimator.addCompletion { _ in
//if true set to false, if false set to true
self.cardVisible = !self.cardVisible
self.runningAnimations.removeAll()
}
frameAnimator.startAnimation()
runningAnimations.append(frameAnimator)
let cornerRadiusAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear) {
switch state {
case .expanded:
self.cardViewController.view.layer.cornerRadius = 12
case .collapsed:
self.cardViewController.view.layer.cornerRadius = 0
}
}
cornerRadiusAnimator.startAnimation()
}
}
'Can't interact' should mean that you can't press. If that is the case the most likely cause is that the button is covered with something (it could be transparent so ensure when testing that there is no transparent backgrounds until you resolve this). The other possible reason would be that you have set some property of the button that would cause this behavior, but you would probably know that so it is almost certainly the former.
I solve it. This code should work perfectly. The reason why it didn't before was because I added in a visualEffectView. It was covering the parent.

Label text not updating after scroll event

I'm trying to update text of a label after a scroll event. I have a print command that prints the correct value but the label is not updating.
Here's my code
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let x = scrollView.contentOffset.x
let w = scrollView.bounds.size.width
let p = Int(x/w)
print("page \(p)") // this prints correct value
self.signalLabel.text = signalText[Int(x/w)] // this does not update
}
what's the deal?
Here's the complete view controller code. This view is called from a button click on the initial view controller. This view contains a UIScrollView and UIPageControl. The UIScrollView contains two images that can be scrolled back and forth. I want to update the label text based on image that is shown.
import UIKit
class SignalOneViewController: UIViewController, UIScrollViewDelegate {
// MARK: Properties
#IBOutlet weak var signalScrollView: UIScrollView!
#IBOutlet weak var signalPageControl: UIPageControl!
#IBOutlet weak var signalLabel: UILabel!
// MARK: - Button Actions
#IBAction func signalOneButton(_ sender: Any) {
print("signal one button clicked")
performSegue(withIdentifier: "SignalOneSegue", sender: self)
}
#IBAction func onCancelButton(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
let signalImages = ["signal1a.png", "signal1b.png"]
let signalText = ["Ready for play", "Untimed down"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidLayoutSubviews() {
self.loadScrollView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func loadScrollView() {
let pageCount : CGFloat = CGFloat(signalImages.count)
signalLabel.text = signalText[0]
signalScrollView.backgroundColor = UIColor.clear
signalScrollView.delegate = self
signalScrollView.isPagingEnabled = true
signalScrollView.contentSize = CGSize(width: signalScrollView.frame.size.width * pageCount, height: signalScrollView.frame.size.height)
signalScrollView.showsHorizontalScrollIndicator = false
signalScrollView.showsVerticalScrollIndicator = false
signalPageControl.numberOfPages = Int(pageCount)
signalPageControl.pageIndicatorTintColor = UIColor.lightGray
signalPageControl.currentPageIndicatorTintColor = UIColor.blue
signalPageControl.addTarget(self, action: #selector(self.pageChanged), for: .valueChanged)
for i in 0..<Int(pageCount) {
print(self.signalScrollView.frame.size.width)
let image = UIImageView(frame: CGRect(x: self.signalScrollView.frame.size.width * CGFloat(i), y: 0, width: self.signalScrollView.frame.size.width, height: self.signalScrollView.frame.size.height))
image.image = UIImage(named: signalImages[i])!
image.contentMode = UIViewContentMode.scaleAspectFit
self.signalScrollView.addSubview(image)
}
}
//MARK: UIScrollView Delegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewWidth: CGFloat = scrollView.frame.size.width
// content offset - tells by how much the scroll view has scrolled.
let pageNumber = floor((scrollView.contentOffset.x - viewWidth / 50) / viewWidth) + 1
signalPageControl.currentPage = Int(pageNumber)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let x = scrollView.contentOffset.x
let w = scrollView.bounds.size.width
let p = Int(x/w)
print("page \(p)")
self.signalLabel.text = signalText[p]
print(">>> \(signalText[Int(x/w)])")
}
//MARK: page tag action
#objc func pageChanged() {
let pageNumber = signalPageControl.currentPage
var frame = signalScrollView.frame
frame.origin.x = frame.size.width * CGFloat(pageNumber)
frame.origin.y = 0
signalScrollView.scrollRectToVisible(frame, animated: true)
}
}
Make sure signalLabe IBOutlet is attached to your label in storyboard or xib

UIView disappears after user interaction

whenever I click a textfield inside the view, then click the other text field, the view disappears. Strange... Can anyone help?
I animate the view using facebook pop. Here is my animation engine code:
import UIKit
import pop
class AnimationEngine {
class var offScreenRightPosition: CGPoint {
return CGPoint(x: UIScreen.main.bounds.width + 250,y: UIScreen.main.bounds.midY - 75)
}
class var offScreenLeftPosition: CGPoint{
return CGPoint(x: -UIScreen.main.bounds.width,y: UIScreen.main.bounds.midY - 75)
}
class var offScreenTopPosition: CGPoint{
return CGPoint(x: UIScreen.main.bounds.midX,y: -UIScreen.main.bounds.midY)
}
class var screenCenterPosition: CGPoint {
return CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY - 75)
}
let ANIM_DELAY : Int = 1
var originalConstants = [CGFloat]()
var constraints: [NSLayoutConstraint]!
init(constraints: [NSLayoutConstraint]) {
for con in constraints {
originalConstants.append(con.constant)
con.constant = AnimationEngine.offScreenRightPosition.x
}
self.constraints = constraints
}
func animateOnScreen(_ delay: Int) {
let time = DispatchTime.now() + Double(Int64(Double(delay) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: time) {
var index = 0
repeat {
let moveAnim = POPSpringAnimation(propertyNamed: kPOPLayoutConstraintConstant)
moveAnim?.toValue = self.originalConstants[index]
moveAnim?.springBounciness = 8
moveAnim?.springSpeed = 8
if (index < 0) {
moveAnim?.dynamicsFriction += 10 + CGFloat(index)
}
let con = self.constraints[index]
con.pop_add(moveAnim, forKey: "moveOnScreen")
index += 1
} while (index < self.constraints.count)
}
}
class func animateToPosisition(_ view: UIView, position: CGPoint, completion: ((POPAnimation?, Bool) -> Void)!) {
let moveAnim = POPSpringAnimation(propertyNamed: kPOPLayerPosition)
moveAnim?.toValue = NSValue(cgPoint: position)
moveAnim?.springBounciness = 8
moveAnim?.springSpeed = 8
moveAnim?.completionBlock = completion
view.pop_add(moveAnim, forKey: "moveToPosition")
}
}
Then here is my viewcontroller code where the view is inside in:
import UIKit
import pop
class LoginVC: UIViewController, UITextFieldDelegate {
override var prefersStatusBarHidden: Bool {
return true
}
#IBOutlet weak var emailLoginVCViewConstraint: NSLayoutConstraint!
#IBOutlet weak var emailLoginVCView: MaterialView!
#IBOutlet weak var emailAddressTextField: TextFieldExtension!
#IBOutlet weak var passwordTextField: TextFieldExtension!
var animEngine : AnimationEngine!
override func viewDidAppear(_ animated: Bool) {
self.emailLoginVCView.isUserInteractionEnabled = true
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.view.bringSubview(toFront: emailAddressTextField)
self.animEngine = AnimationEngine(constraints: [emailLoginVCViewConstraint])
self.emailAddressTextField.delegate = self
self.passwordTextField.delegate = self
emailAddressTextField.allowsEditingTextAttributes = false
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if (textField === emailAddressTextField) {
passwordTextField.becomeFirstResponder()
} else if (textField === passwordTextField) {
passwordTextField.resignFirstResponder()
} else {
// etc
}
return true
}
#IBAction func emailTapped(_ sender: AnyObject) {
AnimationEngine.animateToPosisition(emailLoginVCView, position: AnimationEngine.screenCenterPosition, completion: { (POPAnimation, Bool)
in
})
}
#IBAction func exitTapped(_ sender: AnyObject) {
AnimationEngine.animateToPosisition(emailLoginVCView, position: AnimationEngine.offScreenRightPosition, completion: { (POPAnimation, Bool)
in
})
}
}
Last here is my hierchy and options: (my view's name is emailLoginVCView). Also when I was debugging when I clicked another textfield I set a breakpoint so I got this info: enter image description here
I have a constraint that binds the center of the login view with the center of the main screen
when I create the AnimationEngine,I pass it that constraint, and it sets its constant to be the offScreenRightPosition.x
when I bring up the email login sheet, I'm not changing the constant of the constraint; I'm just changing the position of the view
which means that autolayout thinks it’s supposed to still be offscreen
when the second textfield becomes active, that’s somehow triggering auto-layout to re-evaluate the constraints, and it sees that the login view’s position doesn’t match what the constraint says it should be so....
Autolayout moves it offscreen
So if I add this in emailTapped(_:), the problem goes away :)
#IBAction func emailTapped(_ sender: AnyObject) {
AnimationEngine.animateToPosisition(emailLoginVCView, position: AnimationEngine.screenCenterPosition, completion: { (POPAnimation, Bool)
in
self.emailLoginVCViewConstraint.constant = 0
})
}

How to programmatically scroll through a collection view?

I have a collection view in my view. In the cells are image views and it has one section.
I now want to scroll programmatically through the images. I want the animation to happen endless so it starts with item one after reaching item 10.
It also would be helpful if you provide some method where to put animations like the cells getting bigger after getting programmatically swiped in from right.
There are two methods that could help you achieve this:
func scrollToItemAtIndexPath(indexPath: NSIndexPath,
atScrollPosition scrollPosition: UICollectionViewScrollPosition,
animated animated: Bool)
or
func setContentOffset(contentOffset: CGPoint,
animated animated: Bool)
Both are on UICollectionView so you can use whatever seems more convenient. To create a custom animation for this however is more difficult. A quick solution depending on what you need could be this answer.
Swift 4, iOS 11:
// UICollectionView method
func scrollToItem(at indexPath: IndexPath,
at scrollPosition: UICollectionViewScrollPosition,
animated: Bool)
// UIScrollView method
func setContentOffset(_ contentOffset: CGPoint, animated: Bool)
Swift 5
Based on Mr.Bean's answer, here is an elegant way using UICollectionView extension:
extension UICollectionView {
func scrollToNextItem() {
let contentOffset = CGFloat(floor(self.contentOffset.x + self.bounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func scrollToPreviousItem() {
let contentOffset = CGFloat(floor(self.contentOffset.x - self.bounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func moveToFrame(contentOffset : CGFloat) {
self.setContentOffset(CGPoint(x: contentOffset, y: self.contentOffset.y), animated: true)
}
}
Now you can use it wherever you want:
collectionView.scrollToNextItem()
collectionView.scrollToPreviousItem()
By far this is the best approach i have encountered, the trick is to scroll to the frame which is containing next objects. Please refer the code
/* -------------- display previous friends action ----------------*/
#IBAction func actionPreviousFriends(_ sender: Any) {
let collectionBounds = self.collectionView.bounds
let contentOffset = CGFloat(floor(self.collectionView.contentOffset.x - collectionBounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
/* -------------- display next friends action ----------------*/
#IBAction func actionNextFriends(_ sender: Any) {
let collectionBounds = self.collectionView.bounds
let contentOffset = CGFloat(floor(self.collectionView.contentOffset.x + collectionBounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func moveToFrame(contentOffset : CGFloat) {
let frame: CGRect = CGRect(x : contentOffset ,y : self.collectionView.contentOffset.y ,width : self.collectionView.frame.width,height : self.collectionView.frame.height)
self.collectionView.scrollRectToVisible(frame, animated: true)
}
You can use this UICollectionView extension (Swift 4.2)
extension UICollectionView {
func scrollToNextItem() {
let scrollOffset = CGFloat(floor(self.contentOffset.x + self.bounds.size.width))
self.scrollToFrame(scrollOffset: scrollOffset)
}
func scrollToPreviousItem() {
let scrollOffset = CGFloat(floor(self.contentOffset.x - self.bounds.size.width))
self.scrollToFrame(scrollOffset: scrollOffset)
}
func scrollToFrame(scrollOffset : CGFloat) {
guard scrollOffset <= self.contentSize.width - self.bounds.size.width else { return }
guard scrollOffset >= 0 else { return }
self.setContentOffset(CGPoint(x: scrollOffset, y: self.contentOffset.y), animated: true)
}
}
And the usage will be like
yourCollectionView.scrollToNextItem()
yourCollectionView.scrollToPreviousItem()

Resources