Sizing UIButton depending on length of titleLabel - swift

So I have a UIButton and I'm setting the title in it to a string that is dynamic in length. I want the width of the titleLabel to be half of the screen width. I've tried using .sizeToFit() but this causes the button to use the CGSize before the constraint was applied to the titleLabel. I tried using .sizeThatFits(button.titleLabel?.intrinsicContentSize) but this also didn't work. I think the important functions below are the init() & presentCallout(), but I'm showing the entire class just for a more complete understanding. The class I'm playing with looks like:
class CustomCalloutView: UIView, MGLCalloutView {
var representedObject: MGLAnnotation
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
required init(representedObject: MGLAnnotation) {
self.representedObject = representedObject
self.mainBody = UIButton(type: .system)
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
// I thought this would work, but it doesn't.
// mainBody.translatesAutoresizingMaskIntoConstraints = false
// mainBody.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
// mainBody.leftAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// mainBody.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// mainBody.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
delegate?.calloutViewWillAppear?(self)
view.addSubview(self)
// Prepare title label.
mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.titleLabel?.lineBreakMode = .byWordWrapping
mainBody.titleLabel?.numberOfLines = 0
mainBody.sizeToFit()
if isCalloutTappable() {
// Handle taps and eventually try to send them to the delegate (usually the map view).
mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
} else {
// Disable tapping and highlighting.
mainBody.isUserInteractionEnabled = false
}
// Prepare our frame, adding extra space at the bottom for the tip.
let frameWidth = mainBody.bounds.size.width
let frameHeight = mainBody.bounds.size.height + tipHeight
let frameOriginX = rect.origin.x + (rect.size.width/2.0) - (frameWidth/2.0)
let frameOriginY = rect.origin.y - frameHeight
frame = CGRect(x: frameOriginX, y: frameOriginY, width: frameWidth, height: frameHeight)
if animated {
alpha = 0
UIView.animate(withDuration: 0.2) { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.alpha = 1
strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
delegate?.calloutViewDidAppear?(self)
}
}
func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.alpha = 0
}, completion: { [weak self] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}
// MARK: - Callout interaction handlers
func isCalloutTappable() -> Bool {
if let delegate = delegate {
if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
return delegate.calloutViewShouldHighlight!(self)
}
}
return false
}
#objc func calloutTapped() {
if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
delegate!.calloutViewTapped!(self)
}
}
// MARK: - Custom view styling
override func draw(_ rect: CGRect) {
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .white
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
This is what it looks like for a short title and a long title. When the title gets too long, I want the text to wrap and the bubble to get a taller height. As you can see in the image set below, the first 'Short Name' works fine as a map annotation bubble. When the name gets super long though, it just widens the bubble to the point it goes off the screen.
https://imgur.com/a/I5z0zUd
Any help on how to fix is much appreciated. Thanks!

To enable word-wrapping to multiple lines in a UIButton, you need to create your own button subclass.
For example:
class MultilineTitleButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() -> Void {
self.titleLabel?.numberOfLines = 0
self.titleLabel?.textAlignment = .center
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .vertical)
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .horizontal)
}
override var intrinsicContentSize: CGSize {
let size = self.titleLabel!.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
That button will wrap the title onto multiple lines, cooperating with auto-layout / constraints.
I don't have any projects with MapBox, but here is an example using a modified version of your CustomCalloutView. I commented out any MapBox specific code. You may be able to un-comment those lines and use this as-is:
class CustomCalloutView: UIView { //}, MGLCalloutView {
//var representedObject: MGLAnnotation
var repTitle: String = ""
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
// NOTE: this causes a vertical shift when NOT using MapBox
// override var center: CGPoint {
// set {
// var newCenter = newValue
// newCenter.y -= bounds.midY
// super.center = newCenter
// }
// get {
// return super.center
// }
// }
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
//weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
var anchorView: UIView!
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
anchorView.removeFromSuperview()
}
}
//required init(representedObject: MGLAnnotation) {
required init(title: String) {
self.repTitle = title
self.mainBody = MultilineTitleButton()
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.setTitleColor(.black, for: [])
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
mainBody.translatesAutoresizingMaskIntoConstraints = false
let padding: CGFloat = 8.0
NSLayoutConstraint.activate([
mainBody.topAnchor.constraint(equalTo: self.topAnchor, constant: padding),
mainBody.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding),
mainBody.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -padding),
mainBody.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding),
])
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
//delegate?.calloutViewWillAppear?(self)
// since we'll be using auto-layout for the mutli-line button
// we'll add an "anchor view" to the superview
// it will be removed when self is removed
anchorView = UIView(frame: rect)
anchorView.isUserInteractionEnabled = false
anchorView.backgroundColor = .clear
view.addSubview(anchorView)
view.addSubview(self)
// Prepare title label.
//mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.setTitle(self.repTitle, for: .normal)
// if isCalloutTappable() {
// // Handle taps and eventually try to send them to the delegate (usually the map view).
// mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
// } else {
// // Disable tapping and highlighting.
// mainBody.isUserInteractionEnabled = false
// }
self.translatesAutoresizingMaskIntoConstraints = false
anchorView.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin]
NSLayoutConstraint.activate([
self.centerXAnchor.constraint(equalTo: anchorView.centerXAnchor),
self.bottomAnchor.constraint(equalTo: anchorView.topAnchor),
self.widthAnchor.constraint(lessThanOrEqualToConstant: constrainedRect.width),
])
if animated {
alpha = 0
UIView.animate(withDuration: 0.2) { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.alpha = 1
//strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
//delegate?.calloutViewDidAppear?(self)
}
}
func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.alpha = 0
}, completion: { [weak self] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}
// MARK: - Callout interaction handlers
// func isCalloutTappable() -> Bool {
// if let delegate = delegate {
// if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
// return delegate.calloutViewShouldHighlight!(self)
// }
// }
// return false
// }
//
// #objc func calloutTapped() {
// if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
// delegate!.calloutViewTapped!(self)
// }
// }
// MARK: - Custom view styling
override func draw(_ rect: CGRect) {
print(#function)
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .red
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
Here is a sample view controller showing that "Callout View" with various length titles, restricted to 70% of the width of the view:
class CalloutTestVC: UIViewController {
let sampleTitles: [String] = [
"Short Title",
"Slightly Longer Title",
"A ridiculously long title that will need to wrap!",
]
var idx: Int = -1
let tapView = UIView()
var ccv: CustomCalloutView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.8939146399, green: 0.8417750597, blue: 0.7458069921, alpha: 1)
tapView.backgroundColor = .systemBlue
tapView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tapView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tapView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
tapView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
tapView.widthAnchor.constraint(equalToConstant: 60),
tapView.heightAnchor.constraint(equalTo: tapView.widthAnchor),
])
// tap the Blue View to cycle through Sample Titles for the Callout View
// using the Blue view as the "anchor rect"
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap))
tapView.addGestureRecognizer(t)
}
#objc func gotTap() -> Void {
if ccv != nil {
ccv.removeFromSuperview()
}
// increment sampleTitles array index
// to cycle through the strings
idx += 1
let validIdx = idx % sampleTitles.count
let str = sampleTitles[validIdx]
// create a new Callout view
ccv = CustomCalloutView(title: str)
// to restrict the "callout view" width to less-than 1/2 the screen width
// use view.width * 0.5 for the constrainedTo width
// may look better restricting it to 70%
ccv.presentCallout(from: tapView.frame, in: self.view, constrainedTo: CGRect(x: 0, y: 0, width: view.frame.size.width * 0.7, height: 100), animated: false)
}
}
It looks like this:

