Change navigationBar bottom border to a dashed line in swift - ios

I would like to change my navigationBar to have a dotted line as its border like so:
I've found a wonderful answer in this thread about how to change the color of a navigationBar border: Change navigation bar bottom border color Swift In particular: https://stackoverflow.com/a/46224261/436014
Via a UIColor extension:
extension UIColor {
/// Converts this `UIColor` instance to a 1x1 `UIImage` instance and returns it.
///
/// - Returns: `self` as a 1x1 `UIImage`.
func as1ptImage() -> UIImage {
UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
setFill()
UIGraphicsGetCurrentContext()?.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
let image = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
UIGraphicsEndImageContext()
return image
}
}
I was wondering if there was any way to adapt this to build out a dashed line instead. I was thinking it could somehow be done by drawing a fill in alternating colors, but am confused since it's all an extension off of a single UIcolor
navigationController.navigationBar.shadowImage = UIColor.black.as1ptImage()
Thus, something like the following clearly won't work:
UIGraphicsBeginImageContext(CGSize(width: 1, height: 4))
UIGraphicsGetCurrentContext()?.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
UIGraphicsGetCurrentContext()?.fill(CGRect(x: 1, y: 0, width: 3, height: 1))
I was wondering if anyone has an idea how to go about this

Ok, I managed to find a solution through the help of the API documentation for CALayer: https://developer.apple.com/documentation/quartzcore/cashapelayer/1521921-linedashpattern
And this SO thread about converting a CALayer to UIImage: UIImage from CALayer in iOS
The overall process is:
Create a CALayer
Give this CALayer a frame
Draw a dashed line
Convert to UIImage
Use this UIImage as the navigationBar.shadowImage
The nice thing is that it tiles appropriately, so the CALayer/UIImage just need to be as wide as the dashed line you are generating.
extension UIImage {
class func imageWithLayer(layer: CALayer) -> UIImage {
UIGraphicsBeginImageContextWithOptions(layer.bounds.size, layer.isOpaque, 0.0)
layer.render(in: UIGraphicsGetCurrentContext()!)
guard let img = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() }
UIGraphicsEndImageContext()
return img
}
}
let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 6, height: 1)
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 1
shapeLayer.lineDashPattern = [4,2]
let path = CGMutablePath()
path.addLines(between: [CGPoint(x:1, y: 0), CGPoint(x: 6, y:0)])
shapeLayer.path = path
layer.addSublayer(shapeLayer)
navigationController.navigationBar.shadowImage = UIImage.imageWithLayer(layer: layer)

Related

UIView to UIImage using UIGraphicsImageRenderer wrongly renderer image with alpha

I'm converting a UIView to UIImage, to set it as navigationBar background.
Thats view has an gradient layer, and when i use setBackgroundImage(_ backgroundImage: UIImage?, for barMetrics: UIBarMetrics) using the converted view, navigation gets a little transparent, something like 0.8 alpha
Here's the code that i'm using:
let navBackground = UIView(frame: CGRect(x: UIApplication.shared.statusBarFrame.origin.x,
y: UIApplication.shared.statusBarFrame.origin.y,
width: UIApplication.shared.statusBarFrame.width,
height: UIApplication.shared.statusBarFrame.height+self.navigationController!.navigationBar.frame.size.height))
let gradient: CAGradientLayer = CAGradientLayer()
gradient.colors = [UIColor(red:0.16, green:0.22, blue:0.49, alpha:1.0).cgColor, UIColor(red:0.31, green:0.53, blue:0.78, alpha:1.0).cgColor]
gradient.locations = [0.0, 1.0]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
gradient.frame = navBackground.bounds
navBackground.layer.insertSublayer(gradient, above: nil)
let imgBackground = navBackground.asImage()
self.navigationController?.navigationBar.setBackgroundImage(imgBackground, for: UIBarMetrics.default)
asImage() it's a UIView extension that convert UIView to UIImage:
extension UIView {
func asImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}!
Result: sample
Turn off the translucent property of navigation bar. Actually the default navigation background has a blur effect but your converted image has none.

Draw border of certain color around colored UIImage in tab bar

