Swift 3 - Most appropriate way to create a button with a scalable image above text - swift

In Android I was able to accomplish this custom button by creating a LinearLayout with 2 children-an ImageView and a TextView, and just controlling the layout properties (and adding onClick, etc. to recreate button functionality). Using weights, I was able to have the image scale to fill the button while allowing the appropriate proportions for the text.
For Example:
<LinearLayout
android:id="#+id/my_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#drawable/button_background"
android:clickable="true"
android:focusable="true"
android:onClick="onClickFunction"
android:orientation="vertical" >
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="5"
android:layout_marginTop="-8dp"
android:src="#mipmap/icon_test" />
<TextView
android:id="#+id/my_button_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Button Text"
android:textSize="14sp"
android:textAllCaps="true"
android:textStyle="bold"
android:textColor="#color/text_color"
android:textAlignment="center" />
</LinearLayout>
Produces:
What is the most appropriate way to do this in Swift 3? I've read about using insets to control the positioning (even setting negative "Left" insets on the image to position it above the text), but the image dimensions would have to be managed manually. I've also been made aware that setting the image as the background would have it scale, but I don't want to have the text overlap the image. My thought process is that there could be a "workaround" of sorts similar to my example above for Android, but I doubt the solution will be similar.
Furthermore, I'm pretty dependent on the IB (my app is not very complex so it's easily managed), so I'm using this for my button so far:
import UIKit
#IBDesignable
class RoundButton: UIButton {
#IBInspectable var cornerRadius: CGFloat = 0{
didSet{
self.layer.cornerRadius = cornerRadius
}
}
#IBInspectable var borderWidth: CGFloat = 0{
didSet{
self.layer.borderWidth = borderWidth
}
}
#IBInspectable var borderColor: UIColor = UIColor.clear{
didSet{
self.layer.borderColor = borderColor.cgColor
}
}
}
If it's not asking too much I'd like to be able to build off of this.

Here is the example how you can achieve that:
let btnSort = UIButton(type: .system)
btnSort.frame = CGRect(x: 2, y: 74, width: 140, height: 140)
btnSort.tintColor = UIColor.white
btnSort.setImage(UIImage(named:"lamp"), for: .normal)
btnSort.imageEdgeInsets = UIEdgeInsets(top: 6,left: 20,bottom: 30,right: 20)
btnSort.titleEdgeInsets = UIEdgeInsets(top: 100,left: -250,bottom: 0,right: 0)
btnSort.setTitle("BUTTON TEXT", for: .normal)
btnSort.layer.borderWidth = 1.0
btnSort.backgroundColor = UIColor.red
btnSort.layer.borderColor = UIColor.white.cgColor
self.view.addSubview(btnSort)
And result will be:
And here is the original post. I have changed EdgeInsets as per your requirement.

Related

Changing the separator color of UINavigationBar

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)
}
}

UIImageView: applying multiple UIViewContentMode properties to one UIImageView?

How can I add UIViewContentMode.center to this UIImageView while also keeping .scaleAspectFill?
func insertImage() {
let theImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 300))
theImageView.image = #imageLiteral(resourceName: "coolimage")
theImageView.contentMode = UIViewContentMode.scaleAspectFill
view.addSubview(theImageView)
}
Furthermore, can somebody explain to me what the "view" exactly is in the last line "view.addSubview(theImageView)"? Is it the mysterious "view hierarchy" that I read about? Why can't I simply initialize the UIImageView? Why must it be bound to something called "view" that I haven't explicitly created? There is only a UIViewController and a UIImageView so far.
As far as I know, you can't set content mode to both aspect fit and center. However, center will do what aspect fit does providing the image size is smaller than the size of the imageView. If not, use aspect fit. The following code ought to allow you to differentiate between the two:
if (theImageView.bounds.size.width > UIImage(named: "coolimage")?.size.width && theImageView.bounds.size.height > UIImage(named: "coolimage")?.size.height) {
theImageView.contentMode = .aspectFit
} else {
theImageView.contentMode = .center
}
As for the second part of your question, I'll refer you to this thread, which has a fairly comprehensive explanation of UIViewController vs UIView. Hope that helps.

Swift 3 - Custom UIButton radius deletes constraints?

