Animating UIButton configuration change - ios

Modern UIButton configuration API allows for new way of setting button appearance. However the code below skips the animation entirely.
#IBAction func buttonTouched(_ sender: UIButton) {
sender.configuration?.background.backgroundColor = .green
UIView.animate(withDuration: 0.4, delay: 0.5) {
sender.configuration?.background.backgroundColor = .systemMint
}
}
Similar thing can be done like this:
#IBAction func buttonTouched(_ sender: UIButton) {
sender.backgroundColor = .red
UIView.animate(withDuration: 0.4, delay: 0.5) {
sender.backgroundColor = .systemMint
}
}
And this works. The question is how to animate UIButton's configuration changes.

After some quick searching, it appears configuration.background.backgroundColor is not animatable.
Depending on your needs, you can use a .customView for the button's background and then animate the color change for that view.
Quick example (assuming you've added the button as an #IBOutlet):
class TestVC: UIViewController {
#IBOutlet var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let v = UIView()
v.backgroundColor = .red
button.configuration?.background.customView = v
}
#IBAction func buttonTouched(_ sender: UIButton) {
if let v = sender.configuration?.background.customView {
UIView.animate(withDuration: 0.4, delay: 0.5) {
v.backgroundColor = v.backgroundColor == .red ? .systemMint : .red
}
}
}
}

The way to achieve animated configuration change by using transitions is the only workaround I could find so far. Somewhat like this:
#IBAction func buttonTouched(_ sender: UIButton) {
UIView.transition(with: sender, duration: 0.3, options: .transitionCrossDissolve) {
sender.configuration?.background.backgroundColor = .red
} completion: { _ in
UIView.transition(with: sender, duration: 1.0, options: .transitionCrossDissolve) {
sender.configuration?.background.backgroundColor = .cyan
}
}
}
When I tried to optimize and reuse code, I ended with something like:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Another option is subclass `UIButton` and override its `configurationUpdateHandler`
view.subviews.compactMap({ $0 as? UIButton }).forEach {
$0.configurationUpdateHandler = { button in
guard let config = button.configuration else { return }
button.setConfiguration(config,duration: 2)
}
}
}
#IBAction func buttonTouched(_ sender: UIButton) {
// sender.configuration?.background.backgroundColor = .red
// sender.configuration?.baseForegroundColor = .white
// The above would work, but we should rather aggregate all changes at once.
if var config = sender.configuration {
config.background.backgroundColor = .red
config.baseForegroundColor = .white
sender.configuration = config
}
}
}
extension UIButton {
func setConfiguration(_ configuration: UIButton.Configuration, duration: Double = 0.25, completion: ((Bool) -> Void)? = nil) {
UIView.transition(with: self, duration: duration, options: .transitionCrossDissolve) {
self.configuration? = configuration
} completion: { completion?($0) }
}
}
I

Related

Drag to dismiss a UIPresentationController

