How to achieve an angular butterbar effect in swift? - ios

We are looking to achieve something like a "butterbar", which is basically a UIView that changes color from the middle outwards to the edges. An example is this codepen .
How to do this in swift?

Here's a solution for you. Basically, there's a ButterBar UIView subclass, that has an inner view subview, and an array of colours.
Set the background to colour[0]
Set the inner background to colour[1]
Set the inner width to 0
Animate the inner width to the ButterBar width
Set the background colour to the inner background colour
Set the inner background to the next colour
Repeat
Code…
class ButterBar: UIView {
private let innerView = UIView(frame: .zero)
private var colours: [UIColor] = [.black, .white]
private var colourIndex = 0
private var isAnimating = false
private lazy var widthConstraint: NSLayoutConstraint = {
return innerView.widthAnchor.constraint(equalToConstant: 0)
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
func configure(colours: [UIColor]) {
guard colours.count > 1 else { return }
self.colours = colours
}
func startAnimating() {
colourIndex = 0
isAnimating = true
updateColours()
animate()
}
func stopAnimating() {
isAnimating = false
}
}
private extension ButterBar {
func configure() {
innerView.translatesAutoresizingMaskIntoConstraints = false
addSubview(innerView)
topAnchor.constraint(equalTo: innerView.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: innerView.bottomAnchor).isActive = true
centerXAnchor.constraint(equalTo: innerView.centerXAnchor).isActive = true
widthConstraint.isActive = true
}
func updateColours() {
backgroundColor = colours[colourIndex]
colourIndex = (colourIndex + 1) % colours.count
innerView.backgroundColor = colours[colourIndex]
}
func animate() {
widthConstraint.constant = 0
layoutIfNeeded()
widthConstraint.constant = bounds.width
UIView.animate(withDuration: 1, animations: {
self.layoutIfNeeded()
}) { _ in
if self.isAnimating {
self.updateColours()
self.animate()
}
}
}
}
And…
#IBOutlet weak var butterBar: ButterBar!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
butterBar.configure(colours: [.red, .blue, .green, .yellow])
butterBar.startAnimating()
}

You could use two UIViews that are constrained to the center of the parent and possess width constraints. Simply set the background color of the first view and animate its width constraint from 0 to the width of the parent. Once this is done, you can bring the other view to front and animate its width from 0 to the width of the parent, then restart with the initial view to keep the cycle going.

Related

Can't change border colour of switch inside tableview's cell

I've a switch inside table which I'm creating programmatically. I can't change switch's off border colour to gray. I tried tint colour which isn't working either.
How to fix it?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell else {
fatalError("...")
}
//...
let switchView = UISwitch()
switchView.layer.borderColor = UIColor.greyColour.cgColor
cell.accessoryView = switchView
return cell
}
You didn't specify what effect you want to achieve but for layer.borderColor to work you need to setup layer.borderWidth also. However, because switch layer is rectangular it will look like this:
Which might be not what you want. So to make the border follow the switcher's shape you'll need to modify its corner radius:
switchView.layer.borderColor = UIColor.gray.cgColor
switchView.layer.borderWidth = 1.0
switchView.layer.cornerRadius = 16.0
to make it looks like this:
Update
If you want to apply border only for switcher off state it'll be a bit more tricky because you need to handle switcher states changes. The easiest way I could think of is to subclass UISwitch and provide your own behaviour by overriding sendActions method:
class BorderedSwitch: UISwitch {
var borderColor: UIColor = UIColor.gray {
didSet {
layer.borderColor = borderColor.cgColor
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
override var isOn: Bool {
didSet {
updateState()
}
}
override func sendActions(for controlEvents: UIControl.Event) {
super.sendActions(for: controlEvents)
if controlEvents.contains(.valueChanged) {
updateState()
}
}
private func setup() {
layer.borderColor = borderColor.cgColor
layer.cornerRadius = frame.height / 2
layer.borderWidth = 1.0
}
private func updateState() {
layer.borderWidth = isOn ? 0.0 : 1.0
}
}
Notice that I also updated cornerRadius value to frame.height / 2 to avoid magic numbers
If you add a switch with code, it looks just like a switch you add in a storyboard. Neither way of creating a switch has a border color.
The code below adds a switch to a view controller's content view:
#IBOutlet var switchView: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
switchView = UISwitch()
switchView.translatesAutoresizingMaskIntoConstraints = false //Remember to do this for UIViews you create in code
if false {
//Add a gray border to the switch
switchView.layer.borderWidth = 1.0 // Draw a rounded rect around the switchView so you can see it
switchView.layer.borderColor = UIColor.gray.cgColor
switchView.layer.cornerRadius = 16
}
//Add it to the container view
view.addSubview(switchView)
//Create center x & y layout anchors (with no offset to start)
let switchViewXAnchor = switchView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0)
switchViewXAnchor.isActive = true
let switchViewYAnchor = switchView.centerYAnchor.constraint(equalTo: view.centerYAnchor,
constant: 0.0)
switchViewYAnchor.isActive = true
}
It looks perfectly normal.