My view controller possesses a stack view with 3 buttons. One fixed in the center with a fixed width and the stackview set to fill proportionally such that the other two buttons will be symmetrical.
However I am also customizing the corner radius of the buttons and as soon as the application loads the button resizes in an undesired fashion.
Ive attempted numerous stackview distribution and fill settings. Removing the buttons from the stackview and simply trying to contraint them to edges on a normal UIView to no avail as it seems, but uncertain if, the constraints get deleted.
Visually the button will be located at the bottom right hand corner of the screen with 0 space between the edge and the button. Currently it gets laid out in a manner where there is no constraint it seems on multiple devices causing it to have a space on larger displays, and exit the screen on small displays within the simulator.
Attempted coding efforts to round the desired corners:
#IBOutlet fileprivate weak var button: UIButton! { didSet {
button.round(corners: [.topRight, .bottomLeft], radius: 50 , borderColor: UIColor.blue, borderWidth: 5.0)
button.setNeedsUpdateConstraints()
button.setNeedsLayout()
} }
override func viewDidLayoutSubviews() {
button.layoutIfNeeded()
}
ovverride func updateViewContraints() {
button.updateConstraintsIfNeeded()
super.updateViewConstraints()
}
The UIView extension that is being used can be found here:
https://stackoverflow.com/a/35621736/5434541
What solution is available to properly adjust the buttons corner radius' and allow the constraints to update the button to as it should be.
Without seeing your full code, I suspect you're getting this because it's drawing multiple layers with the border, and never removing any! If the buttons get resized, the old layers are still there. Here's a snip that shows removing the old layer on redraw that you should be able to adapt. I tested this inside a stack view, and it also behaves correctly on rotation:
class RoundedCorner: UIButton {
var lastBorderLayer:CAShapeLayer?
override func layoutSubviews() { setup() }
func setup() {
let r = self.bounds.size.height / 2
let path = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.topLeft, .bottomRight],
cornerRadii: CGSize(width: r, height: r))
let mask = CAShapeLayer()
mask.path = path.cgPath
addBorder(mask: mask, borderColor: UIColor.blue, borderWidth: 5)
self.layer.mask = mask
}
func addBorder(mask: CAShapeLayer, borderColor: UIColor, borderWidth: CGFloat) {
let borderLayer = CAShapeLayer()
borderLayer.path = mask.path
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = borderColor.cgColor
borderLayer.lineWidth = borderWidth
borderLayer.frame = bounds
if let last = self.lastBorderLayer, let index = layer.sublayers?.index(of: last) {
layer.sublayers?.remove(at: index)
}
layer.addSublayer(borderLayer)
self.lastBorderLayer = borderLayer
}
}
You also noted in your comment about the app lag. I'm not surprised, since layout can get called many times per update, and you're creating a new layer every single time. You can avoid this by saving the last dimensions of the botton frame and comparing. If it's identical, don't recreate the layer.

How to give an imageView in swift3.0.1 shadow at the same time with rounded corners

I want to give an imageView a shadow at the same time with rounded corners,but I failed.
Here is my solution
Basic idea :
Use an Extra view (say AView) as super view of image view (to those views on which you are willing to have shado) and assign that view class to DGShadoView
Pin Image view to AView (that super view)from left, right, top and bottom with constant 5
Set back ground color of the AView to clear color from storybosrd's Property inspector this is important
Inside idea: Here we are using a Bezier path on the Aview nearly on border and setting all rounded corner properties and shadow properties to that path and we are placing our target image view lie with in that path bound
#IBDesignable
class DGShadoView:UIView {
override func draw(_ rect: CGRect) {
self.rect = rect
decorate(rect: self.rect)
}
func decorate(rect:CGRect) {
//self.backgroundColor = UIColor.clear
//IMPORTANT: dont forgot to set bg color of your view to clear color from story board's property inspector
let ref = UIGraphicsGetCurrentContext()
let contentRect = rect.insetBy(dx: 5, dy: 5);
/*create the rounded oath and fill it*/
let roundedPath = UIBezierPath(roundedRect: contentRect, cornerRadius: 5)
ref!.setFillColor("your color for background".cgColor)
ref!.setShadow(offset: CGSize(width:0,height:0), blur: 5, color: "your color for shado".cgColor)
roundedPath.fill()
/*draw a subtle white line at the top of view*/
roundedPath.addClip()
ref!.setStrokeColor(UIColor.red.cgColor)
ref!.setBlendMode(CGBlendMode.overlay)
ref!.move(to: CGPoint(x:contentRect.minX,y:contentRect.minY+0.5))
ref!.addLine(to: CGPoint(x:contentRect.maxX,y:contentRect.minY+0.5))
}
}
Update
Extension Approach
There is another Approach. Just Make a class with empty and paste Following UIImageView Extension code, Assign this subclass to that ImageView on which you shadow.
import UIKit
class DGShadowView: UIImageView {
#IBInspectable var intensity:Float = 0.2{
didSet{
setShadow()
}
}
override func layoutSubviews()
{
super.layoutSubviews()
setShadow()
}
func setShadow(){
let shadowPath = UIBezierPath(rect: bounds)
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOffset = CGSize(width: 0.0, height: 0.3)
layer.shadowOpacity = intensity
layer.shadowPath = shadowPath.cgPath
}
}
The solution is to create two separate views. One for the shadow and one for the image itself. On the imageView you clipToBounds the layer so that the corner radius is properly added.
Put the imageView on top of the shadowView and you've got your solution!

