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:
Related
I read this question online and found it to be very interesting. If you have a SwiftUI view as shown below. How can you access the selected rating in a UIKit view without changing a single line in RatingView.
struct RatingView: View {
#Binding var rating: Int?
private func starType(index: Int) -> String {
if let rating = rating {
return index <= rating ? "star.fill" : "star"
} else {
return "star"
}
}
var body: some View {
HStack {
ForEach(1...5, id: \.self) { index in
Image(systemName: self.starType(index: index))
.foregroundColor(Color.orange)
.onTapGesture {
rating = index
}
}
}
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
// .constant(3) will be replaced by something in the ViewController so it can handle the changes for the rating
let hostingController = UIHostingController(rootView: RatingView(rating: .constant(3)))
guard let ratingsView = hostingController.view else { return }
self.addChild(hostingController)
self.view.addSubview(ratingsView)
}
UPDATE:
Original source: https://twitter.com/azamsharp/status/1540838477599752192?s=20&t=bbDp3VT9m0Ce4W3Sgr7Iyg
I typed most of the code from the original source. I did not change much.
We can decorate the RatingView and use an ObservableObject to hold on to the source of truth.
class RatingObserver: ObservableObject {
#Published var rating: Int?
}
struct WrappedRatingView: View {
#ObservedObject var ratingObserver: RatingObserver
var body: some View {
RatingView(rating: $ratingObserver.rating)
}
}
Then we can use it in the following way.
class ViewController: UIViewController {
let ratingObserver = RatingObserver()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
let hostingController = UIHostingController(
rootView: WrappedRatingView(ratingObserver: ratingObserver)
)
self.addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Reset Rating", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(resetRating), for: .touchUpInside)
view.addSubview(button)
NSLayoutConstraint.activate([
hostingController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
hostingController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 100)
])
}
#objc func resetRating() {
ratingObserver.rating = nil
}
}
This allows for updating both the ViewController and the SwiftUI view.
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.
I have MainViewController and DetailViewController that are stacked together by a navigation controller. I want to pass a value from DetailViewController back to the previous controller, which is MainViewController.
First, I tried it with UINavigationControllerDelegate:
class DetailViewController: UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
(viewController as? MainViewController)?.myClass = myClass
}
}
which was to be called as DetailViewController is popped:
_ = navigationController?.popViewController(animated: true)
But, the new value doesn't get reflected on MainViewController:
class MainViewController: UIViewController {
var myClass: MyClass
private lazy var commentLabel: UILabel = {
let comment = UILabel()
comment.text = myClass.comment
comment.numberOfLines = 0
return comment
}()
}
even though when I log myClass in MainViewController, I can see that it's being passed properly.
I also tried it with a property observer so that DetailViewController can pass it to a temporary property observer instead:
var temp: MyClass? {
willSet(newValue) {
myClass = newValue
}
}
but, the view controller's interface still doesn't change.
Finally, I tried creating a delegate in MainViewController:
protocol CallBackDelegate {
func callBack(value: MyClass)
}
where the function simply passes the argument:
func callBack(value: MyClass) {
myClass = value
}
I set the delegate to self:
if let vc = self.storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController {
vc.delegate = self
self.navigationController?.pushViewController(vc, animated: true)
}
and invoking the function in DetailViewController:
delegate?.callBack(value: MyClass)
but, still doesn't update the interface. It seems as though passing the value isn't the issue, but having it be reflected is.
This is an example of using the protocol / delegate pattern. It's about as basic as it gets...
Start a new single-view project
add the code below
Set the class of the default view controller to MainViewController
embed it in a Navigation Controller
run the app
Then:
Tap the button labeled "Push to next VC"
Enter some text in the "Edit Me" field
Tap the "Pop back to previous VC"
See that the label has been updated with your entered text.
protocol CallBackDelegate: class {
func callback(_ val: String)
}
class MainViewController: UIViewController, CallBackDelegate {
let btn = UIButton()
let theLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
btn.translatesAutoresizingMaskIntoConstraints = false
theLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn)
view.addSubview(theLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
btn.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
theLabel.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0),
theLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
theLabel.backgroundColor = .yellow
btn.backgroundColor = .red
theLabel.text = "Default text"
btn.setTitle("Push to next VC", for: [])
btn.addTarget(self, action: #selector(self.pushButtonTapped(_:)), for: .touchUpInside)
}
#objc func pushButtonTapped(_ sender: Any?) -> Void {
let vc = DetailViewController()
vc.delegate = self
self.navigationController?.pushViewController(vc, animated: true)
}
func callback(_ val: String) {
theLabel.text = val
}
}
class DetailViewController: UIViewController {
weak var delegate: CallBackDelegate?
let textField = UITextField()
let btn = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
btn.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn)
view.addSubview(textField)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
textField.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
textField.centerXAnchor.constraint(equalTo: g.centerXAnchor),
textField.widthAnchor.constraint(equalToConstant: 200.0),
btn.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 20.0),
btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
textField.backgroundColor = .yellow
textField.borderStyle = .roundedRect
btn.backgroundColor = .blue
textField.placeholder = "Edit me"
btn.setTitle("Pop back to previous VC", for: [])
btn.addTarget(self, action: #selector(self.popButtonTapped(_:)), for: .touchUpInside)
}
#objc func popButtonTapped(_ sender: Any?) -> Void {
if let s = textField.text {
delegate?.callback(s)
}
self.navigationController?.popViewController(animated: true)
}
}
Doesn't seem that you are updating the UILabel value anyhow
var myClass: MyClass? {
didSet {
self.commentLabel.text = myClass?.comment
}
}
You have to update the label text itself, right now it's constant with the first load data
I have got a SwiftUI modal view which I am calling from main UIKit view. I want to add a dismiss button to my modal view. As I can tell, there is no #State variables in UIKit, so I am creating a separate SwiftUI view to store my #State variable but for some reason it is not working. How should I fix this?
My code inside main ViewController:
var hack = StateInUIKitHack()
hack.modalIsPresented = true
let vc = UIHostingController(rootView: MoodCardView(isPresented: hack.$modalIsPresented, entryIndex: entryIndex, note: moodEntries[entryIndex].note ?? ""))
self.present(vc, animated: true, completion: nil)
StateInUIKitHack struct:
struct stateInUIKitHack: View {
#State var modalIsPresented = false
var body: some View {
Text("Hello, World!")
}
}
Inside MoodCardView.swift I have:
#Binding var isPresented: Bool
And if I create my modal sheet from another SwiftUI View the classical way it dismisses OK, but I need to create it from the UIKit view.
Here is a demo of possible approach. Tested with Xcode 11.4 / Playground
Complete playground code:
import UIKit
import SwiftUI
import PlaygroundSupport
class ViewModel {
var closeAction: () -> Void = {}
}
struct ModalView: View {
var vm: ViewModel
var body: some View {
VStack {
Text("I'm SwfitUI")
Button("CloseMe") {
self.vm.closeAction()
}
}
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let button = UIButton(type: .roundedRect)
button.setTitle("ShowIt", for: .normal)
button.addTarget(self, action: #selector(MyViewController.showModal(_:)), for: .touchDown)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
self.view = view
}
#objc func showModal(_ : Any?) {
let bridge = ViewModel()
let vc = UIHostingController(rootView: ModalView(vm: bridge))
bridge.closeAction = { [weak vc] in
vc?.dismiss(animated: true)
}
self.present(vc, animated: true, completion: nil)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
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()