passing parameter to a SwiftUI Sheet - swift

I need to pass a parameter calledFrom to a Sheet in SwiftUI.
Strangely, the parameter is not used on the first call, but it works on the following ones.
import SwiftUI
struct ContentView: View {
#State var showSheet = false
#State var calledFrom = -1
var body: some View {
ForEach((1...4), id: \.self) { i in
getButton(i)
}
.sheet(isPresented: $showSheet) { Dialog(calledFrom: calledFrom) }
.padding()
}
func getButton(_ i : Int) -> some View {
return Button("\(i)"){print("Button \(i) pressed"); calledFrom = i; showSheet = true }
}
}
struct Dialog: View {
var calledFrom : Int
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack{
Text("Called from Button \(calledFrom)")
Button("close"){presentationMode.wrappedValue.dismiss()}
}
.padding()
}
}

You have to use sheet(item:) to get the behavior you're looking for. In iOS 14, the sheet view is calculated before the #State changes:
struct ActiveItem : Identifiable {
var calledFrom: Int
var id: Int { return calledFrom }
}
struct ContentView: View {
#State var activeItem : ActiveItem?
var body: some View {
ForEach((1...4), id: \.self) { i in
getButton(i)
}
.sheet(item: $activeItem) { item in
Dialog(calledFrom: item.calledFrom)
}
.padding()
}
func getButton(_ i : Int) -> some View {
return Button("\(i)"){
print("Button \(i) pressed");
activeItem = ActiveItem(calledFrom: i)
}
}
}

Related

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: Why adding #State to property doesn't update data model

having a problem with updating data in ObservedObject
here's the data MRE:
struct Property: Identifiable, Codable, Hashable {
var id = UUID()
var name : String = ""
var meters : [Meter] = [Meter(name: "electricity"), Meter(name: "water")]
}
struct Meter: Identifiable, Hashable, Codable {
var id = UUID()
var name : String = ""
}
class PropertyData: ObservableObject {
#Published var properties: [Property]
func save() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(properties) {
UserDefaults.standard.set(encoded, forKey: "PropertyData")
}
}
init() {
if let properties = UserDefaults.standard.data(forKey: "PropertyData") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([Property].self, from: properties) {
self.properties = decoded
return
}
}
self.properties = [
Property(name: "Saks /1", meters: [Meter(name: "electricity")]),
Property(name: "Saks /2", meters: [Meter(name: "electricity"), Meter(name: "water")]),
]
}
}
import SwiftUI
struct ContentView: View {
#ObservedObject var data = PropertyData()
var body: some View {
NavigationView {
List {
ForEach(data.properties, id:\.self) {property in
NavigationLink(destination: NavigationLinkView(data: data, property: property)) {
Text(property.name)
}
}
}
}
}
}
struct NavigationLinkView: View {
#ObservedObject var data : PropertyData
var property:Property
var body: some View {
TabView {
MetersView(data: data, property: property)
.tabItem{
VStack {
Image(systemName: "scroll")
Text("Utitity")
}
}
}
}
}
struct MetersView: View {
#ObservedObject var data: PropertyData
#State private var metersPage = 0
#State var property : Property
#State private var addMeters = false
var body: some View {
VStack {
HStack {
Picker("Meters", selection: $metersPage) {
ForEach (0 ..< property.meters.count, id:\.self) {index in
Text(property.meters[index].name)
}
}
.pickerStyle(SegmentedPickerStyle())
Button{
print(property.meters)
addMeters.toggle()
} label: {
Image(systemName: "gear")
}
}.padding()
Spacer()
}.sheet(isPresented: $addMeters){AddMetersView(data: data, property: $property)}
}
}
struct AddMetersView: View {
#ObservedObject var data : PropertyData
#Binding var property : Property
#State var newMeter: String = ""
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Form{
Section {
TextField("Add another meter", text: $newMeter)
.autocapitalization(.none)
Button{
if newMeter != "" {
property.meters.append(Meter(name: newMeter))
data.save()
print(property.meters.count)
} } label: {
Text("Add a meter")
}
}
ForEach(0..<property.meters.count, id:\ .self) {index in
Text(property.meters[index].name)
}
Section() {
Button("That's enough"){
print(property.meters)
presentationMode.wrappedValue.dismiss()}
}
}
}
}
}
I cannot understand, why the meters, that I add on AddMetersView, do update the Meters page, but then just goes away as soon as I go to the ContentView.
Plus, in my app, if I add a property, it does stay, and it persists, but not the Meters
This is similar to your previous question. Change #State var property : Property to
It is all about the connections. You break them in a few places along the way I made some changes and put comments along the line.
import SwiftUI
struct MetersParentView: View {
//Change to StateObject
#StateObject var data = PropertyData()
var body: some View {
NavigationView {
List {
ForEach($data.properties, id:\.id) {$property in
NavigationLink(destination: NavigationLinkView(data: data, property: $property)) {
Text(property.name)
}
}
}
}
}
}
struct NavigationLinkView: View {
#ObservedObject var data : PropertyData
//Make Binding there is no connection without it
#Binding var property : Property
var body: some View {
TabView {
MetersView(data: data, property: $property)
.tabItem{
VStack {
Image(systemName: "scroll")
Text("Utitity")
}
}
}
}
}
struct MetersView: View {
#ObservedObject var data: PropertyData
#State var selectedMeter: Meter = .init()
//Change to Binding
//State is a source of truth, it breaks the connection
#Binding var property : Property
#State private var addMeters = false
var body: some View {
VStack {
HStack {
Picker("Meters", selection: $selectedMeter) {
//Use object not index
ForEach(property.meters, id:\.id) {meter in
//Tag adjustment
Text(meter.name).tag(meter as Meter)
}
}.onAppear(perform: {
selectedMeter = property.meters.first ?? Meter()
})
.pickerStyle(SegmentedPickerStyle())
Button{
print(property.meters)
addMeters.toggle()
} label: {
Image(systemName: "gear")
}
}.padding()
Spacer()
}.sheet(isPresented: $addMeters){
AddMetersView(data: data, property: $property)
}
}
}
struct AddMetersView: View {
#ObservedObject var data : PropertyData
#Binding var property : Property
#State var newMeter: String = ""
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Form{
Section {
TextField("Add another meter", text: $newMeter)
.autocapitalization(.none)
Button{
if newMeter != "" {
print(property.meters.count)
property.meters.append(Meter(name: newMeter))
print(property.meters.count)
data.save()
print(property.meters.count)
}
} label: {
Text("Add a meter")
}
}
//Dont use index
ForEach(property.meters, id:\ .id) {meter in
Text(meter.name)
}
Section() {
Button("That's enough"){
print(property.meters)
presentationMode.wrappedValue.dismiss()}
}
}
}
}
}
struct MetersParentView_Previews: PreviewProvider {
static var previews: some View {
MetersParentView()
}
}