Change Height of UIProgressView in Swift

I've been trying to change the size and rotation of a progress view to adopt the size of the screen. It would basically function have a filling glass effect on the screen of the phone.
I rotated it fairly harmlessly by using
self.masteryProgress.transform = CGAffineTransformMakeRotation((CGFloat(-90) / CGFloat(180.0) * CGFloat(M_PI)))
I got the size of the view encasing the screen using view.bounds trying to get the rect and the width x height. So I'm not stuck there.
I tried using .frame .drawRect but when I use those it either breaks them or does nothing.
The height attribute seems to be locked at a constant value of 2 in interfaceBuilder.
Any solutions before I build a custom animation?
I tried setting the height of the progress bar in interfaceBuilder like some other posts said, but I need to have the progress view fill the whole screen no matter what device it's running on so that solution won't work in my case.
I ended up using
CGAffineTransformScale(masteryProgress.transform, view.bounds.height / 1334, (view.bounds.width / 2))
Since I'm rotating the progressView vertically, I set the progress view to a width of 1334 (the height of the iPhone 6 Plus) and a height of 2. By dividing the height of the screen by these values it should resize to any phone perfectly.
You can scale it by set its transform like so:
Swift 2
masteryProgress.transform = CGAffineTransformScale(masteryProgress.transform, 1, 20)
Swift 3、4
masteryProgress.transform = masteryProgress.transform.scaledBy(x: 1, y: 20)
I faced problem while making the UIProgressView as round rect one. While using only the transform
masteryProgress.transform = CGAffineTransformMakeScale(1, 4)
I achieved which was not my final requirement.
while my final expectation was something like the following
I finally achieved it by following two steps
1. Set Height Constraint from Interface Builder:
2. Create a custom class of UIProgressView and override func layoutSubviews() to add a custom mask layer:
override func layoutSubviews() {
super.layoutSubviews()
let maskLayerPath = UIBezierPath(roundedRect: bounds, cornerRadius: 4.0)
let maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds
maskLayer.path = maskLayerPath.cgPath
layer.mask = maskLayer
}
And here is the final result
Transform still seems to be the only way to accomplish this. Try adding it as an extension to UIProgressView like so.
extension UIProgressView {
#IBInspectable var barHeight : CGFloat {
get {
return transform.d * 2.0
}
set {
// 2.0 Refers to the default height of 2
let heightScale = newValue / 2.0
let c = center
transform = CGAffineTransformMakeScale(1.0, heightScale)
center = c
}
}
}
If you added UIProgressView using Interface Builder, just create height constraint and change to value what you need. That's it.
This will work:
masteryProgress.transform = CGAffineTransformMakeScale(1, 4)
Some modification of 'Sauvik Dolui' answer:
In UIBezierPath you should set cornerRadius: like (self.frame.height / 2).
This is more universal solution than use (4.0).
override func layoutSubviews() {
super.layoutSubviews()
let maskLayerPath = UIBezierPath(roundedRect: bounds, cornerRadius: self.frame.height / 2)
let maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds
maskLayer.path = maskLayerPath.cgPath
layer.mask = maskLayer
}
In Swift 3, it's changed to CGAffineTransform(scaleX: CGFloat, y: CGFloat). So code would be:
masteryProgress.transform = CGAffineTransform(scaleX: 1, y: 4)
In Swift version 3.0.2 use this:
masteryProgress.transform = masteryProgress.transform.scaledBy(x: 1, y: 5)
I'm experiencing same with Swift - UIProgressView fills diagonally
After scale progressbar with
CGAffineTransformMakeScale(1.0, 5.0)
bar animation becomes filling from left top corner to right bottom.
Putting the UIProgressView inside a UIStackView and setting the UIStackView's constraints might work as well. At least worked for me
I tried the accepted answer for Swift 5, had a label pinned to the bottom of the progressView and the label was oddly stretched. To change the height without any other issues I used it's heightAnchor to set it to the height I wanted.
1- create a programmatic UIProgressView:
lazy var progressView: UIProgressView = {
let progressView = UIProgressView(progressViewStyle: .default)
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.trackTintColor = .white
progressView.progressTintColor = .green
progressView.progress = 0
return progressView
}()
2- set the heightAnchor constraint:
progressView.heightAnchor.constraint(equalToConstant: 12).isActive = true
progressView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
progressView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20).isActive = true
progressView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20).isActive = true
It works for me:
progress.transform = progress.transform.scaledBy(x: 1, y: self.bounds.size.height)
"self.bounds.size.height" is the height you want
I gave it height constraint and it worked for me. I gave it 10 Constant. But if you are creating a universal app you can also use multipliers.