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

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

Related

Binding change not detected within List NavigationView

I'm building a picker component and am passing a Binding two layers deep. The issue is that I need to detect it's .onChange on the PickerListView, however, it never gets triggered.
import SwiftUI
public struct SectionItems<T: Hashable>: Hashable {
let header: String
public var items: [T]
public init(header: String, items: [T]) {
self.header = header
self.items = items
}
}
struct PickerListView<T: Hashable>: View {
#Environment(\.dismiss) var dismiss
#Binding var selected: T
#Binding var sections: [SectionItems<T>]
init(selected: Binding<T>, sections: Binding<[SectionItems<T>]>) {
self._selected = selected
self._sections = sections
}
var body: some View {
List {
ForEach(sections, id: \.self) { section in
Section(section.header) {
ForEach(section.items, id: \.self) { item in
Button(action: {
selected = item
dismiss()
}) {
HStack {
Text(String(describing: item))
Spacer()
if selected == item {
Image(systemName: "checkmark")
}
}
}
}
}
}
}
.onChange(of: sections) { _ in
print("PickerListView: \(sections.count)") // Doesn't run
}
}
}
import SwiftUI
public struct PickerView<T: Hashable>: View {
#Binding var selected: T
#Binding var sections: [SectionItems<T>]
public var body: some View {
NavigationView {
NavigationLink(
String(describing: selected),
destination: PickerListView(
selected: $selected,
sections: $sections
)
)
}
.onChange(of: sections) { _ in
print("PickerView: \(sections.count)") // Runs
}
}
}
struct ContentView: View {
#State private var selected = "33"
#State private var sections = [
SectionItems(header: "Itemr", items: [
"33",
"73",
"38"
])
]
var body: some View {
VStack {
PickerView(selected: $selected, sections: $sections)
Button("Add Value", action: {
sections[0].items.append("\(Int.random(in: 1...100))")
})
}
.onChange(of: sections) { _ in
print("ContentView: \(sections.count)") // Runs
}
}
}
You can try a different approach, instead of passing multiple items across many Views you can combine all the variables in a single ObservableObject
Then you can use didSet to make any changes you would make with onChange
///Keeps all the code needed for several views in one spot
class SectionViewModel<T: Hashable>: ObservableObject{
#Published var selected : T
#Published var sections : [SectionItems<T>]{
didSet{
//Use didSet instead of onChange
print("\(type(of: self)) :: \(#function) :: \(sections.count)")
print("\(type(of: self)) :: \(#function) :: section[0]items count: \(sections[0].items.count)")
}
}
init(selected : T, sections : [SectionItems<T>]){
self.selected = selected
self.sections = sections
}
}
Your Views would look something like
struct PickerListView<T: Hashable>: View {
#ObservedObject var vm: SectionViewModel<T>
#Environment(\.dismiss) var dismiss
var body: some View {
List {
ForEach(vm.sections, id: \.self) { section in
Section(section.header) {
ForEach(section.items, id: \.self) { item in
Button(action: {
vm.selected = item
dismiss()
}) {
HStack {
Text(String(describing: item))
Spacer()
if vm.selected == item {
Image(systemName: "checkmark")
}
}
}
}
}
}
}
//Use didset for any changes
// .onChange(of: vm.sections) { _ in
// print("PickerListView: \(vm.sections.count)") // Doesn't run
// }
}
}
public struct PickerView<T: Hashable>: View {
#ObservedObject var vm: SectionViewModel<T>
public var body: some View {
//Navigation View should be at the top
//NavigationView {
NavigationLink(
String(describing: vm.selected),
destination: PickerListView<T>(vm: vm)
)
// }
//Use did set for any actions
// .onChange(of: vm.sections) { _ in
// print("PickerView: \(vm.sections.count)") // Runs
// print("PickerView section[0]items count: \(vm.sections[0].items.count)") // Runs
// }
}
}
struct SampleDeepView: View {
#StateObject var vm: SectionViewModel = .init(selected: "33", sections: [
SectionItems(header: "Itemr", items: [
"33",
"73",
"38"
])
])
var body: some View {
NavigationView{
VStack {
PickerView(vm: vm)
//Adding an item not a section
Button("Add Value", action: {
vm.sections[0].items.append("\(Int.random(in: 1...100))")
})
//Adding a section
Button("Add Section", action: {
vm.sections.append(
SectionItems(header: "Itemr", items: [
"33",
"73",
"38"
])
)
})
}
}
//Use didset for any actions
// .onChange(of: vm.sections) { _ in
// print("ContentView section[0]items count: \(vm.sections[0].items.count)") // Runs
// }
}
}

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.

passing parameter to a SwiftUI Sheet

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

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

Updating Picker on update of ObservedObject

I'm trying up dynamically add new rows to a Picker as follows:
class ViewModel: ObservableObject {
#Published private (set) var drinks = ["Tea", "Coffee", "Wine"]
func addDrink(_ drink: String) {
drinks.append(drink)
}
}
struct PickerTest: View {
#State private var selectedDrink = "Tea"
#State private var customDrink = ""
#ObservedObject private var viewModel = ViewModel()
var body: some View {
VStack {
HStack {
TextField("Enter a drink", text: $customDrink)
Spacer()
Button("Add") {
self.viewModel.addDrink(self.customDrink)
}
}
Picker("Drinks", selection: $selectedDrink) { // Removing the wrapping Picker works
ForEach(viewModel.drinks, id: \.self) { drink in
Text(drink)
}
}
}.padding().labelsHidden()
}
}
This doesn't work. If I remove the Picker wrapping the ForEach, the ForEach updates as expected.
Is there a way to update the Picker dynamically?
It looks like Pickers bug - I hope, that Apple fixes it in future releases of SwiftUI.
I found ugly (I really don't like it) workaround for this problem:
class ViewModel: ObservableObject {
#Published var selectedDrink = "Tea"
#Published var drinks = ["Tea", "Coffee", "Wine"]
#Published var drinksChanged = true
func addDrink(_ drink: String) {
drinks.append(drink)
drinksChanged.toggle()
}
}
struct DrinksPicker: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Picker("Drinks", selection: $viewModel.selectedDrink) {
ForEach(viewModel.drinks, id: \.self) { drink in
Text(drink)
}
}
}
}
struct PickerTest: View {
#State private var customDrink = ""
#ObservedObject private var viewModel = ViewModel()
var body: some View {
VStack {
HStack {
TextField("Enter a drink", text: $customDrink)
Spacer()
Button("Add") {
self.viewModel.addDrink(self.customDrink)
self.customDrink = ""
}
}
if viewModel.drinksChanged {
DrinksPicker(viewModel: viewModel)
} else {
DrinksPicker(viewModel: viewModel)
}
}.padding().labelsHidden()
}
}
You can also hide this if-else in some another container:
struct DrinksPickerContainer: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Group {
if viewModel.drinksChanged {
DrinksPicker(viewModel: viewModel)
} else {
DrinksPicker(viewModel: viewModel)
}
}
}
}
and then use only DrinksPickerContainer(viewModel: viewModel) in PickerTest