In order to understand my problem I will start with a short description of my goal:
In the center of my tab bar I deliberately use a usually too big image (a circle) which extends over the tab bar (the tab bar's background color is white) so it laps over the top border of the tab bar. Since all UITabBarItems' default color is a light gray (apparently it is neither UIColor.lightGray nor .darkGray) and I would like to change the color of this (and only this) UITabBarItem (or rather the image considering this is the only thing which can be seen of this UITabBarItem) to white I've used the following extension/function which works fine:
extension UIImage {
func tabBarImageWithCustomTint(tintColor: UIColor) -> UIImage {
UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
let context: CGContext = UIGraphicsGetCurrentContext()!
context.translateBy(x: 0, y: self.size.height)
context.scaleBy(x: 1.0, y: -1.0)
context.setBlendMode(CGBlendMode(rawValue: 1)!)
let rect: CGRect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
context.clip(to: rect, mask: self.cgImage!)
tintColor.setFill()
context.fill(rect)
var newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
newImage = newImage.withRenderingMode(UIImageRenderingMode.alwaysOriginal)
return newImage
}
}
Link to question where I found this extension
As both the tint color of the image and the background color of the tab bar are white, I would now like to add a border of red color to the now white image. Luckily, I managed to find another question on stackoverflow which answered this question (although I must add that I am not entirely content with this extension because it leaves a very small space between the UIImage and the border):
extension UIImage {
func roundedImageWithBorder(width: CGFloat, color: UIColor) -> UIImage? {
let square = CGSize(width: min(size.width, size.height) + width * 2, height: min(size.width, size.height) + width * 2)
let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square))
imageView.contentMode = .center
imageView.image = self
imageView.layer.cornerRadius = square.width/2
imageView.layer.masksToBounds = true
imageView.layer.borderWidth = width
imageView.layer.borderColor = color.cgColor
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
imageView.layer.render(in: context)
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result
}
}
My problem now is if I use the function consecutively like this...:
let tabRecordButton = UIImage(named: "circle").tabBarImageWithCustomTint(tintColor: .white).roundedImageWithBorder(width: 1, color: .red)
..., the border is drawn but the UITabBarItem's tint color goes back to this default gray aforementioned (not even the border is red).
So my question: Is there a way I can do both, i.e. color the image white and the border red in my UITabBar?
You have to add this line result = result.withRenderingMode(UIImageRenderingMode.alwaysOriginal) in your second extension as well, if you omit this line then your image will take the tint from your tabBar, that is your original issue
replace your roundedImageWithBorder extension method implementation with this one
func roundedImageWithBorder(width: CGFloat, color: UIColor) -> UIImage? {
let square = CGSize(width: min(size.width, size.height) + width * 2, height: min(size.width, size.height) + width * 2)
let imageView = UIImageView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: square))
imageView.contentMode = .center
imageView.image = self
imageView.layer.cornerRadius = square.width/2
imageView.layer.masksToBounds = true
imageView.layer.borderWidth = width
imageView.layer.borderColor = color.cgColor
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
imageView.layer.render(in: context)
var result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
result = result?.withRenderingMode(UIImageRenderingMode.alwaysOriginal)
return result
}
Testing
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.tabBarItem.selectedImage = UIImage(named: "icono-menu")?.tabBarImageWithCustomTint(tintColor: UIColor.magenta).roundedImageWithBorder(width: 1, color: UIColor.blue)
self.tabBarController?.tabBar.tintColor = UIColor.red //note that the tintColor of the tabBar is red
}
Result

CAGradientLayer on UILabel can't be rotated

I want a horizontal color gradient as the text color of my UILabel.
So I'm using a CAGradientLayer as described in https://developer.apple.com/reference/quartzcore/cagradientlayer.
The gradient is rendered perfectly on the text, but vertically.
CATransform3DMakeRotation as apple described doesn't rotate the gradient.
In this answer it says that the CAGradientLayer needs to be added to a View or other Layer first to make the rotation work.
So I tried to add it as a sublayer to my UILabel and remove it after the rendering, but it won't transform the gradient on the text, but adds a horizontal gradient rectangle on top of it, in the size of the UILabel but rotated 90°.
Here's my code:
extension UILabel {
func addGradient() {
// Get size of the label
let size = CGSize(width: frame.width, height: frame.width)
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect(x: 0, y: 0, width: size.height, height: size.width)
gradientLayer.colors = [
UIColor.gradientBlue.cgColor,
UIColor.gradientPink.cgColor,
UIColor.gradientOrange.cgColor
]
// layer.addSublayer(gradientLayer)
gradientLayer.transform = CGAffineTransform
// This does not work
gradientLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2, 0, 0, 1)
UIGraphicsBeginImageContext(size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
// Create UIImage from Gradient
let gradientImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// gradientLayer.removeFromSuperlayer()
textColor = UIColor.init(patternImage: gradientImage!)
}
}
you can use
gradientLayer.startPoint = CGPoint.init(x: 0, y: 0)
gradientLayer.endPoint = CGPoint.init(x: 1, y: 1)
for diagonal gradient. you can play with those points however you want to achieve different results.

