Glitchy map AnnotationView Behavior - ios

I've created a custom MKAnnotationView similar to this project created by Apple. In it, I place an imageView and a label. However, I am noticing very glitchy behavior as I scroll and zoom around on the map. Even right after launching this behavior is displayed. The views can be seen briefly in the upper left-hand corner before they jump around.
Below is a link to a gif of the problem
The Problem
Below is the code for the Custom AnnotationView
Update
I switched to using a modified version of the AnnotationView apple uses with the suggestions below but I'm still experiencing the same jumpy behavior jumpy behavior
a link to the complete code
import Foundation
import UIKit
import MapKit
import SDWebImage
class AppleCustomAnnotationView: MKAnnotationView {
private let boxInset = CGFloat(10)
private let interItemSpacing = CGFloat(10)
private let maxContentWidth = CGFloat(90)
private let contentInsets = UIEdgeInsets(top: 10, left: 30, bottom: 20, right: 20)
private lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [label, imageView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .top
stackView.spacing = interItemSpacing
return stackView
}()
private lazy var label: UILabel = {
let label = UILabel(frame: .zero)
label.textColor = UIColor.white
label.lineBreakMode = .byWordWrapping
label.backgroundColor = UIColor.clear
label.numberOfLines = 2
label.font = UIFont.preferredFont(forTextStyle: .caption1)
return label
}()
private lazy var imageView: UIImageView = {
let imageView = UIImageView(image: nil)
return imageView
}()
private var imageHeightConstraint: NSLayoutConstraint?
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
backgroundColor = UIColor.clear
translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
// Anchor the top and leading edge of the stack view to let it grow to the content size.
stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: contentInsets.left).isActive = true
stackView.topAnchor.constraint(equalTo: self.topAnchor, constant: contentInsets.top).isActive = true
// Limit how much the content is allowed to grow.
imageView.widthAnchor.constraint(lessThanOrEqualToConstant: maxContentWidth).isActive = true
label.widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
label.text = nil
}
override func prepareForDisplay() {
super.prepareForDisplay()
/*
If using the same annotation view and reuse identifier for multiple annotations, iOS will reuse this view by calling `prepareForReuse()`
so the view can be put into a known default state, and `prepareForDisplay()` right before the annotation view is displayed. This method is the view's opportunity to update itself to display content for the new annotation.
*/
if let annotation = annotation as? ImageAnnotation {
label.text = annotation.title
let placeHolder = #imageLiteral(resourceName: "DSC00042")
self.imageView.sd_setImage(with: annotation.photoURL, placeholderImage: placeHolder, options: SDWebImageOptions.refreshCached, completed: {(image,error,imageCacheType,storageReference) in
if let error = error{
print("Uh-Oh an error has occured: \(error.localizedDescription)" )
}
guard let image = image else{
return
}
if let heightConstraint = self.imageHeightConstraint {
self.imageView.removeConstraint(heightConstraint)
}
let ratio = image.size.height / image.size.width
self.imageHeightConstraint = self.imageView.heightAnchor.constraint(equalTo: self.imageView.widthAnchor, multiplier: ratio, constant: 0)
self.imageHeightConstraint?.isActive = true
})
}
// Since the image and text sizes may have changed, require the system do a layout pass to update the size of the subviews.
setNeedsLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
// The stack view will not have a size until a `layoutSubviews()` pass is completed. As this view's overall size is the size
// of the stack view plus a border area, the layout system needs to know that this layout pass has invalidated this view's
// `intrinsicContentSize`.
invalidateIntrinsicContentSize()
// The annotation view's center is at the annotation's coordinate. For this annotation view, offset the center so that the
// drawn arrow point is the annotation's coordinate.
let contentSize = intrinsicContentSize
centerOffset = CGPoint(x: 0, y: -contentSize.height/2)
// Now that the view has a new size, the border needs to be redrawn at the new size.
setNeedsDisplay()
}
override var intrinsicContentSize: CGSize {
var size = stackView.bounds.size
size.width += contentInsets.left + contentInsets.right
size.height += contentInsets.top + contentInsets.bottom + 30
return size
}
override func draw(_ rect: CGRect) {
super.draw(rect)
// Used to draw the rounded background box and pointer.
UIColor.darkGray.setFill()
// let path2 = UIBezierPath(ovalIn: CGRect(x: rect.width/2, y: rect.height - 10, width: 10, height: 10))
//
// let shapeLayer2 = CAShapeLayer()
// shapeLayer2.path = path2.cgPath
// shapeLayer2.fillColor = UIColor.purple.cgColor
// shapeLayer2.strokeColor = UIColor.white.cgColor
// shapeLayer2.lineWidth = 1
//
// layer.addSublayer(shapeLayer2)
// shapeLayer2.position = CGPoint(x: -10, y: 15)
// Draw the pointed shape.
// let pointShape = UIBezierPath(ovalIn: CGRect(x: rect.width/2, y: rect.height - 10, width: 10, height: 10))
// pointShape.move(to: CGPoint(x: 14, y: 0))
// pointShape.addLine(to: CGPoint.zero)
// pointShape.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height))
// pointShape.fill()
// Draw the rounded box.
let box = CGRect(x: boxInset, y: 0, width: rect.size.width - boxInset, height: rect.size.height - 30)
let roundedRect = UIBezierPath(roundedRect: box, cornerRadius: 5)
roundedRect.lineWidth = 2
roundedRect.fill()
UIColor.purple.setFill()
let circleDot = UIBezierPath(ovalIn: CGRect(x: box.midX - 7.5, y: rect.size.height - 15, width: 15, height: 15))
circleDot.lineWidth = 2
circleDot.fill()
}
}
Delegate
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation{
return nil
}
if let annotation = annotation as? ImageAnnotation{
guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "Apple", for: annotation) as? AppleCustomAnnotationView else{
fatalError("Unexpected annotation view type")
}
annotationView.annotation = annotation
annotationView.clusteringIdentifier = MKMapViewDefaultClusterAnnotationViewReuseIdentifier
annotationView.collisionMode = .rectangle
return annotationView
}else if let cluster = annotation as? MKClusterAnnotation{
guard let view = mapView.dequeueReusableAnnotationView(withIdentifier: "ClusterAnnotationView", for: annotation) as? AppleClusterAnnotationView else{
fatalError("Wrong type for cluster annotationview")
}
return view
}
return nil
}

