Drawing issues using SwiftUI view as a PrimaryView in uisplitviewcontroller - swift

I am trying to use SwiftUI View as a primary view in a UISplitViewController.
I have setup and added my PrimaryViewController to the NavigationController in my SplitviewController, in Storyboard.
mySwiftUI view code is as follows:
struct Tile_SwiftUI: View {
var body: some View {
RoundedRectangle(cornerRadius: 15)
.fill( Color.red )
.frame(height: 100)
.padding()
}
}
And my PrimaryViewController setup is as follows:
import UIKit
import SwiftUI
class PrimaryViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let testVC = UIHostingController(rootView: Tile_SwiftUI() )
self.view.backgroundColor = UIColor.white
testVC.view.backgroundColor = UIColor.blue
addChild(testVC)
view.addSubview(testVC.view)
testVC.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
testVC.view.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor ),
testVC.view.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor ),
testVC.view.topAnchor.constraint(equalTo: view.topAnchor ),
testVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor ),
])
testVC.didMove(toParent: self)
}
}
It all works as expected on the iPhone and iPad landscape orientation, but on portrait orientation the primaryViewController gets an odd overlay. Please see attached images.
Here is also a link to the full project:
https://www.sticklets.co/code/SplitViewController_SwiftUI_bug.zip
Any ideas what is causing this?

Related

Adding SwiftUI to UIKit, and navigation back to UIKit

I have an existing application that is entirely UIKit, but it has been decided that new views should be added in SwiftUI. I am trying to add a new onboarding tutorial which is entirely images and was easy to write in SwiftUI.
import SwiftUI
struct OnboardingView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Image("Onboard 1")
.resizable()
.scaledToFit()
.ignoresSafeArea()
.cornerRadius(25)
.shadow(color: .gray, radius: 10)
ZStack {
Rectangle()
.foregroundColor(.black)
Button(action: {
//Navigate back to UIKit here
}, label: {
Text("Exit Tutorial")
.foregroundColor(.white)
.font(.system(size: 14, weight: .bold))
})
}
.frame(width: 120, height: 40)
.cornerRadius(10)
.shadow(color: .gray, radius: 10)
.offset(x: 70, y: 135)
}
Spacer()
Rectangle()
.foregroundColor(.black)
.frame(height: 40)
}.navigationBarHidden(true)
}
}
I've added a new ViewController using storyboard and used a UIHostingController to successfully show the SwiftUI onboarding screens:
import UIKit
import SwiftUI
class OnboardingII: UIViewController, UIPopoverPresentationControllerDelegate {
let hostingController = UIHostingController(rootView: OnboardingView())
override func viewDidLoad() {
openSwiftUIOnboardingScreen()
}
func openSwiftUIOnboardingScreen() {
addChildViewController(hostingController)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
hostingController.didMove(toParent: self)
}
func endOnboarding() {
//Add navigation back to the main app here
print("End Onboarding called")
}
}
I've been looking for 3 days now and tried about a dozen different ways of navigating away from the SwiftUI onboarding process and back to the UIKit portion of the app (which is 99% of the app) but I've been unsuccessful in every attempt so far. Below are a few of the things that I've tried:
Adding a segue to move back to the beginning of the app
performSegue(withIdentifier: "backToBanner", sender: nil)
The rest of the app is managed by a navigation controller, and I've tried to get back in at several different points including the main navigation controller and two sub controller using segues identified on the storyboard, but in each case the crash error states that those views are not in the current window hierarchy.
I have also tried the below code (one at a time):
let viewController: UIViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "TestVC") as UIViewController
self.present(viewController, animated: false, completion: nil)
dismiss(animated: true, completion: nil)
popoverPresentationController?.delegate?.popoverPresentationControllerDidDismissPopover?(popoverPresentationController!)
OnboardingII().dismiss(animated: false)
OnboardingII().removeFromParentViewController()
//BannerViewController.willMove(true)
for child in BannerViewController().childViewControllers {child.removeFromParentViewController()}
self.didMove(toParentViewController: self)
hostingController.view.removeFromSuperview()
hostingController.view.isHidden = true
hostingController.view.backgroundColor = .blue
hostingController.willMove(toParent: self)
hostingController.removeFromParentViewController()
hostingController.dismiss(animated: false)
for child in hostingController.childViewControllers {child.removeFromParentViewController()}
for child in hostingController.view.subviews {child.isHidden = true}
Thinking there must be some way to dismiss the UIHostingController (or hiding it) or anyway to get back to UIKit but I haven't had any success.
The strangest part is that if I change the newest ViewController on the Storyboard from .fullScreen to .automatic then I can swipe down to move back from SwiftUI to UIKit. This isn't the desired way to navigate as a button tap to move has been requested, but I haven't been able to replicated this functionality through code at all, I've tried reviewing rewind segues, but then I started to think that the swipe down to go back to the previous view is something else, but there should be a way to call it via code right?

SwiftUI: UITabBarController that contains UIHostingControllers(rootView: SomeSwiftUIView). How to make full screen cover View

