Disable swipe down to dismiss gesture in page sheet modal with tableview - ios

Similar question to :
Disable gesture to pull down form/page sheet modal presentation
Looking for functionality exactly like this, except with a tableview under the navigation controller:
when I attempt to use the answer given, touchesBegan and touchesEnded do not get called, so I tried calling the functions in scrollViewWillBeginDragging and scrollViewWillEndDragging and while it does get called, it doesnt solve the issue. Scrolling the tableview will still close the modal most of the time.
I do not want to present over full screen.
isModalInPresentation stops it from closing but the bounce down still attempts. I want to stop that altogether.
I want to be able to only close if the user presses cancel or actually grabs the navigation bar to close like in the gif, and swipes within the tableview to only scroll the tableview. What else can I try?

If I understand well, this is an example to do it programmatically... Declare button under your controller class:
let myButton: UIButton = {
let b = UIButton()
b.backgroundColor = .black
b.setTitle("Tap me!", for: .normal)
b.setTitleColor(.white, for: .normal)
b.titleLabel?.font = .systemFont(ofSize: 17, weight: .regular)
b.layer.cornerRadius = 12
b.clipsToBounds = true
b.translatesAutoresizingMaskIntoConstraints = false
return b
}()
Now in viewDidLoad add target and set constraints:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
myButton.addTarget(self, action: #selector(callSeetController), for: .touchUpInside)
view.addSubview(myButton)
myButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
myButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
myButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
myButton.widthAnchor.constraint(equalToConstant: 200).isActive = true
}
After that call the func to present an set sheet attributes:
#objc fileprivate func callSeetController() {
let detailViewController = SecondController()
let nav = UINavigationController(rootViewController: detailViewController)
nav.isModalInPresentation = true // disable swipe down
if let sheet = nav.sheetPresentationController {
sheet.detents = [.large()]
}
let image = UIImage(systemName: "x.circle")
let dismiss = UIBarButtonItem(image: image, primaryAction: .init(handler: { [weak self] _ in
if let sheet = nav.sheetPresentationController {
sheet.animateChanges {
self?.dismiss(animated: true, completion: nil)
}
}
}))
detailViewController.navigationItem.rightBarButtonItem = dismiss
detailViewController.navigationController?.navigationBar.tintColor = .white
present(nav, animated: true, completion: nil)
}
The result:

Related

Custom anonymous closure navigation back button inside of UiView()