There is one very unusual thing you do: you override init and you are doing your main layout work there.
This makes no sense, since an MKAnnotationView can be reused to display different annotations.
You make use of this reuse feature in the delegate.
This way of implementing it is also not shown in the Appl project you are citing.
What you should do in your MKAnnotationView is:
override var annotation: MKAnnotation? {
willSet {
// do what you did in override init(...) {...} before.
// be aware that Apple sets annotation to nil if it is not used any more.
}
}
in your delegate, after dequeueing only do
annotationView.annotation = annotation
make sure you don't display anything in case of view.annotation == nil.
This makes sure the same layout code is used for the dequeue case and when you create a new MKAnnotationView
Hope this helps

Related

Change a custom MKAnnotationView when it is selected

I have a MKMapView with several different types of MKAnnotationViews on it. Chiefly, I have a PricedAnnotationView, which is a fairly complicated MKAnnotationView with a UILabel and some other views. The label can have different text in it, based off of the original MKAnnotation that it is associated with.
class PricedAnnotationView: MKAnnotationView {
static let ReuseID = "pricedAnnotation"
let labelContainingView: UIView = {
let containingView = UIView()
containingView.backgroundColor = .blue
containingView.layer.cornerRadius = 3
containingView.translatesAutoresizingMaskIntoConstraints = false
return containingView
}()
let label: UILabel = {
let label = UILabel(frame: CGRect.zero)
label.textColor = .white
label.font = ViewConstants.Text.BodyFontBold
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let containingView: UIView = {
let v = UIView(frame:CGRect.zero)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
let triangle = UIView(frame: CGRect(x: 0, y: 0, width: 9, height: 9))
triangle.translatesAutoresizingMaskIntoConstraints = false
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 5.5, y: 11))
path.addLine(to: CGPoint(x: 11, y: 0))
path.close()
let triangleLayer = CAShapeLayer()
triangleLayer.path = path.cgPath
triangleLayer.fillColor = UIColor.blue.cgColor
triangle.layer.addSublayer(triangleLayer)
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
self.canShowCallout = false
self.frame = labelContainingView.frame
labelContainingView.addSubview(label)
containingView.addSubview(labelContainingView)
containingView.addSubview(triangle)
self.addSubview(containingView)
// Get the label correctly inset in the labelContainingView
label.topAnchor.constraint(equalTo: labelContainingView.topAnchor, constant: 3).isActive = true
label.leadingAnchor.constraint(equalTo: labelContainingView.leadingAnchor, constant: 6).isActive = true
label.trailingAnchor.constraint(equalTo: labelContainingView.trailingAnchor, constant: -6).isActive = true
label.bottomAnchor.constraint(equalTo: labelContainingView.bottomAnchor, constant: -3).isActive = true
// The triangle.topAnchor purposefully puts the triangle a bit under the label. In testing, while moving around
// the map, a little gap would appear between the label and the triangle. This change fixes that. The triangle
// was made two pixels bigger so that it would appear to be the same size.
triangle.topAnchor.constraint(equalTo: labelContainingView.bottomAnchor, constant: -2).isActive = true
triangle.centerXAnchor.constraint(equalTo: labelContainingView.centerXAnchor).isActive = true
triangle.widthAnchor.constraint(equalToConstant: 11).isActive = true
triangle.heightAnchor.constraint(equalToConstant: 11).isActive = true
containingView.topAnchor.constraint(equalTo: labelContainingView.topAnchor).isActive = true
containingView.leadingAnchor.constraint(equalTo: labelContainingView.leadingAnchor).isActive = true
containingView.trailingAnchor.constraint(equalTo: labelContainingView.trailingAnchor).isActive = true
containingView.bottomAnchor.constraint(equalTo: triangle.bottomAnchor).isActive = true
}
/// - Tag: DisplayConfiguration
override func prepareForDisplay() {
super.prepareForDisplay()
displayPriority = .required
guard let annotation = annotation as? MyAnnotation else { return }
if case let .priced(price, currencyCode) = annotation.status {
label.text = StringFormatter.formatCurrency(amount: price, currencyCode: currencyCode)
}
self.layoutIfNeeded() // Calculate the size from the constraints so we can know the frame.
self.frame = containingView.frame
self.centerOffset = CGPoint(x: 0, y: -(containingView.frame.height / 2)) // Center point should be where the triangle points
}
}
Every other annotation is much simpler and just uses one of two UIImage to support itself. I use two different reuse Ids with those simpler MKAnnotationView so I don't have to keep setting the image; I don't know if that is a best practice or not. Here is a sample of how I do it:
private let unpricedAnnotationClusterId = "unpriced"
private let unpricedAnnotationReuseID = "unpricedAnnotation"
func createUnpricedAnnotation(mapView: MKMapView, annotation: MKAnnotation) -> MKAnnotationView {
let annotationView: MKAnnotationView
if let dequeuedAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: unpricedAnnotationReuseID) {
annotationView = dequeuedAnnotationView
annotationView.annotation = annotation
} else {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: unpricedAnnotationReuseID)
annotationView.image = UIImage(bundleAsset: "map_pin_blue")
annotationView.centerOffset = CGPoint(x: 0, y: -(annotationView.frame.height / 2)) // Center point should be where the pin points
annotationView.clusteringIdentifier = unpricedAnnotationClusterId
}
return annotationView
}
When the user taps on an annotation in the map, I have an information window below the map populate with information about the selected item. That works fine. I would like it if the annotation view changed though so it was clear as to where the item is that is being displayed. I would be okay with just setting it to an image. However, I can't find any example of how to do that; most examples just change the image. When I try that with PricedAnnotationView the previous label does not go away, although the two simpler annotations work fine. I'm also unsure how changing the image would affect the reuse Ids that I am using. I don't see a way to manually change the reuse identifier.
My MKMapViewDelegate has these functions:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if allowSelection {
if let item : MyAnnotation = view.annotation as? MyAnnotation {
...
// display selected item in information view
...
}
view.image = UIImage(bundleAsset: "map_pin_red")
}
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? MyAnnotation else {
return nil
}
switch annotation.status {
case .notPriced:
return createUnpricedAnnotation(mapView: mapView, annotation: annotation)
case .priced(_, _):
return PricedAnnotationView(annotation: annotation, reuseIdentifier: PricedAnnotationView.ReuseID)
default:
return createErrorAnnotation(mapView: mapView, annotation: annotation)
}
}
If I could go the route of just setting the image, how do I change it back after the user has selected something else. Would I need to recreate PricedAnnotationView from scratch if it was previously one of those annotation? What would that do to the reuse Ids and the reuse queue?
When I have just changed the image, the view for PricedAnnotationView does not actually go away, and is just moved to the side as the image is shown beneath.
Ideally, I could do something to trigger a call to mapView(_:viewFor:) and have some intelligence in there to remember if the item is selected or now, but I have not found anything in my research to show how to do that. (It is not completely trivial because the isSelected property does not seem to be set on the MKAnnotationView that I would have just dequeued. No surprise there.)
Any suggestions as to how to handle this would be greatly appreciated.
I've played around with this, and here is what I've done to get around this issue:
I stopped using different reuse ids for the two types of image annotations. I created this function to manage that:
func createImageAnnotation(mapView: MKMapView, annotation: MKAnnotation, imageString: String) -> MKAnnotationView {
let annotationView: MKAnnotationView
if let dequeuedAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: imageAnnotationReuseID) {
annotationView = dequeuedAnnotationView
annotationView.annotation = annotation
} else {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: imageAnnotationReuseID)
}
annotationView.image = UIImage(bundleAsset: imageString)
annotationView.centerOffset = CGPoint(x: 0, y: -(annotationView.frame.height / 2)) // Center point should be where the pin points
annotationView.clusteringIdentifier = imageAnnotationClusterId
return annotationView
}
I then added to the select and deselect calls so that the PricedAnnotationView is hidden and the image set when appropriate. Like this:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if allowSelection {
if let item : MyAnnotation = view.annotation as? MyAnnotation {
...
// display selected item in information view
...
view.image = UIImage(bundleAsset: selectedAnnotationImage)
if case .priced(_,_) = item.rateStatus {
(view as! PricedAnnotationView).containingView.isHidden = true
}
view.centerOffset = CGPoint(x: 0, y: -(view.frame.height / 2)) // Center point should be where the pin points
}
}
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
guard let annotation = view.annotation as? MyAnnotation else { return }
switch annotation.rateStatus {
case .notPriced:
view.image = UIImage(bundleAsset: unpricedAnnotationImage)
case .priced(_, _):
view.image = nil
if let myView = (view as? PricedAnnotationView) {
myView.containingView.isHidden = false
view.frame = myView.containingView.frame
view.centerOffset = CGPoint(x: 0, y: -(myView.containingView.frame.height / 2)) // Center point should be where the triangle points
}
default:
view.image = UIImage(bundleAsset: errorAnnotationImage)
}
}
This seems to mostly work, with the only quirk being that when the PricedAnnotationView is changed to the image, the image briefly and visibly shrinks from the old frame size to the correct frame size. However, if I do not set the frame, then the PricedAnnotationView is oddly offset.

