Adding SwiftUI to UIKit, and navigation back to UIKit - swift

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?

Related

Drawing issues using SwiftUI view as a PrimaryView in uisplitviewcontroller

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?

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.")
}
}
}

Screenshot/Snapshot-Button in Form in NavigationView [iOs 15 iSwiftUI Xcode 13.4.1]

I have got an app with a Form in a NavigationView. In .navigationBarItems I got a Button to take a Screenshot from the whole display. The button runs a function i got from "hacking with swift" (I think this codesnippet is well known link)
The Problem: when I call the function snapshot() on self oder body nothing is happen except the qestion from ios to allow access to the imagefolder. Only when I create an extra view like the guy from "hacking with swift" it works. but then only this extra view. that is not the way I wanna go.
Question: on which object I have to call the snapshot()-function to render the whole display as it is (Navigationview with the Form NOT the textView like in the example)
I tried: self.snapshot(), body.snapshot(). How can i call the navigationbar or the form?
Thanks! :)
import SwiftUI
//FUNCTION TO TAKE A SCREENSHOT
extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(bounds: view!.bounds)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
//EXAMPLE VIEW, THIS VIEW GETS SAVED IN THE EXAMPLE FROM HWS
var textView: some View {
Text("Hello, SwiftUI")
.padding()
.background(.blue)
.foregroundColor(.white)
.clipShape(Capsule())
}
//THE VIEW I WANT TO BE SAVE IN PHOTOS
var body: some View {
NavigationView {
Form {
Section{
HStack{
Text("Test")
Text("test2")
}
Section{
VStack {
Text("test3")
Text("test4")
}
}
}
}
}
.navigationBarTitle(Text("Test"))
//NAVIGATIONBAR WITH THE BUTTON FOR TAKING SCREENSHOT
.navigationBarItems(trailing:
Button(action: {
let image = textView.snapshot() // WHICH OBJECT HAS TO CALL THE SNAPSHOTFUNCTION?
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
},
label: {Image(systemName: "camera.fill")
.foregroundColor(.red)}))
}

How to go full screen with VideoPlayer() in SwiftUI?

