Working on a App to show the status of devices, grouped by tags using SwiftUI. I've got the main view working well showing a list of tags and the status of the devices associated. I can click on the tag and open a new view to show info about the tag and some info about the devices.
I've looked at Programatically navigate to new view in SwiftUI
and get the error "Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type"
class MenuChoice: BindableObject {
let didChange = PassthroughSubject<MenuChoice, Never>()
let willChange = PassthroughSubject<MenuChoice, Never>()
func login() {
self.isSettingsViewActive = true
}
var isSettingsViewActive = false {
didSet {
didChange.send(self)
}
}
}
struct ViewMain: View {
#EnvironmentObject var menuChoice: MenuChoice
var body: some View {
if !menuChoice.isSettingsViewActive {
ViewSettings()
} else {
ViewMain2()
}
}
}
I want the option (buttons) to navigate to:
** a high level Settings view where I can config overall settings
** a view that is exclusive to info about Devices
** a view that is exclusive to info about Tags
The views work, but I can't find a way to navigate to the view and then return from the view.
Perhaps SwiftUI is not quite ready??
You need to put your if statement inside a container, for example: Group
struct ViewMain: View {
#EnvironmentObject var menuChoice: MenuChoice
var body: some View {
Group {
if !menuChoice.isSettingsViewActive {
ViewSettings()
} else {
ViewMain2()
}
}
}
}
Related
I'm looking for some help or clarification on whether my current understanding of SwiftUI breaks MVVM or not. This is part of a code challenge for a job I desperately need so really appreciate any comments.
My main view pulls in models from my view model characterManager to populate a list. Each item in the list contains a NavigationLink to navigate to a detail view. I need to pass the model to the detail view and build a view model for the detail view to use but I'm not sure where this view model should be created. Currently I'm creating the new CharacterDetailViewModel in the CharacterListView and passing to the detail view's init function. I'm not sure if it is right to create a view model directly in the view.
Here's my main view where the list is populated using character models and passed into a detail view with a new CharacterDetailViewModel:
struct CharacterListView: View {
#StateObject var characterManager = CharacterManager()
#StateObject var realmManager = RealmManager()
var body: some View {
NavigationStack {
if characterManager.isLoading {
ProgressView()
} else {
List(characterManager.characters) { character in
NavigationLink {
CharacterDetailView(characterViewModel: CharacterDetailViewModel(character: character))
} label: {
Text(character.name)
}
}
.navigationTitle("Characters")
}
}
.environmentObject(characterManager)
.environmentObject(realmManager)
}
}
And this is the detail view where the view model is set in the init
struct CharacterDetailView: View {
#EnvironmentObject var characterManager: CharacterManager
#ObservedObject var viewModel: CharacterDetailViewModel
init(characterViewModel: CharacterDetailViewModel) {
self.viewModel = characterViewModel
}
var body: some View {
...
}
}
Is this a legal way to do what I'm trying to achieve or is there a better way to do this? The company I'm applying to uses MVVM with SwiftUI so I have to stick with this architecture.
I would make it simple and directly use the Character model into the application without the use if a view model. In SwiftUI, view is the View Model so in most cases you don't need to create separate VM for each screen. I would use the following approach.
#MainActor
class CharacterManager: ObservableObject {
#Published var characters: [Character] = []
func fetchCharacters() {
characters = [Character(name: "Character 1"), Character(name: "Character 2")]
}
}
struct Character: Identifiable {
let id = UUID()
let name: String
}
struct CharacterDetailView: View {
let character: Character
var body: some View {
Text(character.name)
.font(.largeTitle)
}
}
struct ContentView: View {
#StateObject private var characterManager = CharacterManager()
var body: some View {
NavigationStack {
List(characterManager.characters) { character in
NavigationLink {
CharacterDetailView(character: character)
} label: {
Text(character.name)
}
}.onAppear {
characterManager.fetchCharacters()
}
}
}
}
When I update a binding property from an array in a pushed view 2+ layers down, the navigation pops back instantly after a change to the property.
Xcode 13.3 beta, iOS 15.
I created a simple demo and code is below.
Shopping Lists
List Edit
List section Edit
Updating the list title (one view deep) is fine, navigation stack stays same, and changes are published if I return. But when adjusting a section title (two deep) the navigation pops back as soon as I make a single change to the property.
I have a feeling I'm missing basic fundamentals here, and I have a feeling it must be related to the lists id? but I'm struggling to figure it out or work around it.
GIF
Code:
Models:
struct ShoppingList {
let id: String = UUID().uuidString
var title: String
var sections: [ShoppingListSection]
}
struct ShoppingListSection {
let id: String = UUID().uuidString
var title: String
}
View Model:
final class ShoppingListsViewModel: ObservableObject {
#Published var shoppingLists: [ShoppingList] = [
.init(
title: "Shopping List 01",
sections: [
.init(title: "Fresh food")
]
)
]
}
Content View:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}
}
}
ShoppingListsView
struct ShoppingListsView: View {
#StateObject private var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
ShoppingListEditView
struct ShoppingListEditView: View {
#Binding var shoppingList: ShoppingList
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("Title", text: $shoppingList.title)
}
Section(header: Text("Sections")) {
List($shoppingList.sections, id: \.id) { $section in
NavigationLink(destination: ShoppingListSectionEditView(section: $section)) {
Text(section.title)
}
}
}
}
.navigationBarTitle("Edit list")
}
}
ShoppingListSectionEditView
struct ShoppingListSectionEditView: View {
#Binding var section: ShoppingListSection
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("title", text: $section.title)
}
}
.navigationBarTitle("Edit section")
}
}
try this, works for me:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}.navigationViewStyle(.stack) // <--- here
}
}
Try to make you object confirm to Identifiable and return value which unique and stable, for your case is ShoppingList.
Detail view seems will pop when object id changed.
The reason your stack is popping back to the root ShoppingListsView is that the change in the list is published and the root ShoppingListsView is registered to listen for updates to the #StateObject.
Therefore, any change to the list is listened to by ShoppingListsView, causing that view to be re-rendered and for all new views on the stack to be popped in order to render the root ShoppingListsView, which is listening for updates on the #StateObject.
The solution to this is to change the #StateObject to #EnvironmentObject
Please refactor your code to change ShoppingListsViewModel to use an #EnvironmentObject wrapper instead of a #StateObject wrapper
You may pass the environment object in to all your child views and also add a boolean #Published flag to track any updates to the data.
Then your ShoppingListView would look as below
struct ShoppingListsView: View {
#EnvironmentObject var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
Don't forget to pass the viewModel in to all your child views.
That should fix your problem.
I get the following error: Unable to present view. Please file a bug whenever I make an asynchronous call on a view and leave the view (e.g. navigate to another view in the navigation stack) before it can make changes to the ui. Consequently, the next view in the navigation stack is unable to update its view. How can I fix this problem?
An example of the problem occurring is when I switch from view1 to view2 before my GetIoTThingIndex() call finishes and makes an update to the ui.
GetIoTThingIndex.query(device) { error in
DispatchQueue.main.async { [self] in
...
}
}
EDIT:
After doing more investigating, I found that this problem is due to the fact that I am implementing my logic in an MVVM pattern. When I moved my logic directly into the the view and called the functions and state variables inside the view, everything worked fine. It's interesting because when I started building my app with just a few pages with minimal logic and dependencies, this MVVM pattern worked fine without any bugs. However, when my project grew to 20+ pages with more logic and dependencies, the MVVM pattern causes this bug. Is this just a problem I see or has anyone seen anything like this before and have any recommendations for fixing it?
This is the way I had things with MVVM.
View
struct DeviceView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
Text(viewModel.name)
...
}
}
View Model
class DeviceViewModel: ObservableObject {
#Published var name = ""
public func updateUI() {
...
}
...
}
This is the way I have things now (which works without this bug).
View
struct DeviceView: View {
var body: some View {
Text(name)
...
}
#State var name = ""
public func updateUI() {
...
}
...
}
Are you sure this is what is happening?
I've tested the idea of navigating to another view
before the parent can make a change to its view. And all works well.
This is the code I used for the test, click on the button first, then within 3 sec click on the NavigationLink.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var thingToUpdate = ""
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("text \(thingToUpdate)")
Button("click me first") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
thingToUpdate = " is updated now"
}
}
NavigationLink(destination: Text("the detail view")) {
Text("then to DetailView")
}
}
}
}
}
Edit update using ObservableObject that works for me:
class DeviceViewModel: ObservableObject {
#Published var name = "no name"
public func updateUI() {
// simulated delay on the main thread
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.name = "success"
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = DeviceViewModel()
var body: some View {
NavigationView {
VStack (spacing: 40) {
Text("viewModel name is \(viewModel.name)")
Button("click me first") {
viewModel.updateUI()
}
NavigationLink(destination: Text("DetailView")) {
Text("then to DetailView")
}
}
}
}
}
The root view of my onboarding process has a NavigationView. The root view of my app is a login page that also contains a NavigationView. That means when someone launches the app for the first time, they will go through the onboarding process and land at the login screen - resulting in a navigation view within a navigation view.
Is there a way to reset the view stack or simply remove the extra navigation view when necessary?
This is how I implemented #New Dev's solution below. First comes the Tower class. (The name helps me visualize the fact that it's an ObservableObject.) Its job is to keep track of the currentPage and let interested views know when it has changed.
import Foundation
import SwiftUI
import Combine
class Tower: ObservableObject {
enum Views {
case onboarding, login, dashboard
}
let objectWillChange = PassthroughSubject<Tower, Never>()
#Published var currentPage: Views = .onboarding {
didSet {
objectWillChange.send(self)
}
}
}
Next comes the ConductorView. It is notified by the Tower when currentPage changes, and loads the corresponding view.
struct ConductorView: View {
#EnvironmentObject var tower: Tower
var body: some View {
VStack {
if tower.currentPage == .onboarding {
ContentViewA()
} else if tower.currentPage == .login {
ContentViewB()
}
}
}
}
And lastly, a content view.
struct ContentViewA: View {
#EnvironmentObject var tower: Tower
var body: some View {
Button(action: {
self.tower.currentPage = .login
}) {
Text("Go to Login")
}
}
}
}
In addition to New Devs greatly appreciated solution, I also used this article from BLCKBIRDS.
I'll expand on my comment. NavigationView/NavigationLink aren't the only ways to change views - a simple conditional can also be used to determine which view is rendered.
So, say, you have some class that contains the state of the login/onboarding information:
class AppState: ObservableObject {
enum UserFlow {
case onboarding, login, home
}
#Published var userFlow: UserFlow = .onboarding
// ...
}
Then your RootView could determine which user flow to show:
struct RootView: View {
#EnvironmentObject var appState: AppState
var body: some View {
if appState.userFlow == .onboarding {
OnboardingRootView()
} else if appState.userFlow == .login {
LoginRootView()
} else {
ContentView()
}
}
}
I've presented a modal view but I would like the user to go through some steps before it can be dismissed.
Currently the view can be dragged to dismiss.
Is there a way to stop this from being possible?
I've watched the WWDC Session videos and they mention it but I can't seem to put my finger on the exact code I'd need.
struct OnboardingView2 : View {
#Binding
var dismissFlag: Bool
var body: some View {
VStack {
Text("Onboarding here! 🙌🏼")
Button(action: {
self.dismissFlag.toggle()
}) {
Text("Dismiss")
}
}
}
}
I currently have some text and a button I'm going to use at a later date to dismiss the view.
iOS 15+
Starting from iOS 15 we can use interactiveDismissDisabled:
func interactiveDismissDisabled(_ isDisabled: Bool = true) -> some View
We just need to attach it to the sheet. Here is an example from the documentation:
struct PresentingView: View {
#Binding var showTerms: Bool
var body: some View {
AppContents()
.sheet(isPresented: $showTerms) {
Sheet()
}
}
}
struct Sheet: View {
#State private var acceptedTerms = false
var body: some View {
Form {
Button("Accept Terms") {
acceptedTerms = true
}
}
.interactiveDismissDisabled(!acceptedTerms)
}
}
It is easy if you use the 3rd party lib Introspect, which is very useful as it access the corresponding UIKit component easily. In this case, the property in UIViewController:
VStack { ... }
.introspectViewController {
$0.isModalInPresentation = true
}
Not sure this helps or even the method to show the modal you are using but when you present a SwiftUI view from a UIViewController using UIHostingController
let vc = UIHostingController(rootView: <#your swiftUI view#>(<#your parameters #>))
you can set a modalPresentationStyle. You may have to decide which of the styles suits your needs but .currentContext prevents the dragging to dismiss.
Side note:I don't know how to dismiss a view presented from a UIHostingController though which is why I've asked a Q myself on here to find out 😂
I had a similar question here
struct Start : View {
let destinationView = SetUp()
.navigationBarItem(title: Text("Set Up View"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Set Up")
}
}
}
}
The main thing here is that it is hiding the back button. This turns off the back button and makes it so the user can't swipe back ether.
For the setup portion of your app you could create a new SwiftUI file and add a similar thing to get home, while also incorporating your own setup code.
struct SetUp : View {
let destinationView = Text("Your App Here")
.navigationBarItem(title: Text("Your all set up!"), titleDisplayMode: .automatic, hidesBackButton: true)
var body: some View {
NavigationView {
NavigationButton(destination: destinationView) {
Text("Done")
}
}
}
}
There is an extension to make controlling the modal dismission effortless, at https://gist.github.com/mobilinked/9b6086b3760bcf1e5432932dad0813c0
A temporary solution before the official solution released by Apple.
/// Example:
struct ContentView: View {
#State private var presenting = false
var body: some View {
VStack {
Button {
presenting = true
} label: {
Text("Present")
}
}
.sheet(isPresented: $presenting) {
ModalContent()
.allowAutoDismiss { false }
// or
// .allowAutoDismiss(false)
}
}
}