I have made a UIPresentationController that fits any view controller and shows up on half of the screen using this tutorial. Now I would love to add drag to dismiss to this. I'm trying to have the drag feel natural and responsive like the drag experience for "Top Stories" on the Apple iOS 13 stocks app. I thought the iOS 13 modal drag to dismiss would get carried over but it doesn't to this controller but it doesn't.
Every bit of code and tutorial I found had a bad dragging experience. Does anyone know how to do this? I've been trying / searching for the past week. Thank you in advance
Here's my code for the presentation controller
class SlideUpPresentationController: UIPresentationController {
// MARK: - Variables
private var dimmingView: UIView!
//MARK: - View functions
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
setupDimmingView()
}
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
override var frameOfPresentedViewInContainerView: CGRect {
guard let container = containerView else { return super.frameOfPresentedViewInContainerView }
let width = container.bounds.size.width
let height : CGFloat = 300.0
return CGRect(x: 0, y: container.bounds.size.height - height, width: width, height: height)
}
override func presentationTransitionWillBegin() {
guard let dimmingView = dimmingView else { return }
containerView?.insertSubview(dimmingView, at: 0)
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|",
options: [],
metrics: nil,
views: ["dimmingView": dimmingView]))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|",
options: [],
metrics: nil,
views: ["dimmingView": dimmingView]))
guard let coordinator = presentedViewController.transitionCoordinator else {
dimmingView.alpha = 1.0
return
}
coordinator.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 1.0
})
}
override func dismissalTransitionWillBegin() {
guard let coordinator = presentedViewController.transitionCoordinator else {
dimmingView.alpha = 0.0
return
}
coordinator.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 0.0
})
}
func setupDimmingView() {
dimmingView = UIView()
dimmingView.translatesAutoresizingMaskIntoConstraints = false
dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
dimmingView.alpha = 0.0
let recognizer = UITapGestureRecognizer(target: self,
action: #selector(handleTap(recognizer:)))
dimmingView.addGestureRecognizer(recognizer)
}
#objc func handleTap(recognizer: UITapGestureRecognizer) {
presentingViewController.dismiss(animated: true)
}
}
As your description about the dragging experience you wanted is not that clear, hope I didn't get you wrong.
I'm trying to have the drag feel natural and responsive like the drag experience for "Top Stories" on the Apple iOS 13 stocks app.
What I get is, you want to be able to drag the presented view, dismiss it if it reach certain point, else go back to its original position (and of coz you can bring the view to any position you wanted).
To achieve this, we can add a UIPanGesture to the presentedViewController, then
move the presentedView according to the gesture
dismiss / move back the presentedView
class SlideUpPresentationController: UIPresentationController {
// MARK: - Variables
private var dimmingView: UIView!
private var originalX: CGFloat = 0
//MARK: - View functions
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
setupDimmingView()
}
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
override var frameOfPresentedViewInContainerView: CGRect {
guard let container = containerView else { return super.frameOfPresentedViewInContainerView }
let width = container.bounds.size.width
let height : CGFloat = 300.0
return CGRect(x: 0, y: container.bounds.size.height - height, width: width, height: height)
}
override func presentationTransitionWillBegin() {
guard let dimmingView = dimmingView else { return }
containerView?.insertSubview(dimmingView, at: 0)
// add PanGestureRecognizer for dragging the presented view controller
let viewPan = UIPanGestureRecognizer(target: self, action: #selector(viewPanned(_:)))
containerView?.addGestureRecognizer(viewPan)
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
guard let coordinator = presentedViewController.transitionCoordinator else {
dimmingView.alpha = 1.0
return
}
coordinator.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 1.0
})
}
#objc private func viewPanned(_ sender: UIPanGestureRecognizer) {
// how far the pan gesture translated
let translate = sender.translation(in: self.presentedView)
switch sender.state {
case .began:
originalX = presentedViewController.view.frame.origin.x
case .changed:
// move the presentedView according to pan gesture
// prevent it from moving too far to the right
if originalX + translate.x < 0 {
presentedViewController.view.frame.origin.x = originalX + translate.x
}
case .ended:
let presentedViewWidth = presentedViewController.view.frame.width
let newX = presentedViewController.view.frame.origin.x
// if the presentedView move more than 0.75 of the presentedView's width, dimiss it, else bring it back to original position
if presentedViewWidth * 0.75 + newX > 0 {
setBackToOriginalPosition()
} else {
moveAndDismissPresentedView()
}
default:
break
}
}
private func setBackToOriginalPosition() {
// ensure no pending layout change in presentedView
presentedViewController.view.layoutIfNeeded()
UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseIn, animations: {
self.presentedViewController.view.frame.origin.x = self.originalX
self.presentedViewController.view.layoutIfNeeded()
}, completion: nil)
}
private func moveAndDismissPresentedView() {
// ensure no pending layout change in presentedView
presentedViewController.view.layoutIfNeeded()
UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseIn, animations: {
self.presentedViewController.view.frame.origin.x = -self.presentedViewController.view.frame.width
self.presentedViewController.view.layoutIfNeeded()
}, completion: { _ in
// dimiss when the view is completely move outside the screen
self.presentingViewController.dismiss(animated: true, completion: nil)
})
}
override func dismissalTransitionWillBegin() {
guard let coordinator = presentedViewController.transitionCoordinator else {
dimmingView.alpha = 0.0
return
}
coordinator.animate(alongsideTransition: { _ in
self.dimmingView.alpha = 0.0
})
}
func setupDimmingView() {
dimmingView = UIView()
dimmingView.translatesAutoresizingMaskIntoConstraints = false
dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
dimmingView.alpha = 0.0
let recognizer = UITapGestureRecognizer(target: self,
action: #selector(handleTap(recognizer:)))
dimmingView.addGestureRecognizer(recognizer)
}
#objc func handleTap(recognizer: UITapGestureRecognizer) {
presentingViewController.dismiss(animated: true)
}
}
The above code is just an example based on the code you provide, but I hope that explain what's happening under the hood of what you called a drag experience. Hope this helps ;)
Here is the example result:
via GIPHY

Control Center view