I have a UITabBarController that contains 2 Items. FirstVC is UINavigationController that has UIHostingController a SwiftUI HomeView. HomeView has a navigationLink that can navigate to SecondView().
I am trying to implement a full cover view as a loadingView that gets triggered from SecondView().
Since my RootView is the UITabBarController what is the best way to access it in order to show a view on top of UITabBarController to cover everything??
Solution should support iOS 13 and above.
Thank you 🙏
tabBarCnt.tabBar.tintColor = UIColor.black
let firstVc = UINavigationController(rootViewController: UIHostingController(rootView: HomeView()))
firstVc.title = "First"
let secondVc = UIViewController()
secondVc.title = "Second"
tabBarCnt.setViewControllers([firstVc, secondVc], animated: false)
UIApplication.shared.windows.first?.rootViewController = tabBarCnt
UIApplication.shared.windows.first?.makeKeyAndVisible()
HomeView
struct HomeView: View {
var body: some View {
NavigationView {
VStack{
Text("Home View")
NavigationLink {
SecondView()
} label: {
Text("Press here to go to second page")
}
}
}
.navigationBarTitle("NAVIGATION TITLE")
}
}
SecondView
struct SecondView: View {
var body: some View {
VStack(spacing: 20) {
Text("Second View")
Text("How can I make a modal view that covers the entire view and the tabBar ??? the View should get triggered from there. BAsically I want to make a loading view that covers everything.")
}
}
}

UIHostingController view pinned to a UIView is in the wrong position when a VStack updates

I have a SwiftUI view representing a panel of buttons:
import SwiftUI
struct Buttons: View {
#State var isButton1Hidden = false
#State var isButton2Hidden = true
#State var isButton3Hidden = true
var body: some View {
VStack {
if !isButton1Hidden {
Button {
isButton2Hidden = false
} label: {
Image(systemName: "triangle")
.foregroundColor(.white)
}
.frame(width: 50.0, height: 50.0)
.background(.black.opacity(0.7))
.clipShape(Circle())
}
if !isButton2Hidden {
Button {
// Action
} label: {
Image(systemName: "circle")
.foregroundColor(.white)
}
.frame(width: 50.0, height: 50.0)
.background(.black.opacity(0.7))
.clipShape(Circle())
}
if !isButton3Hidden {
Button {
// Action
} label: {
Image(systemName: "square")
.foregroundColor(.white)
}
.frame(width: 50.0, height: 50.0)
.background(.black.opacity(0.7))
.clipShape(Circle())
}
}
}
}
struct Buttons_Previews: PreviewProvider {
static var previews: some View {
Buttons()
}
}
This is added to a view controller view UIHostingController:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let buttons = Buttons()
let hostingController = UIHostingController(rootView: buttons)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
let horizConstraint = hostingController.view.trailingAnchor.constraint(
equalTo: view.trailingAnchor,
constant: -16.0
)
let vertConstraint = hostingController.view.topAnchor.constraint(
equalTo: view.topAnchor,
constant: 16.0
)
NSLayoutConstraint.activate([horizConstraint, vertConstraint])
}
}
The result is as follows:
This is what I expect. The view is pinned correctly to the top of the safeAreaLayoutGuide. However if I change the state in the panel of buttons and hide/unhide (tap the first button) them the view is no longer in the correct position:
What am I doing wrong here? Is there a way to keep this panel aligned to the top?
As you leave height constraint ambiguous UIHostingController specifies constant one for itself by initial root view height. I can recommend the following approach:
give it bottom constraint explicitly, like
let vertConstraint1 = hostingController.view.bottomAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: 16.0
)
NSLayoutConstraint.activate([horizConstraint, vertConstraint, vertConstraint1])
push buttons to top (because they are at center now, by default)
VStack {
Button() {}
Button() {}
Button() {}
Spacer() // << here !!
}
**) by default hosting view background is not transparent, so
hostingController.view.backgroundColor = .clear
Tested with Xcode 13.4 / iOS 15.4
Complete test module is here

How to center NSPopover when using SwiftUI?

If the NSPopover has its contentViewController set to some NSViewController that does not use SwiftUI directly on its view. For instance
final class ViewController: NSViewController {
private lazy var contentView = NSView()
override func loadView() {
view = contentView
}
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
}
let controller = ViewController()
popover.contentViewController = controller
It will be displayed centered.
However, if we simply change the line of the corresponding view:
private lazy var contentView = NSHostingView(rootView: Blah())
Where Blah is
struct Blah: View {
var body: some View {
ZStack {
Text("blah")
}
.frame(width: 400, height: 600, alignment: .center)
.background(Color.green)
}
}
You can see that the view is not centralized anymore. So, how can we let the NSViewController centralized in relation to the NSStatusItem item? (In the images those are one check icon)
If you give a close look at the images, you can see that the image with the white view has the popover arrow in the middle, while the other has not.

Embed a SwiftUI view in an AppKit view

I have a SwiftUI view MySwiftUIView:
import SwiftUI
struct MySwiftUIView: View {
var body: some View {
Text("Hello, World!")
}
}
I want to use it as part of an AppKit view. I tried the following code:
import Cocoa
import SwiftUI
class MyViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview( NSHostingView(rootView: MySwiftUIView()) )
}
}
with the corresponding storyboard:
After the code is built, the result is an empty window:
What I want is this:
How should I make this happen?
You setup subview programmatically, so constraints are on your responsibility, no exception for SwiftUI.
Here is correct variant (tested with Xcode 11.4):
override func viewDidLoad() {
super.viewDidLoad()
let myView = NSHostingView(rootView: MySwiftUIView())
myView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(myView)
myView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
myView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
}