The UIButton class owns the titleLabel and is going to position and set the constraints on that label itself. More likely than not you are going to have to create a subclass of UIButton and override its "updateConstraints" method to position the titleLabel where you want it to go.
Your code should probably not be basing the size of the button off the size of the screen. It might set the size of off some other view in your hierarchy that happens to be the size of the screen but grabbing the screen bounds in the middle of setting a view's size is unusual.

Related

How to fixed vertical positioning glitch in Mapbox callout

So I have a custom AnnotationView class (what the pin itself looks like) and a custom CalloutView class (what the bubble that appears above it looks like) For some reason, I get this odd behavior where if I press the annotation, it selects and appears properly (as seen in the first picture), but then if I deselect the annotation and reselect it (without moving the map at all), the callout reappears in the wrong positioning. The problem appears to be the rect.origin.y value, but I'm not sure--I tried hardcoding it but the problem still persists. In the video, I click my mouse down and it causes the annotation to move up. Any help much appreciated!
class AnnotationView: MGLAnnotationView {
weak var delegate: MapAnnotationDelegate?
var pointAnnotation: MGLPointAnnotation?
required override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
if reuseIdentifier == "default" {
super.centerOffset = CGVector(dx: 0, dy: -17.5)
}
let rect = CGRect(x: 0, y: 0, width: 35, height: 35)
let button = UIButton(frame: rect)
let image = UIImage(named: reuseIdentifier ?? "default")?.resize(targetSize: CGSize(width: 35, height: 35))
button.setImage(image, for: .normal)
button.setImage(image, for: .selected)
button.setImage(image, for: .highlighted)
button.imageView?.contentMode = .scaleAspectFit
button.addTarget(self, action: #selector(annotationAction), for: .touchUpInside)
frame = rect
addSubview(button)
isEnabled = false
}
required init?(coder: NSCoder) {
return nil
}
#objc private func annotationAction() {
delegate?.tappedAnnotation(annotation: pointAnnotation!)
}
}
class CustomCalloutView: UIView, MGLCalloutView {
var representedObject: MGLAnnotation
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
required init(representedObject: MGLAnnotation) {
self.representedObject = representedObject
self.mainBody = UIButton(type: .system)
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
delegate?.calloutViewWillAppear?(self)
view.addSubview(self)
// Prepare title label.
mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.sizeToFit()
mainBody.backgroundColor = .red
if isCalloutTappable() {
// Handle taps and eventually try to send them to the delegate (usually the map view).
mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
} else {
// Disable tapping and highlighting.
mainBody.isUserInteractionEnabled = false
}
// Prepare our frame, adding extra space at the bottom for the tip.
let frameWidth = mainBody.bounds.size.width
let frameHeight = mainBody.bounds.size.height + tipHeight
let frameOriginX = rect.origin.x + (rect.size.width/2.0) - (frameWidth/2.0)
let frameOriginY = rect.origin.y - frameHeight
frame = CGRect(x: frameOriginX, y: frameOriginY, width: frameWidth, height: frameHeight)
if animated {
alpha = 0
UIView.animate(withDuration: 0.2) { \[weak self\] in
guard let strongSelf = self else {
return
}
strongSelf.alpha = 1
strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
delegate?.calloutViewDidAppear?(self)
}
}
func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { \[weak self\] in
self?.alpha = 0
}, completion: { \[weak self\] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}
// MARK: - Callout interaction handlers
func isCalloutTappable() -> Bool {
if let delegate = delegate {
if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
return delegate.calloutViewShouldHighlight!(self)
}
}
return false
}
#objc func calloutTapped() {
if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
delegate!.calloutViewTapped!(self)
}
}
// MARK: - Custom view styling
override func draw(_ rect: CGRect) {
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .white
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
For context, the relevant Mapbox map delegate functions are:
func tappedAnnotation(annotation: MGLPointAnnotation) {
let selectedAnnotation = control.mapView.selectedAnnotations.first
guard control.mapView.annotations != nil, selectedAnnotation !== annotation else { return }
//control is the actual map being passed into this coordinator function
for annotation in control.mapView.annotations! {
if annotation.subtitle == "PrimaryAnno" {
control.mapView.removeAnnotation(annotation)
}
}
control.mapView.selectAnnotation(annotation, animated: false, completionHandler: nil)
}
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if annotation is MGLPointAnnotation {
var reuseid = ""
//I use the subtitles to indicate which image I use for my annotations
if annotation.subtitle! == "primaryAnno" {
reuseid = "default"
} else {
reuseid = annotation.subtitle!
}
if let reusable = mapView.dequeueReusableAnnotationView(withIdentifier: reuseid) as? AnnotationView {
reusable.pointAnnotation = annotation as? MGLPointAnnotation
return reusable
} else {
let new = AnnotationView(reuseIdentifier: reuseid)
new.delegate = self
new.pointAnnotation = annotation as? MGLPointAnnotation
return new
}
} else {
return nil
}
}
And this is a protocol I use to define the tapping on an annotation:
protocol MapAnnotationDelegate: AnyObject {
func tappedAnnotation(annotation: MGLPointAnnotation)
}
UPDATE: I figured out that the 'correction' happens when I click on the map or change the region, so the issue is with the initial tap on annotation. I've tried setting a breakpoint in the tappedAnnotation function and it seems like once that function ends (it subsequently redirects to the annotationAction() function), the callout just renders in the wrong position.
https://youtu.be/4x5o9T6p8aw

