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

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.

Related

Change label's font size dynamically when scrolling tableView

I have tableView and on top of it an imageView. Made stretchable header with this guide https://github.com/abhimuralidharan/StretchableTableViewHeader-Swift
And now I have to add label on my image. This label should change size/font when tableView is scrolling.
I created my imageView in another class:
class LotteryHeaderView: UIImageView {
let ticketsCountLabel: UILabel = {
let label = UILabel()
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.boldSystemFont(ofSize: 100)
return label
}()
let infoLabel: UILabel = {
let label = UILabel()
label.textColor = .white
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 13, weight: .medium)
label.numberOfLines = 2
label.text = Localizable.all_user_tickets_count()
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubViews([ticketsCountLabel, infoLabel])
ticketsCountLabel.centerX(to: self.centerXAnchor)
.centerY(to: self.centerYAnchor)
infoLabel.top(to: ticketsCountLabel.bottomAnchor, constant: 7)
.width(constant: 184)
.centerX(to: self.centerXAnchor)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Initialized it in my controller:
let imageView = LotteryHeaderView(frame: CGRect(x: 0, y: 0, width: Constants.Size.screenWidth, height: 300))
Setup in viewDidLoad:
imageView.frame = CGRect(x: 0, y: 0, width: Constants.Size.screenWidth, height: 300)
imageView.image = R.image.santa_fe()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
view.addSubview(imageView)
Here func which gets called every time the tableview is scrolled, and it works for my image (it collapsable and stretchable):
func scrollImageInHeader(scrollView: UIScrollView) {
let y = 300 - (scrollView.contentOffset.y + 300)
let height = min(max(y, 60), 450)
imageView.frame = CGRect(x: 0, y: 0, width: Constants.Size.screenWidth, height: height)
}
Tried to add in scrollImageInHeader() code under, from this StackOverFlowAnswer, but it doesn't help
let offset = scrollView.contentOffset.y
let scale = min(max(1.0 - offset / 200.0, 0.0), 1.0)
imageView.ticketsCountLabel.transform = CGAffineTransform(scaleX: scale, y: scale)
Please, any help will be appreciated.
UIScrollView is parent UITableViewController. You use UIScrollView with UIScrollViewDelegate for setup label font size with scrollOffset: CGFloat follow .y
extension ViewController: UIScrollViewDelegate{
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var scrollOffset : CGFloat = scrollView.contentOffset.y
// process with scrollOffset
}
}
Actually I found my mistake. Code from Here helped.
Class LotteryHeaderView I changed to UIView and added there an UIImageView.

UIScrollview doesn't work once subviews added

