Custom Tab Bar Menu with horizontal swipe - swift

Just a quick question regarding implementation.
I am trying to recreate this:
https://i.imgur.com/lFXKFXl.mp4
I am wondering if I have to do it from scratch using a UICollectionView, there's a built-in Xcode method, or there is a library online somewhere.
Note: I am not trying to create a tab bar menu at the bottom, but rather at the top underneath the navigation controller navbar with horizontal swipe features. Instagram also has this underneath the search tab.
Thank you!

You can animate your blue bar in following way
func scrollViewDidScroll(_ scrollView: UIScrollView) {
blueBarViewLeadingConstraint.constant = scrollView.contentOffset.x / (scrollView.bounds.width / (TabBarView.frame.size.width / numberOfTab))
}

You can use a UIScrollView and add child viewcontrollers to it. I made an extension to UIViewController that easily allows you to create a swipe view.
extension UIViewController {
public func addViewControllerToContainer(viewController: UIViewController, count: CGFloat = 0, container: UIScrollView, heightDecrease: CGFloat = 0, startIndex: CGFloat = 0) {
let multiplier: CGFloat = count
addChildViewController(viewController)
container.addSubview(viewController.view)
container.contentSize.width += viewController.view.frame.width
container.contentSize.height = viewController.view.frame.height - heightDecrease
container.frame = CGRect(x: viewController.view.frame.origin.x, y: viewController.view.frame.origin.y, width: viewController.view.frame.width, height: viewController.view.frame.height - heightDecrease)
container.frame.origin.y += heightDecrease
container.contentOffset.x = container.frame.width * startIndex
viewController.view.frame.size.width = container.frame.width
viewController.view.frame.size.height = container.frame.height
viewController.view.frame.size.height -= heightDecrease
viewController.view.frame.origin.x = container.frame.width * multiplier
}
public func addViewControllersToContainer(viewControllers: [UIViewController], container: UIScrollView, heightDecrease: CGFloat = 0, startIndex: CGFloat) {
var count: CGFloat = 0
for viewController in viewControllers {
addViewControllerToContainer(viewController: viewController, count: count, container: container, heightDecrease: heightDecrease, startIndex: startIndex)
count += 1
}
}
}
class MainSwipeViewController: UIViewController {
let viewControllers: [UIViewController] = [VC1, VC2, VC3] // view controllers swiped between
let startIndex: CGFloat = 0 // which viewController to begin at (0 means first)
let mainScrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = .lightGray
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.bounces = false
scrollView.contentInsetAdjustmentBehavior = .never
scrollView.backgroundColor = UIColor.clear
return scrollView
}()
private func addViewControllers(_ vc: UIViewController, viewControllers: [UIViewController], startIndex: CGFloat = 0) {
view.addSubview(mainScrollView)
addViewControllersToContainer(viewControllers: viewControllers, container: mainScrollView, startIndex: startIndex)
}
override func viewDidLoad() {
super.viewDidLoad()
addViewControllers(self, viewControllers: viewControllers, startIndex: startIndex)
}
}
As for the tabbar menu, you can easily create that by implementing UIScrollViewDelegate in MainSwipeViewController
extension MainSwipeViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.x / view.frame.width
}
}
contentOffset is the interval of your swipe and increases by one everytime you swipe. This will help you animate the blue bar in your menu.

Related

Animate UIView position on tableview scroll and again tableview drag

