Recreate iOS Flipkart App product detail top view - ios

I am trying to recreate iOS Flipkart app product detail top view as in image below
First image when image is displayed full and detail is at bottom (http://i.stack.imgur.com/1cutn.png)
Second image as user scroll up the detail content (http://i.stack.imgur.com/DXcHf.png)
Feature in above screens that I want to use:
User can scroll through the product images horizontally. User can also scroll up the detail content by dragging vertically over images.
As per my thought table view covers the whole screen with a transparent header view and a collection view stays behind the table view.
My Approach:
Add UIViewController subclass
let viewController = ProductDetailViewController()
self.navigationController?.pushViewController(viewController, animated: true)
Add a UICollectionView below navigation bar
var collectionView: UICollectionView = {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0)
layout.minimumLineSpacing = 5
layout.scrollDirection = UICollectionViewScrollDirection.Horizontal
let collectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: layout)
collectionView.backgroundColor = UIColor.lightGrayColor()
collectionView.showsHorizontalScrollIndicator = false
return collectionView
}()
self.collectionView.registerClass(PDetailImageCell.self, forCellWithReuseIdentifier: PDetailImageCellIdentifier)
func addCollectionViewForProductImages() {
let screenSize = UIScreen.mainScreen().bounds.size
self.collectionView.frame = CGRect(x: 0, y: 0, width: screenSize.width, height: self.tableHeaderHeight)
self.collectionView.pagingEnabled = true
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.tableView.tableHeaderView?.addSubview(self.collectionView)
}
Add UITableView
let screenSize = UIScreen.mainScreen().bounds.size
self.tableView.frame = CGRect(x: 0, y: 0, width: screenSize.width, height: screenSize.height)
self.view.addSubview(self.tableView)
self.tableView.backgroundColor = UIColor.clearColor()
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.canCancelContentTouches = false
Add a transparent UITableHeaderView
let headerView = TableTransparentHeaderView(frame: CGRect(x: 0, y: 0, width: screenSize.width, height: self.tableHeaderHeight))
headerView.backgroundColor = UIColor.clearColor()
self.tableView.tableHeaderView = headerView
Skip touches when user drags on table header. For this, I override the hitTest method in TouchHandledTableView class.
class TouchHandledTableView: UITableView {
var isScrollingHorizontal: Bool?
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, withEvent: event)
let liesInHeaderView = self.tableHeaderView?.pointInside(point, withEvent: event)
if liesInHeaderView == true && self.isScrollingHorizontal != nil {
if self.isScrollingHorizontal == false {
debugPrint("hitTest returned hitView")
return hitView
} else {
debugPrint("hitTest returned nil")
return nil
}
} else {
debugPrint("hitTest returned hitView")
return hitView
}
}
}
But this stopped table scrolling vertically. So I tried to catch weather the tableview is being dragged vertical or horizontal. To catch the scrolling direction, I added swipe gesture, touch event handler,scrollViewDidScroll but all of these are being called after hitTest method called. Once direction detected hitTest is not called in the same phase.

Related

How do you pass a gesture between subviews in UIKit?