Animate UIView's Layer with constrains (Auto Layout Animations)

I am working on project where I need to animate height of view which consist of shadow, gradient and rounded corners (not all corners).
So I have used layerClass property of view.
Below is simple example demonstration.
Problem is that, when I change height of view by modifying its constraint, it was resulting in immediate animation of layer class, which is kind of awkward.
Below is my sample code
import UIKit
class CustomView: UIView{
var isAnimating: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
func setupView(){
self.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
guard let layer = self.layer as? CAShapeLayer else { return }
layer.fillColor = UIColor.green.cgColor
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
layer.shadowOpacity = 0.6
layer.shadowRadius = 5
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override func layoutSubviews() {
super.layoutSubviews()
// While animating `myView` height, this method gets called
// So new bounds for layer will be calculated and set immediately
// This result in not proper animation
// check by making below condition always true
if !self.isAnimating{ //if true{
guard let layer = self.layer as? CAShapeLayer else { return }
layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
layer.shadowPath = layer.path
}
}
}
class TestViewController : UIViewController {
// height constraint for animating height
var heightConstarint: NSLayoutConstraint?
var heightToAnimate: CGFloat = 200
lazy var myView: CustomView = {
let view = CustomView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
return view
}()
lazy var mySubview: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .yellow
return view
}()
lazy var button: UIButton = {
let button = UIButton(frame: .zero)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Animate", for: .normal)
button.setTitleColor(.black, for: .normal)
button.addTarget(self, action: #selector(self.animateView(_:)), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(self.myView)
self.myView.addSubview(self.mySubview)
self.view.addSubview(self.button)
self.myView.leadingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.leadingAnchor).isActive = true
self.myView.trailingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.trailingAnchor).isActive = true
self.myView.topAnchor.constraint(equalTo: self.view.layoutMarginsGuide.topAnchor, constant: 100).isActive = true
self.heightConstarint = self.myView.heightAnchor.constraint(equalToConstant: 100)
self.heightConstarint?.isActive = true
self.mySubview.leadingAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.leadingAnchor).isActive = true
self.mySubview.trailingAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.trailingAnchor).isActive = true
self.mySubview.topAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.topAnchor).isActive = true
self.mySubview.bottomAnchor.constraint(equalTo: self.myView.layoutMarginsGuide.bottomAnchor).isActive = true
self.button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.button.bottomAnchor.constraint(equalTo: self.view.layoutMarginsGuide.bottomAnchor, constant: -20).isActive = true
}
#objc func animateView(_ sender: UIButton){
CATransaction.begin()
CATransaction.setAnimationDuration(5.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
UIView.animate(withDuration: 5.0, animations: {
self.myView.isAnimating = true
self.heightConstarint?.constant = self.heightToAnimate
// this will call `myView.layoutSubviews()`
// and layer's new bound will set automatically
// this causing layer to be directly jump to height 200, instead of smooth animation
self.view.layoutIfNeeded()
}) { (success) in
self.myView.isAnimating = false
}
let shadowPathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.shadowPath))
let pathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))
let toValue = UIBezierPath(
roundedRect:CGRect(x: 0, y: 0, width: self.myView.bounds.width, height: heightToAnimate),
cornerRadius: 10
).cgPath
shadowPathAnimation.fromValue = self.myView.layer.shadowPath
shadowPathAnimation.toValue = toValue
pathAnimation.fromValue = (self.myView.layer as! CAShapeLayer).path
pathAnimation.toValue = toValue
self.myView.layer.shadowPath = toValue
(self.myView.layer as! CAShapeLayer).path = toValue
self.myView.layer.add(shadowPathAnimation, forKey: #keyPath(CAShapeLayer.shadowPath))
self.myView.layer.add(pathAnimation, forKey: #keyPath(CAShapeLayer.path))
CATransaction.commit()
}
}
While animating view, it will call its layoutSubviews() method, which will result into recalculating bounds of shadow layer.
So I checked if view is currently animating, then do not recalculate bounds of shadow layer.
Is this approach right ? or there is any better way to do this ?
I know it's a tricky question. Actually, you don't need to care about layoutSubViews at all. The key here is when you set the shapeLayer. If it's setup well, i.e. after the constraints are all working, you don't need to care that during the animation.
//in CustomView, comment out the layoutSubViews() and add updateLayer()
func updateLayer(){
guard let layer = self.layer as? CAShapeLayer else { return }
layer.path = UIBezierPath(roundedRect: layer.bounds, cornerRadius: 10).cgPath
layer.shadowPath = layer.path
}
// override func layoutSubviews() {
// super.layoutSubviews()
//
// // While animating `myView` height, this method gets called
// // So new bounds for layer will be calculated and set immediately
// // This result in not proper animation
//
// // check by making below condition always true
//
// if !self.isAnimating{ //if true{
// guard let layer = self.layer as? CAShapeLayer else { return }
//
// layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
// layer.shadowPath = layer.path
// }
// }
in ViewController: add viewDidAppear() and remove other is animation block
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myView.updateLayer()
}
#objc func animateView(_ sender: UIButton){
CATransaction.begin()
CATransaction.setAnimationDuration(5.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
UIView.animate(withDuration: 5.0, animations: {
self.heightConstarint?.constant = self.heightToAnimate
// this will call `myView.layoutSubviews()`
// and layer's new bound will set automatically
// this causing layer to be directly jump to height 200, instead of smooth animation
self.view.layoutIfNeeded()
}) { (success) in
self.myView.isAnimating = false
}
....
Then you are good to go. Have a wonderful day.
Below code also worked for me, As I want to use layout subviews without any flags.
UIView.animate(withDuration: 5.0, animations: {
let shadowPathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.shadowPath))
let pathAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))
let toValue = UIBezierPath(
roundedRect:CGRect(x: 0, y: 0, width: self.myView.bounds.width, height: self.heightToAnimate),
cornerRadius: 10
).cgPath
shadowPathAnimation.fromValue = self.myView.layer.shadowPath
shadowPathAnimation.toValue = toValue
pathAnimation.fromValue = (self.myView.layer as! CAShapeLayer).path
pathAnimation.toValue = toValue
self.heightConstarint?.constant = self.heightToAnimate
self.myView.layoutIfNeeded()
self.myView.layer.shadowPath = toValue
(self.myView.layer as! CAShapeLayer).path = toValue
self.myView.layer.add(shadowPathAnimation, forKey: #keyPath(CAShapeLayer.shadowPath))
self.myView.layer.add(pathAnimation, forKey: #keyPath(CAShapeLayer.path))
CATransaction.commit()
})
And overriding layoutSubview as follows
override func layoutSubviews() {
super.layoutSubviews()
guard let layer = self.layer as? CAShapeLayer else { return }
layer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
layer.shadowPath = layer.path
}

