Animation lags on device, but not in Simulator - swift

I want to trigger a CAAnimation with a Button. In playground and in the simulator, this works exactly as I want it to. However, when I run the same code on a device, the animation happens only after a short delay.
Apparently, the issue only happens on iOS 11.2.6. I updated my device and can now not reproduce the issue anymore. Can anyone confirm, or find out, how it would work on iOS 11.2.6?
import UIKit
class MyViewController : UIViewController {
let animatedView = UIView()
override func loadView() {
let view = UIView()
view.backgroundColor = .white
// Add a button
let button = UIButton(type: .system)
button.frame = CGRect(x: 150, y: 200, width: 200, height: 50)
button.setTitle("Animate", for: .normal)
button.addTarget(self, action: #selector(tap), for: .touchUpInside)
// Set color and frame of the view, that is animated.
animatedView.backgroundColor = UIColor.blue
animatedView.frame = CGRect(x: 50, y: 50, width: 50, height: 50)
// Add the views to the view hierarchy
view.addSubview(animatedView)
view.addSubview(button)
self.view = view
}
/// On Tap create an animation, that changes the position of the animated view.
#objc func tap() {
let originalY = animatedView.layer.position.y
let animation = CABasicAnimation(keyPath: "position.y")
animation.fromValue = originalY
animation.toValue = 300.0
animation.duration = 1.0
animatedView.layer.add(animation, forKey: "positionAnimation")
}
}

Can you try this code with iOS 11.2.9, I have tested your code and it's working fine.
// MARK: Animations
public extension CALayer {
var ani: CAAnimation {
let startPointAnim = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.startPoint))
startPointAnim.fromValue = CGPoint(x: 0, y: 0.0)
startPointAnim.toValue = CGPoint(x:0, y: 300)
let endPointAnim = CABasicAnimation(keyPath: #keyPath(CAGradientLayer.endPoint))
endPointAnim.fromValue = CGPoint(x: 0, y: 0)
endPointAnim.toValue = CGPoint(x:2, y: 300)
let animGroup = CAAnimationGroup()
animGroup.animations = [startPointAnim, endPointAnim]
animGroup.duration = 1.0
animGroup.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
return animGroup
}
func playAnimation(_ anim: SkeletonLayerAnimation, key: String) {
recursiveSearch(inArray: skeletonSublayers,
leafBlock: { add(anim(self), forKey: key) }) {
$0.playAnimation(anim, key: key)
}
}
func stopAnimation(forKey key: String) {
recursiveSearch(inArray: skeletonSublayers,
leafBlock: { removeAnimation(forKey: key) }) {
$0.stopAnimation(forKey: key)
}
}
}

Related

How to dispose from UIView subviews on one ViewController when navigating away from it