I've got a ViewController with three subviews. I'm trying to get them to detect touches in their bounds from a starting point outside their bounds without the user lifting their finger (ie the user dragging into the view). I thought hitTest would do this but it only works for separate taps. I assume this is probably passing a gesture through instead but I've not found out how to implement this.
class SuperViewController: UIViewController {
var view01 = UIView(frame: CGRect(x: 0, y: 0, width: 1000,
height: 800))
var view02 = UIView(frame: CGRect(x: 0, y: 0, width: 600,
height: 400))
let view03 = UIView(frame: CGRect(x: 0, y: 0, width: 300,
height: 200))
override func viewDidLoad() {
super.viewDidLoad()
self.view = TestView()
view01.backgroundColor = .orange
view02.backgroundColor = .blue
view03.backgroundColor = .green
self.view.addSubview(view01)
self.view.addSubview(view02)
self.view.addSubview(view03)
}
}
Which produces this
And then I've subclassed UIView for the SuperViewController's view.
class TestView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard self.isUserInteractionEnabled, !isHidden, alpha > 0.01 else {return nil}
if self.point(inside: point, with: event) {
for subview in subviews.reversed() {
let hitView = subview.hitTest(point, with: event)
if hitView != nil {
hitView?.backgroundColor = .red
return hitView
}
}
return self
}
return nil
}
}
So each one turns red when the user taps. But ideally I want them to each respond with one drag from the top left corner of the screen to the other.
You can accomplish this with a UIPanGestureRecognizer.
Here's an example below:
class ViewController: UIViewController {
var view01 = UIView(frame: CGRect(x: 0, y: 0, width: 1000,
height: 800))
var view02 = UIView(frame: CGRect(x: 0, y: 0, width: 600,
height: 400))
let view03 = UIView(frame: CGRect(x: 0, y: 0, width: 300,
height: 200))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view01.backgroundColor = .orange
view02.backgroundColor = .blue
view03.backgroundColor = .green
self.view.addSubview(view01)
self.view.addSubview(view02)
self.view.addSubview(view03)
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
self.view.addGestureRecognizer(gestureRecognizer)
}
#objc
private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let view = gestureRecognizer.view else {
return
}
let translation = gestureRecognizer.translation(in: view)
for subview in view.subviews.reversed() {
if let hitView = subview.hitTest(translation, with: nil) {
hitView.backgroundColor = .red
return
}
}
}
}

Image Zoom not working in a UIScrollView inside a Root UIScrollView

