How do I navigate to a View Controller while also using SwiftUI? - ios

I am trying to navigate to a View Controller while currently using NavigationLink from SwiftUI. I am assuming I am not able to do this, so I am wondering how I can navigate to my View Controller in another way while still being able to click on a button from my HomeView and navigate to my ViewController.
Below is my HomeView where I want a button with the text 'Time Sheets' to navigate to my ViewController
struct HomeView: View {
var body: some View {
NavigationView {
VStack {
Image("OELogo")
.resizable()
.frame(width: 400, height: 300)
NavigationLink(destination: ViewController()) {
Text("Time Sheets")
.fontWeight(.bold)
.frame(minWidth: 325, minHeight: 50)
.font(.title)
.foregroundColor(.gray)
.padding()
.overlay(
RoundedRectangle(cornerRadius: 50)
.stroke(Color.gray, lineWidth: 2)
)
}
Below is the start to my code of ViewController file that I want to navigate to
import UIKit
import KVKCalendar
import SwiftUI
final class ViewController: UIViewController {
private var events = [Event]()
private var selectDate: Date = {
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
return formatter.date(from: "27.7.2020") ?? Date()
}()```

To achieve this, you would need to create a new struct that conforms to UIViewControllerRepresentable. This acts like a wrapper for UIKits UIViewController.
There is a similar protocol for UIView, UIViewRepresentable.
struct YourViewControllerView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ViewController {
// this will work if you are not using Storyboards at all.
return ViewController()
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
// update code
}
}
Alternatively, this struct will work if you have your ViewController inside a storyboard.
struct YourViewControllerViewWithStoryboard: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ViewController {
guard let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "ViewController") as? ViewController else {
fatalError("ViewController not implemented in storyboard")
}
return viewController
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
// update code
}
}
Remember to set the Restoration ID and Storyboard ID in the Interface builder if you are using a storyboard

Related

How to navigate from swift UI to storyboard on button click in swift UI?

I am working on an app that has both swiftUI and storyboard. I have a button in swift UI. On click of this I need to navigate to a storyboard screen. I tried the below code but it is not getting called. Kindly help....
in my swiftUI, the button code is as below,
Button(action:{ TestController()
}, label:
{
Text("Click me").foregroundColor(.white)
Image(systemName: "chevron.forward.2").imageScale(.large)})
struct TestController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
let storyboard = UIStoryboard(name: "test", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier: "testView")
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
Please help me...
Use NavigationLink with NavigationView
struct MyTestView: View {
var body: some View {
NavigationView {
NavigationLink(
destination: TestController(),
label: {
Text("Click me").foregroundColor(.white)
Image(systemName: "chevron.forward.2").imageScale(.large)}
)
}
}
}
I found a solution over here,
How to show NavigationLink as a button in SwiftUI
This works perfectly,
Button(action: {
print("Floating Button Click")
}, label: {
NavigationLink(destination: AddItemView()) {
Text("Open View")
}
})

Swift UI - HostingController adds unwanted navigation bar

I am attempting to integrate SwiftUI into my project, and I am currently using a storyboard which is launched via my app delegate with the following code:
_rootNavigiationController = [[UINavigationController alloc] init];
_rootNavigiationController.navigationBarHidden = YES;
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:StoryboardLoginRegister bundle:nil];
BasicInformation *basicInfo = (BasicInformation *)[storyboard instantiateViewControllerWithIdentifier:#"basic-info"];
[self.rootNavigiationController setViewControllers:#[basicInfo]];
So essentially my App delegate is in objective-c and the windows root controller is a UINavigation controller.
My BasicInformation class looks like:
class BasicInfo: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.isNavigationBarHidden = true;
// Do any additional setup after loading the view.
}
#IBSegueAction func addSwiftUi(_ coder: NSCoder) -> UIViewController? {
let BasicInfoUI = BasicInfo_UI();
let hostingController = UIHostingController(coder: coder, rootView: BasicInfoUI);
hostingController?.navigationController?.isNavigationBarHidden = true;
return hostingController;
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
And the Swift UI for the basic information is the following:
struct BasicInfo_UI: View {
#State var username: String = ""
#State var isPrivate: Bool = true
#State var notificationsEnabled: Bool = false
#State private var previewIndex = 0
var previewOptions = ["Always", "When Unlocked", "Never"]
var body: some View {
Form {
Section(header: Text("PROFILE")) {
TextField("Username", text: $username)
Toggle(isOn: $isPrivate) {
Text("Private Account")
}
}
Section(header: Text("NOTIFICATIONS")) {
Toggle(isOn: $notificationsEnabled) {
Text("Enabled")
}
Picker(selection: $previewIndex, label: Text("Show Previews")) {
ForEach(0 ..< previewOptions.count) {
Text(self.previewOptions[$0])
}
}
}
Section(header: Text("ABOUT")) {
HStack {
Text("Version")
Spacer()
Text("2.2.1")
}
}
Section {
Button(action: {
print("Perform an action here...")
}) {
Text("Reset All Settings")
}
}
}
}
}
struct BasicInfo_UI_Previews: PreviewProvider {
static var previews: some View {
BasicInfo_UI()
}
}
My only issue is i can't seem to figure out why i have a navigation bar at the top of the UI in my app
Hoping somebody can explain to me why exactly there is a navigation bar at the top of my controller event though i've explicitly set navigationbarhidden to true in multiple places in my app
Try to hide navigation bar by SwiftUI explicitly, like
Form {
// ... other code
}
.navigationBarTitle("")
.navigationBarHidden(true)
After long attempts, I have the same behavior in my UIKit project and SwiftUI Views with UIHostingController as with just UIKit.
In my storyboard, the UIHostingController is embedded in the NavigationController and this in turn is connected to the UITabBarController.
The first thing to do is to uncheck "Shows Navigation Bar" in the Attributes Inspector of the NavigationController.
In the SwiftUI View I have the list in the NavigationView with the modifier .navigationBarTitle("ViewTitle", displayMode: .large) and the next SwiftUI Views without NavigationView and the list with .navigationBarTitle ("SecondViewTitle", displayMode: .inline) modifier.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setNavigationBarHidden(true, animated: false)
}
This works for me.

Center SwiftUI view in top-level view

I am creating a loading indicator in SwiftUI that should always be centered in the top-level view of the view hierarchy (i.e centered in the whole screen in a fullscreen app). This would be easy in UIKit, but SwiftUI centres views relative to their parent view only and I am not able to get the positions of the parent views of the parent view.
Sadly my app is not fully SwiftUI based, so I cannot easily set properties on my root views that I could then access in my loading view - I need this view to be centered regardless of what the view hierarchy looks like (mixed UIKit - SwiftUI parent views). This is why answers like SwiftUI set position to centre of different view don't work for my use case, since in that example, you need to modify the view in which you want to centre your child view.
I have tried playing around with the .offset and .position functions of View, however, I couldn't get the correct inputs to always dynamically centre my loadingView regardless of screen size or regardless of what part of the whole screen rootView takes up.
Please find a minimal reproducible example of the problem below:
/// Loading view that should always be centered in the whole screen on the XY axis and should be the top view in the Z axis
struct CenteredLoadingView<RootView: View>: View {
private let rootView: RootView
init(rootView: RootView) {
self.rootView = rootView
}
var body: some View {
ZStack {
rootView
loadingView
}
// Ensure that `AnimatedLoadingView` is displayed above all other views, whose `zIndex` would be higher than `rootView`'s by default
.zIndex(.infinity)
}
private var loadingView: some View {
VStack {
Color.white
.frame(width: 48, height: 72)
Text("Loading")
.foregroundColor(.white)
}
.frame(width: 142, height: 142)
.background(Color.primary.opacity(0.7))
.cornerRadius(10)
}
}
View above which the loading view should be displayed:
struct CenterView: View {
var body: some View {
return VStack {
Color.gray
HStack {
CenteredLoadingView(rootView: list)
otherList
}
}
}
var list: some View {
List {
ForEach(1..<6) {
Text($0.description)
}
}
}
var otherList: some View {
List {
ForEach(6..<11) {
Text($0.description)
}
}
}
}
This is what the result looks like:
This is how the UI should look like:
I have tried modifying the body of CenteredLoadingView using a GeometryReader and .frame(in: .global) to get the global screen size, but what I've achieved is that now my loadingView is not visible at all.
var body: some View {
GeometryReader<AnyView> { geo in
let screen = geo.frame(in: .global)
let stack = ZStack {
self.rootView
self.loadingView
.position(x: screen.midX, y: screen.midY)
// Offset doesn't work either
//.offset(x: -screen.origin.x, y: -screen.origin.y)
}
// Ensure that `AnimatedLoadingView` is displayed above all other views, whose `zIndex` would be higher than `rootView`'s by default
.zIndex(.infinity)
return AnyView(stack)
}
}
Here is a demo of possible approach. The idea is to use injected UIView to access UIWindow and then show loading view as a top view of window's root viewcontroller view.
Tested with Xcode 12 / iOS 14 (but SwiftUI 1.0 compatible)
Note: animations, effects, etc. are possible but are out scope for simplicity
struct CenteredLoadingView<RootView: View>: View {
private let rootView: RootView
#Binding var isActive: Bool
init(rootView: RootView, isActive: Binding<Bool>) {
self.rootView = rootView
self._isActive = isActive
}
var body: some View {
rootView
.background(Activator(showLoading: $isActive))
}
struct Activator: UIViewRepresentable {
#Binding var showLoading: Bool
#State private var myWindow: UIWindow? = nil
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async {
self.myWindow = view.window
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
guard let holder = myWindow?.rootViewController?.view else { return }
if showLoading && context.coordinator.controller == nil {
context.coordinator.controller = UIHostingController(rootView: loadingView)
let view = context.coordinator.controller!.view
view?.backgroundColor = UIColor.black.withAlphaComponent(0.8)
view?.translatesAutoresizingMaskIntoConstraints = false
holder.addSubview(view!)
holder.isUserInteractionEnabled = false
view?.leadingAnchor.constraint(equalTo: holder.leadingAnchor).isActive = true
view?.trailingAnchor.constraint(equalTo: holder.trailingAnchor).isActive = true
view?.topAnchor.constraint(equalTo: holder.topAnchor).isActive = true
view?.bottomAnchor.constraint(equalTo: holder.bottomAnchor).isActive = true
} else if !showLoading {
context.coordinator.controller?.view.removeFromSuperview()
context.coordinator.controller = nil
holder.isUserInteractionEnabled = true
}
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator {
var controller: UIViewController? = nil
}
private var loadingView: some View {
VStack {
Color.white
.frame(width: 48, height: 72)
Text("Loading")
.foregroundColor(.white)
}
.frame(width: 142, height: 142)
.background(Color.primary.opacity(0.7))
.cornerRadius(10)
}
}
}
struct CenterView: View {
#State private var isLoading = false
var body: some View {
return VStack {
Color.gray
HStack {
CenteredLoadingView(rootView: list, isActive: $isLoading)
otherList
}
Button("Demo", action: load)
}
.onAppear(perform: load)
}
func load() {
self.isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.isLoading = false
}
}
var list: some View {
List {
ForEach(1..<6) {
Text($0.description)
}
}
}
var otherList: some View {
List {
ForEach(6..<11) {
Text($0.description)
}
}
}
}

how to make the modal view with a transparent background in SwiftUI?

Anyone knows how to to make the modal view with a transparent background.
Exactly like the below link in swift
Swift Modal View Controller with transparent background
I am using coordinator pattern, creating view in assembly
let view = UIHostingController(rootView: swiftuiview)
view.view.backgroundColor = .clear
inside router just present this UIHostingController
module.modalPresentationStyle = .overCurrentContext
navigationController.present(module, animated: animated, completion: nil)
If you wanting to blur the background of a SwiftUI from UIKit Project and are possibly using SwiftUI View for a Modal View then I had the same problem recently and created a UIViewController that takes the UIHostController (UIViewController basically), then alters the HostingController View's alpha, puts a blur at the back and presents it to the parent view.
I Have created a gist with the file in it for public use
https://gist.github.com/Ash-Bash/93fd55d89c1e36f592d3868f6b29b259
Heres the working example:
// Initialises BlurredHostingController
var blurredHostingController = BlurredHostingController()
// Sets the Hosting View for the SwiftUI View Logic
blurredHostingController.hostingController = UIHostingController(rootView: ContentView())
// Blur Tweaks for blurredHostingController
blurredHostingController.blurEffect = .systemMaterial
blurredHostingController.translucentEffect = .ultrathin
// Presents View Controller as a Modal View Controller
self.present(blurredHostingController animated: true, completion: nil)
Here's the result from macCatalyst
Present:
let rootView = Text("Hello world")
let controller = UIHostingController(rootView: rootView)
controller.view.backgroundColor = .clear
UIApplication.shared.windows.first?.rootViewController?.present(controller, animated: true)
Dismiss:
UIApplication.shared.windows.first?.rootViewController?.dismiss(animated: true)
I didn't get the ideal way to do so, but I got a workaround for this.
So, In order to present a view modally, you can take a ZStack and group multiple views in it and handle it with a #State variable like this.
Here I have given the background colour to the view for better explanation.
struct ContentView : View {
#State private var showModally = false
var body : some View {
ZStack {
Color.red
VStack {
Button(action: {
withAnimation{
self.showModally = true
}
}) {
Text("Push Modally")
}
}
ModalView(show: $showModally)
.offset(y: self.showModally ? 0 : UIScreen.main.bounds.height)
.animation(.spring())
}
}
}
struct ModalView: View {
#Binding var show : Bool
var body: some View {
VStack {
Spacer()
VStack {
Color.white
}
.frame(height : 400)
.cornerRadius(10)
.padding(.horizontal)
}
.background(Color.clear)
.onTapGesture {
self.show = false
}
}
}
In this, the Modal View will be presented modally over the content view and will be dismissed by a tap.

Send tapAction from SwiftUI button action to UIView function

I'm trying to find a way to trigger an action that will call a function in my UIView when a button gets tapped inside swiftUI.
Here's my setup:
foo()(UIView) needs to run when Button(SwiftUI) gets tapped
My custom UIView class making use of AVFoundation frameworks
class SomeView: UIView {
func foo() {}
}
To use my UIView inside swiftUI I have to wrap it in UIViewRepresentable
struct SomeViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> CaptureView {
SomeView()
}
func updateUIView(_ uiView: CaptureView, context: Context) {
}
}
SwiftUI View that hosts my UIView()
struct ContentView : View {
var body: some View {
VStack(alignment: .center, spacing: 24) {
SomeViewRepresentable()
.background(Color.gray)
HStack {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
}) {
Text("Tap Here")
}
}
}
}
}
You can store an instance of your custom UIView in your representable struct (SomeViewRepresentable here) and call its methods on tap actions:
struct SomeViewRepresentable: UIViewRepresentable {
let someView = SomeView() // add this instance
func makeUIView(context: Context) -> SomeView { // changed your CaptureView to SomeView to make it compile
someView
}
func updateUIView(_ uiView: SomeView, context: Context) {
}
func callFoo() {
someView.foo()
}
}
And your view body will look like this:
let someView = SomeViewRepresentable()
var body: some View {
VStack(alignment: .center, spacing: 24) {
someView
.background(Color.gray)
HStack {
Button(action: {
print("SwiftUI: Button tapped")
// Call func in SomeView()
self.someView.callFoo()
}) {
Text("Tap Here")
}
}
}
}
To test it I added a print to the foo() method:
class SomeView: UIView {
func foo() {
print("foo called!")
}
}
Now tapping on your button will trigger foo() and the print statement will be shown.
M Reza's solution works for simple situations, however if your parent SwiftUI view has state changes, every time when it refreshes, it will cause your UIViewRepresentable to create new instance of UIView because of this: let someView = SomeView() // add this instance. Therefore someView.foo() is calling the action on the previous instance of SomeView you created, which is already outdated upon refreshing, so you might not see any updates of your UIViewRepresentable appear on your parent view.
See: https://medium.com/zendesk-engineering/swiftui-uiview-a-simple-mistake-b794bd8c5678
A better practice would be to avoid creating and referencing that instance of UIView when calling its function.
My adaption to M Reza's solution would be calling the function indirectly through parent view's state change, which triggers updateUIView :
var body: some View {
#State var buttonPressed: Bool = false
VStack(alignment: .center, spacing: 24) {
//pass in the #State variable which triggers actions in updateUIVIew
SomeViewRepresentable(buttonPressed: $buttonPressed)
.background(Color.gray)
HStack {
Button(action: {
buttonPressed = true
}) {
Text("Tap Here")
}
}
}
}
struct SomeViewRepresentable: UIViewRepresentable {
#Binding var buttonPressed: Bool
func makeUIView(context: Context) -> SomeView {
return SomeView()
}
//called every time buttonPressed is updated
func updateUIView(_ uiView: SomeView, context: Context) {
if buttonPressed {
//called on that instance of SomeView that you see in the parent view
uiView.foo()
buttonPressed = false
}
}
}
Here's another way to do it using a bridging class.
//SwiftUI
struct SomeView: View{
var bridge: BridgeStuff?
var body: some View{
Button("Click Me"){
bridge?.yo()
}
}
}
//UIKit or AppKit (use NS instead of UI)
class BridgeStuff{
var yo:() -> Void = {}
}
class YourViewController: UIViewController{
override func viewDidLoad(){
let bridge = BridgeStuff()
let view = UIHostingController(rootView: SomeView(bridge: bridge))
bridge.yo = { [weak self] in
print("Yo")
self?.howdy()
}
}
func howdy(){
print("Howdy")
}
}
Here is yet another solution! Communicate between the superview and the UIViewRepresentable using a closure:
struct ContentView: View {
/// This closure will be initialized in our subview
#State var closure: (() -> Void)?
var body: some View {
SomeViewRepresentable(closure: $closure)
Button("Tap here!") {
closure?()
}
}
}
Then initialize the closure in the UIViewRepresentable:
struct SomeViewRepresentable: UIViewRepresentable {
// This is the same closure that our superview will call
#Binding var closure: (() -> Void)?
func makeUIView(context: Context) -> UIView {
let uiView = UIView()
// Since `closure` is part of our state, we can only set it on the main thread
DispatchQueue.main.async {
closure = {
// Perform some action on our UIView
}
}
return uiView
}
}
#ada10086 has a great answer. Just thought I'd provide an alternative solution that would be more convenient if you want to send many different actions to your UIView.
The key is to use PassthroughSubject from Combine to send messages from the superview to the UIViewRepresentable.
struct ContentView: View {
/// This will act as a messenger to our subview
private var messenger = PassthroughSubject<String, Never>()
var body: some View {
SomeViewRepresentable(messenger: messenger) // Pass the messenger to our subview
Button("Tap here!") {
// Send a message
messenger.send("button-tapped")
}
}
}
Then we monitor the PassthroughSubject in our subview:
struct SomeViewRepresentable: UIViewRepresentable {
let messenger = PassthroughSubject<String, Never>()
#State private var subscriptions: Set<AnyCancellable> = []
func makeUIView(context: Context) -> UIView {
let uiView = UIView()
// This must be run on the main thread
DispatchQueue.main.async {
// Subscribe to messages
messenger.sink { message in
switch message {
// Call funcs in `uiView` depending on which message we received
}
}
.store(in: &subscriptions)
}
return uiView
}
}
This approach is nice because you can send any string to the subview, so you can design a whole messaging scheme.
My solution is to create an intermediary SomeViewModel object. The object stores an optional closure, which is assigned an action when SomeView is created.
struct ContentView: View {
// parent view holds the state object
#StateObject var someViewModel = SomeViewModel()
var body: some View {
VStack(alignment: .center, spacing: 24) {
SomeViewRepresentable(model: someViewModel)
.background(Color.gray)
HStack {
Button {
someViewModel.foo?()
} label: {
Text("Tap Here")
}
}
}
}
}
struct SomeViewRepresentable: UIViewRepresentable {
#ObservedObject var model: SomeViewModel
func makeUIView(context: Context) -> SomeView {
let someView = SomeView()
// we don't want the model to hold on to a reference to 'someView', so we capture it with the 'weak' keyword
model.foo = { [weak someView] in
someView?.foo()
}
return someView
}
func updateUIView(_ uiView: SomeView, context: Context) {
}
}
class SomeViewModel: ObservableObject {
var foo: (() -> Void)? = nil
}
Three benefits doing it this way:
We avoid the original problem that #ada10086 identified with #m-reza's solution; creating the view only within the makeUIView function, as per the guidance from Apple Docs, which state that we "must implement this method and use it to create your view object."
We avoid the problem that #orschaef identified with #ada10086's alternative solution; we're not modifying state during a view update.
By using ObservableObject for the model, we can add #Published properties to the model and communicate state changes from the UIView object. For instance, if SomeView uses KVO for some of its properties, we can create an observer that will update some #Published properties, which will be propagated to any interested SwiftUI views.

Resources