Configure preview for ObservedObject and State object - swift

So I am still learning Swift and for some reason, I am having the hardest trouble with the previews and how to configure them.
So I have the following code:
struct MainView: View {
// The app's model that the containing scene passes in
#ObservedObject var model: MainViewModel
#State var activeTab = 0
var body: some View {
VStack {
TabView(selection: $activeTab) {
Group {
WorldView(model: model, activeTab: $activeTab)
.tabItem {
Label(Tabs.explore.rawValue, systemImage: Tabs.explore.icon)
.environment(\.symbolVariants, .none)
}
.tag(0)
ListView(model: model, activeTab: $activeTab)
.tabItem {
Label(Tabs.list.rawValue, systemImage: Tabs.list.icon)
.environment(\.symbolVariants, .none)
}
.tag(1)
FavoritesView(activeTab: $activeTab)
.tabItem {
Label(Tabs.favorite.rawValue, systemImage: Tabs.favorite.icon)
.environment(\.symbolVariants, .none)
}
.tag(2)
ProfileView(model: model, activeTab: $activeTab)
.tabItem {
Label(Tabs.profile.rawValue, systemImage: Tabs.profile.icon)
.environment(\.symbolVariants, .none)
}
.tag(3)
}
.environmentObject(model)
}
.tint(.accentColor)
.onChange(of: activeTab, perform: { value in
log.info("\n 🟢: (MainView: 46) - User has selected tab: \(value).")
print("")
})
}
.onAppear() {
model.fetchPlaces()
}
}
}
Then I have the preview, as such:
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView(model: model, activeTab: activeTab)
}
}
I am getting the two errors on the previews:
Cannot find 'activeTab' in scope
Cannot find 'model' in scope
If I define it as such:
struct MainView_Previews: PreviewProvider {
#ObservedObject var model: MainViewModel
#State var activeTab = 0
static var previews: some View {
MainView(model: model, activeTab: activeTab)
}
}
I get the following errors:
Instance member 'activeTab' cannot be used on type 'MainView_Previews'
Instance member 'model' cannot be used on type 'MainView_Previews'
Does anyone know how I can configure the preview so that it works properly and doesn't crash?

This happens because you are either passing in non existing paremeters in Previews, or because you cannot initialize objects in previews. Instead, do this:
struct MyExamplePreviews: PreviewProvider{
static var previews: some View {
MainView(model: MainViewModel(), activeTab: 0)
}
}
This will allow you to preview the UI. What this does:
Creates a new model that is passed in at the top level - again, you cannot create this anywhere but right here in the previews
Makes it so that activeTab will be set to 0 - you can have multiple preview devices with different tabs if needed. See the docs for Previews to learn more.