I'm using the following code in my project and it works great.
But I need to know if its possible to play the video in full screen when the user taps on a button or any other action.
This is my code:
VideoPlayer(player: AVPlayer(url: URL(string: "https://xxx.mp4")!)) {
VStack {
Text("Watermark")
.foregroundColor(.black)
.background(Color.white.opacity(0.7))
Spacer()
}
.frame(width: 400, height: 300)
}
.frame(width: UIScreen.main.bounds.width, height: 300)
So basically, the idea is to make the VideoPlayer to go full screen similar to youtube's video player.
Is this possible using the VideoPlayer() in swiftUI?
They should include an option or have that button by default in VideoPlayer I don't know why apple did this if someone knows add a comment about the same, anyways
There is a simpler method though
struct CustomVideoPlayer: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
let url: String = "https://xxx.mp4"
let player1 = AVPlayer(url: URL(string: url)!)
controller.player = player1
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
}
Try this code and call CustomVideoPlayer() and give it a frame with any height and you will have a fullscreen option on the top left corner just like youtube
EDIT: I tried the accepted solution but I don't think it is going fullscreen
I took #Viraj D's answer and created a Swift package called AZVideoPlayer. Add the package with SPM or feel free to just copy the source code into your project.
The package also adds the following:
Addresses the issue where onDisappear is called when full screen presentation begins by allowing a way to ignore the call in that particular case.
Allows you to have the video continue playing when ending full screen presentation (it pauses by default).
Here's an example:
import SwiftUI
import AVKit
import AZVideoPlayer
struct ContentView: View {
var player: AVPlayer?
#State var willBeginFullScreenPresentation: Bool = false
init(url: URL) {
self.player = AVPlayer(url: url)
}
var body: some View {
AZVideoPlayer(player: player,
willBeginFullScreenPresentationWithAnimationCoordinator: willBeginFullScreen,
willEndFullScreenPresentationWithAnimationCoordinator: willEndFullScreen)
.onDisappear {
// onDisappear is called when full screen presentation begins, but the view is
// not actually disappearing in this case so we don't want to reset the player
guard !willBeginFullScreenPresentation else {
willBeginFullScreenPresentation = false
return
}
player?.pause()
player?.seek(to: .zero)
}
}
func willBeginFullScreen(_ playerViewController: AVPlayerViewController,
_ coordinator: UIViewControllerTransitionCoordinator) {
willBeginFullScreenPresentation = true
}
func willEndFullScreen(_ playerViewController: AVPlayerViewController,
_ coordinator: UIViewControllerTransitionCoordinator) {
// This is a static helper method provided by AZVideoPlayer to keep
// the video playing if it was playing when full screen presentation ended
AZVideoPlayer.continuePlayingIfPlaying(player, coordinator)
}
}
Here's how I managed to do it. I hope this helps someone else in the future.
This is a rough idea but it works as it was intended to.
Basically, I've put the VideoPlayer in a VStack and gave it a full width and height.
I gave the VStack a set height and full width.
I gave the VStack a tapGesture and changed its width and height on double tap and this way, all its content (VideoPlayer) will resize accordingly.
This is the code:
struct vidPlayerView: View {
#State var animate = false
var body: some View {
VStack{
VideoPlayer(player: AVPlayer(url: URL(string: "https://xxxx.mp4")!)) {
VStack {
Image(systemName: "info.circle")
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
Spacer()
}
.frame(width: 400, height: 300, alignment: .topLeading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Spacer()
}.onTapGesture(count: 2) {
withAnimation {
self.animate.toggle()
}
}
.frame(maxWidth: self.animate ? .infinity : UIScreen.main.bounds.width, maxHeight: self.animate ? .infinity : 300, alignment: .topLeading)
}
}
The above code can be executed by taping on a button or any other actions.

React on drag over view

I have a view that I want to change whenever long press or drag occurs over it. It can start outside of the view, but also inside.
struct ContentView: View {
#State var active: Bool = false
var body: some View {
Rectangle()
.fill(self.active ? Color.red : Color.secondary)
}
}
An example of this behaviour is the iPhone keyboard: when you long press on a key it pops up (active = true). When you move outside it, it pops down (active = false) but the next key is then active.
I have tried using LongPressGesture but cannot figure out how to make it behave as I want.
I made example for you in playgrounds to display how to use a LongPressGestureRecognizer.
You add the gesture recognizer to the view you want long pressed, with target being the parent controller handling the gesture recognition (ContentView in your case) and action being what happens with a long press.
In my implementation, a long press to the view bodyView changes its color from clear to red. This happens inside the didSet of the showBody property, which calls showBodyToggled(). I'm checking the state of the gesture, because the gesture recognizer will send messages for each state (I'm only performing the action if the state is .began).
Let me know if you have any questions:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
var bodyView: UIView!
var showBody = false {
didSet {
showBodyToggled()
}
}
override func viewDidLoad() {
super.viewDidLoad()
configureSubviews()
configureLongPressRecognizer()
}
func configureSubviews() {
bodyView = UIView()
bodyView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bodyView)
NSLayoutConstraint.activate([
bodyView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
bodyView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
bodyView.heightAnchor.constraint(equalToConstant: 200),
bodyView.widthAnchor.constraint(equalToConstant: 300)
])
}
func configureLongPressRecognizer() {
bodyView.addGestureRecognizer(
UILongPressGestureRecognizer(
target: self,
action: #selector(longPressed(_:))
)
)
}
#objc func longPressed(_ sender: UILongPressGestureRecognizer) {
// the way I'm doing it: only acts when the long press first happens.
// delete this state check if you'd prefer the default long press implementation
switch sender.state {
case .began:
showBody = !showBody
default:
break
}
}
func showBodyToggled() {
UIView.animate(withDuration: 0.4) { [weak self] in
guard let self = self else { return }
self.bodyView.backgroundColor = self.showBody ? .red : .clear
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
EDIT:
Here's an example in SwiftUI where a long press of 0.5 seconds toggles the color of a circle between red and black
struct ContentView: View {
#State var active = false
var longPress: some Gesture {
LongPressGesture()
.onEnded { _ in
withAnimation(.easeIn(duration: 0.4)) {
self.active = !self.active
}
}
}
var body: some View {
Circle()
.fill(self.active ? Color.red : Color.black)
.frame(width: 100, height: 100, alignment: .center)
.gesture(longPress)
}
}