I have a list of items which a header menu at the top.
When the user scrolls up the menu should scroll off screen also and when they return to the top of the list the menu should be visible.
In much the same way as a tableViewHeader behaves.
However, should the header be off screen and the user ends a drag down on the list, this header view should animate down from the top. If the user then ends a drag up on the list, the header should animate back off screen.
I have achieved the first part of this below, however am struggling to achieve the animation effect.
I had considered using 2 views for the menu, one that scrolls and one that animates it's position, however this feels off, I'm sure there must be a better way without duplicating views.
class TableViewScene: UIViewController {
let data = Array(0...99)
var headerViewOneTopConstraint: NSLayoutConstraint!
var tableViewTopConstraint: NSLayoutConstraint!
lazy var headerViewOne: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .purple
return view
}()
lazy var tableView: UITableView = {
let view = UITableView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.delegate = self
view.dataSource = self
view.tableFooterView = .init()
view.refreshControl = .init()
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.isTranslucent = false
headerViewOneTopConstraint = headerViewOne.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
tableViewTopConstraint = tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 184)
[headerViewOne, tableView].forEach(view.addSubview)
NSLayoutConstraint.activate([
headerViewOneTopConstraint,
headerViewOne.leadingAnchor.constraint(equalTo: view.leadingAnchor),
headerViewOne.trailingAnchor.constraint(equalTo: view.trailingAnchor),
headerViewOne.heightAnchor.constraint(equalToConstant: 184),
tableViewTopConstraint,
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
extension TableViewScene: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
headerViewOneTopConstraint.constant = max(-184, min(0, -offsetY))
tableViewTopConstraint.constant = 184 - max(0, offsetY)
}
}
I had also tried adding this to the view
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview)
if translation.y > 0 {
headerViewOne.transform = .init(translationX: 0, y: 184)
} else if translation.y < 0 {
headerViewOne.transform = .identity
}
}
This does not work and instead just renders a black space the view used to fill on scroll to the top.
It's hard to say exactly what you are looking for but this might work for you. In this implementation when the user drags up the header will hide and when the user drags down the header will show. This is true regardless of the scroll position.
I made a change to one of your constraints so that the top of the tableView is pinned to the bottom of the header:
tableViewTopConstraint = tableView.topAnchor.constraint(equalTo: headerViewOne.bottomAnchor)
I'm also tracking the state of the header like so:
enum HeaderState {
case hidden
case revealed
case hiding
case revealing
}
var headerState: HeaderState = .revealed
Now for the scrolling logic. If the scroll position is near the top then we want to manually show/hide the header. However, if the scroll position is near the bottom or center then we want to animate the show/hide functionality.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview)
if scrollView.contentOffset.y < 184 {
if translation.y > 0 && headerState != .revealed && headerState != .revealing {
headerViewOneTopConstraint.constant = max(-184, min(0, -scrollView.contentOffset.y))
} else if translation.y < 0 {
headerViewOneTopConstraint.constant = max(-184, min(0, -scrollView.contentOffset.y))
}
return
}
if translation.y > 0 && headerState == .hidden {
self.headerState = .revealing
UIView.animate(withDuration: 0.4, animations: {
self.headerViewOneTopConstraint.constant = 0
self.view.layoutIfNeeded()
}, completion: { _ in
self.headerState = .revealed
})
} else if translation.y < 0 && headerState == .revealed {
self.headerState = .hiding
UIView.animate(withDuration: 0.4, animations: {
self.headerViewOneTopConstraint.constant = -184
self.view.layoutIfNeeded()
}, completion: { _ in
self.headerState = .hidden
})
}
}
Give this a shot and see if it meets your needs.

How can I animate this tableview header Y position change