I have a question, how is it possible to implement the creation of a custom back navigation button inside an UIView(). I have a main controller which contains a collectionView, clicking on any cell goes to a second controller which contains a tableView. I created a separate custom view inside the tableView headers where I added labels, pictures, buttons. I need when clicking a backButton inside a custom view, it will go to the main controller. How can be implemented? I making app only programmatically - (No Storyboard)
CustomView.swift
lazy var backButton: UIButton = {
let button = UIButton(type: .system)
let image = UIImage(systemName: "chevron.left")
button.setImage(image, for: UIControl.State())
button.tintColor = .white
button.isHidden = true
button.addTarget(self, action: #selector(goToBack), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
#objc func goToBack() {
}
First add a callback function in the CustomView. Then call this callback closure from goToBack() method.
class CustomView: UIView {
var backButtonTapped: (() -> Void)?
lazy var backButton: UIButton = {
let button = UIButton(type: .system)
let image = UIImage(systemName: "chevron.left")
button.setImage(image, for: UIControl.State())
button.tintColor = .white
button.isHidden = true
button.addTarget(self, action: #selector(goToBack), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
#objc func goToBack() {
backButtonTapped?()
}
}
In UIViewController where you initialise this CustomView, give the action of the closure.
let view = CustomView()
view.backButtonTapped = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
You will need to create a delegate for this. In your CustomView make a property weak var delegate: ButtonDelegate
protocol ButtonDelegate: class {
func onTap()
}
And your ViewController holding the CustomView has do implement that protocol and do navigationController.popViewController() in the implemented onTap() method.
Call delegate?.onTap() in your CustomView goToBack() method.

How to bind SwiftUI and UIViewController behavior

I have a UIKit project with UIViewControllers, and I'd like to present an action sheet built on SwiftUI from my ViewController. I need to bind the appearance and disappearance of the action sheet back to the view controller, enabling the view controller to be dismissed (and for the display animation to happen only on viewDidAppear, to avoid some weird animation behavior that happens when using .onAppear). Here is a code example of how I expect the binding to work and how it's not doing what I'm expecting:
import UIKit
import SwiftUI
class ViewController: UIViewController {
let button = UIButton(type: .system)
var show = true
lazy var isShowing: Binding<Bool> = .init {
self.show
} set: { show in
// This code gets called
self.show = show
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
button.setTitle("TAP THIS BUTTON", for: .normal)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
}
#objc private func tapped() {
let vc = UIHostingController(rootView: BindingProblemView(testBinding: isShowing))
vc.modalPresentationStyle = .overCurrentContext
present(vc, animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [self] in
isShowing.wrappedValue.toggle()
isShowing.update()
}
}
}
struct BindingProblemView: View {
#Binding var testBinding: Bool
#State var state = "ON"
var body: some View {
ZStack {
if testBinding {
Color.red.ignoresSafeArea().padding(0)
} else {
Color.green.ignoresSafeArea().padding(0)
}
Button("Test Binding is \(state)") {
testBinding.toggle()
}.onChange(of: testBinding, perform: { value in
// This code never gets called
state = testBinding ? "ON" : "OFF"
})
}
}
}
What happens is that onChange never gets called after viewDidAppear when I set the binding value true. Am I just completely misusing the new combine operators?
You can pass the data through ObservableObjects, rather than with Bindings. The idea here is that ViewController has the reference to a PassedData instance, which is passed to the SwiftUI view which receives changes to the object as it's an #ObservedObject.
This now works, so you can click on the original button to present the SwiftUI view. The button in that view then toggles passedData.isShowing which changes the background color. Since this is a class instance, the ViewController also has access to this value. As an example, isShowing is also toggled within tapped() after 5 seconds to show the value can be changed from ViewController or BindingProblemView.
Although it is no longer needed, the onChange(of:perform:) still triggers.
Code:
class PassedData: ObservableObject {
#Published var isShowing = true
}
class ViewController: UIViewController {
let button = UIButton(type: .system)
let passedData = PassedData()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
button.setTitle("TAP THIS BUTTON", for: .normal)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
}
#objc private func tapped() {
let newView = BindingProblemView(passedData: passedData)
let vc = UIHostingController(rootView: newView)
vc.modalPresentationStyle = .overCurrentContext
present(vc, animated: false)
// Example of toggling from in view controller
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
self.passedData.isShowing.toggle()
}
}
}
struct BindingProblemView: View {
#ObservedObject var passedData: PassedData
var body: some View {
ZStack {
if passedData.isShowing {
Color.red.ignoresSafeArea().padding(0)
} else {
Color.green.ignoresSafeArea().padding(0)
}
Button("Test Binding is \(passedData.isShowing ? "ON" : "OFF")") {
passedData.isShowing.toggle()
}
}
}
}
Result:

UIPasteboard retrieving previous item

