How to walk through page view controller vertically using swift - ios

I want to use walkthrough animation in my application with the vertical navigation, using the following code from this tutorial I could navigate horizontally. I want to know what changes should I make to walkthrough vertically. Could anyone help?. Thanks in advance.
BWWalkthroughPageViewController.swift
import UIKit
public enum WalkthroughAnimationType:String{
case Linear = "Linear"
case Curve = "Curve"
case Zoom = "Zoom"
case InOut = "InOut"
init(_ name:String){
if let tempSelf = WalkthroughAnimationType(rawValue: name){
self = tempSelf
}else{
self = .Linear
}
}
}
open class BWWalkthroughPageViewController: UIViewController, BWWalkthroughPage {
fileprivate var animation:WalkthroughAnimationType = .Linear
fileprivate var subsWeights:[CGPoint] = Array()
fileprivate var notAnimatableViews:[Int] = [] // Array of views' tags that should not be animated during the scroll/transition
// MARK: Inspectable Properties
// Edit these values using the Attribute inspector or modify directly the "User defined runtime attributes" in IB
#IBInspectable var speed:CGPoint = CGPoint(x: 0.0, y: 0.0); // Note if you set this value via Attribute inspector it can only be an Integer (change it manually via User defined runtime attribute if you need a Float)
#IBInspectable var speedVariance:CGPoint = CGPoint(x: 0.0, y: 0.0) // Note if you set this value via Attribute inspector it can only be an Integer (change it manually via User defined runtime attribute if you need a Float)
#IBInspectable var animationType:String {
set(value){
self.animation = WalkthroughAnimationType(rawValue: value)!
}
get{
return self.animation.rawValue
}
}
#IBInspectable var animateAlpha:Bool = false
#IBInspectable var staticTags:String { // A comma separated list of tags that you don't want to animate during the transition/scroll
set(value){
self.notAnimatableViews = value.components(separatedBy: ",").map{Int($0)!}
}
get{
return notAnimatableViews.map{String($0)}.joined(separator: ",")
}
}
// MARK: BWWalkthroughPage Implementation
override open func viewDidLoad() {
super.viewDidLoad()
self.view.layer.masksToBounds = true
subsWeights = Array()
for v in view.subviews{
speed.x += speedVariance.x
speed.y += speedVariance.y
if !notAnimatableViews.contains(v.tag) {
subsWeights.append(speed)
}
}
}
open func walkthroughDidScroll(_ position: CGFloat, offset: CGFloat) {
for i in 0 ..< subsWeights.count
{
// Perform Transition/Scale/Rotate animations
switch animation
{
case .Linear:
animationLinear(i, offset)
case .Zoom:
animationZoom(i, offset)
case .Curve:
animationCurve(i, offset)
case .InOut:
animationInOut(i, offset)
}
// Animate alpha
if(animateAlpha)
{
animationAlpha(i, offset)
}
}
}
// MARK: Animations (WIP)
private func animationAlpha(_ index:Int, _ offset:CGFloat){
var offset = offset
let cView = view.subviews[index]
if(offset > 1.0){
offset = 1.0 + (1.0 - offset)
}
cView.alpha = (offset)
}
fileprivate func animationCurve(_ index:Int, _ offset:CGFloat){
var transform = CATransform3DIdentity
let x:CGFloat = (1.0 - offset) * 10
transform = CATransform3DTranslate(transform, (pow(x,3) - (x * 25)) * subsWeights[index].x, (pow(x,3) - (x * 20)) * subsWeights[index].y, 0 )
applyTransform(index, transform: transform)
}
fileprivate func animationZoom(_ index:Int, _ offset:CGFloat){
var transform = CATransform3DIdentity
var tmpOffset = offset
if(tmpOffset > 1.0){
tmpOffset = 1.0 + (1.0 - tmpOffset)
}
let scale:CGFloat = (1.0 - tmpOffset)
transform = CATransform3DScale(transform, 1 - scale , 1 - scale, 1.0)
applyTransform(index, transform: transform)
}
fileprivate func animationLinear(_ index:Int, _ offset:CGFloat){
var transform = CATransform3DIdentity
let mx:CGFloat = (1.0 - offset) * 100
transform = CATransform3DTranslate(transform, mx * subsWeights[index].x, mx * subsWeights[index].y, 0 )
applyTransform(index, transform: transform)
}
fileprivate func animationInOut(_ index:Int, _ offset:CGFloat){
var transform = CATransform3DIdentity
//var x:CGFloat = (1.0 - offset) * 20
var tmpOffset = offset
if(tmpOffset > 1.0){
tmpOffset = 1.0 + (1.0 - tmpOffset)
}
transform = CATransform3DTranslate(transform, (1.0 - tmpOffset) * subsWeights[index].x * 100, (1.0 - tmpOffset) * subsWeights[index].y * 100, 0)
applyTransform(index, transform: transform)
}
fileprivate func applyTransform(_ index:Int, transform:CATransform3D){
let subview = view.subviews[index]
if !notAnimatableViews.contains(subview.tag){
view.subviews[index].layer.transform = transform
}
}
}
BWWalkthroughViewController.swift:
import UIKit
// MARK: - Protocols -
/**
Walkthrough Delegate:
This delegate performs basic operations such as dismissing the Walkthrough or call whatever action on page change.
Probably the Walkthrough is presented by this delegate.
**/
#objc public protocol BWWalkthroughViewControllerDelegate{
#objc optional func walkthroughCloseButtonPressed() // If the skipRequest(sender:) action is connected to a button, this function is called when that button is pressed.
#objc optional func walkthroughNextButtonPressed() //
#objc optional func walkthroughPrevButtonPressed() //
#objc optional func walkthroughPageDidChange(_ pageNumber:Int) // Called when current page changes
}
/**
Walkthrough Page:
The walkthrough page represents any page added to the Walkthrough.
At the moment it's only used to perform custom animations on didScroll.
**/
#objc public protocol BWWalkthroughPage
{
// While sliding to the "next" slide (from right to left), the "current" slide changes its offset from 1.0 to 2.0 while the "next" slide changes it from 0.0 to 1.0
// While sliding to the "previous" slide (left to right), the current slide changes its offset from 1.0 to 0.0 while the "previous" slide changes it from 2.0 to 1.0
// The other pages update their offsets whith values like 2.0, 3.0, -2.0... depending on their positions and on the status of the walkthrough
// This value can be used on the previous, current and next page to perform custom animations on page's subviews.
#objc func walkthroughDidScroll(_ position:CGFloat, offset:CGFloat) // Called when the main Scrollview...scrolls
}
#objc open class BWWalkthroughViewController: UIViewController, UIScrollViewDelegate{
// MARK: - Public properties -
weak open var delegate:BWWalkthroughViewControllerDelegate?
// TODO: If you need a page control, next or prev buttons add them via IB and connect them with these Outlets
#IBOutlet open var pageControl:UIPageControl?
#IBOutlet open var nextButton:UIButton?
#IBOutlet open var prevButton:UIButton?
#IBOutlet open var closeButton:UIButton?
open var currentPage:Int{ // The index of the current page (readonly)
get{
let page = Int((scrollview.contentOffset.x / view.bounds.size.width))
return page
}
}
open var currentViewController:UIViewController{ //the controller for the currently visible page
get{
let currentPage = self.currentPage;
return controllers[currentPage];
}
}
open var numberOfPages:Int{ //the total number of pages in the walkthrough
get{
return self.controllers.count;
}
}
// MARK: - Private properties -
open let scrollview:UIScrollView!
fileprivate var controllers:[UIViewController]!
fileprivate var lastViewConstraint:NSArray?
// MARK: - Overrides -
required public init?(coder aDecoder: NSCoder) {
// Setup the scrollview
scrollview = UIScrollView()
scrollview.showsHorizontalScrollIndicator = false
scrollview.showsVerticalScrollIndicator = false
scrollview.isPagingEnabled = true
// Controllers as empty array
controllers = Array()
super.init(coder: aDecoder)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?){
scrollview = UIScrollView()
controllers = Array()
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
override open func viewDidLoad() {
super.viewDidLoad()
// Initialize UI Elements
pageControl?.addTarget(self, action: #selector(BWWalkthroughViewController.pageControlDidTouch), for: UIControlEvents.touchUpInside)
// Scrollview
scrollview.delegate = self
scrollview.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(scrollview, at: 0) //scrollview is inserted as first view of the hierarchy
// Set scrollview related constraints
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[scrollview]-0-|", options:[], metrics: nil, views: ["scrollview":scrollview]))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[scrollview]-0-|", options:[], metrics: nil, views: ["scrollview":scrollview]))
}
override open func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated);
pageControl?.numberOfPages = controllers.count
pageControl?.currentPage = 0
}
// MARK: - Internal methods -
/**
* Progresses to the next page, or calls the finished delegate method if already on the last page
*/
#IBAction open func nextPage(){
if (currentPage + 1) < controllers.count {
delegate?.walkthroughNextButtonPressed?()
gotoPage(currentPage + 1)
}
}
#IBAction open func prevPage(){
if currentPage > 0 {
delegate?.walkthroughPrevButtonPressed?()
gotoPage(currentPage - 1)
}
}
// TODO: If you want to implement a "skip" button
// connect the button to this IBAction and implement the delegate with the skipWalkthrough
#IBAction open func close(_ sender: AnyObject){
delegate?.walkthroughCloseButtonPressed?()
}
func pageControlDidTouch(){
if let pc = pageControl{
gotoPage(pc.currentPage)
}
}
fileprivate func gotoPage(_ page:Int){
if page < controllers.count{
var frame = scrollview.frame
frame.origin.x = CGFloat(page) * frame.size.width
scrollview.scrollRectToVisible(frame, animated: true)
}
}
/**
addViewController
Add a new page to the walkthrough.
To have information about the current position of the page in the walkthrough add a UIVIewController which implements BWWalkthroughPage
*/
open func addViewController(_ vc:UIViewController)->Void{
controllers.append(vc)
// Setup the viewController view
vc.view.translatesAutoresizingMaskIntoConstraints = false
scrollview.addSubview(vc.view)
// Constraints
let metricDict = ["w":vc.view.bounds.size.width,"h":vc.view.bounds.size.height]
// - Generic cnst
vc.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[view(h)]", options:[], metrics: metricDict, views: ["view":vc.view]))
vc.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[view(w)]", options:[], metrics: metricDict, views: ["view":vc.view]))
scrollview.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[view]|", options:[], metrics: nil, views: ["view":vc.view,]))
// cnst for position: 1st element
if controllers.count == 1{
scrollview.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[view]", options:[], metrics: nil, views: ["view":vc.view,]))
// cnst for position: other elements
}else{
let previousVC = controllers[controllers.count-2]
let previousView = previousVC.view;
scrollview.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[previousView]-0-[view]", options:[], metrics: nil, views: ["previousView":previousView!,"view":vc.view]))
if let cst = lastViewConstraint{
scrollview.removeConstraints(cst as! [NSLayoutConstraint])
}
lastViewConstraint = NSLayoutConstraint.constraints(withVisualFormat: "H:[view]-0-|", options:[], metrics: nil, views: ["view":vc.view]) as NSArray
scrollview.addConstraints(lastViewConstraint! as! [NSLayoutConstraint])
}
}
/**
Update the UI to reflect the current walkthrough status
**/
fileprivate func updateUI(){
// Get the current page
pageControl?.currentPage = currentPage
// Notify delegate about the new page
delegate?.walkthroughPageDidChange?(currentPage)
// Hide/Show navigation buttons
if currentPage == controllers.count - 1{
nextButton?.isHidden = true
}else{
nextButton?.isHidden = false
}
if currentPage == 0{
prevButton?.isHidden = true
}else{
prevButton?.isHidden = false
}
}
// MARK: - Scrollview Delegate -
open func scrollViewDidScroll(_ sv: UIScrollView) {
for i in 0 ..< controllers.count {
if let vc = controllers[i] as? BWWalkthroughPage{
let mx = ((scrollview.contentOffset.x + view.bounds.size.width) - (view.bounds.size.width * CGFloat(i))) / view.bounds.size.width
// While sliding to the "next" slide (from right to left), the "current" slide changes its offset from 1.0 to 2.0 while the "next" slide changes it from 0.0 to 1.0
// While sliding to the "previous" slide (left to right), the current slide changes its offset from 1.0 to 0.0 while the "previous" slide changes it from 2.0 to 1.0
// The other pages update their offsets whith values like 2.0, 3.0, -2.0... depending on their positions and on the status of the walkthrough
// This value can be used on the previous, current and next page to perform custom animations on page's subviews.
// print the mx value to get more info.
// println("\(i):\(mx)")
// We animate only the previous, current and next page
if(mx < 2 && mx > -2.0){
vc.walkthroughDidScroll(scrollview.contentOffset.x, offset: mx)
}
}
}
}
open func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
updateUI()
}
open func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
updateUI()
}
/* WIP */
override open func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
print("CHANGE")
}
override open func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
print("SIZE")
}
}

