UIScrollView Reusable Views - ios

I would like to create a UIScrollView that cycles between 3 paging views infinitely while I change the subviews to hold different reusable view controllers giving the illusion of infinitely many screen while only allocating the resources for 3 screens at a time similar to a UICollectionView.
Below is an attempt I've made at implementing such a view based on this answer Infinite UIScrollView however my project has not been working as expected.
Ive been trying to use 3 views as the basis for infinite scrolling and many labels as the subviews to be added and removed as the user scrolls.
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
var scrollView:UIScrollView = {
let scrollView:UIScrollView = UIScrollView()
scrollView.backgroundColor = UIColor.orange
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
var labels:[CustomLabel] = [CustomLabel]()
var pages:[Page] = [Page]()
var visiblePages:Set<Page>!{
didSet{
print("visible pages count: \(recycledPages.count)")
}
}
var recycledPages:Set<Page>!{
didSet{
print("recycled pages count: \(recycledPages.count)")
}
}
var visibleLabels:Set<CustomLabel>!
var recycledLabels:Set<CustomLabel>!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
recycledPages = Set<Page>()
visiblePages = Set<Page>()
recycledLabels = Set<CustomLabel>()
visibleLabels = Set<CustomLabel>()
scrollView.contentSize = CGSize(width: view.bounds.width * 3, height: view.bounds.height)
scrollView.delegate = self
scrollView.isPagingEnabled = true
scrollView.indicatorStyle = .white
view.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
configureLabels()
setUpPages()
for i in 0..<pages.count{
scrollView.addSubview(pages[i])
}
}
func setUpPages(){
let page1 = Page(),page2 = Page(),page3 = Page()
page1.backgroundColor = .red
page2.backgroundColor = .green
page3.backgroundColor = .blue
page1.index = 0
page2.index = 1
page3.index = 2
pages = [page1,page2,page3]
visiblePages = [page1,page2,page3]
setContentViewFrames()
}
func configureLabels(){
let label1 = CustomLabel(), label2 = CustomLabel(), label3 = CustomLabel()
label1.text = "label1"
label2.text = "label2"
label3.text = "label3"
label1.backgroundColor = .red
label2.backgroundColor = .green
label3.backgroundColor = .blue
labels = [label1,label2,label3]
setContentViewFrames()
}
// func dequeueRecycledPage()->CustomLabel?{
// let page = recycledPages.first
// if let page = page{
// recycledPages.remove(page)
// return page
// }
// return nil
// }
var currentIndex = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.contentOffset.x < 0) {
let newOffset = CGPoint(x: scrollView.bounds.width + scrollView.contentOffset.x, y: 0)
scrollView.contentOffset.x = newOffset.x
rotateViewsRight()
}else if(scrollView.contentOffset.x > scrollView.bounds.size.width*2){
let newOffset = CGPoint(x: scrollView.contentOffset.x - scrollView.bounds.width, y: 0)
scrollView.contentOffset.x = newOffset.x
rotateViewsLeft()
}
print("current index: \(currentIndex)")
}
lazy var previousOffset:CGPoint = CGPoint()
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
print("currentIndex called")
if previousOffset.x < scrollView.contentOffset.x{
currentIndex += 1
}else if previousOffset.x > scrollView.contentOffset.x{
currentIndex -= 1
}
previousOffset = scrollView.contentOffset
}
func rotateViewsRight(){
let endView = pages.removeLast()
pages.insert(endView, at: 0)
setContentViewFrames()
}
func rotateViewsLeft(){
let endView = pages.removeFirst()
pages.append(endView)
setContentViewFrames()
}
func setContentViewFrames(){
for i in 0..<pages.count{
// let adjustedIndex = i % pages.count
// let view = pages[i]
// view.frame = CGRect(origin: CGPoint(x: view.bounds.width * CGFloat(i), y: 0), size: view.bounds.size)
pages[i].frame = CGRect(x: CGFloat(i)*view.bounds.width, y: 0, width: view.bounds.width, height: view.bounds.height)
let label = labels[currentIndex%labels.count]
label.text = "current label: \(currentIndex)"
label.frame = pages[i].frame
pages[i].addSubview(label)
}
}
func configurePage(forIndex index:Int){
}
func dequeueRecycledPage()->Page?{
let page = recycledPages.first
if let page = page{
recycledPages.remove(page)
return page
}
return nil
}
}

Related

How do I remove content that is no longer showing in a UIScrollView

