Swift - How to restrict image panning to image bounds - swift

I have an image inside a HStack, which I scale using MagnificationGesture (This is working). I have also created the code to pan the image within HStack (This is also working). The issue I have is:
I would like to restrict the pan to not exceed the image bounds (ie not show any white space around the image). Even when I scale the image, the pan should be restricted to image bounds.
Here is my working code:
import SwiftUI
struct PanMove: View {
#State private var scale : CGFloat = 1.0
#State private var lastScaleValue : CGFloat = 1.0
#State private var minScale : CGFloat = 1.0
#State private var maxScale : CGFloat = 3.0
var body: some View {
VStack {
HStack {
SFImageVR()
.scaleEffect(self.scale)
.gesture(
MagnificationGesture()
.onChanged({ val in
let delta = val/self.lastScaleValue
self.lastScaleValue = val
let newScale = self.scale * delta
if (newScale >= self.minScale && newScale <= self.maxScale) {
self.scale = newScale
}
})
.onEnded({ scale in
self.lastScaleValue = 1.0
})
)
}
.frame(width: 300, height: 450, alignment: .center)
.border(Color.black, width: 1)
.clipped()
}
}
}
struct SFImageVR: UIViewRepresentable {
typealias UIViewType = SFImageView
func makeUIView(context: Context) -> SFImageView {
var imageView = SFImageView(image: UIImage(named: "Image1"))
return imageView
}
func updateUIView(_ uiView: SFImageView, context: Context) {
}
}
class SFImageView: UIImageView, UIGestureRecognizerDelegate {
override init(image: UIImage?) {
super.init(image: image)
self.isUserInteractionEnabled = true
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func setup(){
let gesture = UIPanGestureRecognizer(target: self, action: #selector(onPan))
gesture.minimumNumberOfTouches = 1
gesture.delegate = self
self.addGestureRecognizer(gesture)
}
#objc func onPan(sender: UIPanGestureRecognizer) {
guard sender.view != nil else { return }
if sender.state == .began || sender.state == .changed {
let translation = sender.translation(in: self)
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self)
}
}
}

Related

UIViewRepresentable not show in SwiftUI

I have some view for zoomable images
struct ViewerFrameView: View {
#StateObject var viewModel: ViewerFrameViewModel
#State var image: UIImage = UIImage()
init(viewModel: ViewerFrameViewModel) {
self._viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
Group {
if viewModel.downloading {
LoaderView()
} else {
ZoomableScrollView(image: $image)
}
}
.onAppear {
DispatchQueue.global().async {
viewModel.getContent()
}
}
.onChange(of: viewModel.image, perform: { newValue in
image = newValue
print(newValue.size)
})
}
}
ZoomableScrollView is a UIViewRepresentable view that implement inside UIKit UIScrollView:
struct ZoomableScrollView: UIViewRepresentable {
#Binding var image: UIImage
var scrollView = ImageScrollView()
func makeUIView(context: Context) -> ImageScrollView {
return scrollView
}
func updateUIView(_ uiView: ImageScrollView, context: Context) {
scrollView.set(image: image)
}
}
class ImageScrollView: UIScrollView, UIScrollViewDelegate {
var imageZoomView: UIImageView!
lazy var zoomingTap: UITapGestureRecognizer = {
let zoomingTap = UITapGestureRecognizer(target: self, action: #selector(handleZoomingTap))
zoomingTap.numberOfTapsRequired = 2
return zoomingTap
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.delegate = self
self.showsVerticalScrollIndicator = false
self.showsHorizontalScrollIndicator = false
self.decelerationRate = UIScrollView.DecelerationRate.fast
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func set(image: UIImage) {
imageZoomView?.removeFromSuperview()
imageZoomView = nil
imageZoomView = UIImageView(image: image)
self.addSubview(imageZoomView)
configurateFor(imageSize: image.size)
}
func configurateFor(imageSize: CGSize) {
self.contentSize = imageSize
setCurrentMaxandMinZoomScale()
self.zoomScale = self.minimumZoomScale
self.imageZoomView.addGestureRecognizer(self.zoomingTap)
self.imageZoomView.isUserInteractionEnabled = true
}
override func layoutSubviews() {
super.layoutSubviews()
self.centerImage()
}
func setCurrentMaxandMinZoomScale() {
let boundsSize = self.bounds.size
let imageSize = imageZoomView.bounds.size
let xScale = boundsSize.width / imageSize.width
let yScale = boundsSize.height / imageSize.height
let minScale = min(xScale, yScale)
var maxScale: CGFloat = 1.0
if minScale < 0.1 {
maxScale = 0.3
}
if minScale >= 0.1 && minScale < 0.5 {
maxScale = 0.7
}
if minScale >= 0.5 {
maxScale = max(1.0, minScale)
}
self.minimumZoomScale = minScale
self.maximumZoomScale = maxScale
}
func centerImage() {
let boundsSize = self.bounds.size
var frameToCenter = imageZoomView.frame
if frameToCenter.size.width < boundsSize.width {
frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2
} else {
frameToCenter.origin.x = 0
}
if frameToCenter.size.height < boundsSize.height {
frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2
} else {
frameToCenter.origin.y = 0
}
imageZoomView.frame = frameToCenter
}
// gesture
#objc func handleZoomingTap(sender: UITapGestureRecognizer) {
let location = sender.location(in: sender.view)
self.zoom(point: location, animated: true)
}
func zoom(point: CGPoint, animated: Bool) {
let currectScale = self.zoomScale
let minScale = self.minimumZoomScale
let maxScale = self.maximumZoomScale
if minScale == maxScale && minScale > 1 {
return
}
let toScale = maxScale
let finalScale = (currectScale == minScale) ? toScale : minScale
let zoomRect = self.zoomRect(scale: finalScale, center: point)
self.zoom(to: zoomRect, animated: animated)
}
func zoomRect(scale: CGFloat, center: CGPoint) -> CGRect {
var zoomRect = CGRect.zero
let bounds = self.bounds
zoomRect.size.width = bounds.size.width / scale
zoomRect.size.height = bounds.size.height / scale
zoomRect.origin.x = center.x - (zoomRect.size.width / 2)
zoomRect.origin.y = center.y - (zoomRect.size.height / 2)
return zoomRect
}
// MARK: - UIScrollViewDelegate
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.imageZoomView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
self.centerImage()
}
}
but when loading complete it doesnt work, only empty screen
and in console I have strange logs
2022-09-19 16:58:35.169270+0300 Storage[11768:301086] [Assert] -[UIScrollView _clampedZoomScale:allowRubberbanding:]: Must be called with non-zero scale
2022-09-19 16:58:35.169587+0300 Storage[11768:301086] [Unknown process name] CGAffineTransformInvert: singular matrix.

