I have issues with my side panel for iPad app. I need buttons stacked as below:
Expected Output:
Right now, my output produces:
Current Output:
How can I remove circles and add button sets?
import UIKit
import QuartzCore
public protocol FrostedSidebarDelegate{
func sidebar(sidebar: FrostedSidebar, willShowOnScreenAnimated animated: Bool)
func sidebar(sidebar: FrostedSidebar, didShowOnScreenAnimated animated: Bool)
func sidebar(sidebar: FrostedSidebar, willDismissFromScreenAnimated animated: Bool)
func sidebar(sidebar: FrostedSidebar, didDismissFromScreenAnimated animated: Bool)
func sidebar(sidebar: FrostedSidebar, didTapItemAtIndex index: Int)
func sidebar(sidebar: FrostedSidebar, didEnable itemEnabled: Bool, itemAtIndex index: Int)
}
var sharedSidebar: FrostedSidebar?
public enum SidebarItemSelectionStyle{
case None
se Single
case All
}
public class FrostedSidebar: UIViewController {
public var width: CGFloat = 300.0
/**
If the sidebar should show from the right.
*/
public var showFromRight: Bool = false
/**
The speed at which the sidebar is presented/dismissed.
*/
public var animationDuration: CGFloat = 0.25
/**
The size of the sidebar items.
*/
public var itemSize: CGSize = CGSize(width: 200.0, height: 200.0)
/**
The background color of the sidebar items.
*/
public var itemBackgroundColor: UIColor = UIColor(white: 1, alpha: 0.25)
/**
The width of the ring around selected sidebar items.
*/
public var borderWidth: CGFloat = 2
/**
The sidebar's delegate.
*/
public var delegate: FrostedSidebarDelegate? = nil
/**
A dictionary that holds the actions for each item index.
*/
public var actionForIndex: [Int : ()->()] = [:]
/**
The indexes that are selected and have rings around them.
*/
public var selectedIndices: NSMutableIndexSet = NSMutableIndexSet()
/**
If the sidebar should be positioned beneath a navigation bar that is on screen.
*/
public var adjustForNavigationBar: Bool = false
/**
Returns whether or not the sidebar is currently being displayed
*/
public var isCurrentlyOpen: Bool = false
/**
The selection style for the sidebar.
*/
public var selectionStyle: SidebarItemSelectionStyle = .None{
didSet{
if case .All = selectionStyle{
selectedIndices = NSMutableIndexSet(indexesInRange: NSRange(location: 0, length: images.count))
}
}
}
//MARK: Private Properties
private var contentView: UIScrollView = UIScrollView()
private var blurView: UIVisualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .Dark))
private var dimView: UIView = UIView()
private var tapGesture: UITapGestureRecognizer? = nil
private var images: [UIImage] = []
private var borderColors: [UIColor]? = nil
private var itemViews: [CalloutItem] = []
//MARK: Public Methods
/**
Returns an object initialized from data in a given unarchiver.
*/
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
/**
Returns a sidebar initialized with the given data.
- Parameter itemImages: The images that will be used for each item.
- Parameter colors: The color of rings around each image.
- Parameter selectionStyle: The selection style for the sidebar.
- Precondition: `colors` is either `nil` or contains the same number of elements as `itemImages`.
*/
public init(itemImages: [UIImage], colors: [UIColor]?, selectionStyle: SidebarItemSelectionStyle){
contentView.alwaysBounceHorizontal = false
contentView.alwaysBounceVertical = true
contentView.bounces = true
contentView.clipsToBounds = false
contentView.showsHorizontalScrollIndicator = false
contentView.showsVerticalScrollIndicator = false
if let colors = colors{
assert(itemImages.count == colors.count, "If item color are supplied, the itemImages and colors arrays must be of the same size.")
}
self.selectionStyle = selectionStyle
borderColors = colors
images = itemImages
for (index, image) in images.enumerate(){
let view = CalloutItem(index: index)
view.clipsToBounds = true
view.imageView.image = image
contentView.addSubview(view)
itemViews += [view]
if let borderColors = borderColors{
if selectedIndices.containsIndex(index){
let color = borderColors[index]
view.layer.borderColor = color.CGColor
}
} else{
view.layer.borderColor = UIColor.clearColor().CGColor
}
}
super.init(nibName: nil, bundle: nil)
}
public override func shouldAutorotate() -> Bool {
return true
}
public override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.All
}
public override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
if isViewLoaded(){
dismissAnimated(false, completion: nil)
}
}
public override func loadView() {
super.loadView()
view.backgroundColor = UIColor.clearColor()
view.addSubview(dimView)
view.addSubview(blurView)
view.addSubview(contentView)
tapGesture = UITapGestureRecognizer(target: self, action: #selector(FrostedSidebar.handleTap(_:)))
view.addGestureRecognizer(tapGesture!)
}
/**
Shows the sidebar in a view controller.
- Parameter viewController: The view controller in which to show the sidebar.
- Parameter animated: If the sidebar should be animated.
*/
public func showInViewController(viewController: UIViewController, animated: Bool){
layoutItems()
if let bar = sharedSidebar{
bar.dismissAnimated(false, completion: nil)
}
delegate?.sidebar(self, willShowOnScreenAnimated: animated)
sharedSidebar = self
addToParentViewController(viewController, callingAppearanceMethods: true)
view.frame = viewController.view.bounds
dimView.backgroundColor = UIColor.blackColor()
dimView.alpha = 0
dimView.frame = view.bounds
let parentWidth = view.bounds.size.width
var contentFrame = view.bounds
contentFrame.origin.x = showFromRight ? parentWidth : -width
contentFrame.size.width = width
contentView.frame = contentFrame
contentView.contentOffset = CGPoint(x: 0, y: 0)
layoutItems()
var blurFrame = CGRect(x: showFromRight ? view.bounds.size.width : 0, y: 0, width: 0, height: view.bounds.size.height)
blurView.frame = blurFrame
blurView.contentMode = showFromRight ? UIViewContentMode.TopRight : UIViewContentMode.TopLeft
blurView.clipsToBounds = true
view.insertSubview(blurView, belowSubview: contentView)
contentFrame.origin.x = showFromRight ? parentWidth - width : 0
blurFrame.origin.x = contentFrame.origin.x
blurFrame.size.width = width
let animations: () -> () = {
self.contentView.frame = contentFrame
self.blurView.frame = blurFrame
self.dimView.alpha = 0.25
}
let completion: (Bool) -> Void = { finished in
if finished{
self.delegate?.sidebar(self, didShowOnScreenAnimated: animated)
}
}
if animated{
UIView.animateWithDuration(NSTimeInterval(animationDuration), delay: 0, options: UIViewAnimationOptions(), animations: animations, completion: completion)
} else{
animations()
completion(true)
}
for (index, item) in itemViews.enumerate(){
item.layer.transform = CATransform3DMakeScale(0.3, 0.3, 1)
item.alpha = 0
item.originalBackgroundColor = itemBackgroundColor
item.layer.borderWidth = borderWidth
animateSpringWithView(item, idx: index, initDelay: animationDuration)
}
self.isCurrentlyOpen = true
}
/**
Dismisses the sidebar.
- Parameter animated: If the sidebar should be animated.
- Parameter completion: Completion handler called when the sidebar is dismissed.
*/
public func dismissAnimated(animated: Bool, completion: ((Bool) -> Void)?){
let completionBlock: (Bool) -> Void = {finished in
self.removeFromParentViewControllerCallingAppearanceMethods(true)
self.delegate?.sidebar(self, didDismissFromScreenAnimated: true)
self.layoutItems()
if let completion = completion{
completion(finished)
}
}
delegate?.sidebar(self, willDismissFromScreenAnimated: animated)
if animated{
let parentWidth = view.bounds.size.width
var contentFrame = contentView.frame
contentFrame.origin.x = showFromRight ? parentWidth : -width
var blurFrame = blurView.frame
blurFrame.origin.x = showFromRight ? parentWidth : 0
blurFrame.size.width = 0
UIView.animateWithDuration(NSTimeInterval(animationDuration), delay: 0, options: UIViewAnimationOptions.BeginFromCurrentState, animations: {
self.contentView.frame = contentFrame
self.blurView.frame = blurFrame
self.dimView.alpha = 0
}, completion: completionBlock)
} else{
completionBlock(true)
}
self.isCurrentlyOpen = false
}
/**
Selects the item at the given index.
- Parameter index: The index of the item to select.
*/
public func selectItemAtIndex(index: Int){
let didEnable = !selectedIndices.containsIndex(index)
if let borderColors = borderColors{
let stroke = borderColors[index]
let item = itemViews[index]
if didEnable{
if case .Single = selectionStyle{
selectedIndices.removeAllIndexes()
for item in itemViews{
item.layer.borderColor = UIColor.clearColor().CGColor
}
}
item.layer.borderColor = stroke.CGColor
let borderAnimation = CABasicAnimation(keyPath: "borderColor")
borderAnimation.fromValue = UIColor.clearColor().CGColor
borderAnimation.toValue = stroke.CGColor
borderAnimation.duration = 0.5
item.layer.addAnimation(borderAnimation, forKey: nil)
selectedIndices.addIndex(index)
} else{
if case .None = selectionStyle{
item.layer.borderColor = UIColor.clearColor().CGColor
selectedIndices.removeIndex(index)
}
}
let pathFrame = CGRect(x: -CGRectGetMidX(item.bounds), y: -CGRectGetMidY(item.bounds), width: item.bounds.size.width, height: item.bounds.size.height)
let path = UIBezierPath(roundedRect: pathFrame, cornerRadius: item.layer.cornerRadius)
let shapePosition = view.convertPoint(item.center, fromView: contentView)
let circleShape = CAShapeLayer()
circleShape.path = path.CGPath
circleShape.position = shapePosition
circleShape.fillColor = UIColor.clearColor().CGColor
circleShape.opacity = 0
circleShape.strokeColor = stroke.CGColor
circleShape.lineWidth = borderWidth
view.layer.addSublayer(circleShape)
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = NSValue(CATransform3D: CATransform3DIdentity)
scaleAnimation.toValue = NSValue(CATransform3D: CATransform3DMakeScale(2.5, 2.5, 1))
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.fromValue = 1
alphaAnimation.toValue = 0
let animation = CAAnimationGroup()
animation.animations = [scaleAnimation, alphaAnimation]
animation.duration = 0.5
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
circleShape.addAnimation(animation, forKey: nil)
}
if let action = actionForIndex[index]{
action()
}
delegate?.sidebar(self, didTapItemAtIndex: index)
delegate?.sidebar(self, didEnable: didEnable, itemAtIndex: index)
}
//MARK: Private Classes
private class CalloutItem: UIView{
var imageView: UIImageView = UIImageView()
var itemIndex: Int
var originalBackgroundColor:UIColor? {
didSet{
backgroundColor = originalBackgroundColor
}
}
required init?(coder aDecoder: NSCoder) {
itemIndex = 0
super.init(coder: aDecoder)
}
init(index: Int){
imageView.backgroundColor = UIColor.clearColor()
imageView.contentMode = UIViewContentMode.ScaleAspectFit
itemIndex = index
super.init(frame: CGRect.zero)
addSubview(imageView)
}
override func layoutSubviews() {
super.layoutSubviews()
let inset: CGFloat = bounds.size.height/2
imageView.frame = CGRect(x: 0, y: 0, width: inset, height: inset)
imageView.center = CGPoint(x: inset, y: inset)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
let darkenFactor: CGFloat = 0.3
var darkerColor: UIColor
if originalBackgroundColor != nil && originalBackgroundColor!.getRed(&r, green: &g, blue: &b, alpha: &a){
darkerColor = UIColor(red: max(r - darkenFactor, 0), green: max(g - darkenFactor, 0), blue: max(b - darkenFactor, 0), alpha: a)
} else if originalBackgroundColor != nil && originalBackgroundColor!.getWhite(&r, alpha: &a){
darkerColor = UIColor(white: max(r - darkenFactor, 0), alpha: a)
} else{
darkerColor = UIColor.clearColor()
assert(false, "Item color should be RBG of White/Alpha in order to darken the button")
}
backgroundColor = darkerColor
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesEnded(touches, withEvent: event)
backgroundColor = originalBackgroundColor
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
super.touchesCancelled(touches, withEvent: event)
backgroundColor = originalBackgroundColor
}
}
//MARK: Private Methods
private func animateSpringWithView(view: CalloutItem, idx: Int, initDelay: CGFloat){
let delay: NSTimeInterval = NSTimeInterval(initDelay) + NSTimeInterval(idx) * 0.1
UIView.animateWithDuration(0.5,
delay: delay,
usingSpringWithDamping: 10.0,
initialSpringVelocity: 50.0,
options: UIViewAnimationOptions.BeginFromCurrentState,
animations: {
view.layer.transform = CATransform3DIdentity
view.alpha = 1
},
completion: nil)
}
#objc private func handleTap(recognizer: UITapGestureRecognizer){
let location = recognizer.locationInView(view)
if !CGRectContainsPoint(contentView.frame, location){
dismissAnimated(true, completion: nil)
} else{
let tapIndex = indexOfTap(recognizer.locationInView(contentView))
if let tapIndex = tapIndex{
selectItemAtIndex(tapIndex)
}
}
}
private func layoutSubviews(){
let x = showFromRight ? parentViewController!.view.bounds.size.width - width : 0
contentView.frame = CGRect(x: x, y: 0, width: width, height: parentViewController!.view.bounds.size.height)
blurView.frame = contentView.frame
layoutItems()
}
private func layoutItems(){
let leftPadding: CGFloat = (width - itemSize.width) / 2
let topPadding: CGFloat = leftPadding
for (index, item) in itemViews.enumerate(){
let idx: CGFloat = adjustForNavigationBar ? CGFloat(index) + 0.5 : CGFloat(index)
let frame = CGRect(x: leftPadding, y: topPadding*idx + itemSize.height*idx + topPadding, width:itemSize.width, height: itemSize.height)
item.frame = frame
item.layer.cornerRadius = frame.size.width / 2
item.layer.borderColor = UIColor.clearColor().CGColor
item.alpha = 0
if selectedIndices.containsIndex(index){
if let borderColors = borderColors{
item.layer.borderColor = borderColors[index].CGColor
}
}
}
let itemCount = CGFloat(itemViews.count)
if adjustForNavigationBar{
contentView.contentSize = CGSizeMake(0, (itemCount + 0.5) * (itemSize.height + topPadding) + topPadding)
} else {
contentView.contentSize = CGSizeMake(0, itemCount * (itemSize.height + topPadding) + topPadding)
}
}
private func indexOfTap(location: CGPoint) -> Int? {
var index: Int?
for (idx, item) in itemViews.enumerate(){
if CGRectContainsPoint(item.frame, location){
index = idx
break
}
}
return index
}
private func addToParentViewController(viewController: UIViewController, callingAppearanceMethods: Bool){
if let _ = parentViewController{
removeFromParentViewControllerCallingAppearanceMethods(callingAppearanceMethods)
}
if callingAppearanceMethods{
beginAppearanceTransition(true, animated: false)
}
viewController.addChildViewController(self)
viewController.view.addSubview(view)
didMoveToParentViewController(self)
if callingAppearanceMethods{
endAppearanceTransition()
}
}
private func removeFromParentViewControllerCallingAppearanceMethods(callAppearanceMethods: Bool){
if callAppearanceMethods{
beginAppearanceTransition(false, animated: false)
}
willMoveToParentViewController(nil)
view.removeFromSuperview()
removeFromParentViewController()
if callAppearanceMethods{
endAppearanceTransition()
}
}
}
Instead of putting each button in it's own view, you need to create a view that contains 3 buttons and then add the circle to the view.
Related
'''
import UIKit
class CanvasView: UIView {
var circleViewTag = 1000
var coordinatePoints: [String] = ["243,103","534,86","243,286","426,286"] {
didSet {
self.updateCoordinateArray()
self.drawPoints()
}
}
fileprivate var coordArray: [CGPoint] = []
var shape = CAShapeLayer()
var path = UIBezierPath()
/*// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}*/
private func drawPoints() -> Void {
CommonMethods.printLog("\(coordinatePoints)")
self.layer.addSublayer(shape)
shape.opacity = 0.5
shape.lineWidth = 2
shape.lineJoin = CAShapeLayerLineJoin.miter
shape.strokeColor = UIColor.white.cgColor
shape.fillColor = UIColor(red: 1.0, green: 0.2, blue: 0.2, alpha: 0.2).cgColor
if let firstCoord = self.coordArray.first {
path.move(to: firstCoord)
}
for (index, cgPoint) in self.coordArray.enumerated() {
self.drawCircularPoint(points: cgPoint)
if index == 0 {
continue
}
path.addLine(to: cgPoint)
}
path.close()
shape.path = path.cgPath
//self.drawLineBetweenPoints()
}
private func drawCircularPoint(points: CGPoint) -> Void {
let circleView = UIView.init(frame: .zero)
circleViewTag = circleViewTag + 1
circleView.tag = circleViewTag
circleView.frame.size = CGSize.init(width: 30.0, height: 30.0)
circleView.layer.cornerRadius = 15.0
circleView.center = points
circleView.backgroundColor = .random()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.draggedView(_:)))
panGesture.view?.tag = circleView.tag
circleView.isUserInteractionEnabled = true
circleView.addGestureRecognizer(panGesture)
self.addSubview(circleView)
}
#objc func draggedView(_ sender:UIPanGestureRecognizer){
guard let getTag = sender.view?.tag else { return }
if let viewToDrag = self.viewWithTag(getTag) as? UIView {
var currentPoint: CGPoint = .zero
if path.contains(viewToDrag.center) {
print("YES")
currentPoint = path.currentPoint
}
let translation = sender.translation(in: self)
viewToDrag.center = CGPoint(x: viewToDrag.center.x + translation.x, y: viewToDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self)
if sender.state == .began && currentPoint != .zero {
let coordinateIndex = self.coordArray.firstIndex { (cgpoint) -> Bool in
if currentPoint == cgpoint {
return true
}
return false
}
if coordinateIndex != nil {
self.coordArray[coordinateIndex!] = viewToDrag.center
self.shape.removeFromSuperlayer()
self.path.removeAllPoints()
self.setNeedsDisplay()
self.layer.addSublayer(self.shape)
self.shape.opacity = 0.5
self.shape.lineWidth = 2
self.shape.lineJoin = CAShapeLayerLineJoin.miter
self.shape.strokeColor = UIColor.white.cgColor
self.shape.fillColor = UIColor(red: 1.0, green: 0.2, blue: 0.2, alpha: 0.2).cgColor
if let firstCoord = self.coordArray.first {
path.move(to: firstCoord)
}
for (index, cgPoint) in self.coordArray.enumerated() {
//self.drawCircularPoint(points: cgPoint)
if index == 0 {
continue
}
path.addLine(to: cgPoint)
}
path.close()
shape.path = path.cgPath
}
}
}
//self.bringSubviewToFront(viewDrag)
}
private func updateCoordinateArray() -> Void {
for singleCoordinate in self.coordinatePoints {
if singleCoordinate.contains(",") == true {
let splitCoordinate = singleCoordinate.split(separator: ",")
if splitCoordinate.count == 2 {
let xPos = CGFloat(Float(splitCoordinate[0]) ?? 0.0)
let yPos = CGFloat(Float(splitCoordinate[1]) ?? 0.0)
let cgPoint = CGPoint(x: xPos, y: yPos)
self.coordArray.append(cgPoint)
}
}
}
var penultimateIndex: Int?
if let penultimateCoordinate = self.coordArray.penultimate() {
penultimateIndex = self.coordArray.firstIndex { (cgpoint) -> Bool in
if penultimateCoordinate == cgpoint {
return true
}
return false
}
}
var lastIndex: Int?
if let lastCoordinate = self.coordArray.last {
lastIndex = self.coordArray.firstIndex { (cgpoint) -> Bool in
if lastCoordinate == cgpoint {
return true
}
return false
}
}
if penultimateIndex != nil && lastIndex != nil {
self.coordArray.swapAt(penultimateIndex!, lastIndex!)
}
}
'''
I am creating a polygon using UIBezierpath and CAShapelayer. Added pan gesture on all 4 points that is UIView. When I drag the point A,B,C,D the expected behaviour is that bezierpath and CAShapelayer gets updated with the updated points. And when user drag the inner part of the shape all the path gets updated. But I am unable to update the path and shape. Can anyone help me with this?
yon set name for layer
var shape = CAShapeLayer()
shape.name = "name1"
and then, instead of updating, you can delete it by first searching by name, and then add
I'm trying to do the below progress
Using MKMagneticProgress, I was able to apply the bottom space, but now I'm stuck on how to do the spaces in the circle, for example in this image we have the progress of 7 items out of 10.
What I want is to update the MKMagneticProgress code to add the spaces.
the code taken from MKMagneticProgress with my update (I added the total)
import UIKit
// MARK: - Line Cap Enum
public enum LineCap : Int{
case round, butt, square
public func style() -> String {
switch self {
case .round:
return CAShapeLayerLineCap.round.rawValue
case .butt:
return CAShapeLayerLineCap.butt.rawValue
case .square:
return CAShapeLayerLineCap.square.rawValue
}
}
}
// MARK: - Orientation Enum
public enum Orientation: Int {
case left, top, right, bottom
}
#IBDesignable
open class CircleProgress: UIView {
// MARK: - Variables
private let titleLabelWidth:CGFloat = 100
private let percentLabel = UILabel(frame: .zero)
#IBInspectable open var titleLabel = UILabel(frame: .zero)
/// Stroke background color
#IBInspectable open var clockwise: Bool = true {
didSet {
layoutSubviews()
}
}
/// Stroke background color
#IBInspectable open var backgroundShapeColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
updateShapes()
}
}
/// Progress stroke color
#IBInspectable open var progressShapeColor: UIColor = .blue {
didSet {
updateShapes()
}
}
/// Line width
#IBInspectable open var lineWidth: CGFloat = 8.0 {
didSet {
updateShapes()
}
}
/// Space value
#IBInspectable open var spaceDegree: CGFloat = 45.0 {
didSet {
// if spaceDegree < 45.0{
// spaceDegree = 45.0
// }
//
// if spaceDegree > 135.0{
// spaceDegree = 135.0
// }
layoutSubviews()
updateShapes()
}
}
/// The progress shapes line width will be the `line width` minus the `inset`.
#IBInspectable open var inset: CGFloat = 0.0 {
didSet {
updateShapes()
}
}
// The progress percentage label(center label) format
#IBInspectable open var percentLabelFormat: String = "%.f %%" {
didSet {
percentLabel.text = String(format: percentLabelFormat, progress * 100)
}
}
#IBInspectable open var percentColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
percentLabel.textColor = percentColor
}
}
/// progress text (progress bottom label)
#IBInspectable open var title: String = "" {
didSet {
titleLabel.text = title
}
}
#IBInspectable open var titleColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
titleLabel.textColor = titleColor
}
}
// progress text (progress bottom label)
#IBInspectable open var font: UIFont = .systemFont(ofSize: 13) {
didSet {
titleLabel.font = font
percentLabel.font = font
}
}
// progress Orientation
open var orientation: Orientation = .bottom {
didSet {
updateShapes()
}
}
/// Progress shapes line cap.
open var lineCap: LineCap = .round {
didSet {
updateShapes()
}
}
/// Returns the total Items
private var _total: CGFloat = 1.0
#IBInspectable open var total: CGFloat {
set {
self._total = newValue
self.setProgress(progress: self.progress)
}
get {
return self._total
}
}
/// Returns the current progress.
#IBInspectable open private(set) var progress: CGFloat {
set {
progressShape?.strokeEnd = newValue
}
get {
return progressShape.strokeEnd
}
}
/// Duration for a complete animation from 0.0 to 1.0.
open var completeDuration: Double = 2.0
private var backgroundShape: CAShapeLayer!
private var progressShape: CAShapeLayer!
private var progressAnimation: CABasicAnimation!
// MARK: - Init
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
backgroundShape = CAShapeLayer()
backgroundShape.fillColor = nil
backgroundShape.strokeColor = backgroundShapeColor.cgColor
layer.addSublayer(backgroundShape)
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = 0.0
progressShape.strokeEnd = 0.1
layer.addSublayer(progressShape)
progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
percentLabel.frame = self.bounds
percentLabel.textAlignment = .center
// percentLabel.textColor = self.progressShapeColor
self.addSubview(percentLabel)
percentLabel.text = String(format: "%.1f%%", progress * 100)
titleLabel.frame = CGRect(x: (self.bounds.size.width-titleLabelWidth)/2, y: self.bounds.size.height-21, width: titleLabelWidth, height: 21)
titleLabel.textAlignment = .center
// titleLabel.textColor = self.progressShapeColor
titleLabel.text = title
titleLabel.contentScaleFactor = 0.3
// textLabel.adjustFontSizeToFit()
titleLabel.numberOfLines = 2
//textLabel.adjustFontSizeToFit()
titleLabel.adjustsFontSizeToFitWidth = true
self.addSubview(titleLabel)
}
// MARK: - Progress Animation
public func setProgress(progress: CGFloat, animated: Bool = true) {
let actualProgress = progress/self._total
if actualProgress > 1.0 {
return
}
var start = progressShape.strokeEnd
if let presentationLayer = progressShape.presentation(){
if let count = progressShape.animationKeys()?.count, count > 0 {
start = presentationLayer.strokeEnd
}
}
let duration = abs(Double(progress - start)) * completeDuration
percentLabel.text = String(format: percentLabelFormat, actualProgress * 100)
progressShape.strokeEnd = actualProgress
if animated {
progressAnimation.fromValue = start
progressAnimation.toValue = actualProgress
progressAnimation.duration = duration
progressShape.add(progressAnimation, forKey: progressAnimation.keyPath)
}
}
// MARK: - Layout
open override func layoutSubviews() {
super.layoutSubviews()
backgroundShape.frame = bounds
progressShape.frame = bounds
let rect = rectForShape()
backgroundShape.path = pathForShape(rect: rect).cgPath
progressShape.path = pathForShape(rect: rect).cgPath
self.titleLabel.frame = CGRect(x: (self.bounds.size.width - titleLabelWidth)/2, y: self.bounds.size.height-50, width: titleLabelWidth, height: 42)
updateShapes()
percentLabel.frame = self.bounds
}
private func updateShapes() {
backgroundShape?.lineWidth = lineWidth
backgroundShape?.strokeColor = backgroundShapeColor.cgColor
backgroundShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
progressShape?.strokeColor = progressShapeColor.cgColor
progressShape?.lineWidth = lineWidth - inset
progressShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
switch orientation {
case .left:
titleLabel.isHidden = true
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi / 2, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1.0)
case .right:
titleLabel.isHidden = true
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi * 1.5, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 1.5, 0, 0, 1.0)
case .bottom:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: temp.bounds.size.height-50, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi * 2, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 2, 0, 0, 1.0)
case .top:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: 0, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.progressShape.transform = CATransform3DMakeRotation( CGFloat.pi, 0, 0, 1.0)
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi, 0, 0, 1.0)
}
}
// MARK: - Helper
private func rectForShape() -> CGRect {
return bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
}
private func pathForShape(rect: CGRect) -> UIBezierPath {
let startAngle:CGFloat!
let endAngle:CGFloat!
if clockwise{
startAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
endAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
}else{
startAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
endAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
}
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY), radius: rect.size.width / 2.0, startAngle: startAngle, endAngle: endAngle
, clockwise: clockwise)
return path
}
}
My output:
Thanks.
I was able to figure out how to update the code to add multiple arcs here is the updated code
import UIKit
// MARK: - Line Cap Enum
public enum LineCap : Int{
case round, butt, square
public func style() -> String {
switch self {
case .round:
return CAShapeLayerLineCap.round.rawValue
case .butt:
return CAShapeLayerLineCap.butt.rawValue
case .square:
return CAShapeLayerLineCap.square.rawValue
}
}
}
// MARK: - Orientation Enum
public enum Orientation: Int {
case left, top, right, bottom
}
#IBDesignable
open class CircleProgress: UIView {
// MARK: - Variables
private let titleLabelWidth:CGFloat = 100
private let percentLabel = UILabel(frame: .zero)
#IBInspectable open var titleLabel = UILabel(frame: .zero)
/// Stroke background color
#IBInspectable open var clockwise: Bool = true {
didSet {
layoutSubviews()
}
}
/// Stroke background color
#IBInspectable open var backgroundShapeColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
updateShapes()
}
}
/// Progress stroke color
#IBInspectable open var progressShapeColor: UIColor = .blue {
didSet {
updateShapes()
}
}
/// Line width
#IBInspectable open var lineWidth: CGFloat = 5.0 {
didSet {
updateShapes()
}
}
/// Space value
#IBInspectable open var spaceDegree: CGFloat = 45.0 {
didSet {
// if spaceDegree < 45.0{
// spaceDegree = 45.0
// }
//
// if spaceDegree > 135.0{
// spaceDegree = 135.0
// }
layoutSubviews()
updateShapes()
}
}
/// The progress shapes line width will be the `line width` minus the `inset`.
#IBInspectable open var inset: CGFloat = 0.0 {
didSet {
updateShapes()
}
}
// The progress percentage label(center label) format
#IBInspectable open var percentLabelFormat: String = "%.f %%" {
didSet {
percentLabel.text = String(format: percentLabelFormat, progress * 100)
}
}
#IBInspectable open var percentColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
percentLabel.textColor = percentColor
}
}
/// progress text (progress bottom label)
#IBInspectable open var title: String = "" {
didSet {
titleLabel.text = title
}
}
#IBInspectable open var titleColor: UIColor = UIColor(white: 0.9, alpha: 0.5) {
didSet {
titleLabel.textColor = titleColor
}
}
// progress text (progress bottom label)
#IBInspectable open var font: UIFont = .systemFont(ofSize: 13) {
didSet {
titleLabel.font = font
percentLabel.font = font
}
}
// progress Orientation
open var orientation: Orientation = .bottom {
didSet {
updateShapes()
}
}
/// Progress shapes line cap.
open var lineCap: LineCap = .round {
didSet {
updateShapes()
}
}
/// Returns the total Items
private var _total: Int = 1
#IBInspectable open var total: Int {
set {
self._total = newValue
self.addProgressShapes()
}
get {
return self._total
}
}
/// Returns the current progress.
private var _progress: CGFloat = 0.0
#IBInspectable open private(set) var progress: CGFloat {
set {
self._progress = newValue
}
get {
return self._progress
}
}
/// Duration for a complete animation from 0.0 to 1.0.
open var completeDuration: Double = 1.0
private var backgroundShape: CAShapeLayer!
private var progressShapes: [CAShapeLayer]!
private var progressAnimation: CABasicAnimation!
// MARK: - Init
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
private func setup() {
backgroundShape = CAShapeLayer()
backgroundShape.fillColor = nil
backgroundShape.strokeColor = backgroundShapeColor.cgColor
layer.addSublayer(backgroundShape)
progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
percentLabel.frame = self.bounds
percentLabel.textAlignment = .center
// percentLabel.textColor = self.progressShapeColor
self.addSubview(percentLabel)
percentLabel.text = String(format: "%.1f%%", progress * 100)
titleLabel.frame = CGRect(x: (self.bounds.size.width-titleLabelWidth)/2, y: self.bounds.size.height-21, width: titleLabelWidth, height: 21)
titleLabel.textAlignment = .center
// titleLabel.textColor = self.progressShapeColor
titleLabel.text = title
titleLabel.contentScaleFactor = 0.3
// textLabel.adjustFontSizeToFit()
titleLabel.numberOfLines = 2
//textLabel.adjustFontSizeToFit()
titleLabel.adjustsFontSizeToFitWidth = true
self.addSubview(titleLabel)
}
// MARK: - Progress Animation
private func addProgressShapes(){
self.progressShapes = []
let progressSize = 1.0
var size:CGFloat = CGFloat(progressSize / Double(self.total))
let padingPercent = 0.2
let pading: Double = padingPercent * Double(self.total)/10 * Double(progressSize / Double(self.total - 1))
size = size - CGFloat(pading)
print("")
print("size: \(size) | pading: \(pading)")
print("------------------------")
var start: CGFloat = 0.0
var end: CGFloat = size
print("start: \(start) | end: \(end) ")
print("------------------------")
let progressShape: CAShapeLayer!
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = start
progressShape.strokeEnd = end
layer.addSublayer(progressShape)
self.progressShapes.append(progressShape)
for _ in 1..<self._total {
start = CGFloat(end + CGFloat(pading))
end = CGFloat(Double(start + size))
print("------------------------")
print("start: \(start) | end: \(end) ")
let progressShape: CAShapeLayer!
progressShape = CAShapeLayer()
progressShape.fillColor = nil
progressShape.strokeStart = start
progressShape.strokeEnd = end
layer.addSublayer(progressShape)
self.progressShapes.append(progressShape)
}
}
private func setProgressShapeFrame(){
guard self.progressShapes != nil else { return }
for shape in self.progressShapes {
shape.frame = bounds
let rect = rectForShape()
shape.path = pathForShape(rect: rect).cgPath
}
}
private func updateShapesWidth(){
guard self.progressShapes != nil else { return }
for i in 0..<self._total {
let shape = self.progressShapes[i]
let color = CGFloat(i) >= self.progress ? UIColor.white.withAlphaComponent(0.1).cgColor:progressShapeColor.cgColor
shape.strokeColor = color
shape.lineWidth = lineWidth - inset
shape.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
}
}
private func transformShapes(_ transform: CATransform3D){
guard self.progressShapes != nil else { return }
for shape in self.progressShapes {
shape.transform = transform
}
}
public func setProgress(progress: CGFloat, animated: Bool = true) {
self._progress = progress
// let actualProgress = progress/self._total
// if actualProgress > 1.0 {
// return
// }
//
// var start = progressShape.strokeEnd
// if let presentationLayer = progressShape.presentation(){
// if let count = progressShape.animationKeys()?.count, count > 0 {
// start = presentationLayer.strokeEnd
// }
// }
//
// let duration = abs(Double(progress - start)) * completeDuration
// percentLabel.text = String(format: percentLabelFormat, actualProgress * 100)
// progressShape.strokeEnd = actualProgress
//
// if animated {
// progressAnimation.fromValue = start
// progressAnimation.toValue = actualProgress
// progressAnimation.duration = duration
// progressShape.add(progressAnimation, forKey: progressAnimation.keyPath)
// }
}
// MARK: - Layout
open override func layoutSubviews() {
super.layoutSubviews()
backgroundShape.frame = bounds
let rect = rectForShape()
backgroundShape.path = pathForShape(rect: rect).cgPath
setProgressShapeFrame()
self.titleLabel.frame = CGRect(x: (self.bounds.size.width - titleLabelWidth)/2, y: self.bounds.size.height-50, width: titleLabelWidth, height: 42)
updateShapes()
percentLabel.frame = self.bounds
}
private func updateShapes() {
backgroundShape?.lineWidth = lineWidth
backgroundShape?.strokeColor = backgroundShapeColor.cgColor
backgroundShape?.lineCap = CAShapeLayerLineCap(rawValue: lineCap.style())
self.updateShapesWidth()
switch orientation {
case .left:
titleLabel.isHidden = true
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi / 2, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1.0)
case .right:
titleLabel.isHidden = true
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi * 1.5, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 1.5, 0, 0, 1.0)
case .bottom:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: temp.bounds.size.height-50, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi * 2, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi * 2, 0, 0, 1.0)
case .top:
titleLabel.isHidden = false
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [] , animations: { [weak self] in
if let temp = self{
temp.titleLabel.frame = CGRect(x: (temp.bounds.size.width - temp.titleLabelWidth)/2, y: 0, width: temp.titleLabelWidth, height: 42)
}
}, completion: nil)
self.transformShapes(CATransform3DMakeRotation( CGFloat.pi, 0, 0, 1.0))
self.backgroundShape.transform = CATransform3DMakeRotation(CGFloat.pi, 0, 0, 1.0)
}
}
// MARK: - Helper
private func rectForShape() -> CGRect {
return bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
}
private func pathForShape(rect: CGRect) -> UIBezierPath {
let startAngle:CGFloat!
let endAngle:CGFloat!
if clockwise{
startAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
endAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
}else{
startAngle = CGFloat((360.0 - spaceDegree) * (.pi / 180.0)) + (0.5 * .pi)
endAngle = CGFloat(spaceDegree * .pi / 180.0) + (0.5 * .pi)
}
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY), radius: rect.size.width / 2.0, startAngle: startAngle, endAngle: endAngle
, clockwise: clockwise)
return path
}
}
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)
}
}
I have tested the following code on all simulators and it works fine except on the iPad Pro. On the the iPad Pro is does not display. The gradient layer will work fine, it is only when I try and apply a mask that it will not appear on the iPad Pro Simulator:
func createOverlay()
{
if !(gradientLayer != nil)
{
self.gradientLayer = CAGradientLayer()
self.layer.addSublayer(gradientLayer)
}
gradientLayer.frame = self.bounds
print(gradientLayer.frame)
gradientLayer.colors = [appColour.CGColor, appColourDark.CGColor]
//--------FROM HERE ON DOES NOT WORK ON IPAD PRO, NO CRASH BUT LAYER DOES NOT APPEAR
self.alpha = maskAlpha
let maskLayer = CAShapeLayer()
let path = CGPathCreateMutable()
let rect: CGRect = CGRect(x: xOffset - offset, y: yOffset - offset, width: circleWidth + (offset * 2), height: circleHeight + (offset * 2))
let bPath = UIBezierPath(ovalInRect: rect)
CGPathAddRect(path, nil, CGRectMake(0, 0, self.frame.width, self.frame.height))
CGPathAddPath(path, nil, bPath.CGPath)
maskLayer.backgroundColor = UIColor.blackColor().CGColor
maskLayer.path = path
maskLayer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = maskLayer
self.clipsToBounds = true
}
I am hoping this is just a simulator issue but if you see something in my code that might be causing a problem please let me know.
I have tried replacing the gradient layer with a normal layer but it still does not display.
Here is full code, it is a custom sub-class of UIView and is the top layer of a view controller setup in IB:
import UIKit
protocol TipSpeechDelegate
{
func stopSpeaking()
}
#IBDesignable
class HoleMaskView: UIView
{
var xOffset : CGFloat = 0.0
var yOffset : CGFloat = 0.0
var circleWidth: CGFloat = 0.0
var circleHeight: CGFloat = 0.0
var maskAlpha: CGFloat = 0.9
var offset: CGFloat = 10.0
var inset: CGFloat = 8.0
var tipText: String = ""
var myLabel: UILabel?
var gradientLayer: CAGradientLayer!
var relativeCorner: RelativeCornerType = RelativeCornerType.upperLeftCorner
var delegate: TipSpeechDelegate!
override func layoutSubviews()
{
super.layoutSubviews()
}
override func drawRect(rect: CGRect)
{
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(HoleMaskView.handleTap(_:)))
self.addGestureRecognizer(gestureRecognizer)
}
func drawTipText()
{
createOverlay()
let height: CGFloat = self.frame.size.height
if (self.myLabel != nil)
{
myLabel?.removeFromSuperview()
}
if (yOffset < height/2)
{
self.myLabel = UILabel(frame: CGRect(x: inset, y: (yOffset + circleHeight), width: self.frame.width-(inset*2), height: self.frame.height-(yOffset + circleHeight)))
}
else
{
self.myLabel = UILabel(frame: CGRect(x: inset, y: 0, width: self.frame.width-(inset*2), height: yOffset))
}
self.myLabel?.text = tipText
self.myLabel?.textColor = UIColor.whiteColor()
self.myLabel?.font = UIFont(name: "Avenir-Medium", size: 20.0)
self.myLabel?.textAlignment = .Center
self.myLabel?.lineBreakMode = .ByWordWrapping
self.myLabel?.numberOfLines = 0
self.myLabel?.setNeedsLayout()
self.addSubview(myLabel!)
}
func updateTipText(text: String, circle: CGRect)
{
self.tipText = text
yOffset = circle.origin.y
xOffset = circle.origin.x
circleWidth = circle.size.width
circleHeight = circle.size.height
self.drawTipText()
}
func tipText(text: String, rFrame: CGRect, inView: UIView) -> Bool
{
showTipMask()
let convertedPoint = inView.convertPoint(rFrame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = rFrame.size.width
circleHeight = rFrame.size.height
self.drawTipText()
return true
}
func tipText(text: String, button: UIButton) -> Bool
{
if button.hidden
{
return false
}
showTipMask()
let convertedPoint = button.superview!.convertPoint(button.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = button.frame.size.width
circleHeight = button.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String, label: UILabel) -> Bool
{
if label.hidden
{
return false
}
showTipMask()
let convertedPoint = label.superview!.convertPoint(label.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = label.frame.size.width
circleHeight = label.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String, textView: UITextView) -> Bool
{
if textView.hidden
{
return false
}
showTipMask()
let convertedPoint = textView.superview!.convertPoint(textView.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = textView.frame.size.width
circleHeight = textView.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String) -> Bool
{
showTipMask()
self.tipText = text
yOffset = 0.0
xOffset = self.frame.size.width/2
circleWidth = 0.0
circleHeight = 0.0
self.drawTipText()
return true
}
func tipText(text: String, view: UIView) -> Bool
{
if view.hidden
{
return false
}
showTipMask()
let convertedPoint = view.superview!.convertPoint(view.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = view.frame.size.width
circleHeight = view.frame.size.height
self.drawTipText()
return true
}
func tipText(text: String, stepper: UIStepper) -> Bool
{
if stepper.hidden
{
return false
}
showTipMask()
let convertedPoint = stepper.superview!.convertPoint(stepper.frame.origin, toView: self)
self.tipText = text
yOffset = convertedPoint.y
xOffset = convertedPoint.x
circleWidth = stepper.frame.size.width
circleHeight = stepper.frame.size.height
self.drawTipText()
return true
}
func showTipMask()
{
self.alpha = alphaHide
self.hidden = false
UIView.animateWithDuration(0.5, animations:
{
self.alpha = alphaShow
}
)
}
func handleTap(gestureRecognizer: UIGestureRecognizer)
{
if delegate != nil
{
delegate.stopSpeaking()
}
print("tapped internal")
UIView.animateWithDuration(0.25, delay: 0.0, options: UIViewAnimationOptions.TransitionNone, animations:
{
() -> Void in
self.alpha = alphaHide
},
completion:
{
(finished: Bool) -> Void in
self.hidden = true
}
)
}
func createOverlay()
{
if !(gradientLayer != nil)
{
self.gradientLayer = CAGradientLayer()
self.layer.addSublayer(gradientLayer)
}
gradientLayer.frame = self.bounds
print(gradientLayer.frame)
gradientLayer.colors = [appColour.CGColor, appColourDark.CGColor]
self.alpha = maskAlpha
let maskLayer = CAShapeLayer()
let path = CGPathCreateMutable()
let rect: CGRect = CGRect(x: xOffset - offset, y: yOffset - offset, width: circleWidth + (offset * 2), height: circleHeight + (offset * 2))
let bPath = UIBezierPath(ovalInRect: rect)
CGPathAddRect(path, nil, CGRectMake(0, 0, self.frame.width, self.frame.height))
CGPathAddPath(path, nil, bPath.CGPath)
maskLayer.backgroundColor = UIColor.blackColor().CGColor
maskLayer.path = path
maskLayer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = maskLayer
self.clipsToBounds = true
}
}
Thanks
Greg
I am facing the same issue.
Currently I
turn on the Debug -> Optimize Rendering for Window Scale
scale it down to 50%
then the graph appears correctly.
But one of my colleges told me that he has to turn off the ORWS option. Wired.
Can anyone recommend a library for implementing growl or toast-style notifications on iOS? For example, after a user saves a profile, I want to have a notification fade in, linger for 3 seconds, report "profile saved", and fade out. Right now I have a UIAlertView that interrupts the user's workflow with a single "OK" button, and I feel like that is overkill.
The Android Toast class is an example of what I am looking for on iOS.
Thanks!
I created a solution that I think you'll find useful:
https://github.com/scalessec/toast
It's written as a obj-c category, essentially adding makeToast methods to any instance of UIView. eg:
[self.view makeToast:#"Profile saved"
duration:2.0
position:#"bottom"];
I solved it this way:
Create common label on your view. Make it all screen wide, give it the size you will need and center text in it.
Set it's position "on top" - this label must be below all of your controls in the list of controls.
Add it to interface, properties, synthesize (let's call it "toastLabel" there).
Associate in your XIB file with "toastLabel"
Add following line to your viewWillAppear to hide label for beginning:
[toastLabel setHidden:TRUE];
Add the following code on Button click (or some other event):
toastLabel.text = #"Our toast text";
[toastLabel setHidden:TRUE];
[toastLabel setAlpha:1.0];
CGPoint location;
location.x = 160;
location.y = 220;
toastLabel.center = location;
location.x = 160;
location.y = 320;
[toastLabel setHidden:FALSE];
[UIView animateWithDuration:0.9 animations:^{
toastLabel.alpha = 0.0;
toastLabel.center = location;
}];
This label will "fall down" and disappear.
Albeit a little late, here's my take on it:
https://github.com/pcperini/PCToastMessage
You could try my open source library TSMessages: https://github.com/toursprung/TSMessages
It's really easy to use and looks beautiful on iOS 5/6 and on iOS 7 as well.
I made my own. The class linked to by Krishnan was ugly and didn't rotate correctly.
https://github.com/esilverberg/ios-toast
Here's what it looks like:
I made it in this way :
+ (void)showToastMessage:(NSString *)message {
UIAlertView *toast = [[UIAlertView alloc] initWithTitle:nil
message:message
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:nil, nil];
[toast show];
// duration in seconds
int duration = 2;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[toast dismissWithClickedButtonIndex:0 animated:YES];
});
}
Updated solution for iOS9+:
+ (void)showToastMessage:(NSString *)message
root:(id)view {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil
message:message
preferredStyle:UIAlertControllerStyleAlert];
// duration in seconds
int duration = 2;
[view presentViewController:alertController animated:YES completion:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[alertController dismissViewControllerAnimated:YES completion:nil];
});
}
MBProgressHUD?
Hey you are looking for this.
https://github.com/PaulSolt/WEPopover#readme
This one is exactly what you want.
This one is quite handy as well since it has a completion block, please have a look :) https://github.com/PrajeetShrestha/EkToast
Swift 2.0:
The idea is to work out a Toast class with zero dependency on CocoaPods.
Reference: https://github.com/scalessec/Toast-Swift
Make an empty Swift file (file-New-File- Empty Swift File - Name it Toast.)
Add the following code to it.
// Toast.swift
import UIKit
import ObjectiveC
enum ToastPosition {
case Top
case Center
case Bottom
}
extension UIView {
private struct ToastKeys {
static var Timer = "CSToastTimerKey"
static var Duration = "CSToastDurationKey"
static var Position = "CSToastPositionKey"
static var Completion = "CSToastCompletionKey"
static var ActiveToast = "CSToastActiveToastKey"
static var ActivityView = "CSToastActivityViewKey"
static var Queue = "CSToastQueueKey"
}
private class ToastCompletionWrapper {
var completion: ((Bool) -> Void)?
init(_ completion: ((Bool) -> Void)?) {
self.completion = completion
}
}
private enum ToastError: ErrorType {
case InsufficientData
}
private var queue: NSMutableArray {
get {
if let queue = objc_getAssociatedObject(self, &ToastKeys.Queue) as? NSMutableArray {
return queue
} else {
let queue = NSMutableArray()
objc_setAssociatedObject(self, &ToastKeys.Queue, queue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return queue
}
}
}
// MARK: - Make Toast Methods
func makeToast(message: String) {
self.makeToast(message, duration: ToastManager.shared.duration, position: ToastManager.shared.position)
}
func makeToast(message: String, duration: NSTimeInterval, position: ToastPosition) {
self.makeToast(message, duration: duration, position: position, style: nil)
}
func makeToast(message: String, duration: NSTimeInterval, position: CGPoint) {
self.makeToast(message, duration: duration, position: position, style: nil)
}
func makeToast(message: String, duration: NSTimeInterval, position: ToastPosition, style: ToastStyle?) {
self.makeToast(message, duration: duration, position: position, title: nil, image: nil, style: style, completion: nil)
}
func makeToast(message: String, duration: NSTimeInterval, position: CGPoint, style: ToastStyle?) {
self.makeToast(message, duration: duration, position: position, title: nil, image: nil, style: style, completion: nil)
}
func makeToast(message: String?, duration: NSTimeInterval, position: ToastPosition, title: String?, image: UIImage?, style: ToastStyle?, completion: ((didTap: Bool) -> Void)?) {
var toastStyle = ToastManager.shared.style
if let style = style {
toastStyle = style
}
do {
let toast = try self.toastViewForMessage(message, title: title, image: image, style: toastStyle)
self.showToast(toast, duration: duration, position: position, completion: completion)
} catch ToastError.InsufficientData {
print("Error: message, title, and image are all nil")
} catch {}
}
func makeToast(message: String?, duration: NSTimeInterval, position: CGPoint, title: String?, image: UIImage?, style: ToastStyle?, completion: ((didTap: Bool) -> Void)?) {
var toastStyle = ToastManager.shared.style
if let style = style {
toastStyle = style
}
do {
let toast = try self.toastViewForMessage(message, title: title, image: image, style: toastStyle)
self.showToast(toast, duration: duration, position: position, completion: completion)
} catch ToastError.InsufficientData {
print("Error: message, title, and image cannot all be nil")
} catch {}
}
// MARK: - Show Toast Methods
func showToast(toast: UIView) {
self.showToast(toast, duration: ToastManager.shared.duration, position: ToastManager.shared.position, completion: nil)
}
func showToast(toast: UIView, duration: NSTimeInterval, position: ToastPosition, completion: ((didTap: Bool) -> Void)?) {
let point = self.centerPointForPosition(position, toast: toast)
self.showToast(toast, duration: duration, position: point, completion: completion)
}
func showToast(toast: UIView, duration: NSTimeInterval, position: CGPoint, completion: ((didTap: Bool) -> Void)?) {
objc_setAssociatedObject(toast, &ToastKeys.Completion, ToastCompletionWrapper(completion), .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if let _ = objc_getAssociatedObject(self, &ToastKeys.ActiveToast) as? UIView where ToastManager.shared.queueEnabled {
objc_setAssociatedObject(toast, &ToastKeys.Duration, NSNumber(double: duration), .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(toast, &ToastKeys.Position, NSValue(CGPoint: position), .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
self.queue.addObject(toast)
} else {
self.showToast(toast, duration: duration, position: position)
}
}
// MARK: - Activity Methods
func makeToastActivity(position: ToastPosition) {
// sanity
if let _ = objc_getAssociatedObject(self, &ToastKeys.ActiveToast) as? UIView {
return
}
let toast = self.createToastActivityView()
let point = self.centerPointForPosition(position, toast: toast)
self.makeToastActivity(toast, position: point)
}
func makeToastActivity(position: CGPoint) {
// sanity
if let _ = objc_getAssociatedObject(self, &ToastKeys.ActiveToast) as? UIView {
return
}
let toast = self.createToastActivityView()
self.makeToastActivity(toast, position: position)
}
//Dismisses the active toast activity indicator view.
func hideToastActivity() {
if let toast = objc_getAssociatedObject(self, &ToastKeys.ActivityView) as? UIView {
UIView.animateWithDuration(ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.CurveEaseIn, .BeginFromCurrentState], animations: { () -> Void in
toast.alpha = 0.0
}, completion: { (finished: Bool) -> Void in
toast.removeFromSuperview()
objc_setAssociatedObject(self, &ToastKeys.ActivityView, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
})
}
}
// MARK: - Private Activity Methods
private func makeToastActivity(toast: UIView, position: CGPoint) {
toast.alpha = 0.0
toast.center = position
objc_setAssociatedObject(self, &ToastKeys.ActivityView, toast, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
self.addSubview(toast)
UIView.animateWithDuration(ToastManager.shared.style.fadeDuration, delay: 0.0, options: .CurveEaseOut, animations: { () -> Void in
toast.alpha = 1.0
}, completion: nil)
}
private func createToastActivityView() -> UIView {
let style = ToastManager.shared.style
let activityView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: style.activitySize.width, height: style.activitySize.height))
activityView.backgroundColor = style.backgroundColor
activityView.autoresizingMask = [.FlexibleLeftMargin, .FlexibleRightMargin, .FlexibleTopMargin, .FlexibleBottomMargin]
activityView.layer.cornerRadius = style.cornerRadius
if style.displayShadow {
activityView.layer.shadowColor = style.shadowColor.CGColor
activityView.layer.shadowOpacity = style.shadowOpacity
activityView.layer.shadowRadius = style.shadowRadius
activityView.layer.shadowOffset = style.shadowOffset
}
let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .WhiteLarge)
activityIndicatorView.center = CGPoint(x: activityView.bounds.size.width / 2.0, y: activityView.bounds.size.height / 2.0)
activityView.addSubview(activityIndicatorView)
activityIndicatorView.startAnimating()
return activityView
}
// MARK: - Private Show/Hide Methods
private func showToast(toast: UIView, duration: NSTimeInterval, position: CGPoint) {
toast.center = position
toast.alpha = 0.0
if ToastManager.shared.tapToDismissEnabled {
let recognizer = UITapGestureRecognizer(target: self, action: "handleToastTapped:")
toast.addGestureRecognizer(recognizer)
toast.userInteractionEnabled = true
toast.exclusiveTouch = true
}
objc_setAssociatedObject(self, &ToastKeys.ActiveToast, toast, .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
self.addSubview(toast)
UIView.animateWithDuration(ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.CurveEaseOut, .AllowUserInteraction], animations: { () -> Void in
toast.alpha = 1.0
}) { (Bool finished) -> Void in
let timer = NSTimer(timeInterval: duration, target: self, selector: "toastTimerDidFinish:", userInfo: toast, repeats: false)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
objc_setAssociatedObject(toast, &ToastKeys.Timer, timer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
private func hideToast(toast: UIView) {
self.hideToast(toast, fromTap: false)
}
private func hideToast(toast: UIView, fromTap: Bool) {
UIView.animateWithDuration(ToastManager.shared.style.fadeDuration, delay: 0.0, options: [.CurveEaseIn, .BeginFromCurrentState], animations: { () -> Void in
toast.alpha = 0.0
}) { (didFinish: Bool) -> Void in
toast.removeFromSuperview()
objc_setAssociatedObject(self, &ToastKeys.ActiveToast, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if let wrapper = objc_getAssociatedObject(toast, &ToastKeys.Completion) as? ToastCompletionWrapper, completion = wrapper.completion {
completion(fromTap)
}
if let nextToast = self.queue.firstObject as? UIView, duration = objc_getAssociatedObject(nextToast, &ToastKeys.Duration) as? NSNumber, position = objc_getAssociatedObject(nextToast, &ToastKeys.Position) as? NSValue {
self.queue.removeObjectAtIndex(0)
self.showToast(nextToast, duration: duration.doubleValue, position: position.CGPointValue())
}
}
}
// MARK: - Events
func handleToastTapped(recognizer: UITapGestureRecognizer) {
if let toast = recognizer.view, timer = objc_getAssociatedObject(toast, &ToastKeys.Timer) as? NSTimer {
timer.invalidate()
self.hideToast(toast, fromTap: true)
}
}
func toastTimerDidFinish(timer: NSTimer) {
if let toast = timer.userInfo as? UIView {
self.hideToast(toast)
}
}
// MARK: - Toast Construction
func toastViewForMessage(message: String?, title: String?, image: UIImage?, style: ToastStyle) throws -> UIView {
// sanity
if message == nil && title == nil && image == nil {
throw ToastError.InsufficientData
}
var messageLabel: UILabel?
var titleLabel: UILabel?
var imageView: UIImageView?
let wrapperView = UIView()
wrapperView.backgroundColor = style.backgroundColor
wrapperView.autoresizingMask = [.FlexibleLeftMargin, .FlexibleRightMargin, .FlexibleTopMargin, .FlexibleBottomMargin]
wrapperView.layer.cornerRadius = style.cornerRadius
if style.displayShadow {
wrapperView.layer.shadowColor = UIColor.blackColor().CGColor
wrapperView.layer.shadowOpacity = style.shadowOpacity
wrapperView.layer.shadowRadius = style.shadowRadius
wrapperView.layer.shadowOffset = style.shadowOffset
}
if let image = image {
imageView = UIImageView(image: image)
imageView?.contentMode = .ScaleAspectFit
imageView?.frame = CGRect(x: style.horizontalPadding, y: style.verticalPadding, width: style.imageSize.width, height: style.imageSize.height)
}
var imageRect = CGRectZero
if let imageView = imageView {
imageRect.origin.x = style.horizontalPadding
imageRect.origin.y = style.verticalPadding
imageRect.size.width = imageView.bounds.size.width
imageRect.size.height = imageView.bounds.size.height
}
if let title = title {
titleLabel = UILabel()
titleLabel?.numberOfLines = style.titleNumberOfLines
titleLabel?.font = style.titleFont
titleLabel?.textAlignment = style.titleAlignment
titleLabel?.lineBreakMode = .ByTruncatingTail
titleLabel?.textColor = style.titleColor
titleLabel?.backgroundColor = UIColor.clearColor();
titleLabel?.text = title;
let maxTitleSize = CGSize(width: (self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, height: self.bounds.size.height * style.maxHeightPercentage)
let titleSize = titleLabel?.sizeThatFits(maxTitleSize)
if let titleSize = titleSize {
titleLabel?.frame = CGRect(x: 0.0, y: 0.0, width: titleSize.width, height: titleSize.height)
}
}
if let message = message {
messageLabel = UILabel()
messageLabel?.text = message
messageLabel?.numberOfLines = style.messageNumberOfLines
messageLabel?.font = style.messageFont
messageLabel?.textAlignment = style.messageAlignment
messageLabel?.lineBreakMode = .ByTruncatingTail;
messageLabel?.textColor = style.messageColor
messageLabel?.backgroundColor = UIColor.clearColor()
let maxMessageSize = CGSize(width: (self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, height: self.bounds.size.height * style.maxHeightPercentage)
let messageSize = messageLabel?.sizeThatFits(maxMessageSize)
if let messageSize = messageSize {
messageLabel?.frame = CGRect(x: 0.0, y: 0.0, width: messageSize.width, height: messageSize.height)
}
}
var titleRect = CGRectZero
if let titleLabel = titleLabel {
titleRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding
titleRect.origin.y = style.verticalPadding
titleRect.size.width = titleLabel.bounds.size.width
titleRect.size.height = titleLabel.bounds.size.height
}
var messageRect = CGRectZero
if let messageLabel = messageLabel {
messageRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding
messageRect.origin.y = titleRect.origin.y + titleRect.size.height + style.verticalPadding
messageRect.size.width = messageLabel.bounds.size.width
messageRect.size.height = messageLabel.bounds.size.height
}
let longerWidth = max(titleRect.size.width, messageRect.size.width)
let longerX = max(titleRect.origin.x, messageRect.origin.x)
let wrapperWidth = max((imageRect.size.width + (style.horizontalPadding * 2.0)), (longerX + longerWidth + style.horizontalPadding))
let wrapperHeight = max((messageRect.origin.y + messageRect.size.height + style.verticalPadding), (imageRect.size.height + (style.verticalPadding * 2.0)))
wrapperView.frame = CGRect(x: 0.0, y: 0.0, width: wrapperWidth, height: wrapperHeight)
if let titleLabel = titleLabel {
titleLabel.frame = titleRect
wrapperView.addSubview(titleLabel)
}
if let messageLabel = messageLabel {
messageLabel.frame = messageRect
wrapperView.addSubview(messageLabel)
}
if let imageView = imageView {
wrapperView.addSubview(imageView)
}
return wrapperView
}
// MARK: - Helpers
private func centerPointForPosition(position: ToastPosition, toast: UIView) -> CGPoint {
let padding: CGFloat = ToastManager.shared.style.verticalPadding
switch(position) {
case .Top:
return CGPoint(x: self.bounds.size.width / 2.0, y: (toast.frame.size.height / 2.0) + padding)
case .Center:
return CGPoint(x: self.bounds.size.width / 2.0, y: self.bounds.size.height / 2.0)
case .Bottom:
return CGPoint(x: self.bounds.size.width / 2.0, y: (self.bounds.size.height - (toast.frame.size.height / 2.0)) - padding)
}
}
}
// MARK: - Toast Style
struct ToastStyle {
var backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.8)
var titleColor = UIColor.whiteColor()
var messageColor = UIColor.whiteColor()
var maxWidthPercentage: CGFloat = 0.8 {
didSet {
maxWidthPercentage = max(min(maxWidthPercentage, 1.0), 0.0)
}
}
var maxHeightPercentage: CGFloat = 0.8 {
didSet {
maxHeightPercentage = max(min(maxHeightPercentage, 1.0), 0.0)
}
}
var horizontalPadding: CGFloat = 10.0
var verticalPadding: CGFloat = 10.0
var cornerRadius: CGFloat = 10.0;
var titleFont = UIFont.boldSystemFontOfSize(16.0)
var messageFont = UIFont.systemFontOfSize(16.0)
var titleAlignment = NSTextAlignment.Left
var messageAlignment = NSTextAlignment.Left
var titleNumberOfLines = 0;
var messageNumberOfLines = 0;
var displayShadow = false;
var shadowColor = UIColor.blackColor()
var shadowOpacity: Float = 0.8 {
didSet {
shadowOpacity = max(min(shadowOpacity, 1.0), 0.0)
}
}
var shadowRadius: CGFloat = 6.0
var shadowOffset = CGSize(width: 4.0, height: 4.0)
var imageSize = CGSize(width: 80.0, height: 80.0)
var activitySize = CGSize(width: 100.0, height: 100.0)
var fadeDuration: NSTimeInterval = 0.2
}
// MARK: - Toast Manager
class ToastManager {
static let shared = ToastManager()
var style = ToastStyle()
var tapToDismissEnabled = true
var queueEnabled = true
var duration: NSTimeInterval = 3.0
var position = ToastPosition.Bottom
}
Using Toast.swift:
// basic usage
self.view.makeToast("Sample Toast")
// toast with a specific duration and position
self.view.makeToast("Sample Toast", duration: 3.0, position: .Top)
// toast with all possible options
self.view.makeToast("Sample Toast", duration: 2.0, position: CGPoint(x: 110.0, y: 110.0), title: "Toast Title", image: UIImage(named: "ic_120x120.png"), style:nil) { (didTap: Bool) -> Void in
if didTap {
print("completion from tap")
} else {
print("completion without tap")
}
}
//display toast with an activity spinner
self.view.makeToastActivity(.Center)
// display any view as toast
let sampleView = UIView(frame: CGRectMake(100,100,200,200))
sampleView.backgroundColor = UIColor(patternImage: UIImage(named: "ic_120x120")!)
self.view.showToast(sampleView)
self.view.showToast(sampleView, duration: 3.0, position: .Top, completion: nil)
You can download a sample project from https://github.com/alvinreuben/ToastSample
This is something, which was needed by you. Created with lot of Animation Options, screen positions, and duration. Even you can provide your own duration. Check that out below.
https://github.com/avistyles/AVIToast
Swift 3
Have been using Rannie/Toast-Swift for Swift 3 very happily for a while now and can recommend it for a very similar "Android-like" experience. Its very simple to implement without needing another pod and pretty customizable depending on your needs.
Easy Peasy
view.makeToastActivity()
view.hideToastActivity()