I want to slide my table view header into view if the use scrolls up my feed and the header is off screen. If the user then scrolls down I want to hide it again.
I believe I have this working using the following:
final class AccountSettingsController: UITableViewController {
let items: [Int] = Array(0...500)
private lazy var menuView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemPink
view.heightAnchor.constraint(equalToConstant: 120).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = []
extendedLayoutIncludesOpaqueBars = false
tableView.tableHeaderViewWithAutolayout = menuView
tableView.tableFooterView = .init()
tableView.refreshControl = .init()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "This is setting #\(indexPath.row)"
return cell
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
let transY = scrollView.panGestureRecognizer.translation(in: scrollView).y
if scrollView.contentOffset.y > 120 {
if transY > 0 {
menuView.transform = .init(translationX: 0, y: contentOffset)
} else if transY < 0 {
menuView.transform = .identity
}
return
}
if scrollView.contentOffset.y <= 120 {
if transY < 0 {
menuView.transform = .identity
} else if transY > 0 {
menuView.transform = .init(translationX: 0, y: contentOffset)
}
}
}
}
I would like to animate the slide in / slide out of the header, so it slides down and up, rather than just appears.
I tried to add UIView.animate blocks such as
if scrollView.contentOffset.y <= 120 {
if transY < 0 {
menuView.transform = .identity
} else if transY > 0 {
UIView.animate(withDuration: 0.25, animations: {
self.menuView.transform = .init(translationX: 0, y: contentOffset)
})
}
}
but this produces a random jumping effect on the header and does not achieve what I would like at all.
I've attached a gif that shows the current effect, as you can see it just appears and disappears. I'd like this to animate down and up for for show / hide.
I also have an extension to set the correct size for my table view header, which you can find here -
extension UITableView {
var tableHeaderViewWithAutolayout: UIView? {
set (view) {
tableHeaderView = view
if let view = view {
lowerPriorities(view)
view.frame.size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
tableHeaderView = view
}
}
get {
return tableHeaderView
}
}
fileprivate func lowerPriorities(_ view: UIView) {
for cons in view.constraints {
if cons.priority.rawValue == 1000 {
cons.priority = UILayoutPriority(rawValue: 999)
}
for v in view.subviews {
lowerPriorities(v)
}
}
}
}
I agree with Claudio, using the tableHeaderView is probably complicating things as it is not really intended to be manipulated in this way.
Try the below and see if this gets you what you are looking for.
Instead of manipulating the offset of the header, manipulate the height. You can still give the impression of a scroll.
You could also introduce scrollView.panGestureRecognizer.velocity(in: scrollView) if you wanted to apply a threshold so the menu appears only after a particular scroll speed.
class MyTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private let data: [Int] = Array(0...99)
private let menuView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .purple
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var tableView: UITableView = {
let view = UITableView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.dataSource = self
view.delegate = self
// view.refreshControl = .init()
view.contentInsetAdjustmentBehavior = .never
view.tableFooterView = .init()
return view
}()
var menuViewHeightAnchor: NSLayoutConstraint!
var tableViewTopAnchor: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
menuViewHeightAnchor = menuView.heightAnchor.constraint(equalToConstant: 120)
tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 120)
edgesForExtendedLayout = []
[tableView, menuView].forEach(view.addSubview)
NSLayoutConstraint.activate([
menuViewHeightAnchor,
menuView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
menuView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
menuView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableViewTopAnchor,
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(frame: .zero)
cell.textLabel?.text = "Cell #\(indexPath.row)"
return cell
}
private var previousOffsetY: CGFloat = 0
private var velocityThreshold: CGFloat = 500
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = min(120, max(0, scrollView.contentOffset.y))
let translation = scrollView.panGestureRecognizer.translation(in: scrollView)
let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
let offsetYDiff = previousOffsetY - offsetY
previousOffsetY = offsetY
let adjustedOffset = menuViewHeightAnchor.constant + offsetYDiff
tableViewTopAnchor.constant = 120 - max(0, offsetY)
menuViewHeightAnchor.constant = min(120, adjustedOffset)
guard offsetY == 120 else { return }
if translation.y > 0 { // DRAGGED DOWN
UIView.animate(withDuration: 0.33, animations: {
self.menuViewHeightAnchor.constant = 120
self.view.layoutIfNeeded()
})
} else if translation.y < 0 { // DRAGGED UP
UIView.animate(withDuration: 0.33, animations: {
self.menuViewHeightAnchor.constant = 0
self.view.layoutIfNeeded()
})
}
}
}
I believe the best approach is by setting the menuView as a subview of the tableViewController, for example on the viewDidLoad method and also adding an inset on the tableView like this:
let menuHeight: CGFloat = 120
var scrollStartingYPoint: CGFloat = 0
private lazy var menuView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemPink
view.heightAnchor.constraint(equalToConstant: menuHeight).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(menuView)
NSLayoutConstraint.activate([
menuView.leftAnchor.constraint(equalTo: view.leftAnchor),
menuView.topAnchor.constraint(equalTo: view.topAnchor)
])
edgesForExtendedLayout = []
extendedLayoutIncludesOpaqueBars = false
tableView.contentInset.top = menuHeight
}
Then you can add the scrolling logic to show/hide the menu, I've done it a little different than you using the scrollViewWillBeginDragging method to store the initial scroll offset, to then determine the scroll direction and offset:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
scrollStartingYPoint = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableYScrollOffset = scrollView.contentOffset.y
let offset = abs(tableYScrollOffset - scrollStartingYPoint)
if tableYScrollOffset < scrollStartingYPoint { // scrolling down
if menuView.frame.origin.y >= 0 {
return
}
var translationY = -menuHeight + offset
if translationY > 0 {
translationY = 0
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
} else { // scrolling up
if menuView.frame.origin.y <= -menuHeight {
return
}
var translationY = -offset
if translationY < -menuHeight {
translationY = -menuHeight
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
}
}
If you want to animate the presentation of the menu, then you can change the previous code and use your animation code like this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableYScrollOffset = scrollView.contentOffset.y
let offset = abs(tableYScrollOffset - scrollStartingYPoint)
if tableYScrollOffset < scrollStartingYPoint { // scrolling down
UIView.animate(withDuration: 0.25, animations: {
self.menuView.transform = .identity
})
} else { // scrolling up
if menuView.frame.origin.y <= -menuHeight {
return
}
var translationY = -offset
if translationY < -menuHeight {
translationY = -menuHeight
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
}
}

UIScrollView not zooming: What am I missing?

I can scroll, but not zoom. I've seen a lot of identical looking code while searching, but still there's no zoom.
The pinchGestureRecognizer is not nil.
viewForZoomingInScrollView(scrollView: UIScrollView) is not being called.
scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) reports scale = 1.0.
What am I missing here?
(We need to do this programmatically rather than in IB).
import UIKit
class ScrollViewController: UIViewController, UIScrollViewDelegate {
var scrollView: UIScrollView!
var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
imageView = UIImageView(image: UIImage(named: "image.png"))
scrollView = UIScrollView(frame: view.bounds)
scrollView.backgroundColor = UIColor.black
scrollView.contentSize = imageView.bounds.size
scrollView.contentOffset = CGPoint(x: 1000, y: 450)
scrollView.addSubview(imageView)
view.addSubview(scrollView)
scrollView.delegate = self
scrollView.minimumZoomScale = 0.1
scrollView.maximumZoomScale = 4.0
scrollView.zoomScale = 1.0
}
//delegate
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return imageView
}
}
Did you convert your codes from an older version of Swift? Because the delegate function for returning the view for zooming is now
func viewForZooming(in scrollView: UIScrollView) -> UIView?