I'm new to working with ScrollViews, and I'm doing it all programatically. I must be missing something super simple, but when I have no subviews, the scrollview shows up properly and scrolls up and down. But any time I add any subviews, the whole thing refuses to show up at all.
class DetailedPostScrollView: UIScrollView {
let topLabel: UILabel = {
let label = UILabel()
label.text = "this is the top"
return label
}()
let bottomLabel: UILabel = {
let label = UILabel()
label.text = "this is the bottom"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentSize = CGSize(width: frame.width, height: 2000)
alwaysBounceVertical = true
addSubviewUsingAutoLayout(topLabel, bottomLabel)
topLabel.centerXAnchor.constrain(to: self.centerXAnchor)
topLabel.widthAnchor.constrain(to: 200)
topLabel.heightAnchor.constrain(to: 50)
topLabel.topAnchor.constrain(to: self.topAnchor, with: 100)
bottomLabel.centerXAnchor.constrain(to: self.centerXAnchor)
bottomLabel.widthAnchor.constrain(to: 200)
bottomLabel.heightAnchor.constrain(to: 50)
bottomLabel.bottomAnchor.constrain(to: self.bottomAnchor)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And in my viewController, I instantiate and add the scroll view
let detailedPostScrollView = DetailedPostScrollView(frame: UIScreen.main.bounds)
detailedPostScrollView.backgroundColor = UIColor.purple
view.addSubviewUsingAutoLayout(detailedPostScrollView)
Again, I"m sure it's something super simple but I checked out all the questions and tutorial videos and couldn't see where I"m going wrong. Thanks for your help.
Edit: It seems to work fine when I do it all programmatically from a viewcontroller, as follows :
let scrollView: UIScrollView = {
let sv = UIScrollView()
sv.contentSize = CGSize(width: view.frame.width, height: 2000)
sv.backgroundColor = UIColor.purple
return sv
}()
view.addSubviewUsingAutoLayout(scrollView)
scrollView.topAnchor.constrain(to: view.topAnchor, with: 100)
scrollView.leadingAnchor.constrain(to: view.leadingAnchor)
scrollView.trailingAnchor.constrain(to: view.trailingAnchor)
scrollView.bottomAnchor.constrain(to: view.bottomAnchor)
let topLabel: UILabel = {
let label = UILabel()
label.text = "this is the top"
return label
}()
scrollView.addSubviewUsingAutoLayout(topLabel)
topLabel.centerXAnchor.constrain(to: scrollView.centerXAnchor)
topLabel.widthAnchor.constrain(to: 200)
topLabel.heightAnchor.constrain(to: 50)
topLabel.topAnchor.constrain(to: scrollView.topAnchor)
Something is off when I create a custom scrollview and instantiate that in my vc.
Try this :
class ViewController : UIViewController , UIScrollViewDelegate{
var scrollView : UIScrollView! ;
var containerView = UIView();
var contentSize : CGSize {
get {
return CGSize(width: view.frame.width, height: 2000);
}
}
override func viewDidLoad() {
super.viewDidLoad();
setupScrollView();
setupItems();
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews();
self.scrollView.frame = self.view.bounds;
containerView.frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height);
}
private func setupScrollView() {
scrollView = UIScrollView();
scrollView.delegate = self;
scrollView.backgroundColor = .white;
scrollView.contentSize = contentSize;
self.view.addSubview(scrollView);
containerView.backgroundColor = .purple;
self.scrollView.addSubview(containerView);
}
private func setupItems() {
let topButton = UIButton();
topButton.translatesAutoresizingMaskIntoConstraints = false;
topButton.setTitle("TOP BUTTON", for: .normal);
topButton.setTitleColor(.white, for: .normal);
self.containerView.addSubview(topButton);
let bottomTextField = UITextField();
bottomTextField.translatesAutoresizingMaskIntoConstraints = false;
bottomTextField.borderStyle = .roundedRect;
bottomTextField.placeholder = "BOTTOM TEXT FIELD";
self.containerView.addSubview(bottomTextField);
topButton.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true;
topButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 50).isActive = true;
topButton.widthAnchor.constraint(equalToConstant: 100).isActive = true;
topButton.heightAnchor.constraint(equalToConstant: 50).isActive = true;
bottomTextField.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true;
bottomTextField.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -50).isActive = true;
bottomTextField.widthAnchor.constraint(equalToConstant: 200).isActive = true;
bottomTextField.heightAnchor.constraint(equalToConstant: 40).isActive = true;
}
}
Set the scroll view frame to view bound in viewDidLayoutViews instead of using the layout , and add a container view as a subview for scroll view and set the size to same content size for scroll view again in viewDidLayoutViews so when ever user rotate the phone he get the correct width , and finally add all your views to container view .

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

UISwipeGestureRecognizer not recognized when added to UIView's subviews

I currently have a subclass of UIView which contains numerous subviews. I wish to add a UISwipeGesture to the subviews but unfortunately the swipe gesture is not recognized. I've set userInteractionEnabled = true and direction of the swipe gesture but nothing works.
public class CardStackView: UIView{
public var dataSource = [UIImage]()
private var swipeGuesture: UISwipeGestureRecognizer!
override public func layoutSubviews() {
for img in dataSource{
let view = AppView(image: img, frame: self.frame)
self.addSubview(view)
}
animateSubview()
self.userInteractionEnabled = true
}
func animateSubview(){
for (index, sView) in self.subviews.enumerate() {
swipeGuesture = UISwipeGestureRecognizer(target: self, action: #selector(self.swipeGuestureDidSwipeRight(_:)))
swipeGuesture.direction = .Right
sView.addGestureRecognizer(swipeGuesture)
sView.userInteractionEnabled = true
let move: CGFloat = CGFloat(-20 + index * 20)
let opacity = Float(1 - 0.2 * CGFloat(index))
sView.shadowOpacity(opacity).shadowOffset(CGSizeMake(20 - CGFloat(index) * 5, 20 - CGFloat(index) * 5)).shadowRadius(5).moveX(-move).moveY(-move).gravity().shadowColor(UIColor.grayColor()).duration(1)
.completion({
}).animate()
}
}
func swipeGuestureDidSwipeRight(gesture: UISwipeGestureRecognizer) {
print("Swiped right")
let subview = self.subviews[0]
subview.moveX(-60).duration(1).animate()
}
}
Example
class ExampleController: UIViewController {
var stackView: CardStackView!
override func viewDidLoad() {
super.viewDidLoad()
stackView = CardStackView(frame: CGRect(x: 20, y: 80, width: 200, height: 200))
stackView.dataSource = [UIImage(named: "2008")!, UIImage(named: "2008")!]
self.view.addSubview(stackView)
}
}
self.view.bringSubviewToFront(yourSubview)
try this code for all your subviews and if it doesn't work try this in your controller class for your CardStackView.
Try to call setNeedsLayout for stackView:
override func viewDidLoad() {
super.viewDidLoad()
stackView = CardStackView(frame: CGRect(x: 20, y: 80, width: 200, height: 200))
stackView.dataSource = [UIImage(named: "2008")!, UIImage(named: "2008")!]
stackView.setNeedsLayout()
self.view.addSubview(stackView)
}