I'm extending the UIView to create a custom view that will be used inside my all UIViewControllers in the app, this is created like:
extension UIView {
class func customBar() -> UIView {
let barview = UIView()
barView.backgroundColor = .red
barView.translatesAutoresizingMaskIntoConstraints = false
}
}
The caller of this method is always a UIViewController, I want to reference the caller ViewController's view's dimensions to add a constraints for the bar, this way it will always show at the same location. So I tried this:
class func customBar() -> UIView {
let barview = UIView()
barView.backgroundColor = .red
barView.translatesAutoresizingMaskIntoConstraints = false
barView.bottomAnchor.constraint(equalTo: barView.superView!.bottomAnchor, constant: -8).isActive = true
barView.rightAnchor.constraint(equalTo: barView.superView!.rightAnchor).isActive = true
barView.leftAnchor.constraint(equalTo: barView.superView!.leftAnchor).isActive = true
barView.heightAnchor.constraint(equalToConstant: 44).isActive = true
}
But this will cause a crash because when I call this function, it's not yet added to the viewController's view, thus, the superView is nil. Here's how I call it inside of viewDidLoad()
let customBar = UIView.customBar()
self.view.addSubview(customBar)
Please, if you have any question, ask me instead of downvoting.
But this will cause a crash because when I call this function, it's not yet added to the viewController's view, thus, the superView is nil.
Since the constraint-adding code must not run until the view has a superview, put it in the view's didMoveToSuperview.
Related
Posting a question for the first time here.
So I have been trying to make an animation of an UIimageView. I did that so far. So the image moves from the middle of the screen to the top. I want to be able to make that animation with constraints. But while trying to add some constraints, I receive this error "Unable to activate constraint with anchors error".
here is the code which I try to add some constraints to banditLogo imageview.
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(banditLogo)
view.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
chooseLabel.alpha = 0
signInButtonOutlet.alpha = 0
self.banditLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 304).isActive = true
self.banditLogo.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
self.banditLogo.widthAnchor.constraint(equalToConstant: 224).isActive = true
self.banditLogo.heightAnchor.constraint(equalToConstant: 289).isActive = true
}
and here is the func that makes the animation.
this func is being called in viewDidAppear and animatedImage variable of the function is referred to banditLogo UIimageView.
so when the view screen loads up, the image moves to top of the view.
func logoAnimate(animatedImage: UIImageView!, animatedLabel: UILabel!) {
UIView.animate(withDuration: 1.5, delay: 1, options: [.allowAnimatedContent]) {
animatedImage.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 5).isActive = true
animatedImage.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
} completion: { (true) in
UIView.animate(withDuration: 0.25) {
animatedLabel.alpha = 1
}
}
}
You may find it easier to create a class-level property to hold the image view's top constraint, then change that constraint's .constant value when you want to move it.
Here's a quick example - tapping anywhere on the view will animate the image view up or down:
class AnimLogoViewController: UIViewController {
let banditLogo = UIImageView()
// we'll change this constraint's .constant to change the image view's position
var logoTopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
if let img = UIImage(systemName: "person.fill") {
banditLogo.image = img
}
view.addSubview(banditLogo)
// I assume this was a typo... you want to set it on the image view, not the controller's view
//view.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
banditLogo.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
// create the image view's top constraint
logoTopConstraint = banditLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 304)
// activate it
logoTopConstraint.isActive = true
// non-changing constraints
self.banditLogo.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
self.banditLogo.widthAnchor.constraint(equalToConstant: 224).isActive = true
self.banditLogo.heightAnchor.constraint(equalToConstant: 289).isActive = true
// animate the logo when you tap the view
let t = UITapGestureRecognizer(target: self, action: #selector(self.didTap(_:)))
view.addGestureRecognizer(t)
}
#objc func didTap(_ g: UITapGestureRecognizer) -> Void {
// if the logo image view is at the top, animate it down
// else, animate it up
if logoTopConstraint.constant == 5.0 {
logoTopConstraint.constant = 304.0
} else {
logoTopConstraint.constant = 5.0
}
UIView.animate(withDuration: 1.5, animations: {
self.view.layoutIfNeeded()
})
}
}
I animate views that have constraints by changing constraints, not setting them. Leave the constraints that are static "as is" - that is, use isActive = true. But those you wish to change? Put them in two arrays and activate/deactivte them. Complete the animation like you are by using UIView.animate.
For instance, let's say you wish to move banditLogo from top 304 to top 5, which appears to me to be what you trying to do. Leave all other constraints as is - left (which your code doesn't seem to change), height, and width. Now, create two arrays:
var start = [NSLayoutConstraint]()
var finish = [NSLayoutConstraint]()
Add in the constraints that change. Note that I'm not setting them as active:
start.append(banditLogo.topAnchor.constraint(equalTo: safeAreaView.topAnchor, constant: 305))
finish.append(banditLogo.topAnchor.constraint(equalTo: safeAreaView.topAnchor, constant: 5))
Initialize things in viewDidLoad or any other view controller method as needed:
NSLayoutConstraint.activate(start)
Finally, when you wish to do the animation, deactivate/activate and tell the view to show the animation:
NSLayoutConstraint.deactivate(start)
NSLayoutConstraint.activate(finish)
UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() }
Last piece of critique, made with no intent of being offending.
Something in your code posted feels messy to me. Creating a function to move a single view should directly address the view IMHO, not pass the view into it. Maybe you are trying to move several views this way - in which case this is good code - but nothing in your question suggests it. It's okay to do the animation in a function - that way you can call it when needed. I do this all the time for something like this - sliding a tool overlay in and out. But if you are doing this to a single view, just address it directly. The code is more readable to other coders.
Also, my preference for the start is in viewDidLoad unless the VC is part of a navigation stack. But in that case, don't just use viewDidAppear, set things back to start in viewDidDisappear.
EDIT: looking at the comments, I assumed that yes you have already used translatesAutoresizingMaskIntoConstraints = false properly on every view needed.
I am building my UI from code and need to present another view controller as a popover, over the current controller. The popover will act as a modal dialog and will use about 50% of the screen size. The contents of the popover must be constrained in the screen center and support device rotation.
My initial implementation was done in viewDidLoad() of the PopoverController class and it worked as expected. However, I do not want view code in my controllers. After moving the popover view to a separate file, the fun began; auto layout and device rotation no longer worked as expected.
I have made a basic app showcasing this. The main controller has a button for displaying the popover. The popover has yellow background and should display a blue view in the center.
MainViewController :: Test button for displaying the popover
#objc func boxButtonTapped() {
let vc = PopoverController()
vc.modalPresentationStyle = .overCurrentContext
vc.modalTransitionStyle = .crossDissolve
present(vc, animated: true, completion: nil)
}
Popover Controller
class PopoverController: UIViewController {
var popoverView = PopoverView()
override func viewDidLoad() {
super.viewDidLoad()
}
override func loadView() {
view = popoverView
}
}
Popover View
class PopoverView: UIView {
let blueBox: UIView = {
let bb = UIView()
bb.backgroundColor = .blue
//bb.frame.size = CGSize(width: 100, height: 100)
bb.translatesAutoresizingMaskIntoConstraints = false
return bb
}()
override init(frame: CGRect) {
super.init(frame: frame)
createSubViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createSubViews()
}
func createSubViews() {
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .yellow
addSubview(blueBox)
blueBox.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
blueBox.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
blueBox.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.7).isActive = true
blueBox.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.4).isActive = true
}
}
When running the above code, the popover doesn`t even display! This is my first question, can anyone see why?
If I change translatesAutoresizingMaskIntoConstraints = false to true. Then the popover displays, but it does not resize when the device rotates. I do not know why and I don´t understand why translatesAutoresizingMaskIntoConstraints needs to be set to true in order for the view to display in the first place.
There must be a simple solution for such a trivial task. I have tried so many different approaches and listing them here would be impossible. I am new to IOS dev and Swift and I really would like to get a good start with clean code. Thank you for your time!
Update
I published the sample project on GitHub. So if you have the time and want a challenge/prove a concept then here it is: https://github.com/igunther/CleanController
If you are not adding constraints yourself - which, in this case, you are not - you still need to tell the view how to behave.
Change your createSubviews() function to this:
func createSubViews() {
//translatesAutoresizingMaskIntoConstraints = false
autoresizingMask = [.flexibleHeight, .flexibleWidth]
backgroundColor = .yellow
addSubview(blueBox)
blueBox.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
blueBox.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
blueBox.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.7).isActive = true
blueBox.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.4).isActive = true
}
That will allow auto-layout to re-layout your view(s) on device rotation.
According to Apple docs for translatesAutoresizingMaskIntoCon
If this property’s value is true, the system creates a set of constraints that duplicate the behavior specified by the view’s autoresizing mask.
and
By default, the property is set to true for any view you programmatically create. If you add views in Interface Builder, the system automatically sets this property to false.
My guess is that when set to false it's looking for constraints that don't exist.
As far as the rotation goes, you could put shouldAutorotate property in your VC?
I have these constraints in place for my UItableView (from storyboard):
musicHomeTableView.translatesAutoresizingMaskIntoConstraints = false
musicHomeTableView.topAnchor.constraint(equalTo: view.topAnchor ).isActive = true
musicHomeTableView.bottomAnchor.constraint(equalTo: mainTabBar.topAnchor).isActive = true
musicHomeTableView.widthAnchor.constraint(equalToConstant: view.frame.width).isActive = true
The above code is called from viewDidLoad and it works fine. Yet when I try to execute this code later:
musicHomeTableView.bottomAnchor.constraint(equalTo: playerView.topAnchor).isActive = true
Nothing happens. mainTabBar and playerView are both programmatically setUp views and subviews of view.
I've tried view.layoutSubviews() and view.layOutIfNeeded()
musicHomeTableView already has a bottomAnchor constraint. You need to remove this constraint before adding another one to the same anchor. Setting another constraint to the same anchor does not remove the old constraint.
class ViewController {
var musicHomeTableViewBottomAnchorConstraint: NSLayoutConstraint?
func viewDidLoad() {
musicHomeTableViewBottomAnchorConstraint = musicHomeTableView.bottomAnchor.constraint(equalTo: view.topAnchor)
musicHomeTableViewBottomAnchorConstraint!.isActive = true
}
func addNewConstraint() {
if let constraint = musicHomeTableViewBottomAnchorConstraint {
constraint.isActive = false
}
// Add new constraint here
}
}
There are other ways to do this, but this is one.
I have a UIView in which I have a circular border around it. Here is the code for it:
let v = UIView()
self.view.addSubview(v)
v.backgroundColor = .orange
v.translatesAutoresizingMaskIntoConstraints = false
v.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
v.rightAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
v.topAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
self.view.layoutIfNeeded()
v.layer.cornerRadius = v.frame.width / 2
If I set the ViewController view like this: self.view = mainView (mainView is a subclass of MainView which contains some other subviews), then the result of the corner radius is no longer a circle: Resulting "circle".
However, if I use self.view.addSubview(mainView) (and add autolayout constraints to mainView) and replace self.view.addSubview(v) with self.mainView.addSubview(v) then the circle turns out to be fine.
Why does the circle turn out weird only when I set self.view = mainView, but is fine when I do self.view.addSubview(mainView)?
First where are you replacing the UIViewControllers view with mainView? You should be overriding the loadView method of the UIViewController not doing it in the viewDidLoad method.
Second if you are changing the UIViewControllers view then it will not have it's frame setup when you get to viewDidLoad like the original will and so the UIView v is getting the incorrect layout size which it is then using to determine its corner radius.
Third you shouldn't use layoutIfNeeded in the viewDidLoad as this is way to early to be trying to determine the final layout of the main view (everything is still loading). What you should be doing is overriding viewDidLLayoutSubviews method and setting the corner radius there like this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
v.layer.cornerRadius = v.frame.width / 2
}
What that also does is if the size of the UIView v changes (orientation change, etc) then it will remain a circle at it's new size otherwise it will just have rounded corners.
Of course you have to make UIView v a class instance variable in order to be able to access it here.
I have a page which has radius corners.
When the page appear, corners are not rounded at first. After seconds, corners become rounded.
I want to make corners rounded from at first.
I set properties to view at custom view's initializer.
class ModalView: UIView {
init() {
super.init(frame: .zero)
self.backgroundColor = UIColor.whiteColor()
self.clipsToBounds = true
self.layer.cornerRadius = 10
}
}
class ViewController: UIViewController {
override func loadView() {
let customView = ModalView()
customView.frame = self.view.frame
self.view = customView
}
}
This problem was solved.
The situation was like below.
image
So, I set cornerRadius to navigation controller's view.
self.navigationController?.view.layer.cornerRadius = 10
self.navigationController?.view.clipsToBounds = true
Your code does not compile.
First, initWithCoder is missing, maybe you omitted it.
Secondly, in init you call super initWithFrame, which internally is calling again init, here is where the program crashes, i don't get how yours is working but this explains the delay.
And third, loadView is used for controllers purely made in code, if you have a xib you already have a view.
Four, UIViewController does not extend a UIView, i don't even understand what you wanted to do there, you can't redefine a class.