Share Data Binding Between Class and Struct View - swift

For example, when I get data from API success or unsuccess I would like to show Alert but Alert there need isPresented type is Binding that's why I want to use concept Share Data as Binding between class and View is possible or have another way to do please help thanks advance.

#Published properties work well for sharing data between ObservableObjects and Views.
For example:
class ViewModel : ObservableObject {
struct APIError : Identifiable {
var id = UUID()
var message : String
}
#Published var error : APIError?
func apiCall() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.error = APIError(message: "Error message")
}
}
}
struct ContentView : View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Text("Hello, world!")
.alert(item: $viewModel.error) { item in
Alert(title: Text(item.message))
}
}
.onAppear {
viewModel.apiCall()
}
}
}
You could also do this with a custom Binding, but it's a little messier. The above example would definitely be my go-to.
class ViewModel : ObservableObject {
#Published var errorMessage : String?
var alertBinding : Binding<Bool> {
.init {
self.errorMessage != nil
} set: { newValue in
if !newValue { self.errorMessage = nil }
}
}
func apiCall() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.errorMessage = "Error!"
}
}
}
struct ContentView : View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Text("Hello, world!")
.alert(isPresented: viewModel.alertBinding) {
Alert(title: Text(viewModel.errorMessage ?? "(unknown)"))
}
}
.onAppear {
viewModel.apiCall()
}
}
}

Related

How do I get a textfield to display attributes of a core data entity?

I have a list of fruits. the struct FruitRowView provides the layout for the view of each row. In this FruitRowView, there's a TextField which I want to display the name of each fruit. I am having trouble doing this. The reason why I want to use a TextField to display the name of each fruit rather than a Text is so that users can easily edit the name of the fruit right from that TextField. In this case, fruits are the Core Data entity and the fruit name is an attribute of this entity.
Here is my core data class:
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntities: [FruitEntity] = []
init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error with coreData. \(error)")
}
}
fetchFruits()
}
func fetchFruits() {
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
do {
savedEntities = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching. \(error)")
}
}
func addFruit(text: String) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
saveData()
}
func saveData() {
do {
try container.viewContext.save()
fetchFruits()
} catch let error {
print("Error saving. \(error)")
}
}
}
Here is my contentView:
struct ContentView: View {
//sheet variable
#State var showSheet: Bool = false
#StateObject var vm = CoreDataViewModel()
#State var refresh: Bool
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button(action: {
showSheet.toggle()
}, label: {
Text("Add Fruit")
})
List {
ForEach(vm.savedEntities) { fruit in
FruitRowView(vm: vm, fruit: fruit)
}
}
}
.navigationTitle("Fruits")
.sheet(isPresented: $showSheet, content: {
SecondScreen(refresh: $refresh, vm: vm)
})
}
}
}
Here is my popup screen (used to create a new fruit)
struct SecondScreen: View {
#Binding var refresh: Bool
#Environment(\.presentationMode) var presentationMode
#ObservedObject var vm: CoreDataViewModel
#State var textFieldText: String = ""
var body: some View {
TextField("Add fruit here...", text: $textFieldText)
.font(.headline)
.padding(.horizontal)
Button(action: {
guard !textFieldText.isEmpty else { return }
vm.addFruit(text: textFieldText)
textFieldText = ""
presentationMode.wrappedValue.dismiss()
refresh.toggle()
}, label: {
Text("Save")
})
}
}
Here is my FruitRowView:
struct FruitRowView: View {
//instance of core data model
#ObservedObject var vm: CoreDataViewModel
var fruit: FruitEntity
#State var fruitName = fruit.name
var body: some View {
TextField("Enter fruit name", text: $fruitName)
}
}
So the error that I'm getting is: 'Cannot use instance member 'fruit' within property initializer; property initializers run before 'self' is available'. This error occurs in the FruitRowView when I try to assign fruitName to fruit.name. I assume that there's an easy workaround for this but I haven't been able to figure it out.
Since the fruitEntities in the view model is a published property, you don't need a state variable in the row. You need the binding for the row view, and you should pass it in the content view.
You don't need to pass the view model to the row view as well.
struct FruitRowView: View {
// No need to pass view model to child, only pass the data
// #ObservedObject var vm: CoreDataViewModel
#Binding var fruitName: String
// You don't need this.
// #State var fruitName = fruit.name
var body: some View {
TextField("Enter fruit name", text: $fruitName)
}
}
struct ContentView: View {
...
List {
ForEach(vm.savedEntities) { fruit in
FruitRowView(fruitName: $fruit.name)
}
}
...
}