Related

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.

Change views on UIScrollView so music changes

I have a UIScrollView setup but when I change view the music doesn't stop. How do I make it so the music stops when you change view?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear( animated)
meditationState = .on
setTrackForPlayerWith(trackName: "Bigsur")
player.play()
player.numberOfLoops = -1
}
Here is the whole class from viewcontroller.swift. I've added the func scrollViewDidScroll and the self part that you mentioned but it's still not working.
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
let vc0 = ViewController0(nibName: "ViewController0", bundle: nil)
var frame0 = vc0.view.frame
frame0.origin.x = self.view.frame.size.width
vc0.view.frame = frame0
self.addChildViewController(vc0)
self.scrollView.addSubview(vc0.view)
vc0.didMove(toParentViewController: self)
let vc1 = ViewController1(nibName: "ViewController1", bundle: nil)
var frame1 = vc1.view.frame
frame1.origin.x = self.view.frame.size.width
vc1.view.frame = frame1
self.addChildViewController(vc1)
self.scrollView.addSubview(vc1.view)
vc1.didMove(toParentViewController: self)
let vc2 = ViewController2(nibName: "ViewController2", bundle: nil)
var frame2 = vc2.view.frame
frame2.origin.x = self.view.frame.size.width * 2
vc2.view.frame = frame2
self.addChildViewController(vc2)
self.scrollView.addSubview(vc2.view)
vc2.didMove(toParentViewController: self)
let vc3 = ViewController3(nibName: "ViewController3", bundle: nil)
var frame3 = vc3.view.frame
frame3.origin.x = self.view.frame.size.width * 3
vc1.view.frame = frame3
self.addChildViewController(vc3)
self.scrollView.addSubview(vc3.view)
vc3.didMove(toParentViewController: self)
self.scrollView.contentSize = CGSize(width: Double(self.view.frame.size.width * 4), height: Double(self.view.frame.size.height - 66))
self.scrollView.delegate = self
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x > self.view.frame.size.x {
player.stop()
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
ViewController3 where 'player' is declared:
import UIKit
import AVFoundation
enum MeditationState {
case on
case off
}
class ViewController3: UIViewController {
var player:AVAudioPlayer = AVAudioPlayer()
var player1:AVAudioPlayer = AVAudioPlayer()
var meditationState: MeditationState?
var replicatorLayer = CAReplicatorLayer()
var dot = CALayer()
func updateTimer(){
seconds += 1
timerclock.text = "\(seconds)"
}
// Animation starts running
func animation2() {
// A layer that creates a specified number of copies of its sublayers (the source layer), each copy potentially having geometric, temporal, and color transformations applied to it.
replicatorLayer = CAReplicatorLayer()
// The layer’s bounds rectangle. Animatable.
replicatorLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 300.0, height: 300.0)
// The radius to use when drawing rounded corners for the layer’s background. Animatable.
replicatorLayer.cornerRadius = 10.0
// The background color of the receiver. Animatable.
replicatorLayer.backgroundColor = UIColor(white: 0.0, alpha: 0.0).cgColor
// The layer’s position in its superlayer’s coordinate space. Animatable.
replicatorLayer.position = view.center
// calling this method creates an array for that property and adds the specified layer to it.
view.layer.addSublayer(replicatorLayer)
// connectng the animation to the content
// An object that manages image-based content and allows you to perform animations on that content
dot = CALayer()
// The layer’s bounds rectangle. Animatable.
dot.bounds = CGRect(x: 0.0, y: 0.0, width: 12.0, height: 12.0)
//The layer’s position in its superlayer’s coordinate space. Animatable.
dot.position = CGPoint(x: 150.0, y: 40.0)
//The background color of the receiver. Animatable.
dot.backgroundColor = UIColor(white: 0.2, alpha: 1.0).cgColor
// The color of the layer’s border. Animatable.
dot.borderColor = UIColor(white: 1.0, alpha: 1.0).cgColor
// The width of the layer’s border. Animatable.
dot.borderWidth = 1.0
//The radius to use when drawing rounded corners for the layer’s background. Animatable.
dot.cornerRadius = 5.0
//Appends the layer to the layer’s list of sublayers.
replicatorLayer.addSublayer(dot)
// number of copies of layer is instanceCount
let nrDots: Int = 1000
//The number of copies to create, including the source layers.
replicatorLayer.instanceCount = nrDots
// The basic type for floating-point scalar values in Core Graphics and related frameworks.
let angle = CGFloat(2*M_PI) / CGFloat(nrDots)
// The transform matrix applied to the previous instance to produce the current instance. Animatable.
replicatorLayer.instanceTransform = CATransform3DMakeRotation(angle, 0.0, 0.0, 1.0)
// Type used to represent elapsed time in seconds.
let duration: CFTimeInterval = 10.0
// animation capabilities for a layer property.
// An object that provides basic, single-keyframe animation capabilities for a layer property.
let shrink = CABasicAnimation(keyPath: "transform.scale")
// Defines the value the receiver uses to start interpolation.
shrink.fromValue = 1.0
// Defines the value the receiver uses to end interpolation.
shrink.toValue = 0.1
// Specifies the basic duration of the animation, in seconds.
shrink.duration = duration
// Determines the number of times the animation will repeat.
shrink.repeatCount = Float.infinity
// Add the specified animation object to the layer’s render tree.
dot.add(shrink, forKey: "shrink")
// Specifies the delay, in seconds, between replicated copies. Animatable.
replicatorLayer.instanceDelay = duration/Double(nrDots)
// The transform applied to the layer’s contents. Animatable.
dot.transform = CATransform3DMakeScale(0.01, 0.01, 0.01)
}
// connecting the breathe in label
#IBOutlet weak var label: UILabel!
// instant delay
#IBOutlet weak var instantDelay: UIButton!
#IBAction func delayBtn(_ sender: Any) {
dot.removeAnimation(forKey: "shrink")
timer1.invalidate()
seconds = 0
timer2.invalidate()
timerclock.text = "\(seconds)"
time = 0
timerLabel.text = "Breathe in"
timerisOn = false
pauseBtn.isHidden = true
playBtn.isHidden = false
label.isHidden = true
replicatorLayer.isHidden = true
instantDelay.isHidden = true
instantDelay1.isHidden = false
slider.isHidden = false
}
// Delay 1
#IBOutlet weak var instantDelay1: UIButton!
#IBAction func delayBtn1(_ sender: Any) {
instantDelay1.isHidden = true
instantDelay.isHidden = false
label.isHidden = false
slider.isHidden = true
}
//Slider for changing animation speed
#IBOutlet weak var slider: UISlider!
#IBAction func slider(_ sender: Any) {
}
#IBAction func speed(_ sender: UISlider) {
view.layer.speed = sender.value
}
//Sound On button
#IBOutlet weak var soundOn: UIButton!
#IBAction func SoundOn(_ sender: Any) {
meditationState = .on
setTrackForPlayerWith(trackName: "Mute")
player.play()
soundoff.isHidden = false
soundOn.isHidden = true
}
//Sound Off button
#IBOutlet weak var soundoff: UIButton!
#IBAction func SoundOff(_ sender: Any) {
meditationState = .off
setTrackForPlayerWith(trackName: "Bigsur")
player.play()
soundoff.isHidden = true
soundOn.isHidden = false
}
//Timerclock at top of screen label
#IBOutlet weak var timerclock: UILabel!
// creating vars to set things
var animation = CFTimeInterval()
var timer1 = Timer()
var timer2 = Timer()
var time = 0
var seconds = 0
var timerisOn = false
// connecting breathe in label
#IBOutlet var question: UILabel!
var arrayOfStrings: [String] = [""]
// connecting timerclick and starting it
#IBOutlet var timerLabel: UILabel!
// changes the amount of time on the label of different labels
func increaseTimer() {
time += 1
switch time {
case 0 ... 7:
timerLabel.text = "Hold"
case 8 ... 10:
timerLabel.text = "Breathe Out"
case 11 ... 12:
timerLabel.text = "Breathe in"
default:
time = 0
}
}
// connecting the play button and vars
#IBOutlet weak var playBtn: UIButton!
#IBAction func play(sender: AnyObject) {
bell(trackName: "Bell")
player1.play()
timer1 = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController3.increaseTimer), userInfo: nil, repeats: true)
pauseBtn.isHidden = false
playBtn.isHidden = true
if timerisOn == false {
timer2 = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
timerisOn = true
}
animation2()
}
// pausing the timer with the vars
#IBOutlet weak var pauseBtn: UIButton!
#IBAction func pause(sender: AnyObject) {
dot.removeAnimation(forKey: "shrink")
timer1.invalidate()
seconds = 0
timer2.invalidate()
timerclock.text = "\(seconds)"
time = 0
timerLabel.text = "Breathe in"
timerisOn = false
pauseBtn.isHidden = true
playBtn.isHidden = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear( animated)
meditationState = .on
setTrackForPlayerWith(trackName: "Bigsur")
player.play()
player.numberOfLoops = -1
}
override func viewDidLoad() {
super.viewDidLoad()
time += 1
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient)
print("AVAudioSession Category Playback OK")
do {
try AVAudioSession.sharedInstance().setActive(true)
print("AVAudioSession is Active")
} catch let error as NSError {
print(error.localizedDescription)
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
func setTrackForPlayerWith(trackName: String) {
do
{
let audioPath = Bundle.main.path(forResource: trackName, ofType: "mp3")
try player = AVAudioPlayer(contentsOf: NSURL(fileURLWithPath: audioPath!) as URL)
}
catch
{
//ERROR
}
}
func bell(trackName: String) {
do
{
let audioPath = Bundle.main.path(forResource: trackName, ofType: "mp3")
try player1 = AVAudioPlayer(contentsOf: NSURL(fileURLWithPath: audioPath!) as URL)
}
catch
{
//ERROR
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Are you looking for viewWillDisappear(_:)? In that method you can just add player.stop() to stop the music when you change views.
The smartass way to do this is just ....
let's say it's a vertical table.
1. Each table view cell has an audio track associated with it. Think of one particular cell - C - it has an audio track A.
2. As the view scrolls (ie, whenever it is moving), just get the frame of C
3. Just take the height. Then take the height of the screen SH. Then get the distance of C from the center of the screen .. so Abs(SH - H). Then just get that figure as a fraction (zero to one) of the SH. So, Abs(SH - H)/H
(Depending on your situation, it may be better if that is divided by the height of cells, rather than screen height.)
4. Now ... simply set the volume of all the audio track A, to that fraction. And in fact, simply do that for every cell.
As you scroll, the audio will mix between the various tracks.
"Magic" :)
You can observe the changes made in your UIScrollView subclass in various delegate methods.
First of all, lets make sure your scrollView's delegate is assigned to the viewController. To do so, one option is to add following to viewDidLoad()
// `scrollView` should be whatever is your scrollView called in your VC
self.scrollView.delegate = self
Once this is done, lets make your UIViewController subclass conform to UIScrollViewDelegate
class ViewController: UIViewController, UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x > self.view.frame.size.x {
player.stop()
}
}
}
UIScrollViewDelegate has numerous methods to observe changes in your scrollView. scrollViewDidScroll(_:) will be called every time there is an interaction with the scrollView, so as soon as the contentOffset is greater than the width of the view, lets stop the music.

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
})
}