'NSInvalidArgumentException' when using MapKit

i'm creating a simple view controller with a map and 100-200 MKPointAnnotation using the iOS 11 MKMarkerAnnotationView
This is the viewDidLoad of the controller
override func viewDidLoad() {
super.viewDidLoad()
self.mapView.register(StationAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
self.mapView.register(StationClusterView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier)
locationDelegate.delegate = self
self.mapView.delegate = self
self.mapView.showsUserLocation = true
self.refreshData()
self.establishUserPosition()
}
Then i download the stations from a JSON (network object) and i add all of them to the mapview
func reloadViews(){
if let network = network{
for station in network.stations{
let annotation = StationAnnotation(station: station)
annotations.append(annotation) // I add the annotations to an array to prevent them to be deallocated
mapView.addAnnotation(annotation)
}
}
}
This is my personal annotation
class StationAnnotation : MKPointAnnotation{
var station : Station?
var tintColor : UIColor?{
if self.station?.free_bikes ?? 0 > 0 {
return .green
}else{
return .red
}
}
var glyphImage : UIImage?{
if self.station?.extra.status == "online"{
return UIImage(named: "Bicycle")
}else{
return UIImage(named: "Ban")
}
}
override init() {
super.init()
}
convenience init(station : Station){
self.init()
self.title = station.name
self.coordinate = CLLocationCoordinate2D(latitude: station.latitude, longitude: station.longitude)
self.station = station
if station.extra.status == "online"{
self.subtitle = "Bikes: \(station.free_bikes) - Slots: \(station.empty_slots)"
}else{
self.subtitle = station.extra.status
}
}
}
And my customs Views
class StationAnnotationView : MKMarkerAnnotationView{
override var annotation: MKAnnotation? {
willSet {
if let annotation = newValue as? StationAnnotation{
self.markerTintColor = annotation.tintColor
self.clusteringIdentifier = "station"
self.glyphImage = annotation.glyphImage
}
}
}
}
class StationClusterView: MKAnnotationView {
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var annotation: MKAnnotation? {
willSet {
if let cluster = newValue as? MKClusterAnnotation {
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 40, height: 40))
let count = cluster.memberAnnotations.count
let onlineCount = cluster.memberAnnotations.filter { member -> Bool in
return (member as! StationAnnotation).station?.extra.status == "online"
}.count
image = renderer.image { _ in
// Fill full circle with tricycle color
UIColor(named: "Forbidden")?.setFill()
UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 40, height: 40)).fill()
// Fill pie with unicycle color
UIColor(named: "Available")?.setFill()
let piePath = UIBezierPath()
piePath.addArc(withCenter: CGPoint(x: 20, y: 20), radius: 20,
startAngle: 0, endAngle: (CGFloat.pi * 2.0 * CGFloat(onlineCount)) / CGFloat(count),
clockwise: true)
piePath.addLine(to: CGPoint(x: 20, y: 20))
piePath.close()
piePath.fill()
// Fill inner circle with white color
UIColor.white.setFill()
UIBezierPath(ovalIn: CGRect(x: 8, y: 8, width: 24, height: 24)).fill()
// Finally draw count text vertically and horizontally centered
let attributes = [ NSAttributedStringKey.foregroundColor: UIColor.black,
NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 20)]
let text = "\(count)"
let size = text.size(withAttributes: attributes)
let rect = CGRect(x: 20 - size.width / 2, y: 20 - size.height / 2, width: size.width, height: size.height)
text.draw(in: rect, withAttributes: attributes)
}
}
}
}
}
I don't know why the app while pinching , zooming, or panning, crash with SIGABRT signal and this exception
*** Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '*** -[__NSDictionaryM setObject:forKey:]: key cannot be nil'
I've tried every kind of debug system and the use of exception breakpoint didn't helped... have you any suggestions?
Hllo everybody, i find solutions.
At first - it s..t happens when we use
mapView.register(AnyClass?, forAnnotationViewWithReuseIdentifier: String)
and
mapView.dequeueReusableAnnotationView(withIdentifier: String)
returns nil.
So hot fix:
Add:
ViewController: UIViewController, MKMapViewDelegate
add
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
mapView.register(MarkerPointView.self, forAnnotationViewWithReuseIdentifier: "marker")
mapView.register(ClusterView.self, forAnnotationViewWithReuseIdentifier: "cluster")
}
and finaly:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let marker = annotation as? MarkerAnnotation{
var view = mapView.dequeueReusableAnnotationView(withIdentifier: "marker") as? MarkerPointView
if view == nil {
//Very IMPORTANT
print("nil for Marker")
view = MarkerPointView(annotation: marker, reuseIdentifier: "marker")
}
return view
}else if let cluster = annotation as? MKClusterAnnotation{
var view = mapView.dequeueReusableAnnotationView(withIdentifier: "cluster") as? ClusterView
if view == nil{
//Very IMPORTANT
print("nil for Cluster")
view = ClusterView(annotation: cluster, reuseIdentifier: "cluster")
}
return view
}
else{
return nil
}
}
hope it's help for somebody, and on next revs apple fix it, because we can use it like they said on wwdc2017 on 36:50 - we CAN'T delete it!!!!!!!!
original post on forums.developer.apple.com

