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
Related
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.
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()
}
}
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)
}
}
}
}
}
I have the following code. When app started MasterView is opening and then I click a row and going to DetailView. After I'm changing the tab in RootTabView to OtherView. And then when I turned back to the MasterView its automatically opens the DetailView.
Also both of vm.getList() and vm.getDetail() methods works. Why is that happening in SwiftUI 2? Because in SwiftUI 1 it wasn't work like that.
struct RootTabView: View {
#State var tabSelection = 0
#State private var vm = ViewModel()
var body: some View {
TabView(selection: $tabSelection) {
MasterView(vm: vm).tabItem({
Text("Master")
}).tag(0)
OtherView().tabItem({
Text("Other")
}).tag(1)
}
}
}
struct MasterView: View {
#ObservedObject var vm: ViewModel
var body: some View {
NavigationView {
List(vm.toDoList, id: \.self) { toDo in
NavigationLink(destination: DetailView(vm: vm)) {
Text(toDo)
}
}
}
.onAppear {
vm.getList()
}
}
}
struct DetailView: View {
#ObservedObject var vm: ViewModel
var body: some View {
Text(vm.toDoItem)
.onAppear {
vm.getDetail()
}
}
}
class ViewModel: ObservableObject {
#Published var toDoList: [String] = []
#Published var toDoItem: String = ""
func getList() {
toDoList = ["a", "b", "c"]
}
func getDetail() {
// do some stuffs
toDoItem = "A"
}
}
I can't implement var on struct View to class ObservableObject in swiftui
struct History: View {
var busId : String
#ObservedObject var historyData = getHistory()
var body: some View {
ZStack{
ScrollView(.vertical, showsIndicators: false){
VStack(alignment: .leading){
ForEach(historyDataSatu.data){i in
Text("Something")
}
}
}
}
}
class getHistory: ObservableObject {
#Published var data = [history]()
init() {
let db = Firestore.firestore().collection("Bus").document(busId)
// An error occurs when I implement busId into document(busId). Please help me.
}
}
}
struct history: Identifiable {
var id : String
var day : String
}
You can initiate your ObservedObject object historyData in init method like. hope it helps you
struct History: View {
var busId : String
#ObservedObject var historyData = getHistory(busId: "")
init() {
self.historyData = getHistory(busId: busId)
}
var body: some View {
ZStack{
ScrollView(.vertical, showsIndicators: false){
VStack(alignment: .leading){
ForEach(historyData.data) { i in
Text("Something")
}
}
}
}
}
}
class getHistory: ObservableObject {
#Published var data = [history]()
init(busId: String) {
let db = Firestore.firestore().collection("Bus").document(busId)
}
}