How to add a custom border to viewcontroller (ios) - ios

I have a view (UIPopoverPresentation) which functionality works fine, but I need to add a custom border. I'm currently using a borderWidth and borderColor but I cannot seem to find a way to make a customized border, as seen in the photo below. How do I go about creating this customized border? Make a CGRect?
What I need:
What I have:
I've attempted to add an image to the background of the popover and it resulted in this:
EDIT: //PopOverView (presented using UIPopOverPresentation)
override func viewDidLoad() {
super.viewDidLoad()
self.view.layer.cornerRadius = 10.0
self.view.layer.borderWidth = 1.5
self.view.layer.borderColor = UIColor.whiteColor().CGColor
self.navigationController?.navigationBarHidden = true
self.popViewTableView.delegate = self
self.popViewTableView.dataSource = self
self.popViewTableView.alwaysBounceVertical = false
self.popViewTableView.backgroundColor = UIColor(red: 151.0/255.0, green: 87.0/255.0, blue: 172.0/255.0, alpha: 1.0)
}
//Base View Controller. When button is pressed, this function is called which presents the popover
func presentPopOver() {
let contentView = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("popViewController") as! DeckPopViewController
contentView.modalPresentationStyle = UIModalPresentationStyle.Popover
contentView.preferredContentSize = CGSizeMake(deckSelectionCGRect.width, 160)
let popoverMenuViewController = contentView.popoverPresentationController!
popoverMenuViewController.delegate = self
popoverMenuViewController.sourceView = view
popoverMenuViewController.permittedArrowDirections = UIPopoverArrowDirection(rawValue:0)
popoverMenuViewController.sourceRect = CGRectMake((self.view.bounds.width/2) - (deckSelectionCGRect.width/2), 120, deckSelectionCGRect.width, deckSelectionCGRect.height)
presentViewController(contentView, animated: true, completion: nil)
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}
//ALSO: On the third image, I placed the border imageview over the entire view controller in the storyboard, set padding to zero on all sides (autolayout). It doesn't look good, though.

Related

How to keep UIButton back to its position after dismissing keyboard in iOS?

I have a UIButton at bottom of the screen and when the user types in UITextView that button gets attached to the keypad (input accessory view) like the screenshot I have attached (violet arrow mark). Now once the keyboard is dismissed then I want this button to be at the bottom of the screen instead of the input accessory view (check yellow arrow mark flow).
Here is the code I used
override func viewDidLoad() {
super.viewDidLoad()
confirmButtonUI()
subscribeToShowKeyboardNotifications()
// Do any additional setup after loading the view.
}
func confirmButtonUI() {
confirmButton.layer.cornerRadius = 20.0
confirmButton.layer.shadowRadius = 1.0
confirmButton.layer.shadowColor = UIColor(displayP3Red: 33/255, green: 68/255, blue: 27/255, alpha: 0.18).cgColor
confirmButton.layer.backgroundColor = UIColor(displayP3Red: 164/255, green: 208/255, blue: 208/255, alpha: 1).cgColor
confirmButton.isEnabled = false
confirmButton.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
confirmButton.layer.shadowOpacity = 1.0
confirmButton.layer.masksToBounds = false
}
func subscribeToShowKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector:
#selector(keyboardWillShow(_:)), name:
UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector:
#selector(keyboardWillHide(_:)), name:
UIResponder.keyboardWillHideNotification, object: nil)
}
#objc func keyboardWillShow(_ notification: Notification) {
let userInfo = notification.userInfo
confirmButton.layer.cornerRadius = 0.0
othersTextField.inputAccessoryView = confirmButton
let keyboardSize = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue
_ = keyboardSize.cgRectValue.height
let animationDuration = userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double
UIView.animate(withDuration: animationDuration) {
self.view.layoutIfNeeded()
}
}
#objc func keyboardWillHide(_ notification: Notification) {
othersTextField.inputAccessoryView = nil
confirmButtonBottomConstrains.constant = 57 //Crash here
let userInfo = notification.userInfo
let animationDuration = userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double
UIView.animate(withDuration: animationDuration) {
self.view.layoutIfNeeded()
}
}
In this method its crashing on this line " confirmButtonBottomConstrains.constant = 57 "
What i was doing is once the keyboard is dismissing am making inputAccessoryView as nil and then am trying to use the bottom nslayout constraint for the button to set as 57 (like i set in UI) but this line crashing with following message
Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
Please help me to solve this
Problem
You haven't provided details about whether the view is setup programmatically or in a storyboard. I am assuming it was a storyboard (since I do not see any view initialization).
Also, I am not sure how you got the button to stick as an inputAccessoryView. I couldn't replicate this behavior. In your case, setting a view that is already part of the view hierarchy as an accessory view might be causing the undocumented behavior where existing constraints are removed.
Solution
Instead of using the same button, you can very easily create a new button and assign it as an inputAccessoryView to your UITextField / UITextView.
This would eliminate problems with missing constraints since the original view is never changed.
It would look something like this, (in addition to your disabled button):
Code to setup input accessory view:
override func viewDidLoad() {
super.viewDidLoad()
setupInputAccessory()
othersTextField.delegate = self
}
func setupInputAccessory(){
let width = UIScreen.main.bounds.width
let frame = CGRect(x: 0, y: 0, width: width, height: 44.0)
let btn = UIButton(frame: frame)
btn.backgroundColor = UIColor.purple
btn.setTitleColor(UIColor.white, for: .normal)
btn.setTitle("Confirm", for: .normal)
btn.addTarget(self, action: #selector(accessoryConfirmTapped), for: .touchUpInside)
othersTextField.inputAccessoryView = btn
}
#objc func accessoryConfirmTapped(){
othersTextField.resignFirstResponder()
}
Notes
The above is a code snippet in addition to your existing code. However, with this approach I have removed the following from your code.
Changing the othersTextField.inputAccessoryView in keyboard events.
Confirm Button bottom constraint.
But the gist of it is, I have two separate buttons. One that is setup by the storyboard and the other setup in code and attached to the TextField. I do not share the button with the controller's view and as the input accessory.
I have the complete solution in a GitHub Repo if you want to check it out further.