align object with changeable constraints to center x point on the view

I want my swift code below to align the box to the center of the x axis. You can see from the gif below of what my code is doing. When the purple button is pressed I would like the box to be align. I am not sure how to d this because some of the constraints are declared as var I dont know where to go next.
import UIKit
class ViewController: UIViewController {
var slizer = UISlider()
var viewDrag = UIImageView()
var b2 = UIButton()
var panGesture = UIPanGestureRecognizer()
// Width, Leading and CenterY constraints for viewDrag
var widthConstraints: NSLayoutConstraint!
var viewDragLeadingConstraint: NSLayoutConstraint!
var viewDragCenterYConstraint: NSLayoutConstraint!
var tim: CGFloat = 50.0
var slidermultiplier: CGFloat = 0.6
override func viewDidLoad() {
super.viewDidLoad()
[viewDrag,slizer,b2].forEach{
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
b2.backgroundColor = .purple
NSLayoutConstraint.activate([
b2.bottomAnchor.constraint(equalTo: slizer.topAnchor),
b2.leadingAnchor.constraint(equalTo: view.leadingAnchor),
b2.heightAnchor.constraint(equalTo: view.heightAnchor,multiplier: 0.05),
b2.widthAnchor.constraint(equalTo: view.widthAnchor,multiplier: 1),
slizer.bottomAnchor.constraint(equalTo: view.bottomAnchor),
slizer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
slizer.heightAnchor.constraint(equalTo: view.heightAnchor,multiplier: 0.2),
slizer.widthAnchor.constraint(equalTo: view.widthAnchor,multiplier: 1),
])
slizer.addTarget(self, action: #selector(increase), for: .valueChanged)
viewDrag.backgroundColor = .orange
// no point setting a frame, since
// viewDrag has .translatesAutoresizingMaskIntoConstraints = false
//viewDrag.frame = CGRect(x: view.center.x-view.frame.width * 0.05, y: view.center.y-view.frame.height * 0.05, width: view.frame.width * 0.1, height: view.frame.height * 0.1)
// start with viewDrag
// width = "slidermultiplier" percent of view width
widthConstraints = viewDrag.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: slidermultiplier)
// Leading = "tim" pts from view leading
viewDragLeadingConstraint = viewDrag.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: tim)
// centered vertically
viewDragCenterYConstraint = viewDrag.centerYAnchor.constraint(equalTo: view.centerYAnchor)
NSLayoutConstraint.activate([
// viewDrag height will never change, so we can set it here
viewDrag.heightAnchor.constraint(equalTo: view.heightAnchor,multiplier: 0.3),
// activate the 3 "modifiable" constraints
widthConstraints,
viewDragLeadingConstraint,
viewDragCenterYConstraint,
])
panGesture = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
viewDrag.isUserInteractionEnabled = true
viewDrag.addGestureRecognizer(panGesture)
// start the slider at the same percentage we've used
// for viewDrag's initial width
slizer.value = Float(slidermultiplier)
b2.addTarget(self, action: #selector(press), for: .touchDown)
}
#objc func press(){
}
#objc func draggedView(_ sender: UIPanGestureRecognizer) {
// old swift syntax
//self.view.bringSubview(toFront: viewDrag)
self.view.bringSubview(toFront: viewDrag)
let translation = sender.translation(in: self.view)
viewDragLeadingConstraint.constant += translation.x
viewDragCenterYConstraint.constant += translation.y
// don't do this
//viewDrag.center = CGPoint(x: viewDrag.center.x + translation.x , y: viewDrag.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
#objc func increase() {
// get the new value of the slider
slidermultiplier = CGFloat(slizer.value)
// deactivate widthConstraints
widthConstraints.isActive = false
// create new widthConstraints with slider value as a multiplier
widthConstraints = viewDrag.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: slidermultiplier)
// activate the new widthConstraints
widthConstraints.isActive = true
}
}
I recommend to work directly with frame not constraints.
Constraints are suitable when to deal with mutable screen sizes and screen rotation.
Step 1: Put your movable objects into a container view
Step 2: Handle objects by coordinate inside container view
Step 3: Add container to your view controller's view and constraint layouting it with other elements
class ViewController: UIViewController {
#IBOutlet weak var container: Container!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
container.addPanGestures()
}
#IBAction func didTapLeftButton(_ sender: Any) {
container.setLeftAlign()
}
#IBAction func didTapCenterButton(_ sender: Any) {
container.setCenterXAlign()
}
#IBAction func didTapRightButton(_ sender: Any) {
container.setRightAlign()
}
#IBAction func slide(_ sender: UISlider) {
let scale = CGFloat(max(sender.value, 0.1))
container.setWidthScale(scale)
}
}
class Container: UIView {
func setLeftAlign() {
for view in self.subviews {
view.frame.origin = CGPoint(x: 0, y: view.frame.origin.y)
}
}
func setRightAlign() {
for view in self.subviews {
view.frame.origin = CGPoint(x: self.frame.width - view.frame.width, y:view.frame.origin.y)
}
}
func setCenterXAlign() {
for view in self.subviews {
var center = view.center
center.x = self.frame.width / 2
view.center = center
}
}
func setWidthScale(_ ratio: CGFloat) {
for view in self.subviews {
var frame = view.frame
let center = view.center
frame.size.width = self.frame.width * ratio
view.frame = frame
view.center = center
}
}
func addPanGestures() {
for v in self.subviews {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
v.addGestureRecognizer(panGesture)
}
}
private var initCenter: CGPoint!
#objc func didPan(_ sender: UIPanGestureRecognizer) {
let view = sender.view!
let translation = sender.translation(in: view)
switch sender.state {
case .began:
initCenter = view.center
case .changed:
view.center = CGPoint(x: initCenter.x + translation.x, y: initCenter.y + translation.y)
case .cancelled:
view.center = initCenter
default:
return
}
}
}