How to correctly handle movement on a zoomed image with a UIScrollView?

I am new to Swift and I'm trying to reproduce the behavior most applications have when you zoom in an image (like Twitter, Facebook etc...). Basically, you tap on the image, it opens fullscreen and then you can zoom in and move freely around the zoomed in image. However, I'm having trouble with the last two parts.
My main ViewController has an instance of a custom UIImageView called TappableImage, which inherits from UIImageView. I use this class as a wrapper to handle touch and transitions, code is below:
import UIKit
class TappableImage: UIImageView {
private let windowBounds: CGRect = UIScreen.main.bounds
private let imageV: UIImageView = UIImageView()
private var startingFrame: CGRect = .zero
private var backdrop: ImageBackdrop!
private var isOpen: Bool = false
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
image = UIImage(named: "kaws") // horizontal
// image = UIImage(named: "portrait") // vertical
// image = UIImage(named: "ksg") // square
isUserInteractionEnabled = true
startingFrame = frame
imageV.image = self.image
imageV.frame = startingFrame
imageV.contentMode = .scaleAspectFill
imageV.isUserInteractionEnabled = true
backdrop = ImageBackdrop(frame: windowBounds, with: imageV)
let openTapGesture = UITapGestureRecognizer(target: self, action: #selector(onTap))
openTapGesture.numberOfTapsRequired = 1
addGestureRecognizer(openTapGesture)
}
#objc private func onTap(_ sender: UITapGestureRecognizer) {
guard let window = UIApplication.shared.windows.first else {
print("no window...")
return
}
window.addSubview(backdrop)
UIView.animate(withDuration: 0.25, animations: {
self.backdrop.alpha = 1
let h = (self.windowBounds.width / self.startingFrame.width) * self.startingFrame.height
let y = self.windowBounds.height / 2 - h / 2
self.imageV.frame = CGRect(x: 0, y: y, width: self.windowBounds.width, height: h)
}) { (_) in
self.isOpen = true
}
}
}
When a user tap the image, I bring a custom UIScrollView to the front in which I added my TappableImage. The UIScrollView is a basic implementation just handling pinching, code is below:
import UIKit
class ImageBackdrop: UIScrollView, UIScrollViewDelegate {
private var imageView: UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
convenience init(frame: CGRect, with imgv: UIImageView) {
self.init(frame: frame)
imageView = imgv
setupScrollView()
addSubview(imageView)
}
private func setupScrollView() -> Void {
delegate = self
backgroundColor = .black
alpha = 0
minimumZoomScale = 1.0
maximumZoomScale = 5.0
contentSize = imageView.bounds.size
autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? { imageView }
}
My issue is that when I zoom in, the image is shifted down and I cannot move freely around with one finger. If I try to move to the bottom of the image, I am "slingshotted" back to the top, like when you arrive at the end of a list, you can find a video of this behavior here.
My guess is that the UIImageView center or size is incorrect but I cannot find a way to fix it.

Fill UIView using a percentage

I have an UIView and percentage number that comes from an API. I need to have the UIView fill with some color based on the percentage.
Here's the UIView
I've got the basics from another question here.
class BadgeView: UIView {
private let fillView = UIView(frame: CGRect.zero)
private var coeff:CGFloat = 0.5 {
didSet {
// Make sure the fillView frame is updated everytime the coeff changes
updateFillViewFrame()
}
}
override func awakeFromNib() {
setupView()
}
private func setupView() {
// Setup the layer
layer.cornerRadius = bounds.height/2.0
layer.masksToBounds = true
// Setup filledView backgroundColor and add it as a subview
fillView.backgroundColor = UIColor(red: 220.0/255.0, green: 220.0/255.0, blue: 220.0/255.0, alpha: 0.4)
addSubview(fillView)
// Update fillView frame in case coeff already has a value
updateFillViewFrame()
}
private func updateFillViewFrame() {
fillView.frame = CGRect(x: 0, y: bounds.height*(1-coeff), width: bounds.width, height: bounds.height*coeff)
}
// Setter function to set the coeff animated. If setting it not animated isn't necessary at all, consider removing this func and animate updateFillViewFrame() in coeff didSet
public func setCoeff(coeff: CGFloat, animated: Bool) {
if animated {
UIView.animate(withDuration: 4.0, animations:{ () -> Void in
self.coeff = coeff
})
} else {
self.coeff = coeff
}
}
}
This is the function that returns the percentage:
func cargaOKCheckStatus(resultado: NSDictionary) {
let JSON = resultado
if let ElapsedPercentual:Int = JSON.value(forKeyPath: "ResponseEntity.ElapsedPercentual") as? Int {
porcentaje = ElapsedPercentual
print(porcentaje)
}
}
The API returns 0%, 10%, 20%, 30% and so on. So if it's 10%, the UIView should be filled a 10% with the light gray. Right now, it is always half filled.
Few things:
You can use autolayout to simplify things
Override both init(frame:) and init(coder:) when creating custom views and call the setup in both methods
class BadgeView: UIView {
private let fillView = UIView(frame: CGRect.zero)
private var fillHeightConstraint: NSLayoutConstraint!
private(set) var coeff: CGFloat = 0.0 {
didSet {
updateFillViewFrame()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
layer.cornerRadius = bounds.height / 2.0
layer.masksToBounds = true
fillView.backgroundColor = UIColor(red: 220.0/255.0, green: 220.0/255.0, blue: 220.0/255.0, alpha: 0.4)
fillView.translatesAutoresizingMaskIntoConstraints = false // ensure autolayout works
addSubview(fillView)
// pin view to leading, trailing and bottom to the container view
fillView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
fillView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
fillView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
// save the constraint to be changed later
fillHeightConstraint = fillView.heightAnchor.constraint(equalToConstant: 0)
fillHeightConstraint.isActive = true
updateFillViewFrame()
}
private func updateFillViewFrame() {
fillHeightConstraint.constant = bounds.height * coeff // change the constraint value
layoutIfNeeded() // update the layout when a constraint changes
}
public func setCoeff(coeff: CGFloat, animated: Bool) {
if animated {
UIView.animate(withDuration: 4.0, animations:{ () -> Void in
self.coeff = coeff
})
} else {
self.coeff = coeff
}
}
}
This should work, assuming that coeff is a value between 0.0 and 1.0

Cannot change navigation bar item height in iOS 11

After customizing the navigation bar height bigger than the default value (44pt), I want to change the height of my right side navigation bar item button, but it's limited in 44pt. How can I make it taller? I know that in iOS 11, the button now is inside a UIBarButtonStackView, it seems we cannot change the stack view frame?
I use this code to change the width and height of the button:
button.widthAnchor.constraint(equalToConstant: 40).isActive = true
button.heightAnchor.constraint(equalToConstant: 60).isActive = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(image, for: .normal)
let barButton = UIBarButtonItem(customView: button)
self.navigationItem.rightBarButtonItem = barButton
Thank you!
You can change the width of navigation bar button item by using this code -
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var frame: CGRect? = navigationItem.leftBarButtonItem?.customView?.frame
frame?.size.width = 5 // change the width of your item bar button
self.navigationItem.leftBarButtonItem?.customView?.frame = frame!
}
override var prefersStatusBarHidden : Bool {
return true
}
Or from storyboard -
Make sure your Assets.xcassets image is set as Render As - Original Image Just like -
Using subclass of UInavigationcontroller class and NavigationBar class you can achieve this.
I am sharing some code of snipt:
class ARVNavigationController {
init(rootViewController: UIViewController) {
super.init(navigationBarClass: AVNavigationBar.self, toolbarClass: nil)
viewControllers = [rootViewController] }}
class AVNavigationBar {
let AVNavigationBarHeight: CGFloat = 80.0
init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
init(frame: CGRect) {
super.init(frame: frame ?? CGRect.zero)
initialize()
}
func initialize() {
transform = CGAffineTransform(translationX: 0, y: +AVNavigationBarHeight)
}
func layoutSubviews() {
super.layoutSubviews()
let classNamesToReposition = ["_UINavigationBarBackground", "UINavigationItemView", "UINavigationButton"]
for view: UIView? in subviews() {
if classNamesToReposition.contains(NSStringFromClass(view.self)) {
let bounds: CGRect = self.bounds()
let frame: CGRect? = view?.frame
frame?.origin.y = bounds.origin.y + CGFloat(AVNavigationBarHeight)
frame?.size.height = bounds.size.height - 20.0
view?.frame = frame ?? CGRect.zero
}
}
}
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
}

How do I animate the height of an input accessory view?

I am experiencing strange behavior when animating the height of an input accessory view. What am I doing wrong?
I create a UIInputView subclass (InputView) with a single subview. The height of InputView and its intrinsicContentSize are controlled by the subview. InputView is 50 pixels tall when isVisible is true and 0 pixels tall when isVisible is false.
import UIKit
class InputView: UIInputView {
private let someHeight: CGFloat = 50.0, zeroHeight: CGFloat = 0.0
private let subView = UIView()
private var hide: NSLayoutConstraint?, show: NSLayoutConstraint?
var isVisible: Bool {
get {
return show!.isActive
}
set {
// Always deactivate constraints before activating conflicting ones
if newValue == true {
hide?.isActive = false
show?.isActive = true
} else {
show?.isActive = false
hide?.isActive = true
}
}
}
// MARK: Sizing
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: size.width, height: someHeight)
}
override var intrinsicContentSize: CGSize {
return CGSize.init(width: bounds.size.width, height: subView.bounds.size.height)
}
// MARK: Initializers
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect, inputViewStyle: UIInputViewStyle) {
super.init(frame: frame, inputViewStyle: inputViewStyle)
addSubview(subView)
subView.backgroundColor = UIColor.purple
translatesAutoresizingMaskIntoConstraints = false
subView.translatesAutoresizingMaskIntoConstraints = false
subView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor).isActive = true
subView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
subView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
subView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true
show = subView.heightAnchor.constraint(equalToConstant: someHeight)
hide = subView.heightAnchor.constraint(equalToConstant: zeroHeight)
hide?.isActive = true
}
}
The host view controller toggles isVisible in a one-second animation block when a button is pressed.
import UIKit
class MainViewController: UIViewController {
let testInputView = InputView.init(frame: .zero, inputViewStyle: .default)
#IBAction func button(_ sender: AnyObject) {
UIView.animate(withDuration: 1.0) {
let isVisible = self.testInputView.isVisible
self.testInputView.isVisible = !isVisible
self.testInputView.layoutIfNeeded()
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override var inputAccessoryView: UIView? {
return testInputView
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
I expect the input accessory view to smoothly grow from the botton of the screen when isVisible is set to true, and smoothly shrink to the button of the screen when isVisible is set to false. Instead, the keyboard background overlay appears at full 50-pixel height as soon as isVisible is true and the input accessory view grows from the center of its frame.
When shrinking, the input accessory view instantly loses some of its height before continuing the animation smoothly.
I created an input accessory view demonstration project that displays this unexpected behavior.
This will give you the correct animation:
UIView.animate(withDuration: 1.0) {
let isVisible = self.testInputView.isVisible
self.testInputView.isVisible = !isVisible
self.testInputView.superview?.superview?.layoutIfNeeded()
}
However, it's never a good practice to call superview if Apple changes the design. So there may be a better answer.
This is what the superviews represent:
print(testInputView.superview) // UIInputSetHostView
print(testInputView.superview?.superview) // UIInputSetContainerView
EDIT: ADDED A SAFER SOLUTION
I'm not too familiar with the UIInputView. But one way of solving it without calling the superview would be to only animate the height change of the subview:
Step 1:
Move the isVisible outside the animation block.
#IBAction func button(_ sender: AnyObject) {
let isVisible = self.testInputView.isVisible
self.testInputView.isVisible = !isVisible
UIView.animate(withDuration: 1.0) {
self.testInputView.layoutIfNeeded()
}
}
Step 2:
Create a new method in your InputView which changes the height constraint of the InputView instead of the intrinsicContentSize.
private func updateHeightConstraint(height: CGFloat) {
for constraint in constraints {
if constraint.firstAttribute == .height {
constraint.constant = height
}
}
self.layoutIfNeeded()
}
Step 3:
And call that method inside the setter.
if newValue == true {
updateHeightConstraint(height: someHeight)
hide?.isActive = false
show?.isActive = true
} else {
updateHeightConstraint(height: zeroHeight)
show?.isActive = false
hide?.isActive = true
}
Step 4:
Lastly some changes in the init.
override init(frame: CGRect, inputViewStyle: UIInputViewStyle) {
super.init(frame: frame, inputViewStyle: inputViewStyle)
addSubview(subView)
backgroundColor = .clear
subView.backgroundColor = UIColor.purple
subView.translatesAutoresizingMaskIntoConstraints = false
subView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
subView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
subView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor).isActive = true
show = subView.heightAnchor.constraint(equalToConstant: someHeight)
hide = subView.heightAnchor.constraint(equalToConstant: zeroHeight)
hide?.isActive = true
}
Conclusion:
This result in the InputView changes it's height before animating the height of the purple subview. The only downside is the UIInputView, which has some kind of gray background as default and cannot be changed to Clear. However, you can use the same backgroundColor as the VC.
But if you instead should go with a regular UIView as InputAccessoryView it will be UIColor.clear as default. Than the first "jump" will not be noticed.

Resources