Mixing storyboard and constraints in code - ios

I'm trying to implement view controller from this wireframe
As we have different screen resolutions - different number of yellow views can fit on the screen. All yellow views are same size UIImageViews, containing the same image.
What I'm trying to do is to layout view controller in storyboard except the yellow views. Then I calculate number of yellow views screen can fit and add them using auto layout in code. Like this:
let numberOfViewsPerSide = numberOfViewsOnEachSide()
let viewImage = UIImage (named: "repeat_image")
var prevView = centerImageView
if let viewImage = viewImage {
for index in 0...(numberOfViewsPerSide - 1) {
var newView = UIImageView(image: viewImage)
newView.setTranslatesAutoresizingMaskIntoConstraints(false)
let views = ["prevView" : prevView, "newView" : newView]
self.view.addSubview(newView)
let constraint_H = NSLayoutConstraint.constraintsWithVisualFormat("H:[newView]-15-[prevView]", options: NSLayoutFormatOptions.AlignAllCenterY, metrics: nil, views: views)
self.view.addConstraints(constraint_H)
prevView = newView
}
}
The problem is that in viewDidLoad and viewWillAppear methods auto layout isn't triggered yet. So centerImageView doesn't have right size and position. Moreover its superview is nil at this point. So I basically can't add constraint to view without parent.
My code works in viewDidAppear method but that means that all yellow views will appear only after transition animation is completed.
Do you have any idea how to implement that without doing everything in storyboard or purely in code. UILabel, UIButton on the picture positioned deleted to the green centerView - if I add centerView in code I need to set all constraints for this screen in code.
UPDATE:
Solution offered by #pteofil worked for me
so I call code above in viewWillAppear like that:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.view.layoutIfNeeded()
addRepeatedImages()
}

You can use layoutIfNeeded to get the autolayout triggered and have your views the correct size.
But your yellow views almost looks like you could use a UICollectionView to manage them...

You can embed all the yellow views in a container view you position in interface builder. Now you can do all the stuff inside this view in code.

Related

What is a good way to add UIPageViewController to parent UIViewController without considering status bar height?