Adding a partial mask over an UIImageView

I want to add a 0.5 alpha mask over just one part of an image (that I will calculate in code).
Basically, it's a 5-star rating control, but the stars are not one color, but some nice images like this:
The image has a transparent background that I need to respect. So I'd like to be able to add a mask or to somehow set the alpha of just half of the image for example, when your rating is 3.5. (2 full stars and one with half of it with less alpha)
I can't just put a UIView over it with 0.5 alpha, because that will also impact with the background where the stars are displayed.
Any ideas?
You can use a CAGradientLayer as a mask:
gLayer.startPoint = CGPoint.zero
gLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
gLayer.locations = [
0.0, 0.5, 0.5, 1.0,
]
gLayer.colors = [
UIColor.black.cgColor,
UIColor.black.cgColor,
UIColor.black.withAlphaComponent(0.5).cgColor,
UIColor.black.withAlphaComponent(0.5).cgColor,
]
This would create a horizontal gradient, with the left half full alpha and the right half 50% alpha.
So, a white view with this as a mask would look like this:
If we set the image to your star, it looks like this:
If we want the star to be "75% filled" we change the locations:
gLayer.locations = [
0.0, 0.75, 0.75, 1.0,
]
resulting in:
Here is an example implementation for a "Five Star" rating view:
#IBDesignable
class FiveStarRatingView: UIView {
#IBInspectable
public var rating: CGFloat = 0.0 {
didSet {
var r = rating
stack.arrangedSubviews.forEach {
if let v = $0 as? PercentImageView {
v.percent = min(1.0, r)
r -= 1.0
}
}
}
}
#IBInspectable
public var ratingImage: UIImage = UIImage() {
didSet {
stack.arrangedSubviews.forEach {
if let v = $0 as? PercentImageView {
v.image = ratingImage
}
}
}
}
#IBInspectable
public var tranparency: CGFloat = 0.5 {
didSet {
stack.arrangedSubviews.forEach {
if let v = $0 as? PercentImageView {
v.tranparency = tranparency
}
}
}
}
override var intrinsicContentSize: CGSize {
return CGSize(width: 100.0, height: 20.0)
}
private let stack: UIStackView = {
let v = UIStackView()
v.axis = .horizontal
v.alignment = .center
v.distribution = .fillEqually
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() -> Void {
addSubview(stack)
// constrain stack view to all 4 sides
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: topAnchor),
stack.leadingAnchor.constraint(equalTo: leadingAnchor),
stack.trailingAnchor.constraint(equalTo: trailingAnchor),
stack.bottomAnchor.constraint(equalTo: bottomAnchor),
])
// add 5 Percent Image Views to the stack view
for _ in 1...5 {
let v = PercentImageView(frame: .zero)
stack.addArrangedSubview(v)
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
}
}
private class PercentImageView: UIImageView {
var percent: CGFloat = 0.0 {
didSet {
setNeedsLayout()
}
}
var tranparency: CGFloat = 0.5 {
didSet {
setNeedsLayout()
}
}
private let gLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
gLayer.startPoint = CGPoint.zero
gLayer.endPoint = CGPoint(x: 1.0, y: 0.0)
layer.mask = gLayer
}
override func layoutSubviews() {
super.layoutSubviews()
// we don't want the layer's intrinsic animation
CATransaction.begin()
CATransaction.setDisableActions(true)
gLayer.frame = bounds
gLayer.locations = [
0.0, percent as NSNumber, percent as NSNumber, 1.0,
]
gLayer.colors = [
UIColor.black.cgColor,
UIColor.black.cgColor,
UIColor.black.withAlphaComponent(tranparency).cgColor,
UIColor.black.withAlphaComponent(tranparency).cgColor,
]
CATransaction.commit()
}
}
}
class StarRatingViewController: UIViewController {
let ratingView = FiveStarRatingView()
let slider = UISlider()
let valueLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
guard let starImage = UIImage(named: "star") else {
fatalError("Could not load image named \"star\"")
}
// add a slider and a couple labels so we can change the rating
let minLabel = UILabel()
let maxLabel = UILabel()
[slider, valueLabel, minLabel, maxLabel].forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
if let v = $0 as? UILabel {
v.textAlignment = .center
}
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
valueLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
valueLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
slider.topAnchor.constraint(equalTo: valueLabel.bottomAnchor, constant: 8.0),
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 32.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -32.0),
minLabel.topAnchor.constraint(equalTo: slider.bottomAnchor, constant: 8.0),
minLabel.centerXAnchor.constraint(equalTo: slider.leadingAnchor, constant: 0.0),
maxLabel.topAnchor.constraint(equalTo: slider.bottomAnchor, constant: 8.0),
maxLabel.centerXAnchor.constraint(equalTo: slider.trailingAnchor, constant: 0.0),
])
minLabel.text = "0"
maxLabel.text = "5"
ratingView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(ratingView)
NSLayoutConstraint.activate([
// constrain the rating view centered in the view
// 300-pts wide
// height will be auto-set by the rating view
ratingView.topAnchor.constraint(equalTo: minLabel.bottomAnchor, constant: 20.0),
ratingView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
ratingView.widthAnchor.constraint(equalToConstant: 240.0),
])
// use the star image
ratingView.ratingImage = starImage
// start at rating of 0 stars
updateValue(0.0)
slider.value = 0
slider.addTarget(self, action: #selector(self.sliderChanged(_:)), for: .valueChanged)
}
#objc func sliderChanged(_ sender: UISlider) {
// round the slider value to 2 decimal places
updateValue((sender.value * 5.0).rounded(digits: 2))
}
func updateValue(_ v: Float) -> Void {
valueLabel.text = String(format: "%.2f", v)
ratingView.rating = CGFloat(v)
}
}
extension Float {
func rounded(digits: Int) -> Float {
let multiplier = Float(pow(10.0, Double(digits)))
return (self * multiplier).rounded() / multiplier
}
}
Result:
Note that the FiveStarRatingView class is marked #IBDesignable so you can add it in Storyboard / IB and set image, amount of transparency and rating at design-time.