SwitUI parent child binding: #Published in #StateObject doesn't work while #State does

I have a custom modal structure coming from this question (code below). Some property is modified in the modal view and is reflected in the source with a Binding. The catch is that when the property is coming from a #StateObject + #Published the changes are not reflected back in the modal view. It's working when using a simple #State.
Minimal example (full code):
class Model: ObservableObject {
#Published var selection: String? = nil
}
struct ParentChildBindingTestView: View {
#State private var isPresented = false
// not working with #StateObject
#StateObject private var model = Model()
// working with #State
// #State private var selection: String? = nil
var body: some View {
VStack(spacing: 20) {
Button("Show child", action: { isPresented = true })
Text("selection: \(model.selection ?? "nil")") // replace: selection
}
.modalBottom(isPresented: $isPresented, view: {
ChildView(selection: $model.selection) // replace: $selection
})
}
}
struct ChildView: View {
#Environment(\.dismissModal) var dismissModal
#Binding var selection: String?
var body: some View {
VStack {
Button("Dismiss", action: { dismissModal() })
VStack(spacing: 0) {
ForEach(["Option 1", "Option 2", "Option 3", "Option 4"], id: \.self) { choice in
Button(action: { selection = choice }) {
HStack(spacing: 12) {
Circle().fill(choice == selection ? Color.purple : Color.black)
.frame(width: 26, height: 26, alignment: .center)
Text(choice)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
}
.padding(50)
.background(Color.gray)
}
}
extension View {
func modalBottom<Content: View>(isPresented: Binding<Bool>, #ViewBuilder view: #escaping () -> Content) -> some View {
onChange(of: isPresented.wrappedValue) { isPresentedValue in
if isPresentedValue == true {
present(view: view(), dismissCallback: { isPresented.wrappedValue = false })
}
else {
topMostController().dismiss(animated: false)
}
}
.onAppear {
if isPresented.wrappedValue {
present(view: view(), dismissCallback: { isPresented.wrappedValue = false })
}
}
}
fileprivate func present<Content: View>(view: Content, dismissCallback: #escaping () -> ()) {
DispatchQueue.main.async {
let topMostController = self.topMostController()
let someView = VStack {
Spacer()
view
.environment(\.dismissModal, dismissCallback)
}
let viewController = UIHostingController(rootView: someView)
viewController.view?.backgroundColor = .clear
viewController.modalPresentationStyle = .overFullScreen
topMostController.present(viewController, animated: false, completion: nil)
}
}
}
extension View {
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.windows.first!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
}
private struct ModalDismissKey: EnvironmentKey {
static let defaultValue: () -> Void = {}
}
extension EnvironmentValues {
var dismissModal: () -> Void {
get { self[ModalDismissKey.self] }
set { self[ModalDismissKey.self] = newValue }
}
}
struct ParentChildBindingTestView_Previews: PreviewProvider {
static var previews: some View {
ZStack {
ParentChildBindingTestView()
}
}
}
The changes are reflected properly when replacing my custom structure with a fullScreenCover, so the problem comes from there. But I find it surprising that it works with a #State and not with a #StateObject + #Published. I thought those were identical.
If having #StateObject is a must for your code, and your ChildView has to update the data back to its ParentView, then you can still make this works around #StateObject.
Something like this:
struct Parent: View {
#StateObject var h = Helper()
var body: some View {
TextField("edit child view", text: $h.helper)
Child(helper: $h.helper)
}
}
struct Child: View {
#Binding var helper: String
var body: some View {
Text(helper)
}
}
class Helper: ObservableObject {
#Published var helper = ""
}
I think your can get anwser here
with #State we use onChange because it uses for only current View
with #Published we use onReceive because it uses for many Views
#State should be used with #Binding
#StateObject with #ObservedObject
In your case, you would pass the model to the child view and update it's properties there.

SwiftUI: ViewModel making app crash when loading a view

I want to rename an item in a ForEach list. When i try to load the EditListView for a selected list the entire app crashes.
This is a SwiftUI macOS app and the items are saved using CoreData.
The crash happens as soon as you click on "Edit List" for any of the list items.
It doesn't crash if i remove this view model var listVM: MyListViewModel from the EditListViewModel.
Here's the EditListView
struct EditListView: View {
let name: String
#Binding var isVisible: Bool
var list: MyListViewModel
#ObservedObject var editListVM: EditListViewModel
init(name: String,list: MyListViewModel, isVisible: Binding<Bool> ) {
self.list = list
editListVM = EditListViewModel(listVM: list)
_isVisible = isVisible
self.name = name
}
var body: some View {
VStack {
Text(name)
Button(action: {
editListItemVM.update()
}) {
Text("Update List Name")
}
Button(action: {
self.isVisible = false
}) {
Text("Cancel")
}
}......
EditListViewModel
class EditListViewModel: ObservableObject {
var listVM: MyListViewModel
#Published var name: String = ""
init(listVM: MyListViewModel) {
self.listVM = listVM
name = listVM.name
}
func update(){
....}
}
MyListViewModel
struct MyListViewModel: Identifiable {
private let myList: MyList
init(myList: MyList) {
self.myList = myList
}
var id: NSManagedObjectID {
myList.objectID
}
var name: String {
myList.name ?? ""
}
}
MyList Model
#objc(MyList)
public class MyList: NSManagedObject, BaseModel {
static var all: NSFetchRequest<MyList> {
let request: NSFetchRequest<MyList> = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
extension MyList {
#nonobjc public class func fetchRequest() -> NSFetchRequest<MyList> {
return NSFetchRequest<MyList>(entityName: "MyList")
}
#NSManaged public var name: String?
}
extension MyList : Identifiable {
}
Here's the Main View
struct MyListsView: View {
#StateObject var vm: MyListsViewModel
#State private var showPopover: Bool = false
init(vm: MyListsViewModel) {
_vm = StateObject(wrappedValue: vm)
}
List {
Text("My Lists")
ForEach(vm.myLists) { myList in
NavigationLink {
MyListItemsHeaderView(name: myList.name)
.sheet(isPresented: $showPopover) {
EditListView(name: myList.name, list: MyListViewModel(myList: MyList()), isVisible: $showPopover)
}
}
}.contextMenu {
Button {
showPopover = true
// Show the EditListView
} label: {
Label("Edit List", systemImage: "pen.circle")
}......
First get rid of your view model objects we don't use those in SwiftUI. We use the View struct and the property wrappers like #FetchRequest make the struct behave like an object. It looks like this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
ItemView(item: item)
I recommend looking at Xcode's app template with core data checked.
Then for editing you can use .sheet(item: like this:
struct ItemEditor: View {
#ObservedObject var item: Item // this is the scratch pad item
#Environment(\.managedObjectContext) private var context
#Environment(\.dismiss) private var dismiss // causes body to run
let onSave: () -> Void
#State var errorMessage: String?
var body: some View {
NavigationView {
Form {
Text(item.timestamp!, formatter: itemFormatter)
if let errorMessage = errorMessage {
Text(errorMessage)
}
Button("Update Time") {
item.timestamp = Date()
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
// first save the scratch pad context then call the handler which will save the view context.
do {
try context.save()
errorMessage = nil
onSave()
} catch {
let nsError = error as NSError
errorMessage = "Unresolved error \(nsError), \(nsError.userInfo)"
}
}
}
}
}
}
}
struct ItemEditorConfig: Identifiable {
let id = UUID()
let context: NSManagedObjectContext
let item: Item
init(viewContext: NSManagedObjectContext, objectID: NSManagedObjectID) {
// create the scratch pad context
context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.parent = viewContext
// load the item into the scratch pad
item = context.object(with: objectID) as! Item
}
}
struct EditItemButton: View {
let itemObjectID: NSManagedObjectID
#Environment(\.managedObjectContext) private var viewContext
#State var itemEditorConfig: ItemEditorConfig?
var body: some View {
Button(action: edit) {
Text("Edit")
}
.sheet(item: $itemEditorConfig, onDismiss: didDismiss) { config in
ItemEditor(item: config.item) {
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
itemEditorConfig = nil // dismiss the sheet
}
.environment(\.managedObjectContext, config.context)
}
}
func edit() {
itemEditorConfig = ItemEditorConfig(viewContext: viewContext, objectID: itemObjectID)
}
func didDismiss() {
// Handle the dismissing action.
}
}
struct ItemView: View {
#ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditItemButton(itemObjectID: item.objectID)
}
}
}
}
the params for EditListView in the main view were incorrect.
Fixed it with the following params:
.sheet(isPresented: $showPopover) {
EditListView(name: myList.name, list: myList, isVisible: $showPopover)
}

Deselect all other Button selection if new one is selected

I have this code :
import SwiftUI
struct PlayButton: View {
#Binding var isClicked: Bool
var body: some View {
Button(action: {
self.isClicked.toggle()
}) {
Image(systemName: isClicked ? "checkmark.circle.fill" : "circle")
}
}
}
struct ContentView: View {
#State private var isPlaying: Bool = false
var players : [String] = ["Crown" , "King" , "Queen" , "Prince"]
var body: some View {
VStack {
ForEach(players, id: \.self) { player in
HStack {
Text(player)
PlayButton(isClicked: $isPlaying)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I want to deselect all other previously selected buttons if i select a new one. For example , if i select King and select queen , then King is deselected. How can i do that
What i have done. I honestly could not come with a solution .
I understand this might look like a lot more code to provide the answer but my assumption is you are trying to make a real world app. A real world app should be testable and so my answer is coming from a place where you can test your logic separate from your UI. This solution allows you to use the data to drive what your view is doing from a model perspective.
import SwiftUI
class PlayerModel {
let name: String
var isSelected : Bool = false
init(_ name: String){
self.name = name
}
}
class AppModel: ObservableObject {
let players : [PlayerModel] = [PlayerModel("Crown") , PlayerModel("King") ,PlayerModel("Queen") ,PlayerModel("Prince")]
var activePlayerIndex: Int?
init(){
}
func selectPlayer(_ player: PlayerModel){
players.forEach{
$0.isSelected = false
}
player.isSelected = true
objectWillChange.send()
}
}
struct PlayButton: View {
let isSelected: Bool
let action : ()->Void
var body: some View {
Button(action: {
self.action()
}) {
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
}
}
}
struct ContentView: View {
#ObservedObject var model = AppModel()
var body: some View {
VStack {
ForEach(model.players, id: \.name) { player in
HStack {
Text(player.name)
PlayButton(isSelected: player.isSelected, action: { self.model.selectPlayer(player) })
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
PlayerView()
}
}
For a single selection, at a time you can pass selectedData to PlayButton view
struct PlayButton: View {
#Binding var selectedData: String
var data: String
var body: some View {
Button(action: {
selectedData = data
}) {
Image(systemName: data == selectedData ? "checkmark.circle.fill" : "circle")
}
}
}
struct ContentView: View {
#State private var selectedPlayer: String = ""
private var players : [String] = ["Crown" , "King" , "Queen" , "Prince"]
var body: some View {
VStack {
ForEach(players.indices) { index in
let obj = players[index]
HStack {
Text(obj)
PlayButton(selectedData: $selectedPlayer, data: obj)
}
}
}
}
}

Update a row in a list (SwiftUI)

I'm an early bird in programming so I know this question can be ridiculous from the point of view of an expert but I'm stuck in this situation from several days.
I would like to update a row by using a button "Edit" (pencil) after having used another button to store the item with a TextField.
Here's the code:
class Food: Hashable, Codable, Equatable {
var id : UUID = UUID()
var name : String
init(name: String) {
self.name = name
}
static func == (lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
class Manager: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var shoppingChart: [Food] = []
init() {
let milk = Food(name: "Milk")
let coffee = Food(name: "Coffee")
shoppingChart.append(milk)
shoppingChart.append(coffee)
}
func newFood(name: String) {
let food = Food(name: name)
shoppingChart.insert(food, at: 0)
}
}
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
var body: some View {
VStack {
List {
ForEach(self.dm.shoppingChart, id:\.self) { food in
HStack {
Text(food.name)
Image(systemName: "pencil")
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}
struct Add: View {
#ObservedObject var dm : Manager
#State var fieldtext : String = ""
#Binding var isAddFoodOpened : Bool
var body: some View {
VStack {
TextField("Write a food", text: $fieldtext)
buttonSave
}
}
var buttonSave : some View {
Button(action: {
self.dm.newFood(name: self.fieldtext)
self.isAddFoodOpened = false
}) {
Text("Save")
}
}
}
The #ObservedObject var dm : Manager object is never initialized.
Try initialized dm in ContentView like this:
#ObservedObject var dm = Manager()
Ok, so if I understand correctly you want to update/edit a row by using a button "Edit".
This will do it:
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
#State var isEditOpened = false
#State var fieldtext : String = ""
var body: some View {
VStack {
List {
ForEach(0..<self.dm.shoppingChart.count, id:\.self) { i in
HStack {
Text(self.dm.shoppingChart[i].name)
Button(action: { self.isEditOpened.toggle() }) {
Image(systemName: "pencil")
}.sheet(isPresented: self.$isEditOpened) {
TextField(self.dm.shoppingChart[i].name, text: self.$fieldtext, onEditingChanged: { _ in
self.dm.shoppingChart[i].name = self.fieldtext
})
}
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}