I have created a modified paging UIScrollView. I have implemented lazy loaded pages however I would like to remove the content that is no longer being displayed on screen in order to conserve memory. Currently I only remove subviews from the UIScrollView but not content.
Below is the code that I have thus far how would go about removing content dynamically.
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
// MARK: - Properties
var scrollView: UIScrollView!
let numPages = 6
var pages = [UIView?]()
var imageViews:[UIImageView] = [UIImageView](){
didSet{
print("imageViews.count: \(imageViews.count)")
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
_ = setupInitialPages
}
override func viewDidLoad() {
super.viewDidLoad()
scrollView = UIScrollView(frame: view.bounds)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.isPagingEnabled = true
view.addSubview(self.scrollView)
scrollView.backgroundColor = UIColor.blue
scrollView.delegate = self
scrollView.indicatorStyle = .white
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: self.view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
pages = [UIView?](repeating: nil, count: numPages)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Initial Setup
lazy var setupInitialPages: Void = {
adjustScrollView()
loadPage(0)
loadPage(1)
}()
// MARK: - Utilities
fileprivate func removeAnyImages() {
for page in pages where page != nil {
page?.removeFromSuperview()
}
}
fileprivate func adjustScrollView() {
scrollView.contentSize =
CGSize(width: scrollView.frame.width * CGFloat(numPages),
height: scrollView.frame.height - topLayoutGuide.length)
}
func getImage(page:Int)->UIImage{
let resourceName = String(page + 1)
return #imageLiteral(resourceName: resourceName)
}
fileprivate func loadPage(_ page: Int) {
guard page < numPages && page != -1 else { return }
if pages[page] == nil {
let newImageView = getRecycledImageView()
newImageView.image = getImage(page: page)
newImageView.contentMode = .scaleAspectFit
newImageView.backgroundColor = UIColor.yellow
var frame = scrollView.frame
frame.origin.x = frame.width * CGFloat(page)
frame.origin.y = -self.topLayoutGuide.length
frame.size.height += self.topLayoutGuide.length
let canvasView = UIView(frame: frame)
scrollView.addSubview(canvasView)
newImageView.translatesAutoresizingMaskIntoConstraints = false
canvasView.addSubview(newImageView)
NSLayoutConstraint.activate([
(newImageView.leadingAnchor.constraint(equalTo: canvasView.leadingAnchor)),
(newImageView.trailingAnchor.constraint(equalTo: canvasView.trailingAnchor)),
(newImageView.topAnchor.constraint(equalTo: canvasView.topAnchor)),
(newImageView.bottomAnchor.constraint(equalTo: canvasView.bottomAnchor))
])
pages[page] = canvasView
// }
}
}
func getRecycledImageView()->UIImageView{
let unusedImageViews = imageViews.filter { (imageView) -> Bool in
return imageView.isDescendant(of: scrollView) == false
}
if let unusedImageView = unusedImageViews.first{
print("reusing imageView")
return unusedImageView
}else{
let imageView = UIImageView()
imageViews.append(imageView)
return imageView
}
}
fileprivate func loadCurrentPages(page: Int) {
guard (page > 0 && page + 1 < numPages) || transitioning else { return }
// Remove all of the images and start over.
removeAnyImages()
pages = [UIView?](repeating: nil, count: numPages)
loadPage(Int(page) - 1)
loadPage(Int(page))
loadPage(Int(page) + 1)
}
fileprivate func gotoPage(page: Int, animated: Bool) {
loadCurrentPages(page: page)
var bounds = scrollView.bounds
bounds.origin.x = bounds.width * CGFloat(page)
bounds.origin.y = 0
scrollView.scrollRectToVisible(bounds, animated: animated)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageWidth = scrollView.frame.width
let page = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1
loadCurrentPages(page: Int(page))
}
}

Not able to remove custom UIView from SuperView

This is extremely odd. I am trying to remove the view from superview when I drag the view to either left or right. If the view doesn't contain any subviews then I am easily able to remove the view from the superView by using this card.removeFromSuperview() - however, what I have noticed is that if add two views as subviews inside the card view then I am not able to remove it from superView and the entire thing goes bezerk.
Here is the card view class:
class MainSwipeCardView: UIView {
//MARK: - Properties
var swipeView = UIView()
var shadowView = UIView()
var text: String?
var label = UILabel()
var bgColor : UIColor? {
didSet {
swipeView.backgroundColor = bgColor
}
}
var cardsarraydata : CardDataModel? {
didSet {
bgColor = cardsarraydata?.backgroundColor
label.text = cardsarraydata?.title
}
}
var delegate : CardDelegate?
//MARK :- Init
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .clear
configureShadowView()
configureSwipeView()
configureLabelView()
addPanGestureOnCards()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Configurations
func configureShadowView() {
shadowView.backgroundColor = .clear
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
shadowView.layer.shadowOpacity = 0.8
shadowView.layer.shadowRadius = 4.0
addSubview(shadowView)
shadowView.translatesAutoresizingMaskIntoConstraints = false
shadowView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
shadowView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
shadowView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
shadowView.topAnchor.constraint(equalTo: topAnchor).isActive = true
}
func configureSwipeView() {
swipeView.layer.cornerRadius = 15
swipeView.clipsToBounds = true
shadowView.addSubview(swipeView)
swipeView.translatesAutoresizingMaskIntoConstraints = false
swipeView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
swipeView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
swipeView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
swipeView.topAnchor.constraint(equalTo: topAnchor).isActive = true
}
func configureLabelView() {
swipeView.addSubview(label)
label.backgroundColor = .white
label.textColor = .black
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 18)
label.translatesAutoresizingMaskIntoConstraints = false
label.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
label.heightAnchor.constraint(equalToConstant: 85).isActive = true
}
func addPanGestureOnCards() {
self.isUserInteractionEnabled = true
addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture)))
}
//MARK: - Handlers
#objc func handlePanGesture(sender: UIPanGestureRecognizer){
let card = sender.view as! MainSwipeCardView
let point = sender.translation(in: self)
let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y)
switch sender.state {
case .ended:
if (card.center.x) > 400 {
delegate?.swipeDidEnd(on: card)
UIView.animate(withDuration: 0.2) {
card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75)
card.alpha = 0
self.layoutIfNeeded()
}
return
}else if card.center.x < -115 {
delegate?.swipeDidEnd(on: card)
UIView.animate(withDuration: 0.2) {
card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75)
card.alpha = 0
self.layoutIfNeeded()
}
return
}
UIView.animate(withDuration: 0.2) {
card.transform = .identity
card.center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
self.layoutIfNeeded()
}
default:
break
}
}
In this subclass I have two UIViews, I am adding the views sequentially. Then on swipeView I am adding the text and label and background color. This is how the cards look like:
I am also using a UIPanInteraction on it and so if I drag it to left or right, then I call the delegate which removes the entire MainSwipeCardView from the container view. In doing so this is what happens:
It keeps adding more and more in the background even though this is what I am calling in the delegate function:
func swipeDidEnd(on card: MainSwipeCardView) {
card.removeFromSuperview()
print(visibleCards.count)
}
The visibleCards is essentially an array of subviews in the container view. It should decrease so for example from 3 -> 2 -> 1; but it increases in non linear way ( not able to really get a relationship out of it)
The most confusing thing is that I am actually able to run this whole code just fine if I donot add the SwipeView and shadowView properties inside the custom view and just use the customView itself to house the label and the backgroundColor. When I add these two properties, then this whole thing seem to go haywire.
Please any kind of help will be extremely appreciated. Thanks!
ContainerView code is as follows:
class SwipeCardContainerView: UIView, CardDelegate {
//MARK: - Properties
var numberOfCards: Int = 0
var remainingCards: Int = 0
var cardsView : [MainSwipeCardView] = []
var numberOfAllowedCard: Int = 3
let horizontalInset: CGFloat = 8.0
let verticalInset: CGFloat = 8.0
var visibleCards : [MainSwipeCardView] {
return subviews as? [MainSwipeCardView] ?? []
}
var datasource : CardDataSource? {
didSet {
loadData()
}
}
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Configuration
func loadData() {
guard let datasource = datasource else { return }
numberOfCards = datasource.numberOfCards()
remainingCards = numberOfCards
for i in 0..<min(numberOfCards,numberOfAllowedCard) {
addCardView(at: i, card: datasource.createCard(at: i))
}
setNeedsLayout()
}
func addCardView(at index: Int, card: MainSwipeCardView) {
card.delegate = self
addCardFrame(index: index, cardView: card)
cardsView.append(card)
insertSubview(card, at: 0)
remainingCards -= 1
}
func addCardFrame(index: Int, cardView: MainSwipeCardView){
cardsView.append(cardView)
var cardViewFrame = bounds
let horizontalInset = (CGFloat(index) * self.horizontalInset)
let verticalInset = CGFloat(index) * self.verticalInset
cardViewFrame.size.width -= 2 * horizontalInset
cardViewFrame.origin.x += horizontalInset
cardViewFrame.origin.y += verticalInset
cardView.frame = cardViewFrame
}
// Delegate Method
func swipeDidEnd(on card: MainSwipeCardView) {
card.removeFromSuperview()
print(visibleCards.count)
}
Main ViewController Code:
class ViewController: UIViewController {
//MARK: - Properties
var stackContainer : SwipeCardContainerView!
var cardDataArray : [CardDataModel] = [CardDataModel(backgroundColor: .orange, title: "Hello"),
CardDataModel(backgroundColor: .red, title: "Red"),
CardDataModel(backgroundColor: .blue, title: "Blue"),
CardDataModel(backgroundColor: .orange, title: "Orange")]
//MARK: - Init
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red:0.93, green:0.93, blue:0.93, alpha:1.0)
stackContainer = SwipeCardContainerView()
view.addSubview(stackContainer)
configureSwipeContainerView()
stackContainer.translatesAutoresizingMaskIntoConstraints = false
}
//MARK : - Configurations
func configureSwipeContainerView() {
stackContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50).isActive = true
stackContainer.widthAnchor.constraint(equalToConstant: 300).isActive = true
stackContainer.heightAnchor.constraint(equalToConstant: 350).isActive = true
}
override func viewDidLayoutSubviews() {
stackContainer.datasource = self
}
//MARK : - Handlers
}
extension ViewController : CardDataSource {
func numberOfCards() -> Int {
return cardDataArray.count
}
func createCard(at index: Int) -> MainSwipeCardView {
let card = MainSwipeCardView()
card.cardsarraydata = cardDataArray[index]
return card
}
func emptyCard() -> UIView? {
return nil
}
}
I've investigated the problem.
First issue is in the ViewController:
override func viewDidLayoutSubviews() {
stackContainer.datasource = self
}
Just remove this code. In each layout you set datasource... and loadData... this is incorrect approach, also super.viewDidLayoutSubviews() is missing...
And also stackContainer.datasource = self:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red:0.93, green:0.93, blue:0.93, alpha:1.0)
stackContainer = SwipeCardContainerView()
view.addSubview(stackContainer)
configureSwipeContainerView()
stackContainer.translatesAutoresizingMaskIntoConstraints = false
stackContainer.datasource = self
Second issue is in func loadData(), just replace it with
func loadData() {
guard let datasource = datasource else { return }
setNeedsLayout()
layoutIfNeeded()
numberOfCards = datasource.numberOfCards()
remainingCards = numberOfCards
for i in 0..<min(numberOfCards,numberOfAllowedCard) {
addCardView(at: i, card: datasource.createCard(at: i))
}
}
or find better solution with layout of SwipeCardContainerView

