I'm using the new NavigationLink in iOS 16 and hitting a problem where the target of the .navigationDestination is being called twice. Example code:
struct Content: View {
var body: some View {
NavigationStack {
ScrollView {
VStack {
ForEach(0..<10) { number in
NavigationLink(value: number) {
Text("\(number)")
}
}
}
}.navigationDestination(for: Int.self) { value in
SubView(model: Model(value: value))
}
}
}
}
class Model: ObservableObject {
#Published var value: Int
init(value: Int) {
print("Init for value \(value)")
self.value = value
}
}
struct SubView: View {
#ObservedObject var model: Model
var body: some View {
Text("\(model.value)")
}
}
When I touch one of the numbers in the Content view the init message in the model is shown twice, indicating that the class has been instantiated more than once. This is not a problem in a trivial example like this, but in my app the model does a network fetch and calculation so this is being done twice, which is more of an issue.
Any thoughts?
I think problem is caused by ObservableObject. It needs to store it's state. I think it's better to use StateObject. Please try that one for SubView. :)
struct SubView: View {
#StateObject private var model: Model
init(value: Int) {
self._model = StateObject(wrappedValue: Model(value: value))
}
var body: some View {
Text("\(model.value)")
}
}
Related
I have a question regarding the combination of SwiftUI and MVVM.
Before we start, I have read some posts discussing whether the combination of SwiftUI and MVVM is necessary. But I don't want to discuss this here, as it has been covered elsewhere. I just want to know if it is possible and, if yes, how. :)
So here comes the code. I tried to add the ViewModel Layer in between the updated Object class that contains a number that should be updated when a button is pressed. The problem is that as soon as I put the ViewModel Layer in between, the UI does not automatically update when the button is pressed.
View:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#ObservedObject var numberStorage = NumberStorage()
var body: some View {
VStack {
// Text("\(viewModel.getNumberObject().number)")
// .padding()
// Button("IncreaseNumber") {
// viewModel.increaseNumber()
// }
Text("\(numberStorage.getNumberObject().number)")
.padding()
Button("IncreaseNumber") {
numberStorage.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published var number: NumberStorage
init() {
self.number = NumberStorage()
}
func increaseNumber() {
self.number.increaseNumber()
}
func getNumberObject() -> NumberObject {
self.number.getNumberObject()
}
}
Model:
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func getNumberObject() -> NumberObject {
return self.numberObject
}
public func increaseNumber() {
self.numberObject.number+=1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
} ```
Looking forward to your feedback!
I think your code is breaking MVVM, as you're exposing to the view a storage model. In MVVM, your ViewModel should hold only two things:
Values that your view should display. These values should be automatically updated using a binding system (in your case, Combine)
Events that the view may produce (in your case, a button tap)
Having that in mind, your ViewModel should wrap, adapt and encapsulate your model. We don't want model changes to affect the view. This is a clean approach that does that:
View:
struct ContentView: View {
#StateObject // When the view creates the object, it must be a state object, or else it'll be recreated every time the view is recreated
private var viewModel = ViewModel()
var body: some View {
VStack {
Text("\(viewModel.currentNumber)") // We don't want to use functions here, as that will create a new object , as SwiftUI needs the same reference in order to keep track of changes
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published
private(set) var currentNumber: Int = 0 // Private set indicates this should only be mutated by the viewmodel
private let numberStorage = NumberStorage()
init() {
numberStorage.currentNumber
.map { $0.number }
.assign(to: &$currentNumber) // Here we're binding the current number on the storage to the published var that the view is listening to.`&$` basically assigns it to the publishers address
}
func increaseNumber() {
self.numberStorage.increaseNumber()
}
}
Model:
class NumberStorage {
private let currentNumberSubject = CurrentValueSubject<NumberObject, Never>(NumberObject())
var currentNumber: AnyPublisher<NumberObject, Never> {
currentNumberSubject.eraseToAnyPublisher()
}
func increaseNumber() {
let currentNumber = currentNumberSubject.value.number
currentNumberSubject.send(.init(number: currentNumber + 1))
}
}
struct NumberObject: Identifiable { // I'd not use this, just send and int directly
let id = UUID()
var number = 0
}
It's a known problem. Nested observable objects are not supported yet in SwiftUI. I don't think you need ViewModel+Model here since ViewModel seems to be enough.
To make this work you have to trigger objectWillChange of your viewModel manually when objectWillChange of your model is triggered:
class ViewModel: ObservableObject {
init() {
number.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: &cancellables)
}
}
You better listen to only the object you care not the whole observable class if it is not needed.
Plus:
Since instead of injecting, you initialize your viewModel in your view, you better use StateObject instead of ObservedObject. See the reference from Apple docs: Managing model data in your app
One way you could handle this is to observe the publishers in your Storage class and send the objectWillChange publisher when it changes. I have done this in personal projects by adding a class that all my view models inherit from which provides a nice interface and handles the Combine stuff like this:
Parent ViewModel
import Combine
class ViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
func publish<T>(on publisher: Published<T>.Publisher) {
publisher.sink { [weak self] _ in self?.objectWillChange.send() }
.store(in: &cancellables)
}
}
Specific ViewModel
class ContentViewModel: ViewModel {
private let numberStorage = NumberStorage()
var number: Int { numberStorage.numberObject.number }
override init() {
super.init()
publish(on: numberStorage.$numberObject)
}
func increaseNumber() {
numberStorage.increaseNumber()
}
}
View
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text("\(viewModel.number)")
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
Model/Storage
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func increaseNumber() {
self.numberObject.number += 1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
}
This results in the view re-rendering any time Storage.numberObject changes.
SwiftUI 4.0 introduces a new NavigationStack view.
Let's consider this simple structure.
struct Item: Identifiable, Hashable {
static let sample = [Item(), Item(), Item()]
let id = UUID()
}
When a NavigationLink is nested in another one, the navigation loses its animation and the backtracking takes directly to the root. Did I miss something, or is this a bug?
struct ItemDetailView: View {
let item: Item
var body: some View {
Text(item.id.uuidString)
}
}
struct ItemListView: View {
var body: some View {
List(Item.sample) { item in
NavigationLink(item.id.uuidString, value: item)
}
}
}
struct ExploreView: View {
var body: some View {
List {
Section {
NavigationLink {
ItemListView()
} label: {
Text("Items")
}
}
}
.navigationTitle("Explore")
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
}
struct ContentView: View {
var body: some View {
NavigationStack {
ExploreView()
}
}
}
Thanks!
Found the solution thanks to #Asperi's comment.
First, create a Hashable enum containing the destinations.
enum Destination: Hashable {
case items
var view: some View {
switch self {
case .items:
return ItemListView()
}
}
var title: LocalizedStringKey {
switch self {
case .items:
return "Items"
}
}
}
Next, use the new NavigationLink initializer.
NavigationLink(Destination.items.title, value: Destination.items)
And finally, add a new .navigationDestination modifier to catch all Destination values.
.navigationDestination(for: Destination.self) { destination in
destination.view
}
I just stuck trying to properly implement MVVM pattern in SwiftUI
I got such application
ContainerView is the most common view. It contains single business View Model object - ItemsViewModel and one surrogate - Bool to toggle it's value and force re-render of whole View.
ItemsView contains of array of business objects - ItemView
What I want is to figure out:
how to implement bindings which actually works❓
call back event and pass value from child view to parent❓
I came from React-Redux world, it done easy there. But for the several days I cant figure out what should I do... I chose MVVM though as I also got some WPF experience and thought it'll give some boost, there its done with ObservableCollection for bindable arrays
ContainerViewModel.swift⤵️
final class ContainerViewModel: ObservableObject {
#Published var items: ItemsViewModel;
// variable used to refresh most common view
#Published var refresh: Bool = false;
init() {
self.items = ItemsViewModel();
}
func buttonRefresh_onClick() -> Void {
self.refresh.toggle();
}
func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemViewModel())
}
}
ContainerView.swift⤵️
struct ContainerView: View {
// enshure that enviroment creates single View Model of ContainerViewModel with StateObject for ContainerView
#StateObject var viewModel: ContainerViewModel = ContainerViewModel();
var body: some View {
ItemsView(viewModel: $viewModel.items).padding()
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item from ContainerView")
}
Button(action: viewModel.buttonRefresh_onClick) {
Text("Refresh")
}.padding()
}
}
ItemsViewModel.swift⤵️
final class ItemsViewModel: ObservableObject {
#Published var items: [ItemViewModel] = [ItemViewModel]();
init() {
}
func buttonAddItem_onClick() -> Void {
self.items.append(ItemViewModel());
}
}
ItemsView.swift⤵️
struct ItemsView: View {
#Binding var viewModel: ItemsViewModel;
var body: some View {
Text("Items quantity: \(viewModel.items.count)")
ScrollView(.vertical) {
ForEach($viewModel.items) { item in
ItemView(viewModel: item).padding()
}
}
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item form ItemsView")
}
}
}
ItemViewModel.swift⤵️
final class ItemViewModel: ObservableObject, Identifiable, Equatable {
//implementation of Identifiable
#Published public var id: UUID = UUID.init();
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
// business property
#Published public var intProp: Int;
init() {
self.intProp = 0;
}
func buttonIncrementIntProp_onClick() -> Void {
self.intProp = self.intProp + 1;
}
func buttonDelete_onClick() -> Void {
//todo ❗ I want to delete item in parent component
}
}
ItemView.swift⤵️
struct ItemView: View {
#Binding var viewModel: ItemViewModel;
var body: some View {
HStack {
Text("int prop: \(viewModel.intProp)")
Button(action: viewModel.buttonIncrementIntProp_onClick) {
Image(systemName: "plus")
}
Button(action: viewModel.buttonDelete_onClick) {
Image(systemName: "trash")
}
}.padding().border(.gray)
}
}
I read official docs and countless SO topics and articles, but nowhere got solution for exact my case (or me doing something wrong). It only works if implement all UI part in single view
UPD 1:
Is it even possible to with class but not a struct in View Model?
Updates works perfectly if I use struct instead of class:
ItemsViewModel.swift⤵️
struct ItemsViewModel {
var items: [ItemViewModel] = [ItemViewModel]();
init() {
}
mutating func buttonAddItem_onClick() -> Void {
self.items.append(ItemViewModel());
}
}
ItemViewModel.swift⤵️
struct ItemViewModel: Identifiable, Equatable {
//implementation of Identifiable
public var id: UUID = UUID.init();
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
// business property
public var intProp: Int;
init() {
self.intProp = 0;
}
mutating func buttonIncrementIntProp_onClick() -> Void {
self.intProp = self.intProp + 1;
}
func buttonDelete_onClick() -> Void {
}
}
But is it ok to use mutating functions? I also tried to play with Combine and objectWillChange, but unable to make it work
UPD 2
Thanks #Yrb for response. With your suggestion and this article I came added Model structures and ended up with such results:
ContainerView.swift⤵️
struct ContainerView: View {
// enshure that enviroment creates single View Model of ContainerViewModel with StateObject for ContainerView
#StateObject var viewModel: ContainerViewModel = ContainerViewModel();
var body: some View {
ItemsView(viewModel: ItemsViewModel(viewModel.items)).padding()
Button(action: viewModel.buttonAddItem_onClick) {
Text("Add item from ContainerView")
}
}
}
ContainerViewModel.swift⤵️
final class ContainerViewModel: ObservableObject {
#Published var items: ItemsModel;
init() {
self.items = ItemsModel();
}
#MainActor func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemModel())
}
}
ContainerModel.swift⤵️
struct ContainerModel {
public var items: ItemsModel;
}
ItemsView.swift⤵️
struct ItemsView: View {
#ObservedObject var viewModel: ItemsViewModel;
var body: some View {
Text("Items quantity: \(viewModel.items.items.count)")
ScrollView(.vertical) {
ForEach(viewModel.items.items) { item in
ItemView(viewModel: ItemViewModel(item)).padding()
}
}
Button(action: {
viewModel.buttonAddItem_onClick()
}) {
Text("Add item form ItemsView")
}
}
}
ItemsViewModel.swift⤵️
final class ItemsViewModel: ObservableObject {
#Published var items: ItemsModel;
init(_ items: ItemsModel) {
self.items = items;
}
#MainActor func buttonAddItem_onClick() -> Void {
self.items.items.append(ItemModel());
}
}
ItemsModel.swift⤵️
struct ItemsModel {
public var items: [ItemModel] = [ItemModel]();
}
ItemView.swift⤵️
struct ItemView: View {
#StateObject var viewModel: ItemViewModel;
var body: some View {
HStack {
Text("int prop: \(viewModel.item.intProp)")
Button(action: {
viewModel.buttonIncrementIntProp_onClick()
}) {
Image(systemName: "plus")
}
Button(action: {
viewModel.buttonDelete_onClick()
}) {
Image(systemName: "trash")
}
}.padding().border(.gray)
}
}
ItemViewModel.swift⤵️
final class ItemViewModel: ObservableObject, Identifiable, Equatable {
//implementation of Identifiable
#Published private(set) var item: ItemModel;
// implementation of Equatable
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
return lhs.id == rhs.id;
}
init(_ item: ItemModel) {
self.item = item;
self.item.intProp = 0;
}
#MainActor func buttonIncrementIntProp_onClick() -> Void {
self.item.intProp = self.item.intProp + 1;
}
#MainActor func buttonDelete_onClick() -> Void {
}
}
ItemModel.swift⤵️
struct ItemModel: Identifiable {
//implementation of Identifiable
public var id: UUID = UUID.init();
// business property
public var intProp: Int;
init() {
self.intProp = 0;
}
}
This code runs and works perfectly, at least I see no problems. But I'm not shure if I properly initializes and "bind" ViewModels and Models - code looks quite messy. Also I'n not shure I correctly set ObservedObject in ItemsView and StateObject in ItemView. So please check me
We don't need MVVM in SwiftUI, see: "MVVM has no place in SwiftUI."
In SwiftUI, the View struct is already the view model. We use property wrappers like #State and #Binding to make it behave like an object. If you were to actually use objects instead, then you'll face all the bugs and inconsistencies of objects that SwiftUI and its use of value types was designed to eliminate.
I recommend Data Essentials in SwiftUI WWDC 2020 for learning SwiftUI. The first half is about view data and the second half is about model data. It takes a few watches to understand it. Pay attention to the part about model your data with value type and manage its life cycle with a reference type.
It's best to also use structs for your model types. Start with one single ObservableObject to hold the model types (usually arrays of structs) as #Published properties. Use environmentObject to pass the object into the View hierarchy. Use #Binding to pass write access to the structs in the object. Apple's sample ScrumDinger is a good starting point.
I have a problem with Navigation View hierarchy.
All screens in my app use the same ViewModel.
When a screen inside navigation link updates the ViewModel (here it is called DataManager), the navigation view automatically goes back to the first screen, as if the "Back" button was pressed.
Here's what it looks like
I tried to shrink my code as much as I could
struct DataModel: Identifiable, Codable {
var name: String
var isPinned: Bool = false
var id: String = UUID().uuidString
}
class DataManager: ObservableObject {
#Published private(set) var allModules: [DataModel]
var pinnedModules: [DataModel] {
allModules.filter { $0.isPinned }
}
var otherModules: [DataModel] {
allModules.filter { !$0.isPinned }
}
func pinModule(id: String) {
if let moduleIndex = allModules.firstIndex(where: { $0.id == id }) {
allModules[moduleIndex].isPinned = true
}
}
func unpinModule(id: String) {
if let moduleIndex = allModules.firstIndex(where: { $0.id == id }) {
allModules[moduleIndex].isPinned = false
}
}
static let instance = DataManager()
fileprivate init() {
allModules =
[DataModel(name: "One"),
DataModel(name: "Two"),
DataModel(name: "Three"),
DataModel(name: "Four"),
DataModel(name: "Five")]
}
}
struct ModulesList: View {
#StateObject private var dataStorage = DataManager.instance
var body: some View {
NavigationView {
List {
Section("Pinned") {
ForEach(dataStorage.pinnedModules) { module in
ModulesListCell(module: module)
}
}
Section("Other") {
ForEach(dataStorage.otherModules) { module in
ModulesListCell(module: module)
}
}
}
}
}
fileprivate struct ModulesListCell: View {
let module: DataModel
var body: some View {
NavigationLink {
SingleModuleScreen(module: module)
} label: {
Text(module.name)
}
}
}
}
struct SingleModuleScreen: View {
#State var module: DataModel
#StateObject var dataStorage = DataManager.instance
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(module.name)
.font(.title)
Button {
dataStorage.pinModule(id: module.id)
} label: {
Text("Pin")
}
}
}
}
}
I can guess because when your dataStorage changed, the ModulesList will be redrawn, that cause all current ModulesListCell removed from memory.
Your cells are NavigationLink and when it destroyed, the navigation stack doesn't keep the screen that's being linked.
I would recommend to watch this wwdc https://developer.apple.com/videos/play/wwdc2021/10022/ and you will know how to manage your view identity properly when your data's changed.
When you tap Pin, your otherModules array is recreated and you do not have the view in Navigation Stack from where you navigated. Thus you are going back automatically, which is desired behaviour. So the solution is Don't destroy your array from where your NavigationLink is created. Make a temporary published array, load Other modules from that array and change the array onAppear like below:
As a workaround which is working in my end:
Add this line in DataManger:
#Published var tempOtherModules:[DataModel] = []
Change your ModulesList like below
struct ModulesList: View {
#StateObject private var dataStorage = DataManager.instance
var body: some View {
NavigationView {
List {
Section("Pinned") {
ForEach(dataStorage.pinnedModules) { module in
ModulesListCell(module: module)
}
}
Section("Other") {
ForEach(dataStorage.tempOtherModules) { module in
ModulesListCell(module: module)
}
}
}.onAppear {
dataStorage.tempOtherModules = dataStorage.otherModules
}
}
}
}
I have already read this thread
SwiftUI - Button - How to pass a function (with parameters) request to parent from child
however after the original poster edited his own answer he proposed a way that didn't match his own question.
Unfortunately I have not yet reached enough points to post comments in this thread
This is the code example from the post above repeated to explain the problem:
struct ChildView: View {
var function: () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text("Button")
})
}
}
struct ContentView: View {
var body: some View {
ChildView(function: { self.setViewBackToNil() })
}
func setViewBackToNil() {
print("I am the parent")
}
}
And now I want to add a String parameter to setViewBackToNil(myStringParameter: String)
Here is possible solution. Tested with Xcode 11.4 / iOS 13.4
struct ChildView: View {
var function: (String) -> Void
#State private var value = "Child Value"
var body: some View {
Button(action: {
self.function(self.value)
}, label: {
Text("Button")
})
}
}
struct ContentView: View {
var body: some View {
ChildView { self.setViewBackToNil(myStringParameter: $0) }
}
func setViewBackToNil(myStringParameter: String) {
print("I am the parent: \(myStringParameter)")
}
}