I'm starting to learn Swift and decided to build an app but without storyboard.
My SceneDelegate scene function instantiates a TabBarController
window = UIWindow(frame: UIScreen.main.bounds)
let tb = TabBarController()
window?.rootViewController = tb
window?.makeKeyAndVisible()
window?.windowScene = windowSceme
I have a TabBarController that extends from UITabBarController which pretty much styles the tab bar and sets it all up
For each item of the Tabbar I have a ViewController. For the purpose of this question I'm going to specify the first "Home".
In Home, which extends ViewController, I have the following
let homePageView = HomePageView()
override func viewWillLayoutSubviews() {
navigationController?.isNavigationBarHidden = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
homePageView.setupHomePage()
view.addSubview(homePageView)
}
override func viewWillDisappear(_ animated: Bool) {
homePageView.dispose()
}
The Controller is pretty much only in charge of calling whatever is going to be displayed on the screen within HomePageView(), the main component. This last, holds two more views. One carrousel HomePageCarrousel() and one header HomePageSocialUp(). This view instantiates the two latter referred to and sets the layout programmatically and adds them as subviews. It also includes a dispose function that sets the instantiated classes to nil such as --> Instantiate - homePageCarrousel = HomePageCarrousel() and the dispose function has homePageCarrousel = nil
That works perfectly fine but when I navigate away from the current view controller via the tab bar and navigate back to it, now I have two instances of HomePageCarrousel() and HomePageSocialUp() within HomeView
I'm probably holding a strong reference somewhere but I can't figure out when. Could someone point me to where should I look to debug it or what might be that is creating the issue.
I'm also providing the code for the two views duplicated in case the issue is there
HomePageSocialUp
class HomePageSocialUp: UIView {
let logo = UIImage(named: "LogoSmiles")
let socialImages: [UIImage] = [UIImage(named: "tripadvisor")!, UIImage(named: "instagram")!, UIImage(named: "facebook")!, UIImage(named: "linkedin")!]
func setupHeaderCircle() {
guard let insetTop = UIApplication.shared.keyWindow?.safeAreaInsets.top else {return}
let circlePath = UIBezierPath(arcCenter: CGPoint(x: UIScreen.main.bounds.width / 2, y: insetTop + 60), radius: CGFloat(90), startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.white.cgColor
layer.addSublayer(shapeLayer)
}
func setupSocialHeader() {
setupHeaderCircle()
layer.masksToBounds = true;
backgroundColor = UIColor.TintColor
layer.shadowColor = UIColor.lightGray.cgColor
layer.shadowOpacity = 0.8
layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
layer.shadowRadius = 3.0
layer.masksToBounds = false
frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 280)
let imageView = UIImageView()
guard let logo = logo else {return}
guard let insetTop = UIApplication.shared.keyWindow?.safeAreaInsets.top else {return}
imageView.frame = CGRect(x: CGFloat(Int((UIScreen.main.bounds.width - (logo.size.width)))) / 2, y: 120 - (logo.size.width / 2), width: logo.size.width, height: logo.size.height)
imageView.image = logo
addSubview(imageView)
}
}
HomePageCarrousel
class HomePageCarrousel: UIScrollView {
var images: [UIImage]?
var originX = 0
var numberOfIterations = 0
var timer: Timer?
func setupCarrousel() {
let x = (Int(UIScreen.main.bounds.width) - (Int(UIScreen.main.bounds.width) - 60)) / 2
frame = CGRect(x: x, y: (Int(frame.origin.y) - 350) / 2, width: Int(UIScreen.main.bounds.width) - 60, height: 350)
let newImage = textToImage(drawText: "Creating Smiles in unique places.", frame: frame, inImage: UIImage(named: "smiles1")!, atPoint: CGPoint(x: frame.origin.x + 20, y: frame.height - 20))
images = [newImage, UIImage(named: "smiles2")!]
guard timer == nil else { return }
timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(startScrolling), userInfo: nil, repeats: true)
guard let imageCount = images?.count, let images = images else { return }
contentSize = CGSize(width: frame.width * CGFloat(images.count), height: frame.height)
for image in 0..<imageCount {
let imageView = UIImageView()
imageView.image = images[image]
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
let xPosition = frame.width * CGFloat(image)
imageView.frame = CGRect(x: xPosition, y: 0, width: frame.width, height: frame.height)
addSubview(imageView)
}
timer?.fire()
}
func textToImage(drawText text: String, frame: CGRect, inImage image: UIImage, atPoint point: CGPoint) -> UIImage {
let textColor = UIColor.white
let textFont = UIFont(name: "Helvetica Bold" , size: 12)!
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(image.size, false, scale)
let textFontAttributes = [
NSAttributedString.Key.font: textFont,
NSAttributedString.Key.foregroundColor: textColor,
] as [NSAttributedString.Key : Any]
image.draw(in: CGRect(origin: CGPoint.zero, size: image.size))
text.draw(in: frame, withAttributes: textFontAttributes)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage!
}
#objc func startScrolling() {
print(originX)
guard let images = images else { return }
if originX == images.count {
originX = 0
numberOfIterations += 1
}
if numberOfIterations > 2 {
timer?.invalidate()
timer = nil
numberOfIterations = 0
}
let x = CGFloat(originX) * frame.size.width
setContentOffset(CGPoint(x: x, y: 0), animated: true)
originX += 1
}
}
Thanks upfront
Did you tried to remove the subviews, something like
homePageView.subviews.forEach { (view) in
//taking appropriate action to whichever view you want
view.removeFromSuperview()//for removing views
}

How to remove programmatically created UIViews from superview