I'm trying to zoom an image that is inside a UIScrollView, and that UIScrollView it's inside of a root UIScrollView that allow paging, similar to the Photos app.
In the viewDidLoad() inside the view that contains the root scrollView I call prepareScrollView()
func prepareScrollView(){
let tap = UITapGestureRecognizer(target: self, action: #selector(hideNavigationItem))
scrollView.addGestureRecognizer(tap)
scrollView.delegate = self
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 4.0
scrollView.zoomScale = 1.0
let width = self.view.bounds.width
let height = self.view.bounds.height
totalWidth = width * CGFloat((imageArray!.count)!) //The width must be the width of the screen by the number of the images in the array
scrollView.contentSize = CGSize(width: totalWidth, height: height)
scrollView.isPagingEnabled = true
loadPages()
}
The loadPages() function load every scrollView with their image and add it to the root scrollview.
func loadPages(){
for i in 0...((imageArray!.count)! - 1){
let pageScroll = Page(frame: CGRect(x: self.view.bounds.size.width * CGFloat(i), y: self.view.frame.origin.y, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
pageScroll.minimumZoomScale = 1.0
pageScroll.maximumZoomScale = 4.0
pageScroll.zoomScale = 1.0
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
imageView.image = UIImage(data: (imageArray![i])!)
imageView.contentMode = .scaleToFill
pageScroll.addSubview(imageView)
scrollView.addSubview(pageScroll)
}
}
The Page object it's just a UIScrollView object.
class Page: UIScrollView {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.subviews[0]//Return the image
}
}
And inside the root scrollView viewForZooming function it calls the viewForZooming function of the correspond child scrollview.
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
if(self.scrollView.subviews.count > 0){
let childScrollView = self.scrollView.subviews[currentPage] as! Page
let image = childScrollView.viewForZooming(in: scrollView)
return image
}
return nil
}
With this approach I can navigate horizontally but I can't zoom in the current image. The viewForZooming function executes in both objects. What I'm doing wrong?
Finally I solve my issue. I changed the Page object to a simple UIScrollView and then set the delegate of the scrollView object to self.
func loadPages(){
for i in 0...((comic?.comicsPages!.count)! - 1){
let pageScroll = UIScrollView(frame: CGRect(x: self.view.bounds.size.width * CGFloat(i), y: self.view.frame.origin.y, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
pageScroll.minimumZoomScale = 1.0
pageScroll.maximumZoomScale = 4.0
pageScroll.zoomScale = 1.0
pageScroll.isUserInteractionEnabled = true
pageScroll.delegate = self //Here
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
imageView.image = UIImage(data: (imageArray![i])!)
imageView.contentMode = .scaleToFill
pageScroll.addSubview(imageView)
scrollView.addSubview(pageScroll)
}
}
And then in the viewForZooming function of the root scrollView I change the code for this:
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
if(scrollView.subviews.count > 0){
return scrollView.subviews[0]
}
return nil
}
The scrollView parameter is the child scrollview so it works as expected.

Creating custom UIView, or child VCs

So, i have the following case. I got a custom tab layout, ie a horizontal UIScollview with paging. In each page there are some controls and a UITablevie, "bundles" in a custom UIView.
I create a ContainerVC, which has the scrollview, and then I initialize the two UIViews and add them as subviews in ContainerVC. To do so, i have created a custom UIView class and have have set this UIView as the owner of the .xib file. I also add the .xib class as my custom UIView.
Although this works, at least UI wise, i have some functionality problems, like the following.
In the init method of each UIView I initialize the view with
Bundle.main.loadNibNamed(kCONTENT_XIB_NAME, owner: self, options: nil)
and then set the UITableViewDelegate and UITableViewDateSource to self.
However, when the data that feed the table arrives in the view, and i reload the tableview, nothing happens (the reload method is run in the main Thread), until i scroll the UITableview.
Is this the correct course of action?
VC
func createUsageHistoryTabs() -> [Consumption] {
var tabs = [Consumption]()
let v1 = Consumption(pageIndex: 0, viewModel: viewModel)
let v2 = Consumption(pageIndex: 1, viewModel: viewModel)
tabs.append(v1)
tabs.append(v2)
return tabs
}
func setupScrollView() {
if(!hasBeenCreated) {
let tabs = createUsageHistoryTabs()
scrollview.frame = CGRect(x: 0, y: 0, width: container.frame.width, height: container.frame.height)
scrollview.contentSize = CGSize(width: view.frame.width * CGFloat(tabs.count), height: 1.0)
scrollview.isPagingEnabled = true
for i in 0 ..< tabs.count {
let slideView = tabs[i]
slideView.frame = CGRect(x: view.frame.width * CGFloat(i), y: scrollview.frame.origin.y, width: scrollview.frame.width, height: scrollview.frame.height)
scrollview.addSubview(slideView)
}
hasBeenCreated.toggle()
}
}
UIView:
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
convenience init(pageIndex: Int, viewModel: UsageHistoryViewModel) {
self.init(frame: CGRect.zero)
self.pageIndex = pageIndex
self.viewModel = viewModel
commonInit()
}
func commonInit() {
Bundle.main.loadNibNamed(kCONTENT_XIB_NAME, owner: self, options: nil)
contentView.fixInView(self)
setupView()
footer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleFooterTap)))
viewModel?.cdrDelegate = self
tableView.register(EmptyCell.self, forCellReuseIdentifier: "EmptyCell")
tableView.register(UINib(nibName: "TVCell", bundle: nil), forCellReuseIdentifier: "TVCell")
tableView.allowsSelection = false
tableView.delegate = self
tableView.dataSource = self
}
In this func:
func setupScrollView() {
if(!hasBeenCreated) {
// this line creates a LOCAL array
let tabs = createUsageHistoryTabs()
scrollview.frame = CGRect(x: 0, y: 0, width: container.frame.width, height: container.frame.height)
scrollview.contentSize = CGSize(width: view.frame.width * CGFloat(tabs.count), height: 1.0)
scrollview.isPagingEnabled = true
for i in 0 ..< tabs.count {
let slideView = tabs[i]
slideView.frame = CGRect(x: view.frame.width * CGFloat(i), y: scrollview.frame.origin.y, width: scrollview.frame.width, height: scrollview.frame.height)
scrollview.addSubview(slideView)
}
hasBeenCreated.toggle()
}
}
You are creating a local array of Consumption objects. From each object, you add its view to your scrollView. As soon as that func exits, tabs no longer exists... it goes "out-of-scope".
You want to create a class-level var:
var tabsArray: [Consumption]?
and then modify that var instead of creating a local instance:
func setupScrollView() {
if(!hasBeenCreated) {
// this line creates a LOCAL array
tabsArray = createUsageHistoryTabs()
...
Your custom classes should then be available until you delete them (or tabsArray goes out-of-scope).
You may need to make some other changes, but that should, at least, be a start.
EDIT after comment discussion...
If the tableviews are working, but not "updating" until the are scrolled - you said reload is being called on the main thread - you might try either of these options (or both... experiment):
tableView.reloadData()
tableView.setNeedsLayout()
tableView.layoutIfNeeded()
tableView.reloadData()
tableView.beginUpdates()
tableView.endUpdates()

Change touch receiver while scrolling a scroll view

Apps like Apple's maps app or Google maps use scrollable bottom sheet overlays to present additional content. While this behavior is not too difficult to rebuild, I struggle to implement one important feature:
When there is a scroll view embedded inside the bottom sheet, then the user can scroll it to the top but then – instead of bouncing off at the top – the bottom sheet starts scrolling down instead of the table view.
Here's an example video of what I mean:
Example Video:
This is a nice user experience as there is no interruption in the scrolling and it's what I expect as a user: It's as if once the content scroll view has reached its top the gesture receiver is automatically handed over the super scroll view.
In order to achieve this behavior, I see three different approaches:
I track the content scroll view's contentOffset in the scroll view's scrollViewDidScroll(_:) delegate method. Then I do
if contentScrollView.contentOffset.y < 0 {
contentScrollView.contentOffset.y = 0
}
to keep the content scroll view from scrolling above the top of its content. Instead, I pass the y distance that it would have scrolled to the super scroll view which scrolls the whole bottom sheet.
I find a way to change the receiver of the scrolling (pan) gesture recognizer from the content scroll view to the super scroll view as soon as the content scroll view has scrolled to its top.
I handle everything inside the super scroll view. It asks its content view controller through a delegate protocol if it wants to handle the touches and only if it doesn't (because its content scroll view has reached the top) the super scroll view scrolls by itself.
While I have managed to implement the first variant (it's what you see in the video), I'd strongly prefer to use approach 2 or 3. It's a much cleaner way to have the view controller that controls the bottom sheet manage all the scrolling logic without exposing its internals.
Unfortunately, I haven't found a way to somehow split the pan gesture into two components (one that controls the receiver scroll view and one that controls another scroll view)
Any ideas on how to achieve this kind of behavior?
I am very interested in this question and I hope by providing how I would implement it, it does not stifle an answer that might show how to truly pass around the responder. The trick I think which I put in the comments is keeping track of the touches. I forgot about how scrollview gobbles those up but you can use a UIPanGesture. See if this is close to what you are looking for. The only case I ran into that might take more thought is using the scroll to dismiss the bottom view. Most of this code is setup to get a working scrollview in the view. I think property animations might be best to make it interruptible or even my personal fav Facebook Pop animations. To keep it simple I just used UIView animations. Let me know if this solves what you are looking for. The code is below and here is the result
. The scrollview remains scrollable and active. I animate the frames but updating constraints could work as well.
import UIKit
class ViewController: UIViewController{
//setup
var items : [Int] = []
lazy var tableView : UITableView = {
let tv = UITableView(frame: CGRect(x: 0, y: topViewHeight, width: self.view.frame.width, height: self.view.frame.height))
tv.autoresizingMask = [.flexibleWidth,.flexibleHeight]
tv.delegate = self
tv.dataSource = self
tv.layer.cornerRadius = 4
return tv
}()
lazy var topView : UIView = {
let v = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: topViewHeight))
v.backgroundColor = .green
v.autoresizingMask = [.flexibleWidth,.flexibleHeight]
return v
}()
let cellIdentifier = "ourCell"
//for animation
var isAnimating = false
var lastOffset : CGPoint = .zero
var startingTouch : CGPoint?
let topViewHeight : CGFloat = 500
var isShowing : Bool = false
let maxCollapse : CGFloat = 50
override func viewDidLoad() {
super.viewDidLoad()
for x in 0...100{
items.append(x)
}
// Do any additional setup after loading the view, typically from a nib.
self.view.addSubview(topView)
self.view.addSubview(tableView)
self.tableView.reloadData()
let pan = UIPanGestureRecognizer(target: self, action: #selector(moveFunction(pan:)))
pan.delegate = self
self.view.addGestureRecognizer(pan)
}
#objc func moveFunction(pan:UIPanGestureRecognizer) {
let point:CGPoint = pan.location(in: self.view)
switch pan.state {
case .began:
startingTouch = point
break
case .changed:
processMove(touchPoint:point.y)
break
default:
processEnding(currentPointY: point.y)
break
}
}
}
extension ViewController : UITableViewDelegate,UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell : UITableViewCell!
cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
}
cell.textLabel?.text = "\(items[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 30
}
}
extension ViewController : UIScrollViewDelegate{
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if isAnimating == true{
scrollView.contentOffset = lastOffset
return
}
lastOffset = scrollView.contentOffset
}
}
extension ViewController : UIGestureRecognizerDelegate{
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
extension ViewController{
func processMove(touchPoint:CGFloat){
if let start = startingTouch{
if touchPoint <= topViewHeight && start.y > topViewHeight{
isAnimating = true
tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
return
}else if touchPoint >= self.maxCollapse && isShowing == true && start.y < self.maxCollapse{
isAnimating = true
tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
return
}else if isShowing == true && self.tableView.contentOffset.y <= 0{
//this is the only one i am slightly unsure about
isAnimating = true
tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
return
}
}
self.isAnimating = false
}
func processEnding(currentPointY:CGFloat){
startingTouch = nil
if isAnimating{
if currentPointY < topViewHeight/2{
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: .curveEaseInOut, animations: {
self.tableView.frame = CGRect(x: 0, y:self.maxCollapse, width: self.view.frame.width, height: self.view.frame.height)
}) { (finished) in
self.isShowing = true
}
}else{
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: .curveEaseInOut, animations: {
self.tableView.frame = CGRect(x: 0, y:self.topViewHeight, width: self.view.frame.width, height: self.view.frame.height)
}) { (finished) in
self.isShowing = false
}
}
}
self.isAnimating = false
}
}

