So I'm trying to clone the Apple weather app. My ViewHierarchy looks like this:
ViewControllerA contains a UIScrollView which in turn contains a UIView (UIView1) and the UIView contains child elements.There is also a button below the UIScrollView to add more UIView inside the UIScrollView.
This UIViewController is designed in IB using AutoLayout.
When I click this button ideally I want to clone the UIView1 and add it to the UIScrollView, to the righthandside end of the currentView inside the UIScrollView. This is a horizontal scroll function. This is where I am stuck.
What I have tried are the following:
Create a copy of UIView1 in a xib and load that.
Create a UIView programmatically and load that.
In both the cases I'm facing the auto layout constraints issue. When I load the second view, it's overwritten on top of the existing view.
I can hardcode the frame sizes for the cloned UIView and get it to work but obviously that won't work across devices.
So I'm adding constraints - something like this:
func buildView(startX:CGFloat, model:CityModel) -> UIView {
var frame:CGRect = CGRectMake(0, 0, insideScrollView.bounds.width, insideScrollView.bounds.height)
var cityView:UIView = UIView(frame: frame)
var lFrame:CGRect = CGRectMake(0, 0, 100,50)
var cLabel:UILabel = UILabel(frame: lFrame)
cLabel.text = model.name
cLabel.sizeToFit()
cLabel.textAlignment = .Center
//cityView.addSubview(cLabel)
cityView.layer.backgroundColor = UIColor.blueColor().CGColor
cityView.setTranslatesAutoresizingMaskIntoConstraints(false)
var constX = NSLayoutConstraint(item: cityView, attribute: NSLayoutAttribute.LeftMargin, relatedBy: NSLayoutRelation.Equal, toItem: cityScrollView, attribute: NSLayoutAttribute.LeftMargin, multiplier: 1, constant: 0)
cityView.addConstraint(constX)
return cityView
}
The app crashes unable to load this constraint indicating that the view hierarchy does not support this constraint as all the views are not loaded.
I'll keep digging on how to resolve this but any help will be greatly appreciated. Here's my viewDidLoad method. Cities is an array of models containing the view data
override func viewDidLoad() {
super.viewDidLoad()
var startScroll = insideScrollView.bounds.width
if cities != nil {
for model in cities.cityModels {
if model.selected {
cityScrollView.addSubview(buildView(startScroll, model: model))
startScroll += cityScrollView.bounds.width
}
}
} else {
var cityModel: CityModel = CityModel(name: "default")
cityModel.selected = false
cities = SearchCityModels.sharedInstance
cities.cityModels.append(cityModel)
}
}
Posting the answer on behalf of #k6sandeep. I had not loaded the view inside the scrollview but was adding constraints to it. So fixed the same by adding constraints after the insideview was loaded.
Related
I have a custom view. I am trying to add this custom view and center it in my ViewController. I created the view in storyboard but am adding it to my ViewController programmatically. The init function requires that I give a frame. I don't want to specify a frame because I want the view to be autosized based on what content is in the view controller and then I just want to use my constraints to center the view.
This is the code within my viewcontroller that I use to add my custom view
let reportWindow = ReportWindow(user: self.uid!, frame: CGRect(x: 0, y: 0, width: 100, height: 100))
self.view.addSubview(reportWindow)
let centerX = NSLayoutConstraint(item: reportWindow, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1, constant: 0)
let centerY = NSLayoutConstraint(item: reportWindow, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([centerX, centerY])
This is the code for my custom view class
class ReportWindow: UIView{
var uid: String
#IBOutlet weak var title: UILabel?
#IBOutlet weak var textView: UITextView!
#IBOutlet weak var contentView: UIView!
init(user uid: String, frame: CGRect) {
self.uid = uid
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#IBAction func cancel(_ sender: UIButton) {
self.removeFromSuperview()
}
#IBAction func confirm(_ sender: UIButton) {
self.removeFromSuperview()
}
}
extension ReportWindow{
func commonInit() {
let view = loadViewFromNib()
view.frame = bounds
view.autoresizingMask = UIView.AutoresizingMask(rawValue: UIView.AutoresizingMask.RawValue(UInt8(UIView.AutoresizingMask.flexibleWidth.rawValue) | UInt8(UIView.AutoresizingMask.flexibleHeight.rawValue)))
self.addSubview(view)
textView.delegate = self
textView.textAlignment = .center
self.translatesAutoresizingMaskIntoConstraints = false
}
func loadViewFromNib() -> UIView {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "ReportWindow", bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil)[0] as! UIView
return view
}
}
This is what my custom view looks like in storyboard
And this is what my custom view looks like while the application is running, the background is missing, and I cannot interact with the buttons or the text view. I've read that this is because my content is outside of my frame. I want the frame to auto place itself around all my content though, I don't want to have to specify the size and position of the frame.
You're going to have a much easier time with this if you simply present the view inside a modal view controller programatically or using a segue.
Copy your view to a new view controller and present that view controller modally. If you do so using a segue, you will need to set the uid of the view controller inside your preformSegue function. Otherwise, you would set it where you present the view controller. You can handle the button tap inside that view controller.
If that doesn't achieve the look you're going for (a floating view) by default, you can set the new view controller's view to be clear and have a smaller view (your already defined view) anchored to its center.
As a sidenote, I would recommend renaming your class to ReportUserView as it is not a UIWindow. It is a UIView, which is what you want. Windows are a bit different in iOS. You usually only have one and just deal with views and controllers inside it.
I have ViewController that looks like this. There is parent ScrollView that holds UIView (wrapperView) and this wrapper view has TextView and ImageView inside.
This simple structure is created form interface builder with all constraints needed so it works fine.
In some cases I need to programmatically add one more UIView inside my wrapperView right under ImageView (green arrow points there).
So I created xib file for my new UIView and separate class for it
class MyView : UIView {
class func instanceFromNib() -> UIView {
return UINib(nibName: "MyView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! UIView
}
}
And I code of my ViewController I added
let myView = MyView.instanceFromNib()
myView = CGRect(x: 0, y: 0, width: 300, height: 80)
wrapperView.addSubview(myView)
now i see myView on the screen in top of wrapperView but when i try to apply constraints like
NSLayoutConstraint(item: myView, attribute: .top, relatedBy: .equal, toItem: myImageView, attribute: .bottom, multiplier: 1.0, constant: 8).isActive = true
I got
Unable to simultaneously satisfy constraints
What am I doing wrong?
First you need to set this:
myView.translatesAutoresizingMaskIntoConstraints = false
And then you have to make sure that there are no your own constraints that are in conflict. In storyboard you have probably added a constraint that binds imageView.bottomAnchor to wrapperView.bottomAnchor. Now if you put between these two anchors another view, you need to deactivate that constraint.
Otherwise if you try to squeeze myView there, it may result in constraint conflicts - imagine there is a constraint saying that
imageView.bottomAnchor should be 10 points from wrapperView.bottomAnchor,
but there are also two more constraints saying that
imageView.bottomAnchor should be 10 points from myView.topAnchor and myView.bottomAnchor should be 10 points from wrapperView.bottomAnchor.
Obviously both these scenarios cannot be satisfied.
I think, that the easiest solution would be add an UIStackView to the wrapperView in the interface builder with no height, just add needed constraints (top, left, bottom, right). Then drag an IBOutlet of the UIStackView to your ViewController and when you will need to add a subview:
#IBOutlet weak var stackView: UIStackView!
func someFunc() {
let myView = MyView.instanceFromNib()
myView = CGRect(x: 0, y: 0, width: 300, height: 80)
stackView.addArrangedSubview(myView)
}
UIStackView will change its size based on its subviews.
You may use the Debug View Hierarchy to debug your UI issues including the constraints which are failing to be satisfied. Most probably your .top constraint which is added at runtime is conflicting with .top constraint which you have set in your storyboard.
Similar to what the Spotify or Apple Music app does when a song is playing, it places a custom view on top of the UITabBar:
Solutions I've tried:
UITabBarController in a ViewController with a max-sized Container View, and the custom view on top of the Container View49pt above the Bottom Layout Guide:
Problem: Any content in ViewControllers embedded in the UITabBarController constrained to the bottom don't show because they're hidden behind the custom layout. I've tried overriding size forChildContentContainer in UITabBarController, tried updating the bottom layout guide, Nothing. I need to resize the frame of container view of the UITabBarController.
Tried #1 again, but tried solving the problem of content hiding behind it by increasing the size of UITabBar, and then using ImageInset on every TabBarItem to bring it down, and adding my custom view on top of the UITabBar. Hasn't worked really well. There are going to be times when I want to hide my custom view.
UITabBarController as root, with each children being a ViewController with a Container View + my custom view:
But now I have multiple instances of my custom view floating around. If I want to change a label on it, have to change it to all views. Or hide, etc.
Override the UITabBar property of UITabBarController and return my custom UITabBar (inflated it with a xib) that has a UITabBar + my custom view. Problem: Probably the most frustrating attempt of all. If you override that property with an instance of class MyCustomTabBar : UITabBar {}, no tab shows up! And yes, I set the delegate of myCustomTabBar to self.
Leaning towards #3, but looking for a better solution.
This is actually very easy if you subclass UITabBarController and add your view programmatically. Using this technique automatically supports rotation and size changes of the tab bar, regardless of which version you are on.
class CustomTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
//...do some of your custom setup work
// add a container view above the tabBar
let containerView = UIView()
containerView.backgroundColor = .red
view.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// anchor your view right above the tabBar
containerView.bottomAnchor.constraint(equalTo: tabBar.topAnchor).isActive = true
containerView.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
}
I got it!
In essence, I increased the size of the original UITabBar to accomodate a custom view (and to shrink the frame of the viewcontrollers above), and then adds a duplicate UITabBar + custom view right on top of it.
Here's the meat of what I had to do. I uploaded a functioning example of it and can be found in this repo:
class TabBarViewController: UITabBarController {
var currentlyPlaying: CurrentlyPlayingView!
static let maxHeight = 100
static let minHeight = 49
static var tabbarHeight = maxHeight
override func viewDidLoad() {
super.viewDidLoad()
currentlyPlaying = CurrentlyPlayingView(copyFrom: tabBar)
currentlyPlaying.tabBar.delegate = self
view.addSubview(currentlyPlaying)
tabBar.isHidden = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
currentlyPlaying.tabBar.items = tabBar.items
currentlyPlaying.tabBar.selectedItem = tabBar.selectedItem
}
func hideCurrentlyPlaying() {
TabBarViewController.tabbarHeight = TabBarViewController.minHeight
UIView.animate(withDuration: 0.5, animations: {
self.currentlyPlaying.hideCustomView()
self.updateSelectedViewControllerLayout()
})
}
func updateSelectedViewControllerLayout() {
tabBar.sizeToFit()
tabBar.sizeToFit()
currentlyPlaying.sizeToFit()
view.setNeedsLayout()
view.layoutIfNeeded()
viewControllers?[self.selectedIndex].view.setNeedsLayout()
viewControllers?[self.selectedIndex].view.layoutIfNeeded()
}
}
extension UITabBar {
open override func sizeThatFits(_ size: CGSize) -> CGSize {
var sizeThatFits = super.sizeThatFits(size)
sizeThatFits.height = CGFloat(TabBarViewController.tabbarHeight)
return sizeThatFits
}
}
Since iOS 11 this became a little easier. When you add your view, you can do the following:
viewControllers?.forEach {
$0.additionalSafeAreaInsets = UIEdgeInsets(
top: 0,
left: 0,
bottom: yourView.height,
right: 0
)
}
Your idea to put it in a wrapper viewcontroller is good, but it will only cause overhead (more viewcontrollers to load in memory), and issues when you want to change the code later on. If you want the bar to always show on your UITabBarController, then you should add it there.
You should subclass UITabBarController and load the custom bar from a nib. There you will have access to the tabbar (so you can place your bar correctly above it), and you will only load it in once (which solves your problem that you will face having a different bar on each tab).
As for your views not reacting to the size of the custom bar, I don't know how you can do that, but my best suggestion is to use a public variable and notifications that you listen to in your individual tabs.
You can then use that to change the bottom constraint.
Besides playing with UITabBar or container vc, you could also consider adding the view in the App Delegate to the main window like in following post:
View floating above all ViewControllers
Since your view is all around along with the Tab bar, it is totally ok to make it in the App Delegate.
You can always access the Floating view from App Delegate Singleton by making it a property of the App Delegate. It is easy then to control its visibility in anywhere of your code.
Changing constant of the Constraints between the Floating view and super view window can adjust the position of the view, thus handsomely respond to orientation changes.
Another(similar) approach is to make the floating view another window like the uid button.
Unless I've misunderstood, you could create a custom view from your UITabBarController class. You can then insert it above and constrain it to the tabBar object, which is the tabBar associated with the controller.
So from your UITabBarController class, create your custom view
class CustomTabBarController: UITabBarController {
var customView: UIView = {
let bar = UIView()
bar.backgroundColor = .white
bar.translatesAutoresizingMaskIntoConstraints = false
return bar
}()
In viewDidLoad() add your custom view to the UITabBarController's view object and place it above the tabBar object
override func viewDidLoad() {
super.viewDidLoad()
...
self.view.insertSubview(customView, aboveSubview: tabBar)
Then after your custom view is added as a subView, add constraints so it's positioned correctly. This should also be done in viewDidLoad() but only after your view is inserted.
self.view.addConstraints([
NSLayoutConstraint(item: customView, attribute: .leading, relatedBy: .equal, toItem: tabBar, attribute: .leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: customView, attribute: .trailing, relatedBy: .equal, toItem: tabBar, attribute: .trailing, multiplier: 1, constant: 0),
NSLayoutConstraint(item: customView, attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: -50),
NSLayoutConstraint(item: customView, attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0)
])
There's a bunch of creative ways you can setup constraints to do what you want, but the constraints above should attach a view above your tabBar with a height of 50.
Make the view's frame with the height of tab bar and brings it to top, 2. set tabBar hidden is true.
I have two subviews that I created, one in Storyboard one with code, I want to anchor the second view (created with code) to the first view (in storyboard) with some constraints so that the second view sits below the first view:
class ViewController: UIViewController{
#IBOutlet weak var view1: UIView!
override func viewDidLoad(){
super.viewDidLoad()
setUp()
}
func setUp(){
var view2 = UIView()
view2.setTranslatesAutoresizingMaskIntoConstraints(false)
view2.frame = CGRectMake(10,10,10,10)
self.view.addSubview(view2)
self.view.addConstraint(NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.TopMargin, relatedBy: NSLayoutRelation.Equal, toItem: view2, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 10))
}
}
The problem is that I got an error saying When added to a view, the constraint's items must be descendants of that view. Is it bad practice to have some views in storyboard and others in code?
You have a few problems.
First, the error means that view1 and view2 are not both part of self.view's view hierarchy. This is probably because view1 hasn't been decoded from the storyboard yet. Try moving this code from viewDidLoad() to awakeFromNib(), which is when they're guaranteed to have been loaded.
Second, you're trying to set the view's frame:
view2.frame = CGRectMake(10,10,10,10)
This frame will get overwritten by the layout engine making it pointless. Delete this line.
If you're going to use auto-layout, you need to unambiguously specify both the size (height & width) and position (x & y) of the view. The constraint you added only specifies the y-origin, so you also need to add more constraints (probably 3 more) to specify the x-origin, the width, and the height, which are currently undefined.
Is it bad practice to have some views in storyboard and others in code?
No, it's common.
Ok so I'm struggling here and haven't been able to find a working solution. I've been self learning Swift without Objective C experience (I know, I know).
In my app, I have my main UIViewController, a subview that is transparent but slides in from the bottom of the screen, and then 4 subviews of the sliding subview that are all working UIScrollViews. I have paging enabled and it works great but I'd like to add a UIPageControl for each of them. I seriously can't grasp delegates and how to implement the using swift. Any help would be much appreciated!
Also, I'm doing this all programmatically, so no IB please. Happy to provide code if it'll help. Thanks
I think you and/or anyone else looking for how to do this will find this answer helpful. The code example enabled me to create a page control indicator on my scrollView, and it was the first time attempting to do this. I found it very clear.
The lines you probably need to add to your project are:
1: add UIScrollViewDelegate as a protocol when you first name your view controller class.
2: in the class declaration create a pageControl variable. You will need to play with the frame numbers to get it to appear where you want it. the current numbers made one in the middle of the window for me.
For reference the numbers mean (x position for top left corner of indicator, y coordinate for top left corner, width of page indicator, height of page indicator)
var pageControl : UIPageControl = UIPageControl(frame: CGRectMake(50, 300, 200, 20))
in viewDidLoad set the scrollView delegate and call `configurePageControl():
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
configurePageControl()
}
you need to add two methods after viewDidLoad. one is called in viewDidLoad
func configurePageControl() {
self.pageControl.numberOfPages = <some reference to the number of pages>
self.pageControl.currentPage = 0
self.pageControl.tintColor = UIColor.redColor()
self.pageControl.pageIndicatorTintColor = UIColor.blackColor()
self.pageControl.currentPageIndicatorTintColor = UIColor.greenColor()
self.view.addSubview(pageControl)
}
and
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
pageControl.currentPage = Int(pageNumber)
}
The scrollView delegate is actually very simple to set up. Add UIScollViewDelegate as a protocol that your ViewController class will implement by adding it after the class declaration: class YourClassName: UIScrollViewDelegate. And then in viewDidLoad(), you complete the delegate setup by assigning the scroll view's delegate property to your class with the line scrollView.delegate = self. (again see the example I linked for if you need further clarification of where these commands go)
Just setup it in code like this:
private var pageControl = UIPageControl(frame: .zero)
private func setupPageControl() {
pageControl.numberOfPages = controllers.count
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.currentPageIndicatorTintColor = UIColor.orange
pageControl.pageIndicatorTintColor = UIColor.lightGray.withAlphaComponent(0.8)
let leading = NSLayoutConstraint(item: pageControl, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1, constant: 0)
let trailing = NSLayoutConstraint(item: pageControl, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1, constant: 0)
let bottom = NSLayoutConstraint(item: pageControl, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0)
view.insertSubview(pageControl, at: 0)
view.bringSubview(toFront: pageControl)
view.addConstraints([leading, trailing, bottom])
}
UIPageControl Integration using Swift 4
func configurePageControl() {
self.pageview.numberOfPages = items.count
self.pageview.currentPage = 0
self.pageview.tintColor = UIColor.red
self.pageview.pageIndicatorTintColor = UIColor.black
self.pageview.currentPageIndicatorTintColor = UIColor.green
}
Delegates are just methods of a class, that can be palmed off to another class. These methods are usually callbacks.
e.g. A callback for a TextField, when the user hits return, can be implemented in a Delegate. The delegate can be implemented in the ViewController class.
Now when the user hits return the TextField Object will call the delegate method in the ViewController object. The great thing about this is, you can access all the variables and methods of the ViewController object from the delegated method. Otherwise you would need a handle to the ViewController object within the TextField object itself.
Delegates are implemented as protocols, which are just interfaces. So if the ViewController implements the TextFieldDelegate protocol, all your textfield callbacks can be called from within the ViewController object.
I hope this helps.