CAEmitterLayer Stops Displaying

I adapted this code for SwiftUI to display confetti particles, but sometimes the particle emitter does not work. I've noticed that this often happens after sending to background (not killing the app entirely) and reopening it, or simply letting the app sit for a while then trying again.
I've tried using beginTime as other answers have mentioned (on both the emitter and cells), but that fully breaks things. I've also tried toggling various other emitter properties (birthRate, isHidden). It might have to do with the fact that I'm adapting this with UIViewRepresentable. It seems like the emitter layer just disappears, even though the debug console says its still visible.
class ConfettiParticleView: UIView {
var emitter: CAEmitterLayer!
public var colors: [UIColor]!
public var intensity: Float!
private var active: Bool!
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup() {
colors = [UIColor(Color.red),
UIColor(Color.blue),
UIColor(Color.orange),
]
intensity = 0.7
active = false
emitter = CAEmitterLayer()
emitter.emitterPosition = CGPoint(x: UIScreen.main.bounds.width / 2.0, y: 0) // emit from top of view
emitter.emitterShape = .line
emitter.emitterSize = CGSize(width: UIScreen.main.bounds.width, height: 100) // line spans the whole top of view
// emitter.beginTime = CACurrentMediaTime()
var cells = [CAEmitterCell]()
for color in colors {
cells.append(confettiWithColor(color: color))
}
emitter.emitterCells = cells
emitter.allowsGroupOpacity = false
self.layer.addSublayer(emitter)
}
func startConfetti() {
emitter.lifetime = 1
// i've tried toggling other properties here like birthRate, speed
active = true
}
func stopConfetti() {
emitter.lifetime = 0
active = false
}
func confettiWithColor(color: UIColor) -> CAEmitterCell {
let confetti = CAEmitterCell()
confetti.birthRate = 32.0 * intensity
confetti.lifetime = 15.0 * intensity
confetti.lifetimeRange = 0
confetti.name = "confetti"
confetti.color = color.cgColor
confetti.velocity = CGFloat(450.0 * intensity) // orig 450
confetti.velocityRange = CGFloat(80.0 * intensity)
confetti.emissionLongitude = .pi
confetti.emissionRange = .pi / 4
confetti.spin = CGFloat(3.5 * intensity)
confetti.spinRange = 300 * (.pi / 180.0)
confetti.scaleRange = CGFloat(intensity)
confetti.scaleSpeed = CGFloat(-0.1 * intensity)
confetti.contents = #imageLiteral(resourceName: "confetti").cgImage
confetti.beginTime = CACurrentMediaTime()
return confetti
}
func isActive() -> Bool {
return self.active
}
}
view representable
struct ConfettiView: UIViewRepresentable {
#Binding var isStarted: Bool
func makeUIView(context: Context) -> ConfettiParticleView {
return ConfettiParticleView()
}
func updateUIView(_ uiView: ConfettiParticleView, context: Context) {
if isStarted && !uiView.isActive() {
uiView.startConfetti()
print("confetti started")
} else if !isStarted {
uiView.stopConfetti()
print("confetti stopped")
}
}
}
swiftui view for testing
struct ConfettiViewTest: View {
#State var isStarted = false
var body: some View {
ZStack {
ConfettiView(isStarted: $isStarted)
.ignoresSafeArea()
Button(action: {
isStarted = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
isStarted = false
}
}) {
Text("toggle")
.padding()
.background(Color.white)
}
}
}
}

