Attributed Strings don't seem to retain their attributes when created in the ViewModel and passed to a SwiftUI View. Is there a way to make #Published strings with bold/italics/etc in a ViewModel that retain their attributes in the View?
The following correctly applies bold styling
struct ContentView: View {
var body: some View {
Text( "**This** should be bold")
}
}
However, when using a ViewModel it produces: **This** should be bold
final class ViewModel: ObservableObject {
#Published var attributedString = "**This** should be bold"
}
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
var body: some View {
Text(viewModel.attributedString)
}
}
I've also tried setting the type as AttributedString, however that doesn't seem to make a difference.
You can use AttributedString(markdown: ) but as it throws, you have to use it e.g.in the init.
final class ViewModel: ObservableObject {
#Published var attributedString: AttributedString
init() {
self.attributedString = ""
try? attributedString = AttributedString(markdown: "**This** should be bold")
}
}
Related
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 trying to use a ViewModel between the ContentView and Core Data in SwiftUI. Xcode builder runs the App but I get an immediate error: Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) for var recList.
Can anyone help?
Following a simple example of what I'm doing:
ListViewModel:
class ListViewModel: ObservableObject {
var recRequest: FetchRequest<Newdb>
var recList: FetchedResults<Newdb>{recRequest.wrappedValue} <-------- error appears here
#Published var records = [ViewModel]()
init() {
self.recRequest = FetchRequest(entity: Newdb.entity(), sortDescriptors: [])
fetchEntries()
}
func fetchEntries() {
self.records = recList.map(ViewModel.init)
}
}
ViewModel:
class ViewModel {
var name: String = ""
init(db: Newdb) {
self.name = db.name!
}
}
ContentView:
struct ContentView: View {
#ObservedObject var listViewModel: ListViewModel
init() {
self.listViewModel = ListViewModel()
}
var body: some View {
ForEach(listViewModel.records, id: \.name) { index in
Text(index.name)
}
}
}
two things I noticed; your ListViewModel is an ObservableObject but you do not have any #Published var ...
Also when creating a class such as ListViewModel you cannot use "recRequest" as you do in recList, because it is not created yet. It is created in the init() method not before.
Do your "recList = FetchedResults{recRequest.wrappedValue}" somewhere else, like in the fetchEntries().
From what I can tell, FetchRequest is a property wrapper.
It is supposed to wrap something, e.g.;
#FetchRequest(
entity: User.entity(),
sortDescriptors: []
) var users: FetchedResults<User> // users are 'wrapped' in a FetchRequest instance
It makes sense that wrappedValue is nil because there's nothing to be wrapped in
self.recRequest = FetchRequest(entity: Newdb.entity(), sortDescriptors: [])
You might want to double-check its usage.
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'm trying to make an edit form that can take a value as a #Binding, edit it, and commit the change. In this case, I'm populating a list with Core Data records using the #FetchRequest property wrapper. I want to tap on a row to navigate from the List to a Detail view, then on the Detail view I want to navigate to the Edit view.
I tried doing this without the #Binding and the code will compile but when I make an edit, it is not reflected on the previous screens. It seems like I need to use #Binding but I can't figure out a way to get a NSManagedObject instance inside of a List or ForEach, and pass it to a view that can use it as a #Binding.
List View
struct TimelineListView: View {
#Environment(\.managedObjectContext) var managedObjectContext
// The Timeline class has an `allTimelinesFetchRequest` function that can be used here
#FetchRequest(fetchRequest: Timeline.allTimelinesFetchRequest()) var timelines: FetchedResults<Timeline>
#State var openAddModalSheet = false
var body: some View {
return NavigationView {
VStack {
List {
Section(header:
Text("Lists")
) {
ForEach(self.timelines) { timeline in
// ✳️ How to I use the timeline in this list as a #Binding?
NavigationLink(destination: TimelineDetailView(timeline: $timeline)) {
TimelineCell(timeline: timeline)
}
}
}
.font(.headline)
}
.listStyle(GroupedListStyle())
}
.navigationBarTitle(Text("Lists"), displayMode: .inline)
}
} // End Body
}
Detail View
struct TimelineDetailView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#Binding var timeline: Timeline
var body: some View {
List {
Section {
NavigationLink(destination: TimelineEditView(timeline: $timeline)) {
TimelineCell(timeline: timeline)
}
}
Section {
Text("Event data here")
Text("Another event here")
Text("A third event here")
}
}.listStyle(GroupedListStyle())
}
}
Edit View
struct TimelineEditView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var newDataValue = ""
#Binding var timeline: Timeline
var body: some View {
return VStack {
TextField("Data to edit", text: self.$newDataValue)
.shadow(color: .secondary, radius: 1, x: 0, y: 0)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear {
self.newDataValue = self.timeline.name ?? ""
}.padding()
Spacer()
}
.navigationBarItems(
leading:
Button(action: ({
// Dismiss the modal sheet
self.newDataValue = ""
self.presentationMode.wrappedValue.dismiss()
})) {
Text("Cancel")
},
trailing: Button(action: ({
self.timeline.name = self.newDataValue
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
// Dismiss the modal sheet
self.newDataValue = ""
self.presentationMode.wrappedValue.dismiss()
})) {
Text("Done")
}
)
}
}
I should mention, the only reason I'm even trying to do this is because the modal .sheet() stuff is super buggy.
To implement creation and editing functionality with Core Data it is best to use nested managed object contexts. If we inject a child managed object context, derived from the main view context, as well as the managed object being created or edited that is associated with a child context, we get a safe space where we can make changes and discard them if needed without altering the context that drives our UI.
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = viewContext
let childItem = childContext.object(with: objectID) as! Item
return ItemHost(item: childItem)
.environment(\.managedObjectContext, childContext)
Once we are done with our changes, we just need to save the child context and the changes will be pushed up to the main view context and can be saved right away or later, depending on your architecture. If we are unhappy with our changes we can discard them by calling rollback() on our child context.
childContext.rollback()
Regarding the question of binding managed objects with SwiftUI views, once we have our child object injected into our edit form, we can bind its properties directly to SwiftUI controls. This is possible since NSManagedObject class conforms to ObservableObject protocol. All we have to do is mark a property that holds a reference to our child object with #ObservedObject and we get its bindings. The only complication here is that there are often type mismatches. For example, managed objects store strings as String?, but TextField expects String. To go around that we can use Binding’s initializer init?(_ base: Binding<Value?>).
We can now use our bindings, provided that the name attribute has a default empty string set in the managed object model, or else this will crash.
TextField("Title", text: Binding($item.title)!)
This way we can cleanly implement the SwiftUI philosophy of no shared state. I have provided a sample project for further reference.
#Binding only works with structs.
But CoreData result are Objects (NSManagedObject adopting ObservableObject). You need to use #ObservedObject to register for changes.
When using #Published property wrapper following current SwiftUI syntax, it seems very hard to define a protocol that includes a property with #Published, or I definitely need help :)
As I'm implementing dependency injection between a View and it's ViewModel, I need to define a ViewModelProtocol so to inject mock data to preview easily.
This is what I first tried,
protocol PersonViewModelProtocol {
#Published var person: Person
}
I get "Property 'person' declared inside a protocol cannot have a wrapper".
Then I tried this,
protocol PersonViewModelProtocol {
var $person: Published
}
Obviously didn't work because '$' is reserved.
I'm hoping a way to put a protocol between View and it's ViewModel and also leveraging the elegant #Published syntax. Thanks a lot.
You have to be explicit and describe all synthetized properties:
protocol WelcomeViewModel {
var person: Person { get }
var personPublished: Published<Person> { get }
var personPublisher: Published<Person>.Publisher { get }
}
class ViewModel: ObservableObject {
#Published var person: Person = Person()
var personPublished: Published<Person> { _person }
var personPublisher: Published<Person>.Publisher { $person }
}
My MVVM approach:
// MARK: View
struct ContentView<ViewModel: ContentViewModel>: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text(viewModel.name)
TextField("", text: $viewModel.name)
.border(Color.black)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: ContentViewModelMock())
}
}
// MARK: View model
protocol ContentViewModel: ObservableObject {
var name: String { get set }
}
final class ContentViewModelImpl: ContentViewModel {
#Published var name = ""
}
final class ContentViewModelMock: ContentViewModel {
var name: String = "Test"
}
How it works:
ViewModel protocol inherits ObservableObject, so View will subscribe to ViewModel changes
property name has getter and setter, so we can use it as Binding
when View changes name property (via TextField) then View is notified about changes via #Published property in ViewModel (and UI is updated)
create View with either real implementation or mock depending on your needs
Possible downside: View has to be generic.
A workaround my coworker came up with is to use a base class that declares the property wrappers, then inherit it in the protocol. It still requires inheriting it in your class that conforms to the protocol as well, but looks clean and works nicely.
class MyPublishedProperties {
#Published var publishedProperty = "Hello"
}
protocol MyProtocol: MyPublishedProperties {
func changePublishedPropertyValue(newValue: String)
}
class MyClass: MyPublishedProperties, MyProtocol {
changePublishedPropertyValue(newValue: String) {
publishedProperty = newValue
}
}
Then in implementation:
class MyViewModel {
let myClass = MyClass()
myClass.$publishedProperty.sink { string in
print(string)
}
myClass.changePublishedPropertyValue("World")
}
// prints:
// "Hello"
// "World"
This is how I suppose it should be done:
public protocol MyProtocol {
var _person: Published<Person> { get set }
}
class MyClass: MyProtocol, ObservableObject {
#Published var person: Person
public init(person: Published<Person>) {
self._person = person
}
}
Although the compiler seems to sort of like it (the "type" part at least), there is a mismatch in the property's access control between the class and the protocol (https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html). I tried different combinations: private, public, internal, fileprivate. But none worked. Might be a bug? Or missing functionality?
Until 5.2 we don't have support for property wrapper. So it's necessary expose manually the publisher property.
protocol PersonViewModelProtocol {
var personPublisher: Published<Person>.Publisher { get }
}
class ConcretePersonViewModelProtocol: PersonViewModelProtocol {
#Published private var person: Person
// Exposing manually the person publisher
var personPublisher: Published<Person>.Publisher { $person }
init(person: Person) {
self.person = person
}
func changePersonName(name: String) {
person.name = name
}
}
final class PersonDetailViewController: UIViewController {
private let viewModel = ConcretePersonViewModelProtocol(person: Person(name: "Joao da Silva", age: 60))
private var cancellables: Set<AnyCancellable> = []
func bind() {
viewModel.personPublisher
.receive(on: DispatchQueue.main)
.sink { person in
print(person.name)
}
.store(in: &cancellables)
viewModel.changePersonName(name: "Joao dos Santos")
}
}
We've encountered this as well. As of Catalina beta7, there doesn't seem to be any workaround, so our solution is to add in a conformance via an extension like so:
struct IntView : View {
#Binding var intValue: Int
var body: some View {
Stepper("My Int!", value: $intValue)
}
}
protocol IntBindingContainer {
var intValue$: Binding<Int> { get }
}
extension IntView : IntBindingContainer {
var intValue$: Binding<Int> { $intValue }
}
While this is a bit of extra ceremony, we can then add in functionality to all the IntBindingContainer implementations like so:
extension IntBindingContainer {
/// Reset the contained integer to zero
func resetToZero() {
intValue$.wrappedValue = 0
}
}
I came up with a fairly clean workaround by creating a generic ObservableValue class that you can include in your protocols.
I am unsure if there are any major drawbacks to this, but it allows me to easily create mock/injectable implementations of my protocol while still allowing use of published properties.
import Combine
class ObservableValue<T> {
#Published var value: T
init(_ value: T) {
self.value = value
}
}
protocol MyProtocol {
var name: ObservableValue<String> { get }
var age: ObservableValue<Int> { get }
}
class MyImplementation: MyProtocol {
var name: ObservableValue<String> = .init("bob")
var age: ObservableValue<Int> = .init(29)
}
class MyViewModel {
let myThing: MyProtocol = MyImplementation()
func doSomething() {
let myCancellable = myThing.age.$value
.receive(on: DispatchQueue.main)
.sink { val in
print(val)
}
}
}
Try this
import Combine
import SwiftUI
// MARK: - View Model
final class MyViewModel: ObservableObject {
#Published private(set) var value: Int = 0
func increment() {
value += 1
}
}
extension MyViewModel: MyViewViewModel { }
// MARK: - View
protocol MyViewViewModel: ObservableObject {
var value: Int { get }
func increment()
}
struct MyView<ViewModel: MyViewViewModel>: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(viewModel.value)")
Button("Increment") {
self.viewModel.increment()
}
}
}
}
I succeeded in just requiring the plain variable, and by adding the #Published in the fulfilling class:
final class CustomListModel: IsSelectionListModel, ObservableObject {
#Published var list: [IsSelectionListEntry]
init() {
self.list = []
}
...
protocol IsSelectionListModel {
var list: [IsSelectionListEntry] { get }
...