SwiftUI Executing a method from a view after confirmation from the subview

Is it technically possible to call a method from a view after getting the confirmation from the subview ? I could call it from the SubStruct if I pass the viewModel and item to it, but I am just curious about the code below which results in
Segmentation Fault: 11
import SwiftUI
struct ContentView: View {
var body: some View {
MainStruct(viewModel: MyViewModel())
}
}
struct MainStruct: View {
#StateObject var viewModel: MyViewModel
#State var functionToPass: ()
let items = ["test1", "test2", "test2"]
var body: some View {
ForEach(items, id: \.self) { (item) in
Text(item).onTapGesture {
functionToPass = viewModel.deleteItem(name: item)
}
}
SubStruct(passedFunction: {functionToPass})
}
struct SubStruct: View {
var passedFunction: () -> Void
var body: some View {
Button(action: {passedFunction()}, label: {
Text("confirm deletion")
})
}
}
}
class MyViewModel: ObservableObject {
func deleteItem(name: String) {
print(name)
///deletion logic
}
}
Try this :
struct MainStruct: View {
#StateObject var viewModel: MyViewModel
#State private var functionToPass: () -> Void = {}
let items = ["test1", "test2", "test2"]
var body: some View {
VStack {
ForEach(items, id: \.self) { item in
Text(item)
.onTapGesture {
functionToPass = {
viewModel.deleteItem(name: item)
}
}
}
SubStruct(passedFunction: functionToPass)
}
}
struct SubStruct: View {
var passedFunction: () -> Void
var body: some View {
Button(action: {
passedFunction()
}, label: {
Text("confirm deletion")
})
}
}
}

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)
}
}
}