I wanted to implement a loading overlay whilst I have content loading in from an API call however when I go to dismiss the view; I have no success.
func viewLoading(show:Bool, boxView: UIView, error: Bool, errorMessage: String){
let myNewView=UIView(frame: CGRect(x: 0, y: 0, width: boxView.frame.width, height: boxView.frame.height))
if show{
// Change UIView background colour
myNewView.backgroundColor = UIColor.black.withAlphaComponent(0.75)
myNewView.isOpaque = false
// Add rounded corners to UIView
myNewView.layer.cornerRadius = boxView.layer.cornerRadius
let activityView = UIActivityIndicatorView(style: .whiteLarge)
activityView.center = myNewView.center
activityView.startAnimating()
boxView.addSubview(myNewView)
myNewView.addSubview(activityView)
}else{
print("Done")
DispatchQueue.main.async(execute: { () -> Void in
myNewView.removeFromSuperview()
self.view.bringSubviewToFront(boxView)
})
myNewView.isHidden = true
}
}
None of the options after else have worked and I am lost at a solution.
Edit: I want the same function(s) to accommodate three different views within the one view controller.
Move myNewView outside of the viewLoading function scope, and it is better to create separate methods with their own responsibilities, like so:
class ViewController: UIViewController {
var loaderView: UIView?
func showLoading(boxView: UIView, error: Bool, errorMessage: String) {
if (self.loaderView != nil) {
self.hideLoading()
}
let newView = UIView(frame: CGRect(x: 0, y: 0, width: boxView.frame.width, height: boxView.frame.height))
newView.backgroundColor = UIColor.black.withAlphaComponent(0.75)
newView.isOpaque = false
// Add rounded corners to UIView
newView.layer.cornerRadius = boxView.layer.cornerRadius
let activityView = UIActivityIndicatorView(style: .whiteLarge)
activityView.center = newView.center
activityView.startAnimating()
boxView.addSubview(newView)
newView.addSubview(activityView)
self.loaderView = newView
}
func hideLoading() {
guard
let loaderView = self.loaderView,
let boxView = loaderView.superview
else { return }
DispatchQueue.main.async {
loaderView.removeFromSuperview()
self.view.bringSubviewToFront(boxView) // need this?
self.loaderView = nil
}
}
}
You are creating a new view every time that method is called and then you are trying to dismish that newly created view. Instead, you should save a reference of the view when you show it and call removeFromSuperview on that instance when you need to hide it.
Check this..
CommonMethods.swift
import UIKit
class CommonMethods: UIViewController {
static let actInd: UIActivityIndicatorView = UIActivityIndicatorView()
static let container: UIView = UIView()
static let loadingView: UIView = UIView()
static func showActivityIndicatory(uiView: UIView) {
container.frame = uiView.frame
container.center = uiView.center
container.backgroundColor = UIColor(red:255/255, green:255/255, blue:255/255, alpha: 0.3)
loadingView.frame = CGRect(origin: CGPoint(x: 0,y :0), size: CGSize(width: 80, height: 80))
loadingView.center = uiView.center
loadingView.backgroundColor = UIColor(red:44/255, green:44/255, blue:44/255, alpha: 0.7)
loadingView.clipsToBounds = true
loadingView.layer.cornerRadius = 10
actInd.frame = CGRect(origin: CGPoint(x: 0,y :0), size: CGSize(width: 40, height: 40))
actInd.style =
UIActivityIndicatorView.Style.whiteLarge
actInd.center = CGPoint(x: loadingView.frame.size.width / 2, y: loadingView.frame.size.height / 2);
loadingView.addSubview(actInd)
container.addSubview(loadingView)
uiView.addSubview(container)
actInd.startAnimating()
}
static func hideActivityIndicatory(uiView: UIView) {
container.removeFromSuperview()
actInd.stopAnimating()
}
}
call it from viewcontroller class like
CommonMethods.showActivityIndicatory(uiView: self.view)
CommonMethods.hideActivityIndicatory(uiView: self.view)

For Loop Overriding Animations