page based application overload memory ARC don´t release pages

may you can help me with this memory issue. So I builded a app based on the project preset "Page-Based Application" everything works well, but over time the single views load every Viewcontroller into the physical memory and don´t release them. If the memory is full the app will crash.
here ist my code:
The RootViewController (CatalougeViewController):
import UIKit
class CatalougeViewController: UIViewController, UIPageViewControllerDelegate, UIGestureRecognizerDelegate {
var pageViewController: UIPageViewController?
var zoomTransform: CGAffineTransform?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Configure the page view controller and add it as a child view controller.
self.pageViewController = UIPageViewController(transitionStyle: .PageCurl, navigationOrientation: .Horizontal, options: nil)
self.pageViewController!.delegate = self
let startingViewController: DataViewController = self.modelController.viewControllerAtIndex(0, storyboard: self.storyboard!)!
let viewControllers = [startingViewController]
self.pageViewController!.setViewControllers(viewControllers, direction: .Forward, animated: true, completion: {done in })
self.pageViewController!.dataSource = self.modelController
self.addChildViewController(self.pageViewController!)
self.view.addSubview(self.pageViewController!.view)
// Set the page view controller's bounds using an inset rect so that self's view is visible around the edges of the pages.
var pageViewRect = self.view.bounds
if UIDevice.currentDevice().userInterfaceIdiom == .Pad {
pageViewRect = CGRectInset(pageViewRect, 0.0, 0.0)
}
self.pageViewController!.view.frame = pageViewRect
self.pageViewController!.didMoveToParentViewController(self)
/*
// Add the page view controller's gesture recognizers to the book view controller's view so that the gestures are started more easily.
self.view.gestureRecognizers = self.pageViewController!.gestureRecognizers
*/
self.view.gestureRecognizers = self.pageViewController?.gestureRecognizers
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: "pinchDetected:")
self.view.addGestureRecognizer(pinchRecognizer)
let panRecognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
panRecognizer.minimumNumberOfTouches = 2
panRecognizer.maximumNumberOfTouches = 2
self.view.addGestureRecognizer(panRecognizer)
}
func pinchDetected(pinchRecognizer: UIPinchGestureRecognizer) {
if (UIGestureRecognizerState.Began == pinchRecognizer.state) || (UIGestureRecognizerState.Changed == pinchRecognizer.state) {
// Use the x or y scale, they should be the same for typical zooming (non-skewing)
let curScale = pinchRecognizer.view!.layer.valueForKeyPath("transform.scale.x")!.floatValue
let currentScale = CGFloat(curScale!)
// Variables to adjust the max/min values of zoom
let minScale: CGFloat = 1.0;
let maxScale: CGFloat = 2.0;
let zoomSpeed: CGFloat = 0.5;
var deltaScale: CGFloat = pinchRecognizer.scale
// You need to translate the zoom to 0 (origin) so that you
// can multiply a speed factor and then translate back to "zoomSpace" around 1
deltaScale = ((deltaScale - 1) * zoomSpeed) + 1
// Limit to min/max size (i.e maxScale = 2, current scale = 2, 2/2 = 1.0)
// A deltaScale is ~0.99 for decreasing or ~1.01 for increasing
// A deltaScale of 1.0 will maintain the zoom size
deltaScale = min(deltaScale, maxScale / currentScale)
deltaScale = max(deltaScale, minScale / currentScale)
zoomTransform = CGAffineTransformScale(pinchRecognizer.view!.transform, deltaScale, deltaScale);
pinchRecognizer.view!.transform = zoomTransform!;
// Reset to 1 for scale delta's
// Note: not 0, or we won't see a size: 0 * width = 0
pinchRecognizer.scale = 1;
}
}
func handlePan(recognizer:UIPanGestureRecognizer) {
let translation = recognizer.translationInView(self.view)
if let view = recognizer.view {
view.center = CGPoint(x:view.center.x + translation.x,
y:view.center.y + translation.y)
}
recognizer.setTranslation(CGPointZero, inView: self.view)
if recognizer.state == UIGestureRecognizerState.Ended {
// 1
let velocity = recognizer.velocityInView(self.view)
let magnitude = sqrt((velocity.x * velocity.x) + (velocity.y * velocity.y))
let slideMultiplier = magnitude / 200
// print("magnitude: \(magnitude), slideMultiplier: \(slideMultiplier)")
// 2
let slideFactor = 0.1 * slideMultiplier //Increase for more of a slide
// 3
var finalPoint = CGPoint(x:recognizer.view!.center.x + (velocity.x * slideFactor),
y:recognizer.view!.center.y + (velocity.y * slideFactor))
// 4
finalPoint.x = min(max(finalPoint.x, 0), self.view.bounds.size.width)
finalPoint.y = min(max(finalPoint.y, 0), self.view.bounds.size.height)
// 5
UIView.animateWithDuration(Double(slideFactor),
delay: 0,
// 6
options: UIViewAnimationOptions.CurveEaseOut,
animations: {recognizer.view!.center = finalPoint },
completion: nil)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
var modelController: CatalougeViewControllerModel {
// Return the model controller object, creating it if necessary.
// In more complex implementations, the model controller may be passed to the view controller.
if _modelController == nil {
_modelController = CatalougeViewControllerModel()
}
return _modelController!
}
var _modelController: CatalougeViewControllerModel? = nil
// MARK: - UIPageViewController delegate methods
func pageViewController(pageViewController: UIPageViewController, spineLocationForInterfaceOrientation orientation: UIInterfaceOrientation) -> UIPageViewControllerSpineLocation {
if (orientation == .Portrait) || (orientation == .PortraitUpsideDown) || (UIDevice.currentDevice().userInterfaceIdiom == .Phone) {
// In portrait orientation or on iPhone: Set the spine position to "min" and the page view controller's view controllers array to contain just one view controller. Setting the spine position to 'UIPageViewControllerSpineLocationMid' in landscape orientation sets the doubleSided property to true, so set it to false here.
let currentViewController = self.pageViewController!.viewControllers![0]
let viewControllers = [currentViewController]
self.pageViewController!.setViewControllers(viewControllers, direction: .Forward, animated: true, completion: {done in })
self.pageViewController!.doubleSided = false
return .Min
}
// In landscape orientation: Set set the spine location to "mid" and the page view controller's view controllers array to contain two view controllers. If the current page is even, set it to contain the current and next view controllers; if it is odd, set the array to contain the previous and current view controllers.
let currentViewController = self.pageViewController!.viewControllers![0] as! DataViewController
var viewControllers: [UIViewController]
let indexOfCurrentViewController = self.modelController.indexOfViewController(currentViewController)
if (indexOfCurrentViewController == 0) || (indexOfCurrentViewController % 2 == 0) {
let nextViewController = self.modelController.pageViewController(self.pageViewController!, viewControllerAfterViewController: currentViewController)
viewControllers = [currentViewController, nextViewController!]
} else {
let previousViewController = self.modelController.pageViewController(self.pageViewController!, viewControllerBeforeViewController: currentViewController)
viewControllers = [previousViewController!, currentViewController]
}
self.pageViewController!.setViewControllers(viewControllers, direction: .Forward, animated: true, completion: {done in })
return .Mid
}
}
Thats the ViewControllerModel (CatalougeViewControllerModel):
class CatalougeViewControllerModel: NSObject, UIPageViewControllerDataSource {
override init() {
super.init()
}
func viewControllerAtIndex(index: Int, storyboard: UIStoryboard) -> DataViewController? {
// Return the data view controller for the given index.
if (PDF3.imagePath.count == 0) || (index >= PDF3.imagePath.count) {
return nil
}
// Create a new view controller and pass suitable data.
let dataViewController = storyboard.instantiateViewControllerWithIdentifier("DataViewController") as! DataViewController
dataViewController.dataObject = PDF3.imagePath[index]
return dataViewController
}
func indexOfViewController(viewController: DataViewController) -> Int {
// Return the index of the given data view controller.
// For simplicity, this implementation uses a static array of model objects and the view controller stores the model object; you can therefore use the model object to identify the index.
return PDF3.imagePath.indexOf(viewController.dataObject!) ?? NSNotFound
}
// MARK: - Page View Controller Data Source Seiten werden hochgezählt
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
var index = self.indexOfViewController(viewController as! DataViewController)
if (index == 0) || (index == NSNotFound) {
return nil
}
index--
return self.viewControllerAtIndex(index, storyboard: viewController.storyboard!)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
var index = self.indexOfViewController(viewController as! DataViewController)
if index == NSNotFound {
return nil
}
index++
if index == PDF3.imagePath.count {
return nil
}
return self.viewControllerAtIndex(index, storyboard: viewController.storyboard!)
}
}
Thank you for your time to help me...
There exists a method on iOS that gets called whenever a device's memory gets constrained. By default, only the UIApplicationDelegate's gets called, but you can use NSNotificationCenter to add any class and/or object so they also get called.
When the method is called, it's a signal for your app to free some memory. You can then purge data that you don't need anymore.
I fixed my problem. So sorry guys the solution was in the DataViewController and I doesn´t posted my DataViewController file so anyway.
Here is my solution:
old DataViewController:
import UIKit
class DataViewController: UIViewController {
#IBOutlet weak var myImageView: UIImageView!
var dataObject: String?
override func viewDidLoad() {
super.viewDidLoad()
self.myImageView.image = UIImage(named: dataObject!)
// my Images was cached because I used "named:"
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Thats the new DataViewController:
import UIKit
class DataViewController: UIViewController {
#IBOutlet weak var myImageView: UIImageView!
var dataObject: String?
override func viewDidLoad() {
super.viewDidLoad()
autoreleasepool { () -> () in
self.myImageView.image = UIImage(contentsOfFile: dataObject!) // with "contentsOfFile:" the images are uncached...
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

Control "active" dot in UIPageControl

My designer is asking that I display 3 dots in a UIPageViewController for 10 views.
When the first 3 view controllers display, the 0th dot should be highlighted; when the next 4 view controllers display, the 1st dot should be highlighted; when the final 3 view controllers display, the 2nd dot should be highlighted.
So far I'm able to display 3 dots in the UIPageControl, but the indicator dot just rotates around indicating the n%3 position as active.
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return 3
}
I haven't seen any documentation on how to control with UIPageControl index is the active index, so I'm not sure if this is something Apple wants you to be able to override.
If there is a way to accomplish what I'm trying to do, I'd appreciate the help.
It turns out that what I'm trying to accomplish can't be done with a UIPageViewController. By default the UIPageControl in this class cannot be overridden directly.
Instead, I was able to use a combination of a UICollectionView (with a hack that allows it to resemble a UIPageViewController in its page changing effects) and a UIPageControl, as subviews to the same overarching UIViewController.
class MyPageViewController : UIViewController {
// MARK: subviews
private var collectionView:UICollectionView!
/// the collection layout controls the scrolling behavior of the collection view
private var collectionLayout = MyLayout()
private var pageControl = UIPageControl()
let CollectionViewCellReuseIdentifer = "CollectionViewCellReuseIdentifier"
// MARK: autolayout
private var autolayoutConstraints:[NSLayoutConstraint] = [NSLayoutConstraint]()
// MARK: constructors
init() {
super.init(nibName: nil, bundle: nil)
}
// MARK: UIViewController lifecycle methods
override func viewDidLoad() {
super.viewDidLoad()
self.setupView()
}
/**
Set up the collection view, page control, skip & log in buttons
*/
func setupView() {
self.setupCollectionView()
self.setupPageControl()
self.setupConstraints()
self.view.addConstraints(self.autolayoutConstraints)
}
/**
Set up the collection view
*/
func setupCollectionView() {
self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.collectionLayout)
self.collectionView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.registerClass(MyPageView.self, forCellWithReuseIdentifier: self.CollectionViewCellReuseIdentifer)
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.collectionView.backgroundColor = UIColor.whiteColor()
self.collectionView.scrollEnabled = true
self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast;
self.collectionLayout.minimumInteritemSpacing = 1
self.collectionLayout.minimumLineSpacing = 1
self.collectionLayout.scrollDirection = .Horizontal
self.collectionLayout.delegate = self
self.view.addSubview(self.collectionView)
}
/**
Set up view showing pagination dots for slideshow items
*/
func setupPageControl() {
self.pageControl.translatesAutoresizingMaskIntoConstraints = false
self.pageControl.numberOfPages = 3
self.pageControl.backgroundColor = UIColor.whiteColor()
self.view.addSubview(self.pageControl)
}
func setupConstraints() {
let views:[String:AnyObject] = [
"collectionView" : self.collectionView,
"pageControl" : self.pageControl,
]
self.autolayoutConstraints.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"V:|[collectionView][pageControl]|",
options: .AlignAllCenterX,
metrics: nil,
views: views
)
)
self.autolayoutConstraints.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|[collectionView]|",
options: .AlignAllCenterY,
metrics: nil,
views: views
)
)
self.autolayoutConstraints.appendContentsOf(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|[pageControl]|",
options: NSLayoutFormatOptions(),
metrics: nil,
views: views
)
)
}
}
extension MyPageViewController : MyPageViewControllerDelegate {
func didSwitchToPage(imageIndex: Int) {
if imageIndex < 3 {
self.pageControl.currentPage = 0
} else if imageIndex < 7 {
self.pageControl.currentPage = 1
} else {
self.pageControl.currentPage = 2
}
self.pageControl.setNeedsDisplay()
}
}
The layout class was derived from an answer my coworker found when researching a similar issue. http://karmadust.com/centered-paging-with-preview-cells-on-uicollectionview/
/**
* Delegate for slide interactions
*/
protocol MyPageViewControllerDelegate {
/**
Triggered when a new page has been 'snapped' into place
- parameter imageIndex: index of the image that has been snapped to
*/
func didSwitchToPage(imageIndex: Int)
}
class MyLayout : UICollectionViewFlowLayout {
var delegate:MyPageViewControllerDelegate?
/*
Allows different items in the collection to 'snap' onto the current screen section.
Based off of http://karmadust.com/centered-paging-with-preview-cells-on-uicollectionview/
*/
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let cv = self.collectionView {
let cvBounds = cv.bounds
let halfWidth = cvBounds.size.width * 0.5;
let proposedContentOffsetCenterX = proposedContentOffset.x + halfWidth;
if let attributesForVisibleCells = self.layoutAttributesForElementsInRect(cvBounds) {
var candidateAttributes : UICollectionViewLayoutAttributes?
// the index of the image selected
var index:Int = 0
for attributes in attributesForVisibleCells {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != UICollectionElementCategory.Cell {
index++
continue
}
if let candAttrs = candidateAttributes {
let a = attributes.center.x - proposedContentOffsetCenterX
let b = candAttrs.center.x - proposedContentOffsetCenterX
if fabsf(Float(a)) < fabsf(Float(b)) {
candidateAttributes = attributes;
}
}
else { // == First time in the loop == //
candidateAttributes = attributes;
index++
continue;
}
}
// Beautification step , I don't know why it works!
if(proposedContentOffset.x == -(cv.contentInset.left)) {
return proposedContentOffset
}
if let delegate = self.delegate {
delegate.didSwitchToPage((candidateAttributes?.indexPath.row)!)
}
return CGPoint(x: floor(candidateAttributes!.center.x - halfWidth), y: proposedContentOffset.y)
}
}
// fallback
return super.targetContentOffsetForProposedContentOffset(proposedContentOffset)
}
}
Note: I trimmed down the actual code I used and replaced a bunch of names to make them more appropriate for examples. I did not run this specific code and did not test for errors in my IDE. That being said, the approach behind the code is solid.

Resources