How can I position these UIView elements from the right using CGRect to position

I have a UIView sub class that allows me to create a group of 'tags' for the footer of some content. At the moment however they are position aligned to the left edge, I would like them to be positioned from the right.
I have included a playground below that should run the screen shot you can see.
The position is set within the layoutSubviews method of CloudTagView.
I tried to play around with their position but have not been able to start them from the right however.
import UIKit
import PlaygroundSupport
// CLOUD VIEW WRAPPER - THIS IS THE CONTAINER FOR THE TAGS AND SETS UP THEIR FRAME
class CloudTagView: UIView {
weak var delegate: TagViewDelegate?
override var intrinsicContentSize: CGSize {
return frame.size
}
var removeOnDismiss = true
var resizeToFit = true
var tags = [TagView]() {
didSet {
layoutSubviews()
}
}
var padding = 5 {
didSet {
layoutSubviews()
}
}
var maxLengthPerTag = 0 {
didSet {
layoutSubviews()
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
clipsToBounds = true
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isUserInteractionEnabled = true
clipsToBounds = true
}
override func layoutSubviews() {
for tag in subviews {
tag.removeFromSuperview()
}
var xAxis = padding
var yAxis = padding
var maxHeight = 0
for (index, tag) in tags.enumerated() {
setMaxLengthIfNeededIn(tag)
tag.delegate = self
if index == 0 {
maxHeight = Int(tag.frame.height)
}else{
let expectedWidth = xAxis + Int(tag.frame.width) + padding
if expectedWidth > Int(frame.width) {
yAxis += maxHeight + padding
xAxis = padding
maxHeight = Int(tag.frame.height)
}
if Int(tag.frame.height) > maxHeight {
maxHeight = Int(tag.frame.height)
}
}
tag.frame = CGRect(x: xAxis, y: yAxis, width: Int(tag.frame.size.width), height: Int(tag.frame.size.height))
addSubview(tag)
tag.layoutIfNeeded()
xAxis += Int(tag.frame.width) + padding
}
if resizeToFit {
frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.size.width, height: CGFloat(yAxis + maxHeight + padding))
}
}
// MARK: Methods
fileprivate func setMaxLengthIfNeededIn(_ tag: TagView) {
if maxLengthPerTag > 0 && tag.maxLength != maxLengthPerTag {
tag.maxLength = maxLengthPerTag
}
}
}
// EVERYTHING BELOW HERE IS JUST SETUP / REQUIRED TO RUN IN PLAYGROUND
class ViewController:UIViewController{
let cloudView: CloudTagView = {
let view = CloudTagView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
let tags = ["these", "are", "my", "tags"]
tags.forEach { tag in
let t = TagView(text: tag)
t.backgroundColor = .darkGray
t.tintColor = .white
cloudView.tags.append(t)
}
view.backgroundColor = .white
view.addSubview(cloudView)
NSLayoutConstraint.activate([
cloudView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
cloudView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
cloudView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
cloudView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])
}
}
// Tag View
class TagView: UIView {
weak var delegate: TagViewDelegate?
var text = "" {
didSet {
layoutSubviews()
}
}
var marginTop = 5 {
didSet {
layoutSubviews()
}
}
var marginLeft = 10 {
didSet {
layoutSubviews()
}
}
var iconImage = UIImage(named: "close_tag_2", in: Bundle(for: CloudTagView.self), compatibleWith: nil) {
didSet {
layoutSubviews()
}
}
var maxLength = 0 {
didSet {
layoutSubviews()
}
}
override var backgroundColor: UIColor? {
didSet {
layoutSubviews()
}
}
override var tintColor: UIColor? {
didSet {
layoutSubviews()
}
}
var font: UIFont = UIFont.systemFont(ofSize: 12) {
didSet {
layoutSubviews()
}
}
fileprivate let dismissView: UIView
fileprivate let icon: UIImageView
fileprivate let textLabel: UILabel
public override init(frame: CGRect) {
dismissView = UIView()
icon = UIImageView()
textLabel = UILabel()
super.init(frame: frame)
isUserInteractionEnabled = true
addSubview(textLabel)
addSubview(icon)
addSubview(dismissView)
dismissView.isUserInteractionEnabled = true
textLabel.isUserInteractionEnabled = true
dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.iconTapped)))
textLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.labelTapped)))
backgroundColor = UIColor(white: 0.0, alpha: 0.6)
tintColor = UIColor.white
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public init(text: String) {
dismissView = UIView()
icon = UIImageView()
textLabel = UILabel()
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
isUserInteractionEnabled = true
addSubview(textLabel)
addSubview(icon)
addSubview(dismissView)
dismissView.isUserInteractionEnabled = true
textLabel.isUserInteractionEnabled = true
dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.iconTapped)))
textLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.labelTapped)))
self.text = text
backgroundColor = UIColor(white: 0.0, alpha: 0.6)
tintColor = UIColor.white
}
override func layoutSubviews() {
icon.frame = CGRect(x: marginLeft, y: marginTop + 4, width: 8, height: 8)
icon.image = iconImage?.withRenderingMode(.alwaysTemplate)
icon.tintColor = tintColor
let textLeft: Int
if icon.image != nil {
dismissView.isUserInteractionEnabled = true
textLeft = marginLeft + Int(icon.frame.width ) + marginLeft / 2
} else {
dismissView.isUserInteractionEnabled = false
textLeft = marginLeft
}
textLabel.frame = CGRect(x: textLeft, y: marginTop, width: 100, height: 20)
textLabel.backgroundColor = UIColor(white: 0, alpha: 0.0)
if maxLength > 0 && text.count > maxLength {
textLabel.text = text.prefix(maxLength)+"..."
}else{
textLabel.text = text
}
textLabel.textAlignment = .center
textLabel.font = font
textLabel.textColor = tintColor
textLabel.sizeToFit()
let tagHeight = Int(max(textLabel.frame.height,14)) + marginTop * 2
let tagWidth = textLeft + Int(max(textLabel.frame.width,14)) + marginLeft
let dismissLeft = Int(icon.frame.origin.x) + Int(icon.frame.width) + marginLeft / 2
dismissView.frame = CGRect(x: 0, y: 0, width: dismissLeft, height: tagHeight)
frame = CGRect(x: Int(frame.origin.x), y: Int(frame.origin.y), width: tagWidth, height: tagHeight)
layer.cornerRadius = bounds.height / 2
}
// MARK: Actions
#objc func iconTapped(){
delegate?.tagDismissed?(self)
}
#objc func labelTapped(){
delegate?.tagTouched?(self)
}
}
// MARK: TagViewDelegate
#objc protocol TagViewDelegate {
#objc optional func tagTouched(_ tag: TagView)
#objc optional func tagDismissed(_ tag: TagView)
}
extension CloudTagView: TagViewDelegate {
public func tagDismissed(_ tag: TagView) {
delegate?.tagDismissed?(tag)
if removeOnDismiss {
if let index = tags.firstIndex(of: tag) {
tags.remove(at: index)
}
}
}
public func tagTouched(_ tag: TagView) {
delegate?.tagTouched?(tag)
}
}
let viewController = ViewController()
PlaygroundPage.current.liveView = viewController
PlaygroundPage.current.needsIndefiniteExecution = true
UIStackView can line subviews up in a row for you, including with trailing alignment. Here is a playground example:
import SwiftUI
import PlaygroundSupport
class V: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let tags = ["test", "testing", "test more"].map { word -> UIView in
let label = UILabel()
label.text = word
label.translatesAutoresizingMaskIntoConstraints = false
let background = UIView()
background.backgroundColor = .cyan
background.layer.cornerRadius = 8
background.clipsToBounds = true
background.addSubview(label)
NSLayoutConstraint.activate([
background.centerXAnchor.constraint(equalTo: label.centerXAnchor),
background.centerYAnchor.constraint(equalTo: label.centerYAnchor),
background.widthAnchor.constraint(equalTo: label.widthAnchor, constant: 16),
background.heightAnchor.constraint(equalTo: label.heightAnchor, constant: 16),
])
return background
}
let stack = UIStackView.init(arrangedSubviews: [UIView()] + tags)
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .horizontal
stack.alignment = .trailing
stack.spacing = 12
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: view.topAnchor),
stack.widthAnchor.constraint(equalTo: view.widthAnchor),
])
view.backgroundColor = .white
}
}
PlaygroundPage.current.liveView = V()

