How to Add floating button as Facebook's App Rooms using the storyboard?
I can't drag UIView on the tableview that's the only way I know.
This button code below:
Adds Shadow
Makes the button round
Adds an animation to draw attention
Swift 4.2: COMPLETE VIEW CONTROLLER IMPLEMENTATION
import Foundation
public class FloatingButtonViewController: UIViewController {
private var floatingButton: UIButton?
// TODO: Replace image name with your own image:
private let floatingButtonImageName = "NAME OF YOUR IMAGE"
private static let buttonHeight: CGFloat = 75.0
private static let buttonWidth: CGFloat = 75.0
private let roundValue = FloatingButtonViewController.buttonHeight/2
private let trailingValue: CGFloat = 15.0
private let leadingValue: CGFloat = 15.0
private let shadowRadius: CGFloat = 2.0
private let shadowOpacity: Float = 0.5
private let shadowOffset = CGSize(width: 0.0, height: 5.0)
private let scaleKeyPath = "scale"
private let animationKeyPath = "transform.scale"
private let animationDuration: CFTimeInterval = 0.4
private let animateFromValue: CGFloat = 1.00
private let animateToValue: CGFloat = 1.05
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
createFloatingButton()
}
public override func viewWillDisappear(_ animated: Bool) {
guard floatingButton?.superview != nil else { return }
DispatchQueue.main.async {
self.floatingButton?.removeFromSuperview()
self.floatingButton = nil
}
super.viewWillDisappear(animated)
}
private func createFloatingButton() {
floatingButton = UIButton(type: .custom)
floatingButton?.translatesAutoresizingMaskIntoConstraints = false
floatingButton?.backgroundColor = .white
floatingButton?.setImage(UIImage(named: floatingButtonImageName), for: .normal)
floatingButton?.addTarget(self, action: #selector(doThisWhenButtonIsTapped(_:)), for: .touchUpInside)
constrainFloatingButtonToWindow()
makeFloatingButtonRound()
addShadowToFloatingButton()
addScaleAnimationToFloatingButton()
}
// TODO: Add some logic for when the button is tapped.
#IBAction private func doThisWhenButtonIsTapped(_ sender: Any) {
print("Button Tapped")
}
private func constrainFloatingButtonToWindow() {
DispatchQueue.main.async {
guard let keyWindow = UIApplication.shared.keyWindow,
let floatingButton = self.floatingButton else { return }
keyWindow.addSubview(floatingButton)
keyWindow.trailingAnchor.constraint(equalTo: floatingButton.trailingAnchor,
constant: self.trailingValue).isActive = true
keyWindow.bottomAnchor.constraint(equalTo: floatingButton.bottomAnchor,
constant: self.leadingValue).isActive = true
floatingButton.widthAnchor.constraint(equalToConstant:
FloatingButtonViewController.buttonWidth).isActive = true
floatingButton.heightAnchor.constraint(equalToConstant:
FloatingButtonViewController.buttonHeight).isActive = true
}
}
private func makeFloatingButtonRound() {
floatingButton?.layer.cornerRadius = roundValue
}
private func addShadowToFloatingButton() {
floatingButton?.layer.shadowColor = UIColor.black.cgColor
floatingButton?.layer.shadowOffset = shadowOffset
floatingButton?.layer.masksToBounds = false
floatingButton?.layer.shadowRadius = shadowRadius
floatingButton?.layer.shadowOpacity = shadowOpacity
}
private func addScaleAnimationToFloatingButton() {
// Add a pulsing animation to draw attention to button:
DispatchQueue.main.async {
let scaleAnimation: CABasicAnimation = CABasicAnimation(keyPath: self.animationKeyPath)
scaleAnimation.duration = self.animationDuration
scaleAnimation.repeatCount = .greatestFiniteMagnitude
scaleAnimation.autoreverses = true
scaleAnimation.fromValue = self.animateFromValue
scaleAnimation.toValue = self.animateToValue
self.floatingButton?.layer.add(scaleAnimation, forKey: self.scaleKeyPath)
}
}
}
Its a bit late, but may be it can help someone. You can do it very easily via programatically. Just create an instance of the button, set the desired property of button It is preferable to use constraints over frame cause it'll render the button at same position on every screen size. I am posting the code which will make the button round. Play with the value of constraints to change the position and size of button.
Swift 3
var roundButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
self.roundButton = UIButton(type: .custom)
self.roundButton.setTitleColor(UIColor.orange, for: .normal)
self.roundButton.addTarget(self, action: #selector(ButtonClick(_:)), for: UIControlEvents.touchUpInside)
self.view.addSubview(roundButton)
}
override func viewWillLayoutSubviews() {
roundButton.layer.cornerRadius = roundButton.layer.frame.size.width/2
roundButton.backgroundColor = UIColor.lightGray
roundButton.clipsToBounds = true
roundButton.setImage(UIImage(named:"your-image"), for: .normal)
roundButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
roundButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -3),
roundButton.bottomAnchor.constraint
(equalTo: self.view.bottomAnchor, constant: -53),
roundButton.widthAnchor.constraint(equalToConstant: 50),
roundButton.heightAnchor.constraint(equalToConstant: 50)])
}
/** Action Handler for button **/
#IBAction func ButtonClick(_ sender: UIButton){
/** Do whatever you wanna do on button click**/
}
Put a table view inside a view controller. You should then be able to add the the button on top of the table view.
Following the answer of #Kunal Kumar, it works on the UIScrollView, and UIView, but it doesn't works on the UITableView. I found it is easy to make it works with UITableView, only need to add one line code. Thank you so much #Kunal Kumar
in the viewDidLoad, change self.view.addSubview(roundButton) to
self.navigationController?.view.addSubview(roundButton) and it will work!!
by the way, I think we need to add super.viewWillLayoutSubviews() in the viewWillLayoutSubviews() method.
below is the all code with above mentioned,
Swift 3
// MARK: Floating Button
var roundButton = UIButton()
func createFloatingButton() {
self.roundButton = UIButton(type: .custom)
self.roundButton.setTitleColor(UIColor.orange, for: .normal)
self.roundButton.addTarget(self, action: #selector(ButtonClick(_:)), for: UIControlEvents.touchUpInside)
//change view to navigationController?.view, if you have a navigationController in this tableview
self.navigationController?.view.addSubview(roundButton)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
roundButton.layer.cornerRadius = roundButton.layer.frame.size.width/2
roundButton.backgroundColor = UIColor.lightGray
roundButton.clipsToBounds = true
roundButton.setImage(UIImage(named:"ic_wb_sunny_48pt"), for: .normal)
roundButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
roundButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -3),
roundButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -53),
roundButton.widthAnchor.constraint(equalToConstant: 50),
roundButton.heightAnchor.constraint(equalToConstant: 50)])
}
It's simply because your UIButton is under the UITableView, move it in the storyboard's tree so it appears like this :
| View
|-- Table View
|-- Button
Related
I have a simple log in view implemented as follows :
import UIKit
class LoginViewController: UIViewController {
private var safeArea : UILayoutGuide!
private let scrollView : UIScrollView = {
let view = UIScrollView()
view.translatesAutoresizingMaskIntoConstraints = false
view.keyboardDismissMode = .onDrag
return view
}()
private let containerView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let logoView : UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFill
view.layer.cornerRadius = 8
view.image = UIImage(named: "logo")!
return view
}()
private let emailOrPhoneTextFieldView : UITextField = {
let view = UITextField()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.cornerRadius = 10
view.placeholder = "Email or phone"
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textColor = .black
view.autocapitalizationType = .none
view.tintColor = UIColor(named: "myColor")
view.backgroundColor = .systemGray
return view
}()
private let passwordTextFieldView : UITextField = {
let view = UITextField()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderColor = UIColor.lightGray.cgColor
view.layer.borderWidth = 0.5
view.layer.cornerRadius = 10
view.placeholder = "Password"
view.font = UIFont.systemFont(ofSize: 16, weight: .regular)
view.textColor = .black
view.autocapitalizationType = .none
view.tintColor = UIColor(named: "myColor")
view.isSecureTextEntry = true
view.backgroundColor = .systemGray
return view
}()
private let logInButtonView : UIButton = {
let view = UIButton()
view.setTitle("Log in", for: .normal)
view.setTitleColor(.white, for : .normal)
view.setBackgroundImage( UIImage(named: "blue_pixel")!, for: .normal)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
view.translatesAutoresizingMaskIntoConstraints = false
view.addTarget(self, action: #selector(logInButtonClickedHandler), for: .touchUpInside)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
safeArea = view.layoutMarginsGuide
setupViews()
}
private func setupViews()
{
view.addSubview(scrollView)
containerView.addSubview(logoView)
containerView.addSubview(emailOrPhoneTextFieldView)
containerView.addSubview(passwordTextFieldView)
containerView.addSubview(logInButtonView)
scrollView.addSubview(containerView)
let constraints = [
scrollView.topAnchor.constraint(equalTo: safeArea.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
logoView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 120),
logoView.widthAnchor.constraint(equalToConstant: 100),
logoView.heightAnchor.constraint(equalToConstant: 100),
logoView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
emailOrPhoneTextFieldView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16),
emailOrPhoneTextFieldView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16),
emailOrPhoneTextFieldView.topAnchor.constraint(equalTo: logoView.bottomAnchor, constant: 120),
emailOrPhoneTextFieldView.heightAnchor.constraint(equalToConstant: 50),
passwordTextFieldView.topAnchor.constraint(equalTo: emailOrPhoneTextFieldView.bottomAnchor),
passwordTextFieldView.leadingAnchor.constraint(equalTo: emailOrPhoneTextFieldView.leadingAnchor),
passwordTextFieldView.heightAnchor.constraint(equalToConstant: 50),
passwordTextFieldView.trailingAnchor.constraint(equalTo: emailOrPhoneTextFieldView.trailingAnchor),
logInButtonView.topAnchor.constraint(equalTo: passwordTextFieldView.bottomAnchor, constant: 16),
logInButtonView.leadingAnchor.constraint(equalTo: passwordTextFieldView.leadingAnchor),
logInButtonView.trailingAnchor.constraint(equalTo: passwordTextFieldView.trailingAnchor),
logInButtonView.heightAnchor.constraint(equalToConstant: 50),
logInButtonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
#objc private func logInButtonClickedHandler() {
print("button pressed")
}
}
//MARK: Keyboard Notifications
private extension LoginViewController {
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
scrollView.contentInset.bottom = keyboardSize.height
scrollView.verticalScrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
}
}
#objc func keyboardWillHide(notification: NSNotification) {
scrollView.contentInset.bottom = .zero
scrollView.verticalScrollIndicatorInsets = .zero
}
}
Everything is fine with the implementation but 2 things looks very strange for me and I guess I misunderstood smth
If I comment out
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
I see that my container view does not fit the whole screen width (actually it's about 50% of it)
Why? I set trailing and leading constraints to scrollview, which is 100% of view width.
If I comment out
logInButtonView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
I don't get button click events and I'm not able to input anything inside textfields. What is the issue here?
From the Apple Docs:
Constraints between the edges or margins of the scroll view and its
content attach to the scroll view’s content area.
Constraints between the height, width, or centers attach to the scroll
view’s frame.
Hence you need the width constraint in order to make the contentView the full width of the ScrollView's frame.
As above, without that constraint the contentView only has constraints to the top/bottom edge of the scrollView this doesn't define its height and so you need to add full top-to-bottom constraints on the subviews of the contentView in order to define its height.
If you use the View Hierarchy Debugger you'll see the contentView has 0 height without that constraint (it just isn't clipping the content), hence why you can't tap on any controls.
It's worth giving the 'Working with Scroll Views' section of Apple Auto-Layout docs a read.
I am trying to center a Button onto the bottom of a view but it never appears. The only time it appears is when I uncomment takePhotoButton.frame. What is the proper way to do this?
import UIKit
import AVFoundation
class InputViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let photoPreviewImageView = UIImageView()
photoPreviewImageView.frame = view.bounds
photoPreviewImageView.backgroundColor = UIColor.green
view.addSubview(photoPreviewImageView)
let imageOfPhotoButton = UIImage(named: "smallcircle.circle.fill") as UIImage?
let takePhotoButton = UIButton(type: .custom) as UIButton
takePhotoButton.setImage(imageOfPhotoButton, for: .normal)
//takePhotoButton.frame = CGRect(x: 10, y: 10, width: 60, height: 60) // It will appear with this code however i took it away because im trying to center it at the bottom of the screen
takePhotoButton.center = view.center
photoPreviewImageView.addSubview(takePhotoButton)
}
}
Use constraint anchors. After you add the takePhotoButton set them the following way:
takePhotoButton.bottomAnchor.constraint(equalTo: photoPreviewImageView.bottomAnchor).isActive = true
takePhotoButton.centerXAnchor.constraint(equalTo: photoPreviewImageView.centerXAnchor).isActive = true
This will set make your button have the same bottom and center as it's container.
Good day,
you have to add constraint.
import UIKit
class ViewController: UIViewController {
var loginButton : UIButton = {
let button = UIButton(type: .system)
button.setTitle("Login", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .red
button.tintColor = .white
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
constraintsInit()
}
func constraintsInit(){
view.addSubview(loginButton)
NSLayoutConstraint.activate([
loginButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
loginButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
loginButton.heightAnchor.constraint(equalToConstant: 30),
loginButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor,constant: 30),
loginButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor,constant: -30),
])
}
}
on youtube you can find several people that explain how create the views, using only code.
I've spent almost a couple of hours to figure it out. However, it did not happen and finally, I had to come here. Two things are required to be achieved:
Firstly I'd like to have a spontaneous corner radius at the top (which is basically TopRight & TopLeft) of UITabbar.
Secondly, I'd like to have a shadow above those corner radius(shown in below image).
Please have a look at below image
Let me know if anything further required from my side, I'll surely provide that.
Any help will be appreciated.
Edit 1
One more little question arose here along, suppose, Even if, However, we were able to accomplish this, Would Apple review team accept the application?
I'm being little nervous and curious about it.
Q : One more little question arose here along, suppose, Even if, However, we were able to accomplish this, Would Apple review team accept the application?
A: Yes They are accept your app I have Add This Kind Of TabBar.
Create Custom TabBar
HomeTabController
import UIKit
class HomeTabController: UITabBarController
{
var viewCustomeTab : CustomeTabView!
var lastSender : UIButton!
//MARK:- ViewController Methods
override func viewDidLoad()
{
super.viewDidLoad()
UITabBar.appearance().shadowImage = UIImage()
allocateTabItems()
}
//MARK:- Prepare Methods
// Allocate shop controller with tab bar
func allocateTabItems()
{
let vc1 = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "Avc") as? Avc
let item1 = UINavigationController(rootViewController: vc1!)
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage()
self.viewControllers = [item1]
createTabBar()
}
func createTabBar()
{
viewCustomeTab = CustomeTabView.instanceFromNib()
viewCustomeTab.translatesAutoresizingMaskIntoConstraints = false
viewCustomeTab.call()
self.view.addSubview(viewCustomeTab)
if #available(iOS 11, *)
{
let guide = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([guide.bottomAnchor.constraint(equalToSystemSpacingBelow: viewCustomeTab.bottomAnchor, multiplier: 0), viewCustomeTab.leadingAnchor.constraint(equalToSystemSpacingAfter: guide.leadingAnchor, multiplier: 0), guide.trailingAnchor.constraint(equalToSystemSpacingAfter: viewCustomeTab.trailingAnchor, multiplier: 0), viewCustomeTab.heightAnchor.constraint(equalToConstant: 70) ])
}
else
{
let standardSpacing: CGFloat = 0
NSLayoutConstraint.activate([viewCustomeTab.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor, constant: standardSpacing), bottomLayoutGuide.topAnchor.constraint(equalTo: viewCustomeTab.bottomAnchor, constant: standardSpacing)
])
}
viewCustomeTab.btnTab1.addTarget(self, action: #selector(HomeTabController.buttonTabClickAction(sender:)), for: .touchUpInside)
viewCustomeTab.btnTab2.addTarget(self, action: #selector(HomeTabController.buttonTabClickAction(sender:)), for: .touchUpInside)
viewCustomeTab.btnTab3.addTarget(self, action: #selector(HomeTabController.buttonTabClickAction(sender:)), for: .touchUpInside)
viewCustomeTab.btnTab4.addTarget(self, action: #selector(HomeTabController.buttonTabClickAction(sender:)), for: .touchUpInside)
viewCustomeTab.btnTab5.addTarget(self, action: #selector(HomeTabController.buttonTabClickAction(sender:)), for: .touchUpInside)
//self.view.layoutIfNeeded()
viewCustomeTab.layoutIfNeeded()
viewCustomeTab.btnTab1.alignContentVerticallyByCenter(offset: 3)
viewCustomeTab.btnTab2.alignContentVerticallyByCenter(offset: 3)
viewCustomeTab.btnTab3.alignContentVerticallyByCenter(offset: 3)
viewCustomeTab.btnTab4.alignContentVerticallyByCenter(offset: 3)
viewCustomeTab.btnTab5.alignContentVerticallyByCenter(offset: 3)
viewCustomeTab.btnTab1.isSelected = true
}
//MARK:- Button Click Actions
//Manage Tab From Here
func setSelect(sender:UIButton)
{
viewCustomeTab.btnTab1.isSelected = false
viewCustomeTab.btnTab2.isSelected = false
viewCustomeTab.btnTab3.isSelected = false
viewCustomeTab.btnTab4.isSelected = false
viewCustomeTab.btnTab5.isSelected = false
sender.isSelected = true
}
#objc func buttonTabClickAction(sender:UIButton)
{
//self.selectedIndex = sender.tag
if sender.tag == 0
{
let vc1 = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "Bvc") as? Bvc
let item1 = UINavigationController(rootViewController: vc1!)
item1.navigationBar.isHidden = false
self.viewControllers = [item1]
setSelect(sender: viewCustomeTab.btnTab1)
return
}
if sender.tag == 1
{
let vc2 = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "Cvc") as? Cvc
let item2 = UINavigationController(rootViewController: vc2!)
item2.navigationBar.isHidden = false
item2.navigationBar.isTranslucent = false
self.viewControllers = [item2]
setSelect(sender: viewCustomeTab.btnTab2)
return
}
if sender.tag == 2
{
let vc3 = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: "Dvc") as? Dvc
let item3 = UINavigationController(rootViewController: vc3!)
item3.navigationBar.isHidden = false
item3.navigationBar.isTranslucent = false
self.viewControllers = [item3]
setSelect(sender: viewCustomeTab.btnTab3)
return
}
if sender.tag == 3
{
}
if sender.tag == 4
{
}
}
}
Create Custom View For Shadow Effect and For + Button.
import UIKit
class CustomeTabView: UIView
{
#IBOutlet weak var btnTab5: UIButton!
#IBOutlet weak var btnTab4: UIButton!
#IBOutlet weak var btnTab3: UIButton!
#IBOutlet weak var btnTab2: UIButton!
#IBOutlet weak var btnTab1: UIButton!
#IBOutlet weak var vRadius: UIView!
class func instanceFromNib() -> CustomeTabView
{
return UINib(nibName: "CustomeTabView", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! CustomeTabView
}
private var shadowLayer: CAShapeLayer!
override func layoutSubviews()
{
super.layoutSubviews()
let shadowSize : CGFloat = 2.0
let shadowPath = UIBezierPath(roundedRect: CGRect(x: -shadowSize / 2, y: -shadowSize / 2, width: self.vRadius.frame.size.width, height: self.vRadius.frame.size.height), cornerRadius : 20)
self.vRadius.layer.masksToBounds = false
self.vRadius.layer.shadowColor = UIColor.black.cgColor
self.vRadius.layer.shadowOffset = CGSize.zero//(width: self.vRadius.frame.size.width, height: self.vRadius.frame.size.height)
self.vRadius.layer.shadowOpacity = 0.5
self.vRadius.layer.shadowPath = shadowPath.cgPath
self.vRadius.layer.cornerRadius = 20
}
OpenImg
Swift 4.2
You can achieve this with some custom view with a custom tab bar controller. You can customize the colors and shadows by editing only the custom views.
Custom Tab Bar Controller
import UIKit
class MainTabBarController: UITabBarController{
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
tabBar.backgroundImage = UIImage.from(color: .clear)
tabBar.shadowImage = UIImage()
let tabbarBackgroundView = RoundShadowView(frame: tabBar.frame)
tabbarBackgroundView.cornerRadius = 25
tabbarBackgroundView.backgroundColor = .white
tabbarBackgroundView.frame = tabBar.frame
view.addSubview(tabbarBackgroundView)
let fillerView = UIView()
fillerView.frame = tabBar.frame
fillerView.roundCorners([.topLeft, .topRight], radius: 25)
fillerView.backgroundColor = .white
view.addSubview(fillerView)
view.bringSubviewToFront(tabBar)
}
Rounded Shadow View
import UIKit
class RoundShadowView: UIView {
let containerView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
layoutView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layoutView() {
// set the shadow of the view's layer
layer.backgroundColor = UIColor.clear.cgColor
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0, height: -8.0)
layer.shadowOpacity = 0.12
layer.shadowRadius = 10.0
containerView.layer.cornerRadius = cornerRadius
containerView.layer.masksToBounds = true
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
// pin the containerView to the edges to the view
containerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
containerView.topAnchor.constraint(equalTo: topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
}
UIImage extension
import UIKit
extension UIImage {
static func from(color: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(color.cgColor)
context!.fill(rect)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!
}
}
To add any radius or shape you can use a UIBezierPath. The example that I put has left and right corners with a radius and you can use more designable personalizations if you want.
#IBDesignable class TabBarWithCorners: UITabBar {
#IBInspectable var color: UIColor?
#IBInspectable var radii: CGFLoat = 15.0
private var shapeLayer: CALayer?
override func draw(_ rect: CGRect) {
addShape()
}
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.gray.withAlphaComponent(0.1).cgColor
shapeLayer.fillColor = color?.cgColor ?? UIColor.white.cgColor
shapeLayer.lineWidth = 1
if let oldShapeLayer = self.shapeLayer {
layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
private func createPath() -> CGPath {
let path = UIBezierPath(
roundedRect: bounds,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: radii, height: 0.0))
return path.cgPath
}
}
Swift 5.3.1, XCode 11+, iOS 14
For using in storyboards:
import UIKit
class CustomTabBar: UITabBar {
override func awakeFromNib() {
super.awakeFromNib()
layer.masksToBounds = true
layer.cornerRadius = 20
layer.maskedCorners = [.layerMinXMinYCorner,.layerMaxXMinYCorner]
}
}
Subclassing UITabBarController then overried viewWillLayoutSubviews()
and add this code .
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.tabBar.layer.masksToBounds = true
self.tabBar.layer.cornerRadius = 12 // whatever you want
self.tabBar.layer.maskedCorners = [.layerMinXMinYCorner,.layerMaxXMinYCorner] // only the top right and left corners
}
This will be the result
I'm creating a custom, reusable segmented controller using UIViews and I'm having a problem with overlapping views. It currently looks like this:
You can see that the blue selector is under the buttons but I want it to sit at the bottom and be four pixels high. To do this, I have:
let numberOfButtons = CGFloat(buttonTitles.count)
let selectorWidth = frame.width / numberOfButtons
let selectorYPosition = frame.height - 3 <--- This cause it to be hidden behind the button
selector = UIView(frame: CGRect(x: 0, y: selectorYPosition, width: selectorWidth, height: 4))
selector.layer.cornerRadius = 0
selector.backgroundColor = selectorColor
addSubview(selector)
bringSubviewToFront(selector) <--- I thought this would work but it does nothing
which results in the selector UIView being hidden behind the segment UIView (I have the Y position set to - 3 so you can see how it's being covered up. I actually want it to be - 4, but that makes it disappear entirely):
I thought using bringSubviewToFront() would bring it in front of the segment UIView but it doesn't seem to do anything. I've looked through Apple View Programming Guide and lots of SO threads but can't find an answer.
Can anybody help me see what I'm missing?
Full code:
class CustomSegmentedControl: UIControl {
var buttons = [UIButton]()
var selector: UIView!
var selectedButtonIndex = 0
var borderWidth: CGFloat = 0 {
didSet {
layer.borderWidth = borderWidth
}
}
var borderColor: UIColor = UIColor.black {
didSet {
layer.borderColor = borderColor.cgColor
}
}
var separatorBorderColor: UIColor = UIColor.lightGray {
didSet {
}
}
var commaSeparatedTitles: String = "" {
didSet {
updateView()
}
}
var textColor: UIColor = .lightGray {
didSet {
updateView()
}
}
var selectorColor: UIColor = .blue {
didSet {
updateView()
}
}
var selectorTextColor: UIColor = .black {
didSet {
updateView()
}
}
func updateView() {
buttons.removeAll()
subviews.forEach { $0.removeFromSuperview() }
// create buttons
let buttonTitles = commaSeparatedTitles.components(separatedBy: ",")
for buttonTitle in buttonTitles {
let button = UIButton(type: .system)
button.setTitle(buttonTitle, for: .normal)
button.setTitleColor(textColor, for: .normal)
button.backgroundColor = UIColor.white
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
buttons.append(button)
}
// make first button selected
buttons[0].setTitleColor(selectorTextColor, for: .normal)
let numberOfButtons = CGFloat(buttonTitles.count)
let selectorWidth = frame.width / numberOfButtons
let selectorYPosition = frame.height - 3
selector = UIView(frame: CGRect(x: 0, y: selectorYPosition, width: selectorWidth, height: 4))
selector.layer.cornerRadius = 0
selector.backgroundColor = selectorColor
addSubview(selector)
bringSubviewToFront(selector)
let stackView = UIStackView(arrangedSubviews: buttons)
stackView.axis = .horizontal
stackView.alignment = .fill
stackView.distribution = .fillEqually
addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
stackView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
stackView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
}
#objc func buttonTapped(button: UIButton) {
for (buttonIndex, btn) in buttons.enumerated() {
btn.setTitleColor(textColor, for: .normal)
if btn == button {
let numberOfButtons = CGFloat(buttons.count)
let selectorStartPosition = frame.width / numberOfButtons * CGFloat(buttonIndex)
UIView.animate(withDuration: 0.3, animations: { self.selector.frame.origin.x = selectorStartPosition })
btn.setTitleColor(selectorTextColor, for: .normal)
}
}
sendActions(for: .valueChanged)
}
}
You are covering up your selector with the stackView.
You need to do:
bringSubviewToFront(selector)
after you have added all of the views. Move that line to the bottom of updateView().
I'm trying to implement a programmatic version of the Dynamic Stack View in Apple's Auto Layout Cookbook. An "Add Item" button is supposed to add new views to a vertical stackView, including a delete button to remove each view. My programmatic code works fine for 1 touch of the "Add Item" button, but then that button becomes inactive. As a result, I can only add 1 item to the stackView. If I used the delete button, the "Add Item" becomes active again. I've included an animated gif to illustrate.
I'm posting both my Programmatic code (which has the problem) and below that the original storyboard-based code (which works fine). I've tried putting a debug breakpoint at the addEntry func, but that didn't help. -Thanks
Programmatic Code ("Add Item" button only works once):
import UIKit
class CodeDynamStackVC: UIViewController {
// MARK: Properties
var scrollView = UIScrollView()
var stackView = UIStackView()
var button = UIButton()
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
// Set up the scrollview
let insets = UIEdgeInsets(top: 20, left: 0.0, bottom: 0.0, right: 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
setupInitialVertStackView()
}
//setup initial button inside vertical stackView
func setupInitialVertStackView() {
// make inital "Add Item" button
button = UIButton(type: .system)
button.setTitle("Add Item", for: .normal)
button.setTitleColor(UIColor.blue, for: .normal)
button.addTarget(self, action: #selector(addEntry), for: .touchUpInside)
//enclose button in a vertical stackView
stackView.addArrangedSubview(button)
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .equalSpacing
stackView.spacing = 5
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
let viewsDictionary = ["v0":stackView]
let stackView_H = NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewsDictionary)
let stackView_V = NSLayoutConstraint.constraints(withVisualFormat: "V:|-20-[v0(25)]|", options: NSLayoutFormatOptions(rawValue:0), metrics: nil, views: viewsDictionary)
view.addConstraints(stackView_H)
view.addConstraints(stackView_V)
}
// MARK: Interface Builder actions
func addEntry() {
guard let addButtonContainerView = stackView.arrangedSubviews.last else { fatalError("Expected at least one arranged view in the stack view.") }
let nextEntryIndex = stackView.arrangedSubviews.count - 1
let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + addButtonContainerView.bounds.size.height)
let newEntryView = createEntryView()
newEntryView.isHidden = true
stackView.insertArrangedSubview(newEntryView, at: nextEntryIndex)
UIView.animate(withDuration: 0.25, animations: {
newEntryView.isHidden = false
self.scrollView.contentOffset = offset
})
}
func deleteStackView(_ sender: UIButton) {
guard let entryView = sender.superview else { return }
UIView.animate(withDuration: 0.25, animations: {
entryView.isHidden = true
}, completion: { _ in
entryView.removeFromSuperview()
})
}
// MARK: Convenience
/// Creates a horizontal stackView entry to place within the parent vertical stackView
fileprivate func createEntryView() -> UIView {
let date = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)
let number = UUID().uuidString
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .fill
stack.spacing = 8
let dateLabel = UILabel()
dateLabel.text = date
dateLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
let numberLabel = UILabel()
numberLabel.text = number
numberLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption2)
numberLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow - 1.0, for: .horizontal)
numberLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh - 1.0, for: .horizontal)
let deleteButton = UIButton(type: .roundedRect)
deleteButton.setTitle("Del", for: UIControlState())
deleteButton.addTarget(self, action: #selector(DynamStackVC.deleteStackView(_:)), for: .touchUpInside)
stack.addArrangedSubview(dateLabel)
stack.addArrangedSubview(numberLabel)
stack.addArrangedSubview(deleteButton)
return stack
}
}
Storyboard-based Code ("Add Item" button always works)
import UIKit
class DynamStackVC: UIViewController {
// MARK: Properties
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var stackView: UIStackView!
// MARK: UIViewController
override func viewDidLoad() {
super.viewDidLoad()
// Set up the scrollview.
let insets = UIEdgeInsets(top: 20, left: 0.0, bottom: 0.0, right: 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
}
// MARK: Interface Builder actions
#IBAction func addEntry(_: AnyObject) {
guard let addButtonContainerView = stackView.arrangedSubviews.last else { fatalError("Expected at least one arranged view in the stack view.") }
let nextEntryIndex = stackView.arrangedSubviews.count - 1
let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + addButtonContainerView.bounds.size.height)
let newEntryView = createEntryView()
newEntryView.isHidden = true
stackView.insertArrangedSubview(newEntryView, at: nextEntryIndex)
UIView.animate(withDuration: 0.25, animations: {
newEntryView.isHidden = false
self.scrollView.contentOffset = offset
})
}
func deleteStackView(_ sender: UIButton) {
guard let entryView = sender.superview else { return }
UIView.animate(withDuration: 0.25, animations: {
entryView.isHidden = true
}, completion: { _ in
entryView.removeFromSuperview()
})
}
// MARK: Convenience
/// Creates a horizontal stack view entry to place within the parent `stackView`.
fileprivate func createEntryView() -> UIView {
let date = DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .none)
let number = UUID().uuidString
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.distribution = .fillProportionally
stack.spacing = 8
let dateLabel = UILabel()
dateLabel.text = date
dateLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
let numberLabel = UILabel()
numberLabel.text = number
numberLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.caption2)
numberLabel.setContentHuggingPriority(UILayoutPriorityDefaultLow - 1.0, for: .horizontal)
numberLabel.setContentCompressionResistancePriority(UILayoutPriorityDefaultHigh - 1.0, for: .horizontal)
let deleteButton = UIButton(type: .roundedRect)
deleteButton.setTitle("Del", for: UIControlState())
deleteButton.addTarget(self, action: #selector(DynamStackVC.deleteStackView(_:)), for: .touchUpInside)
stack.addArrangedSubview(dateLabel)
stack.addArrangedSubview(numberLabel)
stack.addArrangedSubview(deleteButton)
return stack
}
}
I figured it out by putting background colors on all buttons and labels within the dynamic stackView. As you can see in the new animated gif, the cyan color of the "Add Item" button disappears after the first button press. To confirm, I doubled the original height of the button (i.e., from (25) to (50) at the left in the gif), which then allowed for two button pressed before it no longer worked and the cyan background disappeared. This taught me a lot about how the dynamic stackView works, and I hope it will help someone else.