<commit_before>//
//  APSlidableTabPageController.swift
//  WebWrapper
//
//  Created by <NAME> on 14/01/16.
//  Copyright © 2016. All rights reserved.
//

import UIKit

//TODO: Think about the transition methods of the child view controllers.
//TODO: Make UI configurable. Make use of UIAppearance?


///Model of an index bar element of one page
public enum APIndexBarElement {
    case title(String)
    case image(UIImage, UIImage)
}

///Model of one page, consisting of an index bar element and a content view controller
public struct APSlidableTabPageControllerPage {
    public let indexBarElement: APIndexBarElement
    public let contentViewController: UIViewController
    
    public init(indexBarElement: APIndexBarElement, contentViewController: UIViewController) {
        self.indexBarElement = indexBarElement
        self.contentViewController = contentViewController
    }
    
    public init(contentViewController: UIViewController) {
        let indexBarElement: APIndexBarElement
        if let image = contentViewController.tabBarItem.image,
            let selectedImage = contentViewController.tabBarItem.selectedImage {
            indexBarElement = APIndexBarElement.image(image, selectedImage)
        } else {
            indexBarElement = APIndexBarElement.title(contentViewController.title ?? "")
        }
        self.init(indexBarElement: indexBarElement, contentViewController: contentViewController)
    }
}


public struct APSlidableTabPageControllerFactory {
    
    static public func make(pages: [APSlidableTabPageControllerPage]) -> APSlidableTabPageController {
        let nib = UINib(nibName: String(describing: APSlidableTabPageController.self),
                        bundle: Bundle(for: APSlidableTabPageController.self))
            .instantiate(withOwner: nil, options: nil)
        let vc = nib.first as! APSlidableTabPageController
        vc.pages = pages
        return vc
    }
}

public protocol APSlidableTabPageControllerDelegate: class {
    func slidableTabPageController(_ slidableTabPageController: APSlidableTabPageController,
                                   didNavigateFrom oldPage: Int, to newPage: Int)
}

public class APSlidableTabPageController: UIViewController, UIScrollViewDelegate  {
    
    public enum IndexBarPosition {
        case top
        case bottom
    }
    
    //MARK: IBOutlets
    
    @IBOutlet weak var verticalStackView: UIStackView!
    @IBOutlet public weak var indexBarScrollView: UIScrollView!
    @IBOutlet public weak var indexBarContainerView: UIView!
    @IBOutlet public weak var indexBarHeightConstraint: NSLayoutConstraint!

    @IBOutlet public weak var indexIndicatorView: UIView!
    @IBOutlet public weak var indexIndicatorViewCenterXConstraint: NSLayoutConstraint!
    private var indexIndicatorViewWidthConstraint: NSLayoutConstraint?
    
    @IBOutlet public weak var contentScrollView: UIScrollView!
    @IBOutlet public weak var contentContainerView: UIView!
    
    
    //MARK: Properties
    
    public weak var delegate: APSlidableTabPageControllerDelegate?

    internal var indexBarElements: [APIndexBarElementView] = []
    
    //Decides whether the indexBar should scroll to follow the index indicator view.
    private var indexBarShouldTrackIndicatorView = true
    
    //The current page before the trait collection changes, e.g. prior to rotation occurrs
    private var pageIndexBeforeTraitCollectionChange: Int = 0
    
    //Keeps track of the current page index in order to track scroll direction (i.e. if scrolling backwards or forwards)
    public private(set) var currentPageIndex: Int = 0 {
        didSet {
            guard oldValue != currentPageIndex,
                let delegate = delegate else {
                    return
            }
            delegate.slidableTabPageController(self, didNavigateFrom: oldValue, to: currentPageIndex)
        }
    }
    public var pages: [APSlidableTabPageControllerPage] = [] {
        willSet {
            removeContentView()
        }
        
        didSet {
            setupContentView()
            setupIndexBar()
            updateIndexIndicatorXPosition(percentage: 0)
        }
    }

    //MARK: Configurable
    
    public var maxNumberOfIndexBarElementsPerScreen = 3.5 {
        didSet {
            setupIndexBar()
            updateIndexIndicatorXPosition(percentage: 0)
        }
    }
    
    public var indexBarPosition: IndexBarPosition = .top {
        didSet {
            let index: Int
            switch indexBarPosition {
            case oldValue:          return                                     //Already at specified position
            case .top:              index = 0                                  //First
            case .bottom:           index = verticalStackView.subviews.count-1 //Last
            }
            
            DispatchQueue.main.async {
                self.verticalStackView.insertArrangedSubview(self.indexBarScrollView, at: index)
            }
        }
    }
    
    public var indexBarElementColor = UIColor.black {
        didSet { indexBarElements.forEach { $0.defaultColor = indexBarElementColor } }
    }
    