Swift - How to create this form bubble effect

So I am trying to create the following effect using xcode swift.
I am trying to re-create the form bubble which contains the text fields and the text fields themselves, in the following style.
What I have tried so far:
Creating a UI View with curved borders, then using a transparent textfield and UILabels to indicate the field content.
I assume that the that must be a UItableview given the indent line but I am not sure how to style the tableview the same way. I assumed I can use the layer.cornerRadius as I did for the UIView but this doesn't seem to work.
Also is the entire view controller a UItableview controller or UICollectionView?
Any help on how to create the form as below would be appreciated.
In iOS 13 you can simply set the Style to Inset Grouped and the UITableView looks exactly like that - no additional changes necessary.
Using layer.cornerRadius for the whole tableview will not work. You should use a UITableviewController or a normal UIViewController which contains a tableView then divide your tableview into 3 sections
First section should be profile: no doubt about this section, border your cell the be the same as design
Second section should be personal information. 3 cells, each cell contain the text field. You can calculate and create border for the cell by corresponding position (top cell will be bordered top left & right, bottom cell will be bordered bottom left & right, otherwise no bordered)
Third section: no doubt about this, just one cell (switch account)
SWIFT Solution
Thanks to Paulo Silva
Step 1:
You can simply create a swift file named it as 'CUIView' (or your own wish) and use following IBDesignable code inside it and save .
//
// CUIView.swift
// CustomUIView
//
// Created by Paulo Silva on 23/08/2019.
// Copyright © 2019 example. All rights reserved.
//
import UIKit
import UIKit
import CoreGraphics
#IBDesignable class CUIView: UIView {
// MARK: - Private Variables -
private let containerView = UIView()
private var containerImageView = UIImageView()
// MARK: - Public Attributes -
#IBInspectable public var backgroundImage: UIImage? {
get {
return self.containerImageView.image
}
set {
// addShadowColorFromBackgroundImage()
self.containerImageView.image = newValue
}
}
override open var backgroundColor: UIColor? {
didSet(new) {
if let color = new {
containerView.backgroundColor = color
}
if backgroundColor != UIColor.clear { backgroundColor = UIColor.clear }
}
}
#IBInspectable var borderColor: UIColor {
get {
return UIColor(cgColor: self.containerView.layer.borderColor!)
}
set {
self.layer.borderColor = newValue.cgColor
self.containerView.layer.borderColor = newValue.cgColor
}
}
#IBInspectable var borderWidth: CGFloat {
get {
return self.containerView.layer.borderWidth
}
set {
self.layer.borderWidth = newValue
self.containerView.layer.borderWidth = newValue
}
}
#IBInspectable var cornerRadius: CGFloat {
get {
return self.containerView.layer.cornerRadius
}
set {
self.layer.cornerRadius = newValue
self.containerView.layer.cornerRadius = newValue
}
}
#IBInspectable var shadowOpacity: Float {
get {
return self.layer.shadowOpacity
}
set {
self.layer.shadowOpacity = newValue
}
}
#IBInspectable var shadowRadius: CGFloat {
get {
return self.layer.shadowRadius
}
set {
self.layer.shadowRadius = newValue
}
}
#IBInspectable var shadowOffset: CGSize {
get {
return self.layer.shadowOffset
}
set {
self.layer.shadowOffset = newValue
}
}
#IBInspectable var shadowColor: UIColor {
get {
return UIColor(cgColor: self.layer.shadowColor!)
}
set {
self.layer.shadowColor = newValue.cgColor
}
}
// #IBInspectable var shadowColorFormImage: Bool = false {
// didSet {
// addShadowColorFromBackgroundImage()
// }
// }
// MARK: - Life Cycle -
override init(frame: CGRect) {
super.init(frame: frame)
addViewLayoutSubViews()
refreshViewLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addViewLayoutSubViews()
refreshViewLayout()
}
override open func draw(_ rect: CGRect) {
super.draw(rect)
refreshViewLayout()
// addShadowColorFromBackgroundImage()
}
override func layoutSubviews() {
super.layoutSubviews()
refreshViewLayout()
// addShadowColorFromBackgroundImage()
}
// MARK: - Private Methods -
private func refreshViewLayout() {
// View
self.clipsToBounds = true
self.layer.masksToBounds = false
self.layer.cornerRadius = cornerRadius
// Shadow
self.layer.shadowOpacity = shadowOpacity
self.layer.shadowColor = shadowColor.cgColor
self.layer.shadowOffset = shadowOffset
self.layer.shadowRadius = shadowRadius
// Container View
self.containerView.layer.masksToBounds = true
self.containerView.layer.cornerRadius = cornerRadius
// Image View
self.containerImageView.backgroundColor = UIColor.clear
self.containerImageView.image = backgroundImage
self.containerImageView.layer.cornerRadius = cornerRadius
self.containerImageView.layer.masksToBounds = true
self.containerImageView.clipsToBounds = true
self.containerImageView.contentMode = .redraw
}
private func addViewLayoutSubViews() {
// add subViews
self.addSubview(self.containerView)
self.containerView.addSubview(self.containerImageView)
// add image constraints
self.containerImageView.translatesAutoresizingMaskIntoConstraints = false
self.containerImageView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
self.containerImageView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
self.containerImageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
self.containerImageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
// add view constraints
self.containerView.translatesAutoresizingMaskIntoConstraints = false
self.containerView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
self.containerView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
self.containerView.topAnchor.constraint(equalTo: topAnchor).isActive = true
self.containerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
// private func addShadowColorFromBackgroundImage() {
// // Get the averageColor from the image for set the Shadow Color
// if shadowColorFormImage {
// let week = self
// DispatchQueue.main.async {
// week.shadowColor = (week.containerImageView.image?.averageColor)!
// }
// }
// }
}
extension UIImage {
static func imageWithColor(tintColor: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
tintColor.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
func withBackground(color: UIColor, opaque: Bool = true) -> UIImage {
UIGraphicsBeginImageContextWithOptions(size, opaque, scale)
guard let ctx = UIGraphicsGetCurrentContext() else { return self }
defer { UIGraphicsEndImageContext() }
let rect = CGRect(origin: .zero, size: size)
ctx.setFillColor(color.cgColor)
ctx.fill(rect)
ctx.concatenate(CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: size.height))
ctx.draw(cgImage!, in: rect)
return UIGraphicsGetImageFromCurrentImageContext() ?? self
}
var averageColor: UIColor? {
guard let inputImage = CIImage(image: self) else { return nil }
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else { return nil }
guard let outputImage = filter.outputImage else { return nil }
var bitmap = [UInt8](repeating: 0, count: 4)
let context = CIContext(options: [.workingColorSpace: kCFNull as Any])
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)
return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
}
}
extension NSLayoutConstraint {
func constraintWithMultiplier(_ multiplier: CGFloat) -> NSLayoutConstraint {
return NSLayoutConstraint(item: self.firstItem!, attribute: self.firstAttribute, relatedBy: self.relation, toItem: self.secondItem, attribute: self.secondAttribute, multiplier: multiplier, constant: self.constant)
}
}
extension UIScreen {
enum SizeType: CGFloat {
case unknown = 0.0
case iPhone4 = 960.0
case iPhone5 = 1136.0
case iPhone6 = 1334.0
case iPhone6Plus = 1920.0
}
var sizeType: SizeType {
let height = nativeBounds.height
guard let sizeType = SizeType(rawValue: height) else { return .unknown }
return sizeType
}
}
Step 2 : Connect your view associated class as 'CUIView' as following image
Step 3: Give corner radius and set background as like as following image .
For all the group textField and textfield you can use a view as parents view and associated class to get this outcome . For underline you can use simple view with minimum height to get desired design .