I've got the following code:
[[UIApplication sharedApplication] sendAction:#selector(copy:) to:nil from:self forEvent:nil];
NSString *string = [UIPasteboard generalPasteboard].string;
The problem is that the pasteboard is returning what I've copied previously, not currently. Somehow it's hanging onto the previous item I've copied and returning that, and then next time around I get the item that I wanted and copied... strange behaviour.. any ideas?
Edit
I've got a solution using an observer, but not sure how elegant it is.. there seems to be some delay between copy and setting an item.. enough for it not to be set and for the previous one to be still on the UIPasteBoard.
I have composed a little sample to your code for copy/paste behavior and it works as expected: selected text is printed to console after tapping on the button.
class ViewController: UIViewController {
let textField = UITextField()
let copyButton = UIButton(type: .roundedRect)
#objc func copySelectedText() {
UIApplication.shared.sendAction(#selector(UIResponder.copy(_:)), to: nil, from: self, for: nil)
let copiedString = UIPasteboard.general.string
print(copiedString)
}
override func viewDidLoad() {
super.viewDidLoad()
textField.borderStyle = .roundedRect
view.backgroundColor = .darkGray
display(textField, copyButton)
copyButton.setTitle("tap to copy selected text", for: .normal)
copyButton.addTarget(self, action: #selector(copySelectedText), for: .touchUpInside)
}
private func display(_ textField: UITextField, _ copyButton: UIButton) {
copyButton.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(copyButton)
view.addSubview(textField)
textField.backgroundColor = .white
textField.widthAnchor.constraint(greaterThanOrEqualToConstant: 200).isActive = true
textField.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
textField.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
copyButton.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 20).isActive = true
copyButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
}

UINavigationBar slides away instead of staying on place

I created demo project to show the problem.
We have two view controllers inside UINavigationController.
MainViewController which is the root.
class MainViewController: UIViewController {
lazy var button: UIButton = {
let button = UIButton()
button.setTitle("Detail", for: .normal)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Main"
view.backgroundColor = .blue
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
button.widthAnchor.constraint(equalToConstant: 150).isActive = true
button.heightAnchor.constraint(equalToConstant: 42).isActive = true
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#objc func buttonTapped(_ sender: UIButton) {
navigationController?.pushViewController(DetailViewController(), animated: true)
}
}
And DetailViewController which is pushed.
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
}
}
As you can see I want to hide UINavigationBar in DetailViewController:
Question
The problem is that, UINavigationBar slides away instead of stay of his place together with whole MainViewController. How can I change that behavior and keep pop gesture?
in your MainViewController add that method
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 0) {
self.navigationController?.setNavigationBarHidden(false, animated: false)
}
}
and replace your method with below method in DetailViewController
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
}
The following code is hacking.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 0) {
self.navigationController?.setNavigationBarHidden(false, animated: false)
}
}
Do not write this bizarre code, as suggested by #sagarbhut in his post (in this thread).
You have two choices.
Hack
Do not hack.
Use convenience functions like this one
https://developer.apple.com/documentation/uikit/uiview/1622562-transition
Create a custom segue, if you are using storyboards.
https://www.appcoda.com/custom-segue-animations/
Implement the UIViewControllerAnimatedTransitioning protocol
https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning
You can get some great results but I'm afraid you will need to work hard. There are numerous tutorials online that discuss how to implement the above.
Twitter's navigation transition where the pushed ViewController's view seems to take the entire screen "hiding the navigationBar", but still having the pop gesture animation and the navigationBar visible in the pushing ViewController even during the transition animation obviously cannot be achieved by setting the bar's hidden property.
Implementing a custom navigation system is one way to do it but I suggest a simple solution by playing on navigationBar's layer and its zPosition property. You need two steps,
Set the navigationBar's layer zPosition to a value that'd place it under its siblings which include the current visible view controller's view in the navigation stack: navigationController?.navigationBar.layer.zPosition = -1
The pushing VC's viewDidLoad could be a good place to do that.
Now that the navigationBar is placed behind the VC's view, you'll need to adjust the view's frame to make sure it doesn't overlap with the navigationBar (that'd cause navigationBar to be covered). You can use viewWillLayoutSubviews to change the view's origin.y to start under navigationBar's floor (statusBarHeight + navigationBarHeight).
That'll do the job. You don't need to modify the pushed VC unless you wanna add e.g. a custom back button like in the Twitter's profile screen case. The detail controller's view will be on top of navigation bar while letting you keep the pop gesture transition. Below is your sample code modified with this changes:
class MainViewController: UIViewController {
lazy var button: UIButton = {
let button = UIButton()
button.setTitle("Detail", for: .normal)
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Main"
view.backgroundColor = .blue
// Default value of layer's zPosition is 0 so setting it to -1 will place it behind its siblings.
navigationController?.navigationBar.layer.zPosition = -1
// The `view` will be under navigationBar so lets set a background color to the bar
// as the view's backgroundColor to simulate the default behaviour.
navigationController?.navigationBar.backgroundColor = view.backgroundColor
// Hide the back button transition image.
navigationController?.navigationBar.backIndicatorImage = UIImage()
navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage()
view.addSubview(button)
addConstraints()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// Place `view` under navigationBar.
let statusBarPlusNavigationBarHeight: CGFloat = (navigationController?.navigationBar.bounds.height ?? 0)
+ UIApplication.shared.statusBarFrame.height
let viewHeight = UIScreen.main.bounds.height - statusBarPlusNavigationBarHeight
view.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: viewHeight))
view.frame.origin.y = statusBarPlusNavigationBarHeight
}
#objc func buttonTapped(_ sender: UIButton) {
navigationController?.pushViewController(DetailViewController(), animated: true)
}
private func addConstraints() {
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
button.widthAnchor.constraint(equalToConstant: 150).isActive = true
button.heightAnchor.constraint(equalToConstant: 42).isActive = true
}
}
class DetailViewController: UIViewController {
// Some giant button to replace the navigationBar's back button item :)
lazy var button: UIButton = {
let b: UIButton = UIButton(frame: CGRect(origin: .zero, size: CGSize(width: 80, height: 40)))
b.frame.origin.y = UIApplication.shared.statusBarFrame.height
b.backgroundColor = .darkGray
b.setTitle("back", for: .normal)
b.addTarget(self, action: #selector(DetailViewController.backButtonTapped), for: .touchUpInside)
return b
}()
#objc func backButtonTapped() {
navigationController?.popViewController(animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(button)
}
}
This might be what you're looking for...
Start the NavBar hide / show animations before starting the push / pop:
class MainViewController: UIViewController {
lazy var button: UIButton = {
let button = UIButton()
button.setTitle("Detail", for: .normal)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Main"
view.backgroundColor = .blue
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
button.widthAnchor.constraint(equalToConstant: 150).isActive = true
button.heightAnchor.constraint(equalToConstant: 42).isActive = true
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#objc func buttonTapped(_ sender: UIButton) {
navigationController?.setNavigationBarHidden(true, animated: true)
navigationController?.pushViewController(DetailViewController(), animated: true)
}
}
class DetailViewController: UIViewController {
lazy var button: UIButton = {
let button = UIButton()
button.setTitle("Go Back", for: .normal)
button.backgroundColor = .red
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
button.widthAnchor.constraint(equalToConstant: 150).isActive = true
button.heightAnchor.constraint(equalToConstant: 42).isActive = true
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#objc func buttonTapped(_ sender: UIButton) {
navigationController?.setNavigationBarHidden(false, animated: true)
navigationController?.popViewController(animated: true)
}
}
Use the custom push transition from this post stackoverflow.com/a/5660278/7270113. The in order to eliminate the back gesture (that's what I understand is what you want to do), just kill the navigation stack. You will have to provide an alternative way to exit the DetailViewController, as even if you unhide the navigation controller, the backbitten will be gone since the navigation stack is empty.
#objc func buttonTapped(_ sender: UIButton) {
let transition = CATransition()
transition.duration = 0.5
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.type = kCATransitionFade
navigationController?.view.layer.add(transition, forKey: nil)
let storyboard = UIStoryboard(name: "NameOfYourStoryBoard", bundle: .main)
let viewController = storyboard.instantiateViewController(withIdentifier: "IdentifierOfDetailViewController") as! DetailViewController
navigationController?.setViewControllers([viewController], animated: true) // This method will perform a push
}
Your navigation controller will from now on use this transition animation, if you want to remove it you could use
navigationController?.view.layer.removeAllAnimations()

Button to next view controller

I want to create a Button that presents another view controller when it is tapped.
I am trying to to this via selector but when I click the button I get a Thread1: signal SAGABRT error.
So I commented out the presenting and just put in a print to console-statement. I also get the same error. :(
My code is the following:
override func viewDidLoad() {
var menuView = UIView()
var newPlayButton = UIButton()
var newPlayImage = UIImage(named: "new_game_button_5cs")
var newPlayImageView = UIImageView(image: UIImage(named: "new_game_button_5cs"))
newPlayButton.frame = CGRectMake(0, 0, newPlayImageView.frame.width, newPlayImageView.frame.height)
newPlayButton.setImage(newPlayImage, forState: .Normal)
newPlayButton.addTarget(self, action:"showNewPlay:", forControlEvents: UIControlEvents.TouchUpInside)
menuView.addSubview(newPlayButton)
self.view.addSubview(menuView)
}
func showNewPlay(sender:UIButton!) {
//var vc = self.storyboard?.instantiateViewControllerWithIdentifier("newGameID") as ViewController
//self.presentViewController(vc, animated: false, completion: nil)
println("Button tapped")
}

Resources