I'm trying to create a new animated line every second on the screen. Each second I do get a new line, however it overrides the old one. I don't know why but it's probably something idiotic I'm overlooking. Here's my code:
func repeatThis() {
for x in 1...10 {
let time = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), Int64(x) * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue()) {
var topLinePatha: UIBezierPath {
return UIBezierPath(rect: CGRect(x: 0, y: 0 + (x * 10), width: 1, height: 10))
}
var topLinePathb: UIBezierPath {
return UIBezierPath(rect: CGRect(x: 0, y: 0 + (x * 10), width: Int(UIScreen.mainScreen().bounds.width), height: 10))
}
let expAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expAnimation.fromValue = topLinePatha.CGPath
expAnimation.toValue = topLinePathb.CGPath
expAnimation.duration = self.animationTime
expAnimation.fillMode = kCAFillModeForwards
expAnimation.removedOnCompletion = false
self.addAnimation(expAnimation, forKey: nil)
print(x)
}
}
}
Thanks for the help
Edit 1:
Here's an issue I'm having with the animation timing where basically the animations override each other:
func repeatThis() {
var runningPath = UIBezierPath()
for x in 0...10 {
delay(Double(x) / 10) {
let topLineStartPath = UIBezierPath(rect: CGRect(x: 0, y: x * 10, width: 1, height: 10))
let topLineEndPath = UIBezierPath(rect: CGRect(x: 0, y: x * 10, width: Int(self.bounds.width), height: 10))
let fullStartPath = runningPath.copy() as! UIBezierPath
fullStartPath.appendPath(topLineStartPath)
let fullEndPath = runningPath.copy() as! UIBezierPath
fullEndPath.appendPath(topLineEndPath)
let expAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expAnimation.fromValue = fullStartPath.CGPath
expAnimation.toValue = fullEndPath.CGPath
expAnimation.duration = self.animationTime
expAnimation.fillMode = kCAFillModeForwards
expAnimation.removedOnCompletion = false
self.addAnimation(expAnimation, forKey: nil)
print(x)
runningPath = fullEndPath
}
}
}
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
Your code is replacing the CAShapeLayer's path each time you do an animation, so for each "line" that you animate in, you lose the past lines.
To show multiple lines, you could either:
Add multiple subpaths to the CAShapeLayer's path, one for each line, using the method UIBezierPath.appendPath.
Use multiple CAShapeLayers, one for each line.
Here's alternative #1, which is a smaller change from your current code. This is a self-contained example that you can add into a new iOS project in a view controller called ViewController.
import UIKit
class MyShapeLayer: CAShapeLayer {
var animationTime: NSTimeInterval = 0.75
func repeatThis() {
// Keep track of the path so far
var runningPath = UIBezierPath()
for x in 1...10 {
let time = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), Int64(x) * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue()) {
// We will add a rectangular subpath onto runningPath.
// It will be animated starting with:
let topLineStartPath = UIBezierPath(rect: CGRect(x: 0, y: x * 10, width: 1, height: 10))
// and ending with:
let topLineEndPath = UIBezierPath(rect: CGRect(x: 0, y: x * 10, width: Int(self.bounds.width), height: 10))
// Copy the running path, and add the starting and ending subpaths onto it
let fullStartPath = runningPath.copy() as! UIBezierPath
fullStartPath.appendPath(topLineStartPath)
let fullEndPath = runningPath.copy() as! UIBezierPath
fullEndPath.appendPath(topLineEndPath)
// Animate from fullStartPath to fullEndPath
let expAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expAnimation.fromValue = fullStartPath.CGPath
expAnimation.toValue = fullEndPath.CGPath
expAnimation.duration = self.animationTime
expAnimation.fillMode = kCAFillModeForwards
expAnimation.removedOnCompletion = false
self.addAnimation(expAnimation, forKey: nil)
print(x)
// The next time through the loop, add on to this iteration's ending path
runningPath = fullEndPath
}
}
}
}
class MyView: UIView {
override class func layerClass() -> AnyClass {
return MyShapeLayer.self
}
}
class ViewController: UIViewController {
override func loadView() {
self.view = MyView()
self.view.backgroundColor = UIColor.whiteColor()
}
override func viewDidLoad() {
if let myShapeLayer = self.view.layer as? MyShapeLayer {
myShapeLayer.repeatThis()
}
}
}
And the result:
Here's a way to do alternative #2. I made the animationTime longer so you can see that the animations for each line can overlap.
class LineAtATimeView: UIView {
var animationTime = 1.25 // longer duration so line animations overlap
func repeatAddingLines() {
for x in 1...10 {
let time = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), Int64(x) * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue()) {
let newLayer = CAShapeLayer()
newLayer.frame = self.bounds
self.layer.addSublayer(newLayer)
let topLineStartPath = UIBezierPath(rect: CGRect(x: 0, y: x * 10, width: 1, height: 10))
let topLineEndPath = UIBezierPath(rect: CGRect(x: 0, y: x * 10, width: Int(self.bounds.width), height: 10))
let expAnimation: CABasicAnimation = CABasicAnimation(keyPath: "path")
expAnimation.fromValue = topLineStartPath.CGPath
expAnimation.toValue = topLineEndPath.CGPath
expAnimation.duration = self.animationTime
expAnimation.fillMode = kCAFillModeForwards
expAnimation.removedOnCompletion = false
newLayer.addAnimation(expAnimation, forKey: nil)
}
}
}
}
class ViewController2: UIViewController {
override func loadView() {
self.view = LineAtATimeView()
self.view.backgroundColor = UIColor.whiteColor()
}
override func viewDidLoad() {
if let v = self.view as? LineAtATimeView {
v.repeatAddingLines()
}
}
}

Saving NSUserDefaults with UISwitch in Swift 2

