I am trying to create a Recpipe object (CoreData entity) and then pass that object to a ChildView after it's created. What's the best way to do this?
My first attempt was to create a #StateObject in the MainView and then pass that to ChildViews as an #ObservedObject, but this seems to only work great for objects that already exist. The #StateObject is a 'get only' property so I can't modify it based on the function return. Perhaps I don't really want to create a #StateObject in this circumstance?
struct MainView: View {
#State private var presentChildView: Bool = false
#StateObject private var recipe: Recipe = Recipe()
var body: some View {
VStack {
NavigationLink(destination: ChildView(recipe: recipe), isActive: $presentChildView) {
EmptyView()
}
Button("Action", action: {
recipe = functionThatReturnsRecipe()
presentChildView = true
})
}
}
}
struct ChildView: View {
#ObservedObject private var recipe: Recipe
var body: some View {
Text(recipe.title)
}
}
Normally you hold the object in #State or even better inside a struct to hold all the ChildView's related vars so it can be tested independently, e.g.
struct ChildViewConfig {
var recipe: Recipe?
var isPresented = false
mutating func present(viewContext: NSManagedObjectContext) {
recipe = Recipe(context: viewContext)
isPresented = true
}
// might have other save or dismiss mutating funcs here.
}
struct MainView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var config = ChildViewConfig()
var body: some View {
VStack {
Button("Present") {
config.present(context: viewContext)
}
}
.sheet(isPresented: $config.isPresented, onDismiss: nil) {
ChildView(recipe: config.recipe!)
}
}
}
struct ChildView: View {
#ObservedObject private var recipe: Recipe
var body: some View {
Text(recipe.title)
}
}
This answer to another question uses the more advanced .sheet(item:onDismiss) that you may prefer because is designed for this use case of editing an item and allows for the use of a child context scratch pad because when setting the item to nil means the item.childContext will be discarded if the editing in the sheet is cancelled.
Learn more about this #State Config struct pattern at 4:18 in Data Essentials in SwiftUI (WWDC 2020)
You can have instead some concept of app state or manager (anything represented business logic) which would hold/update your recipes, like
class AppState: ObservableObject {
#Published var recipe: Recipe = Recipe()
func refreshRecipe() {
self.recipe = functionThatReturnsRecipe() // << now works !!
}
private func functionThatReturnsRecipe() -> Recipe {
Recipe()
}
}
struct MainView: View {
#State private var presentChildView: Bool = false
#StateObject private var appState = AppState() // << non-changeable
var body: some View {
VStack {
NavigationLink(destination: ChildView(recipe: appState.recipe), isActive: $presentChildView) {
EmptyView()
}
Button("Action", action: {
appState.refreshRecipe()
presentChildView = true
})
}
}
}
I'm having trouble wrapping my head around how to use Combine in SwiftUI. I'm accustomed to using key-value observation in AppKit an UIKit because view controllers don't need to know about each other and can just react to some global objects that help determine state.
For example, in an AppKit/UIKit app, I would create a global state object like this:
//Global State file
#objc class AppState: NSObject {
#objc dynamic var project: Project?
}
//Create an instance I can access anywhere in my app
let app = AppState()
Then in a view controller, I can get notified of any changes to my app-wide project instance and react accordingly:
//View Controller
class MyViewController: NSViewController{
var observerProject: NSKeyValueObservation?
override func viewDidLoad() {
observerProject = app.observe(\.project) { object, change in
self.refreshData()
}
}
func refreshData(){
//Query my persistent store and update my UI
}
}
What is the Combine/SwiftUI analog to this?
Do I need to create a Publisher and then listen to my global object changes? If so, how do I make my Core Data #FetchRequest (whose predicate includes my global Project object) respond in real-time?
I've done things the old way for so long that this transition to SwiftUI/Combine is rather confusing to me. 🙂
#FetchRequest doesn't work well with a dynamic predicate (There are some workarounds in SO) you will have to use the "old school" NSFetchedResultsController for that and put it into an ObservableObject. Here is a video with a sample. It is a lot of setup and code.
import SwiftUI
import Combine
class AppState: ObservableObject {
static let shared = AppState()
#Published var project: String?
//Just to mimic updates
var count = 0
private init() {
//Mimic getting updates to project
Timer.scheduledTimer(withTimeInterval: 2, repeats: true){ timer in
print("Timer")
self.project = self.count.description
self.count += 1
if self.count >= 15{
timer.invalidate()
}
}
}
}
class MyViewModel: ObservableObject{
#Published var refreshedData: String = "init"
let appState: AppState = AppState.shared
var projectCancellable: AnyCancellable?
init() {
projectCancellable = appState.$project.sink(receiveValue: {
value in
print(value ?? "nil")
self.refreshData()
})
}
func refreshData() {
refreshedData = Int.random(in: 0...100).description
}
}
struct MyView: View {
#StateObject var vm: MyViewModel = MyViewModel()
var body: some View {
VStack{
Text(vm.refreshedData)
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}
I have a view class showing list of items coming from ViewModel class, in picker. Initial state of this picker is first element from the array of objects of the viewModel class.
On selection of item from picker, I want to do different actions in that view - 1. send the object info to different screen on button click. 2. display information with respected to selected object from picker.
import SwiftUI
import Combine
struct SetConfiguration: View {
#ObservedObject var profileListVM : ProfilesListViewModel = ProfilesListViewModel()
#State private var selectedConfiguration = 0 ///show "Add" as initial state
var body: some View {
HStack {
Text("Configuration:")
Picker(selection: $selectedConfiguration.onChange(connectToConfiguration), label: EmptyView()) {
ForEach(profileListVM.profiles, id: \.self) {
choice in
Text(choice.name).tag(choice)
}
}
Text (“Selcted item is: \(self. selectedconfiguration.name)”)
Button(action: {
}) {
Text("Edit")
}.sheet(isPresented: $showEditConfig) {
EditConfigurationView()
// TODO pass selectedConfiguration as profile object
}
}
}
viewModel class:
class ProfilesListViewModel: ObservableObject {
#Published var profiles: [ProfileViewModel] = [ProfileViewModel]()
static var addNewProfile = ProfileViewModel(name: "Add Configuration")
init() {
fetchAllProfiles()
}
func fetchAllProfiles() {
profiles.append(ProfilesListViewModel.addNewProfile) ///Add is first object
self.profiles = CoreDataManager.shared.getConfigurations().map(ProfileViewModel.init) /// fetch all profile objects
}
}
I believe this is the context for your question. Here is the working example:
// MARK: MOCKS FOR MODELS
struct ProfileViewModel: Hashable {
let id = UUID()
let name: String
}
class CoreDataManager {
static let shared = CoreDataManager()
func getConfigurations() -> [ProfileViewModel] {
return [ProfileViewModel(name: "first"), ProfileViewModel(name: "second"), ProfileViewModel(name: "third")]
}
}
// MARK: changed class because it's not even working because of lack of code
class ProfilesListViewModel: ObservableObject {
#Published var profiles: [ProfileViewModel] = [ProfileViewModel]()
static var addNewProfile = ProfileViewModel(name: "Add Configuration")
init() {
fetchAllProfiles()
}
func fetchAllProfiles() {
print("fetched")
profiles.append(ProfilesListViewModel.addNewProfile) ///Add is first object
self.profiles = CoreDataManager.shared.getConfigurations()
}
}
// MARK: the solution
struct SetConfiguration: View {
#ObservedObject var profileListVM: ProfilesListViewModel = ProfilesListViewModel()
#State private var selectedConfiguration = 0 ///show "Add" as initial state
#State private var choosedConfiguration = 0
var body: some View {
VStack {
HStack {
Picker(selection: $selectedConfiguration.onChange(selectNewConfig), label: Text("Configuration")) {
ForEach(0 ..< self.profileListVM.profiles.count) { choice in
Text(self.profileListVM.profiles[choice].name).tag(choice)
}
}
}
Text("Selected item is: \(choosedConfiguration)")
}
}
func selectNewConfig(_ newValue: Int) {
print(newValue)
withAnimation {
choosedConfiguration = newValue
}
}
}
Tips
To avoid misunderstandings in the future:
you should add all the working code and links, or simplify it to be clear what you want to achieve. Not every swift developer know about extension Binding, so they will just say: onChange will not ever work and they will be right;
format your code;
add some examples of your models or remove/simplify them.
I believe, you don't need choosedConfiguration, you can do some experiments with this.
I have a swiftui view that depends on a view model, the view model has some published properties. I want define a protocol and default implementation for the view model hierarchy, and make the view dependent on the protocol not the concrete class?
I want to be able to write the following:
protocol ItemViewModel: ObservableObject {
#Published var title: String
func save()
func delete()
}
extension ItemViewModel {
#Published var title = "Some default Title"
func save() {
// some default behaviour
}
func delete() {
// some default behaviour
}
}
struct ItemView: View {
#ObservedObject var viewModel: ItemViewModel
var body: some View {
TextField($viewModel.title, text: "Item Title")
Button("Save") { self.viewModel.save() }
}
}
// What I have now is this:
class AbstractItemViewModel: ObservableObject {
#Published var title = "Some default Title"
func save() {
// some default behaviour
}
func delete() {
// some default behaviour
}
}
class TestItemViewModel: AbstractItemViewModel {
func delete() {
// some custom behaviour
}
}
struct ItemView: View {
#ObservedObject var viewModel: AbstractItemViewModel
var body: some View {
TextField($viewModel.title, text: "Item Title")
Button("Save") { self.viewModel.save() }
}
}
Wrappers and stored properties are not allowed in swift protocols and extensions, at least for now. So I would go with the following approach mixing protocols, generics and classes... (all compilable and tested with Xcode 11.2 / iOS 13.2)
// base model protocol
protocol ItemViewModel: ObservableObject {
var title: String { get set }
func save()
func delete()
}
// generic view based on protocol
struct ItemView<Model>: View where Model: ItemViewModel {
#ObservedObject var viewModel: Model
var body: some View {
VStack {
TextField("Item Title", text: $viewModel.title)
Button("Save") { self.viewModel.save() }
}
}
}
// extension with default implementations
extension ItemViewModel {
var title: String {
get { "Some default Title" }
set { }
}
func save() {
// some default behaviour
}
func delete() {
// some default behaviour
}
}
// concrete implementor
class SomeItemModel: ItemViewModel {
#Published var title: String
init(_ title: String) {
self.title = title
}
}
// testing view
struct TestItemView: View {
var body: some View {
ItemView(viewModel: SomeItemModel("test"))
}
}
backup
We have found a solution in our small library by writing a custom property wrapper. You can have a look at XUI.
There are essentially two issues at hand:
the associated type requirement in ObservableObject
the generic constraint on ObservedObject
By creating a similar protocol to ObservableObject (without associated type) and a protocol wrapper similar to ObservedObject (without the generic constraint), we can make this work!
Let me show you the protocol first:
protocol AnyObservableObject: AnyObject {
var objectWillChange: ObservableObjectPublisher { get }
}
That is essentially the default form of ObservableObject, which makes it quite easy for new and existing components to conform to that protocol.
Secondly, the property wrapper - it is a bit more complex, which is why I will simply add a link. It has a generic attribute without a constraint, which means that we can use it with protocols as well (simply a language restriction as of now). However, you will need to make sure to only use this type with objects conforming to AnyObservableObject. We call that property wrapper #Store.
Okay, now let's go through the process of creating and using a view model protocol:
Create view model protocol
protocol ItemViewModel: AnyObservableObject {
var title: String { get set }
func save()
func delete()
}
Create view model implementation
class MyItemViewModel: ItemViewModel, ObservableObject {
#Published var title = ""
func save() {}
func delete() {}
}
Use the #Store property wrapper in your view:
struct ListItemView: View {
#Store var viewModel: ListItemViewModel
var body: some View {
// ...
}
}
This post is similar to some others, but it's just the required template for a published variable without distractions.
protocol MyViewModel: ObservableObject {
var lastEntry: String { get }
}
class ActualViewModel: MyViewModel {
#Published private(set) var lastEntry: String = ""
}
struct MyView<ViewModel>: View where ViewModel: MyViewModel {
#ObservedObject var viewModel: ViewModel
var body: some View {
Text(viewModel.lastEntry)
}
}
The View's generic ViewModel: MyViewModel constraint lets the compiler know that it needs to build out logic for any type that use the MyViewModel protocol
I think type erasure is the best answer to this.
So, your protocol remains unchanged. You have:
protocol ItemViewModel: ObservableObject {
var title: String { get set }
func save()
func delete()
}
So we need a concrete type the view can always depend on (things can get crazy if too many views become generic on the view model). So we'll create a type erasing implementation.
class AnyItemViewModel: ItemViewModel {
var title: title: String { titleGetter() }
private let titleGetter: () -> String
private let saver: () -> Void
private let deleter: () -> Void
let objectWillChange: AnyPublisher<Void, Never>
init<ViewModel: ItemViewModel>(wrapping viewModel: ViewModel) {
self.objectWillChange = viewModel
.objectWillChange
.map { _ in () }
.eraseToAnyPublisher()
self.titleGetter = { viewModel.title }
self.saver = viewModel.save
self.deleter = viewModel.delete
}
func save() { saver() }
func delete() { deleter() }
}
For convenience, we can also add an extension to erase ItemViewModel with a nice trailing syntax:
extension ItemViewModel {
func eraseToAnyItemViewModel() -> AnyItemViewModel {
AnyItemViewModel(wrapping: self)
}
}
At this point your view can be:
struct ItemView: View {
#ObservedObject var viewModel: AnyItemViewModel
var body: some View {
TextField($viewModel.title, text: "Item Title")
Button("Save") { self.viewModel.save() }
}
}
You can create it like this (Great for previews):
ItemView(viewModel: DummyItemViewModel().eraseToAnyItemViewModel())
Technically, you can do the type erasing in the view initializer, but then you actually would have to write that initializer and it feels a little off to do that.
Ok I spent some time figuring these out, but once I got it right, everything makes sense.
At the moment it is not possible to use PropertyWrappers in protocols. But what you can do is use generics in your View and expect your ViewModels to comply to your protocol. This is specially great if you are testing things or you need to setup something lightweight for the Preview.
I have some sample example here so you can get yours right
Protocol:
protocol UploadStoreProtocol:ObservableObject {
var uploads:[UploadModel] {get set}
}
ViewModel:
You want to make sure your view model is ObservableObject and add #Published to variables that can change
// For Preview
class SamplePreviewStore:UploadStoreProtocol {
#Published var uploads:[UploadModel] = []
init() {
uploads.append( UploadModel(id: "1", fileName: "Image 1", progress: 0, started: true, errorMessage: nil))
uploads.append( UploadModel(id: "2", fileName: "Image 2", progress: 47, started: true, errorMessage: nil))
uploads.append( UploadModel(id: "3", fileName: "Image 3", progress: 0, started: false, errorMessage: nil))
}
}
// Real Storage
class UploadStorage:UploadStoreProtocol {
#Published var uploads:[UploadModel] = []
init() {
uploads.append( UploadModel(id: "1", fileName: "Image 1", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "2", fileName: "Image 2", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "3", fileName: "Image 3", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "4", fileName: "Image 4", progress: 0, started: false, errorMessage: nil))
uploads.append( UploadModel(id: "5", fileName: "Image 5", progress: 0, started: false, errorMessage: nil))
}
func addItem(){
uploads.append( UploadModel(id: "\(Int.random(in: 100 ... 100000))", fileName: "Image XX", progress: 0, started: false, errorMessage: nil))
}
func removeItemAt(index:Int){
uploads.remove(at: index)
}
}
For the UI View you can use generics:
struct UploadView<ViewModel>: View where ViewModel:UploadStoreProtocol {
#ObservedObject var store:ViewModel
var body: some View {
List(store.uploads.indices){ item in
ImageRow(item: $store.uploads[item])
}.padding()
}
}
struct ImageRow: View {
#Binding var item:UploadModel
var body: some View {
HStack{
Image(item.id ?? "")
.resizable()
.frame(width: 50.0, height: 50.0)
VStack (alignment: .leading, spacing: nil, content: {
Text(item.fileName ?? "-")
Text(item.errorMessage ?? "")
.font(.caption)
.foregroundColor(.red)
})
Spacer()
VStack {
if (item.started){
Text("\(item.progress)").foregroundColor(.purple)
}
UploadButton(is_started: $item.started)
}
}
}
}
Now your view is ready to get the ViewModel, you can have your store setup externally like this:
#main
struct SampleApp: App {
#StateObject var uploadStore = UploadStorage()
var body: some Scene {
WindowGroup {
UploadView(store: uploadStore)
}
}
}
and for Preview you can have:
struct ContentView_Previews: PreviewProvider {
#StateObject static var uploadStore = SamplePreviewStore()
static var previews: some View {
UploadView(store: uploadStore)
}
}
I am not sure how to use #property wrapper in a protocol. Except that, normal swift rule applies.
protocol ItemViewModel: ObservableObject {
var title: String{get set}
func save()
func delete()
}
extension ItemViewModel {
//var title = "Some default Title"
func save() {
// some default behaviour
title = "save in protocol"
print("save in protocol")
}
func delete() {
// some default behaviour
print("delete in protocol")
}
}
// What I have now is this:
class AbstractItemViewModel: ItemViewModel{
#Published var title = "Some default Title"
// func save() {
// print("save in class")
// // some default behaviour
// }
//
// func delete() {
// print("delete in class")
// // some default behaviour
// }
}
class TestItemViewModel: AbstractItemViewModel {
func delete() {
// some custom behaviour
title = "delete in"
print("delete in ")
}
}
struct ItemView: View {
#ObservedObject var viewModel: TestItemViewModel
var body: some View {
VStack{
Button(action: { self.viewModel.save()}){
Text("protocol save")
}
Button(action: { self.viewModel.delete()}){
Text("class delete")
}
TextField.init ("Item Title", text: $viewModel.title)}
}
}
What would be the best approach to map particular type to a Func of TResult? For example:
ViewModelBase GetScreen(Type type)
{
// mapping code here
}
ScreenA GetScreenA()
{
// returns new instance of ScreenA
}
// usage
var screen = GetScreen(typeof(ScreenA));
What I need to do here is to map ScreenA type to GetScreenA() method (strongly typed). Each screen inherits from ViewModelBase. What would be the best way to achieve this? I am not considering bunch of ifs as solution.
if (type = typeof(ScreenA))
return GetScreenA();
else if ....
You could use a a dictionary to map to the different actions instead of using if statements.
private Dictionary<Type , Func<ViewModelBase>> Method2ObjectMap
= new Dictionary<Type , Func<ViewModelBase>>
{
{ ScreenA, GetScreenA },
{ ScreenB, GetScreenB },
{ ScreenC, GetScreenC }
};
And then call it with something like:
if(Method2ObjectMap.ContainsKey(ScreenB))
{
return Method2ObjectMap[ScreenB];
}
Something like this will help you
class TypeA
{
}
class TypeB:TypeA
{
}
class TypeC : TypeA
{
}
private static Dictionary<Type, Func<TypeA>> ScreenMap =
new Dictionary<Type, Func<TypeA>>
{
{typeof(TypeA),()=> new TypeA() },
{typeof(TypeB) ,()=> new TypeB() },
{typeof(TypeC),()=> new TypeC() }
};
And to use it
TypeA a= ScreenMap[typeof(TypeA)]();