Set initial ScrollView

I want to set "ViewController2" as initial page in the ScrollView but changing setContentOffset(CGPointMake(0,0), animated: false) sets the first page as initial, i tried modified it with different numbers but it just messed up the pages, any tips?
import UIKit
class ViewController: UIViewController {
#IBOutlet var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.bounces = false
scrollView.pagingEnabled = true
scrollView.setContentOffset(CGPointMake(0,0), animated: false)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewDidLayoutSubviews() {
initScrollView()
}
func initScrollView(){
let viewController1 = storyboard?.instantiateViewControllerWithIdentifier("ViewController1") as! ViewController1
viewController1.willMoveToParentViewController(self)
viewController1.view.frame = scrollView.bounds
let viewController2 = storyboard?.instantiateViewControllerWithIdentifier("ViewController2") as! ViewController2
viewController2.willMoveToParentViewController(self)
viewController2.view.frame.size = scrollView.frame.size
viewController2.view.frame.origin = CGPoint(x: view.frame.width, y: 0)
let viewController3 = storyboard?.instantiateViewControllerWithIdentifier("ViewController3") as! ViewController3
viewController3.willMoveToParentViewController(self)
viewController3.view.frame.size = scrollView.frame.size
viewController3.view.frame.origin = CGPoint(x: view.frame.width * 2, y: 0)
scrollView.contentSize = CGSize(width: 3 * scrollView.frame.width, height: scrollView.frame.height)
scrollView.addSubview(viewController3.view)
self.addChildViewController(viewController3)
viewController3.didMoveToParentViewController(self)
scrollView.addSubview(viewController2.view)
self.addChildViewController(viewController2)
viewController2.didMoveToParentViewController(self)
scrollView.addSubview(viewController1.view)
self.addChildViewController(viewController1)
viewController1.didMoveToParentViewController(self)
}}
Try to de-select the scroll view inset adjustment... in vc2:
By putting scrollView.setContentOffset(CGPointMake(375,0), animated: false) in viewDidlayoutSubViews it loads the next view, note that i am using iphone 4,7 inch screen so the width of a viewController is 375. change 375 to the width of your viewController, for example if you wanna load the last page first you do add 375 to 375 and so on..
override func viewDidLayoutSubviews() {
initScrollView()
scrollView.setContentOffset(CGPointMake(375,0), animated: false)
}

View does not animate

I have a view, which shall move up like a drawer from the bottom of the screen. But it does not do anything. It just sits there =)
Can anyone please tell me, why it is doing that?
This is my code:
import UIKit
class InfoPopUpVC: UIViewController {
var superView: UIView!
var labelText: String!
let textLabel = UILabel()
let height = CGFloat(80)
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.center.y = 50
})
}
override func viewDidLoad() {
super.viewDidLoad()
setupTextLabel()
view.frame = CGRectMake(0, superView.frame.maxY-height, superView.frame.width, height)
AnimationHelper.blurBackgroundForView(view)
view.backgroundColor = .greenColor()
}
func setupTextLabel(){
textLabel.text = labelText
textLabel.frame = CGRectMake(0, 0, view.frame.width, view.frame.height)
textLabel.numberOfLines = 3
textLabel.textAlignment = .Center
textLabel.frame.inset(dx: 10, dy: 8)
textLabel.sizeToFit()
textLabel.font = UIFont(name: "HelveticaNeue-Light", size: 17)
textLabel.textColor = .whiteColor()
view.addSubview(textLabel)
}
}
Try to put your code as follow inside viewDidAppear or viewWillAppear and with dispatch async. Otherwise your animation might not work.
override func viewDidAppear(animated: Bool) {
dispatch_async(dispatch_get_main_queue(), {
UIView.animateWithDuration(0.4, animations: { () -> Void in
self.view.center.y = 50
})
})
}
You cannot animate a frame or similar property, if the UIViewis constrained using autolayout.
You have two options:
Get rid of autolayout and animate the frames Directory
Use autolayout and animate the constraints (e.g. via outlets)
See the following links for examples:
How do I animate constraint-changes
IOS: ANIMATING AUTOLAYOUT CONSTRAINTS