I use core data to store something,and state it as
#Environment(\.managedObjectContext) private var viewContext
#StateObject var exeplan: Explan
Explan is a entity
ForEach(Array(exeplan.exercisearray.enumerated()), id: \.element) { (index,item) in
Button(action: {
item.duration -= 15
do {
try viewContext.save()
} catch {} }, label: {
Image(systemName: "minus.circle.fill")})
Text("\(item.duration)")
Button(action: {item.duration += 15
do {
try viewContext.save()
} catch {})
the text Text("(item.duration)") is no update immediately, and it's an Int32 style,
but when I print it, the num is a new one. also when I navigate back
and I use another int32 to replace it, it has changed。
I mean it's a closure, and how can I pass a new array to replace it?
of another way, it fixes this problem?
try to "manually" state update the Explan, by adding
exeplan.objectWillChange.send()
just before
item.duration -= 15
Related
I have a very simple codes and I want keep it as much as possible simple, I am using a ForEach to render some simple Text, for understanding what is happening undercover I made a TextView to get notified each time this View get called by SwiftUI, unfortunately each time I add new element to my array, SwiftUI is going to render all array elements from begging to end, which I want and expecting it call TextView just for new element, So there is a way to defining an array of View/Text which would solve the issue, but that is over kill for such a simple work, I mean me and you would defiantly use ForEach in our projects, and we could use a simple Text inside ForEach or any other custom View, how we could solve this issue to stop SwiftUI initializing same thing again and again, whith this in mind that I want just use a simple String array and not going to crazy and defining a View array.
My Goal is using an simple array of String to this work without being worry to re-initializing issue.
Maybe it is time to re-think about using ForEach in your App!
SwiftUI would fall to re-rendering trap even with updating an element of the array! which is funny. so make yourself ready if you got 50 or 100 or 1000 rows and you are just updating 1 single row, swiftUI would re render the all entire your array, it does not matter you are using simple Text or your CustomView. So I would wish SwiftUI would be smart to not rendering all array again, and just making necessary render in case.
import SwiftUI
struct ContentView: View {
#State private var arrayOfString: [String] = [String]()
var body: some View {
ForEach(arrayOfString.indices, id:\.self) { index in
TextView(stringOfText: arrayOfString[index])
}
Spacer()
Button("append new element") {
arrayOfString.append(Int.random(in: 1...1000).description)
}
.padding(.bottom)
Button("update first element") {
if arrayOfString.count > 0 {
arrayOfString[0] = "updated!"
}
}
.padding(.bottom)
}
}
struct TextView: View {
let stringOfText: String
init(stringOfText: String) {
self.stringOfText = stringOfText
print("initializing TextView for:", stringOfText)
}
var body: some View {
Text(stringOfText)
}
}
Initializing and rendering are not the same thing. The views get initialized, but not necessarily re-rendered.
Try this with your original ContentView:
struct TextView: View {
let stringOfText: String
init(stringOfText: String) {
self.stringOfText = stringOfText
print("initializing TextView for:", stringOfText)
}
var body: some View {
print("rendering TextView for:", stringOfText)
return Text(stringOfText)
}
}
You'll see that although the views get initialized, they do not in fact get re-rendered.
If you go back to your ContentView, and add dynamic IDs to each element:
TextView(stringOfText: arrayOfString[index]).id(UUID())
You'll see that in this case, they actually do get re-rendered.
You are always iterating from index 0, so that’s an expected outcome. If you want forEach should only execute for newly added item, you need to specify correct range. Check code below-:
import SwiftUI
struct ContentViewsss: View {
#State private var arrayOfString: [String] = [String]()
var body: some View {
if arrayOfString.count > 0 {
ForEach(arrayOfString.count...arrayOfString.count, id:\.self) { index in
TextView(stringOfText: arrayOfString[index - 1])
}
}
Spacer()
Button("append new element") {
arrayOfString.append(Int.random(in: 1...1000).description)
}
}
}
struct TextView: View {
let stringOfText: String
init(stringOfText: String) {
self.stringOfText = stringOfText
print("initializing TextView for:", stringOfText)
}
var body: some View {
Text(stringOfText)
}
}
You need to use LazyVStack here
LazyVStack {
ForEach(arrayOfString.indices, id:\.self) { index in
TextView(stringOfText: arrayOfString[index])
}
}
so it reuse view that goes out of visibility area.
Also note that SwiftUI creates view here and there very often because they are just value type and we just should not put anything heavy into custom view init.
The important thing is not to re-render view when it is not visible or not changed and exactly this thing is what you should think about. First is solved by Lazy* container, second is by Equatable protocol (see next for details https://stackoverflow.com/a/60483313/12299030)
I have a 3-part picker, and I'm trying to make the values of one Picker to be based on the value of another. Specifically adding/removing the s on the end of "Days","Weeks",etc. I have read a similar post (here) on this type of situation, but the proposed Apple solution for IOS 14+ deployments is not working. Given that the other question focuses primarily on pre-14 solutions, I thought starting a new question would be more helpful.
Can anyone shed any light on why the .onChange is never getting called? I set a breakpoint there, and it is never called when the middle wheels value change between 1 and any other value as it should.
The unconventional init is just so I could encapsulate this code removed from a larger project.
Also, I have the .id for the 3rd picker commented out in the code below, but can un-comment if the only problem remaining is for the 3rd picker to update on the change.
import SwiftUI
enum EveryType:String, Codable, CaseIterable, Identifiable {
case every="Every"
case onceIn="Once in"
var id: EveryType {self}
var description:String {
get {
return self.rawValue
}
}
}
enum EveryInterval:String, Codable, CaseIterable, Identifiable {
case days = "Day"
case weeks = "Week"
case months = "Month"
case years = "Year"
var id: EveryInterval {self}
var description:String {
get {
return self.rawValue
}
}
}
struct EventItem {
var everyType:EveryType = .onceIn
var everyInterval:EveryInterval = .days
var everyNumber:Int = Int.random(in:1...3)
}
struct ContentView: View {
init(eventItem:Binding<EventItem> = .constant(EventItem())) {
_eventItem = eventItem
}
#Binding var eventItem:EventItem
#State var intervalId:UUID = UUID()
var body: some View {
GeometryReader { geometry in
HStack {
Picker("", selection: self.$eventItem.everyType) {
ForEach(EveryType.allCases)
{ type in Text(type.description)
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: geometry.size.width * 0.3, height:100)
.compositingGroup()
.padding(0)
.clipped()
Picker("", selection: self.$eventItem.everyNumber
) {
ForEach(1..<180, id: \.self) { number in
Text(String(number)).tag(number)
}
}
//The purpase of the == 1 below is to only fire if the
// everyNumber values changes between being a 1 and
// any other value.
.onChange(of: self.eventItem.everyNumber == 1) { _ in
intervalId = UUID() //Why won't this ever happen?
}
.pickerStyle(WheelPickerStyle())
.frame(width: geometry.size.width * 0.25, height:100)
.compositingGroup()
.padding(0)
.clipped()
Picker("", selection: self.$eventItem.everyInterval) {
ForEach(EveryInterval.allCases) { interval in
Text("\(interval.description)\(self.eventItem.everyNumber == 1 ? "" : "s")")
}
}
.pickerStyle(WheelPickerStyle())
.frame(width: geometry.size.width * 0.4, height:100)
.compositingGroup()
.clipped()
//.id(self.intervalId)
}
}
.frame(height:100)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(eventItem: .constant(EventItem()))
}
}
For Picker, its item data type must conform Identifiable and we must pass a property of item into "tag" modifier as "id" to let Picker trigger selection and return that property in Binding variable with selection.
For example :
Picker(selection: $selected, label: Text("")){
ForEach(data){item in //data's item type must conform Identifiable
HStack{
//item view
}
.tag(item.property)
}
}
.onChange(of: selected, perform: { value in
//handle value of selected here (selected = item.property when user change selection)
})
//omg! I spent whole one day to find out this
Try the following
.onChange(of: self.eventItem.everyNumber) { newValue in
if newValue == 1 {
intervalId = UUID()
}
}
but it might also depend on how do you use this view, because with .constant binding nothing will change ever.
The answer by Thang Dang, above, turned out to be very helpful to me. I did not know how to conform my tag to Identifiable, but changed my tags from tag(1) to a string, as in the SwiftUI code below. The tag with a mere number in it caused nothing to happen when the Picker was set to Icosahedron (my breakpoint on setShape was never triggered), but the other three caused the correct shape to be passed in to setShape.
// set the current Shape
func setShape(value: String) {
print(value)
}
#State var shapeSelected = "Cube"
VStack {
Picker(selection: $shapeSelected, label: Text("$\(shapeSelected)")) {
Text("Cube").tag("Cube")
Text("Simplex").tag("Simplex")
Text("Pentagon (3D)").tag("Pentagon")
Text("Icosahedron").tag(1)
}.onChange(of: shapeSelected, perform: { tag in
setShape(value: "\(tag)")
})
}
I am trying to get a sheet representation work nested in multiple views inside a ScrollView.
Without the ScrollView the .sheet modifier works fine, but when I wrap everything inside the scroll view the modifier, triggers only once. So on the first tap the sheet appears fine, but after dismissing it I can not trigger it again. I am unsure whether this is a bug in SwiftUI itself or if I am doing something here.
Note: If I add the .sheet modifier to the ScrollView itself, everything is working. But for my use case the .sheet modifier is added deeply nested inside a custom view inside the ScrollView.
I am using the Xcode Beta 5
Without ScrollView - Works
struct SheetWorks: View {
#State var showSheet = false
var strings = [
"Hello", "World", "!"
]
var body: some View {
HStack {
ForEach(strings) { string in
Button(action: {self.showSheet.toggle()}) {
Text(string)
}
.sheet(isPresented: self.$showSheet) {
Text("Here is the sheet")
}
}
}
.padding()
}
}
With ScrollView - Does not work
struct SheetDoesntWork: View {
#State var showSheet = false
var strings = [
"Hello", "World", "!"
]
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(strings) { string in
Button(action: {self.showSheet.toggle()}) {
Text(string)
}
.sheet(isPresented: self.$showSheet) {
Text("Here is the sheet")
}
}
}
.padding()
}
}
}
Maybe someone has experienced something similar or can point me in the right direction. I really appreciate any help.
Edit: This problem still persists in Beta 6
Take a look at my answer here: https://stackoverflow.com/a/57259687/554203
Basically you should use just one .sheet outside the loop and dynamically open the desired view based on a local var.
var covers = coverData
var selectedTag = 0
Group {
ForEach(covers) { item in
Button(action: {
self.selectedTag = item.tag
self.isPresented.toggle()
}) {
CoverAttributes(
title: item.title,
alternativeTitle: alternativeTitle,
tapForMore: item.tapForMore,
color: item.color,
shadowColor: item.shadowColor)
}
}
}
.sheet(isPresented: self.$isPresented, content: {
Text("Destination View \(self.selectedTag)")
// Here you could use a switch statement on selectedTag if you want
})
As of Xcode 11.1 GM Seed I can approve that the .sheet modifier now works correctly inside the ScrollView. Furthermore also a multiline Text is now correctly rendered.
I want to change another unrelated #State variable when a Picker gets changed, but there is no onChanged and it's not possible to put a didSet on the pickers #State. Is there another way to solve this?
Deployment target of iOS 14 or newer
Apple has provided a built in onChange extension to View, which can be used like this:
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor, label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }
}
}
Deployment target of iOS 13 or older
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor.onChange(colorChange), label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
}
func colorChange(_ tag: Int) {
print("Color tag: \(tag)")
}
}
Using this helper
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
return Binding(
get: { self.wrappedValue },
set: { selection in
self.wrappedValue = selection
handler(selection)
})
}
}
First of all, full credit to ccwasden for the best answer. I had to modify it slightly to make it work for me, so I'm answering this question hoping someone else will find it useful as well.
Here's what I ended up with (tested on iOS 14 GM with Xcode 12 GM)
struct SwiftUIView: View {
#State private var selection = 0
var body: some View {
Picker(selection: $selection, label: Text("Some Label")) {
ForEach(0 ..< 5) {
Text("Number \($0)") }
}.onChange(of: selection) { _ in
print(selection)
}
}
}
The inclusion of the "_ in" was what I needed. Without it, I got the error "Cannot convert value of type 'Int' to expected argument type '()'"
I think this is simpler solution:
#State private var pickerIndex = 0
var yourData = ["Item 1", "Item 2", "Item 3"]
// USE this if needed to notify parent
#Binding var notifyParentOnChangeIndex: Int
var body: some View {
let pi = Binding<Int>(get: {
return self.pickerIndex
}, set: {
self.pickerIndex = $0
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// USE this if needed to notify parent
self.notifyParentOnChangeIndex = $0
})
return VStack{
Picker(selection: pi, label: Text("Yolo")) {
ForEach(self.yourData.indices) {
Text(self.yourData[$0])
}
}
.pickerStyle(WheelPickerStyle())
.padding()
}
}
I know this is a year old post, but I thought this solution might help others that stop by for a visit in need of a solution. Hope it helps someone else.
import Foundation
import SwiftUI
struct MeasurementUnitView: View {
#State var selectedIndex = unitTypes.firstIndex(of: UserDefaults.standard.string(forKey: "Unit")!)!
var userSettings: UserSettings
var body: some View {
VStack {
Spacer(minLength: 15)
Form {
Section {
Picker(selection: self.$selectedIndex, label: Text("Current UnitType")) {
ForEach(0..<unitTypes.count, id: \.self) {
Text(unitTypes[$0])
}
}.onReceive([self.selectedIndex].publisher.first()) { (value) in
self.savePick()
}
.navigationBarTitle("Change Unit Type", displayMode: .inline)
}
}
}
}
func savePick() {
if (userSettings.unit != unitTypes[selectedIndex]) {
userSettings.unit = unitTypes[selectedIndex]
}
}
}
I use a segmented picker and had a similar requirement. After trying a few things I just used an object that had both an ObservableObjectPublisher and a PassthroughSubject publisher as the selection. That let me satisfy SwiftUI and with an onReceive() I could do other stuff as well.
// Selector for the base and radix
Picker("Radix", selection: $base.value) {
Text("Dec").tag(10)
Text("Hex").tag(16)
Text("Oct").tag(8)
}
.pickerStyle(SegmentedPickerStyle())
// receiver for changes in base
.onReceive(base.publisher, perform: { self.setRadices(base: $0) })
base has both an objectWillChange and a PassthroughSubject<Int, Never> publisher imaginatively called publisher.
class Observable<T>: ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<T, Never>()
var value: T {
willSet { objectWillChange.send() }
didSet { publisher.send(value) }
}
init(_ initValue: T) { self.value = initValue }
}
typealias ObservableInt = Observable<Int>
Defining objectWillChange isn't strictly necessary but when I wrote that I liked to remind myself that it was there.
For people that have to support both iOS 13 and 14, I added an extension which works for both. Don't forget to import Combine.
Extension View {
#ViewBuilder func onChangeBackwardsCompatible<T: Equatable>(of value: T, perform completion: #escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *) {
self.onChange(of: value, perform: completion)
} else {
self.onReceive([value].publisher.first()) { (value) in
completion(value)
}
}
}
}
Usage:
Picker(selection: $selectedIndex, label: Text("Color")) {
Text("Red").tag(0)
Text("Blue").tag(1)
}.onChangeBackwardsCompatible(of: selectedIndex) { (newIndex) in
print("Do something with \(newIndex)")
}
Important note: If you are changing a published property inside an observed object within your completion block, this solution will cause an infinite loop in iOS 13. However, it is easily fixed by adding a check, something like this:
.onChangeBackwardsCompatible(of: showSheet, perform: { (shouldShowSheet) in
if shouldShowSheet {
self.router.currentSheet = .chosenSheet
showSheet = false
}
})
SwiftUI 1 & 2
Use onReceive and Just:
import Combine
import SwiftUI
struct ContentView: View {
#State private var selection = 0
var body: some View {
Picker("Some Label", selection: $selection) {
ForEach(0 ..< 5, id: \.self) {
Text("Number \($0)")
}
}
.onReceive(Just(selection)) {
print("Selected: \($0)")
}
}
}
iOS 14 and CoreData entities with relationships
I ran into this issue while trying to bind to a CoreData entity and found that the following works:
Picker("Level", selection: $contact.level) {
ForEach(levels) { (level: Level?) in
HStack {
Circle().fill(Color.green)
.frame(width: 8, height: 8)
Text("\(level?.name ?? "Unassigned")")
}
.tag(level)
}
}
.onChange(of: contact.level) { _ in savecontact() }
Where "contact" is an entity with a relationship to "level".
The Contact class is an #ObservedObject var contact: Contact
saveContact is a do-catch function to try viewContext.save()...
The very important issue : we must pass something to "tag" modifier of Picker item view (inside ForEach) to let it "identify" items and trigger selection change event. And the value we passed will return to Binding variable with "selection" of Picker.
For example :
Picker(selection: $selected, label: Text("")){
ForEach(data){item in //data's item type must conform Identifiable
HStack{
//item view
}
.tag(item.property)
}
}
.onChange(of: selected, perform: { value in
//handle value of selected here (selected = item.property when user change selection)
})
When I run a Picker Code in the Simulator or the Canvas, the Picker goes always back to the first option with an animation or just freezes. This happens since last Thursday/Friday. So I checked some old simple code, where it worked before that and it doesn't work for me there, too.
This is the simple old Code. It doesn't work anymore in beta 3, 4 and 5.
struct PickerView : View {
#State var selectedOptionIndex = 0
var body: some View {
VStack {
Text("Option: \(selectedOptionIndex)")
Picker(selection: $selectedOptionIndex, label: Text("")) {
Text("Option 1")
Text("Option 2")
Text("Option 3")
}
}
}
}
In my newer code, I used #ObservedObject, but also here it doesn't work.
Also I don't get any errors and it builds and runs.
Thank you for any pointers.
----EDIT----- Please look at the answer first
After the help, that I could use the .tag() behind all Text()like Text("Option 1").tag(), it now takes the initial value and updates it inside the view. If I use #ObservedObject like here:
struct PickerView: View {
#ObservedObject var data: Model
let width: CGFloat
let height: CGFloat
var body: some View {
VStack(alignment: .leading) {
Picker(selection: $data.exercise, label: Text("select exercise")) {
ForEach(data.exercises, id: \.self) { exercise in
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise))
}
}
.frame(width: width, height: (height/2), alignment: .center)
}
}
}
}
Unfortunately it doesn't reflect changes on the value, if I make these changes in another view, one navigationlink further. And also it doesn't seem to work with the my code above, where I use firstIndex(of: exercise)
---EDIT---
Now the code above works if I change
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise))
into
Text("\(exercise)").tag(self.data.exercises.firstIndex(of: exercise)!)
because it couldn't work with an optional.
The answer summarized:
With the .tag() behind the Options it works. It would look like following:
Picker(selection: $selectedOptionIndex, label: Text("")) {
ForEach(1...3) { index in
Text("Option \(index)").tag(index)
}
}
If you use a range of Objects it could look like this:
Picker(selection: $data.exercises, label: Text("")) {
ForEach(0..<data.exercises.count) { index in
Text("\(data.exercises[index])").tag(index)
}
}
I am not sure if it is intended, that .tag() is needed to be used here, but it's at least a workaround.
I found a way to simplify the code a bit without the need of operating on indicies and tags.
At first, make sure to conform your model to Identifiable protocol like this (this is actually a key part, as it enables SwiftUI to differentiate elements):
public enum EditScheduleMode: String, CaseIterable, Identifiable {
case closeSchedule
case openSchedule
public var id: EditScheduleMode { self }
var localizedTitle: String { ... }
}
Then you can declare viewModel like this:
public class EditScheduleViewModel: ObservableObject {
#Published public var editScheduleMode = EditScheduleMode.closeSchedule
public let modes = EditScheduleMode.allCases
}
and UI:
struct ModeSelectionView: View {
private let elements: [EditScheduleMode]
#Binding private var selectedElement: EditScheduleMode
internal init?(elements: [EditScheduleMode],
selectedElement: Binding<EditScheduleMode>) {
self.elements = elements
_selectedElement = selectedElement
}
internal var body: some View {
VStack {
Picker("", selection: $selectedElement) {
ForEach(elements) { element in
Text(element.localizedTitle)
}
}
.pickerStyle(.segmented)
}
}
}
With all of those you can create a view like this:
ModeSelectionView(elements: viewModel.modes, selectedElement: $viewModel.editScheduleMode)