How to show a SwiftUI View programmatically without a button - ios

I have a problem right now:
What I want: The first time the app starts my already existing view should be presented. I already implemented something in the AppDelegate that checks if the app launched for the first time. And if thats the case another view should be presented. Is there a method to do this directly in the AppDelegate like it was possible with Storyboards?
Thank you in advance.

In your AppDelegate you have a hosting controller that bootstraps the main SwiftUI view. So one way to achieve this is to conditionally set the rootView.
UIHostingController(rootView: isFirstTime ? FirstTimeView() : ContentView())

I would create an initial RootView that merely switches between content and provides an EnvironmentValues that's passed to it.
struct RootView: View {
#Environment(\.isInitialLaunch) var isInitialLaunch: Bool
var body: some View {
Group {
if isInitialLaunch {
FirstTimeView()
} else {
ContentView()
}
}
}
Then, in SceneDelegate:
self.window?.rootViewController = UIHostingController(rootView: RootView().environment(\.isInitialLaunch, isInitialLaunch))
Or, make isInitialLaunch a #State (or #Binding, #ObservedObject, etc.) variable. This way, after your onboarding process, if you change it to false, SwiftUI will actually automatically animate users to the ContentView.

Related

SwiftUI: modal Sheet hides ProgressView

I'm trying to have a system-wide progress bar in my SwiftUI application, so I defined this view (be aware: I'm targeting iOS 13+):
import SwiftUI
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { _ in
ZStack {
self.content()
if self.isShowing {
VStack {
ActivityIndicator()
Text("Loading...")
}
}
}
}
}
}
struct ActivityIndicator: UIViewRepresentable {
typealias UIView = UIActivityIndicatorView
fileprivate var configuration = { (_: UIView) in }
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIView { UIView() }
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<Self>) {
uiView.startAnimating()
configuration(uiView)
}
}
and is used in ContenView.swift like this
import SwiftUI
struct ContentView: View {
#EnvironmentObject var myViewModel: MyViewModel
var body: some View {
let isLoading = Binding<Bool>(
get: { self.myViewModel.isLoading },
set: { _ in }
)
LoadingView(isShowing: isLoading) {
NavigationView {
Home()
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
where MyViewModel is a pretty standard ViewModel with a #Published var for isLoading, and Home() is the main entry point for the app.
Whenever some action that trigger isLoading in MyViewModel is done, the progress bar is shown (and hidden) correctly.
But, if I present a .sheet either by one of the view inside the NavigationView, or using the ContentView itself, no matter what it hides the progress bar.
Even if I use the builtin 14+ ProgressView the problem persists.
.Zindex() does not help either.
Any way to have that view always on top when showed, no matter what .sheet, .alert or any overlay view is available on SwiftUI is present on the screen?
Thanks in advance!
As already written in the comments, a modal view will be shown on top of any other view. A modal view is meant to establish a Computer-Human communication, or dialog (thus modal views frequently will be named "Dialog").
The observation, that a sheet (modal view) covers the loading indicator is expected behaviour.
But, IMO the issue described in the question and refined in the comments, can be solved nicely without breaking the behaviour of the modal views:
When you want to show data, that is not yet complete or even completely absent, you may show a "blank" screen, and in additions to this, let the view model generate a view state that says, that the view should show an "Input Sheet".
So initially, the user sees an input form over a blank screen.
Once the user made the input and submits the form (which will be handled in the View Model) the input sheet disappears (controlled by the View State generated by the View Model), and reveals the "blank" view underneath it.
So, the View Model could now present another sheet, or it realises that the input is complete.
Once the input is complete, the view model loads data and since this may take a while, it reflects this in the View State accordingly, for example using a "loading" state or flag. The view renders this accordingly, which is a loading indicator above the "blank" view.
When the view model receives data, it clears the loading state and sets the view state accordingly, passing through the data as well.
The view now renders the data view.
If the loading task failed, the view model composes a view state where the content is "absent" and with an error info.
Again the view renders this, possibly showing an alert with the message above a "blank" view, since there is still no data.
Ensure, the user can dismiss the error alert and the view model handles it by removing the "modal error" state, but the content is still "absent".
Now, the user is starring at a blank view. You may embed an error message here, or even add a "Retry" button. In any case, ensure the user can navigate away from that screen.
And so on. ;)

How to present a View without embedding it into a current navigation flow in SwiftUI?

I have a logging onboarding being finished, and I need to present a HomeView, which knows nothing about previous navigation flow.
var body: some View {
if viewModel.isValidated {
destination()
} else {
LoadingView()
}
Doing it this way I have a navigation bar at the top of destination(). I guess I can hide it, but it would still be the same navigation flow and I need to start a new one. How can I achieve that?(iOS 13)
One way to handle this is with an #Environment object created from a BaseViewModel. The way that this works is to essentially control the state of the presented view from a BaseView or a view controller. I'll attempt to simplify it for you the best I can.
class BaseViewModel: ObservableObject {
#Published var baseView: UserFlow = .loading
init() {
//Handle your condition if already logged in, change
//baseView to whatever you need it to be.
}
enum UserFlow {
case loading, onboarding, login, home
}
}
Once you've setup your BaseViewModel you'll want to use it, I use it in a switch statement with a binding to an #EnvironmentObject so that it can be changed from any other view.
struct BaseView: View {
#EnvironmentObject var appState: BaseViewModel
var body: some View {
Group {
switch appState.userFlow {
case .loading:
LoadingView()
case .onboarding:
Text("Not Yet Implemented")
case .login:
LandingPageView()
case .home:
BaseHomeScreenView().environmentObject(BaseHomeScreenViewModel())
}
}
}
}
Your usage, likely at the end of your register/login flow, will look something like this.
struct LoginView: View {
#EnvironmentObject var appState: BaseViewModel
var body: some View {
Button(action: {appState = .home}, label: Text("Log In"))
}
}
So essentially what's happening here is that you're storing your app flow in a particular view which is never disposed of. Think of it like a container. Whenever you change it, it changes the particular view you want to present. The especially good thing about this is that you can build a separate navigation hierarchy without the use of navigation links, if you wanted.

Reset main content view - Swift UI

My app has one main screen that the user uses, then once they're done go to another view, currently implemented as a .fullscreencover. I want the user to be able to press a button and the app pretty much resets, turning everything back to the way it is when the app is launched for the first time and resetting all variables and classes.
The one method I have tried is opening the view again on top of the final view, however this doesn't reset it. Here is the code I have tried but doesn't work:
Button("New Game"){
newGame.toggle()
}
.fullScreenCover(isPresented: $newGame){
ContentView()
}
Alongside this I have tried navigation views however this causes more issues with the functionality of my app.
Is there a line of code that allows you to do this?
The possible approach is to use global app state
class AppState: ObservableObject {
static let shared = AppState()
#Published var gameID = UUID()
}
and have root content view be dependent on that gameID
#main
struct SomeApp: App {
#StateObject var appState = AppState.shared // << here
var body: some Scene {
WindowGroup {
ContentView().id(appState.gameID) // << here
}
}
}
and now to reset everything to initial state from any place we just set new gameID:
Button("New Game"){
AppState.shared.gameID = UUID()
}

Hide the Home Indicator in Swift 2.0

I'm looking to hide the Home Indicator and while this is straightforward in Swift doesn't appear to be as easy in SwiftUI.
I attempted to use this:
How to hide the home indicator with SwiftUI?
But with the removal of the SceneDelegate I'm too green to know how to properly translate that for the new app protocol.
Anyone have any thoughts?
Dan
Here is possible approach to replace default WindowGroup window's hosting controller with any custom one (in this case w/o home indicator).
The helper extension are taken from before provided solution in https://stackoverflow.com/a/63276688/12299030.
Tested with Xcode 12 / iOS 14
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(MyAppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
Text("") // << temporary placeholder
.withHostingWindow { window in
let contentView = ContentView().environmentObject(SomeObservableObject())
window?.rootViewController =
HideHomeIndicatorController(rootView: contentView)
}
}
}
}
and simplified variant of hosting controller to hide home indicator
class HideHomeIndicatorController<Content:View>: UIHostingController<Content> {
override var prefersHomeIndicatorAutoHidden: Bool {
true
}
}

How to re-initialise classes in a SwiftUI NavigationView

I have two views - a MasterView and DetailView. When opening the DetailView, I initialise a new class that tracks data about the view (in the real implementation, the detail view involves a game).
However, when I press the back button from the DetailView to return to the MasterView, and then press the button to return to the DetailView, my class is unchanged. However, I would like to re-initialise a new copy of this class (in my case to re-start the game) whenever I move from the MasterView to the DetailView.
I have condensed the problem to this code:
import SwiftUI
import Combine
class Model: ObservableObject {
#Published var mytext: String = "mytext"
}
struct MasterView: View {
var body: some View {
NavigationView {
NavigationLink(destination: DetailView(model: Model())) {
Text("press me")
}
}
}
}
struct DetailView: View {
#ObservedObject var model: Model = Model()
var body: some View {
TextField("Enter here", text: $model.mytext)
}
}
struct MasterView_Previews: PreviewProvider {
static var previews: some View {
MasterView()
}
}
I would like to create a new instance of Model every time I click the NavigationLink to the detail view, but it seems like it always refers back to the same original instance - I can see this by typing a change into the text field of the DetailView, which persists if I go back and forward again.
Is there any way of doing this?
Based on your comments - and correct me where wrong - here's how I'd set things up.
Your needs are:
A "base" class. Call it MasterView, "settings", "view state", whatever. This is where everything starts.
A "current game".... well, it could be a struct, a class, even properties in an ObservableObject.
I think that's about it. Hierarchically, your model could be:
ViewState
...Player
......Properties, including ID and history
...Current Game
...... Properties, including difficulty
Please note, I've changed some names and am being very vague on properties. The point is, you can encapsulate all of this in an ObservableObject, create an `EnvironmentObject of it, and have all your SwiftUI views "react" to changes in it.
Leaving out views, hopefully you can see where this "model" can contain just about all the Swift code you wish to do everything - now all you need is to tie in your views.
(1) Create your ObservableObject. It needs to (a) be a class object and (b) conform to the ObservableObject protocol. Here's a simple one:
class ViewState : ObservableObject {
var objectWillChange = PassthroughSubject<Void, Never>()
#Published var playerID = "" {
willSet {
objectWillChange.send()
}
}
}
You can create more structs/classes and instantiate them as needed in your model.
(2) Instantiate ViewState once min your environment. In SceneDelegate:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView()
.environmentObject(ViewState())
)
self.window = window
window.makeKeyAndVisible()
}
}
Note that there's a single line added here and that ViewState is instantiated a single time.
(3) Finally, in any SwiftUI view that needs to know your view state, bind it by adding one line of code:
#EnvironmentObject var model: ViewState
If you want, you can do virtually anything in your model (ViewState) from instantiating a new game, flag something to result in a modal popup, add a player to an array, whatever.
The main thing I hope I'm explaining is there's no need to instantiate a second view state - rather instantiate a second game instance inside your single view state.
Again, if I'm way off from your needs, let me know - I'll gladly delete my answer. Good luck!

Resources