After adding a gradient layer to the statusbar and make it light or dark for different view controllers, at some random point in navigation, my clock in the status bar of notch iPhones has become truncated. like 1... or ...
I have tried these two solutions for make statusbar content white; But this has no effect on this random clock behaviour.
this:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.barStyle = .blackOpaque
self.setNeedsStatusBarAppearanceUpdate()
}
or this:
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
Both of them changes my status bar color of content. Any idea about what is cause of this behavior???
and this is how I make the status bar gradient:
extension UIViewController {
func makeStatusBarGradient(){
let statusBarView = UIView(frame: UIApplication.shared.statusBarFrame)
let gradientLayer1 = CAGradientLayer()
gradientLayer1.frame = statusBarView.frame
gradientLayer1.colors = [UIColor.APColors.redGradient.cgColor, UIColor.APColors.orangeGradient.cgColor]
gradientLayer1.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer1.endPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer1.cornerRadius = 0
gradientLayer1.zPosition = -10
gradientLayer1.name = "gradient"
//Change status bar color
if let statusBar = UIApplication.shared.value(forKey: "statusBar") as? UIView{
statusBar.layer.addSublayer(gradientLayer1)
}
setNeedsStatusBarAppearanceUpdate()
}
func clearStatusBarGradient(){
if let statusBar = UIApplication.shared.value(forKey: "statusBar") as? UIView{
if let layers = statusBar.layer.sublayers?.filter({$0.name == "gradient"}){
if (layers.count > 0){
layers.first?.removeFromSuperlayer()
}
}
}
setNeedsStatusBarAppearanceUpdate()
}
}
and, the APColors are:
extension UIColor {
struct APColors {
static var redGradient : UIColor { return UIColor(red: 1.00, green: 0.42, blue: 0.24, alpha: 1) }
static var orangeGradient : UIColor { return UIColor(red: 0.95, green: 0.19, blue: 0.42, alpha: 1)}
}
}
You are using extension on UILabel, which override it's intrinsicContentSize. (or maybe you swizzling this method) So status bar clock label trigger it and got wrong intrinsicContentSize. Try to fix textSize calculation in this overrided method, or avoid overriding it if possible.
I can guess you're using UILabel+Padding from popular SO answer, so check if you've got this:
if let insets = padding {
insetsWidth += insets.left + insets.right
insetsHeight += insets.top + insets.bottom
textWidth -= insetsWidth
}
if you do, then simply change it:
if let insets = padding {
insetsWidth += insets.left + insets.right
insetsHeight += insets.top + insets.bottom
textWidth -= insetsWidth
} else {
return contentSize
}
As i do not know what's the APColor, so I changed the line with below & everything is working fine.
gradientLayer1.colors = [UIColor.red.cgColor, UIColor.orange.cgColor]
Try to change the same line and check your issue is solved or not. Most probably, APColor does not caused the issue.
Check is their any multiple stuffs is in you statusbar is available or not. e.g. indicator etc.
If you added anything else in your status bar then let me know.
Output
Related
I know this has been asked and answered quite a lot but I can't seem to make it work.
I have an UIViewController with a full size WKWebView. Everything works fine, except for the status bar that looks out of place on notch devices.
The problem is, nothing works, even my own code from other apps.
Things that I tried so far (this works just fine in another app...):
if #available(iOS 13, *)
{
let statusBar = UIView(frame: (UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.windowScene?.statusBarManager?.statusBarFrame)!)
//statusBar.backgroundColor = UIColor(red: 242/255, green: 242/255, blue: 242/255, alpha: 1.0)
statusBar.backgroundColor = UIColor.red
UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.addSubview(statusBar)
}
else
{
let statusBar: UIView = UIApplication.shared.value(forKey: "statusBar") as! UIView
if statusBar.responds(to:#selector(setter: UIView.backgroundColor))
{
statusBar.backgroundColor = UIColor(red: 242/255, green: 242/255, blue: 242/255, alpha: 1.0)
}
}
And this:
extension UIApplication {
var statusBarUIView: UIView? {
if #available(iOS 13.0, *) {
let tag = 3848245
let keyWindow: UIWindow? = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
if let statusBar = keyWindow?.viewWithTag(tag) {
return statusBar
} else {
let height = keyWindow?.windowScene?.statusBarManager?.statusBarFrame ?? .zero
let statusBarView = UIView(frame: height)
statusBarView.tag = tag
statusBarView.layer.zPosition = 999999
keyWindow?.addSubview(statusBarView)
return statusBarView
}
} else {
if responds(to: Selector(("statusBar"))) {
return value(forKey: "statusBar") as? UIView
}
}
return nil
}
}
And everything from here:
How to change the status bar background color and text color on iOS 13?
Change Status Bar Color in iOS 13?
How to set Status Bar Style in Swift 3
Even tried doing it from my webpage displayed inside the WKWebView:
https://itnext.io/make-your-pwas-look-handsome-on-ios-fd8fdfcd5777
The only workaround that kinda fixes the issue is to change the background color of the main view.
The problem is, this will also change the color of the bottom section.
I even tried messing around with the style in the info.plist file.
Any ideas on what I'm missing?
you can just add that in your controller and then you check the hasNotch
extension UIDevice {
/// Returns `true` if the device has a notch
var hasNotch: Bool {
guard #available(iOS 11.0, *),
let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }),
window.safeAreaInsets.bottom >= 10 else {
return false
}
return true
}
}
I want to custom color my tab bar element in my custom UITabController sub-class and it works fine when I'm doing it with:
tabBar.barTintColor = .blue (With any system or custom color)
But when I'm trying to add a gradient using my custom UIImage extension
extension UIImage {
static func gradientImageWithBounds(bounds: CGRect, colors: [CGColor]) -> UIImage {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
gradientLayer.masksToBounds = true
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
tabBar.barTintColor = UIColor(patternImage: UIImage.gradientImageWithBounds(bounds: tabBar.bounds, colors: [Colors.tabBarTopGradient, Colors.tabBarBottomGradient]))
I having trouble with gradient not getting properly applied to the bottom part of the tab bar around the phone's safe area. What am I missing here? It looks like that:
Please try setting the gradient tint in viewWillLayoutSubviews(). Hope that helps!
I think you create the gradient based on tab bar height, what you need is to add the additional height to the tab bar height
override func viewDidLoad() {
super.viewDidLoad()
var frame = tabBar.bounds
let safeAreaHeight = safeAreaInsets.bottom
frame.size.height = frame.size.height + safeAreaHeight
tabBar.barTintColor = UIColor(patternImage: UIImage.gradientImageWithBounds(bounds: frame, colors: [UIColor.red.cgColor, UIColor.blue.cgColor]))
}
public var safeAreaInsets: UIEdgeInsets {
guard let window: UIWindow = UIApplication.shared.windows.first else {
return .zero
}
if #available(iOS 11.0, *),
UIWindow.instancesRespond(to: #selector(getter: window.safeAreaInsets)) {
return window.safeAreaInsets
}
return .zero
}
I'm not able to add kern space into the tab bar attributed text.
The UITabBar in question is a custom tabBar, you can find the code below.
I'm using the "attributed key" dictionary to add attributes to the items title, but I'm having an issue with the kern space.
class ProfileTabBar: UITabBar {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setStyle()
}
required override init(frame: CGRect) {
super.init(frame: frame)
self.setStyle()
}
func setStyle() {
self.tintColor = Style.shared.primary1
// Disable the default border
self.layer.borderWidth = 0.0
self.clipsToBounds = true
// Create a new bottom border
let bottomLine = CALayer()
let screenWidth = UIScreen.main.bounds.width
//let viewForFrame = self.superview ?? self
//let screenWidth = viewForFrame.bounds.width
bottomLine.frame = CGRect(x: 0.0, y: self.frame.height - 1, width: screenWidth, height: 2.0)
bottomLine.backgroundColor = UIColor(red: 235.0/255, green: 235.0/255, blue: 235.0/255, alpha: 1.0).cgColor
self.layer.addSublayer(bottomLine)
// Get the size of a single item
let markerSize = CGSize(width: screenWidth/CGFloat(self.items!.count), height: self.frame.height)
// Create the selection indicator
self.selectionIndicatorImage = UIImage().createSelectionIndicator(color: self.tintColor, size: markerSize , lineWidth: 3.0)
// Customizing the items
if let items = self.items {
for item in items {
item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -15)
let attributes: [NSAttributedStringKey : Any] = [
NSAttributedStringKey.font: UIFont(name: Style.shared.fontBold.fontName, size: 14) as Any,
NSAttributedStringKey.kern: NSNumber(value: 1.0)
]
item.setTitleTextAttributes(attributes, for: .normal)
}
}
}
All the attributes works except for the kern. What I'm doing wrong?
This question is old and there is an even older answer here. It appears that UITabBarItem appearance ignores NSAttributedString.Key.kern. That leaves us with a few options.
Subclass UITabBarItem this isn't easy because UITabBarItem inherits from UIBarItem which is an NSObject not a UIView.
Subclass UITabBar this can be done, but involves a decent amount of work for just some kern. You'll have to use UIButton instead of UITabBarItem so that the kern is applied.
You can add spacing using unicode characters in your title. This is really easy and can probably achieve the spacing you're looking for with just a few lines of code.
Unicode spacing:
U+0020 1/4 em
U+2004 1/3 em
U+2005 1/4 em
U+2006 1/6 em
U+2008 The width of a period “.”
U+2009 1/5 em (or sometimes 1/6 em)
You can use a unicode character in a String in Swift like this "\u{2006}". That means we can insert a small space between all the characters in our tabBarItem title. Like this:
extension String {
var withOneSixthEmSpacing: String {
let letters = Array(self)
return letters.map { String($0) + "\u{2006}" }.joined()
}
Using this for our tabBarItems:
self.navigationController.tabBarItem = UITabBarItem(
title: "Home".withOneSixthEmSpacing,
image: homeImage,
selectedImage: homeSelectedImage
)
Visually we end up with:
Instead of:
Another workaround is to subclass UITabBarController, and set the kerning in viewDidLayoutSubviews.
class FooTabBarController: UITabBarController {
private var tabBarButtons: [UIControl] {
tabBar.subviews.compactMap { $0 as? UIControl }
}
private var tabBarButtonLabels: [UILabel] {
tabBarButtons.compactMap { $0.subviews.first { $0 is UILabel } as? UILabel }
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tabBarButtonLabels.forEach {
if let attributedText = $0.attributedText {
let mutable = NSMutableAttributedString(attributedString: attributedText)
mutable.addAttribute(.kern, value: 0.5, range: .init(location: 0, length: mutable.length))
$0.attributedText = mutable
$0.sizeToFit()
}
}
}
}
The caveats to this solution are:
It is somewhat fragile. It can break if Apple changes the view structure in the tab bar, ie if they stop using UIControl, or if they change the subview heirarchy.
It isn't all that efficient because the kerning has to be set every layout cycle.
I loved #DoesData's answer, it really helped me out a lot.
Here's a more "swifty" version of it I came up with if it helps anyone:
extension String {
var withAddedSpacing: String {
Array(self)
.compactMap { String($0) }
.joined(separator: "\u{2006}")
}
}
I am attempting to use radial gradience within my app on a background UIView. My issue comes to play, where I want to update the view colors of the gradience multiple times. I have no errors with my code, but I can't seem to figure out how to get around this.
What I have tried is reloading the Input Views within the regular UIView as-well as the gradience class; remove the subview of the uiview, and adding a new view to the screen, which worked for only change of set colors; and I have looked over the internet, but can't seem to resolve this. All I want is for the UIView to update its colors based on the new color parameters I give it.
Here is my radial gradience code:
import UIKit
class RadialGradient: UIView {
var innerColor = UIColor.yellow
var outterColor = UIColor.red
override func draw(_ rect: CGRect) {
let colors = [innerColor.cgColor, outterColor.cgColor] as CFArray
let endRadius = min(frame.width, frame.height)
let center = CGPoint(x: bounds.size.width/2, y: bounds.size.height/2)
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
UIGraphicsGetCurrentContext()!.drawRadialGradient(gradient!,
startCenter: center,
startRadius: 0.0,
endCenter: center,
endRadius: endRadius,
options: CGGradientDrawingOptions.drawsAfterEndLocation)
}
}
Here is where I am using it:
import UIKit
class TestIssuesVC: UIViewController {
var check : Bool = false
#IBAction func buttonClicked(_ sender: Any) {
if check == true {
backgroundsetting.removeFromSuperview()
print("Why wont you change to purple and black?????")
cheapFix(inner: UIColor.purple, outter: UIColor.black)
} else {
backgroundsetting.removeFromSuperview()
cheapFix(inner: UIColor.red, outter: UIColor.blue)
check = true
}
}
func cheapFix(inner: UIColor, outter: UIColor) {
let backgroundsetting = RadialGradient()
backgroundsetting.innerColor = inner
backgroundsetting.outterColor = outter
backgroundsetting.frame = (frame: CGRect(x: self.view.frame.size.width * 0, y: self.view.frame.size.height * 0, width:self.view.frame.size.width, height: self.view.frame.size.height))
self.view.addSubview(backgroundsetting)
self.view.sendSubview(toBack: backgroundsetting)
self.reloadInputViews()
}
let backgroundsetting = RadialGradient()
override func viewWillAppear(_ animated: Bool) {
backgroundsetting.innerColor = UIColor.green
backgroundsetting.outterColor = UIColor.red
backgroundsetting.frame = (frame: CGRect(x: self.view.frame.size.width * 0, y: self.view.frame.size.height * 0, width:self.view.frame.size.width, height: self.view.frame.size.height))
self.view.addSubview(backgroundsetting)
self.view.sendSubview(toBack: backgroundsetting)
self.reloadInputViews()
}
}
I see two things.
Your cheapFix method never updates the backgroundsetting property. It creates its own local variable of the same name. So you are actually adding new views over and over but each is sent to the back so you only ever see the first one. This is why nothing ever appears to change.
None of that is necessary. Simply create one RadialGradient view. When you want its colors to change, simply update its colors. That class needs to be fixed so it redraws itself when its properties are updated.
Make the following change to the two properties in your RadialGradient class:
var innerColor = UIColor.yellow {
didSet {
setNeedsDisplay()
}
}
var outterColor = UIColor.red {
didSet {
setNeedsDisplay()
}
}
Those changes will ensure the view redraws itself when its colors are updated.
I'm trying to make a line (so basically UIView) that has fixed height and width and is divided to nine segments. I want to be able to control the height of each segment and its color. E.g. I want the first segment be yellow and 30% of the total height of the line, the second to be red and 8% of the total height etc.
I'm not really skilled in Swift, so my solution would be to make 9 UIViews, stack them on top of each other on my storyboard and then manually set the height and background color of every view, so they'd seem like a one multicolored line. Is there cleaner and less bulky solution? Thanks
I would highly recommend using Core Graphics for this.
As the drawing is dead simple (you just want to stack some colored lines within a view), you can easily achieve this by subclassing UIView and overriding drawRect() and drawing them in Core Graphics.
It's certainly a much cleaner solution than adding 9 subviews!
Something like this should achieve the desired result:
class LineView : UIView {
let colors:[UIColor] = [UIColor.redColor(), UIColor.blueColor(), UIColor.greenColor()]
let values:[CGFloat] = [0.35, 0.45, 0.2]
override func drawRect(rect: CGRect) {
let r = self.bounds // the view's bounds
let numberOfSegments = values.count // number of segments to render
let ctx = UIGraphicsGetCurrentContext() // get the current context
var cumulativeValue:CGFloat = 0 // store a cumulative value in order to start each line after the last one
for i in 0..<numberOfSegments {
CGContextSetFillColorWithColor(ctx, colors[i]) // set fill color to the given color
CGContextFillRect(ctx, CGRectMake(0, cumulativeValue*r.size.height, r.size.width, values[i]*r.size.height)) // fill that given segment
cumulativeValue += values[i] // increment cumulative value
}
}
}
Going further...
You could allow the colors and values properties to be changed from outside the LineView class, allowing for much greater flexibility. You just have to override the didSet to trigger the view to be redrawn when the properties change.
For example:
class LineView : UIView {
/// An array of optional UIColors (clearColor is used when nil is provided) defining the color of each segment.
var colors : [UIColor?] = [UIColor?]() {
didSet {
self.setNeedsDisplay()
}
}
/// An array of CGFloat values to define how much of the view each segment occupies. Should add up to 1.0.
var values : [CGFloat] = [CGFloat]() {
didSet {
self.setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clearColor()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
let r = self.bounds // the view's bounds
let numberOfSegments = values.count // number of segments to render
let ctx = UIGraphicsGetCurrentContext() // get the current context
var cumulativeValue:CGFloat = 0 // store a cumulative value in order to start each line after the last one
for i in 0..<numberOfSegments {
CGContextSetFillColorWithColor(ctx, colors[i]?.CGColor ?? UIColor.clearColor().CGColor) // set fill color to the given color if it's provided, else use clearColor
CGContextFillRect(ctx, CGRectMake(0, cumulativeValue*r.size.height, r.size.width, values[i]*r.size.height)) // fill that given segment
cumulativeValue += values[i] // increment cumulative value
}
}
}
Usage:
let lineView = LineView(frame: CGRectMake(50, 50, 20, view.bounds.size.height-100))
lineView.colors = [
UIColor(red: 1.0, green: 31.0/255.0, blue: 73.0/255.0, alpha: 1.0), // red
UIColor(red:1.0, green: 138.0/255.0, blue: 0.0, alpha:1.0), // orange
UIColor(red: 122.0/255.0, green: 108.0/255.0, blue: 1.0, alpha: 1.0), // purple
UIColor(red: 0.0, green: 100.0/255.0, blue: 1.0, alpha: 1.0), // dark blue
UIColor(red: 100.0/255.0, green: 241.0/255.0, blue: 183.0/255.0, alpha: 1.0), // green
UIColor(red: 0.0, green: 222.0/255.0, blue: 1.0, alpha: 1.0) // blue
]
lineView.values = [0.15, 0.1, 0.35, 0.15, 0.1, 0.15]
view.addSubview(lineView);
(I've only added 6 colors here, but you can add as many as you want).
Full project: https://github.com/hamishknight/Color-Segment-Line-View
I've just realized that this was not what you needed.
I leave the answer anyway so that maybe could be helpful to somebody else in the future.
Make sure your line view has it's own UIView subclass, so that we can override drawRect and achieve your goal.
Then a simple implementation would be:
class BarLine: UIView {
override func drawRect(rect: CGRect) {
//Height of each segment, in percentage
var heights : [CGFloat] = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
//Lets create 9 rects and set each rect width to be 1/9th of the view size, then add them to the array
let width : CGFloat = rect.size.width / 9.0
var i : Int = Int()
//Loop to generate 9 segmnets
for (i = 0; i < 9; i++) {
//Each rect origin must be translated by i * width
let origin = CGPointMake(CGFloat(i) * width, rect.height)
//Generate a random color
let color = UIColor(red: heights[i], green: 0.5, blue: 0.5, alpha: 1)
let segment = CGRect(x: origin.x, y: origin.y, width: width, height: -heights[i] * rect.height)
//Set the color
color.set()
//Add the segment to the view by drawing it
UIRectFill(segment)
}
}
}
This will produce something like :
(Remember to set your UIView class to you custom class in IB)
I hope this helped
To make #Hamish code compatible for Swift 5, here the LineView class (and invert width & height in draw->fill to make it horizontal):
import UIKit
public class ColorLineView : UIView {
public var colors : [UIColor?] = [UIColor?]() {
didSet {
self.setNeedsDisplay()
}
}
public var values : [CGFloat] = [CGFloat]() {
didSet {
self.setNeedsDisplay()
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public override func draw(_ rect: CGRect) {
let r = self.bounds
let numberOfSegments = values.count
guard let ctx = UIGraphicsGetCurrentContext() else { return }
var cumulativeValue:CGFloat = 0
for i in 0..<numberOfSegments {
ctx.setFillColor(colors[i]?.cgColor ?? UIColor.clear.cgColor)
ctx.fill(CGRect(x: 0, y: cumulativeValue*r.size.height, width: r.size.width, height: values[i]*r.size.height))
cumulativeValue += values[i]
}
}
}