So I am in a mildly complicated situation,
The issue: There is a thin blue line that goes across my Custom UITableViewSectionHeader only when under navigation bar that I do not know where is coming from:
I have:
A tableview nested inside a UIViewController
A GradientView (inherits UIView) directly under my NavigationBar
A TableView that overlaps my GradientView
A Custom TableViewSectionHeader Class (Subclass UITableViewCell)
[
My theory:
That border line is either from:
- The bottom border of navigation bar
- The top border of tableview
- Maybe a separator from the Section header (but seems unlikely)
- A bottom border from the GradientView
Anybody have an idea what could be causing that line?
I have tried to remove it with:
ViewController:
override func viewDidLoad() {
// self.tableview.separatorStyle = .none
// self.tableview.layer.borderWidth = 0
// self.view.layer.borderWidth = 0
}
GradientView:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.borderWidth = 0
}
SectionHeader:
self.separatorInset.left = 1000
self.layer.borderWidth = 0
Any thoughts?
Navigation bars has an image view with a line which is somewhere between less than or equals to 1px you have to loop through NavigationController navigationBar to find that imageView and set that to hidden.
You can directly loop on navigationBar and find all subViews or if you want to have a reference to the view then here how I would have done it.
var lineImageView: UIImageView? = { [unowned self] in
// guard is great try to use it whenever you can
guard let navigationBar = self.navigationController?.navigationBar else {
return nil
}
return self.findLineImageView(for: navigationBar)
}()
now add this function which loops through till it finds an imageView and return it back to our lineImageView
// remember even **navigationBar** is a UI remember **UINavigationBar**
func findLineImageView(for view: UIView) -> UIImageView? {
// as I said above the line is not more than 1px so we look for a view which is less than or equals to 1px in height
if view is UIImageView && view.bounds.size.height <= 1 {
return (view as! UIImageView)
}
// we loop till we find the line image view and return it
for subview in view.subviews {
if let imageView = findLineImageView(for: subview) {
return imageView
}
}
// if there is no imageView with that height we return nil that's why we return an optional UIImageView
return nil
}
Now the magic part. in viewWillApear set the lineImageView to hidden
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// remember as I said the lineImageView we returned an optional that's why it has question mark which means we are safe
lineImageView?.isHidden = true
}
Related
I notice that, if I perform add/ expand animation within an UIScrollView, it will cause unwanted scrolling behavior, when the UIScrollView fill with enough content to become scroll-able.
As you can see in the following animation, initially, the add/ expand animation works just fine.
When we have added enough item till the UIScrollView scrollable, whenever a new item is added, and UIScrollView will first perform scroll down, and then scroll up again!
My expectation is that, the UIScrollView should remain static, when add/ expand animation is performed.
Here's the code which performs add/ expand animation.
Add/ expand animation
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
// Clear off horizontal swipe in animation caused by addArrangedSubview
stackView.superview?.layoutIfNeeded()
customView.show()
// Perform expand animation.
UIView.animate(withDuration: 1) {
self.stackView.superview?.layoutIfNeeded()
}
}
Here's the constraint setup of the UIScrollView & added custom view item
Constraint setup
Custom view
class CustomView: UIView {
private var zeroHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.cornerRadius = stackView.frame.height / 2
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
zeroHeightConstraint = self.safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 0)
zeroHeightConstraint.isActive = false
}
func hide() {
zeroHeightConstraint.isActive = true
}
func show() {
zeroHeightConstraint.isActive = false
}
}
Here's the complete source code
https://github.com/yccheok/add-expand-animation-in-scroll-view
Do you have any idea why such problem occur, and we can fix such? Thanks.
Because of the way stack views arrange their subviews, animation can be problematic.
One approach that you may find works better is to embed the stack view in a "container" view.
That way, you can use the .isHidden property when adding an arranged subview, and allow the animation to update the "container" view:
The "add view" function now becomes (I added a Bool so we can skip the animation on the initial add in viewDidLoad()):
func addCustomView(_ animated: Bool) {
let customView = CustomView.instanceFromNib()
stackView.addArrangedSubview(customView)
customView.isHidden = true
if animated {
DispatchQueue.main.async {
UIView.animate(withDuration: 1) {
customView.isHidden = false
}
}
} else {
customView.isHidden = false
}
}
And we can get rid of all of the hide() / show() and zeroHeightConstraint in the custom view class:
class CustomView: UIView {
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
borderView.layer.cornerRadius = borderView.bounds.height * 0.5
}
}
Since it's a bit difficult to clearly show everything here, I forked your project with the changes: https://github.com/DonMag/add-expand-animation-in-scroll-view
Edit
Another "quirk" of animating a stack view shows up when adding the first arranged subview (also, when removing the last one).
One way to get around that is to add an empty view as the first subview.
So, for this example, in viewDidLoad() before adding an instance of CustomView:
let v = UIView()
stackView.addArrangedSubview(v)
This will make the first arranged subview a zero-height view (so it won't be visible).
Then, if you're implementing removing custom views, just make sure you don't remove that first, empty view.
If your stack view has .spacing = 0 noting else is needed.
If your stack view has a non-zero spacing, add another line:
let v = UIView()
stackView.addArrangedSubview(v)
stackView.setCustomSpacing(0, after: v)
I did a little research on this and the consensus was to update the isHidden and alpha properties when inserting a view with animations.
In CustomView:
func hide() {
alpha = 0.0
isHidden = true
zeroHeightConstraint.isActive = true
}
func show() {
alpha = 1.0
isHidden = false
zeroHeightConstraint.isActive = false
}
In your view controller:
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
self.stackView.layoutIfNeeded()
UIView.animate(withDuration: 00.5) {
customView.show()
self.stackView.layoutIfNeeded()
}
}
Also, the constraints in your storyboard aren't totally correct. You are seeing a red constraint error because autolayout doesn't know the height of your stackView. You can give it a fake height and make sure that "Remove at build time" is checked.
Also, get rid of your scrollView contentView height constraint defined as View.height >= Frame Layout Guide.height. Autolayout doesn't need to know the height, it just needs to know how subviews inside of the contentView stack up to define its vertical content size.
Everything else looks pretty good.
I'm working with a UITableViewController which when scrolling makes the navigationBar disappear. Now when the navigation bar is hidden when the user swipes the table view the contents of the cells are seen below the status bar ...
To solve this problem I tried to insert a UIView to simulate a background of the status bar and everything works but the problem is that when I close the UITableViewController the background view of the status bar is not removed from the superview
For now my code is this, can you help me understand where I am wrong? why can't I remove the UIView from the superview?
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setupStatusBarView()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.isHidden = true
UIApplication.shared.windows.first?.viewWithTag(1)?.removeFromSuperview()
}
//MARK: - Setup Status Bar View
func setupStatusBarView() {
let height = view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
let statusBarView = UIView()
statusBarView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height:height+5)
statusBarView.backgroundColor = .systemBackground
statusBarView.tag = 1
UIApplication.shared.windows.first?.addSubview(statusBarView)
}
viewDidLayoutSubviews get calls multiple times and you have put setupStatusBarView() in viewDidLayoutSubviews that means your background view has been added multiple times and this is totally wrong flow!
You are removing topmost view only not previous ones!
You should set frame in viewDidLayoutSubviews and should add the view from viewDidLoad!
try this one
let subviewArray = UIApplication.shared.windows.first?.subviews
for view in subviewArray!{
if view.tag == 1{
view.removeFromSuperview()
}
}
I'm working on my view and I'm having an issue with getting a shadow around a button within the stack view. Most of the work I have done has been within the storyboard directly.
Here is the method I am using to apply the shadow to the view
func addShadow(to view: UIView) {
view.layer.shadowColor = shadowColor
view.layer.shadowOpacity = shadowOpacity
view.layer.shadowOffset = shadowOffset
if let bounds = view.subviews.first?.bounds {
view.layer.shadowPath = UIBezierPath(rect: bounds).cgPath
}
view.layer.shouldRasterize = true
}
and this is how I'm finding the button within the view from ViewController.swift
for subview in self.view.subviews {
if subview.isKind(of: UIButton.self) && subview.tag == 1 {
addShadow(to: subview)
}
}
I know the problem stems from the stack view and the UIView inside of the stack view that holds the button. (self.view > UIStackView > UIView > [UIButton, UILabel])
I know I could do this with recursion in the for-loop but I'm trying to be a little more precise to optimize performance and would prefer to add the shadows in one shot.
You have a few options:
add the shadow in the storyboard itself
add an outlet to the button, then add shadow in code
add the button to a collection, then enumerate over the collection adding shadows
recursively add the shadows (this isn't going to hit performance nearly as hard as you're thinking, adding the shadows hurts performance more than doing this recursively)
You are correct in that the button is a view on the stack view, so your for loop doesn't hit the button directly to add a shadow to it.
The easiest way to solve this is by far the recursive way, or something like this:
func addShadowsTo(subviews: [UIView]) {
for subview in subviews {
if subview.isKind(of: UIButton.self) && subview.tag == 1 {
addShadow(to: subview)
}
if let stackView = subview as? UIStackView {
addShadowToSubviews(subviews: stackView.subviews)
}
}
}
func viewDidload() {
super.viewDidLoad()
addShadowsTo(subviews: view.subviews)
}
If you want some instructions on how to do any of the other ways, just comment.
I want to make UI like following,
I tried to increase the height of UINavigationController like
[self.navigationController.navigationBar setFrame:CGRectMake(0, 0, 320, 110)];
but navigation title and BarButtonItem appears down.
i can not add hide navigation as i am using third party side menu which uses Left BarButtonItem.
i can very well keep navigation as it is and below i have added uiview as subview with same background colour as that of navigation but there is fine line appears in between...:(
You can't increase height of UINavigationBar. Add an UIView with those elements just below the UINavigationBar.
Then there will be extra line below UINavigationBar. You can remove that line. Refer How to hide iOS7 UINavigationBar 1px bottom line
or else use this code(I have just copied it from that link in swift, convert it to Objective-C)
import UIKit
class ViewController: UIViewController {
private var shadowImageView: UIImageView?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if shadowImageView == nil {
shadowImageView = findShadowImage(under: navigationController!.navigationBar)
}
shadowImageView?.isHidden = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
shadowImageView?.isHidden = false
}
private func findShadowImage(under view: UIView) -> UIImageView? {
if view is UIImageView && view.bounds.size.height <= 1 {
return (view as! UIImageView)
}
for subview in view.subviews {
if let imageView = findShadowImage(under: subview) {
return imageView
}
}
return nil
}
}
This is the result
Actually you cant change height of UINavigationBar. Your actual ViewController's view will start from 64 pixel from TopLayoutGuide (Vertical Space)
Status Bar Height is 20 Pixel
Navigation Bar Height is 44 Pixel
So to design such UI, simply do these steps
Hide NavigationBar on that view controller like
self.navigationController.navigationBarHidden = YES;
self.navigationItem.hidesBackButton = YES;
Then add View from storyboard with vertical space as 0 from TopLayoutGuide. And design it as per your requirement. So you can
simply give this look and handle it using IBOutlets and IBActions
for each item
Also you can add UILable to show title of screen.
You can't increase the size of the UINavigationBar. That is of standard size 44 (Nav Height) + 20 (size of the status bar). Here you've two options to achieve.
First, by adding menu, dashboard title label & notification icon to the UINavigationBar. Add Jobs, Conditions, and Interview as a separate view below to navigation bar.
Second by customizing the entire view, as mentioned by #Pushkraj i.e, by hiding the current Navigation Bar and adding your own view which contains all the UI elements.
I am trying to create a UITableViewCell subclass containing two rounded views, one on top and one on bottom, that together end up as a rounded rectangular view inside the cell, with indented space on all 4 sides (set by auto layout constrains in the storyboard for the prototype cell). These cells are part of a tableview that is loaded into a UIContainerView which has its contents swapped out based on the selection of a selection control.
Here is what I want the cell to look like (blacked out):
Here is what it looks like briefly, when first loading:
Here is what it looks like after it first loads:
When I switch to a different tab, then come back, it renders the cell correctly.
I use this method in the parent view controller (adapted from this)
func cycleFromViewController(oldViewController: UIViewController, toViewController newViewController: UIViewController) {
oldViewController.willMoveToParentViewController(nil)
self.addChildViewController(newViewController)
self.addSubView(newViewController.view, toView:self.containerView!)
newViewController.view.alpha = 0
newViewController.view.layoutIfNeeded()
UIView.animateWithDuration(0.25, animations: {
newViewController.view.alpha = 1
oldViewController.view.alpha = 0
},
completion: { finished in
oldViewController.view.removeFromSuperview()
oldViewController.removeFromParentViewController()
newViewController.didMoveToParentViewController(self)
})
}
The parent view controller's viewDidLoad method is called like this:
override func viewDidLoad() {
... // grab data in a background network call, populating the array of model objects
self.currentSelectedViewController!.view.translatesAutoresizingMaskIntoConstraints = false
self.addChildViewController(self.currentSelectedViewController!)
self.addSubView(self.currentSelectedViewController!.view, toView: self.containerView)
self.refreshContainerView()
super.viewDidLoad()
}
refreshContainerView looks like this:
func refreshContainerView() {
let currentVC = self.currentSelectedViewController as! MyTableViewController
currentVC.modelObjectList = self.modelObjectList
self.label.hidden = true
self.button.hidden = true
currentVC.tableView.reloadData()
}
Here is my cell's layout subviews method:
override func layoutSubviews() {
super.layoutSubviews()
self.reminderView.backgroundColor = UIColor.grayColor()
if let aModel = self.model {
self.configureWithModel(aModel)
}
self.setMaskToView(self.topView, corners: UIRectCorner.TopLeft.union(UIRectCorner.TopRight))
self.setMaskToView(self.bottomView, corners: UIRectCorner.BottomLeft.union(UIRectCorner.BottomRight))
}
Any thoughts as to how to fix
1. the initial brief loading without the insets and
2. the final rendering of the initial load with the rounded corners on the right side not properly rendering?
This cell exists in a storyboard as a prototype, with the insets created via auto layout constraints. (a constant setting the top and bottom view's distance from the top, bottom, right and left as appropriate). Clearly these constraints work when the cell is reloaded, but not on the initial load for some reason that is escaping me.
Evidently the answer was fairly simple. The mask method was being called in layoutSubviews for the cell, the the views themselves did not yet have their bounds set. So I subclassed the view into a new RoundedView class, and added a var for the corners and a modified mask method:
class RoundedView: UIView {
var corners : UIRectCorner = []
override func layoutSubviews() {
self.setMaskForCorners(corners)
}
func setMaskForCorners(corners: UIRectCorner) {
let rounded = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: 10, height: 10))
let mask = CAShapeLayer()
mask.path = rounded.CGPath
self.layer.mask = mask
}
}
Then I changed the views to be that subclass and then call it like this:
self.topView.corners = UIRectCorner.TopLeft.union(UIRectCorner.TopRight)
self.bottomView.corners = UIRectCorner.BottomLeft.union(UIRectCorner.BottomRight)