Am trying to create a view like control centre in iPhone. As per my requirement the view have to be at bottom when user swap up, View need to be present from bottom to top, while dismissing view need to be swap bottom.Exactly like control centre I need a view, Can Anyone help me?
You can achieve that by the following code:
class ViewController: UIViewController, UIGestureRecognizerDelegate {
var viewControlCenter : UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
createControlCenterView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Gesture Methods
#objc func respondToSwipeGesture(sender: UIGestureRecognizer? = nil) {
if let swipeGesture = sender as? UISwipeGestureRecognizer {
switch swipeGesture.direction {
case UISwipeGestureRecognizerDirection.up:
if self.viewControlCenter.frame.origin.y != 0 {
UIView.animate(withDuration: 0.5, animations: {
self.viewControlCenter.frame.origin.y = 0
}, completion: nil)
}
break
case UISwipeGestureRecognizerDirection.down:
if self.viewControlCenter.frame.origin.y == 0 {
UIView.animate(withDuration: 0.5, animations: {
self.viewControlCenter.frame.origin.y = self.view.frame.size.height
}, completion: nil)
}
break
default:
break
}
}
}
#objc func onViewTapped(sender: UITapGestureRecognizer? = nil) {
if self.viewControlCenter.frame.origin.y == 0 {
UIView.animate(withDuration: 0.5, animations: {
self.viewControlCenter.frame.origin.y = self.view.frame.size.height
}, completion: nil)
}
}
// MARK: - Custom Methods
func createControlCenterView() {
viewControlCenter = UIView.init(frame: self.view.bounds)
self.viewControlCenter.frame.origin.y = self.view.frame.size.height
let viewBG : UIView = UIView.init(frame: self.view.bounds)
viewBG.backgroundColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.6)
viewControlCenter.addSubview(viewBG)
self.view.addSubview(viewControlCenter)
let swipeUp = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeGesture(sender:)))
swipeUp.direction = .up
self.view.addGestureRecognizer(swipeUp)
let swipeDown = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeGesture(sender:)))
swipeDown.direction = .down
self.view.addGestureRecognizer(swipeDown)
let tap = UITapGestureRecognizer(target: self, action: #selector(onViewTapped(sender:)))
tap.delegate = self
viewControlCenter.addGestureRecognizer(tap)
}
}

Swiftycam media capture library delegate crashes

I am using this library called SwiftyCam As it is mentioned in its page, this part is a bit confusing:
SwiftyCam is a drop-in convenience framework. To create a Camera instance, create a new UIViewController subclass. Replace the UIViewController subclass declaration with SwiftyCamViewController.
So i created a new view controller extends class SwiftyCameraViewController: SwiftyCamViewController, SwiftyCamViewControllerDelegate as in sample project. And i called this class from my main UIViewcontroller(MainViewController.swift) as:
let swiftyCamera = SwiftyCameraViewController()
self.present(swiftyCamera, animated: true, completion: nil)
So, this is SwiftyCameraViewController:
import UIKit
class SwiftyCameraViewController: SwiftyCamViewController, SwiftyCamViewControllerDelegate {
#IBOutlet weak var captureButton: SwiftyRecordButton!
#IBOutlet weak var flipCameraButton: UIButton!
#IBOutlet weak var flashButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
cameraDelegate = self
maximumVideoDuration = 10.0
shouldUseDeviceOrientation = true
allowAutoRotate = true
audioEnabled = true
}
override var prefersStatusBarHidden: Bool {
return true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
captureButton.delegate = self
}
func swiftyCam(_ swiftyCam: SwiftyCamViewController, didTake photo: UIImage) {
let newVC = PhotoViewController(image: photo)
self.present(newVC, animated: true, completion: nil)
}
func swiftyCam(_ swiftyCam: SwiftyCamViewController, didBeginRecordingVideo camera: SwiftyCamViewController.CameraSelection) {
print("Did Begin Recording")
captureButton.growButton()
UIView.animate(withDuration: 0.25, animations: {
self.flashButton.alpha = 0.0
self.flipCameraButton.alpha = 0.0
})
}
func swiftyCam(_ swiftyCam: SwiftyCamViewController, didFinishRecordingVideo camera: SwiftyCamViewController.CameraSelection) {
print("Did finish Recording")
captureButton.shrinkButton()
UIView.animate(withDuration: 0.25, animations: {
self.flashButton.alpha = 1.0
self.flipCameraButton.alpha = 1.0
})
}
func swiftyCam(_ swiftyCam: SwiftyCamViewController, didFinishProcessVideoAt url: URL) {
let newVC = VideoViewController(videoURL: url)
self.present(newVC, animated: true, completion: nil)
}
func swiftyCam(_ swiftyCam: SwiftyCamViewController, didFocusAtPoint point: CGPoint) {
let focusView = UIImageView(image: #imageLiteral(resourceName: "focus"))
focusView.center = point
focusView.alpha = 0.0
view.addSubview(focusView)
UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseInOut, animations: {
focusView.alpha = 1.0
focusView.transform = CGAffineTransform(scaleX: 1.25, y: 1.25)
}, completion: { (success) in
UIView.animate(withDuration: 0.15, delay: 0.5, options: .curveEaseInOut, animations: {
focusView.alpha = 0.0
focusView.transform = CGAffineTransform(translationX: 0.6, y: 0.6)
}, completion: { (success) in
focusView.removeFromSuperview()
})
})
}
func swiftyCam(_ swiftyCam: SwiftyCamViewController, didChangeZoomLevel zoom: CGFloat) {
print(zoom)
}
func swiftyCam(_ swiftyCam: SwiftyCamViewController, didSwitchCameras camera: SwiftyCamViewController.CameraSelection) {
print(camera)
}
func swiftyCam(_ swiftyCam: SwiftyCamViewController, didFailToRecordVideo error: Error) {
print(error)
}
#IBAction func cameraSwitchTapped(_ sender: Any) {
switchCamera()
}
#IBAction func toggleFlashTapped(_ sender: Any) {
flashEnabled = !flashEnabled
if flashEnabled == true {
flashButton.setImage(#imageLiteral(resourceName: "flash"), for: UIControlState())
} else {
flashButton.setImage(#imageLiteral(resourceName: "flashOutline"), for: UIControlState())
}
}
}
It crashed at line 33: captureButton.delegate = self, because captureButton is nil
I appreciate any kind of help. Thank you.
You must load the viewController with identifier
let swiftyCamera = self.storyboard?.instantiateViewController(withIdentifier:"swiftyCameraID")
self.present(swiftyCamera, animated: true, completion: nil)