SwiftPages updateUI Does Not Work with Swift 3

I'm using Swiftpages. When app is opened it looks like first picture.
But app goes to background and opened different app on device, after open again my app it looks like second picture.
I updated to Swift 3, but I can't figure out the issue, I write about it on Github but no reply from them.
public class SwiftPages: UIView {
private lazy var token = 0
var containerVieww: UIView!
private var scrollView: UIScrollView!
private var topBar: UIView!
var animatedBar: UIView!
var viewControllerIDs = [String]()
private var buttonTitles = [String]()
private var buttonImages = [UIImage]()
private var pageViews = [UIViewController?]()
private var currentPage: Int = 0
// Container view position variables
private var xOrigin: CGFloat = 0
private var yOrigin: CGFloat = 64
private var distanceToBottom: CGFloat = 0
// Color variables
private var animatedBarColor = UIColor(red: 28/255, green: 95/255, blue: 185/255, alpha: 1)
private var topBarBackground = UIColor.white
private var buttonsTextColor = UIColor.gray
private var containerViewBackground = UIColor.white
// Item size variables
private var topBarHeight: CGFloat = 52
private var animatedBarHeight: CGFloat = 3
// Bar item variables
private var aeroEffectInTopBar = false //This gives the top bap a blurred effect, also overlayes the it over the VC's
private var buttonsWithImages = false
var barShadow = true
private var shadowView : UIView!
private var shadowViewGradient = CAGradientLayer()
private var buttonsTextFontAndSize = UIFont(name: "HelveticaNeue-Light", size: 20)!
private var blurView : UIVisualEffectView!
private var barButtons = [UIButton?]()
// MARK: - Positions Of The Container View API -
public func setOriginX (origin : CGFloat) { xOrigin = origin }
public func setOriginY (origin : CGFloat) { yOrigin = origin }
public func setDistanceToBottom (distance : CGFloat) { distanceToBottom = distance }
// MARK: - API's -
public func setAnimatedBarColor (color : UIColor) { animatedBarColor = color }
public func setTopBarBackground (color : UIColor) { topBarBackground = color }
public func setButtonsTextColor (color : UIColor) { buttonsTextColor = color }
public func setContainerViewBackground (color : UIColor) { containerViewBackground = color }
public func setTopBarHeight (pointSize : CGFloat) { topBarHeight = pointSize}
public func setAnimatedBarHeight (pointSize : CGFloat) { animatedBarHeight = pointSize}
public func setButtonsTextFontAndSize (fontAndSize : UIFont) { buttonsTextFontAndSize = fontAndSize}
public func enableAeroEffectInTopBar (boolValue : Bool) { aeroEffectInTopBar = boolValue}
public func enableButtonsWithImages (boolValue : Bool) { buttonsWithImages = boolValue}
public func enableBarShadow (boolValue : Bool) { barShadow = boolValue}
override public func draw(_ rect: CGRect) {
DispatchQueue.main.async {
let pagesContainerHeight = self.frame.height - self.yOrigin - self.distanceToBottom
let pagesContainerWidth = self.frame.width
// Set the notifications for an orientation change & BG mode
let defaultNotificationCenter = NotificationCenter.default
defaultNotificationCenter.addObserver(self, selector: #selector(SwiftPages.applicationWillEnterBackground), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
defaultNotificationCenter.addObserver(self, selector: #selector(SwiftPages.orientationWillChange), name: NSNotification.Name.UIApplicationWillChangeStatusBarOrientation, object: nil)
defaultNotificationCenter.addObserver(self, selector: #selector(SwiftPages.orientationDidChange), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
defaultNotificationCenter.addObserver(self, selector: #selector(SwiftPages.applicationWillEnterForeground), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
// Set the containerView, every item is constructed relative to this view
self.containerVieww = UIView(frame: CGRect(x: self.xOrigin, y: self.yOrigin, width: pagesContainerWidth, height: pagesContainerHeight))
self.containerVieww.backgroundColor = self.containerViewBackground
self.containerVieww.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.containerVieww)
//Add the constraints to the containerView.
if #available(iOS 9.0, *) {
let horizontalConstraint = self.containerVieww.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let verticalConstraint = self.containerVieww.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let widthConstraint = self.containerVieww.widthAnchor.constraint(equalTo: self.widthAnchor)
let heightConstraint = self.containerVieww.heightAnchor.constraint(equalTo: self.heightAnchor)
NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint, widthConstraint, heightConstraint])
}
// Set the scrollview
if self.aeroEffectInTopBar {
self.scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: self.containerVieww.frame.size.width, height: self.containerVieww.frame.size.height))
} else {
self.scrollView = UIScrollView(frame: CGRect(x: 0, y: self.topBarHeight, width: self.containerVieww.frame.size.width, height: self.containerVieww.frame.size.height - self.topBarHeight))
}
self.scrollView.isPagingEnabled = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.delegate = self
self.scrollView.backgroundColor = UIColor.clear
self.scrollView.contentOffset = CGPoint(x: 0, y: 0)
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
self.scrollView.isScrollEnabled = false
self.containerVieww.addSubview(self.scrollView)
// Add the constraints to the scrollview.
if #available(iOS 9.0, *) {
let leadingConstraint = self.scrollView.leadingAnchor.constraint(equalTo: self.containerVieww.leadingAnchor)
let trailingConstraint = self.scrollView.trailingAnchor.constraint(equalTo: self.containerVieww.trailingAnchor)
let topConstraint = self.scrollView.topAnchor.constraint(equalTo: self.containerVieww.topAnchor)
let bottomConstraint = self.scrollView.bottomAnchor.constraint(equalTo: self.containerVieww.bottomAnchor)
NSLayoutConstraint.activate([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])
}
// Set the top bar
self.topBar = UIView(frame: CGRect(x: 0, y: 0, width: self.containerVieww.frame.size.width, height: self.topBarHeight))
self.topBar.backgroundColor = self.topBarBackground
if self.aeroEffectInTopBar {
// Create the blurred visual effect
// You can choose between ExtraLight, Light and Dark
self.topBar.backgroundColor = UIColor.clear
let blurEffect: UIBlurEffect = UIBlurEffect(style: .light)
self.blurView = UIVisualEffectView(effect: blurEffect)
self.blurView.frame = self.topBar.bounds
self.blurView.translatesAutoresizingMaskIntoConstraints = false
self.topBar.addSubview(self.blurView)
}
self.topBar.translatesAutoresizingMaskIntoConstraints = false
self.containerVieww.addSubview(self.topBar)
// Set the top bar buttons
// Check to see if the top bar will be created with images ot text
if self.buttonsWithImages {
var buttonsXPosition: CGFloat = 0
for (index, image) in self.buttonImages.enumerated() {
let frame = CGRect(x: buttonsXPosition, y: 0, width: self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count), height: self.topBarHeight)
let barButton = UIButton(frame: frame)
barButton.backgroundColor = UIColor.clear
barButton.imageView?.contentMode = .scaleAspectFit
barButton.setImage(image, for: .normal)
barButton.tag = index
barButton.addTarget(self, action: #selector(SwiftPages.barButtonAction), for: .touchUpInside)
self.topBar.addSubview(barButton)
self.barButtons.append(barButton)
buttonsXPosition += self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count)
}
} else {
var buttonsXPosition: CGFloat = 0
for (index, title) in self.buttonTitles.enumerated() {
let frame = CGRect(x: buttonsXPosition, y: 0, width: self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count), height: self.topBarHeight)
let barButton = UIButton(frame: frame)
barButton.backgroundColor = UIColor.clear
barButton.titleLabel!.font = self.buttonsTextFontAndSize
barButton.setTitle(title, for: .normal)
barButton.setTitleColor(self.buttonsTextColor, for: .normal)
barButton.tag = index
barButton.addTarget(self, action: #selector(SwiftPages.barButtonAction), for: .touchUpInside)
self.topBar.addSubview(barButton)
self.barButtons.append(barButton)
buttonsXPosition += self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count)
}
}
// Set up the animated UIView
self.animatedBar = UIView(frame: CGRect(x: 0, y: self.topBarHeight - self.animatedBarHeight + 1, width: (self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count)) * 0.8, height: self.animatedBarHeight))
self.animatedBar.center.x = self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count << 1)
self.animatedBar.backgroundColor = self.animatedBarColor
self.containerVieww.addSubview(self.animatedBar)
// Add the bar shadow (set to true or false with the barShadow var)
if self.barShadow {
self.shadowView = UIView(frame: CGRect(x: 0, y: self.topBarHeight, width: self.containerVieww.frame.size.width, height: 4))
self.shadowViewGradient.frame = self.shadowView.bounds
self.shadowViewGradient.colors = [UIColor(red: 150/255, green: 150/255, blue: 150/255, alpha: 0.28).cgColor, UIColor.clear.cgColor]
self.shadowView.layer.insertSublayer(self.shadowViewGradient, at: 0)
self.containerVieww.addSubview(self.shadowView)
}
let pageCount = self.viewControllerIDs.count
// Fill the array containing the VC instances with nil objects as placeholders
for _ in 0..<pageCount {
self.pageViews.append(nil)
}
// Defining the content size of the scrollview
let pagesScrollViewSize = self.scrollView.frame.size
self.scrollView.contentSize = CGSize(width: pagesScrollViewSize.width * CGFloat(pageCount), height: pagesScrollViewSize.height)
// Load the pages to show initially
self.loadVisiblePages()
// Do the initial alignment of the subViews
self.alignSubviews()
}
}
// MARK: - Initialization Functions -
public func initializeWithVCIDsArrayAndButtonTitlesArray (VCIDsArray: [String], buttonTitlesArray: [String]) {
// Important - Titles Array must Have The Same Number Of Items As The viewControllerIDs Array
if VCIDsArray.count == buttonTitlesArray.count {
viewControllerIDs = VCIDsArray
buttonTitles = buttonTitlesArray
buttonsWithImages = false
} else {
print("Initilization failed, the VC ID array count does not match the button titles array count.")
}
}
public func initializeWithVCIDsArrayAndButtonImagesArray (VCIDsArray: [String], buttonImagesArray: [UIImage]) {
// Important - Images Array must Have The Same Number Of Items As The viewControllerIDs Array
if VCIDsArray.count == buttonImagesArray.count {
viewControllerIDs = VCIDsArray
buttonImages = buttonImagesArray
buttonsWithImages = true
} else {
print("Initilization failed, the VC ID array count does not match the button images array count.")
}
}
public func loadPage(page: Int) {
// If it's outside the range of what you have to display, then do nothing
guard page >= 0 && page < viewControllerIDs.count else { return }
// Do nothing if the view is already loaded.
guard pageViews[page] == nil else { return }
print("Loading Page \(page)")
// The pageView instance is nil, create the page
var frame = scrollView.bounds
frame.origin.x = frame.size.width * CGFloat(page)
frame.origin.y = 0.0
// Look for the VC by its identifier in the storyboard and add it to the scrollview
let newPageView = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: viewControllerIDs[page])
newPageView.view.frame = frame
scrollView.addSubview(newPageView.view)
// Replace the nil in the pageViews array with the VC just created
pageViews[page] = newPageView
}
public func loadVisiblePages() {
// First, determine which page is currently visible
let pageWidth = scrollView.frame.size.width
let page = Int(floor((scrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth * 2.0)))
// Work out which pages you want to load
let firstPage = page - 1
let lastPage = page + 1
// Load pages in our range
for index in firstPage...lastPage {
loadPage(page: index)
}
}
public func barButtonAction(sender: UIButton?) {
let index = sender!.tag
let pagesScrollViewSize = scrollView.frame.size
scrollView.setContentOffset(CGPoint(x: pagesScrollViewSize.width * CGFloat(index), y: 0), animated: true)
currentPage = index
}
// MARK: - Orientation Handling Functions -
public func alignSubviews() {
let pageCount = viewControllerIDs.count
// Setup the new frames
scrollView.contentSize = CGSize(width: CGFloat(pageCount) * scrollView.bounds.size.width, height: scrollView.bounds.size.height)
topBar.frame = CGRect(x: 0, y: 0, width: containerVieww.frame.size.width, height: topBarHeight)
blurView?.frame = topBar.bounds
animatedBar.frame.size = CGSize(width: (containerVieww.frame.size.width / (CGFloat)(viewControllerIDs.count)) * 0.8, height: animatedBarHeight)
if barShadow {
shadowView.frame.size = CGSize(width: containerVieww.frame.size.width, height: 4)
shadowViewGradient.frame = shadowView.bounds
}
// Set the new frame of the scrollview contents
for (index, controller) in pageViews.enumerated() {
controller?.view.frame = CGRect(x: CGFloat(index) * scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
}
// Set the new frame for the top bar buttons
var buttonsXPosition: CGFloat = 0
for button in barButtons {
button?.frame = CGRect(x: buttonsXPosition, y: 0, width: containerVieww.frame.size.width / CGFloat(viewControllerIDs.count), height: topBarHeight)
buttonsXPosition += containerVieww.frame.size.width / CGFloat(viewControllerIDs.count)
}
}
func applicationWillEnterBackground() {
//Save the current page
currentPage = Int(scrollView.contentOffset.x / scrollView.bounds.size.width)
print("Haydar")
}
func orientationWillChange() {
//Save the current page
currentPage = Int(scrollView.contentOffset.x / scrollView.bounds.size.width)
}
func orientationDidChange() {
//Update the view
alignSubviews()
scrollView.contentOffset = CGPoint(x: CGFloat(currentPage) * scrollView.frame.size.width, y: 0)
}
func applicationWillEnterForeground() {
alignSubviews()
scrollView.contentOffset = CGPoint(x: CGFloat(currentPage) * scrollView.frame.size.width, y: 0)
initializeWithVCIDsArrayAndButtonTitlesArray(VCIDsArray: buttonTitles, buttonTitlesArray: buttonTitles)
print("ForegroundHound")
}
public func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let previousPage : NSInteger = currentPage
let pageWidth : CGFloat = scrollView.frame.size.width
let fractionalPage = scrollView.contentOffset.x / pageWidth
let page : NSInteger = Int(round(fractionalPage))
if (previousPage != page) {
currentPage = page;
}
}
deinit {
NotificationCenter.default.removeObserver(self)
print("deinittta")
}
}
extension SwiftPages: UIScrollViewDelegate {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Load the pages that are now on screen
loadVisiblePages()
// The calculations for the animated bar's movements
// The offset addition is based on the width of the animated bar (button width times 0.8)
let offsetAddition = (containerVieww.frame.size.width / CGFloat(viewControllerIDs.count)) * 0.1
animatedBar.frame = CGRect(x: (offsetAddition + (scrollView.contentOffset.x / CGFloat(viewControllerIDs.count))), y: animatedBar.frame.origin.y, width: animatedBar.frame.size.width, height: animatedBar.frame.size.height)
}
}