I recommend the below approach, then you can use #EnvironmentObject var model: Model in any View that needs it and don't have to pass it into every View.
class Model: ObservableObject {
#Published var items: [Item] = []
static var shared = Model()
static var preview = Model(preview: true)
init(preview: Bool) {
if preview {
items = // set some test items
return
}
load()
}
fun load(){
// load items from disk
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(Model.shared)
}
}
}
struct ContentView: View {
#EnvironmentObject var: model: Model
var body: some View {
NavigationStack {
List {
ForEach($model.items) $item in {
TextField("Title" text: $item.title)
}
.navigationTitle("Items")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(Model.preview)
}
}

Related

SwiftUI - Nested links within NavigationStack inside a NavigationSplitView not working

I'm playing around with the new navigation API's offered in ipadOS16/macOS13, but having some trouble working out how to combine NavigationSplitView, NavigationStack and NavigationLink together on macOS 13 (Testing on a Macbook Pro M1). The same code does work properly on ipadOS.
I'm using a two-column NavigationSplitView. Within the 'detail' section I have a list of SampleModel1 instances wrapped in a NavigationStack. On the List I've applied navigationDestination's for both SampleModel1 and SampleModel2 instances.
When I select a SampleModel1 instance from the list, I navigate to a detailed view that itself contains a list of SampleModel2 instances. My intention is to navigate further into the NavigationStack when clicking on one of the SampleModel2 instances but unfortunately this doesn't seem to work. The SampleModel2 instances are selectable but no navigation is happening.
When I remove the NavigationSplitView completely, and only use the NavigationStack the problem does not arise, and i can successfully navigate to the SampleModel2 instances.
Here's my sample code:
// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}
struct SampleModel2: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}
// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
enum NavItem {
case first
}
var body: some View {
NavigationSplitView {
NavigationLink(value: NavItem.first) {
Label("First", systemImage: "house")
}
} detail: {
SampleListView()
}
}
}
// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
#State var path = NavigationPath()
#State var selection: SampleModel1.ID? = nil
var body: some View {
NavigationStack(path: $path) {
List(SampleModel1.samples, selection: $selection) { model in
NavigationLink("\(model.id)", value: model)
}
.navigationDestination(for: SampleModel1.self) { model in
SampleDetailView(model: model)
}
.navigationDestination(for: SampleModel2.self) { model in
Text("Model 2 ID \(model.id)")
}
}
}
}
// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
var model: SampleModel1
var body: some View {
Text("Model 1 ID \(model.id)")
List (SampleModel2.samples) { model2 in
NavigationLink("\(model2.id)", value: model2)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I removed this unclear ZStack and all works fine. Xcode 14b3 / iOS 16
// ZStack { // << this !!
SampleListView()
// }
Apple just releases macos13 beta 5 and they claimed this was resolved through feedback assistant, but unfortunately this doesn't seem to be the case.
I cross-posted this question on the apple developers forum and user nkalvi posted a workaround for this issue. I’ll post his example code here for future reference.
import SwiftUI
// Sample model definitions used to trigger navigation with navigationDestination API.
struct SampleModel1: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel1(), SampleModel1(), SampleModel1()]
}
struct SampleModel2: Hashable, Identifiable {
let id = UUID()
static let samples = [SampleModel2(), SampleModel2(), SampleModel2()]
}
// The initial view loaded by the app. This will initialize the NavigationSplitView
struct ContentView: View {
#State var path = NavigationPath()
enum NavItem: Hashable, Equatable {
case first
}
var body: some View {
NavigationSplitView {
List {
NavigationLink(value: NavItem.first) {
Label("First", systemImage: "house")
}
}
} detail: {
SampleListView(path: $path)
}
}
}
// A list of SampleModel1 instances wrapped in a NavigationStack with multiple navigationDestinations
struct SampleListView: View {
// Get the selection from DetailView and append to path
// via .onChange
#State var selection2: SampleModel2? = nil
#Binding var path: NavigationPath
var body: some View {
NavigationStack(path: $path) {
VStack {
Text("Path: \(path.count)")
.padding()
List(SampleModel1.samples) { model in
NavigationLink("Model1: \(model.id)", value: model)
}
.navigationDestination(for: SampleModel2.self) { model in
Text("Model 2 ID \(model.id)")
.navigationTitle("navigationDestination(for: SampleModel2.self)")
}
.navigationDestination(for: SampleModel1.self) { model in
SampleDetailView(model: model, path: $path, selection2: $selection2)
.navigationTitle("navigationDestination(for: SampleModel1.self)")
}
.navigationTitle("First")
}
.onChange(of: selection2) { newValue in
path.append(newValue!)
}
}
}
}
// A detailed view of a single SampleModel1 instance. This includes a list
// of SampleModel2 instances that we would like to be able to navigate to
struct SampleDetailView: View {
var model: SampleModel1
#Binding var path: NavigationPath
#Binding var selection2: SampleModel2?
var body: some View {
NavigationStack {
Text("Path: \(path.count)")
.padding()
List(SampleModel2.samples, selection: $selection2) { model2 in
NavigationLink("Model2: \(model2.id)", value: model2)
// This also works (without .onChange):
// Button(model2.id.uuidString) {
// path.append(model2)
// }
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to create a NavigationManager that would change tab within view models?

I have a NavigationManager to handle changing SwiftUI tab bar selection.
It work if it is set as a #EnvironmentObject in my SwiftUI views, but not when the NavigationManager is called as a service in my view models. The thing is that I would like to use a simpler solution than passing around #EnvironmentObject var navigationManager around and pass them inside view model initializer as I have a lot of them and I am looking for a cleaner approach.
How can I use my NavigationManager to change tabs from inside my view models without passing it in init()?
import SwiftUI
struct ContentView: View {
#StateObject var navigationManager = NavigationManager()
var body: some View {
TabView(selection: $navigationManager.selection) {
AccountView()
.tabItem {
Text("Account")
Image(systemName: "person.crop.circle") }
.tag(NavigationItem.account)
SettingsView()
.tabItem {
Text("Settings")
Image(systemName: "gear") }
.tag(NavigationItem.settings)
.environmentObject(navigationManager)
}
}
}
The navigation manager that I would like to use within view models.
class NavigationManager: ObservableObject {
#Published var selection: NavigationItem = .account
}
enum NavigationItem {
case account
case settings
}
My AccountViewModel and Settings View Model:
class AccountViewModel: ObservableObject {
let navigationManager = NavigationManager()
}
struct AccountView: View {
#StateObject var viewModel = AccountViewModel()
var body: some View {
VStack(spacing: 16) {
Text("AccountView")
.font(.title3)
Button(action: {
viewModel.navigationManager.selection = .settings
}) {
Text("Go to Settings tab")
.font(.headline)
}
.buttonStyle(.borderedProminent)
}
}
}
class SettingsViewModel: ObservableObject {
let navigationManager = NavigationManager()
}
struct SettingsView: View {
#EnvironmentObject var navigationManager: NavigationManager
#StateObject var viewModel = SettingsViewModel()
var body: some View {
VStack(spacing: 16) {
Text("SettingsView")
.font(.title3)
Button(action: {
navigationManager.selection = .account
}) {
Text("Go to Account tab")
.font(.headline)
}
.buttonStyle(.borderedProminent)
}
}
}
I managed to successfully inject my navigationManager as a shared dependency using a property wrapper and change its selection variable using Combine.
So I have created a protocol to wrap NavigationManager to a property wrapper #Injection and make its value a CurrentValueSubject
import Combine
final class NavigationManager: NavigationManagerProtocol, ObservableObject {
var selection = CurrentValueSubject<NavigationItem, Never>(.settings)
}
protocol NavigationManagerProtocol {
var selection: CurrentValueSubject<NavigationItem, Never> { get set }
}
This is the #Injection property wrapper to pass my instance of NavigationManager between .swift files.
#propertyWrapper
struct Injection<T> {
private let keyPath: WritableKeyPath<InjectedDependency, T>
var wrappedValue: T {
get { InjectedDependency[keyPath] }
set { InjectedDependency[keyPath] = newValue }
}
init(_ keyPath: WritableKeyPath<InjectedDependency, T>) {
self.keyPath = keyPath
}
}
protocol InjectedKeyProtocol {
associatedtype Value
static var currentValue: Self.Value { get set }
}
struct InjectedDependency {
private static var current = InjectedDependency()
static subscript<K>(key: K.Type) -> K.Value where K: InjectedKeyProtocol {
get { key.currentValue }
set { key.currentValue = newValue }
}
static subscript<T>(_ keyPath: WritableKeyPath<InjectedDependency, T>) -> T {
get { current[keyPath: keyPath] }
set { current[keyPath: keyPath] = newValue }
}
}
extension InjectedDependency {
var navigationManager: NavigationManagerProtocol {
get { Self[NavigationManagerKey.self] }
set { Self[NavigationManagerKey.self] = newValue }
}
}
private struct NavigationManagerKey: InjectedKeyProtocol {
static var currentValue: NavigationManagerProtocol = NavigationManager()
}
With this in place, I can pass my NavigationManager between my view models and send new value using Combine on button tap:
class AccountViewModel: ObservableObject {
#Injection(\.navigationManager) var navigationManager
}
struct AccountView: View {
var viewModel = AccountViewModel()
var body: some View {
VStack(spacing: 16) {
Text("AccountView")
.font(.title3)
Button(action: {
viewModel.navigationManager.selection.send(.settings)
}) {
Text("Go to Settings tab")
.font(.headline)
}
.buttonStyle(.borderedProminent)
}
}
}
class SettingsViewModel: ObservableObject {
#Injection(\.navigationManager) var navigationManager
}
struct SettingsView: View {
#StateObject var viewModel = SettingsViewModel()
var body: some View {
VStack(spacing: 16) {
Text("SettingsView")
.font(.title3)
Button(action: {
viewModel.navigationManager.selection.send(.account)
}) {
Text("Go to Account tab")
.font(.headline)
}
.buttonStyle(.borderedProminent)
}
}
}
To wrap things up, I inject NavigationManager in my ContentView and use the .onReceive(_:action:) modifier to keep track of the newly selected tab from anywhere in code.
struct ContentView: View {
#Injection(\.navigationManager) var navigationManager
#State var selection: NavigationItem = .account
var body: some View {
TabView(selection: $selection) {
AccountView()
.tabItem {
Text("Account")
Image(systemName: "person.crop.circle") }
.tag(NavigationItem.account)
SettingsView()
.tabItem {
Text("Settings")
Image(systemName: "gear") }
.tag(NavigationItem.settings)
}
.onReceive(navigationManager.selection) { newValue in
selection = newValue
}
}
}

Trouble getting EnvironmentObject to update the UI

I originally posted another question asking this in the context of a project I was trying to develop, but I can't even get it to work in a vacuum so I figured I'd start with the basics. As the title suggests, my EnvironmentObjects don't update the UI as they should; in the following code, the user enters text on the ContentView and should be able to see that text in the next screen SecondView.
EDITED:
import SwiftUI
class NameClass: ObservableObject {
#Published var name = ""
}
struct ContentView: View {
#StateObject var myName = NameClass()
var body: some View {
NavigationView {
VStack {
TextField("Type", text: $myName.name)
NavigationLink(destination: SecondView()) {
Text("View2")
}
}
}.environmentObject(myName)
}
}
struct SecondView: View {
#EnvironmentObject var myself: NameClass
var body: some View {
Text("\(myself.name)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(NameClass())
}
}
However, the SecondView doesn't show the text that the user has written, but the default value of name (blank). What am I doing wrong here?
class NameClass: ObservableObject {
#Published var name = ""
}
struct ContentView: View {
#StateObject var myName = NameClass()
var body: some View {
NavigationView {
VStack {
TextField("Type", text: $myName.name)
NavigationLink(destination: SecondView()) {
Text("View2")
}
}
}.environmentObject(myName)
}
}
struct SecondView: View {
#EnvironmentObject var myself: NameClass
var body: some View {
Text("\(myself.name)")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(NameClass())
}
}

Difference between toggle() and Toggle

I am learning Modals in SwiftUI and the code is below:
ContentView.swift:
import SwiftUI
struct ContentView: View {
#State private var showingAddUser = false
var body: some View {
return VStack {
Text("Modal View")
}.onTapGesture {
self.showingAddUser.toggle()
print(self.showingAddUser) //for console
}
.sheet(isPresented: $showingAddUser) {
Addview(isPresented: self.$showingAddUser)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AddView.swift:
import SwiftUI
struct Addview: View {
#Binding var isPresented: Bool
var body: some View {
Button("Dismiss") {
self.isPresented = false
}
}
}
struct Addview_Previews: PreviewProvider {
static var previews: some View {
Addview(isPresented: .constant(false))
}
}
When I try to run the code for the first time and check the print output in console, boolean value changes to true however if I initialise #State variable showingAddUser with true the console output is unchanged that is it remains true. Should't toggle() flip the boolean value to false?
Is this toggle() different from Toggle switch from a concept point of view?
The toggle() is a mutating function on value type Bool. If you set the initial value of showingAddUser as true it will display the AddUser View when launched initially and it's not if set to false, that's the difference.
Toggle is a SwiftUI View. It can be used as any other View in SwiftUI body, like this:
struct ContentView: View {
#State var bool: Bool
var body: some View {
Toggle(isOn: $bool) {
Text("Hello world!")
}
}
}
There is no need for isPresented Boolean in Add View
Try This
ContentView.swift
import SwiftUI
struct ContentView: View {
#State private var showingAddUser = false
var body: some View {
return VStack {
Text("Modal View")
}.onTapGesture {
self.showingAddUser = true
print(self.showingAddUser) //for console
}
.sheet(isPresented: $showingAddUser) {
Addview()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AddView.swift
import SwiftUI
struct AddView: View {
var body: some View {
Button(action:
// Do Your Things
) {
Text("MyButton")
}
}
}
struct Addview_Previews: PreviewProvider {
static var previews: some View {
Addview()
}

How to set an Environment Object in preview

My view needs an environment object which is set in the SceneDelegate by adding it to the window.rootViewController. How can I set an environment object to be used for the preview?
You add it using .environmentObject(_:) modifier:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(YourObject())
}
}
This userData property gets its value automatically, as long as the environmentObject(_:) modifier has been applied to a parent.
struct UserList: View {
#EnvironmentObject var userData: UserData
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Users Fav only")
}
ForEach(landmarkData) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
UserRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Users"))
}
}
}
struct UserList_Previews: PreviewProvider {
static var previews: some View {
UserList()
.environmentObject(UserData())
}
}