I made a simple custom view in Swift which has a background image view with a photo and some labels in front. I then added the custom view to my storyboard and it displays well, I can see the background image and labels.
But when I run my app on my device the image view doesn't show immediately, if I navigate to another view then back, it is displayed. I only have a timer which scheduled some tasks in background in my scene. Did I miss anything here?
Here is the code of my custom view
import UIKit
#IBDesignable class GaugeView: UIImageView {
#IBOutlet weak var speedLabel: UILabel!
let circlePathLayer = CAShapeLayer()
var circleRadius: CGFloat = 50.0
var speed: CGFloat {
get {
return circlePathLayer.strokeEnd
}
set {
speedLabel.text = String(format: "%.2f", newValue)
if newValue < 20.0 {
circlePathLayer.strokeColor = UIColor.blueColor().CGColor
} else if newValue >= 25.0 {
circlePathLayer.strokeColor = UIColor.redColor().CGColor
} else {
circlePathLayer.strokeColor = UIColor.yellowColor().CGColor
}
if (newValue > 50) {
circlePathLayer.strokeEnd = 1
} else if (newValue < 0) {
circlePathLayer.strokeEnd = 0
} else {
circlePathLayer.strokeEnd = newValue / 49.9
}
}
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xibSetup()
}
override init(frame: CGRect) {
super.init(frame: frame)
xibSetup()
}
override func layoutSubviews() {
super.layoutSubviews()
circleRadius = bounds.width / 2 - 2.6
circlePathLayer.frame = bounds
circlePathLayer.path = circlePath().CGPath
}
// Our custom view from the XIB file
var view: UIView!
let nibName = "GaugeView"
func xibSetup() {
view = loadViewFromNib()
// use bounds not frame or it'll be offset
view.frame = bounds
// Make the view stretch with containing view
view.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight
// Adding custom subview on top of our view (over any custom drawing > see note below)
addSubview(view)
// Configure speed circle layer
configure()
}
func configure() {
speed = 0.0
circlePathLayer.frame = bounds
circlePathLayer.lineWidth = 6
circlePathLayer.fillColor = UIColor.clearColor().CGColor
circlePathLayer.strokeColor = UIColor.blueColor().CGColor
layer.addSublayer(circlePathLayer)
backgroundColor = UIColor.whiteColor()
}
func circleFrame() -> CGRect {
var circleFrame = CGRect(x: 0, y: 0, width: 2*circleRadius, height: 2*circleRadius)
circleFrame.origin.x = CGRectGetMidX(circlePathLayer.bounds) - CGRectGetMidX(circleFrame)
circleFrame.origin.y = CGRectGetMidY(circlePathLayer.bounds) - CGRectGetMidY(circleFrame)
return circleFrame
}
func circlePath() -> UIBezierPath {
let center: CGPoint = CGPointMake(CGRectGetMidX(circlePathLayer.bounds), CGRectGetMidY(circlePathLayer.bounds))
let start = CGFloat(1.33 * M_PI_2)
let end = CGFloat( 1.64 * M_2_PI)
return UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: start, endAngle: end, clockwise: true)
}
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: nibName, bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
}
Then I added the custom view to my storyboard and navigate to the scene programatically with performSegueWithIdentifier("dashboardSegue", sender: nil). I just noticed not only the custome view, but two progress bar on the scene are not displayed as well.
It's better show your code, but i am guessing your timer stuck the main UI thread. When you navigate to another view, your timer.invalidate() was called. Try to remove the NSTimer to see if it works well.
Related
I`ve implemented a UIView that display a CustomLottie in the center of the screen, it has show() and hide() methods.
How can I give it the ability of hide() it from another place than where show() was called??
This is the code:
class LottieProgressHUD: UIView {
static let shared = LottieProgressHUD()
let hudView: UIView
var animationView: AnimationView
//options
var hudWidth:CGFloat = 200
var hudHeight:CGFloat = 200
var animationFileName = "coinLoading"
override init(frame: CGRect) {
self.hudView = UIView()
self.animationView = AnimationView()
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
self.hudView = UIView()
self.animationView = AnimationView()
super.init(coder: aDecoder)
self.setup()
}
func setup() {
self.addSubview(hudView)
show()
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
if let superview = self.superview {
self.animationView.removeFromSuperview()
let screenRect = UIScreen.main.bounds
let screenWidth = screenRect.size.width
let screenHeight = screenRect.size.height
let width: CGFloat = self.hudWidth
let height: CGFloat = self.hudHeight
self.frame = CGRect(x: (screenWidth / 2) - (width / 2) ,y: (screenHeight / 2) - (height / 2), width: width, height: height)
hudView.frame = self.bounds
layer.masksToBounds = true
self.animationView = AnimationView(name: self.animationFileName)
self.animationView.frame = CGRect(x: 0, y: 0, width: self.hudView.frame.width, height: self.hudView.frame.size.height)
self.animationView.contentMode = .scaleAspectFill
self.hudView.addSubview(animationView)
self.animationView.loopMode = .loop
self.animationView.play()
self.hide()
}
}
func show() {
self.isHidden = false
}
func hide() {
self.isHidden = true
}
}
This is how I call it inside the VC:
let progressHUD = LottieProgressHUD.shared
self.view.addSubview(progressHUD)
progressHUD.show()
I would like to call progressHUD.hide() inside the VM
You already have a static property that you can reference the same instance from anywhere. Should be able to call your show and hide methods from any class.
LottieProgressHUD.shared.show()
LottieProgressHUD.shared.hide()
I have some requirement to show popup as some customisation options.
So, I took custom view as UIView with Xib file.
class CustomAlertView : UIView {
#IBOutlet weak var customAlertView : UIView!
#IBOutlet weak var button1 : UIButton!
#IBOutlet weak var button2 : UIButton!
override init(frame: CGRect) { // for using CustomView in code
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) { // for using CustomView in IB
super.init(coder: aDecoder)
self.commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("CustomAlertView", owner: self)
guard let content = customAlertView else { return }
content.frame = self.bounds
content.autoresizingMask = [.flexibleHeight, .flexibleWidth]
self.addSubview(content)
}
}
And given outlet connections.
And I am able to load the view in viewcontroller class.
But, the issue is, its not showing as pop up view and its not showing any border color, etc even I added too.
And the present self.view (Main view from viewcontroller) still moving it has tableview, while I clicking on the buttons on custom view, nothing happening.
func someAction() {
self.view.addSubview(customAlert)
self.customAlert.layer.cornerRadius = 13
self.customAlert.layer.borderWidth = 10
self.customAlert.layer.borderColor = UIColor.gray.cgColor
self.customAlert.clipsToBounds = false
let radius: CGFloat = self.customAlert.frame.width / 2.0 //change it to .height if you need spread for height
let shadowPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 2.1 * radius, height: self.customAlert.frame.height))
self.customAlert.layer.masksToBounds = false
self.customAlert.layer.cornerRadius = 8; // if you like rounded corners
self.customAlert.layer.shadowOffset = CGSize(width:-15, height:20);
self.customAlert.layer.shadowRadius = 5;
self.customAlert.layer.shadowOpacity = 0.5;
self.customAlert.layer.shadowPath = shadowPath.cgPath
self.customAlert.byMonthlyBtn.addTarget(self, action: #selector(button1tapped), for: .touchUpInside)
self.customAlert.byAnnuallyBtn.addTarget(self, action: #selector(button2tapped), for: .touchUpInside)
}
And its looks like below screenshot
Any suggestions?
private func commonInit() {
Bundle.main.loadNibNamed("CustomAlertView", owner: self)
guard let content = customAlertView else { return }
content.frame = self.bounds
content.autoresizingMask = [.flexibleHeight, .flexibleWidth]
self.addSubview(content)
}
Your problem is you are using the CustomAlertView you loaded. Try adjust your code like below.
private func commonInit() {
guard let view = Bundle.main.loadNibNamed("CustomAlertView", owner: self)?.first as? CustomAlertView else {
return
}
view.frame = bounds
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(view)
}
I'm building a custom view using an XIB file. However, I am facing a problem where the layers I add to the view (trackLayer) are not shown on the xib (circleLayer is an animation and I don't expect it to render in xib which is not possible to my knowledge). The code of the owner class for the XIB is shown as follows:
#IBDesignable
class SpinningView: UIView {
#IBOutlet var contentView: SpinningView!
//MARK: Properties
let circleLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let circularAnimation: CABasicAnimation = {
let animation = CABasicAnimation()
animation.fromValue = 0
animation.toValue = 1
animation.duration = 2
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
return animation
}()
//MARK: IB Inspectables
#IBInspectable var strokeColor: UIColor = UIColor.red {
...
}
#IBInspectable var trackColor: UIColor = UIColor.lightGray {
...
}
#IBInspectable var lineWidth: CGFloat = 5 {
...
}
#IBInspectable var fillColor: UIColor = UIColor.clear {
...
}
#IBInspectable var isAnimated: Bool = true {
...
}
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
initSubviews()
}
//MARK: Private functions
private func setup() {
setupTrack()
setupAnimationOnTrack()
}
private func setupTrack(){
trackLayer.lineWidth = lineWidth
trackLayer.fillColor = fillColor.cgColor
trackLayer.strokeColor = trackColor.cgColor
layer.addSublayer(trackLayer)
}
private func setupAnimationOnTrack(){
circleLayer.lineWidth = lineWidth
circleLayer.fillColor = fillColor.cgColor
circleLayer.strokeColor = strokeColor.cgColor
circleLayer.lineCap = kCALineCapRound
layer.addSublayer(circleLayer)
updateAnimation()
}
private func updateAnimation() {
if isAnimated {
circleLayer.add(circularAnimation, forKey: "strokeEnd")
}
else {
circleLayer.removeAnimation(forKey: "strokeEnd")
}
}
//MARK: Layout contraints
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width / 2, bounds.height / 2) - circleLayer.lineWidth / 2
let startAngle = -CGFloat.pi / 2
let endAngle = 3 * CGFloat.pi / 2
let path = UIBezierPath(arcCenter: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
trackLayer.position = center
trackLayer.path = path.cgPath
circleLayer.position = center
circleLayer.path = path.cgPath
}
private func initSubviews() {
let bundle = Bundle(for: SpinningView.self)
let nib = UINib(nibName: "SpinningView", bundle: bundle)
nib.instantiate(withOwner: self, options: nil)
contentView.frame = bounds
addSubview(contentView)
}
}
When a view is subclassed in the Main.storyboard, I can see the image and the tracklayer as follows IB UIView but when I go over to the XIB, it does not have the trackLayer(circle around the image) XIB Image. While one can argue that it is working and why I am bothering with this, I think it is important that I design the XIB properly since another person might just see it as an view with only an image (no idea of the animating feature)
When you are designing your XIB, it is not an instance of your #IBDesignable SpinningView. You are, effectively, looking at the source of your SpinningView.
So, the code behind it is not running at that point - it only runs when you add an instance of SpinningView to another view.
Take a look at this simple example:
#IBDesignable
class SpinningView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet var theLabel: UILabel!
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}
private func initSubviews() {
let bundle = Bundle(for: SpinningView.self)
let nib = UINib(nibName: "SpinningView", bundle: bundle)
nib.instantiate(withOwner: self, options: nil)
contentView.frame = bounds
addSubview(contentView)
// this will change the label's textColor in Storyboard
// when a UIView's class is set to SpinningView
theLabel.textColor = .red
}
}
When I am designing the XIB, this is what I see:
But when I add a UIView to another view, and assign its class as SpinningView, this is what I see:
The label's text color get's changed to red, because the code is now running.
Another way to think about it... you have trackColor, fillColor, etc vars defined as #IBInspectable. When you're designing your XIB, you don't see those properties in Xcode's Attributes Inspector panel - you only see them when you've selected an instance of SpinningView that has been added elsewhere.
Hope that makes sense.
I have a customView where I overwrite drawRect:. I also have some cases, where I want to display a UILabel ontop of my customView:
if self.ticks.count < 50 {
self.label.frame = self.bounds
self.label.text = "This chart requires 50 or more ticks.\nKeep Ticking."
self.addSubview(label)
}
However if I call this code before the drawRect is called, then my label doesn't show up. Is there a callback method like viewDidDraw or something?
EDIT
Someone asked for it, so here is all the code that is now doing what I need it to do. I think the answer to the question is to override layoutSubviews.
#IBDesignable open class CustomView: UIView {
#objc open var ticks:[Tick] = []
let label = UILabel()
required public init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
required public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup(){
self.backgroundColor = UIColor.clear
self.label.frame = self.bounds
self.label.text = "This chart requires 50 or more ticks.\nKeep Ticking."
self.label.numberOfLines = 0
self.label.textAlignment = .center
self.addSubview(label)
}
override open func layoutSubviews() {
self.label.frame = self.bounds
}
#objc open func setSessions(sessions:[Session]){
self.ticks = []
for session in sessions.reversed(){
if self.ticks.count < 250{
self.ticks = self.ticks + (session.getShots() as! [Shot])
}else{
break
}
}
if self.ticks.count < 50 {
self.label.isHidden = false
}else{
self.label.isHidden = true
}
self.bringSubview(toFront: self.label)
}
override open func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
let width = rect.width - 10;
let height = rect.height - 10;
let center = CGPoint(x: width/2+5, y: height/2+5)
let big_r = min(width, height)/2
context.setLineWidth(1.5)
UIColor.white.setStroke()
//draw the rings
for i in stride(from: 1, to: 7, by: 1){
let r = big_r * CGFloat(i) / 6.0;
context.addArc(center:center, radius:r, startAngle: 0, endAngle: CGFloat(2*Double.pi), clockwise: false)
context.strokePath()
}
//draw the ticks
if self.ticks.count > 49 {
UIColor.red().setFill()
for (i, tick) in self.ticks.prefix(250).enumerated(){
let radius = min((100.0-CGFloat(shot.score))/30.0 * big_r, big_r);
let x = Utilities.calculateX(tick, radius)
let y = Utilities.calculateY(tick, radius)
let point = CGPoint(x: x+center.x,y: y+center.y)
context.addArc(center:point, radius:CGFloat(DOT_WIDTH/2), startAngle: 0, endAngle: CGFloat(2*Double.pi), clockwise: false)
context.fillPath()
}
}
}
}
You can make your custom view redraw by calling setNeedsDisplay() when needed. After that, you can add the label
You can set a delegation for the View class and call the delegate method at the end of the draw function. However, it seems that if you call that directly, the draw function will only end until the delegate function get processed. Which means that the code will be executed before the new drawing refresh on the screen. In that case, you may need to call it asynchronously.
// at end of draw function
DispatchQueue.main.async {
self.delegate?.drawEnd()
}
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 !