Currently, I have a UIViewController, with its top component consists of a horizontal UICollectionView (MenuTabsView.swift)
Now, I would like to add a UIPageViewController, just below the MenuTabsView.
I have tried the following few approaches.
Programatically without taking status bar height into consideration
func presentPageVCOnView() {
self.pageController = storyboard?.instantiateViewController(withIdentifier: "PageControllerVC") as! PageControllerVC
self.pageController.view.frame = CGRect.init(x: 0, y: menuBarView.frame.maxY, width: self.view.frame.width, height: self.view.frame.height - menuBarView.frame.maxY)
self.addChildViewController(self.pageController)
self.view.addSubview(self.pageController.view)
self.pageController.didMove(toParentViewController: self)
}
Here's the outcome.
From 1st glance, it seems that UIPageViewController's view need to offset by Y status bar distance. (But why?)
Programatically by taking status bar height into consideration
func presentPageVCOnView() {
let statusBarHeight = CGFloat(20.0)
self.pageController = storyboard?.instantiateViewController(withIdentifier: "PageControllerVC") as! PageControllerVC
self.pageController.view.frame = CGRect.init(x: 0, y: menuBarView.frame.maxY + statusBarHeight, width: self.view.frame.width, height: self.view.frame.height - menuBarView.frame.maxY - statusBarHeight)
self.addChildViewController(self.pageController)
self.view.addSubview(self.pageController.view)
self.pageController.didMove(toParentViewController: self)
}
Now, it looks way better.
Use container view without status bar offset
But, I don't feel comfortable, on why we need to manually consider status bar height, during programatically way. I was thinking, maybe I can add a ContainerView to UIViewController, and "attach" the UIPageViewController's view to it?
(I am not sure why during adding Container View to storyboard, an additional UIViewController will be added along. Anyhow, I just manually delete the additional UIViewController)
Then, I use the following code to "attach" the UIPageViewController's view to new container view.
func presentPageVCOnView() {
self.pageController = storyboard?.instantiateViewController(withIdentifier: "PageControllerVC") as! PageControllerVC
self.pageController.view.frame = containerView.frame
self.addChildViewController(self.pageController)
self.view.addSubview(self.pageController.view)
self.pageController.didMove(toParentViewController: self)
}
But, the outcome is not what as expected. Y offset still happen!!!
Use container view with status bar offset
I try to make sure, there are space of 20, between the top component MenuTabsViews and UIPageViewController's view.
I was wondering, is there any good practice/ solution, to ensure we can add UIPageViewController's view below another component, without affecting by status bar height?
You can do this all without any code -- it just takes an understanding of how UIContainerView works.
There's no real UIContainerView class... it is an automated way of adding a child view controller via Storyboard / Interface Builder. When you add a UIContainerView, IB automatically creates a "default" view controller connected to the container view with an Embed segue. You can change that default controller.
Here's step-by-step (images are large, so you'll probably want to click them to see the details)...
Start with a fresh UIViewController:
Add your "Menu Bar View" - I have it constrained Top/Leading/Trailing to safe-area, Height of 60:
Drag a UIContainerView onto the view - note that it creates a default view controller at the current size of the container view. Also note that it shows a segue. If you inspect that segue, you'll see it is an Embed segue:
Constrain the Top of the container view to the Bottom of your Menu Bar View, and Leading/Trailing/Bottom to safe-area. Notice that the size of the embedded view controller automatically takes the new size of the container view:
Select that default controller... and delete it:
Drag a new UIPageViewController onto your Storyboard and set its Custom Class to PageControllerVC:
Now, Ctrl-Click-Drag from the Container view to the newly added page view controller. When you release the mouse button, select Embed from the popup:
You now have an Embed segue from the container view to your page view controller. Notice that it automatically adjusted its size to match the container view size:
Since the Menu Bar View top is constrained to the safe-area, it will behave as expected.
Since the container view top is constrained to the bottom of the Menu Bar View, it will stay there, and should give you what you want.
No Code Needed :)
Edit
The most likely reason you ran into trouble with loading via code is with you frame setting.
If you try to set frames in viewDidLoad(), for example, auto-layout has not configured the rest of the view hierarchy... so framing will not be what you expect.
You're much better off using auto-layout / constraints, rather than setting explicit frames anyway.
Here is how I would do it from code (assumes you have your "Menu Bar View" connected via #IBOutlet):
class ViewController: UIViewController {
#IBOutlet var menuBarView: UIView!
var pageControllerVC: PageControllerVC?
override func viewDidLoad() {
super.viewDidLoad()
guard let vc = storyboard?.instantiateViewController(withIdentifier: "PageControllerVC") as? PageControllerVC else {
fatalError("Could not instantiate PageControllerVC!!!")
}
guard let v = vc.view else {
fatalError("loaded PageControllerVC had no view ????")
}
addChild(vc)
view.addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: menuBarView.bottomAnchor),
v.leadingAnchor.constraint(equalTo: g.leadingAnchor),
v.trailingAnchor.constraint(equalTo: g.trailingAnchor),
v.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
vc.didMove(toParent: self)
self.pageControllerVC = vc
}
}
You should remove safeArea pinning for pageVC.
Safe area includes status bar and iPhone 11+ top space
tabBar.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor)
// to this
tabBar.topAnchor.constraint(equalTo: self.view.topAnchor)
And in storyboards change Safe Area to SuperView

Fitting a UIScrollView to a UIView