UIScrollView doesn't scroll upwards sometimes

I am using 2 scroll Views When the inner Scroll View is scrolled upwards, outer scrollView also moves upwards(positive contentoffset.y) and when inner SV's contentoffset.y goes less than y, both scroll Views come back to their original positions.
Here is the code:
//secondView is a UIView inside outerScrollView and above inner SV(scrollView).
var scollView: UIScrollView!
var yOffset: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.automaticallyAdjustsScrollViewInsets = true
self.outerScrollView.delegate = self
self.scrollView = UIScrollView(frame: CGRect(x: 0, y: self.secondView.frame.origin.y + self.secondView.frame.height, width: self.view.frame.width, height: self.view.frame.height - (264)))
self.scrollView.delegate = self
self.outerView.addSubview(self.scrollView)
self.scrollView.setContentOffset(CGPoint.zero, animated: true)
}
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
if (scrollView == self.scrollView) {
if (scrollView.contentOffset.y > 0) {
if (self.outerScrollView.contentOffset.y <= 0 ) {
let contentnOffset:CGPoint = CGPoint(x: 0.0, y: self.yOffset)
DispatchQueue.main.async {
self.outerScrollView.setContentOffset(contentnOffset, animated: true)
self.scrollView.frame.size = CGSize(width: self.view.frame.width, height: self.view.frame.height - self.qLabel.frame.height - 9)
if (scrollView.contentSize.height < self.view.frame.height - self.qLabel.frame.height - 9) {
let downPanGesture: UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(self.handlePan))
downPanGesture.direction = .down
self.scrollView.addGestureRecognizer(downPanGesture)
}
}
}
} else {
DispatchQueue.main.async {
self.outerScrollView.setContentOffset(CGPoint.zero, animated: true)
self.scrollView.frame.size = CGSize(width: self.view.frame.width, height: self.view.frame.height - (self.scrollView.frame.origin.y))
}
}
}
}
Although everything works fine, there is just 1 small problem. After the inner scrollView is scrolled upwards and contentoffset has changed and I want to keep on scrolling this view, it sometimes just doesn't scroll(upwards). So, I have to first scrooll it downwards and then it will again start scrolling fine. It only happens rarely but it's quite annoying.
Can anybody tell me what could be the possible problem?
Thanks!
P.S. I have tried all other scrollView's delegate methods like didenddecelerating() or didenddragging() etc.

Resources