Zoom on UIView contained in UIScrollView

I have some trouble handling zoom on UIScrollView that contains many subviews. I already saw many answers on SO, but none of them helped me.
So, basically what I'm doing is simple : I have a class called UIFreeView :
import UIKit
class UIFreeView: UIView, UIScrollViewDelegate {
var mainUIView: UIView!
var scrollView: UIScrollView!
var arrayOfView: [UIView]?
override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate func setupView() {
// MainUiView
self.mainUIView = UIView(frame: self.frame)
self.addSubview(mainUIView)
// ScrollView
self.scrollView = UIScrollView(frame: self.frame)
self.scrollView.delegate = self
self.addSubview(self.scrollView)
}
func reloadViews(postArray:[Post]?) {
if let postArray = postArray {
print("UIFreeView::reloadVIew.postArraySize = \(postArray.count)")
let size: CGFloat = 80.0
let margin: CGFloat = 20.0
scrollView.contentSize.width = (size * CGFloat(postArray.count))+(margin*CGFloat(postArray.count))
scrollView.contentSize.height = (size * CGFloat(postArray.count))+(margin*CGFloat(postArray.count))
for item in postArray {
let view = buildPostView(item)
self.scrollView.addSubview(view)
}
}
}
fileprivate func buildPostView(_ item:Post) -> UIView {
// Const
let size: CGFloat = 80.0
let margin: CGFloat = 5.0
// Var
let view = UIView()
let textView = UITextView()
let backgroundImageView = UIImageView()
// Setup view
let x = CGFloat(UInt64.random(lower: UInt64(0), upper: UInt64(self.scrollView.contentSize.width)))
let y = CGFloat(UInt64.random(lower: UInt64(0), upper: UInt64(self.scrollView.contentSize.height)))
view.frame = CGRect(x: x,
y: y,
width: size,
height: size)
// Setup background view
backgroundImageView.frame = CGRect(x: 0,
y: 0,
width: view.frame.size.width,
height: view.frame.size.height)
var bgName = ""
if (item.isFromCurrentUser) {
bgName = "post-it-orange"
} else {
bgName = "post-it-white"
}
backgroundImageView.contentMode = .scaleAspectFit
backgroundImageView.image = UIImage(named: bgName)
view.addSubview(backgroundImageView)
// Setup text view
textView.frame = CGRect(x: margin,
y: margin,
width: view.frame.size.width - margin*2,
height: view.frame.size.height - margin*2)
textView.backgroundColor = UIColor.clear
textView.text = item.content
textView.isEditable = false
textView.isSelectable = false
textView.isUserInteractionEnabled = false
view.addSubview(textView)
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
view.addGestureRecognizer(gestureRecognizer)
return view
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.scrollView
}
func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: self.scrollView)
// note: 'view' is optional and need to be unwrapped
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.scrollView)
}
}
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
}
This class is working perfectly, I can scroll through all my views, but I can't zoom-in and zoom-out in order to see less/more views on my screen.
I think the solution is simple, but I can't seem to find a real solution for my problem ..
Thanks !

Activity indicator with custom image