How to add UIGestureRecongnizer programmatically on custom views that are created via code?

I want to add a UITapGestureRecognizer to my view named SetView. My setviews are created programmatically on another custom view called GridView.
This is what I have tried so far but I am not seeing any action while tapping my subvies.
import UIKit
#IBDesignable
class GridView: UIView {
private(set) lazy var deckOfCards = createDeck()
lazy var grid = Grid(layout: Grid.Layout.fixedCellSize(CGSize(width: 128.0, height: 110.0)), frame: CGRect(origin: CGPoint(x: bounds.minX, y: bounds.minY), size: CGSize(width: bounds.width, height: bounds.height)))
lazy var listOfSetCard = createSetCards()
private func createDeck() -> [SetCard] {
var deck = [SetCard]()
for shape in SetCard.Shape.allShape {
for color in SetCard.Color.allColor {
for content in SetCard.Content.allContent {
for number in SetCard.Number.allNumbers {
deck.append(SetCard(shape: shape, color: color, content: content, rank: number))
}
}
}
}
deck.shuffle()
return deck
}
private func createSetCards() -> [SetView] {
var cards = [SetView]()
for _ in 0..<cardsOnScreen {
let card = SetView()
let contentsToBeDrawn = deckOfCards.removeFirst()
card.combinationOnCard.shape = contentsToBeDrawn.shape
card.combinationOnCard.color = contentsToBeDrawn.color
card.combinationOnCard.content = contentsToBeDrawn.content
card.combinationOnCard.rank = contentsToBeDrawn.rank
/* print(contentsToBeDrawn.color) */
addSubview(card)
cards.append(card)
}
return cards
}
override func layoutSubviews() {
super.layoutSubviews()
for index in listOfSetCard.indices {
let card = listOfSetCard[index]
if let rect = grid[index] {
card.frame = rect.insetBy(dx: 2.5, dy: 2.5)
card.frame.origin = rect.origin
print(card.frame.origin)
}
}
}
Here is the function didTap(sender: UITapGestureRecognizer) that I wrote on SetView:
#objc func didTap(sender: UITapGestureRecognizer) {
switch sender.state {
case .changed,.ended:
let rect = UIBezierPath(rect: bounds)
fillBoundingRect(inRect: rect, color: UIColor.gray)
default:
break
}
And ViewController:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...12 {
let card = game.drawModelCard()
game.deck.append(card)
}
}
lazy var game = SetGame()
weak var setView : SetView! {
didSet {
let tapGestureRecognizer = UITapGestureRecognizer(target:
setView, action: #selector(SetView.didTap(sender:)))
setView.isUserInteractionEnabled = true
setView.addGestureRecognizer(tapGestureRecognizer)
}
}
}
My subviews(SetViews) should change the background color once tapped.

how to pinch zoom in on code created scrollview