How do I get my backgroundImageView to be draggable instead of main view

class PhotoViewController: UIViewController {
override var prefersStatusBarHidden: Bool {
return true
}
private var backgroundImage: UIImage
init(image: UIImage) {
self.backgroundImage = image
super.init(nibName: nil, bundle: nil)
print(image)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.gray
let backgroundImageView = UIImageView(frame: view.frame)
backgroundImageView.image = backgroundImage
backgroundImageView.isUserInteractionEnabled = true
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction(_:)))
backgroundImageView.addGestureRecognizer(panGestureRecognizer)
view.addSubview(backgroundImageView)
}
func panGestureRecognizerAction(_ gesture: UIPanGestureRecognizer){
let translation = gesture.translation(in: view)
view.frame.origin.y = translation.y
if gesture.state == .ended{
let velocity = gesture.velocity(in: view)
if velocity.y > 1500{
UIView.animate(withDuration: 0.5, animations: {
self.dismiss(animated: true, completion: nil)
})
} else{
UIView.animate(withDuration: 0.3, animations: {
self.view.frame.origin = CGPoint(x: 0, y: 0)
})
}
}
}
My entire view moves and reveals a black background behind it. How do i get the backgroundImageView to move instead? I am trying to display a view underneath. If any other info is required please let me know.
In your panGestureRecognizerAction you are asking it to move view, which is the view of your viewController. Instead, use the view assigned to the gestureRecognizer by saying:
if let bgImage = gesture.view {
bgImage.frame.origin.y = translation.y
}
gesture.view is an optional, which is why I check it first. You could also say gesture.view?.frame.origin.y = translation.y and that'd work just fine too.

Animate UIView out scaled behaves strangely

I've had this problem before, but couldn't figure out what it is.
If I'm animating a view "standalone" in the Playground, this sort of thing looks fine, but now it just looks like in the provided GIF.
Animate it in look fine, but when I want to animate it out (scale) it just gets maxed-out size then disappears.
Here's the code that animates it:
self.circleView.transform = CGAffineTransformIdentity
UIView.animateWithDuration(0.5, delay: 0, options: [], animations: {
self.circleView.transform = CGAffineTransformScale(CGAffineTransformIdentity, 0.001, 0.001);
}, completion: {
finished in
if finished {
self.resetStyle()
self.circleView.hidden = true
self.circleView.transform = CGAffineTransformIdentity
}
})
func resetStyle() {
self.circleView.transform = CGAffineTransformIdentity
self.circleView.backgroundColor = nil
self.textLabel.textColor = UIColor.blackColor()
}
Try this:
#IBOutlet weak var circleView: UIView!
#IBAction func leftAction(sender: AnyObject) {
self.circleView.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1)
UIView.animateWithDuration(0.5) {
self.circleView.layer.transform = CATransform3DIdentity
}
}
#IBAction func rightAction(sender: AnyObject) {
circleView.layer.transform = CATransform3DIdentity
UIView.animateWithDuration(0.5) {
self.circleView.layer.transform = CATransform3DMakeScale(0.001, 0.001, 1)
}
}
override func viewDidLoad() {
super.viewDidLoad()
circleView.backgroundColor = .blueColor()
circleView.layer.cornerRadius = CGRectGetMidY(circleView.bounds)
}

Resources