I am trying to use a UIScrollView to show a series of UIViews. In my storyboard I have a View Controller containing a UIView that is constrained using AutoLayout.
View Controller (UIView in grey)
In order to call the UIScrollView I am using the following method:
func initScrollview() {
self.mainScrollView = UIScrollView(frame: self.mainView.bounds)
self.mainScrollView!.contentSize = CGSizeMake((self.mainView.bounds.width)*CGFloat(3), self.mainView.frame.height)
self.mainScrollView!.backgroundColor = UIColor.greenColor() // For visualization of the UIScrollView
self.mainScrollView!.pagingEnabled = true
self.mainScrollView!.maximumZoomScale = 1.0
self.mainScrollView!.minimumZoomScale = 1.0
self.mainScrollView!.bounces = false
self.mainScrollView!.showsHorizontalScrollIndicator = true;
self.mainScrollView!.showsVerticalScrollIndicator = false;
self.mainScrollView!.delegate = self
for i in 0...3 {
var tempView = SubView(frame: self.mainView.bounds)
pages.insert(tempView, atIndex: i)
self.mainScrollView!.addSubview(pages[i]);
}
self.mainScrollView!.scrollRectToVisible(CGRectMake(mainScrollView!.frame.size.width, 0, mainScrollView!.frame.size.width, mainScrollView!.frame.size.height), animated: false)
}
When I run my code, the UIScrollView does not fit the UIView. Instead it is too short and too wide. The result looks like this:
UIView in grey, UIScrollView in green
What am I doing wrong that is causing the UIScrollView to be incorrectly sized?
You should put the codes that you init the UI element sizes base on the screen size(UIView of UIViewController) in viewDidLayoutSubviews. Because in viewDidLoad, the screen didn't adjust its size yet,
In the above code, there no mention of adding the mainScrollView to the mainVew.
To whose view are you adding the mainScrollView? My opinion would be you are trying to add it to self.view whereas it should be to self.mainView
After the initScrollView() function is called try adding it the below code
self.mainView.addSubview(mainScrollView!)
This would probably be easier if you had placed all these views directly in your storyboard instead of programatically. I don't see anything in your code that can't be done visually in IB. Also, if you have autoLayout active in your storyboard, setting frames and sizes in code won't work. (auto-layout will change your values on the next pass)

UIView Not Changing Size On Rotation Autolayout

I have a UIScrollView with a UIView for content inside of it. On the UIView, I have some buttons I'm using for a menu. Everything works great until I rotate to landscape. Once I rotate, the buttons on the UIView can't be clicked but the top buttons still work. I'm assuming it's because the UIView holding the buttons isn't being resized so it's not responding to taps. This can be seen in the 3d exploded image (Image 4). I have tried all of these lines of code to try to get it to resize, buttons have worked (code is commented because I tried different combos).
override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) {
//hides and displays tab bar on orientation changes
if toInterfaceOrientation.isLandscape {
tabBarController!.tabBar.hidden = true
//self.navigationController!.view.sizeToFit()
//self.navigationController!.view.setNeedsLayout()
//self.viewForContent.setNeedsLayout()
//self.viewForMenuItems.setNeedsLayout()
//self.viewForContent.contentSize = CGSizeMake(900, 900)
//self.viewForContent.setNeedsLayout()
}
else if toInterfaceOrientation.isPortrait {
tabBarController!.tabBar.hidden = false
//self.navigationController!.view.setNeedsLayout()
//self.viewForContent.setNeedsLayout()
//self.viewForMenuItems.setNeedsLayout()
//self.viewForContent.contentSize = CGSizeMake(900, 900)
//self.viewForContent.setNeedsLayout()
}
}
Also I have this in the DidLoad and DidLayoutSubviews. When I comment out the DidLayoutSubviews the buttons work again, but the scrollview isn't large enough to allow me to scroll to the bottom buttons when in landscape
override func viewDidLoad() {
super.viewDidLoad()
let size = UIScreen.mainScreen().bounds.size
viewForContent.contentSize = CGSizeMake(size.width, 458)
// Do any additional setup after loading the view.
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let size = UIScreen.mainScreen().bounds.size
viewForContent.contentSize = CGSizeMake(size.width, 458)
}
The My Information, My Staff, My Colleagues, and My Residents are the buttons that work in landscape mode.
I have used autolayout constraints to setup the views. Also, I am using XCode7 with Swift.
Any help would be appreciated
Thanks
First I setup my views as outlined in the link posted above in the comments by Kusul (Thank you). But when I did this and added the buttons to the content view they wouldn't show on when the screen was rotated to landscape because they were off of the bottom and the UIScrollView didn't "grow".
I figured out that I needed to add a bottom constraint to my last button. So the last button now has a constraint to the top and the bottom. This forced the scroll view to "grow" to the proper size. I could then eliminate setting the size in the didLoad and didLayoutSubviews. Thanks for the help