My code creates a scrollview and image view that displays a picture array from a previous view controller. However, I am trying to implement code to make it so the user may zoom in on a picture. But what ever I do, it does not work. Any suggestions on what I am doing wrong, or where to implement the zoom in code? Thank you!
import UIKit
class DestinationVC: UIViewController {
#IBOutlet weak var myScrollView: UIScrollView!
var mySelectedProtocol:Protocol?
var pageControl:UIPageControl?
var currentPageIndex:Int=0
fileprivate var count:Int=0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if mySelectedProtocol == nil { self.navigationController?.popViewController(animated: true) }
if mySelectedProtocol!.imagesName!.count == 0 { self.navigationController?.popViewController(animated: true) }
/// We have Data
print("Img Array with Name ==> \(mySelectedProtocol?.imagesName ?? [])")
DispatchQueue.main.async {
self.addPageView()
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
myScrollView.delegate = self
myScrollView.minimumZoomScale = 1.0
myScrollView.maximumZoomScale = 5.0
return myScrollView
}
private func addPageView() {
myScrollView.backgroundColor=UIColor.black
myScrollView.isUserInteractionEnabled=true
myScrollView.showsHorizontalScrollIndicator=true
myScrollView.isPagingEnabled=true
myScrollView.delegate=self
myScrollView.bounces=false
self.count=mySelectedProtocol!.imagesName!.count
for i in 0..<self.count {
///Get Origin
let xOrigin : CGFloat = CGFloat(i) * myScrollView.frame.size.width
///Create a imageView
let imageView = UIImageView()
imageView.frame = CGRect(x: xOrigin, y: 0, width: myScrollView.frame.size.width, height: myScrollView.frame.size.height)
imageView.contentMode = .scaleAspectFit
imageView.image=UIImage(named: mySelectedProtocol!.imagesName![i])
myScrollView.addSubview(imageView)
}
setUpPageControl()
///Set Content Size to Show
myScrollView.contentSize = CGSize(width: myScrollView.frame.size.width * CGFloat(self.count), height: myScrollView.frame.size.height)
}
private func setUpPageControl() {
if pageControl == nil { pageControl=UIPageControl() }
pageControl!.numberOfPages = self.count
pageControl!.currentPageIndicatorTintColor = UIColor.red
pageControl!.pageIndicatorTintColor = UIColor.white
pageControl!.frame = CGRect(x: 0, y: 20, width: self.view.frame.width, height: self.view.frame.height*0.2)
pageControl!.currentPage=currentPageIndex
self.view.addSubview(pageControl!)
self.view.bringSubview(toFront: pageControl!)
}
}
extension DestinationVC: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let scrollW : CGFloat = scrollView.frame.size.width
currentPageIndex = Int(scrollView.contentOffset.x / scrollW)
self.pageControl!.currentPage=currentPageIndex
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let scrollW : CGFloat = scrollView.frame.size.width
currentPageIndex = Int(scrollView.contentOffset.x / scrollW)
self.pageControl!.currentPage=currentPageIndex
}
}
You set the minimumScale to 1.0 - that's just 1x the normal scale. If you want it to be zoom down closer, you could try setting the minimum zoom like this:
let scaleWidth = scrollView.frame.size.width / scrollView.contentSize.width
let scaleHeight = scrollView.frame.size.height / scrollView.contentSize.height
let minScale = min(scaleWidth, scaleHeight)
scrollView.minimumZoomScale = minScale
scrollView.maximumZoomScale = 1.0
scrollView.zoomScale = minScale
And set the maximumZoomScale to 1.0 - the size of the content.

Issue in blocking Vertical scroll on UIScrollView in Swift 4.0

