How can I launch a SwiftUI View without Navigation back tracking? - swiftui

I want to launch a View as a standalone View without the navigation hierarchy. The reason that I don't want to use a NavigationButton is that I don't want the user to return to the calling form.
I have tried the following approach that is similar to how the first view is launched in ScenceDelegate but nothing happens:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let window = appDelegate.getNewWindow()
window.rootViewController = UIHostingController(rootView: NewView())
window.makeKeyAndVisible()
I have a legitimate reason not to use the navigation UI, I'm leaving the explanation out to keep this short. I'm avoiding Storyboards to keep this as a simple as possible.
Thank you for any solution suggestions.

This is how I accomplished loading a new root View.
I added the following to the AppDelegate code
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
...
func loadNewRootSwiftUIView(rootViewController: UIViewController)
{
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = rootViewController
self.window = window
window.makeKeyAndVisible()
}
}
And placed the following in my form:
struct LaunchView : View {
var body: some View {
VStack {
Button(
action: {
LaunchLoginView()
},
label: {
Text("Login")
}
)
}
}
}
func LaunchLoginView(){
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let vContoller = UIHostingController(rootView: LoginView())
appDelegate.loadNewRootSwiftUIView(rootViewController: vContoller)
}

Related

SwiftUI UIViewRepresentable ScrollView is not zooming

I am developing an iOS app using the SwiftUI. I need to implement the "pinch to zoom" feature in the app so i tried using the SwiftUI's ScrollView but went in to the problems of not able to pinch and drag the content at the same time as discussed in the question here. So i tried using the UIKit's UIScrollView in an UIViewRepresentable as suggested in the same thread. The problem i am facing now is when i pinch zoom the view the following error is displayed in the console and view doesn't zoom.
[Assert] -[UIScrollView _clampedZoomScale:allowRubberbanding:]: Must be called with non-zero scale
My UIViewRepresentable does not occupy the entire screen and is embedded in a VStack that occupies some portion on the screen along with other elements. I am using NavigationView that holds the VStack. I have an ObservableObject as a state on the main view. I update few #Published properties on this ObservableObject from .onAppear() of the main view. When i stop updating these properties, the zoom seems to be working as expected. I am not sure what is causing the issue, can some one please help if you have faced the above error? Thanks in advance.
I could replicate this with a sample code provided below.
TestScrollViewApp.swift
#main
struct TestScrollViewApp: App {
var body: some Scene {
WindowGroup {
TestView(interactor: TestInteractor())
}
}
}
TestView.swift
import Foundation
import SwiftUI
struct TestView: View {
#ObservedObject var interactor : TestInteractor
var body: some View {
ZStack{
NavigationView{
VStack{
Text("Hi there!")
HStack{
Text("HI i am beside map")
NativeScrollView()
}
Text("Hi I am footer!")
}
}.navigationViewStyle(StackNavigationViewStyle())
}.onAppear{
interactor.setUp()
}
}
}
struct NativeScrollView: UIViewRepresentable {
let imageview = UIImageView()
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIScrollViewDelegate {
let parent: NativeScrollView
var zoomableView: UIView?
init(_ parent: NativeScrollView) {
self.parent = parent
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return zoomableView
}
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.delegate = context.coordinator
scrollView.isScrollEnabled = true
scrollView.bouncesZoom = true;
imageview.contentMode = .scaleAspectFit
imageview.autoresizingMask = [.flexibleWidth,.flexibleHeight]
//add the image view
imageview.image = UIImage(named: "mapscreen")
scrollView.addSubview(imageview)
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 4.0
return scrollView
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
context.coordinator.zoomableView = imageview
}
}
TestInteractor.swift
class TestInteractor:ObservableObject{
#Published var hasFooter = true
func setUp(){
hasFooter = false
}
}

UIButton in SwiftUI Catalyst Mac app doesn't work when clicked second time