    public var indexBarElementHighlightedColor = UIColor.red {
        didSet { indexBarElements.forEach { $0.highlightedColor = indexBarElementHighlightedColor } }
    }
    
    //MARK: Rotation related events
    
    override public func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
        super.willTransition(to: newCollection, with: coordinator)
        pageIndexBeforeTraitCollectionChange = contentScrollView.ap_currentPage()
    }
    
    override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        //Restore previous page.
        //A slight delay is required since the scroll view's frame size has not yet been updated to reflect the new trait collection.
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            CATransaction.begin()
            CATransaction.setCompletionBlock {
                //Scroll the index indicator view to visible only after the its position has been completely updated
                self.scrollIndexIndicatorViewToVisible()
            }
            
            self.contentScrollView.ap_scrollToPageAtIndex(self.pageIndexBeforeTraitCollectionChange, animated: false)
            
            //Update the indicator view position manually in case no scroll was performed
            self.updateIndexIndicatorXPosition(percentage: self.contentScrollView.ap_horizontalPercentScrolled())
            CATransaction.commit()
        }
    }
    
    
    //MARK: Setup
    
    private func setupIndexBar() {
        guard pages.count > 0 else {
            return
        }
        
        indexBarElements.forEach { $0.removeFromSuperview() }
        indexBarElements = createIndexBarElements()
        indexBarContainerView.ap_addViewsHorizontally(indexBarElements)
        
        //Make sure the indicator is not hidden behind the index bar elements
        indexBarContainerView.bringSubview(toFront: indexIndicatorView)
        
        NSLayoutConstraint.activate(
            indexBarElements.map { element -> NSLayoutConstraint in
                element.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: indexBarElementWidthMultiplier())
            })
        
        indexIndicatorViewWidthConstraint?.isActive = false
        let clampedWidth = clampedIndexIndicatorWidth(width: indexBarElements.first!.intrinsicContentSize.width)
        indexIndicatorViewWidthConstraint = indexIndicatorView.widthAnchor.constraint(equalToConstant: clampedWidth)
        indexIndicatorViewWidthConstraint?.isActive = true
    }
    
    
    /// Returns a clamped width within the min and max values
    private func clampedIndexIndicatorWidth(width: CGFloat) -> CGFloat {
        let minWidth: CGFloat = 15<commit_msg>Fix rotation related bug on iPad<commit_after>//
//  APSlidableTabPageController.swift
//  WebWrapper
//
//  Created by <NAME> on 14/01/16.
//  Copyright © 2016. All rights reserved.
//

import UIKit

//TODO: Think about the transition methods of the child view controllers.
//TODO: Make UI configurable. Make use of UIAppearance?


///Model of an index bar element of one page
public enum APIndexBarElement {
    case title(String)
    case image(UIImage, UIImage)
}

///Model of one page, consisting of an index bar element and a content view controller
public struct APSlidableTabPageControllerPage {
    public let indexBarElement: APIndexBarElement
    public let contentViewController: UIViewController
    
    public init(indexBarElement: APIndexBarElement, contentViewController: UIViewController) {
        self.indexBarElement = indexBarElement
        self.contentViewController = contentViewController
    }
    
    public init(contentViewController: UIViewController) {
        let indexBarElement: APIndexBarElement
        if let image = contentViewController.tabBarItem.image,
            let selectedImage = contentViewController.tabBarItem.selectedImage {
            indexBarElement = APIndexBarElement.image(image, selectedImage)
        } else {
            indexBarElement = APIndexBarElement.title(contentViewController.title ?? "")
        }
        self.init(indexBarElement: indexBarElement, contentViewController: contentViewController)
    }
}


public struct APSlidableTabPageControllerFactory {
    
    static public func make(pages: [APSlidableTabPageControllerPage]) -> APSlidableTabPageController {
        let nib = UINib(nibName: String(describing: APSlidableTabPageController.self),
                        bundle: Bundle(for: APSlidableTabPageController.self))
            .instantiate(withOwner: nil, options: nil)
        let vc = nib.first as! APSlidableTabPageController
        vc.pages = pages
        return vc
    }
}

public protocol APSlidableTabPageControllerDelegate: class {
    func slidableTabPageController(_ slidableTabPageController: APSlidableTabPageController,
                                   didNavigateFrom oldPage: Int, to newPage: Int)
}

public class APSlidableTabPageController: UIViewController, UIScrollViewDelegate  {
    
    public enum IndexBarPosition {
        case top
        case bottom
    }
    
    //MARK: IBOutlets
    
    @IBOutlet weak var verticalStackView: UIStackView!
    @IBOutlet public weak var indexBarScrollView: UIScrollView!
    @IBOutlet public weak var indexBarContainerView: UIView!
    @IBOutlet public weak var indexBarHeightConstraint: NSLayoutConstraint!

    @IBOutlet public weak var indexIndicatorView: UIView!
    @IBOutlet public weak var indexIndicatorViewCenterXConstraint: NSLayoutConstraint!
    private var indexIndicatorViewWidthConstraint: NSLayoutConstraint?
    
    @IBOutlet public weak var contentScrollView: UIScrollView!
    @IBOutlet public weak var contentContainerView: UIView!
    
    
    //MARK: Properties
    
    public weak var delegate: APSlidableTabPageControllerDelegate?

    internal var indexBarElements: [APIndexBarElementView] = []
    
    //Decides whether the indexBar should scroll to follow the index indicator view.
    private var indexBarShouldTrackIndicatorView = true
    
    private var pageIndexBeforeRotation: Int = 0
    
    //Keeps track of the current page index in order to track scroll direction (i.e. if scrolling backwards or forwards)
    public private(set) var currentPageIndex: Int = 0 {
        didSet {
            guard oldValue != currentPageIndex,
                let delegate = delegate else {
                    return
            }
            delegate.slidableTabPageController(self, didNavigateFrom: oldValue, to: currentPageIndex)
        }
    }
    public var pages: [APSlidableTabPageControllerPage] = [] {
        willSet {
            removeContentView()
        }
        
        didSet {
            setupContentView()
            setupIndexBar()
            updateIndexIndicatorXPosition(percentage: 0)
        }
    }

    //MARK: Configurable
    
    public var maxNumberOfIndexBarElementsPerScreen = 3.5 {
        didSet {
            setupIndexBar()
            updateIndexIndicatorXPosition(percentage: 0)
        }
    }
    
    public var indexBarPosition: IndexBarPosition = .top {
        didSet {
            let index: Int
            switch indexBarPosition {
            case oldValue:          return                                     //Already at specified position
            case .top:              index = 0                                  //First
            case .bottom:           index = verticalStackView.subviews.count-1 //Last
            }
            
            DispatchQueue.main.async {
                self.verticalStackView.insertArrangedSubview(self.indexBarScrollView, at: index)
            }
        }
    }
    
    public var indexBarElementColor = UIColor.black {
        didSet { indexBarElements.forEach { $0.defaultColor = indexBarElementColor } }
    }
    
    public var indexBarElementHighlightedColor = UIColor.red {
        didSet { indexBarElements.forEach { $0.highlightedColor = indexBarElementHighlightedColor } }
    }
    
    //MARK: Rotation related events
    
    override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        pageIndexBeforeRotation = contentScrollView.ap_currentPage()
        
        coordinator.animate(alongsideTransition: { context in
            self.contentScrollView.ap_scrollToPageAtIndex(self.pageIndexBeforeRotation, animated: false)
            //Update the indicator view position manually in case no scroll was performed
            self.updateIndexIndicatorXPosition(percentage: self.contentScrollView.ap_horizontalPercentScrolled())
        }, completion: { _ in
            //Scroll the index indicator view to visible only after the its position has been completely updated
            self.scrollIndexIndicatorViewToVisible()
        })
        
        super.viewWillTransition(to: size, with: coordinator)
    }
    
    //MARK: Setup
    
    private func setupIndexBar() {
        guard pages.count > 0 else {
            return
        }
        
        indexBarElements.forEach { $0.removeFromSuperview() }
        indexBarElements = createIndexBarElements()
        indexBarContainerView.ap_addViewsHorizontally(indexBarElements)
        
        //Make sure the indicator is not hidden behind the index bar elements
        indexBarContainerView.bringSubview(toFront: indexIndicatorView)
        
        NSLayoutConstraint.activate(
            indexBarElements.map { element -> NSLayoutConstraint in
                element.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: indexBarElementWidthMultiplier())
            })
        
        indexIndicatorViewWidthConstraint?.isActive = false
        let clampedWidth = clampedIndexIndicatorWidth(width: indexBarElements.first!.intrinsicContentSize.width)
        indexIndicatorViewWidthConstraint = indexIndicatorView.widthAnchor.constraint(equalToConstant: clampedWidth)
        indexIndicatorViewWidthConstraint?.isActive = true
    }
    
    
    /// Returns a clamped width within the min and max values
    private func clampedIndexIndicatorWidth(width: CGFloat) -> CGFloat {
        let minWidth: CGFloat = 15
{'fraction_non_alphanumeric': 0.060418280402788536, 'fraction_numerical': 0.003227472243738704, 'mean_word_length': 2.5963324048282264, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1785426', 'n_tokens_mistral': 3956, 'n_tokens_neox': 3818, 'n_words': 1020}