Take a look at the screenshot:
I marked the "Top Navigation Bar" red, which I want to remove, as there is an unused top bar...
You have to know that I code using Storyboards, but this specific page is holding a subview of SwiftUI View!
This is the SwiftUI ContentView:
import SwiftUI
import UIKit
struct ContentView: View {
var body: some View {
NavigationView{
MasterView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
struct MasterView: View {
var body: some View {
Form {
Section(header: Text("Geplant")) {
Section {
NavigationLink(destination: UIKitView()) { Text("Berlin") }
}
}
}
.navigationBarTitle("Wohin gehts?")
}
}
struct UIKitView: UIViewControllerRepresentable {
typealias UIViewControllerType = SwipeViewController
func makeUIViewController(context: Context) -> SwipeViewController {
let sb = UIStoryboard(name: "Storyboard", bundle: nil)
let viewController = sb.instantiateViewController(identifier: "swipe") as! SwipeViewController
return viewController
}
func updateUIViewController(_ uiViewController: SwipeViewController, context: Context) {
}
}
And this is the UIViewController, which is holding the SwiftUI Subview:
import UIKit
import SwiftUI
class StartViewController: UIViewController {
#IBOutlet weak var btn: UIButton!
#IBOutlet weak var container: UIView!
let contentView = UIHostingController(rootView: ContentView())
override func viewDidLoad() {
super.viewDidLoad()
configureBackgroundGradient()
addChild(contentView)
view.addSubview(contentView.view)
setupContraints()
}
fileprivate func setupContraints(){
contentView.view.translatesAutoresizingMaskIntoConstraints = false
contentView.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
private func configureBackgroundGradient() {
let backgroundGray = UIColor(red: 244 / 255, green: 247 / 255, blue: 250 / 255, alpha: 1)
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.white.cgColor, backgroundGray.cgColor]
gradientLayer.frame = view.bounds
view.layer.insertSublayer(gradientLayer, at: 0) //Background Color
}
}
Can anyone can help? :))
Thank you! Feel free to ask me for more screenshots or code!
You can show a view in full screen with the SwiftUI view modifiere fullScreenCover. https://www.hackingwithswift.com/quick-start/swiftui/how-to-present-a-full-screen-modal-view-using-fullscreencover
Let us take simple FullScreenModalView struct that can dismiss itself, then presents it from ContentView when another button is pressed:
struct FullScreenModalView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss Modal") {
presentationMode.wrappedValue.dismiss()
}
}
}
And here is the code for ContentView -
struct ContentView: View {
#State private var isPresented = false
var body: some View {
Button("Present!") {
isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}
}
Happy to help.
Thanks.
Related
It seems like there's a bug in iOS 16 where a UISearchBar in a UISplitViewController's primary position displays with a square background. This doesn't happen if the search bar is in other positions, or if the split view is collapsed (eg. on iPhone).
I've reported it (FB10847490) but any ideas how I could work around this in the meantime? It seems like .background/.backgroundColor and searchTextField.background/backgroundColor both affect other subviews and not the view that is causing the square appearance.
Sample app:
struct ContentView: View {
var body: some View {
HostingController()
}
}
struct HostingController: UIViewControllerRepresentable {
#State private var text = ""
func makeUIViewController(context: Context) -> some UIViewController {
let controller = UISplitViewController(style: .doubleColumn)
let contentView = UIHostingController(rootView: VStack {
SearchBar(text: $text)
})
let primaryController = UINavigationController(rootViewController: contentView)
controller.setViewController(primaryController, for: .primary)
controller.setViewController(UIHostingController(rootView: SearchBar(text: $text)), for: .secondary)
controller.preferredSplitBehavior = .overlay
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
struct SearchBar: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UISearchBar {
let searchBar = UISearchBar()
searchBar.placeholder = "Search..."
searchBar.returnKeyType = .done
searchBar.enablesReturnKeyAutomatically = false
searchBar.searchBarStyle = .minimal
searchBar.text = text
searchBar.searchBarStyle = .minimal
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: Context) {
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPad")
}
}
I am a SwiftUI newbie struggling to add SwiftUI functionality to my existing UIKit/Storyboard code. I would like to call a UIKit function from SwiftUI button. Greatly appreciate your help. Here is the relevant code simplified for this discussion.
From the code below, I'd like to call the functions startAction() and stopAction() from the If statement in SwiftUI...
if (startStop_flag) {
//******** call StartAction()
} else {
//******* call StopAction()
}
The entire code below.
Some context: when the app is run, the bottom half of the screen will show "UIkit Storyboard View" and show the button "Open Swift Container View". When the user clicks this button, the SwiftUI container view will open up. This view will display "This is a swiftUI view" and display a button "Start/Stop". When the user clicks this button, StartAction() or StopAction() needs to be called. These two functions reside in the UIViewController. I hope I am clear with the problem and the request.
ViewController.swift
class ViewController: UIViewController {
#IBOutlet weak var nativeView: UIView!
#IBOutlet weak var nativeView_Label: UILabel!
#IBOutlet weak var nativeView_openSwiftViewBtn: UIButton!
#IBOutlet weak var containerView_forSwiftUI: UIView!
#IBSegueAction func embedSwiftUIView(_ coder: NSCoder) -> UIViewController? {
return UIHostingController(coder: coder, rootView: SwiftUIView2(text: "Container View"))
}
var toggleOpenCloseContainerView : Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
containerView_forSwiftUI.isHidden = true
}
#IBAction func openContainerView_touchInside(_ sender: Any) {
if (toggleOpenCloseContainerView) {
containerView_forSwiftUI.isHidden = false
toggleOpenCloseContainerView = false
nativeView_openSwiftViewBtn.setTitle("Close Swift Container View", for: .normal)
} else {
containerView_forSwiftUI.isHidden = true
toggleOpenCloseContainerView = true
nativeView_openSwiftViewBtn.setTitle("Open Swift Container View", for: .normal)
}
}
// These two functions need to be called from the SwiftUI's button.
func startAction() {
print("Start Action called from SwiftUI's button")
}
func stopAction() {
print("Stop Action called from SwiftUI's button")
}
}
The swiftUI functions are in this file
struct SwiftUIView2: View {
var text: String
#State var startStop_flag: Bool = true
var body: some View {
VStack {
Text(text)
HStack {
Image(systemName: "smiley")
Text("This is a SwiftUI View")
Spacer()
Button("\(startStop_flag ? "Start": "Stop")") {
startStop_flag = !startStop_flag
if (startStop_flag) {
//******** call StartAction()
} else {
//******* call StopAction()
}
} .padding()
.background(Color.red)
.cornerRadius(40)
.foregroundColor(.white)
.padding(5)
.overlay(
RoundedRectangle(cornerRadius: 40)
.stroke(Color.red, lineWidth: 1)
)
}
}
.font(.largeTitle)
.background(Color.blue)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView2(text: "Sample Text")
}
}
You can use closures for this. First, define and call them inside your SwiftUI view.
struct SwiftUIView2: View {
var text: String
var startAction: (() -> Void) /// define closure 1
var stopAction: (() -> Void) /// define closure 2
...
...
Button("\(startStop_flag ? "Start": "Stop")") {
startStop_flag = !startStop_flag
if (startStop_flag) {
//******** call StartAction()
startAction()
} else {
//******* call StopAction()
stopAction()
}
}
}
Then, just assign the closure's contents inside ViewController.swift.
#IBSegueAction func embedSwiftUIView(_ coder: NSCoder) -> UIViewController? {
return UIHostingController(
coder: coder,
rootView: SwiftUIView2(
text: "Container View",
startAction: { [weak self] in
self?.startAction()
},
stopAction: { [weak self] in
self?.stopAction()
}
)
)
}
Here is the easiest way to open UIkit ViewController from SwiftUI Button Press.
Button("Next Page") {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootViewController = storyboard.instantiateViewController(withIdentifier: "ProfileVC")
if let window = UIApplication.shared.windows.first {
window.rootViewController!.present(rootViewController, animated: true)
}
}
}
Would like to return to my presenting UIKit View Controller after pressing the save button on my SwiftUI View (presented via a HostingViewController).
This is how a navigate to my SwiftUI view from my UIKit VC.
let profileView = suiProfileView().environmentObject(suiProfileViewModel())
let profileVC = UIHostingController(rootView: profileView)
let navVC = UINavigationController(rootViewController: profileVC)
navVC.modalPresentationStyle = .fullScreen
SideMenuManager.default.leftMenuNavigationController?.present(navVC, animated: true , completion: nil)
This is my SwiftUI View that i'd like to dismiss after pressing the Save button
import SwiftUI
import Combine
struct suiProfileView: View {
#SwiftUI.Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#EnvironmentObject var profileViewModel: suiProfileViewModel
#State var suiTitleText = t("general.save")
var body: some View {
GeometryReader { geo in
VStack {
personalInfoSection
Spacer()
Button("Save", action: profileViewModel.saveProfile)
.frame(width: geo.size.width * 0.8, height: geo.size.height * 0.08, alignment: .center)
}
.onReceive(profileViewModel.viewDismissalModePublisher) { shouldDismiss in
if shouldDismiss {
self.presentationMode.wrappedValue.dismiss()
}
}
.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
.padding()
}
}
}
This is my ProfileViewModel class which publishes the shouldDismissView variable using Combine after running some business logic.
class suiProfileViewModel: suiProfileViewModelProtocol {
//private var model: suiProfileFormProtocol
#Published var profile = suiProfileForm()
var viewDismissalModePublisher = PassthroughSubject<Bool, Never>()
private var shouldDismissView = false {
didSet {
viewDismissalModePublisher.send(shouldDismissView)
}
}
func businessLogicThatDeterminesIfShouldDismissView() {
//....
}
}
For some reason self.presentationMode.wrappedValue.dismiss() which was called in my SwiftUI view is not dismissing my SwiftUI view and not allowing me to go back to my initial UIKit View Controller who presented it in the first place. Any help would greatly appreciated. Thanks in advance
A couple problems that I see:
You're trying to dismiss your suiProfileView , but really, it's wrapped in a UINavigationController before you present it.
According to a basic test that I just did, even if your suiProfileView were presented without the navigation controller, presentationMode still doesn't work on it -- my suspicion is that it's only passed accurately when going SwiftUI->SwiftUI and can't be trusted to be communicated through the UIHostingController
I would suggest passing a closure to suiProfileView that can run the dismiss code from your original presenting view controller. Here's a simplified example:
import UIKit
import SwiftUI
class ViewController: UIViewController {
var presentedController : UINavigationController?
func dismissVC() {
presentedController?.dismiss(animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let swiftUIView = ContentView(dismiss: self.dismissVC)
let hostVC = UIHostingController(rootView: swiftUIView)
let navVC = UINavigationController(rootViewController: hostVC)
navVC.modalPresentationStyle = .fullScreen
self.present(navVC, animated: true , completion: nil)
self.presentedController = navVC
}
}
}
struct ContentView : View {
var dismiss : () -> Void
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var body: some View {
Button(action: {
dismiss()
//presentationMode.wrappedValue.dismiss() <-- doesn't function
}) {
Text("Close")
}
}
}
I have a SwiftUI view, which consists of a TextField. I want that whenever I type in the TextField it should send the value to the control on the UIKit UIViewController.
// Here is the ContentView
class ContentViewDelegate: ObservableObject {
var didChange = PassthroughSubject<ContentViewDelegate, Never>()
var name: String = "" {
didSet {
self.didChange.send(self)
}
}
}
struct ContentView: View {
#ObservedObject var delegate: ContentViewDelegate
init(delegate: ContentViewDelegate) {
self.delegate = delegate
}
var body: some View {
VStack {
TextField("Enter name", text: self.$delegate.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
.background(Color.green)
}
}
I checked didChange does get fired in the above code. But in the code below, the sink is never fired.
class ViewController: UIViewController {
private var delegate = ContentViewDelegate()
private var contentView: ContentView!
override func viewDidLoad() {
super.viewDidLoad()
self.contentView = ContentView(delegate: self.delegate)
let controller = UIHostingController(rootView: self.contentView)
controller.view.translatesAutoresizingMaskIntoConstraints = false
self.addChild(controller)
self.view.addSubview(controller.view)
controller.didMove(toParent: self)
NSLayoutConstraint.activate([
controller.view.widthAnchor.constraint(equalToConstant: 200),
controller.view.heightAnchor.constraint(equalToConstant: 44),
controller.view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
controller.view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
])
_ = self.delegate.didChange.sink { delegate in
print(delegate.name)
}
}
Any ideas why didChange.sink is not getting fired?
If you assign the publisher to _ then it is deallocated when viewDidLoad returns. Early examples from Apple show the assignment to _ and it used to work.
You need to ensure you keep a strong reference to your publisher using a property:
class ViewController: UIViewController {
private var delegate = ContentViewDelegate()
private var contentView: ContentView!
private var textChangePublisher: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
self.contentView = ContentView(delegate: self.delegate)
let controller = UIHostingController(rootView: self.contentView)
controller.view.translatesAutoresizingMaskIntoConstraints = false
self.addChild(controller)
self.view.addSubview(controller.view)
controller.didMove(toParent: self)
NSLayoutConstraint.activate([
controller.view.widthAnchor.constraint(equalToConstant: 200),
controller.view.heightAnchor.constraint(equalToConstant: 44),
controller.view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
controller.view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
])
self.textChangePublisher = self.delegate.didChange.sink { delegate in
print(delegate.name)
}
}
The interactive pop gesture recognizer should allow the user to go back the the previous view in navigation stack when they swipe further than half the screen (or something around those lines). In SwiftUI the gesture doesn't get canceled when the swipe wasn't far enough.
SwiftUI: https://imgur.com/xxVnhY7
UIKit: https://imgur.com/f6WBUne
Question:
Is it possible to get the UIKit behaviour while using SwiftUI views?
Attempts
I tried to embed a UIHostingController inside a UINavigationController but that gives the exact same behaviour as NavigationView.
struct ContentView: View {
var body: some View {
UIKitNavigationView {
VStack {
NavigationLink(destination: Text("Detail")) {
Text("SwiftUI")
}
}.navigationBarTitle("SwiftUI", displayMode: .inline)
}.edgesIgnoringSafeArea(.top)
}
}
struct UIKitNavigationView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UINavigationController {
let host = UIHostingController(rootView: content())
let nvc = UINavigationController(rootViewController: host)
return nvc
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
}
I ended up overriding the default NavigationView and NavigationLink to get the desired behaviour. This seems so simple that I must be overlooking something that the default SwiftUI views do?
NavigationView
I wrap a UINavigationController in a super simple UIViewControllerRepresentable that gives the UINavigationController to the SwiftUI content view as an environmentObject. This means the NavigationLink can later grab that as long as it's in the same navigation controller (presented view controllers don't receive the environmentObjects) which is exactly what we want.
Note: The NavigationView needs .edgesIgnoringSafeArea(.top) and I don't know how to set that in the struct itself yet. See example if your nvc cuts off at the top.
struct NavigationView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UINavigationController {
let nvc = UINavigationController()
let host = UIHostingController(rootView: content().environmentObject(nvc))
nvc.viewControllers = [host]
return nvc
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
}
extension UINavigationController: ObservableObject {}
NavigationLink
I create a custom NavigationLink that accesses the environments UINavigationController to push a UIHostingController hosting the next view.
Note: I didn't implement the selection and isActive that the SwiftUI.NavigationLink has because I don't fully understand what they do yet. If you want to help with that please comment/edit.
struct NavigationLink<Destination: View, Label:View>: View {
var destination: Destination
var label: () -> Label
public init(destination: Destination, #ViewBuilder label: #escaping () -> Label) {
self.destination = destination
self.label = label
}
/// If this crashes, make sure you wrapped the NavigationLink in a NavigationView
#EnvironmentObject var nvc: UINavigationController
var body: some View {
Button(action: {
let rootView = self.destination.environmentObject(self.nvc)
let hosted = UIHostingController(rootView: rootView)
self.nvc.pushViewController(hosted, animated: true)
}, label: label)
}
}
This solves the back swipe not working correctly on SwiftUI and because I use the names NavigationView and NavigationLink my entire project switched to these immediately.
Example
In the example I show modal presentation too.
struct ContentView: View {
#State var isPresented = false
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 30) {
NavigationLink(destination: Text("Detail"), label: {
Text("Show detail")
})
Button(action: {
self.isPresented.toggle()
}, label: {
Text("Show modal")
})
}
.navigationBarTitle("SwiftUI")
}
.edgesIgnoringSafeArea(.top)
.sheet(isPresented: $isPresented) {
Modal()
}
}
}
struct Modal: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 30) {
NavigationLink(destination: Text("Detail"), label: {
Text("Show detail")
})
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Dismiss modal")
})
}
.navigationBarTitle("Modal")
}
}
}
Edit: I started off with "This seems so simple that I must be overlooking something" and I think I found it. This doesn't seem to transfer EnvironmentObjects to the next view. I don't know how the default NavigationLink does that so for now I manually send objects on to the next view where I need them.
NavigationLink(destination: Text("Detail").environmentObject(objectToSendOnToTheNextView)) {
Text("Show detail")
}
Edit 2:
This exposes the navigation controller to all views inside NavigationView by doing #EnvironmentObject var nvc: UINavigationController. The way to fix this is making the environmentObject we use to manage navigation a fileprivate class. I fixed this in the gist: https://gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb
You can do this by descending into UIKit and using your own UINavigationController.
First create a SwipeNavigationController file:
import UIKit
import SwiftUI
final class SwipeNavigationController: UINavigationController {
// MARK: - Lifecycle
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
// This needs to be in here, not in init
interactivePopGestureRecognizer?.delegate = self
}
deinit {
delegate = nil
interactivePopGestureRecognizer?.delegate = nil
}
// MARK: - Overrides
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
duringPushAnimation = true
super.pushViewController(viewController, animated: animated)
}
var duringPushAnimation = false
// MARK: - Custom Functions
func pushSwipeBackView<Content>(_ content: Content) where Content: View {
let hostingController = SwipeBackHostingController(rootView: content)
self.delegate = hostingController
self.pushViewController(hostingController, animated: true)
}
}
// MARK: - UINavigationControllerDelegate
extension SwipeNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
}
// MARK: - UIGestureRecognizerDelegate
extension SwipeNavigationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == interactivePopGestureRecognizer else {
return true // default value
}
// Disable pop gesture in two situations:
// 1) when the pop animation is in progress
// 2) when user swipes quickly a couple of times and animations don't have time to be performed
let result = viewControllers.count > 1 && duringPushAnimation == false
return result
}
}
This is the same SwipeNavigationController provided here, with the addition of the pushSwipeBackView() function.
This function requires a SwipeBackHostingController which we define as
import SwiftUI
class SwipeBackHostingController<Content: View>: UIHostingController<Content>, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.delegate = nil
}
}
We then set up the app's SceneDelegate to use the SwipeNavigationController:
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let hostingController = UIHostingController(rootView: ContentView())
window.rootViewController = SwipeNavigationController(rootViewController: hostingController)
self.window = window
window.makeKeyAndVisible()
}
Finally use it in your ContentView:
struct ContentView: View {
func navController() -> SwipeNavigationController {
return UIApplication.shared.windows[0].rootViewController! as! SwipeNavigationController
}
var body: some View {
VStack {
Text("SwiftUI")
.onTapGesture {
self.navController().pushSwipeBackView(Text("Detail"))
}
}.onAppear {
self.navController().navigationBar.topItem?.title = "Swift UI"
}.edgesIgnoringSafeArea(.top)
}
}