how to custom flip and transition to another view controller with button click ? (n swift)

basically my current setup is like this
one storyboard ViewController with 3 types of UI View(container, front view, back view) inside of it.
what i want to accomplish (and i don't know how to implement #2)
user enters the data on the form(front of the card- View Controller number 1)
clicks the save button (do animation flipping and redirect to a new view controller)
the new view controller loads up (back of the card - View Controller number 2)
this is the current code flip example:
import UIKit
class HomeViewController: UIViewController {
#IBOutlet weak var goButton: UIButton!
#IBOutlet weak var optionsSegment: UISegmentedControl!
let owlImageView = UIImageView(image: UIImage(named:"img-owl"))
let catImageView = UIImageView(image: UIImage(named:"img-cat"))
var isReverseNeeded = false
override func viewDidLoad() {
super.viewDidLoad()
title = "Transitions Test"
setupView()
}
fileprivate func setupView() {
let screen = UIScreen.main.bounds
goButton.layer.cornerRadius = 22
//container to hold the two UI views
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 250, height: 250))
containerView.backgroundColor = UIColor(red: 6/255, green: 111/255, blue: 165/255, alpha: 1.0)
containerView.layer.borderColor = UIColor.white.cgColor
containerView.layer.borderWidth = 2
containerView.layer.cornerRadius = 20
containerView.center = CGPoint(x: screen.midX, y: screen.midY)
view.addSubview(containerView)
//front view
catImageView.frame.size = CGSize(width: 100, height: 100)
catImageView.center = CGPoint(x: containerView.frame.width/2, y: containerView.frame.height/2)
catImageView.layer.cornerRadius = 50
catImageView.clipsToBounds = true
//back view
owlImageView.frame.size = CGSize(width: 100, height: 100)
owlImageView.center = CGPoint(x: containerView.frame.width/2, y: containerView.frame.height/2)
owlImageView.layer.cornerRadius = 50
owlImageView.clipsToBounds = true
containerView.addSubview(owlImageView)
}
#IBAction func goButtonClickHandler(_ sender: Any) {
doTransition()
}
fileprivate func doTransition() {
let duration = 0.5
var option:UIViewAnimationOptions = .transitionCrossDissolve
switch optionsSegment.selectedSegmentIndex {
case 0: option = .transitionFlipFromLeft
case 1: option = .transitionFlipFromRight
case 2: option = .transitionCurlUp
case 3: option = .transitionCurlDown
case 4: option = .transitionCrossDissolve
case 5: option = .transitionFlipFromTop
case 6: option = .transitionFlipFromBottom
default:break
}
if isReverseNeeded {
UIView.transition(from: catImageView, to: owlImageView, duration: duration, options: option, completion: nil)
} else {
UIView.transition(from: owlImageView, to: catImageView, duration: duration, options: option, completion: nil)
}
isReverseNeeded = !isReverseNeeded
}
}
There are a few alternatives for transition between view controllers with a flipping animation:
You can define a segue in IB, configure that segue to do a horizontal flipping animation:
If you want to invoke that segue programmatically, give the segue a “Identifier” string in the attributes inspector and then you can perform it like so:
performSegue(withIdentifier: "SecondViewController", sender: self)
Alternatively, give the actual destination view controller’s scene a storyboard identifier, and the presenting view controller can just present the second view controller:
guard let vc = storyboard?.instantiateViewController(identifier: "SecondViewController") else { return }
vc.modalTransitionStyle = .flipHorizontal
vc.modalPresentationStyle = .currentContext
show(vc, sender: self)
If this standard flipping animation isn’t quite what you want, you can customize it to your heart’s content. iOS gives us rich control over custom transitions between view controller by specifying transitioning delegate, supplying an animation controller, etc. It’s a little complicated, but it’s outlined in WWDC 2017 Advances in UIKit Animations and Transitions: Custom View Controller Transitions (about 23:06 into the video) and WWDC 2013 Custom Transitions Using View Controllers.