Here's the code I have:
private struct ShareButton: UIViewRepresentable {
func makeUIView(context: Context) -> UIButton {
let activityViewController = UIActivityViewController(activityItems: [URL(string: "https://www.apple.com/")!], applicationActivities: nil)
let action = UIAction(title: "Share") { _ in UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController?.present(activityViewController, animated: false) }
let button = UIButton(primaryAction: action)
activityViewController.popoverPresentationController?.sourceView = button
return button
}
func updateUIView(_ uiView: UIButton, context: Context) { }
}
Basically it's creating a UIButton with a UIAction, inside which there's a UIActivityViewController that set sourceView for the share menu to be the UIButton.
Here's a demo of the issue:
The UIButton is created when the SwiftUI view is created, and set as the sourceView. My guess is that the issue occur because the UIButton is somehow destroyed and recreated due to some SwiftUI mechanism? I can be entirely wrong though. Anyway to solve this?
Or any other way to do share button in a SwiftUI Catalyst Mac app?
"Or any other way to do share button in a SwiftUI Catalyst Mac app?"
You could try this approach, using the extension from:
How to get rid of message " 'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead" with AdMob banner?
public extension UIApplication {
func currentUIWindow() -> UIWindow? {
let connectedScenes = UIApplication.shared.connectedScenes
.filter({
$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
let window = connectedScenes.first?
.windows
.first { $0.isKeyWindow }
return window
}
}
struct ContentView: View {
let holaTxt = "Hola 😀 "
var body: some View {
Button(action: {
let AV = UIActivityViewController(activityItems: [holaTxt], applicationActivities: nil)
UIApplication.shared.currentUIWindow()?.rootViewController?.present(AV, animated: true, completion: nil)
}) {
Text("Share")
}
}
}
Found an elegant solution for creating a share button in a SwiftUI Catalyst Mac app (in fact, all SwiftUI app), see https://github.com/SwiftUI-Plus/ActivityView

Popping views using SceneDelegate.toView()

I have a UI where you can navigate/push views like this:
AView -> BView -> CView -> DView
I want to pop a couple of views (get to BView from DView) instead of placing a new view (BView) on top of existing stack and I found the way to do this:
(UIApplication.shared.connectedScenes.first?.delegate as?
SceneDelegate)?.toBView()
What I don't understand is why does it call CView's init() when I try to return from DView to BView?
Here's the output i get in debug console:
AView init
BView init
CView init
DView init
contract.
button pressed
BView init
**CView init**
Why does it call CView's init() and how to avoid this behaviour?
AView.swift:
import SwiftUI
struct AView: View {
init() {
print("AView init")
}
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: BView()) {
Text("This is View A, now go to View B.")
}
}
}
}
}
struct BView: View {
init() {
print("BView init")
}
var body: some View {
NavigationLink(destination: CView()) {
Text("This is View B, now go to View C.")
}
}
}
struct CView: View {
init() {
print("CView init")
}
var body: some View {
NavigationLink(destination: DView()) {
Text("This is View C, now go to View D.")
}
}
}
struct DView: View {
init() {
print("DView init")
}
var body: some View {
Button(action: {
print("button pressed")
(UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.toBView()
},
label: {
Text("Back!")
})
}
}
SceneDelegate.swift:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let aView = AView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: AView)
self.window = window
window.makeKeyAndVisible()
}
}
func toBView() {
let bView = BView()
window?.rootViewController = UIHostingController(rootView: bView)
}
}
This behavior is not wrong. If you start your app and display AView you will see in the console that it prints init from A and B.
It's because BView() is already initialized in your AView, even though you haven't activated the NavigationLink.
NavigationLink(destination: BView()) {
So this behavior is not specific to your pop back action, happens already at the beginning. See this solution from Asperi regarding multiple init() aswell, if you are concerned about calling init() more than once

How to launch a tutorial on first app launch using SwiftUI?

I would like to launch a tutorial when my SwiftUI app first launches. Where in the project should this code go and how do I launch the tutorial, which is just a SwiftUI View, when the app first launches?
I already know how to check if the app has launched before using UserDefaults. I am wanting to know how to launch the SwiftUI view and then how to launch the standard ContentView after the user completes the tutorial.
let hasLaunchedBefore = UserDefaults.standard.bool(forKey: "hasLaunchedBefore")
if hasLaunchedBefore {
// Not first launch
// Load ContentView here
} else {
// Is first launch
// Load tutorial SwiftUI view here
UserDefaults.standard.set(true, forKey: "hasLaunchedBefore") // Set hasLaunchedBefore key to true
}
Try put this in your sceneDelegate
let hasLaunchedBefore = UserDefaults.standard.bool(forKey: "hasLaunchedBefore")
let content = ContentView()
let tutorial = TutorialView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
if hasLaunchedBefore {
window.rootViewController = UIHostingController(rootView: content)
} else {
window.rootViewController = UIHostingController(rootView: tutorial)
UserDefaults.standard.set(true, forKey: "hasLaunchedBefore")
}
self.window = window
window.makeKeyAndVisible()
}
In your SampleApp which after SwiftUI 2.0:
import SwiftUI
#main
struct SampleApp: App {
#AppStorage("didLaunchBefore") var didLaunchBefore: Bool = true
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
if didLaunchBefore {
SplashView()
} else {
ContentView()
}
}
}
}
Then add a button with action like below in SplashView:
UserDefaults.standard.set(false, forKey: "didLaunchBefore")

Problem: How to call a SwiftUI View Struct Method from SceneDelegate

Currently I'm the one not getting it.
Problem:
Trying to connect to a View Method from a Scene Delegate e.g.:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
...
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
// guard let contentView = ???? as? ContentView else { return }
// contentView.callMethod(parameter: true)
}
}
struct ContentView: View {
var body: some View {
...
}
func callMethode(parameter: Bool) {
print("called")
}
}
Any clue how to connect to the View and call a method in-between?
thx
Jo
This is counter to the design of the SwiftUI framework. You should not have any persistent view around to call methods on. Instead, views are created and displayed as needed in response to your app's state changing.
For example, if your SceneDelegate had a reference to an instance of a model class, and your view depended on that model, you could modify the model in scene(_:continue:) and your view would update automatically.
If you can provide more specifics on what you're attempting to do, I may be able to give a more specific answer.
Many thanks. Fully understood... Will read the docs...
So is this a possible way as an example:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let activity = UserActivityManager()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(activity))
self.window = window
window.makeKeyAndVisible()
if let userActvity = connectionOptions.userActivities.first {
guard let title = userActvity.title else { return }
activity.doAction(action: title)
}
}
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard let title = userActivity.title else { return }
activity.doAction(action: title)
}
}