adding a uipageviewcontroller to a uistackview

I'm trying to add a sliding photo gallery functionality to the top portion of a view.
To give context, a user taps on a button or row or something. Then i load a scrollview with a uistackview in it. organized vertically, i had an image, and then another stack view with some text in it. Now, i want that image to become part of a larger "gallery". My research told me to implement UIPageviewcontroller and add the other images to a childVC.
i used this as a tutorial (the first example): http://www.raywenderlich.com/76436/use-uiscrollview-scroll-zoom-content-swift
the only relevant deviation from the tutorial my app has is that it creates things programmatically.
With my proof of concept for the gallery functionality, i wanted to integrate it with the previously mentioned stack view. my plan was to first add the pageviewcontroller stuff into the overall stack view with the original image view right below it and then simply remove the original image view to leave me the final product.
i was able to add the pageviewcontroller.view to the stackview, but the gallery doesn't show. taking a look at the UI Inspector, i can see that the gallery is kinda loaded, but it's messed up.
it's as if the uiview has a frame of 0 height and so the other stack view items don't respect the images that the pageviewcontroller is trying to show.
I think it could be that stack views can only handle specific views, not stuff as complicated as pageviewcontrollers.
also note: my implementation is all programmatic, no storyboards, and so for no xibs. so maybe i missed something here.
here is some code, if it helps:
note the constrain functions you see are from the "cartography" pod
this adds the "gallery" to the stack view, it's a delegate function from my view
func addZoomStuff(sender: UIStackView) {
let zoomer = PageBaseViewController()
addChildViewController(zoomer)
zoomer.view.translatesAutoresizingMaskIntoConstraints = false
zoomer.view.tag = 5
sender.addArrangedSubview(zoomer.view)
zoomer.didMoveToParentViewController(self)
}
this is what creates the scrollview, image view, etc for the gallery items:
override func viewDidLoad() {
super.viewDidLoad()
//MARK: - Zoom View Elements
// prep
scrollView = UIScrollView(frame: self.view.frame)
scrollView.delegate = self
self.view.addSubview(scrollView)
constrain(scrollView, view) { view, view2 in
view.edges == view2.edges
}
self.view.setNeedsUpdateConstraints()
// 1
let image = UIImage(named: imageName)!
imageView = UIImageView(image: image)
// 2
scrollView.addSubview(imageView)
constrain(imageView){ view in
view.edges == view.superview!.edges
}
scrollView.contentSize = image.size
i tried adding the constraints like this but there was no effect
func addZoomStuff(sender: UIStackView) {
let zoomer = PageBaseViewController()
addChildViewController(zoomer)
view.addSubview(zoomer.view)
constrain(zoomer.view, view) { view, view2 in
view.width == view2.width
view.height == view2.height * 2 / 3
view.leading == view2.leading
view.top == view2.top
}
zoomer.view.setNeedsUpdateConstraints()
zoomer.view.removeFromSuperview()
zoomer.view.translatesAutoresizingMaskIntoConstraints = false
zoomer.view.tag = 5
print("sender.subviews: \(sender.subviews)")
sender.addArrangedSubview(zoomer.view)
zoomer.didMoveToParentViewController(self)
print("sender.subviews: \(sender.subviews)")
}
if this method isn't going to work, can i do a nested horizontal stack view instead of the pageviewcontroller and somehow get that same scrolling/snap effect to see on image view at a time?
TLDR;
Create a subclass of UIPageViewController, make it it's own delegate.
Initialize the subclass with a plain UIViewController, only set a backgroundcolor.
In the pageviewcontroller subclass, implement the two delegate callbacks for a next and previous viewcontroller: create a plain viewcontrolller, with some random backgroundcolor.
If this works, replace the plain viewcontroller by your actual contentviewcontroller.
Long version:
Have you seen this: Maybe this link will help: https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/ViewControllerCatalog/Chapters/PageViewControllers.html
It might help, as it explains the details of UIPageViewController. Basically, you need to create a viewController (not a view!), that shows one page of the gallery. So this VC has a stackview, and manages the content of it. The pageviewcontroller is initialized with your first contentviewController. If you create a subclass of the uipageviewcontroller, you can set self of that subclass as the delegate of it. Implement the delegate callbacks that return the next or previous viewController and thats it. For this last part, it is convenient to have a property on the contentviewcontroller from which the subclasses of the pageviewcontroller can figure out what data to set on the next or previousviewcontroller.
Your title seeks to hint at some confusion: its not possible to add a viewcontroller to a view. You can only add other views to a (stack)view. A viewcontroller owns and manages a viewhierarchy. A pageviewcontroller has no content, but manages the insertion and removal of viewcontrollers. as the pageviewcontroller is a containerviewcontroller, it will als take the contentViewcontrollers' views and place them in the viewhierarchy. But this is not something your code has to do when you subclass UIPageViewControlller and implement it's delegates on itself (and don't forget to assign self to be the delegate).

Autolayout with Navigation Bar - Remove Specific Constraint

I'm building an application for iOS, which is using a navigation controller. I want to put a view in the titleView, and have it fill the whole width of the navigation bar.
I'm calling setupNavBar in viewDidLoad of the view controller that is embedded in the navigation controller. Here is how I do:
func setupNavBar() {
let navBar = navigationController?.navigationBar
// navBar!.translatesAutoresizingMaskIntoConstraints = false
// navBar!.frame.size.height = CGFloat(100)
let searchBar = UIView(frame: navBar!.frame)
searchBar.bounds = CGRectMake(0, 0, navBar!.frame.width, navBar!.frame.height)
searchBar.backgroundColor = UIColor.brownColor()
navigationItem.titleView = searchBar
}
But the view (brown - "searchBar"), doesn't cover the full navigation bar:
So I figured out that the problem was related to Autoresizing and Constraints, because if I call navBar!.translatesAutoresizingMaskIntoConstraints = false, I can freely set the sizes of views frame, so there must be some constraints that change the view's bounds. But I would like to keep as much of the autolayout behaviour as possible.
Is there a way to only change the contraints on the titleView?
I'm doing everything programmatically, I don't use the storyboard or xib's!
EDIT:
But it doesn't seem like there is any constraints on either navigationItem or navBar:
for someObject in navigationItem.titleView!.constraints {
print(someObject)
}
It doesn't print any constraints. neither if I use navBar.constraints!
EDIT 2:
I have a screenshot from "View UI Hierarchy" from the debug navigator:
It seems that the view(brown) alligns with the Navigation Bar Back Indicator View, maybe this is a clue to what causes the problem?
How come the view is resized?
Two different suggestions here:
1. You can try the
navigationItem.titleView.sizeToFit()
Otherwise you could set the background colour of the navbar to brown as it appears you wish the brown bar to cover the entire width of the navbar. If you want to add other views on top of that you then can.
You could also try to make an outlet to the title view and add an NSLayoutConstraint using
navigationItem.titleView.addConstraint(NSLayoutConstraint: NSLayoutConstraint)
I am not entirely sure whether that will work, however.

Resources