Is is possible to set an item with two colors on the tab bar?
(I'm a beginner in swift)
What I want to achieve:
I want to have something-like a white background for each item in both active and inactive states.
-> Example of what I want
What I have tried so far:
I've been trying by using a png with transparency, white color and black color but it seems that anyhing that is not a transparent pixel is taken as the same color. (The asset I've working with is rendered as a whole color image instead of differentiating black and white)
-> The asset I thought it would work
What I think its the way but don't know how:
(This is an assumption)
I think this could be done by setting an inactive item in the background (first layer, white color) and an active item (second layer, black color) in the same coordinates with the color I want to change:
-> Two assets for Layer 1 and layer 2
I'm using an storyboard with storyboard references to set initial configuration for the assets of both active an inactive states.
I'm setting active state color programatically.
Suggestions to achive this in more elegant ways are welcome.
Thank you :)
Thank you all for your help.
Inspired by #Jake link, I kept searching and found that Xcode has different modes to render image assets.
Render as Template:
The default one, renders images as they were templates. (You can take any image and colorize it programatically, but it does not recognice if it has more than 1 color)
Render as Original:
The other mode, renders images as they are. So it actually recognizes if it has more than 1 color and leave them that way. This allows you to have tab bar items with multiple colors.
How can this property be modified?
Programatically:
(as a property)
tabBarItem.image = tabBarImage.withRenderingMode(.alwaysOriginal)
//or
tabBarItem.image = tabBarImage.withRenderingMode(.alwaysTemplate)
Via Interface Builder:
1. Assets Folder, 2. Select an asset, 3. Attribute Inspector
Render as: Default, Original Image, Template Image.
Subclass your TabbarviewController, and set the color:
tabBar.tintColor = .red
I would probably just draw this in quartz. It's a very simple vector and would be easy to do in few lines of code. It would also react to a change of state and update accordingly. See below for an example UIView that does exactly this:
class TabIcon: UIView {
var enabled: Bool = false {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let ctx = UIGraphicsGetCurrentContext()
ctx?.setFillColor(UIColor.white.cgColor)
ctx?.addEllipse(in: rect)
ctx?.fillPath()
ctx?.setFillColor(enabled ? UIColor.purple.cgColor : UIColor.lightGray.cgColor)
ctx?.addEllipse(in: CGRect(x: rect.minX + 5, y: rect.minY + 5, width: rect.width - 10, height: rect.height - 10))
ctx?.fillPath()
ctx?.setFillColor(UIColor.white.cgColor)
ctx?.addEllipse(in: CGRect(x: rect.minX + 15, y: rect.minY + 15, width: rect.width - 30, height: rect.height - 30))
ctx?.fillPath()
}
}
All you have to do is use this view as the tab icon, changing the enabled property when it is selected will automatically trigger a redraw. The result looks like this:
Of course, fine tune the values I have used to match your design exactly. Also note I've hardcoded values, you could determine your frames based on a proportion of the overall size for better reusability.
Hope this helps!
Related
Rather than using a normal button, I subclassed a UIControl because I needed to add a gradient to it. I also have a way to add a shadow and an activity indicator (not visible in the image below) as a stateful button to stop users hammering the button if (for example) an API call is being made.
It was really tricky to try to get the UIControl to rotate, and to be able to do this I added the shadow as a separate view to a container view containing the UIControl so a shadow could be added.
Now the issue is the control does not behave quite like a view on rotation - let me show you a screen grab for context:
This is mid-rotation but is just about visible to the eye - the image shows that the Gradient is 75% of the length of a blue UIView in the image.
https://github.com/stevencurtis/statefulbutton
In order to perform this rotation I remove the shadowview and then change the frame of the gradient frame to its bounds, and this is the problem.
func viewRotated() {
CATransaction.setDisableActions(true)
shadowView!.removeFromSuperview()
shadowView!.frame = self.frame
shadowView!.layer.masksToBounds = false
shadowView!.layer.shadowOffset = CGSize(width: 0, height: 3)
shadowView!.layer.shadowRadius = 3
shadowView!.layer.shadowOpacity = 0.3
shadowView!.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 20, height: 20)).cgPath
shadowView!.layer.shouldRasterize = true
shadowView!.layer.rasterizationScale = UIScreen.main.scale
self.gradientViewLayer.frame = self.bounds
self.selectedViewLayer.frame = self.bounds
CATransaction.commit()
self.insertSubview(shadowView!, at: 0)
}
So this rotation method is called through the parent view controller:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { context in
context.viewController(forKey: UITransitionContextViewControllerKey.from)
//inform the loginButton that it is being rotated
self.loginButton.viewRotated()
}, completion: { context in
// can call here when completed the transition
})
}
I know this is the problem, and I guess it is not happening at quite the right time to act the same way as a UIView. Now the issue is that I have tried many things to get this to work, and my best solution (above) is not quite there.
It isn't helpful to suggest to use a UIButton, to use an image for the gradient (please don't suggest using a gradient image as a background for a UIButton, I've tried this) or a third party library. This is my work, it functions but does not work acceptably to me and I want to get it to work as well as a usual view (or at least know why not). I have tried the other solutions above as well, and have gone for my own UIControl. I know I can lock the view if there is an API call, or use other ways to stop the user pressing the button too many times. I'm trying to fix my solution, not invent ways of getting around this issue with CAGradientLayer.
The problem: I need to make a UIControlView with a CAGradientLayer as a background rotate in the same way as a UIView, and not exhibit the issue shown in the image above.
Full Example:
https://github.com/stevencurtis/statefulbutton
Here is working code:
https://gist.github.com/alldne/22d340b36613ae5870b3472fa1c64654
These are my recommendations to your code:
1. A proper place for setting size and the position of sublayers
The size of a view, namely your button, is determined after the layout is done. What you should do is just to set the proper size of sublayers after the layout. So I recommend you to set the size and position of the gradient sublayers in layoutSubviews.
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)
selectedViewLayer.bounds = self.bounds
selectedViewLayer.position = center
gradientViewLayer.bounds = self.bounds
gradientViewLayer.position = center
}
2. You don’t need to use an extra view to draw shadow
Remove shadowView and just set the layer properties:
layer.shadowOffset = CGSize(width: 0, height: 3)
layer.shadowRadius = 3
layer.shadowOpacity = 0.3
layer.shadowColor = UIColor.black.cgColor
clipsToBounds = false
If you have to use an extra view to draw shadow, then you can add the view once in init() and set the proper size and position in layoutSubviews or you can just programmatically set auto layout constraints to the superview.
3. Animation duration & timing function
After setting proper sizes, your animation of the gradient layers and the container view doesn’t sync well.
It seems that:
During the rotation transition, coordinator(UIViewControllerTransitionCoordinator) has its own transition duration and easing function.
And the duration and easing function are applied automatically to all the subviews (UIView).
However, those values are not applied to the CALayer without an associated UIView. Consequently, it uses the default timing function and duration of CoreAnimation.
To sync the animations, explicitly set the animation duration and the timing function like below:
class ViewController: UIViewController {
...
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
CATransaction.setAnimationDuration(coordinator.transitionDuration)
CATransaction.setAnimationTimingFunction(coordinator.completionCurve.timingFunction)
}
...
}
// Swift 4
extension UIView.AnimationCurve {
var timingFunction: CAMediaTimingFunction {
let functionName: CAMediaTimingFunctionName
switch self {
case .easeIn:
functionName = kCAMediaTimingFunctionEaseIn as CAMediaTimingFunctionName
case .easeInOut:
functionName = kCAMediaTimingFunctionEaseInEaseOut as CAMediaTimingFunctionName
case .easeOut:
functionName = kCAMediaTimingFunctionEaseOut as CAMediaTimingFunctionName
case .linear:
functionName = kCAMediaTimingFunctionLinear as CAMediaTimingFunctionName
}
return CAMediaTimingFunction(name: functionName as String)
}
}
I am using CoreGraphics to draw single glyphs alongside with primitives in a CGContext. The following code works in a swift playground in XCode 9.2 When started in the playground a small rectangle with twice the letter A should appear at the given coordinates in the playground liveView.
import Cocoa
import PlaygroundSupport
class MyView: NSView {
init(inFrame: CGRect) {
super.init(frame: inFrame)
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
// setup context properties
let context: CGContext = NSGraphicsContext.current!.cgContext
context.setStrokeColor(CGColor.black)
context.setTextDrawingMode(.fill)
context.setFillColor(CGColor(red: 0.99, green: 0.99, blue: 0.85, alpha: 1))
context.beginPath()
context.addRect(rect)
context.fillPath()
context.setFillColor(.black)
// prepare variables and constants
var font = CTFontCreateWithName("Helvetica" as CFString, 48, nil)
var glyph = CTFontGetGlyphWithName(font, "A" as CFString)
var glyph1Position = CGPoint(x: self.frame.minX, y: self.frame.maxY/2)
var glyph2Position = CGPoint(x: self.frame.minX+150, y: self.frame.maxY/2)
let text = "Hello"
var textOrigin = NSPoint(x: self.frame.minX+50, y: self.frame.maxY/2)
// draw one character
CTFontDrawGlyphs(font, &glyph, &glyph1Position, 1, context)
// *** *** when the next line is uncommented the bug appears *** ***
// text.draw(at: textOrigin)
CTFontDrawGlyphs(font, &glyph, &glyph2Position, 1, context)
}
}
var frameRect = CGRect(x: 0, y: 0, width: 200, height: 100)
PlaygroundPage.current.liveView = MyView(inFrame: frameRect)
Now I want to draw regular text in the same context. However, when the text string is drawn between the drawing of the two glyphs via its own drawing method, the current context seems to be messed up, the second glyph will not show. When the text is drawn after both single glyphs are drawn everything is fine.
So obviously drawing the text seems to have an impact on the current CGContext, but I cannot find out what exactly is happening. I tried the saveGstate() method befor drawing the string and restoring afterwards, but without success.
I also tried using CoreText methods to create an attributed String with CTFramesetterCreateWithAttributedString and showing it with CTFramesetterCreateFrame, it also does not work, here after the creation of the framesetter the context is messed up.
My actual playground is more complex, there the glyphs do not entirely disappear but are shown at a wrong vertical position, but the basic problem - and question is the same:
How can I draw the text into the currentContext whithout any other changes to the context being done in the background?
You need to set the text matrix (the transform applied to text drawing). You should always set this before drawing text, because it isn't part of the graphics state and may get trashed by other drawing code such as AppKit's string drawing extensions.
Add the following before your call to CTFontDrawGlyphs:
context.textMatrix = .identity
This should be called in the initial setup of the context since there is no promise that the text matrix will be identity before calling drawRect. Then, any time you have made calls to something that modifies the text matrix you will need to set it back to what you want (identity in this case, but it could be something else if you wanted to draw in a fancy way).
It is not always obvious what will modify the text matrix. AppKit drawing functions almost always do (though I'm not aware of any documentation indicating this). Core Text functions that modify the context, like CTFrameDraw and CTLineDraw, will generally document this fact with a statement such as:
This call can leave the context in any state and does not flush it after the draw operation.
Similarly CTFontDrawGlyphs warns:
This function modifies graphics state including font, text size, and text matrix if these attributes are specified in font. These attributes are not restored.
As a rule I discourage mixing text drawing systems. Use AppKit or Core Text, but don't mix them. If you pick one and stick to it, then this generally isn't a problem (as long as you initialize the matrix once at the top of drawRect). For example, if you did all the drawing with CTFontDrawGlyphs, you wouldn't need to reset the matrix each time; you'd stay in the Core Text matrix and it'd be fine (which is why this works when you comment out the draw(at:) call).
I’m working on a iOS-App based on Xcode’s “Master-Detail Template” and want to use custom colors for some of the UI elements.
However, I couldn’t find out how to change the right separator of the UINavigationBar:
I’ve already tried to change the backgroundColor of UINavigationBar, UINavigationItem and its titleView but without success.
Would be great if someone has a clue.
EDIT:
I’ve just noted that viewed in vertical mode, it’s the whole separator that I want to ink?
A slight modification of this answer gives you
extension UINavigationBar {
func setRightBorderColor(color: UIColor, width: CGFloat) {
let rightBorderRect = CGRect(x: frame.width, y: 0, width: width, height: frame.height)
let rightBorderView = UIView(frame: rightBorderRect)
rightBorderView.backgroundColor = color
addSubview(rightBorderView)
}
}
I'm trying to create a simple spinning loading dash. I know how to do the loop but I can't seem to make it on a single line. Any ideas?
let loop = 1
while loop > 0 {
// spinning dash
}
I will not provide you with all the code to your question but rather a guideline of what to do. In general, its a two step algorithm.
Draw a line
Perform a 360° rotation of it for a desired time, t
The code posted below implements the first portion. I have added comments and I believe it should be self explanatory. After reviewing it, I'd recommend you read about UIBezierPath.
As for the second part, there are two ways of going about this.
1. Rotate the line itself (recommended)
Should you choose this method, here's a tutorial from Ray Wenderlich which covers it extensively along with the Math behind it. Follow through both portions of the tutorial if possible.
2. Rotate the view encompassing the line
Changing the outer view's background color to clear then rotating itself will give the illusion that the line inside is the one rotated. Here's a video guide for view rotations.
import UIKit
class ViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
// This is the black subview in which the line will be drawn into
let lineView: GeneralDraw = GeneralDraw(frame: CGRect(origin: CGPoint(x: 20, y: 30), size: CGSize(width: 300, height: 300)))
// uncomment this to remove the black colour
// lineView.backgroundColor = .clear
// add this lineView to the mainView
self.view.addSubview(lineView)
}
}
// This handles the drawing inside a given view
class GeneralDraw: UIView
{
override init(frame: CGRect)
{
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect)
{
let linePath = UIBezierPath()
// start point of the line
linePath.move(to: CGPoint(x:50, y:10))
// end point of the line
linePath.addLine(to: CGPoint(x:200, y:10))
linePath.close()
// cosmetic settings for the line
UIColor.red.set()
linePath.stroke()
linePath.fill()
}
}
I would use a CAReplicatorLayer for this. You start with a layer that draws a horizontal bar and combine it with transforms that show the bar in the other positions. Then you animate the fading out of the bar, with an offset coordinated to the fading.
In this gif, I've deliberately slowed down the animation. (There is a mild glitch at the point where the gif repeats, but ignore that; the real project doesn't have that glitch.)
1. Solution: rotate Images
create a set of images which shows the dash rotating.
set the images to an array. then animate that `UIImageView.startAnimating()
see section "Animating a Sequence of Images" of UIImageView.
2. Solution: standard iOS activity indicator
But better go with the standard UIActivityIndicatorView
see also:
iOS Human Interface Guidelines: Progress Indicators
Reference for UIActivityIndicatorView
Im trying to change the background color of a header row in a view-based NSTableView but not having any luck
tried this:
tableView.headerView?.layer?.backgroundColor = CGColor.black
and this (as was suggested in an earlier Obj-C post from a while ago):
for column in tableView.tableColumns {
column.headerCell.backgroundColor = NSColor(red: 0/255, green: 108/255, blue: 178/255, alpha: 1)
column.headerCell.textColor = NSColor.blue
}
Creating a custom NSTableHeaderCell subclass and setting NSTableColumn's headerCell property allows you to customize the appearance of your table's header row.
Apple's documentation recommends overriding the drawInterior:withFrame method when customizing a header cell, but this will not fill the background color for the whole cell - overriding draw:withFrame is needed. Calling drawInterior will draw the header's label and other elements. If you want to keep the header's content centered vertically in the cell, adjusting the interior frame is necessary.
override func draw(withFrame cellFrame: NSRect, in controlView: NSView) {
NSColor.gray.setFill()
NSBezierPath.fill(cellFrame)
let interiorFrame = CGRect(x: cellFrame.origin.x, y: cellFrame.origin.y + 4.0, width: cellFrame.size.width, height: 14.0)
drawInterior(withFrame: interiorFrame, in: controlView)
}
One thing to note - the column separator lines will need to be drawn as well if you want them.
I also have same problem. I found out that the headercell is inherit from NSTextFieldCell. Which have background Color properties. you can access to that and set it to new color. remember to set draw background to YES.
*when you create new column*
[[newCol headerCell] setDrawsBackground:YES];
- (void)tableView:(NSTableView *)tableView didClickTableColumn:(NSTableColumn *)tableColumn{
[[tableColumn headerCell] setBackgroundColor:<color you want to change to>];
}
Adding a colored subview is probably the cleanest way to achieve this. It goes between the default background and the cells that draw the header text and controls
NSRect rect = tableview.headerView.frame;
rect.size.width = 10000; /// just pick something wide enough here
MyColoredView * coverView = [[MyColoredView alloc] initWithFrame:rect];
coverView.autoresizingMask = tableview.headerView.autoresizingMask;
[tableview.headerView.superview addSubview:aView positioned:NSWindowBelow relativeTo:tableview.headerView];