I am loading a UIWebView and in the meantime I wan't to show a blank page with this activity indicator spinning (siri activity indicator). From what I have understand you can not change the image, but can't I use that image and create an animation with it rotating 360° and looping? Or will that drain the battery?
something like this?:
- (void)webViewDidStartLoad:(UIWebView *)webView {
//set up animation
[self.view addSubview:self.loadingImage];
//start animation
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//stop animation
[self.loadingImage removeFromSuperview];
}
What should I do?
Thanks in advance!
Most of this is found in Stack Overflow. Let me summarize:
Create an UIImageView which will serve as an activity indicator (inside storyboard scene, NIB, code ... wherever you wish). Let's call it _activityIndicatorImage
Load your image: _activityIndicatorImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"activity_indicator"]];
You need to use animation to rotate it. Here is the method I use:
+ (void)rotateLayerInfinite:(CALayer *)layer
{
CABasicAnimation *rotation;
rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotation.fromValue = [NSNumber numberWithFloat:0];
rotation.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
rotation.duration = 0.7f; // Speed
rotation.repeatCount = HUGE_VALF; // Repeat forever. Can be a finite number.
[layer removeAllAnimations];
[layer addAnimation:rotation forKey:#"Spin"];
}
Inside my layoutSubviews method I initiate rotation. You could place this in your webViewDidStartLoad and webViewDidFinishLoad if this is better for your case:
- (void)layoutSubviews
{
[super layoutSubviews];
// some other code
[Utils rotateLayerInfinite:_activityIndicatorImage.layer];
}
You could always always stop rotation using [_activityIndicatorImage.layer removeAllAnimations];
You may use this beautiful loader inspired from Tumblr app:
Asich/AMTumblrHud
Swift 5
Another answer working perfect
Step 1.
Create swift file "CustomLoader.swift" and put this code in that file
import UIKit
import CoreGraphics
import QuartzCore
class CustomLoader: UIView
{
//MARK:- NOT ACCESSABLE OUT SIDE
fileprivate var duration : CFTimeInterval! = 1
fileprivate var isAnimating :Bool = false
fileprivate var backgroundView : UIView!
//MARK:- ACCESS INSTANCE ONLY AND CHANGE ACCORDING TO YOUR NEEDS *******
let colors : [UIColor] = [.red, .blue, .orange, .purple]
var defaultColor : UIColor = UIColor.red
var isUsrInteractionEnable : Bool = false
var defaultbgColor: UIColor = UIColor.white
var loaderSize : CGFloat = 80.0
/// **************** ****************** ////////// **************
//MARK:- MAKE SHARED INSTANCE
private static var Instance : CustomLoader!
static let sharedInstance : CustomLoader = {
if Instance == nil
{
Instance = CustomLoader()
}
return Instance
}()
//MARK:- DESTROY TO SHARED INSTANCE
#objc fileprivate func destroyShardInstance()
{
CustomLoader.Instance = nil
}
//MARK:- SET YOUR LOADER INITIALIZER FRAME ELSE DEFAULT IS CENTER
func startAnimation()
{
let win = UIApplication.shared.keyWindow
backgroundView = UIView()
backgroundView.frame = (UIApplication.shared.keyWindow?.frame)!
backgroundView.backgroundColor = UIColor.init(white: 0, alpha: 0.4)
win?.addSubview(backgroundView)
self.frame = CGRect.init(x: ((UIScreen.main.bounds.width) - loaderSize)/2, y: ((UIScreen.main.bounds.height) - loaderSize)/2, width: loaderSize, height: loaderSize)
self.addCenterImage()
self.isHidden = false
self.backgroundView.addSubview(self)
self.layer.cornerRadius = loaderSize/2
self.layer.masksToBounds = true
backgroundView.accessibilityIdentifier = "CustomLoader"
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.NSExtensionHostDidBecomeActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(CustomLoader.ResumeLoader), name: NSNotification.Name.NSExtensionHostDidBecomeActive, object: nil)
self.layoutSubviews()
}
//MARK:- AVOID STUCKING LOADER WHEN CAME BACK FROM BACKGROUND
#objc fileprivate func ResumeLoader()
{
if isAnimating
{
self.stopAnimation()
self.AnimationStart()
}
}
override func layoutSubviews()
{
super.layoutSubviews()
self.backgroundColor = defaultbgColor
UIApplication.shared.keyWindow?.isUserInteractionEnabled = isUsrInteractionEnable
self.AnimationStart()
}
#objc fileprivate func addCenterImage()
{
/// add image in center
let centerImage = UIImage(named: "Logo")
let imageSize = loaderSize/2.5
let centerImgView = UIImageView(image: centerImage)
centerImgView.frame = CGRect(
x: (self.bounds.width - imageSize) / 2 ,
y: (self.bounds.height - imageSize) / 2,
width: imageSize,
height: imageSize
)
centerImgView.contentMode = .scaleAspectFit
centerImgView.layer.cornerRadius = imageSize/2
centerImgView.clipsToBounds = true
self.addSubview(centerImgView)
}
//MARK:- CALL IT TO START THE LOADER , AFTER INITIALIZE THE LOADER
#objc fileprivate func AnimationStart()
{
if isAnimating
{
return
}
let size = CGSize.init(width: loaderSize , height: loaderSize)
let dotNum: CGFloat = 10
let diameter: CGFloat = size.width / 5.5 //10
let dot = CALayer()
let frame = CGRect(
x: (layer.bounds.width - diameter) / 2 + diameter * 2,
y: (layer.bounds.height - diameter) / 2,
width: diameter/1.3,
height: diameter/1.3
)
dot.backgroundColor = colors[0].cgColor
dot.cornerRadius = frame.width / 2
dot.frame = frame
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame = layer.bounds
replicatorLayer.instanceCount = Int(dotNum)
replicatorLayer.instanceDelay = 0.1
let angle = (2.0 * M_PI) / Double(replicatorLayer.instanceCount)
replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
layer.addSublayer(replicatorLayer)
replicatorLayer.addSublayer(dot)
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.toValue = 0.4
scaleAnimation.duration = 0.5
scaleAnimation.autoreverses = true
scaleAnimation.repeatCount = .infinity
scaleAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
dot.add(scaleAnimation, forKey: "scaleAnimation")
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.toValue = -2.0 * Double.pi
rotationAnimation.duration = 6.0
rotationAnimation.repeatCount = .infinity
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
replicatorLayer.add(rotationAnimation, forKey: "rotationAnimation")
if colors.count > 1 {
var cgColors : [CGColor] = []
for color in colors {
cgColors.append(color.cgColor)
}
let colorAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")
colorAnimation.values = cgColors
colorAnimation.duration = 2
colorAnimation.repeatCount = .infinity
colorAnimation.autoreverses = true
dot.add(colorAnimation, forKey: "colorAnimation")
}
self.isAnimating = true
self.isHidden = false
}
//MARK:- CALL IT TO STOP THE LOADER
func stopAnimation()
{
if !isAnimating
{
return
}
UIApplication.shared.keyWindow?.isUserInteractionEnabled = true
let winSubviews = UIApplication.shared.keyWindow?.subviews
if (winSubviews?.count)! > 0
{
for viw in winSubviews!
{
if viw.accessibilityIdentifier == "CustomLoader"
{
viw.removeFromSuperview()
// break
}
}
}
layer.sublayers = nil
isAnimating = false
self.isHidden = true
self.destroyShardInstance()
}
//MARK:- GETTING RANDOM COLOR , AND MANAGE YOUR OWN COLORS
#objc fileprivate func randomColor()->UIColor
{
let randomRed:CGFloat = CGFloat(drand48())
let randomGreen:CGFloat = CGFloat(drand48())
let randomBlue:CGFloat = CGFloat(drand48())
return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0)
}
override func draw(_ rect: CGRect)
{
}
}
find the func name and "addCenterImage" and replace the image name with your custom image.
Step 2
Create the AppDelegate class instance out side of the AppDelegate class like this.
var AppInstance: AppDelegate!
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate
{ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
AppInstance = self
}
Step 3.
put these two func in your AppDelegate
//MARK: - Activity Indicator -
func showLoader()
{
CustomLoader.sharedInstance.startAnimation()
}
func hideLoader()
{
CustomLoader.sharedInstance.stopAnimation()
}
Step 4. Use the functions like this whenever you want to animate your loader and stop.
AppInstance.showLoader()
AppInstance.hideLoader()
HAPPY LOADING...
SWIFT 4 Sweet And Simply just put extension UIView{}
Modified answer of #gandhi Mena
if you want to create your own custom Loading indicator
Create a UIView extension which create and customize your brand logo as a custom indicator put this code in you global declaration file.
extension UIView{
func customActivityIndicator(view: UIView, widthView: CGFloat?,backgroundColor: UIColor?, textColor:UIColor?, message: String?) -> UIView{
//Config UIView
self.backgroundColor = backgroundColor //Background color of your view which you want to set
var selfWidth = view.frame.width
if widthView != nil{
selfWidth = widthView ?? selfWidth
}
let selfHeigh = view.frame.height
let loopImages = UIImageView()
let imageListArray = ["image1", "image2"] // Put your desired array of images in a specific order the way you want to display animation.
loopImages.animationImages = imageListArray
loopImages.animationDuration = TimeInterval(0.8)
loopImages.startAnimating()
let imageFrameX = (selfWidth / 2) - 30
let imageFrameY = (selfHeigh / 2) - 60
var imageWidth = CGFloat(60)
var imageHeight = CGFloat(60)
if widthView != nil{
imageWidth = widthView ?? imageWidth
imageHeight = widthView ?? imageHeight
}
//ConfigureLabel
let label = UILabel()
label.textAlignment = .center
label.textColor = .gray
label.font = UIFont(name: "SFUIDisplay-Regular", size: 17.0)! // Your Desired UIFont Style and Size
label.numberOfLines = 0
label.text = message ?? ""
label.textColor = textColor ?? UIColor.clear
//Config frame of label
let labelFrameX = (selfWidth / 2) - 100
let labelFrameY = (selfHeigh / 2) - 10
let labelWidth = CGFloat(200)
let labelHeight = CGFloat(70)
// Define UIView frame
self.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width , height: UIScreen.main.bounds.size.height)
//ImageFrame
loopImages.frame = CGRect(x: imageFrameX, y: imageFrameY, width: imageWidth, height: imageHeight)
//LabelFrame
label.frame = CGRect(x: labelFrameX, y: labelFrameY, width: labelWidth, height: labelHeight)
//add loading and label to customView
self.addSubview(loopImages)
self.addSubview(label)
return self }}
Hide an indicator something like this you can remove subview at the top from the subview stack. put this code in the same globally declared swift file.
func hideLoader(removeFrom : UIView){
removeFrom.subviews.last?.removeFromSuperview()
}
Now you can shoot at the mark by this code
To display activity indicator in your view controller put this code when you want to display.
self.view.addSubview(UIView().customActivityIndicator(view: self.view, widthView: nil, backgroundColor:"Desired color", textColor: "Desired color", message: "Loading something"))
To hide animating loader you can user above function you defined in the globally. In your ViewController.swift where you want to hide put this line of code.
hideLoader(removeFrom: self.view)
imageListArray looks like this.
I've faced a similar issue lately. And this is my solution. Basically, it's what topic starter initially wanted: blank page with custom activity indicator on it.
I have partly used #Azharhussain Shaikh answer but I've implemented auto-layout instead of using frames and added a few other refinements with the intention to make usage as simple as possible.
So, it's an extension for UIView with two methods: addActivityIndicator() and removeActivityIndicator()
extension UIView {
func addActivityIndicator() {
// creating a view (let's call it "loading" view) which will be added on top of the view you want to have activity indicator on (parent view)
let view = UIView()
// setting up a background for a view so it would make content under it look like not active
view.backgroundColor = UIColor.white.withAlphaComponent(0.7)
// adding "loading" view to a parent view
// setting up auto-layout anchors so it would cover whole parent view
self.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
view.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// creating array with images, which will be animated
// in my case I have 30 images with names activity0.png ... activity29.png
var imagesArray = [UIImage(named: "activity\(0)")!]
for i in 1..<30 {
imagesArray.append(UIImage(named: "activity\(i)")!)
}
// creating UIImageView with array of images
// setting up animation duration and starting animation
let activityImage = UIImageView()
activityImage.animationImages = imagesArray
activityImage.animationDuration = TimeInterval(0.7)
activityImage.startAnimating()
// adding UIImageView on "loading" view
// setting up auto-layout anchors so it would be in center of "loading" view with 30x30 size
view.addSubview(activityImage)
activityImage.translatesAutoresizingMaskIntoConstraints = false
activityImage.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
activityImage.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
activityImage.widthAnchor.constraint(equalToConstant: 30).isActive = true
activityImage.heightAnchor.constraint(equalToConstant: 30).isActive = true
}
func removeActivityIndicator() {
// checking if a view has subviews on it
guard let lastSubView = self.subviews.last else { return }
// removing last subview with an assumption that last view is a "loading" view
lastSubView.removeFromSuperview()
} }
"Rotating" effect is achieved by those 30 images you've put in imagesArray. Each image is a new frame of a rotating indicator like this.
Usage. In your view controller for showing an activity indicator simply put:
view.addActivityIndicator()
For removing an activity indicator:
view.removeActivityIndicator()
For example, in case of using it with table view (like I do) it can be used like this:
func setLoadingScreen() {
view.addActivityIndicator()
tableView.isScrollEnabled = false
}
func removeLoadingScreen() {
view.removeActivityIndicator()
tableView.isScrollEnabled = true
}
It works in Swift 4.
Swift 5.0 version of accepted Answer
public extension UIImageView {
func spin(duration: Float) {
let rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation.fromValue = 0
rotation.toValue = 2 * Double.pi
rotation.duration = 0.7
rotation.repeatCount = duration
layer.add(rotation, forKey: "spin")
}
func stopSpinning() {
layer.removeAllAnimations()
}
}
Without Image , you can use third party library
for objective C (also support in iOS 6) https://github.com/shebinkoshy/UIControllsRepo
for swift https://github.com/shebinkoshy/Activity-Indicator-Swift
Advantages
-> Able to set colors for spinner
-> Available in different sizes like tiny, small, medium, large, very large
-> Able to set Title (center and bottom) for medium, large, very large sizes
You can set an images to your activityIndicator. I created a function for add custom image to activityIndicator. Here is what I created.
public func showProgressView(view: UIView) -> UIImageView {
let containerView = UIView()
let progressView = UIView()
var activityIndicatorImageView = UIImageView()
if let statusImage = UIImage(named: Constants.ActivityIndicatorImageName1) {
let activityImageView = UIImageView(image: statusImage)
containerView.frame = view.frame
containerView.backgroundColor = UIColor(hex: 0xffffff, alpha: 0.3)
progressView.frame = CGRectMake(0, 0, 80, 80)
progressView.center = CGPointMake(view.bounds.width / 2, view.bounds.height / 2)
progressView.backgroundColor = UIColor(hex: 0x18bda3, alpha: 0.7)
progressView.clipsToBounds = true
progressView.layer.cornerRadius = 10
activityImageView.animationImages = [UIImage(named: Constants.ActivityIndicatorImageName1)!,
UIImage(named: Constants.ActivityIndicatorImageName2)!,
UIImage(named: Constants.ActivityIndicatorImageName3)!,
UIImage(named: Constants.ActivityIndicatorImageName4)!,
UIImage(named: Constants.ActivityIndicatorImageName5)!]
activityImageView.animationDuration = 0.8;
activityImageView.frame = CGRectMake(view.frame.size.width / 2 - statusImage.size.width / 2, view.frame.size.height / 2 - statusImage.size.height / 2, 40.0, 48.0)
activityImageView.center = CGPointMake(progressView.bounds.width / 2, progressView.bounds.height / 2)
dispatch_async(dispatch_get_main_queue()) {
progressView.addSubview(activityImageView)
containerView.addSubview(progressView)
view.addSubview(containerView)
activityIndicatorImageView = activityImageView
}
}
return activityIndicatorImageView
}
You can call this method everywhere in your code. And just call the startAnimating method. If you want to hide just call the stopAnimating method.
it works in both SWITF 3 and 4
var activityIndicator = UIActivityIndicatorView()
var myView : UIView = UIView()
func viewDidLoad() {
spinnerCreation()
}
func spinnerCreation() {
activityIndicator.activityIndicatorViewStyle = .whiteLarge
let label = UILabel.init(frame: CGRect(x: 5, y: 60, width: 90, height: 20))
label.textColor = UIColor.white
label.font = UIFont.boldSystemFont(ofSize: 14.0)
label.textAlignment = NSTextAlignment.center
label.text = "Please wait...."
myView.frame = CGRect(x: (UIScreen.main.bounds.size.width - 100)/2, y: (UIScreen.main.bounds.size.height - 100)/2, width: 100, height: 100)
myView.backgroundColor = UIColor.init(white: 0.0, alpha: 0.7)
myView.layer.cornerRadius = 5
activityIndicator.center = CGPoint(x: myView.frame.size.width/2, y: myView.frame.size.height/2 - 10)
myView.addSubview(activityIndicator)
myView.addSubview(label)
myView.isHidden = true
self.window?.addSubview(myView)
}
#IBAction func activityIndicatorStart(_ sender: Any) {
myView.isHidden = false
self.activityIndicator.startAnimating()
self.view.isUserInteractionEnabled = false
self.view.bringSubview(toFront: myView)
}
#IBAction func activityIndicatorStop(_ sender: Any)() {
myView.isHidden = true
self.activityIndicator.stopAnimating()
self.view.isUserInteractionEnabled = true
}
You can create your custom activity Indicator with this in Swift 3 & 4:
Create a new file with name: UIViewExtension.Swift and copy this code and paste in your new file file:
import UIkit
extension UIView{
func customActivityIndicator(view: UIView, widthView: CGFloat? = nil,backgroundColor: UIColor? = nil, message: String? = nil,colorMessage:UIColor? = nil ) -> UIView{
//Config UIView
self.backgroundColor = backgroundColor ?? UIColor.clear
self.layer.cornerRadius = 10
var selfWidth = view.frame.width - 100
if widthView != nil{
selfWidth = widthView ?? selfWidth
}
let selfHeigh = CGFloat(100)
let selfFrameX = (view.frame.width / 2) - (selfWidth / 2)
let selfFrameY = (view.frame.height / 2) - (selfHeigh / 2)
let loopImages = UIImageView()
//ConfigCustomLoading with secuence images
let imageListArray = [UIImage(named:""),UIImage(named:""), UIImage(named:"")]
loopImages.animationImages = imageListArray
loopImages.animationDuration = TimeInterval(1.3)
loopImages.startAnimating()
let imageFrameX = (selfWidth / 2) - 17
let imageFrameY = (selfHeigh / 2) - 35
var imageWidth = CGFloat(35)
var imageHeight = CGFloat(35)
if widthView != nil{
imageWidth = widthView ?? imageWidth
imageHeight = widthView ?? imageHeight
}
//ConfigureLabel
let label = UILabel()
label.textAlignment = .center
label.textColor = .gray
label.font = UIFont.boldSystemFont(ofSize: 17)
label.numberOfLines = 0
label.text = message ?? ""
label.textColor = colorMessage ?? UIColor.clear
//Config frame of label
let labelFrameX = (selfWidth / 2) - 100
let labelFrameY = (selfHeigh / 2) - 10
let labelWidth = CGFloat(200)
let labelHeight = CGFloat(70)
//add loading and label to customView
self.addSubview(loopImages)
self.addSubview(label)
//Define frames
//UIViewFrame
self.frame = CGRect(x: selfFrameX, y: selfFrameY, width: selfWidth , height: selfHeigh)
//ImageFrame
loopImages.frame = CGRect(x: imageFrameX, y: imageFrameY, width: imageWidth, height: imageHeight)
//LabelFrame
label.frame = CGRect(x: labelFrameX, y: labelFrameY, width: labelWidth, height: labelHeight)
return self
}
}
And then you can use it in your ViewController like this:
import UIKit
class ExampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(UIView().customActivityIndicator(view: self.view,backgroundColor: UIColor.green))
}
//function for stop and desappear loading
func deseappearLoading(){
self.view.subviews.last?.removeFromSuperview()
}
}
Don't forget replace [UIImage(named:" "),UIImage(named:" "), UIImage(named:" ")] with your names of images and adjust the TimeInterval(1.3). Enjoy it.

Resources