Paging UIScrollView seems to place content off centre

I have a UIScrollView that I want to have paging functionality (think an initial splash screen). I want that content (a UILabel and a UIImageView) to be placed centrally in each paging view on the scrollView. My problem is is that it is always slightly off centre ().
Here is the complete code:
var splashScreenObjects = [SplashScreenObject]()
var imageViewArray = [UIImageView]()
var subtitleViewArray = [UILabel]()
#IBOutlet var scrollView: UIScrollView!
#IBOutlet var pageControl: UIPageControl!
override func viewDidLoad() {
super.viewDidLoad()
createSplashScreenObjects()
configurePageControl()
configureScrollView()
}
func createSplashScreenObjects() {
let firstScreen: SplashScreenObject = SplashScreenObject(subtitle: "Medication reminders on your phone. Never miss your next dose", image: UIImage(named: "splashScreen1")!)
let secondScreen: SplashScreenObject = SplashScreenObject(subtitle: "Track how good you have been with your medication", image: UIImage(named: "splashScreen2")!)
let thirdScreen: SplashScreenObject = SplashScreenObject(subtitle: "The better you are with your medication, the more points you'll earn!", image: UIImage(named: "splashScreen3")!)
splashScreenObjects.append(firstScreen)
splashScreenObjects.append(secondScreen)
splashScreenObjects.append(thirdScreen)
}
func configureScrollView() {
self.scrollView.layoutIfNeeded()
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.pagingEnabled = true
self.scrollView.delegate = self
let width = view.frame.size.width
for index in 0..<splashScreenObjects.count {
let subtitle = UILabel(frame: CGRectMake((width * CGFloat(index)) + 25, self.scrollView.frame.size.height-75, width-50, 75))
subtitle.text = splashScreenObjects[index].subtitle
subtitle.textAlignment = NSTextAlignment.Center
subtitle.textColor = UIColor.whiteColor()
subtitle.font = UIFont(name:"Ubuntu", size: 16)
subtitle.numberOfLines = 2
subtitle.backgroundColor = UIColor.clearColor()
self.scrollView.addSubview(subtitle)
self.subtitleViewArray.append(subtitle)
subtitle.alpha = 0
let mainImage = UIImageView(frame: CGRectMake((width * CGFloat(index)), 50, width, self.scrollView.frame.size.height-150))
mainImage.image = splashScreenObjects[index].image
mainImage.contentMode = UIViewContentMode.ScaleAspectFit
self.scrollView.addSubview(mainImage)
self.imageViewArray.append(mainImage)
mainImage.alpha = 0
}
self.scrollView.contentSize = CGSizeMake(width * CGFloat(splashScreenObjects.count), self.scrollView.frame.size.height-50)
animateViews(Int(0))
}
func configurePageControl() {
self.pageControl.numberOfPages = splashScreenObjects.count
self.pageControl.currentPage = 0
self.view.addSubview(pageControl)
pageControl.addTarget(self, action: #selector(SplashViewController.changePage(_:)), forControlEvents: UIControlEvents.ValueChanged)
}
func changePage(sender: AnyObject) -> () {
let x = CGFloat(pageControl.currentPage) * self.view.frame.size.width
scrollView.setContentOffset(CGPointMake(x, 0), animated: true)
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let pageNumber = round(scrollView.contentOffset.x / self.view.frame.size.width)
pageControl.currentPage = Int(pageNumber)
animateViews(Int(pageNumber))
}
func animateViews(pageNumber: Int) {
UIView.animateWithDuration(0.5, animations: {
self.imageViewArray[pageNumber].alpha = 1.0
self.subtitleViewArray[pageNumber].alpha = 1.0
})
}
Here are my auto layout constraints for the UIScrollView:
Your leading and trailing spaces are both -20, which means that the scroll view is 40 points wider than its superview. Change these to 0.
You should replace
self.scrollView.layoutIfNeeded()
to
self.view.layoutIfNeeded()
because layoutIfNeeded layout caller subviews, not itself. So, scrollView, when you add subtitle and mainImage on it, has wrong frame.

Horizontal scrolling with parallax background for ios

I'm facing a challenge of creating an introduction view, something like the "Cleanio" app (https://itunes.apple.com/fr/app/cleanio-pressing-la-demande/id885856031?mt=8).
Here is how it looks like:
So, the background and the overlay are moving independently and not in the same speed.
Does anyone have a start point how to realize that?
Yep.
What you need is two UIScrollViews. These should both be subviews of the main view (not contained in each other.
The bottom one has your image in it and the top one has the content.
Call them imageScrollView and contentScrollView.
Become the delegate of contentScrollView.
The contents will look something like this...
contents: [---page 1---][---page 2---][---page 3---][---page 4---]
image: [------------the image------------]
screen: [---screen---]
Key is that image is smaller than all the pages and bigger than the screen.
The frames of the scrollviews are the same width as the screen. This diagram is just to show the content widths not the frame widths.
So, the screen stays where it is and the two scroll views move over it.
Now the parallax part...
- (CGFloat)maxOffsetForScrollView:(UIScrollView *)scrollView
{
CGFloat contentWidth = scrollView.contentSize.width;
CGFloat frameWidth = CGRectGetWidth(scrollView.frame);
return contentWidth - frameWidth;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// this is the delegate method for the content scroll view.
// I'm only doing horizontal stuff here, you can do vertical too if you want
CGFloat maximumContentOffset = [self maximumOffsetForScrollView:self.contentScrollView];
CGFloat currentOffset = self.contentScrollView.contentOffset.x;
CGFloat percentageOffset = currentOffset/maximumContentOffset;
CGFloat maximumImageOffset = [self maximumContentOffsetForScrollView:self.imageScrollView];
CGFloat actualImageOffset = maximumImageOffset * percentageOffset;
[self.imageScrollView setContentOffset:CGPointMake(actualImageOffset, 0)];
}
This takes the percentage offset from the content view and offsets the image view by the same percentage offset.
The result is a parallax effect. You can make it faster or slower by changing the relative sizes of the image and the content. More pages (or smaller image) = slower parallax.
I had been struggling with this parallax effect for a little while. I wouldn't have done it without #Fogmeister instructions, though I still had to figure out a couple of things by myself.
Anyway, here is a Swift 2.0 version, (hopefully a bit more complete):
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
var backgroundScrollView: UIScrollView!
var contentScrollView: UIScrollView!
var imageView: UIImageView!
var contentView: UIView!
var maxContentOffset: CGFloat!
var maxBackgroundOffset: CGFloat!
var cvbw: CGFloat!
var page: Int = 1
override func viewDidLoad() {
super.viewDidLoad()
self.createBackground()
self.createContent()
}
func createBackground() {
self.imageView = UIImageView(image: UIImage(named: "ParalaxMaterial.jpg")) //ParalaxMaterial.jpg is of size: 2500 x 2668px
self.imageView.frame = CGRectMake(0, 0, ((self.view.bounds.height/2668) * 2500), self.view.bounds.height)
self.backgroundScrollView = UIScrollView(frame: view.bounds)
self.backgroundScrollView.backgroundColor = UIColor.redColor()
self.backgroundScrollView.contentSize = imageView.bounds.size
self.backgroundScrollView.addSubview(self.imageView)
self.view.addSubview(self.backgroundScrollView)
self.maxBackgroundOffset = self.maxOffsetForScrollView(self.backgroundScrollView)
}
func createContent() {
self.contentView = UIView(frame: CGRectMake(0, 0, (self.view.bounds.width * 3), self.view.bounds.height))
self.contentScrollView = UIScrollView(frame: view.bounds)
self.contentScrollView.backgroundColor = UIColor.clearColor()
self.contentScrollView.contentSize = self.contentView.bounds.size
self.contentScrollView.delegate = self
let firstButton = UIButton()
firstButton.frame = CGRectMake(((self.contentView.bounds.width / 6) - 150), 300, 300, 100)
firstButton.setTitle("START", forState: UIControlState.Normal)
firstButton.titleLabel?.font = UIFont(name: "Arial", size: 18)
firstButton.addTarget(self, action: "firstAction:", forControlEvents: UIControlEvents.TouchUpInside)
self.contentView.addSubview(firstButton)
let firstLabel = UILabel()
firstLabel.frame = CGRectMake(((self.contentView.bounds.width / 6) - 100), 0, 200, 200)
firstLabel.text = "#BrusselsLockDown"
firstLabel.textAlignment = NSTextAlignment.Center
self.contentView.addSubview(firstLabel)
let secondLabel = UILabel()
secondLabel.frame = CGRectMake(((self.contentView.bounds.width / 2) - 100), 0, 200, 200)
secondLabel.text = "#LolCats"
secondLabel.textAlignment = NSTextAlignment.Center
self.contentView.addSubview(secondLabel)
let thirdLabel = UILabel()
thirdLabel.frame = CGRectMake((((self.contentView.bounds.width / 6) * 5) - 100), 0, 200, 200)
thirdLabel.text = "#Final"
thirdLabel.textAlignment = NSTextAlignment.Center
self.contentView.addSubview(thirdLabel)
self.contentScrollView.addSubview(self.contentView)
self.view.addSubview(self.contentScrollView)
self.maxContentOffset = self.maxOffsetForScrollView(self.contentScrollView)
self.cvbw = self.contentView.bounds.width
}
func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {
if self.page == 1 {
if scrollView.contentOffset.x > 0 {
scrollView.setContentOffset(CGPointMake((self.cvbw / 3), 0), animated: true)
}
} else if self.page == 2 {
if scrollView.contentOffset.x < (self.cvbw * 4/12) {
scrollView.setContentOffset(CGPointMake(0, 0), animated: true)
} else if scrollView.contentOffset.x > (self.cvbw * 4/12) {
scrollView.setContentOffset(CGPointMake(((self.cvbw / 3) * 2), 0), animated: true)
}
} else if self.page == 3 {
if scrollView.contentOffset.x < (self.cvbw * 8/12) {
scrollView.setContentOffset(CGPointMake((self.cvbw / 3), 0), animated: true)
}
} else {
print("self.page messed up")
}
}
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView == self.contentScrollView {
let percentageOffset: CGFloat = self.contentScrollView.contentOffset.x / self.maxContentOffset
let currentBackgroundOffsetPoint: CGPoint = CGPointMake(((self.maxBackgroundOffset * percentageOffset) + 50), 0) self.backgroundScrollView.setContentOffset(currentBackgroundOffsetPoint, animated: false)
}
if self.contentScrollView.contentOffset.x == 0 {
print("page 1")
self.page = 1
} else if self.contentScrollView.contentOffset.x == 320 {
print("page 2")
self.page = 2
} else if self.contentScrollView.contentOffset.x == 640 {
print("page 3")
self.page = 3
}
}
func maxOffsetForScrollView(scrollView: UIScrollView) -> CGFloat {
let contentWidth: CGFloat = scrollView.contentSize.width - 100
let frameWidth: CGFloat = CGRectGetWidth(scrollView.frame)
return contentWidth - frameWidth
}
func firstAction (sender: UIButton) {
print("firstAction")
self.contentScrollView.setContentOffset(CGPointMake((self.cvbw / 3), 0), animated: true)
}
}

Resources