I have posted all my code below to fully demonstrate what I am trying to accomplish. I have a UI Switch, that I would like to save a NSUserDefault on what game mode they would like. I am not sure why this is not working correctly, as it currently does not save- as the switch doesn't work.
import SpriteKit
class ModeScene: SKScene {
var modeSwitch = UISwitch()
var motionLabel = UILabel()
var touchLabel = UILabel()
override func didMoveToView(view: SKView) {
let MotionMode = NSUserDefaults.standardUserDefaults()
let TouchMode = NSUserDefaults.standardUserDefaults()
modeSwitch.center = CGPoint(x: view.center.x, y: view.frame.size.height / 1.3)
modeSwitch.addTarget(self, action: Selector("updateMode"), forControlEvents: UIControlEvents.ValueChanged)
if TouchMode {
modeSwitch.on = false
}
else{
modeSwitch.on = true
}
self.view?.addSubview(modeSwitch)
motionLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 700, height: 200))
motionLabel.center = CGPoint(x: view.center.x * 3.4, y: view.frame.size.height / 1.3)
motionLabel.text = "Accelerometer"
motionLabel.textColor = UIColor.whiteColor()
motionLabel.font = UIFont(name:"ArialRoundedMTBold", size: 16)
self.view?.addSubview(motionLabel)
touchLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 700, height: 200))
touchLabel.center = CGPoint(x: view.center.x * 2.25, y: view.frame.size.height / 1.3)
touchLabel.text = "Touch To Move"
touchLabel.textColor = UIColor.whiteColor()
touchLabel.font = UIFont(name:"ArialRoundedMTBold", size: 16)
self.view?.addSubview(touchLabel)
DoneBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 400, height: 30))
DoneBtn.center = CGPoint(x: view.center.x * 1.7, y: view.frame.size.height / 14)
DoneBtn.setTitle("Done", forState: UIControlState.Normal)
DoneBtn.setTitleColor(UIColor.whiteColor(), forState: UIControlState.Normal)
DoneBtn.addTarget(self, action: Selector("done"), forControlEvents: UIControlEvents.TouchUpInside)
self.view?.addSubview(DoneBtn)
updateMode()
}
func updateMode(){
if modeSwitch.on {
// mode is accel
let MotionMode = NSUserDefaults.standardUserDefaults()
MotionMode.setValue(true, forKey: "MotionMode")
NSLog("Mode is accel")
}
else{
// mode is touch
let TouchMode = NSUserDefaults.standardUserDefaults()
TouchMode.setObject(true, forKey: "TouchMode")
NSLog("Mode is touch")
}
}
func done(){
self.scene?.view?.presentScene(StartScene(size: self.scene!.size), transition: SKTransition.fadeWithColor(UIColor.blackColor(), duration: 1))
headerLabel.removeFromSuperview()
DoneBtn.removeFromSuperview()
modeSwitch.removeFromSuperview()
touchLabel.removeFromSuperview()
motionLabel.removeFromSuperview()
}
}
Code Update:
import SpriteKit
class ModeScene: SKScene {
var modeSwitch = UISwitch()
var motionLabel = UILabel()
var touchLabel = UILabel()
var DoneBtn : UIButton!
override func didMoveToView(view: SKView) {
backgroundColor = SKColor.blackColor()
modeSwitch.center = CGPoint(x: view.center.x, y: view.frame.size.height / 1.3)
modeSwitch.addTarget(self, action: Selector("updateMode"), forControlEvents: UIControlEvents.ValueChanged)
self.view?.addSubview(modeSwitch)
motionLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 700, height: 200))
motionLabel.center = CGPoint(x: view.center.x * 3.4, y: view.frame.size.height / 1.3)
motionLabel.text = "Accelerometer"
motionLabel.textColor = UIColor.whiteColor()
motionLabel.font = UIFont(name:"ArialRoundedMTBold", size: 16)
self.view?.addSubview(motionLabel)
touchLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 700, height: 200))
touchLabel.center = CGPoint(x: view.center.x * 2.25, y: view.frame.size.height / 1.3)
touchLabel.text = "Touch To Move"
touchLabel.textColor = UIColor.whiteColor()
touchLabel.font = UIFont(name:"ArialRoundedMTBold", size: 16)
self.view?.addSubview(touchLabel)
DoneBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 400, height: 30))
DoneBtn.center = CGPoint(x: view.center.x * 1.7, y: view.frame.size.height / 14)
DoneBtn.setTitle("Done", forState: UIControlState.Normal)
DoneBtn.setTitleColor(UIColor.whiteColor(), forState: UIControlState.Normal)
DoneBtn.addTarget(self, action: Selector("done"), forControlEvents: UIControlEvents.TouchUpInside)
self.view?.addSubview(DoneBtn)
let defaults = NSUserDefaults.standardUserDefaults()
if let motionMode = defaults.objectForKey("motionMode") as? Bool {
if motionMode {
// true
modeSwitch.on = true
} else {
// false
modeSwitch.on = false
}
}
updateMode()
}
func updateMode(){
let defaults = NSUserDefaults.standardUserDefaults()
if modeSwitch.on {
// mode is accel
defaults.setBool(true, forKey: "motionMode")
NSLog("Mode is accel")
}
else{
// mode is touch
defaults.setBool(true, forKey: "touchMode")
NSLog("Mode is touch")
}
}
func done(){
self.scene?.view?.presentScene(StartScene(size: self.scene!.size), transition: SKTransition.fadeWithColor(UIColor.blackColor(), duration: 1))
headerLabel.removeFromSuperview()
DoneBtn.removeFromSuperview()
modeSwitch.removeFromSuperview()
touchLabel.removeFromSuperview()
motionLabel.removeFromSuperview()
}
}
You forgot to actually load the values from NSUserDefaults.
You set them with setValue and setObject, but you never load them back.
Use the valueForKey or objectForKey methods to retrieve your values.
let defaults = NSUserDefaults.standardUserDefaults()
let MotionMode = defaults.objectForKey("MotionMode") as! Bool
if MotionMode {
// true
} else {
// false
}
Even better with safe unwrapping:
let defaults = NSUserDefaults.standardUserDefaults()
if let MotionMode = defaults.objectForKey("MotionMode") as? Bool {
if MotionMode {
// true
} else {
// false
}
}
A few notes: variables should begin with a lowercase letter, so "MotionMode" should be "motionMode", etc. You could use the specialized object getters like boolForKey instead of the generic objectForKey which forces you to typecast to Bool. Of course it goes with setBool on the other side. Last: the variable you call "MotionMode" actually holds NSUserDefaults.standardUserDefaults(), so I called it "defaults" instead in my example.
Your question has several parts, but it's better to make a single post for a single question. So I've focused about the failing NSUserDefaults part because it was the most important. Using my answer, if your code still doesn't work because of the UI parts, then you should post a new different question about it to keep things clear.