How to give settings(alpha value for background view) in SideMenu in swift?

I’m using left side menu from jonkykong/SideMenu this framework. Here i need to change background view colour alpha value while open(means click on button) the side menu and when i close the side menu i need back the original background colour.
for that I have tried two methods:
Method 1):
func sidemenuOpenCloseColorChang(){
let menu = storyboard!.instantiateViewController(withIdentifier: "UISideMenuNavigationController") as! UISideMenuNavigationController
var set = SideMenuSettings()
set.statusBarEndAlpha = 0
set.presentationStyle = SideMenuPresentationStyle.menuSlideIn
set.presentationStyle.presentingEndAlpha = 0.5
set.menuWidth = min(view.frame.width, view.frame.height) * 0.90
//menu.sideMenuManager.addScreenEdgePanGesturesToPresent(toView: self.view)
menu.settings = set
//SideMenuManager.default.addScreenEdgePanGesturesToPresent(toView: view)
SideMenuManager.default.leftMenuNavigationController = menu
}
getting below error:
Use of unresolved identifier 'SideMenuSettings'
Use of unresolved identifier 'SideMenuPresentationStyle'
Value of type 'SideMenuManager' has no member 'leftMenuNavigationController'
Method 2): using UISideMenuNavigationControllerDelegate Metod
#IBAction func sideMenubtn(_ sender: Any) {
view?.backgroundColor = UIColor(white: 1, alpha: 0.9)
}
extension ProfileViewController : UISideMenuNavigationControllerDelegate {
private func sideMenuWillDisappear(menu: UISideMenuNavigationControllerDelegate, animated: Bool) {
//*do the color thing*
print("sidemenu disappear")
view?.backgroundColor = UIColor.white
}
Nothing worked for me, please help me in this code.
Thanks in advance

How to remove shadow below UINavigationBar with UISearchController

I could successfully remove the shadow below the navigation bar with the following line of code.
self.navigationController?.navigationBar.shadowImage = UIImage()
When I added a search controller however, the shadow reappeared.
self.navigationItem.searchController = UISearchController(searchResultsController: nil)
I tried the following, but resulted an unexpected behavior.
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.barTintColor = .white
self.navigationController?.navigationBar.isTranslucent = false
How do I remove the shadow under a navigation bar when there is a search controller attached?
I have not found good solution too...
for now I will hide it this way:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let imageView = navigationItem.searchController?.searchBar.superview?.subviews.first?.subviews.first as? UIImageView {
imageView.isHidden = true
}
}
Here's the solution that I use
Create a separate class extending a UINavigationController instance, lets call it BaseNavigationController. Here's your class for you
If you are using storyboards, assign BaseNavigationController to your UINavigationController scene in the storyboard
If you are initializing a navigation controller via code, ex: UINavigationController.init(rootViewController: someViewControllerInstance) then simply use BaseNavigationController.init(rootViewController: someViewControllerInstance)
An example of the class is shown below:
open class BaseNavigationController:UINavigationController {
override open func viewDidLoad() {
super.viewDidLoad()
setNavigationBar()
setNavBarBorder(false)
}
func setNavigationBar(color:UIColor?=UIColor.white, tint:UIColor?=UIColor.darkGray){
let appearance = UIBarButtonItem.appearance()
appearance.setBackButtonTitlePositionAdjustment(UIOffset.init(horizontal: 0.0, vertical: 0), for: .default)
self.navigationBar.barTintColor = color!
self.navigationBar.isTranslucent = false
self.navigationBar.tintColor = tint!
self.navigationBar.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: tint! ]
}
func setTitleColor(_ color:UIColor?=UIColor.darkGray){
}
func setNavBarBorder(_ enable:Bool) {
self.navigationBar.setBackgroundImage((enable ? nil : UIImage()), for: UIBarMetrics.default)
self.navigationBar.shadowImage = (enable ? nil : UIImage())
self.navigationBar.setValue(true, forKey: "hidesShadow")
}
}
Now, the fun part, if you are handling ambiguous layout you might need to do this in viewWillLayoutSubviews otherwise putting this bit of code in viewWillAppear
of the viewController instance.
(self.navigationController as? BaseNavigationController)?. setNavBarBorder(false)
the interesting bit of code is in self.navigationBar.setValue(true, forKey: "hidesShadow"), for some versions of iOS.
You can instead add searchBar to your viewController in storyBoard and set its Search Style property to Minimal and it'll be look like this:
My little contribution about this issue. Of course, it is not the right way to do it but it's working for opaque navigation bar. The idea is to add a view on top of this shadow.
let searchBar = searchController.searchBar
let maskView = UIView(frame: .zero)
maskView.backgroundColor = .white // or any color you want
searchBar.addSubview(maskView)
//We need to use constraint so the mask view follow the search bar animation
maskView.translatesAutoresizingMaskIntoConstraints = false
let views = ["maskView": maskView]
searchBar.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-(0)-[maskView]-(0)-|",
options: NSLayoutConstraint.FormatOptions.alignAllCenterY,
metrics: nil,
views: views))
searchBar.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[maskView(1)]-(-1)-|",
options: NSLayoutConstraint.FormatOptions.alignAllCenterX,
metrics: nil,
views: views))