I have an Image carousel in my app I use a UIScrollView to show the images inside. everything works fine, it's just that I want to know how do I block up movements in the UIScrollView
I'm trying to block the vertical scroll by doing:
scrollView.showsVerticalScrollIndicator = false
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: 0) //disable vertical
everything in that works fine and it really blocks the vertical scroll
The problem is,
that I also have a timer, that moves the UIScrollView programmatically by doing:
var frame: CGRect = scrollView.frame
frame.origin.x = frame.size.width * CGFloat(pageToMove)
frame.origin.y = -35
scrollView.scrollRectToVisible(frame, animated: true)
and once I block the vertical scroll,
this function to scrollReactToVisible doesn't do anything.
and I don't get any error for that.
is there a way currently to also block the scroll vertically (and allow to scroll right and left as usual) and also move the scrollview programmatically?
I'm attaching my full view controller:
class CaruselleScreenViewController: UIViewController, CaruselleScreenViewProtocol, UIScrollViewDelegate {
var myPresenter: CaruselleScreenPresenterProtocol?
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var scrollView: UIScrollView!
var slides:[CaruselleTipsCard] = [];
var timer:Timer?
var currentPageMultiplayer = 0
override func viewDidLoad() {
super.viewDidLoad()
myPresenter = CaruselleScreenPresenter(controller: self)
//initlizes view
pageControl.numberOfPages = slides.count
pageControl.currentPage = 0
view.bringSubview(toFront: pageControl)
//delegates
scrollView.delegate = self
////blocks vertical movement
scrollView.showsVerticalScrollIndicator = false
//scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: 0) //disable vertical
}
func scheduleTimer(_ timeInterval: TimeInterval){
timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(timerCall), userInfo: nil, repeats: false)
}
#objc func timerCall(){
print("Timer executed")
currentPageMultiplayer = currentPageMultiplayer + 1
if (currentPageMultiplayer == 5) {
currentPageMultiplayer = 0
}
pageControl.currentPage = currentPageMultiplayer
scrollToPage(pageToMove: currentPageMultiplayer)
scheduleTimer(5)
}
func scrollToPage(pageToMove: Int) {
print ("new one")
var frame: CGRect = scrollView.frame
frame.origin.x = frame.size.width * CGFloat(pageToMove)
frame.origin.y = -35
scrollView.scrollRectToVisible(frame, animated: true)
}
func createSlides() -> [CaruselleTipsCard] {
let slide1:CaruselleTipsCard = Bundle.main.loadNibNamed("CaruselleTipsCard", owner: self, options: nil)?.first as! CaruselleTipsCard
slide1.mainPic.image = UIImage(named: "backlightingIllo")
//
let slide2:CaruselleTipsCard = Bundle.main.loadNibNamed("CaruselleTipsCard", owner: self, options: nil)?.first as! CaruselleTipsCard
slide2.mainPic.image = UIImage(named: "comfortableIllo")
//
let slide3:CaruselleTipsCard = Bundle.main.loadNibNamed("CaruselleTipsCard", owner: self, options: nil)?.first as! CaruselleTipsCard
slide3.mainPic.image = UIImage(named: "pharmacyIllo")
//
let slide4:CaruselleTipsCard = Bundle.main.loadNibNamed("CaruselleTipsCard", owner: self, options: nil)?.first as! CaruselleTipsCard
slide4.mainPic.image = UIImage(named: "batteryIllo")
//
let slide5:CaruselleTipsCard = Bundle.main.loadNibNamed("CaruselleTipsCard", owner: self, options: nil)?.first as! CaruselleTipsCard
slide5.mainPic.image = UIImage(named: "wiFiIllo")
return [slide1, slide2, slide3, slide4, slide5]
}
func setupSlideScrollView(slides : [CaruselleTipsCard]) {
scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
scrollView.contentSize = CGSize(width: view.frame.width * CGFloat(slides.count), height: view.frame.height)
scrollView.isPagingEnabled = true
for i in 0 ..< slides.count {
slides[i].frame = CGRect(x: view.frame.width * CGFloat(i), y: 0, width: view.frame.width, height: view.frame.height)
scrollView.addSubview(slides[i])
}
}
//////
/*
* default function called when view is scrolled. In order to enable callback
* when scrollview is scrolled, the below code needs to be called:
* slideScrollView.delegate = self or
*/
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex = round(scrollView.contentOffset.x/view.frame.width)
pageControl.currentPage = Int(pageIndex)
let maximumHorizontalOffset: CGFloat = scrollView.contentSize.width - scrollView.frame.width
let currentHorizontalOffset: CGFloat = scrollView.contentOffset.x
// vertical
let maximumVerticalOffset: CGFloat = scrollView.contentSize.height - scrollView.frame.height
let currentVerticalOffset: CGFloat = scrollView.contentOffset.y
let percentageHorizontalOffset: CGFloat = currentHorizontalOffset / maximumHorizontalOffset
let percentageVerticalOffset: CGFloat = currentVerticalOffset / maximumVerticalOffset
/*
* below code changes the background color of view on paging the scrollview
*/
// self.scrollView(scrollView, didScrollToPercentageOffset: percentageHorizontalOffset)
/*
* below code scales the imageview on paging the scrollview
*/
let percentOffset: CGPoint = CGPoint(x: percentageHorizontalOffset, y: percentageVerticalOffset)
if(percentOffset.x > 0 && percentOffset.x <= 0.25) {
slides[0].mainPic.transform = CGAffineTransform(scaleX: (0.25-percentOffset.x)/0.25, y: (0.25-percentOffset.x)/0.25)
slides[1].mainPic.transform = CGAffineTransform(scaleX: percentOffset.x/0.25, y: percentOffset.x/0.25)
} else if(percentOffset.x > 0.25 && percentOffset.x <= 0.50) {
slides[1].mainPic.transform = CGAffineTransform(scaleX: (0.50-percentOffset.x)/0.25, y: (0.50-percentOffset.x)/0.25)
slides[2].mainPic.transform = CGAffineTransform(scaleX: percentOffset.x/0.50, y: percentOffset.x/0.50)
} else if(percentOffset.x > 0.50 && percentOffset.x <= 0.75) {
slides[2].mainPic.transform = CGAffineTransform(scaleX: (0.75-percentOffset.x)/0.25, y: (0.75-percentOffset.x)/0.25)
slides[3].mainPic.transform = CGAffineTransform(scaleX: percentOffset.x/0.75, y: percentOffset.x/0.75)
} else if(percentOffset.x > 0.75 && percentOffset.x <= 1) {
slides[3].mainPic.transform = CGAffineTransform(scaleX: (1-percentOffset.x)/0.25, y: (1-percentOffset.x)/0.25)
slides[4].mainPic.transform = CGAffineTransform(scaleX: percentOffset.x, y: percentOffset.x)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "findingClinitionSugue" {
let destination = segue.destination as! FirstAvailableSearchViewController
//destination.consumer = consumer
}
if (timer != nil) {
timer?.invalidate()
}
}
// protocol functions
func initlizeSlides() {
slides = createSlides()
setupSlideScrollView(slides: slides)
}
func initlizeTimer() {
scheduleTimer(5)
}
}
The problem might be about setting the contentSize height value to 0 initally, so even though timer wants scrollView to move, it cannot do that.
Can you try replacing this line:
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: 0)
With:
scrollView.contentInsetAdjustmentBehavior = .never
Depending the application and functionality required within the scrollview - could you disable user interaction of the scrollview so it can still be moved programmatically?
That would just be
scrollView.isUserInteractionEnabled = false
This would of course depend on whether you need items in the scrollview to be interactive
Maybe you can subclass your UIScrollView, and override touchesBegan.
class CustomScrollView: UIScrollView {
var touchesDisabled = false
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if touchesDisabled {
// here parse the touches, if they go in the horizontal direction, allow scrolling
// set tolerance for vertical movement
let tolerance: CGFloat = 5.0
let variance = touches.reduce(0, { Yvariation, touch in
Yvariation + abs(touch.location(in: view).y - touch.previousLocation(in: view).y)
})
if variance <= tolerance * CGFloat(touches.count) {
let Xtravelled = touches.reduce(0, { Xstep, touch in
Xstep + (touch.location(in: view).x - touch.previousLocation(in: view).x)
})
// scroll horizontally by the x component of hand gesture
var newFrame: CGRect = scrollView.frame
newFrame.origin.x += Xtravelled
self.scrollRectToVisible(frame, animated: true)
}
}
else {
super.touchesBegan(touches: touches, withEvent: event)
}
}
}
This way you can manually move the scrollview horizontally while disabling vertical movement when touchesDisabled is set true.
If I've understood you problem well, you can stop scrolling whenever you want with this
scrollView.isScrollEnabled = false
Using UIScrollViewDelegate (or KVO on scrollView's contentOffset), you can just counteract any vertical movement in the carousel. Something like this:
var oldYOffset: CGFloat ....
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let deltaY = oldYOffset - scrollView.contentOffset.y
oldYOffset = scrollView.contentOffset.y
scrollView.contentOffset.y -= deltaY
}
This offset change will not be visible to the user. You could even use this to increase the speed of the scrolling, invert the scrolling (pan left and scrollView scrolls right), or entirely lock the motion of the scrollView without touching isScrollEnabled, contentSize, etc.
This turned out to be quite an interesting problem...
While it is easy to lock UIScrollView scrolling to one axis only using the UIScrollViewDelegate, it is impossible to provide smooth scrolling while changing the scrolling programmatically (as you do with the Timer) at the same time.
Below, you will find a DirectionLockingScrollView class I just wrote that should make things easier for you. It's a UIScrollView that you can initialize either programmatically, or via the Interface Builder.
It features isHorizontalScrollingEnabled and isVerticalScrollingEnabled properties.
HOW IT WORKS INTERNALLY
It adds a second "control" UIScrollView that is identical to the main DirectionLockingScrollView and propagates to it all pan events intended for the main scroll view. Every time the "control" scroll view's bounds change, the change is propagated to the main scroll view BUT x and y are altered (based on isHorizontalScrollingEnabled and isVerticalScrollingEnabled) to disable scrolling on the requested axis.
DirectionLockingScrollView.swift
/// `UIScrollView` subclass that supports disabling scrolling on any direction
/// while allowing the other direction to be changed programmatically (via
/// `setContentOffset(_:animated)` or `scrollRectToVisible(_:animated)` or changing the
/// bounds etc.
///
/// Can be initialized programmatically or via the Interface Builder.
class DirectionLockingScrollView: UIScrollView {
var isHorizontalScrollingEnabled = true
var isVerticalScrollingEnabled = true
/// The control scrollview is added below the `DirectionLockingScrollView`
/// and is used to implement all native scrollview behaviours (such as bouncing)
/// based on user input.
///
/// It is required to be able to change the bounds of the `DirectionLockingScrollView`
/// while maintaining scrolling in only one direction and allowing for setting the contentOffset
/// (changing scrolling for any axis - even the disabled ones) programmatically.
private let _controlScrollView = UIScrollView(frame: .zero)
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
installCustomScrollView()
}
override init(frame: CGRect) {
super.init(frame: frame)
installCustomScrollView()
}
override func layoutSubviews() {
super.layoutSubviews()
updateCustomScrollViewFrame()
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
guard let superview = superview else {
_controlScrollView.removeFromSuperview()
return
}
superview.insertSubview(_controlScrollView, belowSubview: self)
updateCustomScrollViewFrame()
}
// MARK: - UIEvent propagation
func viewIgnoresEvents(_ view: UIView?) -> Bool {
let viewIgnoresEvents =
view == nil ||
view == self ||
!view!.isUserInteractionEnabled ||
!(view is UIControl && (view!.gestureRecognizers ?? []).count == 0)
return viewIgnoresEvents
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if viewIgnoresEvents(view) {
return _controlScrollView
}
return view
}
// MARK: - Main scrollview settings propagation to `controlScrollView`
override var contentInset: UIEdgeInsets {
didSet {
_controlScrollView.contentInset = contentInset
}
}
override var contentScaleFactor: CGFloat {
didSet {
_controlScrollView.contentScaleFactor = contentScaleFactor
}
}
override var contentSize: CGSize {
didSet {
_controlScrollView.contentSize = contentSize
}
}
override var bounces: Bool {
didSet {
_controlScrollView.bounces = bounces
}
}
override var bouncesZoom: Bool {
didSet {
_controlScrollView.bouncesZoom = bouncesZoom
}
}
}
extension DirectionLockingScrollView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateBoundsFromCustomScrollView(scrollView)
}
}
private extension DirectionLockingScrollView {
/// Propagates `controlScrollView` bounds to the actual scrollview.
/// - Parameter scrollView: If the scrollview provided is not the `controlScrollView`
// the main scrollview bounds are not updated.
func updateBoundsFromCustomScrollView(_ scrollView: UIScrollView) {
if scrollView != _controlScrollView {
return
}
var newBounds = scrollView.bounds.origin
if !isHorizontalScrollingEnabled {
newBounds.x = self.contentOffset.x
}
if !isVerticalScrollingEnabled {
newBounds.y = self.contentOffset.y
}
bounds.origin = newBounds
}
func installCustomScrollView() {
_controlScrollView.delegate = self
_controlScrollView.contentSize = contentSize
_controlScrollView.showsVerticalScrollIndicator = false
_controlScrollView.showsHorizontalScrollIndicator = false
// The panGestureRecognizer is removed because pan gestures might be triggered
// on subviews of the scrollview which do not ignore touch events (determined
// by `viewIgnoresEvents(_ view: UIView?)`). This can happen for example
// if you tap and drag on a button inside the scroll view.
removeGestureRecognizer(panGestureRecognizer)
}
func updateCustomScrollViewFrame() {
if _controlScrollView.frame == frame { return }
_controlScrollView.frame = frame
}
}
USAGE
After you've included the above class in your app, don't forget to change your scroll view's class to DirectionLockingScrollView in your .xib or .storyboard.
Then update your code as below (only two lines changed, marked with // *****).
class CaruselleScreenViewController: UIViewController, CaruselleScreenViewProtocol, UIScrollViewDelegate {
var myPresenter: CaruselleScreenPresenterProtocol?
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var scrollView: DirectionLockingScrollView! // *****
var slides:[CaruselleTipsCard] = [];
var timer:Timer?
var currentPageMultiplayer = 0
override func viewDidLoad() {
super.viewDidLoad()
myPresenter = CaruselleScreenPresenter(controller: self)
//initlizes view
pageControl.numberOfPages = slides.count
pageControl.currentPage = 0
view.bringSubview(toFront: pageControl)
scrollView.isHorizontalScrollingEnabled = false // *****
//delegates
scrollView.delegate = self
////blocks vertical movement
scrollView.showsVerticalScrollIndicator = false
//scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: 0) //disable vertical
}
func scheduleTimer(_ timeInterval: TimeInterval){
timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(timerCall), userInfo: nil, repeats: false)
}
#objc func timerCall(){
print("Timer executed")
currentPageMultiplayer = currentPageMultiplayer + 1
if (currentPageMultiplayer == 5) {
currentPageMultiplayer = 0
}
pageControl.currentPage = currentPageMultiplayer
scrollToPage(pageToMove: currentPageMultiplayer)
scheduleTimer(5)
}
func scrollToPage(pageToMove: Int) {
print ("new one")
var frame: CGRect = scrollView.frame
frame.origin.x = frame.size.width * CGFloat(pageToMove)
frame.origin.y = -35
scrollView.scrollRectToVisible(frame, animated: true)
}
func createSlides() -> [CaruselleTipsCard] {
let slide1:CaruselleTipsCard = Bundle.main.loadNibNamed("CaruselleTipsCard", owner: self, options: nil)?.first as! CaruselleTipsCard
slide1.mainPic.image = UIImage(named: "backlightingIllo")
//
let slide2:CaruselleTipsCard = Bundle.main.loadNibNamed("CaruselleTipsCard", owner: self, options: nil)?.first as! CaruselleTipsCard
slide2.mainPic.image = UIImage(named: "comfortableIllo")
//
let slide3:CaruselleTipsCard = Bundle.main.loadNibNamed("CaruselleTipsCard", owner: self, options: nil)?.first as! CaruselleTipsCard
slide3.mainPic.image = UIImage(named: "pharmacyIllo")
//
let slide4:CaruselleTipsCard = Bundle.main.loadNibNamed("CaruselleTipsCard", owner: self, options: nil)?.first as! CaruselleTipsCard
slide4.mainPic.image = UIImage(named: "batteryIllo")
//
let slide5:CaruselleTipsCard = Bundle.main.loadNibNamed("CaruselleTipsCard", owner: self, options: nil)?.first as! CaruselleTipsCard
slide5.mainPic.image = UIImage(named: "wiFiIllo")
return [slide1, slide2, slide3, slide4, slide5]
}
func setupSlideScrollView(slides : [CaruselleTipsCard]) {
scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
scrollView.contentSize = CGSize(width: view.frame.width * CGFloat(slides.count), height: view.frame.height)
scrollView.isPagingEnabled = true
for i in 0 ..< slides.count {
slides[i].frame = CGRect(x: view.frame.width * CGFloat(i), y: 0, width: view.frame.width, height: view.frame.height)
scrollView.addSubview(slides[i])
}
}
//////
/*
* default function called when view is scrolled. In order to enable callback
* when scrollview is scrolled, the below code needs to be called:
* slideScrollView.delegate = self or
*/
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex = round(scrollView.contentOffset.x/view.frame.width)
pageControl.currentPage = Int(pageIndex)
let maximumHorizontalOffset: CGFloat = scrollView.contentSize.width - scrollView.frame.width
let currentHorizontalOffset: CGFloat = scrollView.contentOffset.x
// vertical
let maximumVerticalOffset: CGFloat = scrollView.contentSize.height - scrollView.frame.height
let currentVerticalOffset: CGFloat = scrollView.contentOffset.y
let percentageHorizontalOffset: CGFloat = currentHorizontalOffset / maximumHorizontalOffset
let percentageVerticalOffset: CGFloat = currentVerticalOffset / maximumVerticalOffset
/*
* below code changes the background color of view on paging the scrollview
*/
// self.scrollView(scrollView, didScrollToPercentageOffset: percentageHorizontalOffset)
/*
* below code scales the imageview on paging the scrollview
*/
let percentOffset: CGPoint = CGPoint(x: percentageHorizontalOffset, y: percentageVerticalOffset)
if(percentOffset.x > 0 && percentOffset.x <= 0.25) {
slides[0].mainPic.transform = CGAffineTransform(scaleX: (0.25-percentOffset.x)/0.25, y: (0.25-percentOffset.x)/0.25)
slides[1].mainPic.transform = CGAffineTransform(scaleX: percentOffset.x/0.25, y: percentOffset.x/0.25)
} else if(percentOffset.x > 0.25 && percentOffset.x <= 0.50) {
slides[1].mainPic.transform = CGAffineTransform(scaleX: (0.50-percentOffset.x)/0.25, y: (0.50-percentOffset.x)/0.25)
slides[2].mainPic.transform = CGAffineTransform(scaleX: percentOffset.x/0.50, y: percentOffset.x/0.50)
} else if(percentOffset.x > 0.50 && percentOffset.x <= 0.75) {
slides[2].mainPic.transform = CGAffineTransform(scaleX: (0.75-percentOffset.x)/0.25, y: (0.75-percentOffset.x)/0.25)
slides[3].mainPic.transform = CGAffineTransform(scaleX: percentOffset.x/0.75, y: percentOffset.x/0.75)
} else if(percentOffset.x > 0.75 && percentOffset.x <= 1) {
slides[3].mainPic.transform = CGAffineTransform(scaleX: (1-percentOffset.x)/0.25, y: (1-percentOffset.x)/0.25)
slides[4].mainPic.transform = CGAffineTransform(scaleX: percentOffset.x, y: percentOffset.x)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "findingClinitionSugue" {
let destination = segue.destination as! FirstAvailableSearchViewController
//destination.consumer = consumer
}
if (timer != nil) {
timer?.invalidate()
}
}
// protocol functions
func initlizeSlides() {
slides = createSlides()
setupSlideScrollView(slides: slides)
}
func initlizeTimer() {
scheduleTimer(5)
}
}