I'm trying to create the effect of a bar underneath the selected segment control. I'm creating an UIView that is attached to the bottom and moves depending of the selected element. However there's a bug that causes the bar to go back to the first element whenever you tap it for the first time.
I've tried both using auto layout and frames
#IBOutlet weak var segmentControl: UISegmentedControl!
fileprivate var buttonBar = UIView()
//viewDidLoad
buttonBar = UIView()
self.view.insertSubview(buttonBar, aboveSubview: segmentControl)
// This needs to be false since we are using auto layout constraints
buttonBar.translatesAutoresizingMaskIntoConstraints = false
buttonBar.backgroundColor = UIColor.white
buttonBar.topAnchor.constraint(equalTo: segmentControl.bottomAnchor).isActive = true
buttonBar.heightAnchor.constraint(equalToConstant: 2).isActive = true
// Constrain the button bar to the left side of the segmented control
buttonBar.leftAnchor.constraint(equalTo: segmentControl.leftAnchor).isActive = true
// Constrain the button bar to the width of the segmented control divided by the number of segments
buttonBar.widthAnchor.constraint(equalTo: segmentControl.widthAnchor, multiplier: 1 / CGFloat(segmentControl.numberOfSegments)).isActive = true
#objc func segmentedControlValueChanged(_ sender: UISegmentedControl) {
showSearchResults()
DispatchQueue.main.async {
UIView.animate(withDuration: 0.3) {
let segmentSize = (self.segmentControl.frame.width / CGFloat(self.segmentControl.numberOfSegments))
let segmentOrigin = self.segmentControl.frame.origin.x
let index = CGFloat(self.segmentControl.selectedSegmentIndex)
self.buttonBar.frame.origin.x = segmentOrigin + (segmentSize * index)
}
}
}
Related
Problem
I have a custom UIView that has an image and selection (border) subview. I want to be able to add this custom UIView as a subview of a larger blank view. Here's the catch, the larger blank view needs to clip all of the subviews to its bounds (clipToBounds). However, the user can select one of the custom UIViews within the large blank view, where the subview is then highlighted by a border.
The problem is that because the large blank view clips to bounds, the outline for the selected subview is cut off.
I want the image in the subview to clip to the bounds of the large blank view, but still be able to see the full selection outline of the subview (which is cut off due to the large blank view's corner radius.
I am using UIKit and Swift
👎 What I Currently Have:
👍 What I Want:
The image part of the subview clips to the bounds (corner radius) of the large blank view, but the outline selection view in the subview should not.
Thanks in advance for all your help!
I think what you are looking for is not technically possible as defined by the docs
From the docs:
clipsToBounds
Setting this value to true causes subviews to be clipped to the bounds of the receiver. If set to false, subviews whose frames extend beyond the visible bounds of the receiver are not clipped. The default value is false.
So the subviews do not have control of whether they get clipped or not, it's the container view that decides.
So I believe Matic's answer is right in that the structure he proposes gives you the most flexibility.
With that being said, here are a couple of work arounds I can think of:
First, set up to recreated your scenario
Custom UIView
// Simple custom UIView with image view and selection UIView
fileprivate class CustomBorderView: UIView
{
private var isSelected = false
{
willSet
{
toggleBorder(newValue)
}
}
var imageView = UIImageView()
var selectionView = UIView()
init()
{
super.init(frame: CGRect.zero)
configureImageView()
configureSelectionView()
}
required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews()
{
super.layoutSubviews()
}
private func configureImageView()
{
imageView.image = UIImage(named: "image-test")
imageView.contentMode = .scaleAspectFill
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
imageView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
private func configureSelectionView()
{
selectionView.backgroundColor = .clear
selectionView.layer.borderWidth = 3
selectionView.layer.borderColor = UIColor.clear.cgColor
addSubview(selectionView)
selectionView.translatesAutoresizingMaskIntoConstraints = false
selectionView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
selectionView.topAnchor.constraint(equalTo: topAnchor).isActive = true
selectionView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
selectionView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
configureTapGestureRecognizer()
}
private func configureTapGestureRecognizer()
{
let tapGesture = UITapGestureRecognizer(target: self,
action: #selector(didTapSelectionView))
selectionView.addGestureRecognizer(tapGesture)
}
#objc
private func didTapSelectionView()
{
isSelected = !isSelected
}
private func toggleBorder(_ on: Bool)
{
if on
{
selectionView.layer.borderColor = UIColor(red: 28.0/255.0,
green: 244.0/255.0,
blue: 162.0/255.0,
alpha: 1.0).cgColor
return
}
selectionView.layer.borderColor = UIColor.clear.cgColor
}
}
Then in the view controller
class ClippingTestViewController: UIViewController
{
private let mainContainerView = UIView()
private let customView = CustomBorderView()
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .white
title = "Clipping view"
configureMainContainerView()
configureCustomBorderView()
mainContainerView.layer.cornerRadius = 50
mainContainerView.clipsToBounds = true
}
private func configureMainContainerView()
{
mainContainerView.backgroundColor = .white
view.addSubview(mainContainerView)
mainContainerView.translatesAutoresizingMaskIntoConstraints = false
mainContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor,
constant: 20).isActive = true
mainContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
constant: 20).isActive = true
mainContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor,
constant: -20).isActive = true
mainContainerView.heightAnchor.constraint(equalToConstant: 300).isActive = true
view.layoutIfNeeded()
}
private func configureCustomBorderView()
{
mainContainerView.addSubview(customView)
customView.translatesAutoresizingMaskIntoConstraints = false
customView.leadingAnchor.constraint(equalTo: mainContainerView.leadingAnchor).isActive = true
customView.topAnchor.constraint(equalTo: mainContainerView.safeAreaLayoutGuide.topAnchor).isActive = true
customView.trailingAnchor.constraint(equalTo: mainContainerView.trailingAnchor).isActive = true
customView.bottomAnchor.constraint(equalTo: mainContainerView.bottomAnchor).isActive = true
view.layoutIfNeeded()
}
}
This gives me your current experience
Work Around 1. - Shrink subviews on selection
When the view is not selected, everything looks fine. When the view is selected, you could reduce the width and height of the custom subview with some animation while adding the border.
Work Around 2. - Manually clip desired subviews
You go through each subview in your container view and:
Apply the clipping to any subview you desire
Apply the corner radius to the views you clip
Leaving the container view unclipped and without a corner radius
To do that, I created a custom UIView subclass for the container view
class ClippingSubView: UIView
{
override var clipsToBounds: Bool
{
didSet
{
if clipsToBounds
{
clipsToBounds = false
clipImageViews(in: self)
layer.cornerRadius = 0
}
}
}
// Recursively go through all subviews
private func clipImageViews(in view: UIView)
{
for subview in view.subviews
{
// I am only checking image view, you could check which you want
if subview is UIImageView
{
print(layer.cornerRadius)
subview.layer.cornerRadius = layer.cornerRadius
subview.clipsToBounds = true
}
clipImageViews(in: subview)
}
}
}
Then make sure to adjust the following lines where you create your views:
let mainContainerView = ClippingSubView()
// Do this only after you have added all the subviews for this to work
mainContainerView.layer.cornerRadius = 50
mainContainerView.clipsToBounds = true
This gives me your desired output
This is a pretty common problem which may have multiple solutions. In the end though I always find it best to simply go one level higher:
ContainerView (Does not clip)
ContentView (Clips)
HighlightingView (Does not clip)
You would put all your current views on ContentView. Then introduce another view which represents your selection and put it on the same level as your ContentView.
In the end this will give you most flexibility. It can still get a bit more complicated when you add things like shadows. But again "more views" is usually the end solution.
You'll likely run into a lot of problems trying to get a subview's border to display outside its superView's clipping bounds.
One approach is to add an "Outline View" as a sibling of the "Clipping View":
When you select a clippingView's subview - and drag it around - set the frame of the outlineView to match the frame of that subview.
You'll want to set .isUserInteractionEnabled = false on the outlineView so it doesn't interfere with touches on the subviews.
So I found the following UI pattern online and I have been attempting to implement it in Xcode. However, I have been unsuccessful. I am unsure as to whether to create the optimal approach would be to
create three different UIViewControllers (in which case I am not sure as to how to get them to animate in and out of view/how to get them to overlap one another)
or to use a UITableView with custom overlapping cells. However, I am not sure whether this approach will allow me to animate properly upon pressing each cell. For this second approach, I saw this post, but this solution does not allow for touch interaction in the overlapping areas, something which I need.
I looked for libraries online that would allow for functionality such as this, but I was unsuccessful in finding any. The animation I am trying to achieve can be found here.
I would use a StackView to hold all the views of your ViewControllers.
Then you can set the stack view's spacing property to a negative value to make the views overlap. If you wish show the complete view of one of the view controllers on tap, you add a tap gesture recognizer to that view that changes the stackView's spacing for just that view to 0.
A really simple example:
class PlaygroundViewController: UIViewController {
let firstViewController = UIViewController()
let secondViewController = UIViewController()
let thirdViewController = UIViewController()
lazy var viewControllersToAdd = [firstViewController, secondViewController, thirdViewController]
let heightOfView: CGFloat = 300
let viewOverlap: CGFloat = 200
let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
firstViewController.view.backgroundColor = .red
secondViewController.view.backgroundColor = .blue
thirdViewController.view.backgroundColor = .green
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = -viewOverlap
viewControllersToAdd.forEach { (controller: UIViewController) in
if let childView = controller.view {
stackView.addArrangedSubview(childView)
NSLayoutConstraint.activate([
childView.heightAnchor.constraint(equalToConstant: heightOfView),
childView.widthAnchor.constraint(equalTo: stackView.widthAnchor)
])
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapChildView(sender:)))
childView.addGestureRecognizer(gestureRecognizer)
childView.isUserInteractionEnabled = true
}
addChild(controller)
controller.didMove(toParent: self)
}
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.rightAnchor.constraint(equalTo: view.rightAnchor),
stackView.leftAnchor.constraint(equalTo: view.leftAnchor),
stackView.topAnchor.constraint(equalTo: view.topAnchor),
])
}
#objc func didTapChildView(sender: UITapGestureRecognizer) {
if let targetView = sender.view {
UIView.animate(withDuration: 0.3, animations: {
let currentSpacing = self.stackView.customSpacing(after: targetView)
if currentSpacing == 0 {
// targetView is already expanded, collapse it
self.stackView.setCustomSpacing(-self.viewOverlap, after: targetView)
} else {
// expand view
self.stackView.setCustomSpacing(0, after: targetView)
}
})
}
}
}
I'm trying to set the origin and width/height of one UIView (red) to a second UIView (blue).
I am calling UIView.frame.origin or size and for some reason the y origin doesn't work.
I've also tried with layout constraints (see it commented out below), but this is overriding my blue fully constrained view.
Then I have a button that animates the red view to the side so you can see the blue view underneath, but I can't get them to line up to start with. Below is my code. In interface builder, I have both UIViews set up as containers. Blue is fully constrained with auto layout and red has no constraints.
import UIKit
class ViewController: UIViewController
{
#IBOutlet weak var blueContainer: UIView!
#IBOutlet weak var redContainer: UIView!
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
print(redContainer.frame)
redContainer.frame.origin.x = view.frame.width/2
redContainer.frame.size.width = view.frame.width
//try to line up y with origin and size
redContainer.frame.origin.y = blueContainer.frame.origin.y
redContainer.frame.size.height = blueContainer.frame.size.height
//also tried by using constraints
//redContainer.topAnchor.constraint(equalTo: blueContainer.topAnchor).isActive = true
//redContainer.heightAnchor.constraint(equalTo: blueContainer.heightAnchor).isActive = true
print(redContainer.frame)
}
#IBAction func slideRed(_ sender: Any) {
if redContainer.frame.origin.x == 0 {
UIView.animate(withDuration: 0.5) {
self.redContainer.frame.origin.x = self.view.frame.width/2
}
button.setTitle("Come Back Red!", for: .normal)
} else {
UIView.animate(withDuration: 0.5) {
self.redContainer.frame.origin.x = 0
}
button.setTitle("Go Away Red!", for: .normal)
}
}
}
ViewDidLoad does not guarantee the view has laid out its constraints. So when blueContainer's frame and size is zero, you will not see any effect on redContainer. You should use viewDidLayoutSubviews to get the correct frame and size from blueContainer.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
redContainer.frame.origin.x = view.frame.width/2
redContainer.frame.size.width = view.frame.width
//try to line up y with origin and size
redContainer.frame.origin.y = blueContainer.frame.origin.y
redContainer.frame.size.height = blueContainer.frame.size.height
}
I've got the following structure for example:
I want to rotate my label by 270degrees to achieve this:
via CGAffineTransform.rotated next way:
credentialsView.text = "Developed in EVNE Developers"
credentialsView.transform = credentialsView.transform.rotated(by: CGFloat(Double.pi / 2 * 3))
but instead of expected result i've got the following:
So, what is the correct way to rotate view without changing it's bounds to square or whatever it does, and keep leading 16px from edge of screen ?
I tried a lot of ways, including extending of UILabel to see rotation directly in storyboard, putted dat view in stackview with leading and it also doesn't helps, and etc.
Here is the solution which will rotate your label in an appropriate way forth and back to vertical-horizontal state. Before running the code, set constraints for your label in storyboard: leading to 16 and vertically centered.
Now check it out:
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
// Your leading constraint from storyboard, initially set to 16
#IBOutlet weak var leadingConstraint: NSLayoutConstraint!
var isHorizontal: Bool = true
var defaultLeftInset: CGFloat = 16.0
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
label.text = "This is my label"
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapAction)))
}
#objc func tapAction() {
if self.isHorizontal {
// Here goes some magic
// constraints do not depend on transform matrix,
// so we have to adjust a leading one to fit our requirements
leadingConstraint.constant = defaultLeftInset - label.frame.width/2 + label.frame.height/2
self.label.transform = CGAffineTransform(rotationAngle: .pi/2*3)
}
else {
leadingConstraint.constant = defaultLeftInset
self.label.transform = .identity
}
self.isHorizontal = !self.isHorizontal
}
}
Screenshot from twitter
I want this type of segmented control in Swift 4. I've researched Apple Documents but I couldn't find some of things which are I need such as removing borders, removing cornerRadius etc. How can I customize Segmented Control like Twitter's ? Or is there any another tab/segment solution ? Thanks.
Here is how to customize the UISegmentedControl to display a bottom border when it is selected:
Create a container view for the segmented control and pin it with Auto Layout to its super view.
Add a segmented control to the container view as a subview, and pin it with Auto Layout to the container view's edges.
Create a bottom underline view, add it as a subview to the container view, and apply Auto Layout to it (see example implementation).
Then set up an event listener: on the segmented control's value changed event, change the origin of the bottom underline view so that it is moved below the selected segment.
I also added some code on how to format the segmented control labels' font and text color, see it in the example below.
This is how it will look like:
Example implementation where the container and segmented control views are created programmatically:
Swift 4.2:
import UIKit
class ViewController: UIViewController {
private enum Constants {
static let segmentedControlHeight: CGFloat = 40
static let underlineViewColor: UIColor = .blue
static let underlineViewHeight: CGFloat = 2
}
// Container view of the segmented control
private lazy var segmentedControlContainerView: UIView = {
let containerView = UIView()
containerView.backgroundColor = .clear
containerView.translatesAutoresizingMaskIntoConstraints = false
return containerView
}()
// Customised segmented control
private lazy var segmentedControl: UISegmentedControl = {
let segmentedControl = UISegmentedControl()
// Remove background and divider colors
segmentedControl.backgroundColor = .clear
segmentedControl.tintColor = .clear
// Append segments
segmentedControl.insertSegment(withTitle: "First", at: 0, animated: true)
segmentedControl.insertSegment(withTitle: "Second", at: 1, animated: true)
segmentedControl.insertSegment(withTitle: "Third", at: 2, animated: true)
// Select first segment by default
segmentedControl.selectedSegmentIndex = 0
// Change text color and the font of the NOT selected (normal) segment
segmentedControl.setTitleTextAttributes([
NSAttributedStringKey.foregroundColor: UIColor.black,
NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16, weight: .regular)], for: .normal)
// Change text color and the font of the selected segment
segmentedControl.setTitleTextAttributes([
NSAttributedStringKey.foregroundColor: UIColor.blue,
NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16, weight: .bold)], for: .selected)
// Set up event handler to get notified when the selected segment changes
segmentedControl.addTarget(self, action: #selector(segmentedControlValueChanged), for: .valueChanged)
// Return false because we will set the constraints with Auto Layout
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
return segmentedControl
}()
// The underline view below the segmented control
private lazy var bottomUnderlineView: UIView = {
let underlineView = UIView()
underlineView.backgroundColor = Constants.underlineViewColor
underlineView.translatesAutoresizingMaskIntoConstraints = false
return underlineView
}()
private lazy var leadingDistanceConstraint: NSLayoutConstraint = {
return bottomUnderlineView.leftAnchor.constraint(equalTo: segmentedControl.leftAnchor)
}()
override func viewDidLoad() {
super.viewDidLoad()
// Add subviews to the view hierarchy
// (both segmentedControl and bottomUnderlineView are subviews of the segmentedControlContainerView)
view.addSubview(segmentedControlContainerView)
segmentedControlContainerView.addSubview(segmentedControl)
segmentedControlContainerView.addSubview(bottomUnderlineView)
// Constrain the container view to the view controller
let safeLayoutGuide = self.view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
segmentedControlContainerView.topAnchor.constraint(equalTo: safeLayoutGuide.topAnchor),
segmentedControlContainerView.leadingAnchor.constraint(equalTo: safeLayoutGuide.leadingAnchor),
segmentedControlContainerView.widthAnchor.constraint(equalTo: safeLayoutGuide.widthAnchor),
segmentedControlContainerView.heightAnchor.constraint(equalToConstant: Constants.segmentedControlHeight)
])
// Constrain the segmented control to the container view
NSLayoutConstraint.activate([
segmentedControl.topAnchor.constraint(equalTo: segmentedControlContainerView.topAnchor),
segmentedControl.leadingAnchor.constraint(equalTo: segmentedControlContainerView.leadingAnchor),
segmentedControl.centerXAnchor.constraint(equalTo: segmentedControlContainerView.centerXAnchor),
segmentedControl.centerYAnchor.constraint(equalTo: segmentedControlContainerView.centerYAnchor)
])
// Constrain the underline view relative to the segmented control
NSLayoutConstraint.activate([
bottomUnderlineView.bottomAnchor.constraint(equalTo: segmentedControl.bottomAnchor),
bottomUnderlineView.heightAnchor.constraint(equalToConstant: Constants.underlineViewHeight),
leadingDistanceConstraint,
bottomUnderlineView.widthAnchor.constraint(equalTo: segmentedControl.widthAnchor, multiplier: 1 / CGFloat(segmentedControl.numberOfSegments))
])
}
#objc private func segmentedControlValueChanged(_ sender: UISegmentedControl) {
changeSegmentedControlLinePosition()
}
// Change position of the underline
private func changeSegmentedControlLinePosition() {
let segmentIndex = CGFloat(segmentedControl.selectedSegmentIndex)
let segmentWidth = segmentedControl.frame.width / CGFloat(segmentedControl.numberOfSegments)
let leadingDistance = segmentWidth * segmentIndex
UIView.animate(withDuration: 0.3, animations: { [weak self] in
self?.leadingDistanceConstraint.constant = leadingDistance
self?.layoutIfNeeded()
})
}
}
You may use part of CarbonKit. It has TabBar like you want. But it is necessary to code analyzing a little.
There is class CarbonTabSwipeSegmentedControl. There is property UIImageView indicator with tint color background (line 41) that draws upside or at bottom of string of segment. Also there is function updateIndicatorWithAnimation that resizes segment line. Also there is variable that helps draw and control this drawing.
I don't sure but you may simply include this class to your project and use it even in swift.