I want to add a view with a tableview in it in code.
I define the view in storyboard like in the image.
In my view controller I set the place and size using the following code.
override func viewDidLoad() {
super.viewDidLoad()
// bunch of non-related code
timeTableView.delegate = self
timeTableView.dataSource = self
timeTableView.rowHeight = UITableViewAutomaticDimension
timeTableView.estimatedRowHeight = 32
timeChooser.translatesAutoresizingMaskIntoConstraints=false
}
#IBAction func onTimeLabelPressed(_ sender: AnyObject){
view.addSubview(timeChooser)
let topConstraint = timeChooser.topAnchor.constraint(equalTo: viewdayTime.bottomAnchor)
let leftConstraint = timeChooser.leftAnchor.constraint(equalTo: viewdayTime.leftAnchor)
//let heightConstraint = timeChooser.heightAnchor.constraint(greaterThanOrEqualToConstant: 128)
//let widthConstraint = timeChooser.widthAnchor.constraint(lessThanOrEqualTo: viewdayTime.widthAnchor)
NSLayoutConstraint.activate([topConstraint,leftConstraint, /*heightConstraint, widthconstraint*/])
view.layoutIfNeeded()
timeChooser.alpha=0
UIView.animate(withDuration: 0.2){
self.timeChooser.alpha=1.0}
}
The function onTimeLabelPressed makes the view appear under and left aligned with a label.
Problem is the tableview will not scroll or respond otherwise as long as the line
timeChooser.translatesAutoresizingMaskIntoConstraints=false
is present. Removing it breaks the positioning constraints but the tableview behaves normally.
Anybody any ideas? Did not find any question describing my witnessed behavior.
You need to give your timeChooser constraints to establish its width and height. It would probably be best to set right and bottom constraints.
let rightConstraint = timeChooser.rightAnchor.constraint(equalTo: view.rightAnchor)
let bottomConstraint = timeChooser.bottomAnchor.constraint(equalTo: view.bottomAnchor)
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 working on a project where I want the user to be able to select two methods of input for the same form. I came up with a scrollview that contains two custom UIViews (made programmatically). Here is the code for the responsible view controller:
import UIKit
class MainVC: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
var customView1: CustomView1 = CustomView1()
var customView2: customView2 = CustomView2()
var frame = CGRect.zero
func setupScrollView() {
pageControl.numberOfPages = 2
frame.origin.x = 0
frame.size = scrollView.frame.size
customView1 = customView1(frame: frame)
self.scrollView.addSubview(customView1)
frame.origin.x = scrollView.frame.size.width
frame.size = scrollView.frame.size
customView2 = CustomView2(frame: frame)
self.scrollView.addSubview(customView2)
self.scrollView.contentSize = CGSize(width: scrollView.frame.size.width * 2, height: scrollView.frame.size.height)
self.scrollView.delegate = self
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
pageControl.currentPage = Int(pageNumber)
}
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
scrollView.delegate = self
}
While it works, Xcode gives me an error message for auto layout:
Scrollable content size is ambiguous for "ScrollView"
Also a problem: content on the second UIView is not centered, even though it should be:
picture of the not centered content
import UIKit
class customView2: UIView {
lazy var datePicker: UIDatePicker = {
let datePicker = UIDatePicker()
datePicker.translatesAutoresizingMaskIntoConstraints = false
return datePicker
}()
//initWithFrame to init view from code
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
//initWithCode to init view from xib or storyboard
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
func setupView () {
self.backgroundColor = .systemYellow
datePicker.datePickerMode = .date
datePicker.addTarget(self, action: #selector(self.datePickerValueChanged(_:)), for: .valueChanged)
addSubview(datePicker)
setupLayout()
}
func setupLayout() {
let view = self
NSLayoutConstraint.activate([
datePicker.centerXAnchor.constraint(equalTo: view.centerXAnchor),
datePicker.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
datePicker.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
datePicker.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2)
])
}
#objc func datePickerValueChanged(_ sender: UIDatePicker) {
let dateFormatter: DateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd.MM.yyyy"
let selectedDate: String = dateFormatter.string(from: sender.date)
print("Selected value \(selectedDate)")
}
Any ideas on how to solve this? Thank you very much in advance. And please go easy on me, this is my first question on stackoverflow. I am also fairly new to programming in swift.
To make things easier on yourself,
add a horizontal UIStackView to the scroll view
set .distribution = .fillEqually
constrain all 4 sides to the scroll view's .contentLayoutGuide
constrain its height to the scroll view's .frameLayoutGuide
add your custom views to the stack view
constrain the width of the first custom view to the width of the scroll view's .frameLayoutGuide
Here is your code, modified with that approach:
class MainVC: UIViewController, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
var customView1: CustomView1 = CustomView1()
var customView2: CustomView2 = CustomView2()
func setupScrollView() {
pageControl.numberOfPages = 2
// let's put the two custom views in a horizontal stack view
let stack = UIStackView()
stack.axis = .horizontal
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(customView1)
stack.addArrangedSubview(customView2)
// add the stack view to the scroll view
scrollView.addSubview(stack)
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain stack view to all 4 sides of content layout guide
stack.topAnchor.constraint(equalTo: contentG.topAnchor),
stack.leadingAnchor.constraint(equalTo: contentG.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: contentG.trailingAnchor),
stack.bottomAnchor.constraint(equalTo: contentG.bottomAnchor),
// stack view Height equal to scroll view frame layout guide height
stack.heightAnchor.constraint(equalTo: frameG.heightAnchor),
// stack is set to fillEqually, so we only need to set
// width of first custom view equal to scroll view frame layout guide width
customView1.widthAnchor.constraint(equalTo: frameG.widthAnchor),
])
self.scrollView.delegate = self
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
pageControl.currentPage = Int(pageNumber)
}
override func viewDidLoad() {
super.viewDidLoad()
setupScrollView()
scrollView.delegate = self
}
}
Edit
Couple additional notes...
UIScrollView layout ambiguity.
As I said in my initial comment, if we add a UIScrollView in Storyboard / Interface Builder, but do NOT give it any constrained content, IB will complain that it has Scrollable Content Size Ambiguity -- because it does. We haven't told IB what the content will be.
We can either ignore it, or select the scroll view and, at the bottom of the Size Inspector pane, change Ambiguity to Never Verify.
As a general rule, you should correct all auto-layout warnings / errors, but in specific cases such as this - where we know that it's setup how we want, and we'll be satisfying constraints at run-time - it doesn't hurt to leave it alone.
UIDatePicker not being centered horizontally.
It actually is centered. If you add this line:
datePicker.backgroundColor = .green
You'll see that the object frame itself is centered, but the UI elements inside the frame are left-aligned:
From quick research, it doesn't appear that can be changed.
Now, from Apple's docs, we see:
You should integrate date pickers in your layout using Auto Layout. Although date pickers can be resized, they should be used at their intrinsic content size.
Curiously, if we add a UIDatePicker in Storyboard, change its Preferred Style to Compact, and give it centerX and centerY constraints... Storyboard doesn't believe it has an intrinsic content size.
If we add it via code, giving it only X/Y position constraints, it will show up where we want it at its intrinsic content size. But... if we jump into Debug View Hierarchy, Xcode tells us its Position and size are ambiguous.
Now, what's even more fun...
Tap that control and watch the Debug console fill with 535 Lines of auto-layout errors / warnings!!!
Some quick investigation -- these are all internal auto-layout issues, and have nothing to do with our code or layout.
We see similar issues with the iOS built-in keyboard when it starts showing auto-complete options.
Those are safe to ignore.
So I found the following UI pattern online and I have been attempting to implement it in Xcode. However, I have been unsuccessful. I am unsure as to whether to create the optimal approach would be to
create three different UIViewControllers (in which case I am not sure as to how to get them to animate in and out of view/how to get them to overlap one another)
or to use a UITableView with custom overlapping cells. However, I am not sure whether this approach will allow me to animate properly upon pressing each cell. For this second approach, I saw this post, but this solution does not allow for touch interaction in the overlapping areas, something which I need.
I looked for libraries online that would allow for functionality such as this, but I was unsuccessful in finding any. The animation I am trying to achieve can be found here.
I would use a StackView to hold all the views of your ViewControllers.
Then you can set the stack view's spacing property to a negative value to make the views overlap. If you wish show the complete view of one of the view controllers on tap, you add a tap gesture recognizer to that view that changes the stackView's spacing for just that view to 0.
A really simple example:
class PlaygroundViewController: UIViewController {
let firstViewController = UIViewController()
let secondViewController = UIViewController()
let thirdViewController = UIViewController()
lazy var viewControllersToAdd = [firstViewController, secondViewController, thirdViewController]
let heightOfView: CGFloat = 300
let viewOverlap: CGFloat = 200
let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
firstViewController.view.backgroundColor = .red
secondViewController.view.backgroundColor = .blue
thirdViewController.view.backgroundColor = .green
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = -viewOverlap
viewControllersToAdd.forEach { (controller: UIViewController) in
if let childView = controller.view {
stackView.addArrangedSubview(childView)
NSLayoutConstraint.activate([
childView.heightAnchor.constraint(equalToConstant: heightOfView),
childView.widthAnchor.constraint(equalTo: stackView.widthAnchor)
])
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapChildView(sender:)))
childView.addGestureRecognizer(gestureRecognizer)
childView.isUserInteractionEnabled = true
}
addChild(controller)
controller.didMove(toParent: self)
}
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.rightAnchor.constraint(equalTo: view.rightAnchor),
stackView.leftAnchor.constraint(equalTo: view.leftAnchor),
stackView.topAnchor.constraint(equalTo: view.topAnchor),
])
}
#objc func didTapChildView(sender: UITapGestureRecognizer) {
if let targetView = sender.view {
UIView.animate(withDuration: 0.3, animations: {
let currentSpacing = self.stackView.customSpacing(after: targetView)
if currentSpacing == 0 {
// targetView is already expanded, collapse it
self.stackView.setCustomSpacing(-self.viewOverlap, after: targetView)
} else {
// expand view
self.stackView.setCustomSpacing(0, after: targetView)
}
})
}
}
}
I've been struggling thinking how to setup this kind of layout in my tableViewCell. See photo:
More info:
Data is dynamic. There might be other days and each days might consists of multiple set of time.
Auto-layout is needed of course for dynamic/responsive height of my cell.
What I already did:
I did try doing this using IB.
I also tried it programmatically.
Both in IB and programmatically, I used UIStackView. But kinda hard to set it up.
I'm thinking to set it up using UIViews as containers, just like UIStackView but less complex.
I'm setting this up row by row. First is to line up the time vertically, and then the view or stackview of that will be paired horizontally with the Day. After that, do the same with the other days.
For formality of the question, here is a part of my code in my cell for setting up this layout, I suggest not taking an effort to read it, I believe I know what I am doing, and I think I just need another approach from you guys.
var job: Job! {
didSet {
_ = self.subviews.map {
if $0 is UIStackView {
$0.removeFromSuperview()
}
}
GPLog(classSender: self, log: "🎉Setting up stackview for JobShift")
// Add the main vertical stackView
let stackView_Vertical = UIStackView(frame: .zero)
stackView_Vertical.translatesAutoresizingMaskIntoConstraints = false
stackView_Vertical.alignment = .fill
stackView_Vertical.distribution = .fillProportionally
stackView_Vertical.axis = .vertical
stackView_Vertical.spacing = 16.0
self.addSubview(stackView_Vertical)
// Add constraints
stackView_Vertical.topAnchor.constraint(equalTo: self.topAnchor, constant: 15.0).isActive = true
stackView_Vertical.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -15.0).isActive = true
stackView_Vertical.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15.0).isActive = true
stackView_Vertical.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -15.0).isActive = true
if let dummyJson = self.readJson() {
if let shiftsJsonArray = dummyJson.array {
for shiftJson in shiftsJsonArray {
let newShift = DummyDataShift(json: shiftJson)
if let day = newShift.day,
let schedules = newShift.schedule {
let generatedStackView = self.generateDayScheduleStackView(day: day, schedules: schedules)
stackView_Vertical.addArrangedSubview(generatedStackView)
}
}
}
}
}
}
// MARK: - Functions
// Generate the full schedule stack view.
func generateDayScheduleStackView(day: String, schedules: [DummyDataSchedule]) -> UIStackView {
// label day (e.g. MONDAY)
let newLabel_Day = self.shiftLabel
newLabel_Day.translatesAutoresizingMaskIntoConstraints = false
newLabel_Day.text = day
newLabel_Day.backgroundColor = .red
newLabel_Day.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
// Prepare the vertical schedule stackView
let stackView_Schedule = UIStackView(frame: .zero)
stackView_Schedule.alignment = .fill
stackView_Schedule.distribution = .fillEqually
stackView_Schedule.axis = .vertical
// Add the schedules to the stackView vertically
for schedule in schedules {
let newLabel_Time = self.shiftLabel
newLabel_Time.text = "\(schedule.timeIn!) - \(schedule.timeOut!)"
newLabel_Time.backgroundColor = self.getRandomColor()
newLabel_Time.translatesAutoresizingMaskIntoConstraints = false
newLabel_Time.heightAnchor.constraint(equalToConstant: 30.0).isActive = true
stackView_Schedule.addArrangedSubview(newLabel_Time)
}
// Prepare the horizontal dayScheduleStackView
let stackView_DaySchedule = UIStackView(frame: .zero)
stackView_DaySchedule.alignment = .fill
stackView_DaySchedule.distribution = .fillProportionally
stackView_DaySchedule.axis = .horizontal
// Add arranged subViews
stackView_DaySchedule.addArrangedSubview(newLabel_Day)
stackView_DaySchedule.addArrangedSubview(stackView_DaySchedule)
return stackView_DaySchedule
}
Problem is: Lots of warnings for broken constraints, I do know how to set up and fix constraints. But when I fix it, nothing is displaying. I feel like I'm wasting my time pushing and trying hard to continue this approach. So I thought that it would be me a lot if I ask for suggestions?
There is multiple ways of solving your problem. I will try to give you one approach to it.
1) Use a TableView, with each section cell containing the "day of the week" label plus a vertical StackView for the time labels (use equal spacing).
2) If you set your constraints properly, you can return UITableViewAutomaticDimension on sizeForRow: delegate.
override func tableView(_ tableView: UITableView, heightForRowAt
indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
Then, on the cellForRowAt method, you can append labels to your vertical stack view with the times. The height of each cell will be correct as it will come from the constraints.
Actually, I think stack views a perfectly good way to go. They are about arranging things in rows and columns, which is precisely what you want to do. I had no difficulty arranging some labels in imitation of your specifications, using stack views alone, in Interface Builder:
The delightful thing about stack views is that once they are configured properly, they adapt as arranged subviews are added or removed. So they are dynamic in exactly the way you desire.
I am trying to make my uitextview's corner radius dynamic depending on the number of lines displayed. The text is retrieved from my backend, so the number of lines will vary...
I've tried setting the corner radius in viewDidLoad after the text view is created by using its frame but for some reason that doesn't work. I'm assuming its returning 0 for some reason.
Any help is greatly appreciated.
(I'm cutting out a lot of code just to keep it simple here. Everything is added to the subview properly. Everything is added programmatically - not using the storyboard at all here. The text views display as expected besides the corner radius)
inside viewDidLoad:
questionOneTextBox.layer.cornerRadius = self.questionOneTextBox.frame.height * 0.5
questionTwoTextBox.layer.cornerRadius = self.questionTwoTextBox.frame.height * 0.5
If you are creating the text view programmatically with constraints, then you need to call self.questionOneTextBox.layoutIfNeeded() and self.questionTwoTextBox.layoutIfNeeded(). This will initialise the bounds.
class ViewController: UIViewController {
var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Create the Text View
self.textView = UITextView()
self.textView.text = "This text will appear in the text view."
self.textView.backgroundColor = .lightGray
self.view.addSubview(self.textView)
// Set the constraints
self.textView.translatesAutoresizingMaskIntoConstraints = false
self.textView.widthAnchor.constraint(equalToConstant: 280).isActive = true
self.textView.heightAnchor.constraint(equalToConstant: 60).isActive = true
self.textView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 60).isActive = true
self.textView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
// This will initialise the bounds
self.textView.layoutIfNeeded()
// Now the this should work
self.textView.layer.cornerRadius = self.textView.frame.height * 0.5
}
}
This what this code looks like.
Views are not laid out (sized) when viewDidLoad is called. Move your corner radius sizing to viewDidLayoutSubviews or viewWillAppear, at which time the sizing has occurred.
Did you set clipsToBounds to true? This might be causing your problem. I hope this helped