How to put a black color with opacity on an image in Swift 3

I'd like to know how can I put a black rectangle with opacity on the top of an image in Swift 3. For example:
you can create a UIView that is black with an opacity and make it the same size of your UIImageView and add it as subview. Try something like:
let tintView = UIView()
tintView.backgroundColor = UIColor(white: 0, alpha: 0.5) //change to your liking
tintView.frame = CGRect(x: 0, y: 0, width: imageView.frame.width, height: imageView.frame.height)
imageView.addSubview(tintView)
imageView = Your UIImageView
func drawRectOn(image: UIImage) -> UIImage {
UIGraphicsBeginImageContext(image.size)
image.draw(at: CGPoint.zero)
let context = UIGraphicsGetCurrentContext()
// set fill gray and alpha
context!.setFillColor(gray: 0, alpha: 0.5)
context!.move(to: CGPoint(x: 0, y: 0))
context!.fill(CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
context!.strokePath()
let resultImage = UIGraphicsGetImageFromCurrentImageContext()
// end the graphics context
UIGraphicsEndImageContext()
return resultImage!
}

Set background color of active tab bar item in Swift

I'm hoping to accomplish this without the use of images, if at all possible. Is there a way to create the effect shown in the image programmatically without have to render each tab out as an image?
Every question I've reviewed on SO has the tabs saved as JPGs, which is more work than I feel it should be.
Any ideas?
I took a similar approach to #matcartmill but without the need for a special image. This solution is just based on your color.
// set red as selected background color
let numberOfItems = CGFloat(tabBar.items!.count)
let tabBarItemSize = CGSize(width: tabBar.frame.width / numberOfItems, height: tabBar.frame.height)
tabBar.selectionIndicatorImage = UIImage.imageWithColor(color: UIColor.red, size: tabBarItemSize).resizableImage(withCapInsets: UIEdgeInsets.zero)
// remove default border
tabBar.frame.size.width = self.view.frame.width + 4
tabBar.frame.origin.x = -2
I'm making use of the following extension of UIImage:
extension UIImage {
class func imageWithColor(color: UIColor, size: CGSize) -> UIImage {
let rect: CGRect = CGRectMake(0, 0, size.width, size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
I hope this helps!
for swift 4
extension UIImage {
class func imageWithColor(color: UIColor, size: CGSize) -> UIImage {
let rect: CGRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
Update to SWIFT 3:
let numberOfItems = CGFloat((tabBarController?.tabBar.items!.count)!)
let tabBarItemSize = CGSize(width: (tabBarController?.tabBar.frame.width)! / numberOfItems,
height: (tabBarController?.tabBar.frame.height)!)
tabBarController?.tabBar.selectionIndicatorImage
= UIImage.imageWithColor(color: UIColor.black,
size: tabBarItemSize).resizableImage(withCapInsets: .zero)
tabBarController?.tabBar.frame.size.width = self.view.frame.width + 4
tabBarController?.tabBar.frame.origin.x = -2
extension UIImage
{
class func imageWithColor(color: UIColor, size: CGSize) -> UIImage
{
let rect: CGRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
So here's what I ended up doing. It's a mix of using a 640x49 PNG that's the color of the blue "highlighted" background I need.
In AppDelegate.swift:
var selectedBG = UIImage(named:"tab-selected-full")?.resizableImageWithCapInsets(UIEdgeInsetsMake(0, 0, 0, 0))
UITabBar.appearance().selectionIndicatorImage = selectedBG
And then in the first View Controller that gets loaded, I have:
tabBarController?.tabBar.frame.size.width = self.view.frame.width+4
tabBarController?.tabBar.frame.origin.x = -2
The reason for the above two lines is that, by default, Apple has a 2px border between the left and right sides of the tab bar and the tab bar items.
In the above I simply make the tab bar 4px wider, and then offset it so the border on the left falls just outside of the view, thus the border on the right will also fall outside of the view.

Resources