Swift add badge to navigation barButtonItem and UIButton

I am trying to display badge on my notification button, in app as displayed on AppIcon.
So far whatever i have researched is related to Obj. C, but nothing that specifically discussed way to implement that solution into Swift,
Please help to find a solution to add a custom class / code to achieve Badge on UiBarbutton and UiButton.
Researched so far:
https://github.com/Marxon13/M13BadgeView
along with MKBadge class etc.
There is a more elegant solution with an extension for UIButtonItem
extension CAShapeLayer {
func drawCircleAtLocation(location: CGPoint, withRadius radius: CGFloat, andColor color: UIColor, filled: Bool) {
fillColor = filled ? color.cgColor : UIColor.white.cgColor
strokeColor = color.cgColor
let origin = CGPoint(x: location.x - radius, y: location.y - radius)
path = UIBezierPath(ovalIn: CGRect(origin: origin, size: CGSize(width: radius * 2, height: radius * 2))).cgPath
}
}
private var handle: UInt8 = 0
extension UIBarButtonItem {
private var badgeLayer: CAShapeLayer? {
if let b: AnyObject = objc_getAssociatedObject(self, &handle) as AnyObject? {
return b as? CAShapeLayer
} else {
return nil
}
}
func addBadge(number: Int, withOffset offset: CGPoint = CGPoint.zero, andColor color: UIColor = UIColor.red, andFilled filled: Bool = true) {
guard let view = self.value(forKey: "view") as? UIView else { return }
badgeLayer?.removeFromSuperlayer()
// Initialize Badge
let badge = CAShapeLayer()
let radius = CGFloat(7)
let location = CGPoint(x: view.frame.width - (radius + offset.x), y: (radius + offset.y))
badge.drawCircleAtLocation(location: location, withRadius: radius, andColor: color, filled: filled)
view.layer.addSublayer(badge)
// Initialiaze Badge's label
let label = CATextLayer()
label.string = "\(number)"
label.alignmentMode = CATextLayerAlignmentMode.center
label.fontSize = 11
label.frame = CGRect(origin: CGPoint(x: location.x - 4, y: offset.y), size: CGSize(width: 8, height: 16))
label.foregroundColor = filled ? UIColor.white.cgColor : color.cgColor
label.backgroundColor = UIColor.clear.cgColor
label.contentsScale = UIScreen.main.scale
badge.addSublayer(label)
// Save Badge as UIBarButtonItem property
objc_setAssociatedObject(self, &handle, badge, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
func updateBadge(number: Int) {
if let text = badgeLayer?.sublayers?.filter({ $0 is CATextLayer }).first as? CATextLayer {
text.string = "\(number)"
}
}
func removeBadge() {
badgeLayer?.removeFromSuperlayer()
}
}
This great code was created by Stefano Vettor and you can find all the details at:
https://gist.github.com/freedom27/c709923b163e26405f62b799437243f4
Working Solution :
Step 1:
Firstly create new swift file which is a subclass to UIButton as follows:
import UIKit
class BadgeButton: UIButton {
var badgeLabel = UILabel()
var badge: String? {
didSet {
addbadgetobutton(badge: badge)
}
}
public var badgeBackgroundColor = UIColor.red {
didSet {
badgeLabel.backgroundColor = badgeBackgroundColor
}
}
public var badgeTextColor = UIColor.white {
didSet {
badgeLabel.textColor = badgeTextColor
}
}
public var badgeFont = UIFont.systemFont(ofSize: 12.0) {
didSet {
badgeLabel.font = badgeFont
}
}
public var badgeEdgeInsets: UIEdgeInsets? {
didSet {
addbadgetobutton(badge: badge)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
addbadgetobutton(badge: nil)
}
func addbadgetobutton(badge: String?) {
badgeLabel.text = badge
badgeLabel.textColor = badgeTextColor
badgeLabel.backgroundColor = badgeBackgroundColor
badgeLabel.font = badgeFont
badgeLabel.sizeToFit()
badgeLabel.textAlignment = .center
let badgeSize = badgeLabel.frame.size
let height = max(18, Double(badgeSize.height) + 5.0)
let width = max(height, Double(badgeSize.width) + 10.0)
var vertical: Double?, horizontal: Double?
if let badgeInset = self.badgeEdgeInsets {
vertical = Double(badgeInset.top) - Double(badgeInset.bottom)
horizontal = Double(badgeInset.left) - Double(badgeInset.right)
let x = (Double(bounds.size.width) - 10 + horizontal!)
let y = -(Double(badgeSize.height) / 2) - 10 + vertical!
badgeLabel.frame = CGRect(x: x, y: y, width: width, height: height)
} else {
let x = self.frame.width - CGFloat((width / 2.0))
let y = CGFloat(-(height / 2.0))
badgeLabel.frame = CGRect(x: x, y: y, width: CGFloat(width), height: CGFloat(height))
}
badgeLabel.layer.cornerRadius = badgeLabel.frame.height/2
badgeLabel.layer.masksToBounds = true
addSubview(badgeLabel)
badgeLabel.isHidden = badge != nil ? false : true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addbadgetobutton(badge: nil)
fatalError("init(coder:) is not implemented")
}
}
Step 2:
Create a function in your base file which u can use in each View Controller :
func addBadge(itemvalue: String) {
let bagButton = BadgeButton()
bagButton.frame = CGRect(x: 0, y: 0, width: 44, height: 44)
bagButton.tintColor = UIColor.darkGray
bagButton.setImage(UIImage(named: "ShoppingBag")?.withRenderingMode(.alwaysTemplate), for: .normal)
bagButton.badgeEdgeInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 15)
bagButton.badge = itemvalue
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: bagButton)
}
Step 3 :
Use above function from any View Controller in this way :
self.addBadge(itemvalue: localStorage.string(forKey: "total_products_in_cart") ?? "0")
First create label, then right bar button. On right bar button add subview which will be badge count. Finally add navigation right bar button.
SWIFT 5
let badgeCount = UILabel(frame: CGRect(x: 22, y: -05, width: 20, height: 20))
badgeCount.layer.borderColor = UIColor.clear.cgColor
badgeCount.layer.borderWidth = 2
badgeCount.layer.cornerRadius = badgeCount.bounds.size.height / 2
badgeCount.textAlignment = .center
badgeCount.layer.masksToBounds = true
badgeCount.textColor = .white
badgeCount.font = badgeCount.font.withSize(12)
badgeCount.backgroundColor = .red
badgeCount.text = "4"
let rightBarButton = UIButton(frame: CGRect(x: 0, y: 0, width: 35, height: 35))
rightBarButton.setBackgroundImage(UIImage(named: "NotificationBell"), for: .normal)
rightBarButton.addTarget(self, action: #selector(self.onBtnNotification), for: .touchUpInside)
rightBarButton.addSubview(badgeCount)
let rightBarButtomItem = UIBarButtonItem(customView: rightBarButton)
navigationItem.rightBarButtonItem = rightBarButtomItem
I had the same task. I didn't want to use third-party libraries. Firstly, I tried Stefano's solution and it's great however I decided to implement my own way to solve it.
In my humble opinion, there are simple steps described below briefly:
Create UIView instance within .xib file and put necessary items like UILabel or UIImageView instance depending on your design requirements.
The final action I did in this step is putting invisible button in the top of view's hierarchy.
Create YourCustomView.swift and link all #IBOutlets from xib to current file inside your custom view class implementation.
Next, implement class function in YourCustomView class which will load custom view from xib and return it as YourCustomView instance.
Finally, add your custom badge to your custom view controller instance!
My result is..
P.S. If you need to implement #IBActions I recommend to link your custom view and custom view controller through the delegate pattern.
using M13BadgeView.. use this code
(im using fontawesome.swift for buttons :: https://github.com/thii/FontAwesome.swift)
let rightButton = UIButton(frame: CGRect(x:0,y:0,width:30,height:30))
rightButton.titleLabel?.font = UIFont.fontAwesome(ofSize: 22)
rightButton.setTitle(String.fontAwesomeIcon(name: .shoppingBasket), for: .normal)
let rightButtonItem : UIBarButtonItem = UIBarButtonItem(customView: rightButton)
let badgeView = M13BadgeView()
badgeView.text = "1"
badgeView.textColor = UIColor.white
badgeView.badgeBackgroundColor = UIColor.red
badgeView.borderWidth = 1.0
badgeView.borderColor = UIColor.white
badgeView.horizontalAlignment = M13BadgeViewHorizontalAlignmentLeft
badgeView.verticalAlignment = M13BadgeViewVerticalAlignmentTop
badgeView.hidesWhenZero = true
rightButton.addSubview(badgeView)
self.navigationItem.rightBarButtonItem = rightButtonItem
Good answer #Julio Bailon (https://stackoverflow.com/a/45948819/1898973)!
Here is the author's site with full explanation: http://www.stefanovettor.com/2016/04/30/adding-badge-uibarbuttonitem/.
It seems not to be working on iOS 11, maybe because the script try to access the "view" property of the UIBarButtonItem. I made it work:
By creating a UIButton and then creating the UIBarButtonItem using the UIButton as a customView:
navigationItem.rightBarButtonItem = UIBarButtonItem.init(
customView: shoppingCartButton)
By replacing the line in the UIBarButtonItem extension:
guard let view = self.value(forKey: "view") as? UIView else { return }
with the following:
guard let view = self.customView else { return }
Seems elegant to me and, best of all, it worked!
You can set below constraints to UILabel with respect to UIButton
align UILabel's top and trailing to UIButton
And when you need to show badge set text to UILabel and when you don't want to show badge then set empty string to UILabel
Download This
For BarButtonItem : Drag and Drop UIBarButtonItem+Badge.h and UIBarButtonItem+Badge.m class in project.
Write this code for set Badges:
self.navigationItem.rightBarButtonItem.badgeValue = "2"
self.navigationItem.rightBarButtonItem.badgeBGColor = UIColor.black
For UIButtton : Drag and Drop UIButton+Badge.h and UIButton+Badge.m class in project.
self.notificationBtn.badgeValue = "2"
self.notificationBtn.badgeBGColor = UIColor.black
Answer with extension from Julio will not work.
Starting from iOS 11 this code will not work cause line of code below will not cast UIView. Also it's counting as private API and seems to be will not pass AppStore review.
guard let view = self.value(forKey: "view") as? UIView else { return }
Thread on Apple Developer Forum
Second thing that this snippet always draws circle, so it can't fit numbers bigger than 9.
Here the simplified version by using custom view
Easy and clear solution if you are looking for only adding the red dot without the number;
private var handle: UInt8 = 0;
extension UIBarButtonItem {
private var badgeLayer: CAShapeLayer? {
if let b: AnyObject = objc_getAssociatedObject(self, &handle) as AnyObject? {
return b as? CAShapeLayer
} else {
return nil
}
}
func setBadge(offset: CGPoint = .zero, color: UIColor = .red, filled: Bool = true, fontSize: CGFloat = 11) {
badgeLayer?.removeFromSuperlayer()
guard let view = self.value(forKey: "view") as? UIView else {
return
}
var font = UIFont.systemFont(ofSize: fontSize)
if #available(iOS 9.0, *) {
font = UIFont.monospacedDigitSystemFont(ofSize: fontSize, weight: .regular)
}
//Size of the dot
let badgeSize = UILabel(frame: CGRect(x: 22, y: -05, width: 10, height: 10))
// initialize Badge
let badge = CAShapeLayer()
let height = badgeSize.height
let width = badgeSize.width
// x position is offset from right-hand side
let x = view.frame.width + offset.x - 17
let y = view.frame.height + offset.y - 34
let badgeFrame = CGRect(origin: CGPoint(x: x, y: y), size: CGSize(width: width, height: height))
badge.drawRoundedRect(rect: badgeFrame, andColor: color, filled: filled)
view.layer.addSublayer(badge)
// initialiaze Badge's label
let label = CATextLayer()
label.alignmentMode = .center
label.font = font
label.fontSize = font.pointSize
label.frame = badgeFrame
label.foregroundColor = filled ? UIColor.white.cgColor : color.cgColor
label.backgroundColor = UIColor.clear.cgColor
label.contentsScale = UIScreen.main.scale
badge.addSublayer(label)
// save Badge as UIBarButtonItem property
objc_setAssociatedObject(self, &handle, badge, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
// bring layer to front
badge.zPosition = 1_000
}
private func removeBadge() {
badgeLayer?.removeFromSuperlayer()
}
}
// MARK: - Utilities
extension CAShapeLayer {
func drawRoundedRect(rect: CGRect, andColor color: UIColor, filled: Bool) {
fillColor = filled ? color.cgColor : UIColor.white.cgColor
strokeColor = color.cgColor
path = UIBezierPath(roundedRect: rect, cornerRadius: 7).cgPath
}
}
The source of the code:
https://gist.github.com/freedom27/c709923b163e26405f62b799437243f4
I only made a few changes to eliminate the number.
The MIBadgeButton-Swift is working also on UIBarButtonItems.
Here is my code after the navigation bar is created:
let rightBarButtons = self.navigationItem.rightBarButtonItems
let alarmsBarButton = rightBarButtons?.last
let alarmsButton = alarmsBarButton.customView as! MIBadgeButton?
alarmsButton.badgeString = "10"
You can do it programmatically with
self.tabBarItem.badgeColor = .red
or use the storyboard. See: