Sliding a UIView "Out of nowhere" - swift

i am trying to create a custom dropdown menu. I will have several cards on my view which should have a dropdown menu when tapped.
Right now i am having the dropDownView having cardView.frame.maxY as its frame.origin.y value with a height of 0 and when I tap the card view I set the height of the dropDownView to its real height-value within an animation.
But that looks kind of ugly since it looks like it stretches out of nowhere. I want it to slide out of nowhere.
By that i mean it having its original size right away and sitting below the card view (cardView.frame.maxY = dropDownView.frame.maxY) When the cardView is tapped the dropDownView slides down (dropDownView.frame.origin.y = cardView.frame.maxY) within an animation.
The Problem is, that the dropDownView is bigger than the cardView. So when it sits behind the cardView it is visible above the cardView. I tried to illustrate the Problem :)
This is state A (Before View A[cardView] is tapped) (View C is just some Background View which should be visible above and below View A)
This is state B (after cardView is tapped)
Any Ideas how to solve this problem? Thank you!
In addition heres a little sample code:
class cardViewComplete: UIView {
var card: CardView!
var dropDownMenu: DropDownView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
func initSubViews() {
self.clipsToBounds = true
card = CardView()
card.frame = self.bounds
card.addTarget(self, action: #selector(YellowTackleTicketComplete.ticketTapped), forControlEvents: .TouchDown)
dropDownMenu = DropDownView()
dropDownMenu.frame = CGRect(x: 0, y: self.bounds.maxY, width: self.bounds.width, height: 350)
dropDownMenu.hidden = true
dropDownMenu.backgroundColor = UIColor.clearColor()
self.addSubview(card)
self.insertSubview(dropDownMenu, belowSubview: card)
dropDownMenu)
}
func showDropdown() {
dropDownMenu.hidden = false
originalHeight = self.frame.size.height
print("showing")
if !animating {
animating = true
UIView.animateWithDuration(
0.7,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: [],
animations: {
self.frame.size.height = self.frame.size.height + 350
}, completion: { _ in
self.animating = false
}
)
}
self.setNeedsDisplay()
self.dropDownMenu!.setNeedsDisplay()
dropped = true
}
func ticketTapped() {
showDropdown()
}
}

What I would do is place both View A and View B inside a new view, we can call this containerView.
containerView should be big enough to hold both A and B (when B is moved down). Then set containerView to clip at bounds. So when View B is in the "up" position, it is sitting both behind View A, and clipped at the top of containerView. Therefore it is not seen at all.
Once you're ready for View B to drop to it's "down" position, just animate it going down, where it will appear to come out from the bottom of View A. Since the containerView's frame will extend far enough down to accommodate A and B (in it's down position), nothing will be clipped and both views will be visible.
card = CardView()
card.frame = self.bounds
card.addTarget(self, action: #selector(YellowTackleTicketComplete.ticketTapped), forControlEvents: .TouchDown)
dropDownMenu = DropDownView()
// I changed the frame to place it right underneath the card view
dropDownMenu.frame = CGRect(x: 0, y: card.frame.size.height - 350, width: self.bounds.width, height: 350)
dropDownMenu.hidden = true
dropDownMenu.backgroundColor = UIColor.clearColor()
let containerView = UIView()
containerView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: card.frame.size.height + dropDownMenu.frame.size.height)
containerView.backgroundColor = nil
containerView.clipsAtBounds = true
containerView.addSubview(dropDownMenu)
containerView.addSubview(card)

Related

Don't understand why my UILabel won't follow safeareaLayout constraints

Am not working with storyboards, and below is the full code for my UIViewController for my Main Menu screen. While everything appears to work, I made an error, but don't understand the outcome.
myView, the gray area is set to the safeareaLayout constraints
fillRects is a function where I prefill all the rects for the labels and buttons that I will place on myView
By accident, I passed the wrong view to fillRects, not myView, as intended. Therefore the UILabel I create below is larger than it should be.
But my understanding was that it should have been cropped since it is a child of myView, which is constrained to the safeAreaLayout guide. Yet from the included image, you can see that it goes beyond myView's area on the screen.
Is my error in the way I applied the safeareaLayout guides? Or my understanding as to how they work?
import UIKit
class MainMenuCtrl: UIViewController {
var viewBounds : CGRect = .zero
var topLabelRect : CGRect = .zero
var bottomLabelRect : CGRect = .zero
var menuRect : CGRect = .zero
private let myView : UIView = {
let myView = UIView()
myView.translatesAutoresizingMaskIntoConstraints = false
myView.backgroundColor = .gray
return myView
}()
override func viewDidLoad() {
super.viewDidLoad()
// Set background color func
setBGC(vc: view)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
view.backgroundColor = .green
view.addSubview(myView)
addContraints(main: view, child: myView)
////fill the CGRects for all the labels, and buttons
fillRects(vc: self)
let label = UILabel(frame: self.topLabelRect)
label.textAlignment = .center
label.backgroundColor = .red
label.text = "hello"
label.textColor = nameColor
label.font = .systemFont(ofSize: 40)
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.7
myView.addSubview(label)
}
override var prefersStatusBarHidden: Bool {
return false
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .darkContent
}
}
Here is the code for fillRects
func fillRects (vc: MainMenuCtrl) {
vc.viewBounds = vc.view.frame
vc.topLabelRect = CGRect(x: vc.viewBounds.minX, y: vc.viewBounds.minY,
width: vc.viewBounds.width, height: vc.viewBounds.height * 0.05)
vc.bottomLabelRect = CGRect(x: vc.viewBounds.minX, y: vc.viewBounds.height * 0.9,
width: vc.viewBounds.width, height: vc.viewBounds.height * 0.05)
vc.menuRect = CGRect(x: vc.viewBounds.minX, y: vc.viewBounds.height * 0.2,
width: vc.viewBounds.width, height: vc.viewBounds.height * 0.6)
}
A view has a clipToBounds property that dictates whether subViews are restricted to the bounds of their parent view. The default value for this is false, which explains the behaviour you are experiencing.
Setting view.clipToBounds = true on the parent view should result in the sub view behaving as you expected.

CGAffineTransform doesn't work with custom UIView

I'm trying to create a custom view class that is a popup with a UICollectionView on it. I've created functions to animate the view in and out, but only the animate out function is actually working.
I'm setting constraints as follows...
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit(){
...
self.addSubview(collectionView)
collectionView.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
collectionView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.7).isActive = true
...
self.animateIn()
}
Then to handle the animation I'm using the following...
func animateIn(){
self.collectionView.transform = CGAffineTransform(translationX: 0, y: self.collectionView.frame.height)
self.blurredBackgroundView.alpha = 0
UIView.animate(withDuration: 0.5, animations: {
self.blurredBackgroundView.alpha = 1
self.collectionView.transform = .identity
})
}
And the animateOut function (which works) is...
func animateOut(){
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
self.collectionView.transform = CGAffineTransform(translationX: 0, y: self.collectionView.frame.height)
self.blurredBackgroundView.alpha = 0
}) { (complete) in
if complete {
self.removeFromSuperview()
}
}
}
But the initial CGAfineTransform isn't working so the view is appearing in its final position when it first appears. I'm confused as it's working perfectly for the animateOut function which is called when a button is pressed to close the popup.
Can anyone tell me what I'm doing wrong? I feel like there must be something basic I'm missing.
Thanks
You're trying to start your animation from an initializer, but by that time, the view isn't a part of view hierarchy. Since it's impossible to animate a view that isn't part of the view hierarchy, the transform is applied immediately. animateOut works fine, because when you call it the view is already displayed.
What you need to do is to call your animateIn function explicitly after the view has been added to view hierarchy
let yourView = YourView(frame: someFrame)
superview.addSubview(yourView)
yourView.animateIn()

How filter and forward gestures on UIView who covers another UIView with the same size and position?

Description is simple. I have 2 subviews (topView and bottomView UIViews) into main view and both cover whole screen. bottomView is bellow topView.
For topView I added double-tap gesture using UITapGestureRecognizer and for bottomView I added pinch gesture using UIPinchGestureRecognizer.
The goal is: When I made pinch gesture (on the topView) I want to forward that pinch gesture to the bottomView. Double-tap gesture should be executed on the topView. In other words, how to forward only pinch gesture to the bottomView and every other gesture should be executed by topView.
Note: Keep in mind that both UIViews cover whole screen.
I provided code for the start.
I tried to solve this problem overriding UIView's methods hitTest and point, but no luck.
import UIKit
let width = UIScreen.main.bounds.width
let height = UIScreen.main.bounds.height
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let bottomView: UIView = {
let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: width, height: height)
view.backgroundColor = .yellow
return view
}()
let topView: UIView = {
let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: width, height: height)
view.backgroundColor = .orange
return view
}()
let doubleTouch = UITapGestureRecognizer(target: self, action: #selector(doubleTap(sender:)))
doubleTouch.numberOfTapsRequired = 2
topView.addGestureRecognizer(doubleTouch)
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(pinch(sender:)))
bottomView.addGestureRecognizer(pinch)
view.addSubview(bottomView)
view.addSubview(topView)
}
#objc func doubleTap(sender: UITapGestureRecognizer) {
print("Double Tap")
}
#objc func pinch(sender: UIPinchGestureRecognizer) {
print("Pinch")
}
}

UIScrollView showing subview not correctly

I'm trying to show simple custom view into scrollView. Here's my code :
struct scrollViewDataStruct {
let title: String?
let image: UIImage?
}
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
var scrollViewData = [scrollViewDataStruct]()
override func viewDidLoad() {
super.viewDidLoad()
scrollViewData = [
scrollViewDataStruct(title: "First", image: #imageLiteral(resourceName: "iPhone 8 Copy 2")),
scrollViewDataStruct(title: "Second", image: #imageLiteral(resourceName: "iPhone 8 Copy 3"))
]
self.scrollView.backgroundColor = .yellow
scrollView.contentSize.width = self.scrollView.frame.width * CGFloat(scrollViewData.count)
var i = 0
for _ in scrollViewData {
let view = CustomView(frame: CGRect(x: self.scrollView.frame.width * CGFloat(i), y: 0, width: scrollView.frame.width, height: self.scrollView.frame.height))
self.scrollView.addSubview(view)
i += 1
}
// Do any additional setup after loading the view, typically from a nib.
}
}
class CustomView: UIView {
let imageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = UIColor.blue
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(imageView)
imageView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
imageView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
As you can see, the CustomView's frame = scrollView's frame but when i ran application it's not as I expected :
Then, in storyboard, i change device from iphone8 to iphone 8 plus and run again. It's show CustomView correctly. I have no idea, the scrollView is always correct but the CustomView is not .
Any suggest ?
Your problem is that you are accessing the frame of the scrollView before Auto Layout has run and established the size of the frame for the actual device. A quick fix is to move your setup code into an override of viewDidLayoutSubviews.
You have to be careful though, because unlike viewDidLoad, viewDidLayoutSubviews will run more than once, so you have to make sure you don't add your views multiple times.
// property - have we set up the views yet?
var setup = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !setup {
scrollView.contentSize.width = self.scrollView.frame.width * CGFloat(scrollViewData.count)
var i = 0
for _ in scrollViewData {
let view = CustomView(frame: CGRect(x: self.scrollView.frame.width * CGFloat(i), y: 0, width: scrollView.frame.width, height: self.scrollView.frame.height))
self.scrollView.addSubview(view)
i += 1
}
setup = true
}
}
You should consider using constraints to place your views within your scrollView content instead of messing with the frame calculations, then Auto Layout would just automatically do the right thing.
In viewDidLoad, UI component will suppose to have the size you have taken in storyboard.
There are 2 ways to do this:
1. Use autoresizingMask property
autoresizingMask property will resize the view, if its containerView's frames changed
var i = 0
for _ in scrollViewData {
let view = CustomView(frame: CGRect(x: self.scrollView.frame.width * CGFloat(i), y: 0, width: scrollView.frame.width, height: self.scrollView.frame.height))
view.autoresizingMask = .flexibleHeight
self.scrollView.addSubview(view)
i += 1
}
2. Use fixed parameters, say UIScreen.main.bounds.size.height
Just update your code for custom view's height with reference to screen height rather than scroll view's height. It will work fine
var i = 0
let height = UIScreen.main.bounds.size.height
for _ in scrollViewData {
let view = CustomView(frame: CGRect(x: self.scrollView.frame.width * CGFloat(i), y: 0, width: scrollView.frame.width, height: height))
self.scrollView.addSubview(view)
i += 1
}

iOS - add image and text in title of Navigation bar

I would like to create a nav bar similar to what's in the image that's attached.
The title of the nav bar will be a combination of an image and text.
Should this be done per any best practice?
How can it be done?
As this answer shows, the easiest solution is to add the text to your image and add that image to the navigation bar like so:
var image = UIImage(named: "logo.png")
self.navigationItem.titleView = UIImageView(image: image)
But if you have to add text and an image separately (for example, in the case of localization), you can set your navigation bar's title view to contain both image and text by adding them to a UIView and setting the navigationItem's title view to that UIView, for example (assuming the navigation bar is part of a navigation controller):
// Only execute the code if there's a navigation controller
if self.navigationController == nil {
return
}
// Create a navView to add to the navigation bar
let navView = UIView()
// Create the label
let label = UILabel()
label.text = "Text"
label.sizeToFit()
label.center = navView.center
label.textAlignment = NSTextAlignment.Center
// Create the image view
let image = UIImageView()
image.image = UIImage(named: "Image.png")
// To maintain the image's aspect ratio:
let imageAspect = image.image!.size.width/image.image!.size.height
// Setting the image frame so that it's immediately before the text:
image.frame = CGRect(x: label.frame.origin.x-label.frame.size.height*imageAspect, y: label.frame.origin.y, width: label.frame.size.height*imageAspect, height: label.frame.size.height)
image.contentMode = UIViewContentMode.ScaleAspectFit
// Add both the label and image view to the navView
navView.addSubview(label)
navView.addSubview(image)
// Set the navigation bar's navigation item's titleView to the navView
self.navigationItem.titleView = navView
// Set the navView's frame to fit within the titleView
navView.sizeToFit()
Use horizontal UIStackView should be much cleaner and easier
Please add the next extension to UIViewController
extension UIViewController {
func setTitle(_ title: String, andImage image: UIImage) {
let titleLbl = UILabel()
titleLbl.text = title
titleLbl.textColor = UIColor.white
titleLbl.font = UIFont.systemFont(ofSize: 20.0, weight: .bold)
let imageView = UIImageView(image: image)
let titleView = UIStackView(arrangedSubviews: [imageView, titleLbl])
titleView.axis = .horizontal
titleView.spacing = 10.0
navigationItem.titleView = titleView
}
}
then use it inside your viewController:
setTitle("yourTitle", andImage: UIImage(named: "yourImage"))
(this will align the text and the icon together to the center, if you want the text to be centered and the icon in the left, just add an empty UIView with width constraint equal to the icon width)
here is my 2 cents for Swift 4, since accepted answer didn't work for me (was mostly off the screen):
// .. in ViewController
var navBar = CustomTitleView()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// =================== navBar =====================
navBar.loadWith(title: "Budget Overview", leftImage: Images.pie_chart)
self.navigationItem.titleView = navBar
}
class CustomTitleView: UIView
{
var title_label = CustomLabel()
var left_imageView = UIImageView()
override init(frame: CGRect){
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
setup()
}
func setup(){
self.addSubview(title_label)
self.addSubview(left_imageView)
}
func loadWith(title: String, leftImage: UIImage?)
{
//self.backgroundColor = .yellow
// =================== title_label ==================
//title_label.backgroundColor = .blue
title_label.text = title
title_label.font = UIFont.systemFont(ofSize: FontManager.fontSize + 5)
// =================== imageView ===================
left_imageView.image = leftImage
setupFrames()
}
func setupFrames()
{
let height: CGFloat = Navigation.topViewController()?.navigationController?.navigationBar.frame.height ?? 44
let image_size: CGFloat = height * 0.8
left_imageView.frame = CGRect(x: 0,
y: (height - image_size) / 2,
width: (left_imageView.image == nil) ? 0 : image_size,
height: image_size)
let titleWidth: CGFloat = title_label.intrinsicContentSize.width + 10
title_label.frame = CGRect(x: left_imageView.frame.maxX + 5,
y: 0,
width: titleWidth,
height: height)
contentWidth = Int(left_imageView.frame.width)
self.frame = CGRect(x: 0, y: 0, width: CGFloat(contentWidth), height: height)
}
var contentWidth: Int = 0 //if its CGFloat, it infinitely calls layoutSubviews(), changing franction of a width
override func layoutSubviews() {
super.layoutSubviews()
self.frame.size.width = CGFloat(contentWidth)
}
}
Swift 4.2 + Interface Builder Solution
As a follow-on to Lyndsey Scott's answer, you can also create a UIView .xib in Interface Builder, use that to lay out your title and image, and then update it on-the-fly via an #IBOutlet. This is useful for dynamic content, internationalization, maintainability etc.
Create a UIView subclass with a UILabel outlet and assign your new .xib to this class:
import UIKit
class FolderTitleView: UIView {
#IBOutlet weak var title : UILabel!
/// Create an instance of the class from its .xib
class func instanceFromNib() -> FolderTitleView {
return UINib(nibName: "FolderTitleView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! FolderTitleView
}
}
Connect the label to your outlet (title in my example) in your .xib, then in your UIViewController:
/// Reference to the title view
var folderTitleView : FolderTitleView?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Set the screen title to match the active folder
updateTitle()
}
/// Updates the title of the navigation controller.
func updateTitle() {
self.title = ""
if folderTitleView == nil {
folderTitleView = FolderTitleView.instanceFromNib()
self.navigationItem.titleView = folderTitleView
}
folderTitleView!.title.text = "Listening"
folderTitleView!.layoutIfNeeded()
}
This results in a nice self-centering title bar with an embedded image that you can easily update from code.
// worked for me
create a view and set the frame
now add the image in the view and set the frame
after adding the image, add the label in same view and set the frame
after adding the image and label to view, add same view to navigationItem
let navigationView = UIView(frame: CGRect(x: 0, y: 0, width: 50 , height: 55))
let labell : UILabel = UILabel(frame: CGRect(x: -38, y: 25, width: 150, height: 25))
labell.text = "Your text"
labell.textColor = UIColor.black
labell.font = UIFont.boldSystemFont(ofSize: 10)
navigationView.addSubview(labell)
let image : UIImage = UIImage(named: ValidationMessage.headerLogoName)!
let imageView = UIImageView(frame: CGRect(x: -20, y: 0, width: 100, height: 30))
imageView.contentMode = .scaleAspectFit
imageView.image = image
//navigationItem.titleView = imageView
navigationView.addSubview(imageView)
navigationItem.titleView = navigationView