UINavigationBar color animation synchronized with push animation

I want to achieve smooth animation between views with a different UINavigationBar background colors. Embedded views have the same background color as UINavigationBar and I want to mimic push/pop transition animation like:
I've prepared custom transition:
class CustomTransition: NSObject, UIViewControllerAnimatedTransitioning {
private let duration: TimeInterval
private let isPresenting: Bool
init(duration: TimeInterval = 1.0, isPresenting: Bool) {
self.duration = duration
self.isPresenting = isPresenting
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView
guard
let toVC = transitionContext.viewController(forKey: .to),
let fromVC = transitionContext.viewController(forKey: .from),
let toView = transitionContext.view(forKey: .to),
let fromView = transitionContext.view(forKey: .from)
else {
return
}
let rightTranslation = CGAffineTransform(translationX: container.frame.width, y: 0)
let leftTranslation = CGAffineTransform(translationX: -container.frame.width, y: 0)
toView.transform = isPresenting ? rightTranslation : leftTranslation
container.addSubview(toView)
container.addSubview(fromView)
fromVC.navigationController?.navigationBar.backgroundColor = .clear
fromVC.navigationController?.navigationBar.setBackgroundImage(UIImage.fromColor(color: .clear), for: .default)
UIView.animate(
withDuration: self.duration,
animations: {
fromVC.view.transform = self.isPresenting ? leftTranslation :rightTranslation
toVC.view.transform = .identity
},
completion: { _ in
fromView.transform = .identity
toVC.navigationController?.navigationBar.setBackgroundImage(
UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
for: .default
)
transitionContext.completeTransition(true)
}
)
}
}
And returned it in the UINavigationControllerDelegate method implementation:
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomTransition(isPresenting: operation == .push)
}
While push animation works pretty well pop doesn't.
Questions:
Why after clearing NavBar color before pop animation it remains yellow?
Is there any better way to achieve my goal? (navbar can't just be transparent all the time because it's only a part of the flow)
Here is the link to my test project on GitHub.
EDIT
Here is the gif presenting the full picture of discussed issue and the desired effect:
These components are always very difficult to customize. I think, Apple wants system components to look and behave equally in every app, because it allows to keep shared user experience around whole iOS environment.
Sometimes, it easier to implement your own components from scratch instead of trying to customize system ones. Customization often could be tricky because you do not know for sure how components are designed inside. As a result, you have to handle lots of edge cases and deal with unnecessary side effects.
Nevertheless, I believe I have a solution for your situation. I have forked your project and implemented behavior you had described.
You can find my implementation on GitHub. See animation-implementation branch.
UINavigationBar
The root cause of pop animation does not work properly, is that UINavigationBar has it's own internal animation logic. When UINavigationController's stack changes, UINavigationController tells UINavigationBar to change UINavigationItems. So, at first, we need to disable system animation for UINavigationItems. It could be done by subclassing UINavigationBar:
class CustomNavigationBar: UINavigationBar {
override func pushItem(_ item: UINavigationItem, animated: Bool) {
return super.pushItem(item, animated: false)
}
override func popItem(animated: Bool) -> UINavigationItem? {
return super.popItem(animated: false)
}
}
Then UINavigationController should be initialized with CustomNavigationBar:
let nc = UINavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)
UINavigationController
Since there is requirement to keep animation smooth and synchronized between UINavigationBar and presented UIViewController, we need to create custom transition animation object for UINavigationController and use CoreAnimation with CATransaction.
Custom transition
Your implementation of transition animator almost perfect, but from my point of view few details were missed. In the article Customizing the Transition Animations you can find more info. Also, please pay attention to methods comments in UIViewControllerContextTransitioning protocol.
So, my version of push animation looks as follows:
func animatePush(_ transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView
guard let toVC = transitionContext.viewController(forKey: .to),
let toView = transitionContext.view(forKey: .to) else {
return
}
let toViewFinalFrame = transitionContext.finalFrame(for: toVC)
toView.frame = toViewFinalFrame
container.addSubview(toView)
let viewTransition = CABasicAnimation(keyPath: "transform")
viewTransition.duration = CFTimeInterval(self.duration)
viewTransition.fromValue = CATransform3DTranslate(toView.layer.transform, container.layer.bounds.width, 0, 0)
viewTransition.toValue = CATransform3DIdentity
CATransaction.begin()
CATransaction.setAnimationDuration(CFTimeInterval(self.duration))
CATransaction.setCompletionBlock = {
let cancelled = transitionContext.transitionWasCancelled
if cancelled {
toView.removeFromSuperview()
}
transitionContext.completeTransition(cancelled == false)
}
toView.layer.add(viewTransition, forKey: nil)
CATransaction.commit()
}
Pop animation implementation is almost the same. The only difference in CABasicAnimation values of fromValue and toValue properties.
UINavigationBar animation
In order to animate UINavigationBar we have to add CATransition animation on UINavigationBar layer:
let transition = CATransition()
transition.duration = CFTimeInterval(self.duration)
transition.type = kCATransitionPush
transition.subtype = self.isPresenting ? kCATransitionFromRight : kCATransitionFromLeft
toVC.navigationController?.navigationBar.layer.add(transition, forKey: nil)
The code above will animate whole UINavigationBar. In order to animate only background of UINavigationBar we need to retrieve background view from UINavigationBar. And here is the trick: first subview of UINavigationBar is _UIBarBackground view (it could be explored using Xcode Debug View Hierarchy). Exact class is not important in our case, it is enough that it is successor of UIView.
Finally we could add our animation transition on _UIBarBackground's view layer direcly:
let backgroundView = toVC.navigationController?.navigationBar.subviews[0]
backgroundView?.layer.add(transition, forKey: nil)
I would like to note, that we are making prediction that first subview is a background view. View hierarchy could be changed in future, just keep this in mind.
It is important to add both animations in one CATransaction, because in this case these animations will run simultaneously.
You could setup UINavigationBar background color in viewWillAppear method of every view controller.
Here is how final animation looks like:
I hope this helps.
The way I would do it is by making the navigation controller completely transparent. This way the animation of the contained view controller should give the effect you want.
Edit: You can get also "white content" by having a containerView constrained under the navigation bar. In the sample code I did that. The push picks randomly a color and gives to the container view randomly white or clear. You will see that all the scenarios in your gif are covered by this example.
Try this in playground:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
var containerView: UIView!
override func loadView() {
let view = UIView()
view.backgroundColor = .white
containerView = UIView()
containerView.backgroundColor = .white
containerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(containerView)
containerView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
let button = UIButton()
button.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
button.setTitle("push", for: .normal)
button.setTitleColor(.black, for: .normal)
button.addTarget(self, action: #selector(push), for: .touchUpInside)
containerView.addSubview(button)
self.view = view
}
#objc func push() {
let colors: [UIColor] = [.yellow, .red, .blue, .purple, .gray, .darkGray, .green]
let controller = MyViewController()
controller.title = "Second"
let randColor = Int(arc4random()%UInt32(colors.count))
controller.view.backgroundColor = colors[randColor]
let clearColor: Bool = (arc4random()%2) == 1
controller.containerView.backgroundColor = clearColor ? .clear: .white
navigationController?.pushViewController(controller, animated: true)
}
}
// Present the view controller in the Live View window
let controller = MyViewController()
controller.view.backgroundColor = .white
let navController = UINavigationController(rootViewController: controller)
navController.navigationBar.setBackgroundImage(UIImage(), for: .default)
controller.title = "First"
PlaygroundPage.current.liveView = navController
remove this code from your project:
toVC.navigationController?.navigationBar.setBackgroundImage(
UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
for: .default
)

Resources